Introduction

ol.vips wraps libvips’s image graph in Closeable Clojure values. Loader functions such as v/from-file, v/from-buffer, and v/from-stream return image handles, and operations such as ops/resize, ops/colourspace, and v/call return new handles rather than mutating the original image.

This model is immutable and pipeline-oriented. In the common case, libvips reads only enough of the input to discover image metadata and build an operation graph. Calling loaders and transforms usually does not decode the whole image up front.

libvips is demand-driven, so the pixel work happens when something downstream asks for image data. In ol.vips, that usually means a sink such as v/write-to-file, v/write-to-buffer, or v/write-to-stream. At that point libvips pulls pixels through the pipeline, processing small regions with a horizontally threaded execution model instead of materializing every intermediate image in memory.

Some operations that need actual pixel values can trigger evaluation earlier, but metadata queries such as v/width, v/height, and v/metadata can often be satisfied from loader metadata alone. Because the image graph is immutable and operations are generally side-effect free, libvips can also reuse and cache operation results internally.

ol.vips adds a small amount of Clojure-specific structure on top of that model. The ol.vips.operations namespace is generated from libvips introspection data, ol.vips.enums exposes the enum keywords used by many options, and the top-level ol.vips namespace provides the file, buffer, stream, metadata, and convenience helpers you use most often.

Why libvips?

The typical libvips workflow is exactly what many applications need: load a large source image, resize or transform it, and write a smaller web-friendly JPEG, PNG, WebP, AVIF, or TIFF result. ol.vips gives Clojure access to that model without shelling out to external image tools or spawning child processes.

libvips is blazing fast and memory efficient because it usually works on small regions of uncompressed image data at a time, takes advantage of multiple CPU cores, and avoids materializing every intermediate result in memory. That execution model is a strong fit for image pipelines, thumbnails, upload processing, preprocessing for vision workloads, and other server-side image tasks.

It also brings broad format and operation support: 300 operations covering arithmetic, histograms, convolution, morphology, colour, resampling, statistics, and more, with correct handling of colour spaces, embedded ICC profiles, and alpha channels.

libvips was originally created at Birkbeck College and is currently maintained by John Cupitt and other contributors.

Bindings Generation

libvips itself is very amenable to automatic bindings generation. The core of ol.vips is small introspection layer that loads the native library then introspects it to generate ol.vips.operations and ol.vips.enums, and then a small runtime for loading/reading/writing.

As a convenience for Clojure users there are separate jars available with the native libraries per-platform, but ol.vips it self does not depend on them directly. You can depend on one (or more) of those native deps, or provide your own libvips build with -Dol.vips.native.preload. You’ll need to choose one.

This means that you can even use a custom libvips build or a newer version of libvips without having to wait for ol.vips to catch up (though I expect to keep it up to date). Any new or updated operations in libvips can be used with this library using the lower level ol.vips/call function. You can even generate the bindings yourself if you need to, simply use ol.vips.codegen.

Going to Production

See Going to Production for production guidance on pipeline shape, sequential access, untrusted inputs, sanity checks, allocator tuning, and cache settings in ol.vips.

See Examples for runnable snippets that cover file, buffer, stream, metadata, thumbnailing, transforms, composition, and animated-image workflows.

Loading the native library

ol.vips uses this procedure to load the native components:

  1. Load any explicit libraries listed in -Dol.vips.native.preload.

  2. Load the extracted libraries from the platform native jar on the classpath.

  3. If that fails, fall back to the system library loader, which can resolve libvips from LD_LIBRARY_PATH.

1. Preload

-Dol.vips.native.preload is for exact native library file paths. Use it when you want to point ol.vips at a specific custom build in a non-standard location, or when you need to preload dependency libraries before libvips itself. On Nix-like systems that can include things like libstdc++.so.6 as well as your libvips library. The value is a single string containing one or more full library file paths separated by the OS path separator, which is : on Linux and macOS and ; on Windows.

If ol.vips.native.preload is set, those entries are always loaded first. If any preload entry fails to load, the packaged native-jar path is abandoned for that initialization attempt and ol.vips falls back to the system library loader instead of continuing with the native jar.

2. Classpath native bundle

ol.vips detects the current platform, builds a resource path like ol/vips/native/<os>-<arch> on macOS and Windows or ol/vips/native/<os>-<arch>-<libc> on Linux, and then looks for manifest.edn under that path.

For example, Linux x86-64 glibc resolves to ol/vips/native/linux-x86-64-gnu/manifest.edn. If that resource is present, ol.vips reads the manifest, extracts the bundled native libraries into the local cache, and loads those extracted files.

You can override parts of that platform detection with -Dol.vips.native.platform-id, -Dol.vips.native.os, -Dol.vips.native.arch, and -Dol.vips.native.libc.

In practice, this means the companion jar for the current runtime platform needs to be on the classpath, meaning the current OS, CPU architecture, and Linux libc when applicable. This path is attempted after any explicit ol.vips.native.preload entries.

In the successful packaged case, v/init! will report :native-load-source :packaged and expose the extracted primary library path in :primary-library-path, which is useful when debugging exactly what got loaded.

3. System fallback

The system-library fallback loads by library name rather than full file path. Use this when you want the OS or JVM loader to resolve libvips from LD_LIBRARY_PATH, standard system locations, or other platform-specific loader configuration. A platform native jar is not required for this path; if the packaged load fails, including because no matching native jar is present on the classpath, ol.vips can still fall back to the system loader.

By default the system fallback tries vips-cpp and then vips. Most users should not need to change this. -Dol.vips.native.system-libs exists for the uncommon case where your environment exposes libvips under different system library names. Like ol.vips.native.preload, it accepts multiple entries separated by the OS path separator.

The fallback only applies to native library loading. If the packaged libraries load successfully but vips_init fails afterwards, ol.vips does not automatically retry via the system loader. For debugging, v/init! exposes runtime state including whether initialization came from the packaged path or the system fallback.