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