Java Image and Video Libraries

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

See the section bibliography for the reference material consulted while preparing these pages.

Baseline date: 2026-10-07. Versions below are the latest releases listed on Maven Central on that date. Check each project’s repository for newer releases and for its licence before you adopt it.

Choosing an image library

Library Licence Status Notes

Thumbnailator

MIT

Maintained (0.4.21)

ImageIO-based. Resize, crop, rotate, watermark, with a fluent API. The recommended default for JVM thumbnails.

imgscalr

Apache-2.0

Unmaintained: last release 4.2, 2012

Legacy only. Do not choose it for new code.

TwelveMonkeys ImageIO

BSD-3-Clause

Maintained (3.15.3)

Plugins for ImageIO: JPEG including CMYK, TIFF, PSD and more. WebP is read only. No AVIF or HEIC.

webp-imageio

Apache-2.0

Check the repository

ImageIO plugin that writes WebP through native libraries.

Scrimage

Apache-2.0

Maintained (4.6.8)

Immutable image API with filters and a WebP writer (scrimage-webp) that runs a bundled native binary.

JVips and vips-ffm

Check each repository

Check each repository

Bindings to libvips, for the fastest throughput on large images. vips-ffm (1.9.9) uses the Java foreign function API and needs a recent JDK and libvips installed.

ImageIO on its own reads JPEG, PNG, GIF and BMP and writes the same. WebP, AVIF and HEIC need extra libraries or a native engine such as libvips. When AVIF output is a requirement, libvips is the practical route on the JVM, see image formats and encoding.

Thumbnailator

<dependency>
  <groupId>net.coobird</groupId>
  <artifactId>thumbnailator</artifactId>
  <version>0.4.21</version>
</dependency>
import net.coobird.thumbnailator.Thumbnails;
import net.coobird.thumbnailator.geometry.Positions;
import java.io.File;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;

// A 2:3 cover thumbnail: scale to fill, crop the overflow from the centre, JPEG at 80.
Thumbnails.of(new File("original.jpg"))
    .size(640, 960)
    .crop(Positions.CENTER)
    .outputFormat("jpg")
    .outputQuality(0.8)
    .toFile(new File("w640.jpg"));

// A width-bounded variant. Thumbnailator has no "do not enlarge" option, so .width(1280) would upscale
// a smaller original: read the width from the image header first and cap the target at it.
File original = new File("original.jpg");
int sourceWidth;
try (ImageInputStream in = ImageIO.createImageInputStream(original)) {
    ImageReader reader = ImageIO.getImageReaders(in).next();
    reader.setInput(in);
    sourceWidth = reader.getWidth(0);    // header width, before EXIF rotation: compare with the height for orientations 5 to 8
    reader.dispose();
}
Thumbnails.of(original)
    .width(Math.min(1280, sourceWidth))
    .toFile(new File("w1280.jpg"));

Thumbnailator applies the EXIF orientation by default. References: Thumbnailator on GitHub and its Javadoc.

TwelveMonkeys and WebP

<dependency>
  <groupId>com.twelvemonkeys.imageio</groupId>
  <artifactId>imageio-jpeg</artifactId>
  <version>3.15.3</version>
</dependency>
<dependency>
  <groupId>com.twelvemonkeys.imageio</groupId>
  <artifactId>imageio-webp</artifactId>
  <version>3.15.3</version>
</dependency>

Adding the plugins to the classpath registers them with ImageIO, so ImageIO.read(file) can then read WebP and unusual JPEGs. They do not write WebP. To write WebP from the JVM, use webp-imageio, Scrimage’s WebP module, or call libvips or cwebp. Reference: TwelveMonkeys ImageIO.

Scrimage

<dependency>
  <groupId>com.sksamuel.scrimage</groupId>
  <artifactId>scrimage-core</artifactId>
  <version>4.6.8</version>
</dependency>
<dependency>
  <groupId>com.sksamuel.scrimage</groupId>
  <artifactId>scrimage-webp</artifactId>
  <version>4.6.8</version>
</dependency>
import com.sksamuel.scrimage.ImmutableImage;
import com.sksamuel.scrimage.webp.WebpWriter;
import java.io.File;

ImmutableImage image = ImmutableImage.loader().fromFile(new File("original.jpg"));
image.scaleToWidth(640).output(WebpWriter.DEFAULT.withQ(78), new File("w640.webp"));

Reference: Scrimage on GitHub.

libvips from Java

<dependency>
  <groupId>app.photofox.vips-ffm</groupId>
  <artifactId>vips-ffm-core</artifactId>
  <version>1.9.9</version>
</dependency>

vips-ffm needs the libvips shared library on the host and a JDK that supports the foreign function and memory API. Check the repository README for the exact JDK and libvips requirements, and for the API, which mirrors libvips operations (thumbnail, heifsave, webpsave). The criteo/JVips binding is an alternative that uses JNI. When neither fits, a worker that runs the vips command line tool through ProcessBuilder is a robust option.

BlurHash in Java

blurhash-java is the encoder listed in the BlurHash implementations list. Encode from a downscaled BufferedImage, never from the original.

