Commit 3c5ad12adf for ffmpeg

commit 3c5ad12adfbc794bbba1f666e46822c212287495
Author: Jun Zhao <barryjzhao@tencent.com>
Date:   Mon Oct 19 07:26:41 2026 +0800

    tests/fate/apng: test sub-8-bit frames with a frame offset

    Generate 1/2/4-bit palette animations whose later frames sit on an odd
    x offset, so the offset is not a byte offset in the packed row. The
    fixtures are built by apnggen, so the tests do not need fate-suite samples.

    Signed-off-by: Jun Zhao <barryjzhao@tencent.com>

diff --git a/Makefile b/Makefile
index 9eaeb27b7a..745131c75f 100644
--- a/Makefile
+++ b/Makefile
@@ -18,7 +18,7 @@ vpath %.ptx  $(SRC_PATH)
 vpath %.metal $(SRC_PATH)
 vpath %/fate_config.sh.template $(SRC_PATH)

-TESTTOOLS   = audiogen videogen rotozoom tiny_psnr tiny_ssim base64 audiomatch amrgen
+TESTTOOLS   = audiogen videogen rotozoom tiny_psnr tiny_ssim base64 audiomatch amrgen apnggen
 HOSTPROGS  := $(TESTTOOLS:%=tests/%) doc/print_options

 ALLFFLIBS =            \
diff --git a/tests/Makefile b/tests/Makefile
index 35db2edb3b..f1a3ef2f42 100644
--- a/tests/Makefile
+++ b/tests/Makefile
@@ -69,6 +69,10 @@ tests/data/fate/%.amr: TAG = GEN
 tests/data/fate/%.amr: tests/amrgen$(HOSTEXESUF) | tests/data/fate
 	$(M)./$< $@

+tests/data/fate/apng-small-bpp-%.png: TAG = GEN
+tests/data/fate/apng-small-bpp-%.png: tests/apnggen$(HOSTEXESUF) | tests/data/fate
+	$(M)./$< $@
+
 RUNNING_FATE := $(filter check fate%,$(filter-out fate-rsync,$(MAKECMDGOALS)))

 # Check sanity of dependencies when running FATE tests.
