# image_processing: one chainable Ruby pipeline for ImageMagick and libvips

> The image_processing gem gives Ruby applications a single chainable API for resizing and converting images, backed by either ImageMagick or libvips. It exists so attachment libraries stop each shipping their own private helpers, and it ships with instrumentation hooks for measuring the work.

**janko/image_processing** — Web-friendly image processing macros for libvips and ImageMagick

- Repository: https://github.com/janko/image_processing
- Stars: 961 · Forks: 88
- Language: Ruby
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/janko-image-processing

## The problem: every attachment gem ships its own private resize helpers

Upload handling in Ruby has a recurring duplication problem, and the README names it directly. Attachment gems such as Paperclip, CarrierWave, Refile, Dragonfly, and Active Storage each implement their own custom image helper methods, which the project argues is not very DRY. The premise behind image_processing is that image processing is a dark and mysterious art, and that the sensible response is one library gathering the good ideas from all of those separate gems. The intended users are Ruby developers running web applications that accept user images: profile pictures, upload forms, e-commerce product photos. The problem the gem solves is not the act of resizing, which both ImageMagick and libvips already do well. It is the layer above: a Ruby API that stays stable while the underlying native library underneath changes. That framing explains the project's structure, its two interchangeable backends, and its emphasis on instrumentation.

## One chainable API, two native backends

Processing runs through the ImageProcessing::Vips module or the ImageProcessing::MiniMagick module, and the README states plainly that both share the same chainable API for defining the processing pipeline. Each chain call adds an operation, and nothing executes until you say so. ImageMagick is described as a good default choice, especially when migrating from another gem or library that already uses ImageMagick. Libvips is described as newer and able to process images very rapidly, often multiple times faster than ImageMagick, which the README links to the libvips speed and memory notes. Choosing a backend is therefore mostly a migration decision rather than a capability decision, which is the design's central claim. The chainable interface was heavily inspired by HTTP.rb, and the MiniMagick functionality was extracted from the refile-mini_magick project. Module specific API documentation lives under doc/vips.md and doc/minimagick.md, and the project wiki carries additional how to guides that anyone can edit.

## Installing the native libraries and the gems

The gem is a thin Ruby layer over native binaries, so installation has two halves. First the native library, on macOS through Homebrew:

```sh
  $ brew install imagemagick # if using ImageMagick
  $ brew install vips # if using libvips
```

and on debian or ubuntu through apt:

```sh
  $ sudo apt install imagemagick # if using ImageMagick
  $ sudo apt install libvips # if using libvips
```

Then the gems themselves go into the Gemfile:

```rb
gem "image_processing", "~> 2.0"
gem "mini_magick", "~> 5.0" # if using ImageMagick
gem "ruby-vips", "~> 2.0" # if using libvips
```

The version constraints are worth reading as a pair. image_processing sits on the 2.0 series, and you pick the driver gem that matches your native library. You can install either driver or both, and the test suite wants both native libraries present regardless of which backend you ship. Note that the repository publishes no GitHub releases, so the gem version on RubyGems rather than a tagged release is what you track, and the CHANGELOG.md file in the repository root is where version level changes are recorded.

## Your first pipeline, from source to tempfile

A complete pipeline reads like a sentence. You require the driver, name a source, chain operations, and call it:

```rb
require "image_processing/mini_magick"

processed = ImageProcessing::MiniMagick
  .source(file)
  .resize_to_limit(400, 400)
  .convert("png")
  .call
```

The result of that call, when no options are given, is a Tempfile object. The source may be a String, a Pathname, anything that responds to #path, or an already constructed Vips::Image or MiniMagick::Tool object, and the README lists all four forms:

```rb
ImageProcessing::Vips.source(File.open("source.jpg"))
ImageProcessing::Vips.source("source.jpg")
ImageProcessing::Vips.source(Pathname.new("source.jpg"))
ImageProcessing::Vips.source(Vips::Image.new_from_file("source.jpg"))
```

One constraint deserves emphasis because it catches people out: the processed file is always saved to a new location, and in-place processing is not supported. If your application currently mutates uploads on disk, this is the behaviour you will have to design around. You can also take control of the destination, or skip saving entirely and get the raw object back:

```rb
pipeline = ImageProcessing::Vips.source(image)

pipeline.call #=> #<Tempfile ...>
pipeline.call(save: false) #=> #<Vips::Image ...>
pipeline.call(destination: "/path/to/destination")
```

That last form is the one to reach for when a job worker needs to write into object storage or a specific path rather than a temporary file that gets cleaned up.

## Bang methods and reusable pipelines for multiple derivatives

Execution happens on #call, or when a processing method is called with a bang suffix. Both spellings produce the same output:

```rb
processed = ImageProcessing::MiniMagick
  .convert("png")
  .resize_to_limit(400, 400)
  .call(image)

# OR

processed = ImageProcessing::MiniMagick
  .source(image) # declare source image
  .convert("png")
  .resize_to_limit(400, 400)
  .call

# OR

processed = ImageProcessing::MiniMagick
  .source(image)
  .convert("png")
  .resize_to_limit!(400, 400) # bang method
```

The bang form is what makes branching cheap. A single pipeline can produce several derivatives from one source, which is the standard pattern for a thumbnail set:

```rb
pipeline = ImageProcessing::Vips
  .source(file)
  .convert("png")

large  = pipeline.resize_to_limit!(800, 800)
medium = pipeline.resize_to_limit!(500, 500)
small  = pipeline.resize_to_limit!(300, 300)
```

Because the pipeline is a plain object, it can be inspected before anything runs. Calling pipeline.options returns the accumulated hash, including the source, loader, saver, format, the ordered operations, and the processor class:

