diff --git a/autotests/read/avif/metadata.avif b/autotests/read/avif/metadata.avif new file mode 100644 index 0000000..158093a Binary files /dev/null and b/autotests/read/avif/metadata.avif differ diff --git a/autotests/read/avif/metadata.avif.json b/autotests/read/avif/metadata.avif.json new file mode 100644 index 0000000..6aeffa1 --- /dev/null +++ b/autotests/read/avif/metadata.avif.json @@ -0,0 +1,59 @@ +[ + { + "fileName" : "metadata.png", + "metadata" : [ + { + "key" : "CreationDate", + "value" : "2025-02-19T08:27:22+01:00" + }, + { + "key" : "Software" , + "value" : "GIMP 3.0.0-RC3" + }, + { + "key" : "Altitude", + "value" : "34" + }, + { + "key" : "Author", + "value" : "KDE Project" + }, + { + "key" : "Copyright", + "value" : "@2025 KDE Project" + }, + { + "key" : "Description", + "value" : "TV broadcast test image." + }, + { + "key" : "Latitude", + "value" : "44.6478" + }, + { + "key" : "LensManufacturer", + "value" : "KDE Glasses" + }, + { + "key" : "LensModel", + "value" : "A1234" + }, + { + "key" : "Longitude", + "value" : "10.9254" + }, + { + "key" : "Manufacturer", + "value" : "KFramework" + }, + { + "key" : "Model", + "value" : "KImageFormats" + } + ], + "resolution" : { + "dotsPerMeterX" : 11811, + "dotsPerMeterY" : 5905 + } + } +] diff --git a/autotests/read/avif/metadata.png b/autotests/read/avif/metadata.png new file mode 100644 index 0000000..046a449 Binary files /dev/null and b/autotests/read/avif/metadata.png differ diff --git a/autotests/write/basic/avif.json b/autotests/write/basic/avif.json new file mode 100644 index 0000000..c640941 --- /dev/null +++ b/autotests/write/basic/avif.json @@ -0,0 +1,57 @@ +{ + "format" : "avif", + "metadata" : [ + { + "key" : "CreationDate", + "value" : "2025-01-14T13:53:32+01:00" + }, + { + "key" : "Software" , + "value" : "Adobe Photoshop 26.2 (Windows)" + }, + { + "key" : "Altitude", + "value" : "34" + }, + { + "key" : "Author", + "value" : "KDE Project" + }, + { + "key" : "Copyright", + "value" : "@2025 KDE Project" + }, + { + "key" : "Description", + "value" : "TV broadcast test image." + }, + { + "key" : "Latitude", + "value" : "44.6478" + }, + { + "key" : "LensManufacturer", + "value" : "KDE Glasses" + }, + { + "key" : "LensModel", + "value" : "A1234" + }, + { + "key" : "Longitude", + "value" : "10.9254" + }, + { + "key" : "Manufacturer", + "value" : "KFramework" + }, + { + "key" : "Model", + "value" : "KImageFormats" + } + ], + "resolution" : { + "dotsPerMeterX" : 11811, + "dotsPerMeterY" : 11812 + } +} diff --git a/src/imageformats/CMakeLists.txt b/src/imageformats/CMakeLists.txt index 8a0d082..c1b473e 100644 --- a/src/imageformats/CMakeLists.txt +++ b/src/imageformats/CMakeLists.txt @@ -25,7 +25,7 @@ kimageformats_add_plugin(kimg_ani SOURCES ani.cpp) ################################## if (TARGET avif) - kimageformats_add_plugin(kimg_avif SOURCES "avif.cpp") + kimageformats_add_plugin(kimg_avif SOURCES avif.cpp microexif.cpp) target_link_libraries(kimg_avif PRIVATE "avif") endif() diff --git a/src/imageformats/avif.cpp b/src/imageformats/avif.cpp index 7abbcdf..616580d 100644 --- a/src/imageformats/avif.cpp +++ b/src/imageformats/avif.cpp @@ -12,6 +12,7 @@ #include #include "avif_p.h" +#include "microexif_p.h" #include "util_p.h" #include @@ -151,9 +152,6 @@ bool QAVIFHandler::ensureDecoder() m_decoder = avifDecoderCreate(); - m_decoder->ignoreExif = AVIF_TRUE; - m_decoder->ignoreXMP = AVIF_TRUE; - #if AVIF_VERSION >= 80400 m_decoder->maxThreads = qBound(1, QThread::idealThreadCount(), 64); #endif @@ -534,12 +532,54 @@ bool QAVIFHandler::decode_one_frame() m_current_image.setColorSpace(colorspace); + if (m_decoder->image->exif.size) { + auto exif = MicroExif::fromRawData(reinterpret_cast(m_decoder->image->exif.data), m_decoder->image->exif.size); + // set image resolution + if (exif.horizontalResolution() > 0) + m_current_image.setDotsPerMeterX(qRound(exif.horizontalResolution() / 25.4 * 1000)); + if (exif.verticalResolution() > 0) + m_current_image.setDotsPerMeterY(qRound(exif.verticalResolution() / 25.4 * 1000)); + // set image metadata + exif.toImageMetadata(m_current_image); + } + + if (m_decoder->image->xmp.size) { + auto ba = QByteArray::fromRawData(reinterpret_cast(m_decoder->image->xmp.data), m_decoder->image->xmp.size); + m_current_image.setText(QStringLiteral(META_KEY_XMP_ADOBE), QString::fromUtf8(ba)); + } + m_estimated_dimensions = m_current_image.size(); m_must_jump_to_next_image = false; return true; } +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(xmp.constData()), xmp.size()); + if (res != AVIF_RESULT_OK) { + qWarning("ERROR in avifImageSetMetadataXMP: %s", avifResultToString(res)); + } +#else + avifImageSetMetadataXMP(avif, reinterpret_cast(xmp.constData()), xmp.size()); +#endif + } + auto exif = MicroExif::fromImage(image).toByteArray(); + if (!exif.isEmpty()) { +#if AVIF_VERSION >= 1000000 + auto res = avifImageSetMetadataExif(avif, reinterpret_cast(exif.constData()), exif.size()); + if (res != AVIF_RESULT_OK) { + qWarning("ERROR in avifImageSetMetadataExif: %s", avifResultToString(res)); + } +#else + avifImageSetMetadataExif(avif, reinterpret_cast(exif.constData()), exif.size()); +#endif + } +} + bool QAVIFHandler::read(QImage *image) { if (!ensureOpened()) { @@ -689,6 +729,8 @@ bool QAVIFHandler::write(const QImage &image) #else avifImageAllocatePlanes(avif, AVIF_PLANES_YUV); #endif + // set EXIF and XMP metadata + setMetadata(avif, tmpgrayimage); if (tmpgrayimage.colorSpace().isValid()) { avif->colorPrimaries = (avifColorPrimaries)1; @@ -915,6 +957,9 @@ bool QAVIFHandler::write(const QImage &image) avif->colorPrimaries = primaries_to_save; avif->transferCharacteristics = transfer_to_save; + // set EXIF and XMP metadata + setMetadata(avif, tmpcolorimage); + if (iccprofile.size() > 0) { #if AVIF_VERSION >= 1000000 res = avifImageSetProfileICC(avif, reinterpret_cast(iccprofile.constData()), iccprofile.size()); diff --git a/src/imageformats/microexif.cpp b/src/imageformats/microexif.cpp index 61418ae..78e129e 100644 --- a/src/imageformats/microexif.cpp +++ b/src/imageformats/microexif.cpp @@ -1024,6 +1024,13 @@ MicroExif MicroExif::fromByteArray(const QByteArray &ba) return fromDevice(&buf); } +MicroExif MicroExif::fromRawData(const char *data, size_t size) +{ + if (data == nullptr || size == 0) + return {}; + return fromByteArray(QByteArray::fromRawData(data, size)); +} + MicroExif MicroExif::fromDevice(QIODevice *device) { if (device == nullptr || device->isSequential()) diff --git a/src/imageformats/microexif_p.h b/src/imageformats/microexif_p.h index 724a0ac..f5a7863 100644 --- a/src/imageformats/microexif_p.h +++ b/src/imageformats/microexif_p.h @@ -269,6 +269,14 @@ public: */ static MicroExif fromByteArray(const QByteArray &ba); + /*! + * \brief fromRawData + * Creates the class from RAW EXIF data. + * \return The created class (empty on error). + * \sa isEmpty + */ + static MicroExif fromRawData(const char *data, size_t size); + /*! * \brief fromDevice * Creates the class from a device.