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
* Adds EXIF metadata to QImage.
* \param img The image.
* \param irs The image resource section.
* \param exif The decoded EXIF data.
* \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())
return false;
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.
* \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)
static bool HasMergedData(const PSDImageResourceSection &irs)
{
if (!irs.contains(IRI_VERSIONINFO))
return true;
@ -1107,39 +1103,161 @@ bool readChannel(QByteArray& target, QDataStream &stream, quint32 compressedSize
return stream.status() == QDataStream::Ok;
}
// Load the PSD image.
static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img)
} // Private
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
auto isPsb = header.version == 2;
bool ok = false;
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
auto cmds = readColorModeDataSection(stream, &ok);
m_cmds = readColorModeDataSection(stream, &ok);
if (!ok) {
qDebug() << "Error while skipping Color Mode Data section";
return false;
}
// Image Resources Section
auto irs = readImageResourceSection(stream, &ok);
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(irs)) {
if (!hasMergedData()) {
qDebug() << "No merged data found";
return false;
}
// Layer and Mask section
auto lms = readLayerAndMaskSection(stream, isPsb, &ok);
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.
// Known values:
// 0: no compression
@ -1151,19 +1269,13 @@ static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img)
return false;
}
// 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 = header.color_mode == CM_RGB;
if (!lms.isNull())
alpha = lms.hasAlpha();
const QImage::Format format = imageFormat(header, alpha);
const QImage::Format format = d->format();
if (format == QImage::Format_Invalid) {
qWarning() << "Unsupported image format. color_mode:" << header.color_mode << "depth:" << header.depth << "channel_count:" << header.channel_count;
return false;
}
img = imageAlloc(header.width, header.height, format);
img = imageAlloc(d->size(), format);
if (img.isNull()) {
qWarning() << "Failed to allocate image, invalid dimensions?" << QSize(header.width, header.height);
return false;
@ -1379,7 +1491,7 @@ static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img)
}
// EXIF data
if (!setExifData(img, irs)) {
if (!setExifData(img, d->m_exif)) {
// 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()));
}
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;
return true;
}
@ -1452,6 +1510,12 @@ bool PSDHandler::supportsOption(ImageOption option) const
{
if (option == QImageIOHandler::Size)
return true;
if (option == QImageIOHandler::ImageFormat)
return true;
if (option == QImageIOHandler::ImageTransformation)
return true;
if (option == QImageIOHandler::Description)
return true;
return false;
}
@ -1459,18 +1523,37 @@ QVariant PSDHandler::option(ImageOption option) const
{
QVariant v;
if (option == QImageIOHandler::Size) {
auto&& header = d->m_header;
if (IsValid(header)) {
v = QVariant::fromValue(QSize(header.width, header.height));
} else if (auto dev = device()) {
auto ba = dev->peek(sizeof(PSDHeader));
QDataStream s(ba);
if (auto dev = device()) {
if (!d->isValid()) {
QDataStream s(dev);
s.setByteOrder(QDataStream::BigEndian);
d->readInfo(s);
}
}
s >> header;
if (s.status() == QDataStream::Ok && IsValid(header))
v = QVariant::fromValue(QSize(header.width, header.height));
if (option == QImageIOHandler::Size) {
if (d->isValid()) {
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);
}
}