DEV Community

Masaki Komagata
Masaki Komagata

Posted on • Edited on

Active Storage without libvips or ImageMagick: a pure-Ruby alternative

Update (2026-04-23): v0.3.0 dropped the last remaining gem dependency (image_processing) — the library is now fully zero-dependency. There's also a browser playground where you can run ImageProcessing::Pura chains directly in your browser via ruby.wasm.

TL;DR

I built pura-image — a pure-Ruby image processing library that drops into Rails Active Storage as a replacement for ImageProcessing::Vips / ImageProcessing::MiniMagick. No brew install vips. No apt install imagemagick. No C compiler. Just gem install.

# Gemfile
  gem "image_processing"
+ gem "pura-image"
Enter fullscreen mode Exit fullscreen mode
# config/application.rb
config.active_storage.variant_processor = :pura
Enter fullscreen mode Exit fullscreen mode
# Dockerfile
- RUN apt-get install -y libvips-dev imagemagick
Enter fullscreen mode Exit fullscreen mode

That's the entire integration. Models and views stay exactly the same.

Try it in your browser

If you'd rather see it run before reading further:

komagata.github.io/pura-image

The page boots ruby.wasm and runs ImageProcessing::Pura chains on a generated 400×300 image (or one you drop in). The exact code shown is the same code you'd put in a Rails Active Storage variant definition — but to be precise, only the ImageProcessing::Pura chain runs in the browser, not Active Storage itself (no blob storage, no orchestration). Still, if you've ever wondered whether your Rails variant code can survive in a wasm sandbox: yes, it can.

Why I built this

I teach Rails to beginners, and the single most common stumbling block isn't Ruby syntax or Rails conventions — it's the system-library install dance for Active Storage's image processing.

  • macOS: brew install vips
  • Ubuntu/Debian: apt install libvips-dev
  • Windows: …good luck
  • Docker: another layer in your RUN apt-get install chain
  • CI: another minute on every build
  • Heroku/Fly.io/Render: hope your buildpack covers it
  • ruby.wasm: not happening

Every one of those is a wall a learner can hit. Some of them just give up before writing their first has_one_attached.

So I asked: what if the whole thing just worked from gem install?

What pura-image is

A pure-Ruby implementation of decoders and encoders for the seven image formats Active Storage users actually care about:

Format Decode Encode
JPEG
PNG
BMP
GIF
TIFF
ICO/CUR
WebP

Format detection is by magic bytes, so you don't need to trust file extensions on decode. Encode picks the format from the output extension.

It also ships an ImageProcessing::Pura adapter that's API-compatible with ImageProcessing::Vips, which is what makes the Active Storage integration a one-line config change.

As of v0.3.0, even the runtime gem dependency on image_processing is gone — replaced by a small in-house pura-processing gem. So "no dependencies" is now literal rather than just "no system dependencies."

Standalone usage

require "pura-image"

# Magic-byte detection — works on any supported format
image = Pura::Image.load("photo.jpg")
image.width   #=> 800
image.height  #=> 600

# Save in any format
Pura::Image.save(image, "output.png")

# Chain operations
Pura::Image.load("photo.jpg")
  .resize_to_limit(800, 600)
  .rotate(90)
  .grayscale
Enter fullscreen mode Exit fullscreen mode

All the standard image_processing operations are supported: resize_to_limit, resize_to_fit, resize_to_fill, resize_and_pad, resize_to_cover, crop, rotate, grayscale.

Active Storage integration

Add the gem, set the variant processor, and you're done.

# Gemfile
gem "image_processing"
gem "pura-image"
Enter fullscreen mode Exit fullscreen mode
# config/application.rb
config.active_storage.variant_processor = :pura
Enter fullscreen mode Exit fullscreen mode

Existing code keeps working unchanged:

class User < ApplicationRecord
  has_one_attached :avatar do |attachable|
    attachable.variant :thumb, resize_to_limit: [200, 200]
  end
end
Enter fullscreen mode Exit fullscreen mode
<%= image_tag user.avatar.variant(:thumb) %>
Enter fullscreen mode Exit fullscreen mode

Benchmarks

I expected pure Ruby to be uniformly slower than C. It isn't.

Measurements: 400×400 image, Ruby 4.0.2 with YJIT, vs ffmpeg (C + SIMD) invoked via process spawn (the realistic comparison for a Rails app).

Decode

Format pura-* ffmpeg vs ffmpeg
TIFF 14 ms 59 ms 🚀 4× faster
BMP 39 ms 59 ms 🚀 1.5× faster
GIF 77 ms 65 ms ~1× (comparable)
PNG 111 ms 60 ms 1.9× slower
WebP 207 ms 66 ms 3.1× slower
JPEG 304 ms 55 ms 5.5× slower

Encode

Format pura-* ffmpeg vs ffmpeg
TIFF 0.8 ms 58 ms 🚀 73× faster
BMP 35 ms 58 ms 🚀 1.7× faster
PNG 52 ms 61 ms 🚀 faster
JPEG 238 ms 62 ms 3.8× slower
GIF 377 ms 59 ms 6.4× slower

5 out of 11 operations are faster than C. Process-spawn overhead dominates ffmpeg's runtime for small images, and a pure-Ruby in-process call wins. JPEG and WebP are the formats where C's heavy compression algorithms still win convincingly — fine for thumbnails, not what you want for a high-volume transcoding pipeline.

Side benefits of going pure Ruby

Beyond the original "make Rails setup easier for beginners" goal, removing the C dependency unlocks some surprising places:

  • Smaller Docker images — drop libvips-dev, imagemagick, and their transitive deps from your final layer
  • Faster CI — no apt-get install line means seconds back on every job
  • PaaS without buildpack drama — Heroku, Fly.io, Render all just work
  • Runs on ruby.wasm — image processing in the browser, in a sandbox, on the edge (live demo)
  • Runs on JRuby and TruffleRuby — no C extension, so no JVM/Truffle compatibility headaches
  • Works on Windows — no MSYS2 dance

Limitations

I want to be upfront about where this isn't the right tool:

  • JPEG and WebP encoding are slower than libvips. If you're generating millions of thumbnails per hour, libvips is still the right call.
  • Very large images (multi-thousand pixel sides) widen the gap with C implementations.
  • The library is young. Run benchmarks and integration tests against your real workload before betting a production system on it.

What's next

This release line covers the use case I built it for: Rails Active Storage with reasonable image sizes. If you try it, I'd love to know:

  • Star on GitHub if it's useful
  • 🐛 Issues and PRs welcome — especially benchmark contributions on different workloads
  • 💬 Reply here with what you'd want to see next

The current Rails ecosystem trend — Solid Queue, Solid Cache, Solid Cable — is removing external infrastructure dependencies in favor of "it just works with what you already have." pura-image is the same idea applied to image processing.

Links

Top comments (0)