Skip to content

Animated WebP

Scrimage supports both reading and writing animated WebPs, using the libwebp toolchain under the hood:

Operation Tool wrapped Scrimage class
Decompose into frames + metadata anim_dump + webpmux AnimatedWebpReaderAnimatedWebp
Compose from a sequence of frames img2webp StreamingWebpWriter

All required binaries ship with the scrimage-webp module for Linux (x86-64, aarch64), macOS (x86-64, arm64) and Windows (x64); see the Webp page for how to override the bundled binaries.

Reading

We read an instance of an AnimatedWebp by using AnimatedWebpReader.read, passing in an ImageSource. The image source can be constructed from files, bytes, input streams and so on.

Once we have the AnimatedWebp, we can inspect it to retrieve an ImmutableImage per frame, the total number of frames, the per-frame display delay, the canvas dimensions, and the loop count (0 means loop forever).

AnimatedWebp webp = AnimatedWebpReader.read(ImageSource.of(new File("animated.webp")));
int frameCount        = webp.getFrameCount();
ImmutableImage first  = webp.getFrame(0);
ImmutableImage last   = webp.getFrame(frameCount - 1);
Duration firstDelay   = webp.getDelay(0);
Dimension canvas      = webp.getDimensions();
int loopCount         = webp.getLoopCount();   // 0 = forever
val webp = AnimatedWebpReader.read(ImageSource.of(File("animated.webp")))
val firstFrame = webp.frames.first()
val lastFrame  = webp.frames.last()
val firstDelay = webp.getDelay(0)
val webp = AnimatedWebpReader.read(ImageSource.of(new File("animated.webp")))
val firstFrame = webp.getFrames.get(0)
val lastFrame  = webp.getFrames.get(webp.getFrameCount - 1)

Note

Each frame returned by getFrame(n) is the fully composed image for that point in the animation — anim_dump applies inter-frame disposal and blending for you. You don't need to manually composite earlier frames to reproduce the on-screen result.

The reader also preserves the original WebP bytes, accessible via getBytes(), in case you need to forward the file unchanged after inspecting its metadata.

Writing

Using the StreamingWebpWriter class, first we create an instance of the writer, configuring the frame delay (delay between images) and whether the animation should loop forever. Then we open a stream, specifying either an output path or an output stream — the AWT image type isn't required because img2webp accepts any image scrimage can render.

StreamingWebpWriter writer = new StreamingWebpWriter()
   .withFrameDelay(Duration.ofSeconds(2))
   .withInfiniteLoop(true);
StreamingWebpWriter.WebpStream webp = writer.prepareStream("/path/to/animated.webp");
val writer = StreamingWebpWriter()
   .withFrameDelay(Duration.ofSeconds(2))
   .withInfiniteLoop(true)
val webp = writer.prepareStream("/path/to/animated.webp")
val writer = new StreamingWebpWriter()
   .withFrameDelay(Duration.ofSeconds(2))
   .withInfiniteLoop(true)
val webp = writer.prepareStream("/path/to/animated.webp")

Next we can add as many images as we want, each an instance of an ImmutableImage. The default frame delay applies, or you can pass a Duration to override the delay for an individual frame.

webp.writeFrame(image0);
webp.writeFrame(image1, Duration.ofMillis(500));
webp.writeFrame(imageN);

Finally, we close the stream — at which point the buffered frames are encoded into the destination. WebpStream extends AutoCloseable, so the recommended pattern is try-with-resources:

try (StreamingWebpWriter.WebpStream webp = writer.prepareStream("/path/to/animated.webp")) {
   webp.writeFrame(image0);
   webp.writeFrame(image1);
   webp.writeFrame(imageN);
}
writer.prepareStream("/path/to/animated.webp").use { webp ->
   webp.writeFrame(image0)
   webp.writeFrame(image1)
   webp.writeFrame(imageN)
}
val webp = writer.prepareStream("/path/to/animated.webp")
try {
   webp.writeFrame(image0)
   webp.writeFrame(image1)
   webp.writeFrame(imageN)
} finally {
   webp.close()
}

If you'd rather close it explicitly:

webp.close();

Encoding options

In addition to withFrameDelay and withInfiniteLoop, the writer exposes the WebP-native compression knobs:

Method Effect
withLossless(boolean) true (the default) emits -lossless, otherwise -lossy.
withQ(int) RGB quality factor in [0, 100].
withM(int) Encoding method in [0, 6] — higher is slower but produces smaller output.

For example:

StreamingWebpWriter writer = new StreamingWebpWriter()
   .withFrameDelay(Duration.ofMillis(100))
   .withInfiniteLoop(true)
   .withLossless(false)
   .withQ(75)
   .withM(4);

Note

Browsers have a minimum frame delay. If you try to set the frame delay lower than the minimum for that browser, the browser will use the default frame delay. The default frame delay is not equal to the minimum. Chrome and Firefox have a minimum frame delay of 0.2 seconds and IE and Safari 0.6 seconds.