avif: enable decoding of files with invalid EXIF metadata

When 1st parsing ends with invalid EXIF payload error,
we try 2nd time again while ignoring the EXIF metadata.

Dropped support for legacy 0.x libavif versions,
libavif 1.0.0+ is now required for the AVIF support.
This commit is contained in:
Daniel Novomeský
2026-07-26 19:11:31 +02:00
parent abc2ac2de1
commit 5c3bdcddf2
6 changed files with 92 additions and 103 deletions

View File

@@ -53,10 +53,7 @@ set_package_properties(OpenEXR PROPERTIES
PURPOSE "Required for the QImage plugin for OpenEXR images" PURPOSE "Required for the QImage plugin for OpenEXR images"
) )
find_package(libavif 0.8.2 CONFIG QUIET)
if(NOT libavif_FOUND)
find_package(libavif 1 CONFIG) find_package(libavif 1 CONFIG)
endif()
set_package_properties(libavif PROPERTIES set_package_properties(libavif PROPERTIES
TYPE OPTIONAL TYPE OPTIONAL
PURPOSE "Required for the QImage plugin for AVIF images" PURPOSE "Required for the QImage plugin for AVIF images"

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

View File

@@ -0,0 +1,7 @@
[
{
"fileName": "invalid-exif.jpg",
"fuzziness": 30,
"description": "Minimum fuzziness value to pass the test on all architectures."
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

View File

@@ -151,6 +151,41 @@ bool QAVIFHandler::ensureOpened() const
return false; return false;
} }
bool QAVIFHandler::initDecoder(bool ignore_exif)
{
if (m_decoder) {
avifDecoderDestroy(m_decoder);
m_decoder = nullptr;
}
m_decoder = avifDecoderCreate();
if (!m_decoder) {
qCWarning(LOG_AVIFPLUGIN, "ERROR: avifDecoderCreate failed");
m_parseState = ParseAvifError;
return false;
}
m_decoder->ignoreExif = ignore_exif ? AVIF_TRUE : AVIF_FALSE;
m_decoder->maxThreads = qBound(1, QThread::idealThreadCount(), 64);
m_decoder->strictFlags = AVIF_STRICT_DISABLED;
m_decoder->imageDimensionLimit = std::max(AVIF_MAX_IMAGE_WIDTH, AVIF_MAX_IMAGE_HEIGHT) - 1;
avifResult decodeResult = avifDecoderSetIOMemory(m_decoder, m_rawAvifData.data, m_rawAvifData.size);
if (decodeResult != AVIF_RESULT_OK) {
qCWarning(LOG_AVIFPLUGIN, "ERROR: avifDecoderSetIOMemory failed: %s", avifResultToString(decodeResult));
avifDecoderDestroy(m_decoder);
m_decoder = nullptr;
m_parseState = ParseAvifError;
return false;
}
return true;
}
bool QAVIFHandler::ensureDecoder() bool QAVIFHandler::ensureDecoder()
{ {
if (m_decoder) { if (m_decoder) {
@@ -167,34 +202,29 @@ bool QAVIFHandler::ensureDecoder()
return false; return false;
} }
m_decoder = avifDecoderCreate(); if (!initDecoder(false)) {
return false;
#if AVIF_VERSION >= 80400 }
m_decoder->maxThreads = qBound(1, QThread::idealThreadCount(), 64);
#endif
#if AVIF_VERSION >= 90100
m_decoder->strictFlags = AVIF_STRICT_DISABLED;
#endif
#if AVIF_VERSION >= 110000
m_decoder->imageDimensionLimit = std::max(AVIF_MAX_IMAGE_WIDTH, AVIF_MAX_IMAGE_HEIGHT) - 1;
#endif
avifResult decodeResult; avifResult decodeResult;
decodeResult = avifDecoderSetIOMemory(m_decoder, m_rawAvifData.data, m_rawAvifData.size); decodeResult = avifDecoderParse(m_decoder);
if (decodeResult != AVIF_RESULT_OK) { if (decodeResult != AVIF_RESULT_OK) {
qCWarning(LOG_AVIFPLUGIN, "ERROR: avifDecoderSetIOMemory failed: %s", avifResultToString(decodeResult)); if (decodeResult == AVIF_RESULT_INVALID_EXIF_PAYLOAD) {
qCWarning(LOG_AVIFPLUGIN, "AVIF image has invalid EXIF metadata!");
// trying to parse input again while ignoring EXIF
if (!initDecoder(true)) {
return false;
}
decodeResult = avifDecoderParse(m_decoder);
if (decodeResult != AVIF_RESULT_OK) {
qCWarning(LOG_AVIFPLUGIN, "ERROR: 2nd avifDecoderParse failed: %s", avifResultToString(decodeResult));
avifDecoderDestroy(m_decoder); avifDecoderDestroy(m_decoder);
m_decoder = nullptr; m_decoder = nullptr;
m_parseState = ParseAvifError; m_parseState = ParseAvifError;
return false; return false;
} }
} else { // first attempt to parse failed
decodeResult = avifDecoderParse(m_decoder);
if (decodeResult != AVIF_RESULT_OK) {
qCWarning(LOG_AVIFPLUGIN, "ERROR: Failed to parse input: %s", avifResultToString(decodeResult)); qCWarning(LOG_AVIFPLUGIN, "ERROR: Failed to parse input: %s", avifResultToString(decodeResult));
avifDecoderDestroy(m_decoder); avifDecoderDestroy(m_decoder);
@@ -202,6 +232,7 @@ bool QAVIFHandler::ensureDecoder()
m_parseState = ParseAvifError; m_parseState = ParseAvifError;
return false; return false;
} }
}
m_container_width = m_decoder->image->width; m_container_width = m_decoder->image->width;
m_container_height = m_decoder->image->height; m_container_height = m_decoder->image->height;
@@ -380,7 +411,8 @@ bool QAVIFHandler::decode_one_frame()
q_trc = QColorSpace::TransferFunction::Hlg; q_trc = QColorSpace::TransferFunction::Hlg;
break; break;
default: default:
qCWarning(LOG_AVIFPLUGIN, "CICP colorPrimaries: %d, transferCharacteristics: %d\nThe colorspace is unsupported by this plug-in yet.", qCWarning(LOG_AVIFPLUGIN,
"CICP colorPrimaries: %d, transferCharacteristics: %d\nThe colorspace is unsupported by this plug-in yet.",
m_decoder->image->colorPrimaries, m_decoder->image->colorPrimaries,
m_decoder->image->transferCharacteristics); m_decoder->image->transferCharacteristics);
q_trc = QColorSpace::TransferFunction::SRgb; q_trc = QColorSpace::TransferFunction::SRgb;
@@ -420,9 +452,7 @@ bool QAVIFHandler::decode_one_frame()
avifRGBImage rgb; avifRGBImage rgb;
avifRGBImageSetDefaults(&rgb, m_decoder->image); avifRGBImageSetDefaults(&rgb, m_decoder->image);
#if AVIF_VERSION >= 1000000
rgb.maxThreads = m_decoder->maxThreads; rgb.maxThreads = m_decoder->maxThreads;
#endif
if (resultdepth > 8) { if (resultdepth > 8) {
rgb.depth = 16; rgb.depth = 16;
@@ -439,12 +469,10 @@ bool QAVIFHandler::decode_one_frame()
rgb.format = AVIF_RGB_FORMAT_ARGB; rgb.format = AVIF_RGB_FORMAT_ARGB;
#endif #endif
#if AVIF_VERSION >= 80400
if (m_decoder->imageCount > 1) { if (m_decoder->imageCount > 1) {
/* accelerate animated AVIF */ /* accelerate animated AVIF */
rgb.chromaUpsampling = AVIF_CHROMA_UPSAMPLING_FASTEST; rgb.chromaUpsampling = AVIF_CHROMA_UPSAMPLING_FASTEST;
} }
#endif
if (loadgray) { if (loadgray) {
resultformat = QImage::Format_Grayscale8; resultformat = QImage::Format_Grayscale8;
@@ -518,11 +546,7 @@ bool QAVIFHandler::decode_one_frame()
} }
if (m_decoder->image->transformFlags & AVIF_TRANSFORM_IMIR) { if (m_decoder->image->transformFlags & AVIF_TRANSFORM_IMIR) {
#if AVIF_VERSION > 90100 && AVIF_VERSION < 1000000
switch (m_decoder->image->imir.mode) {
#else
switch (m_decoder->image->imir.axis) { switch (m_decoder->image->imir.axis) {
#endif
case 0: // top-to-bottom case 0: // top-to-bottom
result = result.flipped(Qt::Vertical); result = result.flipped(Qt::Vertical);
break; break;
@@ -561,25 +585,17 @@ static void setMetadata(avifImage *avif, const QImage& image)
{ {
auto xmp = image.text(QStringLiteral(META_KEY_XMP_ADOBE)).toUtf8(); auto xmp = image.text(QStringLiteral(META_KEY_XMP_ADOBE)).toUtf8();
if (!xmp.isEmpty()) { if (!xmp.isEmpty()) {
#if AVIF_VERSION >= 1000000
auto res = avifImageSetMetadataXMP(avif, reinterpret_cast<const uint8_t *>(xmp.constData()), xmp.size()); auto res = avifImageSetMetadataXMP(avif, reinterpret_cast<const uint8_t *>(xmp.constData()), xmp.size());
if (res != AVIF_RESULT_OK) { if (res != AVIF_RESULT_OK) {
qCWarning(LOG_AVIFPLUGIN, "ERROR in avifImageSetMetadataXMP: %s", avifResultToString(res)); qCWarning(LOG_AVIFPLUGIN, "ERROR in avifImageSetMetadataXMP: %s", avifResultToString(res));
} }
#else
avifImageSetMetadataXMP(avif, reinterpret_cast<const uint8_t *>(xmp.constData()), xmp.size());
#endif
} }
auto exif = MicroExif::fromImage(image).toByteArray(); auto exif = MicroExif::fromImage(image).toByteArray();
if (!exif.isEmpty()) { if (!exif.isEmpty()) {
#if AVIF_VERSION >= 1000000
auto res = avifImageSetMetadataExif(avif, reinterpret_cast<const uint8_t *>(exif.constData()), exif.size()); auto res = avifImageSetMetadataExif(avif, reinterpret_cast<const uint8_t *>(exif.constData()), exif.size());
if (res != AVIF_RESULT_OK) { if (res != AVIF_RESULT_OK) {
qCWarning(LOG_AVIFPLUGIN, "ERROR in avifImageSetMetadataExif: %s", avifResultToString(res)); qCWarning(LOG_AVIFPLUGIN, "ERROR in avifImageSetMetadataExif: %s", avifResultToString(res));
} }
#else
avifImageSetMetadataExif(avif, reinterpret_cast<const uint8_t *>(exif.constData()), exif.size());
#endif
} }
} }
@@ -626,7 +642,10 @@ bool QAVIFHandler::write(const QImage &image)
} }
if ((image.width() > 32768) || (image.height() > 32768)) { if ((image.width() > 32768) || (image.height() > 32768)) {
qCWarning(LOG_AVIFPLUGIN, "Image (%dx%d) has a dimension above 32768 pixels, saved AVIF may not work in other software!", image.width(), image.height()); qCWarning(LOG_AVIFPLUGIN,
"Image (%dx%d) has a dimension above 32768 pixels, saved AVIF may not work in other software!",
image.width(),
image.height());
} }
} else { } else {
qCWarning(LOG_AVIFPLUGIN, "Image has zero dimension!"); qCWarning(LOG_AVIFPLUGIN, "Image has zero dimension!");
@@ -644,7 +663,9 @@ bool QAVIFHandler::write(const QImage &image)
if (avifCodecName(AVIF_CODEC_CHOICE_AOM, AVIF_CODEC_FLAG_CAN_ENCODE)) { if (avifCodecName(AVIF_CODEC_CHOICE_AOM, AVIF_CODEC_FLAG_CAN_ENCODE)) {
lossless = true; lossless = true;
} else { } else {
qCWarning(LOG_AVIFPLUGIN, "You are using %s encoder. It is recommended to enable libAOM encoder in libavif to use lossless compression.", encoder_name); qCWarning(LOG_AVIFPLUGIN,
"You are using %s encoder. It is recommended to enable libAOM encoder in libavif to use lossless compression.",
encoder_name);
} }
} }
@@ -654,11 +675,6 @@ bool QAVIFHandler::write(const QImage &image)
m_quality = KIMG_AVIF_DEFAULT_QUALITY; m_quality = KIMG_AVIF_DEFAULT_QUALITY;
} }
#if AVIF_VERSION < 1000000
int maxQuantizer = AVIF_QUANTIZER_WORST_QUALITY * (100 - qBound(0, m_quality, 100)) / 100;
int minQuantizer = 0;
int maxQuantizerAlpha = 0;
#endif
avifResult res; avifResult res;
bool save_grayscale; // true - monochrome, false - colors bool save_grayscale; // true - monochrome, false - colors
@@ -704,16 +720,6 @@ bool QAVIFHandler::write(const QImage &image)
break; break;
} }
#if AVIF_VERSION < 1000000
// deprecated quality settings
if (maxQuantizer > 20) {
minQuantizer = maxQuantizer - 20;
if (maxQuantizer > 40) { // we decrease quality of alpha channel here
maxQuantizerAlpha = maxQuantizer - 40;
}
}
#endif
if (save_grayscale && !image.hasAlphaChannel()) { // we are going to save grayscale image without alpha channel if (save_grayscale && !image.hasAlphaChannel()) { // we are going to save grayscale image without alpha channel
if (save_depth > 8) { if (save_depth > 8) {
tmpformat = QImage::Format_Grayscale16; tmpformat = QImage::Format_Grayscale16;
@@ -723,15 +729,11 @@ bool QAVIFHandler::write(const QImage &image)
QImage tmpgrayimage = image.convertToFormat(tmpformat); QImage tmpgrayimage = image.convertToFormat(tmpformat);
avif = avifImageCreate(tmpgrayimage.width(), tmpgrayimage.height(), save_depth, AVIF_PIXEL_FORMAT_YUV400); avif = avifImageCreate(tmpgrayimage.width(), tmpgrayimage.height(), save_depth, AVIF_PIXEL_FORMAT_YUV400);
#if AVIF_VERSION >= 110000
res = avifImageAllocatePlanes(avif, AVIF_PLANES_YUV); res = avifImageAllocatePlanes(avif, AVIF_PLANES_YUV);
if (res != AVIF_RESULT_OK) { if (res != AVIF_RESULT_OK) {
qCWarning(LOG_AVIFPLUGIN, "ERROR in avifImageAllocatePlanes: %s", avifResultToString(res)); qCWarning(LOG_AVIFPLUGIN, "ERROR in avifImageAllocatePlanes: %s", avifResultToString(res));
return false; return false;
} }
#else
avifImageAllocatePlanes(avif, AVIF_PLANES_YUV);
#endif
// set EXIF and XMP metadata // set EXIF and XMP metadata
setMetadata(avif, tmpgrayimage); setMetadata(avif, tmpgrayimage);
@@ -776,15 +778,11 @@ bool QAVIFHandler::write(const QImage &image)
QByteArray iccprofile_gray = tmpgrayimage.colorSpace().iccProfile(); QByteArray iccprofile_gray = tmpgrayimage.colorSpace().iccProfile();
if (iccprofile_gray.size() > 0) { if (iccprofile_gray.size() > 0) {
#if AVIF_VERSION >= 1000000
res = avifImageSetProfileICC(avif, reinterpret_cast<const uint8_t *>(iccprofile_gray.constData()), iccprofile_gray.size()); res = avifImageSetProfileICC(avif, reinterpret_cast<const uint8_t *>(iccprofile_gray.constData()), iccprofile_gray.size());
if (res != AVIF_RESULT_OK) { if (res != AVIF_RESULT_OK) {
qCWarning(LOG_AVIFPLUGIN, "ERROR in avifImageSetProfileICC: %s", avifResultToString(res)); qCWarning(LOG_AVIFPLUGIN, "ERROR in avifImageSetProfileICC: %s", avifResultToString(res));
return false; return false;
} }
#else
avifImageSetProfileICC(avif, reinterpret_cast<const uint8_t *>(iccprofile_gray.constData()), iccprofile_gray.size());
#endif
} }
} else { // convert to grayscale with SRgb } else { // convert to grayscale with SRgb
tmpgrayimage.convertToColorSpace(QColorSpace(QPointF(0.3127f, 0.329f), QColorSpace::TransferFunction::SRgb), QImage::Format_Grayscale16); tmpgrayimage.convertToColorSpace(QColorSpace(QPointF(0.3127f, 0.329f), QColorSpace::TransferFunction::SRgb), QImage::Format_Grayscale16);
@@ -1003,15 +1001,11 @@ bool QAVIFHandler::write(const QImage &image)
setMetadata(avif, tmpcolorimage); setMetadata(avif, tmpcolorimage);
if (iccprofile.size() > 0) { if (iccprofile.size() > 0) {
#if AVIF_VERSION >= 1000000
res = avifImageSetProfileICC(avif, reinterpret_cast<const uint8_t *>(iccprofile.constData()), iccprofile.size()); res = avifImageSetProfileICC(avif, reinterpret_cast<const uint8_t *>(iccprofile.constData()), iccprofile.size());
if (res != AVIF_RESULT_OK) { if (res != AVIF_RESULT_OK) {
qCWarning(LOG_AVIFPLUGIN, "ERROR in avifImageSetProfileICC: %s", avifResultToString(res)); qCWarning(LOG_AVIFPLUGIN, "ERROR in avifImageSetProfileICC: %s", avifResultToString(res));
return false; return false;
} }
#else
avifImageSetProfileICC(avif, reinterpret_cast<const uint8_t *>(iccprofile.constData()), iccprofile.size());
#endif
} }
avifRGBImage rgb; avifRGBImage rgb;
@@ -1048,15 +1042,6 @@ bool QAVIFHandler::write(const QImage &image)
avifEncoder *encoder = avifEncoderCreate(); avifEncoder *encoder = avifEncoderCreate();
encoder->maxThreads = qBound(1, QThread::idealThreadCount(), 64); encoder->maxThreads = qBound(1, QThread::idealThreadCount(), 64);
#if AVIF_VERSION < 1000000
encoder->minQuantizer = minQuantizer;
encoder->maxQuantizer = maxQuantizer;
if (image.hasAlphaChannel()) {
encoder->minQuantizerAlpha = AVIF_QUANTIZER_LOSSLESS;
encoder->maxQuantizerAlpha = maxQuantizerAlpha;
}
#else
encoder->quality = m_quality; encoder->quality = m_quality;
if (image.hasAlphaChannel()) { if (image.hasAlphaChannel()) {
@@ -1066,7 +1051,6 @@ bool QAVIFHandler::write(const QImage &image)
encoder->qualityAlpha = 100 - (KIMG_AVIF_QUALITY_LOW - m_quality) / 2; encoder->qualityAlpha = 100 - (KIMG_AVIF_QUALITY_LOW - m_quality) / 2;
} }
} }
#endif
encoder->speed = 6; encoder->speed = 6;
@@ -1203,7 +1187,8 @@ bool QAVIFHandler::jumpToNextImage()
} }
if ((m_container_width != m_decoder->image->width) || (m_container_height != m_decoder->image->height)) { if ((m_container_width != m_decoder->image->width) || (m_container_height != m_decoder->image->height)) {
qCWarning(LOG_AVIFPLUGIN, "Decoded image sequence size (%dx%d) do not match first image size (%dx%d)!", qCWarning(LOG_AVIFPLUGIN,
"Decoded image sequence size (%dx%d) do not match first image size (%dx%d)!",
m_decoder->image->width, m_decoder->image->width,
m_decoder->image->height, m_decoder->image->height,
m_container_width, m_container_width,
@@ -1257,7 +1242,8 @@ bool QAVIFHandler::jumpToImage(int imageNumber)
} }
if ((m_container_width != m_decoder->image->width) || (m_container_height != m_decoder->image->height)) { if ((m_container_width != m_decoder->image->width) || (m_container_height != m_decoder->image->height)) {
qCWarning(LOG_AVIFPLUGIN, "Decoded image sequence size (%dx%d) do not match declared container size (%dx%d)!", qCWarning(LOG_AVIFPLUGIN,
"Decoded image sequence size (%dx%d) do not match declared container size (%dx%d)!",
m_decoder->image->width, m_decoder->image->width,
m_decoder->image->height, m_decoder->image->height,
m_container_width, m_container_width,
@@ -1303,11 +1289,9 @@ int QAVIFHandler::loopCount() const
return 0; return 0;
} }
#if AVIF_VERSION >= 1000000
if (m_decoder->repetitionCount >= 0) { if (m_decoder->repetitionCount >= 0) {
return m_decoder->repetitionCount; return m_decoder->repetitionCount;
} }
#endif
// Endless loop to work around https://github.com/AOMediaCodec/libavif/issues/347 // Endless loop to work around https://github.com/AOMediaCodec/libavif/issues/347
return -1; return -1;
} }

View File

@@ -47,6 +47,7 @@ private:
static QPointF CompatibleChromacity(qreal chrX, qreal chrY); static QPointF CompatibleChromacity(qreal chrX, qreal chrY);
bool ensureParsed() const; bool ensureParsed() const;
bool ensureOpened() const; bool ensureOpened() const;
bool initDecoder(bool ignore_exif);
bool ensureDecoder(); bool ensureDecoder();
bool decode_one_frame(); bool decode_one_frame();