ActiveStorage: Custom postprocessing of representations/variants after ImageMagick


I normally use Shrine but tried out Active Storage for a new project. I understand that you can do ImageMagick/vips postprocessing via ‘representations’ both for images and video previews.

Let’s say I have a model with a has_one_attached :file (taking both images and videos ) and am using several representations/variants for thumbnail, small and medium sizes (resize_to_fit: …) from either the original image or from the preview image of the attached video.

This works pretty well so far, but I now have to do some additional postprocessing of the variant files. For example running jpegoptim via image_optim on thumbnails, small and medium image variants.

What’s the best way to do this with ActiveStorage or is this not possible at the moment?

I’d like to avoid modifying the files on S3 directly and do this step just after ImageMagick or vips finished and before the new variant is uploaded to S3.

Thanks, Clemens

1 Like

That is not supported. But you can compress directly from active storage by using the saver hash, which is format dependent. For JPEG:

# Vips
file.variant(saver: { 
  strip: true, 
  quality: 80, 
  interlace: true, 
  optimize_coding: true, 
  trellis_quant: true, 
  quant_table: 3

# Magick
file.variant(saver: { 
  strip: true, 
  quality: 80, 
  interlace: "JPEG", 
  sampling_factor: "4:2:0", 
  colorspace: "sRGB"

Thank you, I think this workaround will work for my current project. :slight_smile:

Would still be awesome though to have for example a callback, e.g. like in ActiveRecord before_save, maybe has_one_attached :file, before_upload: :before_upload_func This would allow all sorts of post-processing that is currently hard to achieve, as for example using the smallest possible JPEG quality setting while maintaining a certain perceived visual quality threshold (<= jpeg-recompress from jpeg-archive)

I have never used jpeg-recompress, but if it can tell you what is the best quality for a photo (in a 0 to 100 range), you have another option. Create your own image analyzer. Basically:

Rails.application.config.active_storage.analyzers.prepend YourAnalyzerClass

You can duplicate the code for Analyzer::ImageAnalyzer::Vips or Analyzer::ImageAnalyzer::ImageMagick and instead of inheriting from Analyzer::ImageAnalyzer like they do, just copy the two methods there and add yo your class. Then modify like this:

def metadata
  read_image do |image|
    if rotated_image?(image)
      { width: image.height, height: image.width, optimum_quality: optimum_quality }
      { width: image.width, height: image.height, optimum_quality: optimum_quality }

def optimum_quality
  `jpeg-recompress OPTIONS #{image.filename}` # or image.path if ImageMagick

OPTIONS being whatever options are going to tell jpeg-recompress (or whatever other lib) to return the optimum quality

Later instead of using 80 you can use file.metadata["optimum_quality"]. Has the advantage that this will be done by a sidekiq worker instead of a puma worker.

1 Like

Interesting idea, thank you. :+1:

So, if we get the optimum_quality metadata after the AnalyzeJob ran, which happens after the variant was created (where each variant should get its optimum quality setting), this would mean we store both the unoptimized version of the variant as well as the optimized version of each variant, correct?

If I understand correctly, these are the steps: Original file is uploaded, AnalyzeJob runs and gets optimum_quality for original file. Variants are requested but there is no optimum_quality metadata yet, so I could store them with a fallback quality of 90. Then our AnalyzeJob runs and estimates the optimum quality. Next time the variant is requested, there is a different transformation / changed saver hash, so a new file is created and used from that time on.

Is there a way to only store the optimized variants or at least clear the from now on unused unoptimized ones?

Again, there’s no easy way, but you definitely can, if you know what were the transformations and make it a scheduled task, maybe in cron.

YourModel.where(created_at: Date.current - 24.hours).each do |model|
  blob = model.file.variant(saver: { quality: 90 }) # and whatever other transformation you used
  blob.purge if blob.processed?