Skip to content

Commit

Permalink
Do not allow for monochrome + identity matrix
Browse files Browse the repository at this point in the history
  • Loading branch information
vrabaud committed Mar 5, 2025
1 parent 6b31024 commit ec46c83
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 76 deletions.
4 changes: 4 additions & 0 deletions apps/shared/avifjpeg.c
Original file line number Diff line number Diff line change
Expand Up @@ -909,6 +909,10 @@ static avifBool avifJPEGReadInternal(FILE * f,
unsigned int iccDataLen;
if (read_icc_profile(&cinfo, &iccDataTmp, &iccDataLen)) {
iccData = iccDataTmp;
if (requestedFormat == AVIF_PIXEL_FORMAT_YUV400) {
fprintf(stderr, "The RGB ICC profile is asked to be kept while the output image is gray.\n");
goto cleanup;
}
if (avifImageSetProfileICC(avif, iccDataTmp, (size_t)iccDataLen) != AVIF_RESULT_OK) {
fprintf(stderr, "Setting ICC profile failed: %s (out of memory)\n", inputFilename);
goto cleanup;
Expand Down
7 changes: 7 additions & 0 deletions apps/shared/avifpng.c
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ avifBool avifPNGRead(const char * inputFilename,
png_structp png = NULL;
png_infop info = NULL;
png_bytep * volatile rowPointers = NULL;
fprintf(stderr, "Conversion to YUV failed: %d %d\n", (int)avif->matrixCoefficients, (int)avif->yuvFormat);

avifRGBImage rgb;
memset(&rgb, 0, sizeof(avifRGBImage));
Expand Down Expand Up @@ -365,6 +366,11 @@ avifBool avifPNGRead(const char * inputFilename,
// When the sRGB / iCCP chunk is present, applications that recognize it and are capable of color management
// must ignore the gAMA and cHRM chunks and use the sRGB / iCCP chunk instead.
if (png_get_iCCP(png, info, &iccpProfileName, &iccpCompression, &iccpData, &iccpDataLen) == PNG_INFO_iCCP) {
if (rawColorType != PNG_COLOR_TYPE_GRAY && rawColorType != PNG_COLOR_TYPE_GRAY_ALPHA &&
avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) {
fprintf(stderr, "The RGB ICC profile is asked to be kept while the output image is gray.\n");
goto cleanup;
}
if (avifImageSetProfileICC(avif, iccpData, iccpDataLen) != AVIF_RESULT_OK) {
fprintf(stderr, "Setting ICC profile failed: out of memory.\n");
goto cleanup;
Expand Down Expand Up @@ -482,6 +488,7 @@ avifBool avifPNGRead(const char * inputFilename,
rgbRow += rgb.rowBytes;
}
png_read_image(png, rowPointers);
fprintf(stderr, "Conversion to YUV failed: %d %d\n", (int)avif->matrixCoefficients, (int)avif->yuvFormat);
if (avifImageRGBToYUV(avif, &rgb) != AVIF_RESULT_OK) {
fprintf(stderr, "Conversion to YUV failed: %s\n", inputFilename);
goto cleanup;
Expand Down
7 changes: 7 additions & 0 deletions src/write.c
Original file line number Diff line number Diff line change
Expand Up @@ -1657,6 +1657,13 @@ static avifResult avifValidateGrid(uint32_t gridCols,
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 with identity matrix coefficients");
return AVIF_RESULT_INVALID_ARGUMENT;
}

if (!cellImage->yuvPlanes[AVIF_CHAN_Y]) {
return AVIF_RESULT_NO_CONTENT;
}
Expand Down
190 changes: 115 additions & 75 deletions tests/gtest/aviflosslesstest.cc
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -12,86 +14,124 @@ namespace {
// Used to pass the data folder path to the GoogleTest suites.
const char* data_path = nullptr;

class LosslessTest
: public testing::TestWithParam<
std::tuple<std::string, avifMatrixCoefficients, avifPixelFormat>> {};

// 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);

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);

// 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);

// 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);

// 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);
}
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 = 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.
const std::string file_path = std::string(data_path) + file_name;
ImagePtr image(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_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);
return;
}
ASSERT_NE(file_format, AVIF_APP_FILE_FORMAT_UNKNOWN);

// Define a ground_truth_image with default matrix coefficients.
ImagePtr ground_truth_image;
if (pixel_format == AVIF_PIXEL_FORMAT_NONE) {
ground_truth_image = testutil::ReadImage(
data_path, file_name.c_str(), pixel_format, /*requested_depth=*/0,
/*chroma_downsampling=*/AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC, ignore_icc);
ASSERT_NE(ground_truth_image, nullptr);
}

// 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);

if (matrix_coefficients == AVIF_MATRIX_COEFFICIENTS_IDENTITY &&
pixel_format != AVIF_PIXEL_FORMAT_NONE &&
pixel_format != 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);

// 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(), pixel_format, /*requested_depth=*/0,
/*chroma_downsampling=*/AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC, ignore_icc);

// Verify that the ground truth and decoded images are the same.
const bool are_images_equal = testutil::AreImagesEqual(
ground_truth_image ? *ground_truth_image : *image, *decoded_default);

if (matrix_coefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO) {
if (pixel_format == AVIF_PIXEL_FORMAT_NONE) {
// AVIF_MATRIX_COEFFICIENTS_YCGCO is not lossless because it does not
// expand the input bit range for CgCo values.
ASSERT_FALSE(are_images_equal);
} else {
// For AVIF_PIXEL_FORMAT_YUV_400, CgCo is not relevant.
ASSERT_TRUE(are_images_equal);
}
} else if ((pixel_format == AVIF_PIXEL_FORMAT_YUV400 &&
matrix_coefficients == AVIF_MATRIX_COEFFICIENTS_UNSPECIFIED) ||
(pixel_format == AVIF_PIXEL_FORMAT_NONE &&
(matrix_coefficients == AVIF_MATRIX_COEFFICIENTS_IDENTITY ||
matrix_coefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO_RE))) {
ASSERT_TRUE(are_images_equal);
} else {
ASSERT_FALSE(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(AVIF_MATRIX_COEFFICIENTS_IDENTITY,
AVIF_MATRIX_COEFFICIENTS_UNSPECIFIED,
AVIF_MATRIX_COEFFICIENTS_YCGCO,
AVIF_MATRIX_COEFFICIENTS_YCGCO_RE,
AVIF_MATRIX_COEFFICIENTS_YCGCO_RO),
testing::Values(AVIF_PIXEL_FORMAT_NONE, AVIF_PIXEL_FORMAT_YUV400)));

} // namespace
} // namespace avif

Expand Down
2 changes: 1 addition & 1 deletion tests/test_cmd_grid.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ pushd ${TMP_DIR}
"${AVIFDEC}" "${ENCODED_FILE_2x2}" "${DECODED_FILE_2x2}"

echo "Testing monochrome grid with odd width (403 px)"
"${AVIFENC}" -s 8 "${INPUT_PNG}" --grid 2x2 --yuv 400 -o "${ENCODED_FILE_2x2}"
"${AVIFENC}" -s 8 "${INPUT_PNG}" --grid 2x2 --yuv 400 --ignore-icc -o "${ENCODED_FILE_2x2}"
"${AVIFDEC}" "${ENCODED_FILE_2x2}" "${DECODED_FILE_2x2}"

echo "Testing max grid"
Expand Down

0 comments on commit ec46c83

Please sign in to comment.