【Rails】ActiveStorageのvariantを使いこなす!便利な画像変換のメソッドやオプションを実例で解説(!, >, <, ^とは何か?)

rails-prograshi(プロぐらし)-kv Rails
記事内に広告が含まれていることがあります。

RailsのActiveStorageを使うと、既存のレコードに画像を紐づけることができます。

その画像に対してvariantメソッドを使うと、画像のサイズ変更や回転などの処理を加えることができます。

ここでは、variantで使える主な処理を実例で解説しています。


ActiveStorageの使い方

ActiveStorageを使うためにはインストールやマイグレーションおよび、モデルファイルの編集などの準備が必要です。

▼ActiveStorageのダウンロード方法

rails active_storage:install
rails db:migrate

モデルファイルの編集方法や、画像をレコードと紐づける方法については下記をご参考ください。

(参考)ActiveStroageの使い方を実例で解説|複数画像やファイルのアップロードと個別に削除・変更する方法


variantを有効化する

画像処理を行うvariantメソッドを使うためには、image_processingのgemをインストールする必要があります。

Gemfileに以下を追記します。(※Railsのバージョンによってはデフォルトでコメントアウトになっているので有効化します。)

# Use Active Storage variant
gem 'image_processing', '~> 1.2'

gemをインストールするため、bundle installを実行します。

# bundle install

Fetching gem metadata from https://rubygems.org/............
(省略)
Fetching ruby-vips 2.1.2
Installing ruby-vips 2.1.2
Fetching image_processing 1.12.1
Installing image_processing 1.12.1

image_processingとvipsがインストールされます。

gemのインストール結果を反映させるために、Railsアプリケーションを再起動します。

以上でvariantを使う準備は完了です。

tips

ActiveStorageのデフォルトの画像プロセッサにはMinMaginc(gem名 mini_magick)が使われています。

画像のサイズ変換など基本的な処理であればデフォルトのMinMagicで問題ありません。

MinMagickとは?

MinMagickは、ImageMagick(Railsのgem名はRMagick)の軽量版です。

ImageMagickは単純な処理でも100MBを超えるRAMを使用しますが、MiniMagickはメモリの使用量をかなり抑えることができます。


variantの基本的な使い方と注意点

variantメソッドを使用する前に、variantの基本的な使い方と注意点について触れておきます。

variantの基本的な使い方

variantはActiveStorageを使って保存したblob型の画像ファイルに対して使います。

処理を指定する方法は大きく2つあります。resizeなどのように値で指定するパターンと、monochromeのようにtrueで指定するパターンです。

Blobオブジェクト.variant( 処理: 値 )
Blobオブジェクト.variant( 処理: true )


複数の処理を施す

カンマでつなぐことで複数の処理をつなげることもできます。

Blobオブジェクト.variant( 処理1, 処理2,,,, )

例えば、リサイズして、正方形に切り抜く処理は次のようになります。

Blobオブジェクト.variant( resize: "100x100^", gravity: "center", crop: "100x100+0+0" )


改行して記述する

処理が複数ある場合、改行することもできます。

Blobオブジェクト.variant( 
  resize: "100x100^",
  gravity: "center", 
  crop: "100x100+0+0"
)

これは上記の例と同じ記述です。


variantの注意点

variantを使用する上で注意すべき点は大きく2つあります。

  1. 複数の処理がある場合は記述した順番に実行する
  2. 加工後の画像はDBに新しいデータとして保存する

複数の処理がある場合は記述した順番に実行する

variantの中で複数の処理をカンマでつないで記述した場合、処理する順番は記述した順序になります。

例えば次のように画像を正方形に切り抜く場合の処理をみると

Blobオブジェクト.variant( resize: "300x200^", gravity: "center", crop: "100x100+0+0" )

処理の順序は、

  1. サイズを300×200にする
  2. 基準点を真ん中にする
  3. 基準点を(0,0)として100×100サイズに切り抜く

となっています。このため、リサイズ後の画像の真ん中を基準に正方形に切り抜くことができます。

これを、gravityとcropの順番入れ替えた場合、

