Compare commits

...
19 Commits
Author SHA1 Message Date
Mirco MirandaandMirco Miranda 6a3c7f2c86 EXR: support for metadata saved by Photoshop 2026-09-10 07:36:10 +02:00
Albert Astals Cid 43e6efb0e5 ANIHandler::nextImageDelay: Protect against overflow
oss-fuzz:
 issue #558190560
 testcase #6629308500869120
2026-09-09 18:36:51 +00:00
Daniel Novomeský b7a407a752 autotests: add AVIF image-grid image to read test 2026-09-08 16:43:31 +02:00
Daniel Novomeský 62c45582df ossfuzz: update libaom to 3.15.0 2026-09-08 14:20:50 +02:00
Mirco MirandaandMirco Miranda a738553dc9 EXR: fix alpha channel imperpretation 2026-09-04 18:40:44 +02:00
Mirco MirandaandMirco Miranda 0862db49b8 EXR: RGB / YC subtype support 2026-09-04 18:40:44 +02:00
Nicolas Fella 05af59e819 Update version to 6.31.0 2026-09-04 14:36:24 +02:00
Nicolas Fella 5d6dfb9012 Update dependency version to 6.30.0 2026-09-04 13:26:17 +02:00
Albert Astals Cid 72518dcc1a jp2: Fix new[] vs delete by not doing new[]
unique_ptr will call delete which is not the right thing to do with
memory allocated with new[]

BUGS: 525120
2026-09-03 00:13:53 +02:00
Mirco MirandaandAlbert Astals Cid a6027264d4 qRoundOrZero_T: fix possible assert in qRound
also:
- qRoundOrZero_T: added documentation
- EXR: I don't set the ISO Speed ​​if it's zero.

BUG: 524678
2026-08-26 22:45:12 +00:00
Daniel Novomeský 47ea774621 autotests: Add non-standard HEIF image 2026-08-24 18:49:54 +02:00
Daniel Novomeský 5ed0affc95 heif: re-enable decoding of non-standard images
BUG: 495686

IMG_20241019_195232.heic stopped decoding recently because
buffer.contains("Xiaomi") is almost all the time false
(only start of the file is stored now),
except sequential devices (full file is stored).
2026-08-24 12:02:58 +02:00
Daniel Novomeský 42e9733060 Add HEIC test files with crop transformation 2026-08-20 22:21:48 +02:00
Daniel Novomeský bae45287ad heif: check crop values 2026-08-20 19:20:10 +02:00
Daniel Novomeský 458257b7a3 jxl: Do not rewind after reading final frame 2026-08-18 08:48:40 +00:00
Aljo Joby 09452b0964 rgb: reject RLE start offsets that underflow the raster data
The start table is stored as file offsets. We subtract the
header+table size to get an index into the remaining data.
If the file offset is smaller than that, the subtract wraps
and the later start+length check can wrap too.

