carrierwaveで画像フォーマットを複数持つ方法
carrierwaveはサンプルを読む限りでは保存形式をjpegとかpngとかに決め打って、versionではリサイズしかしないのが一般的な使い方のようです。
しかしこれだと「ネイティブアプリではwebp形式で配信し、ブラウザにはjpeg形式で配信したい」というようなユースケースに対応できません。
carrierwaveはこういったフォーマットのマルチ化ができないのかと思っていたのですが、コードレベルでちょっと工夫すれば簡単にできました。
class PictureUploader < CarrierWave::Uploader::Base version :limit_500x_jpeg do process convert: 'jpeg' process resize_to_limit: [500, 10000] end version :limit_500x do process convert: 'webp' process resize_to_limit: [500, 10000] def full_filename(for_file) super(for_file).sub(/\.jpeg/, '.webp') end end version :fill_500x500 do process convert: 'webp' process resize_to_fill: [500, 500] def full_filename(for_file) # ・・・※2 super(for_file).sub(/\.jpeg/, '.webp') end end def filename #・・・※1 "#{secure_token}.jpeg" if original_filename.present? end def extension_white_list %w(jpg jpeg gif png webp) end def secure_token if model.attributes[mounted_as.to_s].present? && !model.send("#{mounted_as}_changed?") model.attributes[mounted_as.to_s].split('.').first else get_secure_token end end def get_secure_token var = :"@#{mounted_as}_secure_token" model.instance_variable_get(var) or model.instance_variable_set(var, SecureRandom.uuid) end end
このように、まずは全体のfilenameをjpegで決め打って(※1の部分)、あとは各バージョンでプライベートメソッドをオーバーライドして(※2の部分)filenameを変更します。 これだけでアップロード先のURLも画像URLもうまく対応できます。
ちなみに本当は※1の部分でjpegを決め打たずにextensionなしのfilenameを格納し、各バージョンごとにextensionをつけたほうが見た目は綺麗なのですがその方法だとmodel.picture.urlというバージョン抜きのURLの拡張子に対応できないので、少し違和感がありますがsubで置換させることにしました。
ちなみにsub置換のコストは手元のベンチマークでは誤差レベルです。
carrierwaveの限界
とはいえ、個人的にはcarrierwaveは便利ですが限界があるように感じてきました。まずリサイズに時間がかかること、画像のサイズ変換というあまりアプリケーションのビジネスロジックに関係ない計算コストを持たないといけないこと、サイズの決め打ちがマルチデバイス対応で辛いこと等です。
大手だと独自のリサイズ配信システムをnginxモジュールなどで作ってCDNを咬ませてキャッシュさせるようなことをしていると思いますが、これも知識を持った人でないと開発・運用は難しいでしょう。
あまりはやってないですがこういう配信サービスもあるので、これを利用するのも手です。 また、google app engineでは標準で画像配信を動的に行う仕組みを持っているので、配信主体のアプリケーションはappengineで作るか、配信部分だけでもapp engineで社内サービス化するのがいいかも。