[frameworks/kimageformats] /: avif: enable decoding of files with invalid EXIF metadata

Daniel Novomeský <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit 5c3bdcddf2ff2024d556e06412d0d9889a70567f by Daniel Novomeský.
Committed on 27/07/2026 at 12:33.
Pushed by novomeskyd into branch 'master'.

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.

M  +1    -4    CMakeLists.txt
A  +-    --    autotests/read/avif/invalid-exif.avif
A  +7    -0    autotests/read/avif/invalid-exif.avif.json
A  +-    --    autotests/read/avif/invalid-exif.jpg
M  +78   -94   src/imageformats/avif.cpp
M  +1    -0    src/imageformats/avif_p.h

https://invent.kde.org/frameworks/kimageformats/-/commit/5c3bdcddf2ff2024d556e06412d0d9889a70567f

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 30743f16..be2d2462 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -53,10 +53,7 @@ set_package_properties(OpenEXR PROPERTIES
     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)
-endif()
+find_package(libavif 1 CONFIG)
 set_package_properties(libavif PROPERTIES
     TYPE OPTIONAL
     PURPOSE "Required for the QImage plugin for AVIF images"
diff --git a/autotests/read/avif/invalid-exif.avif b/autotests/read/avif/invalid-exif.avif
new file mode 100644
index 00000000..da5d30a0
Binary files /dev/null and b/autotests/read/avif/invalid-exif.avif differ
diff --git a/autotests/read/avif/invalid-exif.avif.json b/autotests/read/avif/invalid-exif.avif.json
new file mode 100644
index 00000000..2628a20a
--- /dev/null
+++ b/autotests/read/avif/invalid-exif.avif.json
@@ -0,0 +1,7 @@
+[
+    {
+        "fileName": "invalid-exif.jpg",
+        "fuzziness": 30,
+        "description": "Minimum fuzziness value to pass the test on all architectures."
+    }
+]
diff --git a/autotests/read/avif/invalid-exif.jpg b/autotests/read/avif/invalid-exif.jpg
new file mode 100644
index 00000000..652cd4ef
Binary files /dev/null and b/autotests/read/avif/invalid-exif.jpg differ
diff --git a/src/imageformats/avif.cpp b/src/imageformats/avif.cpp
index 4baa7815..2af1071a 100644
--- a/src/imageformats/avif.cpp
+++ b/src/imageformats/avif.cpp
@@ -151,39 +151,29 @@ bool QAVIFHandler::ensureOpened() const
     return false;
 }
 
-bool QAVIFHandler::ensureDecoder()
+bool QAVIFHandler::initDecoder(bool ignore_exif)
 {
     if (m_decoder) {
-        return true;
+        avifDecoderDestroy(m_decoder);
+        m_decoder = nullptr;
     }
 
-    m_rawData = deviceRead(device(), kMaxQVectorSize);
-
-    m_rawAvifData.data = reinterpret_cast<const uint8_t *>(m_rawData.constData());
-    m_rawAvifData.size = m_rawData.size();
-
-    if (avifPeekCompatibleFileType(&m_rawAvifData) == AVIF_FALSE) {
+    m_decoder = avifDecoderCreate();
+    if (!m_decoder) {
+        qCWarning(LOG_AVIFPLUGIN, "ERROR: avifDecoderCreate failed");
         m_parseState = ParseAvifError;
         return false;
     }
 
-    m_decoder = avifDecoderCreate();
+    m_decoder->ignoreExif = ignore_exif ? AVIF_TRUE : AVIF_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;
-
-    decodeResult = avifDecoderSetIOMemory(m_decoder, m_rawAvifData.data, m_rawAvifData.size);
+    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));
 
@@ -193,16 +183,57 @@ bool QAVIFHandler::ensureDecoder()
         return false;
     }
 
-    decodeResult = avifDecoderParse(m_decoder);
-    if (decodeResult != AVIF_RESULT_OK) {
-        qCWarning(LOG_AVIFPLUGIN, "ERROR: Failed to parse input: %s", avifResultToString(decodeResult));
+    return true;
+}
 
