This is short post announcing a new JVM image-manipulation-related library I’ve shipped to Maven Central - vips-ffm 🎉.

When I started planning Photo Fox (a self-hosted photo management tool I’m working on), I knew I’d need some sort of image library. Photo Fox has a few requirements for image manipulation: creating high quality thumbnails, supporting modern file types to do transcodes, doing some basic edits like cropping, and reading image metadata like EXIF data. Although Java includes some image manipulation tools in the JDK, they don’t have the best reputation, and there’s no guarantee it’ll stay up to date with newer formats like HEIC and JXL, which I want to support.

My initial investigation took me to a long-standing library called ImageMagick1, and then to a newer library called libvips2. It turns out Mastodon recently switched from the former to the latter, and after having a look at the benchmarks, libvips seemed like a good choice. JVM bindings do seem to exist, but use JNI, which has a bad reputation and I wasn’t too keen to use, knowing that JDK 22 just shipped tools to replace it. Specifically, the “Foreign Function & Memory API” (JEP 4543) for calling C APIs, and the “Class-File API” (JEP 4574) for generating class files.

After a couple of weeks of work, I built something that could automatically generate JVM bindings from the libvips headers. This means there’s very little work involved in staying up to date with new upstream versions, there’s less human (rabbit?) error in the bindings, and mistakes can be corrected across the API in one go. On top of the raw bindings generated with jextract5 there’s another generated “helper API” to make usage safer and avoid mistakes with pointers and lifetimes, as the libvips documentation correctly warns about. The beauty of generating the bindings is that there is, in theory, close to 100% API coverage in the very first versions. Here’s an example of what some basic usage looks like right now, in Kotlin:

import app.photofox.vipsffm.generated.Vips
import java.lang.foreign.Arena

// ...

Arena.ofConfined().use { arena ->
    val vips = Vips(arena)

    val version = vips.versionString()
    logger.info("vips version: $version")
  
    val sourceImage = vips.imageNewFromFile(
        "sample/src/main/resources/sample_images/rabbit.jpg",
        VipsIntOption("access", VipsRaw.VIPS_ACCESS_SEQUENTIAL())
    )
    val sourceWidth = VipsImage.Xsize(sourceImage)
    val sourceHeight = VipsImage.Ysize(sourceImage)
    logger.info("source image size: $sourceWidth x $sourceHeight")

    val outputPath = workingDirectory.resolve("rabbit_copy.jpg")
    vips.imageWriteToFile(sourceImage, outputPath.absolutePathString())

    val outputImagePointer = VipsOutputPointer(arena)
    vips.thumbnail("sample/src/main/resources/sample_images/rabbit.jpg", outputImagePointer, 400)
    val thumbnailImage = outputImagePointer.dereferencedOrThrow()
    val thumbnailPath = workingDirectory.resolve("rabbit_thumbnail_400.jpg")
    vips.imageWriteToFile(thumbnailImage, thumbnailPath.absolutePathString())

    val thumbnailWidth = VipsImage.Xsize(thumbnailImage)
    val thumbnailHeight = VipsImage.Ysize(thumbnailImage)
    logger.info("thumbnail image size: $thumbnailWidth x $thumbnailHeight")
}

The code samples also function as correctness and memory leak tests during development, which has worked really well. I’m being conservative about version numbers and will promote the library to 1.0.0 when other users have a chance to test it out, but it should be ready for some real-world usage. Check it out, give feedback if you have any, and lend me some dopamine by giving the repo a star over on GitHub at github.com/lopcode/vips-ffm 🌟.


  1. ImageMagick - https://imagemagick.org/ 

  2. libvips - https://www.libvips.org/ 

  3. JEP 454 - Foreign Function & Memory API - https://openjdk.org/jeps/454 

  4. JEP 457 - Class-File API - https://openjdk.org/jeps/457 

  5. jextract - https://github.com/openjdk/jextract 

« Staying Organised With a Spicy Brain