My custom analyzer
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
exception in blurhash method
(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
But if I create a new Vips::Image with the same file, it works:
(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>
I checked Rails 7.0.3 Analyzer::ImageAnalyzer::Vips
source code:
...
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
it's the same way to create Vips::Image as I did above, but if I use it directly will get the exception.
I know this issue is related to https://github.com/libvips/pyvips/issues/96, but I did not rotate here.
CodePudding user response:
This happens when you open an image in streaming mode but then try to read from it more than once.
There's a chapter in the docs with some background:
https://www.libvips.org/API/current/How-it-opens-files.md.html
For example, if you process a file like this:
image = Vips::Image.new_from_file "something.jpg", access: :sequential
image = 255 - image
image.write_to_file "something-inverted.jpg"
libvips will delay all computation until the final write_to_file
, and then stream the image. The decode, process and reencode will all execute at the same time and in parallel, and it will only keep a small part of the image in memory.
The downside is that you can only do one-shot processing. This will fail for example:
image = Vips::Image.new_from_file "something.jpg", access: :sequential
image = 255 - image
image.write_to_file "something-inverted.jpg"
avg = image.avg()
Since you can't compute the average after the pipeline has executed because the image has been read, processed and disposed, and there are no pixels left.
If you use the default random-access mode, it works fine:
image = Vips::Image.new_from_file "something.jpg"
image = 255 - image
image.write_to_file "something-inverted.jpg"
avg = image.avg()
Now the JPG file will be decoded to a memory array, only the process and save will run in parallel, and the pixels will still be there to compute the average later.
In your case, the image has been opened in sequential mode but you are trying to read the pixels twice. You need to either open the original in random acceess mode, or you need to do image = image.copy_memory()
to make a copy in memory that you can reuse.