diff --git a/autotests/ossfuzz/build_fuzzers.sh b/autotests/ossfuzz/build_fuzzers.sh index a864d82..294c607 100755 --- a/autotests/ossfuzz/build_fuzzers.sh +++ b/autotests/ossfuzz/build_fuzzers.sh @@ -186,7 +186,7 @@ echo "$HANDLER_TYPES" | while read format; do /usr/libexec/moc $SRC/kimageformats/src/imageformats/$format.cpp -o $format.moc header=`ls $SRC/kimageformats/src/imageformats/$format*.h` /usr/libexec/moc $header -o moc_`basename $header .h`.cpp - $CXX $CXXFLAGS $LDFLAGS -fPIC -DKIMG_FUZZER_${format}=1 -std=c++17 autotests/ossfuzz/kimgio_fuzzer.cc $SRC/kimageformats/src/imageformats/$format.cpp $SRC/kimageformats/src/imageformats/scanlineconverter.cpp $SRC/kimageformats/src/imageformats/microexif.cpp $SRC/kimageformats/src/imageformats/chunks.cpp -o $OUT/$fuzz_target_name -DJXL_STATIC_DEFINE -DJXL_THREADS_STATIC_DEFINE -DJXL_CMS_STATIC_DEFINE -D__ANSI__ -I $SRC/kimageformats/src/imageformats/ -I $SRC/libavif/include/ -I $SRC/libjxl/build/lib/include/ -I $SRC/libjxl/lib/include/ -I /usr/local/include/OpenEXR/ -I /usr/local/include/KF6/KArchive/ -I /usr/local/include/openjpeg-2.5 -I /usr/local/include/Imath -I $SRC/jxrlib/common/include -I $SRC/jxrlib/jxrgluelib -I $SRC/jxrlib/image/sys -I /usr/include/QtCore/ -I /usr/include/QtGui/ -I . $SRC/libavif/build/libavif.a /usr/local/lib/libheif.a /usr/local/lib/libde265.a /usr/local/lib/libopenh264.a $SRC/aom/build.libavif/libaom.a $SRC/libjxl/build/lib/libjxl_threads.a $SRC/libjxl/build/lib/libjxl.a $SRC/libjxl/build/lib/libjxl_cms.a $SRC/libjxl/build/third_party/highway/libhwy.a $SRC/libjxl/build/third_party/brotli/libbrotlidec.a $SRC/libjxl/build/third_party/brotli/libbrotlienc.a $SRC/libjxl/build/third_party/brotli/libbrotlicommon.a -lQt6Gui -lQt6Core -lQt6BundledLibpng -lQt6BundledHarfbuzz -lm -lQt6BundledPcre2 -ldl -lpthread $LIB_FUZZING_ENGINE /usr/local/lib/libz.a /usr/local/lib/x86_64-linux-gnu/libKF6Archive.a /usr/local/lib/libz.a /usr/local/lib/libraw.a /usr/local/lib/libOpenEXR-3_4.a /usr/local/lib/libIex-3_4.a /usr/local/lib/libImath-3_2.a /usr/local/lib/libIlmThread-3_4.a /usr/local/lib/libOpenEXRCore-3_4.a /usr/local/lib/libOpenEXRUtil-3_4.a /usr/local/lib/libopenjp2.a /usr/local/lib/libzstd.a $SRC/jxrlib/build/libjxrglue.a $SRC/jxrlib/build/libjpegxr.a /usr/local/lib/liblzma.a /usr/local/lib/libbz2.a -lclang_rt.builtins + $CXX $CXXFLAGS $LDFLAGS -fPIC -DKIMG_FUZZER_${format}=1 -std=c++17 autotests/ossfuzz/kimgio_fuzzer.cc $SRC/kimageformats/src/imageformats/$format.cpp $SRC/kimageformats/src/imageformats/scanlineconverter.cpp $SRC/kimageformats/src/imageformats/photoshop.cpp $SRC/kimageformats/src/imageformats/microexif.cpp $SRC/kimageformats/src/imageformats/chunks.cpp -o $OUT/$fuzz_target_name -DJXL_STATIC_DEFINE -DJXL_THREADS_STATIC_DEFINE -DJXL_CMS_STATIC_DEFINE -D__ANSI__ -I $SRC/kimageformats/src/imageformats/ -I $SRC/libavif/include/ -I $SRC/libjxl/build/lib/include/ -I $SRC/libjxl/lib/include/ -I /usr/local/include/OpenEXR/ -I /usr/local/include/KF6/KArchive/ -I /usr/local/include/openjpeg-2.5 -I /usr/local/include/Imath -I $SRC/jxrlib/common/include -I $SRC/jxrlib/jxrgluelib -I $SRC/jxrlib/image/sys -I /usr/include/QtCore/ -I /usr/include/QtGui/ -I . $SRC/libavif/build/libavif.a /usr/local/lib/libheif.a /usr/local/lib/libde265.a /usr/local/lib/libopenh264.a $SRC/aom/build.libavif/libaom.a $SRC/libjxl/build/lib/libjxl_threads.a $SRC/libjxl/build/lib/libjxl.a $SRC/libjxl/build/lib/libjxl_cms.a $SRC/libjxl/build/third_party/highway/libhwy.a $SRC/libjxl/build/third_party/brotli/libbrotlidec.a $SRC/libjxl/build/third_party/brotli/libbrotlienc.a $SRC/libjxl/build/third_party/brotli/libbrotlicommon.a -lQt6Gui -lQt6Core -lQt6BundledLibpng -lQt6BundledHarfbuzz -lm -lQt6BundledPcre2 -ldl -lpthread $LIB_FUZZING_ENGINE /usr/local/lib/libz.a /usr/local/lib/x86_64-linux-gnu/libKF6Archive.a /usr/local/lib/libz.a /usr/local/lib/libraw.a /usr/local/lib/libOpenEXR-3_4.a /usr/local/lib/libIex-3_4.a /usr/local/lib/libImath-3_2.a /usr/local/lib/libIlmThread-3_4.a /usr/local/lib/libOpenEXRCore-3_4.a /usr/local/lib/libOpenEXRUtil-3_4.a /usr/local/lib/libopenjp2.a /usr/local/lib/libzstd.a $SRC/jxrlib/build/libjxrglue.a $SRC/jxrlib/build/libjpegxr.a /usr/local/lib/liblzma.a /usr/local/lib/libbz2.a -lclang_rt.builtins # -lclang_rt.builtins in the previous line is a temporary workaround to avoid a linker error "undefined reference to __truncsfhf2". Investigate why this is needed here, but not anywhere else, and possibly remove it. diff --git a/autotests/read/exr/alphatest_rgba.exr.json b/autotests/read/exr/alphatest_rgba.exr.json new file mode 100644 index 0000000..c500c73 --- /dev/null +++ b/autotests/read/exr/alphatest_rgba.exr.json @@ -0,0 +1,33 @@ +[ + { + "fileName" : "alphatest_rgba.tif", + "colorSpace" : { + "description" : "sRGB build-in (Profilo RGB lineare)", + "primaries" : "SRgb", + "transferFunction" : "Linear", + "gamma" : 1 + }, + "metadata" : [ + { + "key" : "ModificationDate", + "value" : "2026-09-04T11:45:06" + }, + { + "key" : "Software" , + "value" : "Adobe Photoshop 27.10 (Windows)" + }, + { + "key" : "Author", + "value" : "Mirco Miranda" + }, + { + "key" : "Description", + "value" : "This test is designed to detect problems with alpha encoding. Specifically, it detects incorrect use of associated values ​​(premultiplied alpha) and unassociated values ​​(normal alpha)." + } + ], + "resolution" : { + "dotsPerMeterX" : 11811, + "dotsPerMeterY" : 11811 + } + } +] diff --git a/autotests/read/exr/ps2026_testcard_rgb.exr.json b/autotests/read/exr/ps2026_testcard_rgb.exr.json index 73df355..80d0d0b 100644 --- a/autotests/read/exr/ps2026_testcard_rgb.exr.json +++ b/autotests/read/exr/ps2026_testcard_rgb.exr.json @@ -7,9 +7,19 @@ "transferFunction" : "Linear", "gamma" : 1 }, + "metadata" : [ + { + "key" : "ModificationDate", + "value" : "2026-04-15T10:48:48" + }, + { + "key" : "Software" , + "value" : "Adobe Photoshop 27.5 (Windows)" + } + ], "resolution" : { - "dotsPerMeterX" : 3937, - "dotsPerMeterY" : 3937 + "dotsPerMeterX" : 11811, + "dotsPerMeterY" : 11811 } } ] diff --git a/src/imageformats/CMakeLists.txt b/src/imageformats/CMakeLists.txt index 2398f48..a79214e 100644 --- a/src/imageformats/CMakeLists.txt +++ b/src/imageformats/CMakeLists.txt @@ -57,7 +57,7 @@ endif() ################################## if(OpenEXR_FOUND) - kimageformats_add_plugin(kimg_exr SOURCES exr.cpp scanlineconverter.cpp) + kimageformats_add_plugin(kimg_exr SOURCES exr.cpp microexif.cpp photoshop.cpp scanlineconverter.cpp) if(TARGET OpenEXR::OpenEXR) target_link_libraries(kimg_exr PRIVATE OpenEXR::OpenEXR) else() @@ -117,7 +117,7 @@ kimageformats_add_plugin(kimg_pfm SOURCES pfm.cpp) ################################## -kimageformats_add_plugin(kimg_psd SOURCES psd.cpp microexif.cpp scanlineconverter.cpp) +kimageformats_add_plugin(kimg_psd SOURCES psd.cpp microexif.cpp photoshop.cpp scanlineconverter.cpp) ################################## diff --git a/src/imageformats/exr.cpp b/src/imageformats/exr.cpp index 8eccfd3..9dccede 100644 --- a/src/imageformats/exr.cpp +++ b/src/imageformats/exr.cpp @@ -8,6 +8,8 @@ */ #include "exr_p.h" +#include "microexif_p.h" +#include "photoshop_p.h" #include "scanlineconverter_p.h" #include "util_p.h" @@ -385,6 +387,34 @@ static void readMetadata(const Imf::Header &header, QImage &image) if (auto focalLen = header.findTypedAttribute("effectiveFocalLength")) { image.setText(QStringLiteral(META_KEY_FOCALLENGTH), QLocale::c().toString(focalLen->value())); } + + // Photoshop image resource section + if (auto adobe = header.findTypedAttribute("adobe_rsrc")) { + auto &&data = adobe->data(); + auto ba = QByteArray(data, data.size()); + ba.prepend((data.size()) & 0xFF); + ba.prepend((data.size() >> 8) & 0xFF); + ba.prepend((data.size() >> 16) & 0xFF); + ba.prepend((data.size() >> 24) & 0xFF); + QDataStream ds(ba); + ds.setByteOrder(QDataStream::BigEndian); + auto ok = false; + auto irs = readImageResourceSection(ds, &ok); + if (ok) { + if (irs.contains(IRI_EXIFDATA1)) { + auto exif = MicroExif::fromByteArray(irs.value(IRI_EXIFDATA1).data); + exif.updateImageMetadata(image); + exif.updateImageResolution(image); + } + + if (irs.contains(IRI_XMPMETADATA)) { + auto irb = irs.value(IRI_XMPMETADATA); + auto xmp = QString::fromUtf8(irb.data); + if (!xmp.isEmpty()) + image.setText(QStringLiteral(META_KEY_XMP_ADOBE), xmp); + } + } + } } /*! diff --git a/src/imageformats/photoshop.cpp b/src/imageformats/photoshop.cpp new file mode 100644 index 0000000..4e8103a --- /dev/null +++ b/src/imageformats/photoshop.cpp @@ -0,0 +1,140 @@ +/* + This file is part of the KDE project + SPDX-FileCopyrightText: 2026 Mirco Miranda + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ +#include "photoshop_p.h" +#include "util_p.h" + +#include + +#ifdef QT_DEBUG +Q_LOGGING_CATEGORY(LOG_PSDSHARED, "kf.imageformats.plugins.psdshared", QtDebugMsg) +#else +Q_LOGGING_CATEGORY(LOG_PSDSHARED, "kf.imageformats.plugins.psdshared", QtWarningMsg) +#endif + +QString readPascalString(QDataStream &s, qint32 alignBytes, qint32 *size) +{ + 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; +} + +PSDImageResourceSection readImageResourceSection(QDataStream &s, bool *ok) +{ + PSDImageResourceSection irs; + + bool tmp = true; + if (ok == nullptr) + ok = &tmp; + *ok = true; + + // Section size + quint32 tmpSize; + s >> tmpSize; + qint64 sectioSize = tmpSize; + + // Reading Image resource block + for (auto size = sectioSize; size > 0;) { + +#define DEC_SIZE(value) \ + if ((size -= qint64(value)) < 0) { \ + *ok = false; \ + break; } + + // 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; + DEC_SIZE(sizeof(signature)) + // NOTE: MeSa signature is not documented but found in some old PSD take from Photoshop 7.0 CD. + if (signature != S_8BIM && signature != S_MeSa) { // 8BIM and MeSa + qCDebug(LOG_PSDSHARED) << "Invalid Image Resource Block Signature!"; + *ok = false; + break; + } + + // id + quint16 id; + s >> id; + DEC_SIZE(sizeof(id)) + + // getting data + PSDImageResourceBlock irb; + + // name + qint32 bytes = 0; + irb.name = readPascalString(s, 2, &bytes); + DEC_SIZE(bytes) + + // data read + quint32 dataSize; + s >> dataSize; + DEC_SIZE(sizeof(dataSize)) + if (auto dev = s.device()) { + if (dataSize > size) { + qCDebug(LOG_PSDSHARED) << "Invalid Image Resource Block Data Size!"; + *ok = false; + break; + } + irb.data = deviceRead(dev, dataSize); + } + auto read = irb.data.size(); + if (read > 0) { + DEC_SIZE(read) + } + if (read != qint64(dataSize)) { + qCDebug(LOG_PSDSHARED) << "Image Resource Block Read Error!"; + *ok = false; + break; + } + + if (auto pad = dataSize % 2) { + auto skipped = s.skipRawData(pad); + if (skipped > 0) { + DEC_SIZE(skipped); + } + } + + // insert IRB + irs.insert(ImageResourceId(id), irb); + +#undef DEC_SIZE + } + + return irs; +} diff --git a/src/imageformats/photoshop_p.h b/src/imageformats/photoshop_p.h new file mode 100644 index 0000000..6db37eb --- /dev/null +++ b/src/imageformats/photoshop_p.h @@ -0,0 +1,80 @@ +/* + This file is part of the KDE project + SPDX-FileCopyrightText: 2026 Mirco Miranda + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ +#ifndef PHOTOSHOP_P_H +#define PHOTOSHOP_P_H + +#include +#include +#include + +enum Signature : quint32 { + S_8BIM = 0x3842494D, // '8BIM' + S_8B64 = 0x38423634, // '8B64' + + S_MeSa = 0x4D655361 // 'MeSa' +}; + +enum ColorMode : quint16 { + CM_BITMAP = 0, + CM_GRAYSCALE = 1, + CM_INDEXED = 2, + CM_RGB = 3, + CM_CMYK = 4, + CM_MULTICHANNEL = 7, + CM_DUOTONE = 8, + CM_LABCOLOR = 9, +}; + +enum ImageResourceId : quint16 { + IRI_RESOLUTIONINFO = 0x03ED, + IRI_ICCPROFILE = 0x040F, + IRI_TRANSPARENCYINDEX = 0x0417, + IRI_ALPHAIDENTIFIERS = 0x041D, + IRI_VERSIONINFO = 0x0421, + IRI_EXIFDATA1 = 0x0422, + IRI_EXIFDATA3 = 0x0423, // never seen + IRI_XMPMETADATA = 0x0424 +}; + +enum LayerId : quint32 { + LI_MT16 = 0x4D743136, // 'Mt16', + LI_MT32 = 0x4D743332, // 'Mt32', + LI_MTRN = 0x4D74726E // 'Mtrn' +}; + +/*! + * \brief The PSDImageResourceBlock class + * Raw data of a PSD image resource block + */ +struct PSDImageResourceBlock { + QString name; + QByteArray data; +}; + +using PSDImageResourceSection = QHash; + + +/*! + * \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. + */ +QString readPascalString(QDataStream &s, qint32 alignBytes = 1, qint32 *size = nullptr); + +/*! + * \brief readImageResourceSection + * Reads the image resource section. + * \param s The stream. + * \param ok Pointer to the operation result variable. + * \return The image resource section raw data. + */ +PSDImageResourceSection readImageResourceSection(QDataStream &s, bool *ok = nullptr); + +#endif // PHOTOSHOP_P_H diff --git a/src/imageformats/psd.cpp b/src/imageformats/psd.cpp index 91cc376..be066c2 100644 --- a/src/imageformats/psd.cpp +++ b/src/imageformats/psd.cpp @@ -29,12 +29,12 @@ #include "fastmath_p.h" #include "microexif_p.h" #include "packbits_p.h" +#include "photoshop_p.h" #include "psd_p.h" #include "scanlineconverter_p.h" #include "util_p.h" #include -#include #include #include @@ -99,41 +99,6 @@ namespace // Private. #define NATIVE_CMYK (CMYK_FORMAT != QImage::Format_Invalid) -enum Signature : quint32 { - S_8BIM = 0x3842494D, // '8BIM' - S_8B64 = 0x38423634, // '8B64' - - S_MeSa = 0x4D655361 // 'MeSa' -}; - -enum ColorMode : quint16 { - CM_BITMAP = 0, - CM_GRAYSCALE = 1, - CM_INDEXED = 2, - CM_RGB = 3, - CM_CMYK = 4, - CM_MULTICHANNEL = 7, - CM_DUOTONE = 8, - CM_LABCOLOR = 9, -}; - -enum ImageResourceId : quint16 { - IRI_RESOLUTIONINFO = 0x03ED, - IRI_ICCPROFILE = 0x040F, - IRI_TRANSPARENCYINDEX = 0x0417, - IRI_ALPHAIDENTIFIERS = 0x041D, - IRI_VERSIONINFO = 0x0421, - IRI_EXIFDATA1 = 0x0422, - IRI_EXIFDATA3 = 0x0423, // never seen - IRI_XMPMETADATA = 0x0424 -}; - -enum LayerId : quint32 { - LI_MT16 = 0x4D743136, // 'Mt16', - LI_MT32 = 0x4D743332, // 'Mt32', - LI_MTRN = 0x4D74726E // 'Mtrn' -}; - struct PSDHeader { PSDHeader() { memset(this, 0, sizeof(PSDHeader)); @@ -149,11 +114,6 @@ struct PSDHeader { ushort color_mode; }; -struct PSDImageResourceBlock { - QString name; - QByteArray data; -}; - /*! * \brief The PSDDuotoneOptions struct * \note You can decode the duotone data using the "Duotone Options" @@ -172,7 +132,6 @@ struct PSDColorModeDataSection { QList palette; }; -using PSDImageResourceSection = QHash; struct PSDLayerInfo { qint64 size = -1; @@ -273,145 +232,6 @@ static bool skip_section(QDataStream &s, bool psb = false) return skip_data(s, section_length); } -/*! - * \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. - * \param ok Pointer to the operation result variable. - * \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 - quint32 tmpSize; - s >> tmpSize; - qint64 sectioSize = tmpSize; - - // Reading Image resource block - for (auto size = sectioSize; size > 0;) { - -#define DEC_SIZE(value) \ - if ((size -= qint64(value)) < 0) { \ - *ok = false; \ - break; } - - // 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; - DEC_SIZE(sizeof(signature)) - // NOTE: MeSa signature is not documented but found in some old PSD take from Photoshop 7.0 CD. - if (signature != S_8BIM && signature != S_MeSa) { // 8BIM and MeSa - qCDebug(LOG_PSDPLUGIN) << "Invalid Image Resource Block Signature!"; - *ok = false; - break; - } - - // id - quint16 id; - s >> id; - DEC_SIZE(sizeof(id)) - - // getting data - PSDImageResourceBlock irb; - - // name - qint32 bytes = 0; - irb.name = readPascalString(s, 2, &bytes); - DEC_SIZE(bytes) - - // data read - quint32 dataSize; - s >> dataSize; - DEC_SIZE(sizeof(dataSize)) - if (auto dev = s.device()) { - if (dataSize > size) { - qCDebug(LOG_PSDPLUGIN) << "Invalid Image Resource Block Data Size!"; - *ok = false; - break; - } - irb.data = deviceRead(dev, dataSize); - } - auto read = irb.data.size(); - if (read > 0) { - DEC_SIZE(read) - } - if (read != qint64(dataSize)) { - qCDebug(LOG_PSDPLUGIN) << "Image Resource Block Read Error!"; - *ok = false; - break; - } - - if (auto pad = dataSize % 2) { - auto skipped = s.skipRawData(pad); - if (skipped > 0) { - DEC_SIZE(skipped); - } - } - - // insert IRB - irs.insert(id, irb); - -#undef DEC_SIZE - } - - return irs; -} - PSDAdditionalLayerInfo readAdditionalLayer(QDataStream &s, bool *ok = nullptr) { PSDAdditionalLayerInfo li;