Add avifImageExtractExifOrientationToIrotImir()
Move avifGetExifTiffHeaderOffset() from write.c to exif.c.
Add tests in avifmetadatatest.
Add avifenc change to CHANGELOG.md.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ff86177..773127b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -27,6 +27,7 @@
and avifRGBImage::avoidLibYUV to AVIF_TRUE.
* avifRGBImage::chromaUpsampling now only applies to conversions that need
upsampling chroma from 4:2:0 or 4:2:2 and has no impact on the use of libyuv.
+* Exif and XMP metadata is imported from PNG and JPEG files.
### Removed
* alphaRange field was removed from the avifImage struct. It it presumed that
@@ -38,6 +39,7 @@
* Add imageDimensionLimit field to avifDecoder struct
* Add autoTiling field to avifEncoder struct
* Add AVIF_CHROMA_DOWNSAMPLING_SHARP_YUV value to avifChromaDownsampling enum
+* Add avifImageExtractExifOrientationToIrotImir()
* avifdec: Add --dimension-limit, which specifies the image dimension limit
(width or height) that should be tolerated
* avifenc: Add --sharpyuv, which enables "sharp" RGB to YUV420 conversion, which
@@ -45,7 +47,8 @@
of the libwebp repository) at compile time.
* avifenc: Add --ignore-exif and --ignore-xmp flags.
* avifenc: Add --autotiling, which sets --tilerowslog2 and --tilecolslog2
- automatically
+ automatically.
+* avifenc: Input Exif orientation is converted to irot/imir by default.
## [0.10.1] - 2022-04-11
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 37e22ee..b20794c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -195,6 +195,7 @@
src/avif.c
src/colr.c
src/diag.c
+ src/exif.c
src/io.c
src/mem.c
src/obu.c
diff --git a/apps/avifenc.c b/apps/avifenc.c
index 918d521..84fbd8b 100644
--- a/apps/avifenc.c
+++ b/apps/avifenc.c
@@ -891,6 +891,8 @@
}
if (exifOverride.size) {
avifImageSetMetadataExif(image, exifOverride.data, exifOverride.size);
+ // Ignore any Exif parsing failure.
+ (void)avifImageExtractExifOrientationToIrotImir(image);
}
if (xmpOverride.size) {
avifImageSetMetadataXMP(image, xmpOverride.data, xmpOverride.size);
diff --git a/apps/shared/avifjpeg.c b/apps/shared/avifjpeg.c
index 2e2bd60..b20399e 100644
--- a/apps/shared/avifjpeg.c
+++ b/apps/shared/avifjpeg.c
@@ -347,6 +347,8 @@
goto cleanup;
}
avifImageSetMetadataExif(avif, marker->data + tagExif.size, marker->data_length - tagExif.size);
+ // Ignore any Exif parsing failure.
+ (void)avifImageExtractExifOrientationToIrotImir(avif);
found = AVIF_TRUE;
}
}
diff --git a/apps/shared/avifpng.c b/apps/shared/avifpng.c
index d40ab1a..9512585 100644
--- a/apps/shared/avifpng.c
+++ b/apps/shared/avifpng.c
@@ -123,6 +123,8 @@
return AVIF_FALSE;
}
avifImageSetMetadataExif(avif, exif, exifSize);
+ // Ignore any Exif parsing failure.
+ (void)avifImageExtractExifOrientationToIrotImir(avif);
*ignoreExif = AVIF_TRUE; // Ignore any other Exif chunk.
}
}
diff --git a/examples/avif_example_encode.c b/examples/avif_example_encode.c
index 34b6cdc..443aed8 100644
--- a/examples/avif_example_encode.c
+++ b/examples/avif_example_encode.c
@@ -31,7 +31,7 @@
// * transferCharacteristics
// * matrixCoefficients
// * avifImageSetProfileICC()
- // * avifImageSetMetadataExif()
+ // * avifImageSetMetadataExif() + avifImageExtractExifOrientationToIrotImir()
// * avifImageSetMetadataXMP()
// * yuvRange
// * alphaPremultiplied
diff --git a/include/avif/avif.h b/include/avif/avif.h
index 9e0203d..eead360 100644
--- a/include/avif/avif.h
+++ b/include/avif/avif.h
@@ -482,10 +482,17 @@
AVIF_API void avifImageSetProfileICC(avifImage * image, const uint8_t * icc, size_t iccSize);
-// Warning: If the Exif payload is set and invalid, avifEncoderWrite() may return AVIF_RESULT_INVALID_EXIF_PAYLOAD
+// Set Exif metadata. It is highly recommended to match image->transformFlags, image->irot and image->imir to any Exif
+// orientation contained in image->exif before encoding it to AVIF. See avifImageExtractExifOrientationToIrotImir().
+// Warning: If the Exif payload is set and invalid, avifEncoderWrite() may return AVIF_RESULT_INVALID_EXIF_PAYLOAD.
AVIF_API void avifImageSetMetadataExif(avifImage * image, const uint8_t * exif, size_t exifSize);
+// Set XMP metadata.
AVIF_API void avifImageSetMetadataXMP(avifImage * image, const uint8_t * xmp, size_t xmpSize);
+// 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);
+
AVIF_API avifResult avifImageAllocatePlanes(avifImage * image, avifPlanesFlags planes); // Ignores any pre-existing planes
AVIF_API void avifImageFreePlanes(avifImage * image, avifPlanesFlags planes); // Ignores already-freed planes
AVIF_API void avifImageStealPlanes(avifImage * dstImage, avifImage * srcImage, avifPlanesFlags planes);
diff --git a/include/avif/internal.h b/include/avif/internal.h
index 5cc5aab..ff01e45 100644
--- a/include/avif/internal.h
+++ b/include/avif/internal.h
@@ -42,10 +42,16 @@
float avifRoundf(float v);
+// H (host) is platform-dependent. Could be little- or big-endian.
+// N (network) is big-endian: most- to least-significant bytes.
+// C (custom) is little-endian: least- to most-significant bytes.
+// Never read N or C values; only access after casting to uint8_t*.
uint16_t avifHTONS(uint16_t s);
uint16_t avifNTOHS(uint16_t s);
+uint16_t avifCTOHS(uint16_t s);
uint32_t avifHTONL(uint32_t l);
uint32_t avifNTOHL(uint32_t l);
+uint32_t avifCTOHL(uint32_t l);
uint64_t avifHTON64(uint64_t l);
uint64_t avifNTOH64(uint64_t l);
@@ -199,6 +205,12 @@
avifBool avifAreGridDimensionsValid(avifPixelFormat yuvFormat, uint32_t imageW, uint32_t imageH, uint32_t tileW, uint32_t tileH, avifDiagnostics * diag);
// ---------------------------------------------------------------------------
+// Metadata
+
+// Validates the first bytes of the Exif payload and finds the TIFF header offset.
+avifResult avifGetExifTiffHeaderOffset(const avifRWData * exif, uint32_t * offset);
+
+// ---------------------------------------------------------------------------
// avifCodecDecodeInput
// Legal spatial_id values are [0,1,2,3], so this serves as a sentinel value for "do not filter by spatial_id"
@@ -354,7 +366,7 @@
void avifDiagnosticsPrintf(avifDiagnostics * diag, const char * format, ...);
// ---------------------------------------------------------------------------
-// avifStream
+// avifStream (network byte order; big-endian unless specified)
typedef size_t avifBoxMarker;
@@ -382,7 +394,9 @@
avifBool avifROStreamSkip(avifROStream * stream, size_t byteCount);
avifBool avifROStreamRead(avifROStream * stream, uint8_t * data, size_t size);
avifBool avifROStreamReadU16(avifROStream * stream, uint16_t * v);
+avifBool avifROStreamReadU16Endianness(avifROStream * stream, uint16_t * v, avifBool littleEndian);
avifBool avifROStreamReadU32(avifROStream * stream, uint32_t * v);
+avifBool avifROStreamReadU32Endianness(avifROStream * stream, uint32_t * v, avifBool littleEndian);
avifBool avifROStreamReadUX8(avifROStream * stream, uint64_t * v, uint64_t factor); // Reads a factor*8 sized uint, saves in v
avifBool avifROStreamReadU64(avifROStream * stream, uint64_t * v);
avifBool avifROStreamReadString(avifROStream * stream, char * output, size_t outputSize);
diff --git a/src/exif.c b/src/exif.c
new file mode 100644
index 0000000..fc73f8c
--- /dev/null
+++ b/src/exif.c
@@ -0,0 +1,119 @@
+// Copyright 2022 Google LLC. All rights reserved.
+// SPDX-License-Identifier: BSD-2-Clause
+
+#include "avif/internal.h"
+
+#include <stdint.h>
+#include <string.h>
+
+avifResult avifGetExifTiffHeaderOffset(const avifRWData * exif, uint32_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)) {
+ return AVIF_RESULT_OK;
+ }
+ }
+ // Couldn't find the TIFF header
+ return AVIF_RESULT_INVALID_EXIF_PAYLOAD;
+}
+
+avifResult avifImageExtractExifOrientationToIrotImir(avifImage * image)
+{
+ const avifTransformFlags otherFlags = image->transformFlags & ~(AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR);
+ uint32_t offset;
+ const avifResult result = avifGetExifTiffHeaderOffset(&image->exif, &offset);
+ if (result != AVIF_RESULT_OK) {
+ // Couldn't find the TIFF header
+ return result;
+ }
+
+ avifROData raw = { image->exif.data + offset, image->exif.size - offset };
+ const avifBool littleEndian = (raw.data[0] == 'I');
+ avifROStream stream;
+ avifROStreamStart(&stream, &raw, NULL, NULL);
+
+ // TIFF Header
+ uint32_t offsetTo0thIfd;
+ if (!avifROStreamSkip(&stream, 4) || // Skip tiffHeaderBE or tiffHeaderLE.
+ !avifROStreamReadU32Endianness(&stream, &offsetTo0thIfd, littleEndian)) {
+ return AVIF_RESULT_INVALID_EXIF_PAYLOAD;
+ }
+
+ avifROStreamSetOffset(&stream, offsetTo0thIfd);
+ uint16_t fieldCount;
+ if (!avifROStreamReadU16Endianness(&stream, &fieldCount, littleEndian)) {
+ return AVIF_RESULT_INVALID_EXIF_PAYLOAD;
+ }
+ for (uint16_t field = 0; field < fieldCount; ++field) { // for each field interoperability array
+ uint16_t tag;
+ uint16_t type;
+ uint32_t count;
+ uint16_t firstHalfOfValueOffset;
+ if (!avifROStreamReadU16Endianness(&stream, &tag, littleEndian) || !avifROStreamReadU16Endianness(&stream, &type, littleEndian) ||
+ !avifROStreamReadU32Endianness(&stream, &count, littleEndian) ||
+ !avifROStreamReadU16Endianness(&stream, &firstHalfOfValueOffset, littleEndian) || !avifROStreamSkip(&stream, 2)) {
+ return AVIF_RESULT_INVALID_EXIF_PAYLOAD;
+ }
+ // 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;
+ }
+ }
+ }
+ // Orientation is in the 0th IFD, so no need to parse the following ones.
+
+ // 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.
+ image->transformFlags = otherFlags;
+ image->irot.angle = 0; // ignored
+ image->imir.mode = 0; // ignored
+ return AVIF_RESULT_OK;
+}
diff --git a/src/stream.c b/src/stream.c
index a0cf021..a5935b4 100644
--- a/src/stream.c
+++ b/src/stream.c
@@ -108,6 +108,13 @@
return AVIF_TRUE;
}
+avifBool avifROStreamReadU16Endianness(avifROStream * stream, uint16_t * v, avifBool littleEndian)
+{
+ AVIF_CHECK(avifROStreamRead(stream, (uint8_t *)v, sizeof(uint16_t)));
+ *v = littleEndian ? avifCTOHS(*v) : avifNTOHS(*v);
+ return AVIF_TRUE;
+}
+
avifBool avifROStreamReadU32(avifROStream * stream, uint32_t * v)
{
AVIF_CHECK(avifROStreamRead(stream, (uint8_t *)v, sizeof(uint32_t)));
@@ -115,6 +122,13 @@
return AVIF_TRUE;
}
+avifBool avifROStreamReadU32Endianness(avifROStream * stream, uint32_t * v, avifBool littleEndian)
+{
+ AVIF_CHECK(avifROStreamRead(stream, (uint8_t *)v, sizeof(uint32_t)));
+ *v = littleEndian ? avifCTOHL(*v) : avifNTOHL(*v);
+ return AVIF_TRUE;
+}
+
avifBool avifROStreamReadU64(avifROStream * stream, uint64_t * v)
{
AVIF_CHECK(avifROStreamRead(stream, (uint8_t *)v, sizeof(uint64_t)));
diff --git a/src/utils.c b/src/utils.c
index 2e5cb7c..697cb0e 100644
--- a/src/utils.c
+++ b/src/utils.c
@@ -29,6 +29,12 @@
return (uint16_t)((data[1] << 0) | (data[0] << 8));
}
+uint16_t avifCTOHS(uint16_t s)
+{
+ const uint8_t * data = (const uint8_t *)&s;
+ return (uint16_t)((data[0] << 0) | (data[1] << 8));
+}
+
uint32_t avifHTONL(uint32_t l)
{
uint32_t result;
@@ -46,6 +52,12 @@
return ((uint32_t)data[3] << 0) | ((uint32_t)data[2] << 8) | ((uint32_t)data[1] << 16) | ((uint32_t)data[0] << 24);
}
+uint32_t avifCTOHL(uint32_t l)
+{
+ const uint8_t * data = (const uint8_t *)&l;
+ return ((uint32_t)data[0] << 0) | ((uint32_t)data[1] << 8) | ((uint32_t)data[2] << 16) | ((uint32_t)data[3] << 24);
+}
+
uint64_t avifHTON64(uint64_t l)
{
uint64_t result;
diff --git a/src/write.c b/src/write.c
index 12438ac..8f0bb30 100644
--- a/src/write.c
+++ b/src/write.c
@@ -690,27 +690,11 @@
static avifResult avifEncoderDataCreateExifItem(avifEncoderData * data, const avifRWData * exif)
{
- // Validate Exif payload (if any) and find TIFF header offset
- uint32_t exifTiffHeaderOffset = 0;
- if (exif->size < 4) {
- // Can't even fit the TIFF header, something is wrong
- return AVIF_RESULT_INVALID_EXIF_PAYLOAD;
- }
-
- const uint8_t tiffHeaderBE[4] = { 'M', 'M', 0, 42 };
- const uint8_t tiffHeaderLE[4] = { 'I', 'I', 42, 0 };
- for (; exifTiffHeaderOffset < (exif->size - 4); ++exifTiffHeaderOffset) {
- if (!memcmp(&exif->data[exifTiffHeaderOffset], tiffHeaderBE, sizeof(tiffHeaderBE))) {
- break;
- }
- if (!memcmp(&exif->data[exifTiffHeaderOffset], tiffHeaderLE, sizeof(tiffHeaderLE))) {
- break;
- }
- }
-
- if (exifTiffHeaderOffset >= exif->size - 4) {
+ uint32_t exifTiffHeaderOffset;
+ const avifResult result = avifGetExifTiffHeaderOffset(exif, &exifTiffHeaderOffset);
+ if (result != AVIF_RESULT_OK) {
// Couldn't find the TIFF header
- return AVIF_RESULT_INVALID_EXIF_PAYLOAD;
+ return result;
}
avifEncoderItem * exifItem = avifEncoderDataCreateItem(data, "Exif", "Exif", 5, 0);
diff --git a/tests/data/README.md b/tests/data/README.md
index 5d5228c..4044f84 100644
--- a/tests/data/README.md
+++ b/tests/data/README.md
@@ -86,6 +86,21 @@
| 152612 | tEXt | 7832 | `Raw profile type xmp..XMP.0000` |
| 160456 | IEND | 0 | |
+### File [paris_exif_orientation_5.jpg](paris_exif_orientation_5.jpg)
+
+![](paris_exif_orientation_5.jpg)
+
+License: [same as libavif](https://github.com/AOMediaCodec/libavif/blob/main/LICENSE)
+
+Source: `paris_exif_xmp_icc.jpg` stripped from all metadata with `exiftool -all=` and Exif orientation added
+with `exiv2 -k -M "set Exif.Image.Orientation 5"`
+
+| address | marker | length | data |
+|--------:|-------------|--------:|--------------------------------------|
+| 0 | 0xffd8 SOI | | |
+| 2 | 0xffe1 APP1 | 34 | `Exif..II*......................` |
+| | | | ... |
+
## Grid
### File [sofa_grid1x5_420.avif](sofa_grid1x5_420.avif)
diff --git a/tests/data/paris_exif_orientation_5.jpg b/tests/data/paris_exif_orientation_5.jpg
new file mode 100644
index 0000000..aed77d0
--- /dev/null
+++ b/tests/data/paris_exif_orientation_5.jpg
Binary files differ
diff --git a/tests/gtest/avifmetadatatest.cc b/tests/gtest/avifmetadatatest.cc
index 5017f13..7372b97 100644
--- a/tests/gtest/avifmetadatatest.cc
+++ b/tests/gtest/avifmetadatatest.cc
@@ -63,6 +63,9 @@
if (use_exif) {
avifImageSetMetadataExif(image.get(), kSampleExif.data(),
kSampleExif.size());
+ // kSampleExif is not a valid Exif payload, just some part of it.
+ ASSERT_EQ(avifImageExtractExifOrientationToIrotImir(image.get()),
+ AVIF_RESULT_INVALID_EXIF_PAYLOAD);
}
if (use_xmp) {
avifImageSetMetadataXMP(image.get(), kSampleXmp.data(), kSampleXmp.size());
@@ -214,6 +217,99 @@
//------------------------------------------------------------------------------
+TEST(MetadataTest, ExifButDefaultIrotImir) {
+ const testutil::AvifImagePtr image =
+ testutil::ReadImage(data_path, "paris_exif_xmp_icc.jpg");
+ ASSERT_NE(image, nullptr);
+ // The Exif metadata contains orientation information: 1.
+ // It is converted to no irot/imir.
+ EXPECT_GT(image->exif.size, 0u);
+ EXPECT_EQ(image->transformFlags & (AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR),
+ avifTransformFlags{AVIF_TRANSFORM_NONE});
+
+ const testutil::AvifRwData encoded =
+ testutil::Encode(image.get(), AVIF_SPEED_FASTEST);
+ const testutil::AvifImagePtr decoded =
+ testutil::Decode(encoded.data, encoded.size);
+ ASSERT_NE(decoded, nullptr);
+
+ // No irot/imir after decoding because 1 maps to default no irot/imir.
+ EXPECT_TRUE(testutil::AreByteSequencesEqual(image->exif, decoded->exif));
+ EXPECT_EQ(
+ decoded->transformFlags & (AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR),
+ avifTransformFlags{AVIF_TRANSFORM_NONE});
+}
+
+TEST(MetadataTest, ExifOrientation) {
+ const testutil::AvifImagePtr image =
+ testutil::ReadImage(data_path, "paris_exif_orientation_5.jpg");
+ ASSERT_NE(image, nullptr);
+ // The Exif metadata contains orientation information: 5.
+ EXPECT_GT(image->exif.size, 0u);
+ 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);
+
+ const testutil::AvifRwData encoded =
+ testutil::Encode(image.get(), AVIF_SPEED_FASTEST);
+ const testutil::AvifImagePtr decoded =
+ testutil::Decode(encoded.data, encoded.size);
+ ASSERT_NE(decoded, nullptr);
+
+ // irot/imir are expected.
+ EXPECT_TRUE(testutil::AreByteSequencesEqual(image->exif, decoded->exif));
+ EXPECT_EQ(
+ decoded->transformFlags & (AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR),
+ avifTransformFlags{AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR});
+ EXPECT_EQ(decoded->irot.angle, 1u);
+ EXPECT_EQ(decoded->imir.mode, 0u);
+}
+
+TEST(MetadataTest, ExifOrientationAndForcedImir) {
+ const testutil::AvifImagePtr image =
+ testutil::ReadImage(data_path, "paris_exif_orientation_5.jpg");
+ ASSERT_NE(image, nullptr);
+ // The Exif metadata contains orientation information: 5.
+ // Force irot/imir to values that have a different meaning than 5.
+ // This is not recommended but for testing only.
+ EXPECT_GT(image->exif.size, 0u);
+ image->transformFlags = AVIF_TRANSFORM_IMIR;
+ image->imir.mode = 1;
+
+ const testutil::AvifRwData encoded =
+ testutil::Encode(image.get(), AVIF_SPEED_FASTEST);
+ const testutil::AvifImagePtr decoded =
+ testutil::Decode(encoded.data, encoded.size);
+ ASSERT_NE(decoded, nullptr);
+
+ // Exif orientation is still there but irot/imir do not match it.
+ EXPECT_TRUE(testutil::AreByteSequencesEqual(image->exif, decoded->exif));
+ EXPECT_EQ(decoded->transformFlags, avifTransformFlags{AVIF_TRANSFORM_IMIR});
+ EXPECT_EQ(decoded->irot.angle, 0u);
+ EXPECT_EQ(decoded->imir.mode, image->imir.mode);
+}
+
+TEST(MetadataTest, ExifIfdOffsetLoopingTo8) {
+ const testutil::AvifImagePtr image(avifImageCreateEmpty(), avifImageDestroy);
+ ASSERT_NE(image, nullptr);
+ const uint8_t kBadExifPayload[128] = {
+ 'M', 'M', 0, 42, // TIFF header
+ 0, 0, 0, 8, // Offset to 0th IFD
+ 0, 1, // fieldCount
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // tag, type, count, valueOffset
+ 0, 0, 0, 8 // Invalid IFD offset, infinitely looping back to 0th IFD.
+ };
+ avifRWDataSet(&image->exif, kBadExifPayload,
+ sizeof(kBadExifPayload) / sizeof(kBadExifPayload[0]));
+ // avifImageExtractExifOrientationToIrotImir() does not verify the whole
+ // payload, only the parts necessary to extract Exif orientation.
+ ASSERT_EQ(avifImageExtractExifOrientationToIrotImir(image.get()),
+ AVIF_RESULT_OK);
+}
+
+//------------------------------------------------------------------------------
+
} // namespace
} // namespace libavif
diff --git a/tests/gtest/aviftest_helpers.cc b/tests/gtest/aviftest_helpers.cc
index 57d8929..701c46f 100644
--- a/tests/gtest/aviftest_helpers.cc
+++ b/tests/gtest/aviftest_helpers.cc
@@ -29,6 +29,11 @@
avifRGBImageAllocatePixels(this);
}
+AvifRwData::AvifRwData(AvifRwData&& other) : avifRWData{other} {
+ other.data = nullptr;
+ other.size = 0;
+}
+
//------------------------------------------------------------------------------
RgbChannelOffsets GetRgbChannelOffsets(avifRGBFormat format) {
@@ -242,11 +247,33 @@
ignore_exif, ignore_xmp, image.get(), /*outDepth=*/nullptr,
/*sourceTiming=*/nullptr,
/*frameIter=*/nullptr) == AVIF_APP_FILE_FORMAT_UNKNOWN) {
- return {nullptr, avifImageDestroy};
+ return {nullptr, nullptr};
}
return image;
}
+AvifRwData Encode(const avifImage* image, int speed) {
+ testutil::AvifEncoderPtr encoder(avifEncoderCreate(), avifEncoderDestroy);
+ if (!encoder) return {};
+ encoder->speed = speed;
+ testutil::AvifRwData bytes;
+ if (avifEncoderWrite(encoder.get(), image, &bytes) != AVIF_RESULT_OK) {
+ return {};
+ }
+ return bytes;
+}
+
+AvifImagePtr Decode(const uint8_t* bytes, size_t num_bytes) {
+ testutil::AvifImagePtr decoded(avifImageCreateEmpty(), avifImageDestroy);
+ testutil::AvifDecoderPtr decoder(avifDecoderCreate(), avifDecoderDestroy);
+ if (!decoded || !decoder ||
+ (avifDecoderReadMemory(decoder.get(), decoded.get(), bytes, num_bytes) !=
+ AVIF_RESULT_OK)) {
+ return {nullptr, nullptr};
+ }
+ return decoded;
+}
+
//------------------------------------------------------------------------------
static avifResult avifIOLimitedReaderRead(avifIO* io, uint32_t readFlags,
diff --git a/tests/gtest/aviftest_helpers.h b/tests/gtest/aviftest_helpers.h
index a0b1c05..556b244 100644
--- a/tests/gtest/aviftest_helpers.h
+++ b/tests/gtest/aviftest_helpers.h
@@ -24,6 +24,8 @@
class AvifRwData : public avifRWData {
public:
AvifRwData() : avifRWData{nullptr, 0} {}
+ AvifRwData(const AvifRwData&) = delete;
+ AvifRwData(AvifRwData&& other);
~AvifRwData() { avifRWDataFree(this); }
};
@@ -68,7 +70,7 @@
bool ignore_alpha = false);
//------------------------------------------------------------------------------
-// Shorter versions of avifutil.h functions
+// Shorter versions of libavif functions
// Reads the image named file_name located in directory at folder_path.
// Returns nullptr in case of error.
@@ -80,6 +82,14 @@
avifBool ignore_icc = false, avifBool ignore_exif = false,
avifBool ignore_xmp = false);
+// Encodes the image with default parameters.
+// Returns an empty payload in case of error.
+AvifRwData Encode(const avifImage* image, int speed = AVIF_SPEED_DEFAULT);
+
+// Decodes the bytes to an image with default parameters.
+// Returns nullptr in case of error.
+AvifImagePtr Decode(const uint8_t* bytes, size_t num_bytes);
+
//------------------------------------------------------------------------------
// avifIO overlay