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) 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 set(KF_DEP_VERSION "6.27.0") # handled by release scripts
project(KImageFormats VERSION ${KF_VERSION}) 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( ecm_set_disabled_deprecation_versions(
QT 6.11.0 QT 6.11.0
KF 6.26.0 KF 6.27.0
) )
add_subdirectory(src) add_subdirectory(src)

View File

@@ -157,7 +157,7 @@ bool QAVIFHandler::ensureDecoder()
return true; return true;
} }
m_rawData = device()->readAll(); m_rawData = deviceRead(device(), kMaxQVectorSize);
m_rawAvifData.data = reinterpret_cast<const uint8_t *>(m_rawData.constData()); m_rawAvifData.data = reinterpret_cast<const uint8_t *>(m_rawData.constData());
m_rawAvifData.size = m_rawData.size(); m_rawAvifData.size = m_rawData.size();
@@ -1181,7 +1181,7 @@ bool QAVIFHandler::jumpToNextImage()
if (m_decoder->imageIndex >= 0) { if (m_decoder->imageIndex >= 0) {
if (m_decoder->imageCount < 2) { if (m_decoder->imageCount < 2) {
m_parseState = ParseAvifSuccess; m_parseState = ParseAvifSuccess;
return true; return false;
} }
if (m_decoder->imageIndex >= m_decoder->imageCount - 1) { // start from beginning if (m_decoder->imageIndex >= m_decoder->imageCount - 1) { // start from beginning

View File

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

View File

@@ -14,6 +14,7 @@
#include <libheif/heif_properties.h> #include <libheif/heif_properties.h>
#include <QColorSpace> #include <QColorSpace>
#include <QImageReader>
#include <QLoggingCategory> #include <QLoggingCategory>
#include <QPointF> #include <QPointF>
#include <QSysInfo> #include <QSysInfo>
@@ -64,6 +65,58 @@ bool HEIFHandler::m_hej2_decoder_available = false;
bool HEIFHandler::m_hej2_encoder_available = false; bool HEIFHandler::m_hej2_encoder_available = false;
bool HEIFHandler::m_avci_decoder_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" { extern "C" {
static struct heif_error heifhandler_write_callback(struct heif_context * /* ctx */, const void *data, size_t size, void *userdata) 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; 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)) { if (!HEIFHandler::isSupportedBMFFType(buffer) && !HEIFHandler::isSupportedHEJ2(buffer) && !HEIFHandler::isSupportedAVCI(buffer)) {
m_parseState = ParseHeicError; m_parseState = ParseHeicError;
return false; return false;
} }
struct heif_context *ctx = heif_context_alloc(); 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) { 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); heif_context_free(ctx);
m_parseState = ParseHeicError; m_parseState = ParseHeicError;
return false; return false;

View File

@@ -12,7 +12,6 @@
#include <QColorSpace> #include <QColorSpace>
#include <QIODevice> #include <QIODevice>
#include <QImage> #include <QImage>
#include <QImageReader>
#include <QLoggingCategory> #include <QLoggingCategory>
#include <QThread> #include <QThread>
@@ -187,7 +186,7 @@ public:
bool isImageValid(const opj_image_t *i) const 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 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 // OpenJPEG uses a shadow copy @32-bit/channel so we need to do a check
const int allocationLimit = QImageReader::allocationLimit(); if (!checkImageSize(width, height, nchannels * 4)) {
if (allocationLimit > 0) { qCCritical(LOG_JP2PLUGIN) << "Rejecting image as it exceeds the current allocation limit.";
auto maxBytes = qint64(allocationLimit) * 1024 * 1024; return false;
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;
}
} }
return true; return true;
@@ -384,6 +377,11 @@ public:
if (isImageValid(m_jp2_image)) { if (isImageValid(m_jp2_image)) {
auto &&c0 = m_jp2_image->comps[0]; auto &&c0 = m_jp2_image->comps[0];
auto tmp = QSize(c0.w, c0.h); 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)) if (checkSizeLimits(tmp, m_jp2_image->numcomps))
sz = tmp; sz = tmp;
} }

View File

