Updated: CarrierWave: Processing images with libvips

Posted . Visible to the public. Auto-destruct in 43 days

mutate blocks are only required for draw operations, and for writing metadata. You cannot call operations like scale() on mutable images.

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.

Changes

  • When you write your next [CarrierWave](https://github.com/carrierwaveuploader/carrierwave) uploader, consider processing your images with [libvips](https://www.libvips.org/) instead of ImageMagick.
  • # Reasons for libvips
  • There are several **upsides** to using libvips over ImageMagick:
  • - [libvips is considerably faster and uses less memory](https://github.com/libvips/libvips/wiki/Speed-and-memory-use).
  • - ImageMagick has a large attack surface that has repeatedly caused security incidents in the past (compare [ImageMagick CVEs](https://www.cvedetails.com/vulnerability-list/vendor_id-1749/Imagemagick.html) with [libvips CVEs](https://www.cvedetails.com/vulnerability-list/vendor_id-22008/Libvips-Project.html)).
  • - Ubuntu is sometimes slow to fix the numerous ImageMagick vulnerabilities. E.g. [this Ubuntu update from June 2021](http://changelogs.ubuntu.com/changelogs/pool/universe/i/imagemagick/imagemagick_6.9.10.23+dfsg-2.1ubuntu11.4/changelog) is fixing ImageMagick CVEs from November 2020.
  • - We repeatedly had [major pains](https://makandracards.com/makandra/487405-issues-and-their-solutions-after-an-upgrade-to-ubuntu-20-04#section-fix-imagemagick) 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](https://github.com/libvips/ruby-vips) gem).
  • # Installing libvips
  • ## Ubuntu packages
  • On a recent Ubuntu version, you can install libvips like this:
  • ```bash
  • 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](https://makandracards.com/makandra/621073-installing-modern-libvips-versions-from-our-ppa).
  • If you're hosting on [makandra servers](https://opscomplete.com/ruby) 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](https://github.com/libvips/ruby-vips). 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](https://makandracards.com/makandra/62567-carrierwave-built-in-resize-methods) like `resize_to_fit` or `resize_to_fill`:
  • ```ruby
  • 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](https://makandracards.com/makandra/473154-always-convert-and-strip-user-provided-images-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:
  • ```ruby
  • 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](https://makandracards.com/makandra/621073-installing-modern-libvips-versions-from-our-ppa) 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](https://www.color.org/srgbprofiles.xalter) and [ISOcoated_v2_eci.icc](https://www.pointprepress.de/download/index.php) and place it in a `lib` folder.
  • ```ruby
  • 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](https://kornel.ski/en/color) when no profile is embedded, this can be a way to save some bytes.
  • ```ruby
  • 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](https://makandracards.com/makandra/480893-carrierwave-how-to-generate-versions-with-different-file-extensions).
  • ```ruby
  • 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:
  • ```ruby
  • 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:
  • ```ruby
  • 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.
  • ```ruby
  • 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](https://makandracards.com/makandra/621073-installing-modern-libvips-versions-from-our-ppa) 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`](https://www.rubydoc.info/gems/ruby-vips/Vips/Image) instance for the image being processed:
  • ```ruby
  • 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`](https://www.rubydoc.info/gems/ruby-vips/Vips/Image) 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:
  • ```ruby
  • 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
  • ```
  • -There are a few destructive methods that can only be called from inside a `#mutate` block:
  • +> [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](https://www.libvips.org/API/current/How-it-works.html) section on libvips.org for details.
  • +
  • +Some destructive methods (drawing operations, writing metadata) can only be called from inside a `#mutate` block:
  • ```ruby
  • 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
  • ```
  • -### Chaining multiple writes efficiently
  • -
  • -You can also [use `mutate` to prevent the generation of unused intermediate images](https://www.libvips.org/2021/03/08/ruby-vips-mutate.html) when you run multiple edit operations in a sequence. This will improve the performance of your processing method.
  • -
  • -```ruby
  • -class DocumentUploader < CarrierWave::Uploader::Base
  • - include CarrierWave::Vips
  • -
  • - process :scale_and_blur
  • -
  • - private
  • -
  • - def set_copyright_field
  • - mutated = vips_image.mutate do |mutable|
  • - mutable.scale(2)
  • - mutable.guassblur(20)
  • - end
  • -
  • - mutated.write_to_file(current_path)
  • - end
  • -
  • -end
  • -```
  • -
  • > [note]
Henning Koch
Last edit
Henning Koch
License
Source code in this card is licensed under the MIT License.
Posted by Henning Koch to makandra dev (2024-07-01 08:16)