Repository navigation
feat(image): read Windows animated cursors (.ani) - #1390
Conversation
An .ani is a RIFF "ACON" file whose frames are whole .cur/.ico files. Filee's own reader (Ani) takes the frames out, has ImageMagick decode each at its largest size, and builds the animation step by step, following the "seq " and "rate" chunks (jiffies of 1/60 s, rounded to hundredths) with an endless loop. - GIF, APNG, WebP and the other animated targets keep every step and its time; other targets get the first step. - To CUR, the first frame is written as stored (every size and its hotspot) unless the preset changes the picture; then the hotspot follows a resize, turn or mirror. ImageMagick writes CUR as an icon (type 1, no hotspot), so that file is made a cursor afterwards. - Files are read defensively: bounded size, frame and step counts, frame dimensions; raw-bitmap frames and damaged files are refused with a clear message. - Read only, specialist, Developer field (like CUR). - Settings schema 80: the images donut learns .ani; custom claims stay put. (May need renumbering depending on merge order: 79 is taken by KnifeLemon#1384.) Fixes KnifeLemon#1365 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPxh5pr2HpsMpvTujh7kW6
ImageMagick writes "CUR" as an icon: header type 1, and each directory entry holds colour planes and bits per pixel where Windows reads the hotspot, so a PNG -> CUR from Filee clicked at (1, 32). CursorFile now makes every .cur Filee writes (ImageEncoder.Write, Merge images) a type-2 cursor clicking at the top left corner. From a CUR or ANI source the hotspot follows the picture through a resize, turn or mirror. The ANI-only patch (Ani.KeepHotspot, Ani.Moved) moves into CursorFile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPxh5pr2HpsMpvTujh7kW6
KnifeLemon
left a comment
There was a problem hiding this comment.
Thanks a lot for this one: the ANI reader is careful and the tests build every file themselves. Nice catch on ImageMagick's CUR writer, too.
One change before we merge:
- Check the pixel budget before decoding.
ReadandHotspotcallDecode(frames)first, and only then compare againstMaxAnimationPixels.Decodehas ImageMagick read every directory entry of every frame.Entrieschecks each entry's side, but not how many entries a frame has, or whether several entries point at the same bytes. A small file can therefore list tens of thousands of entries that all point at one well-compressed 1024×1024 PNG, and ImageMagick would decode all of them. Could you add the budget inEntries/Parse, before anything is decoded? For example: the sum of width × height over all entries of all frames, held to the same limit, and a small cap on entries per frame (real cursors have only a few).
Two notes, nothing to do on your side:
Review of KnifeLemon#1390: Read and Hotspot decoded every frame before comparing with the pixel budget, and a frame's directory could list any number of sizes, all pointing at one well-compressed 1024 px PNG, which ImageMagick would decode each time. Each frame's directory is now read and checked when the frame is parsed: at most 32 sizes per frame (real cursors have a handful; Bibata's largest have 14), and, before anything is decoded, the sum of width x height over every size of every frame, and the steps times the largest canvas, are both held to the same limit (1,024 steps of a 256 px cursor). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPxh5pr2HpsMpvTujh7kW6
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPxh5pr2HpsMpvTujh7kW6
|
Thanks for the review! Done in 3d12ba1 (and a doc note in 302b38d): each frame's directory is now read when the frame is parsed, with at most 32 sizes per frame, and before anything is decoded both the sum of width × height over every size of every frame and steps × the largest canvas are held to the same limit. New tests cover 33 sizes in a frame, a small file whose 3 × 32 sizes all point at one 1024 px PNG, and 4,096 steps of a 256 px frame; all are refused before ImageMagick decodes anything. |
Schema 79 went to iPhone ringtones (KnifeLemon#1349): the .ani migration stays at 80, after it. The grade counts take both: M4R is general (97), ANI specialist (116). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPxh5pr2HpsMpvTujh7kW6
FastTracker 2 .xm, Impulse Tracker .it and Scream Tracker 3 .s3m modules are read-only audio formats that go to every audio target in one FFmpeg step. FFmpeg plays them only through its libopenmpt (or libmodplug) demuxer: the gyan.dev full build and BtbN's Linux GPL builds that Filee downloads have it, Homebrew's FFmpeg on macOS does not. Filee reads `ffmpeg -demuxers` once per executable and, without either demuxer, stops the file with error.tracker_needs_openmpt instead of FFmpeg's "Invalid data found". Settings schema 81 adds the three extensions to existing audio donuts without taking custom claims (80 is claimed by KnifeLemon#1390). Amiga .mod stays the camcorder video it already is. Closes KnifeLemon#1376 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A .zxp, the package of an After Effects, Premiere Pro or Photoshop extension, is a signed ZIP: the bundled 7-Zip extracts it and it converts to every archive target, its META-INF/signatures.xml kept as a file. It is never a target, since Adobe's apps install only packages signed with the developer's certificate. Settings schema 82 adds .zxp to existing archives donuts without taking custom claims (80 and 81 are claimed by KnifeLemon#1390 and KnifeLemon#1376). Closes KnifeLemon#1382 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Schemas 80 to 83 are taken on main (PDF/A, tracker modules, .zxp, .mpg4): the .ani migration moves to 84, after them. The specialist count takes ANI on top of main's (119). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPxh5pr2HpsMpvTujh7kW6
… schema 85 Main took 84 for the folder compression restore (KnifeLemon#1345). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
KnifeLemon
left a comment
There was a problem hiding this comment.
Thanks, that's exactly it. Reading each frame's directory while parsing, capping the sizes per frame, and checking both pixel budgets before ImageMagick decodes anything closes the gap, and the three new tests pin it down nicely.
We merged main into your branch, moved the migration to schema 85 (main has taken up to 84 since) and updated the format counts. Thanks again for the ANI reader and the CUR fix!
What does this change?
Filee now reads Windows animated cursors (
.ani). Closes #1365An
.aniis a RIFFACONfile whose frames are whole.cur/.icofiles. ImageMagick doesn't read ANI but reads CUR/ICO, so a small reader of Filee's own (src/Filee.Engines/Magick/Ani.cs) takes the frames out, has ImageMagick decode each one at its largest size, and builds the animation step by step:seqchunk (frames can repeat), for the time given by theratechunk or the header (jiffies of 1/60 s, rounded to hundredths), looping forever. Frames of different sizes share one canvas.→ CURconversion. ImageMagick writes "CUR" as an icon: the header says type 1, and each directory entry holds colour planes and bits per pixel where Windows reads the hotspot. So until now a PNG → CUR from Filee was an .ico named .cur that clicked at (1, 32). The newCursorFilemakes every .cur Filee writes (normal conversions and Merge images) a real cursor (type 2) clicking at the top left corner. From a CUR or ANI source, the hotspot follows the picture. It is in this PR because the ANI → CUR fix needed exactly this code.AF_ICON), are refused with a clear message.Classification (docs/FORMAT-RULES.md): read only (
Writable: false, likekra/livp), so rule 5 admits it as an input. It is graded specialist in the Developer field, the same ascur: animated cursors are made by people who make cursor themes, the same people who make static cursors, so the reasoning that put CUR there applies unchanged.Settings schema 84: the images donut learns
.ani; custom claims stay put. The number follows the last one onmain(83) and may need renumbering depending on merge order: #1384 (Paint.NET) also takes 84 for now. Whichever lands second also needs one more in theSpecialistcount ofFormatFieldsTests.Type of change
How was it tested?
dotnet format --verify-no-changesdotnet test: newAniTests(28, Engines; files built byAniBuilder: no seq/rate, seq repeating frames, per-step rate, short rate chunk, several sizes per frame, frames of different sizes, hotspot kept as is / after resize and turn, truncated / fake / damaged / raw-bitmap files, detection by extension),CursorFileTests(3, Engines: PNG → CUR is type 2 clicking at 0,0; a CUR resized or mirrored keeps its hotspot; a trimmed one goes to 0,0) andAniFormatTests(Core: detection, classification, schema 80 migration). Full Engines and Core suites run in a Linux container: the only failures are ones that already fail there onmain(Pandoc, FFmpeg and LibreOffice not installed;FolderCopyTestsrunning as root). The Engines suite was run again after the CUR fix (3,062 passed, the same 18 known failures); Core was not, as that commit touches only the engines. App tests were run only by filter (DefaultPlacement, FieldsPage, PaletteChipTip, FormatRequest, ToolbarEditor, LocalizationResource), before the CUR fix; all passed.Busy.aniandWork.ani, 3 sizes; not committed) withfilee-clion Linux. To GIF/WebP/APNG they keep 54 frames at their timing; to PNG, PDF, ICNS, SVG, TIFF, AVIF, JXL and BMP they convert; to CUR the result is byte-identical to the stored first frame (14 sizes, hotspots kept). Not tested on Windows, and ANI → video was not tried (no FFmpeg in the container).Checklist
src/Filee.App/Assets/i18n/*.jsonfiles (none added; errors are English messages, as for KRA)THIRD-PARTY-NOTICES.md(none)