Git Inbox Mirror of the ffmpeg-devel mailing list - see https://ffmpeg.org/mailman/listinfo/ffmpeg-devel
 help / color / mirror / Atom feed
* [FFmpeg-devel] [PATCH] avcodec/psd: Support auxiliary channels (PR #20966)
@ 2025-11-19  5:26 jchw via ffmpeg-devel
  0 siblings, 0 replies; only message in thread
From: jchw via ffmpeg-devel @ 2025-11-19  5:26 UTC (permalink / raw)
  To: ffmpeg-devel; +Cc: jchw

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

Photoshop documents can contain additional "auxiliary" channels that don't take part in layer compositing, therefore, using the channel count to determine the presence of an alpha channel in the merged image is incorrect. Instead, as per the PSD specification, use the sign of the layer count (present in the layers and masks section) to determine if there is an alpha channel, then determine the number of "primary" channels using both the presence of the alpha channel and the pixel format.

(And yes, this really is the correct way to handle this; it's [documented here](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_pgfId-1031423) and I manually verified this behavior going back to at least Photoshop 4.0.1.)

I've attached a new FATE sample (`psd/lena-rgbxx.psd`) that tests this, an rgb image (no alpha channel) with two extra channels. I'm new to contributing here and it seems like the contributing guide is a bit out of date so please bear with me, I wasn't sure if I should've emailed the FATE sample ahead of time or not.


>From 8e7d560c61910abc2ce6a4a1c9aad1302ddd94c9 Mon Sep 17 00:00:00 2001
From: John Chadwick <john@jchw.io>
Date: Tue, 18 Nov 2025 23:45:34 -0500
Subject: [PATCH 1/3] avcodec/psd: Support auxiliary channels

Photoshop documents can contain additional "auxiliary" channels that
don't take part in layer compositing, therefore, using the channel count
to determine the presence of an alpha channel in the merged image is
incorrect. Instead, as per the PSD specification, use the sign of the
layer count (present in the layers and masks section) to determine if
there is an alpha channel, then determine the number of primary channels
using both the presence of the alpha channel and the pixel format.
---
 libavcodec/psd.c | 120 ++++++++++++++++++++++++++++-------------------
 1 file changed, 73 insertions(+), 47 deletions(-)

diff --git a/libavcodec/psd.c b/libavcodec/psd.c
index ea88f254bf..086a926698 100644
--- a/libavcodec/psd.c
+++ b/libavcodec/psd.c
@@ -52,6 +52,7 @@ typedef struct PSDContext {
 
     uint16_t channel_count;
     uint16_t channel_depth;
+    uint16_t primary_channels;
 
     uint64_t uncompressed_size;
     unsigned int pixel_size;/* 1 for 8 bits, 2 for 16 bits */
@@ -60,6 +61,8 @@ typedef struct PSDContext {
     int width;
     int height;
 
+    short layer_count;
+
     enum PsdCompr compression;
     enum PsdColorMode color_mode;
 
@@ -193,6 +196,13 @@ static int decode_header(PSDContext * s)
         return AVERROR_INVALIDDATA;
     }
 
+    if (len_section >= 6) {
+        /* layer count (in layers and masks section) */
+        bytestream2_skip(&s->gb, 4);
+        s->layer_count = bytestream2_get_be16(&s->gb);
+        len_section -= 6;
+    }
+
     if (bytestream2_get_bytes_left(&s->gb) < len_section) {
         av_log(s->avctx, AV_LOG_ERROR, "Incomplete file.\n");
         return AVERROR_INVALIDDATA;
@@ -301,11 +311,13 @@ static int decode_frame(AVCodecContext *avctx, AVFrame *picture,
     uint8_t plane_number;
 
     PSDContext *s = avctx->priv_data;
-    s->avctx     = avctx;
-    s->channel_count = 0;
-    s->channel_depth = 0;
-    s->tmp           = NULL;
-    s->line_size     = 0;
+    s->avctx            = avctx;
+    s->channel_count    = 0;
+    s->channel_depth    = 0;
+    s->primary_channels = 0;
+    s->tmp              = NULL;
+    s->line_size        = 0;
+    s->layer_count      = 0;
 
     bytestream2_init(&s->gb, avpkt->data, avpkt->size);
 
@@ -317,35 +329,28 @@ static int decode_frame(AVCodecContext *avctx, AVFrame *picture,
 
     switch (s->color_mode) {
     case PSD_BITMAP:
-        if (s->channel_depth != 1 || s->channel_count != 1) {
+        if (s->channel_depth != 1 || s->channel_count < 1) {
             av_log(s->avctx, AV_LOG_ERROR,
                     "Invalid bitmap file (channel_depth %d, channel_count %d)\n",
                     s->channel_depth, s->channel_count);
             return AVERROR_INVALIDDATA;
         }
         s->line_size = s->width + 7 >> 3;
+        s->primary_channels = 1;
         avctx->pix_fmt = AV_PIX_FMT_MONOWHITE;
         break;
     case PSD_INDEXED:
-        if (s->channel_depth != 8 || s->channel_count != 1) {
+        if (s->channel_depth != 8 || s->channel_count < 1) {
             av_log(s->avctx, AV_LOG_ERROR,
                    "Invalid indexed file (channel_depth %d, channel_count %d)\n",
                    s->channel_depth, s->channel_count);
             return AVERROR_INVALIDDATA;
         }
+        s->primary_channels = 1;
         avctx->pix_fmt = AV_PIX_FMT_PAL8;
         break;
     case PSD_CMYK:
-        if (s->channel_count == 4) {
-            if (s->channel_depth == 8) {
-                avctx->pix_fmt = AV_PIX_FMT_GBRP;
-            } else if (s->channel_depth == 16) {
-                avctx->pix_fmt = AV_PIX_FMT_GBRP16BE;
-            } else {
-                avpriv_report_missing_feature(avctx, "channel depth %d for cmyk", s->channel_depth);
-                return AVERROR_PATCHWELCOME;
-            }
-        } else if (s->channel_count == 5) {
+        if (s->layer_count < 0 && s->channel_count >= 5) {
             if (s->channel_depth == 8) {
                 avctx->pix_fmt = AV_PIX_FMT_GBRAP;
             } else if (s->channel_depth == 16) {
@@ -354,22 +359,26 @@ static int decode_frame(AVCodecContext *avctx, AVFrame *picture,
                 avpriv_report_missing_feature(avctx, "channel depth %d for cmyk", s->channel_depth);
                 return AVERROR_PATCHWELCOME;
             }
+            s->primary_channels = 5;
+        } else if (s->channel_count >= 4) {
+            if (s->channel_depth == 8) {
+                avctx->pix_fmt = AV_PIX_FMT_GBRP;
+            } else if (s->channel_depth == 16) {
+                avctx->pix_fmt = AV_PIX_FMT_GBRP16BE;
+            } else {
+                avpriv_report_missing_feature(avctx, "channel depth %d for cmyk", s->channel_depth);
+                return AVERROR_PATCHWELCOME;
+            }
+            s->primary_channels = 4;
         } else {
-            avpriv_report_missing_feature(avctx, "channel count %d for cmyk", s->channel_count);
-            return AVERROR_PATCHWELCOME;
+            av_log(s->avctx, AV_LOG_ERROR,
+                   "Invalid cmyk file (channel_count %d)\n",
+                   s->channel_count);
+            return AVERROR_INVALIDDATA;
         }
         break;
     case PSD_RGB:
-        if (s->channel_count == 3) {
-            if (s->channel_depth == 8) {
-                avctx->pix_fmt = AV_PIX_FMT_GBRP;
-            } else if (s->channel_depth == 16) {
-                avctx->pix_fmt = AV_PIX_FMT_GBRP16BE;
-            } else {
-                avpriv_report_missing_feature(avctx, "channel depth %d for rgb", s->channel_depth);
-                return AVERROR_PATCHWELCOME;
-            }
-        } else if (s->channel_count == 4) {
+        if (s->layer_count < 0 && s->channel_count >= 4) {
             if (s->channel_depth == 8) {
                 avctx->pix_fmt = AV_PIX_FMT_GBRAP;
             } else if (s->channel_depth == 16) {
@@ -378,15 +387,38 @@ static int decode_frame(AVCodecContext *avctx, AVFrame *picture,
                 avpriv_report_missing_feature(avctx, "channel depth %d for rgb", s->channel_depth);
                 return AVERROR_PATCHWELCOME;
             }
+            s->primary_channels = 4;
+        } else if (s->channel_count >= 3) {
+            if (s->channel_depth == 8) {
+                avctx->pix_fmt = AV_PIX_FMT_GBRP;
+            } else if (s->channel_depth == 16) {
+                avctx->pix_fmt = AV_PIX_FMT_GBRP16BE;
+            } else {
+                avpriv_report_missing_feature(avctx, "channel depth %d for rgb", s->channel_depth);
+                return AVERROR_PATCHWELCOME;
+            }
+            s->primary_channels = 3;
         } else {
-            avpriv_report_missing_feature(avctx, "channel count %d for rgb", s->channel_count);
-            return AVERROR_PATCHWELCOME;
+            av_log(s->avctx, AV_LOG_ERROR,
+                   "Invalid rgb file (channel_count %d)\n",
+                   s->channel_count);
+            return AVERROR_INVALIDDATA;
         }
         break;
     case PSD_DUOTONE:
         av_log(avctx, AV_LOG_WARNING, "ignoring unknown duotone specification.\n");
     case PSD_GRAYSCALE:
-        if (s->channel_count == 1) {
+        if (s->layer_count < 0 && s->channel_count >= 2) {
+            if (s->channel_depth == 8) {
+                avctx->pix_fmt = AV_PIX_FMT_YA8;
+            } else if (s->channel_depth == 16) {
+                avctx->pix_fmt = AV_PIX_FMT_YA16BE;
+            } else {
+                avpriv_report_missing_feature(avctx, "channel depth %d for grayscale", s->channel_depth);
+                return AVERROR_PATCHWELCOME;
+            }
+            s->primary_channels = 2;
+        } else if (s->channel_count >= 1) {
             if (s->channel_depth == 8) {
                 avctx->pix_fmt = AV_PIX_FMT_GRAY8;
             } else if (s->channel_depth == 16) {
@@ -397,18 +429,12 @@ static int decode_frame(AVCodecContext *avctx, AVFrame *picture,
                 avpriv_report_missing_feature(avctx, "channel depth %d for grayscale", s->channel_depth);
                 return AVERROR_PATCHWELCOME;
             }
-        } else if (s->channel_count == 2) {
-            if (s->channel_depth == 8) {
-                avctx->pix_fmt = AV_PIX_FMT_YA8;
-            } else if (s->channel_depth == 16) {
-                avctx->pix_fmt = AV_PIX_FMT_YA16BE;
-            } else {
-                avpriv_report_missing_feature(avctx, "channel depth %d for grayscale", s->channel_depth);
-                return AVERROR_PATCHWELCOME;
-            }
+            s->primary_channels = 1;
         } else {
-            avpriv_report_missing_feature(avctx, "channel count %d for grayscale", s->channel_count);
-            return AVERROR_PATCHWELCOME;
+            av_log(s->avctx, AV_LOG_ERROR,
+                   "Invalid grayscale file (channel_count %d)\n",
+                   s->channel_count);
+            return AVERROR_INVALIDDATA;
         }
         break;
     default:
@@ -446,10 +472,10 @@ static int decode_frame(AVCodecContext *avctx, AVFrame *picture,
     /* Store data */
     if ((avctx->pix_fmt == AV_PIX_FMT_YA8)||(avctx->pix_fmt == AV_PIX_FMT_YA16BE)){/* Interleaved */
         ptr = picture->data[0];
-        for (c = 0; c < s->channel_count; c++) {
+        for (c = 0; c < 2; c++) {
             for (y = 0; y < s->height; y++) {
                 for (x = 0; x < s->width; x++) {
-                    index_out = y * picture->linesize[0] + x * s->channel_count * s->pixel_size + c * s->pixel_size;
+                    index_out = y * picture->linesize[0] + x * 2 * s->pixel_size + c * s->pixel_size;
                     for (p = 0; p < s->pixel_size; p++) {
                         ptr[index_out + p] = *ptr_data;
                         ptr_data ++;
@@ -518,10 +544,10 @@ static int decode_frame(AVCodecContext *avctx, AVFrame *picture,
             }
         }
     } else {/* Planar */
-        if (s->channel_count == 1)/* gray 8 or gray 16be */
+        if (s->primary_channels == 1)/* bitmap, indexed, grayscale */
             eq_channel[0] = 0;/* assign first channel, to first plane */
 
-        for (c = 0; c < s->channel_count; c++) {
+        for (c = 0; c < s->primary_channels; c++) {
             plane_number = eq_channel[c];
             ptr = picture->data[plane_number];/* get the right plane */
             for (y = 0; y < s->height; y++) {
-- 
2.49.1


>From 306907257009904d64e7bddd51d1eac39e7ead6d Mon Sep 17 00:00:00 2001
From: John Chadwick <john@jchw.io>
Date: Wed, 19 Nov 2025 00:17:21 -0500
Subject: [PATCH 2/3] tests/ref/fate: Add psd-rgbxx

This is an rgb test image with two auxilliary channels and no alpha
channels.
---
 tests/ref/fate/psd-rgbxx | 6 ++++++
 1 file changed, 6 insertions(+)
 create mode 100644 tests/ref/fate/psd-rgbxx

diff --git a/tests/ref/fate/psd-rgbxx b/tests/ref/fate/psd-rgbxx
new file mode 100644
index 0000000000..7f8f550afe
--- /dev/null
+++ b/tests/ref/fate/psd-rgbxx
@@ -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,    49152, 0xe0013dee
-- 
2.49.1


>From a075417d629858aca2ab07b92d6b1bdb3a43bfc4 Mon Sep 17 00:00:00 2001
From: John Chadwick <john@jchw.io>
Date: Wed, 19 Nov 2025 00:17:34 -0500
Subject: [PATCH 3/3] tests/fate/image: Add psd-rgbxx

---
 tests/fate/image.mak | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/fate/image.mak b/tests/fate/image.mak
index 3facd8aaa8..be4b9623ad 100644
--- a/tests/fate/image.mak
+++ b/tests/fate/image.mak
@@ -444,7 +444,7 @@ FATE_PSD-$(call DEMDEC, IMAGE2, PSD, SCALE_FILTER) += fate-psd-$(1)
 fate-psd-$(1): CMD = framecrc -i $(TARGET_SAMPLES)/psd/lena-$(1).psd -sws_flags +accurate_rnd+bitexact -pix_fmt rgb24 -vf scale
 endef
 
-PSD_COLORSPACES = gray8 gray16 rgb24 rgb48 rgba rgba64 ya8 ya16
+PSD_COLORSPACES = gray8 gray16 rgb24 rgb48 rgba rgbxx rgba64 ya8 ya16
 $(foreach CLSP,$(PSD_COLORSPACES),$(eval $(call FATE_IMGSUITE_PSD,$(CLSP))))
 
 FATE_PSD += fate-psd-lena-127x127-rgb24
-- 
2.49.1

_______________________________________________
ffmpeg-devel mailing list -- ffmpeg-devel@ffmpeg.org
To unsubscribe send an email to ffmpeg-devel-leave@ffmpeg.org

^ permalink raw reply	[flat|nested] only message in thread

only message in thread, other threads:[~2025-11-19  5:33 UTC | newest]

Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-11-19  5:26 [FFmpeg-devel] [PATCH] avcodec/psd: Support auxiliary channels (PR #20966) jchw via ffmpeg-devel

Git Inbox Mirror of the ffmpeg-devel mailing list - see https://ffmpeg.org/mailman/listinfo/ffmpeg-devel

This inbox may be cloned and mirrored by anyone:

	git clone --mirror https://master.gitmailbox.com/ffmpegdev/0 ffmpegdev/git/0.git

	# If you have public-inbox 1.1+ installed, you may
	# initialize and index your mirror using the following commands:
	public-inbox-init -V2 ffmpegdev ffmpegdev/ https://master.gitmailbox.com/ffmpegdev \
		ffmpegdev@gitmailbox.com
	public-inbox-index ffmpegdev

Example config snippet for mirrors.


AGPL code for this site: git clone https://public-inbox.org/public-inbox.git