微炭酸ログ

Ruby や Rails を中心に。

【Rails】CarrierWave で画像の縦横の大きさの値をバリデーションする

ファイルサイズのバリデーション(10MBまで可、みたいな)は CarrierWave の中に組み込まれていますが、縦横の大きさについてバリデーションする機能は現状ありません(ActiveStorage なら簡単にできるらしいですね)。

今回はその「CarrierWave で縦横の大きさの値をバリデーションする機能」について自前で実装してみました。

実装

アップローダーごとにバリデーションの有無と基準値を定義することができました。
具体的には、以下のように min_dimensions を定義したアップローダーのみ、その値を基準にしたバリデーションがかかります。

↓ app/uploaders/application_uploader.rb

class ApplicationUploader < CarrierWave::Uploader::Base
  include CarrierWave::MiniMagick

  before :cache, :check_dimensions!

  def min_dimensions
    nil
  end

  private

  def check_dimensions!(new_file)
    # NOTE: オリジナル画像だけチェックする(リサイズしたものはチェックしない)
    return if version_name.present?

    expected_min_width, expected_min_height = min_dimensions
    return if expected_min_width.blank? && expected_min_height.blank?

    width, height = ::MiniMagick::Image.open(new_file.file)[:dimensions]
    if width < expected_min_width || height < expected_min_height
      raise CarrierWave::IntegrityError, I18n.translate(:'errors.messages.dimensions_error', min_width: expected_min_width, min_height: expected_min_height)
    end
  end
end

↓ app/uploaders/image_uploader.rb(例:1000 × 1000 より小さいものを弾くバリデーション)

class ImageUploader < ApplicationUploader
  def min_dimensions
    [1000, 1000]
  end
end

参考にした情報など

基本的には CarrierWave のファイルサイズのバリデーションの実装↓を参考にしました。
(コールバックや raise の部分、アップローダーごとにバリデーションを使うかどうかを決められるようにするための書き方など)
Added errors on file size by gautampunhani · Pull Request #1026 · carrierwaveuploader/carrierwave

縦横の値(dimensions)を取得する方法も、CarrierWave の Wiki にありました。
How to: Get image dimensions · carrierwaveuploader/carrierwave Wiki

CarrierWave 独自のコールバック(フック)についても、Wiki を参考にしました。
How to: use callbacks · carrierwaveuploader/carrierwave Wiki

そのほかのポイント

version を使ってサムネイルなども一緒に生成している場合に、リサイズ後のファイルでもバリデーションが実行されてしまいました。

ここで少しハマりましたが、コールバックの Wiki を読んでいると、「version_name の値を見ることでそれがオリジナルなのかどうかがわかる(nil ならオリジナル)」と知り、無事以下のようにガードすることができました。

def check_dimensions!(new_file)
  # NOTE: オリジナル画像だけチェックする(リサイズしたものはチェックしない)
  return if version_name.present?

  # ...

テストコード

特にポイントなどありませんが一応載せておきます。

context 'メイン画像の大きさが 1000 × 1000 の場合' do
  let(:book) { build(:book, image: Rack::Test::UploadedFile.new(Rails.root.join('spec/fixtures/images/1000x1000.png'))) }
  it 'valid' do
    expect(book).to be_valid
  end
end

context 'メイン画像の大きさが 1000 × 999 の場合' do
  let(:book) { build(:book, image: Rack::Test::UploadedFile.new(Rails.root.join('spec/fixtures/images/1000x999.png'))) }
  it 'is invalid' do
    expect(book).to be_invalid
    expect(book.errors[:image]).to include '縦:1000px 以上 かつ 横:1000px 以上のファイルをアップロードしてください'
  end
end

context 'メイン画像の大きさが 999 × 1000 の場合' do
  let(:book) { build(:book, image: Rack::Test::UploadedFile.new(Rails.root.join('spec/fixtures/images/999x1000.png'))) }
  it 'is invalid' do
    expect(book).to be_invalid
    expect(book.errors[:image]).to include '縦:1000px 以上 かつ 横:1000px 以上のファイルをアップロードしてください'
  end
end