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

@ -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;
@ -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
{
public:
PSDHandlerPrivate()
{
}
~PSDHandlerPrivate()
{
}
bool isPsb() const
{
return m_header.version == 2;
}
bool isValid() const
{
return IsValid(m_header);
}
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 // Checking for PSB
auto isPsb = header.version == 2; auto ok = false;
bool 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 // Color Mode Data section
auto cmds = readColorModeDataSection(stream, &ok); m_cmds = readColorModeDataSection(stream, &ok);
if (!ok) { if (!ok) {
qDebug() << "Error while skipping Color Mode Data section"; qDebug() << "Error while skipping Color Mode Data section";
return false; return false;
} }
// Image Resources Section // Image Resources Section
auto irs = readImageResourceSection(stream, &ok); m_irs = readImageResourceSection(stream, &ok);
if (!ok) { if (!ok) {
qDebug() << "Error while reading Image Resources Section"; qDebug() << "Error while reading Image Resources Section";
return false; return false;
} }
// Checking for merged image (Photoshop compatibility data) // Checking for merged image (Photoshop compatibility data)
if (!hasMergedData(irs)) { if (!hasMergedData()) {
qDebug() << "No merged data found"; qDebug() << "No merged data found";
return false; return false;
} }
// Layer and Mask section // Layer and Mask section
auto lms = readLayerAndMaskSection(stream, isPsb, &ok); m_lms = readLayerAndMaskSection(stream, isPsb(), &ok);
if (!ok) { if (!ok) {
qDebug() << "Error while skipping Layer and Mask section"; qDebug() << "Error while skipping Layer and Mask section";
return false; 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;
@ -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);
} }
} }