Extract Exif to avifImage in avifPNGRead()
Use png_get_eXIf_1().
Add ignoreICC, ignoreExif, ignoreXMP to avifReadImage().
Add --ignore-exif and --ignore-xmp to avifenc.c.
Test in avifmetadatatest.cc and test_cmd.sh.
Add paris_exif_xmp_icc.jpg, paris_exif_xmp_icc.png and
paris_exif_at_end.png. I took and strongly compressed these
pictures. I redacted the GPS coordinates from the Exif entries.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 24573f9..510475f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -38,6 +38,7 @@
* avifenc: Add --sharpyuv, which enables "sharp" RGB to YUV420 conversion, which
reduces artifacts caused by 420 chroma downsampling. Needs libsharpyuv (part
of the libwebp repository) at compile time.
+* avifenc: Add --ignore-exif and --ignore-xmp flags.
## [0.10.1] - 2022-04-11
diff --git a/apps/avifenc.c b/apps/avifenc.c
index 2e72a1c..c263a56 100644
--- a/apps/avifenc.c
+++ b/apps/avifenc.c
@@ -91,14 +91,16 @@
AVIF_SPEED_SLOWEST,
AVIF_SPEED_FASTEST);
printf(" -c,--codec C : AV1 codec to use (choose from versions list below)\n");
- printf(" --exif FILENAME : Provide an Exif metadata payload to be associated with the primary item\n");
- printf(" --xmp FILENAME : Provide an XMP metadata payload to be associated with the primary item\n");
- printf(" --icc FILENAME : Provide an ICC profile payload to be associated with the primary item\n");
+ printf(" --exif FILENAME : Provide an Exif metadata payload to be associated with the primary item (implies --ignore-exif)\n");
+ printf(" --xmp FILENAME : Provide an XMP metadata payload to be associated with the primary item (implies --ignore-xmp)\n");
+ printf(" --icc FILENAME : Provide an ICC profile payload to be associated with the primary item (implies --ignore-icc)\n");
printf(" -a,--advanced KEY[=VALUE] : Pass an advanced, codec-specific key/value string pair directly to the codec. avifenc will warn on any not used by the codec.\n");
printf(" --duration D : Set all following frame durations (in timescales) to D; default 1. Can be set multiple times (before supplying each filename)\n");
printf(" --timescale,--fps V : Set the timescale to V. If all frames are 1 timescale in length, this is equivalent to frames per second (Default: 30)\n");
printf(" If neither duration nor timescale are set, avifenc will attempt to use the framerate stored in a y4m header, if present.\n");
printf(" -k,--keyframe INTERVAL : Set the forced keyframe interval (maximum frames between keyframes). Set to 0 to disable (default).\n");
+ printf(" --ignore-exif : If the input file contains embedded Exif metadata, ignore it (no-op if absent)\n");
+ printf(" --ignore-xmp : If the input file contains embedded XMP metadata, ignore it (no-op if absent)\n");
printf(" --ignore-icc : If the input file contains an embedded ICC profile, ignore it (no-op if absent)\n");
printf(" --pasp H,V : Add pasp property (aspect ratio). H=horizontal spacing, V=vertical spacing\n");
printf(" --crop CROPX,CROPY,CROPW,CROPH : Add clap property (clean aperture), but calculated from a crop rectangle\n");
@@ -257,6 +259,9 @@
}
static avifAppFileFormat avifInputReadImage(avifInput * input,
+ avifBool ignoreICC,
+ avifBool ignoreExif,
+ avifBool ignoreXMP,
avifImage * image,
uint32_t * outDepth,
avifAppSourceTiming * sourceTiming,
@@ -288,6 +293,9 @@
input->requestedFormat,
input->requestedDepth,
flags,
+ ignoreICC,
+ ignoreExif,
+ ignoreXMP,
image,
outDepth,
sourceTiming,
@@ -441,6 +449,8 @@
avifCodecChoice codecChoice = AVIF_CODEC_CHOICE_AUTO;
avifRange requestedRange = AVIF_RANGE_FULL;
avifBool lossless = AVIF_FALSE;
+ avifBool ignoreExif = AVIF_FALSE;
+ avifBool ignoreXMP = AVIF_FALSE;
avifBool ignoreICC = AVIF_FALSE;
avifEncoder * encoder = avifEncoderCreate();
avifImage * image = NULL;
@@ -646,6 +656,7 @@
returnCode = 1;
goto cleanup;
}
+ ignoreExif = AVIF_TRUE;
} else if (!strcmp(arg, "--xmp")) {
NEXTARG();
if (!readEntireFile(arg, &xmpOverride)) {
@@ -653,6 +664,7 @@
returnCode = 1;
goto cleanup;
}
+ ignoreXMP = AVIF_TRUE;
} else if (!strcmp(arg, "--icc")) {
NEXTARG();
if (!readEntireFile(arg, &iccOverride)) {
@@ -660,6 +672,7 @@
returnCode = 1;
goto cleanup;
}
+ ignoreICC = AVIF_TRUE;
} else if (!strcmp(arg, "--duration")) {
NEXTARG();
int durationInt = atoi(arg);
@@ -707,6 +720,10 @@
}
avifEncoderSetCodecSpecificOption(encoder, tempBuffer, value);
free(tempBuffer);
+ } else if (!strcmp(arg, "--ignore-exif")) {
+ ignoreExif = AVIF_TRUE;
+ } else if (!strcmp(arg, "--ignore-xmp")) {
+ ignoreXMP = AVIF_TRUE;
} else if (!strcmp(arg, "--ignore-icc")) {
ignoreICC = AVIF_TRUE;
} else if (!strcmp(arg, "--pasp")) {
@@ -832,7 +849,8 @@
avifInputFile * firstFile = avifInputGetNextFile(&input);
uint32_t sourceDepth = 0;
avifAppSourceTiming firstSourceTiming;
- avifAppFileFormat inputFormat = avifInputReadImage(&input, image, &sourceDepth, &firstSourceTiming, flags);
+ avifAppFileFormat inputFormat =
+ avifInputReadImage(&input, ignoreICC, ignoreExif, ignoreXMP, image, &sourceDepth, &firstSourceTiming, flags);
if (inputFormat == AVIF_APP_FILE_FORMAT_UNKNOWN) {
fprintf(stderr, "Cannot determine input file format: %s\n", firstFile->filename);
returnCode = 1;
@@ -864,9 +882,6 @@
}
}
- if (ignoreICC) {
- avifImageSetProfileICC(image, NULL, 0);
- }
if (iccOverride.size) {
avifImageSetProfileICC(image, iccOverride.data, iccOverride.size);
}
@@ -1040,7 +1055,8 @@
cellImage->alphaPremultiplied = image->alphaPremultiplied;
gridCells[gridCellIndex] = cellImage;
- avifAppFileFormat nextInputFormat = avifInputReadImage(&input, cellImage, NULL, NULL, flags);
+ avifAppFileFormat nextInputFormat =
+ avifInputReadImage(&input, ignoreICC, ignoreExif, ignoreXMP, cellImage, NULL, NULL, flags);
if (nextInputFormat == AVIF_APP_FILE_FORMAT_UNKNOWN) {
returnCode = 1;
goto cleanup;
@@ -1179,7 +1195,8 @@
nextImage->yuvRange = image->yuvRange;
nextImage->alphaPremultiplied = image->alphaPremultiplied;
- avifAppFileFormat nextInputFormat = avifInputReadImage(&input, nextImage, NULL, NULL, flags);
+ avifAppFileFormat nextInputFormat =
+ avifInputReadImage(&input, ignoreICC, ignoreExif, ignoreXMP, nextImage, NULL, NULL, flags);
if (nextInputFormat == AVIF_APP_FILE_FORMAT_UNKNOWN) {
returnCode = 1;
goto cleanup;
diff --git a/apps/shared/avifpng.c b/apps/shared/avifpng.c
index 14ce4c5..07882fe 100644
--- a/apps/shared/avifpng.c
+++ b/apps/shared/avifpng.c
@@ -6,6 +6,9 @@
#include "png.h"
+#include <ctype.h>
+#include <limits.h>
+#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -17,6 +20,150 @@
typedef png_charp png_iccp_datap;
#endif
+// 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.
+// AVIF_FALSE is returned if fewer than numExpectedBytes hexadecimal pairs are converted.
+static avifBool avifHexStringToBytes(const char * hexString, size_t hexStringLength, size_t numExpectedBytes, avifRWData * bytes)
+{
+ // Remove any leading new line. They are not part of the numExpectedBytes.
+ while ((hexStringLength != 0) && (hexString[0] == '\n')) {
+ ++hexString;
+ --hexStringLength;
+ }
+ if (hexStringLength < (numExpectedBytes * 2)) {
+ fprintf(stderr, "Exif extraction failed: " AVIF_FMT_ZU " missing characters\n", (numExpectedBytes * 2) - hexStringLength);
+ return AVIF_FALSE;
+ }
+
+ // Preprocess the input hexString by removing a tag commonly added at encoding, if present.
+ // HEIF specification ISO-23008 section A.2.1 allows including and excluding it from AVIF files.
+ // The PNG 1.5 extension mentions the omission of this header for the modern standard eXIf chunk.
+ const char tagExif00[] = "457869660000"; // "Exif\0\0" tag encoded as a hexadecimal string.
+ const size_t tagExif00Len = 6 * 2;
+ if ((numExpectedBytes >= (tagExif00Len / 2)) && !memcmp(hexString, tagExif00, tagExif00Len)) {
+ hexString += tagExif00Len;
+ hexStringLength -= tagExif00Len;
+ numExpectedBytes -= (tagExif00Len / 2);
+ }
+ if (numExpectedBytes == 0) {
+ fprintf(stderr, "Exif extraction failed: empty payload\n");
+ return AVIF_FALSE;
+ }
+
+ avifRWDataRealloc(bytes, numExpectedBytes);
+ size_t numBytes = 0;
+ for (size_t i = 0; (i + 1 < hexStringLength) && (numBytes < numExpectedBytes);) {
+ if (hexString[i] == '\n') {
+ ++i;
+ continue;
+ }
+ if (!isxdigit(hexString[i]) || !isxdigit(hexString[i + 1])) {
+ avifRWDataFree(bytes);
+ fprintf(stderr, "Exif extraction failed: invalid character at " AVIF_FMT_ZU "\n", i);
+ return AVIF_FALSE;
+ }
+ const char twoHexDigits[] = { hexString[i], hexString[i + 1], '\0' };
+ bytes->data[numBytes] = (uint8_t)strtol(twoHexDigits, NULL, 16);
+ ++numBytes;
+ i += 2;
+ }
+
+ if (numBytes != numExpectedBytes) {
+ avifRWDataFree(bytes);
+ fprintf(stderr, "Exif extraction failed: expected " AVIF_FMT_ZU " tokens but got " AVIF_FMT_ZU "\n", numExpectedBytes, numBytes);
+ return AVIF_FALSE;
+ }
+ return AVIF_TRUE;
+}
+
+// Parses the raw profile string of profileLength characters and extracts the payload.
+static avifBool avifCopyRawProfile(const char * profile, size_t profileLength, avifRWData * payload)
+{
+ // ImageMagick formats 'raw profiles' as "\n<name>\n<length>(%8lu)\n<hex payload>\n".
+ if (!profile || (profileLength == 0) || (profile[0] != '\n')) {
+ fprintf(stderr, "Exif extraction failed: truncated or malformed raw profile\n");
+ return AVIF_FALSE;
+ }
+
+ const char * lengthStart = NULL;
+ for (size_t i = 1; i < profileLength; ++i) { // i starts at 1 because the first '\n' was already checked above.
+ if (profile[i] == '\0') {
+ // This should not happen as libpng provides this guarantee but extra safety does not hurt.
+ fprintf(stderr, "Exif extraction failed: malformed raw profile, unexpected null character at " AVIF_FMT_ZU "\n", i);
+ return AVIF_FALSE;
+ }
+ if (profile[i] == '\n') {
+ if (!lengthStart) {
+ // Skip the name and store the beginning of the string containing the length of the payload.
+ lengthStart = &profile[i + 1];
+ } else {
+ const char * hexPayloadStart = &profile[i + 1];
+ const size_t hexPayloadMaxLength = profileLength - (i + 1);
+ // Parse the length, now that we are sure that it is surrounded by '\n' within the profileLength characters.
+ char * lengthEnd;
+ const long expectedLength = strtol(lengthStart, &lengthEnd, 10);
+ if (lengthEnd != &profile[i]) {
+ fprintf(stderr, "Exif extraction failed: malformed raw profile, expected '\\n' but got '\\x%.2X'\n", *lengthEnd);
+ return AVIF_FALSE;
+ }
+ // No need to check for errno. Just make sure expectedLength is not LONG_MIN and not LONG_MAX.
+ if ((expectedLength <= 0) || (expectedLength == LONG_MAX) ||
+ ((unsigned long)expectedLength > (hexPayloadMaxLength / 2))) {
+ fprintf(stderr, "Exif extraction failed: invalid length %ld\n", expectedLength);
+ return AVIF_FALSE;
+ }
+ // Note: The profile may be malformed by containing more data than the extracted expectedLength bytes.
+ // Be lenient about it and consider it as a valid payload.
+ return avifHexStringToBytes(hexPayloadStart, hexPayloadMaxLength, (size_t)expectedLength, payload);
+ }
+ }
+ }
+ fprintf(stderr, "Exif extraction failed: malformed or truncated raw profile\n");
+ return AVIF_FALSE;
+}
+
+// Returns AVIF_TRUE if there was no Exif metadata located at info or if the Exif metadata located at info
+// was correctly parsed and imported to avif->exif. Returns AVIF_FALSE in case of error.
+static avifBool avifExtractExif(png_structp png, png_infop const info, avifImage * avif)
+{
+#ifdef PNG_eXIf_SUPPORTED
+ png_uint_32 exifSize = 0;
+ png_bytep exif = NULL;
+ if (png_get_eXIf_1(png, info, &exifSize, &exif) == PNG_INFO_eXIf) {
+ if ((exifSize == 0) || !exif) {
+ fprintf(stderr, "Exif extraction failed: empty eXIf chunk\n");
+ return AVIF_FALSE;
+ }
+ avifImageSetMetadataExif(avif, exif, exifSize);
+ return AVIF_TRUE;
+ }
+#endif // PNG_eXIf_SUPPORTED
+
+ png_textp text = NULL;
+ const png_uint_32 numTextChunks = png_get_text(png, info, &text, NULL);
+ for (png_uint_32 i = 0; i < numTextChunks; ++i, ++text) {
+ if (!strcmp(text->key, "Raw profile type exif") || !strcmp(text->key, "Raw profile type APP1")) {
+ png_size_t textLength;
+ switch (text->compression) {
+#ifdef PNG_iTXt_SUPPORTED
+ case PNG_ITXT_COMPRESSION_NONE:
+ case PNG_ITXT_COMPRESSION_zTXt:
+ textLength = text->itxt_length;
+ break;
+#endif
+ case PNG_TEXT_COMPRESSION_NONE:
+ case PNG_TEXT_COMPRESSION_zTXt:
+ default:
+ textLength = text->text_length;
+ break;
+ }
+ return avifCopyRawProfile(text->text, textLength, &avif->exif);
+ }
+ }
+ return AVIF_TRUE;
+}
+
// Note on setjmp() and volatile variables:
//
// K & R, The C Programming Language 2nd Ed, p. 254 says:
@@ -36,11 +183,13 @@
avifPixelFormat requestedFormat,
uint32_t requestedDepth,
avifRGBToYUVFlags flags,
+ avifBool ignoreExif,
uint32_t * outPNGDepth)
{
volatile avifBool readResult = AVIF_FALSE;
png_structp png = NULL;
png_infop info = NULL;
+ png_infop infoEnd = NULL;
png_bytep * volatile rowPointers = NULL;
avifRGBImage rgb;
@@ -90,6 +239,7 @@
if (png_get_iCCP(png, info, &iccpProfileName, &iccpCompression, (png_iccp_datap *)&iccpData, &iccpDataLen) == PNG_INFO_iCCP) {
avifImageSetProfileICC(avif, iccpData, iccpDataLen);
}
+ // TODO(yguyon): Also check if there is a cICp chunk (https://github.com/AOMediaCodec/libavif/pull/1065#discussion_r958534232)
int rawWidth = png_get_image_width(png, info);
int rawHeight = png_get_image_height(png, info);
@@ -162,6 +312,26 @@
fprintf(stderr, "Conversion to YUV failed: %s\n", inputFilename);
goto cleanup;
}
+
+ if (!ignoreExif) {
+ // Read Exif metadata at the beginning of the file.
+ if (!avifExtractExif(png, info, avif)) {
+ goto cleanup;
+ }
+ // Read Exif metadata at the end of the file if there was none at the beginning.
+ if (!avif->exif.data) {
+ infoEnd = png_create_info_struct(png);
+ if (!infoEnd) {
+ fprintf(stderr, "Cannot init libpng (infoEnd): %s\n", inputFilename);
+ goto cleanup;
+ }
+ png_read_end(png, infoEnd);
+ if (!avifExtractExif(png, infoEnd, avif)) {
+ goto cleanup;
+ }
+ }
+ }
+ // TODO(yguyon): Extract XMP to avif->xmp, if any.
readResult = AVIF_TRUE;
cleanup:
@@ -170,6 +340,7 @@
}
if (png) {
png_destroy_read_struct(&png, &info, NULL);
+ png_destroy_read_struct(&png, &infoEnd, NULL);
}
if (rowPointers) {
free(rowPointers);
diff --git a/apps/shared/avifpng.h b/apps/shared/avifpng.h
index 7e946e4..c15d7a1 100644
--- a/apps/shared/avifpng.h
+++ b/apps/shared/avifpng.h
@@ -16,6 +16,7 @@
avifPixelFormat requestedFormat,
uint32_t requestedDepth,
avifRGBToYUVFlags flags,
+ avifBool ignoreExif,
uint32_t * outPNGDepth);
avifBool avifPNGWrite(const char * outputFilename,
const avifImage * avif,
diff --git a/apps/shared/avifutil.c b/apps/shared/avifutil.c
index b38082a..0752dbf 100644
--- a/apps/shared/avifutil.c
+++ b/apps/shared/avifutil.c
@@ -228,6 +228,9 @@
avifPixelFormat requestedFormat,
int requestedDepth,
avifRGBToYUVFlags flags,
+ avifBool ignoreICC,
+ avifBool ignoreExif,
+ avifBool ignoreXMP,
avifImage * image,
uint32_t * outDepth,
avifAppSourceTiming * sourceTiming,
@@ -249,7 +252,8 @@
*outDepth = 8;
}
} else if (format == AVIF_APP_FILE_FORMAT_PNG) {
- if (!avifPNGRead(filename, image, requestedFormat, requestedDepth, flags, outDepth)) {
+ (void)ignoreICC, (void)ignoreXMP; // TODO(yguyon): Implement
+ if (!avifPNGRead(filename, image, requestedFormat, requestedDepth, flags, ignoreExif, outDepth)) {
return AVIF_APP_FILE_FORMAT_UNKNOWN;
}
} else {
diff --git a/apps/shared/avifutil.h b/apps/shared/avifutil.h
index 6543787..c1b77cd 100644
--- a/apps/shared/avifutil.h
+++ b/apps/shared/avifutil.h
@@ -64,6 +64,9 @@
avifPixelFormat requestedFormat,
int requestedDepth,
avifRGBToYUVFlags flags,
+ avifBool ignoreICC,
+ avifBool ignoreExif,
+ avifBool ignoreXMP,
avifImage * image,
uint32_t * outDepth,
avifAppSourceTiming * sourceTiming,
diff --git a/doc/avifenc.1.md b/doc/avifenc.1.md
index bf381fb..95e450e 100644
--- a/doc/avifenc.1.md
+++ b/doc/avifenc.1.md
@@ -152,13 +152,16 @@
- **svt**
**\--exif** _FILENAME_
-: Provide an Exif metadata payload to be associated with the primary item.
+: Provide an Exif metadata payload to be associated with the primary item
+ (implies --ignore-exif).
**\--xmp** _FILENAME_
-: Provide an XMP metadata payload to be associated with the primary item.
+: Provide an XMP metadata payload to be associated with the primary item
+ (implies --ignore-xmp).
**\--icc** _FILENAME_
-: Provide an ICC profile payload to be associated with the primary item.
+: Provide an ICC profile payload to be associated with the primary item
+ (implies --ignore-icc).
**-a**, **\--advanced** _KEY_[_=VALUE_]
: Pass an advanced, codec-specific key/value string pair directly to the
@@ -185,6 +188,14 @@
Set to **0** to disable.
Default is 0.
+**\--ignore-exif**
+: If the input file contains embedded Exif metadata, ignore it (no-op if
+ absent).
+
+**\--ignore-xmp**
+: If the input file contains embedded XMP metadata, ignore it (no-op if
+ absent).
+
**\--ignore-icc**
: If the input file contains an embedded ICC profile, ignore it (no-op if
absent).
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 1bbe042..8636010 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -92,9 +92,9 @@
add_test(NAME avifincrtest COMMAND avifincrtest ${CMAKE_CURRENT_SOURCE_DIR}/data)
add_executable(avifmetadatatest gtest/avifmetadatatest.cc)
- target_link_libraries(avifmetadatatest aviftest_helpers ${GTEST_BOTH_LIBRARIES})
+ target_link_libraries(avifmetadatatest aviftest_helpers avif_apps ${GTEST_LIBRARIES})
target_include_directories(avifmetadatatest PRIVATE ${GTEST_INCLUDE_DIRS})
- add_test(NAME avifmetadatatest COMMAND avifmetadatatest)
+ add_test(NAME avifmetadatatest COMMAND avifmetadatatest ${CMAKE_CURRENT_SOURCE_DIR}/data)
add_executable(avifrgbtoyuvtest gtest/avifrgbtoyuvtest.cc)
target_link_libraries(avifrgbtoyuvtest aviftest_helpers ${GTEST_BOTH_LIBRARIES})
@@ -119,6 +119,5 @@
target_link_libraries(are_images_equal aviftest_helpers avif_apps)
add_test(
NAME test_cmd
- COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cmd.sh ${CMAKE_BINARY_DIR}
- WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
+ COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cmd.sh ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/data)
endif()
diff --git a/tests/data/paris_exif_at_end.png b/tests/data/paris_exif_at_end.png
new file mode 100644
index 0000000..53bfc08
--- /dev/null
+++ b/tests/data/paris_exif_at_end.png
Binary files differ
diff --git a/tests/data/paris_exif_xmp_icc.jpg b/tests/data/paris_exif_xmp_icc.jpg
new file mode 100644
index 0000000..805f9df
--- /dev/null
+++ b/tests/data/paris_exif_xmp_icc.jpg
Binary files differ
diff --git a/tests/data/paris_exif_xmp_icc.png b/tests/data/paris_exif_xmp_icc.png
new file mode 100644
index 0000000..df1f02e
--- /dev/null
+++ b/tests/data/paris_exif_xmp_icc.png
Binary files differ
diff --git a/tests/gtest/are_images_equal.cc b/tests/gtest/are_images_equal.cc
index 1bfe65f..ac9c1d2 100644
--- a/tests/gtest/are_images_equal.cc
+++ b/tests/gtest/are_images_equal.cc
@@ -31,8 +31,10 @@
// Make sure no color conversion happens.
decoded[i]->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_IDENTITY;
if (avifReadImage(argv[i + 1], requestedFormat, kRequestedDepth,
- AVIF_RGB_TO_YUV_DEFAULT, decoded[i].get(), &depth[i],
- nullptr, nullptr) == AVIF_APP_FILE_FORMAT_UNKNOWN) {
+ AVIF_RGB_TO_YUV_DEFAULT, /*ignoreICC=*/AVIF_FALSE,
+ /*ignoreExif=*/AVIF_FALSE, /*ignoreXMP=*/AVIF_FALSE,
+ decoded[i].get(), &depth[i], nullptr,
+ nullptr) == AVIF_APP_FILE_FORMAT_UNKNOWN) {
std::cerr << "Image " << argv[i + 1] << " cannot be read." << std::endl;
return 2;
}
diff --git a/tests/gtest/avifmetadatatest.cc b/tests/gtest/avifmetadatatest.cc
index c4fd1be..690e83c 100644
--- a/tests/gtest/avifmetadatatest.cc
+++ b/tests/gtest/avifmetadatatest.cc
@@ -6,16 +6,21 @@
#include "avif/avif.h"
#include "aviftest_helpers.h"
+#include "avifutil.h"
#include "gtest/gtest.h"
using ::testing::Bool;
using ::testing::Combine;
+using ::testing::Values;
namespace libavif {
namespace {
//------------------------------------------------------------------------------
+// Used to pass the data folder path to the GoogleTest suites.
+const char* data_path = nullptr;
+
// ICC color profiles are not checked by libavif so the content does not matter.
// This is a truncated widespread ICC color profile.
const std::array<uint8_t, 24> kSampleIcc = {
@@ -35,14 +40,15 @@
0x67, 0x69, 0x6e, 0x3d, 0x22, 0xef, 0xbb, 0xbf, 0x22, 0x20, 0x69, 0x64};
//------------------------------------------------------------------------------
+// AVIF encode/decode metadata tests
-class MetadataTest
+class AvifMetadataTest
: public testing::TestWithParam<
std::tuple</*use_icc=*/bool, /*use_exif=*/bool, /*use_xmp=*/bool>> {};
// Encodes, decodes then verifies that the output metadata matches the input
// metadata defined by the parameters.
-TEST_P(MetadataTest, EncodeDecode) {
+TEST_P(AvifMetadataTest, EncodeDecode) {
const bool use_icc = std::get<0>(GetParam());
const bool use_exif = std::get<1>(GetParam());
const bool use_xmp = std::get<2>(GetParam());
@@ -81,32 +87,148 @@
AVIF_RESULT_OK);
// Compare input and output metadata.
- if (use_icc) {
- ASSERT_EQ(decoded->icc.size, kSampleIcc.size());
- EXPECT_TRUE(
- std::equal(kSampleIcc.begin(), kSampleIcc.end(), decoded->icc.data));
- } else {
- EXPECT_EQ(decoded->icc.size, 0u);
- }
- if (use_exif) {
- ASSERT_EQ(decoded->exif.size, kSampleExif.size());
- EXPECT_TRUE(
- std::equal(kSampleExif.begin(), kSampleExif.end(), decoded->exif.data));
- } else {
- EXPECT_EQ(decoded->exif.size, 0u);
- }
- if (use_xmp) {
- ASSERT_EQ(decoded->xmp.size, kSampleXmp.size());
- EXPECT_TRUE(
- std::equal(kSampleXmp.begin(), kSampleXmp.end(), decoded->xmp.data));
- } else {
- EXPECT_EQ(decoded->xmp.size, 0u);
- }
+ EXPECT_TRUE(testutil::AreByteSequencesEqual(
+ decoded->icc.data, decoded->icc.size, kSampleIcc.data(),
+ use_icc ? kSampleIcc.size() : 0u));
+ EXPECT_TRUE(testutil::AreByteSequencesEqual(
+ decoded->exif.data, decoded->exif.size, kSampleExif.data(),
+ use_exif ? kSampleExif.size() : 0u));
+ EXPECT_TRUE(testutil::AreByteSequencesEqual(
+ decoded->xmp.data, decoded->xmp.size, kSampleXmp.data(),
+ use_xmp ? kSampleXmp.size() : 0u));
}
-INSTANTIATE_TEST_SUITE_P(All, MetadataTest,
+INSTANTIATE_TEST_SUITE_P(All, AvifMetadataTest,
Combine(/*use_icc=*/Bool(), /*use_exif=*/Bool(),
/*use_xmp=*/Bool()));
+//------------------------------------------------------------------------------
+// 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>> {};
+
+// zTXt "Raw profile type exif" at the beginning of a PNG file.
+TEST_P(MetadataTest, Read) {
+ const std::string file_path =
+ std::string(data_path) + "/" + 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());
+
+ avifImage* image = avifImageCreateEmpty();
+ ASSERT_NE(image, nullptr);
+ image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_IDENTITY; // lossless
+ ASSERT_NE(avifReadImage(file_path.c_str(), AVIF_PIXEL_FORMAT_NONE, 0,
+ AVIF_RGB_TO_YUV_DEFAULT, !use_icc, !use_exif,
+ !use_xmp, image, nullptr, nullptr, nullptr),
+ AVIF_APP_FILE_FORMAT_UNKNOWN);
+ EXPECT_NE(image->width * image->height, 0u);
+ if (expect_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) {
+ 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) {
+ 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);
+ }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ PngNone, MetadataTest,
+ Combine(Values("paris_exif_xmp_icc.png"), // zTXt iCCP iTXt IDAT
+ /*use_icc=*/Values(false), /*use_exif=*/Values(false),
+ /*use_xmp=*/Values(false),
+ // ignoreICC is not yet implemented.
+ /*expected_icc=*/Values(true),
+ /*expected_exif=*/Values(false), /*expected_xmp=*/Values(false)));
+INSTANTIATE_TEST_SUITE_P(
+ PngAll, MetadataTest,
+ Combine(Values("paris_exif_xmp_icc.png"), /*use_icc=*/Values(true),
+ /*use_exif=*/Values(true), /*use_xmp=*/Values(true),
+ /*expected_icc=*/Values(true), /*expected_exif=*/Values(true),
+ // XMP extraction is not yet implemented.
+ /*expected_xmp=*/Values(false)));
+
+INSTANTIATE_TEST_SUITE_P(
+ PngExifAtEnd, MetadataTest,
+ Combine(Values("paris_exif_at_end.png"), // iCCP IDAT eXIf
+ /*use_icc=*/Values(true), /*use_exif=*/Values(true),
+ /*use_xmp=*/Values(true), /*expected_icc=*/Values(true),
+ /*expected_exif=*/Values(true), /*expected_xmp=*/Values(false)));
+
+INSTANTIATE_TEST_SUITE_P(
+ Jpeg, MetadataTest,
+ Combine(Values("paris_exif_xmp_icc.jpg"), /*use_icc=*/Values(true),
+ /*use_exif=*/Values(true), /*use_xmp=*/Values(true),
+ /*expected_icc=*/Values(true),
+ // Exif and XMP are not yet implemented.
+ /*expected_exif=*/Values(false), /*expected_xmp=*/Values(false)));
+
+// Verify all parsers lead exactly to the same metadata bytes.
+TEST(MetadataTest, Compare) {
+ constexpr const char* kFileNames[] = {"paris_exif_at_end.png",
+ "paris_exif_xmp_icc.jpg",
+ "paris_exif_xmp_icc.png"};
+ avifImage* images[sizeof(kFileNames) / sizeof(kFileNames[0])];
+ avifImage** image_it = images;
+ for (const char* file_name : kFileNames) {
+ const std::string file_path = std::string(data_path) + "/" + file_name;
+
+ *image_it = avifImageCreateEmpty();
+ ASSERT_NE(*image_it, nullptr);
+ ASSERT_NE(avifReadImage(file_path.c_str(), AVIF_PIXEL_FORMAT_NONE, 0,
+ AVIF_RGB_TO_YUV_DEFAULT, /*ignoreICC=*/false,
+ /*ignoreExif=*/false, /*ignoreXMP=*/false,
+ *image_it, nullptr, nullptr, nullptr),
+ AVIF_APP_FILE_FORMAT_UNKNOWN);
+ ++image_it;
+ }
+
+ for (avifImage* image : images) {
+ if (image->exif.size != 0) { // Not implemented for JPEG.
+ EXPECT_TRUE(
+ testutil::AreByteSequencesEqual(image->exif, images[0]->exif));
+ }
+ if (image->xmp.size != 0) { // Not implemented.
+ EXPECT_TRUE(testutil::AreByteSequencesEqual(image->xmp, images[0]->xmp));
+ }
+ EXPECT_TRUE(testutil::AreByteSequencesEqual(image->icc, images[0]->icc));
+ }
+}
+
+//------------------------------------------------------------------------------
+
} // namespace
} // namespace libavif
+
+int main(int argc, char** argv) {
+ ::testing::InitGoogleTest(&argc, argv);
+ if (argc != 2) {
+ std::cerr << "There must be exactly one argument containing the path to "
+ "the test data folder"
+ << std::endl;
+ return 1;
+ }
+ libavif::data_path = argv[1];
+ return RUN_ALL_TESTS();
+}
diff --git a/tests/gtest/aviftest_helpers.cc b/tests/gtest/aviftest_helpers.cc
index e108bc6..493512a 100644
--- a/tests/gtest/aviftest_helpers.cc
+++ b/tests/gtest/aviftest_helpers.cc
@@ -159,6 +159,16 @@
//------------------------------------------------------------------------------
+bool AreByteSequencesEqual(const uint8_t data1[], size_t data1_length,
+ const uint8_t data2[], size_t data2_length) {
+ if (data1_length != data2_length) return false;
+ return data1_length == 0 || std::equal(data1, data1 + data1_length, data2);
+}
+
+bool AreByteSequencesEqual(const avifRWData& data1, const avifRWData& data2) {
+ return AreByteSequencesEqual(data1.data, data1.size, data2.data, data2.size);
+}
+
// Returns true if image1 and image2 are identical.
bool AreImagesEqual(const avifImage& image1, const avifImage& image2,
bool ignore_alpha) {
@@ -212,7 +222,9 @@
row2 += row_bytes2;
}
}
- return true;
+ return AreByteSequencesEqual(image1.icc, image2.icc) &&
+ AreByteSequencesEqual(image1.exif, image2.exif) &&
+ AreByteSequencesEqual(image1.xmp, image2.xmp);
}
static avifResult avifIOLimitedReaderRead(avifIO* io, uint32_t readFlags,
diff --git a/tests/gtest/aviftest_helpers.h b/tests/gtest/aviftest_helpers.h
index 510e6b3..f7f2a2c 100644
--- a/tests/gtest/aviftest_helpers.h
+++ b/tests/gtest/aviftest_helpers.h
@@ -54,13 +54,21 @@
void FillImageChannel(avifRGBImage* image, uint32_t channel_offset,
uint32_t value);
-// Returns true if both images have the same features and pixel values.
-// If ignore_alpha is true, the alpha channel is not taken into account in the
-// comparison.
+// Returns true if both arrays are empty or have the same length and bytes.
+// data1 may be null only when data1_length is 0.
+// data2 may be null only when data2_length is 0.
+bool AreByteSequencesEqual(const uint8_t data1[], size_t data1_length,
+ const uint8_t data2[], size_t data2_length);
+bool AreByteSequencesEqual(const avifRWData& data1, const avifRWData& data2);
+
+// Returns true if both images have the same features, pixel values and
+// metadata. If ignore_alpha is true, the alpha channel is not taken into
+// account in the comparison.
bool AreImagesEqual(const avifImage& image1, const avifImage& image2,
bool ignore_alpha = false);
//------------------------------------------------------------------------------
+// avifIO overlay
struct AvifIOLimitedReader {
static constexpr uint64_t kNoClamp = std::numeric_limits<uint64_t>::max();
diff --git a/tests/test_cmd.sh b/tests/test_cmd.sh
index c280c0f..fde09a2 100755
--- a/tests/test_cmd.sh
+++ b/tests/test_cmd.sh
@@ -34,16 +34,17 @@
AVIFENC="${BINARY_DIR}/avifenc"
AVIFDEC="${BINARY_DIR}/avifdec"
ARE_IMAGES_EQUAL="${BINARY_DIR}/tests/are_images_equal"
-TMP_ENCODED_FILE=/tmp/encoded.avif
-DECODED_FILE=/tmp/decoded.png
-PNG_FILE=/tmp/kodim03.png
-TMP_ENCODED_FILE_WTH_DASH=-encoded.avif
+ENCODED_FILE=/tmp/avif_test_cmd_encoded.avif
+ENCODED_FILE_NO_METADATA=/tmp/avif_test_cmd_encoded_no_metadata.avif
+ENCODED_FILE_WITH_DASH=-avif_test_cmd_encoded.avif
+DECODED_FILE=/tmp/avif_test_cmd_decoded.png
+PNG_FILE=/tmp/avif_test_cmd_kodim03.png
# Prepare some extra data.
set +x
echo "Generating a color PNG"
-"${AVIFENC}" -s 10 "${TESTDATA_DIR}"/kodim03_yuv420_8bpc.y4m -o "${TMP_ENCODED_FILE}" > /dev/null
-"${AVIFDEC}" "${TMP_ENCODED_FILE}" "${PNG_FILE}" > /dev/null
+"${AVIFENC}" -s 10 "${TESTDATA_DIR}/kodim03_yuv420_8bpc.y4m" -o "${ENCODED_FILE}" > /dev/null
+"${AVIFDEC}" "${ENCODED_FILE}" "${PNG_FILE}" > /dev/null
set -x
# Basic calls.
@@ -52,35 +53,48 @@
# Lossless test.
echo "Testing basic lossless"
-"${AVIFENC}" -s 10 -l "${PNG_FILE}" -o "${TMP_ENCODED_FILE}"
-"${AVIFDEC}" "${TMP_ENCODED_FILE}" "${DECODED_FILE}"
+"${AVIFENC}" -s 10 -l "${PNG_FILE}" -o "${ENCODED_FILE}"
+"${AVIFDEC}" "${ENCODED_FILE}" "${DECODED_FILE}"
"${ARE_IMAGES_EQUAL}" "${PNG_FILE}" "${DECODED_FILE}" 0
+# Metadata test.
+echo "Testing metadata enc/dec"
+"${AVIFENC}" "${TESTDATA_DIR}/paris_exif_xmp_icc.png" -o "${ENCODED_FILE}"
+"${AVIFENC}" "${TESTDATA_DIR}/paris_exif_xmp_icc.png" -o "${ENCODED_FILE_NO_METADATA}" --ignore-exif
+cmp "${ENCODED_FILE}" "${ENCODED_FILE_NO_METADATA}" && exit 1
+
# Argument parsing test with filenames starting with a dash.
-"${AVIFENC}" -s 10 "${PNG_FILE}" -- "${TMP_ENCODED_FILE_WTH_DASH}"
-"${AVIFDEC}" --info -- "${TMP_ENCODED_FILE_WTH_DASH}"
+"${AVIFENC}" -s 10 "${PNG_FILE}" -- "${ENCODED_FILE_WITH_DASH}"
+"${AVIFDEC}" --info -- "${ENCODED_FILE_WITH_DASH}"
# Passing a filename starting with a dash without using -- should fail.
set +e
-"${AVIFENC}" -s 10 "${PNG_FILE}" "${TMP_ENCODED_FILE_WTH_DASH}"
+"${AVIFENC}" -s 10 "${PNG_FILE}" "${ENCODED_FILE_WITH_DASH}"
if [[ $? -ne 1 ]]; then
echo "Argument parsing should fail for avifenc"
exit 1
fi
-"${AVIFDEC}" --info "${TMP_ENCODED_FILE_WTH_DASH}"
+"${AVIFDEC}" --info "${ENCODED_FILE_WITH_DASH}"
if [[ $? -ne 1 ]]; then
echo "Argument parsing should fail for avifdec"
exit 1
fi
set -e
-rm -- "${TMP_ENCODED_FILE_WTH_DASH}"
+rm -- "${ENCODED_FILE_WITH_DASH}"
# Test code that should fail.
set +e
-"${ARE_IMAGES_EQUAL}" "${TESTDATA_DIR}"/kodim23_yuv420_8bpc.y4m "${DECODED_FILE}" 0
+"${ARE_IMAGES_EQUAL}" "${TESTDATA_DIR}/kodim23_yuv420_8bpc.y4m" "${DECODED_FILE}" 0
if [[ $? -ne 1 ]]; then
echo "Image should be different"
+
+ # Cleanup
+ rm "${ENCODED_FILE}" "${ENCODED_FILE_NO_METADATA}" "${DECODED_FILE}" "${PNG_FILE}"
exit 1
fi
echo "TEST OK"
+
+# Cleanup
+rm "${ENCODED_FILE}" "${ENCODED_FILE_NO_METADATA}" "${DECODED_FILE}" "${PNG_FILE}"
+
exit 0