PR #24443 opened by Forgejo_Fairy
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24443
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24443.patch

PSD merged previews contain color channels composited against white. Copying 
them directly into straight-alpha frames produces washed-out colors and white 
fringes. In the sample from #23596, pixel (50, 50) currently decodes as RGBA 
(191, 128, 128, 127); this change produces the expected (126, 0, 0, 127).

Remove the white contribution from supported 8-bit and 16-bit alpha output, 
including grayscale and RGB converted from CMYK. Preserve alpha, clip negative 
reconstructed colors, and use zero color for fully transparent pixels. Unsigned 
multiplication accommodates the full 16-bit product. Existing alpha detection 
continues to distinguish merged transparency from auxiliary channels.

Update three affected FATE references and add a native grayscale-alpha test 
using the existing lena-ya8.psd sample. No new FATE samples are required.

Validation on Linux aarch64:

- The supplied gradient matches the corrected attachment pixel for pixel.
- All 17 PSD FATE tests pass with `make fate-psd SAMPLES=/opt/fate-suite`.
- All 48 generated fixtures pass across 8/16-bit RGB, grayscale and CMYK, 
RAW/RLE, transparency present/absent, and auxiliary channels present/absent.
- Corrected RGB FATE samples match psd-tools 1.19.0 for all nonzero-alpha 
pixels, and alpha matches everywhere.

The standalone validator and investigation record are preserved on 
`fairy/issue23596-tooling` under `tools/issue23596/`. Reconstruction remains 
subject to quantization already present in the stored composite.

Fixes #23596.


>From b713d73057497cf200cbae8519bf5a3a5abd0398 Mon Sep 17 00:00:00 2001
From: Michael Niedermayer <[email protected]>
Date: Thu, 10 Sep 2026 18:26:21 +0000
Subject: [PATCH] avcodec/psd: remove white matte from merged transparency

PSD merged previews blend color channels against white. Copying those
channels into a straight-alpha frame causes washed-out colors and white
fringes in partially transparent areas.

Undo the white composite for 8- and 16-bit alpha output, including grayscale
and RGB converted from CMYK. Preserve alpha, clip negative reconstructed
colors and use zero color for fully transparent pixels. Use unsigned
multiplication to accommodate the full 16-bit product without overflow.

Update the affected FATE references and add native grayscale-alpha output
coverage using the existing sample. No new FATE samples are needed.

The supplied gradient now matches the issue's corrected PNG pixel for
pixel. All 17 PSD FATE tests and 48 generated RAW/RLE fixtures pass.

Fixes: #23596
Assisted-by: Fairy
---
 libavcodec/psd.c                            | 37 +++++++++++++++++++++
 tests/fate/image.mak                        |  3 ++
 tests/ref/fate/psd-lena-rgba-rle-128x128-8b |  2 +-
 tests/ref/fate/psd-rgba                     |  2 +-
 tests/ref/fate/psd-ya8                      |  2 +-
 tests/ref/fate/psd-ya8-alpha                |  6 ++++
 6 files changed, 49 insertions(+), 3 deletions(-)
 create mode 100644 tests/ref/fate/psd-ya8-alpha

diff --git a/libavcodec/psd.c b/libavcodec/psd.c
index 9dc56f8f5e..684e26feb6 100644
--- a/libavcodec/psd.c
+++ b/libavcodec/psd.c
@@ -300,6 +300,39 @@ static int decode_rle(PSDContext * s){
     return 0;
 }
 
