diff --git a/autotests/read/psd/53alphas.png b/autotests/read/psd/53alphas.png index 3987b5e..442d360 100644 Binary files a/autotests/read/psd/53alphas.png and b/autotests/read/psd/53alphas.png differ diff --git a/autotests/read/psd/argb16-raw-affinityphoto-1.8.5.png b/autotests/read/psd/argb16-raw-affinityphoto-1.8.5.png index 9af91c1..4945a7d 100644 Binary files a/autotests/read/psd/argb16-raw-affinityphoto-1.8.5.png and b/autotests/read/psd/argb16-raw-affinityphoto-1.8.5.png differ diff --git a/autotests/read/psd/birthday.png b/autotests/read/psd/birthday.png index 84abb36..a79391f 100644 Binary files a/autotests/read/psd/birthday.png and b/autotests/read/psd/birthday.png differ diff --git a/autotests/read/psd/laba_16bit.png b/autotests/read/psd/laba_16bit.png index d3d317c..929df20 100644 Binary files a/autotests/read/psd/laba_16bit.png and b/autotests/read/psd/laba_16bit.png differ diff --git a/autotests/read/psd/laba_8bit.png b/autotests/read/psd/laba_8bit.png index c691745..fe14b7e 100644 Binary files a/autotests/read/psd/laba_8bit.png and b/autotests/read/psd/laba_8bit.png differ diff --git a/src/imageformats/psd.cpp b/src/imageformats/psd.cpp index d1a2484..3ea66d7 100644 --- a/src/imageformats/psd.cpp +++ b/src/imageformats/psd.cpp @@ -9,8 +9,8 @@ */ /* - * This code is based on Thacher Ulrich PSD loading code released - * into the public domain. See: http://tulrich.com/geekstuff/ + * The early version of this code was based on Thacher Ulrich PSD loading code + * released into the public domain. See: http://tulrich.com/geekstuff/ */ /* @@ -733,9 +733,9 @@ static QImage::Format imageFormat(const PSDHeader &header, bool alpha) switch(header.color_mode) { case CM_RGB: if (header.depth == 16 || header.depth == 32) - format = header.channel_count < 4 || !alpha ? QImage::Format_RGBX64 : QImage::Format_RGBA64; + format = header.channel_count < 4 || !alpha ? QImage::Format_RGBX64 : QImage::Format_RGBA64_Premultiplied; else - format = header.channel_count < 4 || !alpha ? QImage::Format_RGB888 : QImage::Format_RGBA8888; + format = header.channel_count < 4 || !alpha ? QImage::Format_RGB888 : QImage::Format_RGBA8888_Premultiplied; break; case CM_MULTICHANNEL: // Treat MCH as CMYK (number of channel check is done in IsSupported()) case CM_CMYK: // Photoshop supports CMYK/MCH 8-bits and 16-bits only @@ -814,7 +814,7 @@ inline void planarToChunchy(uchar *target, const char *source, qint32 width, qin auto s = reinterpret_cast(source); auto t = reinterpret_cast(target); for (qint32 x = 0; x < width; ++x) { - t[x*cn+c] = xchg(s[x]); + t[x * cn + c] = xchg(s[x]); } } @@ -826,7 +826,45 @@ inline void planarToChunchyFloat(uchar *target, const char *source, qint32 width for (qint32 x = 0; x < width; ++x) { auto tmp = xchg(s[x]); auto ftmp = (*reinterpret_cast(&tmp) - double(min)) / (double(max) - double(min)); - t[x*cn+c] = quint16(std::min(ftmp * std::numeric_limits::max() + 0.5, double(std::numeric_limits::max()))); + t[x * cn + c] = quint16(std::min(ftmp * std::numeric_limits::max() + 0.5, double(std::numeric_limits::max()))); + } +} + +enum class PremulConversion { + PS2P, // Photoshop premul to qimage premul (required by RGB) + PS2A, // Photoshop premul to unassociated alpha (required by RGB, CMYK and L* components of LAB) + PSLab2A // Photoshop premul to unassociated alpha (required by a* and b* components of LAB) +}; + +template +inline void premulConversion(char *stride, qint32 width, qint32 ac, qint32 cn, const PremulConversion &conv) +{ + auto s = reinterpret_cast(stride); + auto max = qint64(std::numeric_limits::max()); + + for (qint32 c = 0; c < ac; ++c) { + if (conv == PremulConversion::PS2P) { + for (qint32 x = 0; x < width; ++x) { + auto xcn = x * cn; + auto alpha = *(s + xcn + ac); + if (alpha > 0) + *(s + xcn + c) = *(s + xcn + c) + alpha - max; + } + } else if (conv == PremulConversion::PS2A || (conv == PremulConversion::PSLab2A && c == 0)) { + for (qint32 x = 0; x < width; ++x) { + auto xcn = x * cn; + auto alpha = *(s + xcn + ac); + if (alpha > 0) + *(s + xcn + c) = ((*(s + xcn + c) + alpha - max) * max + alpha / 2) / alpha; + } + } else if (conv == PremulConversion::PSLab2A) { + for (qint32 x = 0; x < width; ++x) { + auto xcn = x * cn; + auto alpha = *(s + xcn + ac); + if (alpha > 0) + *(s + xcn + c) = ((*(s + xcn + c) + (alpha - max + 1) / 2) * max + alpha / 2) / alpha; + } + } } } @@ -839,6 +877,18 @@ inline void monoInvert(uchar *target, const char* source, qint32 bytes) } } +template +inline void rawChannelsCopy(uchar *target, qint32 targetChannels, const char *source, qint32 sourceChannels, qint32 width) +{ + auto s = reinterpret_cast(source); + auto t = reinterpret_cast(target); + for (qint32 c = 0, cs = std::min(targetChannels, sourceChannels); c < cs; ++c) { + for (qint32 x = 0; x < width; ++x) { + t[x * targetChannels + c] = s[x * sourceChannels + c]; + } + } +} + template inline void cmykToRgb(uchar *target, qint32 targetChannels, const char *source, qint32 sourceChannels, qint32 width, bool alpha = false) { @@ -1060,7 +1110,15 @@ static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img) QByteArray rawStride; rawStride.resize(raw_count); - if (header.color_mode == CM_CMYK || header.color_mode == CM_LABCOLOR || header.color_mode == CM_MULTICHANNEL) { + // clang-format off + // checks the need of color conversion (that requires random access to the image) + auto randomAccess = (header.color_mode == CM_CMYK) || + (header.color_mode == CM_LABCOLOR) || + (header.color_mode == CM_MULTICHANNEL) || + (header.color_mode != CM_INDEXED && img.hasAlphaChannel()); + // clang-format on + + if (randomAccess) { // In order to make a colorspace transformation, we need all channels of a scanline QByteArray psdScanline; psdScanline.resize(qsizetype(header.width * std::min(header.depth, quint16(16)) * header.channel_count + 7) / 8); @@ -1080,31 +1138,56 @@ static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img) auto scanLine = reinterpret_cast(psdScanline.data()); if (header.depth == 8) { planarToChunchy(scanLine, rawStride.data(), header.width, c, header.channel_count); - } - else if (header.depth == 16) { + } else if (header.depth == 16) { planarToChunchy(scanLine, rawStride.data(), header.width, c, header.channel_count); - } - else if (header.depth == 32) { // Not currently used + } else if (header.depth == 32) { planarToChunchyFloat(scanLine, rawStride.data(), header.width, c, header.channel_count); } } + // Convert premultiplied data to unassociated data + if (img.hasAlphaChannel()) { + if (header.color_mode == CM_CMYK) { + if (header.depth == 8) + premulConversion(psdScanline.data(), header.width, 4, header.channel_count, PremulConversion::PS2A); + else if (header.depth == 16) + premulConversion(psdScanline.data(), header.width, 4, header.channel_count, PremulConversion::PS2A); + } + if (header.color_mode == CM_LABCOLOR) { + if (header.depth == 8) + premulConversion(psdScanline.data(), header.width, 3, header.channel_count, PremulConversion::PSLab2A); + else if (header.depth == 16) + premulConversion(psdScanline.data(), header.width, 3, header.channel_count, PremulConversion::PSLab2A); + } + if (header.color_mode == CM_RGB) { + if (header.depth == 8) + premulConversion(psdScanline.data(), header.width, 3, header.channel_count, PremulConversion::PS2P); + else if (header.depth == 16 || header.depth == 32) + premulConversion(psdScanline.data(), header.width, 3, header.channel_count, PremulConversion::PS2P); + } + } + // Conversion to RGB if (header.color_mode == CM_CMYK || header.color_mode == CM_MULTICHANNEL) { if (header.depth == 8) cmykToRgb(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width, alpha); - else + else if (header.depth == 16) cmykToRgb(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width, alpha); } if (header.color_mode == CM_LABCOLOR) { if (header.depth == 8) labToRgb(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width, alpha); - else + else if (header.depth == 16) labToRgb(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width, alpha); } + if (header.color_mode == CM_RGB) { + if (header.depth == 8) + rawChannelsCopy(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width); + else if (header.depth == 16 || header.depth == 32) + rawChannelsCopy(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width); + } } - } - else { + } else { // Linear read (no position jumps): optimized code usable only for the colorspaces supported by QImage for (qint32 c = 0; c < channel_num; ++c) { for (qint32 y = 0, h = header.height; y < h; ++y) { @@ -1115,16 +1198,13 @@ static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img) } auto scanLine = img.scanLine(y); - if (header.depth == 1) { // Bitmap + if (header.depth == 1) { // Bitmap monoInvert(scanLine, rawStride.data(), std::min(rawStride.size(), img.bytesPerLine())); - } - else if (header.depth == 8) { // 8-bits images: Indexed, Grayscale, RGB/RGBA + } else if (header.depth == 8) { // 8-bits images: Indexed, Grayscale, RGB/RGBA planarToChunchy(scanLine, rawStride.data(), header.width, c, imgChannels); - } - else if (header.depth == 16) { // 16-bits integer images: Grayscale, RGB/RGBA + } else if (header.depth == 16) { // 16-bits integer images: Grayscale, RGB/RGBA planarToChunchy(scanLine, rawStride.data(), header.width, c, imgChannels); - } - else if (header.depth == 32) { // 32-bits float images: Grayscale, RGB/RGBA (coverted to equivalent integer 16-bits) + } else if (header.depth == 32) { // 32-bits float images: Grayscale, RGB/RGBA (coverted to equivalent integer 16-bits) planarToChunchyFloat(scanLine, rawStride.data(), header.width, c, imgChannels); } } @@ -1267,6 +1347,9 @@ bool PSDHandler::canRead(QIODevice *device) if (header.color_mode == CM_CMYK || header.color_mode == CM_LABCOLOR || header.color_mode == CM_MULTICHANNEL) { return false; } + if (header.color_mode == CM_RGB && header.channel_count > 3) { + return false; // supposing extra channel as alpha + } } return IsSupported(header);