-        avifDecoderDestroy(m_decoder);
-        m_decoder = nullptr;
+bool QAVIFHandler::ensureDecoder()
+{
+    if (m_decoder) {
+        return true;
+    }
+
+    m_rawData = deviceRead(device(), kMaxQVectorSize);
+
+    m_rawAvifData.data = reinterpret_cast<const uint8_t *>(m_rawData.constData());
+    m_rawAvifData.size = m_rawData.size();
+
+    if (avifPeekCompatibleFileType(&m_rawAvifData) == AVIF_FALSE) {
         m_parseState = ParseAvifError;
         return false;
     }
 
+    if (!initDecoder(false)) {
+        return false;
+    }
+
+    avifResult decodeResult;
+
+    decodeResult = avifDecoderParse(m_decoder);
+    if (decodeResult != AVIF_RESULT_OK) {
+        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);
+                m_decoder = nullptr;
+                m_parseState = ParseAvifError;
+                return false;
+            }
+        } else { // first attempt to parse failed
+            qCWarning(LOG_AVIFPLUGIN, "ERROR: Failed to parse input: %s", avifResultToString(decodeResult));
+
+            avifDecoderDestroy(m_decoder);
+            m_decoder = nullptr;
+            m_parseState = ParseAvifError;
+            return false;
+        }
+    }
+
     m_container_width = m_decoder->image->width;
     m_container_height = m_decoder->image->height;
 
@@ -380,9 +411,10 @@ bool QAVIFHandler::decode_one_frame()
             q_trc = QColorSpace::TransferFunction::Hlg;
             break;
         default:
-            qCWarning(LOG_AVIFPLUGIN, "CICP colorPrimaries: %d, transferCharacteristics: %d\nThe colorspace is unsupported by this plug-in yet.",
-                     m_decoder->image->colorPrimaries,
-                     m_decoder->image->transferCharacteristics);
+            qCWarning(LOG_AVIFPLUGIN,
+                      "CICP colorPrimaries: %d, transferCharacteristics: %d\nThe colorspace is unsupported by this plug-in yet.",
+                      m_decoder->image->colorPrimaries,
+                      m_decoder->image->transferCharacteristics);
             q_trc = QColorSpace::TransferFunction::SRgb;
             break;
         }
@@ -420,9 +452,7 @@ bool QAVIFHandler::decode_one_frame()
     avifRGBImage rgb;
     avifRGBImageSetDefaults(&rgb, m_decoder->image);
 
-#if AVIF_VERSION >= 1000000
     rgb.maxThreads = m_decoder->maxThreads;
-#endif
 
     if (resultdepth > 8) {
         rgb.depth = 16;
@@ -439,12 +469,10 @@ bool QAVIFHandler::decode_one_frame()
         rgb.format = AVIF_RGB_FORMAT_ARGB;
 #endif
 
-#if AVIF_VERSION >= 80400
         if (m_decoder->imageCount > 1) {
             /* accelerate animated AVIF */
             rgb.chromaUpsampling = AVIF_CHROMA_UPSAMPLING_FASTEST;
         }
-#endif
 
         if (loadgray) {
             resultformat = QImage::Format_Grayscale8;
@@ -518,11 +546,7 @@ bool QAVIFHandler::decode_one_frame()
     }
 
     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) {
-#endif
         case 0: // top-to-bottom
             result = result.flipped(Qt::Vertical);
             break;
@@ -557,29 +581,21 @@ bool QAVIFHandler::decode_one_frame()
     return true;
 }
 