Drop the file instead of walking off the buffer.
2026-08-14 21:03:38 +05:30
Daniel Novomeský c7baa057a6 avif: Do not rewind in jumpToNextImage() 2026-08-10 20:10:46 +02:00
Daniel Novomeský c57af25d36 ossfuzz: call functions used in animations 2026-08-10 17:46:33 +00:00
Nicolas Fella 320ded493a Update version to 6.30.0 2026-08-07 23:07:47 +02:00
63 changed files with 512 additions and 272 deletions
+3 -3
View File
@@ -1,11 +1,11 @@
cmake_minimum_required(VERSION 3.29) cmake_minimum_required(VERSION 3.29)
set(KF_VERSION "6.29.0") # handled by release scripts set(KF_VERSION "6.31.0") # handled by release scripts
set(KF_DEP_VERSION "6.29.0") # handled by release scripts set(KF_DEP_VERSION "6.30.0") # handled by release scripts
project(KImageFormats VERSION ${KF_VERSION}) project(KImageFormats VERSION ${KF_VERSION})
include(FeatureSummary) include(FeatureSummary)
find_package(ECM 6.29.0 NO_MODULE) find_package(ECM 6.30.0 NO_MODULE)
set_package_properties(ECM PROPERTIES TYPE REQUIRED DESCRIPTION "Extra CMake Modules." URL "https://commits.kde.org/extra-cmake-modules") set_package_properties(ECM PROPERTIES TYPE REQUIRED DESCRIPTION "Extra CMake Modules." URL "https://commits.kde.org/extra-cmake-modules")
feature_summary(WHAT REQUIRED_PACKAGES_NOT_FOUND FATAL_ON_MISSING_REQUIRED_PACKAGES) feature_summary(WHAT REQUIRED_PACKAGES_NOT_FOUND FATAL_ON_MISSING_REQUIRED_PACKAGES)
+16
View File
@@ -392,6 +392,22 @@ The plugin can set the following additional metadata:
- `EXRLayerName`: A string containing the name of the EXR layer used to decode - `EXRLayerName`: A string containing the name of the EXR layer used to decode
the image. 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 EPS plugin
The plugin uses `Ghostscript` to convert the raster image. When reading it 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 mkdir build.libavif
cd build.libavif cd build.libavif
extra_libaom_flags='-DAOM_MAX_ALLOCABLE_MEMORY=536870912 -DDO_RANGE_CHECK_CLAMP=1' 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 ninja install
# Build libavif # 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 /usr/libexec/moc $SRC/kimageformats/src/imageformats/$format.cpp -o $format.moc
header=`ls $SRC/kimageformats/src/imageformats/$format*.h` header=`ls $SRC/kimageformats/src/imageformats/$format*.h`
/usr/libexec/moc $header -o moc_`basename $header .h`.cpp /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. # -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.
+21 -1
View File
@@ -125,7 +125,27 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
b.open(QIODevice::ReadOnly); b.open(QIODevice::ReadOnly);
handler->setDevice(&b); handler->setDevice(&b);
handler->canRead(); handler->canRead();
handler->read(&i);
bool is_animated = false;
if (handler->supportsOption(QImageIOHandler::Animation)) {
is_animated = handler->option(QImageIOHandler::Animation).toBool();
}
if (is_animated) { // animated image
if (handler->supportsOption(QImageIOHandler::Size)) {
handler->option(QImageIOHandler::Size).toSize();
}
handler->imageCount();
handler->loopCount();
handler->read(&i);
handler->nextImageDelay();
handler->currentImageNumber();
handler->jumpToImage(0);
handler->jumpToNextImage();
} else { // static image
handler->read(&i);
}
delete handler; delete handler;
+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/qtbase.git
git clone --depth 1 --branch=dev git://code.qt.io/qt/qttools.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 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 -b v1.4.2 https://github.com/AOMediaCodec/libavif.git
git clone --depth 1 https://github.com/strukturag/libde265.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 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", "transferFunction" : "Linear",
"gamma" : 1 "gamma" : 1
}, },
"metadata" : [
{
"key" : "ModificationDate",
"value" : "2026-04-15T10:48:48"
},
{
"key" : "Software" ,
"value" : "Adobe Photoshop 27.5 (Windows)"
}
],
"resolution" : { "resolution" : {
"dotsPerMeterX" : 3937, "dotsPerMeterX" : 11811,
"dotsPerMeterY" : 3937 "dotsPerMeterY" : 11811
} }
} }
] ]
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 199 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 242 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 232 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 221 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 222 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 243 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 228 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 244 B

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

After

