PSD: improved option support

Added support for the following options:
- `ImageTransformation`: uses EXIF data (same behaviour of Photoshop and GIMP)
- `Description`: uses EXIF data
- `ImageFormat` 

Closes #17
This commit is contained in:
Mirco Miranda 2025-01-18 22:32:15 +00:00 committed by Albert Astals Cid
parent 873ec1bb5f
commit e83458a5d8
18 changed files with 289 additions and 118 deletions

Binary file not shown.

View File

@ -0,0 +1,11 @@
[
{
"fileName" : "orientation_all.png",
"metadata" : [
{
"key" : "Software" ,
"value" : "LIFE Pro 2.18.5 (Windows)"
}
]
}
]

Binary file not shown.

View File

@ -0,0 +1,11 @@
[
{
"fileName" : "orientation_all.png",
"metadata" : [
{
"key" : "Software" ,
"value" : "LIFE Pro 2.18.5 (Windows)"
}
]
}
]

Binary file not shown.

View File

@ -0,0 +1,11 @@
[
{
"fileName" : "orientation_all.png",
"metadata" : [
{
"key" : "Software" ,
"value" : "LIFE Pro 2.18.5 (Windows)"
}
]
}
]

Binary file not shown.

View File

@ -0,0 +1,11 @@
[
{
"fileName" : "orientation_all.png",
"metadata" : [
{
"key" : "Software" ,
"value" : "LIFE Pro 2.18.5 (Windows)"
}
]
}
]

Binary file not shown.

View File

@ -0,0 +1,11 @@
[
{
"fileName" : "orientation_all.png",
"metadata" : [
{
"key" : "Software" ,
"value" : "LIFE Pro 2.18.5 (Windows)"
}
]
}
]

Binary file not shown.

View File

@ -0,0 +1,11 @@
[
{
"fileName" : "orientation_all.png",
"metadata" : [
{
"key" : "Software" ,
"value" : "LIFE Pro 2.18.5 (Windows)"
}
]
}
]

Binary file not shown.

View File

@ -0,0 +1,11 @@
[
{
"fileName" : "orientation_all.png",
"metadata" : [
{
"key" : "Software" ,
"value" : "LIFE Pro 2.18.5 (Windows)"
}
]
}
]

Binary file not shown.

View File

