CLI tool
libvips/ruby-vips avatar
libvips/ruby-vips

ruby-vips: calling libvips from Ruby through FFI instead of a native extension

Ruby extension for the libvips image processing library.

904 stars67 forksRubyMIT

At a glance

What is it?
ruby-vips is an MIT licensed Ruby binding for the libvips image processing library that talks to it through ruby-ffi rather than compiling a C extension, which keeps installation to a package manager plus one gem. It is aimed at Ruby services that resize, crop and transform images and want the demand-driven pipeline libvips is built around.
Who is it for?
ruby-vips fits a Ruby codebase that already has a system libvips available and wants its image work to run on libvips' demand-driven pipeline, because the binding exposes the native method set directly instead of reimplementing it, and because the FFI route removes compilation from the install.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 6 days ago.
What is it written in?
Mainly Ruby, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A binding that ships no compiled C extension

ruby-vips is a Ruby binding for the libvips image processing library, and the mechanism it uses to reach libvips is the first thing to understand about it.

It uses ruby-ffi to call functions in the libvips library. FFI, the foreign function interface, lets Ruby call into a native shared library at runtime through a declared signature, without a compiled extension that has to match the interpreter's ABI. That single decision explains almost every practical property of this gem.

It explains why the gem installs cleanly. There is no compilation step, no toolchain requirement on the Ruby side, and no platform-specific build. It also explains the one hard prerequisite: the libvips binary has to exist on the machine, because the FFI layer is looking for a shared library to bind to at runtime. On Windows the README notes that the gemspec pulls in the msys libvips for you, so the single gem install is enough there. On Linux and macOS the library comes from the system package manager, and if it is missing you get a runtime failure rather than a build failure.

The README states the testing matrix: the gem has been tested on Linux, macOS and Windows, and with ruby 2, ruby 3 and jruby. JRuby support is worth a second look, since it is a plausible reason to reach for an FFI binding rather than a C extension, which would need a JVM-compatible build.

The project is MIT licensed, with the licence file named LICENSE.txt at the root of the tree, alongside a Gemfile, a Rakefile, a gemspec, a CHANGELOG.md, a VERSION file, a TODO file, a lib/ directory, an example/ directory and a spec/ directory. The default branch is master.

Who this is for is a Ruby service that does image work: a Rails application resizing uploads, a thumbnailer, a document pipeline. The libvips library itself is a demand-driven, horizontally threaded image processing library, and the README notes that compared to similar libraries libvips runs quickly and uses little memory.

Install order differs between Unix and Windows

The installation instructions split by platform, and the difference is worth reading carefully because the order of the two steps is not the same.

On Linux and macOS, you install the libvips binary with your package manager first, and the README gives two examples, apt install libvips42 and brew install vips, and links to the libvips install instructions for everything else. Then you install the gem:

bash
gem install ruby-vips

The README also says you can include `gem "ruby-vips"` in your gemfile instead, which is the normal path for anything using Bundler, since the system library is then expected to be present on the machine rather than installed by the gem.

On Windows the sequence collapses to one step. The gemspec will pull in the msys libvips for you, so all you need is:

bash
gem install ruby-vips

The README adds a note here that it was tested with the ruby and msys from choco, but that others may work. That is an unusually honest caveat, since it means the Windows path is validated against one specific distribution of Ruby and one specific package manager.

The difference between the two platforms is the point: on Unix the native library is a system dependency you manage, on Windows the gem brings its own. If you are writing deployment tooling that provisions machines, that is the difference between a package manager entry in your Dockerfile and nothing at all.

Once installed, the first thing to check is that the binding can find the library and that the version is one your code can use. The library's version matters because the method set grows over time, and the README's own example calls the embed operation, which the comment links to a specific versioned documentation page rather than a current one.

The example pipeline, from file to file

The README's example is worth reading closely, because it shows the shape of the API rather than a list of methods.

ruby
require "vips"

im = Vips::Image.new_from_file filename

# put im at position (100, 100) in a 3000 x 3000 pixel image, 
# make the other pixels in the image by mirroring im up / down / 
# left / right, see
# https://www.libvips.org/API/current/method.Image.embed.html
im = im.embed 100, 100, 3000, 3000, extend: :mirror

# multiply the green (middle) band by 2, leave the other two alone
im *= [1, 2, 1]

# make an image from an array constant, convolve with it
mask = Vips::Image.new_from_array [
    [-1, -1, -1],
    [-1, 16, -1],
    [-1, -1, -1]], 8
im = im.conv mask, precision: :integer

# finally, write the result back to a file on disk
im.write_to_file output_filename

Three idioms stand out. Operations return an image, so the pipeline is a chain of assignments rather than in-place mutation with output parameters. The band multiplier im *= [1, 2, 1] multiplies the green band by 2 and leaves the others alone, which is a libvips feature rather than a Ruby one. And keyword arguments such as extend: :mirror and precision: :integer map directly onto the native options, so the Ruby API and the libvips reference manual use the same vocabulary.

That last point is the practical payoff of the FFI design. There is no intermediate layer inventing a Ruby-flown abstraction, so the libvips reference manual, which the README links as having a complete explanation of every method, is the documentation for this gem too. The methods are the native ones with the same names and the same options.