-static void setMetadata(avifImage *avif, const QImage& image)
+static void setMetadata(avifImage *avif, const QImage &image)
 {
     auto xmp = image.text(QStringLiteral(META_KEY_XMP_ADOBE)).toUtf8();
     if (!xmp.isEmpty()) {
-#if AVIF_VERSION >= 1000000
         auto res = avifImageSetMetadataXMP(avif, reinterpret_cast<const uint8_t *>(xmp.constData()), xmp.size());
         if (res != AVIF_RESULT_OK) {
             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();
     if (!exif.isEmpty()) {
-#if AVIF_VERSION >= 1000000
         auto res = avifImageSetMetadataExif(avif, reinterpret_cast<const uint8_t *>(exif.constData()), exif.size());
         if (res != AVIF_RESULT_OK) {
             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)) {
-            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 {
         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)) {
             lossless = true;
         } 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;
     }
 
-#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;
 
     bool save_grayscale; // true - monochrome, false - colors
@@ -704,16 +720,6 @@ bool QAVIFHandler::write(const QImage &image)
         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_depth > 8) {
             tmpformat = QImage::Format_Grayscale16;
@@ -723,15 +729,11 @@ bool QAVIFHandler::write(const QImage &image)
         QImage tmpgrayimage = image.convertToFormat(tmpformat);
 
         avif = avifImageCreate(tmpgrayimage.width(), tmpgrayimage.height(), save_depth, AVIF_PIXEL_FORMAT_YUV400);
-#if AVIF_VERSION >= 110000
         res = avifImageAllocatePlanes(avif, AVIF_PLANES_YUV);
         if (res != AVIF_RESULT_OK) {
             qCWarning(LOG_AVIFPLUGIN, "ERROR in avifImageAllocatePlanes: %s", avifResultToString(res));
             return false;
         }
-#else
-        avifImageAllocatePlanes(avif, AVIF_PLANES_YUV);
-#endif
         // set EXIF and XMP metadata
         setMetadata(avif, tmpgrayimage);
 
@@ -776,15 +778,11 @@ bool QAVIFHandler::write(const QImage &image)
                     QByteArray iccprofile_gray = tmpgrayimage.colorSpace().iccProfile();
 
                     if (iccprofile_gray.size() > 0) {
-#if AVIF_VERSION >= 1000000
                         res = avifImageSetProfileICC(avif, reinterpret_cast<const uint8_t *>(iccprofile_gray.constData()), iccprofile_gray.size());
                         if (res != AVIF_RESULT_OK) {
                             qCWarning(LOG_AVIFPLUGIN, "ERROR in avifImageSetProfileICC: %s", avifResultToString(res));
                             return false;
                         }
-#else
-                        avifImageSetProfileICC(avif, reinterpret_cast<const uint8_t *>(iccprofile_gray.constData()), iccprofile_gray.size());
-#endif
                     }
                 } else { // convert to grayscale with SRgb
                     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);
 
         if (iccprofile.size() > 0) {
-#if AVIF_VERSION >= 1000000
             res = avifImageSetProfileICC(avif, reinterpret_cast<const uint8_t *>(iccprofile.constData()), iccprofile.size());
             if (res != AVIF_RESULT_OK) {
                 qCWarning(LOG_AVIFPLUGIN, "ERROR in avifImageSetProfileICC: %s", avifResultToString(res));
                 return false;
             }
-#else
-            avifImageSetProfileICC(avif, reinterpret_cast<const uint8_t *>(iccprofile.constData()), iccprofile.size());
-#endif
         }
 
         avifRGBImage rgb;
@@ -1048,15 +1042,6 @@ bool QAVIFHandler::write(const QImage &image)
     avifEncoder *encoder = avifEncoderCreate();
     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;
 
     if (image.hasAlphaChannel()) {
@@ -1066,7 +1051,6 @@ bool QAVIFHandler::write(const QImage &image)
             encoder->qualityAlpha = 100 - (KIMG_AVIF_QUALITY_LOW - m_quality) / 2;
         }
     }
-#endif
 
     encoder->speed = 6;
 
@@ -1203,11 +1187,12 @@ bool QAVIFHandler::jumpToNextImage()
     }
 
     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)!",
-                 m_decoder->image->width,
-                 m_decoder->image->height,
-                 m_container_width,
-                 m_container_height);
+        qCWarning(LOG_AVIFPLUGIN,
+                  "Decoded image sequence size (%dx%d) do not match first image size (%dx%d)!",
+                  m_decoder->image->width,
+                  m_decoder->image->height,
+                  m_container_width,
+                  m_container_height);
 
         m_parseState = ParseAvifError;
         return false;
@@ -1257,11 +1242,12 @@ bool QAVIFHandler::jumpToImage(int imageNumber)
     }
 
     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)!",
-                 m_decoder->image->width,
-                 m_decoder->image->height,
-                 m_container_width,
-                 m_container_height);
+        qCWarning(LOG_AVIFPLUGIN,
+                  "Decoded image sequence size (%dx%d) do not match declared container size (%dx%d)!",
+                  m_decoder->image->width,
+                  m_decoder->image->height,
+                  m_container_width,
+                  m_container_height);
 
         m_parseState = ParseAvifError;
         return false;
@@ -1303,11 +1289,9 @@ int QAVIFHandler::loopCount() const
         return 0;
     }
 
-#if AVIF_VERSION >= 1000000
     if (m_decoder->repetitionCount >= 0) {
         return m_decoder->repetitionCount;
     }
-#endif
     // Endless loop to work around https://github.com/AOMediaCodec/libavif/issues/347
     return -1;
 }
diff --git a/src/imageformats/avif_p.h b/src/imageformats/avif_p.h
index 4387db60..66639183 100644
--- a/src/imageformats/avif_p.h
+++ b/src/imageformats/avif_p.h
@@ -47,6 +47,7 @@ private:
     static QPointF CompatibleChromacity(qreal chrX, qreal chrY);
     bool ensureParsed() const;
     bool ensureOpened() const;
+    bool initDecoder(bool ignore_exif);
     bool ensureDecoder();
     bool decode_one_frame();
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.