blob: 2fcff5faedec6a4da49dadb98b2142c7b2cfbe28 [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <algorithm>
#include <cmath>
#include <tuple>
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gfx/color_space.h"
#include "ui/gfx/skia_color_space_util.h"
namespace gfx {
namespace {
// Returns the L-infty difference of u and v.
float Diff(const skia::Vector4& u, const skia::Vector4& v) {
float result = 0;
for (size_t i = 0; i < 4; ++i)
result = std::max(result, std::abs(u.fData[i] - v.fData[i]));
return result;
}
TEST(ColorSpace, RGBToYUV) {
const float kEpsilon = 1.0e-3f;
const size_t kNumTestRGBs = 3;
skia::Vector4 test_rgbs[kNumTestRGBs] = {
skia::Vector4(1.f, 0.f, 0.f, 1.f),
skia::Vector4(0.f, 1.f, 0.f, 1.f),
skia::Vector4(0.f, 0.f, 1.f, 1.f),
};
const size_t kNumColorSpaces = 4;
gfx::ColorSpace color_spaces[kNumColorSpaces] = {
gfx::ColorSpace::CreateREC601(),
gfx::ColorSpace::CreateREC709(),
gfx::ColorSpace::CreateJpeg(),
gfx::ColorSpace::CreateXYZD50(),
};
skia::Vector4 expected_yuvs[kNumColorSpaces][kNumTestRGBs] = {
// REC601
{
skia::Vector4(0.3195f, 0.3518f, 0.9392f, 1.0000f),
skia::Vector4(0.5669f, 0.2090f, 0.1322f, 1.0000f),
skia::Vector4(0.1607f, 0.9392f, 0.4286f, 1.0000f),
},
// REC709
{
skia::Vector4(0.2453f, 0.3994f, 0.9392f, 1.0000f),
skia::Vector4(0.6770f, 0.1614f, 0.1011f, 1.0000f),
skia::Vector4(0.1248f, 0.9392f, 0.4597f, 1.0000f),
},
// Jpeg
{
skia::Vector4(0.2990f, 0.3313f, 1.0000f, 1.0000f),
skia::Vector4(0.5870f, 0.1687f, 0.0813f, 1.0000f),
skia::Vector4(0.1140f, 1.0000f, 0.4187f, 1.0000f),
},
// XYZD50
{
skia::Vector4(1.0000f, 0.0000f, 0.0000f, 1.0000f),
skia::Vector4(0.0000f, 1.0000f, 0.0000f, 1.0000f),
skia::Vector4(0.0000f, 0.0000f, 1.0000f, 1.0000f),
},
};
for (size_t i = 0; i < kNumColorSpaces; ++i) {
skia::Matrix44 transfer;
color_spaces[i].GetTransferMatrix(/*bit_depth=*/8, &transfer);
skia::Matrix44 range_adjust;
color_spaces[i].GetRangeAdjustMatrix(/*bit_depth=*/8, &range_adjust);
skia::Matrix44 range_adjust_inv;
range_adjust.invert(&range_adjust_inv);
for (size_t j = 0; j < kNumTestRGBs; ++j) {
skia::Vector4 yuv = range_adjust_inv * transfer * test_rgbs[j];
EXPECT_LT(Diff(yuv, expected_yuvs[i][j]), kEpsilon);
}
}
}
TEST(ColorSpace, RangeAdjust) {
const float kEpsilon = 1.0e-3f;
const size_t kNumTestYUVs = 2;
skia::Vector4 test_yuvs[kNumTestYUVs] = {
skia::Vector4(1.f, 1.f, 1.f, 1.f),
skia::Vector4(0.f, 0.f, 0.f, 1.f),
};
const size_t kNumBitDepths = 3;
int bit_depths[kNumBitDepths] = {8, 10, 12};
const size_t kNumColorSpaces = 3;
ColorSpace color_spaces[kNumColorSpaces] = {
ColorSpace::CreateREC601(),
ColorSpace::CreateJpeg(),
ColorSpace(ColorSpace::PrimaryID::INVALID,
ColorSpace::TransferID::INVALID, ColorSpace::MatrixID::YCOCG,
ColorSpace::RangeID::LIMITED),
};
skia::Vector4 expected_yuvs[kNumColorSpaces][kNumBitDepths][kNumTestYUVs] = {
// REC601
{
// 8bpc
{
skia::Vector4(235.f / 255.f, 239.5f / 255.f, 239.5f / 255.f,
1.0000f),
skia::Vector4(16.f / 255.f, 15.5f / 255.f, 15.5f / 255.f,
1.0000f),
},
// 10bpc
{
skia::Vector4(940.f / 1023.f, 959.5f / 1023.f, 959.5f / 1023.f,
1.0000f),
skia::Vector4(64.f / 1023.f, 63.5f / 1023.f, 63.5f / 1023.f,
1.0000f),
},
// 12bpc
{
skia::Vector4(3760.f / 4095.f, 3839.5f / 4095.f, 3839.5f / 4095.f,
1.0000f),
skia::Vector4(256.f / 4095.f, 255.5f / 4095.f, 255.5f / 4095.f,
1.0000f),
},
},
// Jpeg
{
// 8bpc
{
skia::Vector4(1.0000f, 1.0000f, 1.0000f, 1.0000f),
skia::Vector4(0.0000f, 0.0000f, 0.0000f, 1.0000f),
},
// 10bpc
{
skia::Vector4(1.0000f, 1.0000f, 1.0000f, 1.0000f),
skia::Vector4(0.0000f, 0.0000f, 0.0000f, 1.0000f),
},
// 12bpc
{
skia::Vector4(1.0000f, 1.0000f, 1.0000f, 1.0000f),
skia::Vector4(0.0000f, 0.0000f, 0.0000f, 1.0000f),
},
},
// YCoCg
{
// 8bpc
{
skia::Vector4(235.f / 255.f, 235.f / 255.f, 235.f / 255.f,
1.0000f),
skia::Vector4(16.f / 255.f, 16.f / 255.f, 16.f / 255.f, 1.0000f),
},
// 10bpc
{
skia::Vector4(940.f / 1023.f, 940.f / 1023.f, 940.f / 1023.f,
1.0000f),
skia::Vector4(64.f / 1023.f, 64.f / 1023.f, 64.f / 1023.f,
1.0000f),
},
// 12bpc
{
skia::Vector4(3760.f / 4095.f, 3760.f / 4095.f, 3760.f / 4095.f,
1.0000f),
skia::Vector4(256.f / 4095.f, 256.f / 4095.f, 256.f / 4095.f,
1.0000f),
},
},
};
for (size_t i = 0; i < kNumColorSpaces; ++i) {
for (size_t j = 0; j < kNumBitDepths; ++j) {
skia::Matrix44 range_adjust;
color_spaces[i].GetRangeAdjustMatrix(bit_depths[j], &range_adjust);
skia::Matrix44 range_adjust_inv;
range_adjust.invert(&range_adjust_inv);
for (size_t k = 0; k < kNumTestYUVs; ++k) {
skia::Vector4 yuv = range_adjust_inv * test_yuvs[k];
EXPECT_LT(Diff(yuv, expected_yuvs[i][j][k]), kEpsilon);
}
}
}
}
TEST(ColorSpace, Blending) {
ColorSpace display_color_space;
// A linear transfer function being used for HDR should be blended using an
// sRGB-like transfer function.
display_color_space = ColorSpace::CreateSCRGBLinear();
EXPECT_FALSE(display_color_space.IsSuitableForBlending());
// If not used for HDR, a linear transfer function should be left unchanged.
display_color_space = ColorSpace::CreateXYZD50();
EXPECT_TRUE(display_color_space.IsSuitableForBlending());
}
TEST(ColorSpace, ConversionToAndFromSkColorSpace) {
const size_t kNumTests = 5;
skcms_Matrix3x3 primary_matrix = {{
{0.205276f, 0.625671f, 0.060867f},
{0.149185f, 0.063217f, 0.744553f},
{0.609741f, 0.311111f, 0.019470f},
}};
skcms_TransferFunction transfer_fn = {2.1f, 1.f, 0.f, 0.f, 0.f, 0.f, 0.f};
ColorSpace color_spaces[kNumTests] = {
ColorSpace(ColorSpace::PrimaryID::BT709,
ColorSpace::TransferID::IEC61966_2_1),
ColorSpace(ColorSpace::PrimaryID::ADOBE_RGB,
ColorSpace::TransferID::IEC61966_2_1),
ColorSpace(ColorSpace::PrimaryID::SMPTEST432_1,
ColorSpace::TransferID::LINEAR),
ColorSpace(ColorSpace::PrimaryID::BT2020,
ColorSpace::TransferID::IEC61966_2_1),
ColorSpace::CreateCustom(primary_matrix, transfer_fn),
};
sk_sp<SkColorSpace> sk_color_spaces[kNumTests] = {
SkColorSpace::MakeSRGB(),
SkColorSpace::MakeRGB(SkNamedTransferFn::kSRGB, SkNamedGamut::kAdobeRGB),
SkColorSpace::MakeRGB(SkNamedTransferFn::kLinear,
SkNamedGamut::kDisplayP3),
SkColorSpace::MakeRGB(SkNamedTransferFn::kSRGB, SkNamedGamut::kRec2020),
SkColorSpace::MakeRGB(transfer_fn, primary_matrix),
};
// Test that converting from ColorSpace to SkColorSpace is producing an
// equivalent representation.
for (size_t i = 0; i < kNumTests; ++i) {
EXPECT_TRUE(SkColorSpace::Equals(color_spaces[i].ToSkColorSpace().get(),
sk_color_spaces[i].get()))
<< " on iteration i = " << i;
}
// Invariant test: Test that converting a SkColorSpace to a ColorSpace is
// producing an equivalent representation; and then converting the converted
// ColorSpace back to SkColorSpace is also producing an equivalent
// representation.
for (size_t i = 0; i < kNumTests; ++i) {
const ColorSpace from_sk_color_space(*sk_color_spaces[i]);
EXPECT_EQ(color_spaces[i], from_sk_color_space);
EXPECT_TRUE(SkColorSpace::Equals(
sk_color_spaces[i].get(), from_sk_color_space.ToSkColorSpace().get()));
}
}
TEST(ColorSpace, PQToSkColorSpace) {
ColorSpace color_space;
ColorSpace roundtrip_color_space;
float roundtrip_sdr_white_level;
const float kEpsilon = 1.e-5f;
// We expect that when a white point is specified, the conversion from
// ColorSpace -> SkColorSpace -> ColorSpace be the identity. Because of
// rounding error, this will not quite be the case.
color_space = ColorSpace::CreateHDR10(50.f);
roundtrip_color_space = ColorSpace(*color_space.ToSkColorSpace());
EXPECT_TRUE(
roundtrip_color_space.GetSDRWhiteLevel(&roundtrip_sdr_white_level));
EXPECT_NEAR(50.f, roundtrip_sdr_white_level, kEpsilon);
EXPECT_EQ(ColorSpace::TransferID::SMPTEST2084,
roundtrip_color_space.GetTransferID());
// When no white level is specified, we should get an SkColorSpace that
// specifies the default white level. Of note is that in the roundtrip, the
// value of kDefaultSDRWhiteLevel gets baked in.
color_space = ColorSpace::CreateHDR10();
roundtrip_color_space = ColorSpace(*color_space.ToSkColorSpace());
EXPECT_TRUE(
roundtrip_color_space.GetSDRWhiteLevel(&roundtrip_sdr_white_level));
EXPECT_NEAR(ColorSpace::kDefaultSDRWhiteLevel, roundtrip_sdr_white_level,
kEpsilon);
}
TEST(ColorSpace, HLGToSkColorSpace) {
ColorSpace color_space;
ColorSpace roundtrip_color_space;
float roundtrip_sdr_white_level;
const float kEpsilon = 1.0e-3f;
// We expect that when a white point is specified, the conversion from
// ColorSpace -> SkColorSpace -> ColorSpace be the identity. Because of
// rounding error, this will not quite be the case.
constexpr float kSDRWhiteLevel = 50.0f;
color_space = ColorSpace::CreateHLG().GetWithSDRWhiteLevel(kSDRWhiteLevel);
roundtrip_color_space = ColorSpace(*color_space.ToSkColorSpace());
EXPECT_TRUE(
roundtrip_color_space.GetSDRWhiteLevel(&roundtrip_sdr_white_level));
EXPECT_FLOAT_EQ(kSDRWhiteLevel, roundtrip_sdr_white_level);
EXPECT_EQ(ColorSpace::TransferID::ARIB_STD_B67,
roundtrip_color_space.GetTransferID());
// When no white level is specified, we should get an SkColorSpace that
// specifies the default white level. Of note is that in the roundtrip, the
// value of kDefaultSDRWhiteLevel gets baked in.
color_space = ColorSpace::CreateHLG();
roundtrip_color_space = ColorSpace(*color_space.ToSkColorSpace());
EXPECT_TRUE(
roundtrip_color_space.GetSDRWhiteLevel(&roundtrip_sdr_white_level));
EXPECT_NEAR(ColorSpace::kDefaultSDRWhiteLevel, roundtrip_sdr_white_level,
kEpsilon);
}
TEST(ColorSpace, MixedInvalid) {
ColorSpace color_space;
color_space = color_space.GetWithMatrixAndRange(ColorSpace::MatrixID::INVALID,
ColorSpace::RangeID::INVALID);
EXPECT_TRUE(!color_space.IsValid());
color_space = color_space.GetWithMatrixAndRange(
ColorSpace::MatrixID::SMPTE170M, ColorSpace::RangeID::LIMITED);
EXPECT_TRUE(!color_space.IsValid());
}
TEST(ColorSpace, MixedSRGBWithRec601) {
const ColorSpace expected_color_space = ColorSpace(
ColorSpace::PrimaryID::BT709, ColorSpace::TransferID::IEC61966_2_1,
ColorSpace::MatrixID::SMPTE170M, ColorSpace::RangeID::LIMITED);
ColorSpace color_space = ColorSpace::CreateSRGB();
color_space = color_space.GetWithMatrixAndRange(
ColorSpace::MatrixID::SMPTE170M, ColorSpace::RangeID::LIMITED);
EXPECT_TRUE(expected_color_space.IsValid());
EXPECT_EQ(color_space, expected_color_space);
}
TEST(ColorSpace, MixedHDR10WithRec709) {
const ColorSpace expected_color_space = ColorSpace(
ColorSpace::PrimaryID::BT2020, ColorSpace::TransferID::SMPTEST2084,
ColorSpace::MatrixID::BT709, ColorSpace::RangeID::LIMITED);
ColorSpace color_space = ColorSpace::CreateHDR10();
color_space = color_space.GetWithMatrixAndRange(ColorSpace::MatrixID::BT709,
ColorSpace::RangeID::LIMITED);
EXPECT_TRUE(expected_color_space.IsValid());
EXPECT_EQ(color_space, expected_color_space);
}
TEST(ColorSpace, GetsPrimariesTransferMatrixAndRange) {
ColorSpace color_space(
ColorSpace::PrimaryID::BT709, ColorSpace::TransferID::BT709,
ColorSpace::MatrixID::BT709, ColorSpace::RangeID::LIMITED);
EXPECT_EQ(color_space.GetPrimaryID(), ColorSpace::PrimaryID::BT709);
EXPECT_EQ(color_space.GetTransferID(), ColorSpace::TransferID::BT709);
EXPECT_EQ(color_space.GetMatrixID(), ColorSpace::MatrixID::BT709);
EXPECT_EQ(color_space.GetRangeID(), ColorSpace::RangeID::LIMITED);
}
TEST(ColorSpace, PQWhiteLevel) {
constexpr float kCustomWhiteLevel = 200.f;
ColorSpace color_space = ColorSpace::CreateHDR10(kCustomWhiteLevel);
EXPECT_EQ(color_space.GetTransferID(), ColorSpace::TransferID::SMPTEST2084);
float sdr_white_level;
EXPECT_TRUE(color_space.GetSDRWhiteLevel(&sdr_white_level));
EXPECT_EQ(sdr_white_level, kCustomWhiteLevel);
color_space = ColorSpace::CreateHDR10();
EXPECT_EQ(color_space.GetTransferID(), ColorSpace::TransferID::SMPTEST2084);
EXPECT_TRUE(color_space.GetSDRWhiteLevel(&sdr_white_level));
EXPECT_EQ(sdr_white_level, ColorSpace::kDefaultSDRWhiteLevel);
color_space = color_space.GetWithSDRWhiteLevel(kCustomWhiteLevel);
EXPECT_EQ(color_space.GetTransferID(), ColorSpace::TransferID::SMPTEST2084);
EXPECT_TRUE(color_space.GetSDRWhiteLevel(&sdr_white_level));
EXPECT_EQ(sdr_white_level, kCustomWhiteLevel);
constexpr float kCustomWhiteLevel2 = kCustomWhiteLevel * 2;
color_space = color_space.GetWithSDRWhiteLevel(kCustomWhiteLevel2);
EXPECT_EQ(color_space.GetTransferID(), ColorSpace::TransferID::SMPTEST2084);
EXPECT_TRUE(color_space.GetSDRWhiteLevel(&sdr_white_level));
EXPECT_EQ(sdr_white_level, kCustomWhiteLevel2);
}
TEST(ColorSpace, LinearHDRWhiteLevel) {
constexpr float kCustomWhiteLevel = 200.f;
constexpr float kCustomSlope =
ColorSpace::kDefaultScrgbLinearSdrWhiteLevel / kCustomWhiteLevel;
ColorSpace color_space = ColorSpace::CreateSCRGBLinear(kCustomWhiteLevel);
skcms_TransferFunction fn;
EXPECT_EQ(color_space.GetTransferID(), ColorSpace::TransferID::CUSTOM_HDR);
EXPECT_TRUE(color_space.GetTransferFunction(&fn));
EXPECT_EQ(std::make_tuple(fn.g, fn.a, fn.b, fn.c, fn.d, fn.e, fn.f),
std::make_tuple(1.f, kCustomSlope, 0.f, 0.f, 0.f, 0.f, 0.f));
color_space = ColorSpace::CreateSCRGBLinear();
EXPECT_EQ(color_space.GetTransferID(), ColorSpace::TransferID::LINEAR_HDR);
EXPECT_TRUE(color_space.GetTransferFunction(&fn));
EXPECT_EQ(std::make_tuple(fn.g, fn.a, fn.b, fn.c, fn.d, fn.e, fn.f),
std::make_tuple(1.f, 1.f, 0.f, 0.f, 0.f, 0.f, 0.f));
color_space = color_space.GetWithSDRWhiteLevel(kCustomWhiteLevel);
EXPECT_EQ(color_space.GetTransferID(), ColorSpace::TransferID::CUSTOM_HDR);
EXPECT_TRUE(color_space.GetTransferFunction(&fn));
EXPECT_EQ(std::make_tuple(fn.g, fn.a, fn.b, fn.c, fn.d, fn.e, fn.f),
std::make_tuple(1.f, kCustomSlope, 0.f, 0.f, 0.f, 0.f, 0.f));
}
TEST(ColorSpace, ExpectationsMatchSRGB) {
ColorSpace::PrimaryID primary_ids[] = {
ColorSpace::PrimaryID::BT709,
ColorSpace::PrimaryID::BT470M,
ColorSpace::PrimaryID::BT470BG,
ColorSpace::PrimaryID::SMPTE170M,
ColorSpace::PrimaryID::SMPTE240M,
ColorSpace::PrimaryID::FILM,
ColorSpace::PrimaryID::BT2020,
ColorSpace::PrimaryID::SMPTEST428_1,
ColorSpace::PrimaryID::SMPTEST431_2,
ColorSpace::PrimaryID::SMPTEST432_1,
ColorSpace::PrimaryID::XYZ_D50,
ColorSpace::PrimaryID::ADOBE_RGB,
ColorSpace::PrimaryID::APPLE_GENERIC_RGB,
ColorSpace::PrimaryID::WIDE_GAMUT_COLOR_SPIN,
};
// Create a custom color space with the sRGB primary matrix.
ColorSpace srgb = ColorSpace::CreateSRGB();
skcms_Matrix3x3 to_XYZD50;
srgb.GetPrimaryMatrix(&to_XYZD50);
ColorSpace custom_srgb =
ColorSpace::CreateCustom(to_XYZD50, ColorSpace::TransferID::IEC61966_2_1);
for (auto id : primary_ids) {
ColorSpace color_space(id, ColorSpace::TransferID::IEC61966_2_1);
// The precomputed results for Contains(sRGB) should match the calculation
// performed on a custom color space with sRGB primaries.
EXPECT_EQ(color_space.Contains(srgb), color_space.Contains(custom_srgb));
}
}
} // namespace
} // namespace gfx