blob: 62f56d56318027b37a322f8fd28101c0b3246cef [file] [log] [blame]
// Copyright 2022 Google LLC
// SPDX-License-Identifier: BSD-2-Clause
#include <string>
#include "avif/avif.h"
#include "avifjpeg.h"
#include "avifpng.h"
#include "aviftest_helpers.h"
#include "avifutil.h"
#include "gtest/gtest.h"
#include "iccmaker.h"
namespace avif {
namespace {
// Used to pass the data folder path to the GoogleTest suites.
const char* data_path = nullptr;
//------------------------------------------------------------------------------
// Generic tests
bool AreSamplesEqualForAllReadSettings(const char* file_name1,
const char* file_name2) {
constexpr bool kIgnoreMetadata = true;
for (avifPixelFormat requested_format :
{AVIF_PIXEL_FORMAT_YUV444, AVIF_PIXEL_FORMAT_YUV422,
AVIF_PIXEL_FORMAT_YUV420, AVIF_PIXEL_FORMAT_YUV400}) {
for (int requested_depth : {8, 10, 12, 16}) {
for (avifChromaDownsampling chroma_downsampling :
{AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC,
AVIF_CHROMA_DOWNSAMPLING_FASTEST,
AVIF_CHROMA_DOWNSAMPLING_BEST_QUALITY,
AVIF_CHROMA_DOWNSAMPLING_AVERAGE}) {
const ImagePtr image1 = testutil::ReadImage(
data_path, file_name1, requested_format, requested_depth,
chroma_downsampling, kIgnoreMetadata, kIgnoreMetadata,
kIgnoreMetadata);
const ImagePtr image2 = testutil::ReadImage(
data_path, file_name2, requested_format, requested_depth,
chroma_downsampling, kIgnoreMetadata, kIgnoreMetadata,
kIgnoreMetadata);
if (!image1 || !image2 || !testutil::AreImagesEqual(*image1, *image2)) {
return false;
}
}
}
}
return true;
}
TEST(JpegTest, ReadAllSubsamplingsAndAllBitDepths) {
EXPECT_TRUE(AreSamplesEqualForAllReadSettings(
"paris_exif_xmp_icc.jpg", "paris_exif_orientation_5.jpg"));
}
TEST(PngTest, ReadAllSubsamplingsAndAllBitDepths) {
EXPECT_TRUE(AreSamplesEqualForAllReadSettings(
"paris_icc_exif_xmp.png", "paris_icc_exif_xmp_at_end.png"));
}
//------------------------------------------------------------------------------
// PNG color metadata handling tests
// Verify we can read a PNG file with PNG_COLOR_TYPE_PALETTE and a tRNS chunk.
TEST(PngTest, PaletteColorTypeWithTrnsChunk) {
const ImagePtr image = testutil::ReadImage(data_path, "draw_points.png",
AVIF_PIXEL_FORMAT_YUV444, 8);
ASSERT_NE(image, nullptr);
EXPECT_EQ(image->width, 33u);
EXPECT_EQ(image->height, 11u);
EXPECT_NE(image->alphaPlane, nullptr);
}
// Verify we can read a PNG file with PNG_COLOR_TYPE_RGB and a tRNS chunk
// after a PLTE chunk.
TEST(PngTest, RgbColorTypeWithTrnsAfterPlte) {
const ImagePtr image = testutil::ReadImage(
data_path, "circle-trns-after-plte.png", AVIF_PIXEL_FORMAT_YUV444, 8);
ASSERT_NE(image, nullptr);
EXPECT_EQ(image->width, 100u);
EXPECT_EQ(image->height, 60u);
EXPECT_NE(image->alphaPlane, nullptr);
}
// Verify we can read a PNG file with PNG_COLOR_TYPE_RGB and a tRNS chunk
// before a PLTE chunk, with no MSan use-of-uninitialized-value warnings in
// avifImageRGBToYUV(). libpng 1.6.46 or older considers the tRNS chunk as
// invalid and ignores it, so the decoded image has no alpha. The behavior
// changed starting with libpng 1.6.47 (the decoded image has alpha).
// See https://github.com/pnggroup/libpng/blob/libpng16/CHANGES#L6243-L6246.
TEST(PngTest, RgbColorTypeWithTrnsBeforePlte) {
const ImagePtr image = testutil::ReadImage(
data_path, "circle-trns-before-plte.png", AVIF_PIXEL_FORMAT_YUV444, 8);
ASSERT_NE(image, nullptr);
EXPECT_EQ(image->width, 100u);
EXPECT_EQ(image->height, 60u);
}
constexpr size_t kColorProfileSize = 376;
constexpr size_t kGrayProfileSize = 275;
// Verify we can read a color PNG file tagged as gamma 2.2 through gAMA chunk,
// and set transfer characteristics correctly.
TEST(PngTest, ColorGamma22) {
const ImagePtr image = testutil::ReadImage(data_path, "ffffcc-gamma2.2.png");
ASSERT_NE(image, nullptr);
// gamma 2.2 should match BT470M
EXPECT_EQ(image->transferCharacteristics,
AVIF_TRANSFER_CHARACTERISTICS_BT470M);
// should not generate ICC profile
EXPECT_EQ(image->icc.size, 0u);
}
// Verify that color info does not get overwritten if allow_changing_cicp is
// false.
TEST(PngTest, ColorGamma22ForbitChangingCicp) {
const ImagePtr image = testutil::ReadImage(
data_path, "ffffcc-gamma2.2.png",
/*requested_format=*/AVIF_PIXEL_FORMAT_NONE,
/*requested_depth=*/0, AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC,
/*ignore_icc=*/false, /*ignore_exif*/ false,
/*ignore_xmp=*/false, /*allow_changing_cicp=*/false);
ASSERT_NE(image, nullptr);
// Color info should still be unspecified even if file gamma is 2.2
EXPECT_EQ(image->colorPrimaries, AVIF_COLOR_PRIMARIES_UNSPECIFIED);
EXPECT_EQ(image->transferCharacteristics,
AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED);
// should not generate ICC profile
EXPECT_EQ(image->icc.size, 0u);
}
// Verify we can read a color PNG file tagged as gamma 1.6 through gAMA chunk,
// and generate a color profile for it.
TEST(PngTest, ColorGamma16) {
const ImagePtr image = testutil::ReadImage(data_path, "ffffcc-gamma1.6.png");
ASSERT_NE(image, nullptr);
// if ICC profile generated, CP and TC should be set to unspecified
EXPECT_EQ(image->colorPrimaries, AVIF_COLOR_PRIMARIES_UNSPECIFIED);
EXPECT_EQ(image->transferCharacteristics,
AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED);
// should generate a color profile
EXPECT_EQ(image->icc.size, kColorProfileSize);
// Generated profile is tested in test_cmd_icc_profile.sh
}
// Verify we can read a gray PNG file tagged as gamma 2.2 through gAMA chunk,
// and set transfer characteristics correctly.
TEST(PngTest, GrayGamma22) {
const ImagePtr image = testutil::ReadImage(data_path, "ffffff-gamma2.2.png",
AVIF_PIXEL_FORMAT_YUV400);
ASSERT_NE(image, nullptr);
// gamma 2.2 should match BT470M
EXPECT_EQ(image->transferCharacteristics,
AVIF_TRANSFER_CHARACTERISTICS_BT470M);
// should not generate ICC profile
EXPECT_EQ(image->icc.size, 0u);
}
// Verify we can read a gray PNG file tagged as gamma 1.6 through gAMA chunk,
// and generate a gray profile for it.
TEST(PngTest, GrayGamma16) {
const ImagePtr image = testutil::ReadImage(data_path, "ffffff-gamma1.6.png",
AVIF_PIXEL_FORMAT_YUV400);
ASSERT_NE(image, nullptr);
// if ICC profile generated, CP and TC should be set to unspecified
EXPECT_EQ(image->colorPrimaries, AVIF_COLOR_PRIMARIES_UNSPECIFIED);
EXPECT_EQ(image->transferCharacteristics,
AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED);
// should generate a gray profile
EXPECT_EQ(image->icc.size, kGrayProfileSize);
// Generated profile is tested in test_cmd_icc_profile.sh
}
// Verify we can read a color PNG file tagged as sRGB through sRGB chunk,
// and set color primaries and transfer characteristics correctly.
TEST(PngTest, SRGBTagged) {
const ImagePtr image = testutil::ReadImage(data_path, "ffffcc-srgb.png");
ASSERT_NE(image, nullptr);
// should set to BT709 primaries and SRGB transfer
EXPECT_EQ(image->colorPrimaries, AVIF_COLOR_PRIMARIES_BT709);
EXPECT_EQ(image->transferCharacteristics, AVIF_TRANSFER_CHARACTERISTICS_SRGB);
// should not generate ICC profile
EXPECT_EQ(image->icc.size, 0u);
}
// Verify we are not generating profile if asked to ignore it.
TEST(PngTest, IgnoreProfile) {
const ImagePtr image = testutil::ReadImage(
data_path, "ffffcc-gamma1.6.png", AVIF_PIXEL_FORMAT_NONE, 0,
AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC, true);
ASSERT_NE(image, nullptr);
// should be left unspecified
EXPECT_EQ(image->colorPrimaries, AVIF_COLOR_PRIMARIES_UNSPECIFIED);
EXPECT_EQ(image->transferCharacteristics, AVIF_COLOR_PRIMARIES_UNSPECIFIED);
// should not generate ICC profile
EXPECT_EQ(image->icc.size, 0u);
}
// Verify we can read a PNG file tagged as gamma 2.2 through gAMA chunk
// and BT709 primaries through cHRM chunk,
// and set color primaries and transfer characteristics correctly.
TEST(PngTest, BT709Gamma22) {
const ImagePtr image =
testutil::ReadImage(data_path, "ArcTriomphe-cHRM-orig.png");
ASSERT_NE(image, nullptr);
// primaries should match BT709
EXPECT_EQ(image->colorPrimaries, AVIF_COLOR_PRIMARIES_BT709);
// gamma 2.2 should match BT470M
EXPECT_EQ(image->transferCharacteristics,
AVIF_TRANSFER_CHARACTERISTICS_BT470M);
// should not generate ICC profile
EXPECT_EQ(image->icc.size, 0u);
}
// Verify we can read a PNG file tagged as gamma 2.2 through gAMA chunk
// and BT709 primaries with red and green swapped through cHRM chunk,
// and generate a color profile for it.
TEST(PngTest, BT709SwappedGamma22) {
const ImagePtr image =
testutil::ReadImage(data_path, "ArcTriomphe-cHRM-red-green-swap.png");
ASSERT_NE(image, nullptr);
// if ICC profile generated, CP and TC should be set to unspecified
EXPECT_EQ(image->colorPrimaries, AVIF_COLOR_PRIMARIES_UNSPECIFIED);
EXPECT_EQ(image->transferCharacteristics,
AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED);
// should generate a color profile
EXPECT_EQ(image->icc.size, kColorProfileSize);
// Generated profile is tested in test_cmd_icc_profile.sh
}
//------------------------------------------------------------------------------
// ICC metadata tests
constexpr size_t kChecksumOffset = 0x54;
// Verify we wrote correct hash in generated ICC profile.
TEST(ICCTest, GeneratedICCHash) {
float primariesCoords[8];
avifColorPrimariesGetValues(AVIF_COLOR_PRIMARIES_BT709, primariesCoords);
testutil::AvifRwData icc;
EXPECT_EQ(avifGenerateRGBICC(&icc, 2.2f, primariesCoords), AVIF_TRUE);
// Steps to generate this checksum:
// - memset 16 bytes starting from icc.data + kChecksumOffset to 0
// - write `icc` to file
// - run `md5sum` with the written file
const uint8_t expectedChecksumRGB[] = {
// 89b06c4cc611c3110c022e06e6a0f81b
0x89, 0xb0, 0x6c, 0x4c, 0xc6, 0x11, 0xc3, 0x11,
0x0c, 0x02, 0x2e, 0x06, 0xe6, 0xa0, 0xf8, 0x1b,
};
EXPECT_EQ(memcmp(icc.data + kChecksumOffset, expectedChecksumRGB,
sizeof(expectedChecksumRGB)),
0);
EXPECT_EQ(avifGenerateGrayICC(&icc, 2.2f, primariesCoords), AVIF_TRUE);
// Steps to generate this checksum:
// - memset 16 bytes starting from icc.data + kChecksumOffset to 0
// - write `icc` to file
// - run `md5sum` with the written file
const uint8_t expectedChecksumGray[] = {
// 7610e64f148ebe4d00cafa56cf45aea0
0x76, 0x10, 0xe6, 0x4f, 0x14, 0x8e, 0xbe, 0x4d,
0x00, 0xca, 0xfa, 0x56, 0xcf, 0x45, 0xae, 0xa0,
};
EXPECT_EQ(memcmp(icc.data + kChecksumOffset, expectedChecksumGray,
sizeof(expectedChecksumGray)),
0);
}
// Simpler function to read an image.
static avifAppFileFormat avifReadImageForRGB2Gray2RGB(const std::string& path,
avifPixelFormat format,
bool ignore_icc,
ImagePtr& image) {
return avifReadImage(
path.c_str(), 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);
}
// Verify the invalidity of keeping the ICC profile for a gray/color image read
// from a color/gray image.
TEST(ICCTest, RGB2Gray2RGB) {
constexpr char file_name[] = "paris_icc_exif_xmp.png";
const std::string file_path = std::string(data_path) + file_name;
for (auto format : {AVIF_PIXEL_FORMAT_YUV400, AVIF_PIXEL_FORMAT_YUV444}) {
// Read the ground truth image in the appropriate format.
ImagePtr image(avifImageCreateEmpty());
ASSERT_NE(image, nullptr);
ASSERT_NE(avifReadImageForRGB2Gray2RGB(file_path, format,
/*ignore_icc=*/true, image),
AVIF_APP_FILE_FORMAT_UNKNOWN);
// Add an ICC profile.
float primariesCoords[8];
avifColorPrimariesGetValues(AVIF_COLOR_PRIMARIES_BT709, primariesCoords);
testutil::AvifRwData icc;
if (format == AVIF_PIXEL_FORMAT_YUV400) {
EXPECT_EQ(avifGenerateGrayICC(&icc, 2.2f, primariesCoords), AVIF_TRUE);
} else {
EXPECT_EQ(avifGenerateRGBICC(&icc, 2.2f, primariesCoords), AVIF_TRUE);
}
ASSERT_EQ(avifImageSetProfileICC(image.get(), icc.data, icc.size),
AVIF_RESULT_OK);
for (const std::string ext : {"png", "jpg"}) {
// Write the image with the appropriate codec.
const std::string new_path =
testing::TempDir() + "tmp_RGB2Gray2RGB." + ext;
if (ext == "png") {
ASSERT_EQ(
avifPNGWrite(new_path.c_str(), image.get(), /*requestedDepth=*/0,
AVIF_CHROMA_UPSAMPLING_BEST_QUALITY,
/*compressionLevel=*/0),
AVIF_TRUE);
} else {
ASSERT_EQ(
avifJPEGWrite(new_path.c_str(), image.get(), /*jpegQuality=*/75,
AVIF_CHROMA_UPSAMPLING_BEST_QUALITY),
AVIF_TRUE);
}
for (bool ignore_icc : {false, true}) {
for (auto new_format :
{AVIF_PIXEL_FORMAT_YUV400, AVIF_PIXEL_FORMAT_YUV444}) {
ImagePtr new_image(avifImageCreateEmpty());
ASSERT_NE(new_image, nullptr);
const avifAppFileFormat new_file_format =
avifReadImageForRGB2Gray2RGB(new_path, new_format, ignore_icc,
new_image);
if (format == new_format || ignore_icc) {
ASSERT_NE(new_file_format, AVIF_APP_FILE_FORMAT_UNKNOWN);
} else {
// When formats are different, the ICC cannot be kept.
ASSERT_EQ(new_file_format, AVIF_APP_FILE_FORMAT_UNKNOWN);
}
}
}
}
}
}
//------------------------------------------------------------------------------
// Memory management tests
TEST(ImageSizeLimitTest, AllFormats) {
for (const uint32_t image_size_limit :
{1u, std::numeric_limits<uint32_t>::max()}) {
for (const char* filename :
{"paris_exif_xmp_icc.jpg", "paris_icc_exif_xmp.png",
"cosmos1650_yuv444_10bpc_p3pq.y4m"}) {
const std::string filepath = std::string(data_path) + filename;
ImagePtr image(avifImageCreateEmpty());
ASSERT_NE(image, nullptr);
const avifAppFileFormat format = avifReadImage(
filepath.c_str(), AVIF_PIXEL_FORMAT_NONE, /*requestedDepth=*/0,
AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC, /*ignoreColorProfile=*/true,
/*ignoreExif=*/true, /*ignoreXMP=*/true, /*allowChangingCicp=*/true,
/*ignoreGainMap=*/true, image_size_limit, image.get(),
/*outDepth=*/nullptr, /*sourceTiming=*/nullptr,
/*frameIter=*/nullptr);
if (image_size_limit == 1) {
EXPECT_EQ(format, AVIF_APP_FILE_FORMAT_UNKNOWN);
} else {
EXPECT_NE(format, AVIF_APP_FILE_FORMAT_UNKNOWN);
}
}
}
}
//------------------------------------------------------------------------------
} // namespace
} // namespace avif
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
if (argc != 2) {
std::cerr << "There must be exactly one argument containing the path to "
"the test data folder"
<< std::endl;
return 1;
}
avif::data_path = argv[1];
return RUN_ALL_TESTS();
}