はじめに
Railsで画像を扱う方法はいくつかパターンがあり、それぞれ利用するGemやライブラリが違います。
ActiveStorage、CarrierWave、Shrine、refile、ImageMagick、ImageProcessing、MiniMagick、RMagick、libvips。。。
この辺の用語を自分の中で整理したいと思います。
環境
Mac OS Catalina 10.15.7
ruby 2.6.5
Rails 6.0.5
bundler 2.3.13
node 12.16.1
yarn 1.22.18
webpack-dev-server 3.11.2
今はActiveStorageというものがRailsに元から入っているので、これを使うことは前提とします。
必ずしもActiveStorageを使うべきということはないと思います。
例えばバリデーション機能がないことや、エラー時のキャッシュの仕組みがありません。(2022/5現在)
現時点でバリデーションをするためにはactive_storage_validationsやactivestorage-validator等のgemを導入することになるでしょう。
ActiveStorageは以下のコマンドと、has_one_attached
等の設定によってすぐに画像投稿機能が出来上がります。ただshow画面でvariantメソッドを使ったサムネイル画像の表示など、画像処理機能を活用するには、これだけだと足りません。
## そのRailsアプリでActiveStorageを使えるようにする
bin/rails active_storage:install
まず結論
整理した結果、このあたりのパターンが一般的かなと思っています。
- ActiveStorageが、MiniMagickを使ってImageMagickの画像処理機能を使う
- ActiveStorageが、ImageProcessingにあるコマンドならImageProcessing経由で、
無いならMiniMagick経由でImageMagickの画像処理機能を使う - ActiveStorageが、ImageProcessing経由でVipsを使ってlibvipsの画像処理機能を使う
ここからはどのように整理したのかを忘れないように書いておきます。
ImageMagickや、libvipsは、画像処理ソフトウェア
ActiveStorageの画像処理機能を十分に活用するには、ImageMagickかlibvipsが必要
ImageMagickもlibvipsもGemではないので、開発環境では以下のどちらかが必要
brew install imagemagick
brew install vips
情報の豊富なImageMagickか、高速なlibvipsか、選びましょう。
libvipsは、ImageMagickより画像形式のサポートが少ないが、メモリ効率や速度が良いです。
libvipsを使うのに、installするのはvipsなのが、少しわかりづらいかもしれません。
ImageMagickは、libvipsに比べて知名度が高く普及も進んでいます。しかしlibvipsは10倍高速かつメモリ消費も1/10です。JPEGファイルの場合、libjpeg-devをlibjpeg-turbo-devに置き換えると2〜7倍高速になります。 (Railsガイドより引用(Rails7.0版))
ImageProcessingは、MiniMagickを内包した上位互換のGem
- MiniMagickは、ImageMagickをRailsで扱えるようにしてくれるGem
- ImageProcessingは、MiniMagickで提供できない画像サイズを調整する機能があり、ImageProcessingのgemを入れると、MiniMagickのgemも一緒にインストールされる
gem "image_processing", ">= 1.2"
略
image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
略
- ちなみにRMagickは、ImageMagickをrubyで扱えるようにするGemで、MiniMagickと似たようなもの
RailsのデフォルトはMiniMagick経由でImageMagickを使う設定である
- Active StorageのデフォルトのvariantプロセッサはMiniMagickだが、Vipsを指定することもできる
- つまり、variantプロセッサをVipsにして、Vips経由でlibvipsを使うこともできる
ImageMagickではなくlibvipsを使う設定にするには
Vipsをinstallし、バリアントプロセッサをVipsにする
brew install vips #ローカル環境の場合、Macにinstallする
config.active_storage.variant_processor = :vips
VipsとMiniMagickは互換性が高いので、MiniMagickを使うのと大体同じように使える。
なのでMiniMagickを使ったコードサンプルは参考になる。
ただし、たまに書き方を変更しないといけない場合があります。
Railsガイド7.0に、一例が載っています。
Gemごとのサムネイル生成機能の違い
CarrierWave: アップロードのタイミングで生成
Shrine: アップロード後に非同期で生成可能
ActiveStorage: 画像URLにアクセスしたタイミングで生成
ローカルで試してみる
rails new sample_app
cd sample_app
bin/rails active_storage:install
bin/rails g scaffold user name image:attachment
bin/rails db:migrate
bin/rails s
この状態でも、User作成画面で画像の投稿は可能です。
bin/rails g scaffold user name image:attachment
↑rails6.0以降で使えるこのようなモデル生成コマンドによって、
has_one_attached :image
の記述を勝手にしてくれます。
この状態だと、variantを使って、サイズ指定した画像を表示することはできません。
ImageMagickか、libvipsを使う必要がありますが、今回はlibvipsでやってみます。
gem "image_processing", ">= 1.2"
bundle install
brew install vips #ローカル環境の場合、Macにinstallする
config.active_storage.variant_processor = :vips
<%= image_tag @user.image.variant(resize_to_limit: [100, 100]) %>
これで、show画面でサムネイル画像を表示することができました。
libvipsを使っており、ImageMagickは使っていません。
おまけ(ActiveStorageの便利な機能)
ActiveStorageはダイレクトアップロード機能に対応しています。
アプリケーションサーバーを経由せずに直接クラウドストレージにアップロードできるというものです。
サーバーへの負担は減り、アップロード時間は速くなります。
Gemを使ってActiveStorageにバリデーション機能を持たせた場合に、
ダイレクトアップロード機能を使うと、不完全なバリデーションになってしまいます。
不完全なバリデーションを許容するか、非同期でバリデーションを実行するか。。
例) jpgのみ許可するバリデーションを作った。通常であれば本来ファイルの中身を見てjpgかどうか判断するが、
ダイレクトアップロード機能を使っている場合、ファイル名の拡張子のみを見て判断されてしまう。
ローカルで試してみます。
Rails6.0だと、これが記述してあります。
require("@rails/activestorage").start()
この状態で、
<!-- has_many_attached :imageの設定済みです -->
略
<div class="field">
<%= form.label :images %>
<%= form.file_field :images, multiple: true, direct_upload: true %>
</div>
略
ここでは複数ファイルアップロードに対応させています。
direct_upload: true
の記述でダイレクトアップロード機能が使えます。
ダイレクトアップロードには、実行時に発火するJSイベントが用意されており、
アップロードの進捗表示なども可能です。
ソースコード(JSファイルへのコピペでOK)
addEventListener("direct-upload:initialize", event => {
const { target, detail } = event
const { id, file } = detail
target.insertAdjacentHTML("beforebegin", `
<div id="direct-upload-${id}" class="direct-upload direct-upload--pending">
<div id="direct-upload-progress-${id}" class="direct-upload__progress" style="width: 0%"></div>
<span class="direct-upload__filename"></span>
</div>
`)
target.previousElementSibling.querySelector(`.direct-upload__filename`).textContent = file.name
})
addEventListener("direct-upload:start", event => {
const { id } = event.detail
const element = document.getElementById(`direct-upload-${id}`)
element.classList.remove("direct-upload--pending")
})
addEventListener("direct-upload:progress", event => {
const { id, progress } = event.detail
const progressElement = document.getElementById(`direct-upload-progress-${id}`)
progressElement.style.width = `${progress}%`
})
addEventListener("direct-upload:error", event => {
event.preventDefault()
const { id, error } = event.detail
const element = document.getElementById(`direct-upload-${id}`)
element.classList.add("direct-upload--error")
element.setAttribute("title", error)
})
addEventListener("direct-upload:end", event => {
const { id } = event.detail
const element = document.getElementById(`direct-upload-${id}`)
element.classList.add("direct-upload--complete")
})
require("@rails/ujs").start()
略...
略...
require("direct_uploads")
おわりに
libvipsを使って、ローカルではしっかり動いてくれました。
気になることとしてはこの辺りです。
- より高度なことをする場合、libvipsだと情報が少なくて困るかも
- 本番環境でもしっかり動くのか
個人開発アプリにlibvipsを使ってみて、本番で動かしてみたいと思っています。
その時の気づき等があったら、また更新したいと思います。
参考にさせていただいた書籍
パーフェクトRubyonRails 増補改訂版