diff --git a/README.md b/README.md index ae58bec..1a11f81 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The following image formats have read-only support: - Animated Windows cursors (ani) - Gimp (xcf) - OpenEXR (exr) -- Photoshop documents (psd) +- Photoshop documents (psd, psb, pdd, psdt) - Sun Raster (ras) The following image formats have read and write support: diff --git a/autotests/read/psd/16bit_grayscale.png b/autotests/read/psd/16bit_grayscale.png new file mode 100644 index 0000000..81544db Binary files /dev/null and b/autotests/read/psd/16bit_grayscale.png differ diff --git a/autotests/read/psd/16bit_grayscale.psd b/autotests/read/psd/16bit_grayscale.psd new file mode 100644 index 0000000..9d7cb2f Binary files /dev/null and b/autotests/read/psd/16bit_grayscale.psd differ diff --git a/autotests/read/psd/16bit_photoshop.png b/autotests/read/psd/16bit_photoshop.png new file mode 100644 index 0000000..9ac68df Binary files /dev/null and b/autotests/read/psd/16bit_photoshop.png differ diff --git a/autotests/read/psd/16bit_photoshop.psb b/autotests/read/psd/16bit_photoshop.psb new file mode 100644 index 0000000..c09d6bb Binary files /dev/null and b/autotests/read/psd/16bit_photoshop.psb differ diff --git a/autotests/read/psd/32bit-rgb.png b/autotests/read/psd/32bit-rgb.png new file mode 100644 index 0000000..bcabdf3 Binary files /dev/null and b/autotests/read/psd/32bit-rgb.png differ diff --git a/autotests/read/psd/32bit-rgb.psd b/autotests/read/psd/32bit-rgb.psd new file mode 100644 index 0000000..583669b Binary files /dev/null and b/autotests/read/psd/32bit-rgb.psd differ diff --git a/autotests/read/psd/32bit_grayscale.png b/autotests/read/psd/32bit_grayscale.png new file mode 100644 index 0000000..56559dc Binary files /dev/null and b/autotests/read/psd/32bit_grayscale.png differ diff --git a/autotests/read/psd/32bit_grayscale.psd b/autotests/read/psd/32bit_grayscale.psd new file mode 100644 index 0000000..7aa82e3 Binary files /dev/null and b/autotests/read/psd/32bit_grayscale.psd differ diff --git a/autotests/read/psd/8bit-grayscale.png b/autotests/read/psd/8bit-grayscale.png new file mode 100644 index 0000000..038b513 Binary files /dev/null and b/autotests/read/psd/8bit-grayscale.png differ diff --git a/autotests/read/psd/8bit-grayscale.psd b/autotests/read/psd/8bit-grayscale.psd new file mode 100644 index 0000000..e98b865 Binary files /dev/null and b/autotests/read/psd/8bit-grayscale.psd differ diff --git a/autotests/read/psd/8bit-photoshop.png b/autotests/read/psd/8bit-photoshop.png new file mode 100644 index 0000000..5a16ff9 Binary files /dev/null and b/autotests/read/psd/8bit-photoshop.png differ diff --git a/autotests/read/psd/8bit-photoshop.psb b/autotests/read/psd/8bit-photoshop.psb new file mode 100644 index 0000000..88e0a42 Binary files /dev/null and b/autotests/read/psd/8bit-photoshop.psb differ diff --git a/autotests/read/psd/adobehq-2_5.png b/autotests/read/psd/adobehq-2_5.png new file mode 100644 index 0000000..3af5282 Binary files /dev/null and b/autotests/read/psd/adobehq-2_5.png differ diff --git a/autotests/read/psd/adobehq-2_5.psd b/autotests/read/psd/adobehq-2_5.psd new file mode 100644 index 0000000..210a1b7 Binary files /dev/null and b/autotests/read/psd/adobehq-2_5.psd differ diff --git a/autotests/read/psd/birthday.pdd b/autotests/read/psd/birthday.pdd new file mode 100644 index 0000000..006d800 Binary files /dev/null and b/autotests/read/psd/birthday.pdd differ diff --git a/autotests/read/psd/birthday.png b/autotests/read/psd/birthday.png new file mode 100644 index 0000000..84abb36 Binary files /dev/null and b/autotests/read/psd/birthday.png differ diff --git a/autotests/read/psd/bitmap.png b/autotests/read/psd/bitmap.png new file mode 100644 index 0000000..557328e Binary files /dev/null and b/autotests/read/psd/bitmap.png differ diff --git a/autotests/read/psd/bitmap.psd b/autotests/read/psd/bitmap.psd new file mode 100644 index 0000000..59f657d Binary files /dev/null and b/autotests/read/psd/bitmap.psd differ diff --git a/autotests/read/psd/indexed.png b/autotests/read/psd/indexed.png new file mode 100644 index 0000000..286046b Binary files /dev/null and b/autotests/read/psd/indexed.png differ diff --git a/autotests/read/psd/indexed.psd b/autotests/read/psd/indexed.psd new file mode 100644 index 0000000..27678d3 Binary files /dev/null and b/autotests/read/psd/indexed.psd differ diff --git a/src/imageformats/psd.cpp b/src/imageformats/psd.cpp index d0abbe4..11a7341 100644 --- a/src/imageformats/psd.cpp +++ b/src/imageformats/psd.cpp @@ -3,6 +3,7 @@ SPDX-FileCopyrightText: 2003 Ignacio CastaƱo SPDX-FileCopyrightText: 2015 Alex Merry + SPDX-FileCopyrightText: 2022 Mirco Miranda SPDX-License-Identifier: LGPL-2.0-or-later */ @@ -17,13 +18,20 @@ * http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/ */ -#include "psd_p.h" +/* + * Limitations of the current code: + * - 32-bit float image are converted to 16-bit integer image. + * NOTE: Qt 6.2 allow 32-bit float images (RGB only) + * - Other color spaces cannot be read due to lack of QImage support for + * color spaces other than RGB (and Grayscale). + */ -#include "rle_p.h" +#include "psd_p.h" #include #include #include +#include typedef quint32 uint; typedef quint16 ushort; @@ -42,6 +50,14 @@ enum ColorMode { CM_LABCOLOR = 9, }; +enum ImageResourceId : quint16 { + IRI_RESOLUTIONINFO = 0x03ED, + IRI_ICCPROFILE = 0x040F, + IRI_TRANSPARENCYINDEX = 0x0417, + IRI_VERSIONINFO = 0x0421, + IRI_XMPMETADATA = 0x0424 +}; + struct PSDHeader { uint signature; ushort version; @@ -53,6 +69,301 @@ struct PSDHeader { ushort color_mode; }; +struct PSDImageResourceBlock { + QString name; + QByteArray data; +}; + +using PSDImageResourceSection = QHash; + +/*! + * \brief fixedPointToDouble + * Converts a fixed point number to floating point one. + */ +static double fixedPointToDouble(qint32 fixedPoint) +{ + auto i = double(fixedPoint >> 16); + auto d = double((fixedPoint & 0x0000FFFF) / 65536.0); + return (i+d); +} + +/*! + * \brief readPascalString + * Reads the Pascal string as defined in the PSD specification. + * \param s The stream. + * \param alignBytes Alignment of the string. + * \param size Number of stream bytes used. + * \return The string read. + */ +static QString readPascalString(QDataStream &s, qint32 alignBytes = 1, qint32 *size = nullptr) +{ + qint32 tmp = 0; + if (size == nullptr) + size = &tmp; + + quint8 stringSize; + s >> stringSize; + *size = sizeof(stringSize); + + QString str; + if (stringSize > 0) { + QByteArray ba; + ba.resize(stringSize); + auto read = s.readRawData(ba.data(), ba.size()); + if (read > 0) { + *size += read; + str = QString::fromLatin1(ba); + } + } + + // align + if (alignBytes > 1) + if (auto pad = *size % alignBytes) + *size += s.skipRawData(alignBytes - pad); + + return str; +} + +/*! + * \brief readImageResourceSection + * Reads the image resource section. + * \param s The stream. + * \return The image resource section raw data. + */ +static PSDImageResourceSection readImageResourceSection(QDataStream &s, bool *ok = nullptr) +{ + PSDImageResourceSection irs; + + bool tmp = true; + if (ok == nullptr) + ok = &tmp; + *ok = true; + + // Section size + qint32 sectioSize; + s >> sectioSize; + +#ifdef QT_DEBUG + auto pos = qint64(); + if (auto dev = s.device()) + pos = dev->pos(); +#endif + + // Reading Image resource block + for (auto size = sectioSize; size > 0;) { + // Length Description + // ------------------------------------------------------------------- + // 4 Signature: '8BIM' + // 2 Unique identifier for the resource. Image resource IDs + // contains a list of resource IDs used by Photoshop. + // Variable Name: Pascal string, padded to make the size even + // (a null name consists of two bytes of 0) + // 4 Actual size of resource data that follows + // Variable The resource data, described in the sections on the + // individual resource types. It is padded to make the size + // even. + + quint32 signature; + s >> signature; + size -= sizeof(signature); + // NOTE: MeSa signature is not documented but found in some old PSD found in Photoshop 7.0 CD. + if (signature != 0x3842494D && signature != 0x4D655361) { // 8BIM and MeSa + qDebug() << "Invalid Image Resource Block Signature!"; + *ok = false; + break; + } + + // id + quint16 id; + s >> id; + size -= sizeof(id); + + // getting data + PSDImageResourceBlock irb; + + // name + qint32 bytes = 0; + irb.name = readPascalString(s, 2, &bytes); + size -= bytes; + + // data read + quint32 dataSize; + s >> dataSize; + size -= sizeof(dataSize); + irb.data.resize(dataSize); + auto read = s.readRawData(irb.data.data(), irb.data.size()); + if (read > 0) + size -= read; + if (read != irb.data.size()) { + qDebug() << "Image Resource Block Read Error!"; + *ok = false; + break; + } + + if (auto pad = dataSize % 2) { + auto skipped = s.skipRawData(pad); + if (skipped > 0) + size -= skipped; + } + + // insert IRB + irs.insert(id, irb); + } + +#ifdef QT_DEBUG + if (auto dev = s.device()) + Q_ASSERT((dev->pos()-pos) == sectioSize); +#endif + + return irs; +} + +QVector colorTable(QDataStream &s, bool *ok = nullptr) +{ + QVector palette; + + bool tmp = false; + if (ok == nullptr) + ok = &tmp; + + *ok = true; + + qint32 size; + s >> size; + if (size != 768) { + if (s.skipRawData(size) != size) + *ok = false; + return palette; + } + + QVector vect(size); + for (auto&& v : vect) + s >> v; + for (qsizetype i = 0, n = vect.size()/3; i < n; ++i) + palette.append(qRgb(vect.at(i), vect.at(n+i), vect.at(n+n+i))); + + return palette; +} + +/*! + * \brief setColorSpace + * Set the color space to the image. + * \param img The image. + * \param irs The image resource section. + * \return True on success, otherwise false. + */ +static bool setColorSpace(QImage& img, const PSDImageResourceSection& irs) +{ + if (!irs.contains(IRI_ICCPROFILE)) + return false; + auto irb = irs.value(IRI_ICCPROFILE); + auto cs = QColorSpace::fromIccProfile(irb.data); + if (!cs.isValid()) + return false; + img.setColorSpace(cs); + return true; +} + +/*! + * \brief setXmpData + * Adds XMP metadata to QImage. + * \param img The image. + * \param irs The image resource section. + * \return True on success, otherwise false. + */ +static bool setXmpData(QImage& img, const PSDImageResourceSection& irs) +{ + if (!irs.contains(IRI_XMPMETADATA)) + return false; + auto irb = irs.value(IRI_XMPMETADATA); + auto xmp = QString::fromUtf8(irb.data); + if (xmp.isEmpty()) + return false; + // NOTE: "XML:com.adobe.xmp" is the meta set by Qt reader when an + // XMP packet is found (e.g. when reading a PNG saved by Photoshop). + // I'm reusing the same key because a programs could search for it. + img.setText(QStringLiteral("XML:com.adobe.xmp"), xmp); + return true; +} + +/*! + * \brief hasMergedData + * Checks if merged image data are available. + * \param irs The image resource section. + * \return True on success or if the block does not exist, otherwise false. + */ +static bool hasMergedData(const PSDImageResourceSection& irs) +{ + if (!irs.contains(IRI_VERSIONINFO)) + return true; + auto irb = irs.value(IRI_VERSIONINFO); + if (irb.data.size() > 4) + return irb.data.at(4) != 0; + return false; +} + +/*! + * \brief setResolution + * Set the image resolution. + * \param img The image. + * \param irs The image resource section. + * \return True on success or if the block does not exists, otherwise false. + */ +static bool setResolution(QImage& img, const PSDImageResourceSection& irs) +{ + if (!irs.contains(IRI_RESOLUTIONINFO)) + return false; + auto irb = irs.value(IRI_RESOLUTIONINFO); + + QDataStream s(irb.data); + s.setByteOrder(QDataStream::BigEndian); + + qint32 i32; + s >> i32; // Horizontal resolution in pixels per inch. + if (i32 <= 0) + return false; + auto hres = fixedPointToDouble(i32); + + s.skipRawData(4); // Display data (not used here) + + s >> i32; // Vertial resolution in pixels per inch. + if (i32 <= 0) + return false; + auto vres = fixedPointToDouble(i32); + + img.setDotsPerMeterX(hres * 1000 / 25.4); + img.setDotsPerMeterY(vres * 1000 / 25.4); + return true; +} + +/*! + * \brief setTransparencyIndex + * Search for transparency index block and, if found, changes the alpha of the value at the given index. + * \param img The image. + * \param irs The image resource section. + * \return True on success or if the block does not exists, otherwise false. + */ +static bool setTransparencyIndex(QImage& img, const PSDImageResourceSection& irs) +{ + if (!irs.contains(IRI_TRANSPARENCYINDEX)) + return false; + auto irb = irs.value(IRI_TRANSPARENCYINDEX); + QDataStream s(irb.data); + s.setByteOrder(QDataStream::BigEndian); + quint16 idx; + s >> idx; + + auto palette = img.colorTable(); + if (idx < palette.size()) { + auto&& v = palette[idx]; + v = QRgb(v & ~0xFF000000); + img.setColorTable(palette); + return true; + } + + return false; +} + static QDataStream &operator>>(QDataStream &s, PSDHeader &header) { s >> header.signature; @@ -80,66 +391,222 @@ static bool IsValid(const PSDHeader &header) // Check that the header is supported. static bool IsSupported(const PSDHeader &header) { - if (header.version != 1) { + if (header.version != 1 && header.version != 2) { return false; } - if (header.channel_count > 16) { + if (header.depth != 8 && + header.depth != 16 && + header.depth != 32 && + header.depth != 1) { return false; } - if (header.depth != 8 && header.depth != 16) { - return false; - } - if (header.color_mode != CM_RGB) { + if (header.color_mode != CM_RGB && + header.color_mode != CM_GRAYSCALE && + header.color_mode != CM_INDEXED && + header.color_mode != CM_BITMAP) { return false; } return true; } -static void skip_section(QDataStream &s) +static bool skip_section(QDataStream &s, bool psb = false) { - quint32 section_length; + qint64 section_length; + if (!psb) { + quint32 tmp; + s >> tmp; + section_length = tmp; + } + else { + s >> section_length; + } + // Skip mode data. - s >> section_length; - s.skipRawData(section_length); + for (qint32 i32 = 0; section_length; section_length -= i32) { + i32 = std::min(section_length, qint64(std::numeric_limits::max())); + i32 = s.skipRawData(i32); + if (i32 < 1) + return false; + } + return true; } -template -static Trait readPixel(QDataStream &stream) +/*! + * \brief decompress + * Fast PackBits decompression. + * \param input The compressed input buffer. + * \param ilen The input buffer size. + * \param output The uncompressed target buffer. + * \param olen The target buffer size. + * \return The number of valid bytes in the target buffer. + */ +qint64 decompress(const char *input, qint64 ilen, char *output, qint64 olen) { - Trait pixel; - stream >> pixel; - return pixel; + qint64 j = 0; + for (qint64 ip = 0, rr = 0, available = olen; j < olen && ip < ilen; available = olen - j) { + char n = input[ip++]; + if (static_cast(n) == -128) + continue; + + if (static_cast(n) >= 0) { + rr = qint64(n) + 1; + if (available < rr) { + ip--; + break; + } + + if (ip + rr > ilen) + return -1; + memcpy(output + j, input + ip, size_t(rr)); + ip += rr; + } + else if (ip < ilen) { + rr = qint64(1-n); + if (available < rr) { + ip--; + break; + } + memset(output + j, input[ip++], size_t(rr)); + } + + j += rr; + } + return j; } -static QRgb updateRed(QRgb oldPixel, quint8 redPixel) +/*! + * \brief imageFormat + * \param header The PSD header. + * \return The Qt image format. + */ +static QImage::Format imageFormat(const PSDHeader &header) { - return qRgba(redPixel, qGreen(oldPixel), qBlue(oldPixel), qAlpha(oldPixel)); + auto format = QImage::Format_Invalid; + switch(header.color_mode) { + case CM_RGB: + if (header.depth == 16 || header.depth == 32) + format = header.channel_count < 4 ? QImage::Format_RGBX64 : QImage::Format_RGBA64; + else + format = header.channel_count < 4 ? QImage::Format_RGB888 : QImage::Format_RGBA8888; + break; + case CM_GRAYSCALE: + format = header.depth == 8 ? QImage::Format_Grayscale8 : QImage::Format_Grayscale16; + break; + case CM_INDEXED: + format = QImage::Format_Indexed8; + break; + case CM_BITMAP: + format = QImage::Format_Mono; + break; + default: + qDebug() << "Unsupported color mode" << header.color_mode; + } + return format; } -static QRgb updateGreen(QRgb oldPixel, quint8 greenPixel) + +/*! + * \brief imageChannels + * \param format The Qt image format. + * \return The number of channels of the image format. + */ +static qint32 imageChannels(const QImage::Format& format) { - return qRgba(qRed(oldPixel), greenPixel, qBlue(oldPixel), qAlpha(oldPixel)); + qint32 c = 4; + switch(format) { + case QImage::Format_RGB888: + c = 3; + break; + case QImage::Format_Grayscale8: + case QImage::Format_Grayscale16: + case QImage::Format_Indexed8: + case QImage::Format_Mono: + c = 1; + break; + default: + break; + } + return c; } -static QRgb updateBlue(QRgb oldPixel, quint8 bluePixel) + +inline quint8 xchg(quint8 v) { + return v; +} + +inline quint16 xchg(quint16 v) { +#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN + return quint16( (v>>8) | (v<<8) ); +#else + return v; // never tested +#endif +} + +inline quint32 xchg(quint32 v) { +#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN + return quint32( (v>>24) | ((v & 0x00FF0000)>>8) | ((v & 0x0000FF00)<<8) | (v<<24) ); +#else + return v; // never tested +#endif +} + +template +inline void planarToChunchy(uchar *target, const char* source, qint32 width, qint32 c, qint32 cn) { - return qRgba(qRed(oldPixel), qGreen(oldPixel), bluePixel, qAlpha(oldPixel)); + auto s = reinterpret_cast(source); + auto t = reinterpret_cast(target); + for (qint32 x = 0; x < width; ++x) + t[x*cn+c] = xchg(s[x]); } -static QRgb updateAlpha(QRgb oldPixel, quint8 alphaPixel) + +template +inline void planarToChunchyFloat(uchar *target, const char* source, qint32 width, qint32 c, qint32 cn) { - return qRgba(qRed(oldPixel), qGreen(oldPixel), qBlue(oldPixel), alphaPixel); + auto s = reinterpret_cast(source); + auto t = reinterpret_cast(target); + for (qint32 x = 0; x < width; ++x) { + auto tmp = xchg(s[x]); + t[x*cn+c] = quint16(*reinterpret_cast(&tmp) * 65535); + } +} + +inline void monoInvert(uchar *target, const char* source, qint32 bytes) +{ + auto s = reinterpret_cast(source); + auto t = reinterpret_cast(target); + for (qint32 x = 0; x < bytes; ++x) + t[x] = ~s[x]; } -typedef QRgb (*channelUpdater)(QRgb, quint8); // Load the PSD image. static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img) { - // Mode data - skip_section(stream); + // Checking for PSB + auto isPsb = header.version == 2; + bool ok = false; - // Image resources - skip_section(stream); + // Color Mode Data section + auto palette = colorTable(stream, &ok); + if (!ok) { + qDebug() << "Error while skipping Color Mode Data section"; + return false; + } - // Reserved data - skip_section(stream); + // Image Resources Section + auto irs = readImageResourceSection(stream, &ok); + if (!ok) { + qDebug() << "Error while reading Image Resources Section"; + return false; + } + // Checking for merged image (Photoshop compatibility data) + if (!hasMergedData(irs)) { + qDebug() << "No merged data found"; + return false; + } + + // Layer and Mask section + if (!skip_section(stream, isPsb)) { + qDebug() << "Error while skipping Layer and Mask section"; + return false; + } // Find out if the data is compressed. // Known values: @@ -147,103 +614,95 @@ static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img) // 1: RLE compressed quint16 compression; stream >> compression; - if (compression > 1) { qDebug() << "Unknown compression type"; return false; } - quint32 channel_num = header.channel_count; - - QImage::Format fmt = header.depth == 8 ? QImage::Format_RGB32 : QImage::Format_RGBX64; - // Clear the image. - if (channel_num >= 4) { - // Enable alpha. - fmt = header.depth == 8 ? QImage::Format_ARGB32 : QImage::Format_RGBA64; - - // Ignore the other channels. - channel_num = 4; - } - - img = QImage(header.width, header.height, fmt); + img = QImage(header.width, header.height, imageFormat(header)); if (img.isNull()) { qWarning() << "Failed to allocate image, invalid dimensions?" << QSize(header.width, header.height); return false; } img.fill(qRgb(0, 0, 0)); - - const quint32 pixel_count = header.height * header.width; - const quint32 channel_size = pixel_count * header.depth / 8; - - // Verify this, as this is used to write into the memory of the QImage - if (pixel_count > img.sizeInBytes() / (header.depth == 8 ? sizeof(QRgb) : sizeof(QRgba64))) { - qWarning() << "Invalid pixel count!" << pixel_count << "bytes available:" << img.sizeInBytes(); - return false; + if (!palette.isEmpty()) { + img.setColorTable(palette); + setTransparencyIndex(img, irs); } - QRgb *image_data = reinterpret_cast(img.bits()); - - if (!image_data) { - return false; - } - - static const channelUpdater updaters[4] = {updateRed, updateGreen, updateBlue, updateAlpha}; - - typedef QRgba64 (*channelUpdater16)(QRgba64, quint16); - static const channelUpdater16 updaters64[4] = {[](QRgba64 oldPixel, quint16 redPixel) { - return qRgba64((oldPixel & ~(0xFFFFull << 0)) | (quint64(redPixel) << 0)); - }, - [](QRgba64 oldPixel, quint16 greenPixel) { - return qRgba64((oldPixel & ~(0xFFFFull << 16)) | (quint64(greenPixel) << 16)); - }, - [](QRgba64 oldPixel, quint16 bluePixel) { - return qRgba64((oldPixel & ~(0xFFFFull << 32)) | (quint64(bluePixel) << 32)); - }, - [](QRgba64 oldPixel, quint16 alphaPixel) { - return qRgba64((oldPixel & ~(0xFFFFull << 48)) | (quint64(alphaPixel) << 48)); - }}; - - if (compression) { - // Skip row lengths. - int skip_count = header.height * header.channel_count * sizeof(quint16); - if (stream.skipRawData(skip_count) != skip_count) { - return false; + 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; + QVector strides(header.height * header.channel_count, raw_count); + // Read the compressed stride sizes + if (compression) + for (auto&& v : strides) { + if (isPsb) { + stream >> v; + continue; + } + quint16 tmp; + stream >> tmp; + v = tmp; } - for (unsigned short channel = 0; channel < channel_num; channel++) { - bool success = false; - if (header.depth == 8) { - success = decodeRLEData(RLEVariant::PackBits, stream, image_data, channel_size, &readPixel, updaters[channel]); - } else if (header.depth == 16) { - QRgba64 *image_data = reinterpret_cast(img.bits()); - success = decodeRLEData(RLEVariant::PackBits16, stream, image_data, channel_size, &readPixel, updaters64[channel]); + // Read the image + QByteArray rawStride; + rawStride.resize(raw_count); + 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); + if (compression) { + QByteArray tmp; + tmp.resize(strideSize); + if (stream.readRawData(tmp.data(), tmp.size()) != tmp.size()) { + qDebug() << "Error while reading the stream of channel" << c << "line" << y; + return false; + } + if (decompress(tmp.data(), tmp.size(), rawStride.data(), rawStride.size()) < 0) { + qDebug() << "Error while decompressing the channel" << c << "line" << y; + return false; + } + } + else { + if (stream.readRawData(rawStride.data(), rawStride.size()) != rawStride.size()) { + qDebug() << "Error while reading the stream of channel" << c << "line" << y; + return false; + } } - if (!success) { - qDebug() << "decodeRLEData on channel" << channel << "failed"; - return false; - } - } - } else { - for (unsigned short channel = 0; channel < channel_num; channel++) { - if (header.depth == 8) { - for (unsigned i = 0; i < pixel_count; ++i) { - image_data[i] = updaters[channel](image_data[i], readPixel(stream)); - } - } else if (header.depth == 16) { - QRgba64 *image_data = reinterpret_cast(img.bits()); - for (unsigned i = 0; i < pixel_count; ++i) { - image_data[i] = updaters64[channel](image_data[i], readPixel(stream)); - } - } - // make sure we didn't try to read past the end of the stream if (stream.status() != QDataStream::Ok) { - qDebug() << "DataStream status was" << stream.status(); + qDebug() << "Stream read error" << stream.status(); return false; } + + auto scanLine = img.scanLine(y); + 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 + planarToChunchy(scanLine, rawStride.data(), header.width, c, imgChannels); + 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) + planarToChunchyFloat(scanLine, rawStride.data(), header.width, c, imgChannels); } } + // Resolution info + if (!setResolution(img, irs)) { + // qDebug() << "No resolution info found!"; + } + + // ICC profile + if (!setColorSpace(img, irs)) { + // qDebug() << "No colorspace info set!"; + } + + // XMP data + if (!setXmpData(img, irs)) { + // qDebug() << "No XMP data found!"; + } + return true; } @@ -332,7 +791,7 @@ bool PSDHandler::canRead(QIODevice *device) QImageIOPlugin::Capabilities PSDPlugin::capabilities(QIODevice *device, const QByteArray &format) const { - if (format == "psd") { + if (format == "psd" || format == "psb" || format == "pdd" || format == "psdt") { return Capabilities(CanRead); } if (!format.isEmpty()) { diff --git a/src/imageformats/psd.json b/src/imageformats/psd.json index 5a24689..3d559d8 100644 --- a/src/imageformats/psd.json +++ b/src/imageformats/psd.json @@ -1,4 +1,4 @@ { - "Keys": [ "psd" ], - "MimeTypes": [ "image/vnd.adobe.photoshop" ] + "Keys": [ "psd", "psb", "pdd", "psdt" ], + "MimeTypes": [ "image/vnd.adobe.photoshop", "image/vnd.adobe.photoshop", "image/vnd.adobe.photoshop", "image/vnd.adobe.photoshop" ] }