Vips::Image.new_from_file Swaps Width/Height in Rails Server, but Not Console
Bug Description
When loading an image from a file path, Vips::Image.new_from_file(path) returns swapped width and height dimensions. This issue only occurs when the code is executed within a Rails server process (e.g., Puma/Unicorn). The exact same code, reading the exact same file path, returns the correct dimensions when run from a rails console.
A peculiar workaround is that providing any optional argument to the method call (e.g., fail: true or autorotate: false) corrects the behavior in the server environment. This suggests a potential issue with how default options are handled or a caching-related problem specific to the server runtime.
Steps to Reproduce
-
Place a binding.pry in any Rails controller action.
-
In the pry session, download an image that has EXIF orientation data:
require 'down'
url = 'https://raw.githubusercontent.com/recurser/exif-orientation-examples/master/Landscape_6.jpg'
tempfile = Down.download(url)
- Load the image, apply autorot, and overwrite the tempfile:
image = Vips::Image.new_from_file(tempfile.path)
image.autorot.write_to_file(tempfile.path)
- Reload the image from the same path without any options and inspect its dimensions. The dimensions will be swapped.
# In the server pry session
reloaded_image = Vips::Image.new_from_file(tempfile.path)
puts "BUG -> In Server: #{reloaded_image.width}x#{reloaded_image.height}"
# Example Output: BUG -> In Server: 3024x4032
path = tempfile.path # Copy this path for the next step
- In a new terminal, open a rails console. Use the path from the previous step to load the same file:
# In the rails console session
console_image = Vips::Image.new_from_file('PASTE_THE_TEMPFILE_PATH_HERE')
puts "OK -> In Console: #{console_image.width}x#{console_image.height}"
# Example Output: OK -> In Console: 4032x3024
- Go back to the server pry session. Run the same command but add any option, like fail: true. The dimensions will now be correct.
# Back in the server pry session
workaround_image = Vips::Image.new_from_file(tempfile.path, fail: true)
puts "FIXED -> Server with option: #{workaround_image.width}x#{workaround_image.height}"
# Example Output: FIXED -> Server with option: 4032x3024
Expected Behavior
Vips::Image.new_from_file(tempfile.path) should consistently return the correct dimensions (4032x3024 for the example image) in all execution environments, including the Rails server.
Actual Behavior
The dimensions are swapped (3024x4032) only when run inside a Rails server without any extra options. The dimensions are correct when run in a Rails console or when any option is added to the method call in the server.
Screenshots
This is the correct behaviour after adding an option
Top terminal is the Rails server; bottom terminal is the Rails console:
Desktop
- OS:Fedora 42(local) and ubuntu (production environment)
- Ruby-vips 2.2.3
- libvips 8.16.1 (on all envs)
Thank you in advance
Hi @CarlosRoque, sorry for being so slow getting to this.
image = Vips::Image.new_from_file(tempfile.path)
image.autorot.write_to_file(tempfile.path)
I suspect this is the problem -- libvips caches image load, so if you open the same file twice, the second open will return the same file.
You can see this easily in irb, for example:
irb(main):001> require "vips"
=> true
irb(main):002> x = Vips::Image.new_from_file("st-francis.jpg")
=> #<Image 30000x26319 uchar, 3 bands, srgb>
irb(main):003> x.avg
=> 78.85239180017815
That's a pretty big image! You'll see that the new_from_file returns almost instantly (it just reads the image header), but the avg method takes several seconds. It's decompressing the huge JPG to a temp file, then scanning the temp file to compute the average pixel value.
Without quitting irb, try running the same operations:
irb(main):004> y = Vips::Image.new_from_file("st-francis.jpg")
=> #<Image 30000x26319 uchar, 3 bands, srgb>
irb(main):005> y.avg
=> 78.85239180017815
You'll see that the new_from_file is instant again, but now the avg is instant too. libvips has cached and reused the temp file that holds the decompressed image, and it's also cached and reused the result of avg.
In your case, the first new_from_file is decompressing to a temp, and the second new_from_file is reusing the first result:
reloaded_image = Vips::Image.new_from_file(tempfile.path)
An easy fix is to use revalidate to force a reload:
reloaded_image = Vips::Image.new_from_file(tempfile.path, revalidate: true)
That will force libvips to return to the input image and reload it, invalidating the old cache entry. You'll need libvips 8.15 or later.
I should have said, this will invert (photographic negative) an image:
$ vips invert k2.jpg x.jpg
$
But this will NOT work:
$ vips invert k2.jpg k2.jpg
The problem is that libvips is a streaming image processing library: pixels are evaluated on demand, and data is only read from the input when it is needed by the output. This means it will start to write the output before it has read the input, and that means you can't usually write back to the thing you read from.
Your autorot example sort-of works, but only by chance. With many operations, you'll just end up deleting your input.
I would not write directly back to your input. Write to a new temp file, then rename.