Do not allow for monochrome + identity matrix (#2667)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3bea607..3c516ba 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,6 +22,8 @@
* Reject the conversion in avifenc from non-monochrome/monochrome to
monochrome/non-monochrome when an ICC profile is present and not explicitly
discarded.
+* Forbid encoding with AVIF_MATRIX_COEFFICIENTS_IDENTITY and
+ AVIF_PIXEL_FORMAT_YUV400 to be AV1 spec compatible
## [1.2.0] - 2025-02-25
diff --git a/src/reformat.c b/src/reformat.c
index 919829b..ecba09e 100644
--- a/src/reformat.c
+++ b/src/reformat.c
@@ -115,11 +115,11 @@
return AVIF_FALSE;
}
+ // Removing 400 here would break backward behavior but would respect the spec.
if ((image->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_IDENTITY) && (image->yuvFormat != AVIF_PIXEL_FORMAT_YUV444) &&
(image->yuvFormat != AVIF_PIXEL_FORMAT_YUV400)) {
return AVIF_FALSE;
}
-
avifGetPixelFormatInfo(image->yuvFormat, &info->formatInfo);
avifCalcYUVCoefficients(image, &info->kr, &info->kg, &info->kb);
diff --git a/src/write.c b/src/write.c
index 32cf133..502140f 100644
--- a/src/write.c
+++ b/src/write.c
@@ -1657,6 +1657,13 @@
return AVIF_RESULT_INVALID_IMAGE_GRID;
}
+ // AV1 (Version 1.0.0 with Errata 1), Section 6.4.2. Color config semantics
+ // If matrix coefficients is equal to MC_IDENTITY, it is a requirement of bitstream conformance that subsampling_x is equal to 0 and subsampling_y is equal to 0.
+ if (cellImage->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_IDENTITY && cellImage->yuvFormat != AVIF_PIXEL_FORMAT_YUV444) {
+ avifDiagnosticsPrintf(diag, "subsampling must be 0 (4:4:4) with identity matrix coefficients");
+ return AVIF_RESULT_INVALID_ARGUMENT;
+ }
+
if (!cellImage->yuvPlanes[AVIF_CHAN_Y]) {
return AVIF_RESULT_NO_CONTENT;
}
diff --git a/tests/gtest/aviflosslesstest.cc b/tests/gtest/aviflosslesstest.cc
index b5cd313..7d498a2 100644
--- a/tests/gtest/aviflosslesstest.cc
+++ b/tests/gtest/aviflosslesstest.cc
@@ -1,6 +1,8 @@
// Copyright 2023 Google LLC
// SPDX-License-Identifier: BSD-2-Clause
+#include <tuple>
+
#include "avif/avif.h"
#include "aviftest_helpers.h"
#include "avifutil.h"
@@ -12,86 +14,189 @@
// Used to pass the data folder path to the GoogleTest suites.
const char* data_path = nullptr;
+// Tests that AVIF_MATRIX_COEFFICIENTS_YCGCO_RO does not work because the input
+// depth is not odd.
+TEST(LosslessTest, YCGCO_RO) {
+ const std::string file_path =
+ std::string(data_path) + "paris_icc_exif_xmp.png";
+ ImagePtr image(avifImageCreateEmpty());
+ ASSERT_NE(image, nullptr);
+ image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_YCGCO_RO;
+ const avifAppFileFormat file_format = avifReadImage(
+ file_path.c_str(), /*requestedFormat=*/AVIF_PIXEL_FORMAT_NONE,
+ /*requestedDepth=*/0,
+ /*chromaDownsampling=*/AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC,
+ /*ignoreColorProfile=*/false, /*ignoreExif=*/false, /*ignoreXMP=*/false,
+ /*allowChangingCicp=*/true, /*ignoreGainMap=*/true,
+ AVIF_DEFAULT_IMAGE_SIZE_LIMIT, image.get(), /*outDepth=*/nullptr,
+ /*sourceTiming=*/nullptr, /*frameIter=*/nullptr);
+ ASSERT_EQ(file_format, AVIF_APP_FILE_FORMAT_UNKNOWN);
+}
+
+// Reads an image with a simpler API. ASSERT_NO_FATAL_FAILURE should be used
+// when calling it.
+void ReadImageSimple(const std::string& file_name, avifPixelFormat pixel_format,
+ avifMatrixCoefficients matrix_coefficients,
+ avifBool ignore_icc, ImagePtr& image) {
+ const std::string file_path = std::string(data_path) + file_name;
+ image.reset(avifImageCreateEmpty());
+ ASSERT_NE(image, nullptr);
+ image->matrixCoefficients = matrix_coefficients;
+ const avifAppFileFormat file_format = avifReadImage(
+ file_path.c_str(),
+ /*requestedFormat=*/pixel_format,
+ /*requestedDepth=*/0,
+ /*chromaDownsampling=*/AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC,
+ /*ignoreColorProfile=*/ignore_icc, /*ignoreExif=*/false,
+ /*ignoreXMP=*/false, /*allowChangingCicp=*/true,
+ /*ignoreGainMap=*/true, AVIF_DEFAULT_IMAGE_SIZE_LIMIT, image.get(),
+ /*outDepth=*/nullptr, /*sourceTiming=*/nullptr,
+ /*frameIter=*/nullptr);
+ if (matrix_coefficients == AVIF_MATRIX_COEFFICIENTS_IDENTITY &&
+ image->yuvFormat == AVIF_PIXEL_FORMAT_YUV420) {
+ // 420 cannot be converted from RGB to YUV with
+ // AVIF_MATRIX_COEFFICIENTS_IDENTITY due to a decision taken in
+ // avifGetYUVColorSpaceInfo.
+ ASSERT_EQ(file_format, AVIF_APP_FILE_FORMAT_UNKNOWN);
+ image.reset();
+ return;
+ }
+ ASSERT_NE(file_format, AVIF_APP_FILE_FORMAT_UNKNOWN);
+}
+
+// The test parameters are: the file path, the matrix coefficients and the pixel
+// format.
+class LosslessTest
+ : public testing::TestWithParam<std::tuple<std::string, int, int>> {};
+
+// Tests encode/decode round trips.
+TEST_P(LosslessTest, EncodeDecode) {
+ const std::string& file_name = std::get<0>(GetParam());
+ const avifMatrixCoefficients matrix_coefficients =
+ static_cast<avifMatrixCoefficients>(std::get<1>(GetParam()));
+ const avifPixelFormat pixel_format =
+ static_cast<avifPixelFormat>(std::get<2>(GetParam()));
+ // Ignore ICC when going from RGB to gray.
+ const avifBool ignore_icc =
+ pixel_format == AVIF_PIXEL_FORMAT_YUV400 ? AVIF_TRUE : AVIF_FALSE;
+
+ // Read a ground truth image but ask for certain matrix coefficients.
+ ImagePtr image;
+ ASSERT_NO_FATAL_FAILURE(ReadImageSimple(
+ file_name, pixel_format, matrix_coefficients, ignore_icc, image));
+ if (image == nullptr) return;
+
+ // Encode.
+ EncoderPtr encoder(avifEncoderCreate());
+ ASSERT_NE(encoder, nullptr);
+ encoder->speed = AVIF_SPEED_FASTEST;
+ encoder->quality = AVIF_QUALITY_LOSSLESS;
+ testutil::AvifRwData encoded;
+ avifResult result = avifEncoderWrite(encoder.get(), image.get(), &encoded);
+
+ if (matrix_coefficients == AVIF_MATRIX_COEFFICIENTS_IDENTITY &&
+ image->yuvFormat != AVIF_PIXEL_FORMAT_YUV444) {
+ // The AV1 spec does not allow identity with subsampling.
+ ASSERT_EQ(result, AVIF_RESULT_INVALID_ARGUMENT);
+ return;
+ }
+ ASSERT_EQ(result, AVIF_RESULT_OK) << avifResultToString(result);
+
+ // Decode to RAM.
+ ImagePtr decoded(avifImageCreateEmpty());
+ ASSERT_NE(decoded, nullptr);
+ DecoderPtr decoder(avifDecoderCreate());
+ ASSERT_NE(decoder, nullptr);
+ result = avifDecoderReadMemory(decoder.get(), decoded.get(), encoded.data,
+ encoded.size);
+ ASSERT_EQ(result, AVIF_RESULT_OK) << avifResultToString(result);
+
+ // What we read should be what we encoded
+ ASSERT_TRUE(testutil::AreImagesEqual(*image, *decoded));
+}
+
+// Reads an image losslessly, using identiy MC.
+ImagePtr ReadImageLossless(const std::string& path,
+ avifPixelFormat requested_format,
+ avifBool ignore_icc) {
+ ImagePtr image(avifImageCreateEmpty());
+ if (!image) return nullptr;
+ image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_IDENTITY;
+ if (avifReadImage(path.c_str(), requested_format, /*requested_depth=*/0,
+ /*chroma_downsampling=*/AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC,
+ ignore_icc, /*ignore_exif=*/false, /*ignore_xmp=*/false,
+ /*allow_changing_cicp=*/false, /*ignore_gain_map=*/false,
+ AVIF_DEFAULT_IMAGE_SIZE_LIMIT, image.get(),
+ /*outDepth=*/nullptr, /*sourceTiming=*/nullptr,
+ /*frameIter=*/nullptr) == AVIF_APP_FILE_FORMAT_UNKNOWN) {
+ return nullptr;
+ }
+ return image;
+}
+
// Tests encode/decode round trips under different matrix coefficients.
-TEST(BasicTest, EncodeDecodeMatrixCoefficients) {
- for (const auto& file_name :
- {"paris_icc_exif_xmp.png", "paris_exif_xmp_icc.jpg"}) {
- // Read a ground truth image with default matrix coefficients.
- const std::string file_path = std::string(data_path) + file_name;
- const ImagePtr ground_truth_image =
- testutil::ReadImage(data_path, file_name);
+TEST_P(LosslessTest, EncodeDecodeMatrixCoefficients) {
+ const std::string& file_name = std::get<0>(GetParam());
+ const avifMatrixCoefficients matrix_coefficients =
+ static_cast<avifMatrixCoefficients>(std::get<1>(GetParam()));
+ const avifPixelFormat pixel_format =
+ static_cast<avifPixelFormat>(std::get<2>(GetParam()));
+ // Ignore ICC when going from RGB to gray.
+ const avifBool ignore_icc =
+ pixel_format == AVIF_PIXEL_FORMAT_YUV400 ? AVIF_TRUE : AVIF_FALSE;
- for (auto matrix_coefficient :
- {AVIF_MATRIX_COEFFICIENTS_IDENTITY, AVIF_MATRIX_COEFFICIENTS_YCGCO,
- AVIF_MATRIX_COEFFICIENTS_YCGCO_RE,
- AVIF_MATRIX_COEFFICIENTS_YCGCO_RO}) {
- // Read a ground truth image but ask for certain matrix coefficients.
- ImagePtr image(avifImageCreateEmpty());
- ASSERT_NE(image, nullptr);
- image->matrixCoefficients = (avifMatrixCoefficients)matrix_coefficient;
- const avifAppFileFormat file_format = avifReadImage(
- file_path.c_str(),
- /*requestedFormat=*/AVIF_PIXEL_FORMAT_NONE,
- /*requestedDepth=*/0,
- /*chromaDownsampling=*/AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC,
- /*ignoreColorProfile=*/false, /*ignoreExif=*/false,
- /*ignoreXMP=*/false, /*allowChangingCicp=*/true,
- /*ignoreGainMap=*/true, AVIF_DEFAULT_IMAGE_SIZE_LIMIT, image.get(),
- /*outDepth=*/nullptr, /*sourceTiming=*/nullptr,
- /*frameIter=*/nullptr);
- if (matrix_coefficient == AVIF_MATRIX_COEFFICIENTS_YCGCO_RO) {
- // AVIF_MATRIX_COEFFICIENTS_YCGCO_RO does not work because the input
- // depth is not odd.
- ASSERT_EQ(file_format, AVIF_APP_FILE_FORMAT_UNKNOWN);
- continue;
- }
- ASSERT_NE(file_format, AVIF_APP_FILE_FORMAT_UNKNOWN);
+ // Read a ground truth image but ask for certain matrix coefficients.
+ ImagePtr image;
+ ASSERT_NO_FATAL_FAILURE(ReadImageSimple(
+ file_name, pixel_format, matrix_coefficients, ignore_icc, image));
+ if (image == nullptr) return;
- // Encode.
- EncoderPtr encoder(avifEncoderCreate());
- ASSERT_NE(encoder, nullptr);
- encoder->speed = AVIF_SPEED_FASTEST;
- encoder->quality = AVIF_QUALITY_LOSSLESS;
- encoder->qualityAlpha = AVIF_QUALITY_LOSSLESS;
- testutil::AvifRwData encoded;
- avifResult result =
- avifEncoderWrite(encoder.get(), image.get(), &encoded);
- ASSERT_EQ(result, AVIF_RESULT_OK) << avifResultToString(result);
+ // Convert to a temporary PNG and read back losslessly.
+ const std::string tmp_path =
+ testing::TempDir() + "decoded_EncodeDecodeMatrixCoefficients.png";
+ ASSERT_TRUE(testutil::WriteImage(image.get(), tmp_path.c_str()));
+ const ImagePtr decoded_lossless =
+ ReadImageLossless(tmp_path, pixel_format, ignore_icc);
+ if (image->yuvFormat == AVIF_PIXEL_FORMAT_YUV420) {
+ // 420 cannot be converted from RGB to YUV with
+ // AVIF_MATRIX_COEFFICIENTS_IDENTITY due to a decision taken in
+ // avifGetYUVColorSpaceInfo.
+ ASSERT_EQ(decoded_lossless, nullptr);
+ return;
+ }
+ ASSERT_NE(decoded_lossless, nullptr);
- // Decode to RAM.
- ImagePtr decoded(avifImageCreateEmpty());
- ASSERT_NE(decoded, nullptr);
- DecoderPtr decoder(avifDecoderCreate());
- ASSERT_NE(decoder, nullptr);
- result = avifDecoderReadMemory(decoder.get(), decoded.get(), encoded.data,
- encoded.size);
- ASSERT_EQ(result, AVIF_RESULT_OK) << avifResultToString(result);
+ // Verify that the ground truth and decoded images are the same.
+ const ImagePtr ground_truth_lossless = ReadImageLossless(
+ std::string(data_path) + file_name, pixel_format, ignore_icc);
+ ASSERT_NE(ground_truth_lossless, nullptr);
+ const bool are_images_equal =
+ testutil::AreImagesEqual(*ground_truth_lossless, *decoded_lossless);
- // Convert to a temporary PNG and read back, to have default matrix
- // coefficients.
- const std::string temp_dir = testing::TempDir();
- const std::string temp_file = "decoded_default.png";
- const std::string tmp_path = temp_dir + temp_file;
- ASSERT_TRUE(testutil::WriteImage(decoded.get(), tmp_path.c_str()));
- const ImagePtr decoded_default =
- testutil::ReadImage(temp_dir.c_str(), temp_file.c_str());
-
- // Verify that the ground truth and decoded images are the same.
- const bool are_images_equal =
- testutil::AreImagesEqual(*ground_truth_image, *decoded_default);
-
- if (matrix_coefficient == AVIF_MATRIX_COEFFICIENTS_IDENTITY ||
- matrix_coefficient == AVIF_MATRIX_COEFFICIENTS_YCGCO_RE) {
- ASSERT_TRUE(are_images_equal);
- } else {
- // AVIF_MATRIX_COEFFICIENTS_YCGCO is not lossless because it does not
- // expand the input bit range for YCgCo values.
- ASSERT_FALSE(are_images_equal);
- }
- }
+ if (matrix_coefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO) {
+ // AVIF_MATRIX_COEFFICIENTS_YCGCO is not a lossless transform.
+ ASSERT_FALSE(are_images_equal);
+ } else if (matrix_coefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO_RE &&
+ pixel_format == AVIF_PIXEL_FORMAT_YUV400) {
+ // For gray images, information is lost in YCGCO_RE.
+ ASSERT_FALSE(are_images_equal);
+ } else {
+ ASSERT_TRUE(are_images_equal);
}
}
+INSTANTIATE_TEST_SUITE_P(
+ LosslessTestInstantiation, LosslessTest,
+ testing::Combine(
+ testing::Values("paris_icc_exif_xmp.png", "paris_exif_xmp_icc.jpg"),
+ testing::Values(static_cast<int>(AVIF_MATRIX_COEFFICIENTS_IDENTITY),
+ static_cast<int>(AVIF_MATRIX_COEFFICIENTS_YCGCO),
+ static_cast<int>(AVIF_MATRIX_COEFFICIENTS_YCGCO_RE)),
+ testing::Values(static_cast<int>(AVIF_PIXEL_FORMAT_NONE),
+ static_cast<int>(AVIF_PIXEL_FORMAT_YUV444),
+ static_cast<int>(AVIF_PIXEL_FORMAT_YUV420),
+ static_cast<int>(AVIF_PIXEL_FORMAT_YUV400))));
+
} // namespace
} // namespace avif