diff --git a/README.md b/README.md index cd143c8..4d870d2 100644 --- a/README.md +++ b/README.md @@ -414,6 +414,8 @@ PSD support has the following limitations: - Multichannel images are treated as CMYK if they have 2 or more channels. - Multichannel images are treated as Grayscale if they have 1 channel. - Duotone images are treated as grayscale images. +- Grayscale images with alpha channel or at 32 bit depth are converted to + RGBA due to the lack of the appropriate Qt grayscale container. - Extra channels other than alpha are discarded. The following defines can be defined in cmake to modify the behavior of the diff --git a/autotests/read/psd/32bit_grayscale.psd.json b/autotests/read/psd/32bit_grayscale.psd.json index 2b0e102..faf08bc 100644 --- a/autotests/read/psd/32bit_grayscale.psd.json +++ b/autotests/read/psd/32bit_grayscale.psd.json @@ -2,8 +2,8 @@ { "fileName" : "32bit_grayscale.png", "colorSpace" : { - "description" : "Linear Grayscale Profile", - "colorModel" : "Gray", + "description" : "RGB emulation of \"Linear Grayscale Profile\"", + "colorModel" : "Rgb", "primaries" : "Custom", "transferFunction" : "Linear", "gamma" : 1 diff --git a/autotests/read/psd/testcard_graya16.png b/autotests/read/psd/testcard_graya16.png new file mode 100644 index 0000000..278d759 Binary files /dev/null and b/autotests/read/psd/testcard_graya16.png differ diff --git a/autotests/read/psd/testcard_graya16.psd b/autotests/read/psd/testcard_graya16.psd new file mode 100644 index 0000000..9634f6e Binary files /dev/null and b/autotests/read/psd/testcard_graya16.psd differ diff --git a/autotests/read/psd/testcard_graya16.psd.json b/autotests/read/psd/testcard_graya16.psd.json new file mode 100644 index 0000000..cad7551 --- /dev/null +++ b/autotests/read/psd/testcard_graya16.psd.json @@ -0,0 +1,26 @@ +[ + { + "fileName" : "testcard_graya16.png", + "colorSpace" : { + "description" : "RGB emulation of \"Gray Gamma 2.2\"", + "colorModel" : "Rgb", + "primaries" : "SRgb", + "transferFunction" : "Gamma", + "gamma" : 2.19922 + }, + "metadata" : [ + { + "key" : "ModificationDate", + "value" : "2025-11-17T07:27:47" + }, + { + "key" : "Software" , + "value" : "Adobe Photoshop 26.11 (Windows)" + } + ], + "resolution" : { + "dotsPerMeterX" : 11811, + "dotsPerMeterY" : 11811 + } + } +] diff --git a/autotests/read/psd/testcard_graya32.png b/autotests/read/psd/testcard_graya32.png new file mode 100644 index 0000000..3b58f54 Binary files /dev/null and b/autotests/read/psd/testcard_graya32.png differ diff --git a/autotests/read/psd/testcard_graya32.psd b/autotests/read/psd/testcard_graya32.psd new file mode 100644 index 0000000..3d70c12 Binary files /dev/null and b/autotests/read/psd/testcard_graya32.psd differ diff --git a/autotests/read/psd/testcard_graya32.psd.json b/autotests/read/psd/testcard_graya32.psd.json new file mode 100644 index 0000000..e63a800 --- /dev/null +++ b/autotests/read/psd/testcard_graya32.psd.json @@ -0,0 +1,26 @@ +[ + { + "fileName" : "testcard_graya32.png", + "colorSpace" : { + "description" : "RGB emulation of \"Profilo scala di grigio lineare\"", + "colorModel" : "Rgb", + "primaries" : "Custom", + "transferFunction" : "Linear", + "gamma" : 1 + }, + "metadata" : [ + { + "key" : "ModificationDate", + "value" : "2025-11-17T07:29:19" + }, + { + "key" : "Software" , + "value" : "Adobe Photoshop 26.11 (Windows)" + } + ], + "resolution" : { + "dotsPerMeterX" : 11811, + "dotsPerMeterY" : 11811 + } + } +] diff --git a/autotests/read/psd/testcard_graya8.png b/autotests/read/psd/testcard_graya8.png new file mode 100644 index 0000000..fa31ba3 Binary files /dev/null and b/autotests/read/psd/testcard_graya8.png differ diff --git a/autotests/read/psd/testcard_graya8.psd b/autotests/read/psd/testcard_graya8.psd new file mode 100644 index 0000000..710bfbe Binary files /dev/null and b/autotests/read/psd/testcard_graya8.psd differ diff --git a/autotests/read/psd/testcard_graya8.psd.json b/autotests/read/psd/testcard_graya8.psd.json new file mode 100644 index 0000000..29cbd41 --- /dev/null +++ b/autotests/read/psd/testcard_graya8.psd.json @@ -0,0 +1,28 @@ +[ + { + "fileName" : "testcard_graya8.png", + "fuzziness" : 1, + "perceptiveFuzziness" : true, + "colorSpace" : { + "description" : "RGB emulation of \"Gray Gamma 2.2\"", + "colorModel" : "Rgb", + "primaries" : "SRgb", + "transferFunction" : "Gamma", + "gamma" : 2.19922 + }, + "metadata" : [ + { + "key" : "ModificationDate", + "value" : "2025-11-17T07:28:50" + }, + { + "key" : "Software" , + "value" : "Adobe Photoshop 26.11 (Windows)" + } + ], + "resolution" : { + "dotsPerMeterX" : 11811, + "dotsPerMeterY" : 11811 + } + } +] diff --git a/src/imageformats/psd.cpp b/src/imageformats/psd.cpp index f1c1e60..5305dad 100644 --- a/src/imageformats/psd.cpp +++ b/src/imageformats/psd.cpp @@ -546,8 +546,21 @@ static bool setColorSpace(QImage &img, const PSDImageResourceSection &irs) auto cs = QColorSpace::fromIccProfile(irb.data); if (!cs.isValid()) return false; + + if (cs.colorModel() == QColorSpace::ColorModel::Gray && img.pixelFormat().colorModel() != QPixelFormat::Grayscale) { + // I created an RGB from a grayscale without using color profile conversion (fast). + // I'll try to create an RGB profile that looks the same. + if (cs.transferFunction() != QColorSpace::TransferFunction::Custom) { + auto tmp = QColorSpace(QColorSpace::Primaries::SRgb, cs.transferFunction(), cs.gamma()); + tmp.setWhitePoint(cs.whitePoint()); + tmp.setDescription(QStringLiteral("RGB emulation of \"%1\"").arg(cs.description())); + if (tmp.isValid()) + cs = tmp; + } + } + img.setColorSpace(cs); - return true; + return img.colorSpace().isValid(); } /*! @@ -800,6 +813,14 @@ static QImage::Format imageFormat(const PSDHeader &header, bool alpha) } break; case CM_GRAYSCALE: + if (header.depth == 32) { + format = !alpha ? QImage::Format_RGBX32FPx4 : QImage::Format_RGBA32FPx4_Premultiplied; + } else if (header.depth == 16) { + format = !alpha ? QImage::Format_Grayscale16 : QImage::Format_RGBA64_Premultiplied; + } else { + format = !alpha ? QImage::Format_Grayscale8 : QImage::Format_RGBA8888_Premultiplied; + } + break; case CM_DUOTONE: format = header.depth == 8 ? QImage::Format_Grayscale8 : QImage::Format_Grayscale16; break; @@ -1309,7 +1330,6 @@ bool PSDHandler::read(QImage *image) } auto imgChannels = imageChannels(img.format()); - auto channel_num = std::min(qint32(header.channel_count), imgChannels); auto raw_count = qsizetype(header.width * header.depth + 7) / 8; auto native_cmyk = img.format() == CMYK_FORMAT; @@ -1417,6 +1437,14 @@ bool PSDHandler::read(QImage *image) else if (header.depth == 32) premulConversion(scanLine, header.width, 3, header.channel_count, PremulConversion::PS2P); } + if (header.color_mode == CM_GRAYSCALE) { + if (header.depth == 8) + premulConversion(scanLine, header.width, 1, header.channel_count, PremulConversion::PS2P); + else if (header.depth == 16) + premulConversion(scanLine, header.width, 1, header.channel_count, PremulConversion::PS2P); + else if (header.depth == 32) + premulConversion(scanLine, header.width, 1, header.channel_count, PremulConversion::PS2P); + } } // Conversion to RGB @@ -1454,9 +1482,21 @@ bool PSDHandler::read(QImage *image) else if (header.depth == 32) rawChannelsCopy(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width); } + if (header.color_mode == CM_GRAYSCALE) { + for (auto c = 0; c < imgChannels; ++c) { // GRAYA to RGBA + auto sc = qBound(0, c - 2, int(header.channel_count)); + if (header.depth == 8) + rawChannelCopy(img.scanLine(y), imgChannels, c, psdScanline.data(), header.channel_count, sc, header.width); + else if (header.depth == 16) + rawChannelCopy(img.scanLine(y), imgChannels, c, psdScanline.data(), header.channel_count, sc, header.width); + else if (header.depth == 32) + rawChannelCopy(img.scanLine(y), imgChannels, c, psdScanline.data(), header.channel_count, sc, header.width); + } + } } } else { // Linear read (no position jumps): optimized code usable only for the colorspaces supported by QImage + auto channel_num = std::min(qint32(header.channel_count), header.color_mode == CM_GRAYSCALE ? 1 : imgChannels); for (qint32 c = 0; c < channel_num; ++c) { for (qint32 y = 0, h = header.height; y < h; ++y) { auto&& strideSize = strides.at(c * qsizetype(h) + y); @@ -1485,8 +1525,13 @@ bool PSDHandler::read(QImage *image) // 32-bits float images: RGB/RGBA planarToChunchy(scanLine, rawStride.data(), header.width, c, imgChannels); } else if (header.depth == 32 && header.color_mode == CM_GRAYSCALE) { - // 32-bits float images: Grayscale (coverted to equivalent integer 16-bits) - planarToChunchyFloatToUInt16(scanLine, rawStride.data(), header.width, c, imgChannels); + if (imgChannels >= 3) { // GRAY to RGB + planarToChunchy(scanLine, rawStride.data(), header.width, 0, imgChannels); + planarToChunchy(scanLine, rawStride.data(), header.width, 1, imgChannels); + planarToChunchy(scanLine, rawStride.data(), header.width, 2, imgChannels); + } else { // 32-bits float images: Grayscale (coverted to equivalent integer 16-bits) + planarToChunchyFloatToUInt16(scanLine, rawStride.data(), header.width, c, imgChannels); + } } } } @@ -1622,6 +1667,9 @@ bool PSDHandler::canRead(QIODevice *device) if (header.color_mode == CM_RGB && header.channel_count > 3) { return false; // supposing extra channel as alpha } + if (header.color_mode == CM_GRAYSCALE && (header.channel_count > 1 || header.depth == 32)) { + return false; // supposing extra channel as alpha + } } return IsSupported(header);