Add Exif/XMP support in PNG/JPEG export
Change avifGetExifTiffHeaderOffset()'s signature.
Add avifImageGetExifOrientationFromIrotImir() and
avifSetExifOrientation() in internal.h.
Link the CMake target avif_apps to avif_internal.
Drop and warn about long XMP when exporting to JPEG.
Refactor defines in avifjpeg.c.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2a6a112..d3532ca 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,8 @@
from internal.h when BUILD_SHARED_LIBS is ON.
### Changed
+* Exif and XMP metadata is exported to PNG and JPEG files by default,
+ except XMP payloads larger than 65502 bytes in JPEG.
* The --grid flag in avifenc can be used for images that are not evenly divided
into cells.
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7648d3b..3832320 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -511,7 +511,7 @@
avif_apps STATIC apps/shared/avifjpeg.c apps/shared/iccjpeg.c apps/shared/avifpng.c apps/shared/avifutil.c
apps/shared/y4m.c
)
- target_link_libraries(avif_apps avif ${AVIF_PLATFORM_LIBRARIES} ${PNG_LIBRARY} ${ZLIB_LIBRARY} ${JPEG_LIBRARY})
+ target_link_libraries(avif_apps avif_internal ${AVIF_PLATFORM_LIBRARIES} ${PNG_LIBRARY} ${ZLIB_LIBRARY} ${JPEG_LIBRARY})
# In GitHub CI's macos-latest os image, /usr/local/include has not only the headers of libpng
# libjpeg but also the headers of an older version of libavif. Put the avif include directory
# before ${PNG_PNG_INCLUDE_DIR} ${JPEG_INCLUDE_DIR} to prevent picking up old libavif headers
diff --git a/apps/shared/avifjpeg.c b/apps/shared/avifjpeg.c
index 8463b23..b83eaa6 100644
--- a/apps/shared/avifjpeg.c
+++ b/apps/shared/avifjpeg.c
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: BSD-2-Clause
#include "avifjpeg.h"
+#include "avif/internal.h"
#include "avifutil.h"
#include <assert.h>
@@ -243,23 +244,29 @@
return NULL;
}
+#define AVIF_JPEG_MAX_MARKER_DATA_LENGTH 65533
+
+// Exif tag
+#define AVIF_JPEG_EXIF_HEADER "Exif\0\0"
+#define AVIF_JPEG_EXIF_HEADER_LENGTH 6
+
// XMP tags
-#define AVIF_STANDARD_XMP_TAG "http://ns.adobe.com/xap/1.0/\0"
-#define AVIF_STANDARD_XMP_TAG_LENGTH 29
-#define AVIF_EXTENDED_XMP_TAG "http://ns.adobe.com/xmp/extension/\0"
-#define AVIF_EXTENDED_XMP_TAG_LENGTH 35
+#define AVIF_JPEG_STANDARD_XMP_TAG "http://ns.adobe.com/xap/1.0/\0"
+#define AVIF_JPEG_STANDARD_XMP_TAG_LENGTH 29
+#define AVIF_JPEG_EXTENDED_XMP_TAG "http://ns.adobe.com/xmp/extension/\0"
+#define AVIF_JPEG_EXTENDED_XMP_TAG_LENGTH 35
// One way of storing the Extended XMP GUID (generated by a camera for example).
-#define AVIF_XMP_NOTE_TAG "xmpNote:HasExtendedXMP=\""
-#define AVIF_XMP_NOTE_TAG_LENGTH 24
+#define AVIF_JPEG_XMP_NOTE_TAG "xmpNote:HasExtendedXMP=\""
+#define AVIF_JPEG_XMP_NOTE_TAG_LENGTH 24
// Another way of storing the Extended XMP GUID (generated by exiftool for example).
-#define AVIF_ALTERNATIVE_XMP_NOTE_TAG "<xmpNote:HasExtendedXMP>"
-#define AVIF_ALTERNATIVE_XMP_NOTE_TAG_LENGTH 24
+#define AVIF_JPEG_ALTERNATIVE_XMP_NOTE_TAG "<xmpNote:HasExtendedXMP>"
+#define AVIF_JPEG_ALTERNATIVE_XMP_NOTE_TAG_LENGTH 24
-#define AVIF_EXTENDED_XMP_GUID_LENGTH 32
+#define AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH 32
// Offset in APP1 segment (skip tag + guid + size + offset).
-#define AVIF_OFFSET_TILL_EXTENDED_XMP (AVIF_EXTENDED_XMP_TAG_LENGTH + AVIF_EXTENDED_XMP_GUID_LENGTH + 4 + 4)
+#define AVIF_JPEG_OFFSET_TILL_EXTENDED_XMP (AVIF_JPEG_EXTENDED_XMP_TAG_LENGTH + AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH + 4 + 4)
// Note on setjmp() and volatile variables:
//
@@ -377,7 +384,7 @@
}
if (!ignoreExif) {
- const avifROData tagExif = { (const uint8_t *)"Exif\0\0", 6 };
+ const avifROData tagExif = { (const uint8_t *)AVIF_JPEG_EXIF_HEADER, AVIF_JPEG_EXIF_HEADER_LENGTH };
avifBool found = AVIF_FALSE;
for (jpeg_saved_marker_ptr marker = cinfo.marker_list; marker != NULL; marker = marker->next) {
if ((marker->marker == (JPEG_APP0 + 1)) && (marker->data_length > tagExif.size) &&
@@ -387,6 +394,9 @@
fprintf(stderr, "Exif extraction failed: unsupported Exif split into multiple segments or invalid multiple Exif segments\n");
goto cleanup;
}
+ // Exif orientation, if any, is imported to avif->irot/imir and kept in avif->exif.
+ // libheif has the same behavior, see
+ // https://github.com/strukturag/libheif/blob/ea78603d8e47096606813d221725621306789ff2/examples/heif_enc.cc#L403
avifImageSetMetadataExif(avif, marker->data + tagExif.size, marker->data_length - tagExif.size);
found = AVIF_TRUE;
}
@@ -396,33 +406,33 @@
const uint8_t * standardXMPData = NULL;
uint32_t standardXMPSize = 0; // At most 64kB as defined by Adobe XMP Specification Part 3.
for (jpeg_saved_marker_ptr marker = cinfo.marker_list; marker != NULL; marker = marker->next) {
- if ((marker->marker == (JPEG_APP0 + 1)) && (marker->data_length > AVIF_STANDARD_XMP_TAG_LENGTH) &&
- !memcmp(marker->data, AVIF_STANDARD_XMP_TAG, AVIF_STANDARD_XMP_TAG_LENGTH)) {
+ if ((marker->marker == (JPEG_APP0 + 1)) && (marker->data_length > AVIF_JPEG_STANDARD_XMP_TAG_LENGTH) &&
+ !memcmp(marker->data, AVIF_JPEG_STANDARD_XMP_TAG, AVIF_JPEG_STANDARD_XMP_TAG_LENGTH)) {
if (standardXMPData) {
fprintf(stderr, "XMP extraction failed: invalid multiple standard XMP segments\n");
goto cleanup;
}
- standardXMPData = marker->data + AVIF_STANDARD_XMP_TAG_LENGTH;
- standardXMPSize = (uint32_t)(marker->data_length - AVIF_STANDARD_XMP_TAG_LENGTH);
+ standardXMPData = marker->data + AVIF_JPEG_STANDARD_XMP_TAG_LENGTH;
+ standardXMPSize = (uint32_t)(marker->data_length - AVIF_JPEG_STANDARD_XMP_TAG_LENGTH);
}
}
avifBool foundExtendedXMP = AVIF_FALSE;
- uint8_t extendedXMPGUID[AVIF_EXTENDED_XMP_GUID_LENGTH]; // The value is common to all extended XMP segments.
+ uint8_t extendedXMPGUID[AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH]; // The value is common to all extended XMP segments.
for (jpeg_saved_marker_ptr marker = cinfo.marker_list; marker != NULL; marker = marker->next) {
- if ((marker->marker == (JPEG_APP0 + 1)) && (marker->data_length > AVIF_EXTENDED_XMP_TAG_LENGTH) &&
- !memcmp(marker->data, AVIF_EXTENDED_XMP_TAG, AVIF_EXTENDED_XMP_TAG_LENGTH)) {
+ if ((marker->marker == (JPEG_APP0 + 1)) && (marker->data_length > AVIF_JPEG_EXTENDED_XMP_TAG_LENGTH) &&
+ !memcmp(marker->data, AVIF_JPEG_EXTENDED_XMP_TAG, AVIF_JPEG_EXTENDED_XMP_TAG_LENGTH)) {
if (!standardXMPData) {
fprintf(stderr, "XMP extraction failed: extended XMP segment found, missing standard XMP segment\n");
goto cleanup;
}
- if (marker->data_length < AVIF_OFFSET_TILL_EXTENDED_XMP) {
+ if (marker->data_length < AVIF_JPEG_OFFSET_TILL_EXTENDED_XMP) {
fprintf(stderr, "XMP extraction failed: truncated extended XMP segment\n");
goto cleanup;
}
- const uint8_t * guid = &marker->data[AVIF_EXTENDED_XMP_TAG_LENGTH];
- for (size_t c = 0; c < AVIF_EXTENDED_XMP_GUID_LENGTH; ++c) {
+ const uint8_t * guid = &marker->data[AVIF_JPEG_EXTENDED_XMP_TAG_LENGTH];
+ for (size_t c = 0; c < AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH; ++c) {
// According to Adobe XMP Specification Part 3 section 1.1.3.1:
// "128-bit GUID stored as a 32-byte ASCII hex string, capital A-F, no null termination"
if (((guid[c] < '0') || (guid[c] > '9')) && ((guid[c] < 'A') || (guid[c] > 'F'))) {
@@ -431,17 +441,17 @@
}
}
// Size of the current extended segment.
- const size_t extendedXMPSize = marker->data_length - AVIF_OFFSET_TILL_EXTENDED_XMP;
+ const size_t extendedXMPSize = marker->data_length - AVIF_JPEG_OFFSET_TILL_EXTENDED_XMP;
// Expected size of the sum of all extended segments.
// According to Adobe XMP Specification Part 3 section 1.1.3.1:
// "full length of the ExtendedXMP serialization as a 32-bit unsigned integer"
const uint32_t totalExtendedXMPSize =
- avifJPEGReadUint32BigEndian(&marker->data[AVIF_EXTENDED_XMP_TAG_LENGTH + AVIF_EXTENDED_XMP_GUID_LENGTH]);
+ avifJPEGReadUint32BigEndian(&marker->data[AVIF_JPEG_EXTENDED_XMP_TAG_LENGTH + AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH]);
// Offset in totalXMP after standardXMP.
// According to Adobe XMP Specification Part 3 section 1.1.3.1:
// "offset of this portion as a 32-bit unsigned integer"
- const uint32_t extendedXMPOffset =
- avifJPEGReadUint32BigEndian(&marker->data[AVIF_EXTENDED_XMP_TAG_LENGTH + AVIF_EXTENDED_XMP_GUID_LENGTH + 4]);
+ const uint32_t extendedXMPOffset = avifJPEGReadUint32BigEndian(
+ &marker->data[AVIF_JPEG_EXTENDED_XMP_TAG_LENGTH + AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH + 4]);
if (((uint64_t)standardXMPSize + totalExtendedXMPSize) > SIZE_MAX) {
fprintf(stderr, "XMP extraction failed: total XMP size is too large\n");
goto cleanup;
@@ -451,7 +461,7 @@
goto cleanup;
}
if (foundExtendedXMP) {
- if (memcmp(guid, extendedXMPGUID, AVIF_EXTENDED_XMP_GUID_LENGTH)) {
+ if (memcmp(guid, extendedXMPGUID, AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH)) {
fprintf(stderr, "XMP extraction failed: extended XMP segment GUID mismatch\n");
goto cleanup;
}
@@ -460,7 +470,7 @@
goto cleanup;
}
} else {
- memcpy(extendedXMPGUID, guid, AVIF_EXTENDED_XMP_GUID_LENGTH);
+ memcpy(extendedXMPGUID, guid, AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH);
avifRWDataRealloc(&totalXMP, (size_t)standardXMPSize + totalExtendedXMPSize);
memcpy(totalXMP.data, standardXMPData, standardXMPSize);
@@ -473,7 +483,7 @@
}
// According to Adobe XMP Specification Part 3 section 1.1.3.1:
// "A robust JPEG reader should tolerate the marker segments in any order."
- memcpy(&totalXMP.data[standardXMPSize + extendedXMPOffset], &marker->data[AVIF_OFFSET_TILL_EXTENDED_XMP], extendedXMPSize);
+ memcpy(&totalXMP.data[standardXMPSize + extendedXMPOffset], &marker->data[AVIF_JPEG_OFFSET_TILL_EXTENDED_XMP], extendedXMPSize);
// Make sure no previously read data was overwritten by the current segment.
if (memchr(&extendedXMPReadBytes.data[extendedXMPOffset], 1, extendedXMPSize)) {
@@ -494,14 +504,14 @@
// According to Adobe XMP Specification Part 3 section 1.1.3.1:
// "A reader must incorporate only ExtendedXMP blocks whose GUID matches the value of xmpNote:HasExtendedXMP."
- uint8_t xmpNote[AVIF_XMP_NOTE_TAG_LENGTH + AVIF_EXTENDED_XMP_GUID_LENGTH];
- memcpy(xmpNote, AVIF_XMP_NOTE_TAG, AVIF_XMP_NOTE_TAG_LENGTH);
- memcpy(xmpNote + AVIF_XMP_NOTE_TAG_LENGTH, extendedXMPGUID, AVIF_EXTENDED_XMP_GUID_LENGTH);
+ uint8_t xmpNote[AVIF_JPEG_XMP_NOTE_TAG_LENGTH + AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH];
+ memcpy(xmpNote, AVIF_JPEG_XMP_NOTE_TAG, AVIF_JPEG_XMP_NOTE_TAG_LENGTH);
+ memcpy(xmpNote + AVIF_JPEG_XMP_NOTE_TAG_LENGTH, extendedXMPGUID, AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH);
if (!avifJPEGFindSubstr(standardXMPData, standardXMPSize, xmpNote, sizeof(xmpNote))) {
// Try the alternative before returning an error.
- uint8_t alternativeXmpNote[AVIF_ALTERNATIVE_XMP_NOTE_TAG_LENGTH + AVIF_EXTENDED_XMP_GUID_LENGTH];
- memcpy(alternativeXmpNote, AVIF_ALTERNATIVE_XMP_NOTE_TAG, AVIF_ALTERNATIVE_XMP_NOTE_TAG_LENGTH);
- memcpy(alternativeXmpNote + AVIF_ALTERNATIVE_XMP_NOTE_TAG_LENGTH, extendedXMPGUID, AVIF_EXTENDED_XMP_GUID_LENGTH);
+ uint8_t alternativeXmpNote[AVIF_JPEG_ALTERNATIVE_XMP_NOTE_TAG_LENGTH + AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH];
+ memcpy(alternativeXmpNote, AVIF_JPEG_ALTERNATIVE_XMP_NOTE_TAG, AVIF_JPEG_ALTERNATIVE_XMP_NOTE_TAG_LENGTH);
+ memcpy(alternativeXmpNote + AVIF_JPEG_ALTERNATIVE_XMP_NOTE_TAG_LENGTH, extendedXMPGUID, AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH);
if (!avifJPEGFindSubstr(standardXMPData, standardXMPSize, alternativeXmpNote, sizeof(alternativeXmpNote))) {
fprintf(stderr, "XMP extraction failed: standard and extended XMP GUID mismatch\n");
goto cleanup;
@@ -571,9 +581,72 @@
jpeg_start_compress(&cinfo, TRUE);
if (avif->icc.data && (avif->icc.size > 0)) {
+ // TODO(yguyon): Use jpeg_write_icc_profile() instead?
write_icc_profile(&cinfo, avif->icc.data, (unsigned int)avif->icc.size);
}
+ if (avif->exif.data && (avif->exif.size > 0)) {
+ size_t exifTiffHeaderOffset;
+ avifResult result = avifGetExifTiffHeaderOffset(avif->exif.data, avif->exif.size, &exifTiffHeaderOffset);
+ if (result != AVIF_RESULT_OK) {
+ fprintf(stderr, "Error writing JPEG metadata: %s\n", avifResultToString(result));
+ goto cleanup;
+ }
+
+ avifRWData exif = { NULL, 0 };
+ avifRWDataRealloc(&exif, AVIF_JPEG_EXIF_HEADER_LENGTH + avif->exif.size - exifTiffHeaderOffset);
+ memcpy(exif.data, AVIF_JPEG_EXIF_HEADER, AVIF_JPEG_EXIF_HEADER_LENGTH);
+ memcpy(exif.data + AVIF_JPEG_EXIF_HEADER_LENGTH, avif->exif.data + exifTiffHeaderOffset, avif->exif.size - exifTiffHeaderOffset);
+ // Make sure the Exif orientation matches the irot/imir values.
+ // libheif does not have the same behavior. The orientation is applied to samples and orientation data is discarded there,
+ // see https://github.com/strukturag/libheif/blob/ea78603d8e47096606813d221725621306789ff2/examples/encoder_jpeg.cc#L187
+ result = avifSetExifOrientation(&exif, avifImageGetExifOrientationFromIrotImir(avif));
+ if (result != AVIF_RESULT_OK) {
+ // Ignore errors if the orientation is the default one because not being able to set Exif orientation now
+ // means a reader will not be able to parse it later either.
+ if (avifImageGetExifOrientationFromIrotImir(avif) != 1) {
+ fprintf(stderr, "Error writing JPEG metadata: %s\n", avifResultToString(result));
+ avifRWDataFree(&exif);
+ goto cleanup;
+ }
+ }
+
+ avifROData remainingExif = { exif.data, exif.size };
+ while (remainingExif.size > AVIF_JPEG_MAX_MARKER_DATA_LENGTH) {
+ jpeg_write_marker(&cinfo, JPEG_APP0 + 1, remainingExif.data, AVIF_JPEG_MAX_MARKER_DATA_LENGTH);
+ remainingExif.data += AVIF_JPEG_MAX_MARKER_DATA_LENGTH;
+ remainingExif.size -= AVIF_JPEG_MAX_MARKER_DATA_LENGTH;
+ }
+ jpeg_write_marker(&cinfo, JPEG_APP0 + 1, remainingExif.data, (unsigned int)remainingExif.size);
+ avifRWDataFree(&exif);
+ } else if (avifImageGetExifOrientationFromIrotImir(avif) != 1) {
+ // There is no Exif yet, but we need to store the orientation.
+ // TODO(yguyon): Add a valid Exif payload or rotate the samples.
+ }
+
+ if (avif->xmp.data && (avif->xmp.size > 0)) {
+ // See XMP specification part 3.
+ if (avif->xmp.size > 65502) {
+ // libheif just refuses to export JPEG with long XMP, see
+ // https://github.com/strukturag/libheif/blob/18291ddebc23c924440a8a3c9a7267fe3beb5901/examples/encoder_jpeg.cc#L227
+ // But libheif also ignores extended XMP at reading, so converting a JPEG with extended XMP to HEIC and back to JPEG
+ // works, with the extended XMP part dropped, even if it had fit into a single JPEG marker.
+
+ // In libavif the whole XMP payload is dropped if it exceeds a single JPEG marker size limit, with a warning.
+ // The advantage is that it keeps the whole XMP payload, including the extended part, if it fits into a single JPEG
+ // marker. This is acceptable because section 1.1.3.1 of XMP specification part 3 says
+ // "It is unusual for XMP to exceed 65502 bytes; typically, it is around 2 KB."
+ fprintf(stderr, "Warning writing JPEG metadata: XMP payload is too big and was dropped\n");
+ } else {
+ avifRWData xmp = { NULL, 0 };
+ avifRWDataRealloc(&xmp, AVIF_JPEG_STANDARD_XMP_TAG_LENGTH + avif->xmp.size);
+ memcpy(xmp.data, AVIF_JPEG_STANDARD_XMP_TAG, AVIF_JPEG_STANDARD_XMP_TAG_LENGTH);
+ memcpy(xmp.data + AVIF_JPEG_STANDARD_XMP_TAG_LENGTH, avif->xmp.data, avif->xmp.size);
+ jpeg_write_marker(&cinfo, JPEG_APP0 + 1, xmp.data, (unsigned int)xmp.size);
+ avifRWDataFree(&xmp);
+ }
+ }
+
while (cinfo.next_scanline < cinfo.image_height) {
row_pointer[0] = &rgb.pixels[cinfo.next_scanline * rgb.rowBytes];
(void)jpeg_write_scanlines(&cinfo, row_pointer, 1);
diff --git a/apps/shared/avifpng.c b/apps/shared/avifpng.c
index bf8a8e8..b4e0283 100644
--- a/apps/shared/avifpng.c
+++ b/apps/shared/avifpng.c
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: BSD-2-Clause
#include "avifpng.h"
+#include "avif/internal.h"
#include "avifutil.h"
#include "png.h"
@@ -20,6 +21,9 @@
typedef png_charp png_iccp_datap;
#endif
+//------------------------------------------------------------------------------
+// Reading
+
// Converts a hexadecimal string which contains 2-byte character representations of hexadecimal values to raw data (bytes).
// hexString may contain values consisting of [A-F][a-f][0-9] in pairs, e.g., 7af2..., separated by any number of newlines.
// On success the bytes are filled and AVIF_TRUE is returned.
@@ -122,7 +126,17 @@
fprintf(stderr, "Exif extraction failed: empty eXIf chunk\n");
return AVIF_FALSE;
}
- avifImageSetMetadataExif(avif, exif, exifSize);
+ // Avoid avifImageSetMetadataExif() that sets irot/imir.
+ avifRWDataSet(&avif->exif, exif, exifSize);
+ // According to the Extensions to the PNG 1.2 Specification, Version 1.5.0, section 3.7:
+ // "It is recommended that unless a decoder has independent knowledge of the validity of the Exif data,
+ // the data should be considered to be of historical value only."
+ // Try to remove any Exif orientation data to be safe.
+ // It is easier to set it to 1 (the default top-left) than actually removing the tag.
+ // libheif has the same behavior, see
+ // https://github.com/strukturag/libheif/blob/18291ddebc23c924440a8a3c9a7267fe3beb5901/examples/heif_enc.cc#L703
+ // Ignore errors because not being able to set Exif orientation now means it cannot be parsed later either.
+ (void)avifSetExifOrientation(&avif->exif, 1);
*ignoreExif = AVIF_TRUE; // Ignore any other Exif chunk.
}
}
@@ -151,6 +165,7 @@
return AVIF_FALSE;
}
avifRemoveHeader(&exifApp1Header, &avif->exif); // Ignore the return value because the header is optional.
+ (void)avifSetExifOrientation(&avif->exif, 1); // See above.
*ignoreExif = AVIF_TRUE; // Ignore any other Exif chunk.
} else if (!*ignoreXMP && !strcmp(text->key, "Raw profile type xmp")) {
if (!avifCopyRawProfile(text->text, textLength, &avif->xmp)) {
@@ -167,7 +182,8 @@
if (!*ignoreExif && avifRemoveHeader(&exifApp1Header, &metadata)) {
avifRWDataFree(&avif->exif);
avif->exif = metadata;
- *ignoreExif = AVIF_TRUE; // Ignore any other Exif chunk.
+ (void)avifSetExifOrientation(&avif->exif, 1); // See above.
+ *ignoreExif = AVIF_TRUE; // Ignore any other Exif chunk.
} else if (!*ignoreXMP && avifRemoveHeader(&xmpApp1Header, &metadata)) {
avifRWDataFree(&avif->xmp);
avif->xmp = metadata;
@@ -368,11 +384,34 @@
return readResult;
}
+//------------------------------------------------------------------------------
+// Writing
+
+// Does the opposite of avifCopyRawProfile(): writes a payload as a raw profile string.
+static avifBool avifBytesToRawProfile(const avifRWData * bytes, const char * profileName, avifRWData * profile)
+{
+ // The width of the profile length is 8 characters.
+ if (bytes->size > 99999999) {
+ return AVIF_FALSE;
+ }
+ size_t position = 1 + strlen(profileName) + 1 + 8 + 1;
+ const size_t profileLength = position + bytes->size * 2 + 1;
+ avifRWDataRealloc(profile, profileLength);
+ snprintf((char *)profile->data, position + 1, "\n%s\n%08lu\n", profileName, (unsigned long)bytes->size);
+ for (size_t i = 0; i < bytes->size; ++i, position += 2) {
+ snprintf((char *)profile->data + position, 2 + 1, "%02x", bytes->data[i]);
+ }
+ profile->data[position] = '\n';
+ return AVIF_TRUE;
+}
+
avifBool avifPNGWrite(const char * outputFilename, const avifImage * avif, uint32_t requestedDepth, avifChromaUpsampling chromaUpsampling, int compressionLevel)
{
volatile avifBool writeResult = AVIF_FALSE;
png_structp png = NULL;
png_infop info = NULL;
+ avifRWData exif = { NULL, 0 };
+ avifRWData xmp = { NULL, 0 };
png_bytep * volatile rowPointers = NULL;
FILE * volatile f = NULL;
@@ -448,6 +487,68 @@
if (avif->icc.data && (avif->icc.size > 0)) {
png_set_iCCP(png, info, "libavif", 0, (png_iccp_datap)avif->icc.data, (png_uint_32)avif->icc.size);
}
+
+ png_text texts[2];
+ int numTextMetadataChunks = 0;
+ if (avif->exif.data && (avif->exif.size > 0)) {
+#ifdef PNG_eXIf_SUPPORTED
+ if (avif->exif.size > UINT32_MAX) {
+ fprintf(stderr, "Error writing PNG: Exif metadata is too big\n");
+ goto cleanup;
+ }
+ png_set_eXIf_1(png, info, (png_uint_32)avif->exif.size, avif->exif.data);
+#else
+ if (!avifBytesToRawProfile(&avif->exif, "Exif", &exif)) {
+ fprintf(stderr, "Error writing PNG: Exif metadata is too big\n");
+ goto cleanup;
+ }
+ png_text * text = &texts[numTextMetadataChunks++];
+ memset(text, 0, sizeof(*text));
+ text->compression = PNG_TEXT_COMPRESSION_zTXt;
+ text->key = "Raw profile type exif";
+ text->text = (char *)exif.data;
+ text->text_length = exif.size;
+#endif // PNG_eXIf_SUPPORTED
+ }
+ if (avif->xmp.data && (avif->xmp.size > 0)) {
+#ifdef PNG_iTXt_SUPPORTED
+ // The iTXt XMP payload may not contain a zero byte according to section 4.2.3.3 of
+ // the PNG specification, version 1.2. Otherwise, use a raw profile.
+ if (!memchr(avif->xmp.data, '\0', avif->xmp.size)) {
+ // Providing the length through png_text.itxt_length does not work.
+ // The given png_text.text string must end with a zero byte.
+ if (avif->xmp.size >= SIZE_MAX) {
+ fprintf(stderr, "Error writing PNG: XMP metadata is too big\n");
+ goto cleanup;
+ }
+ avifRWDataRealloc(&xmp, avif->xmp.size + 1);
+ memcpy(xmp.data, avif->xmp.data, avif->xmp.size);
+ xmp.data[avif->xmp.size] = '\0';
+ png_text * text = &texts[numTextMetadataChunks++];
+ memset(text, 0, sizeof(*text));
+ text->compression = PNG_ITXT_COMPRESSION_NONE;
+ text->key = "XML:com.adobe.xmp";
+ text->text = (char *)xmp.data;
+ text->itxt_length = xmp.size;
+ } else
+#endif // PNG_iTXt_SUPPORTED
+ {
+ if (!avifBytesToRawProfile(&avif->xmp, "XMP", &xmp)) {
+ fprintf(stderr, "Error writing PNG: XMP metadata is too big\n");
+ goto cleanup;
+ }
+ png_text * text = &texts[numTextMetadataChunks++];
+ memset(text, 0, sizeof(*text));
+ text->compression = PNG_TEXT_COMPRESSION_zTXt;
+ text->key = "Raw profile type xmp";
+ text->text = (char *)xmp.data;
+ text->text_length = xmp.size;
+ }
+ }
+ if (numTextMetadataChunks != 0) {
+ png_set_text(png, info, texts, numTextMetadataChunks);
+ }
+
png_write_info(png, info);
rowPointers = (png_bytep *)malloc(sizeof(png_bytep) * avif->height);
@@ -462,6 +563,9 @@
rowPointers[y] = &rgb.pixels[y * rgb.rowBytes];
}
}
+ if (avifImageGetExifOrientationFromIrotImir(avif) != 1) {
+ // TODO(yguyon): Rotate the samples.
+ }
if (rgbDepth > 8) {
png_set_swap(png);
@@ -479,6 +583,8 @@
if (png) {
png_destroy_write_struct(&png, &info);
}
+ avifRWDataFree(&exif);
+ avifRWDataFree(&xmp);
if (rowPointers) {
free(rowPointers);
}
diff --git a/include/avif/internal.h b/include/avif/internal.h
index 4fee2a1..04d2619 100644
--- a/include/avif/internal.h
+++ b/include/avif/internal.h
@@ -207,11 +207,15 @@
// ---------------------------------------------------------------------------
// Metadata
-// Validates the first bytes of the Exif payload and finds the TIFF header offset.
-avifResult avifGetExifTiffHeaderOffset(const avifRWData * exif, uint32_t * offset);
+// Validates the first bytes of the Exif payload and finds the TIFF header offset (up to UINT32_MAX).
+avifResult avifGetExifTiffHeaderOffset(const uint8_t * exif, size_t exifSize, size_t * offset);
// Attempts to parse the image->exif payload for Exif orientation and sets image->transformFlags, image->irot and
// image->imir on success. Returns AVIF_RESULT_INVALID_EXIF_PAYLOAD on failure.
avifResult avifImageExtractExifOrientationToIrotImir(avifImage * image);
+// Converts image->transformFlags, image->irot and image->imir to the equivalent Exif orientation value in [1:8].
+uint8_t avifImageGetExifOrientationFromIrotImir(const avifImage * image);
+// Attempts to parse the image->exif payload until the Exif orientation is found, then sets it to the given value.
+avifResult avifSetExifOrientation(avifRWData * exif, uint8_t orientation);
// ---------------------------------------------------------------------------
// avifCodecDecodeInput
diff --git a/src/exif.c b/src/exif.c
index 4bdbd7a..2dbdfd8 100644
--- a/src/exif.c
+++ b/src/exif.c
@@ -6,12 +6,13 @@
#include <stdint.h>
#include <string.h>
-avifResult avifGetExifTiffHeaderOffset(const avifRWData * exif, uint32_t * offset)
+avifResult avifGetExifTiffHeaderOffset(const uint8_t * exif, size_t exifSize, size_t * offset)
{
const uint8_t tiffHeaderBE[4] = { 'M', 'M', 0, 42 };
const uint8_t tiffHeaderLE[4] = { 'I', 'I', 42, 0 };
- for (*offset = 0; *offset + 4 < exif->size; ++*offset) {
- if (!memcmp(&exif->data[*offset], tiffHeaderBE, 4) || !memcmp(&exif->data[*offset], tiffHeaderLE, 4)) {
+ exifSize = AVIF_MIN(exifSize, UINT32_MAX);
+ for (*offset = 0; *offset + 4 < exifSize; ++*offset) {
+ if (!memcmp(&exif[*offset], tiffHeaderBE, 4) || !memcmp(&exif[*offset], tiffHeaderLE, 4)) {
return AVIF_RESULT_OK;
}
}
@@ -19,17 +20,17 @@
return AVIF_RESULT_INVALID_EXIF_PAYLOAD;
}
-avifResult avifImageExtractExifOrientationToIrotImir(avifImage * image)
+// Returns the offset to the Exif 8-bit orientation value and AVIF_RESULT_OK, or an error.
+// If the offset is set to exifSize, there was no parsing error but no orientation tag was found.
+static avifResult avifGetExifOrientationOffset(const uint8_t * exif, size_t exifSize, size_t * offset)
{
- const avifTransformFlags otherFlags = image->transformFlags & ~(AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR);
- uint32_t offset;
- const avifResult result = avifGetExifTiffHeaderOffset(&image->exif, &offset);
+ const avifResult result = avifGetExifTiffHeaderOffset(exif, exifSize, offset);
if (result != AVIF_RESULT_OK) {
// Couldn't find the TIFF header
return result;
}
- avifROData raw = { image->exif.data + offset, image->exif.size - offset };
+ avifROData raw = { exif + *offset, exifSize - *offset };
const avifBool littleEndian = (raw.data[0] == 'I');
avifROStream stream;
avifROStreamStart(&stream, &raw, NULL, NULL);
@@ -59,56 +60,79 @@
// Orientation attribute according to JEITA CP-3451C section 4.6.4 (TIFF Rev. 6.0 Attribute Information):
const uint16_t shortType = 0x03;
if (tag == 0x0112 && type == shortType && count == 0x01) {
- // Mapping from Exif orientation as defined in JEITA CP-3451C section 4.6.4.A Orientation
- // to irot and imir boxes as defined in HEIF ISO/IEC 28002-12:2021 sections 6.5.10 and 6.5.12.
- switch (firstHalfOfValueOffset) {
- case 1: // The 0th row is at the visual top of the image, and the 0th column is the visual left-hand side.
- image->transformFlags = otherFlags;
- image->irot.angle = 0; // ignored
- image->imir.mode = 0; // ignored
- return AVIF_RESULT_OK;
- case 2: // The 0th row is at the visual top of the image, and the 0th column is the visual right-hand side.
- image->transformFlags = otherFlags | AVIF_TRANSFORM_IMIR;
- image->irot.angle = 0; // ignored
- image->imir.mode = 1;
- return AVIF_RESULT_OK;
- case 3: // The 0th row is at the visual bottom of the image, and the 0th column is the visual right-hand side.
- image->transformFlags = otherFlags | AVIF_TRANSFORM_IROT;
- image->irot.angle = 2;
- image->imir.mode = 0; // ignored
- return AVIF_RESULT_OK;
- case 4: // The 0th row is at the visual bottom of the image, and the 0th column is the visual left-hand side.
- image->transformFlags = otherFlags | AVIF_TRANSFORM_IMIR;
- image->irot.angle = 0; // ignored
- image->imir.mode = 0;
- return AVIF_RESULT_OK;
- case 5: // The 0th row is the visual left-hand side of the image, and the 0th column is the visual top.
- image->transformFlags = otherFlags | AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR;
- image->irot.angle = 1; // applied before imir according to MIAF spec ISO/IEC 28002-12:2021 - section 7.3.6.7
- image->imir.mode = 0;
- return AVIF_RESULT_OK;
- case 6: // The 0th row is the visual right-hand side of the image, and the 0th column is the visual top.
- image->transformFlags = otherFlags | AVIF_TRANSFORM_IROT;
- image->irot.angle = 3;
- image->imir.mode = 0; // ignored
- return AVIF_RESULT_OK;
- case 7: // The 0th row is the visual right-hand side of the image, and the 0th column is the visual bottom.
- image->transformFlags = otherFlags | AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR;
- image->irot.angle = 3; // applied before imir according to MIAF spec ISO/IEC 28002-12:2021 - section 7.3.6.7
- image->imir.mode = 0;
- return AVIF_RESULT_OK;
- case 8: // The 0th row is the visual left-hand side of the image, and the 0th column is the visual bottom.
- image->transformFlags = otherFlags | AVIF_TRANSFORM_IROT;
- image->irot.angle = 1;
- image->imir.mode = 0; // ignored
- return AVIF_RESULT_OK;
- default: // reserved
- break;
+ // Only consider non-reserved orientation values, so that it is known that
+ // the most meaningful byte of firstHalfOfValueOffset is 0.
+ if (firstHalfOfValueOffset >= 1 && firstHalfOfValueOffset <= 8) {
+ // Offset to the least meaningful byte of firstHalfOfValueOffset.
+ *offset += avifROStreamOffset(&stream) - (littleEndian ? 4 : 3);
+ return AVIF_RESULT_OK;
}
}
}
// Orientation is in the 0th IFD, so no need to parse the following ones.
+ *offset = exifSize; // Signal missing orientation tag in valid Exif payload.
+ return AVIF_RESULT_OK;
+}
+
+avifResult avifImageExtractExifOrientationToIrotImir(avifImage * image)
+{
+ const avifTransformFlags otherFlags = image->transformFlags & ~(AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR);
+ size_t offset;
+ const avifResult result = avifGetExifOrientationOffset(image->exif.data, image->exif.size, &offset);
+ if (result != AVIF_RESULT_OK) {
+ return result;
+ }
+ if (offset < image->exif.size) {
+ const uint8_t orientation = image->exif.data[offset];
+ // Mapping from Exif orientation as defined in JEITA CP-3451C section 4.6.4.A Orientation
+ // to irot and imir boxes as defined in HEIF ISO/IEC 28002-12:2021 sections 6.5.10 and 6.5.12.
+ switch (orientation) {
+ case 1: // The 0th row is at the visual top of the image, and the 0th column is the visual left-hand side.
+ image->transformFlags = otherFlags;
+ image->irot.angle = 0; // ignored
+ image->imir.mode = 0; // ignored
+ return AVIF_RESULT_OK;
+ case 2: // The 0th row is at the visual top of the image, and the 0th column is the visual right-hand side.
+ image->transformFlags = otherFlags | AVIF_TRANSFORM_IMIR;
+ image->irot.angle = 0; // ignored
+ image->imir.mode = 1;
+ return AVIF_RESULT_OK;
+ case 3: // The 0th row is at the visual bottom of the image, and the 0th column is the visual right-hand side.
+ image->transformFlags = otherFlags | AVIF_TRANSFORM_IROT;
+ image->irot.angle = 2;
+ image->imir.mode = 0; // ignored
+ return AVIF_RESULT_OK;
+ case 4: // The 0th row is at the visual bottom of the image, and the 0th column is the visual left-hand side.
+ image->transformFlags = otherFlags | AVIF_TRANSFORM_IMIR;
+ image->irot.angle = 0; // ignored
+ image->imir.mode = 0;
+ return AVIF_RESULT_OK;
+ case 5: // The 0th row is the visual left-hand side of the image, and the 0th column is the visual top.
+ image->transformFlags = otherFlags | AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR;
+ image->irot.angle = 1; // applied before imir according to MIAF spec ISO/IEC 28002-12:2021 - section 7.3.6.7
+ image->imir.mode = 0;
+ return AVIF_RESULT_OK;
+ case 6: // The 0th row is the visual right-hand side of the image, and the 0th column is the visual top.
+ image->transformFlags = otherFlags | AVIF_TRANSFORM_IROT;
+ image->irot.angle = 3;
+ image->imir.mode = 0; // ignored
+ return AVIF_RESULT_OK;
+ case 7: // The 0th row is the visual right-hand side of the image, and the 0th column is the visual bottom.
+ image->transformFlags = otherFlags | AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR;
+ image->irot.angle = 3; // applied before imir according to MIAF spec ISO/IEC 28002-12:2021 - section 7.3.6.7
+ image->imir.mode = 0;
+ return AVIF_RESULT_OK;
+ case 8: // The 0th row is the visual left-hand side of the image, and the 0th column is the visual bottom.
+ image->transformFlags = otherFlags | AVIF_TRANSFORM_IROT;
+ image->irot.angle = 1;
+ image->imir.mode = 0; // ignored
+ return AVIF_RESULT_OK;
+ default: // reserved
+ break;
+ }
+ }
+
// The orientation tag is not mandatory (only recommended) according to JEITA CP-3451C section 4.6.8.A.
// The default value is 1 if the orientation tag is missing, meaning:
// The 0th row is at the visual top of the image, and the 0th column is the visual left-hand side.
@@ -118,6 +142,64 @@
return AVIF_RESULT_OK;
}
+uint8_t avifImageGetExifOrientationFromIrotImir(const avifImage * image)
+{
+ if ((image->transformFlags & AVIF_TRANSFORM_IROT) && (image->irot.angle == 1)) {
+ if (image->transformFlags & AVIF_TRANSFORM_IMIR) {
+ if (image->imir.mode) {
+ return 7; // 90 degrees anti-clockwise then swap left and right.
+ }
+ return 5; // 90 degrees anti-clockwise then swap top and bottom.
+ }
+ return 6; // 90 degrees anti-clockwise.
+ }
+ if ((image->transformFlags & AVIF_TRANSFORM_IROT) && (image->irot.angle == 2)) {
+ if (image->transformFlags & AVIF_TRANSFORM_IMIR) {
+ if (image->imir.mode) {
+ return 4; // 180 degrees anti-clockwise then swap left and right.
+ }
+ return 2; // 180 degrees anti-clockwise then swap top and bottom.
+ }
+ return 3; // 180 degrees anti-clockwise.
+ }
+ if ((image->transformFlags & AVIF_TRANSFORM_IROT) && (image->irot.angle == 3)) {
+ if (image->transformFlags & AVIF_TRANSFORM_IMIR) {
+ if (image->imir.mode) {
+ return 5; // 270 degrees anti-clockwise then swap left and right.
+ }
+ return 7; // 270 degrees anti-clockwise then swap top and bottom.
+ }
+ return 8; // 270 degrees anti-clockwise.
+ }
+ if (image->transformFlags & AVIF_TRANSFORM_IMIR) {
+ if (image->imir.mode) {
+ return 2; // Swap left and right.
+ }
+ return 4; // Swap top and bottom.
+ }
+ return 1; // Default orientation ("top-left", no-op).
+}
+
+avifResult avifSetExifOrientation(avifRWData * exif, uint8_t orientation)
+{
+ size_t offset;
+ const avifResult result = avifGetExifOrientationOffset(exif->data, exif->size, &offset);
+ if (result != AVIF_RESULT_OK) {
+ return result;
+ }
+ if (offset < exif->size) {
+ exif->data[offset] = orientation;
+ return AVIF_RESULT_OK;
+ }
+ // No Exif orientation was found.
+ if (orientation == 1) {
+ // The default orientation is 1, so if the given orientation is 1 too, do nothing.
+ return AVIF_RESULT_OK;
+ }
+ // Adding an orientation tag to an Exif payload is involved.
+ return AVIF_RESULT_NOT_IMPLEMENTED;
+}
+
void avifImageSetMetadataExif(avifImage * image, const uint8_t * exif, size_t exifSize)
{
avifRWDataSet(&image->exif, exif, exifSize);
diff --git a/src/write.c b/src/write.c
index 8b33b0c..9576b77 100644
--- a/src/write.c
+++ b/src/write.c
@@ -692,8 +692,8 @@
static avifResult avifEncoderDataCreateExifItem(avifEncoderData * data, const avifRWData * exif)
{
- uint32_t exifTiffHeaderOffset;
- const avifResult result = avifGetExifTiffHeaderOffset(exif, &exifTiffHeaderOffset);
+ size_t exifTiffHeaderOffset;
+ const avifResult result = avifGetExifTiffHeaderOffset(exif->data, exif->size, &exifTiffHeaderOffset);
if (result != AVIF_RESULT_OK) {
// Couldn't find the TIFF header
return result;
@@ -706,10 +706,10 @@
exifItem->irefToID = data->primaryItemID;
exifItem->irefType = "cdsc";
- avifRWDataRealloc(&exifItem->metadataPayload, sizeof(uint32_t) + exif->size);
- exifTiffHeaderOffset = avifHTONL(exifTiffHeaderOffset);
- memcpy(exifItem->metadataPayload.data, &exifTiffHeaderOffset, sizeof(uint32_t));
- memcpy(exifItem->metadataPayload.data + sizeof(uint32_t), exif->data, exif->size);
+ const uint32_t offset32bit = avifHTONL((uint32_t)exifTiffHeaderOffset);
+ avifRWDataRealloc(&exifItem->metadataPayload, sizeof(offset32bit) + exif->size);
+ memcpy(exifItem->metadataPayload.data, &offset32bit, sizeof(offset32bit));
+ memcpy(exifItem->metadataPayload.data + sizeof(offset32bit), exif->data, exif->size);
return AVIF_RESULT_OK;
}
diff --git a/tests/gtest/avifmetadatatest.cc b/tests/gtest/avifmetadatatest.cc
index 6b3300c..3bdc5a9 100644
--- a/tests/gtest/avifmetadatatest.cc
+++ b/tests/gtest/avifmetadatatest.cc
@@ -5,6 +5,8 @@
#include <tuple>
#include "avif/avif.h"
+#include "avifjpeg.h"
+#include "avifpng.h"
#include "aviftest_helpers.h"
#include "gtest/gtest.h"
@@ -112,21 +114,33 @@
//------------------------------------------------------------------------------
// Jpeg and PNG metadata tests
-class MetadataTest
- : public testing::TestWithParam<
- std::tuple</*file_name=*/const char*, /*use_icc=*/bool,
- /*use_exif=*/bool, /*use_xmp=*/bool, /*expect_icc=*/bool,
- /*expect_exif=*/bool, /*expect_xmp=*/bool>> {};
+testutil::AvifImagePtr WriteAndReadImage(const avifImage& image,
+ const std::string& file_name) {
+ const std::string file_path = testing::TempDir() + file_name;
+ if (file_name.substr(file_name.size() - 4) == ".png") {
+ if (!avifPNGWrite(file_path.c_str(), &image, /*requestedDepth=*/0,
+ AVIF_CHROMA_UPSAMPLING_AUTOMATIC,
+ /*compressionLevel=*/0)) {
+ return {nullptr, nullptr};
+ }
+ } else {
+ if (!avifJPEGWrite(file_path.c_str(), &image, /*jpegQuality=*/100,
+ AVIF_CHROMA_UPSAMPLING_AUTOMATIC)) {
+ return {nullptr, nullptr};
+ }
+ }
+ return testutil::ReadImage(testing::TempDir().c_str(), file_name.c_str());
+}
-// zTXt "Raw profile type exif" at the beginning of a PNG file.
-TEST_P(MetadataTest, Read) {
+class MetadataTest : public testing::TestWithParam<
+ std::tuple</*file_name=*/const char*, /*use_icc=*/bool,
+ /*use_exif=*/bool, /*use_xmp=*/bool>> {};
+
+TEST_P(MetadataTest, ReadWriteReadCompare) {
const char* file_name = std::get<0>(GetParam());
const bool use_icc = std::get<1>(GetParam());
const bool use_exif = std::get<2>(GetParam());
const bool use_xmp = std::get<3>(GetParam());
- const bool expect_icc = std::get<4>(GetParam());
- const bool expect_exif = std::get<5>(GetParam());
- const bool expect_xmp = std::get<6>(GetParam());
const testutil::AvifImagePtr image = testutil::ReadImage(
data_path, file_name, AVIF_PIXEL_FORMAT_NONE, 0,
@@ -134,55 +148,45 @@
ASSERT_NE(image, nullptr);
EXPECT_NE(image->width * image->height, 0u);
- if (expect_icc) {
+ if (use_icc) {
EXPECT_NE(image->icc.size, 0u);
EXPECT_NE(image->icc.data, nullptr);
} else {
EXPECT_EQ(image->icc.size, 0u);
EXPECT_EQ(image->icc.data, nullptr);
}
- if (expect_exif) {
+ if (use_exif) {
EXPECT_NE(image->exif.size, 0u);
EXPECT_NE(image->exif.data, nullptr);
} else {
EXPECT_EQ(image->exif.size, 0u);
EXPECT_EQ(image->exif.data, nullptr);
}
- if (expect_xmp) {
+ if (use_xmp) {
EXPECT_NE(image->xmp.size, 0u);
EXPECT_NE(image->xmp.data, nullptr);
} else {
EXPECT_EQ(image->xmp.size, 0u);
EXPECT_EQ(image->xmp.data, nullptr);
}
+
+ // Writing and reading that same metadata should give the same bytes.
+ for (const std::string extension : {".png", ".jpg"}) {
+ const testutil::AvifImagePtr temp_image =
+ WriteAndReadImage(*image, file_name + extension);
+ ASSERT_NE(temp_image, nullptr);
+ ASSERT_TRUE(testutil::AreByteSequencesEqual(image->icc, temp_image->icc));
+ ASSERT_TRUE(testutil::AreByteSequencesEqual(image->exif, temp_image->exif));
+ ASSERT_TRUE(testutil::AreByteSequencesEqual(image->xmp, temp_image->xmp));
+ }
}
INSTANTIATE_TEST_SUITE_P(
- PngNone, MetadataTest,
- Combine(Values("paris_icc_exif_xmp.png"), // iCCP zTXt zTXt IDAT
- /*use_icc=*/Values(false), /*use_exif=*/Values(false),
- /*use_xmp=*/Values(false), /*expected_icc=*/Values(false),
- /*expected_exif=*/Values(false), /*expected_xmp=*/Values(false)));
-INSTANTIATE_TEST_SUITE_P(
- PngAll, MetadataTest,
- Combine(Values("paris_icc_exif_xmp.png"), // iCCP zTXt zTXt IDAT
- /*use_icc=*/Values(true), /*use_exif=*/Values(true),
- /*use_xmp=*/Values(true), /*expected_icc=*/Values(true),
- /*expected_exif=*/Values(true), /*expected_xmp=*/Values(true)));
-
-INSTANTIATE_TEST_SUITE_P(
- PngExifAtEnd, MetadataTest,
- Combine(Values("paris_icc_exif_xmp_at_end.png"), // iCCP IDAT eXIf tEXt
- /*use_icc=*/Values(true), /*use_exif=*/Values(true),
- /*use_xmp=*/Values(true), /*expected_icc=*/Values(true),
- /*expected_exif=*/Values(true), /*expected_xmp=*/Values(true)));
-
-INSTANTIATE_TEST_SUITE_P(
- Jpeg, MetadataTest,
- Combine(Values("paris_exif_xmp_icc.jpg"), // APP1-Exif, APP1-XMP, APP2-ICC
- /*use_icc=*/Values(true), /*use_exif=*/Values(true),
- /*use_xmp=*/Values(true), /*expected_icc=*/Values(true),
- /*expected_exif=*/Values(true), /*expected_xmp=*/Values(true)));
+ PngJpeg, MetadataTest,
+ Combine(Values("paris_icc_exif_xmp.png", // iCCP zTXt zTXt IDAT
+ "paris_icc_exif_xmp_at_end.png", // iCCP IDAT eXIf tEXt
+ "paris_exif_xmp_icc.jpg"), // APP1-Exif, APP1-XMP, APP2-ICC
+ /*use_icc=*/Bool(), /*use_exif=*/Bool(), /*use_xmp=*/Bool()));
// Verify all parsers lead exactly to the same metadata bytes.
TEST(MetadataTest, Compare) {
@@ -269,6 +273,26 @@
avifTransformFlags{AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR});
EXPECT_EQ(decoded->irot.angle, 1u);
EXPECT_EQ(decoded->imir.mode, 0u);
+
+ // Exif orientation is kept in JPEG export.
+ testutil::AvifImagePtr temp_image =
+ WriteAndReadImage(*image, "paris_exif_orientation_5.jpg");
+ ASSERT_NE(temp_image, nullptr);
+ EXPECT_TRUE(testutil::AreByteSequencesEqual(image->exif, temp_image->exif));
+ EXPECT_EQ(image->transformFlags, temp_image->transformFlags);
+ EXPECT_EQ(image->irot.angle, temp_image->irot.angle);
+ EXPECT_EQ(image->imir.mode, temp_image->imir.mode);
+ EXPECT_EQ(image->width, temp_image->width); // Samples are left untouched.
+
+ // Exif orientation in PNG export should be ignored or discarded.
+ temp_image = WriteAndReadImage(*image, "paris_exif_orientation_5.png");
+ ASSERT_NE(temp_image, nullptr);
+ EXPECT_FALSE(testutil::AreByteSequencesEqual(image->exif, temp_image->exif));
+ EXPECT_EQ(
+ temp_image->transformFlags & (AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR),
+ avifTransformFlags{0});
+ // TODO(yguyon): Fix orientation not being applied to PNG samples.
+ EXPECT_EQ(image->width, temp_image->width /* should be height here */);
}
TEST(MetadataTest, ExifOrientationAndForcedImir) {
@@ -293,6 +317,39 @@
EXPECT_EQ(decoded->transformFlags, avifTransformFlags{AVIF_TRANSFORM_IMIR});
EXPECT_EQ(decoded->irot.angle, 0u);
EXPECT_EQ(decoded->imir.mode, image->imir.mode);
+
+ // Exif orientation is set equivalent to irot/imir in JPEG export.
+ // Existing Exif orientation is overwritten.
+ const testutil::AvifImagePtr temp_image =
+ WriteAndReadImage(*image, "paris_exif_orientation_2.jpg");
+ ASSERT_NE(temp_image, nullptr);
+ EXPECT_FALSE(testutil::AreByteSequencesEqual(image->exif, temp_image->exif));
+ EXPECT_EQ(image->transformFlags, temp_image->transformFlags);
+ EXPECT_EQ(image->imir.mode, temp_image->imir.mode);
+ EXPECT_EQ(image->width, temp_image->width); // Samples are left untouched.
+}
+
+TEST(MetadataTest, RotatedJpegBecauseOfIrotImir) {
+ const testutil::AvifImagePtr image =
+ testutil::ReadImage(data_path, "paris_exif_orientation_5.jpg");
+ ASSERT_NE(image, nullptr);
+ avifImageSetMetadataExif(image.get(), nullptr, 0); // Clear Exif.
+ // Orientation is kept in irot/imir.
+ EXPECT_EQ(image->transformFlags & (AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR),
+ avifTransformFlags{AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR});
+ EXPECT_EQ(image->irot.angle, 1u);
+ EXPECT_EQ(image->imir.mode, 0u);
+
+ // No Exif metadata to store the orientation: the samples should be rotated.
+ const testutil::AvifImagePtr temp_image =
+ WriteAndReadImage(*image, "paris_exif_orientation_5.jpg");
+ ASSERT_NE(temp_image, nullptr);
+ EXPECT_EQ(temp_image->exif.size, 0u);
+ EXPECT_EQ(
+ temp_image->transformFlags & (AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR),
+ avifTransformFlags{0});
+ // TODO(yguyon): Fix orientation not being applied to JPEG samples.
+ EXPECT_EQ(image->width, temp_image->width /* should be height here */);
}
TEST(MetadataTest, ExifIfdOffsetLoopingTo8) {
@@ -326,6 +383,15 @@
testutil::ReadImage(data_path, "dog_exif_extended_xmp_icc.jpg");
ASSERT_NE(image, nullptr);
ASSERT_NE(image->xmp.size, 0u);
+ ASSERT_LT(image->xmp.size,
+ size_t{65503}); // Fits in a single JPEG APP1 marker.
+
+ for (const char* temp_file_name : {"dog.png", "dog.jpg"}) {
+ const testutil::AvifImagePtr temp_image =
+ WriteAndReadImage(*image, temp_file_name);
+ ASSERT_NE(temp_image, nullptr);
+ EXPECT_TRUE(testutil::AreByteSequencesEqual(image->xmp, temp_image->xmp));
+ }
}
TEST(MetadataTest, MultipleExtendedXMPAndAlternativeGUIDTag) {
@@ -333,6 +399,16 @@
testutil::ReadImage(data_path, "paris_extended_xmp.jpg");
ASSERT_NE(image, nullptr);
ASSERT_GT(image->xmp.size, size_t{65536 * 2});
+
+ testutil::AvifImagePtr temp_image =
+ WriteAndReadImage(*image, "paris_extended_xmp.png");
+ ASSERT_NE(temp_image, nullptr);
+ EXPECT_TRUE(testutil::AreByteSequencesEqual(image->xmp, temp_image->xmp));
+
+ // Writing more than 65502 bytes of XMP in a JPEG is not supported.
+ temp_image = WriteAndReadImage(*image, "paris_extended_xmp.jpg");
+ ASSERT_NE(temp_image, nullptr);
+ ASSERT_EQ(temp_image->xmp.size, 0u); // XMP was dropped.
}
//------------------------------------------------------------------------------