psd: Fix alpha blending (KF5)

PSD files are saved with as alpha premultiplied. The problem is that alpha refers to white instead of black so it requires transformation formulas. Then, to conver PS premultiplied to QImage premultiplied you have to use the following formula:

* V = Alpha + Vps - Max (C, M, Y, K, R, G, B, Gray, L\* components)
* V = Vps + (Alpha - Max + 1) / 2 (a\*, b\* components)

Where Max is the maximum value depending on the image depth and Vps is the valued read from the file.

This is a port of MR !143 to KF5.
This commit is contained in:
Mirco Miranda 2023-03-29 17:58:09 +00:00 committed by Albert Astals Cid
parent 64f3303ef0
commit e96b43aef5
6 changed files with 105 additions and 22 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 983 B

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 191 KiB

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 114 KiB

View File

@ -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<const T*>(source);
auto t = reinterpret_cast<T*>(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<float*>(&tmp) - double(min)) / (double(max) - double(min));
t[x*cn+c] = quint16(std::min(ftmp * std::numeric_limits<quint16>::max() + 0.5, double(std::numeric_limits<quint16>::max())));
t[x * cn + c] = quint16(std::min(ftmp * std::numeric_limits<quint16>::max() + 0.5, double(std::numeric_limits<quint16>::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<class T>
inline void premulConversion(char *stride, qint32 width, qint32 ac, qint32 cn, const PremulConversion &conv)
{
auto s = reinterpret_cast<T *>(stride);
auto max = qint64(std::numeric_limits<T>::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<class T>
inline void rawChannelsCopy(uchar *target, qint32 targetChannels, const char *source, qint32 sourceChannels, qint32 width)
{
auto s = reinterpret_cast<const T *>(source);
auto t = reinterpret_cast<T *>(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<class T>
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<unsigned char*>(psdScanline.data());
if (header.depth == 8) {
planarToChunchy<quint8>(scanLine, rawStride.data(), header.width, c, header.channel_count);
}
else if (header.depth == 16) {
} else if (header.depth == 16) {
planarToChunchy<quint16>(scanLine, rawStride.data(), header.width, c, header.channel_count);
}
else if (header.depth == 32) { // Not currently used
} else if (header.depth == 32) {
planarToChunchyFloat<quint32>(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<quint8>(psdScanline.data(), header.width, 4, header.channel_count, PremulConversion::PS2A);
else if (header.depth == 16)
premulConversion<quint16>(psdScanline.data(), header.width, 4, header.channel_count, PremulConversion::PS2A);
}
if (header.color_mode == CM_LABCOLOR) {
if (header.depth == 8)
premulConversion<quint8>(psdScanline.data(), header.width, 3, header.channel_count, PremulConversion::PSLab2A);
else if (header.depth == 16)
premulConversion<quint16>(psdScanline.data(), header.width, 3, header.channel_count, PremulConversion::PSLab2A);
}
if (header.color_mode == CM_RGB) {
if (header.depth == 8)
premulConversion<quint8>(psdScanline.data(), header.width, 3, header.channel_count, PremulConversion::PS2P);
else if (header.depth == 16 || header.depth == 32)
premulConversion<quint16>(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<quint8>(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width, alpha);
else
else if (header.depth == 16)
cmykToRgb<quint16>(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width, alpha);
}
if (header.color_mode == CM_LABCOLOR) {
if (header.depth == 8)
labToRgb<quint8>(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width, alpha);
else
else if (header.depth == 16)
labToRgb<quint16>(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width, alpha);
}
if (header.color_mode == CM_RGB) {
if (header.depth == 8)
rawChannelsCopy<quint8>(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width);
else if (header.depth == 16 || header.depth == 32)
rawChannelsCopy<quint16>(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<quint8>(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<quint16>(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<quint32>(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);