Compare commits

...
12 Commits
Author SHA1 Message Date
Nicolas Fella 5d6dfb9012 Update dependency version to 6.30.0 2026-09-04 13:26:17 +02:00
Albert Astals Cid 72518dcc1a jp2: Fix new[] vs delete by not doing new[]
unique_ptr will call delete which is not the right thing to do with
memory allocated with new[]

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

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

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

Drop the file instead of walking off the buffer.
2026-08-14 21:03:38 +05:30
Daniel Novomeský c7baa057a6 avif: Do not rewind in jumpToNextImage() 2026-08-10 20:10:46 +02:00
Daniel Novomeský c57af25d36 ossfuzz: call functions used in animations 2026-08-10 17:46:33 +00:00
Nicolas Fella 320ded493a Update version to 6.30.0 2026-08-07 23:07:47 +02:00
31 changed files with 132 additions and 79 deletions
+3 -3
View File
@@ -1,11 +1,11 @@
cmake_minimum_required(VERSION 3.29)
set(KF_VERSION "6.29.0") # handled by release scripts
set(KF_DEP_VERSION "6.29.0") # handled by release scripts
set(KF_VERSION "6.30.0") # handled by release scripts
set(KF_DEP_VERSION "6.30.0") # handled by release scripts
project(KImageFormats VERSION ${KF_VERSION})
include(FeatureSummary)
find_package(ECM 6.29.0 NO_MODULE)
find_package(ECM 6.30.0 NO_MODULE)
set_package_properties(ECM PROPERTIES TYPE REQUIRED DESCRIPTION "Extra CMake Modules." URL "https://commits.kde.org/extra-cmake-modules")
feature_summary(WHAT REQUIRED_PACKAGES_NOT_FOUND FATAL_ON_MISSING_REQUIRED_PACKAGES)
+21 -1
View File
@@ -125,7 +125,27 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
b.open(QIODevice::ReadOnly);
handler->setDevice(&b);
handler->canRead();
handler->read(&i);
bool is_animated = false;
if (handler->supportsOption(QImageIOHandler::Animation)) {
is_animated = handler->option(QImageIOHandler::Animation).toBool();
}
if (is_animated) { // animated image
if (handler->supportsOption(QImageIOHandler::Size)) {
handler->option(QImageIOHandler::Size).toSize();
}
handler->imageCount();
handler->loopCount();
handler->read(&i);
handler->nextImageDelay();
handler->currentImageNumber();
handler->jumpToImage(0);
handler->jumpToNextImage();
} else { // static image
handler->read(&i);
}
delete handler;
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 199 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 242 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 232 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 221 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 222 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 243 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 228 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 244 B

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

After

Width:  |  Height:  |  Size: 362 KiB