diff --git a/tests/apnggen.c b/tests/apnggen.c
new file mode 100644
index 0000000000..daf69f1ecb
--- /dev/null
+++ b/tests/apnggen.c
@@ -0,0 +1,225 @@
+/*
+ * Generate the sub-8-bit APNG streams used by the FATE tests.
+ *
+ * The frames are palette images. The first frame fills the canvas. Every
+ * later frame is a smaller rectangle whose x offset is odd, so it is not
+ * aligned to the byte that holds the packed samples. These files are built
+ * here, the same way audiogen builds its wave files, so the tests do not
+ * need media from the fate-suite.
+ *
+ * Copyright (c) 2026 Jun Zhao
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include <errno.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+enum { CANVAS_W = 24, CANVAS_H = 20, FRAMES = 6 };
+
+static const uint8_t palette[16][3] = {
+    {   0,   0,   0 }, { 255,   0,   0 }, {   0, 255,   0 }, {   0,   0, 255 },
+    { 255, 255,   0 }, { 255,   0, 255 }, {   0, 255, 255 }, { 255, 128,   0 },
+    { 128,   0, 255 }, { 128, 255,   0 }, {   0, 128, 255 }, { 255,   0, 128 },
+    {  64,  64,  64 }, { 192, 192, 192 }, { 128,  64,   0 }, { 255, 255, 255 },
+};
+
+typedef struct {
+    uint8_t data[2048];
+    int len;
+} Buf;
+
+static void die(const char *path)
+{
+    fprintf(stderr, "apnggen: %s: %s\n", path, strerror(errno));
+    exit(1);
+}
+
+static uint32_t crc32(const uint8_t *data, size_t size)
+{
+    static uint32_t table[256];
+    uint32_t crc = 0xFFFFFFFFu;
+    size_t i;
+
+    if (!table[1]) {
+        for (i = 0; i < 256; i++) {
+            uint32_t c = i;
+            int k;
+
+            for (k = 0; k < 8; k++)
+                c = (c & 1) ? 0xEDB88320u ^ (c >> 1) : c >> 1;
+            table[i] = c;
+        }
+    }
+    for (i = 0; i < size; i++)
+        crc = table[(crc ^ data[i]) & 0xFF] ^ (crc >> 8);
+    return crc ^ 0xFFFFFFFFu;
+}
+
+static void put(Buf *b, const void *src, int n)
+{
+    if (n < 0 || b->len > (int)sizeof(b->data) - n) {
+        fprintf(stderr, "apnggen: output too large\n");
+        exit(1);
+    }
+    memcpy(b->data + b->len, src, n);
+    b->len += n;
+}
+
+static void put_be32(Buf *b, uint32_t v)
+{
+    uint8_t p[4] = { v >> 24, v >> 16, v >> 8, v };
+
+    put(b, p, 4);
+}
+
+static void chunk(Buf *b, const char type[4], const uint8_t *data, int n)
+{
+    int start = b->len + 4;
+
+    put_be32(b, n);
+    put(b, type, 4);
+    if (n)
+        put(b, data, n);
+    put_be32(b, crc32(b->data + start, 4 + n));
+}
+
+/* First frame covers the canvas. Later frames are 7x5 rectangles on odd x
+ * offsets, one pixel further right each frame. */
+static int sample(int depth, int frame, int x, int y)
+{
+    int max = (1 << depth) - 1;
+
+    if (!frame)
+        return (x / 4 + y / 5) & max;
+    if (depth == 1)
+        return (x + y + frame) & 1;
+    return 1 + (x + y + frame) % max;
+}
+
+static int pack_frame(uint8_t *dst, int depth, int frame, int w, int h)
+{
+    int row_bytes = (w * depth + 7) / 8;
+    int out = 0, y, x;
+
+    for (y = 0; y < h; y++) {
+        dst[out++] = 0;
+        memset(dst + out, 0, row_bytes);
+        for (x = 0; x < w; x++) {
+            int bit = x * depth;
+
+            dst[out + bit / 8] |= sample(depth, frame, x, y) << (8 - depth - bit % 8);
+        }
+        out += row_bytes;
+    }
+    return out;
+}
+
+/* zlib wrapper around one stored deflate block. No compressor needed. */
+static void put_stored(Buf *b, const uint8_t *raw, int n)
+{
+    unsigned neg = ~n & 0xFFFF;
+    uint8_t hdr[7] = { 0x78, 0x01, 0x01, n, n >> 8, neg, neg >> 8 };
+    uint32_t a = 1, s = 0;
+    int i;
+
+    put(b, hdr, 7);
+    put(b, raw, n);
+    for (i = 0; i < n; i++) {
+        a += raw[i];
+        if (a >= 65521)
+            a -= 65521;
+        s += a;
+        if (s >= 65521)
+            s -= 65521;
+    }
+    put_be32(b, (s << 16) | a);
+}
+
+static void write_apng(const char *path, int depth)
+{
+    static const uint8_t sig[8] = { 137, 80, 78, 71, 13, 10, 26, 10 };
+    uint8_t ihdr[13] = { 0, 0, 0, CANVAS_W, 0, 0, 0, CANVAS_H, depth, 3 };
+    uint8_t actl[8] = { 0, 0, 0, FRAMES, 0, 0, 0, 1 };
+    uint8_t plte[16 * 3], raw[512], fctl[26];
+    Buf out = { 0 }, image = { 0 };
+    FILE *f;
+    int colors = 1 << depth, seq = 0, frame, i;
+
+    for (i = 0; i < colors; i++)
+        memcpy(plte + 3 * i, palette[i], 3);
+
+    put(&out, sig, 8);
+    chunk(&out, "IHDR", ihdr, sizeof(ihdr));
+    chunk(&out, "acTL", actl, sizeof(actl));
+    chunk(&out, "PLTE", plte, colors * 3);
+
+    for (frame = 0; frame < FRAMES; frame++) {
+        int sub = frame != 0;
+        int x = sub ? 1 + 2 * (frame - 1) : 0;
+        int y = sub ? 1 + (frame - 1) : 0;
+        int w = sub ? 7 : CANVAS_W;
+        int h = sub ? 5 : CANVAS_H;
+
+        memset(fctl, 0, sizeof(fctl));
+        fctl[3] = seq++;
+        fctl[7] = w;
+        fctl[11] = h;
+        fctl[15] = x;
+        fctl[19] = y;
+        fctl[21] = 1;  /* delay numerator */
+        fctl[23] = 10; /* delay denominator: 1/10 s */
+        chunk(&out, "fcTL", fctl, sizeof(fctl));
+
+        image.len = 0;
+        if (sub)
+            put_be32(&image, seq++);
+        put_stored(&image, raw, pack_frame(raw, depth, frame, w, h));
+        chunk(&out, sub ? "fdAT" : "IDAT", image.data, image.len);
+    }
+    chunk(&out, "IEND", NULL, 0);
+
+    f = fopen(path, "wb");
+    if (!f || fwrite(out.data, 1, out.len, f) != (size_t)out.len || fclose(f))
+        die(path);
+}
+
+int main(int argc, char **argv)
+{
+    const char *path = argc == 2 ? argv[1] : NULL;
+    int depth = 0;
+
+    if (!path) {
+        fprintf(stderr, "usage: apnggen OUTPUT.png\n");
+        return 1;
+    }
+    if (strstr(path, "1bit"))
+        depth = 1;
+    else if (strstr(path, "2bit"))
+        depth = 2;
+    else if (strstr(path, "4bit"))
+        depth = 4;
+    if (!depth) {
+        fprintf(stderr, "apnggen: unsupported file name %s\n", path);
+        return 1;
+    }
+    write_apng(path, depth);
+    return 0;
+}
diff --git a/tests/fate/apng.mak b/tests/fate/apng.mak
index aec06c28a6..5aec0ad7b7 100644
--- a/tests/fate/apng.mak
+++ b/tests/fate/apng.mak
@@ -16,4 +16,22 @@ fate-apng-dispose-background2: CMD = framecrc -i $(TARGET_SAMPLES)/apng/alogo.pn
 FATE_APNG-$(call FRAMECRC, APNG, APNG) += $(FATE_APNG)

 FATE_SAMPLES_FFMPEG += $(FATE_APNG-yes)
