What did you do?
I wrote some code to implement color cycling animation, because the library I'm writing decodes the graphics format from an old DOS game and converts it to modern formats (such as PNG, WEBP or not-so-modern GIF). Thus, I implemented my own color cycling code that changes the palette, and a simple loop that generates multiple frames of animation by cloning the Image object and replacing the palette.
So, in the end I have a list of Image objects with mode P, where each image has the exact same pixel data, but (slightly) different palettes.
Then I tried to save this list of images as an animation. Saving as GIF works fine. Saving as WebP also works fine. But saving as animated PNG doesn't work. APNG animations have all frames sharing a single palette (if a palette is present).
What did you expect to happen?
In summary, after I saved as APNG, I expected to see the animation when opening in the browser. This didn't happen.
In more details, I expected Pillow to detect the different palettes across the multiple frames of animation, and thus the following behavior should happen:
- If the count of all unique colors across all frames is less than 256 (or 255)…
- Pillow would generate an optimal palette that includes all the colors from all the frames.
- Pillow would rewrite the pixels of each frame to use the same palette. This should be lossless.
- If the count of all unique colors is greater than 256…
- Pillow would convert the image to
RGB or RGBA in order to accommodate all colors of all frames.
- Alternatively, Pillow could detect such cases and raise a warning or error.
Additionally, I also expected optimize=True to introduce transparent pixels in the frames, to reduce the overall file size, by using APNG_DISPOSE_OP_NONE for the subsequent frames. This would be similar to GIF optimization.
What actually happened?
-
Pillow ignored the palettes from the later frames, saving all frames using the same palette. Thus, the color-cycling animation did not animate at all.
-
No warning was given. I spent quite some time troubleshooting.
-
I was forced to convert all frames to RGB, and then it worked.
-
Pillow did not "optimize" the frames (as it does for GIF), leading to very large file sizes.
-
Converting to RGBA, as a hint that transparent pixels should be introduced, had no effect. Well, it had one effect: the file size got even larger.
Additionally, saving the exact same list of P images as both GIF and WebP worked flawlessly on the first try, with a mostly-identical code. Yet, APNG was broken.
What are your OS, Python and Pillow versions?
- OS: Linux
- Python: 3.14.6
- Pillow: 12.3.0
--------------------------------------------------------------------
Pillow 12.3.0
Python 3.14.6 (main, Jun 15 2026, 11:36:54) [GCC 16.1.1 20260430]
--------------------------------------------------------------------
Python executable is /usr/bin/python3
System Python files loaded from /usr
--------------------------------------------------------------------
Python Pillow modules loaded from /usr/lib/python3.14/site-packages/PIL
Binary Pillow modules loaded from /usr/lib/python3.14/site-packages/PIL
--------------------------------------------------------------------
--- PIL CORE support ok, compiled for 12.3.0
--- TKINTER support ok, loaded 8.6
--- FREETYPE2 support ok, loaded 2.14.3
--- LITTLECMS2 support ok, loaded 2.19
--- WEBP support ok, loaded 1.6.0
--- AVIF support ok, loaded 1.4.2
--- JPEG support ok, compiled for libjpeg-turbo 3.2.0
--- OPENJPEG (JPEG2000) support ok, loaded 2.5.4
--- ZLIB (PNG/ZIP) support ok, loaded 1.3.2
--- LIBTIFF support ok, loaded 4.7.2
--- RAQM (Bidirectional Text) support ok, loaded 0.10.5
--- LIBIMAGEQUANT (Quantization method) support ok, loaded 4.2.2
--- XCB (X protocol) support ok
--------------------------------------------------------------------
Code snippets
I don't have a full end-to-end short example. I can give you some code snippets.
Code that creates new palettes by color cycling a few ranges:
https://github.com/denilsonsa/jetpack-python-jetpyck/blob/31add53a9a0aa28ca3d6aa2c8546fd2cfd1d0f97/src/jetpyck/gfxdat.py#L482-L529
https://github.com/denilsonsa/jetpack-python-jetpyck/blob/31add53a9a0aa28ca3d6aa2c8546fd2cfd1d0f97/src/jetpyck/gfxdat.py#L1007-L1022
Code that generates multiple P images with different palettes:
https://github.com/denilsonsa/jetpack-python-jetpyck/blob/31add53a9a0aa28ca3d6aa2c8546fd2cfd1d0f97/src/jetpyck/gfxdat.py#L1050-L1061
Code that saves as APNG:
https://github.com/denilsonsa/jetpack-python-jetpyck/blob/31add53a9a0aa28ca3d6aa2c8546fd2cfd1d0f97/src/jetpyck/gfxdat.py#L1220-L1241
Jupyter notebook that loads files from the original DOS game, and then saves as multiple file formats:
https://github.com/denilsonsa/jetpack-python-jetpyck/blob/709d26cd0e69bee6d54e613e3afa62727e4c8821/gfx_tilesets.ipynb#L211-L245
What did you do?
I wrote some code to implement color cycling animation, because the library I'm writing decodes the graphics format from an old DOS game and converts it to modern formats (such as PNG, WEBP or not-so-modern GIF). Thus, I implemented my own color cycling code that changes the palette, and a simple loop that generates multiple frames of animation by cloning the
Imageobject and replacing the palette.So, in the end I have a list of Image objects with mode
P, where each image has the exact same pixel data, but (slightly) different palettes.Then I tried to save this list of images as an animation. Saving as GIF works fine. Saving as WebP also works fine. But saving as animated PNG doesn't work. APNG animations have all frames sharing a single palette (if a palette is present).
What did you expect to happen?
In summary, after I saved as APNG, I expected to see the animation when opening in the browser. This didn't happen.
In more details, I expected Pillow to detect the different palettes across the multiple frames of animation, and thus the following behavior should happen:
RGBorRGBAin order to accommodate all colors of all frames.Additionally, I also expected
optimize=Trueto introduce transparent pixels in the frames, to reduce the overall file size, by usingAPNG_DISPOSE_OP_NONEfor the subsequent frames. This would be similar to GIF optimization.What actually happened?
Pillow ignored the palettes from the later frames, saving all frames using the same palette. Thus, the color-cycling animation did not animate at all.
No warning was given. I spent quite some time troubleshooting.
I was forced to convert all frames to
RGB, and then it worked.Pillow did not "optimize" the frames (as it does for GIF), leading to very large file sizes.
Converting to
RGBA, as a hint that transparent pixels should be introduced, had no effect. Well, it had one effect: the file size got even larger.Additionally, saving the exact same list of
Pimages as both GIF and WebP worked flawlessly on the first try, with a mostly-identical code. Yet, APNG was broken.What are your OS, Python and Pillow versions?
Code snippets
I don't have a full end-to-end short example. I can give you some code snippets.
Code that creates new palettes by color cycling a few ranges:
https://github.com/denilsonsa/jetpack-python-jetpyck/blob/31add53a9a0aa28ca3d6aa2c8546fd2cfd1d0f97/src/jetpyck/gfxdat.py#L482-L529
https://github.com/denilsonsa/jetpack-python-jetpyck/blob/31add53a9a0aa28ca3d6aa2c8546fd2cfd1d0f97/src/jetpyck/gfxdat.py#L1007-L1022
Code that generates multiple
Pimages with different palettes:https://github.com/denilsonsa/jetpack-python-jetpyck/blob/31add53a9a0aa28ca3d6aa2c8546fd2cfd1d0f97/src/jetpyck/gfxdat.py#L1050-L1061
Code that saves as APNG:
https://github.com/denilsonsa/jetpack-python-jetpyck/blob/31add53a9a0aa28ca3d6aa2c8546fd2cfd1d0f97/src/jetpyck/gfxdat.py#L1220-L1241
Jupyter notebook that loads files from the original DOS game, and then saves as multiple file formats:
https://github.com/denilsonsa/jetpack-python-jetpyck/blob/709d26cd0e69bee6d54e613e3afa62727e4c8821/gfx_tilesets.ipynb#L211-L245