Compare commits

..
8 Commits
35 changed files with 689 additions and 206 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.29)
set(KF_VERSION "6.30.0") # handled by release scripts
set(KF_VERSION "6.31.0") # handled by release scripts
set(KF_DEP_VERSION "6.30.0") # handled by release scripts
project(KImageFormats VERSION ${KF_VERSION})
+22 -3
View File
@@ -384,14 +384,33 @@ The following defines can be defined in cmake to modify the behavior of the
plugin:
- `EXR_CONVERT_TO_SRGB`: the linear data is converted to sRGB on read to
accommodate programs that do not support color profiles.
- `EXR_DISABLE_XMP_ATTRIBUTE`: disables the stores XMP values in a non-standard
attribute named "xmp". Note that Gimp reads the "xmp" attribute and Darktable
writes it as well.
- `EXR_DISABLE_XMP_ATTRIBUTE`: disables the storage of XMP values in a
non-standard attribute named "xmp". Note that Gimp reads the "xmp" attribute
and Darktable writes it as well.
- `EXR_DISABLE_ADOBE_ATTRIBUTE`: disables the storage of non-standard
attribute "adobe_rsrc". Note that Photoshop uses "adobe_rsrc" attribute to
read and write XMP/EXIF/IPTC data.
The plugin can set the following additional metadata:
- `EXRLayerName`: A string containing the name of the EXR layer used to decode
the image.
When writing, it is possible to specify compression ratio and quality. The
values you can set depend on the version of the OpenEXR library you're using.
Please check the [exr_p.h](./src/imageformats/exr_p.h) for more
info.
When writing, it is also possible to specify a subtype:
- `RGB` (default): preserves full-resolution color data (R, G, B) without
subsampling or loss of chromatic detail.
- `YC`: converts RGB data into luminance (Y) and 2x2 subsampled chroma
(RY, BY), cutting raw color data by ~50% for significantly smaller file
sizes. **If the image does not have a height and width that are multiples
of 2, `RGB` is used.**
> [!note]
> When writing grayscale images, the subtype makes no difference.
### The EPS plugin
The plugin uses `Ghostscript` to convert the raster image. When reading it
+2 -2
View File
@@ -101,7 +101,7 @@ cd $SRC/aom
mkdir build.libavif
cd build.libavif
extra_libaom_flags='-DAOM_MAX_ALLOCABLE_MEMORY=536870912 -DDO_RANGE_CHECK_CLAMP=1'
cmake -G Ninja -DBUILD_SHARED_LIBS=0 -DENABLE_DOCS=0 -DENABLE_EXAMPLES=0 -DENABLE_TESTDATA=0 -DENABLE_TESTS=0 -DENABLE_TOOLS=0 -DCONFIG_PIC=1 -DAOM_TARGET_CPU=generic -DCONFIG_SIZE_LIMIT=1 -DDECODE_HEIGHT_LIMIT=12288 -DDECODE_WIDTH_LIMIT=12288 -DAOM_EXTRA_C_FLAGS="${extra_libaom_flags}" -DAOM_EXTRA_CXX_FLAGS="${extra_libaom_flags}" ..
cmake -G Ninja -DBUILD_SHARED_LIBS=0 -DENABLE_APPS=0 -DENABLE_DOCS=0 -DENABLE_EXAMPLES=0 -DENABLE_TESTDATA=0 -DENABLE_TESTS=0 -DENABLE_TOOLS=0 -DCONFIG_PIC=1 -DAOM_TARGET_CPU=generic -DCONFIG_SIZE_LIMIT=1 -DDECODE_HEIGHT_LIMIT=12288 -DDECODE_WIDTH_LIMIT=12288 -DAOM_EXTRA_C_FLAGS="${extra_libaom_flags}" -DAOM_EXTRA_CXX_FLAGS="${extra_libaom_flags}" ..
ninja install
# Build libavif
@@ -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.
+1 -1
View File
@@ -33,7 +33,7 @@ git clone --depth 1 -b master https://invent.kde.org/frameworks/extra-cmake-modu
git clone --depth 1 --branch=dev git://code.qt.io/qt/qtbase.git
git clone --depth 1 --branch=dev git://code.qt.io/qt/qttools.git
git clone --depth 1 -b master https://invent.kde.org/frameworks/karchive.git
git clone --depth 1 -b v3.14.1 https://aomedia.googlesource.com/aom
git clone --depth 1 -b v3.15.0 https://aomedia.googlesource.com/aom
git clone --depth 1 -b v1.4.2 https://github.com/AOMediaCodec/libavif.git
git clone --depth 1 https://github.com/strukturag/libde265.git
git clone --depth 1 -b v2.5.4 https://github.com/uclouvain/openjpeg.git
Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