@@ -16,6 +16,7 @@
#include <jxl/cms.h> #include <jxl/cms.h>
#include <jxl/encode.h> #include <jxl/encode.h>
#include <jxl/memory_manager.h>
#include <jxl/thread_parallel_runner.h> #include <jxl/thread_parallel_runner.h>
#include <string.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 #define MAX_IMAGE_PIXELS FEATURE_LEVEL_5_PIXELS
#endif #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() QJpegXLHandler::QJpegXLHandler()
: m_parseState(ParseJpegXLNotParsed) : m_parseState(ParseJpegXLNotParsed)
, m_quality(90) , m_quality(90)
@@ -70,6 +86,7 @@ QJpegXLHandler::QJpegXLHandler()
, m_alpha_channel_id(0) , m_alpha_channel_id(0)
, m_input_image_format(QImage::Format_Invalid) , m_input_image_format(QImage::Format_Invalid)
, m_target_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; return true;
} }
m_rawData = device()->readAll(); m_rawData = deviceRead(device(), kMaxQVectorSize);
if (m_rawData.isEmpty()) { if (m_rawData.isEmpty()) {
return false; return false;
@@ -165,7 +182,14 @@ bool QJpegXLHandler::ensureDecoder()
return false; 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) { if (!m_decoder) {
qCWarning(LOG_JXLPLUGIN, "ERROR: JxlDecoderCreate failed"); qCWarning(LOG_JXLPLUGIN, "ERROR: JxlDecoderCreate failed");
m_parseState = ParseJpegXLError; m_parseState = ParseJpegXLError;

View File

@@ -89,6 +89,8 @@ private:
QImage::Format m_target_image_format; QImage::Format m_target_image_format;
JxlPixelFormat m_input_pixel_format; JxlPixelFormat m_input_pixel_format;
size_t m_maxBytes;
}; };
class QJpegXLPlugin : public QImageIOPlugin 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: // 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). // 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; qCDebug(LOG_PSDPLUGIN) << "PSD header: invalid number of channels" << header.channel_count;
return false; return false;
} }

View File

@@ -105,7 +105,7 @@ static bool IsSupported(const QoiHeader &head)
return false; return false;
} }
// Check if the header is a valid QOI header // 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; return false;
} }
// Set a reasonable upper limit // Set a reasonable upper limit

View File

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

View File

