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