When you write your next CarrierWave Show archive.org snapshot uploader, consider processing your images with libvips Show archive.org snapshot instead of ImageMagick.
Reasons for libvips
There are several upsides to using libvips over ImageMagick:
- libvips is considerably faster and uses less memory Show archive.org snapshot .
- ImageMagick has a large attack surface that has repeatedly caused security incidents in the past (compare ImageMagick CVEs Show archive.org snapshot with libvips CVEs Show archive.org snapshot ).
- Ubuntu is sometimes slow to fix the numerous ImageMagick vulnerabilities. E.g. this Ubuntu update from June 2021 Show archive.org snapshot is fixing ImageMagick CVEs from November 2020.
- We repeatedly had major pains upgrading ImageMagick across major releases of Ubuntu.
I also found a few downsides:
- Documentation and examples for libvips are sometimes sparse.
- When you do find examples, it is often for the libvips C API or the libvips command-line tools. It is not always obvious how to map an example to the Ruby API ( ruby-vips Show archive.org snapshot gem).
Installing libvips
Ubuntu packages
On a recent Ubuntu version, you can install libvips like this:
sudo apt install libvips42 libvips-dev libvips-tools
If you're on an older Ubuntu LTS (20.04, 22.04), your package sources will contain outdated versions of libvips. You can install a modern libvips version using our PPA.
If you're hosting on makandra servers these packages have already been installed on your application servers.
RubyGem
In your Ruby code you will interact with libvips using the
ruby-vips gem
Show archive.org snapshot
. This is already a dependency of carrierwave
, so you won't have to add anything else to your Gemfile
.
libvips is not vendored into the gem. You will still need the Ubuntu packages shown above.
Examples for common requirements
Basic resizing
In your uploader, include CarrierWave::Vips
instead of CarrierWave::MiniMagick
. You can now use basic resize macros like resize_to_fit
or resize_to_fill
:
class DocumentUploader < CarrierWave::Uploader::Base
include CarrierWave::Vips
process :resize_to_fit => [1000, 500]
version :thumbnail do
process :resize_to_fill => [64, 64]
end
end
Converting colors to sRGB
Users may upload images with exotic color profiles, but browsers only support a few standard profiles. Because of this it is often a good idea to convert all image color to sRGB.
Easy mode with colourspace
If you're looking for a quick way to get rid of exotic color profiles, use the colourspace(:srgb)
method:
class DocumentUploader < CarrierWave::Uploader::Base
include CarrierWave::Vips
process :convert_to_srgb
private
def convert_to_srgb
vips! do |builder|
builder.colourspace(:srgb)
end
end
end
Important
This requires a modern version of libvips, e.g. 8.15. In older versions the
colourspace
function ignores embedded color profiles.
Pro mode with icc_transform
If you need more control over the color conversion process, you can use the icc_transform
method. This allows for the following:
- You can enable or disable LCMS black point compensation
- You can choose the rendering intent (
:perceptual
,:relative
,:saturation
or:absolute
) - You can choose the Profile Connection Space (
:lab
or:xyz
) - You can control the target profile using an
.icc
file - You can control which profile to assume when an image has no embedded profile
- You can configure a transformation chain using multiple profiles
The uploader below converts images to the sRGB2014.icc
profile. The sRGB profile will also be embedded into the converted image.
Note that the uploader expects you to download
sRGB2014.icc
Show archive.org snapshot
and
ISOcoated_v2_eci.icc
Show archive.org snapshot
and place it in a lib
folder.
class DocumentUploader < CarrierWave::Uploader::Base
include CarrierWave::Vips
# The target profile for the converted file.
OUTPUT_PROFILE = 'lib/sRGB2014.icc'
# Input profiles used when an image has no embedded profile.
UNKNOWN_SRGB_INPUT_PROFILE = 'lib/sRGB2014.icc'
UNKNOWN_CMYK_INPUT_PROFILE = 'lib/ISOcoated_v2_eci.icc'
# Common options for the vips-icc-transform command. For additional options see:
# https://www.rubydoc.info/gems/ruby-vips/Vips/Image#icc_transform-instance_method
ICC_TRANSFORM_OPTIONS = {
intent: :relative, # one of [:perceptual, :relative, :saturation, :absolute]
black_point_compensation: true,
}
process :convert_to_srgb
private
def convert_to_srgb
if embedded_profile?
srgb_image = converted_image.icc_transform(OUTPUT_PROFILE, embedded: true, **ICC_TRANSFORM_OPTIONS)
elsif cmyk?
srgb_image = converted_image.icc_transform(OUTPUT_PROFILE, input_profile: UNKNOWN_SRGB_INPUT_PROFILE, **ICC_TRANSFORM_OPTIONS)
else
srgb_image = converted_image.icc_transform(OUTPUT_PROFILE, input_profile: UNKNOWN_SRGB_INPUT_PROFILE, **ICC_TRANSFORM_OPTIONS)
end
srgb_image.write_to_file(current_path)
end
def embedded_profile?
vips_image.get("icc-profile-data")
true
rescue Vips::Error
false
end
def cmyk?
vips_image.interpretation == :cmyk
end
end
Stripping color profiles
The uploader below will strip any embedded color profiles from images. It does not convert any color values.
A use case is to remove embedded ICC profiles after converting to sRGB. As browsers will usually default to sRGB Show archive.org snapshot when no profile is embedded, this can be a way to save some bytes.
class DocumentUploader < CarrierWave::Uploader::Base
include CarrierWave::Vips
process :strip_icc_fields
private
def strip_icc_fields
fields = vips_image.get_fields.select { |field| field.start_with?('icc-') }
return if fields.blank?
stripped_image = vips_image.mutate do |mutable|
fields.each do |field|
mutable.remove!(field)
end
end
stripped_image.write_to_file(current_path)
end
end
Generating PDF previews
The uploader below will use the standard convert
method to produce a JPG thumbnail of the first page of a PDF document.
Note that this uses the DoesCarrierwaveFormatPatches
trait from CarrierWave: How to generate versions with different file extensions.
class DocumentUploader < CarrierWave::Uploader::Base
include CarrierWave::Vips
include DoesCarrierwaveFormatPatches
version :thumb do
process :convert => 'jpg'
process :resize_to_fit => [200, 400]
set_file_specs file_type: 'image/jpeg', extension: :jpg
end
end
Stripping metadata
It's often a good idea to strip metadata (like EXIF headers) from an uploaded image, as this may contain private data like the photographer's camera or GPS position.
The uploader below will strip all metadata from an image:
class DocumentUploader < CarrierWave::Uploader::Base
include CarrierWave::Vips
process :strip_all_metadata
private
def strip_all_metadata
fields = vips_image.get_fields
return if fields.blank?
stripped_image = vips_image.mutate do |mutable|
fields.each do |field|
mutable.remove!(field)
end
end
stripped_image.write_to_file(current_path)
end
end
Sometimes you want to strip metadata, but keep embedded ICC color profiles. You can do so like this:
class DocumentUploader < CarrierWave::Uploader::Base
include CarrierWave::Vips
process :strip_metadata_except_icc_profile
private
def strip_metadata_except_icc_profile
fields = vips_image.get_fields.reject { |field| field.start_with?('icc-') }
return if fields.blank?
stripped_image = vips_image.mutate do |mutable|
fields.each do |field|
mutable.remove!(field)
end
end
stripped_image.write_to_file(current_path)
end
end
Orienting an image upright
The uploader below will rotate the pixels of an image if its EXIF header indicates rotated input data. After conversion the image raster will be saved in an upright orientation.
Tip
This conversion is not as useful as it used to be. Chrome, Firefox and Safari will all honor EXIF orientation.
class DocumentUploader < CarrierWave::Uploader::Base
include CarrierWave::Vips
process :auto_orient
private
def auto_orient
rotated = vips_image.autorot
rotated.write_to_file(current_path)
end
end
Important
Image rotation a modern version of libvips, e.g. 8.15. In older versions the
#autorot
function will segfault.
Writing custom processing methods
The examples above mostly using custom process methods you can register with .process
. Here is how you write your own processing methods.
Reading image details
Your processing methods can use #vips_image
method to get a
Vips::Image
Show archive.org snapshot
instance for the image being processed:
class DocumentUploader < CarrierWave::Uploader::Base
include CarrierWave::Vips
process :print_image_details
private
def print_image_details
"Width is #{vips_image.width}"
"Height is #{vips_image.height}"
"Metadata headers are #{vips_image.get_fields}"
end
end
Changing the image
The object returned by #vips_image
has many methods to edit the image. Check the
docs for Vips::Image
Show archive.org snapshot
to see which editing operation are available.
Vips::Image
has a mostly immutable API, where every editing operation returns a new Vips::Image
instance. You must write the results to disk for your changes to be persisted:
class DocumentUploader < CarrierWave::Uploader::Base
include CarrierWave::Vips
process :scale_up
private
def scale_up
scaled = vips_image.scale(2)
scaled.write_to_file(current_path)
end
end
Info
Even though most libvips operations return a new, immutable
Vips::Image
, libvips uses a clever internal representation that avoids the need to allocate memory for every intermediate image. See the How it works Show archive.org snapshot section on libvips.org for details.
Some destructive methods (drawing operations, writing metadata) can only be called from inside a #mutate
block:
class DocumentUploader < CarrierWave::Uploader::Base
include CarrierWave::Vips
process :set_copyright_field
private
def set_copyright_field
mutated = vips_image.mutate do |mutable|
mutable.set!("exif-ifd0-Copyright", "Copyright (c) #{Date.today.year} SuperApp")
end
mutated.write_to_file(current_path)
end
end
Note
You can also use the
vips! { |builder| ... }
pattern here. This yields anImageProcessing::Vips
Show archive.org snapshot object with some additional methods. However I found it more straightforward to use theVips::Image
API directly.