Read more

CarrierWave: Processing images with libvips

Henning Koch
August 04, 2023Software engineer at makandra GmbH

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

Illustration UI/UX Design

UI/UX Design by makandra brand

We make sure that your target audience has the best possible experience with your digital product. You get:

  • Design tailored to your audience
  • Proven processes customized to your needs
  • An expert team of experienced designers
Read more Show archive.org snapshot

There are several upsides to using libvips over ImageMagick:

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

On Ubuntu you can install libvips like this:

sudo apt install libvips42 libvips-dev libvips-tools 

If you're hosting on makandra servers Show archive.org snapshot these packages have already been installed on your application servers.

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.

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

  prviate

  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

There are a few destructive methods that 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

Chaining multiple writes efficiently

You can also use mutate to prevent the generation of unused intermediate images Show archive.org snapshot when you run multiple edit operations in a sequence. This will improve the performance of your processing method.

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

You can also use the vips! { |builder| ... } pattern here. This yields an ImageProcessing::Vips Show archive.org snapshot object with some additional methods. However I found it more straightforward to use the Vips::Image API directly.

Henning Koch
August 04, 2023Software engineer at makandra GmbH
Posted by Henning Koch to makandra dev (2023-08-04 16:55)