The example/ directory in the repository carries this further, with small programs named after the operations they demonstrate: annotate.rb, connection.rb, daltonize8.rb, draw_lines.rb, perspective.rb, progress.rb, revalidate.rb, thumb.rb, trim8.rb, watermark.rb, wobble.rb, plus example1.rb through example5.rb and inheritance_with_refcount.rb. That last name is the one to look at first if you are coming from a library with an explicit destroy method, because it addresses the question this binding raises.

Memory behaviour, and the benchmark the README publishes

The reason libvips is usually chosen over a more conventional imaging stack is the demand-driven model, and the README publishes a benchmark that makes the point.

The benchmark at vips-benchmarks loads a large image, crops, shrinks, sharpens and saves again, and repeats 10 times, and the results are given as real time in seconds for the fastest of five runs, and as peak memory in kilobytes. On the tiff and jpeg columns, ruby-vips.rb records 0.85 and 0.78 seconds, image-magick records 2.03 and 2.44, and rmagick.rb records 3.87 and 3.89. On peak memory, ruby-vips.rb records 43864 and rmagick.rb records 788768.

Two things to take from that. The speed difference is real but a factor of a few, not an order of magnitude, and the memory difference is the dramatic one, roughly eighteen times between the two Ruby bindings in that run. If your service holds several large images in memory at once, that is the number that decides the architecture.

It is also worth being clear about what this benchmark is not. It is one pipeline, on one machine, with one image size, published in the project's README, and the README links to benchmarks at the official libvips website for more. Treat it as an indication of the shape of the difference rather than as a number to plan capacity around.

The underlying mechanism is the demand-driven, horizontally threaded design of libvips itself, which the README links to a wiki page explaining why libvips is quick, and to a second page on speed and memory use. The practical consequence for a Ruby developer is that the pipeline is lazy and threaded inside the native library, so a chain of operations does not build a full-size intermediate image for each step. That is also why the example above can build a 3000 by 3000 image by mirroring a smaller one without caring much about the memory cost.

Where the binding stops, and what the documentation does not cover

There are limits to what a binding can smooth over, and this one is honest about most of them.

The first is the native dependency itself. On Linux and macOS the gem expects libvips to be installed, and the FFI layer will look for a shared library with a particular name at runtime. A container image that does not include libvips will install the gem happily and fail on first use. The README does not document the expected library version range, so that is a question for the libvips install instructions it links to rather than something answered here.

The second is the API surface. Because the binding exposes the native methods rather than a curated subset, the Ruby documentation is a mapping of a C library, and the README acknowledges the friction of that. It points to full API docs for ruby-vips on rubydoc and notes that this sometimes has issues updating, which is why it also keeps a copy on the gh-pages for the project, described as the one that should always work. The Vips section of those docs is the tutorial introduction with examples, and the libvips reference manual is where every method is explained in full. So the documentation chain is: tutorial on rubydoc, complete method reference in the C library's manual, and a second copy of the generated docs in case the gem host lags.

The third is the error surface. The README does not document how libvips errors surface in Ruby, whether as exceptions, as warnings or as both, and it does not describe the behaviour when an operation is impossible for a given image format or band layout. For a service that processes untrusted uploads, that is a gap worth closing by reading the generated API docs rather than by guessing.

One practical point about the library itself: libvips is licensed under the LGPL 2.1+, as the README states, and that is a different licence from the MIT of the gem. Shipping a service that links against LGPL libvips has its own considerations, which this review does not interpret.

Editorial conclusion

ruby-vips fits a Ruby codebase that already has a system libvips available and wants its image work to run on libvips' demand-driven pipeline, because the binding exposes the native method set directly instead of reimplementing it, and because the FFI route removes compilation from the install. It is the wrong choice for a deployment where you cannot install a native library, since on Linux and macOS the gem expects libvips to be present, and it is the wrong choice for a small script that only needs to resize a file, where a simpler Ruby imaging gem is less machinery. Before you adopt it, confirm the libvips version available on your hosts matches what your code needs, read the CHANGELOG.md in the repository root when upgrading, and check the example/ directory for the pipeline patterns you intend to copy.

Frequently asked questions

How do I install ruby-vips on Linux or macOS?

Install the libvips binary with your package manager first, for example `apt install libvips42` or `brew install vips`, then run `gem install ruby-vips`. The README also says you can include `gem "ruby-vips"` in your gemfile instead.

Do I need to install libvips separately on Windows?

No. The README says the gemspec will pull in the msys libvips for you, so `gem install ruby-vips` is enough. It was tested with the ruby and msys from choco, though the README notes that others may work.

Does ruby-vips need a compiled C extension?

No. The gem uses ruby-ffi to call functions in the libvips library, which is why installation needs no compiler and why libvips itself has to be present on the machine on Linux and macOS.

How does the Ruby API compare to the libvips documentation?

The binding exposes the native methods rather than a separate abstraction, so the libvips reference manual explains every method. The README points to a tutorial introduction in the Vips section of the rubydoc API docs, with a copy of the generated docs on the project's gh-pages.

What is the difference between ruby-vips and rmagick in memory use?

In the benchmark quoted in the README, peak memory is 43864 kb for ruby-vips.rb against 788768 kb for rmagick.rb, and real time is 0.85 against 3.87 seconds on the tiff column. The README links to the vips-benchmarks repository for the full run and to benchmarks on the libvips website.

Official sources

  1. Issues
  2. libvips/ruby-vips on GitHub
  3. License: MIT
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/libvips-ruby-vips.svg)](https://hysenlabs.com/projects/libvips-ruby-vips)

Was this useful?