Techioz Blog

ActiveStorage カスタム アラライザーで「Vips::Error Exception: VipsJpeg: out of order read at line 1440」を取得する

概要

私のカスタムアナライザー

class BlurhashAnalyzer < ActiveStorage::Analyzer::ImageAnalyzer::Vips
  def metadata
    read_image do |image|
      if rotated_image?(image)
        { width: image.height, height: image.width }
      else
        { width: image.width, height: image.height }
      end.merge blurhash(image)
    end
  end

  private

  def blurhash(vips_image)
    # Create a thumbnail first, otherwise the Blurhash encoding is very slow
    byebug
    processed_image = ImageProcessing::Vips.source(vips_image).resize_and_pad(200, 200).call
    thumbnail = ::Vips::Image.new_from_file processed_image.path

    {
      blurhash: Blurhash.encode(
        thumbnail.width,
        thumbnail.height,
        ::Vips::Region.new(thumbnail).fetch(0, 0, thumbnail.width, thumbnail.height).unpack('C*')
      )
    }
  rescue StandardError => e
    raise e if Rails.env.development?
    Rails.logger.error "Error while encoding Blurhash: #{e}"
    {}
  end
end

Blurhash メソッドの例外

(process:29640): VIPS-WARNING **: 17:25:01.323: error in tile 0 x 120
*** Vips::Error Exception: VipsJpeg: out of order read at line 1440

ただし、同じファイルで新しい Vips::Image を作成すると、機能します。

(byebug) ImageProcessing::Vips.source(::Vips::Image.new_from_file vips_image.filename).resize_and_pad(200, 200).call
#<Tempfile:/var/folders/_j/m395qb5d2yscnx89dswmrxgm0000gn/T/image_processing20220624-29640-aflgbs.jpg>

(byebug) ImageProcessing::Vips.source(::Vips::Image.new_from_file vips_image.filename, access: :sequential).resize_and_pad(200, 200).call
#<Tempfile:/var/folders/_j/m395qb5d2yscnx89dswmrxgm0000gn/T/image_processing20220624-29640-eflx0v.jpg>

Rails 7.0.3 Analyzer::ImageAnalyzer::Vips のソースコードを確認しました。

...
    def read_image
        download_blob_to_tempfile do |file|
          require "ruby-vips"

          image = instrument("vips") do
            ::Vips::Image.new_from_file(file.path, access: :sequential)
          end

上で行ったのと同じ Vips::Image の作成方法ですが、直接使用すると例外が発生します。

この問題が https://github.com/libvips/pyvips/issues/96 に関連していることはわかっていますが、ここではローテーションしませんでした。

解決策

これは、イメージをストリーミング モードで開いた後、そのイメージを複数回読み取ろうとした場合に発生します。

ドキュメントには背景を説明した章があります。

https://translate.google.com/translate?hl=ja&sl=en&tl=ja&u=https://www.libvips.org/API/current/How-it-opens-files.md.html

たとえば、次のようにファイルを処理するとします。

image = Vips::Image.new_from_file "something.jpg", access: :sequential
image = 255 - image
image.write_to_file "something-inverted.jpg"

libvips は、最後の write_to_file まですべての計算を遅らせてから、イメージをストリーミングします。デコード、処理、再エンコードはすべて同時に並行して実行され、画像のごく一部のみがメモリに保持されます。

欠点は、ワンショット処理しかできないことです。これは、たとえば次のように失敗します。

image = Vips::Image.new_from_file "something.jpg", access: :sequential
image = 255 - image
image.write_to_file "something-inverted.jpg"
avg = image.avg()

画像が読み取られ、処理され、破棄され、ピクセルが残っていないため、パイプラインの実行後は平均を計算できません。

デフォルトのランダム アクセス モードを使用すると、正常に動作します。

image = Vips::Image.new_from_file "something.jpg"
image = 255 - image
image.write_to_file "something-inverted.jpg"
avg = image.avg()

JPG ファイルはメモリ配列にデコードされ、処理と保存のみが並行して実行され、ピクセルは後で平均を計算するためにまだ存在します。

あなたの場合、画像はシーケンシャルモードで開かれていますが、ピクセルを2回読み取ろうとしています。オリジナルをランダム アクセス モードで開くか、image = image.copy_memory() を実行してメモリ内に再利用可能なコピーを作成する必要があります。