PSD: add support for GrayA (8/16/32 bit) and Gray 32 bit

This commit is contained in:
Mirco Miranda
2025-11-16 10:15:10 +01:00
parent 472ff92b96
commit 8061500b79
12 changed files with 136 additions and 6 deletions

View File

@ -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 CMYK if they have 2 or more channels.
- Multichannel images are treated as Grayscale if they have 1 channel. - Multichannel images are treated as Grayscale if they have 1 channel.
- Duotone images are treated as grayscale images. - 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. - Extra channels other than alpha are discarded.
The following defines can be defined in cmake to modify the behavior of the The following defines can be defined in cmake to modify the behavior of the

View File

@ -2,8 +2,8 @@
{ {
"fileName" : "32bit_grayscale.png", "fileName" : "32bit_grayscale.png",
"colorSpace" : { "colorSpace" : {
"description" : "Linear Grayscale Profile", "description" : "RGB emulation of \"Linear Grayscale Profile\"",
"colorModel" : "Gray", "colorModel" : "Rgb",
"primaries" : "Custom", "primaries" : "Custom",
"transferFunction" : "Linear", "transferFunction" : "Linear",
"gamma" : 1 "gamma" : 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

View File

@ -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
}
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

View File

@ -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
}
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

View File

@ -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
}
}
]

View File

@ -546,8 +546,21 @@ static bool setColorSpace(QImage &img, const PSDImageResourceSection &irs)
auto cs = QColorSpace::fromIccProfile(irb.data); auto cs = QColorSpace::fromIccProfile(irb.data);
if (!cs.isValid()) if (!cs.isValid())
return false; 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); img.setColorSpace(cs);
return true; return img.colorSpace().isValid();
} }
/*! /*!
@ -800,6 +813,14 @@ static QImage::Format imageFormat(const PSDHeader &header, bool alpha)
} }
break; break;
case CM_GRAYSCALE: 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: case CM_DUOTONE:
format = header.depth == 8 ? QImage::Format_Grayscale8 : QImage::Format_Grayscale16; format = header.depth == 8 ? QImage::Format_Grayscale8 : QImage::Format_Grayscale16;
break; break;
@ -1309,7 +1330,6 @@ bool PSDHandler::read(QImage *image)
} }
auto imgChannels = imageChannels(img.format()); 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 raw_count = qsizetype(header.width * header.depth + 7) / 8;
auto native_cmyk = img.format() == CMYK_FORMAT; auto native_cmyk = img.format() == CMYK_FORMAT;
@ -1417,6 +1437,14 @@ bool PSDHandler::read(QImage *image)
else if (header.depth == 32) else if (header.depth == 32)
premulConversion<float>(scanLine, header.width, 3, header.channel_count, PremulConversion::PS2P); premulConversion<float>(scanLine, header.width, 3, header.channel_count, PremulConversion::PS2P);
} }
if (header.color_mode == CM_GRAYSCALE) {
if (header.depth == 8)
premulConversion<quint8>(scanLine, header.width, 1, header.channel_count, PremulConversion::PS2P);
else if (header.depth == 16)
premulConversion<quint16>(scanLine, header.width, 1, header.channel_count, PremulConversion::PS2P);
else if (header.depth == 32)
premulConversion<float>(scanLine, header.width, 1, header.channel_count, PremulConversion::PS2P);
}
} }
// Conversion to RGB // Conversion to RGB
@ -1454,9 +1482,21 @@ bool PSDHandler::read(QImage *image)
else if (header.depth == 32) else if (header.depth == 32)
rawChannelsCopy<float>(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width); rawChannelsCopy<float>(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<quint8>(img.scanLine(y), imgChannels, c, psdScanline.data(), header.channel_count, sc, header.width);
else if (header.depth == 16)
rawChannelCopy<quint16>(img.scanLine(y), imgChannels, c, psdScanline.data(), header.channel_count, sc, header.width);
else if (header.depth == 32)
rawChannelCopy<float>(img.scanLine(y), imgChannels, c, psdScanline.data(), header.channel_count, sc, header.width);
}
}
} }
} else { } else {
// Linear read (no position jumps): optimized code usable only for the colorspaces supported by QImage // 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 c = 0; c < channel_num; ++c) {
for (qint32 y = 0, h = header.height; y < h; ++y) { for (qint32 y = 0, h = header.height; y < h; ++y) {
auto&& strideSize = strides.at(c * qsizetype(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 // 32-bits float images: RGB/RGBA
planarToChunchy<float>(scanLine, rawStride.data(), header.width, c, imgChannels); planarToChunchy<float>(scanLine, rawStride.data(), header.width, c, imgChannels);
} else if (header.depth == 32 && header.color_mode == CM_GRAYSCALE) { } else if (header.depth == 32 && header.color_mode == CM_GRAYSCALE) {
// 32-bits float images: Grayscale (coverted to equivalent integer 16-bits) if (imgChannels >= 3) { // GRAY to RGB
planarToChunchyFloatToUInt16<float>(scanLine, rawStride.data(), header.width, c, imgChannels); planarToChunchy<float>(scanLine, rawStride.data(), header.width, 0, imgChannels);
planarToChunchy<float>(scanLine, rawStride.data(), header.width, 1, imgChannels);
planarToChunchy<float>(scanLine, rawStride.data(), header.width, 2, imgChannels);
} else { // 32-bits float images: Grayscale (coverted to equivalent integer 16-bits)
planarToChunchyFloatToUInt16<float>(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) { if (header.color_mode == CM_RGB && header.channel_count > 3) {
return false; // supposing extra channel as alpha 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); return IsSupported(header);