PSD: Performance improvements and support to missing common formats

- Supersedes merge request !55 (PSB support, XMP metadata, ICC color profile, image resolution read)
- Performance improvements: 5 time faster than previous version (tested on a 3.9GB PSB: 9sec instead 47sec)
- New formats support added: INDEXED (8bps), BITMAP (1bps), GRAYSCALE (8, 16, 32bps), RGB (32bps)
- Should fix Bug https://bugs.kde.org/show_bug.cgi?id=397610
- Fix Bug https://bugs.kde.org/show_bug.cgi?id=428238
This commit is contained in:
Mirco Miranda
2022-04-04 17:22:45 +00:00
committed by Albert Astals Cid
parent ae6b724824
commit 98f19c60ae
23 changed files with 570 additions and 111 deletions

View File

@ -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:

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

View File

@ -3,6 +3,7 @@
SPDX-FileCopyrightText: 2003 Ignacio Castaño <castano@ludicon.com>
SPDX-FileCopyrightText: 2015 Alex Merry <alex.merry@kde.org>
SPDX-FileCopyrightText: 2022 Mirco Miranda <mirco.miranda@systemceramics.com>
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 <QDataStream>
#include <QDebug>
#include <QImage>
#include <QColorSpace>
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<quint16, PSDImageResourceBlock>;
/*!
* \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<QRgb> colorTable(QDataStream &s, bool *ok = nullptr)
{
QVector<QRgb> 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<quint8> 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<qint32>::max()));
i32 = s.skipRawData(i32);
if (i32 < 1)
return false;
}
return true;
}
template<class Trait>
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<signed char>(n) == -128)
continue;
if (static_cast<signed char>(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<class T>
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<const T*>(source);
auto t = reinterpret_cast<T*>(target);
for (qint32 x = 0; x < width; ++x)
t[x*cn+c] = xchg(s[x]);
}
static QRgb updateAlpha(QRgb oldPixel, quint8 alphaPixel)
template<class T>
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<const T*>(source);
auto t = reinterpret_cast<quint16*>(target);
for (qint32 x = 0; x < width; ++x) {
auto tmp = xchg(s[x]);
t[x*cn+c] = quint16(*reinterpret_cast<float*>(&tmp) * 65535);
}
}
inline void monoInvert(uchar *target, const char* source, qint32 bytes)
{
auto s = reinterpret_cast<const quint8*>(source);
auto t = reinterpret_cast<quint8*>(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<QRgb *>(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<quint32> 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<quint8>, updaters[channel]);
} else if (header.depth == 16) {
QRgba64 *image_data = reinterpret_cast<QRgba64 *>(img.bits());
success = decodeRLEData(RLEVariant::PackBits16, stream, image_data, channel_size, &readPixel<quint8>, 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<quint8>(stream));
}
} else if (header.depth == 16) {
QRgba64 *image_data = reinterpret_cast<QRgba64 *>(img.bits());
for (unsigned i = 0; i < pixel_count; ++i) {
image_data[i] = updaters64[channel](image_data[i], readPixel<quint16>(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<quint8>(scanLine, rawStride.data(), header.width, c, imgChannels);
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)
planarToChunchyFloat<quint32>(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()) {

View File

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