Call avifImageFixXMP() during jpg,png reading
Strip one null character from XMP chunks extracted from JPG and PNG
files. Add regression test in avifmetadatatest.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 40d9311..8e63627 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -69,6 +69,8 @@
* At decoding, avifIOStats now returns the same values as at encoding.
* avifRGBImageAllocatePixels() now returns avifResult instead of void to report
memory allocation failures.
+* avifReadImage(), avifJPEGRead() and avifPNGRead() now remove the trailing zero
+ byte from read XMP chunks, if any. See avifImageFixXMP().
## [0.11.1] - 2022-10-19
diff --git a/apps/shared/avifjpeg.c b/apps/shared/avifjpeg.c
index b599b3e..1e4d668 100644
--- a/apps/shared/avifjpeg.c
+++ b/apps/shared/avifjpeg.c
@@ -551,6 +551,7 @@
} else if (standardXMPData) {
avifImageSetMetadataXMP(avif, standardXMPData, standardXMPSize);
}
+ avifImageFixXMP(avif); // Remove one trailing null character if any.
}
jpeg_finish_decompress(&cinfo);
ret = AVIF_TRUE;
diff --git a/apps/shared/avifpng.c b/apps/shared/avifpng.c
index 3b00618..5c4614b 100644
--- a/apps/shared/avifpng.c
+++ b/apps/shared/avifpng.c
@@ -193,6 +193,10 @@
*ignoreXMP = AVIF_TRUE; // Ignore any other XMP chunk.
}
}
+ // The iTXt XMP payload may not contain a zero byte according to section 4.2.3.3 of
+ // the PNG specification, version 1.2. Still remove one trailing null character if any,
+ // in case libpng does not strictly enforce that at decoding.
+ avifImageFixXMP(avif);
return AVIF_TRUE;
}
@@ -515,26 +519,23 @@
if (avif->xmp.data && (avif->xmp.size > 0)) {
// The iTXt XMP payload may not contain a zero byte according to section 4.2.3.3 of
// the PNG specification, version 1.2.
- if (memchr(avif->xmp.data, '\0', avif->xmp.size)) {
- fprintf(stderr, "Error writing PNG: XMP metadata contains an invalid null character\n");
+ // The chunk is given to libpng as is. Bytes after a zero byte may be stripped.
+
+ // 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;
- } else {
- // 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;
}
+ 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;
}
if (numTextMetadataChunks != 0) {
png_set_text(png, info, texts, numTextMetadataChunks);
diff --git a/apps/shared/avifutil.c b/apps/shared/avifutil.c
index 8e20090..0f8381f 100644
--- a/apps/shared/avifutil.c
+++ b/apps/shared/avifutil.c
@@ -277,6 +277,22 @@
return format;
}
+void avifImageFixXMP(avifImage * image)
+{
+ // Zero bytes are forbidden in UTF-8 XML: https://en.wikipedia.org/wiki/Valid_characters_in_XML
+ // Keeping zero bytes in XMP may lead to issues at encoding or decoding.
+ // For example, the PNG specification forbids null characters in XMP. See avifPNGWrite().
+ // The XMP Specification Part 3 says "When XMP is encoded as UTF-8,
+ // there are no zero bytes in the XMP packet" for GIF.
+
+ // Consider a single trailing null character following a non-null character
+ // as a programming error. Leave other null characters as is.
+ // See the discussion at https://github.com/AOMediaCodec/libavif/issues/1333.
+ if (image->xmp.size >= 2 && image->xmp.data[image->xmp.size - 1] == '\0' && image->xmp.data[image->xmp.size - 2] != '\0') {
+ --image->xmp.size;
+ }
+}
+
void avifDumpDiagnostics(const avifDiagnostics * diag)
{
if (!*diag->error) {
diff --git a/apps/shared/avifutil.h b/apps/shared/avifutil.h
index be0b2b8..16d7a53 100644
--- a/apps/shared/avifutil.h
+++ b/apps/shared/avifutil.h
@@ -72,6 +72,9 @@
avifAppSourceTiming * sourceTiming,
struct y4mFrameIterator ** frameIter);
+// Removes a single trailing null character from the image->xmp, if there is exactly one.
+void avifImageFixXMP(avifImage * image);
+
// Used by image decoders when the user doesn't explicitly choose a format with --yuv
// This must match the cited fallback for "--yuv auto" in avifenc.c's syntax() function.
#define AVIF_APP_DEFAULT_PIXEL_FORMAT AVIF_PIXEL_FORMAT_YUV444
diff --git a/tests/data/README.md b/tests/data/README.md
index 8755fae..768292f 100644
--- a/tests/data/README.md
+++ b/tests/data/README.md
@@ -106,6 +106,22 @@
| 136627 | 0xffe1 APP1 | 4791 | http://ns.adobe.com/xmp/extensio |
| | | | ... |
+### File [paris_xmp_trailing_null.jpg](paris_xmp_trailing_null.jpg)
+
+![](paris_xmp_trailing_null.jpg)
+
+License: [same as libavif](https://github.com/AOMediaCodec/libavif/blob/main/LICENSE)
+
+Source: `paris_exif_xmp_icc.jpg` loaded with `avifReadImage()`, stripped of ICC and Exif, a zero byte appended to XMP,
+then written with `avifJPEGWrite()` with quality 0 (without calling `avifImageFixXMP()`).
+
+| address | marker | length | data |
+|--------:|-------------|-------:|----------------------------------------------|
+| 0 | 0xffd8 SOI | | |
+| 2 | 0xffe0 APP0 | 16 | `JFIF.........` |
+| 20 | 0xffe1 APP1 | 3930 | `http://ns.adobe.com/xap/1.0/.<?x` |
+| | | | ... |
+
### File [paris_icc_exif_xmp.png](paris_icc_exif_xmp.png)
![](paris_icc_exif_xmp.png)
@@ -148,7 +164,7 @@
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
+Source: `paris_exif_xmp_icc.jpg` stripped of all metadata with `exiftool -all=` and Exif orientation added
with `exiv2 -k -M "set Exif.Image.Orientation 5"`
| address | marker | length | data |
diff --git a/tests/data/paris_xmp_trailing_null.jpg b/tests/data/paris_xmp_trailing_null.jpg
new file mode 100644
index 0000000..2d46824
--- /dev/null
+++ b/tests/data/paris_xmp_trailing_null.jpg
Binary files differ
diff --git a/tests/gtest/avifmetadatatest.cc b/tests/gtest/avifmetadatatest.cc
index 507b573..bb6c7b7 100644
--- a/tests/gtest/avifmetadatatest.cc
+++ b/tests/gtest/avifmetadatatest.cc
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: BSD-2-Clause
#include <array>
+#include <cstring>
#include <tuple>
#include "avif/avif.h"
@@ -413,6 +414,32 @@
//------------------------------------------------------------------------------
+// Regression test for https://github.com/AOMediaCodec/libavif/issues/1333.
+// Coverage for avifImageFixXMP().
+TEST(MetadataTest, XMPWithTrailingNullCharacter) {
+ testutil::AvifImagePtr jpg =
+ testutil::ReadImage(data_path, "paris_xmp_trailing_null.jpg");
+ ASSERT_NE(jpg, nullptr);
+ ASSERT_NE(jpg->xmp.size, 0u);
+ // avifJPEGRead() should strip the trailing null character.
+ ASSERT_EQ(std::memchr(jpg->xmp.data, '\0', jpg->xmp.size), nullptr);
+
+ // Append a zero byte to see what happens when encoded with libpng.
+ avifRWDataRealloc(&jpg->xmp, jpg->xmp.size + 1);
+ jpg->xmp.data[jpg->xmp.size - 1] = '\0';
+ testutil::WriteImage(jpg.get(),
+ (testing::TempDir() + "xmp_trailing_null.png").c_str());
+ const testutil::AvifImagePtr png =
+ testutil::ReadImage(testing::TempDir().c_str(), "xmp_trailing_null.png");
+ ASSERT_NE(png, nullptr);
+ ASSERT_NE(png->xmp.size, 0u);
+ // avifPNGRead() should strip the trailing null character, but the libpng
+ // export during testutil::WriteImage() probably took care of that anyway.
+ ASSERT_EQ(std::memchr(png->xmp.data, '\0', png->xmp.size), nullptr);
+}
+
+//------------------------------------------------------------------------------
+
} // namespace
} // namespace libavif