Compare commits

...

6 Commits

Author SHA1 Message Date
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
13 changed files with 216 additions and 74 deletions

View File

@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.29)
set(KF_VERSION "6.27.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})
@@ -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.26.0
KF 6.27.0
)
add_subdirectory(src)

View File

@@ -157,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();
@@ -1181,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

@@ -3756,9 +3756,7 @@ qint32 DPELChunk::count() const
return 0;
}
auto cnt = i32(data(), 0);
if (cnt < 0 || cnt > 128) {
// an image should have 3, 4 or 5 channels:
// 128 is enough to give an error.
if (cnt < 0 || cnt > KIF_MAX_IMAGE_CHANNELS) {
cnt = 0;
}
return cnt;

View File

@@ -14,6 +14,7 @@
#include <libheif/heif_properties.h>
#include <QColorSpace>
#include <QImageReader>
#include <QLoggingCategory>
#include <QPointF>
#include <QSysInfo>
@@ -64,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)
{
@@ -621,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;

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

@@ -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

@@ -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,18 +753,23 @@ 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());
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;
}
// *** Unpacking selected image
if (rawProcessor->unpack() != LIBRAW_SUCCESS) {
@@ -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,9 +12,15 @@
#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
@@ -88,7 +94,7 @@ enum class ImageInitToZero
* \brief imageAlloc
* Helper function to initialize framework images.
* \param size The image size.
* \param format The image format,
* \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.
*/
@@ -103,21 +109,60 @@ inline QImage imageAlloc(const QSize &size, const QImage::Format &format, const
auto isFloat = pixelFormat.typeInterpretation() == QPixelFormat::FloatingPoint;
auto isPremul = pixelFormat.premultiplied();
if (init == ImageInitToZero::All) {
img.fill(0);
img.fill(Qt::black);
} else if (isFloat && (init == ImageInitToZero::FPOnly || init == ImageInitToZero::FPAndPremul)) {
img.fill(0);
img.fill(Qt::black);
} else if (isPremul && (init == ImageInitToZero::PremulOnly || init == ImageInitToZero::FPAndPremul)) {
img.fill(0);
img.fill(Qt::black);
}
}
return img;
}
/*!
* \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, 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
TI qRoundOrZero_T(SF d, bool *ok = nullptr)
{
@@ -199,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);