+7
View File
@@ -0,0 +1,7 @@
[
{
"fileName": "grid.png",
"fuzziness": 2,
"description": "Minimum fuzziness value to pass the test on all architectures."
}
]
Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.
@@ -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
}
}
]
Binary file not shown.
@@ -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
}
}
]
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2 -2
View File
@@ -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)
##################################
+12 -1
View File
@@ -12,6 +12,7 @@
#include <QScopeGuard>
#include <QVariant>
#include <QtEndian>
#include <QtNumeric>
#include <cstring>
@@ -267,7 +268,17 @@ int ANIHandler::nextImageDelay() const
rate = m_displayRates.at(previousImage);
}
return rate * 1000 / 60;
int delayNumerator;
if (qMulOverflow(rate, 1000, &delayNumerator)) {
int delay;
// We're losing a bit of precision by dividing first and multiplying later
// but this is most probably a broken file so not much of an issue
if (qMulOverflow(rate / 60, 1000, &delay)) {
return 0;
}
return delay;
}
return delayNumerator / 60;
}
bool ANIHandler::supportsOption(ImageOption option) const
+170 -11
View File
@@ -8,6 +8,8 @@
*/
#include "exr_p.h"
#include "microexif_p.h"
#include "photoshop_p.h"
#include "scanlineconverter_p.h"
#include "util_p.h"
@@ -19,8 +21,8 @@
*/
//#define EXR_CONVERT_TO_SRGB // default: commented -> you should define it in your cmake file
/* *** EXR_STORE_XMP_ATTRIBUTE ***
* If defined, disables the stores XMP values in a non-standard attribute named "xmp".
/* *** EXR_DISABLE_XMP_ATTRIBUTE ***
* If defined, disables the storage of XMP values in the non-standard attribute named "xmp"
* The QImage metadata used is "XML:com.adobe.xmp".
* NOTE: The use of non-standard attributes is possible but discouraged by the specification. However,
* metadata is essential for good image management and programs like darktable also set this
@@ -28,6 +30,15 @@
*/
//#define EXR_DISABLE_XMP_ATTRIBUTE // default: commented -> you should define it in your cmake file
/* *** EXR_DISABLE_ADOBE_ATTRIBUTE ***
* If defined, disables the storage of non-standard attribute "adobe_rsrc".
* NOTE: The use of non-standard attributes is possible but discouraged by the specification. However,
* metadata is essential for good image management and programs like Photoshop also set this
* attribute. Photoshop uses "adobe_rsrc" attribute to read/write XMP/EXIF/IPTC data.
*/
//#define EXR_DISABLE_ADOBE_ATTRIBUTE // default: commented -> you should define it in your cmake file
/* *** EXR_MAX_IMAGE_WIDTH and EXR_MAX_IMAGE_HEIGHT ***
* The maximum size in pixel allowed by the plugin.
*/
@@ -90,6 +101,12 @@ Q_LOGGING_CATEGORY(LOG_EXRPLUGIN, "kf.imageformats.plugins.exr", QtDebugMsg)
Q_LOGGING_CATEGORY(LOG_EXRPLUGIN, "kf.imageformats.plugins.exr", QtWarningMsg)
#endif
#ifndef EXR_DISABLE_ADOBE_ATTRIBUTE
#if QT_VERSION_CHECK(OPENEXR_VERSION_MAJOR, OPENEXR_VERSION_MINOR, OPENEXR_VERSION_PATCH) < QT_VERSION_CHECK(3, 3, 0)
#define EXR_DISABLE_ADOBE_ATTRIBUTE
#endif
#endif
class K_IStream : public Imf::IStream
{
public:
@@ -200,12 +217,18 @@ void K_OStream::seekg(Imf::Int64 pos)
m_dev->seek(pos);
}
#define EXR_SUBFORMAT_RGB QByteArray("RGB")
#define EXR_SUBFORMAT_YC QByteArray("YC")
EXRHandler::EXRHandler()
: m_compressionRatio(-1)
, m_quality(-1)
, m_imageNumber(0)
, m_imageCount(0)
, m_startPos(-1)
, m_subType(EXR_SUBFORMAT_RGB)
, m_transformation(QImageIOHandler::TransformationNone)
{
// Set the number of threads to use (0 is allowed)
Imf::setGlobalThreadCount(QThread::idealThreadCount() / 2);
@@ -225,7 +248,7 @@ bool EXRHandler::canRead() const
static QImage::Format imageFormat(const Imf::RgbaInputFile &file)
{
auto isRgba = file.channels() & Imf::RgbaChannels::WRITE_A;
return (isRgba ? QImage::Format_RGBA16FPx4 : QImage::Format_RGBX16FPx4);
return (isRgba ? QImage::Format_RGBA16FPx4_Premultiplied : QImage::Format_RGBX16FPx4);
}
/*!
@@ -285,6 +308,27 @@ static void printAttributes(const Imf::Header &h)
}
#endif
static PSDImageResourceSection readPSDImageResourceSection(const Imf::Header &header)
{
if (auto adobe = header.findTypedAttribute<Imf::OpaqueAttribute>("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) {
return irs;
}
}
return{};
}
/*!
* \brief readMetadata
* Reads EXR attributes from the \a header and set its as metadata in the \a image.
@@ -380,6 +424,20 @@ static void readMetadata(const Imf::Header &header, QImage &image)
if (auto focalLen = header.findTypedAttribute<Imf::FloatAttribute>("effectiveFocalLength")) {
image.setText(QStringLiteral(META_KEY_FOCALLENGTH), QLocale::c().toString(focalLen->value()));
}
// Photoshop image resource section
auto irs = readPSDImageResourceSection(header);
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);
}
}
/*!
@@ -514,9 +572,9 @@ bool makePreview(const QImage &image, Imf::Array2D<Imf::PreviewRgba> &pixels)
QImage preview;
if (w > h) {
preview = image.scaledToWidth(256).convertToFormat(QImage::Format_ARGB32);
preview = image.scaledToWidth(256).convertToFormat(QImage::Format_ARGB32_Premultiplied);
} else {
preview = image.scaledToHeight(256).convertToFormat(QImage::Format_ARGB32);
preview = image.scaledToHeight(256).convertToFormat(QImage::Format_ARGB32_Premultiplied);
}
if (preview.isNull()) {
return false;
@@ -545,8 +603,11 @@ bool makePreview(const QImage &image, Imf::Array2D<Imf::PreviewRgba> &pixels)
* \brief setMetadata
* Reades the metadata from \a image and set its as attributes in the \a header.
*/
static void setMetadata(const QImage &image, Imf::Header &header)
static void setMetadata(const QImage &image, Imf::Header &header, const QImageIOHandler::Transformation &transformation)
{
PSDImageResourceSection irs;
QString xmpData;
auto dateTime = QDateTime::currentDateTime();
for (auto &&key : image.textKeys()) {
auto text = image.text(key);
@@ -577,11 +638,9 @@ static void setMetadata(const QImage &image, Imf::Header &header)
}
}
#ifndef EXR_DISABLE_XMP_ATTRIBUTE // warning: Non-standard attribute!
if (!key.compare(QStringLiteral(META_KEY_XMP_ADOBE), Qt::CaseInsensitive)) {
header.insert("xmp", Imf::StringAttribute(text.toStdString()));
xmpData = text;
}
#endif
if (!key.compare(QStringLiteral(META_KEY_MANUFACTURER), Qt::CaseInsensitive)) {
header.insert("cameraMake", Imf::StringAttribute(text.toStdString()));
@@ -641,6 +700,41 @@ static void setMetadata(const QImage &image, Imf::Header &header)
header.insert("pixelAspectRatio", Imf::FloatAttribute(float(image.dotsPerMeterY()) / float(image.dotsPerMeterX())));
}
#ifndef EXR_DISABLE_XMP_ATTRIBUTE
if (!xmpData.isEmpty()) {
header.insert("xmp", Imf::StringAttribute(xmpData.toStdString()));
}
#endif
#ifndef EXR_DISABLE_ADOBE_ATTRIBUTE
auto resInfo = PSDResolutionInfoBlock::fromImage(image);
if (resInfo.isValid()) {
PSDImageResourceBlock irb;
irb.data = resInfo.toByteArray();
if (!irb.data.isEmpty())
irs.insert(IRI_RESOLUTIONINFO, irb);
}
auto exif = MicroExif::fromImage(image);
if (!exif.isEmpty()) {
exif.setTransformation(transformation);
PSDImageResourceBlock irb;
irb.data = exif.toByteArray(QDataStream::BigEndian);
irs.insert(IRI_EXIFDATA1, irb);
}
if (!xmpData.isEmpty()) {
PSDImageResourceBlock irb;
irb.data = xmpData.toUtf8();
irs.insert(IRI_XMPMETADATA, irb);
}
if (!irs.isEmpty()) {
bool ok = false;
auto ba = irs.toByteArray(&ok);
if (ok) {
header.insert("adobe_rsrc", Imf::OpaqueAttribute("adobe_rsrc", ba.size(), ba.data()));
}
}
#endif
// set default chroma (default constructor ITU-R BT.709-3 -> sRGB)
// The image is converted to Linear sRGB so, the chroma is the default EXR value.
// If a file doesnt have a chromaticities attribute, display software should assume that the
@@ -683,11 +777,15 @@ bool EXRHandler::write(const QImage &image)
}
// set metadata (EXR attributes)
setMetadata(image, header);
setMetadata(image, header, m_transformation);
// write the EXR
K_OStream ostr(device());
auto channelsType = image.hasAlphaChannel() ? Imf::RgbaChannels::WRITE_RGBA : Imf::RgbaChannels::WRITE_RGB;
if (m_subType == EXR_SUBFORMAT_YC && !(width % 2) && !(height % 2)) {
// Works only with images with height and width that are multiples of 2.
channelsType = channelsType == Imf::RgbaChannels::WRITE_RGBA ? Imf::RgbaChannels::WRITE_YCA : Imf::RgbaChannels::WRITE_YC;
}
if (image.format() == QImage::Format_Mono ||
image.format() == QImage::Format_MonoLSB ||
image.format() == QImage::Format_Grayscale16 ||
@@ -699,7 +797,7 @@ bool EXRHandler::write(const QImage &image)
pixels.resizeErase(EXR_LINES_PER_BLOCK, width);
// convert the image and write into the stream
auto convFormat = image.hasAlphaChannel() ? QImage::Format_RGBA32FPx4 : QImage::Format_RGBX32FPx4;
auto convFormat = image.hasAlphaChannel() ? QImage::Format_RGBA32FPx4_Premultiplied : QImage::Format_RGBX32FPx4;
ScanLineConverter slc(convFormat);
slc.setDefaultSourceColorSpace(QColorSpace(QColorSpace::SRgb));
slc.setTargetColorSpace(QColorSpace(QColorSpace::SRgbLinear));
@@ -743,6 +841,24 @@ void EXRHandler::setOption(ImageOption option, const QVariant &value)
m_quality = q;
}
}
if (option == QImageIOHandler::SubType) {
auto subType = value.toByteArray();
auto list = EXRHandler::option(QImageIOHandler::SupportedSubTypes).value<QList<QByteArray>>();
if (list.contains(subType)) {
m_subType = subType;
} else {
m_subType = EXR_SUBFORMAT_RGB;
}
}
#ifndef EXR_DISABLE_ADOBE_ATTRIBUTE
if (option == QImageIOHandler::ImageTransformation) {
auto ok = false;
auto t = value.toInt(&ok);
if (ok) {
m_transformation = QImageIOHandler::Transformation(t);
}
}
#endif
}
bool EXRHandler::supportsOption(ImageOption option) const
@@ -761,6 +877,17 @@ bool EXRHandler::supportsOption(ImageOption option) const
if (option == QImageIOHandler::Quality) {
return true;
}
if (option == QImageIOHandler::SubType) {
return true;
}
if (option == QImageIOHandler::SupportedSubTypes) {
return true;
}
#ifndef EXR_DISABLE_ADOBE_ATTRIBUTE
if (option == QImageIOHandler::ImageTransformation) {
return true;
}
#endif
return false;
}
@@ -815,6 +942,38 @@ QVariant EXRHandler::option(ImageOption option) const
v = QVariant(m_quality);
}
if (option == QImageIOHandler::SupportedSubTypes) {
v = QVariant::fromValue(QList<QByteArray>() << EXR_SUBFORMAT_RGB << EXR_SUBFORMAT_YC);
}
if (option == QImageIOHandler::SubType) {
v = QVariant::fromValue(m_subType);
}
#ifndef EXR_DISABLE_ADOBE_ATTRIBUTE
if (option == QImageIOHandler::ImageTransformation) {
if (auto d = device()) {
// transactions works on both random and sequential devices
d->startTransaction();
if (m_startPos > -1) {
d->seek(m_startPos);
}
try {
K_IStream istr(d);
Imf::RgbaInputFile file(istr);
auto irs = readPSDImageResourceSection(file.header());
if (irs.contains(IRI_EXIFDATA1)) {
auto exif = MicroExif::fromByteArray(irs.value(IRI_EXIFDATA1).data);
v = int(exif.transformation());
}
} catch (const std::exception &) {
// broken file or unsupported version
}
d->rollbackTransaction();
}
}
#endif
return v;
}
+14
View File
@@ -118,6 +118,20 @@ private:
* The initial device position to allow multi image load (cache value).
*/
qint64 m_startPos;
/*!
* \brief m_subType
* The EXR subtype to use when writing:
* - RGB: preserves full-resolution color data (R, G, B) without subsampling or loss of chromatic detail (default).
* - YC: converts RGB data into luminance (Y) and 2x2 subsampled chroma (RY, BY), cutting raw color data by ~50% for significantly smaller file sizes.
*/
QByteArray m_subType;
/*!
* \brief m_transformation
* The Qt image transformation.
*/
QImageIOHandler::Transformation m_transformation;
};
class EXRPlugin : public QImageIOPlugin
+4 -2
View File
@@ -84,6 +84,8 @@
#define EXIF_TAG_SIZEOF(dataType) (quint16(dataType) & 0x3F)
#define EXIF_TAG_DATATYPE(dataType) (quint16(dataType) >> 6)
#define GPS_GPSVERSION_VALUE QList<quint8>{0x02, 0x04, 0x00, 0x00}
enum class ExifTagType : quint16 {
// Base data types
Byte = EXIF_TAG_VALUE(1, 1),
@@ -1325,7 +1327,7 @@ QByteArray MicroExif::gpsIfdByteArray(const QDataStream::ByteOrder &byteOrder, c
QDataStream ds(&ba, QIODevice::WriteOnly);
ds.setByteOrder(byteOrder);
auto gpsTags = m_gpsTags;
gpsTags.insert(GPS_GPSVERSION, QByteArray("2400"));
gpsTags.insert(GPS_GPSVERSION, QVariant::fromValue(GPS_GPSVERSION_VALUE));
TagPos positions;
if (!writeIfd(ds, version, gpsTags, positions, 0, staticGpsTagTypes))
return {};
@@ -1723,7 +1725,7 @@ void MicroExif::updateTags(Tags &tiffTags, Tags &exifTags, Tags &gpsTags, const
tiffTags.remove(EXIF_GPSIFD);
} else {
tiffTags.insert(EXIF_GPSIFD, quint32());
gpsTags.insert(GPS_GPSVERSION, QByteArray("2400"));
gpsTags.insert(GPS_GPSVERSION, QVariant::fromValue(GPS_GPSVERSION_VALUE));
}
}
+265
View File
@@ -0,0 +1,265 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2026 Mirco Miranda <mircomir@outlook.com>
SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "photoshop_p.h"
#include "util_p.h"
#include <QLoggingCategory>
#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;
}
bool writePascalString(const QString &str, QDataStream &s, qint32 alignBytes)
{
auto data = str.toLatin1();
if(data.size() > 250) {
data = data.left(250);
}
auto sz = data.size();
s << quint8(sz);
if (sz && s.writeRawData(data.data(), sz) != sz) {
return false;
}
for(alignBytes = std::max(1, alignBytes), sz += 1; sz % alignBytes; ++sz) {
s << char();
}
return s.status() == QDataStream::Ok;
}
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;
}
bool writeImageResourceSection(const PSDImageResourceSection &irs, QDataStream &s)
{
bool ok = false;
auto ba = irs.toByteArray(&ok);
if (!ok) {
return false;
}
s << quint32(ba.size());
if (s.writeRawData(ba.data(), ba.size()) != ba.size()) {
return false;
}
return (s.status() == QDataStream::Ok);
}
QByteArray PSDImageResourceSection::toByteArray(bool *ok) const
{
QByteArray ba;
bool tmp = true;
if (ok == nullptr)
ok = &tmp;
*ok = true;
if (!isEmpty()) {
QDataStream s(&ba, QDataStream::WriteOnly);
s.setByteOrder(QDataStream::BigEndian);
auto ids = keys();
for(auto &&id : ids) {
auto irb = value(id);
if (irb.data.isEmpty()) {
continue;
}
// signature
s << quint32(S_8BIM);
// resource id
s << quint16(id);
// resource name (2 bytes aligned)
if (!writePascalString(irb.name, s, 2)) {
*ok = false;
return{};
}
// data (2 bytes aligned)
auto sz = irb.data.size();
s << quint32(sz);
if (s.writeRawData(irb.data.data(), sz) != sz) {
*ok = false;
return{};
}
if (sz % 2) {
s << char();
}
if (s.status() != QDataStream::Ok) {
*ok = false;
return{};
}
}
}
Q_ASSERT(ba.size() % 2 == 0);
return ba;
}
PSDResolutionInfoBlock::PSDResolutionInfoBlock(qint32 ppmX, qint32 ppmY)
: m_ppmX(ppmX)
, m_ppmY(ppmY)
{
}
bool PSDResolutionInfoBlock::isValid() const
{
return m_ppmX > 0 && m_ppmY > 0;
}
PSDResolutionInfoBlock PSDResolutionInfoBlock::fromImage(const QImage &image)
{
return PSDResolutionInfoBlock(image.dotsPerMeterX(), image.dotsPerMeterY());
}
QByteArray PSDResolutionInfoBlock::toByteArray() const
{
QByteArray ba;
QDataStream ds(&ba, QIODevice::WriteOnly);
ds.setByteOrder(QDataStream::BigEndian);
auto hres = qRoundOrZero(dppm2dpi(m_ppmX) * 65536);
ds << hres;
ds << quint16(1); // dpi
ds << quint16(2); // cm (display)
auto vres = qRoundOrZero(dppm2dpi(m_ppmY) * 65536);
ds << vres;
ds << quint16(1);
ds << quint16(2);
if (hres == 0 || vres == 0) {
return{};
}
return ba;
}
+143
View File
@@ -0,0 +1,143 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2026 Mirco Miranda <mircomir@outlook.com>
SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef PHOTOSHOP_P_H
#define PHOTOSHOP_P_H
#include <QDataStream>
#include <QHash>
#include <QImage>
#include <QString>
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;
};
class PSDImageResourceSection : public QHash<ImageResourceId, PSDImageResourceBlock>
{
public:
PSDImageResourceSection() : QHash<ImageResourceId, PSDImageResourceBlock>() {}
PSDImageResourceSection(const PSDImageResourceSection& other) = default;
PSDImageResourceSection& operator =(const PSDImageResourceSection& other) = default;
/*!
* \brief toByteArray
* \param ok Pointer to the operation result variable.
* \return The binary IRB to be written into the PSD file.
*/
QByteArray toByteArray(bool *ok = nullptr) const;
};
class PSDResolutionInfoBlock
{
public:
PSDResolutionInfoBlock(qint32 ppmX, qint32 ppmY);
PSDResolutionInfoBlock(const PSDResolutionInfoBlock& other) = default;
PSDResolutionInfoBlock& operator =(const PSDResolutionInfoBlock& other) = default;
/*!
* \brief isValid
* \return true if both m_ppmX and m_ppmY are grater than 0. Otherwise false.
*/
bool isValid() const;
/*!
* \brief fromImage
* Initialize the class using the image resolution.
*/
static PSDResolutionInfoBlock fromImage(const QImage& image);
/*!
* \brief toByteArray
* \return The binary data ready for the IMage Resource Section.
*/
QByteArray toByteArray() const;
private:
qint32 m_ppmX;
qint32 m_ppmY;
};
/*!
* \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 writePascalString
* Writes the Pascal string as defined in the PSD specification.
* \param str The string to be written.
* \param s The stream.
* \param alignBytes Alignment of the string.
* \return True on success, otherwise false.
*/
bool writePascalString(const QString& str, QDataStream &s, qint32 alignBytes = 1);
/*!
* \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);
/*!
* \brief writeImageResourceSection
* Writes the image resource section.
* \param irs The image resource section raw data.
* \param s The stream.
* \return True on success, otherwise false.
*/
bool writeImageResourceSection(const PSDImageResourceSection& irs, QDataStream &s);
#endif // PHOTOSHOP_P_H
+1 -181
View File
@@ -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 <QColorSpace>
#include <QDataStream>
#include <QImage>
#include <QLoggingCategory>
@@ -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<QRgb> palette;
};
using PSDImageResourceSection = QHash<quint16, PSDImageResourceBlock>;
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;