Skip to content

Add APNG R/W Support via libpng & Built-in Quantizer#4944

Open
felixbuenemann wants to merge 2 commits intolibvips:masterfrom
felixbuenemann:apng-support
Open

Add APNG R/W Support via libpng & Built-in Quantizer#4944
felixbuenemann wants to merge 2 commits intolibvips:masterfrom
felixbuenemann:apng-support

Conversation

@felixbuenemann
Copy link
Collaborator

@felixbuenemann felixbuenemann commented Mar 11, 2026

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 supports page/n parameters 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 via sink_disc with palette mode support. Non-animated PNGs are unaffected — the only overhead is an acTL chunk 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.

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
@felixbuenemann
Copy link
Collaborator Author

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.

@felixbuenemann
Copy link
Collaborator Author

The quantiser is actually quite good for its simplicity. It doesn't quite match libimageqaunt, but it is faster and only required ~14MB RAM, while libimagequant memory use depends on image size.

Here's a sample original 24-Bit PNG:
dice_original

Converted with libimagequant:
dice_liq

Converted with the new built-in quantiser:
dice_wu_alphafs

@jcupitt
Copy link
Member

jcupitt commented Mar 11, 2026

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.

@felixbuenemann
Copy link
Collaborator Author

@jcupitt That's fine. Do you prefer if I split the quantiser into a separate PR?

@jcupitt
Copy link
Member

jcupitt commented Mar 12, 2026

I'm ok with one PR, but maybe someone else will review. I'll defer to them.

@jcupitt
Copy link
Member

jcupitt commented Mar 12, 2026

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.

@felixbuenemann
Copy link
Collaborator Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants