PR #25346 opened by Forgejo_Fairy URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25346 Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25346.patch
APNG frames using OVER blending currently fail to decode in rgba64be and ya16be. Add compositing for these formats using big-endian sample access and 64-bit intermediate values, retaining unrounded alpha weights until the final color division. Existing 8-bit blending is unchanged. Add generated RGBA64 and YA16 FATE animations covering transparent, opaque and partial alpha, frame offsets, SOURCE blending, and all disposal operations. Expected pixels were independently checked using rational arithmetic with one and four decoder threads; references reside in tests/ref/fate. No external samples are required. Validation: all ten APNG FATE tests pass, including under AddressSanitizer/UndefinedBehaviorSanitizer with leak detection disabled. Both new test streams fail on unpatched master. The reporter's complete 73-frame attachment decodes successfully with the fix, and the reported lossless WebP conversion completes with -xerror enabled. Fixes #25206. >From fe30c8ed34235e65bf162b3ddbd434f206526b3a Mon Sep 17 00:00:00 2001 From: Michael Niedermayer <[email protected]> Date: Thu, 22 Oct 2026 07:53:29 +0000 Subject: [PATCH] avcodec/pngdec: support 16-bit APNG OVER blending Composite rgba64be and ya16be pixels using big-endian sample access and 64-bit intermediate values. Keep the alpha weights unrounded while computing colors so that low or fractional alpha cannot overflow a component. Add generated FATE animations covering transparent, opaque and partial alpha, subrectangles, SOURCE blending and all disposal operations. Expected frames were checked against an independent rational-arithmetic compositor. Fixes issue #25206. Assisted-by: Fairy --- libavcodec/pngdec.c | 38 +++++++++++++++- tests/Makefile | 4 ++ tests/apnggen.c | 75 +++++++++++++++++++++++++++++--- tests/fate/apng.mak | 13 +++++- tests/ref/fate/apng-blend-rgba64 | 11 +++++ tests/ref/fate/apng-blend-ya16 | 11 +++++ 6 files changed, 144 insertions(+), 8 deletions(-) create mode 100644 tests/ref/fate/apng-blend-rgba64 create mode 100644 tests/ref/fate/apng-blend-ya16 diff --git a/libavcodec/pngdec.c b/libavcodec/pngdec.c index 58c688e51f0..0bb0ae138b2 100644 --- a/libavcodec/pngdec.c +++ b/libavcodec/pngdec.c @@ -1415,6 +1415,34 @@ static void handle_p_frame_png(PNGDecContext *s, AVFrame *p) // apply a fast variant: (X+127)/255 = ((X+127)*257+257)>>16 = ((X+128)*257)>>16 #define FAST_DIV255(x) ((((x) + 128) * 257) >> 16) +/* Composite straight-alpha 16-bit big-endian pixels. Keep the alpha weights + * unrounded until the final division, including when calculating colors. */ +static void apng_blend_pixel16(uint8_t *foreground, const uint8_t *background, + int bpp) +{ + unsigned foreground_alpha = AV_RB16(foreground + bpp - 2); + unsigned background_alpha = AV_RB16(background + bpp - 2); + uint64_t foreground_weight, background_weight, alpha; + + if (foreground_alpha == 65535) + return; + if (foreground_alpha == 0) { + memcpy(foreground, background, bpp); + return; + } + + foreground_weight = foreground_alpha * 65535ULL; + background_weight = (65535 - foreground_alpha) * (uint64_t)background_alpha; + alpha = foreground_weight + background_weight; + + for (int b = 0; b < bpp - 2; b += 2) { + uint64_t color = foreground_weight * AV_RB16(foreground + b) + + background_weight * AV_RB16(background + b); + AV_WB16(foreground + b, (color + alpha / 2) / alpha); + } + AV_WB16(foreground + bpp - 2, (alpha + 32767) / 65535); +} + static int handle_p_frame_apng(AVCodecContext *avctx, PNGDecContext *s, AVFrame *p) { @@ -1428,7 +1456,9 @@ static int handle_p_frame_apng(AVCodecContext *avctx, PNGDecContext *s, if (s->blend_op == APNG_BLEND_OP_OVER && avctx->pix_fmt != AV_PIX_FMT_RGBA && - avctx->pix_fmt != AV_PIX_FMT_GRAY8A) { + avctx->pix_fmt != AV_PIX_FMT_GRAY8A && + avctx->pix_fmt != AV_PIX_FMT_RGBA64BE && + avctx->pix_fmt != AV_PIX_FMT_YA16BE) { avpriv_request_sample(avctx, "Blending with pixel format %s", av_get_pix_fmt_name(avctx->pix_fmt)); return AVERROR_PATCHWELCOME; @@ -1458,6 +1488,12 @@ static int handle_p_frame_apng(AVCodecContext *avctx, PNGDecContext *s, uint8_t foreground_alpha, background_alpha, output_alpha; uint8_t output[10]; + if (avctx->pix_fmt == AV_PIX_FMT_RGBA64BE || + avctx->pix_fmt == AV_PIX_FMT_YA16BE) { + apng_blend_pixel16(foreground, background, bpp); + continue; + } + // Since we might be blending alpha onto alpha, we use the following equations: // output_alpha = foreground_alpha + (1 - foreground_alpha) * background_alpha // output = (foreground_alpha * foreground + (1 - foreground_alpha) * background_alpha * background) / output_alpha diff --git a/tests/Makefile b/tests/Makefile index de71678e5c3..ea32b01a382 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -73,6 +73,10 @@ tests/data/fate/apng-small-bpp-%.png: TAG = GEN tests/data/fate/apng-small-bpp-%.png: tests/apnggen$(HOSTEXESUF) | tests/data/fate $(M)./$< $@ +tests/data/fate/apng-blend-%.png: TAG = GEN +tests/data/fate/apng-blend-%.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 index daf69f1ecb7..a5f3d98a6e5 100644 --- a/tests/apnggen.c +++ b/tests/apnggen.c @@ -1,11 +1,11 @@ /* - * Generate the sub-8-bit APNG streams used by the FATE tests. + * Generate the 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. + * The sub-8-bit frames are palette images with odd x offsets, which are + * not aligned to the bytes holding the packed samples. The 16-bit frames + * exercise alpha blending and disposal. 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 * @@ -201,6 +201,65 @@ static void write_apng(const char *path, int depth) die(path); } +/* Exercise 16-bit OVER blending with zero, full and partial alpha, including + * values that are not multiples of 257. The subrectangles leave a border + * untouched and use all three disposal operations as well as SOURCE. */ +static void write_apng16(const char *path, int channels) +{ + static const uint8_t sig[8] = { 137, 80, 78, 71, 13, 10, 26, 10 }; + static const unsigned alpha[8] = { 0, 1, 32768, 65534, 65535, 12345, 49151, 21845 }; + static const uint8_t dispose[6] = { 0, 0, 1, 2, 0, 0 }; + uint8_t ihdr[13] = { 0, 0, 0, 8, 0, 0, 0, 4, 16, channels == 4 ? 6 : 4 }; + uint8_t actl[8] = { 0, 0, 0, 6, 0, 0, 0, 1 }; + Buf out = { 0 }, image = { 0 }; + uint8_t raw[260], fctl[26]; + FILE *f; + int seq = 0; + + put(&out, sig, sizeof(sig)); + chunk(&out, "IHDR", ihdr, sizeof(ihdr)); + chunk(&out, "acTL", actl, sizeof(actl)); + for (int frame = 0; frame < 6; frame++) { + int w = frame ? 6 : 8; + int h = frame ? 2 : 4; + int n = 0; + + memset(fctl, 0, sizeof(fctl)); + fctl[3] = seq++; + fctl[7] = w; + fctl[11] = h; + fctl[15] = !!frame; + fctl[19] = !!frame; + fctl[21] = 1; + fctl[23] = 10; + fctl[24] = dispose[frame]; + fctl[25] = frame != 0 && frame != 5; + chunk(&out, "fcTL", fctl, sizeof(fctl)); + + for (int y = 0; y < h; y++) { + raw[n++] = 0; + for (int x = 0; x < w; x++) { + for (int c = 0; c < channels; c++) { + unsigned v = c == channels - 1 ? alpha[(x + 3 * y + frame) % 8] : + x % 3 == 0 ? 65535 : + (12345 * x + 23456 * y + 4567 * frame + 34567 * c) & 65535; + raw[n++] = v >> 8; + raw[n++] = v; + } + } + } + image.len = 0; + if (frame) + put_be32(&image, seq++); + put_stored(&image, raw, n); + chunk(&out, frame ? "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; @@ -210,6 +269,10 @@ int main(int argc, char **argv) fprintf(stderr, "usage: apnggen OUTPUT.png\n"); return 1; } + if (strstr(path, "rgba64") || strstr(path, "ya16")) { + write_apng16(path, strstr(path, "rgba64") ? 4 : 2); + return 0; + } if (strstr(path, "1bit")) depth = 1; else if (strstr(path, "2bit")) diff --git a/tests/fate/apng.mak b/tests/fate/apng.mak index 5aec0ad7b71..2dd3609fcd7 100644 --- a/tests/fate/apng.mak +++ b/tests/fate/apng.mak @@ -34,4 +34,15 @@ 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) +FATE_APNG_BLEND += fate-apng-blend-rgba64 +fate-apng-blend-rgba64: CMD = framecrc -i $(TARGET_PATH)/tests/data/fate/apng-blend-rgba64.png -pix_fmt rgba64be -fps_mode passthrough +fate-apng-blend-rgba64: tests/data/fate/apng-blend-rgba64.png + +FATE_APNG_BLEND += fate-apng-blend-ya16 +fate-apng-blend-ya16: CMD = framecrc -i $(TARGET_PATH)/tests/data/fate/apng-blend-ya16.png -pix_fmt ya16be -fps_mode passthrough +fate-apng-blend-ya16: tests/data/fate/apng-blend-ya16.png + +FATE_APNG_BLEND-$(call FRAMECRC, APNG, APNG) += $(FATE_APNG_BLEND) +FATE_FFMPEG += $(FATE_APNG_BLEND-yes) + +fate-apng: $(FATE_APNG-yes) $(FATE_APNG_SMALL_BPP-yes) $(FATE_APNG_BLEND-yes) diff --git a/tests/ref/fate/apng-blend-rgba64 b/tests/ref/fate/apng-blend-rgba64 new file mode 100644 index 00000000000..0bcfb992e7b --- /dev/null +++ b/tests/ref/fate/apng-blend-rgba64 @@ -0,0 +1,11 @@ +#tb 0: 1/10 +#media_type 0: video +#codec_id 0: rawvideo +#dimensions 0: 8x4 +#sar 0: 0/1 +0, 0, 0, 1, 256, 0xd009a046 +0, 1, 1, 1, 256, 0x1e57a961 +0, 2, 2, 1, 256, 0x4c84a3a4 +0, 3, 3, 1, 256, 0xe6a49aa4 +0, 4, 4, 1, 256, 0x0cdf9abc +0, 5, 5, 1, 256, 0x39419d16 diff --git a/tests/ref/fate/apng-blend-ya16 b/tests/ref/fate/apng-blend-ya16 new file mode 100644 index 00000000000..3b894edd25d --- /dev/null +++ b/tests/ref/fate/apng-blend-ya16 @@ -0,0 +1,11 @@ +#tb 0: 1/10 +#media_type 0: video +#codec_id 0: rawvideo +#dimensions 0: 8x4 +#sar 0: 0/1 +0, 0, 0, 1, 128, 0x56f647cc +0, 1, 1, 1, 128, 0x765450ae +0, 2, 2, 1, 128, 0x74af4f5b +0, 3, 3, 1, 128, 0xee2a482f +0, 4, 4, 1, 128, 0xf90845ba +0, 5, 5, 1, 128, 0xb8ec4592 -- 2.54.0 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]
