静岡市でWeb開発しているフリーランスのkazuomatzです。
Rails 5.2で利用可能になったファイルアップロードに利用できるActive Storageを使ってみました。
これまでは画像のアップロードにはPaperclipを使っていました。Paperclipにはアップロードした時に、自動的に画像サイズを変更して、何パターンかの画像を保存する機能がありました。正方形にクリップしたサムネイル画像もアップロード時に作成できるので、無駄に大きな画像をサムネイル表示するようなページを回避できるので重宝していました。
Paperclipでの実装
class User < ApplicationRecord
has_attached_file :avatar,
styles: { m: '1024x1024>', s: '800x800>',thumb: '640x640#' }
end
<div>
<span>長辺を1024pxにしてリサイズされた画像を表示</span>
<%= image_tag @user.avatar(:m)%>
</div>
<div>
<span>長辺を800pxにしてリサイズされた画像を表示</span>
<%= image_tag @user.avatar(:s)%>
</div>
<div>
<span>サムネイル画像(W:640 x H:640にクロップされた画像)を表示</span>
<span><%= image_tag @user.avatar(:thumb)%></span>
</div>
このように、あらかじめ必要とされる画像サイズを決めておけば、用途用途で使い分けることができるのでとても便利です。
しかし、Webサイトを長いこと運用していると、のちのち、横幅1920pxの画像を表示したい、というようなケースが出てきて困ったことになりました。
Modelに新しいサイズを再定義したとしても、これまで登録された画像はそのサイズで保存されていないので、再生成するようなプログラムを書いてまわすみないなことをやればいいですが、それも面倒です。
そのような時にActive Storageの Variantを使うととっても便利です。Variantを使うと新しい画像サイズが欲しくなった時に、オンデマンドで画像が生成されます。
Active Storageの準備
先ほどのPaperclipの実装をActive Storageを使って書き換えてみます。Variantを利用する場合には、ImageMagickがシステムにインストールされていることと、MiniMagick gemのロードが必要です。
# Use ActiveStorage variant
gem 'mini_magick', '~> 4.8'
# Active Storageのインストール
$ rake active_storage:install
# DBマイグレーション
$ rake db:migrate
これで、development環境ではローカルディスクをアップロード先としてActive Storageが利用可能になります。
アップロード先をAWS S3やAzure Storage Service、Google Cloud Storage Serviceなどにし指定することもできますが、詳しくは、本家のリファレンスを参照してください。
では、先ほどのPaperclipの実装をActive Storageを使った実装に変更してみます。
Active Storageでの実装
class User < ApplicationRecord
has_one_attached :avatar
end
まず、Model側の定義はこれだけです。
has_one_attachedはモデルに1つのファイルを紐付けます。has_many_attachedを用いるとモデルに複数のファイルを紐付けることが可能になります。
便利なのはここからです。Viewを見てみましょう。
<div>
<span>長辺を1024pxにしてリサイズされた画像を表示</span>
<%= image_tag @user.avatar.variant(resize:'1024x1024').processed %>
</div>
<div>
<span>長辺を800pxにしてリサイズされた画像を表示</span>
<%= image_tag @user.avatar.variant(resize:'800x800').processed %>
</div>
<div>
<span>サムネイル画像(W:640 x H:640にクロップされた画像)を表示</span>
<span><%= image_tag @user.avatar.variant(combine_options:{gravity: :center, resize:"640x640^",crop:"640x640+0+0"}).processed%></span>
</div>
Paperclipがあらかじめサイズを決めておいて、その中からどのサイズを使うのかを指定するという方式なのに対して、Variantでは、View側で使用したいサイズをその都度指定できるのが大きな違いです。
Active StorageではVariantが呼び出されると、保存された元画像データを指定されたサイズに変換しそのURLを返します。
processedをつけることで、すでにそのサイズで保存されて画像があれば、変換処理は行われず、即時にURLが返されますので、最初の呼び出しだけ多少時間がかかりますが、それ以降の呼び出しでは時間がかかることはありません。

Paperclipの場合、thumb:'640x640#' と指定すると画像の中心をクリップした640pxの正方形の画像を生成してくれましたが、Variantを使う場合は、以下のように書きます。
@user.avatar.variant(combine_options:{gravity: :center, resize:"640x640^", crop:"640x640+0+0"}).processed

Variantの指定は、ImageMagickのコマンドが利用可能です。
まず、gravity:'center'を指定し、resize:'640x640^'を指定し、画像の縦横の短辺の長さを640pxになるようリサイズします。そして、crop:'640x640+0+0'で、画像の中心点からW:640xH:640のサイズで画像を切り抜きます。
リサイズ+切り抜きといった操作をまとめて行う場合は、combine_optionsを指定する必要があります(参考:What Options Can Be Passed to the Active Storage variant Method?)。
以下の例だと、正しい位置からクロップできませんでした(gravityが効いていない)。
Rails 6になってから、combine_optionsを使用しなくても、下記のコードで動作するようになっているようです。
@user.avatar.variant(gravity: :center, resize:"640x640^", crop:"640x640+0+0").processed
また、Rails 6.1からはcombine_optionsは利用できなくなるとのことです。

もし、後から横幅1920pxサイズの画像が欲しくなった場合も簡単に対応できます。
<div>
<span>長辺を1920pxにしてリサイズされた画像を表示</span>
<%= image_tag @user.avatar.variant(resize:'1920x1920').processed %>
</div>
Webサイトの運営上、ページのリデザインを行う場合などに、簡単に適正サイズの画像が生成できるのは大きなメリットだと思います。大きな画像をレンダリングの際にスタイルシートでwidth/heightを指定して縮小(拡大)表示することはよくあることですが、レンダリングスピードや転送速度を考慮するとあまりよいことでありません。
その辺りを考慮すると、Active Storageを使うメリットは大きいと思います。
おまけ
VariantはImageMagickのコマンドを受け付けますので、以下のようなことも簡単にできてしまいます。ご利用は計画的に!
@user.avatar.variant(resize:'200x200',type: :grayscale).processed

@user.avatar.variant(resize:'200x200',flop:true).processed

パラメータが必要のないオプションはtrueを指定します。
@user.avatar.variant(resize:'200x200',blur:50).processed

@user.avatar.variant(resize:'200x200',rotate:30).processed

@user.avatar.variant(combine_options:{resize:'200x200',border:'5',bordercolor:'red'}).processed
