From 78c54640a3fc444ab797491fa952a79d4d0e19eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20K=C3=A4mmerer?= Date: Mon, 28 Sep 2026 19:39:36 +0200 Subject: [PATCH] Leave animated PNGs to the OptiPNG pass The ZopfliPNG pass re-encodes the default image, possibly with a new color type or bit depth (e.g. RGBA -> palette), and then copies the remaining chunks back unchanged. For an APNG those include the fdAT frames, which are still encoded in the original color type, so every frame after the first decodes to garbage. With -strip the animation chunks are not copied back and the APNG becomes a still image. OptiPNG already avoids reductions for APNGs, so skip the ZopfliPNG pass for files with an acTL chunk before IDAT. Other PNGs are unchanged (byte-identical output). Co-Authored-By: Claude Opus 5.5 --- src/zopflipng.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/zopflipng.cpp b/src/zopflipng.cpp index a2234a66..a3c25683 100755 --- a/src/zopflipng.cpp +++ b/src/zopflipng.cpp @@ -518,6 +518,16 @@ static unsigned ZopfliPNGOptimize(const char * Infile, const std::vector& png) { + if (png.size() < 8) {return false;} + const unsigned char* end = png.data() + png.size(); + for (const unsigned char* chunk = png.data() + 8; end - chunk >= 12; chunk = lodepng_chunk_next_const(chunk, end)) { + if (lodepng_chunk_type_equals(chunk, "acTL")) {return true;} + if (lodepng_chunk_type_equals(chunk, "IDAT")) {return false;} + } + return false; +} + int Zopflipng(bool strip, const char * Infile, bool strict, unsigned Mode, int filter, unsigned multithreading, unsigned quiet) { ZopfliPNGOptions png_options; png_options.Mode = Mode; @@ -535,6 +545,9 @@ int Zopflipng(bool strip, const char * Infile, bool strict, unsigned Mode, int f fprintf(stderr, "Could not load PNG %s\n", Infile); return -1; } + // APNG frames (fdAT) are copied unchanged and would no longer match a reduced + // color type, so leave APNGs alone here; the OptiPNG pass does not reduce them. + if (IsAPNG(origpng)) {return 1;} if (filter == 6){ lodepng::getFilterTypes(filters, origpng); if(!filters.size()){