Skip to content

fix(image): keep animation frames in AVIF, both ways - #1230

Open
KnifeLemon wants to merge 3 commits into
mainfrom
fix/animated-avif
Open

KnifeLemon wants to merge 3 commits into
mainfrom
fix/animated-avif

Conversation

@KnifeLemon

@KnifeLemon KnifeLemon commented Nov 6, 2026 •

Copy link
Copy Markdown
Owner

Fixes #1200

Animated GIF and WebP → AVIF came out as one still frame, because AVIF wasn't in ImageEncoder.Animated.

What changed

  • AVIF is now a target for animations. GIF, WebP, APNG and MNG → AVIF keep every frame and its timing (JPEG XL takes the same path, but has no test here).
  • No FFmpeg needed. The bundled Magick.NET 15.0.0 (ImageMagick 7.1.2-33) writes a collection as a real AVIF image sequence (avis brand, with a still primary item for viewers that don't play sequences), so this needs no new route or engine. FFmpeg's avif muxer with libaom-av1 or libsvtav1 also works, but as an optional engine it would have left most users with a still picture.
  • Opaque frames before transparent ones stay visible. A sequence has one alpha track, and ImageMagick stores a frame without an alpha channel as fully transparent in it. ImageEncoder.MatchAlpha gives every frame the channel when any frame is transparent, and none when all are opaque, so an opaque animation (also from an APNG, whose frames all have the channel) gets no alpha track.
  • Still pictures don't change. One frame takes the same frames[i].Write path as before. A test checks the bytes equal a direct ImageMagick write at the preset's quality.
  • Only a real sequence counts as an animation. AvifSequence.IsSequence looks at the file, in the spirit of Apng.IsAnimated: major brand avis and a moov box. Such a file is read as its own source format, so a plain avif source takes the same code paths as on main. A burst, a visible gain map or items of their own sizes stay separate _pN pictures, not cropped, not laid over each other, and not turned into an animation.
  • The reverse. A sequence → GIF, WebP or APNG keeps its frames and timing. A sequence → AVIF stays one file. Other targets, including PDF and HEIC, get the first frame, as with GIF; they used to get one _pN file or PDF page per frame.
  • Routes to video stay as they were. ImageEncoder.Edge is unchanged and still prices AVIF → GIF/APNG as a still picture, so AVIF photos keep reaching video through a full-colour PNG. The cost is that an animated AVIF → MP4 is now a video of its first frame (before, it made one video per frame).
  • Playback speed now shows for AVIF targets and works in milliseconds.
  • docs/ENGINES.md describes all this.

Limits

These are accepted for this PR:

  • Loop count isn't kept. ImageMagick writes no edit list, so a GIF that plays a set number of times loops for ever as AVIF. Chromium reports repetitionCount = Infinity for these files.
  • Slower and larger than FFmpeg's encoder. On one 60-frame 480 × 270 test GIF of 645 KB, at quality 60:
Output Size Encode time
This PR (ImageMagick AVIF) 177 KB about 49 s through the CLI, about 31 s for the write alone
FFmpeg libaom, -cpu-used 6 -crf 32 122 KB about 7 s
Filee WebP 373 KB about 5 s, including CLI startup

heic:speed barely changes the time (write alone: 8 was about 30 s), so the default stays.

  • The write can't be cancelled. collection.Write has no cancellation, so cancelling waits for it to finish. That matters more here because a long GIF takes minutes.

How it was tested

  • AnimatedAvifTests (42 cases, inputs generated in code, 42 passed): frame count, delays including zero, avis brand, resize and rotation on an animation, transparency, opaque frames before transparent ones read back pixel by pixel from GIF, WebP and MNG, no alpha track on opaque animations from GIF, WebP and APNG, quality, playback speed, TIFF pages, the reverse direction, PDF and routes. With FFmpeg present, ffmpeg decodes the frames itself and an FFmpeg-written AVIF is converted.
  • AvifBuilder builds multi-item AVIFs in memory, so no sample files are added. They cover a burst, a larger second item, a smaller second item, and the avis brand without tracks. A smaller item stands in for a visible gain map; those tests don't build the gain-map boxes themselves.
  • With MatchAlpha and the sequence check both switched off, 16 of the then 41 cases failed.
  • Other test classes:
    • AnimationSpeedTests, ApngTests, HeicTests and the video route tests, together with AnimatedAvifTests: 82 passed.
    • ImageFormatTests, ImageAndPdfTests, ImageToolTests and MetadataTests: 126 passed, 51 skipped (all 51 are in MetadataTests, from the merged main).
    • Preset_editor_shows_playback_speed_only_for_targets_that_keep_animations last ran before these fixes and merging main, and its code didn't change.
  • Real CLI conversions on Windows, with the results decoded in Chromium 152 through ImageDecoder:
    • The review's GIF, WebP and MNG inputs with one opaque first frame gave 0/89/89 % transparent, and pixel (7,20) of frame 0 is 255,0,0,255. Two opaque frames gave 0/0/89 %.
    • The same animation → AVIF → GIF and → WebP gave 0/89/89 %.
    • The review's multi-item files (burst, larger and smaller second item, a visible gain map with its real boxes) → PNG and WebP each gave _p1 and _p2. The hidden-thumbnail file and a plain still gave one file.
  • dotnet format --verify-no-changes on the changed code files.

Not tested: Linux and macOS (CI runs the tests there), Safari, Firefox, Windows' own Photos app, and a sequence → HEIC (it uses the same check as PDF, which is tested). Only targeted test classes were run locally, not the whole suites. "As on main" for multi-item files rests on the code paths and the reviewer's report of main, not on a side-by-side run against a main build.

🤖 Generated with Claude Code

An animated GIF or WebP became one still AVIF, because AVIF wasn't among
the formats whose frames are an animation. The bundled ImageMagick already
writes several frames as an AVIF image sequence and reads one back, so
AVIF joins that list and needs no FFmpeg. Still pictures take the same
path as before.

An animated AVIF now also stays animated as GIF, WebP or APNG and gives
one picture for other targets, instead of one file per frame. On the way
to video an AVIF still counts as a photo, so photos don't go through a
256-colour GIF. Playback speed applies to AVIF too.

Fixes #1200

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the bug Something isn't working label Nov 6, 2026
KnifeLemon and others added 2 commits November 7, 2026 03:07
An AVIF sequence has one alpha track, and ImageMagick stores a frame without
an alpha channel as fully transparent in it, so an opaque first frame of a GIF,
WebP or MNG that turns transparent later vanished. Every frame now gets the
channel when any frame is transparent, and none when all are opaque, so opaque
animations carry no alpha track.

Every multi-item AVIF was also taken for an animation: a burst lost its other
pictures as PNG or JPG, and items of other sizes were cropped or laid over the
first. An AVIF now counts as an animation only with the major brand "avis" and a
moov box, and any other AVIF takes the same paths as on main.

The tests build multi-item AVIFs in memory, read frames back pixel by pixel, and
cover delays of zero and resizing and turning an animation.

Refs #1200

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: converting animated gif and animated webp > avif format gives a one frame still image

1 participant