+5 -11
View File
@@ -1160,25 +1160,19 @@ bool QAVIFHandler::jumpToNextImage()
return false;
}
avifResult decodeResult;
if (m_decoder->imageIndex >= 0) {
if (m_decoder->imageCount < 2) {
m_parseState = ParseAvifSuccess;
// single image, there is no next image
return false;
}
if (m_decoder->imageIndex >= m_decoder->imageCount - 1) { // start from beginning
decodeResult = avifDecoderReset(m_decoder);
if (decodeResult != AVIF_RESULT_OK) {
qCWarning(LOG_AVIFPLUGIN, "ERROR in avifDecoderReset: %s", avifResultToString(decodeResult));
m_parseState = ParseAvifError;
return false;
}
if (m_decoder->imageIndex >= m_decoder->imageCount - 1) {
// do not start from beginning when at the end already
return false;
}
}
decodeResult = avifDecoderNextImage(m_decoder);
avifResult decodeResult = avifDecoderNextImage(m_decoder);
if (decodeResult != AVIF_RESULT_OK) {
qCWarning(LOG_AVIFPLUGIN, "ERROR: Failed to decode Next image in sequence: %s", avifResultToString(decodeResult));
+2 -1
View File
@@ -368,7 +368,8 @@ static void readMetadata(const Imf::Header &header, QImage &image)
// shot metadata
if (auto isoSpeed = header.findTypedAttribute<Imf::FloatAttribute>("isoSpeed")) {
image.setText(QStringLiteral(META_KEY_ISOSPEEDRATINGS), QLocale::c().toString(qRound(isoSpeed->value())));
if (auto v = qRoundOrZero(isoSpeed->value()))
image.setText(QStringLiteral(META_KEY_ISOSPEEDRATINGS), QLocale::c().toString(v));
}
if (auto expTime = header.findTypedAttribute<Imf::FloatAttribute>("expTime")) {
image.setText(QStringLiteral(META_KEY_EXPOSURETIME), QLocale::c().toString(expTime->value()));
+25 -4
View File
@@ -642,10 +642,31 @@ bool HEIFHandler::read_crop(void *heif_handle, const void *heif_ctx, const QSize
if (heif_item_get_properties_of_type(ctx, item_id, heif_item_property_type_transform_crop, &crop_id, 1) > 0) {
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));
if (l >= size.width() || r >= size.width() || t >= size.height() || b >= size.height()) {
qCWarning(LOG_HEIFPLUGIN) << "Invalid crop values: left=" << l << "top=" << t << "right=" << r << "bottom=" << b;
}
if (l < 0) {
l = 0;
}
if (t < 0) {
t = 0;
}
if (r < 0) {
r = 0;
}
if (b < 0) {
b = 0;
}
if (l == 0 && t == 0 && r == 0 && b == 0) {
// no cropping needed
return false;
}
crop = QRect(QPoint(l, t), size - QSize(l + r, t + b));
return crop.isValid();
}
return crop.isValid();
return false;
}
bool HEIFHandler::isSupportedBMFFType(const QByteArray &header)
@@ -930,8 +951,8 @@ bool HEIFHandler::ensureDecoder()
struct heif_image *img = nullptr;
err = heif_decode_image(handle, &img, heif_colorspace_RGB, chroma, decoder_option);
if (err.code == heif_error_Invalid_input && err.subcode == heif_suberror_Unknown_NCLX_matrix_coefficients && img == nullptr && buffer.contains("Xiaomi")) {
qCWarning(LOG_HEIFPLUGIN) << "Non-standard HEIF image with invalid matrix_coefficients, probably made by a Xiaomi device!";
if (err.code == heif_error_Invalid_input && err.subcode == heif_suberror_Unknown_NCLX_matrix_coefficients && img == nullptr) {
qCWarning(LOG_HEIFPLUGIN) << "Non-standard HEIF image with invalid matrix_coefficients!";
// second try to decode with strict decoding disabled
decoder_option->strict_decoding = 0;
+12 -13
View File
@@ -622,21 +622,20 @@ public:
m_cparameters.tcp_rates[0] = 100.0 - (m_quality < 10 ? m_quality : 10 + (std::log10(m_quality) - 1) * 90);
}
std::unique_ptr<opj_image_cmptparm_t> cmptparm(new opj_image_cmptparm_t[ncomp]);
for (int i = 0; i < ncomp; ++i) {
auto &&p = cmptparm.get() + i;
memset(p, 0, sizeof(opj_image_cmptparm_t));
p->dx = m_cparameters.subsampling_dx;
p->dy = m_cparameters.subsampling_dy;
p->w = image.width();
p->h = image.height();
p->x0 = 0;
p->y0 = 0;
p->prec = prec;
p->sgnd = 0;
std::vector<opj_image_cmptparm_t> cmptparm(ncomp);
for (opj_image_cmptparm_t &p : cmptparm) {
memset(&p, 0, sizeof(opj_image_cmptparm_t));
p.dx = m_cparameters.subsampling_dx;
p.dy = m_cparameters.subsampling_dy;
p.w = image.width();
p.h = image.height();
p.x0 = 0;
p.y0 = 0;
p.prec = prec;
p.sgnd = 0;
}
m_jp2_image = opj_image_create(ncomp, cmptparm.get(), cs);
m_jp2_image = opj_image_create(ncomp, cmptparm.data(), cs);
if (m_jp2_image == nullptr) {
return false;
}
+32 -33
View File
@@ -60,7 +60,7 @@ Q_LOGGING_CATEGORY(LOG_JXLPLUGIN, "kf.imageformats.plugins.jxl", QtWarningMsg)
void *QtJXLMemoryManagerAlloc(void *opaque, size_t size)
{
if (opaque) {
size_t maxBytes = *(size_t*)opaque;
size_t maxBytes = *(size_t *)opaque;
if (maxBytes && size > maxBytes)
return NULL;
}
@@ -75,8 +75,7 @@ void QtJXLMemoryManagerFree(void *, void *address)
QJpegXLHandler::QJpegXLHandler()
: m_parseState(ParseJpegXLNotParsed)
, m_quality(90)
, m_currentimage_index(0)
, m_previousimage_index(-1)
, m_currentimage_index(-1)
, m_transformations(QImageIOHandler::TransformationNone)
, m_decoder(nullptr)
, m_runner(nullptr)
@@ -183,11 +182,7 @@ bool QJpegXLHandler::ensureDecoder()
}
// Creating a simple memory manager
JxlMemoryManager memory_manager = {
.opaque = &m_maxBytes,
.alloc = QtJXLMemoryManagerAlloc,
.free = QtJXLMemoryManagerFree
};
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) {
@@ -817,17 +812,11 @@ bool QJpegXLHandler::decode_one_frame()
exif.updateImageMetadata(m_current_image);
}
m_currentimage_index++;
m_next_image_delay = m_framedelays[m_currentimage_index];
m_previousimage_index = m_currentimage_index;
if (m_framedelays.count() > 1) {
m_currentimage_index++;
if (m_currentimage_index >= m_framedelays.count()) {
if (!rewind()) {
return false;
}
if (m_currentimage_index >= m_framedelays.count() - 1) {
// all frames in animation have been read
m_parseState = ParseJpegXLFinished;
} else {
@@ -847,9 +836,9 @@ bool QJpegXLHandler::read(QImage *image)
return false;
}
if (m_currentimage_index == m_previousimage_index) {
*image = m_current_image;
return jumpToNextImage();
if (m_parseState == ParseJpegXLFinished) {
// at the end already
return false;
}
if (decode_one_frame()) {
@@ -1757,19 +1746,26 @@ bool QJpegXLHandler::jumpToNextImage()
}
if (m_framedelays.count() > 1) {
m_currentimage_index++;
if (m_currentimage_index >= m_framedelays.count()) {
if (!rewind()) {
return false;
}
if (m_currentimage_index >= (m_framedelays.count() - 1)) {
// we are already at the last frame
return false;
} else {
m_currentimage_index++;
m_next_image_delay = m_framedelays[m_currentimage_index];
JxlDecoderSkipFrames(m_decoder, 1);
if (m_currentimage_index >= (m_framedelays.count() - 1)) {
// last frame reached but no more reading is possible
m_parseState = ParseJpegXLFinished;
} else {
m_parseState = ParseJpegXLSuccess;
}
return true;
}
}
m_parseState = ParseJpegXLSuccess;
return true;
// static image, there is no next frame
return false;
}
bool QJpegXLHandler::jumpToImage(int imageNumber)
@@ -1782,14 +1778,17 @@ bool QJpegXLHandler::jumpToImage(int imageNumber)
return false;
}
if (imageNumber == m_currentimage_index) {
const int next_image_index = m_currentimage_index + 1;
if (imageNumber == next_image_index) {
// This the already the next image to read, no action
m_parseState = ParseJpegXLSuccess;
return true;
}
if (imageNumber > m_currentimage_index) {
JxlDecoderSkipFrames(m_decoder, imageNumber - m_currentimage_index);
m_currentimage_index = imageNumber;
if (imageNumber > next_image_index) {
JxlDecoderSkipFrames(m_decoder, imageNumber - next_image_index);
m_currentimage_index = imageNumber - 1;
m_parseState = ParseJpegXLSuccess;
return true;
}
@@ -1801,7 +1800,7 @@ bool QJpegXLHandler::jumpToImage(int imageNumber)
if (imageNumber > 0) {
JxlDecoderSkipFrames(m_decoder, imageNumber);
}
m_currentimage_index = imageNumber;
m_currentimage_index = imageNumber - 1;
m_parseState = ParseJpegXLSuccess;
return true;
}
@@ -1834,7 +1833,7 @@ int QJpegXLHandler::loopCount() const
bool QJpegXLHandler::rewind()
{
m_currentimage_index = 0;
m_currentimage_index = -1;
JxlDecoderReleaseInput(m_decoder);
JxlDecoderRewind(m_decoder);
-1
View File
@@ -65,7 +65,6 @@ private:
ParseJpegXLState m_parseState;
int m_quality;
int m_currentimage_index;
int m_previousimage_index;
QImageIOHandler::Transformations m_transformations;
QByteArray m_rawData;
+9 -3
View File
@@ -314,6 +314,8 @@ bool SGIImagePrivate::readImage(QImage &img)
_numrows = _ysize * _zsize;
if (_rle) {
// start table holds file offsets; raster data follows header + both tables
const quint32 dataOff = 512 + _numrows * 2 * sizeof(quint32);
uint l;
_starttab = new (std::nothrow) quint32[_numrows];
if (_starttab == nullptr) {
@@ -321,7 +323,10 @@ bool SGIImagePrivate::readImage(QImage &img)
}
for (l = 0; !_stream.atEnd() && l < _numrows; l++) {
_stream >> _starttab[l];
_starttab[l] -= 512 + _numrows * 2 * sizeof(quint32);
if (_starttab[l] < dataOff) {
return false;
}
_starttab[l] -= dataOff;
if (_stream.status() != QDataStream::Ok) {
return false;
}
@@ -353,9 +358,10 @@ bool SGIImagePrivate::readImage(QImage &img)
// sanity check
if (_rle) {
const uint dataSize = uint(_data.size());
for (uint o = 0; o < _numrows; o++) {
// don't change to greater-or-equal!
if (_starttab[o] + _lengthtab[o] > (uint)_data.size()) {
// don't add start+length: uint32 wrap would pass a corrupt file
if (_starttab[o] > dataSize || _lengthtab[o] > dataSize - _starttab[o]) {
// qCDebug(LOG_RGBPLUGIN) << "image corrupt (sanity check failed)";
return false;
}
+16 -9
View File
@@ -165,20 +165,27 @@ inline bool checkImageSize(const QSize& size, qint32 bytesPerPixel)
return checkImageSize(size.width(), size.height(), bytesPerPixel);
}
/*!
* \brief qRoundOrZero_T
* In images, many float values can only be positive (e.g., resolution). This function calculates
* the roundness of the passed value, returning 0 if the value is negative or invalid.
* \return 0 when \a d is negative, NaN, Inf or std::numeric_limits<TI>::max(). Otherwise the qRound of \a d.
*/
template<class TI, class SF> // SF = source FP, TI = target INT
TI qRoundOrZero_T(SF d, bool *ok = nullptr)
{
bool tmp = false;
if (ok == nullptr) {
ok = &tmp;
}
// checks for undefined behavior
if (qIsNaN(d) || qIsInf(d) || d < SF() || d > SF(std::numeric_limits<TI>::max())) {
if (ok) {
*ok = false;
}
return 0;
if (qIsNaN(d) || qIsInf(d) || d < SF()) {
*ok = false;
} else {
*ok = d < SF(std::numeric_limits<TI>::max());
}
if (ok) {
*ok = true;
}
return qRound(d);
return *ok ? qRound(d) : 0;
}
inline qint32 qRoundOrZero(double d, bool *ok = nullptr)