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 runImageProcessing::Purachains 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"
# config/application.rb
config.active_storage.variant_processor = :pura
# Dockerfile
- RUN apt-get install -y libvips-dev imagemagick
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:
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 installchain - 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
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"
# config/application.rb
config.active_storage.variant_processor = :pura
Existing code keeps working unchanged:
class User < ApplicationRecord
has_one_attached :avatar do |attachable|
attachable.variant :thumb, resize_to_limit: [200, 200]
end
end
<%= image_tag user.avatar.variant(:thumb) %>
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 installline 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
- GitHub: https://github.com/komagata/pura-image
- Browser playground (ruby.wasm): https://komagata.github.io/pura-image/
- Original Japanese write-up: https://docs.komagata.org/6443
Top comments (0)