snippet/uncompng: support APNG (Animated PNG)
diff --git a/example/convert-to-nia/convert-to-nia.c b/example/convert-to-nia/convert-to-nia.c
index 5e7a4e8..ef5abc7 100644
--- a/example/convert-to-nia/convert-to-nia.c
+++ b/example/convert-to-nia/convert-to-nia.c
@@ -255,6 +255,17 @@
   bool output_nia_or_crc32_digest;  // Implicitly set.
   bool output_nie;
   bool output_uncompressed_png;
+
+  // There is no "bool output_uncompressed_apng" option. At the file format
+  // level, writing an Animated PNG requires knowing the total number of frames
+  // very early in the output (to write in the acTL chunk). In contrast,
+  // convert-to-nia can convert from image formats (such as GIF) that do not
+  // have an explicit "number of frames" record, only an implicit one (like
+  // NIA), calculated by parsing every frame. But convert-to-nia also does not
+  // assume that its input or output is rewindable. It is designed to operate
+  // under a SECCOMP_MODE_STRICT sandbox in O(1) memory, where it only reads
+  // its input or write its output forward, as non-seekable streams.
+
 } g_flags = {0};
 
 const char*  //
diff --git a/lib/uncompng/uncompng.go b/lib/uncompng/uncompng.go
index fcb8003..87ac5a2 100644
--- a/lib/uncompng/uncompng.go
+++ b/lib/uncompng/uncompng.go
@@ -764,13 +764,12 @@
 		fType |= frameTypeBitLastFrame
 	}
 