Width:  |  Height:  |  Size: 362 KiB

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) 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) if(TARGET OpenEXR::OpenEXR)
target_link_libraries(kimg_exr PRIVATE OpenEXR::OpenEXR) target_link_libraries(kimg_exr PRIVATE OpenEXR::OpenEXR)
else() 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 <QScopeGuard>
#include <QVariant> #include <QVariant>
#include <QtEndian> #include <QtEndian>
#include <QtNumeric>
#include <cstring> #include <cstring>
@@ -267,7 +268,17 @@ int ANIHandler::nextImageDelay() const
rate = m_displayRates.at(previousImage); 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 bool ANIHandler::supportsOption(ImageOption option) const
+5 -11
View File
@@ -1160,25 +1160,19 @@ bool QAVIFHandler::jumpToNextImage()
return false; return false;
} }
avifResult decodeResult;
if (m_decoder->imageIndex >= 0) { if (m_decoder->imageIndex >= 0) {
if (m_decoder->imageCount < 2) { if (m_decoder->imageCount < 2) {
m_parseState = ParseAvifSuccess; // single image, there is no next image
return false; return false;
} }
if (m_decoder->imageIndex >= m_decoder->imageCount - 1) { // start from beginning if (m_decoder->imageIndex >= m_decoder->imageCount - 1) {
decodeResult = avifDecoderReset(m_decoder); // do not start from beginning when at the end already
if (decodeResult != AVIF_RESULT_OK) { return false;
qCWarning(LOG_AVIFPLUGIN, "ERROR in avifDecoderReset: %s", avifResultToString(decodeResult));
m_parseState = ParseAvifError;
return false;
}
} }
} }
decodeResult = avifDecoderNextImage(m_decoder); avifResult decodeResult = avifDecoderNextImage(m_decoder);
if (decodeResult != AVIF_RESULT_OK) { if (decodeResult != AVIF_RESULT_OK) {
qCWarning(LOG_AVIFPLUGIN, "ERROR: Failed to decode Next image in sequence: %s", avifResultToString(decodeResult)); qCWarning(LOG_AVIFPLUGIN, "ERROR: Failed to decode Next image in sequence: %s", avifResultToString(decodeResult));
+68 -5
View File
@@ -8,6 +8,8 @@
*/ */
#include "exr_p.h" #include "exr_p.h"
#include "microexif_p.h"
#include "photoshop_p.h"
#include "scanlineconverter_p.h" #include "scanlineconverter_p.h"
#include "util_p.h" #include "util_p.h"
@@ -200,12 +202,17 @@ void K_OStream::seekg(Imf::Int64 pos)
m_dev->seek(pos); m_dev->seek(pos);
} }
#define EXR_SUBFORMAT_RGB QByteArray("RGB")
#define EXR_SUBFORMAT_YC QByteArray("YC")
EXRHandler::EXRHandler() EXRHandler::EXRHandler()
: m_compressionRatio(-1) : m_compressionRatio(-1)
, m_quality(-1) , m_quality(-1)
, m_imageNumber(0) , m_imageNumber(0)
, m_imageCount(0) , m_imageCount(0)
, m_startPos(-1) , m_startPos(-1)
, m_subType(EXR_SUBFORMAT_RGB)
{ {
// Set the number of threads to use (0 is allowed) // Set the number of threads to use (0 is allowed)
Imf::setGlobalThreadCount(QThread::idealThreadCount() / 2); Imf::setGlobalThreadCount(QThread::idealThreadCount() / 2);
@@ -225,7 +232,7 @@ bool EXRHandler::canRead() const
static QImage::Format imageFormat(const Imf::RgbaInputFile &file) static QImage::Format imageFormat(const Imf::RgbaInputFile &file)
{ {
auto isRgba = file.channels() & Imf::RgbaChannels::WRITE_A; 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);
} }
/*! /*!
@@ -368,7 +375,8 @@ static void readMetadata(const Imf::Header &header, QImage &image)
// shot metadata // shot metadata
if (auto isoSpeed = header.findTypedAttribute<Imf::FloatAttribute>("isoSpeed")) { if (auto isoSpeed = header.findTypedAttribute<Imf::FloatAttribute>("isoSpeed")) {
image.setText(QStringLiteral(META_KEY_ISOSPEEDRATINGS), QLocale::c().toString(qRound(isoSpeed->value()))); if (auto v = qRoundOrZero(isoSpeed->value()))
image.setText(QStringLiteral(META_KEY_ISOSPEEDRATINGS), QLocale::c().toString(v));
} }
if (auto expTime = header.findTypedAttribute<Imf::FloatAttribute>("expTime")) { if (auto expTime = header.findTypedAttribute<Imf::FloatAttribute>("expTime")) {
image.setText(QStringLiteral(META_KEY_EXPOSURETIME), QLocale::c().toString(expTime->value())); image.setText(QStringLiteral(META_KEY_EXPOSURETIME), QLocale::c().toString(expTime->value()));
@@ -379,6 +387,34 @@ static void readMetadata(const Imf::Header &header, QImage &image)
if (auto focalLen = header.findTypedAttribute<Imf::FloatAttribute>("effectiveFocalLength")) { if (auto focalLen = header.findTypedAttribute<Imf::FloatAttribute>("effectiveFocalLength")) {
image.setText(QStringLiteral(META_KEY_FOCALLENGTH), QLocale::c().toString(focalLen->value())); image.setText(QStringLiteral(META_KEY_FOCALLENGTH), QLocale::c().toString(focalLen->value()));
} }
// Photoshop image resource section
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) {
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);
}
}
}
} }
/*! /*!
@@ -513,9 +549,9 @@ bool makePreview(const QImage &image, Imf::Array2D<Imf::PreviewRgba> &pixels)
QImage preview; QImage preview;
if (w > h) { if (w > h) {
preview = image.scaledToWidth(256).convertToFormat(QImage::Format_ARGB32); preview = image.scaledToWidth(256).convertToFormat(QImage::Format_ARGB32_Premultiplied);
} else { } else {
preview = image.scaledToHeight(256).convertToFormat(QImage::Format_ARGB32); preview = image.scaledToHeight(256).convertToFormat(QImage::Format_ARGB32_Premultiplied);
} }
if (preview.isNull()) { if (preview.isNull()) {
return false; return false;
@@ -687,6 +723,10 @@ bool EXRHandler::write(const QImage &image)
// write the EXR // write the EXR
K_OStream ostr(device()); K_OStream ostr(device());
auto channelsType = image.hasAlphaChannel() ? Imf::RgbaChannels::WRITE_RGBA : Imf::RgbaChannels::WRITE_RGB; 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 || if (image.format() == QImage::Format_Mono ||
image.format() == QImage::Format_MonoLSB || image.format() == QImage::Format_MonoLSB ||
image.format() == QImage::Format_Grayscale16 || image.format() == QImage::Format_Grayscale16 ||
@@ -698,7 +738,7 @@ bool EXRHandler::write(const QImage &image)
pixels.resizeErase(EXR_LINES_PER_BLOCK, width); pixels.resizeErase(EXR_LINES_PER_BLOCK, width);
// convert the image and write into the stream // 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); ScanLineConverter slc(convFormat);
slc.setDefaultSourceColorSpace(QColorSpace(QColorSpace::SRgb)); slc.setDefaultSourceColorSpace(QColorSpace(QColorSpace::SRgb));
slc.setTargetColorSpace(QColorSpace(QColorSpace::SRgbLinear)); slc.setTargetColorSpace(QColorSpace(QColorSpace::SRgbLinear));
@@ -742,6 +782,15 @@ void EXRHandler::setOption(ImageOption option, const QVariant &value)
m_quality = q; 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;
}
}
} }
bool EXRHandler::supportsOption(ImageOption option) const bool EXRHandler::supportsOption(ImageOption option) const
@@ -760,6 +809,12 @@ bool EXRHandler::supportsOption(ImageOption option) const
if (option == QImageIOHandler::Quality) { if (option == QImageIOHandler::Quality) {
return true; return true;
} }
if (option == QImageIOHandler::SubType) {
return true;
}
if (option == QImageIOHandler::SupportedSubTypes) {
return true;
}
return false; return false;
} }
@@ -814,6 +869,14 @@ QVariant EXRHandler::option(ImageOption option) const
v = QVariant(m_quality); v = QVariant(m_quality);
} }
if (option == QImageIOHandler::SupportedSubTypes) {
return QVariant::fromValue(QList<QByteArray>() << EXR_SUBFORMAT_RGB << EXR_SUBFORMAT_YC);
}
if (option == QImageIOHandler::SubType) {
return QVariant::fromValue(m_subType);
}
return v; return v;
} }
+8
View File
@@ -118,6 +118,14 @@ private:
* The initial device position to allow multi image load (cache value). * The initial device position to allow multi image load (cache value).
*/ */
qint64 m_startPos; 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;
}; };
class EXRPlugin : public QImageIOPlugin class EXRPlugin : public QImageIOPlugin
+25 -4
View File
@@ -642,10 +642,31 @@ bool HEIFHandler::read_crop(void *heif_handle, const void *heif_ctx, const QSize
if (heif_item_get_properties_of_type(ctx, item_id, heif_item_property_type_transform_crop, &crop_id, 1) > 0) { if (heif_item_get_properties_of_type(ctx, item_id, heif_item_property_type_transform_crop, &crop_id, 1) > 0) {
int l = 0, t = 0, r = 0, b = 0; int l = 0, t = 0, r = 0, b = 0;
heif_item_get_property_transform_crop_borders(ctx, item_id, crop_id, size.width(), size.height(), &l, &t, &r, &b); heif_item_get_property_transform_crop_borders(ctx, item_id, crop_id, size.width(), size.height(), &l, &t, &r, &b);
crop = QRect(QPoint(t, l), size - QSize(b + t, r + l));
if (l >= size.width() || r >= size.width() || t >= size.height() || b >= size.height()) {
qCWarning(LOG_HEIFPLUGIN) << "Invalid crop values: left=" << l << "top=" << t << "right=" << r << "bottom=" << b;
}
if (l < 0) {
l = 0;
}
if (t < 0) {
t = 0;
}
if (r < 0) {
r = 0;
}
if (b < 0) {
b = 0;
}
if (l == 0 && t == 0 && r == 0 && b == 0) {
// no cropping needed
return false;
}
crop = QRect(QPoint(l, t), size - QSize(l + r, t + b));
return crop.isValid();
} }
return crop.isValid(); return false;
} }
bool HEIFHandler::isSupportedBMFFType(const QByteArray &header) bool HEIFHandler::isSupportedBMFFType(const QByteArray &header)
@@ -930,8 +951,8 @@ bool HEIFHandler::ensureDecoder()
struct heif_image *img = nullptr; struct heif_image *img = nullptr;
err = heif_decode_image(handle, &img, heif_colorspace_RGB, chroma, decoder_option); err = heif_decode_image(handle, &img, heif_colorspace_RGB, chroma, decoder_option);
if (err.code == heif_error_Invalid_input && err.subcode == heif_suberror_Unknown_NCLX_matrix_coefficients && img == nullptr && buffer.contains("Xiaomi")) { if (err.code == heif_error_Invalid_input && err.subcode == heif_suberror_Unknown_NCLX_matrix_coefficients && img == nullptr) {
qCWarning(LOG_HEIFPLUGIN) << "Non-standard HEIF image with invalid matrix_coefficients, probably made by a Xiaomi device!"; qCWarning(LOG_HEIFPLUGIN) << "Non-standard HEIF image with invalid matrix_coefficients!";
// second try to decode with strict decoding disabled // second try to decode with strict decoding disabled
decoder_option->strict_decoding = 0; decoder_option->strict_decoding = 0;
+12 -13
View File
@@ -622,21 +622,20 @@ public:
m_cparameters.tcp_rates[0] = 100.0 - (m_quality < 10 ? m_quality : 10 + (std::log10(m_quality) - 1) * 90); m_cparameters.tcp_rates[0] = 100.0 - (m_quality < 10 ? m_quality : 10 + (std::log10(m_quality) - 1) * 90);
} }
std::unique_ptr<opj_image_cmptparm_t> cmptparm(new opj_image_cmptparm_t[ncomp]); std::vector<opj_image_cmptparm_t> cmptparm(ncomp);
for (int i = 0; i < ncomp; ++i) { for (opj_image_cmptparm_t &p : cmptparm) {
auto &&p = cmptparm.get() + i; memset(&p, 0, sizeof(opj_image_cmptparm_t));
memset(p, 0, sizeof(opj_image_cmptparm_t)); p.dx = m_cparameters.subsampling_dx;
p->dx = m_cparameters.subsampling_dx; p.dy = m_cparameters.subsampling_dy;
p->dy = m_cparameters.subsampling_dy; p.w = image.width();
p->w = image.width(); p.h = image.height();
p->h = image.height(); p.x0 = 0;
p->x0 = 0; p.y0 = 0;
p->y0 = 0; p.prec = prec;
p->prec = prec; p.sgnd = 0;
p->sgnd = 0;
} }
m_jp2_image = opj_image_create(ncomp, cmptparm.get(), cs); m_jp2_image = opj_image_create(ncomp, cmptparm.data(), cs);
if (m_jp2_image == nullptr) { if (m_jp2_image == nullptr) {
return false; return false;
} }
+32 -33
View File
@@ -60,7 +60,7 @@ Q_LOGGING_CATEGORY(LOG_JXLPLUGIN, "kf.imageformats.plugins.jxl", QtWarningMsg)
void *QtJXLMemoryManagerAlloc(void *opaque, size_t size) void *QtJXLMemoryManagerAlloc(void *opaque, size_t size)
{ {
if (opaque) { if (opaque) {
size_t maxBytes = *(size_t*)opaque; size_t maxBytes = *(size_t *)opaque;
if (maxBytes && size > maxBytes) if (maxBytes && size > maxBytes)
return NULL; return NULL;
} }
@@ -75,8 +75,7 @@ void QtJXLMemoryManagerFree(void *, void *address)
QJpegXLHandler::QJpegXLHandler() QJpegXLHandler::QJpegXLHandler()
: m_parseState(ParseJpegXLNotParsed) : m_parseState(ParseJpegXLNotParsed)
, m_quality(90) , m_quality(90)
, m_currentimage_index(0) , m_currentimage_index(-1)
, m_previousimage_index(-1)
, m_transformations(QImageIOHandler::TransformationNone) , m_transformations(QImageIOHandler::TransformationNone)
, m_decoder(nullptr) , m_decoder(nullptr)
, m_runner(nullptr) , m_runner(nullptr)
@@ -183,11 +182,7 @@ bool QJpegXLHandler::ensureDecoder()
} }
// Creating a simple memory manager // Creating a simple memory manager
JxlMemoryManager memory_manager = { JxlMemoryManager memory_manager = {.opaque = &m_maxBytes, .alloc = QtJXLMemoryManagerAlloc, .free = QtJXLMemoryManagerFree};
.opaque = &m_maxBytes,
.alloc = QtJXLMemoryManagerAlloc,
.free = QtJXLMemoryManagerFree
};
// Creating the decoder (it makes a deep copy of memory manager) // Creating the decoder (it makes a deep copy of memory manager)
m_decoder = JxlDecoderCreate(&memory_manager); m_decoder = JxlDecoderCreate(&memory_manager);
if (!m_decoder) { if (!m_decoder) {
@@ -817,17 +812,11 @@ bool QJpegXLHandler::decode_one_frame()
exif.updateImageMetadata(m_current_image); exif.updateImageMetadata(m_current_image);
} }
m_currentimage_index++;
m_next_image_delay = m_framedelays[m_currentimage_index]; m_next_image_delay = m_framedelays[m_currentimage_index];
m_previousimage_index = m_currentimage_index;
if (m_framedelays.count() > 1) { if (m_framedelays.count() > 1) {
m_currentimage_index++; if (m_currentimage_index >= m_framedelays.count() - 1) {
if (m_currentimage_index >= m_framedelays.count()) {
if (!rewind()) {
return false;
}
// all frames in animation have been read // all frames in animation have been read
m_parseState = ParseJpegXLFinished; m_parseState = ParseJpegXLFinished;
} else { } else {
@@ -847,9 +836,9 @@ bool QJpegXLHandler::read(QImage *image)
return false; return false;
} }
if (m_currentimage_index == m_previousimage_index) { if (m_parseState == ParseJpegXLFinished) {
*image = m_current_image; // at the end already
return jumpToNextImage(); return false;
} }
if (decode_one_frame()) { if (decode_one_frame()) {
@@ -1757,19 +1746,26 @@ bool QJpegXLHandler::jumpToNextImage()
} }
if (m_framedelays.count() > 1) { if (m_framedelays.count() > 1) {
m_currentimage_index++; if (m_currentimage_index >= (m_framedelays.count() - 1)) {
// we are already at the last frame
if (m_currentimage_index >= m_framedelays.count()) { return false;
if (!rewind()) {
return false;
}
} else { } else {
m_currentimage_index++;
m_next_image_delay = m_framedelays[m_currentimage_index];
JxlDecoderSkipFrames(m_decoder, 1); JxlDecoderSkipFrames(m_decoder, 1);
if (m_currentimage_index >= (m_framedelays.count() - 1)) {
// last frame reached but no more reading is possible
m_parseState = ParseJpegXLFinished;
} else {
m_parseState = ParseJpegXLSuccess;
}
return true;
} }
} }
m_parseState = ParseJpegXLSuccess; // static image, there is no next frame
return true; return false;
} }
bool QJpegXLHandler::jumpToImage(int imageNumber) bool QJpegXLHandler::jumpToImage(int imageNumber)
@@ -1782,14 +1778,17 @@ bool QJpegXLHandler::jumpToImage(int imageNumber)
return false; return false;
} }
if (imageNumber == m_currentimage_index) { const int next_image_index = m_currentimage_index + 1;
if (imageNumber == next_image_index) {
// This the already the next image to read, no action
m_parseState = ParseJpegXLSuccess; m_parseState = ParseJpegXLSuccess;
return true; return true;
} }
if (imageNumber > m_currentimage_index) { if (imageNumber > next_image_index) {
JxlDecoderSkipFrames(m_decoder, imageNumber - m_currentimage_index); JxlDecoderSkipFrames(m_decoder, imageNumber - next_image_index);
m_currentimage_index = imageNumber; m_currentimage_index = imageNumber - 1;
m_parseState = ParseJpegXLSuccess; m_parseState = ParseJpegXLSuccess;
return true; return true;
} }
@@ -1801,7 +1800,7 @@ bool QJpegXLHandler::jumpToImage(int imageNumber)
if (imageNumber > 0) { if (imageNumber > 0) {
JxlDecoderSkipFrames(m_decoder, imageNumber); JxlDecoderSkipFrames(m_decoder, imageNumber);
} }
m_currentimage_index = imageNumber; m_currentimage_index = imageNumber - 1;
m_parseState = ParseJpegXLSuccess; m_parseState = ParseJpegXLSuccess;
return true; return true;
} }
@@ -1834,7 +1833,7 @@ int QJpegXLHandler::loopCount() const
bool QJpegXLHandler::rewind() bool QJpegXLHandler::rewind()
{ {
m_currentimage_index = 0; m_currentimage_index = -1;
JxlDecoderReleaseInput(m_decoder); JxlDecoderReleaseInput(m_decoder);
JxlDecoderRewind(m_decoder); JxlDecoderRewind(m_decoder);
-1
View File
@@ -65,7 +65,6 @@ private:
ParseJpegXLState m_parseState; ParseJpegXLState m_parseState;
int m_quality; int m_quality;
int m_currentimage_index; int m_currentimage_index;
int m_previousimage_index;
QImageIOHandler::Transformations m_transformations; QImageIOHandler::Transformations m_transformations;
QByteArray m_rawData; QByteArray m_rawData;
+140
View File
@@ -0,0 +1,140 @@
/*
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;
}
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;
}
+80
View File
@@ -0,0 +1,80 @@
/*
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 <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;
};
using PSDImageResourceSection = QHash<ImageResourceId, PSDImageResourceBlock>;
/*!
* \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
+1 -181
View File
@@ -29,12 +29,12 @@
#include "fastmath_p.h" #include "fastmath_p.h"
#include "microexif_p.h" #include "microexif_p.h"
#include "packbits_p.h" #include "packbits_p.h"
#include "photoshop_p.h"
#include "psd_p.h" #include "psd_p.h"
#include "scanlineconverter_p.h" #include "scanlineconverter_p.h"
#include "util_p.h" #include "util_p.h"
#include <QColorSpace> #include <QColorSpace>
#include <QDataStream>
#include <QImage> #include <QImage>
#include <QLoggingCategory> #include <QLoggingCategory>
@@ -99,41 +99,6 @@ namespace // Private.
#define NATIVE_CMYK (CMYK_FORMAT != QImage::Format_Invalid) #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 { struct PSDHeader {
PSDHeader() { PSDHeader() {
memset(this, 0, sizeof(PSDHeader)); memset(this, 0, sizeof(PSDHeader));
@@ -149,11 +114,6 @@ struct PSDHeader {
ushort color_mode; ushort color_mode;
}; };
struct PSDImageResourceBlock {
QString name;
QByteArray data;
};
/*! /*!
* \brief The PSDDuotoneOptions struct * \brief The PSDDuotoneOptions struct
* \note You can decode the duotone data using the "Duotone Options" * \note You can decode the duotone data using the "Duotone Options"
@@ -172,7 +132,6 @@ struct PSDColorModeDataSection {
QList<QRgb> palette; QList<QRgb> palette;
}; };
using PSDImageResourceSection = QHash<quint16, PSDImageResourceBlock>;
struct PSDLayerInfo { struct PSDLayerInfo {
qint64 size = -1; qint64 size = -1;
@@ -273,145 +232,6 @@ static bool skip_section(QDataStream &s, bool psb = false)
return skip_data(s, section_length); 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 readAdditionalLayer(QDataStream &s, bool *ok = nullptr)
{ {
PSDAdditionalLayerInfo li; PSDAdditionalLayerInfo li;
+9 -3
View File
@@ -314,6 +314,8 @@ bool SGIImagePrivate::readImage(QImage &img)
_numrows = _ysize * _zsize; _numrows = _ysize * _zsize;
if (_rle) { if (_rle) {
// start table holds file offsets; raster data follows header + both tables
const quint32 dataOff = 512 + _numrows * 2 * sizeof(quint32);
uint l; uint l;
_starttab = new (std::nothrow) quint32[_numrows]; _starttab = new (std::nothrow) quint32[_numrows];
if (_starttab == nullptr) { if (_starttab == nullptr) {
@@ -321,7 +323,10 @@ bool SGIImagePrivate::readImage(QImage &img)
} }
for (l = 0; !_stream.atEnd() && l < _numrows; l++) { for (l = 0; !_stream.atEnd() && l < _numrows; l++) {
_stream >> _starttab[l]; _stream >> _starttab[l];
_starttab[l] -= 512 + _numrows * 2 * sizeof(quint32); if (_starttab[l] < dataOff) {
return false;
}
_starttab[l] -= dataOff;
if (_stream.status() != QDataStream::Ok) { if (_stream.status() != QDataStream::Ok) {
return false; return false;
} }
@@ -353,9 +358,10 @@ bool SGIImagePrivate::readImage(QImage &img)
// sanity check // sanity check
if (_rle) { if (_rle) {
const uint dataSize = uint(_data.size());
for (uint o = 0; o < _numrows; o++) { for (uint o = 0; o < _numrows; o++) {
// don't change to greater-or-equal! // don't add start+length: uint32 wrap would pass a corrupt file
if (_starttab[o] + _lengthtab[o] > (uint)_data.size()) { if (_starttab[o] > dataSize || _lengthtab[o] > dataSize - _starttab[o]) {
// qCDebug(LOG_RGBPLUGIN) << "image corrupt (sanity check failed)"; // qCDebug(LOG_RGBPLUGIN) << "image corrupt (sanity check failed)";
return false; return false;
} }
+16 -9
View File
@@ -165,20 +165,27 @@ inline bool checkImageSize(const QSize& size, qint32 bytesPerPixel)
return checkImageSize(size.width(), size.height(), bytesPerPixel); return checkImageSize(size.width(), size.height(), bytesPerPixel);
} }
/*!
* \brief qRoundOrZero_T
* In images, many float values can only be positive (e.g., resolution). This function calculates
* the roundness of the passed value, returning 0 if the value is negative or invalid.
* \return 0 when \a d is negative, NaN, Inf or std::numeric_limits<TI>::max(). Otherwise the qRound of \a d.
*/
template<class TI, class SF> // SF = source FP, TI = target INT template<class TI, class SF> // SF = source FP, TI = target INT
TI qRoundOrZero_T(SF d, bool *ok = nullptr) TI qRoundOrZero_T(SF d, bool *ok = nullptr)
{ {
bool tmp = false;
if (ok == nullptr) {
ok = &tmp;
}
// checks for undefined behavior // checks for undefined behavior
if (qIsNaN(d) || qIsInf(d) || d < SF() || d > SF(std::numeric_limits<TI>::max())) { if (qIsNaN(d) || qIsInf(d) || d < SF()) {
if (ok) { *ok = false;
*ok = false; } else {
} *ok = d < SF(std::numeric_limits<TI>::max());
return 0;
} }
if (ok) { return *ok ? qRound(d) : 0;
*ok = true;
}
return qRound(d);
} }
inline qint32 qRoundOrZero(double d, bool *ok = nullptr) inline qint32 qRoundOrZero(double d, bool *ok = nullptr)