```rb
pipeline.options
# => {:source=>#<File:/path/to/source.jpg>,
#     :loader=>{:page=>1},
#     :saver=>{},
#     :format=>"png",
#     :operations=>[[:resize_to_limit, [400, 400]], [:strip, []]],
#     :processor_class=>ImageProcessing::MiniMagick::Processor}
```

That inspectability is genuinely useful in tests and in debugging a pipeline that produced the wrong derivative, and it is the practical answer to the complaint that image processing is a dark art.

## Loader and saver options for pages, quality, and failure handling

Two option groups sit on either side of the operations. The loader controls how the source is read, and the saver controls how the result is written. The loader option shown in the README example is .loader(page: 1), which selects a page when the source is a multi page document. The saver carries output settings such as quality, and in the instrumentation example it is set with .saver(quality: 85). There is also a loader option that matters for robustness in a web context: .loader(fail: true), which makes the read step raise instead of continuing when it cannot parse the input. That setting appears in the instrumentation example, so it is part of the documented surface. The README does not enumerate every loader or saver key, so for the full set you need the module documentation for ImageProcessing::Vips and ImageProcessing::MiniMagick. What is documented is enough to cover the common case: page selection for multi page sources, output quality for lossy formats, and a strict mode for untrusted uploads.

## Instrumentation for measuring pipeline execution

For teams that care how long image processing takes, the gem ships an instrumenter hook. You register a block for a given pipeline and it wraps the execution, letting you record performance metrics. The block receives the resolved options and the processing callable:

```rb
pipeline = ImageProcessing::Vips.instrumenter do |**options, &processing|
  options[:source]     #=> #<File:...>
  options[:loader]     #=> { fail: true }
  options[:saver]      #=> { quality: 85 }
  options[:format]     #=> "png"
  options[:operations] #=> [[:resize_to_limit, 500, 500], [:flip, [:horizontal]]]
  options[:processor]  #=> ImageProcessing::Vips::Processor

  ActiveSupport::Notifications.instrument("process.image_processing", **options) do
    processing.call # calls the pipeline
  end
end
```

The README shows the instrumenter being attached to a pipeline and the call at the end triggering it. The example wires the event into ActiveSupport::Notifications, so in a Rails application this is a normal instrumentation event rather than a custom reporting system. Note the dependency this implies: the gem itself does not require Rails, but the shipped example leans on ActiveSupport, so using that exact pattern pulls ActiveSupport into scope. The hook is generic enough to plug into any metrics backend of your own, since the block simply receives the options and the processing callable.

## Running the test suite and the state of the project

Contributing means running the tests, and the README is explicit about the prerequisite: the test suite requires both imagemagick and libvips libraries to be installed. Once they are present:

```sh
$ bundle exec rake test
```

The repository root holds a Rakefile, a Gemfile, an image_processing.gemspec, lib/, test/, and doc/, along with a CODE_OF_CONDUCT.md file. The project is not archived, and the last push was on 2026-09-29, so work on it is recent even though the repository publishes no GitHub releases and version tracking happens through the gem and its CHANGELOG. Licensing is MIT, which for a library that sits inside your request path means you can use it in a proprietary application without the copyleft obligations that a differently licensed image gem might impose, provided the notice is retained. One thing the README does not document is a supported Ruby version matrix or a deprecation schedule for the chainable API, so a team planning a long term dependency should read the CHANGELOG.md before standardising on it.

## Conclusion

image_processing is a good fit for a Rails or Ruby service that already accepts uploads and currently rolls its own resize helpers, especially when you want to move from ImageMagick to libvips without rewriting call sites, since both backends expose the same chainable API. It is a poor fit if you need in-place editing, since processed files are always written to a new location, and it is a poor fit for projects that want zero native dependencies, because ImageMagick or libvips must be installed on the machine. Before adopting it, pin image_processing to the 2.0 series, install both imagemagick and libvips because the test suite requires them, and run bundle exec rake test to see whether your own Ruby version and native libraries agree with the suite.

## FAQ

### How do I install the image_processing gem?

Install the native library first, either imagemagick or libvips, using brew or apt as shown in the README, then add gem "image_processing", "~> 2.0" to your Gemfile along with mini_magick "~> 5.0" or ruby-vips "~> 2.0" for the backend you want.

### Should I use ImageMagick or libvips with image_processing?

Both backends share the same chainable API, so the choice is mostly about migration and speed. The README calls ImageMagick the good default when you are moving from another gem that already uses it, and notes that libvips is newer and can process images much faster, often multiple times faster than ImageMagick.

### Can image_processing modify an image in place?

No. The README states that the processed file is always saved to a new location and that in-place processing is not supported. You can pass destination to #call to control the output path, or pass save: false to get the raw Vips::Image or MiniMagick::Tool object back.

### How do I generate several thumbnail sizes from one source?

Build a pipeline once with source and convert, then call a bang method such as resize_to_limit! for each size. The README example creates large, medium and small derivatives from a single ImageProcessing::Vips pipeline.

### What licence is the image_processing gem released under?

The project is released under the MIT licence, recorded in the README and in the LICENSE.txt file at the repository root. There are no GitHub releases, so version changes are tracked through the gem version and the CHANGELOG.md file.

## Sources

- [Issues](https://github.com/janko/image_processing/issues)
- [janko/image_processing on GitHub](https://github.com/janko/image_processing)
- [License: MIT](https://github.com/janko/image_processing/blob/master/LICENSE)
- [README](https://github.com/janko/image_processing/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/janko-image-processing
