Compare commits

..

24 Commits

Author SHA1 Message Date
Daniel Novomeský
081975807c JXR: remove INITGUID define
The define caused linking problems on Windows
2026-06-19 16:55:27 +02:00
Daniel Novomeský
b58b865ba4 ossfuzz: update libaom and libavif 2026-06-19 16:37:27 +02:00
Mirco Miranda
8768a8cf97 HEIF: use heif_reader for random access devices 2026-06-12 08:18:19 +02:00
Akseli Lahtinen
7edf807082 avif: If we only have single image, return false at jumpToNextImage
We were errorneously returning true here, as we do not have any more
images to jump to. If we only have one image, return false.

This avoids the avif handler getting stuck in a loop with only single images.

BUG: 521200
FIXED-IN: 6.28
2026-06-10 15:05:06 +03:00
Mirco Miranda
52045ff84d Added limit to maximum number of channels 2026-06-10 04:33:37 +02:00
Laurent Montel
8bfdef2e48 GIT_SILENT: Bump kf ecm_set_disabled_deprecation_versions. Make sure that it compiles fine without kf 6.27 deprecated methods 2026-06-09 06:45:33 +02:00
Mirco Miranda
ec640db10e Improve buffer memory management 2026-06-06 01:28:31 +02:00
Nicolas Fella
86b0fe60c5 Update version to 6.28.0 2026-06-05 18:02:51 +02:00
Nicolas Fella
2791c2543b Update dependency version to 6.27.0 2026-06-05 17:33:32 +02:00
Mirco Miranda
a2a8c955df Updated documentation on memory usage 2026-06-04 07:17:54 +02:00
Mirco Miranda
f450e5c9a9 JXR: add a memory allocation barrier in jxrlib 2026-05-27 10:37:09 +02:00
Mirco Miranda
9dfcf67ea9 EXR: reject files with dimensions exceeding 300kx300k pixels 2026-05-25 14:13:15 +02:00
Mirco Miranda
6017099044 OSS Fuzz: set image allocation limit to 2000 MiB 2026-05-23 05:59:01 +02:00
Mirco Miranda
9ddad16767 Improve size limits for AVIF, HEIF, and RAW plugins 2026-05-15 12:13:12 +02:00
Mirco Miranda
0e2b137b32 IFF: fix byte swapping in 16-bit DEEP images 2026-05-12 10:31:43 +02:00
Mirco Miranda
6d5e61f0b0 Add Farbfeld read only support 2026-05-11 07:38:28 +02:00
Mirco Miranda
d7c3174fb6 DDS: fix mime type 2026-05-08 04:58:39 +02:00
Mirco Miranda
49a8fd38c8 IFF: DEEP image support 2026-05-08 01:29:54 +02:00
Mirco Miranda
1206598337 imageAlloc: add image initialization support 2026-05-07 08:41:10 +02:00
Mirco Miranda
3488077d8d Fix uninitialized value 2026-05-04 08:51:34 +02:00
Laurent Montel
ea8a4dccdc GIT_SILENT: Bump kf ecm_set_disabled_deprecation_versions. Make sure that it compiles fine without kf 6.26 deprecated methods 2026-05-03 08:41:08 +02:00
Mirco Miranda
8048279473 HEIF: image transformation support 2026-05-03 00:00:11 +02:00
Nicolas Fella
18d0f93d60 Update CI image 2026-05-02 15:30:10 +02:00
Nicolas Fella
497e612ad3 Update version to 6.27.0 2026-05-01 22:24:21 +02:00
74 changed files with 1842 additions and 190 deletions

View File

@@ -16,7 +16,7 @@ include:
image_json_validate:
stage: validate
image: invent-registry.kde.org/sysadmin/ci-images/suse-qt69:latest
image: invent-registry.kde.org/sysadmin/ci-images/suse-qt611:latest
tags:
- Linux
script:

View File