-fate-apng: $(FATE_APNG-yes)
+
+# Generated here: a 1/2/4-bit palette animation whose later frames sit on an
+# odd x offset, so the offset is not a byte offset in the packed row.
+FATE_APNG_SMALL_BPP += fate-apng-small-bpp-1bit
+fate-apng-small-bpp-1bit: CMD = framecrc -i $(TARGET_PATH)/tests/data/fate/apng-small-bpp-1bit.png -fps_mode passthrough
+fate-apng-small-bpp-1bit: tests/data/fate/apng-small-bpp-1bit.png
+
+FATE_APNG_SMALL_BPP += fate-apng-small-bpp-2bit
+fate-apng-small-bpp-2bit: CMD = framecrc -i $(TARGET_PATH)/tests/data/fate/apng-small-bpp-2bit.png -fps_mode passthrough
+fate-apng-small-bpp-2bit: tests/data/fate/apng-small-bpp-2bit.png
+
+FATE_APNG_SMALL_BPP += fate-apng-small-bpp-4bit
+fate-apng-small-bpp-4bit: CMD = framecrc -i $(TARGET_PATH)/tests/data/fate/apng-small-bpp-4bit.png -fps_mode passthrough
+fate-apng-small-bpp-4bit: tests/data/fate/apng-small-bpp-4bit.png
+
+FATE_APNG_SMALL_BPP-$(call FRAMECRC, APNG, APNG) += $(FATE_APNG_SMALL_BPP)
+FATE_FFMPEG += $(FATE_APNG_SMALL_BPP-yes)
+
+fate-apng: $(FATE_APNG-yes) $(FATE_APNG_SMALL_BPP-yes)
diff --git a/tests/ref/fate/apng-small-bpp-1bit b/tests/ref/fate/apng-small-bpp-1bit
new file mode 100644
index 0000000000..b5b509025c
--- /dev/null
+++ b/tests/ref/fate/apng-small-bpp-1bit
@@ -0,0 +1,11 @@
+#tb 0: 1/10
+#media_type 0: video
+#codec_id 0: rawvideo
+#dimensions 0: 24x20
+#sar 0: 0/1
+0,          0,          0,        1,     1920, 0x119ecd4e
+0,          1,          1,        1,     1920, 0x4ffdcc4f
+0,          2,          2,        1,     1920, 0x877cce4d
+0,          3,          3,        1,     1920, 0x1e02ce4d
+0,          4,          4,        1,     1920, 0x1551cf4c
+0,          5,          5,        1,     1920, 0x2f7ad447
diff --git a/tests/ref/fate/apng-small-bpp-2bit b/tests/ref/fate/apng-small-bpp-2bit
new file mode 100644
index 0000000000..172f4c61f2
--- /dev/null
+++ b/tests/ref/fate/apng-small-bpp-2bit
@@ -0,0 +1,11 @@
+#tb 0: 1/10
+#media_type 0: video
+#codec_id 0: rawvideo
+#dimensions 0: 24x20
+#sar 0: 0/1
+0,          0,          0,        1,     1920, 0xf24744e5
+0,          1,          1,        1,     1920, 0xe89950d9
+0,          2,          2,        1,     1920, 0xec9550d9
+0,          3,          3,        1,     1920, 0xf58c50d9
+0,          4,          4,        1,     1920, 0xbce958d1
+0,          5,          5,        1,     1920, 0xd9c364c5
diff --git a/tests/ref/fate/apng-small-bpp-4bit b/tests/ref/fate/apng-small-bpp-4bit
new file mode 100644
index 0000000000..6ddb9caa0f
--- /dev/null
+++ b/tests/ref/fate/apng-small-bpp-4bit
@@ -0,0 +1,11 @@
+#tb 0: 1/10
+#media_type 0: video
+#codec_id 0: rawvideo
+#dimensions 0: 24x20
+#sar 0: 0/1
+0,          0,          0,        1,     1920, 0xf88da1b4
+0,          1,          1,        1,     1920, 0x67b7c2dd
+0,          2,          2,        1,     1920, 0x910fccd9
+0,          3,          3,        1,     1920, 0x2d6bd617
+0,          4,          4,        1,     1920, 0x630cd7dd
+0,          5,          5,        1,     1920, 0x146fd429