Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package kimageformats for openSUSE:Factory checked in at 2022-05-16 18:06:58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/kimageformats (Old) and /work/SRC/openSUSE:Factory/.kimageformats.new.1538 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "kimageformats" Mon May 16 18:06:58 2022 rev:106 rq:977178 version:5.94.0 Changes: -------- --- /work/SRC/openSUSE:Factory/kimageformats/kimageformats.changes 2022-04-11 23:48:35.951512248 +0200 +++ /work/SRC/openSUSE:Factory/.kimageformats.new.1538/kimageformats.changes 2022-05-16 18:08:47.797289923 +0200 @@ -1,0 +2,23 @@ +Tue May 10 08:17:59 UTC 2022 - Christophe Giboudeaux <[email protected]> + +- Update to 5.94.0 + * New feature release + * For more details please see: + * https://kde.org/announcements/frameworks/5/5.94.0 +- Changes since 5.93.0: + * avif: prepare for breaking change in libavif + * XCF: Support to QImageIOHandler::Size option + * Support to QImageIOHandler::Size option + * QByteArray resize removal + * psd: Fix crash on broken files + * psd: duotone read + * psd: Don't crash with broken images + * psd: Header depth has to be 8 for CM_INDEXED color_mode + * psd: Protect against broken images + * psd: Don't abort on broken images + * avif: lossless support + * psd: Don't assert on broken files + * Add windows CI + * PSD: Performance improvements and support to missing common formats + +------------------------------------------------------------------- Old: ---- kimageformats-5.93.0.tar.xz kimageformats-5.93.0.tar.xz.sig New: ---- kimageformats-5.94.0.tar.xz kimageformats-5.94.0.tar.xz.sig ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ kimageformats.spec ++++++ --- /var/tmp/diff_new_pack.WsRoLz/_old 2022-05-16 18:08:48.397290491 +0200 +++ /var/tmp/diff_new_pack.WsRoLz/_new 2022-05-16 18:08:48.401290494 +0200 @@ -22,7 +22,7 @@ %if 0%{?suse_version} > 1500 || (0%{?is_opensuse} && 0%{?sle_version} >= 150300) %define with_heif 1 %endif -%define _tar_path 5.93 +%define _tar_path 5.94 # Full KF5 version (e.g. 5.33.0) %{!?_kf5_version: %global _kf5_version %{version}} # Last major and minor KF5 version (e.g. 5.33) @@ -30,7 +30,7 @@ # Only needed for the package signature condition %bcond_without released Name: kimageformats -Version: 5.93.0 +Version: 5.94.0 Release: 0 Summary: Image format plugins for Qt License: LGPL-2.1-or-later ++++++ kimageformats-5.93.0.tar.xz -> kimageformats-5.94.0.tar.xz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kimageformats-5.93.0/.gitlab-ci.yml new/kimageformats-5.94.0/.gitlab-ci.yml --- old/kimageformats-5.93.0/.gitlab-ci.yml 2022-04-02 12:00:12.000000000 +0200 +++ new/kimageformats-5.94.0/.gitlab-ci.yml 2022-05-02 11:46:37.000000000 +0200 @@ -7,3 +7,4 @@ - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/freebsd.yml - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux-qt6.yml - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android-qt6.yml + - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows.yml diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kimageformats-5.93.0/README.md new/kimageformats-5.94.0/README.md --- old/kimageformats-5.93.0/README.md 2022-04-02 12:00:12.000000000 +0200 +++ new/kimageformats-5.94.0/README.md 2022-05-02 11:46:37.000000000 +0200 @@ -16,7 +16,7 @@ - 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: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kimageformats-5.93.0/autotests/CMakeLists.txt new/kimageformats-5.94.0/autotests/CMakeLists.txt --- old/kimageformats-5.93.0/autotests/CMakeLists.txt 2022-04-02 12:00:12.000000000 +0200 +++ new/kimageformats-5.94.0/autotests/CMakeLists.txt 2022-05-02 11:46:37.000000000 +0200 @@ -86,8 +86,7 @@ kimageformats_read_tests( avif ) - # because the plug-ins use RGB->YUV conversion which sometimes results in 1 value difference. - kimageformats_write_tests(FUZZ 1 + kimageformats_write_tests( avif-nodatacheck-lossless ) endif() Binary files old/kimageformats-5.93.0/autotests/read/psd/16bit_grayscale.png and new/kimageformats-5.94.0/autotests/read/psd/16bit_grayscale.png differ Binary files old/kimageformats-5.93.0/autotests/read/psd/16bit_grayscale.psd and new/kimageformats-5.94.0/autotests/read/psd/16bit_grayscale.psd differ Binary files old/kimageformats-5.93.0/autotests/read/psd/16bit_photoshop.png and new/kimageformats-5.94.0/autotests/read/psd/16bit_photoshop.png differ Binary files old/kimageformats-5.93.0/autotests/read/psd/16bit_photoshop.psb and new/kimageformats-5.94.0/autotests/read/psd/16bit_photoshop.psb differ Binary files old/kimageformats-5.93.0/autotests/read/psd/32bit-rgb.png and new/kimageformats-5.94.0/autotests/read/psd/32bit-rgb.png differ Binary files old/kimageformats-5.93.0/autotests/read/psd/32bit-rgb.psd and new/kimageformats-5.94.0/autotests/read/psd/32bit-rgb.psd differ Binary files old/kimageformats-5.93.0/autotests/read/psd/32bit_grayscale.png and new/kimageformats-5.94.0/autotests/read/psd/32bit_grayscale.png differ Binary files old/kimageformats-5.93.0/autotests/read/psd/32bit_grayscale.psd and new/kimageformats-5.94.0/autotests/read/psd/32bit_grayscale.psd differ Binary files old/kimageformats-5.93.0/autotests/read/psd/8bit-grayscale.png and new/kimageformats-5.94.0/autotests/read/psd/8bit-grayscale.png differ Binary files old/kimageformats-5.93.0/autotests/read/psd/8bit-grayscale.psd and new/kimageformats-5.94.0/autotests/read/psd/8bit-grayscale.psd differ Binary files old/kimageformats-5.93.0/autotests/read/psd/8bit-photoshop.png and new/kimageformats-5.94.0/autotests/read/psd/8bit-photoshop.png differ Binary files old/kimageformats-5.93.0/autotests/read/psd/8bit-photoshop.psb and new/kimageformats-5.94.0/autotests/read/psd/8bit-photoshop.psb differ Binary files old/kimageformats-5.93.0/autotests/read/psd/adobehq-2_5.png and new/kimageformats-5.94.0/autotests/read/psd/adobehq-2_5.png differ Binary files old/kimageformats-5.93.0/autotests/read/psd/adobehq-2_5.psd and new/kimageformats-5.94.0/autotests/read/psd/adobehq-2_5.psd differ Binary files old/kimageformats-5.93.0/autotests/read/psd/birthday.pdd and new/kimageformats-5.94.0/autotests/read/psd/birthday.pdd differ Binary files old/kimageformats-5.93.0/autotests/read/psd/birthday.png and new/kimageformats-5.94.0/autotests/read/psd/birthday.png differ Binary files old/kimageformats-5.93.0/autotests/read/psd/bitmap.png and new/kimageformats-5.94.0/autotests/read/psd/bitmap.png differ Binary files old/kimageformats-5.93.0/autotests/read/psd/bitmap.psd and new/kimageformats-5.94.0/autotests/read/psd/bitmap.psd differ Binary files old/kimageformats-5.93.0/autotests/read/psd/duotone.png and new/kimageformats-5.94.0/autotests/read/psd/duotone.png differ Binary files old/kimageformats-5.93.0/autotests/read/psd/duotone.psb and new/kimageformats-5.94.0/autotests/read/psd/duotone.psb differ Binary files old/kimageformats-5.93.0/autotests/read/psd/indexed.png and new/kimageformats-5.94.0/autotests/read/psd/indexed.png differ Binary files old/kimageformats-5.93.0/autotests/read/psd/indexed.psd and new/kimageformats-5.94.0/autotests/read/psd/indexed.psd differ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kimageformats-5.93.0/src/imageformats/avif.cpp new/kimageformats-5.94.0/src/imageformats/avif.cpp --- old/kimageformats-5.93.0/src/imageformats/avif.cpp 2022-04-02 12:00:12.000000000 +0200 +++ new/kimageformats-5.94.0/src/imageformats/avif.cpp 2022-05-02 11:46:37.000000000 +0200 @@ -452,8 +452,13 @@ return false; } - if (m_quality >= 100 && !avifCodecName(AVIF_CODEC_CHOICE_AOM, AVIF_CODEC_FLAG_CAN_ENCODE)) { - qWarning("You are using %s encoder. It is recommended to enable libAOM encoder in libavif for better near-lossless compression.", encoder_name); + bool lossless = false; + if (m_quality >= 100) { + if (avifCodecName(AVIF_CODEC_CHOICE_AOM, AVIF_CODEC_FLAG_CAN_ENCODE)) { + lossless = true; + } else { + qWarning("You are using %s encoder. It is recommended to enable libAOM encoder in libavif to use lossless compression.", encoder_name); + } } int maxQuantizer = AVIF_QUANTIZER_WORST_QUALITY * (100 - qBound(0, m_quality, 100)) / 100; @@ -648,43 +653,47 @@ // in case primaries or trc were not identified if ((primaries_to_save == 2) || (transfer_to_save == 2)) { - // upgrade image to higher bit depth - if (save_depth == 8) { - save_depth = 10; - if (tmpcolorimage.hasAlphaChannel()) { - tmpcolorimage = tmpcolorimage.convertToFormat(QImage::Format_RGBA64); - } else { - tmpcolorimage = tmpcolorimage.convertToFormat(QImage::Format_RGBX64); + if (lossless) { + iccprofile = tmpcolorimage.colorSpace().iccProfile(); + } else { + // upgrade image to higher bit depth + if (save_depth == 8) { + save_depth = 10; + if (tmpcolorimage.hasAlphaChannel()) { + tmpcolorimage = tmpcolorimage.convertToFormat(QImage::Format_RGBA64); + } else { + tmpcolorimage = tmpcolorimage.convertToFormat(QImage::Format_RGBX64); + } } - } - if ((primaries_to_save == 2) && (transfer_to_save != 2)) { // other primaries but known trc - primaries_to_save = (avifColorPrimaries)1; // AVIF_COLOR_PRIMARIES_BT709 - matrix_to_save = (avifMatrixCoefficients)1; // AVIF_MATRIX_COEFFICIENTS_BT709 - - switch (transfer_to_save) { - case 8: // AVIF_TRANSFER_CHARACTERISTICS_LINEAR - tmpcolorimage.convertToColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, QColorSpace::TransferFunction::Linear)); - break; - case 4: // AVIF_TRANSFER_CHARACTERISTICS_BT470M - tmpcolorimage.convertToColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, 2.2f)); - break; - case 5: // AVIF_TRANSFER_CHARACTERISTICS_BT470BG - tmpcolorimage.convertToColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, 2.8f)); - break; - default: // AVIF_TRANSFER_CHARACTERISTICS_SRGB + any other - tmpcolorimage.convertToColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, QColorSpace::TransferFunction::SRgb)); + if ((primaries_to_save == 2) && (transfer_to_save != 2)) { // other primaries but known trc + primaries_to_save = (avifColorPrimaries)1; // AVIF_COLOR_PRIMARIES_BT709 + matrix_to_save = (avifMatrixCoefficients)1; // AVIF_MATRIX_COEFFICIENTS_BT709 + + switch (transfer_to_save) { + case 8: // AVIF_TRANSFER_CHARACTERISTICS_LINEAR + tmpcolorimage.convertToColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, QColorSpace::TransferFunction::Linear)); + break; + case 4: // AVIF_TRANSFER_CHARACTERISTICS_BT470M + tmpcolorimage.convertToColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, 2.2f)); + break; + case 5: // AVIF_TRANSFER_CHARACTERISTICS_BT470BG + tmpcolorimage.convertToColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, 2.8f)); + break; + default: // AVIF_TRANSFER_CHARACTERISTICS_SRGB + any other + tmpcolorimage.convertToColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, QColorSpace::TransferFunction::SRgb)); + transfer_to_save = (avifTransferCharacteristics)13; + break; + } + } else if ((primaries_to_save != 2) && (transfer_to_save == 2)) { // recognized primaries but other trc transfer_to_save = (avifTransferCharacteristics)13; - break; + tmpcolorimage.convertToColorSpace(tmpcolorimage.colorSpace().withTransferFunction(QColorSpace::TransferFunction::SRgb)); + } else { // unrecognized profile + primaries_to_save = (avifColorPrimaries)1; // AVIF_COLOR_PRIMARIES_BT709 + transfer_to_save = (avifTransferCharacteristics)13; + matrix_to_save = (avifMatrixCoefficients)1; // AVIF_MATRIX_COEFFICIENTS_BT709 + tmpcolorimage.convertToColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, QColorSpace::TransferFunction::SRgb)); } - } else if ((primaries_to_save != 2) && (transfer_to_save == 2)) { // recognized primaries but other trc - transfer_to_save = (avifTransferCharacteristics)13; - tmpcolorimage.convertToColorSpace(tmpcolorimage.colorSpace().withTransferFunction(QColorSpace::TransferFunction::SRgb)); - } else { // unrecognized profile - primaries_to_save = (avifColorPrimaries)1; // AVIF_COLOR_PRIMARIES_BT709 - transfer_to_save = (avifTransferCharacteristics)13; - matrix_to_save = (avifMatrixCoefficients)1; // AVIF_MATRIX_COEFFICIENTS_BT709 - tmpcolorimage.convertToColorSpace(QColorSpace(QColorSpace::Primaries::SRgb, QColorSpace::TransferFunction::SRgb)); } } } else { // profile is unsupported by Qt @@ -694,6 +703,9 @@ } } + if (lossless && pixel_format == AVIF_PIXEL_FORMAT_YUV444) { + matrix_to_save = (avifMatrixCoefficients)0; + } avif = avifImageCreate(tmpcolorimage.width(), tmpcolorimage.height(), save_depth, pixel_format); avif->matrixCoefficients = matrix_to_save; @@ -712,9 +724,7 @@ if (save_depth > 8) { // 10bit depth rgb.depth = 16; - if (tmpcolorimage.hasAlphaChannel()) { - avif->alphaRange = AVIF_RANGE_FULL; - } else { + if (!tmpcolorimage.hasAlphaChannel()) { rgb.ignoreAlpha = AVIF_TRUE; } @@ -724,7 +734,6 @@ if (tmpcolorimage.hasAlphaChannel()) { rgb.format = AVIF_RGB_FORMAT_RGBA; - avif->alphaRange = AVIF_RANGE_FULL; } else { rgb.format = AVIF_RGB_FORMAT_RGB; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kimageformats-5.93.0/src/imageformats/psd.cpp new/kimageformats-5.94.0/src/imageformats/psd.cpp --- old/kimageformats-5.93.0/src/imageformats/psd.cpp 2022-04-02 12:00:12.000000000 +0200 +++ new/kimageformats-5.94.0/src/imageformats/psd.cpp 2022-05-02 11:46:37.000000000 +0200 @@ -3,6 +3,7 @@ SPDX-FileCopyrightText: 2003 Ignacio Casta??o <[email protected]> SPDX-FileCopyrightText: 2015 Alex Merry <[email protected]> + SPDX-FileCopyrightText: 2022 Mirco Miranda <[email protected]> SPDX-License-Identifier: LGPL-2.0-or-later */ @@ -17,13 +18,27 @@ * http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/ */ +/* + * 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): a conversion to + * RGB must be done. + * - The best way to convert between different color spaces is to use a + * color management engine (e.g. LittleCMS). + * - An approximate way is to ignore the color information and use + * literature formulas (possible but not recommended). + */ + #include "psd_p.h" -#include "rle_p.h" +#include "util_p.h" #include <QDataStream> #include <QDebug> #include <QImage> +#include <QColorSpace> typedef quint32 uint; typedef quint16 ushort; @@ -42,6 +57,14 @@ 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 +76,339 @@ ushort color_mode; }; +struct PSDImageResourceBlock { + QString name; + QByteArray data; +}; + +/*! + * \brief The PSDDuotoneOptions struct + * \note You can decode the duotone data using the "Duotone Options" + * file format found in the "Photoshop File Format" specs. + */ +struct PSDDuotoneOptions { + QByteArray data; +}; + +/*! + * \brief The PSDColorModeDataSection struct + * Only indexed color and duotone have color mode data. + */ +struct PSDColorModeDataSection { + PSDDuotoneOptions duotone; + QVector<QRgb> palette; +}; + +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. + * \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 + 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 take from 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); + // NOTE: Qt device::read() and QDataStream::readRawData() could read less data than specified. + // The read code should be improved. + if(auto dev = s.device()) + irb.data = dev->read(dataSize); + auto read = irb.data.size(); + if (read > 0) + size -= read; + if (read != dataSize) { + 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()) { + if ((dev->pos() - pos) != sectioSize) { + *ok = false; + } + } +#endif + + return irs; +} + +/*! + * \brief readColorModeDataSection + * Read the color mode section + * \param s The stream. + * \param ok Pointer to the operation result variable. + * \return The color mode section. + */ +PSDColorModeDataSection readColorModeDataSection(QDataStream &s, bool *ok = nullptr) +{ + PSDColorModeDataSection cms; + + bool tmp = false; + if (ok == nullptr) + ok = &tmp; + *ok = true; + + qint32 size; + s >> size; + if (size != 768) { // read the duotone data (524 bytes) + // NOTE: A RGB/Gray float image has a 112 bytes ColorModeData that could be + // the "32-bit Toning Options" of Photoshop (starts with 'hdrt'). + // Official Adobe specification tells "Only indexed color and duotone + // (see the mode field in the File header section) have color mode data.". + // See test case images 32bit_grayscale.psd and 32bit-rgb.psd + cms.duotone.data = s.device()->read(size); + if (cms.duotone.data.size() != size) + *ok = false; + } + else { // read the palette (768 bytes) + auto&& palette = cms.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 cms; +} + +/*! + * \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, 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, 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 +436,227 @@ // Check that the header is supported. static bool IsSupported(const PSDHeader &header) { - if (header.version != 1) { - return false; - } - if (header.channel_count > 16) { + if (header.version != 1 && header.version != 2) { return false; } - if (header.depth != 8 && header.depth != 16) { + if (header.depth != 8 && + header.depth != 16 && + header.depth != 32 && + header.depth != 1) { 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_DUOTONE && + 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) +{ + if (header.channel_count == 0) { + return QImage::Format_Invalid; + } + + 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: + case CM_DUOTONE: + format = header.depth == 8 ? QImage::Format_Grayscale8 : QImage::Format_Grayscale16; + break; + case CM_INDEXED: + format = header.depth == 8 ? QImage::Format_Indexed8 : QImage::Format_Invalid; + break; + case CM_BITMAP: + format = header.depth == 1 ? QImage::Format_Mono : QImage::Format_Invalid; + break; + } + return format; +} + +/*! + * \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(redPixel, qGreen(oldPixel), 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; +} + +inline quint8 xchg(quint8 v) { + return v; } -static QRgb updateGreen(QRgb oldPixel, quint8 greenPixel) + +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), greenPixel, qBlue(oldPixel), 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 updateBlue(QRgb oldPixel, quint8 bluePixel) + +template<class T> +inline void planarToChunchyFloat(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<quint16*>(target); + for (qint32 x = 0; x < width; ++x) { + auto tmp = xchg(s[x]); + t[x*cn+c] = std::min(quint16(*reinterpret_cast<float*>(&tmp) * std::numeric_limits<quint16>::max() + 0.5), + std::numeric_limits<quint16>::max()); + } } -static QRgb updateAlpha(QRgb oldPixel, quint8 alphaPixel) + +inline void monoInvert(uchar *target, const char* source, qint32 bytes) { - return qRgba(qRed(oldPixel), qGreen(oldPixel), qBlue(oldPixel), alphaPixel); + 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 cmds = readColorModeDataSection(stream, &ok); + if (!ok) { + qDebug() << "Error while skipping Color Mode Data section"; + return false; + } + + // 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; + } - // Reserved data - skip_section(stream); + // 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 +664,114 @@ // 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; + const QImage::Format format = imageFormat(header); + 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 = QImage(header.width, header.height, fmt); + img = QImage(header.width, header.height, format); 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 (!cmds.palette.isEmpty()) { + img.setColorTable(cmds.palette); + setTransparencyIndex(img, irs); } - QRgb *image_data = reinterpret_cast<QRgb *>(img.bits()); + 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; - if (!image_data) { + if (header.height > kMaxQVectorSize / header.channel_count / sizeof(quint32)) { + qWarning() << "LoadPSD() header height/channel_count too big" << header.height << header.channel_count; 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; - } - - 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]); - } - - if (!success) { - qDebug() << "decodeRLEData on channel" << channel << "failed"; - return false; + 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; } - } 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)); + + // 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; } - } 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)); + if (decompress(tmp.data(), tmp.size(), rawStride.data(), rawStride.size()) < 0) { + qDebug() << "Error while decompressing the channel" << c << "line" << y; + return false; } } - // make sure we didn't try to read past the end of the stream + else { + if (stream.readRawData(rawStride.data(), rawStride.size()) != rawStride.size()) { + qDebug() << "Error while reading the stream of channel" << c << "line" << y; + return false; + } + } + 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!"; + } + + // Duotone images: color data contains the duotone specification (not documented). + // Other applications that read Photoshop files can treat a duotone image as a gray image, + // and just preserve the contents of the duotone information when reading and writing the file. + if (!cmds.duotone.data.isEmpty()) { + img.setText(QStringLiteral("PSDDuotoneOptions"), QString::fromUtf8(cmds.duotone.data.toHex())); + } + return true; } @@ -292,6 +820,38 @@ return true; } +bool PSDHandler::supportsOption(ImageOption option) const +{ + if (option == QImageIOHandler::Size) + return true; + return false; +} + +QVariant PSDHandler::option(ImageOption option) const +{ + QVariant v; + + if (option == QImageIOHandler::Size) { + if (auto d = device()) { + // transactions works on both random and sequential devices + d->startTransaction(); + auto ba = d->read(sizeof(PSDHeader)); + d->rollbackTransaction(); + + QDataStream s(ba); + s.setByteOrder(QDataStream::BigEndian); + + PSDHeader header; + s >> header; + + if (s.status() == QDataStream::Ok && IsValid(header)) + v = QVariant::fromValue(QSize(header.width, header.height)); + } + } + + return v; +} + bool PSDHandler::canRead(QIODevice *device) { if (!device) { @@ -332,7 +892,7 @@ 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()) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kimageformats-5.93.0/src/imageformats/psd.json new/kimageformats-5.94.0/src/imageformats/psd.json --- old/kimageformats-5.93.0/src/imageformats/psd.json 2022-04-02 12:00:12.000000000 +0200 +++ new/kimageformats-5.94.0/src/imageformats/psd.json 2022-05-02 11:46:37.000000000 +0200 @@ -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" ] } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kimageformats-5.93.0/src/imageformats/psd_p.h new/kimageformats-5.94.0/src/imageformats/psd_p.h --- old/kimageformats-5.93.0/src/imageformats/psd_p.h 2022-04-02 12:00:12.000000000 +0200 +++ new/kimageformats-5.94.0/src/imageformats/psd_p.h 2022-05-02 11:46:37.000000000 +0200 @@ -18,6 +18,9 @@ bool canRead() const override; bool read(QImage *image) override; + bool supportsOption(QImageIOHandler::ImageOption option) const override; + QVariant option(QImageIOHandler::ImageOption option) const override; + static bool canRead(QIODevice *device); }; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kimageformats-5.93.0/src/imageformats/ras.cpp new/kimageformats-5.94.0/src/imageformats/ras.cpp --- old/kimageformats-5.93.0/src/imageformats/ras.cpp 2022-04-02 12:00:12.000000000 +0200 +++ new/kimageformats-5.94.0/src/imageformats/ras.cpp 2022-05-02 11:46:37.000000000 +0200 @@ -9,6 +9,8 @@ #include "ras_p.h" +#include "util_p.h" + #include <QDataStream> #include <QDebug> #include <QImage> @@ -102,8 +104,7 @@ { s.device()->seek(RasHeader::SIZE); - // QVector uses some extra space for stuff, hence the 32 here suggested by thiago - if (ras.ColorMapLength > std::numeric_limits<int>::max() - 32) { + if (ras.ColorMapLength > kMaxQVectorSize) { qWarning() << "LoadRAS() unsupported image color map length in file header" << ras.ColorMapLength; return false; } @@ -127,8 +128,7 @@ qWarning() << "LoadRAS() mistmatch between height and width" << ras.Width << ras.Height << ras.Length << ras.Depth; return false; } - // QVector uses some extra space for stuff, hence the 32 here suggested by thiago - if (ras.Length > std::numeric_limits<int>::max() - 32) { + if (ras.Length > kMaxQVectorSize) { qWarning() << "LoadRAS() unsupported image length in file header" << ras.Length; return false; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kimageformats-5.93.0/src/imageformats/util_p.h new/kimageformats-5.94.0/src/imageformats/util_p.h --- old/kimageformats-5.93.0/src/imageformats/util_p.h 1970-01-01 01:00:00.000000000 +0100 +++ new/kimageformats-5.94.0/src/imageformats/util_p.h 2022-05-02 11:46:37.000000000 +0200 @@ -0,0 +1,10 @@ +/* + SPDX-FileCopyrightText: 2022 Albert Astals Cid <[email protected]> + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include <limits> + +// QVector uses some extra space for stuff, hence the 32 here suggested by Thiago Macieira +static constexpr int kMaxQVectorSize = std::numeric_limits<int>::max() - 32; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kimageformats-5.93.0/src/imageformats/xcf.cpp new/kimageformats-5.94.0/src/imageformats/xcf.cpp --- old/kimageformats-5.93.0/src/imageformats/xcf.cpp 2022-04-02 12:00:12.000000000 +0200 +++ new/kimageformats-5.94.0/src/imageformats/xcf.cpp 2022-05-02 11:46:37.000000000 +0200 @@ -3270,6 +3270,52 @@ return false; } +bool XCFHandler::supportsOption(ImageOption option) const +{ + if (option == QImageIOHandler::Size) + return true; + return false; +} + +QVariant XCFHandler::option(ImageOption option) const +{ + QVariant v; + + if (option == QImageIOHandler::Size) { + /* + * The image structure always starts at offset 0 in the XCF file. + * byte[9] "gimp xcf " File type identification + * byte[4] version XCF version + * "file": version 0 + * "v001": version 1 + * "v002": version 2 + * "v003": version 3 + * byte 0 Zero marks the end of the version tag. + * uint32 width Width of canvas + * uint32 height Height of canvas + */ + if (auto d = device()) { + // transactions works on both random and sequential devices + d->startTransaction(); + auto ba9 = d->read(9); // "gimp xcf " + auto ba5 = d->read(4+1); // version + null terminator + auto ba = d->read(8); // width and height + d->rollbackTransaction(); + if (ba9 == QByteArray("gimp xcf ") && ba5.size() == 5) { + QDataStream ds(ba); + quint32 width; + ds >> width; + quint32 height; + ds >> height; + if (ds.status() == QDataStream::Ok) + v = QVariant::fromValue(QSize(width, height)); + } + } + } + + return v; +} + bool XCFHandler::canRead(QIODevice *device) { if (!device) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kimageformats-5.93.0/src/imageformats/xcf_p.h new/kimageformats-5.94.0/src/imageformats/xcf_p.h --- old/kimageformats-5.93.0/src/imageformats/xcf_p.h 2022-04-02 12:00:12.000000000 +0200 +++ new/kimageformats-5.94.0/src/imageformats/xcf_p.h 2022-05-02 11:46:37.000000000 +0200 @@ -20,6 +20,9 @@ bool read(QImage *image) override; bool write(const QImage &image) override; + bool supportsOption(QImageIOHandler::ImageOption option) const override; + QVariant option(QImageIOHandler::ImageOption option) const override; + static bool canRead(QIODevice *device); };