@@ -1,11 +1,11 @@
cmake_minimum_required(VERSION 3.29)
set(KF_VERSION "6.26.0") # handled by release scripts
set(KF_DEP_VERSION "6.26.0") # handled by release scripts
set(KF_VERSION "6.28.0") # handled by release scripts
set(KF_DEP_VERSION "6.27.0") # handled by release scripts
project(KImageFormats VERSION ${KF_VERSION})
include(FeatureSummary)
find_package(ECM 6.26.0 NO_MODULE)
find_package(ECM 6.27.0 NO_MODULE)
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)
@@ -72,7 +72,7 @@ set_property(CACHE KIMAGEFORMATS_HEJ2_TEST PROPERTY STRINGS "OFF" "READ_ONLY" "A
set(KIMAGEFORMATS_AVCI_TEST "ALL" CACHE STRING "Enable AVCI tests: OFF, ALL")
set_property(CACHE KIMAGEFORMATS_AVCI_TEST PROPERTY STRINGS "OFF" "ALL")
if(KIMAGEFORMATS_HEIF)
pkg_check_modules(LibHeif IMPORTED_TARGET libheif>=1.10.0)
pkg_check_modules(LibHeif IMPORTED_TARGET libheif>=1.17.0)
endif()
add_feature_info(LibHeif LibHeif_FOUND "required for the QImage plugin for HEIF/HEIC images")
@@ -107,7 +107,7 @@ add_feature_info(LibJXR LibJXR_FOUND "required for the QImage plugin for JPEG XR
ecm_set_disabled_deprecation_versions(
QT 6.11.0
KF 6.23.0
KF 6.27.0
)
add_subdirectory(src)

View File

@@ -16,6 +16,7 @@ The following image formats have read-only support:
- Animated Windows cursors (ani)
- Camera RAW images (arw, cr2, cr3, dcs, dng, ...)
- Farbfeld (ff)
- Gimp (xcf)
- Interchange Format Files (iff, ilbm, lbm)
- Krita (kra)
@@ -243,21 +244,23 @@ RGB.
Where possible, plugins support large images. By convention, many of the
large image plugins are limited to a maximum of 300,000 x 300,000 pixels.
Anyway, all plugins are also limited by the
`QImageIOReader::allocationLimit()`.
`QImageReader::allocationLimit()`.
> [!note]
> You can change the maximum limit of 300000 pixels by setting the constant
> `KIF_LARGE_IMAGE_PIXEL_LIMIT` to the desired value in the cmake file.
> `KIF_LARGE_IMAGE_PIXEL_LIMIT` to the desired value in the cmake file. It
> cannot be less than 65536.
Below are the maximum sizes for each plugin ('n/a' means no limit, i.e. the
limit depends on the format encoding).
- ANI: n/a
- ANI: same size as Qt's ICO plugin
- AVIF: 32,768 x 32,768 pixels, in any case no larger than 256 megapixels
- DDS: 300,000 x 300,000 pixels
- EXR: 300,000 x 300,000 pixels
- EPS: same size as Qt's JPG plugin
- FF: 300,000 x 300,000 pixels
- HDR: 300,000 x 300,000 pixels
- HEIF: n/a
- HEIF: 65,535 x 65,535 pixels
- IFF: 65,535 x 65,535 pixels
- JP2: 300,000 x 300,000 pixels, in any case no larger than 2 gigapixels
- JXL: 262,144 x 262,144 pixels, in any case no larger than 256 megapixels
@@ -297,8 +300,18 @@ consumption proportional to the size of the image to be saved.
Normally this is not a source of problems because the affected plugins
are limited to maximum images of 2GiB or less.
Note that the value of `QImageReader::allocationLimit()` is only used when
allocating a new `QImage`. Since this parameter was created to limit damage
caused by corrupted files, any conversion of `QImage` (for example, with
`QImage::convertTo()`) is not subject to this limit.
On plugins for formats that support large images, progressive conversion has
been used or the maximum size of the image that can be saved has been limited.
Plugins that use external libraries don't always allow progressive decoding
(e.g., the JPEG series). In these cases, the memory required for reading
may be much larger than the entire decoded image. When the external library has
a maximum memory limit function, the value of `QImageReader::allocationLimit()`
is set.
### Non-RGB formats
@@ -333,6 +346,15 @@ distributions. In particular, it is necessary that the HEIF library has
support for HEVC codec. If HEVC codec is not available the plugin
will compile but will fail the tests.
The following defines can be defined in cmake to modify the behavior of the
plugin:
- `HEIF_DISABLE_QT_TRANSFORMATION`: HEIF transformations, in addition to
rotations and reflections, also support image cropping. Consequently, the
Qt plugin, must also honor the crop. This define is useful in case
of problems: activating it disables Qt's support for transformations,
delegating them to the HEIF libraries (which will therefore always apply
them regardless of what is requested from Qt).
**If you are interested in compiling the plugin without running the tests,
also use the following string options:**
- `KIMAGEFORMATS_HEIF_TEST` to change the behaviour of HEIF tests. Set to
@@ -394,6 +416,7 @@ The plugin supports the following image data:
- FORM IMAG (Compact Disc-Interactive): It supports CLut4, CLut7, CLut8, Rle7
and DYuv formats.
- FORM RGFX: It supports uncompressed images only.
- FORM DEEP: It supports uncompressed, RLE and TVDC images.
- FOR4 CIMG (Maya Image File Format): It supports 24/48-bit RGB and 32/64-bit
RGBA images.
@@ -436,8 +459,13 @@ plugin:
> [!caution]
> The plugin disabled by default due to security issues in [jxrlib](https://github.com/4creators/jxrlib):
> the upstream jxrlib is dead and there is no "hope" they will fix the issues.
>
> **You should not enable it unless you know what you are doing.**
> [!note]
> Security issues in the jxrlib discovered by the [KImageFormats OSS-Fuzz project](https://github.com/google/oss-fuzz/tree/master/projects/kimageformats)
> should be fixed in this [jxrlib fork](https://github.com/mircomir/jxrlib).
The following defines can be defined in cmake to modify the behavior of the
plugin:
- `JXR_DENY_FLOAT_IMAGE`: disables the use of float images and consequently
@@ -450,9 +478,6 @@ plugin:
it only wants (P)BGRA32bpp files (a format not supported by Qt). Only for
this format an hack is activated to guarantee total compatibility of the
plugin with Windows.
- `JXR_ENABLE_ADVANCED_METADATA`: enable metadata support (e.g. XMP). Some
distributions use an incomplete JXR library that does not allow reading
metadata, causing compilation errors.
### The KRA plugin

View File

@@ -76,6 +76,7 @@ endmacro()
# Loads each <format> image in read/<format>/, and compares the
# result against the data read from the corresponding png file
kimageformats_read_tests(
ff
hdr
iff
pcx

View File

@@ -162,6 +162,7 @@ HANDLER_TYPES="ANIHandler ani
QAVIFHandler avif
QDDSHandler dds
EXRHandler exr
FFHandler ff
HDRHandler hdr
HEIFHandler heif
IFFHandler iff

View File

@@ -23,17 +23,19 @@
Usage:
python infra/helper.py build_image kimageformats
python infra/helper.py build_fuzzers --sanitizer undefined|address|memory kimageformats
python infra/helper.py run_fuzzer kimageformats kimgio_[ani|avif|dds|exr|hdr|heif|iff|jp2|jxl|jxr|kra|ora|pcx|pfm|pic|psd|pxr|qoi|ras|raw|rgb|sct|tim|tga|xcf]_fuzzer
python infra/helper.py run_fuzzer kimageformats kimgio_[ani|avif|dds|exr|ff|hdr|heif|iff|jp2|jxl|jxr|kra|ora|pcx|pfm|pic|psd|pxr|qoi|ras|raw|rgb|sct|tim|tga|xcf]_fuzzer
*/
#include <QBuffer>
#include <QCoreApplication>
#include <QImage>
#include <QImageReader>
#include "ani_p.h"
#include "avif_p.h"
#include "dds_p.h"
#include "exr_p.h"
#include "ff_p.h"
#include "hdr_p.h"
#include "heif_p.h"
#include "iff_p.h"
@@ -61,6 +63,8 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
int argc = 0;
QCoreApplication a(argc, nullptr);
QImageReader::setAllocationLimit(512);
QImageIOHandler* handler = new HANDLER();
QImage i;

View File

@@ -33,8 +33,8 @@ 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.13.1 https://aomedia.googlesource.com/aom
git clone --depth 1 -b v1.3.0 https://github.com/AOMediaCodec/libavif.git
git clone --depth 1 -b v3.14.1 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
git clone --depth 1 https://github.com/strukturag/libheif.git

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

View File

@@ -0,0 +1,17 @@
[
{
"fileName" : "orientation_all.png",
"colorSpace" : {
"description" : "GIMP built-in sRGB",
"primaries" : "SRgb",
"transferFunction" : "SRgb",
"gamma" : 0
},
"metadata" : [
{
"key" : "Software" ,
"value" : "LIFE Pro 2.20.35 (Linux)"
}
]
}
]

Binary file not shown.

View File

@@ -0,0 +1,11 @@
[
{
"fileName" : "orientation_all.png",
"metadata" : [
{
"key" : "Software" ,
"value" : "LIFE Pro 2.20.35 (Linux)"
}
]
}
]

Binary file not shown.

View File

@@ -0,0 +1,11 @@
[
{
"fileName" : "orientation_all.png",
"metadata" : [
{
"key" : "Software" ,
"value" : "LIFE Pro 2.20.35 (Linux)"
}
]
}
]

Binary file not shown.

View File

@@ -0,0 +1,11 @@
[
{
"fileName" : "orientation_all.png",
"metadata" : [
{
"key" : "Software" ,
"value" : "LIFE Pro 2.20.35 (Linux)"
}
]
}
]

Binary file not shown.

View File

@@ -0,0 +1,11 @@
[
{
"fileName" : "orientation_all.png",
"metadata" : [
{
"key" : "Software" ,
"value" : "LIFE Pro 2.20.35 (Linux)"
}
]
}
]

Binary file not shown.

View File

@@ -0,0 +1,11 @@
[
{
"fileName" : "orientation_all.png",
"metadata" : [
{
"key" : "Software" ,
"value" : "LIFE Pro 2.20.35 (Linux)"
}
]
}
]

Binary file not shown.

View File

@@ -0,0 +1,11 @@
[
{
"fileName" : "orientation_all.png",
"metadata" : [
{
"key" : "Software" ,
"value" : "LIFE Pro 2.20.35 (Linux)"
}
]
}
]

Binary file not shown.

View File

@@ -0,0 +1,11 @@
[
{
"fileName" : "orientation_all.png",
"metadata" : [
{
"key" : "Software" ,
"value" : "LIFE Pro 2.20.35 (Linux)"
}
]
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -73,6 +73,10 @@ endif()
##################################
kimageformats_add_plugin(kimg_ff SOURCES ff.cpp)
##################################
kimageformats_add_plugin(kimg_hdr SOURCES hdr.cpp)
##################################
@@ -162,7 +166,9 @@ if (LibJXR_FOUND)
kde_enable_exceptions()
target_include_directories(kimg_jxr PRIVATE ${LIBJXR_INCLUDE_DIRS})
target_link_libraries(kimg_jxr PRIVATE ${LIBJXR_LIBRARIES})
target_compile_definitions(kimg_jxr PRIVATE INITGUID)
if(NOT MSVC)
target_compile_definitions(kimg_jxr PRIVATE __ANSI__)
endif()
if (NOT CMAKE_SYSTEM_NAME STREQUAL "Windows")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-error=undef")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-error=undef")

View File

@@ -49,6 +49,16 @@ Quality range - compression/subsampling
#define KIMG_AVIF_QUALITY_LOW 51
#endif
/* *** AVIF_MAX_IMAGE_WIDTH and AVIF_MAX_IMAGE_HEIGHT ***
* The maximum size in pixel allowed by the plugin.
*/
#ifndef AVIF_MAX_IMAGE_WIDTH
#define AVIF_MAX_IMAGE_WIDTH KIF_64K_IMAGE_PIXEL_LIMIT
#endif
#ifndef AVIF_MAX_IMAGE_HEIGHT
#define AVIF_MAX_IMAGE_HEIGHT AVIF_MAX_IMAGE_WIDTH
#endif
QAVIFHandler::QAVIFHandler()
: m_parseState(ParseAvifNotParsed)
, m_quality(KIMG_AVIF_DEFAULT_QUALITY)
@@ -147,7 +157,7 @@ bool QAVIFHandler::ensureDecoder()
return true;
}
m_rawData = device()->readAll();
m_rawData = deviceRead(device(), kMaxQVectorSize);
m_rawAvifData.data = reinterpret_cast<const uint8_t *>(m_rawData.constData());
m_rawAvifData.size = m_rawData.size();
@@ -168,7 +178,7 @@ bool QAVIFHandler::ensureDecoder()
#endif
#if AVIF_VERSION >= 110000
m_decoder->imageDimensionLimit = 65535;
m_decoder->imageDimensionLimit = std::max(AVIF_MAX_IMAGE_WIDTH, AVIF_MAX_IMAGE_HEIGHT) - 1;
#endif
avifResult decodeResult;
@@ -196,7 +206,7 @@ bool QAVIFHandler::ensureDecoder()
m_container_width = m_decoder->image->width;
m_container_height = m_decoder->image->height;
if ((m_container_width > 65535) || (m_container_height > 65535)) {
if ((m_container_width >= AVIF_MAX_IMAGE_WIDTH) || (m_container_height >= AVIF_MAX_IMAGE_HEIGHT)) {
qCWarning(LOG_AVIFPLUGIN, "AVIF image (%dx%d) is too large!", m_container_width, m_container_height);
m_parseState = ParseAvifError;
return false;
@@ -605,7 +615,7 @@ bool QAVIFHandler::write(const QImage &image)
}
if ((image.width() > 0) && (image.height() > 0)) {
if ((image.width() > 65535) || (image.height() > 65535)) {
if ((image.width() >= AVIF_MAX_IMAGE_WIDTH) || (image.height() >= AVIF_MAX_IMAGE_HEIGHT)) {
qCWarning(LOG_AVIFPLUGIN, "Image (%dx%d) is too large to save!", image.width(), image.height());
return false;
}
@@ -1171,7 +1181,7 @@ bool QAVIFHandler::jumpToNextImage()
if (m_decoder->imageIndex >= 0) {
if (m_decoder->imageCount < 2) {
m_parseState = ParseAvifSuccess;
return true;
return false;
}
if (m_decoder->imageIndex >= m_decoder->imageCount - 1) { // start from beginning

View File

@@ -295,6 +295,14 @@ IFFChunk::ChunkList IFFChunk::innerFromDevice(QIODevice *d, bool *ok, IFFChunk *
chunk = QSharedPointer<IFFChunk>(new CTBLChunk());
} else if (cid == DATE_CHUNK) {
chunk = QSharedPointer<IFFChunk>(new DATEChunk());
} else if (cid == DBOD_CHUNK) {
chunk = QSharedPointer<IFFChunk>(new DBODChunk());
} else if (cid == DGBL_CHUNK) {
chunk = QSharedPointer<IFFChunk>(new DGBLChunk());
} else if (cid == DLOC_CHUNK) {
chunk = QSharedPointer<IFFChunk>(new DLOCChunk());
} else if (cid == DPEL_CHUNK) {
chunk = QSharedPointer<IFFChunk>(new DPELChunk());
} else if (cid == DPI__CHUNK) {
chunk = QSharedPointer<IFFChunk>(new DPIChunk());
} else if (cid == EXIF_CHUNK) {
@@ -341,6 +349,8 @@ IFFChunk::ChunkList IFFChunk::innerFromDevice(QIODevice *d, bool *ok, IFFChunk *
chunk = QSharedPointer<IFFChunk>(new SHAMChunk());
} else if (cid == TBHD_CHUNK) {
chunk = QSharedPointer<IFFChunk>(new TBHDChunk());
} else if (cid == TVDC_CHUNK) {
chunk = QSharedPointer<IFFChunk>(new TVDCChunk());
} else if (cid == VDAT_CHUNK) {
chunk = QSharedPointer<IFFChunk>(new VDATChunk());
} else if (cid == VERS_CHUNK) {
@@ -1468,6 +1478,8 @@ bool FORMChunk::innerReadStructure(QIODevice *d)
setChunks(IFFChunk::innerFromDevice(d, &ok, this));
} else if (_type == RGFX_FORM_TYPE) {
setChunks(IFFChunk::innerFromDevice(d, &ok, this));
} else if (_type == DEEP_FORM_TYPE || _type == TVPP_FORM_TYPE) {
setChunks(IFFChunk::innerFromDevice(d, &ok, this));
}
return ok;
}
@@ -1585,6 +1597,72 @@ QImage::Format FORMChunk::rgfxFormat() const
return QImage::Format_Invalid;
}
QImage::Format FORMChunk::deepFormat() const
{
auto pels = IFFChunk::searchT<DPELChunk>(chunks());
if (pels.isEmpty()) {
return QImage::Format_Invalid;
}
auto list = pels.first()->elements();
// support for same depth on all elements
auto depth = -1;
for (auto &&el : list) {
if (depth < 0)
depth = el.depth;
if (depth != el.depth)
return QImage::Format_Invalid;
}
// calculate the image format
if (list.size() == 4) {
if (list.at(0).type == DPELChunk::Red &&
list.at(1).type == DPELChunk::Green &&
list.at(2).type == DPELChunk::Blue &&
list.at(3).type == DPELChunk::Alpha) {
if (depth == 8)
return FORMAT_RGBA_8BIT;
else if (depth == 16)
return QImage::Format_RGBA64;
} else if (list.at(0).type == DPELChunk::Cyan &&
list.at(1).type == DPELChunk::Magenta &&
list.at(2).type == DPELChunk::Yellow &&
list.at(3).type == DPELChunk::Black) {
if (depth == 8)
return QImage::Format_CMYK8888;
} else if (list.at(0).type == DPELChunk::Red &&
list.at(1).type == DPELChunk::Green &&
list.at(2).type == DPELChunk::Blue) {
// unknown type of channel 4 -> ignoring it
if (depth == 8)
return QImage::Format_RGBX8888;
else if (depth == 16)
return QImage::Format_RGBX64;
}
} else if (list.size() == 3) {
if (list.at(0).type == DPELChunk::Red &&
list.at(1).type == DPELChunk::Green &&
list.at(2).type == DPELChunk::Blue) {
if (depth == 8)
return FORMAT_RGB_8BIT;
} else if (list.at(0).type == DPELChunk::Blue &&
list.at(1).type == DPELChunk::Green &&
list.at(2).type == DPELChunk::Red) {
if (depth == 8)
return QImage::Format_BGR888;
}
} else if (list.size() == 1) {
if (depth == 1)
return QImage::Format_Mono;
else if (depth == 8)
return QImage::Format_Grayscale8;
else if (depth == 16)
return QImage::Format_Grayscale16;
}
return QImage::Format_Invalid;
}
QByteArray FORMChunk::formType() const
{
return _type;
@@ -1596,6 +1674,8 @@ QImage::Format FORMChunk::format() const
return cdiFormat();
} else if (formType() == RGFX_FORM_TYPE) {
return rgfxFormat();
} else if (formType() == DEEP_FORM_TYPE || formType() == TVPP_FORM_TYPE) {
return deepFormat();
}
return iffFormat();
}
@@ -1612,6 +1692,15 @@ QSize FORMChunk::size() const
if (!rghds.isEmpty()) {
return rghds.first()->size();
}
} else if (formType() == DEEP_FORM_TYPE || formType() == TVPP_FORM_TYPE) {
auto dlocs = IFFChunk::searchT<DLOCChunk>(chunks());
if (!dlocs.isEmpty()) {
return dlocs.first()->size();
}
auto dgbls = IFFChunk::searchT<DGBLChunk>(chunks());
if (!dgbls.isEmpty()) {
return dgbls.first()->size();
}
} else {
auto bmhds = IFFChunk::searchT<BMHDChunk>(chunks());
if (!bmhds.isEmpty()) {
@@ -1735,6 +1824,8 @@ bool CATChunk::innerReadStructure(QIODevice *d)
setChunks(IFFChunk::innerFromDevice(d, &ok, this));
} else if (_type == RGFX_FORM_TYPE) {
setChunks(IFFChunk::innerFromDevice(d, &ok, this));
} else if (_type == DEEP_FORM_TYPE || _type == TVPP_FORM_TYPE) {
setChunks(IFFChunk::innerFromDevice(d, &ok, this));
}
return ok;
}
@@ -3574,6 +3665,436 @@ quint32 RBODChunk::strideSize(const RGHDChunk *header) const
}
/* ******************
* *** DGBL Chunk ***
* ****************** */
DGBLChunk::~DGBLChunk()
{
}
DGBLChunk::DGBLChunk()
: IFFChunk()
{
}
DGBLChunk::Compression DGBLChunk::compression() const
{
if (!isValid()) {
return Compression::Uncompressed;
}
return Compression(ui16(data(), 4));
}
qint32 DGBLChunk::width() const
{
if (!isValid()) {
return 0;
}
return qint32(ui16(data(), 0));
}
qint32 DGBLChunk::height() const
{
if (!isValid()) {
return 0;
}
return qint32(ui16(data(), 2));
}
quint8 DGBLChunk::xAspectRatio() const
{
if (!isValid()) {
return 0;
}
return quint8(data().at(6));
}
quint8 DGBLChunk::yAspectRatio() const
{
if (!isValid()) {
return 0;
}
return quint8(data().at(7));
}
bool DGBLChunk::isValid() const
{
if (dataBytes() < 8) {
return false;
}
return chunkId() == DGBLChunk::defaultChunkId();
}
bool DGBLChunk::innerReadStructure(QIODevice *d)
{
return cacheData(d);
}
/* ******************
* *** DPEL Chunk ***
* ****************** */
DPELChunk::~DPELChunk()
{
}
DPELChunk::DPELChunk()
: IFFChunk()
{
}
qint32 DPELChunk::count() const
{
if (dataBytes() < 4) {
return 0;
}
auto cnt = i32(data(), 0);
if (cnt < 0 || cnt > KIF_MAX_IMAGE_CHANNELS) {
cnt = 0;
}
return cnt;
}
qint32 DPELChunk::depth() const
{
auto depth = 0;
auto list = elements();
for (auto &&el : list) {
depth += el.depth;
}
return depth;
}
QList<DPELChunk::Element> DPELChunk::elements() const
{
QList<DPELChunk::Element> list;
if (isValid()) {
for (auto n = count(), i = 0; i < n; ++i) {
auto idx = 4 + i * 4;
list << DPELChunk::Element(DataType(ui16(data(), idx)),
ui16(data(), idx + 2));
}
}
return list;
}
bool DPELChunk::isValid() const
{
if (dataBytes() < quint32(4 + count() * 4)) {
return false;
}
return chunkId() == DPELChunk::defaultChunkId();
}
bool DPELChunk::innerReadStructure(QIODevice *d)
{
return cacheData(d);
}
/* ******************
* *** DLOC Chunk ***
* ****************** */
DLOCChunk::~DLOCChunk()
{
}
DLOCChunk::DLOCChunk()
: IFFChunk()
{
}
qint32 DLOCChunk::width() const
{
if (!isValid()) {
return 0;
}
return qint32(ui16(data(), 0));
}
qint32 DLOCChunk::height() const
{
if (!isValid()) {
return 0;
}
return qint32(ui16(data(), 2));
}
QSize DLOCChunk::size() const
{
return QSize(width(), height());
}
qint32 DLOCChunk::xOffset() const
{
if (!isValid()) {
return 0;
}
return qint32(i16(data(), 4));
}
qint32 DLOCChunk::yOffset() const
{
if (!isValid()) {
return 0;
}
return qint32(i16(data(), 6));
}
bool DLOCChunk::isValid() const
{
if (dataBytes() < 8) {
return false;
}
return chunkId() == DLOCChunk::defaultChunkId();
}
bool DLOCChunk::innerReadStructure(QIODevice *d)
{
return cacheData(d);
}
/* ******************
* *** TVDC Chunk ***
* ****************** */
TVDCChunk::~TVDCChunk()
{
}
TVDCChunk::TVDCChunk()
: IFFChunk()
{
}
qint32 TVDCChunk::count() const
{
if (!isValid()) {
return 0;
}
return dataBytes() / 2;
}
QList<quint16> TVDCChunk::table() const
{
QList<quint16> list;
if (isValid()) {
for (auto n = count(), i = 0; i < n; ++i) {
list << ui16(data(), i * 2);
}
}
return list;
}
bool TVDCChunk::isValid() const
{
if (dataBytes() < 32) {
return false;
}
return chunkId() == TVDCChunk::defaultChunkId();
}
bool TVDCChunk::innerReadStructure(QIODevice *d)
{
return cacheData(d);
}
/* ******************
* *** DBOD Chunk ***
* ****************** */
DBODChunk::~DBODChunk()
{
}
DBODChunk::DBODChunk()
: IFFChunk()
{
}
bool DBODChunk::isValid() const
{
return chunkId() == DBODChunk::defaultChunkId();
}
/*!
* \brief rleDeepDecompress
* Each run contains a pixel (all components)
*/
inline qint64 rleDeepDecompress(QIODevice *input, char *output, qint64 olen, qint32 pixelBytes)
{
qint64 j = 0;
pixelBytes = std::max(1, pixelBytes);
for (qint64 rr = 0, available = olen; j < olen; available = olen - j) {
char n;
// check the output buffer space for the next run
if (available < 129 * pixelBytes) {
if (input->peek(&n, 1) != 1) { // end of data (or error)
break;
}
if ((static_cast<signed char>(n) >= 0 ? qint64(n) + 1 : qint64(1 - n)) * pixelBytes > available)
break;
}
// decompress
if (input->read(&n, 1) != 1) { // end of data (or error)
break;
}
if (static_cast<signed char>(n) >= 0) {
rr = input->read(output + j, (qint64(n) + 1) * pixelBytes);
if (rr == -1) {
return -1;
}
}
else {
auto buf = input->read(pixelBytes);
if (buf.size() != pixelBytes) {
break;
}
rr = qint64(1 - static_cast<signed char>(n));
for (qint64 i = 0; i < rr; ++i) {
std::memcpy(output + j + i * pixelBytes, buf.data(), buf.size());
}
rr *= pixelBytes;
}
j += rr;
}
return j;
}
/*!
* \brief tvdcDeepDecompress
* the compression is made line by line for each elementof the chunk DPEL.
* For RGBA for example we have a Red line, a Green line, and so on.
*/
inline qint64 tvdcDeepDecompress(QIODevice *input, char *output, qint64 olen, const QList<quint16>& table)
{
if (table.size() != 16) {
return -1;
}
quint8 v = 0;
quint8 last = 0;
for (qint64 i = 0, pos = 0, lastRead = -1; i < olen; ++i) {
if ((pos >> 1) != lastRead) {
char n;
if (input->read(&n, 1) != 1) {
return -1;
}
lastRead = (pos >> 1);
last = quint8(n);
}
quint8 d = last;
if (pos++ & 1) {
d &= 0xf;
} else {
d >>= 4;
}
v += table.at(d);
output[i] = char(v);
if (!table.at(d)) {
if ((pos >> 1) != lastRead) {
char n;
if (input->read(&n, 1) != 1) {
return -1;
}
lastRead = (pos >> 1);
last = quint8(n);
}
d = last;
if (pos++ & 1) {
d &= 0xf;
} else {
d >>= 4;
}
while (d--) {
if (i < olen - 1) {
output[++i] = char(v);
continue;
}
return -1;
}
}
}
return olen;
}
QByteArray DBODChunk::strideRead(QIODevice *d, qint32, const DGBLChunk *header, const DPELChunk *pel, const DLOCChunk *loc, const TVDCChunk *tvdc) const
{
auto size = strideSize(header, pel, loc);
if (size == 0) {
return {};
}
qint64 rr = 0;
QByteArray planes(size, char());
if (header->compression() == DGBLChunk::Compression::Uncompressed) {
rr = d->read(planes.data(), planes.size());
} else if (header->compression() == DGBLChunk::Compression::Rle) {
rr = rleDeepDecompress(d, planes.data(), planes.size(), pel->depth() / 8);
} else if (header->compression() == DGBLChunk::Compression::TvDeepCompression) {
if (tvdc) { // TVDC is planar, so I have to convert to chunky
auto table = tvdc->table();
for (auto i = 0, n = pel->count(); i < n; ++i) {
QByteArray ba(size / n, char());
rr += tvdcDeepDecompress(d, ba.data(), ba.size(), tvdc->table());
for (auto j = 0, m = int(ba.size()); j < m; ++j) {
planes[i + j * n] = ba.at(j);
}
}
}
} else {
qCDebug(LOG_IFFPLUGIN) << "DBODChunk::strideRead(): unknown compression" << header->compression();
}
// Uncompressed, Rle and TvDeepCompression: one line at a time.
if (rr != size) {
return {};
}
// byte swap
if (auto count = pel->count()) {
if (pel->depth() / count == 16) {
for (auto x = 0, w = qint32(planes.size()) - 1; x < w; x += 2) {
std::swap(planes[x], planes[x + 1]);
}
}
}
return planes;
}
bool DBODChunk::resetStrideRead(QIODevice *d) const
{
return seek(d);
}
quint32 DBODChunk::strideSize(const DGBLChunk *header, const DPELChunk *pel, const DLOCChunk *loc) const
{
auto width = loc ? loc->width() : header->width();
return (width * pel->depth() + 7) / 8;
}
/* ******************
* *** BEAM Chunk ***
* ****************** */

View File

@@ -55,6 +55,10 @@ Q_DECLARE_LOGGING_CATEGORY(LOG_IFFPLUGIN)
#define CAMG_CHUNK QByteArray("CAMG")
#define CMAP_CHUNK QByteArray("CMAP")
#define CMYK_CHUNK QByteArray("CMYK") // https://wiki.amigaos.net/wiki/ILBM_IFF_Interleaved_Bitmap#ILBM.CMYK
#define DBOD_CHUNK QByteArray("DBOD")
#define DGBL_CHUNK QByteArray("DGBL")
#define DLOC_CHUNK QByteArray("DLOC")
#define DPEL_CHUNK QByteArray("DPEL")
#define DPI__CHUNK QByteArray("DPI ")
#define IDAT_CHUNK QByteArray("IDAT")
#define IHDR_CHUNK QByteArray("IHDR")
@@ -65,6 +69,7 @@ Q_DECLARE_LOGGING_CATEGORY(LOG_IFFPLUGIN)
#define RFLG_CHUNK QByteArray("RFLG")
#define RGHD_CHUNK QByteArray("RGHD")
#define RSCM_CHUNK QByteArray("RSCM")
#define TVDC_CHUNK QByteArray("TVDC")
#define XBMI_CHUNK QByteArray("XBMI")
#define YUVS_CHUNK QByteArray("YUVS")
@@ -96,12 +101,14 @@ Q_DECLARE_LOGGING_CATEGORY(LOG_IFFPLUGIN)
// FORM types
#define ACBM_FORM_TYPE QByteArray("ACBM")
#define DEEP_FORM_TYPE QByteArray("DEEP")
#define ILBM_FORM_TYPE QByteArray("ILBM")
#define IMAG_FORM_TYPE QByteArray("IMAG")
#define PBM__FORM_TYPE QByteArray("PBM ")
#define RGB8_FORM_TYPE QByteArray("RGB8")
#define RGBN_FORM_TYPE QByteArray("RGBN")
#define RGFX_FORM_TYPE QByteArray("RGFX")
#define TVPP_FORM_TYPE QByteArray("TVPP") // same as DEEP
#define CIMG_FOR4_TYPE QByteArray("CIMG")
#define TBMP_FOR4_TYPE QByteArray("TBMP")
@@ -642,11 +649,23 @@ class CAMGChunk : public IFFChunk
{
public:
enum ModeId {
LoResLace = 0x0004,
GenLockVideo = 0x0002,
InterlacedDisplay = 0x0004,
DoubleScan = 0x0008,
SuperHighResolution = 0x0020,
PlayfieldBitplaneAdjust = 0x0040,
HalfBrite = 0x0080,
LoResDpf = 0x0400,
Ham = 0x0800,
HiRes = 0x8000
GenLockAudio = 0x0100,
DualPlayfield = 0x0400,
HoldAndModify = 0x0800,
ExtendedMode = 0x1000,
ViewPortHide = 0x2000,
Sprites = 0x4000,
HighResolution = 0x8000,
// aliases
Lace = InterlacedDisplay,
Ham = HoldAndModify
};
Q_DECLARE_FLAGS(ModeIds, ModeId)
@@ -946,6 +965,8 @@ private:
QImage::Format cdiFormat() const;
QImage::Format rgfxFormat() const;
QImage::Format deepFormat() const;
};
@@ -1946,6 +1967,266 @@ private:
};
/*!
* *** DEEP IFF CHUNKS ***
*/
/*!
* \brief The DGBLChunk class
*/
class DGBLChunk : public IFFChunk
{
public:
enum Compression : quint16 {
Uncompressed = 0,
Rle = 1,
Huffman = 2,
DynamicHuffman = 3,
Jpeg = 4,
TvDeepCompression = 5
};
virtual ~DGBLChunk() override;
DGBLChunk();
DGBLChunk(const DGBLChunk& other) = default;
DGBLChunk& operator =(const DGBLChunk& other) = default;
/*!
* \brief compression
* \return The type of compression used.
*/
Compression compression() const;
/*!
* \brief width
* \return Width of the source display in pixels.
*/
qint32 width() const;
/*!
* \brief height
* \return Height of the source display in pixels.
*/
qint32 height() const;
/*!
* \brief size
* \return Size of the source display in pixels.
*/
QSize size() const
{
return QSize(width(), height());
}
/*!
* \brief xAspectRatio
* \return X pixel aspect.
*/
quint8 xAspectRatio() const;
/*!
* \brief yAspectRatio
* \return Y pixel aspect.
*/
quint8 yAspectRatio() const;
virtual bool isValid() const override;
CHUNKID_DEFINE(DGBL_CHUNK)
protected:
virtual bool innerReadStructure(QIODevice *d) override;
};
/*!
* \brief The DPELChunk class
*/
class DPELChunk : public IFFChunk
{
public:
enum DataType : quint16 {
Invalid = 0,
Red = 1,
Green = 2,
Blue = 3,
Alpha = 4,
Yellow = 5,
Cyan = 6,
Magenta = 7,
Black = 8,
Mask = 9,
ZBuffer = 10,
Opacity = 11,
LinearKey = 12,
BinaryKey = 13
};
struct Element {
Element(const DataType& t = DataType::Invalid, quint16 d = 0) : type(t), depth(d) {}
DataType type;
quint16 depth;
};
virtual ~DPELChunk() override;
DPELChunk();
DPELChunk(const DPELChunk& other) = default;
DPELChunk& operator =(const DPELChunk& other) = default;
/*!
* \brief count
* \return The number of elements
*/
qint32 count() const;
/*!
* \brief depth
* \return The pixel depth.
*/
qint32 depth() const;
/*!
* \brief elements
* Elements needed to identify the content of every pixel.
* Pixels will always be padded to byte boundaries.
* \return The list of elements. An empty list on error.
*/
QList<Element> elements() const;
virtual bool isValid() const override;
CHUNKID_DEFINE(DPEL_CHUNK)
protected:
virtual bool innerReadStructure(QIODevice *d) override;
};
/*!
* \brief The DLOCChunk class
*/
class DLOCChunk : public IFFChunk
{
public:
virtual ~DLOCChunk() override;
DLOCChunk();
DLOCChunk(const DLOCChunk& other) = default;
DLOCChunk& operator =(const DLOCChunk& other) = default;
/*!
* \brief width
* \return Width of the bitmap in pixels.
*/
qint32 width() const;
/*!
* \brief height
* \return Height of the bitmap in pixels.
*/
qint32 height() const;
/*!
* \brief size
* \return Size in pixels.
*/
QSize size() const;
/*!
* \brief xOffset
* X offset of origin in source image.
*/
qint32 xOffset() const;
/*!
* \brief yOffset
* \returnX offset of origin in source image.
*/
qint32 yOffset() const;
virtual bool isValid() const override;
CHUNKID_DEFINE(DLOC_CHUNK)
protected:
virtual bool innerReadStructure(QIODevice *d) override;
};
/*!
* \brief The TVDCChunk class
*/
class TVDCChunk : public IFFChunk
{
public:
virtual ~TVDCChunk() override;
TVDCChunk();
TVDCChunk(const TVDCChunk& other) = default;
TVDCChunk& operator =(const TVDCChunk& other) = default;
qint32 count() const;
QList<quint16> table() const;
virtual bool isValid() const override;
CHUNKID_DEFINE(TVDC_CHUNK)
protected:
virtual bool innerReadStructure(QIODevice *d) override;
};
/*!
* \brief The DBODChunk class
*/
class DBODChunk : public IFFChunk
{
public:
virtual ~DBODChunk() override;
DBODChunk();
DBODChunk(const DBODChunk& other) = default;
DBODChunk& operator =(const DBODChunk& other) = default;
virtual bool isValid() const override;
CHUNKID_DEFINE(DBOD_CHUNK)
/*!
* \brief readStride
* \param d The device.
* \param y The current scanline.
* \param header The bitmap header.
* \param pel The pixel elements info.
* \param loc The display location (optional).
* \return The scanline as requested for QImage.
* \warning Call resetStrideRead() once before this one.
*/
QByteArray strideRead(QIODevice *d,
qint32 y,
const DGBLChunk *header,
const DPELChunk *pel,
const DLOCChunk *loc = nullptr,
const TVDCChunk *tvdc = nullptr) const;
/*!
* \brief resetStrideRead
* Reset the stride read set the position at the beginning of the data and reset all buffers.
* \param d The device.
* \return True on success, otherwise false.
* \sa strideRead
* \note Must be called once before strideRead().
*/
bool resetStrideRead(QIODevice *d) const;
protected:
/*!
* \brief strideSize
* \return The size of data to have to decode an image row.
*/
quint32 strideSize(const DGBLChunk *header, const DPELChunk *pel, const DLOCChunk *loc = nullptr) const;
};
/*!
* *** UNDOCUMENTED CHUNKS ***
*/

View File

@@ -1,4 +1,4 @@
{
"Keys": [ "dds" ],
"MimeTypes": [ "image/x-dds" ]
"Keys": [ "dds", "dds" ],
"MimeTypes": [ "image/vnd.ms-dds", "image/x-dds" ]
}

View File

@@ -209,6 +209,8 @@ EXRHandler::EXRHandler()
{
// Set the number of threads to use (0 is allowed)
Imf::setGlobalThreadCount(QThread::idealThreadCount() / 2);
// Set maximum image size
Imf::Header::setMaxImageSize(EXR_MAX_IMAGE_WIDTH, EXR_MAX_IMAGE_HEIGHT);
}
bool EXRHandler::canRead() const

259
src/imageformats/ff.cpp Normal file
View File

@@ -0,0 +1,259 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2026 Mirco Miranda <mircomir@outlook.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
// Specs: https://tools.suckless.org/farbfeld/
#include "ff_p.h"
#include "util_p.h"
#include <QColorSpace>
#include <QIODevice>
#include <QImage>
#include <QLoggingCategory>
#include <QtEndian>
Q_DECLARE_LOGGING_CATEGORY(LOG_FFPLUGIN)
#ifdef QT_DEBUG
Q_LOGGING_CATEGORY(LOG_FFPLUGIN, "kf.imageformats.plugins.ff", QtDebugMsg)
#else
Q_LOGGING_CATEGORY(LOG_FFPLUGIN, "kf.imageformats.plugins.ff", QtWarningMsg)
#endif
/* *** FF_MAX_IMAGE_WIDTH and FF_MAX_IMAGE_HEIGHT ***
* The maximum size in pixel allowed by the plugin.
*/
#ifndef FF_MAX_IMAGE_WIDTH
#define FF_MAX_IMAGE_WIDTH KIF_LARGE_IMAGE_PIXEL_LIMIT
#endif
#ifndef FF_MAX_IMAGE_HEIGHT
#define FF_MAX_IMAGE_HEIGHT FF_MAX_IMAGE_WIDTH
#endif
#define HEADER_SIZE 16
class FFHeader
{
private:
QByteArray m_rawHeader;
public:
FFHeader()
{
}
bool isValid() const
{
if (m_rawHeader.size() < HEADER_SIZE) {
return false;
}
return m_rawHeader.startsWith(QByteArray::fromRawData("farbfeld", 8));
}
bool isSupported() const
{
auto w = width();
auto h = height();
if (w < 1 || w > FF_MAX_IMAGE_WIDTH || h < 1 || h > FF_MAX_IMAGE_HEIGHT) {
return false;
}
return format() != QImage::Format_Invalid;
}
qint32 width() const
{
if (!isValid()) {
return 0;
}
return qFromBigEndian<qint32>(m_rawHeader.data() + 8);
}
qint32 height() const
{
if (!isValid()) {
return 0;
}
return qFromBigEndian<qint32>(m_rawHeader.data() + 12);
}
QSize size() const
{
return QSize(width(), height());
}
QImage::Format format() const
{
if (!isValid()) {
return QImage::Format_Invalid;
}
return QImage::Format_RGBA64;
}
bool read(QIODevice *d)
{
m_rawHeader = d->read(HEADER_SIZE);
if (m_rawHeader.size() != HEADER_SIZE) {
return false;
}
return isValid();
}
bool peek(QIODevice *d)
{
m_rawHeader = d->peek(HEADER_SIZE);
if (m_rawHeader.size() != HEADER_SIZE) {
return false;
}
return isValid();
}
};
class FFHandlerPrivate
{
public:
FFHandlerPrivate() {}
~FFHandlerPrivate() {}
FFHeader m_header;
};
FFHandler::FFHandler()
: QImageIOHandler()
, d(new FFHandlerPrivate)
{
}
bool FFHandler::canRead() const
{
if (canRead(device())) {
setFormat("ff");
return true;
}
return false;
}
bool FFHandler::canRead(QIODevice *device)
{
if (!device) {
qCWarning(LOG_FFPLUGIN) << "FFHandler::canRead() called with no device";
return false;
}
FFHeader h;
if (!h.peek(device)) {
return false;
}
return h.isSupported();
}
bool FFHandler::read(QImage *image)
{
auto&& header = d->m_header;
if (!header.read(device())) {
qCWarning(LOG_FFPLUGIN) << "FFHandler::read() invalid header";
return false;
}
auto img = imageAlloc(header.size(), header.format());
if (img.isNull()) {
qCWarning(LOG_FFPLUGIN) << "FFHandler::read() error while allocating the image";
return false;
}
auto d = device();
auto size = img.bytesPerLine();
for (auto y = 0, h = img.height(); y < h; ++y) {
auto line = reinterpret_cast<char*>(img.scanLine(y));
if (d->read(line, size) != size) {
qCWarning(LOG_FFPLUGIN) << "FFHandler::read() error while reading image scanline";
return false;
}
#if Q_LITTLE_ENDIAN
for (auto i = 0; i < size; i += 2) {
std::swap(line[i], line[i + 1]);
}
#endif
}
img.setColorSpace(QColorSpace(QColorSpace::SRgb));
*image = img;
return true;
}
bool FFHandler::supportsOption(ImageOption option) const
{
if (option == QImageIOHandler::Size) {
return true;
}
if (option == QImageIOHandler::ImageFormat) {
return true;
}
return false;
}
QVariant FFHandler::option(ImageOption option) const
{
QVariant v;
if (option == QImageIOHandler::Size) {
auto&& h = d->m_header;
if (h.isValid()) {
v = QVariant::fromValue(h.size());
} else if (auto d = device()) {
if (h.peek(d)) {
v = QVariant::fromValue(h.size());
}
}
}
if (option == QImageIOHandler::ImageFormat) {
auto&& h = d->m_header;
if (h.isValid()) {
v = QVariant::fromValue(h.format());
} else if (auto d = device()) {
if (h.peek(d)) {
v = QVariant::fromValue(h.format());
}
}
}
return v;
}
QImageIOPlugin::Capabilities FFPlugin::capabilities(QIODevice *device, const QByteArray &format) const
{
if (format == "ff") {
return Capabilities(CanRead);
}
if (!format.isEmpty()) {
return {};
}
if (!device->isOpen()) {
return {};
}
Capabilities cap;
if (device->isReadable() && FFHandler::canRead(device)) {
cap |= CanRead;
}
return cap;
}
QImageIOHandler *FFPlugin::create(QIODevice *device, const QByteArray &format) const
{
QImageIOHandler *handler = new FFHandler;
handler->setDevice(device);
handler->setFormat(format);
return handler;
}
#include "moc_ff_p.cpp"

4
src/imageformats/ff.json Normal file
View File

@@ -0,0 +1,4 @@
{
"Keys": [ "ff" ],
"MimeTypes": [ "image/x-farbfeld" ]
}

42
src/imageformats/ff_p.h Normal file
View File

@@ -0,0 +1,42 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2026 Mirco Miranda <mircomir@outlook.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KIMG_FF_P_H
#define KIMG_FF_P_H
#include <QImageIOPlugin>
#include <QScopedPointer>
class FFHandlerPrivate;
class FFHandler : public QImageIOHandler
{
public:
FFHandler();
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);
private:
const QScopedPointer<FFHandlerPrivate> d;
};
class FFPlugin : public QImageIOPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QImageIOHandlerFactoryInterface" FILE "ff.json")
public:
Capabilities capabilities(QIODevice *device, const QByteArray &format) const override;
QImageIOHandler *create(QIODevice *device, const QByteArray &format = QByteArray()) const override;
};
#endif // KIMG_FF_P_H

View File

@@ -11,8 +11,10 @@
#include "microexif_p.h"
#include "util_p.h"
#include <libheif/heif.h>
#include <libheif/heif_properties.h>
#include <QColorSpace>
#include <QImageReader>
#include <QLoggingCategory>
#include <QPointF>
#include <QSysInfo>
@@ -33,6 +35,28 @@ Q_LOGGING_CATEGORY(LOG_HEIFPLUGIN, "kf.imageformats.plugins.heif", QtWarningMsg)
#define HEIF_MAX_METADATA_SIZE (4 * 1024 * 1024)
#endif
#ifndef HEIF_DISABLE_QT_TRANSFORMATION
/*!
* HEIF transformations, in addition to rotations and reflections,
* also support image cropping. Consequently, the Qt plugin, must
* also honor the crop. This define is useful in case of problems:
* activating it disables Qt's support for transformations,
* delegating them to the HEIF libraries (which will therefore
* always apply them regardless of what is requested from Qt).
*/
// #define HEIF_DISABLE_QT_TRANSFORMATION
#endif
/* *** HEIF_MAX_IMAGE_WIDTH and HEIF_MAX_IMAGE_HEIGHT ***
* The maximum size in pixel allowed by the plugin.
*/
#ifndef HEIF_MAX_IMAGE_WIDTH
#define HEIF_MAX_IMAGE_WIDTH KIF_64K_IMAGE_PIXEL_LIMIT
#endif
#ifndef HEIF_MAX_IMAGE_HEIGHT
#define HEIF_MAX_IMAGE_HEIGHT HEIF_MAX_IMAGE_WIDTH
#endif
size_t HEIFHandler::m_initialized_count = 0;
bool HEIFHandler::m_plugins_queried = false;
bool HEIFHandler::m_heif_decoder_available = false;
@@ -41,6 +65,58 @@ bool HEIFHandler::m_hej2_decoder_available = false;
bool HEIFHandler::m_hej2_encoder_available = false;
bool HEIFHandler::m_avci_decoder_available = false;
/*!
* \brief create_heif_reader_for_qiodevice
* Create a heif_reader structure that wraps a QIODevice for streaming
* \return heif_reader structure with callbacks delegating to QIODevice
*/
static heif_reader create_heif_reader_for_qiodevice()
{
heif_reader reader = {};
reader.reader_api_version = 1;
reader.get_position = [](void* userdata) -> int64_t {
QIODevice* device = static_cast<QIODevice*>(userdata);
return device->pos();
};
reader.read = [](void* data, size_t size, void* userdata) -> int {
QIODevice* device = static_cast<QIODevice*>(userdata);
qint64 bytesRead = device->read(static_cast<char*>(data), size);
if (bytesRead == -1) {
return -1; // Error
}
if (bytesRead != size) {
// We expected to read 'size' bytes but got less.
// This is an error because we should have known the size.
return -1; // Error
}
return 0; // Success
};
reader.seek = [](int64_t position, void* userdata) -> int {
QIODevice* device = static_cast<QIODevice*>(userdata);
return device->seek(position) ? 0 : -1;
};
reader.wait_for_file_size = [](int64_t target_size, void* userdata) -> heif_reader_grow_status {
QIODevice* device = static_cast<QIODevice*>(userdata);
if (target_size <= device->size()) {
return heif_reader_grow_status_size_reached;
}
return heif_reader_grow_status_size_beyond_eof;
};
// Version 2 functions set to NULL as we don't need them for simple streaming
reader.request_range = nullptr;
reader.preload_range_hint = nullptr;
reader.release_file_range = nullptr;
reader.release_error_msg = nullptr; // We use static error strings
return reader;
}
extern "C" {
static struct heif_error heifhandler_write_callback(struct heif_context * /* ctx */, const void *data, size_t size, void *userdata)
{
@@ -72,6 +148,7 @@ static struct heif_error heifhandler_write_callback(struct heif_context * /* ctx
HEIFHandler::HEIFHandler()
: m_parseState(ParseHeicNotParsed)
, m_quality(100)
, m_orientation(0)
{
}
@@ -123,15 +200,16 @@ bool HEIFHandler::write(const QImage &image)
return false;
}
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
if (image.width() >= HEIF_MAX_IMAGE_WIDTH || image.height() >= HEIF_MAX_IMAGE_HEIGHT) {
qCWarning(LOG_HEIFPLUGIN) << "Image size invalid:" << image.width() << "x" << image.height();
return false;
}
startHeifLib();
#endif
bool success = write_helper(image);
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
finishHeifLib();
#endif
return success;
}
@@ -163,12 +241,10 @@ bool HEIFHandler::write_helper(const QImage &image)
}
heif_compression_format encoder_codec = heif_compression_HEVC;
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
if (format() == "hej2") {
encoder_codec = heif_compression_JPEG2000;
save_depth = 8; // for compatibility reasons
}
#endif
heif_chroma chroma;
if (save_depth > 8) {
@@ -325,12 +401,21 @@ bool HEIFHandler::write_helper(const QImage &image)
}
}
if (m_orientation >= 1 && m_orientation <= 8) {
// Function available from HEIF v1.14
encoder_options->image_orientation = heif_orientation(m_orientation);
}
struct heif_image_handle *handle;
err = heif_context_encode_image(context, h_image, encoder, encoder_options, &handle);
// exif metadata
if (err.code == heif_error_Ok) {
auto exif = MicroExif::fromImage(tmpimage);
if (m_orientation >= 1 && m_orientation <= 8) {
// EXIF orientation must be coherent with HEIF orientation
exif.setOrientation(m_orientation);
}
if (!exif.isEmpty()) {
auto ba = exif.toByteArray();
err = heif_context_add_exif_metadata(context, handle, ba.constData(), ba.size());
@@ -376,6 +461,74 @@ bool HEIFHandler::write_helper(const QImage &image)
return true;
}
bool HEIFHandler::read_orientation_helper(void *heif_handle, const void *heif_ctx)
{
if (heif_handle == nullptr || heif_ctx == nullptr) {
return false;
}
auto handle = reinterpret_cast<heif_image_handle *>(heif_handle);
auto ctx = reinterpret_cast<const heif_context *>(heif_ctx);
auto item_id = heif_image_handle_get_item_id(handle);
// get the properties
heif_transform_mirror_direction mirror = heif_transform_mirror_direction::heif_transform_mirror_direction_invalid;
heif_property_id mir_id;
if (heif_item_get_properties_of_type(ctx, item_id, heif_item_property_type_transform_mirror, &mir_id, 1) > 0) {
mirror = heif_item_get_property_transform_mirror(ctx, item_id, mir_id);
if (mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_invalid)
return false;
}
int rotation_ccw = -1;
heif_property_id rot_id;
if (heif_item_get_properties_of_type(ctx, item_id, heif_item_property_type_transform_rotation, &rot_id, 1) > 0) {
rotation_ccw = heif_item_get_property_transform_rotation_ccw(ctx, item_id, rot_id);
if (rotation_ccw == -1)
return false;
}
if (rotation_ccw == -1 && mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_invalid) {
m_orientation = 0;
} else if (rotation_ccw == 0 && mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_invalid) {
m_orientation = 1;
} else if (rotation_ccw <= 0 && mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_horizontal) {
m_orientation = 2;
} else if (rotation_ccw == 180 && mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_invalid) {
m_orientation = 3;
} else if (rotation_ccw <= 0 && mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_vertical) {
m_orientation = 4;
} else if (rotation_ccw == 270 && mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_horizontal) {
m_orientation = 5;
} else if (rotation_ccw == 270 && mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_invalid) {
m_orientation = 6;
} else if (rotation_ccw == 270 && mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_vertical) {
m_orientation = 7;
} else if (rotation_ccw == 90 && mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_invalid) {
m_orientation = 8;
}
return true;
}
bool HEIFHandler::read_crop(void *heif_handle, const void *heif_ctx, const QSize &size, QRect &crop)
{
if (heif_handle == nullptr || heif_ctx == nullptr) {
return false;
}
auto handle = reinterpret_cast<heif_image_handle *>(heif_handle);
auto ctx = reinterpret_cast<const heif_context *>(heif_ctx);
auto item_id = heif_image_handle_get_item_id(handle);
heif_property_id crop_id;
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;
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));
}
return crop.isValid();
}
bool HEIFHandler::isSupportedBMFFType(const QByteArray &header)
{
if (header.size() < 28) {
@@ -456,10 +609,10 @@ QVariant HEIFHandler::option(ImageOption option) const
switch (option) {
case Size:
return m_current_image.size();
break;
case ImageTransformation:
return int(MicroExif::orientationToTransformation(m_orientation));
default:
return QVariant();
break;
}
}
@@ -474,6 +627,9 @@ void HEIFHandler::setOption(ImageOption option, const QVariant &value)
m_quality = 100;
}
break;
case ImageTransformation:
m_orientation = MicroExif::transformationToOrientation(QImageIOHandler::Transformation(value.toUInt()));
break;
default:
QImageIOHandler::setOption(option, value);
break;
@@ -482,7 +638,11 @@ void HEIFHandler::setOption(ImageOption option, const QVariant &value)
bool HEIFHandler::supportsOption(ImageOption option) const
{
return option == Quality || option == Size;
auto ok = option == Quality || option == Size;
#ifndef HEIF_DISABLE_QT_TRANSFORMATION
ok = ok || option == ImageTransformation;
#endif
return ok;
}
bool HEIFHandler::ensureParsed() const
@@ -496,15 +656,12 @@ bool HEIFHandler::ensureParsed() const
HEIFHandler *that = const_cast<HEIFHandler *>(this);
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
startHeifLib();
#endif
bool success = that->ensureDecoder();
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
finishHeifLib();
#endif
return success;
}
@@ -517,17 +674,48 @@ bool HEIFHandler::ensureDecoder()
return false;
}
const QByteArray buffer = device()->readAll();
QIODevice *dev = device();
if (dev == nullptr) {
qCCritical(LOG_HEIFPLUGIN) << "create_heif_reader_for_qiodevice error: device is null";
m_parseState = ParseHeicError;
return false;
}
QByteArray buffer = dev->peek(28);
if (!HEIFHandler::isSupportedBMFFType(buffer) && !HEIFHandler::isSupportedHEJ2(buffer) && !HEIFHandler::isSupportedAVCI(buffer)) {
m_parseState = ParseHeicError;
return false;
}
struct heif_context *ctx = heif_context_alloc();
struct heif_error err = heif_context_read_from_memory(ctx, static_cast<const void *>(buffer.constData()), buffer.size(), nullptr);
struct heif_error err;
#if LIBHEIF_HAVE_VERSION(1, 19, 1)
if (auto heif_limits = heif_context_get_security_limits(ctx)) {
heif_limits->max_image_size_pixels = quint64(HEIF_MAX_IMAGE_WIDTH) * HEIF_MAX_IMAGE_HEIGHT;
heif_limits->max_memory_block_size = quint64(QImageReader::allocationLimit()) * 1024 * 1024;
#if LIBHEIF_HAVE_VERSION(1, 20, 0)
heif_limits->max_total_memory = heif_limits->max_memory_block_size;
#endif
err = heif_context_set_security_limits(ctx, heif_limits);
if (err.code) {
qCWarning(LOG_HEIFPLUGIN) << "heif_context_set_security_limits error:" << err.message;
heif_context_free(ctx);
m_parseState = ParseHeicError;
return false;
}
}
#endif
if (dev->isSequential()) {
buffer = deviceRead(dev, kMaxQVectorSize);
err = heif_context_read_from_memory(ctx, static_cast<const void *>(buffer.constData()), buffer.size(), nullptr);
} else {
heif_reader reader = create_heif_reader_for_qiodevice();
err = heif_context_read_from_reader(ctx, &reader, dev, nullptr);
}
if (err.code) {
qCWarning(LOG_HEIFPLUGIN) << "heif_context_read_from_memory error:" << err.message;
qCWarning(LOG_HEIFPLUGIN) << "heif_context_read_from_reader error:" << err.message;
heif_context_free(ctx);
m_parseState = ParseHeicError;
return false;
@@ -589,16 +777,19 @@ bool HEIFHandler::ensureDecoder()
return false;
}
bool ignore_transformations = false;
struct heif_decoding_options *decoder_option = heif_decoding_options_alloc();
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
decoder_option->strict_decoding = 1;
#ifdef HEIF_DISABLE_QT_TRANSFORMATION
decoder_option->ignore_transformations = ignore_transformations;
#else
decoder_option->ignore_transformations = ignore_transformations = read_orientation_helper(handle, ctx);
#endif
struct heif_image *img = nullptr;
err = heif_decode_image(handle, &img, heif_colorspace_RGB, chroma, decoder_option);
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
if (err.code == heif_error_Invalid_input && err.subcode == heif_suberror_Unknown_NCLX_matrix_coefficients && img == nullptr && buffer.contains("Xiaomi")) {
qCWarning(LOG_HEIFPLUGIN) << "Non-standard HEIF image with invalid matrix_coefficients, probably made by a Xiaomi device!";
@@ -606,7 +797,6 @@ bool HEIFHandler::ensureDecoder()
decoder_option->strict_decoding = 0;
err = heif_decode_image(handle, &img, heif_colorspace_RGB, chroma, decoder_option);
}
#endif
if (decoder_option) {
heif_decoding_options_free(decoder_option);
@@ -623,14 +813,17 @@ bool HEIFHandler::ensureDecoder()
const int imageWidth = heif_image_get_width(img, heif_channel_interleaved);
const int imageHeight = heif_image_get_height(img, heif_channel_interleaved);
QSize imageSize(imageWidth, imageHeight);
QSize imageSize;
if (imageWidth < HEIF_MAX_IMAGE_WIDTH && imageHeight < HEIF_MAX_IMAGE_HEIGHT) {
imageSize = QSize(imageWidth, imageHeight);
}
if (!imageSize.isValid()) {
heif_image_release(img);
heif_image_handle_release(handle);
heif_context_free(ctx);
m_parseState = ParseHeicError;
qCWarning(LOG_HEIFPLUGIN) << "HEIC image size invalid:" << imageSize;
qCWarning(LOG_HEIFPLUGIN) << "HEIC image size invalid:" << imageWidth << "x" << imageHeight;
return false;
}
@@ -852,6 +1045,12 @@ bool HEIFHandler::ensureDecoder()
break;
}
if (ignore_transformations) {
QRect crop_rect;
if (read_crop(handle, ctx, m_current_image.size(), crop_rect))
m_current_image = m_current_image.copy(crop_rect);
}
heif_color_profile_type profileType = heif_image_handle_get_color_profile_type(handle);
if (profileType == heif_color_profile_type_prof || profileType == heif_color_profile_type_rICC) {
size_t rawProfileSize = heif_image_handle_get_raw_color_profile_size(handle);
@@ -1045,34 +1244,27 @@ void HEIFHandler::queryHeifLib()
QMutexLocker locker(&getHEIFHandlerMutex());
if (!m_plugins_queried) {
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
if (m_initialized_count == 0) {
heif_init(nullptr);
}
#endif
m_heif_encoder_available = heif_have_encoder_for_format(heif_compression_HEVC);
m_heif_decoder_available = heif_have_decoder_for_format(heif_compression_HEVC);
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
m_hej2_decoder_available = heif_have_decoder_for_format(heif_compression_JPEG2000);
m_hej2_encoder_available = heif_have_encoder_for_format(heif_compression_JPEG2000);
#endif
#if LIBHEIF_HAVE_VERSION(1, 19, 6)
m_avci_decoder_available = heif_have_decoder_for_format(heif_compression_AVC);
#endif
m_plugins_queried = true;
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
if (m_initialized_count == 0) {
heif_deinit();
}
#endif
}
}
void HEIFHandler::startHeifLib()
{
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
QMutexLocker locker(&getHEIFHandlerMutex());
if (m_initialized_count == 0) {
@@ -1080,12 +1272,10 @@ void HEIFHandler::startHeifLib()
}
m_initialized_count++;
#endif
}
void HEIFHandler::finishHeifLib()
{
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
QMutexLocker locker(&getHEIFHandlerMutex());
if (m_initialized_count == 0) {
@@ -1096,8 +1286,6 @@ void HEIFHandler::finishHeifLib()
if (m_initialized_count == 0) {
heif_deinit();
}
#endif
}
QMutex &HEIFHandler::getHEIFHandlerMutex()

View File

@@ -51,9 +51,24 @@ private:
ParseHeicState m_parseState;
int m_quality;
QImage m_current_image;
quint16 m_orientation;
bool write_helper(const QImage &image);
/*!
* \brief heif_orientation_helper
* Read the transform_mirror and transform_rotation_ccw properties and set \a m_orientation
* \return True on success, otherwise false.
*/
bool read_orientation_helper(void *heif_handle, const void *heif_ctx);
/*!
* \brief read_crop
* Read the crop information.
* \return True on success, otherwise false.
*/
bool read_crop(void *heif_handle, const void *heif_ctx, const QSize& size, QRect &crop);
static void startHeifLib();
static void finishHeifLib();
static void queryHeifLib();

View File

@@ -607,6 +607,69 @@ bool IFFHandler::readRGFXImage(QImage *image)
return true;
}
bool IFFHandler::readDEEPImage(QImage *image)
{
auto forms = d->searchForms<FORMChunk>();
if (forms.isEmpty()) {
return false;
}
auto cin = qBound(0, currentImageNumber(), int(forms.size() - 1));
auto &&form = forms.at(cin);
// show the first one (I don't have a sample with many images)
auto headers = IFFChunk::searchT<DGBLChunk>(form);
if (headers.isEmpty()) {
return false;
}
// create the image
auto &&header = headers.first();
auto size = header->size();
auto locs = IFFChunk::searchT<DLOCChunk>(form);
if (!locs.isEmpty()) {
size = locs.first()->size();
}
auto img = imageAlloc(size, form->format());
if (img.isNull()) {
qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readDEEPImage(): error while allocating the image";
return false;
}
// decoding the image
auto bodies = IFFChunk::searchT<DBODChunk>(form);
if (bodies.isEmpty()) {
img.fill(0);
} else {
auto &&body = bodies.first();
if (!body->resetStrideRead(device())) {
qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readDEEPImage(): error while reading image data";
return false;
}
auto pels = IFFChunk::searchT<DPELChunk>(form);
if (pels.isEmpty()) {
// DPEL is used to calculate the image format, so here should not be empty.
return false;
}
auto tvdcs = IFFChunk::searchT<TVDCChunk>(form);
for (auto y = 0, h = img.height(); y < h; ++y) {
auto line = reinterpret_cast<char*>(img.scanLine(y));
auto ba = body->strideRead(device(), y, header, pels.first(),
locs.isEmpty() ? nullptr : locs.first(),
tvdcs.isEmpty() ? nullptr : tvdcs.first());
if (ba.isEmpty()) {
qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readDEEPImage(): error while reading image scanline";
return false;
}
memcpy(line, ba.constData(), std::min(img.bytesPerLine(), ba.size()));
}
}
// set metadata (including image resolution)
addMetadata(img, form);
*image = img;
return true;
}
bool IFFHandler::read(QImage *image)
{
if (!d->readStructure(device())) {
@@ -630,6 +693,10 @@ bool IFFHandler::read(QImage *image)
return true;
}
if (readDEEPImage(image)) {
return true;
}
qCWarning(LOG_IFFPLUGIN) << "IFFHandler::read(): no supported image found";
return false;
}

View File

@@ -39,6 +39,8 @@ private:
bool readRGFXImage(QImage *image);
bool readDEEPImage(QImage *image);
private:
const QScopedPointer<IFFHandlerPrivate> d;
};

View File

@@ -12,7 +12,6 @@
#include <QColorSpace>
#include <QIODevice>
#include <QImage>
#include <QImageReader>
#include <QLoggingCategory>
#include <QThread>
@@ -187,7 +186,7 @@ public:
bool isImageValid(const opj_image_t *i) const
{
return i && i->comps && i->numcomps > 0;
return i && i->comps && i->numcomps > 0 && i->numcomps < 256;
}
void enableThreads(opj_codec_t *codec) const
@@ -359,15 +358,9 @@ public:
}
// OpenJPEG uses a shadow copy @32-bit/channel so we need to do a check
const int allocationLimit = QImageReader::allocationLimit();
if (allocationLimit > 0) {
auto maxBytes = qint64(allocationLimit) * 1024 * 1024;
auto neededBytes = qint64(width) * height * nchannels * 4;
if (maxBytes > 0 && neededBytes > maxBytes) {
qCCritical(LOG_JP2PLUGIN) << "Allocation limit set to" << (maxBytes / 1024 / 1024) << "MiB but" << (neededBytes / 1024 / 1024)
<< "MiB are needed!";
return false;
}
if (!checkImageSize(width, height, nchannels * 4)) {
qCCritical(LOG_JP2PLUGIN) << "Rejecting image as it exceeds the current allocation limit.";
return false;
}
return true;
@@ -384,6 +377,11 @@ public:
if (isImageValid(m_jp2_image)) {
auto &&c0 = m_jp2_image->comps[0];
auto tmp = QSize(c0.w, c0.h);
for (quint32 c = 1; c < m_jp2_image->numcomps; ++c) {
auto &&cc = m_jp2_image->comps[c];
if (QSize(cc.w, cc.h) != tmp)
tmp = QSize();
}
if (checkSizeLimits(tmp, m_jp2_image->numcomps))
sz = tmp;
}

View File

@@ -16,6 +16,7 @@
#include <jxl/cms.h>
#include <jxl/encode.h>
#include <jxl/memory_manager.h>
#include <jxl/thread_parallel_runner.h>
#include <string.h>
@@ -56,6 +57,21 @@ Q_LOGGING_CATEGORY(LOG_JXLPLUGIN, "kf.imageformats.plugins.jxl", QtWarningMsg)
#define MAX_IMAGE_PIXELS FEATURE_LEVEL_5_PIXELS
#endif
void *QtJXLMemoryManagerAlloc(void *opaque, size_t size)
{
if (opaque) {
size_t maxBytes = *(size_t*)opaque;
if (maxBytes && size > maxBytes)
return NULL;
}
return malloc(size);
}
void QtJXLMemoryManagerFree(void *, void *address)
{
free(address);
}
QJpegXLHandler::QJpegXLHandler()
: m_parseState(ParseJpegXLNotParsed)
, m_quality(90)
@@ -70,6 +86,7 @@ QJpegXLHandler::QJpegXLHandler()
, m_alpha_channel_id(0)
, m_input_image_format(QImage::Format_Invalid)
, m_target_image_format(QImage::Format_Invalid)
, m_maxBytes(size_t(QImageReader::allocationLimit()) * 1024 * 1024)
{
}
@@ -153,7 +170,7 @@ bool QJpegXLHandler::ensureDecoder()
return true;
}
m_rawData = device()->readAll();
m_rawData = deviceRead(device(), kMaxQVectorSize);
if (m_rawData.isEmpty()) {
return false;
@@ -165,7 +182,14 @@ bool QJpegXLHandler::ensureDecoder()
return false;
}
m_decoder = JxlDecoderCreate(nullptr);
// Creating a simple memory manager
JxlMemoryManager memory_manager = {
.opaque = &m_maxBytes,
.alloc = QtJXLMemoryManagerAlloc,
.free = QtJXLMemoryManagerFree
};
// Creating the decoder (it makes a deep copy of memory manager)
m_decoder = JxlDecoderCreate(&memory_manager);
if (!m_decoder) {
qCWarning(LOG_JXLPLUGIN, "ERROR: JxlDecoderCreate failed");
m_parseState = ParseJpegXLError;

View File

@@ -89,6 +89,8 @@ private:
QImage::Format m_target_image_format;
JxlPixelFormat m_input_pixel_format;
size_t m_maxBytes;
};
class QJpegXLPlugin : public QImageIOPlugin

View File

@@ -43,41 +43,39 @@ Q_LOGGING_CATEGORY(LOG_JXRPLUGIN, "kf.imageformats.plugins.jxr", QtWarningMsg)
#endif
/*!
* Support for float images
* \brief JXR_DENY_FLOAT_IMAGE
*
* NOTE: Float images have values greater than 1 so they need an additional in place conversion.
* When defined, disables the support for float images.
* \note Float images have values greater than 1 so they need an additional in place conversion.
*/
#ifndef JXR_DENY_FLOAT_IMAGE
// #define JXR_DENY_FLOAT_IMAGE // default commented
#endif
/*!
* Remove the needs of additional memory by disabling the conversion between
* different color depths (e.g. RGBA64bpp to RGBA32bpp).
* \brief JXR_DISABLE_DEPTH_CONVERSION
*
* NOTE: Leaving deptch conversion enabled (default) ensures maximum read compatibility.
* When defined, removes the needs of additional memory by disabling the conversion between
* different color depths (e.g. RGBA64bpp to RGBA32bpp).
* \note Leaving depth conversion enabled (default) ensures maximum read compatibility.
*/
#ifndef JXR_DISABLE_DEPTH_CONVERSION
// #define JXR_DISABLE_DEPTH_CONVERSION // default commented
#endif
/*!
* \brief JXR_DISABLE_BGRA_HACK
*
* When defined, disables Windows compatibility for BGRs.
*
* Windows displays and opens JXR files correctly out of the box. Unfortunately it doesn't
* seem to open (P)RGBA @32bpp files as it only wants (P)BGRA32bpp files (a format not supported by Qt).
* Only for this format an hack is activated to guarantee total compatibility of the plugin with Windows.
* Only for this format, an hack is activated to guarantee total compatibility of the plugin with Windows,
* at the cost of some overhead.
*/
#ifndef JXR_DISABLE_BGRA_HACK
// #define JXR_DISABLE_BGRA_HACK // default commented
/*!
* The following functions are present in the Debian headers but not in the SUSE ones even if the source version is 1.0.1 on both.
*
* - ERR PKImageDecode_GetXMPMetadata_WMP(PKImageDecode *pID, U8 *pbXMPMetadata, U32 *pcbXMPMetadata);
* - ERR PKImageDecode_GetEXIFMetadata_WMP(PKImageDecode *pID, U8 *pbEXIFMetadata, U32 *pcbEXIFMetadata);
* - ERR PKImageDecode_GetGPSInfoMetadata_WMP(PKImageDecode *pID, U8 *pbGPSInfoMetadata, U32 *pcbGPSInfoMetadata);
* - ERR PKImageDecode_GetIPTCNAAMetadata_WMP(PKImageDecode *pID, U8 *pbIPTCNAAMetadata, U32 *pcbIPTCNAAMetadata);
* - ERR PKImageDecode_GetPhotoshopMetadata_WMP(PKImageDecode *pID, U8 *pbPhotoshopMetadata, U32 *pcbPhotoshopMetadata);
*
* As a result, their use is disabled by default. It is possible to activate their use by defining the
* JXR_ENABLE_ADVANCED_METADATA preprocessor directive
*/
// #define JXR_ENABLE_ADVANCED_METADATA
#endif
/* *** JXR_MAX_IMAGE_WIDTH and JXR_MAX_IMAGE_HEIGHT ***
* The maximum size in pixel allowed by the plugin.
@@ -89,13 +87,25 @@ Q_LOGGING_CATEGORY(LOG_JXRPLUGIN, "kf.imageformats.plugins.jxr", QtWarningMsg)
#define JXR_MAX_IMAGE_HEIGHT JXR_MAX_IMAGE_WIDTH
#endif
#ifndef JXR_MAX_METADATA_SIZE
/*!
* XMP and EXIF maximum size.
* \brief JXR_MAX_METADATA_SIZE
*
* XMP and EXIF maximum size in bytes.
*/
#ifndef JXR_MAX_METADATA_SIZE
#define JXR_MAX_METADATA_SIZE (4 * 1024 * 1024)
#endif
/*
* Compatibility with older libraries
*/
#ifndef JXR_MAKEVERSION
#define JXR_MAKEVERSION(major, minor, patch) (((major) << 16) | ((minor) << 8) | (patch))
#endif
#ifndef JXR_VERSION
#define JXR_VERSION JXR_MAKEVERSION(1, 1, 0)
#endif
class JXRHandlerPrivate : public QSharedData
{
private:
@@ -461,7 +471,7 @@ public:
if (pDecoder == nullptr) {
return xmp;
}
#ifdef JXR_ENABLE_ADVANCED_METADATA
#if JXR_VERSION >= JXR_MAKEVERSION(1, 4, 0)
quint32 size = 0;
if (!PKImageDecode_GetXMPMetadata_WMP(pDecoder, nullptr, &size) && size > 0 && size < JXR_MAX_METADATA_SIZE) {
QByteArray ba(size, 0);
@@ -963,6 +973,12 @@ private:
if (pCodecFactory == nullptr) {
return false;
}
#if JXR_VERSION >= JXR_MAKEVERSION(1, 4, 0)
// Prevents the library from making single large memory allocations.
// Note that it may still exceed it with multiple allocations.
PKAlloc_SetLimit(size_t(QImageReader::allocationLimit()) * 1024 * 1024);
#endif
if (auto err = pCodecFactory->CreateDecoderFromFile(qUtf8Printable(fileName()), &pDecoder)) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::initDecoder() unable to create decoder:" << err;
return false;
@@ -996,7 +1012,7 @@ private:
return true;
}
DESCRIPTIVEMETADATA meta;
DESCRIPTIVEMETADATA meta = {};
if (pDecoder->GetDescriptiveMetadata(pDecoder, &meta)) {
return false;
}
@@ -1031,7 +1047,7 @@ bool JXRHandler::read(QImage *outImage)
PKPixelFormatGUID convFmt;
auto imageFmt = d->imageFormat(&convFmt);
auto img = imageAlloc(d->imageSize(), imageFmt);
auto img = imageAlloc(d->imageSize(), imageFmt, ImageInitToZero::FPOnly);
if (img.isNull()) {
return false;
}

View File

@@ -744,7 +744,42 @@ void MicroExif::setOrientation(quint16 orient)
QImageIOHandler::Transformation MicroExif::transformation() const
{
switch (orientation()) {
return orientationToTransformation(orientation());
}
void MicroExif::setTransformation(const QImageIOHandler::Transformation &t)
{
setOrientation(transformationToOrientation(t));
}
quint16 MicroExif::transformationToOrientation(const QImageIOHandler::Transformation &t)
{
switch (t) {
case QImageIOHandler::TransformationNone:
return 1;
case QImageIOHandler::TransformationMirror:
return 2;
case QImageIOHandler::TransformationRotate180:
return 3;
case QImageIOHandler::TransformationFlip:
return 4;
case QImageIOHandler::TransformationFlipAndRotate90:
return 5;
case QImageIOHandler::TransformationRotate90:
return 6;
case QImageIOHandler::TransformationMirrorAndRotate90:
return 7;
case QImageIOHandler::TransformationRotate270:
return 8;
default:
break;
}
return 0; // no orientation set
}
QImageIOHandler::Transformation MicroExif::orientationToTransformation(quint16 o)
{
switch (o) {
case 1:
return QImageIOHandler::TransformationNone;
case 2:
@@ -767,39 +802,6 @@ QImageIOHandler::Transformation MicroExif::transformation() const
return QImageIOHandler::TransformationNone;
}
void MicroExif::setTransformation(const QImageIOHandler::Transformation &t)
{
switch (t) {
case QImageIOHandler::TransformationNone:
setOrientation(1);
break;
case QImageIOHandler::TransformationMirror:
setOrientation(2);
break;
case QImageIOHandler::TransformationRotate180:
setOrientation(3);
break;
case QImageIOHandler::TransformationFlip:
setOrientation(4);
break;
case QImageIOHandler::TransformationFlipAndRotate90:
setOrientation(5);
break;
case QImageIOHandler::TransformationRotate90:
setOrientation(6);
break;
case QImageIOHandler::TransformationMirrorAndRotate90:
setOrientation(7);
break;
case QImageIOHandler::TransformationRotate270:
setOrientation(8);
break;
default:
break;
}
setOrientation(0); // no orientation set
}
QString MicroExif::software() const
{
return tiffString(TIFF_SOFTWARE);

View File

@@ -210,6 +210,19 @@ public:
QImageIOHandler::Transformation transformation() const;
void setTransformation(const QImageIOHandler::Transformation& t);
/*!
* \brief transformationToOrientation
* \param t The Qt transformation.
* \return The EXIF orientation value or 0 if none.
*/
static quint16 transformationToOrientation(const QImageIOHandler::Transformation& t);
/*!
* \brief orientationToTransformation
* \param o The EXIF orientation.
* \return The orientation converted in the equivalent Qt transformation.
*/
static QImageIOHandler::Transformation orientationToTransformation(quint16 o);
/*!
* \brief software
* \return Name and version number of the software package(s) used to create the image.

View File

@@ -65,11 +65,11 @@ public:
inline int width() const
{
return (XMax - XMin) + 1;
return int(XMax - XMin) + 1;
}
inline int height() const
{
return (YMax - YMin) + 1;
return int(YMax - YMin) + 1;
}
inline bool isCompressed() const
{

View File

@@ -728,7 +728,7 @@ static bool IsValid(const PSDHeader &header)
}
// Specs tells: "Supported range is 1 to 56" but when the alpha channel is present the limit is 57:
// Photoshop does not make you add more (see also 53alphas.psd test case).
if (header.channel_count < 1 || header.channel_count > 57) {
if (header.channel_count < 1 || header.channel_count > std::min(57, KIF_MAX_IMAGE_CHANNELS)) {
qCDebug(LOG_PSDPLUGIN) << "PSD header: invalid number of channels" << header.channel_count;
return false;
}

View File

@@ -105,7 +105,7 @@ static bool IsSupported(const QoiHeader &head)
return false;
}
// Check if the header is a valid QOI header
if (head.Width == 0 || head.Height == 0 || head.Channels < 3 || head.Colorspace > 1) {
if (head.Width == 0 || head.Height == 0 || head.Channels < 3 || head.Channels > 4 || head.Colorspace > 1) {
return false;
}
// Set a reasonable upper limit

View File

@@ -35,7 +35,7 @@
* The maximum size in pixel allowed by the plugin.
*/
#ifndef RAW_MAX_IMAGE_WIDTH
#define RAW_MAX_IMAGE_WIDTH std::min(65535, KIF_LARGE_IMAGE_PIXEL_LIMIT)
#define RAW_MAX_IMAGE_WIDTH KIF_64K_IMAGE_PIXEL_LIMIT
#endif
#ifndef RAW_MAX_IMAGE_HEIGHT
#define RAW_MAX_IMAGE_HEIGHT RAW_MAX_IMAGE_WIDTH
@@ -89,12 +89,14 @@ const auto supported_formats = QSet<QByteArray>{
* \brief rawImageSize
* \return The size in pixels of the RAW image.
*/
static QSize rawImageSize(LibRaw *rawProcessor)
static QSize rawImageSize(LibRaw *rawProcessor, qint32 *bytesPerPixel = nullptr)
{
auto w = libraw_get_iwidth(&rawProcessor->imgdata);
auto h = libraw_get_iheight(&rawProcessor->imgdata);
// flip & 4: taken from LibRaw code
return (rawProcessor->imgdata.sizes.flip & 4) ? QSize(h, w) : QSize(w, h);
int w = 0, h = 0, c = 0, b = 0;
rawProcessor->get_mem_image_format(&w, &h, &c, &b);
if (bytesPerPixel) {
*bytesPerPixel = std::max(1, b * c / 8);
}
return QSize(w, h);
}
inline int raw_scanf_one(const QByteArray &ba, const char *fmt, void *val)
@@ -381,9 +383,9 @@ QString createTag(libraw_gps_info_t gps, const char *tag)
if (gps.latref != '\0') {
auto lc = QLocale::c();
auto value = QStringLiteral("%1,%2%3")
.arg(lc.toString(gps.latitude[0], 'f', 0))
.arg(lc.toString(gps.latitude[1] + gps.latitude[2] / 60, 'f', 4))
.arg(QChar::fromLatin1(gps.latref));
.arg(lc.toString(gps.latitude[0], 'f', 0),
lc.toString(gps.latitude[1] + gps.latitude[2] / 60, 'f', 4),
QChar::fromLatin1(gps.latref));
return createTag(value, tag);
}
}
@@ -391,9 +393,9 @@ QString createTag(libraw_gps_info_t gps, const char *tag)
if (gps.longref != '\0') {
auto lc = QLocale::c();
auto value = QStringLiteral("%1,%2%3")
.arg(lc.toString(gps.longitude[0], 'f', 0))
.arg(lc.toString(gps.longitude[1] + gps.longitude[2] / 60, 'f', 4))
.arg(QChar::fromLatin1(gps.longref));
.arg(lc.toString(gps.longitude[0], 'f', 0),
lc.toString(gps.longitude[1] + gps.longitude[2] / 60, 'f', 4),
QChar::fromLatin1(gps.longref));
return createTag(value, tag);
}
}
@@ -688,7 +690,7 @@ bool LoadTHUMB(QImageIOHandler *handler, QImage &img)
return false;
}
#else
auto all = device->readAll();
auto all = deviceRead(device(), kMaxQVectorSize);
if (rawProcessor->open_buffer(all.data(), all.size()) != LIBRAW_SUCCESS) {
return false;
}
@@ -751,16 +753,21 @@ bool LoadRAW(QImageIOHandler *handler, QImage &img)
return false;
}
#else
auto ba = device->readAll();
auto ba = deviceRead(device(), kMaxQVectorSize);
if (rawProcessor->open_buffer(ba.data(), ba.size()) != LIBRAW_SUCCESS) {
return false;
}
#endif
// *** Limiting the maximum image size on a reasonable size
auto size = rawImageSize(rawProcessor.get());
if (size.width() > RAW_MAX_IMAGE_WIDTH || size.height() > RAW_MAX_IMAGE_HEIGHT) {
qCWarning(LOG_RAWPLUGIN) << "The maximum image size is limited to" << RAW_MAX_IMAGE_WIDTH << "x" << RAW_MAX_IMAGE_HEIGHT << "px";
qint32 bytesPerPixel = 0;
auto size = rawImageSize(rawProcessor.get(), &bytesPerPixel);
if (size.width() >= RAW_MAX_IMAGE_WIDTH || size.height() >= RAW_MAX_IMAGE_HEIGHT) {
qCWarning(LOG_RAWPLUGIN) << "The maximum image size is limited to" << (RAW_MAX_IMAGE_WIDTH - 1) << "x" << (RAW_MAX_IMAGE_HEIGHT - 1) << "px";
return false;
}
if (!checkImageSize(size, bytesPerPixel)) {
qCWarning(LOG_RAWPLUGIN) << "Rejecting image as it exceeds the current allocation limit.";
return false;
}
@@ -1056,7 +1063,7 @@ bool RAWHandler::canRead(QIODevice *device)
LibRaw_QIODevice stream(device);
auto ok = rawProcessor->open_datastream(&stream) == LIBRAW_SUCCESS;
#else
auto ba = device->readAll();
auto ba = deviceRead(device(), kMaxQVectorSize);
auto ok = rawProcessor->open_buffer(ba.data(), ba.size()) == LIBRAW_SUCCESS;
#endif

View File

@@ -300,21 +300,17 @@ bool SGIImagePrivate::readImage(QImage &img)
return false;
}
if (_zsize > KIF_MAX_IMAGE_CHANNELS) {
qCDebug(LOG_RGBPLUGIN) << "Too many channels: the plugin is limited to" << KIF_MAX_IMAGE_CHANNELS << "channels";
return false;
}
img = imageAlloc(size(), format());
if (img.isNull()) {
qCWarning(LOG_RGBPLUGIN) << "Failed to allocate image, invalid dimensions?" << QSize(_xsize, _ysize);
return false;
}
if (_zsize > 4) {
// qCDebug(LOG_RGBPLUGIN) << "using first 4 of " << _zsize << " channels";
// Only let this continue if it won't cause a int overflow later
// this is most likely a broken file anyway
if (_ysize > std::numeric_limits<int>::max() / _zsize) {
return false;
}
}
_numrows = _ysize * _zsize;
if (_rle) {
@@ -353,7 +349,7 @@ bool SGIImagePrivate::readImage(QImage &img)
return false;
}
_data = _dev->readAll();
_data = deviceRead(_dev, kMaxQVectorSize);
// sanity check
if (_rle) {

View File

@@ -12,13 +12,28 @@
#include <QImage>
#include <QImageIOHandler>
#include <QImageReader>
#include <QIODevice>
#include <QPixelFormat>
// Default maximum number of channels (do not exceed 256).
#ifndef KIF_MAX_IMAGE_CHANNELS
#define KIF_MAX_IMAGE_CHANNELS 60
#endif
// Default maximum width and height for the large image plugins.
#ifndef KIF_LARGE_IMAGE_PIXEL_LIMIT
#define KIF_LARGE_IMAGE_PIXEL_LIMIT 300000
#endif
// Maximum size for legacy image formats.
#define KIF_64K_IMAGE_PIXEL_LIMIT 65536
#if KIF_LARGE_IMAGE_PIXEL_LIMIT < KIF_64K_IMAGE_PIXEL_LIMIT
#undef KIF_LARGE_IMAGE_PIXEL_LIMIT
#define KIF_LARGE_IMAGE_PIXEL_LIMIT KIF_64K_IMAGE_PIXEL_LIMIT
#endif
// Image metadata keys to use in plugins (so they are consistent)
#define META_KEY_ALTITUDE "Altitude"
#define META_KEY_AUTHOR "Author"
@@ -63,20 +78,89 @@
// QList uses some extra space for stuff, hence the 32 here suggested by Thiago Macieira
static constexpr int kMaxQVectorSize = std::numeric_limits<int>::max() - 32;
// On Qt 6 to make the plugins fail to allocate if the image size is greater than QImageReader::allocationLimit()
// it is necessary to allocate the image with QImageIOHandler::allocateImage().
inline QImage imageAlloc(const QSize &size, const QImage::Format &format)
/*!
* \brief The ImageInitToZero enum
*/
enum class ImageInitToZero
{
None, /**< Do not initialize the image. */
All, /**< Initialize all images. */
PremulOnly, /**< Initialize only images with premultiplied alpha. */
FPOnly, /**< Initialize only images with floating point format. */
FPAndPremul /**< Initialize floating point and premultiplied formats. */
};
/*!
* \brief imageAlloc
* Helper function to initialize framework images.
* \param size The image size.
* \param format The image format.
* \param init Whether and which images should be initialized to zero.
* \return The allocated image or a null image on error.
*/
inline QImage imageAlloc(const QSize &size, const QImage::Format &format, const ImageInitToZero& init = ImageInitToZero::None)
{
QImage img;
if (!QImageIOHandler::allocateImage(size, format, &img)) {
img = QImage(); // paranoia
}
if (init != ImageInitToZero::None && !img.isNull()) {
auto pixelFormat = img.pixelFormat();
auto isFloat = pixelFormat.typeInterpretation() == QPixelFormat::FloatingPoint;
auto isPremul = pixelFormat.premultiplied();
if (init == ImageInitToZero::All) {
img.fill(Qt::black);
} else if (isFloat && (init == ImageInitToZero::FPOnly || init == ImageInitToZero::FPAndPremul)) {
img.fill(Qt::black);
} else if (isPremul && (init == ImageInitToZero::PremulOnly || init == ImageInitToZero::FPAndPremul)) {
img.fill(Qt::black);
}
}
return img;
}
inline QImage imageAlloc(qint32 width, qint32 height, const QImage::Format &format)
/*!
* \brief imageAlloc
* Helper function to initialize framework images.
* \param width The image width.
* \param height The image height.
* \param format The image format.
* \param init Whether and which images should be initialized to zero.
* \return The allocated image or a null image on error.
*/
inline QImage imageAlloc(qint32 width, qint32 height, const QImage::Format &format, const ImageInitToZero& init = ImageInitToZero::None)
{
return imageAlloc(QSize(width, height), format);
return imageAlloc(QSize(width, height), format, init);
}
/*!
* \brief checkImageSize
* Helper function to make sure the image size does not exceed the limit set in Qt.
* \param width The image width.
* \param height The image height.
* \param bytesPerPixel The number of bytes for each pixel of the image.
* \return True if the limit is respected, false otherwise.
*/
inline bool checkImageSize(qint32 width, qint32 height, qint32 bytesPerPixel)
{
size_t maxBytes = size_t(QImageReader::allocationLimit()) * 1024 * 1024;
if (maxBytes == 0) {
return true;
}
size_t bytes = size_t(width) * height * bytesPerPixel;
return bytes <= maxBytes;
}
/*!
* \brief checkImageSize
* Helper function to make sure the image size does not exceed the limit set in Qt.
* \param size The image size.
* \param bytesPerPixel The number of bytes for each pixel of the image.
* \return True if the limit is respected, false otherwise.
*/
inline bool checkImageSize(const QSize& size, qint32 bytesPerPixel)
{
return checkImageSize(size.width(), size.height(), bytesPerPixel);
}
template<class TI, class SF> // SF = source FP, TI = target INT
@@ -160,7 +244,7 @@ static QByteArray deviceRead(QIODevice *d, qint64 maxSize)
return{};
}
const qint64 blockSize = 32 * 1024 * 1024;
const qint64 blockSize = 1024 * 1024;
auto devSize = d->isSequential() ? qint64() : d->size();
if (devSize > 0) {

View File

@@ -12,16 +12,15 @@
#include <QColorSpace>
#include <QIODevice>
#include <QImage>
#include <QImageReader>
#include <QList>
#include <QLoggingCategory>
#include <QPainter>
#include <QStack>
#include <QtEndian>
#ifndef XCF_QT5_SUPPORT
// Float images are not supported by Qt 5 and can be disabled in QT 6 to reduce memory usage.
// Unfortunately enabling/disabling this define results in slightly different images, so leave the default if possible.
// Float images can be disabled to reduce memory usage.
// Unfortunately enabling/disabling this define results in slightly different images,
// so leave the default if possible.
#define USE_FLOAT_IMAGES // default uncommented
// Let's set a "reasonable" maximum size
@@ -31,11 +30,6 @@
#ifndef XCF_MAX_IMAGE_HEIGHT
#define XCF_MAX_IMAGE_HEIGHT XCF_MAX_IMAGE_WIDTH
#endif
#else
// While it is possible to have images larger than 32767 pixels, QPainter seems unable to go beyond this threshold using Qt 5.
#define XCF_MAX_IMAGE_WIDTH 32767
#define XCF_MAX_IMAGE_HEIGHT 32767
#endif
#ifdef USE_FLOAT_IMAGES
#include <qrgbafloat.h>
@@ -1384,20 +1378,14 @@ bool XCFImageFormat::composeTiles(XCFImage &xcf_image)
}
}
#ifndef XCF_QT5_SUPPORT
// Qt 6 image allocation limit calculation: we have to check the limit here because the image is split in
// tiles of 64x64 pixels. The required memory to build the image is at least doubled because tiles are loaded
// The required memory to build the image is at least doubled because tiles are loaded
// and then the final image is created by copying the tiles inside it.
// NOTE: on Windows to open a 10GiB image the plugin uses 28GiB of RAM
const qint64 channels = 1 + (layer.type == RGB_GIMAGE ? 2 : 0) + (layer.type == RGBA_GIMAGE ? 3 : 0);
const int allocationLimit = QImageReader::allocationLimit();
if (allocationLimit > 0) {
if (qint64(layer.width) * qint64(layer.height) * channels * 2ll / 1024ll / 1024ll > allocationLimit) {
qCDebug(XCFPLUGIN) << "Rejecting image as it exceeds the current allocation limit of" << allocationLimit << "megabytes";
return false;
}
if (!checkImageSize(layer.width, layer.height, channels * 2)) {
qCDebug(XCFPLUGIN) << "Rejecting image as it exceeds the current allocation limit.";
return false;
}
#endif
layer.image_tiles.resize(layer.nrows);