There’s a new full libvips binding for the browser and Node.js. It supports reading and writing JPEG, PNG, WebP and TIFF images out-of-the-box on browsers that supports the SharedArrayBuffer API, it’s on NPM, and it comes with TypeScript declarations.

All the features of libvips can be viewed and executed in the browser within this playground:

https://kleisauke.github.io/wasm-vips/playground

The README in the repository for the binding has more details, including some install notes and an example:

https://github.com/kleisauke/wasm-vips

But briefly, just enter:

npm install wasm-vips

How it’s done

The whole of libvips and its dependencies has been compiled to WebAssembly with Emscripten. The resulting WASM binary is ~4.6 MB in size. It took several patches to make libvips usable in the browser:

Performance

It’s rather tempting to benchmark how close WebAssembly gets to native speed, and how much faster it is than pure JS image processing libraries (i.e. no native code). The repo includes benchmarks which test the performance against alternative Node.js modules, including sharp and jimp.

On this benchmark and on my pc, sharp is 8.3x faster for JPEG, 3.6x faster for PNG, and 2.2x faster for WebP images in comparison with wasm-vips.

wasm-vips on the other hand is 5.9x faster for JPEG and 8% faster for PNG images in comparison with jimp.

Image format Module Ops/sec Speed-up
JPEG jimp 0.91 1.0
wasm-vips 5.36 5.9
sharp 44.35 45.8
PNG jimp 5.20 1.0
wasm-vips 5.63 1.1
sharp 20.30 3.9
WebP wasm-vips 6.20 1.0
sharp 13.73 2.2

The substantial slowdown for JPEG images could be caused due to libjpeg-turbo is compiled without SIMD support. This dependency uses native inline SIMD assembly, which is currently not supported in Emscripten. All code should be written using SIMD intrinsic functions or compiler vector extensions.

Although the dependencies for the other image formats (i.e. libspng and libwebp) are compiled with SIMD support there is still a slowdown noticeable. A possible reason for that is that liborc is not built for WebAssembly. This dependency is used by libvips to improve the performance of the resize, blur and sharpen operations, but this is quite difficult to compile for WebAssembly as it generates SIMD instructions on-the-fly.

Note that these benchmarks are expected to run faster when the WebAssembly proposals for SIMD and threads have been standardized.

How it works

All libvips operations and enumerations are exposed through Embind, so that the compiled code can be used in JavaScript.

The binding itself is a variant of libvips’ C++ API, with additional support for the emscripten::val C++ class to transliterate JavaScript code to C++. For example, consider this JavaScript code:

// Image source: https://www.flickr.com/photos/jasonidzerda/3987784466
const thumbnail = vips.Image.thumbnail('owl.jpg', 128, {
    height: 128,
    crop: vips.Interesting.attention /* or: 'attention' */
});

Which shrinks an image to fit within a 128×128 box. Excess pixels are trimmed away using the attention strategy that positioned the crop box over the most significant feature:

Attention strategy

This function and enumeration was automatically generated within C++ as:

EMSCRIPTEN_BINDINGS(my_module) {
    enum_<VipsInteresting>("Interesting")
        .value("none", VIPS_INTERESTING_NONE)
        .value("centre", VIPS_INTERESTING_CENTRE)
        .value("entropy", VIPS_INTERESTING_ENTROPY)
        .value("attention", VIPS_INTERESTING_ATTENTION)
        .value("low", VIPS_INTERESTING_LOW)
        .value("high", VIPS_INTERESTING_HIGH)
        .value("all", VIPS_INTERESTING_ALL);

    class_<Image>("Image")
        .constructor<>()
        .function("thumbnail", &Image::thumbnail);
}