-	ej := e.encodeFrameHeader(w, delayNumerator, delayDenominator)
+	ej := e.encodeFrameHeader(delayNumerator, delayDenominator)
 	return e.enc.encodeFramePayload(
 		w, fType, &e.seqNum, e.depth, e.colorType, int(e.width), int(e.height), pix, stride, ej)
 }
 
 func (e *AnimationEncoder) encodeFrameHeader(
-	w io.Writer,
 	delayNumerator uint16,
 	delayDenominator uint16) int {
 
diff --git a/script/convert-nia-to-apng.go b/script/convert-nia-to-apng.go
new file mode 100644
index 0000000..79adca3
--- /dev/null
+++ b/script/convert-nia-to-apng.go
@@ -0,0 +1,260 @@
+// Copyright 2026 The Wuffs Authors.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+//
+// SPDX-License-Identifier: Apache-2.0 OR MIT
+
+//go:build ignore
+// +build ignore
+
+package main
+
+// convert-nie-to-png.go decodes NIA from stdin and encodes (animated) PNG to
+// stdout.
+//
+// Usage: go run convert-nia-to-apng.go < foo.nia > foo.png
+
+import (
+	"bytes"
+	"errors"
+	"io"
+	"os"
+
+	"github.com/google/wuffs/lib/uncompng"
+)
+
+func main() {
+	if err := main1(); err != nil {
+		os.Stderr.WriteString(err.Error() + "\n")
+		os.Exit(1)
+	}
+}
+
+func main1() error {
+	// When writing APNG, we need to know (at the start of the file) the
+	// animation's number of frames, but when reading NIA, we only know that at
+	// the end of the file.
+	//
+	// We therefore use io.ReadAll, slurping the entire NIA data into memory
+	// instead of using the io.Reader streaming API directly, so that we can
+	// make two passes over the NIA data.
+	niaBytes, err := io.ReadAll(os.Stdin)
+	if err != nil {
+		return err
+	} else if len(niaBytes) < 24 {
+		return errors.New("input is not in the NIA format")
+	}
+
+	premultiplied := niaBytes[6] == 'p'
+	depth8 := niaBytes[7] == '8'
+	width := u32le(niaBytes[8:])
+	height := u32le(niaBytes[12:])
+	padded := !depth8 && ((width & 1) != 0) && ((height & 1) != 0)
+	if (u32le(niaBytes[:]) != 0x41AF_C36E) || // 0x41AF_C36E is 'nïA'le.
+		(niaBytes[4] != 0xFF) ||
+		(niaBytes[5] != 'b') ||
+		((niaBytes[6] != 'n') && (niaBytes[6] != 'p')) ||
+		((niaBytes[7] != '4') && (niaBytes[7] != '8')) ||
+		(width >= 0x8000_0000) ||
+		(height >= 0x8000_0000) {
+		return errors.New("input is not in the NIA format")
+	} else if (width > 0xFF_FFFF) || (height > 0xFF_FFFF) {
+		return errors.New("input is in an unsupported NIA format")
+	}
+
+	nieSize := calculateNIESize(depth8, width, height)
+	if nieSize < 0 {
+		return errors.New("input image dimensions are unsupported (too large)")
+	} else if nieSize < 16 {
+		panic("unreachable")
+	}
+
+	// NIA's depth8 is measured in *bytes* per *pixel*, but uncompng's depth is
+	// measured *bits* per *channel* (times four channels, for BGRA).
+	depth := uncompng.Depth8
+	if depth8 {
+		depth = uncompng.Depth16
+	}
+
+	expectedNieHeader := append([]byte(nil), niaBytes[:16]...)
+	expectedNieHeader[3] = 'E'
+
+	numFrames := uint64(0)
+	prevCDD := uint64(0)
+	lastCDD := uint64(0)
+	for buf := niaBytes[16:]; len(buf) >= 8; numFrames++ {
+		cdd := u64le(buf)
+		if (cdd >> 32) == 0x8000_0000 {
+			lastCDD = cdd
+			break
+		} else if ((cdd >> 32) > 0x8000_0000) || (cdd < prevCDD) {
+			return errors.New("bad CDD (Cumulative Display Duration)")
+		}
+		buf = buf[8:]
+		if int64(len(buf)) < nieSize {
+			return errors.New("bad NIE frame")
+		} else if !bytes.Equal(buf[:16], expectedNieHeader) {
+			return errors.New("bad NIE header")
+		}
+		buf = buf[nieSize:]
+
+		if padded {
+			if len(buf) < 4 {
+				return errors.New("bad NIE padding")
+			}
+			buf = buf[4:]
+		}
+		prevCDD = cdd
+	}
+
+	enc := uncompng.AnimationEncoder{}
+	if err := enc.EncodeHeader(
+		os.Stdout, depth, uncompng.ColorTypeNRGBA,
+		int(width), int(height), uint32(numFrames), uint32(lastCDD)); err != nil {
+		return err
+	}
+
+	prevCDD = 0
+	for buf := niaBytes[16:]; len(buf) >= 8; {
+		cdd := u64le(buf)
+		if (cdd >> 32) >= 0x8000_0000 {
+			break
+		}
+		buf = buf[8:]
+
+		if depth8 {
+			swapBlueRedEndian8(buf[16:nieSize])
+			if premultiplied {
+				unpremultiply8(buf[16:nieSize])
+			}
+		} else {
+			swapBlueRedEndian4(buf[16:nieSize])
+			if premultiplied {
+				unpremultiply4(buf[16:nieSize])
+			}
+		}
+
+		numer, denom := calculateDurationRatio(cdd - prevCDD)
+		if err := enc.EncodeFrame(
+			os.Stdout, buf[16:nieSize], 4*int(width), numer, denom); err != nil {
+			return err
+		}
+
+		buf = buf[nieSize:]
+
+		if padded {
+			buf = buf[4:]
+		}
+		prevCDD = cdd
+	}
+
+	return nil
+}
+
+// swapBlueRedEndian4 corrects for the fact that NIA uses little-endian BGRA
+// order but the *Go* lib/uncompng library (unlike the *C* snippet/uncompng.c)
+// uses big-endian RGBA order, the same as the Go standard library (and PNG).
+func swapBlueRedEndian4(buf []byte) {
+	for ; len(buf) >= 4; buf = buf[4:] {
+		buf[0], buf[2] = buf[2], buf[0]
+	}
+}
+
+func swapBlueRedEndian8(buf []byte) {
+	for ; len(buf) >= 8; buf = buf[8:] {
+		buf[0], buf[1], buf[4], buf[5] = buf[5], buf[4], buf[1], buf[0]
+		buf[2], buf[3], buf[6], buf[7] = buf[3], buf[2], buf[7], buf[6]
+	}
+}
+
+func unpremultiply4(buf []byte) {
+	for ; len(buf) >= 4; buf = buf[4:] {
+		r := 0x101 * uint32(buf[0])
+		g := 0x101 * uint32(buf[1])
+		b := 0x101 * uint32(buf[2])
+		a := 0x101 * uint32(buf[3])
+		if (a == 0xFFFF) || (a == 0x0000) {
+			continue
+		}
+		r = (r * 0xFFFF) / a
+		g = (g * 0xFFFF) / a
+		b = (b * 0xFFFF) / a
+		buf[0] = byte(r >> 8)
+		buf[1] = byte(g >> 8)
+		buf[2] = byte(b >> 8)
+	}
+}
+
+func unpremultiply8(buf []byte) {
+	for ; len(buf) >= 4; buf = buf[4:] {
+		r := (uint32(buf[0]) << 8) | uint32(buf[1])
+		g := (uint32(buf[2]) << 8) | uint32(buf[3])
+		b := (uint32(buf[4]) << 8) | uint32(buf[5])
+		a := (uint32(buf[6]) << 8) | uint32(buf[7])
+		if (a == 0xFFFF) || (a == 0x0000) {
+			continue
+		}
+		r = (r * 0xFFFF) / a
+		g = (g * 0xFFFF) / a
+		b = (b * 0xFFFF) / a
+		buf[0] = byte(r >> 8)
+		buf[1] = byte(r >> 0)
+		buf[2] = byte(g >> 8)
+		buf[3] = byte(g >> 0)
+		buf[4] = byte(b >> 8)
+		buf[5] = byte(b >> 0)
+	}
+}
+
+// calculateDurationRatio converts from a time duration measured in flicks
+// (frame-ticks, 1 / 705600000 of a second, to APNG's representation, which is
+// a ratio of two uint16 values.
+//
+// TODO: be smarter about how to express a durationInFlicks that isn't an
+// integer number of milliseconds (or is over 65.535 seconds). For now, just
+// hard-code the denominator to 1000, meaning milliseconds, and round to
+// nearest (capped at 65535) to get the numerator.
+func calculateDurationRatio(durationInFlicks uint64) (numerator uint16, denominator uint16) {
+	const flicksPerMillisecond = 705600
+	millis := (durationInFlicks + (flicksPerMillisecond / 2)) / flicksPerMillisecond
+	return uint16(min(0xFFFF, millis)), 1000
+}
+
+func calculateNIESize(depth8 bool, width uint32, height uint32) int64 {
+	const maxInt64 = (1 << 63) - 1
+	const maxUint64 = (1 << 64) - 1
+
+	n := uint64(width) * uint64(height) * 4
+	if depth8 {
+		if n > (maxUint64 / 2) {
+			return -1
+		}
+		n *= 2
+	}
+	if n > (maxInt64 - 16) {
+		return -1
+	}
+	return int64(n + 16)
+}
+
+func u32le(b []byte) uint32 {
+	return (uint32(b[0]) << 0) |
+		(uint32(b[1]) << 8) |
+		(uint32(b[2]) << 16) |
+		(uint32(b[3]) << 24)
+}
+
+func u64le(b []byte) uint64 {
+	return (uint64(b[0]) << 0) |
+		(uint64(b[1]) << 8) |
+		(uint64(b[2]) << 16) |
+		(uint64(b[3]) << 24) |
+		(uint64(b[4]) << 32) |
+		(uint64(b[5]) << 40) |
+		(uint64(b[6]) << 48) |
+		(uint64(b[7]) << 56)
+}
diff --git a/script/example-uncompng-apng-large.c b/script/example-uncompng-apng-large.c
new file mode 100644
index 0000000..8a5b613
--- /dev/null
+++ b/script/example-uncompng-apng-large.c
@@ -0,0 +1,193 @@
+// Copyright 2026 The Wuffs Authors.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+//
+// SPDX-License-Identifier: Apache-2.0 OR MIT
+
+// ----------------
+
+// This exercises snippet/uncompng.c to write a large APNG image to stdout.
+//
+// It is a C port of lib/uncompng's TestAnimationEncoderLarge.
+//
+// Run it from the Wuffs root directory, the one that contains the
+// wuffs-root-directory.txt file, so that it can find and read the
+// test/data/hibiscus.regular.bmp file.
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#define UNCOMPNG_CONFIG__STATIC_FUNCTIONS
+#define UNCOMPNG_IMPLEMENTATION
+#include "../snippet/uncompng.c"
+
+#define WUFFS_CONFIG__MODULES
+#define WUFFS_CONFIG__MODULE__BASE
+#define WUFFS_CONFIG__MODULE__BMP
+#define WUFFS_CONFIG__STATIC_FUNCTIONS
+#define WUFFS_IMPLEMENTATION
+#include "../release/c/wuffs-unsupported-snapshot.c"
+
+#define IMAGE_HEIGHT 442
+#define IMAGE_WIDTH 312
+#define NUM_FRAMES 5
+#define NUM_PLAYS 0
+#define SRC_BMP_SIZE 413850
+
+static uint8_t g_src_bmp[SRC_BMP_SIZE];
+static uint8_t g_src_pixels[IMAGE_WIDTH * IMAGE_HEIGHT * 4];
+
+bool  //
+load_src_bmp() {
+  int fd = open("test/data/hibiscus.regular.bmp", O_RDONLY, 0);
+  if (fd == -1) {
+    fprintf(stderr, "FAIL: open: %s\n", strerror(errno));
+    return false;
+  }
+
+  for (ssize_t num_read = 0; num_read < SRC_BMP_SIZE;) {
+    ssize_t n = read(fd, g_src_bmp + num_read, sizeof(g_src_bmp) - num_read);
+    if (n > 0) {
+      num_read += n;
+    } else if (n == 0) {
+      break;
+    } else if (errno == EINTR) {
+      // No-op.
+    } else {
+      close(fd);
+      fprintf(stderr, "FAIL: read: %s\n", strerror(errno));
+      return false;
+    }
+  }
+
+  close(fd);
+  return true;
+}
+
+bool  //
+load_src_pixels() {
+  static wuffs_bmp__decoder dec;
+
+  wuffs_base__io_buffer src = wuffs_base__make_io_buffer(
+      wuffs_base__make_slice_u8(g_src_bmp, SRC_BMP_SIZE),
+      wuffs_base__make_io_buffer_meta(SRC_BMP_SIZE, 0, 0, true));
+
+  wuffs_base__status status = wuffs_bmp__decoder__initialize(
+      &dec, sizeof dec, WUFFS_VERSION, WUFFS_INITIALIZE__DEFAULT_OPTIONS);
+  if (!wuffs_base__status__is_ok(&status)) {
+    fprintf(stderr, "FAIL: initialize: %s\n",
+            wuffs_base__status__message(&status));
+    return false;
+  }
+
+  wuffs_base__image_config ic;
+  status = wuffs_bmp__decoder__decode_image_config(&dec, &ic, &src);
+  if (!wuffs_base__status__is_ok(&status)) {
+    fprintf(stderr, "FAIL: decode_image_config: %s\n",
+            wuffs_base__status__message(&status));
+    return false;
+  }
+
+  uint32_t w = wuffs_base__pixel_config__width(&ic.pixcfg);
+  uint32_t h = wuffs_base__pixel_config__height(&ic.pixcfg);
+  uint32_t pixfmt = wuffs_base__pixel_config__pixel_format(&ic.pixcfg).repr;
+  if ((w != IMAGE_WIDTH) || (h != IMAGE_HEIGHT) ||
+      (pixfmt != WUFFS_BASE__PIXEL_FORMAT__BGRX)) {
+    fprintf(stderr, "FAIL: decode_image_config: unexpected configuration\n");
+    return false;
+  }
+
+  if ((WUFFS_BASE__PIXEL_FORMAT__BGRX != UNCOMPNG__PIXEL_FORMAT__BGRX) ||
+      (WUFFS_BASE__PIXEL_FORMAT__BGRA_NONPREMUL !=
+       UNCOMPNG__PIXEL_FORMAT__BGRA_NONPREMUL)) {
+    fprintf(stderr, "FAIL: wuffs and uncompng are incompatible\n");
+    return false;
+  }
+
+  wuffs_base__pixel_config__set(&ic.pixcfg,
+                                WUFFS_BASE__PIXEL_FORMAT__BGRA_NONPREMUL,
+                                WUFFS_BASE__PIXEL_SUBSAMPLING__NONE, w, h);
+
+  wuffs_base__pixel_buffer pb;
+  status = wuffs_base__pixel_buffer__set_from_slice(
+      &pb, &ic.pixcfg,
+      wuffs_base__make_slice_u8(&g_src_pixels[0], sizeof g_src_pixels));
+  if (!wuffs_base__status__is_ok(&status)) {
+    fprintf(stderr, "FAIL: set_from_slice: %s\n",
+            wuffs_base__status__message(&status));
+    return false;
+  }
+
+  status = wuffs_bmp__decoder__decode_frame(&dec, &pb, &src,
+                                            WUFFS_BASE__PIXEL_BLEND__SRC,
+                                            wuffs_base__empty_slice_u8(), NULL);
+  if (!wuffs_base__status__is_ok(&status)) {
+    fprintf(stderr, "FAIL: decode_frame: %s\n",
+            wuffs_base__status__message(&status));
+    return false;
+  }
+
+  return true;
+}
+
+int  //
+my_write_func(void* context, const uint8_t* data_ptr, size_t data_len) {
+  static const int stdout_fd = 1;
+  return (write(stdout_fd, data_ptr, data_len) < 0) ? -errno : 0;
+}
+
+int  //
+main(int argc, char** argv) {
+  if (!load_src_bmp() || !load_src_pixels()) {
+    return 1;
+  }
+
+  // Change "if (0)" to "if (1)" to write a still (not animated) PNG.
+  if (0) {
+    const int frame = 0;
+    return uncompng__encode(                                                //
+        &my_write_func, NULL,                                               //
+        UNCOMPNG__PIXEL_FORMAT__BGRA_NONPREMUL, IMAGE_WIDTH, IMAGE_HEIGHT,  //
+        &g_src_pixels[0], sizeof(g_src_pixels), IMAGE_WIDTH * 4);
+  }
+
+  static uint16_t delay_millis[NUM_FRAMES] = {300, 500, 600, 400, 200};
+
+  int err0 = uncompng__encode_apng_header(                                //
+      &my_write_func, NULL,                                               //
+      UNCOMPNG__PIXEL_FORMAT__BGRA_NONPREMUL, IMAGE_WIDTH, IMAGE_HEIGHT,  //
+      NUM_FRAMES, NUM_PLAYS);
+  if (err0) {
+    return err0;
+  }
+
+  for (int frame = 0; frame < NUM_FRAMES; frame++) {
+    int err1 = uncompng__encode_apng_frame(  //
+        &my_write_func, NULL,                //
+        delay_millis[frame], 1000,           //
+        &g_src_pixels[0], sizeof(g_src_pixels), IMAGE_WIDTH * 4);
+    if (err1) {
+      return err1;
+    }
+
+    // Over time, set the R, G, B, A channel values to 0x80.
+    //
+    // Use RGBA order, not BGRA, to match the output of lib/uncompng's
+    // TestAnimationEncoderLarge, in Go.
+    if (frame < 4) {
+      static size_t order[4] = {2, 1, 0, 3};
+      for (size_t i = order[frame]; i < sizeof(g_src_pixels); i += 4) {
+        g_src_pixels[i] = 0x80;
+      }
+    }
+  }
+
+  return 0;
+}
diff --git a/script/example-uncompng-apng-small.c b/script/example-uncompng-apng-small.c
new file mode 100644
index 0000000..12913b7
--- /dev/null
+++ b/script/example-uncompng-apng-small.c
@@ -0,0 +1,87 @@
+// Copyright 2026 The Wuffs Authors.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+//
+// SPDX-License-Identifier: Apache-2.0 OR MIT
+
+// ----------------
+
+// This exercises snippet/uncompng.c to write a small APNG image to stdout.
+//
+// It is a C port of lib/uncompng's TestAnimationEncoderSmall.
+
+#include <errno.h>
+#include <unistd.h>
+
+#define UNCOMPNG_CONFIG__STATIC_FUNCTIONS
+#define UNCOMPNG_IMPLEMENTATION
+#include "../snippet/uncompng.c"
+
+#define IMAGE_HEIGHT 2
+#define IMAGE_WIDTH 3
+#define NUM_FRAMES 2
+#define NUM_PLAYS 10
+
+int  //
+my_write_func(void* context, const uint8_t* data_ptr, size_t data_len) {
+  static const int stdout_fd = 1;
+  return (write(stdout_fd, data_ptr, data_len) < 0) ? -errno : 0;
+}
+
+int  //
+main(int argc, char** argv) {
+  static uint8_t pixels[NUM_FRAMES][IMAGE_WIDTH * IMAGE_HEIGHT * 4] = {
+      {
+          0xFF, 0x00, 0x00, 0xFF,  // Blue.
+          0xFF, 0xFF, 0xFF, 0xFF,  // White.
+          0x00, 0x00, 0xFF, 0xFF,  // Red.
+
+          0xFF, 0x00, 0x00, 0xFF,  // Blue.
+          0xFF, 0xFF, 0xFF, 0xFF,  // White.
+          0x00, 0x00, 0xFF, 0xFF,  // Red.
+      },
+      {
+          0x00, 0xFF, 0x00, 0xFF,  // Green.
+          0xFF, 0xFF, 0xFF, 0xFF,  // White.
+          0x00, 0x00, 0xFF, 0xFF,  // Red.
+
+          0x00, 0xFF, 0x00, 0xFF,  // Green.
+          0xFF, 0xFF, 0xFF, 0xFF,  // White.
+          0x00, 0x00, 0xFF, 0xFF,  // Red.
+      }};
+
+  // Change "if (0)" to "if (1)" to write a still (not animated) PNG.
+  if (0) {
+    const int frame = 0;
+    return uncompng__encode(                                      //
+        &my_write_func, NULL,                                     //
+        UNCOMPNG__PIXEL_FORMAT__BGRX, IMAGE_WIDTH, IMAGE_HEIGHT,  //
+        &pixels[frame][0], sizeof(pixels[frame]), IMAGE_WIDTH * 4);
+  }
+
+  static uint16_t delay_millis[NUM_FRAMES] = {1000, 2000};
+
+  int err0 = uncompng__encode_apng_header(                      //
+      &my_write_func, NULL,                                     //
+      UNCOMPNG__PIXEL_FORMAT__BGRX, IMAGE_WIDTH, IMAGE_HEIGHT,  //
+      NUM_FRAMES, NUM_PLAYS);
+  if (err0) {
+    return err0;
+  }
+
+  for (int frame = 0; frame < NUM_FRAMES; frame++) {
+    int err1 = uncompng__encode_apng_frame(  //
+        &my_write_func, NULL,                //
+        delay_millis[frame], 1000,           //
+        &pixels[frame][0], sizeof(pixels[frame]), IMAGE_WIDTH * 4);
+    if (err1) {
+      return err1;
+    }
+  }
+
+  return 0;
+}
diff --git a/script/example-uncompng-png-julia.c b/script/example-uncompng-png-julia.c
new file mode 100644
index 0000000..3e7cb4c
--- /dev/null
+++ b/script/example-uncompng-png-julia.c
@@ -0,0 +1,67 @@
+// Copyright 2026 The Wuffs Authors.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+//
+// SPDX-License-Identifier: Apache-2.0 OR MIT
+
+// ----------------
+
+// This exercises snippet/uncompng.c to write a Julia Set image to stdout.
+//
+// See also
+// https://nigeltao.github.io/blog/2025/uncompressed-png.html
+
+#include <errno.h>
+#include <unistd.h>
+
+#define UNCOMPNG_CONFIG__STATIC_FUNCTIONS
+#define UNCOMPNG_IMPLEMENTATION
+#include "../snippet/uncompng.c"
+
+#define CONFIG_CX -0.70000f
+#define CONFIG_CY +0.27015f
+#define CONFIG_IMAGE_WIDTH 256
+#define CONFIG_IMAGE_HEIGHT 256
+
+int  //
+my_write_func(void* context, const uint8_t* data_ptr, size_t data_len) {
+  static const int stdout_fd = 1;
+  return (write(stdout_fd, data_ptr, data_len) < 0) ? -errno : 0;
+}
+
+uint8_t  //
+my_pixel_func(float fx, float fy) {
+  static const float escape_distance_squared = 100.0f;
+  for (int i = 255; i > 0; i--) {
+    float distance_squared = (fx * fx) + (fy * fy);
+    if (distance_squared >= escape_distance_squared) {
+      return i;
+    }
+    float gx = CONFIG_CX + (fx * fx) - (fy * fy);
+    float gy = CONFIG_CY + (2 * fx * fy);
+    fx = gx;
+    fy = gy;
+  }
+  return 0;
+}
+
+int  //
+main(int argc, char** argv) {
+  static const float w2 = CONFIG_IMAGE_WIDTH / 2;
+  static const float h2 = CONFIG_IMAGE_HEIGHT / 2;
+  static uint8_t pixels[CONFIG_IMAGE_HEIGHT][CONFIG_IMAGE_WIDTH];
+  for (int iy = 0; iy < CONFIG_IMAGE_HEIGHT; iy++) {
+    float fy = (iy - h2) / h2;
+    for (int ix = 0; ix < CONFIG_IMAGE_WIDTH; ix++) {
+      float fx = (ix - w2) / w2;
+      pixels[iy][ix] = my_pixel_func(fx, fy);
+    }
+  }
+  return uncompng__encode(&my_write_func, NULL, UNCOMPNG__PIXEL_FORMAT__Y,
+                          CONFIG_IMAGE_WIDTH, CONFIG_IMAGE_HEIGHT,
+                          &pixels[0][0], sizeof(pixels), CONFIG_IMAGE_WIDTH);
+}
diff --git a/snippet/uncompng.c b/snippet/uncompng.c
index 2b62c5f..4406675 100644
--- a/snippet/uncompng.c
+++ b/snippet/uncompng.c
@@ -62,8 +62,9 @@
 // also return its own negative error codes, which are passed on.
 #define UNCOMPNG__RESULT__OK 0
 #define UNCOMPNG__RESULT__INVALID_ARGUMENT 1
-#define UNCOMPNG__RESULT__UNSUPPORTED_IMAGE_SIZE 2
-#define UNCOMPNG__RESULT__CONCURRENT_CALL 3
+#define UNCOMPNG__RESULT__INVALID_CALL_SEQUENCE 2
+#define UNCOMPNG__RESULT__UNSUPPORTED_IMAGE_SIZE 3
+#define UNCOMPNG__RESULT__CONCURRENT_CALL 4
 
 // UNCOMPNG__DATA_LEN__INCL_MAX is the inclusive maximum value of write_func's
 // data_len argument. In hexadecimal, it equals 0x10000u.
@@ -101,6 +102,28 @@
                  size_t pixel_len,
                  size_t stride);
 
