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..64d339f 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 8b28979..eae7c37 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,11 +733,11 @@ static QImage::Format imageFormat(const PSDHeader &header, bool alpha) switch(header.color_mode) { case CM_RGB: if (header.depth == 32) - format = header.channel_count < 4 || !alpha ? QImage::Format_RGBX32FPx4 : QImage::Format_RGBA32FPx4; + format = header.channel_count < 4 || !alpha ? QImage::Format_RGBX32FPx4 : QImage::Format_RGBA32FPx4_Premultiplied; else if (header.depth == 16) - 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 @@ -831,7 +831,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]); } } @@ -843,7 +843,48 @@ 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); + // NOTE: to avoid overflows, max is casted to qint64: that is possible because max is always an integer (even if T is float) + auto max = qint64(std::numeric_limits::is_integer ? std::numeric_limits::max() : 1); + + 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; + } + } } } @@ -856,6 +897,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) { @@ -1077,10 +1130,18 @@ 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); + psdScanline.resize(qsizetype(header.width * header.depth * header.channel_count + 7) / 8); for (qint32 y = 0, h = header.height; y < h; ++y) { for (qint32 c = 0; c < header.channel_count; ++c) { auto strideNumber = c * qsizetype(h) + y; @@ -1101,21 +1162,57 @@ static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img) else if (header.depth == 16) { planarToChunchy(scanLine, rawStride.data(), header.width, c, header.channel_count); } + else if (header.depth == 32) { + planarToChunchy(scanLine, rawStride.data(), header.width, c, header.channel_count); + } + } + + // Convert premultiplied data to unassociated data + if (img.hasAlphaChannel()) { + auto scanLine = reinterpret_cast(psdScanline.data()); + if (header.color_mode == CM_CMYK) { + if (header.depth == 8) + premulConversion(scanLine, header.width, 4, header.channel_count, PremulConversion::PS2A); + else if (header.depth == 16) + premulConversion(scanLine, header.width, 4, header.channel_count, PremulConversion::PS2A); + } + if (header.color_mode == CM_LABCOLOR) { + if (header.depth == 8) + premulConversion(scanLine, header.width, 3, header.channel_count, PremulConversion::PSLab2A); + else if (header.depth == 16) + premulConversion(scanLine, header.width, 3, header.channel_count, PremulConversion::PSLab2A); + } + if (header.color_mode == CM_RGB) { + if (header.depth == 8) + premulConversion(scanLine, header.width, 3, header.channel_count, PremulConversion::PS2P); + else if (header.depth == 16) + premulConversion(scanLine, header.width, 3, header.channel_count, PremulConversion::PS2P); + else if (header.depth == 32) + premulConversion(scanLine, 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) + rawChannelsCopy(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width); + else if (header.depth == 32) + rawChannelsCopy(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width); + } } } else { @@ -1284,6 +1381,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);