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 <[email protected]> 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 <[email protected]> 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 <[email protected]> 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 -- [email protected] To unsubscribe send an email to [email protected]
