We do a lot of image processing at Artsy. We have tens of thousands of beautiful original high resolution images from our partners and treat them with care. The files mostly come from professional art photographers, include embedded color profiles and other complicated features that make image processing a big deal.

Once uploaded, these images are converted to JPG, resized into many versions and often resampled. We are using CarrierWave for this process - our typical image uploader starts like a usual CarrierWave implementation with a few additional features.

  • Fallback to a well-known image when an image is missing
  • Support for a local development environment, S3 and CloudFront
  • Image versioning: replaced images get a new path to bust CloudFront caching

Here’s the complete source.

``` ruby app/models/image_uploader.rb class ImageUploader < CarrierWave::Uploader::Base

  include CarrierWave::RMagick

  # default url for a missing image
  def default_url

  # a local path for development environments w/o S3 or CloudFront
  def local_path
    (ENV['CLOUDFRONT_URL'] || ENV['S3_BUCKET']) ? "" : "local/"

  # complete url to an image version
  def image_url_format_string

  # an allowlist for uploading
  def extension_white_list
    %w(jpg jpeg png gif tif tiff bmp)

  # alternate temporary location for Heroku
  def cache_dir

  # relative path for saving a file
  def store_path(for_file = filename)
    "#{local_path}#{self.class.store_path_base(self.model)}#{(version_name || :original).to_s}.jpg"

  # normalized file name for an image converted to JPG
  def filename
    super != nil ? super.split('.').first + '.jpg' : super

  # a location that includes a version number
  def self.store_path_base(model)
    class_name = model.class.name.underscore.pluralize
    image_version = (model.image_version || 0) > 0 ? "#{model.image_version}/" : ""

  # a url prefix depending on environment settings
  def self.image_url_prefix
    if ENV['IMAGES_URL']
    elsif ENV['S3_BUCKET']

end ```

We derive actual uploaders from the ImageUploader class.

``` ruby app/models/widget_uploader.rb class WidgetUploader < ImageUploader

  process :increment_version

  def increment_version
    return if self.is_processing_delayed?
    self.model.image_version = (self.model.image_version.to_i + 1)
    self.model.image_versions = []

  version :small, if: :is_processing_delayed? do
    process :convert => 'jpg'
    process :resize_to_limit => [200, 200]
    process :quality => 70

  version :square, if: :is_processing_delayed? do
    process :convert => 'jpg'
    process :resize_to_fill => [230, 230]
    process :quality => 90
end ```

And the uploader is mounted via mount_uploader.

    mount_uploader :image,  WidgetUploader, delayed: true

You’ll notice a few unusual things here. The versions have an :if condition and there’re mentions of is_processing_delayed?. This comes from a small module @joeyAghion wrote called DelayedImageProcessing. It’s a much more evolved version designed on top of my earlier idea of delaying some image processing for background jobs.

The reason we want to delay image processing is because it takes a long time. The Heroku HTTP request limit is only 30 seconds, so image upload would regularly timeout. And some of the large images can take up to ten minutes to process - we don’t want the user to wait that long.

To use, you will add some code in config/initializers/carrierwave.rb and add DelayedImageProcessing into lib.

The code above works with Mongoid. You will have to do some work to make this work with other storage.

Editor’s Note: This post has been updated as part of an effort to adopt more inclusive language across Artsy’s GitHub repositories and editorial content (RFC).

Categories: CarrierWave, Image Processing