+/* The merged image is composited against white, rather than stored with
+ * straight alpha. The product fits in unsigned int for both supported depths. 
*/
+static unsigned unmatte(int color, int alpha, int max)
+{
+    return alpha ? FFMAX(color + alpha - max, 0) * (unsigned)max / alpha : 0;
+}
+
+static void remove_white_matte(PSDContext *s, AVFrame *frame)
+{
+    int gray = s->color_mode == PSD_GRAYSCALE || s->color_mode == PSD_DUOTONE;
+    int step = s->pixel_size * (gray ? 2 : 1);
+
+    for (int c = 0; c < (gray ? 1 : 3); c++) {
+        uint8_t *color = frame->data[c];
+        const uint8_t *alpha = gray ? frame->data[0] + s->pixel_size : 
frame->data[3];
+
+        for (int y = 0; y < s->height; y++) {
+            if (s->channel_depth == 8) {
+                for (int x = 0; x < s->width; x++)
+                    color[x * step] = unmatte(color[x * step], alpha[x * 
step], 255);
+            } else {
+                for (int x = 0; x < s->width; x++) {
+                    unsigned v = unmatte(AV_RB16(color + x * step),
+                                         AV_RB16(alpha + x * step), 65535);
+                    AV_WB16(color + x * step, v);
+                }
+            }
+            color += frame->linesize[c];
+            alpha += frame->linesize[gray ? 0 : 3];
+        }
+    }
+}
+
 static int decode_frame(AVCodecContext *avctx, AVFrame *picture,
                         int *got_frame, AVPacket *avpkt)
 {
@@ -563,6 +596,10 @@ static int decode_frame(AVCodecContext *avctx, AVFrame 
*picture,
         memcpy(picture->data[1], s->palette, AVPALETTE_SIZE);
     }
 
+    if (avctx->pix_fmt == AV_PIX_FMT_GBRAP || avctx->pix_fmt == 
AV_PIX_FMT_GBRAP16BE ||
+        avctx->pix_fmt == AV_PIX_FMT_YA8   || avctx->pix_fmt == 
AV_PIX_FMT_YA16BE)
+        remove_white_matte(s, picture);
+
     av_freep(&s->tmp);
 
     picture->pict_type = AV_PICTURE_TYPE_I;
diff --git a/tests/fate/image.mak b/tests/fate/image.mak
index a20fe183dc..4f52439c83 100644
--- a/tests/fate/image.mak
+++ b/tests/fate/image.mak
@@ -465,6 +465,9 @@ fate-psd-lena-rgb-rle-127x127-8b: CMD = framecrc -i 
$(TARGET_SAMPLES)/psd/lena-r
 FATE_PSD += fate-psd-lena-rgba-rle-128x128-8b
 fate-psd-lena-rgba-rle-128x128-8b: CMD = framecrc -i 
$(TARGET_SAMPLES)/psd/lena-rgba_rle_128x128_8b.psd
 
+FATE_PSD += fate-psd-ya8-alpha
+fate-psd-ya8-alpha: CMD = framecrc -i $(TARGET_SAMPLES)/psd/lena-ya8.psd
+
 FATE_PSD += fate-psd-lena-256c
 fate-psd-lena-256c: CMD = framecrc -i $(TARGET_SAMPLES)/psd/lena-256c.psd
 
diff --git a/tests/ref/fate/psd-lena-rgba-rle-128x128-8b 
b/tests/ref/fate/psd-lena-rgba-rle-128x128-8b
index f3a11b7c5c..d63c80e5e2 100644
--- a/tests/ref/fate/psd-lena-rgba-rle-128x128-8b
+++ b/tests/ref/fate/psd-lena-rgba-rle-128x128-8b
@@ -3,4 +3,4 @@
 #codec_id 0: rawvideo
 #dimensions 0: 128x128
 #sar 0: 0/1
-0,          0,          0,        1,    65536, 0xaaacdff3
+0,          0,          0,        1,    65536, 0x5c2127b2
diff --git a/tests/ref/fate/psd-rgba b/tests/ref/fate/psd-rgba
index 672898093d..4335c14eb7 100644
--- a/tests/ref/fate/psd-rgba
+++ b/tests/ref/fate/psd-rgba
@@ -3,4 +3,4 @@
 #codec_id 0: rawvideo
 #dimensions 0: 128x128
 #sar 0: 0/1
-0,          0,          0,        1,    49152, 0xb2563b4a
+0,          0,          0,        1,    49152, 0x72620a93
diff --git a/tests/ref/fate/psd-ya8 b/tests/ref/fate/psd-ya8
index 9a59fbf5a8..6d143c42dc 100644
--- a/tests/ref/fate/psd-ya8
+++ b/tests/ref/fate/psd-ya8
@@ -3,4 +3,4 @@
 #codec_id 0: rawvideo
 #dimensions 0: 128x128
 #sar 0: 0/1
-0,          0,          0,        1,    49152, 0xfa0ee1d0
+0,          0,          0,        1,    49152, 0xe0da1ccd
diff --git a/tests/ref/fate/psd-ya8-alpha b/tests/ref/fate/psd-ya8-alpha
new file mode 100644
index 0000000000..619c52ded8
--- /dev/null
+++ b/tests/ref/fate/psd-ya8-alpha
@@ -0,0 +1,6 @@
+#tb 0: 1/25
+#media_type 0: video
+#codec_id 0: rawvideo
+#dimensions 0: 128x128
+#sar 0: 0/1
+0,          0,          0,        1,    32768, 0xb9ca81ce
-- 
2.52.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to