<dependency>
  <groupId>io.trbl</groupId>
  <artifactId>blurhash</artifactId>
  <version>1.0.0</version>
</dependency>
import io.trbl.blurhash.BlurHash;
import net.coobird.thumbnailator.Thumbnails;
import java.awt.image.BufferedImage;
import java.io.File;

BufferedImage small = Thumbnails.of(new File("original.jpg"))
    .size(64, 64)          // fits inside 64 x 64 and keeps the aspect ratio
    .asBufferedImage();

String hash = BlurHash.encode(small, 4, 3);   // 4 x 3 components, 28 characters

Choose the components from the aspect ratio, as explained in the BlurHash page. Reference: hsch/blurhash-java. The Kotlin Multiplatform library vanniktech/blurhash (0.3.0) is an alternative for Kotlin code.

Video from Java

Video from Java means driving FFmpeg. Both libraries below need FFmpeg and FFprobe, either installed on the host or, for JAVE2, bundled.

Library Licence Notes

Jaffree

Apache-2.0

Wraps the FFmpeg and FFprobe command line tools. Latest release listed on Maven Central: 2024.08.29. You tell it where FFmpeg is installed.

JAVE2

GPL-3.0

Bundles FFmpeg binaries for common platforms, which is convenient and brings GPL obligations and the licences of the bundled codecs. Check them against your distribution model before you use it in a closed-source product.

Jaffree: transcode, cover and probe

<dependency>
  <groupId>com.github.kokorin.jaffree</groupId>
  <artifactId>jaffree</artifactId>
  <version>2024.08.29</version>
</dependency>
import com.github.kokorin.jaffree.ffmpeg.FFmpeg;
import com.github.kokorin.jaffree.ffmpeg.UrlInput;
import com.github.kokorin.jaffree.ffmpeg.UrlOutput;
import com.github.kokorin.jaffree.ffprobe.FFprobe;
import com.github.kokorin.jaffree.ffprobe.FFprobeResult;
import java.nio.file.Path;
import java.nio.file.Paths;

Path bin = Paths.get("/usr/bin");           // directory that contains ffmpeg and ffprobe

// Probe first, to reject files that are not what the user claimed.
FFprobeResult probe = FFprobe.atPath(bin)
    .setShowStreams(true)
    .setShowFormat(true)                    // without it ffprobe returns no format section and getFormat() is null
    .setInput("input.mov")
    .execute();
double seconds = probe.getFormat().getDuration();

// Transcode to a 720p H.264 rendition with a keyframe every 2 seconds at 24 fps.
FFmpeg.atPath(bin)
    .addInput(UrlInput.fromPath(Paths.get("input.mov")))
    .addArguments("-vf", "scale=-2:720")
    .addArguments("-c:v", "libx264")
    .addArguments("-crf", "22")
    .addArguments("-g", "48")
    .addArguments("-c:a", "aac")
    .addOutput(UrlOutput.toPath(Paths.get("720p.mp4")))
    .execute();

// Extract a cover frame at 10 percent of the duration.
FFmpeg.atPath(bin)
    .addInput(UrlInput.fromPath(Paths.get("input.mov")).setPosition((long) (seconds * 100)))
    .addOutput(UrlOutput.toPath(Paths.get("cover.jpg")).setFrameCount(null, 1L))
    .execute();

The method names above follow Jaffree’s documented API at the time of writing, so verify them against the README of the version you install. Reference: Jaffree on GitHub.

JAVE2: transcode and cover

<dependency>
  <groupId>ws.schild</groupId>
  <artifactId>jave-core</artifactId>
  <version>${jave.version}</version>
</dependency>
<dependency>
  <groupId>ws.schild</groupId>
  <artifactId>jave-nativebin-linux64</artifactId>
  <version>${jave.version}</version>
</dependency>
import ws.schild.jave.Encoder;
import ws.schild.jave.MultimediaObject;
import ws.schild.jave.encode.AudioAttributes;
import ws.schild.jave.encode.EncodingAttributes;
import ws.schild.jave.encode.VideoAttributes;
import ws.schild.jave.info.VideoSize;
import java.io.File;

AudioAttributes audio = new AudioAttributes();
audio.setCodec("aac");
audio.setBitRate(128000);

VideoAttributes video = new VideoAttributes();
video.setCodec("libx264");
video.setSize(new VideoSize(1280, 720));

EncodingAttributes attrs = new EncodingAttributes();
attrs.setOutputFormat("mp4");
attrs.setAudioAttributes(audio);
attrs.setVideoAttributes(video);

new Encoder().encode(new MultimediaObject(new File("input.mov")), new File("720p.mp4"), attrs);

Set jave.version to the current release from the JAVE2 repository and pick the native binary artifact for your platform. For fine control of rate and keyframes, passing arguments directly to FFmpeg (as with Jaffree) is simpler than JAVE2’s attribute objects.

Whichever library you pick, run it in a worker with a time limit, a memory limit and a sandbox, see security and operations. The same code appears in a job pipeline in the Spring Boot and Quarkus pipeline.