Repository navigation
Smaller animations - #59
Merged
Merged
Conversation
The animation encoders composite the frames and store each as the area that changed since the frame before, and merge identical frames. Within that area, unchanged pixels become transparent, unless the area as it is compresses better: when unchanged pixels are scattered (e.g. in videos), transparent ones break up the image. Which one is chosen by counting the changes from pixel to pixel, which picks the same as compressing both in almost every case, at a fraction of the cost. Pixels that blending can't produce (they get more transparent, or are semi-transparent where APNG and WebP blending isn't exact) make the frame replace its area in APNG and WebP. GIF can't replace: the frame before clears an area covering them instead (disposal "background"), and such frames get a transparent index, which Pillow and ffmpeg restore to (browsers restore to transparent anyway). Re-encoding decoded animations (which have full frames), the files get 33% (GIF), 44% (APNG) and 34% (WebP) of their size, in about the same time; none got larger. APNGs and WebPs show exactly the same animation as before, checked against our decoder and Pillow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ljnxm1Qjrocq1izGajLx8J
A frame whose changed pixels all became transparent crashed encodeGIF(): after the frame before cleared them, nothing was left to store. It's now stored as an empty frame that only takes its time. Sparse frames need transparency, which isn't free when the frame or animation is opaque otherwise: an alpha channel in WebP, a palette entry in GIF (one color less to quantize to), and in APNG, where all frames share one pixel format, RGBA instead of RGB, or a larger palette (8 instead of 4 bit indices, or none at all at the 257th color). WebP and GIF weigh this per frame, APNG for the whole animation: an opaque one whose sparse frames don't pay for it gets them filled in again. Such files got up to 68% larger than before the optimization; on 762 animations (Wikimedia Commons, test suites of Pillow, libwebp and image-rs, files written by libwebp and Pillow) a few still get up to 9% larger, where the estimate is off. The optimizer works on 32-bit pixels, copies opaque runs at once, finds the pixels blending can't produce while it builds the frame instead of in a pass of their own, and copies frames onto a cleared canvas instead of blending them (decoded animations clear every frame): about twice as fast. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MLHc7GW46D6BmwtEM2khgr
A sparse GIF frame needs a palette entry for its unchanged pixels: if its colors fill the palette already, the quantizer had to merge colors, and a GIF that was lossless before wasn't anymore. Such frames stay dense when that fits the palette. The APNG pixel format check stops at the 257th color when the frames aren't grey (truecolor either way), and filling sparse frames in again copies frames that replace their area by rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MLHc7GW46D6BmwtEM2khgr
Checked with Pillow (and libwebp, which Pillow uses for WebP) against our decoder: - WebP: the alpha flag also depends on the canvas, not only the frames' pixels: where the first frame doesn't cover it or frames are disposed, it's transparent. Without the flag, libwebp shows it opaque black. - GIF: the first frame covers the canvas, decoders disagree on what's outside it (browsers show it transparent, Pillow a palette color). If frames restore the background, the first frame has a transparent index too: Pillow decides by it whether the animation has transparency, and restores to the background color otherwise. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MLHc7GW46D6BmwtEM2khgr
{optimize: false} stores the frames as they are, as before: the file
decodes to the same frames, and encoding is faster when little changes
from frame to frame. The changelog has the numbers measured on 762
animations.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MLHc7GW46D6BmwtEM2khgr
matmen
force-pushed
the
smaller-animations
branch
from
October 16, 2026 07:02
58cc16b to
0e07780
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
encodeGIF(),encodeAPNG()andencodeWebP()now optimize frames:{optimize: false}stores the frames as they are, as before.Results, from re-encoding 762 decoded animations (Wikimedia Commons, the Pillow/libwebp/image-rs test suites, and files written by libwebp and Pillow):
Large animations encode faster; small ones take up to about twice as long. A few files still come out up to 9% larger, where the cost estimate is off.
Compatibility: The files show the same animation as before, checked against our decoder and against Pillow/libwebp. They don't decode to the same frames anymore, which is what
{optimize: false}is for. This PR also fixes two things decoders disagreed on:It also fixes a crash in
encodeGIF()for frames whose changed pixels all became transparent.