Add APNG R/W Support via libpng & Built-in Quantizer#4944
Add APNG R/W Support via libpng & Built-in Quantizer#4944felixbuenemann wants to merge 2 commits intolibvips:masterfrom
Conversation
Add animated PNG (APNG) support to the libpng backend, gated behind PNG_APNG_SUPPORTED (libpng 1.6+APNG patch or libpng 1.8+). Load: - Extend Read struct with APNG fields (page/n, canvas, dispose state) - Add page/n parameters to pngload (matching GIF/WebP loaders) - Detect acTL chunk and set animation metadata (n-pages, page-height, delay, loop) - Scan raw PNG chunks via vips_source_map to extract fcTL delays at header time, matching how GIF/WebP loaders handle delay metadata - Frame compositing with DISPOSE_OP_NONE/BACKGROUND/PREVIOUS and BLEND_OP_SOURCE/OVER for both 8-bit and 16-bit - Sequential generate callback for frame-by-frame reading - Compat defines for constant naming differences between libpng 1.6 APNG patch (PNG_DISPOSE_OP_NONE) and 1.8+ (PNG_fcTL_DISPOSE_OP_NONE) - Non-animated PNG fallthrough avoids duplicate png_read_update_info call (rejected by libpng 1.8) Save: - Detect animation via page-height metadata - Write APNG with acTL/fcTL/fdAT chunks via sink_disc callback - Disable interlace for animated output with warning - Palette mode support via quantisation Tests: - Add cogs-apng.png test image (indexed APNG converted from cogs.gif) - Add hand-crafted APNG test images for compositing: dispose-background, dispose-previous, and alpha blend-over - Add test_apng_load: metadata, page handling, single/multi-frame - Add test_apng_dispose_background: verify BACKGROUND dispose clears canvas to transparent - Add test_apng_dispose_previous: verify PREVIOUS dispose restores canvas, including spec rule that PREVIOUS on frame 0 = BACKGROUND - Add test_apng_blend_over: verify alpha-over compositing of semi-transparent frames - Add test_apng_save: lossless round-trip, palette round-trip, GIF to APNG conversion - Add have_apng() / skip_if_no_apng for builds without APNG support
Provides baseline palette quantization when libimagequant/quantizr are not available. Pipeline: Wu partition → nearest-color map → Floyd-Steinberg dithering. Wu's optimal 4D RGBA quantizer: - 5-bit RGB + 4-bit alpha histogram (33^3 × 17 = 610K entries) - 4D prefix-sum with 16-term inclusion-exclusion - Greedy variance-maximizing partition into up to 256 boxes Nearest-color Euclidean distance map: - For each quantized cell, finds the palette entry with minimum RGBA distance for accurate palette assignment Floyd-Steinberg error diffusion with serpentine scanning: - 4-neighbor error distribution (7/16, 3/16, 5/16, 1/16) - Alpha-scaled RGB error propagation (premultiplied-equivalent) prevents bright-dot artifacts at transparency edges - Optimizations: dither=1.0 fast path, int16 error buffers, transparent pixel skip
|
Btw. I also opened a PR for spng (which libvips also supports) to add APNG R/W support at randy408/libspng#283, but it looks like the library is unmaintained since 2023. |
|
This is great @felixbuenemann! Yes, libspng seems to have gone, sadly, so libpng only is fine. I'm trapped in a horrible deadline right now (I must submit a paper in a month presenting work I've still not finished), but after that, and a beer, I'll be back on libvips. |
|
@jcupitt That's fine. Do you prefer if I split the quantiser into a separate PR? |
|
I'm ok with one PR, but maybe someone else will review. I'll defer to them. |
|
Getting rid of a dependency sounds great, especially if we save time and memory. There's an issue somewhere asking for a "find $N most significant colours" operation, which is difficult with libimagequant because of the API. If we had our own cluster finder we could (probably?) add stuff like this relatively easily. |
That shouldn't be too hard to do, but currently the built-in quantizer is only used as a fallback, when neither quantizr nor libimagequant are found. I also have another optimization in the pipeline, that remaps pixels with a fully transparent alpha, but different rgb values to a single 0,0,0,0 alpha palette entry to ensure remaining palette entries are not wasted. I just haven;t come around to generating good test images to exercise the code. |



Summary
Add APNG (animated PNG) load/save support via libpng and a built-in color quantizer.
APNG support adds animated PNG to the existing libpng backend, gated behind
PNG_APNG_SUPPORTED(libpng 1.6+APNG patch or libpng 1.8+). The loader supportspage/nparameters matching the GIF/WebP loaders, full frame compositing (all dispose and blend ops, 8-bit and 16-bit), and sequential access. The saver writes APNG viasink_discwith palette mode support. Non-animated PNGs are unaffected — the only overhead is anacTLchunk check.Built-in quantizer provides baseline palette quantization when libimagequant/quantizr are not available. It implements Wu's optimal 4D RGBA algorithm with nearest-color Euclidean distance mapping and Floyd-Steinberg error diffusion with alpha-scaled error propagation. This enables palette PNG/APNG output in minimal builds without external quantization dependencies.
Both features are included as separate commits and could be split to two PRs if needed.