@@ -300,21 +300,17 @@ bool SGIImagePrivate::readImage(QImage &img)
return false; 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()); img = imageAlloc(size(), format());
if (img.isNull()) { if (img.isNull()) {
qCWarning(LOG_RGBPLUGIN) << "Failed to allocate image, invalid dimensions?" << QSize(_xsize, _ysize); qCWarning(LOG_RGBPLUGIN) << "Failed to allocate image, invalid dimensions?" << QSize(_xsize, _ysize);
return false; 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; _numrows = _ysize * _zsize;
if (_rle) { if (_rle) {
@@ -353,7 +349,7 @@ bool SGIImagePrivate::readImage(QImage &img)
return false; return false;
} }
_data = _dev->readAll(); _data = deviceRead(_dev, kMaxQVectorSize);
// sanity check // sanity check
if (_rle) { if (_rle) {

View File

@@ -12,9 +12,15 @@
#include <QImage> #include <QImage>
#include <QImageIOHandler> #include <QImageIOHandler>
#include <QImageReader>
#include <QIODevice> #include <QIODevice>
#include <QPixelFormat> #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. // Default maximum width and height for the large image plugins.
#ifndef KIF_LARGE_IMAGE_PIXEL_LIMIT #ifndef KIF_LARGE_IMAGE_PIXEL_LIMIT
#define KIF_LARGE_IMAGE_PIXEL_LIMIT 300000 #define KIF_LARGE_IMAGE_PIXEL_LIMIT 300000
@@ -88,7 +94,7 @@ enum class ImageInitToZero
* \brief imageAlloc * \brief imageAlloc
* Helper function to initialize framework images. * Helper function to initialize framework images.
* \param size The image size. * \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. * \param init Whether and which images should be initialized to zero.
* \return The allocated image or a null image on error. * \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 isFloat = pixelFormat.typeInterpretation() == QPixelFormat::FloatingPoint;
auto isPremul = pixelFormat.premultiplied(); auto isPremul = pixelFormat.premultiplied();
if (init == ImageInitToZero::All) { if (init == ImageInitToZero::All) {
img.fill(0); img.fill(Qt::black);
} else if (isFloat && (init == ImageInitToZero::FPOnly || init == ImageInitToZero::FPAndPremul)) { } 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)) { } else if (isPremul && (init == ImageInitToZero::PremulOnly || init == ImageInitToZero::FPAndPremul)) {
img.fill(0); img.fill(Qt::black);
} }
} }
return img; 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) inline QImage imageAlloc(qint32 width, qint32 height, const QImage::Format &format, const ImageInitToZero& init = ImageInitToZero::None)
{ {
return imageAlloc(QSize(width, height), format, init); 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 template<class TI, class SF> // SF = source FP, TI = target INT
TI qRoundOrZero_T(SF d, bool *ok = nullptr) TI qRoundOrZero_T(SF d, bool *ok = nullptr)
{ {
@@ -199,7 +244,7 @@ static QByteArray deviceRead(QIODevice *d, qint64 maxSize)
return{}; return{};
} }
const qint64 blockSize = 32 * 1024 * 1024; const qint64 blockSize = 1024 * 1024;
auto devSize = d->isSequential() ? qint64() : d->size(); auto devSize = d->isSequential() ? qint64() : d->size();
if (devSize > 0) { if (devSize > 0) {

View File

@@ -12,16 +12,15 @@
#include <QColorSpace> #include <QColorSpace>
#include <QIODevice> #include <QIODevice>
#include <QImage> #include <QImage>
#include <QImageReader>
#include <QList> #include <QList>
#include <QLoggingCategory> #include <QLoggingCategory>
#include <QPainter> #include <QPainter>
#include <QStack> #include <QStack>
#include <QtEndian> #include <QtEndian>
#ifndef XCF_QT5_SUPPORT // Float images can be disabled to reduce memory usage.
// 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,
// Unfortunately enabling/disabling this define results in slightly different images, so leave the default if possible. // so leave the default if possible.
#define USE_FLOAT_IMAGES // default uncommented #define USE_FLOAT_IMAGES // default uncommented
// Let's set a "reasonable" maximum size // Let's set a "reasonable" maximum size
@@ -31,11 +30,6 @@
#ifndef XCF_MAX_IMAGE_HEIGHT #ifndef XCF_MAX_IMAGE_HEIGHT
#define XCF_MAX_IMAGE_HEIGHT XCF_MAX_IMAGE_WIDTH #define XCF_MAX_IMAGE_HEIGHT XCF_MAX_IMAGE_WIDTH
#endif #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 #ifdef USE_FLOAT_IMAGES
#include <qrgbafloat.h> #include <qrgbafloat.h>
@@ -1384,20 +1378,14 @@ bool XCFImageFormat::composeTiles(XCFImage &xcf_image)
} }
} }
#ifndef XCF_QT5_SUPPORT // The required memory to build the image is at least doubled because tiles are loaded
// 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
// and then the final image is created by copying the tiles inside it. // 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 // 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 qint64 channels = 1 + (layer.type == RGB_GIMAGE ? 2 : 0) + (layer.type == RGBA_GIMAGE ? 3 : 0);
const int allocationLimit = QImageReader::allocationLimit(); if (!checkImageSize(layer.width, layer.height, channels * 2)) {
if (allocationLimit > 0) { qCDebug(XCFPLUGIN) << "Rejecting image as it exceeds the current allocation limit.";
if (qint64(layer.width) * qint64(layer.height) * channels * 2ll / 1024ll / 1024ll > allocationLimit) { return false;
qCDebug(XCFPLUGIN) << "Rejecting image as it exceeds the current allocation limit of" << allocationLimit << "megabytes";
return false;
}
} }
#endif
layer.image_tiles.resize(layer.nrows); layer.image_tiles.resize(layer.nrows);