+UNCOMPNG__MAYBE_STATIC int  //
+uncompng__encode_apng_header(int (*write_func)(void* context,
+                                               const uint8_t* data_ptr,
+                                               size_t data_len),
+                             void* context,
+                             uint32_t pixel_format,
+                             uint32_t width,
+                             uint32_t height,
+                             uint32_t num_frames,
+                             uint32_t num_plays);
+
+UNCOMPNG__MAYBE_STATIC int  //
+uncompng__encode_apng_frame(int (*write_func)(void* context,
+                                              const uint8_t* data_ptr,
+                                              size_t data_len),
+                            void* context,
+                            uint16_t delay_numerator,
+                            uint16_t delay_denominator,
+                            const uint8_t* pixel_ptr,
+                            size_t pixel_len,
+                            size_t stride);
+
 // --------
 
 #ifdef UNCOMPNG_IMPLEMENTATION
@@ -171,10 +194,109 @@
 
 static uint8_t uncompng__private_impl_buffer[65536];
 
-static void  //
-uncompng__private_impl_initialize_buffer(uint32_t width,
-                                         uint32_t height,
-                                         uint32_t pixel_format) {
+static struct uncompng__private_impl_apng_encoder_state_struct {
+  uint32_t pixel_format;
+  uint32_t width;
+  uint32_t height;
+  uint32_t seq_num;
+  uint32_t num_frames;
+  uint32_t cur_frame;
+} uncompng__private_impl_apng_encoder_state;
+
+static int  //
+uncompng__private_impl_encode_frame_header(uint32_t delay_numerator,
+                                           uint32_t delay_denominator) {
+  uncompng__private_impl_buffer[0x0000] = 0;
+  uncompng__private_impl_buffer[0x0001] = 0;
+  uncompng__private_impl_buffer[0x0002] = 0;
+  uncompng__private_impl_buffer[0x0003] = 0x1A;
+  uncompng__private_impl_buffer[0x0004] = 'f';
+  uncompng__private_impl_buffer[0x0005] = 'c';
+  uncompng__private_impl_buffer[0x0006] = 'T';
+  uncompng__private_impl_buffer[0x0007] = 'L';
+  uncompng__private_impl_buffer[0x0008] =
+      (uint8_t)(uncompng__private_impl_apng_encoder_state.seq_num >> 24);
+  uncompng__private_impl_buffer[0x0009] =
+      (uint8_t)(uncompng__private_impl_apng_encoder_state.seq_num >> 16);
+  uncompng__private_impl_buffer[0x000A] =
+      (uint8_t)(uncompng__private_impl_apng_encoder_state.seq_num >> 8);
+  uncompng__private_impl_buffer[0x000B] =
+      (uint8_t)(uncompng__private_impl_apng_encoder_state.seq_num >> 0);
+  uncompng__private_impl_buffer[0x000C] =
+      (uint8_t)(uncompng__private_impl_apng_encoder_state.width >> 24);
+  uncompng__private_impl_buffer[0x000D] =
+      (uint8_t)(uncompng__private_impl_apng_encoder_state.width >> 16);
+  uncompng__private_impl_buffer[0x000E] =
+      (uint8_t)(uncompng__private_impl_apng_encoder_state.width >> 8);
+  uncompng__private_impl_buffer[0x000F] =
+      (uint8_t)(uncompng__private_impl_apng_encoder_state.width >> 0);
+  uncompng__private_impl_buffer[0x0010] =
+      (uint8_t)(uncompng__private_impl_apng_encoder_state.height >> 24);
+  uncompng__private_impl_buffer[0x0011] =
+      (uint8_t)(uncompng__private_impl_apng_encoder_state.height >> 16);
+  uncompng__private_impl_buffer[0x0012] =
+      (uint8_t)(uncompng__private_impl_apng_encoder_state.height >> 8);
+  uncompng__private_impl_buffer[0x0013] =
+      (uint8_t)(uncompng__private_impl_apng_encoder_state.height >> 0);
+  uncompng__private_impl_buffer[0x0014] = 0;
+  uncompng__private_impl_buffer[0x0015] = 0;
+  uncompng__private_impl_buffer[0x0016] = 0;
+  uncompng__private_impl_buffer[0x0017] = 0;
+  uncompng__private_impl_buffer[0x0018] = 0;
+  uncompng__private_impl_buffer[0x0019] = 0;
+  uncompng__private_impl_buffer[0x001A] = 0;
+  uncompng__private_impl_buffer[0x001B] = 0;
+  uncompng__private_impl_buffer[0x001C] = (uint8_t)(delay_numerator >> 8);
+  uncompng__private_impl_buffer[0x001D] = (uint8_t)(delay_numerator >> 0);
+  uncompng__private_impl_buffer[0x001E] = (uint8_t)(delay_denominator >> 8);
+  uncompng__private_impl_buffer[0x001F] = (uint8_t)(delay_denominator >> 0);
+  uncompng__private_impl_buffer[0x0020] = 0;
+  uncompng__private_impl_buffer[0x0021] = 0;
+  uint32_t fctl_crc32 = uncompng__private_impl_crc32_ieee(
+      uncompng__private_impl_buffer + 0x0004, 0x0022 - 0x0004);
+  uncompng__private_impl_buffer[0x0022] = (uint8_t)(fctl_crc32 >> 24);
+  uncompng__private_impl_buffer[0x0023] = (uint8_t)(fctl_crc32 >> 16);
+  uncompng__private_impl_buffer[0x0024] = (uint8_t)(fctl_crc32 >> 8);
+  uncompng__private_impl_buffer[0x0025] = (uint8_t)(fctl_crc32 >> 0);
+
+  int use_idat = uncompng__private_impl_apng_encoder_state.seq_num ? 0 : 1;
+  uncompng__private_impl_apng_encoder_state.seq_num++;
+
+  uncompng__private_impl_buffer[0x0026] = 0;
+  uncompng__private_impl_buffer[0x0027] = 0;
+  uncompng__private_impl_buffer[0x0028] = 0;
+  uncompng__private_impl_buffer[0x0029] = 0;
+  uncompng__private_impl_buffer[0x002A] = "fI"[use_idat];
+  uncompng__private_impl_buffer[0x002B] = "dD"[use_idat];
+  uncompng__private_impl_buffer[0x002C] = 'A';
+  uncompng__private_impl_buffer[0x002D] = 'T';
+  int ej = 0x002E;
+
+  if (!use_idat) {
+    uint32_t seq_num = uncompng__private_impl_apng_encoder_state.seq_num++;
+    uncompng__private_impl_buffer[0x002E] = (uint8_t)(seq_num >> 24);
+    uncompng__private_impl_buffer[0x002F] = (uint8_t)(seq_num >> 16);
+    uncompng__private_impl_buffer[0x0030] = (uint8_t)(seq_num >> 8);
+    uncompng__private_impl_buffer[0x0031] = (uint8_t)(seq_num >> 0);
+    ej = 0x0032;
+  }
+
+  uncompng__private_impl_buffer[ej + 0x00] = 0x78;
+  uncompng__private_impl_buffer[ej + 0x01] = 0x01;
+  uncompng__private_impl_buffer[ej + 0x02] = 0;
+  uncompng__private_impl_buffer[ej + 0x03] = 0;
+  uncompng__private_impl_buffer[ej + 0x04] = 0;
+  uncompng__private_impl_buffer[ej + 0x05] = 0;
+  uncompng__private_impl_buffer[ej + 0x06] = 0;
+  return ej + 0x07;
+}
+
+static int  //
+uncompng__private_impl_encode_image_header(uint32_t pixel_format,
+                                           uint32_t width,
+                                           uint32_t height,
+                                           uint32_t num_frames,
+                                           uint32_t num_plays) {
   uncompng__private_impl_buffer[0x0000] = 0x89;
   uncompng__private_impl_buffer[0x0001] = 'P';
   uncompng__private_impl_buffer[0x0002] = 'N';
@@ -230,7 +352,7 @@
       color_type = 2;
       break;
     default:
-      return;
+      return 0;
   }
   uncompng__private_impl_buffer[0x0018] = depth;
   uncompng__private_impl_buffer[0x0019] = color_type;
@@ -245,6 +367,32 @@
   uncompng__private_impl_buffer[0x001F] = (uint8_t)(ihdr_crc32 >> 8);
   uncompng__private_impl_buffer[0x0020] = (uint8_t)(ihdr_crc32 >> 0);
 
+  if (num_frames > 0) {
+    uncompng__private_impl_buffer[0x0021] = 0;
+    uncompng__private_impl_buffer[0x0022] = 0;
+    uncompng__private_impl_buffer[0x0023] = 0;
+    uncompng__private_impl_buffer[0x0024] = 0x08;
+    uncompng__private_impl_buffer[0x0025] = 'a';
+    uncompng__private_impl_buffer[0x0026] = 'c';
+    uncompng__private_impl_buffer[0x0027] = 'T';
+    uncompng__private_impl_buffer[0x0028] = 'L';
+    uncompng__private_impl_buffer[0x0029] = (uint8_t)(num_frames >> 24);
+    uncompng__private_impl_buffer[0x002A] = (uint8_t)(num_frames >> 16);
+    uncompng__private_impl_buffer[0x002B] = (uint8_t)(num_frames >> 8);
+    uncompng__private_impl_buffer[0x002C] = (uint8_t)(num_frames >> 0);
+    uncompng__private_impl_buffer[0x002D] = (uint8_t)(num_plays >> 24);
+    uncompng__private_impl_buffer[0x002E] = (uint8_t)(num_plays >> 16);
+    uncompng__private_impl_buffer[0x002F] = (uint8_t)(num_plays >> 8);
+    uncompng__private_impl_buffer[0x0030] = (uint8_t)(num_plays >> 0);
+    uint32_t actl_crc32 = uncompng__private_impl_crc32_ieee(
+        uncompng__private_impl_buffer + 0x0025, 0x0031 - 0x0025);
+    uncompng__private_impl_buffer[0x0031] = (uint8_t)(actl_crc32 >> 24);
+    uncompng__private_impl_buffer[0x0032] = (uint8_t)(actl_crc32 >> 16);
+    uncompng__private_impl_buffer[0x0033] = (uint8_t)(actl_crc32 >> 8);
+    uncompng__private_impl_buffer[0x0034] = (uint8_t)(actl_crc32 >> 0);
+    return 0x0035;
+  }
+
   uncompng__private_impl_buffer[0x0021] = 0;
   uncompng__private_impl_buffer[0x0022] = 0;
   uncompng__private_impl_buffer[0x0023] = 0;
@@ -260,10 +408,7 @@
   uncompng__private_impl_buffer[0x002D] = 0;
   uncompng__private_impl_buffer[0x002E] = 0;
   uncompng__private_impl_buffer[0x002F] = 0;
-  uncompng__private_impl_buffer[0xFFFC] = 0;
-  uncompng__private_impl_buffer[0xFFFD] = 0;
-  uncompng__private_impl_buffer[0xFFFE] = 0;
-  uncompng__private_impl_buffer[0xFFFF] = 1;
+  return 0x0030;
 }
 
 static void  //