@ -0,0 +1,11 @@
[
{
"fileName" : "orientation_all.png",
"metadata" : [
{
"key" : "Software" ,
"value" : "LIFE Pro 2.18.5 (Windows)"
}
]
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -473,9 +473,9 @@ PSDColorModeDataSection readColorModeDataSection(QDataStream &s, bool *ok = null
if (cms.duotone.data.size() != size) if (cms.duotone.data.size() != size)
*ok = false; *ok = false;
} else { // read the palette (768 bytes) } else { // read the palette (768 bytes)
auto&& palette = cms.palette; auto &&palette = cms.palette;
QList<quint8> vect(size); QList<quint8> vect(size);
for (auto&& v : vect) for (auto &&v : vect)
s >> v; s >> v;
for (qsizetype i = 0, n = vect.size()/3; i < n; ++i) 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))); palette.append(qRgb(vect.at(i), vect.at(n+i), vect.at(n+n+i)));
@ -491,7 +491,7 @@ PSDColorModeDataSection readColorModeDataSection(QDataStream &s, bool *ok = null
* \param irs The image resource section. * \param irs The image resource section.
* \return True on success, otherwise false. * \return True on success, otherwise false.
*/ */
static bool setColorSpace(QImage& img, const PSDImageResourceSection& irs) static bool setColorSpace(QImage &img, const PSDImageResourceSection &irs)
{ {
if (!irs.contains(IRI_ICCPROFILE) || img.isNull()) if (!irs.contains(IRI_ICCPROFILE) || img.isNull())
return false; return false;
@ -510,7 +510,7 @@ static bool setColorSpace(QImage& img, const PSDImageResourceSection& irs)
* \param irs The image resource section. * \param irs The image resource section.
* \return True on success, otherwise false. * \return True on success, otherwise false.
*/ */
static bool setXmpData(QImage& img, const PSDImageResourceSection& irs) static bool setXmpData(QImage &img, const PSDImageResourceSection &irs)
{ {
if (!irs.contains(IRI_XMPMETADATA)) if (!irs.contains(IRI_XMPMETADATA))
return false; return false;
@ -529,15 +529,11 @@ static bool setXmpData(QImage& img, const PSDImageResourceSection& irs)
* \brief setExifData * \brief setExifData
* Adds EXIF metadata to QImage. * Adds EXIF metadata to QImage.
* \param img The image. * \param img The image.
* \param irs The image resource section. * \param exif The decoded EXIF data.
* \return True on success, otherwise false. * \return True on success, otherwise false.
*/ */
static bool setExifData(QImage& img, const PSDImageResourceSection& irs) static bool setExifData(QImage &img, const MicroExif &exif)
{ {
if (!irs.contains(IRI_EXIFDATA1))
return false;
auto irb = irs.value(IRI_EXIFDATA1);
auto exif = MicroExif::fromByteArray(irb.data);
if (exif.isEmpty()) if (exif.isEmpty())
return false; return false;
exif.toImageMetadata(img); exif.toImageMetadata(img);
@ -545,12 +541,12 @@ static bool setExifData(QImage& img, const PSDImageResourceSection& irs)
} }
/*! /*!
* \brief hasMergedData * \brief HasMergedData
* Checks if merged image data are available. * Checks if merged image data are available.
* \param irs The image resource section. * \param irs The image resource section.
* \return True on success or if the block does not exist, otherwise false. * \return True on success or if the block does not exist, otherwise false.
*/ */
static bool hasMergedData(const PSDImageResourceSection& irs) static bool HasMergedData(const PSDImageResourceSection &irs)
{ {
if (!irs.contains(IRI_VERSIONINFO)) if (!irs.contains(IRI_VERSIONINFO))
return true; return true;
@ -567,7 +563,7 @@ static bool hasMergedData(const PSDImageResourceSection& irs)
* \param irs The image resource section. * \param irs The image resource section.
* \return True on success, otherwise false. * \return True on success, otherwise false.
*/ */
static bool setResolution(QImage& img, const PSDImageResourceSection& irs) static bool setResolution(QImage &img, const PSDImageResourceSection &irs)
{ {
if (!irs.contains(IRI_RESOLUTIONINFO)) if (!irs.contains(IRI_RESOLUTIONINFO))
return false; return false;
@ -601,7 +597,7 @@ static bool setResolution(QImage& img, const PSDImageResourceSection& irs)
* \param irs The image resource section. * \param irs The image resource section.
* \return True on success, otherwise false. * \return True on success, otherwise false.
*/ */
static bool setTransparencyIndex(QImage& img, const PSDImageResourceSection& irs) static bool setTransparencyIndex(QImage &img, const PSDImageResourceSection &irs)
{ {
if (!irs.contains(IRI_TRANSPARENCYINDEX)) if (!irs.contains(IRI_TRANSPARENCYINDEX))
return false; return false;
@ -613,7 +609,7 @@ static bool setTransparencyIndex(QImage& img, const PSDImageResourceSection& irs
auto palette = img.colorTable(); auto palette = img.colorTable();
if (idx < palette.size()) { if (idx < palette.size()) {
auto&& v = palette[idx]; auto &&v = palette[idx];
v = QRgb(v & ~0xFF000000); v = QRgb(v & ~0xFF000000);
img.setColorTable(palette); img.setColorTable(palette);
return true; return true;
@ -813,7 +809,7 @@ static QImage::Format imageFormat(const PSDHeader &header, bool alpha)
* \param format The Qt image format. * \param format The Qt image format.
* \return The number of channels of the image format. * \return The number of channels of the image format.
*/ */
static qint32 imageChannels(const QImage::Format& format) static qint32 imageChannels(const QImage::Format &format)
{ {
qint32 c = 4; qint32 c = 4;
switch(format) { switch(format) {
@ -1086,7 +1082,7 @@ inline void labToRgb(uchar *target, qint32 targetChannels, const char *source, q
} }
} }
bool readChannel(QByteArray& target, QDataStream &stream, quint32 compressedSize, quint16 compression) bool readChannel(QByteArray &target, QDataStream &stream, quint32 compressedSize, quint16 compression)
{ {
if (compression) { if (compression) {
if (compressedSize > kMaxQVectorSize) { if (compressedSize > kMaxQVectorSize) {
@ -1107,39 +1103,161 @@ bool readChannel(QByteArray& target, QDataStream &stream, quint32 compressedSize
return stream.status() == QDataStream::Ok; return stream.status() == QDataStream::Ok;
} }
// Load the PSD image. } // Private
static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img)
class PSDHandlerPrivate
{ {
// Checking for PSB public:
auto isPsb = header.version == 2; PSDHandlerPrivate()
bool ok = false; {
}
// Color Mode Data section ~PSDHandlerPrivate()
auto cmds = readColorModeDataSection(stream, &ok); {
if (!ok) {
qDebug() << "Error while skipping Color Mode Data section";
return false;
} }
// Image Resources Section bool isPsb() const
auto irs = readImageResourceSection(stream, &ok); {
if (!ok) { return m_header.version == 2;
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 bool isValid() const
auto lms = readLayerAndMaskSection(stream, isPsb, &ok); {
if (!ok) { return IsValid(m_header);
qDebug() << "Error while skipping Layer and Mask section";
return false;
} }
bool isSupported() const
{
return IsSupported(m_header);
}
bool hasAlpha() const
{
// Try to identify the nature of spots: note that this is just one of many ways to identify the presence
// of alpha channels: should work in most cases where colorspaces != RGB/Gray
auto alpha = m_header.color_mode == CM_RGB;
if (!m_lms.isNull())
alpha = m_lms.hasAlpha();
return alpha;
}
bool hasMergedData() const
{
return HasMergedData(m_irs);
}
QSize size() const
{
if (isValid())
return QSize(m_header.width, m_header.height);
return {};
}
QImage::Format format() const
{
return imageFormat(m_header, hasAlpha());
}
QImageIOHandler::Transformations transformation() const
{
return m_exif.transformation();
}
bool readInfo(QDataStream &stream)
{
// Checking for PSB
auto ok = false;
// Header
stream >> m_header;
// Check image file format.
if (stream.atEnd() || !IsValid(m_header)) {
// qDebug() << "This PSD file is not valid.";
return false;
}
// Check if it's a supported format.
if (!IsSupported(m_header)) {
// qDebug() << "This PSD file is not supported.";
return false;
}
// Color Mode Data section
m_cmds = readColorModeDataSection(stream, &ok);
if (!ok) {
qDebug() << "Error while skipping Color Mode Data section";
return false;
}
// Image Resources Section
m_irs = readImageResourceSection(stream, &ok);
if (!ok) {
qDebug() << "Error while reading Image Resources Section";
return false;
}
// Checking for merged image (Photoshop compatibility data)
if (!hasMergedData()) {
qDebug() << "No merged data found";
return false;
}
// Layer and Mask section
m_lms = readLayerAndMaskSection(stream, isPsb(), &ok);
if (!ok) {
qDebug() << "Error while skipping Layer and Mask section";
return false;
}
// storing decoded EXIF
if (m_irs.contains(IRI_EXIFDATA1)) {
m_exif = MicroExif::fromByteArray(m_irs.value(IRI_EXIFDATA1).data);
}
return ok;
}
PSDHeader m_header;
PSDColorModeDataSection m_cmds;
PSDImageResourceSection m_irs;
PSDLayerAndMaskSection m_lms;
// cache to avoid decoding exif multiple times
MicroExif m_exif;
};
PSDHandler::PSDHandler()
: QImageIOHandler()
, d(new PSDHandlerPrivate)
{
}
bool PSDHandler::canRead() const
{
if (canRead(device())) {
setFormat("psd");
return true;
}
return false;
}
bool PSDHandler::read(QImage *image)
{
QDataStream stream(device());
stream.setByteOrder(QDataStream::BigEndian);
if (!d->isValid()) {
if (!d->readInfo(stream))
return false;
}
auto &&header = d->m_header;
auto &&cmds = d->m_cmds;
auto &&irs = d->m_irs;
// auto &&lms = d->m_lms;
auto isPsb = d->isPsb();
auto alpha = d->hasAlpha();
QImage img;
// Find out if the data is compressed. // Find out if the data is compressed.
// Known values: // Known values:
// 0: no compression // 0: no compression
@ -1151,19 +1269,13 @@ static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img)
return false; return false;
} }
// Try to identify the nature of spots: note that this is just one of many ways to identify the presence const QImage::Format format = d->format();
// of alpha channels: should work in most cases where colorspaces != RGB/Gray
auto alpha = header.color_mode == CM_RGB;
if (!lms.isNull())
alpha = lms.hasAlpha();
const QImage::Format format = imageFormat(header, alpha);
if (format == QImage::Format_Invalid) { if (format == QImage::Format_Invalid) {
qWarning() << "Unsupported image format. color_mode:" << header.color_mode << "depth:" << header.depth << "channel_count:" << header.channel_count; qWarning() << "Unsupported image format. color_mode:" << header.color_mode << "depth:" << header.depth << "channel_count:" << header.channel_count;
return false; return false;
} }
img = imageAlloc(header.width, header.height, format); img = imageAlloc(d->size(), format);
if (img.isNull()) { if (img.isNull()) {
qWarning() << "Failed to allocate image, invalid dimensions?" << QSize(header.width, header.height); qWarning() << "Failed to allocate image, invalid dimensions?" << QSize(header.width, header.height);
return false; return false;
@ -1187,7 +1299,7 @@ static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img)
QList<quint32> strides(header.height * header.channel_count, raw_count); QList<quint32> strides(header.height * header.channel_count, raw_count);
// Read the compressed stride sizes // Read the compressed stride sizes
if (compression) { if (compression) {
for (auto&& v : strides) { for (auto &&v : strides) {
if (isPsb) { if (isPsb) {
stream >> v; stream >> v;
continue; continue;
@ -1242,7 +1354,7 @@ static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img)
qDebug() << "Error while seeking the stream of channel" << c << "line" << y; qDebug() << "Error while seeking the stream of channel" << c << "line" << y;
return false; return false;
} }
auto&& strideSize = strides.at(strideNumber); auto &&strideSize = strides.at(strideNumber);
if (!readChannel(rawStride, stream, strideSize, compression)) { if (!readChannel(rawStride, stream, strideSize, compression)) {
qDebug() << "Error while reading the stream of channel" << c << "line" << y; qDebug() << "Error while reading the stream of channel" << c << "line" << y;
return false; return false;
@ -1379,7 +1491,7 @@ static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img)
} }
// EXIF data // EXIF data
if (!setExifData(img, irs)) { if (!setExifData(img, d->m_exif)) {
// qDebug() << "No EXIF data found!"; // qDebug() << "No EXIF data found!";
} }
@ -1390,60 +1502,6 @@ static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img)
img.setText(QStringLiteral("PSDDuotoneOptions"), QString::fromUtf8(cmds.duotone.data.toHex())); img.setText(QStringLiteral("PSDDuotoneOptions"), QString::fromUtf8(cmds.duotone.data.toHex()));
} }
return true;
}
} // Private
class PSDHandlerPrivate
{
public:
PSDHandlerPrivate() {}
~PSDHandlerPrivate() {}
PSDHeader m_header;
};
PSDHandler::PSDHandler()
: QImageIOHandler()
, d(new PSDHandlerPrivate)
{
}
bool PSDHandler::canRead() const
{
if (canRead(device())) {
setFormat("psd");
return true;
}
return false;
}
bool PSDHandler::read(QImage *image)
{
QDataStream s(device());
s.setByteOrder(QDataStream::BigEndian);
auto&& header = d->m_header;
s >> header;
// Check image file format.
if (s.atEnd() || !IsValid(header)) {
// qDebug() << "This PSD file is not valid.";
return false;
}
// Check if it's a supported format.
if (!IsSupported(header)) {
// qDebug() << "This PSD file is not supported.";
return false;
}
QImage img;
if (!LoadPSD(s, header, img)) {
// qDebug() << "Error loading PSD file.";
return false;
}
*image = img; *image = img;
return true; return true;
} }
@ -1452,6 +1510,12 @@ bool PSDHandler::supportsOption(ImageOption option) const
{ {
if (option == QImageIOHandler::Size) if (option == QImageIOHandler::Size)
return true; return true;
if (option == QImageIOHandler::ImageFormat)
return true;
if (option == QImageIOHandler::ImageTransformation)
return true;
if (option == QImageIOHandler::Description)
return true;
return false; return false;
} }
@ -1459,18 +1523,37 @@ QVariant PSDHandler::option(ImageOption option) const
{ {
QVariant v; QVariant v;
if (option == QImageIOHandler::Size) { if (auto dev = device()) {
auto&& header = d->m_header; if (!d->isValid()) {
if (IsValid(header)) { QDataStream s(dev);
v = QVariant::fromValue(QSize(header.width, header.height));
} else if (auto dev = device()) {
auto ba = dev->peek(sizeof(PSDHeader));
QDataStream s(ba);
s.setByteOrder(QDataStream::BigEndian); s.setByteOrder(QDataStream::BigEndian);
d->readInfo(s);
}
}
s >> header; if (option == QImageIOHandler::Size) {
if (s.status() == QDataStream::Ok && IsValid(header)) if (d->isValid()) {
v = QVariant::fromValue(QSize(header.width, header.height)); v = QVariant::fromValue(d->size());
}
}
if (option == QImageIOHandler::ImageFormat) {
if (d->isValid()) {
v = QVariant::fromValue(d->format());
}
}
if (option == QImageIOHandler::ImageTransformation) {
if (d->isValid()) {
v = QVariant::fromValue(int(d->transformation()));
}
}
if (option == QImageIOHandler::Description) {
if (d->isValid()) {
auto descr = d->m_exif.description();
if (!descr.isEmpty())
v = QVariant::fromValue(descr);
} }
} }