Blobオブジェクト.variant( resize: "300x200^", crop: "100x100+0+0",  gravity: "center" )
  1. サイズを300×200にする
  2. 基準点を(0,0)として100×100サイズに切り抜く
  3. 基準点を真ん中にする

となります。切り抜きを実行する時点の基準は画像の真ん中ではなく、デフォルトの左上になっているため、左上から100×100サイズに切り抜かれます。

実質的にgravityの処理は意味がないものになります。

加工後の画像はDBに新しいデータとして保存する

variantメソッドを使って画像を加工すると、加工した画像をDBに保存します。

同じ加工でも別物として保存するため、加工を大量に施す場合はメモリを圧迫することがあります。

この対処法は下記のprocessedメソッドを使います。


processed|既存の画像(バリアント)を使う

variantの後ろに、processedメソッドをつけると既に同じ加工を施したデータがないかを確認し、存在する場合はそれを呼び出します。

Blobオブジェクト.variant( 処理 ).processed
tips

variantメソッドで加工後の画像をバリアントと呼ぶこともあります。

variantとは、変異体(大本が同じで変化したもの)という意味です。


resize|画像サイズの変換

画像のサイズを変更するにはresizeを使います。

resizeの基本形

Blobオブジェクト.variant( resize: "横幅[x高さ!オプション] )
tips
  • 幅の指定は数値のみです。単位(pxなど)はつけません。
  • デフォルトでは横幅に合わせて縮尺を保ち変換します(高さは優先しません)

元の画像のアスペクト比を保ったままリサイズする場合は、横幅のみ指定します。

Blobオブジェクト.variant( resize: "横幅" )

横幅x高さを指定しても!を付けない場合は、横幅のみの指定と同じように変換されます。


実例

 <%= image_tag @user.image.variant( resize: "300" ) %>

これは下記と同じ処理になります。

 <%= image_tag @user.image.variant( resize: "300x200" ) %>

高さを200pxとして指定していますが、横幅を優先した状態でアスペクト比を保つため、高さは指定したとおりにはなりません。

下記のように300×188といったようになります。


resizeの主要オプション

サイズの末尾に特定の記号をつけることで、処理を指定することができます。

オプション内容
!アスペクト比を無視する(強制的に指定したサイズに変換する)
>元の画像の方が大きい場合のみ縮小する
<元の画像の方が小さい場合のみ拡大する
%幅や高さを%で指定する(強制的に指定したサイズに変換する)
^指定した領域を埋める。アスペクト比を保ったまま、空白ができないように埋めます

(参考)


指定したサイズに強制的に変換する

画像のアスペクト比を無視して、指定したサイズに収まるようにサイズ変更するには、末尾に「!」をつけます。

Blobオブジェクト.variant( resize: "横幅x高さ!" )


実例

 <%= image_tag @user.image.variant( resize: "300x200!" ) %>


画像のサイズが指定した通り、300×200となりました。


指定したサイズが小さい場合はサイズ変更する

>を使うと、元の画像のサイズと変更後のサイズを比較して、 指定したサイズが元の画像のサイズより小さい場合はサイズ変更する指定をすることができます。

Blobオブジェクト.variant( resize: "横幅x高さ>" )

実例

例えば、元の画像が640×400のとき、300x200> を指定すれば、縮小します。

 <%= image_tag @user.image.variant( resize: "300x200>" ) %>


元の画像が640×400のとき、800x600> を指定するとリサイズしません。

 <%= image_tag @user.image.variant( resize: "800x600>" ) %>


指定したサイズが大きい場合はサイズ変更する

<を使うと、元の画像のサイズと変更後のサイズを比較して、 指定したサイズが元の画像のサイズより大きい場合はサイズ変更する指定をすることができます。

Blobオブジェクト.variant( resize: "横幅x高さ<" )

実例

例えば、元の画像が640×400のとき、300x200< を指定してもリサイズしません。

 <%= image_tag @user.image.variant( resize: "300x200<" ) %>


元の画像が640×400のとき、800x600< を指定すれば拡大します。

 <%= image_tag @user.image.variant( resize: "800x600<" ) %>


!< と !>

元の画像のアスペクト比を無視して、指定したサイズに強制的に変更したい場合は、!<!>とします。

 <%= image_tag @user.image.variant( resize: "800x600!<" ) %>


%で指定する

パーセント(%)で幅を指定することもできます。

Blobオブジェクト.variant( resize: "横幅%" )

高さも指定する場合は次のようになります。

Blobオブジェクト.variant( resize: "横幅%x高さ%" )
tips

で高さを指定する場合は、数値のみのときと異なり、!がなくても強制的に指定した幅に変換します。


実例1

元の画像が640×400のとき、50%を指定すれば横幅を指定した幅としてアスペクト比を保ち縮小します。

<%= image_tag @user.image.variant( resize: "50%" ) %>


実例2

元の画像が640×400のとき、50%x30%とすれば指定したサイズに変換します。

<%= image_tag @user.image.variant( resize: "50%x30%" ) %>


rotate|回転する

度数(degree)を指定して画像を回転するにはrotateを使います。

Blobオブジェクト.variant( rotate: 数値 )

数値は度数(degree)を単位無しで指定します。

実例1

<%= image_tag @user.image.variant( rotate: 20 ) %>


実例2

マイナスで指定することもできます。

<%= image_tag @user.image.variant( rotate: -20 ) %>


flopなど|反転する

反転するには4つのオプションがあります。

オプション内容
flop左右反転
flip上限反転
transpose上下反転 + 90°回転
transverse左右反転 + 90°回転

指定するには、オプション名: true とします。

Blobオブジェクト.variant( flop: true )


実例

    <%= image_tag @user.image %>
    <%= image_tag @user.image.variant( flop: true) %>
    <%= image_tag @user.image.variant( flip: true) %>
    <%= image_tag @user.image.variant( transverse: true) %>
    <%= image_tag @user.image.variant( transpose: true) %>


blue|ぼかす

blurを使うとぼかしを効かせることができます。

Blobオブジェクト.variant( blur: 数値 )

数値は何を指定しても同じになります。単位はなしです。

実例

    <%= image_tag @user.image.variant( blur: 0 ) %>
    <%= image_tag @user.image.variant( blur: 50 ) %>
    <%= image_tag @user.image.variant( blur: 100 ) %>

上の画像がぼかし加工なしに対して、下がぼかし加工が入った画像です。


monochrome|モノクロにする

monochrome: true

monochrome: trueにするとモノクロの画像を作成できます。

Blobオブジェクト.variant( monochrome: true )

実例

<%= image_tag @user.image.variant( monochrome: true ) %>


type: :bilevel

type: :bilevelにしてもモノクロの画像を作成できます。

Blobオブジェクト.variant( type: :bilevel )
tips

bilevelとは、2つの(バイ)基準(レベル)という意味で、画像処理では1ビットの白黒画像のことを指します。

実例

<%= image_tag @user.image.variant( type: :bilevel ) %>


type: :grayscale

type: :grayscaleにするとグレースケール(白黒のグラデーション)の画像になります。

<%= image_tag @user.image.variant( type: :grayscale ) %>


type: :palette | 水彩画風にする

type: :paletteにすると水彩画風の画像を作成できます。

Blobオブジェクト.variant( type: :palette )

実例

<%= image_tag @user.image.variant( type: :palette ) %>


background|背景色を指定する

backgroundを使うと画像の背景色を指定することができます。

Blobオブジェクト.variant( background: '色' )

実例

<%= image_tag @user.image.variant( background: "yellow", rotate:30) %>


crop|切り抜き(トリミング)

基本形

cropを使うと画像を切り抜くことができます。

Blobオブジェクト.variant( crop: '横幅x高さ+xの位置+yの位置' )

オプションでは4つの値を指定します。まず最初の2つが切り抜く範囲です。次に、切り抜きの基準点を指定します。

基準点は画像右上を(0, 0)としてpxで指定します。

実例

例えば、以下の画像からボートを220×220の正方形で切り出したい場合、切り抜きの基準点は(120, 180)となります。

<%= image_tag @user.image.variant( crop:"220x220+120+180" ) %>

切り抜き後の画像は以下のようになります。

注意点

x座標とy座標の位置が元の画像よりも大きい場合は何も変化しません。

基準点と切り抜きの範囲を足した長さが元の画像よりも大きい場合は、画像がカットされ指定したサイズにはなりません。

tips

切り抜きといえばトリミングという言葉を思い浮かべますが、画像処理におけるcropはトリミングと同じと考えて問題ありません。

MiniMagick(Image Magick)にはcrop以外にtrimオプションも用意されています。

cropは自分で指定して切り抜きを行うのに対し、trimは画像を判定して上下左右で変化のない領域を削り取ります。


gravityとcrop

デフォルトの基準点は左上の(0, 0)の位置ですが、gravityを使うと基準点を中心や右下に変更することができます。

基準点は切り抜きの前に指定する必要があるため、cropよりも前にgravityを指定します。

Blobオブジェクト.variant( gravity: '基準点の指定', crop: '横幅x高さ+xの位置+yの位置' )

gravityで使える値

gravityで基準点を指定するときは以下を使います。

NorthWest, North, NorthEast, West, Center, East, SouthWest, South, SouthEast

内容
center中心
north北(上の真ん中)
northeast北東
northwest北西
east東(右端の真ん中)
west西(左端の真ん中)
south南(下の真ん中)
southwest南西
southeast南東

実例

例えば、以下の画像で、真ん中を基準点として400×400の正方形で切り出す場合にgravity: "center"を使います。

<%= image_tag @user.image.variant( gravity: "center", crop:"400x400+0+0") %>

切り抜き後の画像は以下のようになります。


variantのURL(パス)を取得する

絶対パスの取得

variantメソッドで加工した画像の絶対URLを取得するには、rails_representation_urlメソッドを使います。

rails_representation_url( Blobオブジェクト.variant( 処理 ) )

実例

<%= rails_representation_url(@user.image.variant( resize: "400x400^").processed) %>

http://localhost:3001/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBWWs9IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19–8c3e65bb97bba3a4b6bf15b952e6ba7696536fd8/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2QzNKbGMybDZaVWtpRFRRd01IZzBNREJlQmpzR1ZBPT0iLCJleHAiOm51bGwsInB1ciI6InZhcmlhdGlvbiJ9fQ==–137bb515005af2f19b158401493f25fddd701734/sea.jpg


絶対パスの取得

variantメソッドで加工した画像の絶対URLを取得するには、rails_representation_urlメソッドを使います。

rails_representation_url( Blobオブジェクト.variant( 処理 ) )

実例

<%= rails_representation_url(@user.image.variant( resize: "400x400^").processed) %>


相対パスの取得

variantメソッドで加工した画像の相対URLを取得するには、rails_representation_pathメソッドを使います。

rails_representation_path( Blobオブジェクト.variant( 処理 ) )

実例

<%= rails_representation_path(@user.image.variant( resize: "400x400^" ).processed) %>

/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBWWs9IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19–8c3e65bb97bba3a4b6bf15b952e6ba7696536fd8/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2QzNKbGMybDZaVWtpRFRRd01IZzBNREJlQmpzR1ZBPT0iLCJleHAiOm51bGwsInB1ciI6InZhcmlhdGlvbiJ9fQ==–137bb515005af2f19b158401493f25fddd701734/sea.jpg



参考リンク

',b.captions&&s){var u=J("figcaption");u.id="baguetteBox-figcaption-"+t,u.innerHTML=s,l.appendChild(u)}e.appendChild(l);var c=J("img");c.onload=function(){var e=document.querySelector("#baguette-img-"+t+" .baguetteBox-spinner");l.removeChild(e),!b.async&&n&&n()},c.setAttribute("src",r),c.alt=a&&a.alt||"",b.titleTag&&s&&(c.title=s),l.appendChild(c),b.async&&n&&n()}}function X(){return M(o+1)}function D(){return M(o-1)}function M(e,t){return!n&&0<=e&&e=k.length?(b.animation&&O("right"),!1):(q(o=e,function(){z(o),V(o)}),R(),b.onChange&&b.onChange(o,k.length),!0)}function O(e){l.className="bounce-from-"+e,setTimeout(function(){l.className=""},400)}function R(){var e=100*-o+"%";"fadeIn"===b.animation?(l.style.opacity=0,setTimeout(function(){m.transforms?l.style.transform=l.style.webkitTransform="translate3d("+e+",0,0)":l.style.left=e,l.style.opacity=1},400)):m.transforms?l.style.transform=l.style.webkitTransform="translate3d("+e+",0,0)":l.style.left=e}function z(e){e-o>=b.preload||q(e+1,function(){z(e+1)})}function V(e){o-e>=b.preload||q(e-1,function(){V(e-1)})}function U(e,t,n,o){e.addEventListener?e.addEventListener(t,n,o):e.attachEvent("on"+t,function(e){(e=e||window.event).target=e.target||e.srcElement,n(e)})}function W(e,t,n,o){e.removeEventListener?e.removeEventListener(t,n,o):e.detachEvent("on"+t,n)}function G(e){return document.getElementById(e)}function J(e){return document.createElement(e)}return[].forEach||(Array.prototype.forEach=function(e,t){for(var n=0;n","http://www.w3.org/2000/svg"===(e.firstChild&&e.firstChild.namespaceURI)}(),m.passiveEvents=function i(){var e=!1;try{var t=Object.defineProperty({},"passive",{get:function(){e=!0}});window.addEventListener("test",null,t)}catch(n){}return e}(),function a(){if(r=G("baguetteBox-overlay"))return l=G("baguetteBox-slider"),u=G("previous-button"),c=G("next-button"),void(d=G("close-button"));(r=J("div")).setAttribute("role","dialog"),r.id="baguetteBox-overlay",document.getElementsByTagName("body")[0].appendChild(r),(l=J("div")).id="baguetteBox-slider",r.appendChild(l),(u=J("button")).setAttribute("type","button"),u.id="previous-button",u.setAttribute("aria-label","Previous"),u.innerHTML=m.svg?f:"<",r.appendChild(u),(c=J("button")).setAttribute("type","button"),c.id="next-button",c.setAttribute("aria-label","Next"),c.innerHTML=m.svg?g:">",r.appendChild(c),(d=J("button")).setAttribute("type","button"),d.id="close-button",d.setAttribute("aria-label","Close"),d.innerHTML=m.svg?p:"×",r.appendChild(d),u.className=c.className=d.className="baguetteBox-button",function n(){var e=m.passiveEvents?{passive:!1}:null,t=m.passiveEvents?{passive:!0}:null;U(r,"click",x),U(u,"click",E),U(c,"click",C),U(d,"click",B),U(l,"contextmenu",A),U(r,"touchstart",T,t),U(r,"touchmove",N,e),U(r,"touchend",L),U(document,"focus",P,!0)}()}(),S(e),function s(e,a){var t=document.querySelectorAll(e),n={galleries:[],nodeList:t};return w[e]=n,[].forEach.call(t,function(e){a&&a.filter&&(y=a.filter);var t=[];if(t="A"===e.tagName?[e]:e.getElementsByTagName("a"),0!==(t=[].filter.call(t,function(e){if(-1===e.className.indexOf(a&&a.ignoreClass))return y.test(e.href)})).length){var i=[];[].forEach.call(t,function(e,t){var n=function(e){e.preventDefault?e.preventDefault():e.returnValue=!1,H(i,a),I(t)},o={eventHandler:n,imageElement:e};U(e,"click",n),i.push(o)}),n.galleries.push(i)}}),n.galleries}(e,t)},show:M,showNext:X,showPrevious:D,hide:j,destroy:function e(){!function n(){var e=m.passiveEvents?{passive:!1}:null,t=m.passiveEvents?{passive:!0}:null;W(r,"click",x),W(u,"click",E),W(c,"click",C),W(d,"click",B),W(l,"contextmenu",A),W(r,"touchstart",T,t),W(r,"touchmove",N,e),W(r,"touchend",L),W(document,"focus",P,!0)}(),function t(){for(var e in w)w.hasOwnProperty(e)&&S(e)}(),W(document,"keydown",F),document.getElementsByTagName("body")[0].removeChild(document.getElementById("baguetteBox-overlay")),w={},h=[],o=0}}})
タイトルとURLをコピーしました