@@ -295,39 +440,41 @@
                                                const uint8_t* data_ptr,
                                                size_t data_len),
                              void* context,
-                             int ej,
-                             bool final) {
-  static const int ei_first = 0x0030;
-  static const int ei_later = 0x000D;
-
-  int crc32_start = 0;
-  int ei = 0;
-  if (uncompng__private_impl_buffer[0x0004] == 0x0D) {
-    uint32_t idat_chunk_len = ej - 0x0029;
-    if (final) {
-      idat_chunk_len += 4;
-    }
-    uncompng__private_impl_buffer[0x0021] = (uint8_t)(idat_chunk_len >> 24);
-    uncompng__private_impl_buffer[0x0022] = (uint8_t)(idat_chunk_len >> 16);
-    uncompng__private_impl_buffer[0x0023] = (uint8_t)(idat_chunk_len >> 8);
-    uncompng__private_impl_buffer[0x0024] = (uint8_t)(idat_chunk_len >> 0);
-    crc32_start = 0x0025;
-    ei = ei_first;
-  } else {
-    uint32_t idat_chunk_len = ej - 0x0008;
-    if (final) {
-      idat_chunk_len += 4;
-    }
-    uncompng__private_impl_buffer[0x0000] = (uint8_t)(idat_chunk_len >> 24);
-    uncompng__private_impl_buffer[0x0001] = (uint8_t)(idat_chunk_len >> 16);
-    uncompng__private_impl_buffer[0x0002] = (uint8_t)(idat_chunk_len >> 8);
-    uncompng__private_impl_buffer[0x0003] = (uint8_t)(idat_chunk_len >> 0);
-    crc32_start = 0x0004;
-    ei = ei_later;
+                             uint32_t f_type,
+                             int* ptr_to_ej,
+                             bool final_idat_of_frame) {
+  int idat_chunk_start = 0;
+  if (uncompng__private_impl_buffer[0x0007] == 0x0A) {
+    idat_chunk_start = 0x0021;
+  } else if (uncompng__private_impl_buffer[0x0007] == 0x4C) {
+    idat_chunk_start = 0x0026;
   }
 
+  int ei = idat_chunk_start + 13;
+  if (uncompng__private_impl_buffer[0x0007] != 0x54) {
+    ei += 2;
+  }
+  if ((f_type & 1) == 0) {
+    ei += 4;
+  }
+
+  int ej = *ptr_to_ej;
+  int idat_chunk_len = ej - (idat_chunk_start + 8);
+  if (final_idat_of_frame) {
+    idat_chunk_len += 4;
+  }
+  uncompng__private_impl_buffer[idat_chunk_start + 0] =
+      (uint8_t)(idat_chunk_len >> 24);
+  uncompng__private_impl_buffer[idat_chunk_start + 1] =
+      (uint8_t)(idat_chunk_len >> 16);
+  uncompng__private_impl_buffer[idat_chunk_start + 2] =
+      (uint8_t)(idat_chunk_len >> 8);
+  uncompng__private_impl_buffer[idat_chunk_start + 3] =
+      (uint8_t)(idat_chunk_len >> 0);
+
   uint32_t deflate_block_len = (uint32_t)(ej - ei);
-  uncompng__private_impl_buffer[ei - 5] = final ? 1 : 0;
+
+  uncompng__private_impl_buffer[ei - 5] = final_idat_of_frame ? 1 : 0;
   uncompng__private_impl_buffer[ei - 4] =
       0x00u ^ (uint8_t)(deflate_block_len >> 0);
   uncompng__private_impl_buffer[ei - 3] =
@@ -338,7 +485,7 @@
       0xFFu ^ (uint8_t)(deflate_block_len >> 8);
 
   uncompng__private_impl_update_adler32(ei, ej);
-  if (final) {
+  if (final_idat_of_frame) {
     uncompng__private_impl_buffer[ej + 0] =
         uncompng__private_impl_buffer[0xFFFC];
     uncompng__private_impl_buffer[ej + 1] =
@@ -350,6 +497,7 @@
     ej += 4;
   }
 
+  int crc32_start = idat_chunk_start + 4;
   uint32_t idat_crc32 = uncompng__private_impl_crc32_ieee(
       uncompng__private_impl_buffer + crc32_start, ej - crc32_start);
   uncompng__private_impl_buffer[ej + 0] = (uint8_t)(idat_crc32 >> 24);
@@ -358,19 +506,35 @@
   uncompng__private_impl_buffer[ej + 3] = (uint8_t)(idat_crc32 >> 0);
   ej += 4;
 
-  if (!final) {
+  if (!final_idat_of_frame) {
     int err0 =
         (*write_func)(context, uncompng__private_impl_buffer, (size_t)ej);
     if (err0 != 0) {
       return err0;
     }
-    uncompng__private_impl_buffer[0x0004] = 'I';
-    uncompng__private_impl_buffer[0x0005] = 'D';
+    int t = f_type & 1;
+    uncompng__private_impl_buffer[0x0004] = "fI"[t];
+    uncompng__private_impl_buffer[0x0005] = "dD"[t];
     uncompng__private_impl_buffer[0x0006] = 'A';
     uncompng__private_impl_buffer[0x0007] = 'T';
+    if (t) {
+      *ptr_to_ej = 0x000D + 0;
+      return 0;
+    }
+
+    uint32_t seq_num = uncompng__private_impl_apng_encoder_state.seq_num++;
+    uncompng__private_impl_buffer[0x0008] = (uint8_t)(seq_num >> 24);
+    uncompng__private_impl_buffer[0x0009] = (uint8_t)(seq_num >> 16);
+    uncompng__private_impl_buffer[0x000A] = (uint8_t)(seq_num >> 8);
+    uncompng__private_impl_buffer[0x000B] = (uint8_t)(seq_num >> 0);
+    *ptr_to_ej = 0x000D + 4;
     return 0;
   }
 
+  if ((f_type & 2) == 0) {
+    return (*write_func)(context, uncompng__private_impl_buffer, (size_t)ej);
+  }
+
   static const uint8_t iend_chunk[] = {
       0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
   };
@@ -398,31 +562,31 @@
 }
 
 static int  //
-uncompng__private_impl_do_encode(int (*write_func)(void* context,
-                                                   const uint8_t* data_ptr,
-                                                   size_t data_len),
-                                 void* context,
-                                 const uint8_t* pixel_ptr,
-                                 size_t pixel_len,
-                                 uint32_t width,
-                                 uint32_t height,
-                                 size_t stride,
-                                 uint32_t pixel_format) {
-  static const int ei_first = 0x0030;
-  static const int ei_later = 0x000D;
+uncompng__private_impl_encode_frame_payload(
+    int (*write_func)(void* context, const uint8_t* data_ptr, size_t data_len),
+    void* context,
+    uint32_t f_type,
+    uint32_t pixel_format,
+    uint32_t width,
+    uint32_t height,
+    const uint8_t* pixel_ptr,
+    size_t pixel_len,
+    size_t stride,
+    int ej) {
+  uncompng__private_impl_buffer[0xFFFC] = 0;
+  uncompng__private_impl_buffer[0xFFFD] = 0;
+  uncompng__private_impl_buffer[0xFFFE] = 0;
+  uncompng__private_impl_buffer[0xFFFF] = 1;
+
   static const int ej_max = 0xFFF8;
 
-  uncompng__private_impl_initialize_buffer(width, height, pixel_format);
-
-  int ej = ei_first;
-
   for (uint32_t y = 0; y < height; y++) {
     if ((ej + 1) > ej_max) {
-      int err = uncompng__private_impl_flush(write_func, context, ej, false);
+      int err =
+          uncompng__private_impl_flush(write_func, context, f_type, &ej, false);
       if (err != 0) {
         return err;
       }
-      ej = ei_later;
     }
     uncompng__private_impl_buffer[ej++] = 0;
 
@@ -432,12 +596,11 @@
       case UNCOMPNG__PIXEL_FORMAT__Y:
         for (uint32_t x = 0; x < width; x++) {
           if ((ej + 1) > ej_max) {
-            int err =
-                uncompng__private_impl_flush(write_func, context, ej, false);
+            int err = uncompng__private_impl_flush(write_func, context, f_type,
+                                                   &ej, false);
             if (err != 0) {
               return err;
             }
-            ej = ei_later;
           }
           uncompng__private_impl_buffer[ej++] = row[0];
           row += 1;
@@ -447,12 +610,11 @@
       case UNCOMPNG__PIXEL_FORMAT__Y_16LE:
         for (uint32_t x = 0; x < width; x++) {
           if ((ej + 2) > ej_max) {
-            int err =
-                uncompng__private_impl_flush(write_func, context, ej, false);
+            int err = uncompng__private_impl_flush(write_func, context, f_type,
+                                                   &ej, false);
             if (err != 0) {
               return err;
             }
-            ej = ei_later;
           }
           uncompng__private_impl_buffer[ej++] = row[1];
           uncompng__private_impl_buffer[ej++] = row[0];
@@ -463,12 +625,11 @@
       case UNCOMPNG__PIXEL_FORMAT__YXXX:
         for (uint32_t x = 0; x < width; x++) {
           if ((ej + 1) > ej_max) {
-            int err =
-                uncompng__private_impl_flush(write_func, context, ej, false);
+            int err = uncompng__private_impl_flush(write_func, context, f_type,
+                                                   &ej, false);
             if (err != 0) {
               return err;
             }
-            ej = ei_later;
           }
           uncompng__private_impl_buffer[ej++] = row[0];
           row += 4;
@@ -478,12 +639,11 @@
       case UNCOMPNG__PIXEL_FORMAT__YXXX_4X16LE:
         for (uint32_t x = 0; x < width; x++) {
           if ((ej + 2) > ej_max) {
-            int err =
-                uncompng__private_impl_flush(write_func, context, ej, false);
+            int err = uncompng__private_impl_flush(write_func, context, f_type,
+                                                   &ej, false);
             if (err != 0) {
               return err;
             }
-            ej = ei_later;
           }
           uncompng__private_impl_buffer[ej++] = row[1];
           uncompng__private_impl_buffer[ej++] = row[0];
@@ -494,12 +654,11 @@
       case UNCOMPNG__PIXEL_FORMAT__BGRA_NONPREMUL:
         for (uint32_t x = 0; x < width; x++) {
           if ((ej + 4) > ej_max) {
-            int err =
-                uncompng__private_impl_flush(write_func, context, ej, false);
+            int err = uncompng__private_impl_flush(write_func, context, f_type,
+                                                   &ej, false);
             if (err != 0) {
               return err;
             }
-            ej = ei_later;
           }
           uncompng__private_impl_buffer[ej++] = row[2];
           uncompng__private_impl_buffer[ej++] = row[1];
@@ -512,12 +671,11 @@
       case UNCOMPNG__PIXEL_FORMAT__BGRA_NONPREMUL_4X16LE:
         for (uint32_t x = 0; x < width; x++) {
           if ((ej + 8) > ej_max) {
-            int err =
-                uncompng__private_impl_flush(write_func, context, ej, false);
+            int err = uncompng__private_impl_flush(write_func, context, f_type,
+                                                   &ej, false);
             if (err != 0) {
               return err;
             }
-            ej = ei_later;
           }
           uncompng__private_impl_buffer[ej++] = row[5];
           uncompng__private_impl_buffer[ej++] = row[4];
@@ -534,12 +692,11 @@
       case UNCOMPNG__PIXEL_FORMAT__BGRX:
         for (uint32_t x = 0; x < width; x++) {
           if ((ej + 3) > ej_max) {
-            int err =
-                uncompng__private_impl_flush(write_func, context, ej, false);
+            int err = uncompng__private_impl_flush(write_func, context, f_type,
+                                                   &ej, false);
             if (err != 0) {
               return err;
             }
-            ej = ei_later;
           }
           uncompng__private_impl_buffer[ej++] = row[2];
           uncompng__private_impl_buffer[ej++] = row[1];
@@ -551,12 +708,11 @@
       case UNCOMPNG__PIXEL_FORMAT__BGRX_4X16LE:
         for (uint32_t x = 0; x < width; x++) {
           if ((ej + 6) > ej_max) {
-            int err =
-                uncompng__private_impl_flush(write_func, context, ej, false);
+            int err = uncompng__private_impl_flush(write_func, context, f_type,
+                                                   &ej, false);
             if (err != 0) {
               return err;
             }
-            ej = ei_later;
           }
           uncompng__private_impl_buffer[ej++] = row[5];
           uncompng__private_impl_buffer[ej++] = row[4];
@@ -573,21 +729,20 @@
     }
   }
 
-  return uncompng__private_impl_flush(write_func, context, ej, true);
+  return uncompng__private_impl_flush(write_func, context, f_type, &ej, true);
 }
 
-UNCOMPNG__MAYBE_STATIC int  //
-uncompng__encode(int (*write_func)(void* context,
-                                   const uint8_t* data_ptr,
-                                   size_t data_len),
-                 void* context,
-                 uint32_t pixel_format,
-                 uint32_t width,
-                 uint32_t height,
-                 const uint8_t* pixel_ptr,
-                 size_t pixel_len,
-                 size_t stride) {
-  if (!write_func) {
+static int  //
+uncompng__private_impl_check_arguments(
+    int (*write_func)(void* context, const uint8_t* data_ptr, size_t data_len),
+    uint32_t pixel_format,
+    uint32_t width,
+    uint32_t height,
+    uint32_t num_frames,
+    bool extras,
+    size_t pixel_len,
+    size_t stride) {
+  if (!write_func || !width || !height || !num_frames) {
     return UNCOMPNG__RESULT__INVALID_ARGUMENT;
   }
   uint64_t bytes_per_pixel;
@@ -615,7 +770,7 @@
     return UNCOMPNG__RESULT__UNSUPPORTED_IMAGE_SIZE;
   }
 
-  if (height > 0u) {
+  if (extras && (height > 0u)) {
     // This calculation is similar to the one used in
     // wuffs_base__table__flattened_length.
     uint64_t n = ((uint64_t)stride * (uint64_t)(height - 1u)) +
@@ -625,6 +780,26 @@
     }
   }
 
+  return 0;
+}
+
+UNCOMPNG__MAYBE_STATIC int  //
+uncompng__encode(int (*write_func)(void* context,
+                                   const uint8_t* data_ptr,
+                                   size_t data_len),
+                 void* context,
+                 uint32_t pixel_format,
+                 uint32_t width,
+                 uint32_t height,
+                 const uint8_t* pixel_ptr,
+                 size_t pixel_len,
+                 size_t stride) {
+  int check = uncompng__private_impl_check_arguments(
+      write_func, pixel_format, width, height, 1, true, pixel_len, stride);
+  if (check != 0) {
+    return check;
+  }
+
   // uncompng__private_impl_buffer is a global variable in this C code, unlike
   // the original Go code, so try to reject concurrent use. This isn't perfect,
   // as it doesn't use atomics, but it's better than nothing.
@@ -633,12 +808,122 @@
     return UNCOMPNG__RESULT__CONCURRENT_CALL;
   }
   concurrent = true;
-  int ret = uncompng__private_impl_do_encode(write_func, context, pixel_ptr,
-                                             pixel_len, width, height, stride,
-                                             pixel_format);
+
+  int ej = uncompng__private_impl_encode_image_header(pixel_format, width,
+                                                      height, 0, 0);
+
+  int ret = uncompng__private_impl_encode_frame_payload(  //
+      write_func, context, 3,                             //
+      pixel_format, width, height,                        //
+      pixel_ptr, pixel_len, stride, ej);
+
   concurrent = false;
   return ret;
 }
 
+UNCOMPNG__MAYBE_STATIC int  //
+uncompng__encode_apng_header(int (*write_func)(void* context,
+                                               const uint8_t* data_ptr,
+                                               size_t data_len),
+                             void* context,
+                             uint32_t pixel_format,
+                             uint32_t width,
+                             uint32_t height,
+                             uint32_t num_frames,
+                             uint32_t num_plays) {
+  int check = uncompng__private_impl_check_arguments(
+      write_func, pixel_format, width, height, num_frames, false, 0, 0);
+  if (check != 0) {
+    return check;
+  }
+
+  static volatile bool concurrent = false;
+  if (concurrent) {
+    return UNCOMPNG__RESULT__CONCURRENT_CALL;
+  }
+  concurrent = true;
+
+  uncompng__private_impl_apng_encoder_state.pixel_format = pixel_format;
+  uncompng__private_impl_apng_encoder_state.width = width;
+  uncompng__private_impl_apng_encoder_state.height = height;
+  uncompng__private_impl_apng_encoder_state.seq_num = 0;
+  uncompng__private_impl_apng_encoder_state.num_frames = num_frames;
+  uncompng__private_impl_apng_encoder_state.cur_frame = 0;
+
+  int ej = uncompng__private_impl_encode_image_header(
+      pixel_format, width, height, num_frames, num_plays);
+
+  int ret = (*write_func)(context, uncompng__private_impl_buffer, (size_t)ej);
+
+  concurrent = false;
+  return 0;
+}
+
+UNCOMPNG__MAYBE_STATIC int  //
+uncompng__encode_apng_frame(int (*write_func)(void* context,
+                                              const uint8_t* data_ptr,
+                                              size_t data_len),
+                            void* context,
+                            uint16_t delay_numerator,
+                            uint16_t delay_denominator,
+                            const uint8_t* pixel_ptr,
+                            size_t pixel_len,
+                            size_t stride) {
+  if (uncompng__private_impl_apng_encoder_state.num_frames == 0) {
+    return UNCOMPNG__RESULT__INVALID_CALL_SEQUENCE;
+  } else if (uncompng__private_impl_apng_encoder_state.seq_num >= 0x80000000u) {
+    return UNCOMPNG__RESULT__UNSUPPORTED_IMAGE_SIZE;
+  }
+  int check = uncompng__private_impl_check_arguments(          //
+      write_func,                                              //
+      uncompng__private_impl_apng_encoder_state.pixel_format,  //
+      uncompng__private_impl_apng_encoder_state.width,         //
+      uncompng__private_impl_apng_encoder_state.height,        //
+      uncompng__private_impl_apng_encoder_state.num_frames,    //
+      true, pixel_len, stride);
+  if (check != 0) {
+    return check;
+  }
+
+  static volatile bool concurrent = false;
+  if (concurrent) {
+    return UNCOMPNG__RESULT__CONCURRENT_CALL;
+  }
+  concurrent = true;
+
+  uint32_t f_type = 4;
+  if (uncompng__private_impl_apng_encoder_state.cur_frame == 0) {
+    f_type |= 1;
+  }
+  uncompng__private_impl_apng_encoder_state.cur_frame++;
+  if (uncompng__private_impl_apng_encoder_state.cur_frame ==
+      uncompng__private_impl_apng_encoder_state.num_frames) {
+    f_type |= 2;
+  }
+
+  int ej = uncompng__private_impl_encode_frame_header(delay_numerator,
+                                                      delay_denominator);
+
+  int ret = uncompng__private_impl_encode_frame_payload(       //
+      write_func, context, f_type,                             //
+      uncompng__private_impl_apng_encoder_state.pixel_format,  //
+      uncompng__private_impl_apng_encoder_state.width,         //
+      uncompng__private_impl_apng_encoder_state.height,        //
+      pixel_ptr, pixel_len, stride, ej);
+
+  if (ret || (uncompng__private_impl_apng_encoder_state.num_frames ==
+              uncompng__private_impl_apng_encoder_state.cur_frame)) {
+    uncompng__private_impl_apng_encoder_state.pixel_format = 0;
+    uncompng__private_impl_apng_encoder_state.width = 0;
+    uncompng__private_impl_apng_encoder_state.height = 0;
+    uncompng__private_impl_apng_encoder_state.seq_num = 0;
+    uncompng__private_impl_apng_encoder_state.num_frames = 0;
+    uncompng__private_impl_apng_encoder_state.cur_frame = 0;
+  }
+
+  concurrent = false;
+  return 0;
+}
+
 #endif  // UNCOMPNG_IMPLEMENTATION
 #endif  // UNCOMPNG_INCLUDE_GUARD