Use info disposal and blend when calculating APNG frame deltas - #10061
glaziermag wants to merge 5 commits into
Conversation
When disposal or blend were not passed to save(), the fcTL chunk used the im.info fallback, but the frame delta assumed OP_NONE and OP_SOURCE, so frames were cropped against the wrong base image. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Is there any particular reason there's no test for |
|
Only that I couldn't find a case where it changes the output. In the delta calculation, Your simplified version writes the same bytes as my original commit in all 30,240 cases I tried. However, I've since found that this PR lets from PIL import Image
im = Image.new("L", (8, 8))
im.info["disposal"] = 2
im.save("out.png", save_all=True, append_images=[im.copy()])
# ValueError: images do not matchmain already raises this with |
|
Sure, add that to this PR as well. |
The disposal fill is RGBA, so pasting it onto an L, P, 1 or I;16 frame raised "images do not match", and for RGB it left opaque black instead of the transparent background that the APNG spec uses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Added in f7dc072. Besides fixing the error for L, P, 1 and I;16, this also changes RGB output. After OP_BACKGROUND, the next RGB frame is now compared against a transparent background rather than opaque black, as the APNG spec describes, so black pixels in the cleared area are no longer cropped out. On main, a black RGB frame with a duration after OP_BACKGROUND was merged into the previous frame; the RGB case in the test checks that. RGBA and LA output is unchanged. |
If
disposalorblendis not passed tosave(), the APNG writer uses theim.infovalue for the fcTL chunk (L1306-L1307). The frame delta that decides how much of each frame is written still readsprevious.encoderinfo.get("disposal")(L1236-L1237), which isNonethere, so it assumesOP_NONE. A frame that changes only part of the canvas is cropped to that part, while the previous frame is disposed to transparent, so the rest of the image disappears.Since
Image.open()setsinfo["disposal"], this also affects re-saving an opened APNG.Image.open("Tests/images/iss634.apng").save(out, save_all=True)currently changes 40 of the 41 rendered frames. With this change, it round-trips unchanged.Changes proposed in this pull request:
disposalandblenddefaults that are written to the fcTL chunk. The "test info disposal" part oftest_apng_save_disposalalready relies oninfo["disposal"]being used as that default.OP_BACKGROUNDandOP_PREVIOUS.Tests/test_file_apng.pyandTests/test_file_png.pypass locally (Python 3.14, macOS).This was found and prepared by an AI agent (Claude Code) working from the glaziermag account.
Generated with Claude Code