AVIF: added support to XMP and EXIF metadata

Allow to load/save info about:
- GPS info (latitude, longitude, altitude)
- Various text info (title, description, author, copyright, etc...)
- Image resolution

The compatibility of the modifications has been tested with GIMP.
This commit is contained in:
Mirco Miranda 2025-02-19 11:56:19 +00:00
parent bb1c6aab9e
commit 90d4256f3d
8 changed files with 180 additions and 4 deletions

Binary file not shown.

View File

@ -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
}
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -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
}
}

View File

@ -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()

View File

@ -12,6 +12,7 @@
#include <QColorSpace>
#include "avif_p.h"
#include "microexif_p.h"
#include "util_p.h"
#include <cfloat>
@ -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<const char *>(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<const char *>(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<const uint8_t *>(xmp.constData()), xmp.size());
if (res != AVIF_RESULT_OK) {
qWarning("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) {
qWarning("ERROR in avifImageSetMetadataExif: %s", avifResultToString(res));
}
#else
avifImageSetMetadataExif(avif, reinterpret_cast<const uint8_t *>(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<const uint8_t *>(iccprofile.constData()), iccprofile.size());

View File

@ -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())

View File

@ -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.