blob: 202faa8671d9bdd41b983487729db85fba54ead7 [file] [log] [blame]
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/gfx/color_space.h"
#include <iomanip>
#include <limits>
#include <map>
#include <sstream>
#include "base/atomic_sequence_num.h"
#include "base/lazy_instance.h"
#include "base/logging.h"
#include "base/notreached.h"
#include "base/synchronization/lock.h"
#include "skia/ext/skcolorspace_primaries.h"
#include "skia/ext/skcolorspace_trfn.h"
#include "third_party/skia/include/core/SkColorSpace.h"
#include "third_party/skia/include/core/SkData.h"
#include "third_party/skia/include/core/SkImageInfo.h"
#include "third_party/skia/include/core/SkM44.h"
#include "third_party/skia/modules/skcms/skcms.h"
#include "ui/gfx/display_color_spaces.h"
#include "ui/gfx/icc_profile.h"
#include "ui/gfx/skia_color_space_util.h"
namespace gfx {
namespace {
// Videos that are from a 10 or 12 bit source, but are stored in a 16-bit
// format (e.g, PIXEL_FORMAT_P016LE) will report having 16 bits per pixel.
// Assume they have 10 bits per pixel.
// https://crbug.com/1381100
int BitDepthWithWorkaroundApplied(int bit_depth) {
return bit_depth == 16 ? 10 : bit_depth;
}
static bool FloatsEqualWithinTolerance(const float* a,
const float* b,
int n,
float tol) {
for (int i = 0; i < n; ++i) {
if (std::abs(a[i] - b[i]) > tol) {
return false;
}
}
return true;
}
skcms_TransferFunction GetPQSkTransferFunction(float sdr_white_level) {
// Note that SkColorSpace doesn't have the notion of an unspecified SDR white
// level.
if (sdr_white_level == 0.f)
sdr_white_level = ColorSpace::kDefaultSDRWhiteLevel;
// The generic PQ transfer function produces normalized luminance values i.e.
// the range 0-1 represents 0-10000 nits for the reference display, but we
// want to map 1.0 to |sdr_white_level| nits so we need to scale accordingly.
const double w = 10000. / sdr_white_level;
// Distribute scaling factor W by scaling A and B with X ^ (1/F):
// ((A + Bx^C) / (D + Ex^C))^F * W = ((A + Bx^C) / (D + Ex^C) * W^(1/F))^F
// See https://crbug.com/1058580#c32 for discussion.
skcms_TransferFunction fn = SkNamedTransferFn::kPQ;
const double ws = pow(w, 1. / fn.f);
fn.a = ws * fn.a;
fn.b = ws * fn.b;
return fn;
}
skcms_TransferFunction GetHLGSkTransferFunction(float sdr_white_level) {
// Note that SkColorSpace doesn't have the notion of an unspecified SDR white
// level.
if (sdr_white_level == 0.f)
sdr_white_level = ColorSpace::kDefaultSDRWhiteLevel;
// The kHLG constant will evaluate to values in the range [0, 12].
skcms_TransferFunction fn = SkNamedTransferFn::kHLG;
// The value of k is equal to kHLG evaluated at 0.75 (3.77) , divided by kHLG
// evaluated at 1 (12), multiplied by 203 nits. This value is selected such
// that a signal of 0.75 will map to the same value that a PQ signal for 203
// nits will map to.
constexpr float k = 63.84549817071231f;
fn.f = k / sdr_white_level - 1;
return fn;
}
bool PrimaryIdContainsSRGB(ColorSpace::PrimaryID id) {
DCHECK(id != ColorSpace::PrimaryID::INVALID &&
id != ColorSpace::PrimaryID::CUSTOM);
switch (id) {
case ColorSpace::PrimaryID::BT709:
case ColorSpace::PrimaryID::BT2020:
case ColorSpace::PrimaryID::SMPTEST428_1:
case ColorSpace::PrimaryID::SMPTEST431_2:
case ColorSpace::PrimaryID::P3:
case ColorSpace::PrimaryID::XYZ_D50:
case ColorSpace::PrimaryID::ADOBE_RGB:
case ColorSpace::PrimaryID::WIDE_GAMUT_COLOR_SPIN:
return true;
default:
return false;
}
}
} // namespace
// static
constexpr float ColorSpace::kDefaultSDRWhiteLevel;
ColorSpace::ColorSpace(PrimaryID primaries,
TransferID transfer,
MatrixID matrix,
RangeID range,
const skcms_Matrix3x3* custom_primary_matrix,
const skcms_TransferFunction* custom_transfer_fn)
: primaries_(primaries),
transfer_(transfer),
matrix_(matrix),
range_(range) {
if (custom_primary_matrix) {
DCHECK_EQ(PrimaryID::CUSTOM, primaries_);
SetCustomPrimaries(*custom_primary_matrix);
}
if (custom_transfer_fn) {
SetCustomTransferFunction(*custom_transfer_fn);
}
}
ColorSpace::ColorSpace(const SkColorSpace& sk_color_space, bool is_hdr)
: ColorSpace(PrimaryID::INVALID,
TransferID::INVALID,
MatrixID::RGB,
RangeID::FULL) {
skcms_TransferFunction fn;
if (sk_color_space.isNumericalTransferFn(&fn)) {
transfer_ = is_hdr ? TransferID::CUSTOM_HDR : TransferID::CUSTOM;
SetCustomTransferFunction(fn);
} else if (skcms_TransferFunction_isHLGish(&fn)) {
transfer_ = TransferID::HLG;
} else if (skcms_TransferFunction_isPQish(&fn)) {
transfer_ = TransferID::PQ;
} else {
// Construct an invalid result: Unable to extract necessary parameters
return;
}
skcms_Matrix3x3 to_XYZD50;
if (!sk_color_space.toXYZD50(&to_XYZD50)) {
// Construct an invalid result: Unable to extract necessary parameters
return;
}
SetCustomPrimaries(to_XYZD50);
}
bool ColorSpace::IsValid() const {
return primaries_ != PrimaryID::INVALID && transfer_ != TransferID::INVALID &&
matrix_ != MatrixID::INVALID && range_ != RangeID::INVALID;
}
// static
ColorSpace ColorSpace::CreateExtendedSRGB10Bit() {
return ColorSpace(PrimaryID::P3, TransferID::CUSTOM_HDR, MatrixID::RGB,
RangeID::FULL, nullptr,
&SkNamedTransferFnExt::kSRGBExtended1023Over510);
}
// static
ColorSpace ColorSpace::CreatePiecewiseHDR(
PrimaryID primaries,
float sdr_joint,
float hdr_level,
const skcms_Matrix3x3* custom_primary_matrix) {
// If |sdr_joint| is 1, then this is just sRGB (and so |hdr_level| must be 1).
// An |sdr_joint| higher than 1 breaks.
DCHECK_LE(sdr_joint, 1.f);
if (sdr_joint == 1.f)
DCHECK_EQ(hdr_level, 1.f);
// An |hdr_level| of 1 has no HDR. An |hdr_level| less than 1 breaks.
DCHECK_GE(hdr_level, 1.f);
ColorSpace result(primaries, TransferID::PIECEWISE_HDR, MatrixID::RGB,
RangeID::FULL, custom_primary_matrix, nullptr);
result.transfer_params_[0] = sdr_joint;
result.transfer_params_[1] = hdr_level;
return result;
}
// static
ColorSpace ColorSpace::CreateCustom(const skcms_Matrix3x3& to_XYZD50,
const skcms_TransferFunction& fn) {
ColorSpace result(ColorSpace::PrimaryID::CUSTOM,
ColorSpace::TransferID::CUSTOM, ColorSpace::MatrixID::RGB,
ColorSpace::RangeID::FULL, &to_XYZD50, &fn);
return result;
}
// static
ColorSpace ColorSpace::CreateCustom(const skcms_Matrix3x3& to_XYZD50,
TransferID transfer) {
ColorSpace result(ColorSpace::PrimaryID::CUSTOM, transfer,
ColorSpace::MatrixID::RGB, ColorSpace::RangeID::FULL,
&to_XYZD50, nullptr);
return result;
}
void ColorSpace::SetCustomPrimaries(const skcms_Matrix3x3& to_XYZD50) {
const PrimaryID kIDsToCheck[] = {
PrimaryID::BT709,
PrimaryID::BT470M,
PrimaryID::BT470BG,
PrimaryID::SMPTE170M,
PrimaryID::SMPTE240M,
PrimaryID::FILM,
PrimaryID::BT2020,
PrimaryID::SMPTEST428_1,
PrimaryID::SMPTEST431_2,
PrimaryID::P3,
PrimaryID::XYZ_D50,
PrimaryID::ADOBE_RGB,
PrimaryID::APPLE_GENERIC_RGB,
PrimaryID::WIDE_GAMUT_COLOR_SPIN,
};
for (PrimaryID id : kIDsToCheck) {
skcms_Matrix3x3 matrix;
GetPrimaryMatrix(id, &matrix);
if (FloatsEqualWithinTolerance(&to_XYZD50.vals[0][0], &matrix.vals[0][0], 9,
0.001f)) {
primaries_ = id;
return;
}
}
memcpy(custom_primary_matrix_, &to_XYZD50, 9 * sizeof(float));
primaries_ = PrimaryID::CUSTOM;
}
void ColorSpace::SetCustomTransferFunction(const skcms_TransferFunction& fn) {
DCHECK(transfer_ == TransferID::CUSTOM ||
transfer_ == TransferID::CUSTOM_HDR);
auto check_transfer_fn = [this, &fn](TransferID id) {
skcms_TransferFunction id_fn;
GetTransferFunction(id, &id_fn);
if (!FloatsEqualWithinTolerance(&fn.g, &id_fn.g, 7, 0.001f)) {
return false;
}
transfer_ = id;
return true;
};
if (transfer_ == TransferID::CUSTOM) {
// These are all TransferIDs that will return a transfer function from
// GetTransferFunction. When multiple ids map to the same function, this
// list prioritizes the most common name (eg SRGB).
const TransferID kIDsToCheck[] = {
TransferID::SRGB, TransferID::LINEAR,
TransferID::GAMMA18, TransferID::GAMMA22,
TransferID::GAMMA24, TransferID::GAMMA28,
TransferID::SMPTE240M, TransferID::BT709_APPLE,
TransferID::SMPTEST428_1,
};
for (TransferID id : kIDsToCheck) {
if (check_transfer_fn(id))
return;
}
}
if (transfer_ == TransferID::CUSTOM_HDR) {
// This list is the same as above, but for HDR TransferIDs.
const TransferID kIDsToCheckHDR[] = {
TransferID::SRGB_HDR,
TransferID::LINEAR_HDR,
};
for (TransferID id : kIDsToCheckHDR) {
if (check_transfer_fn(id)) {
return;
}
}
}
transfer_params_[0] = fn.a;
transfer_params_[1] = fn.b;
transfer_params_[2] = fn.c;
transfer_params_[3] = fn.d;
transfer_params_[4] = fn.e;
transfer_params_[5] = fn.f;
transfer_params_[6] = fn.g;
}
// static
size_t ColorSpace::TransferParamCount(TransferID transfer) {
switch (transfer) {
case TransferID::CUSTOM:
return 7;
case TransferID::CUSTOM_HDR:
return 7;
case TransferID::PIECEWISE_HDR:
return 2;
default:
return 0;
}
}
bool ColorSpace::operator==(const ColorSpace& other) const {
if (primaries_ != other.primaries_ || transfer_ != other.transfer_ ||
matrix_ != other.matrix_ || range_ != other.range_) {
return false;
}
if (primaries_ == PrimaryID::CUSTOM) {
if (memcmp(custom_primary_matrix_, other.custom_primary_matrix_,
sizeof(custom_primary_matrix_))) {
return false;
}
}
if (size_t param_count = TransferParamCount(transfer_)) {
if (memcmp(transfer_params_, other.transfer_params_,
param_count * sizeof(float))) {
return false;
}
}
return true;
}
bool ColorSpace::IsWide() const {
// These HDR transfer functions are always wide
if (transfer_ == TransferID::SRGB_HDR ||
transfer_ == TransferID::LINEAR_HDR ||
transfer_ == TransferID::CUSTOM_HDR)
return true;
if (primaries_ == PrimaryID::BT2020 ||
primaries_ == PrimaryID::SMPTEST431_2 || primaries_ == PrimaryID::P3 ||
primaries_ == PrimaryID::ADOBE_RGB ||
primaries_ == PrimaryID::WIDE_GAMUT_COLOR_SPIN ||
// TODO(cblume/ccameron): Compute if the custom primaries actually are
// wide. For now, assume so.
primaries_ == PrimaryID::CUSTOM)
return true;
return false;
}
bool ColorSpace::IsHDR() const {
return transfer_ == TransferID::PQ || transfer_ == TransferID::HLG ||
transfer_ == TransferID::LINEAR_HDR ||
transfer_ == TransferID::SRGB_HDR ||
transfer_ == TransferID::CUSTOM_HDR ||
transfer_ == TransferID::PIECEWISE_HDR ||
transfer_ == TransferID::SCRGB_LINEAR_80_NITS;
}
bool ColorSpace::IsToneMappedByDefault() const {
switch (transfer_) {
case TransferID::PQ:
case TransferID::HLG:
return true;
default:
return false;
}
}
bool ColorSpace::IsAffectedBySDRWhiteLevel() const {
switch (transfer_) {
case TransferID::PQ:
case TransferID::HLG:
case TransferID::SCRGB_LINEAR_80_NITS:
return true;
default:
return false;
}
}
bool ColorSpace::FullRangeEncodedValues() const {
return transfer_ == TransferID::LINEAR_HDR ||
transfer_ == TransferID::SRGB_HDR ||
transfer_ == TransferID::CUSTOM_HDR ||
transfer_ == TransferID::PIECEWISE_HDR ||
transfer_ == TransferID::SCRGB_LINEAR_80_NITS ||
transfer_ == TransferID::BT1361_ECG ||
transfer_ == TransferID::IEC61966_2_4;
}
bool ColorSpace::operator!=(const ColorSpace& other) const {
return !(*this == other);
}
bool ColorSpace::operator<(const ColorSpace& other) const {
if (primaries_ < other.primaries_)
return true;
if (primaries_ > other.primaries_)
return false;
if (transfer_ < other.transfer_)
return true;
if (transfer_ > other.transfer_)
return false;
if (matrix_ < other.matrix_)
return true;
if (matrix_ > other.matrix_)
return false;
if (range_ < other.range_)
return true;
if (range_ > other.range_)
return false;
if (primaries_ == PrimaryID::CUSTOM) {
int primary_result =
memcmp(custom_primary_matrix_, other.custom_primary_matrix_,
sizeof(custom_primary_matrix_));
if (primary_result < 0)
return true;
if (primary_result > 0)
return false;
}
if (size_t param_count = TransferParamCount(transfer_)) {
int transfer_result = memcmp(transfer_params_, other.transfer_params_,
param_count * sizeof(float));
if (transfer_result < 0)
return true;
if (transfer_result > 0)
return false;
}
return false;
}
size_t ColorSpace::GetHash() const {
size_t result = (static_cast<size_t>(primaries_) << 0) |
(static_cast<size_t>(transfer_) << 8) |
(static_cast<size_t>(matrix_) << 16) |
(static_cast<size_t>(range_) << 24);
if (primaries_ == PrimaryID::CUSTOM) {
const uint32_t* params =
reinterpret_cast<const uint32_t*>(custom_primary_matrix_);
result ^= params[0];
result ^= params[4];
result ^= params[8];
}
{
// Note that |transfer_params_| must be zero when they are unused.
const uint32_t* params =
reinterpret_cast<const uint32_t*>(transfer_params_);
result ^= params[3];
result ^= params[6];
}
return result;
}
#define PRINT_ENUM_CASE(TYPE, NAME) \
case TYPE::NAME: \
ss << #NAME; \
break;
std::string ColorSpace::ToString() const {
std::stringstream ss;
ss << std::fixed << std::setprecision(4);
if (primaries_ != PrimaryID::CUSTOM)
ss << "{primaries:";
switch (primaries_) {
PRINT_ENUM_CASE(PrimaryID, INVALID)
PRINT_ENUM_CASE(PrimaryID, BT709)
PRINT_ENUM_CASE(PrimaryID, BT470M)
PRINT_ENUM_CASE(PrimaryID, BT470BG)
PRINT_ENUM_CASE(PrimaryID, SMPTE170M)
PRINT_ENUM_CASE(PrimaryID, SMPTE240M)
PRINT_ENUM_CASE(PrimaryID, FILM)
PRINT_ENUM_CASE(PrimaryID, BT2020)
PRINT_ENUM_CASE(PrimaryID, SMPTEST428_1)
PRINT_ENUM_CASE(PrimaryID, SMPTEST431_2)
PRINT_ENUM_CASE(PrimaryID, P3)
PRINT_ENUM_CASE(PrimaryID, XYZ_D50)
PRINT_ENUM_CASE(PrimaryID, ADOBE_RGB)
PRINT_ENUM_CASE(PrimaryID, APPLE_GENERIC_RGB)
PRINT_ENUM_CASE(PrimaryID, WIDE_GAMUT_COLOR_SPIN)
case PrimaryID::CUSTOM:
ss << skia::SkColorSpacePrimariesToString(GetPrimaries());
break;
}
ss << ", transfer:";
switch (transfer_) {
PRINT_ENUM_CASE(TransferID, INVALID)
PRINT_ENUM_CASE(TransferID, BT709)
PRINT_ENUM_CASE(TransferID, BT709_APPLE)
PRINT_ENUM_CASE(TransferID, GAMMA18)
PRINT_ENUM_CASE(TransferID, GAMMA22)
PRINT_ENUM_CASE(TransferID, GAMMA24)
PRINT_ENUM_CASE(TransferID, GAMMA28)
PRINT_ENUM_CASE(TransferID, SMPTE170M)
PRINT_ENUM_CASE(TransferID, SMPTE240M)
PRINT_ENUM_CASE(TransferID, LINEAR)
PRINT_ENUM_CASE(TransferID, LOG)
PRINT_ENUM_CASE(TransferID, LOG_SQRT)
PRINT_ENUM_CASE(TransferID, IEC61966_2_4)
PRINT_ENUM_CASE(TransferID, BT1361_ECG)
PRINT_ENUM_CASE(TransferID, SRGB)
PRINT_ENUM_CASE(TransferID, BT2020_10)
PRINT_ENUM_CASE(TransferID, BT2020_12)
PRINT_ENUM_CASE(TransferID, SMPTEST428_1)
PRINT_ENUM_CASE(TransferID, SRGB_HDR)
PRINT_ENUM_CASE(TransferID, LINEAR_HDR)
case TransferID::HLG:
ss << "HLG (SDR white point ";
if (transfer_params_[0] == 0.f)
ss << "default " << kDefaultSDRWhiteLevel;
else
ss << transfer_params_[0];
ss << " nits)";
break;
case TransferID::PQ:
ss << "PQ (SDR white point ";
if (transfer_params_[0] == 0.f)
ss << "default " << kDefaultSDRWhiteLevel;
else
ss << transfer_params_[0];
ss << " nits)";
break;
case TransferID::CUSTOM: {
skcms_TransferFunction fn;
GetTransferFunction(&fn);
ss << fn.c << "*x + " << fn.f << " if x < " << fn.d << " else (" << fn.a
<< "*x + " << fn.b << ")**" << fn.g << " + " << fn.e;
break;
}
case TransferID::CUSTOM_HDR: {
skcms_TransferFunction fn;
GetTransferFunction(&fn);
if (fn.g == 1.0f && fn.a > 0.0f && fn.b == 0.0f && fn.c == 0.0f &&
fn.d == 0.0f && fn.e == 0.0f && fn.f == 0.0f) {
ss << "LINEAR_HDR (slope " << fn.a << ")";
break;
}
ss << fn.c << "*x + " << fn.f << " if |x| < " << fn.d << " else sign(x)*("
<< fn.a << "*|x| + " << fn.b << ")**" << fn.g << " + " << fn.e;
break;
}
case TransferID::PIECEWISE_HDR: {
skcms_TransferFunction fn;
GetTransferFunction(&fn);
ss << "sRGB to 1 at " << transfer_params_[0] << ", linear to "
<< transfer_params_[1] << " at 1";
break;
}
case TransferID::SCRGB_LINEAR_80_NITS:
ss << "scRGB linear (80 nit white)";
break;
}
ss << ", matrix:";
switch (matrix_) {
PRINT_ENUM_CASE(MatrixID, INVALID)
PRINT_ENUM_CASE(MatrixID, RGB)
PRINT_ENUM_CASE(MatrixID, BT709)
PRINT_ENUM_CASE(MatrixID, FCC)
PRINT_ENUM_CASE(MatrixID, BT470BG)
PRINT_ENUM_CASE(MatrixID, SMPTE170M)
PRINT_ENUM_CASE(MatrixID, SMPTE240M)
PRINT_ENUM_CASE(MatrixID, YCOCG)
PRINT_ENUM_CASE(MatrixID, BT2020_NCL)
PRINT_ENUM_CASE(MatrixID, BT2020_CL)
PRINT_ENUM_CASE(MatrixID, YDZDX)
PRINT_ENUM_CASE(MatrixID, GBR)
}
ss << ", range:";
switch (range_) {
PRINT_ENUM_CASE(RangeID, INVALID)
PRINT_ENUM_CASE(RangeID, LIMITED)
PRINT_ENUM_CASE(RangeID, FULL)
PRINT_ENUM_CASE(RangeID, DERIVED)
}
ss << "}";
return ss.str();
}
#undef PRINT_ENUM_CASE
ColorSpace ColorSpace::GetAsFullRangeRGB() const {
ColorSpace result(*this);
if (!IsValid())
return result;
result.matrix_ = MatrixID::RGB;
result.range_ = RangeID::FULL;
return result;
}
ContentColorUsage ColorSpace::GetContentColorUsage() const {
if (IsHDR())
return ContentColorUsage::kHDR;
if (IsWide())
return ContentColorUsage::kWideColorGamut;
return ContentColorUsage::kSRGB;
}
ColorSpace ColorSpace::GetAsRGB() const {
ColorSpace result(*this);
if (IsValid())
result.matrix_ = MatrixID::RGB;
return result;
}
ColorSpace ColorSpace::GetScaledColorSpace(float factor) const {
ColorSpace result(*this);
skcms_Matrix3x3 to_XYZD50;
GetPrimaryMatrix(&to_XYZD50);
for (int row = 0; row < 3; ++row) {
for (int col = 0; col < 3; ++col) {
to_XYZD50.vals[row][col] *= factor;
}
}
result.SetCustomPrimaries(to_XYZD50);
return result;
}
bool ColorSpace::IsSuitableForBlending() const {
switch (transfer_) {
case TransferID::PQ:
// PQ is not an acceptable space to do blending in -- blending 0 and 1
// evenly will get a result of sRGB 0.259 (instead of 0.5).
return false;
case TransferID::HLG:
case TransferID::LINEAR_HDR:
case TransferID::SCRGB_LINEAR_80_NITS:
// If the color space is nearly-linear, then it is not suitable for
// blending -- blending 0 and 1 evenly will get a result of sRGB 0.735
// (instead of 0.5).
return false;
case TransferID::CUSTOM_HDR: {
// A gamma close enough to linear is treated as linear.
skcms_TransferFunction fn;
if (GetTransferFunction(&fn)) {
constexpr float kMinGamma = 1.25;
if (fn.g < kMinGamma)
return false;
}
break;
}
default:
break;
}
return true;
}
ColorSpace ColorSpace::GetWithMatrixAndRange(MatrixID matrix,
RangeID range) const {
ColorSpace result(*this);
if (!IsValid())
return result;
result.matrix_ = matrix;
result.range_ = range;
return result;
}
sk_sp<SkColorSpace> ColorSpace::ToSkColorSpace(
absl::optional<float> sdr_white_level) const {
// Handle only valid, full-range RGB spaces.
if (!IsValid() || matrix_ != MatrixID::RGB || range_ != RangeID::FULL)
return nullptr;
// Use the named SRGB and linear-SRGB instead of the generic constructors.
if (primaries_ == PrimaryID::BT709) {
if (transfer_ == TransferID::SRGB)
return SkColorSpace::MakeSRGB();
if (transfer_ == TransferID::LINEAR || transfer_ == TransferID::LINEAR_HDR)
return SkColorSpace::MakeSRGBLinear();
}
skcms_TransferFunction transfer_fn = SkNamedTransferFnExt::kSRGB;
switch (transfer_) {
case TransferID::SRGB:
break;
case TransferID::LINEAR:
case TransferID::LINEAR_HDR:
transfer_fn = SkNamedTransferFn::kLinear;
break;
case TransferID::HLG:
transfer_fn = GetHLGSkTransferFunction(
sdr_white_level.value_or(kDefaultSDRWhiteLevel));
break;
case TransferID::PQ:
transfer_fn = GetPQSkTransferFunction(
sdr_white_level.value_or(kDefaultSDRWhiteLevel));
break;
default:
if (!GetTransferFunction(&transfer_fn, sdr_white_level)) {
DLOG(ERROR) << "Failed to get transfer function for SkColorSpace";
return nullptr;
}
break;
}
skcms_Matrix3x3 gamut = SkNamedGamut::kSRGB;
switch (primaries_) {
case PrimaryID::BT709:
break;
case PrimaryID::ADOBE_RGB:
gamut = SkNamedGamut::kAdobeRGB;
break;
case PrimaryID::P3:
gamut = SkNamedGamut::kDisplayP3;
break;
case PrimaryID::BT2020:
gamut = SkNamedGamut::kRec2020;
break;
default:
GetPrimaryMatrix(&gamut);
break;
}
sk_sp<SkColorSpace> sk_color_space =
SkColorSpace::MakeRGB(transfer_fn, gamut);
if (!sk_color_space)
DLOG(ERROR) << "SkColorSpace::MakeRGB failed.";
return sk_color_space;
}
const struct _GLcolorSpace* ColorSpace::AsGLColorSpace() const {
return reinterpret_cast<const struct _GLcolorSpace*>(this);
}
ColorSpace::PrimaryID ColorSpace::GetPrimaryID() const {
return primaries_;
}
ColorSpace::TransferID ColorSpace::GetTransferID() const {
return transfer_;
}
ColorSpace::MatrixID ColorSpace::GetMatrixID() const {
return matrix_;
}
ColorSpace::RangeID ColorSpace::GetRangeID() const {
return range_;
}
bool ColorSpace::HasExtendedSkTransferFn() const {
return matrix_ == MatrixID::RGB;
}
bool ColorSpace::IsTransferFunctionEqualTo(
const skcms_TransferFunction& fn) const {
if (transfer_ == TransferID::PQ)
return skcms_TransferFunction_isPQish(&fn);
if (transfer_ == TransferID::HLG)
return skcms_TransferFunction_isHLGish(&fn);
if (!skcms_TransferFunction_isSRGBish(&fn))
return false;
skcms_TransferFunction transfer_fn;
GetTransferFunction(&transfer_fn);
return fn.a == transfer_fn.a && fn.b == transfer_fn.b &&
fn.c == transfer_fn.c && fn.d == transfer_fn.d &&
fn.e == transfer_fn.e && fn.f == transfer_fn.f &&
fn.g == transfer_fn.g;
}
bool ColorSpace::Contains(const ColorSpace& other) const {
if (primaries_ == PrimaryID::INVALID ||
other.primaries_ == PrimaryID::INVALID)
return false;
// Contains() is commonly used to check if a color space contains sRGB. The
// computation can be bypassed for known primary IDs.
if (primaries_ != PrimaryID::CUSTOM && other.primaries_ == PrimaryID::BT709)
return PrimaryIdContainsSRGB(primaries_);
// |matrix| is the primary transform matrix from |other| to this color space.
skcms_Matrix3x3 other_to_xyz;
skcms_Matrix3x3 this_to_xyz;
skcms_Matrix3x3 xyz_to_this;
other.GetPrimaryMatrix(&other_to_xyz);
GetPrimaryMatrix(&this_to_xyz);
skcms_Matrix3x3_invert(&this_to_xyz, &xyz_to_this);
skcms_Matrix3x3 matrix = skcms_Matrix3x3_concat(&xyz_to_this, &other_to_xyz);
// Return true iff each primary is in the range [0, 1] after transforming.
// Transforming a primary vector by |matrix| always results in a column of
// |matrix|. So the multiplication can be skipped, and we can just check if
// each value in the matrix is in the range [0, 1].
constexpr float epsilon = 0.001f;
for (int r = 0; r < 3; r++) {
for (int c = 0; c < 3; c++) {
if (matrix.vals[r][c] < -epsilon || matrix.vals[r][c] > 1 + epsilon)
return false;
}
}
return true;
}
// static
SkColorSpacePrimaries ColorSpace::GetColorSpacePrimaries(
PrimaryID primary_id,
const skcms_Matrix3x3* custom_primary_matrix = nullptr) {
SkColorSpacePrimaries primaries = SkNamedPrimariesExt::kInvalid;
if (custom_primary_matrix && primary_id == PrimaryID::CUSTOM)
return skia::GetD65PrimariesFromToXYZD50Matrix(*custom_primary_matrix);
switch (primary_id) {
case ColorSpace::PrimaryID::CUSTOM:
case ColorSpace::PrimaryID::INVALID:
break;
case ColorSpace::PrimaryID::BT709:
// BT709 is our default case. Put it after the switch just
// in case we somehow get an id which is not listed in the switch.
// (We don't want to use "default", because we want the compiler
// to tell us if we forgot some enum values.)
return SkNamedPrimariesExt::kRec709;
case ColorSpace::PrimaryID::BT470M:
return SkNamedPrimariesExt::kRec470SystemM;
case ColorSpace::PrimaryID::BT470BG:
return SkNamedPrimariesExt::kRec470SystemBG;
case ColorSpace::PrimaryID::SMPTE170M:
return SkNamedPrimariesExt::kRec601;
case ColorSpace::PrimaryID::SMPTE240M:
return SkNamedPrimariesExt::kSMPTE_ST_240;
case ColorSpace::PrimaryID::APPLE_GENERIC_RGB:
return SkNamedPrimariesExt::kAppleGenericRGB;
case ColorSpace::PrimaryID::WIDE_GAMUT_COLOR_SPIN:
return SkNamedPrimariesExt::kWideGamutColorSpin;
case ColorSpace::PrimaryID::FILM:
return SkNamedPrimariesExt::kGenericFilm;
case ColorSpace::PrimaryID::BT2020:
return SkNamedPrimariesExt::kRec2020;
case ColorSpace::PrimaryID::SMPTEST428_1:
return SkNamedPrimariesExt::kSMPTE_ST_428_1;
case ColorSpace::PrimaryID::SMPTEST431_2:
return SkNamedPrimariesExt::kSMPTE_RP_431_2;
case ColorSpace::PrimaryID::P3:
return SkNamedPrimariesExt::kP3;
case ColorSpace::PrimaryID::XYZ_D50:
return SkNamedPrimariesExt::kXYZD50;
case ColorSpace::PrimaryID::ADOBE_RGB:
return SkNamedPrimariesExt::kA98RGB;
}
return primaries;
}
SkColorSpacePrimaries ColorSpace::GetPrimaries() const {
skcms_Matrix3x3 matrix;
memcpy(&matrix, custom_primary_matrix_, 9 * sizeof(float));
return GetColorSpacePrimaries(primaries_, &matrix);
}
// static
void ColorSpace::GetPrimaryMatrix(PrimaryID primary_id,
skcms_Matrix3x3* to_XYZD50) {
SkColorSpacePrimaries primaries = GetColorSpacePrimaries(primary_id);
if (primary_id == PrimaryID::CUSTOM || primary_id == PrimaryID::INVALID) {
*to_XYZD50 = SkNamedGamut::kXYZ; // Identity
return;
}
primaries.toXYZD50(to_XYZD50);
}
void ColorSpace::GetPrimaryMatrix(skcms_Matrix3x3* to_XYZD50) const {
if (primaries_ == PrimaryID::CUSTOM) {
memcpy(to_XYZD50, custom_primary_matrix_, 9 * sizeof(float));
} else {
GetPrimaryMatrix(primaries_, to_XYZD50);
}
}
SkM44 ColorSpace::GetPrimaryMatrix() const {
skcms_Matrix3x3 toXYZ_3x3;
GetPrimaryMatrix(&toXYZ_3x3);
return SkM44FromRowMajor3x3(&toXYZ_3x3.vals[0][0]);
}
// static
bool ColorSpace::GetTransferFunction(TransferID transfer,
skcms_TransferFunction* fn) {
// Default to F(x) = pow(x, 1)
fn->a = 1;
fn->b = 0;
fn->c = 0;
fn->d = 0;
fn->e = 0;
fn->f = 0;
fn->g = 1;
switch (transfer) {
case ColorSpace::TransferID::LINEAR:
case ColorSpace::TransferID::LINEAR_HDR:
*fn = SkNamedTransferFn::kLinear;
return true;
case ColorSpace::TransferID::GAMMA18:
fn->g = 1.801f;
return true;
case ColorSpace::TransferID::GAMMA22:
*fn = SkNamedTransferFnExt::kRec470SystemM;
return true;
case ColorSpace::TransferID::GAMMA24:
fn->g = 2.4f;
return true;
case ColorSpace::TransferID::GAMMA28:
*fn = SkNamedTransferFnExt::kRec470SystemBG;
return true;
case ColorSpace::TransferID::SMPTE240M:
*fn = SkNamedTransferFnExt::kSMPTE_ST_240;
return true;
case ColorSpace::TransferID::BT709:
case ColorSpace::TransferID::SMPTE170M:
case ColorSpace::TransferID::BT2020_10:
case ColorSpace::TransferID::BT2020_12:
// With respect to rendering BT709
// * SMPTE 1886 suggests that we should be using gamma 2.4.
// * Most displays actually use a gamma of 2.2, and most media playing
// software uses the sRGB transfer function.
// * User studies shows that users don't really care.
// * Apple's CoreVideo uses gamma=1.961.
// Bearing all of that in mind, use the same transfer function as sRGB,
// which will allow more optimization, and will more closely match other
// media players.
case ColorSpace::TransferID::SRGB:
case ColorSpace::TransferID::SRGB_HDR:
*fn = SkNamedTransferFnExt::kSRGB;
return true;
case ColorSpace::TransferID::BT709_APPLE:
*fn = SkNamedTransferFnExt::kRec709Apple;
return true;
case ColorSpace::TransferID::SMPTEST428_1:
*fn = SkNamedTransferFnExt::kSMPTE_ST_428_1;
return true;
case ColorSpace::TransferID::IEC61966_2_4:
// This could potentially be represented the same as SRGB, but it handles
// negative values differently.
break;
case ColorSpace::TransferID::HLG:
case ColorSpace::TransferID::BT1361_ECG:
case ColorSpace::TransferID::LOG:
case ColorSpace::TransferID::LOG_SQRT:
case ColorSpace::TransferID::PQ:
case ColorSpace::TransferID::CUSTOM:
case ColorSpace::TransferID::CUSTOM_HDR:
case ColorSpace::TransferID::PIECEWISE_HDR:
case ColorSpace::TransferID::SCRGB_LINEAR_80_NITS:
case ColorSpace::TransferID::INVALID:
break;
}
return false;
}
bool ColorSpace::GetTransferFunction(
skcms_TransferFunction* fn,
absl::optional<float> sdr_white_level) const {
switch (transfer_) {
case TransferID::CUSTOM:
case TransferID::CUSTOM_HDR:
fn->a = transfer_params_[0];
fn->b = transfer_params_[1];
fn->c = transfer_params_[2];
fn->d = transfer_params_[3];
fn->e = transfer_params_[4];
fn->f = transfer_params_[5];
fn->g = transfer_params_[6];
return true;
case TransferID::SCRGB_LINEAR_80_NITS:
if (sdr_white_level) {
fn->a = 80.f / *sdr_white_level;
fn->b = 0;
fn->c = 0;
fn->d = 0;
fn->e = 0;
fn->f = 0;
fn->g = 1;
return true;
} else {
// Using SCRGB_LINEAR_80_NITS without specifying an SDR white level is
// guaranteed to produce incorrect results.
return false;
}
default:
return GetTransferFunction(transfer_, fn);
}
}
bool ColorSpace::GetInverseTransferFunction(
skcms_TransferFunction* fn,
absl::optional<float> sdr_white_level) const {
if (!GetTransferFunction(fn, sdr_white_level))
return false;
*fn = SkTransferFnInverse(*fn);
return true;
}
bool ColorSpace::GetPiecewiseHDRParams(float* sdr_joint,
float* hdr_level) const {
if (transfer_ != TransferID::PIECEWISE_HDR)
return false;
*sdr_joint = transfer_params_[0];
*hdr_level = transfer_params_[1];
return true;
}
SkM44 ColorSpace::GetTransferMatrix(int bit_depth) const {
bit_depth = BitDepthWithWorkaroundApplied(bit_depth);
DCHECK_GE(bit_depth, 8);
// If chroma samples are real numbers in the range of −0.5 to 0.5, an offset
// of 0.5 is added to get real numbers in the range of 0 to 1. When
// represented as an unsigned |bit_depth|-bit integer, this 0.5 offset is
// approximated by 1 << (bit_depth - 1). chroma_0_5 is this approximate value
// converted to a real number in the range of 0 to 1.
//
// TODO(wtc): For now chroma_0_5 is only used for YCgCo. It should also be
// used for YUV.
const float chroma_0_5 =
static_cast<float>(1 << (bit_depth - 1)) / ((1 << bit_depth) - 1);
float Kr = 0;
float Kb = 0;
switch (matrix_) {
case ColorSpace::MatrixID::RGB:
case ColorSpace::MatrixID::INVALID:
return SkM44();
case ColorSpace::MatrixID::BT709:
Kr = 0.2126f;
Kb = 0.0722f;
break;
case ColorSpace::MatrixID::FCC:
Kr = 0.30f;
Kb = 0.11f;
break;
case ColorSpace::MatrixID::BT470BG:
case ColorSpace::MatrixID::SMPTE170M:
Kr = 0.299f;
Kb = 0.114f;
break;
case ColorSpace::MatrixID::SMPTE240M:
Kr = 0.212f;
Kb = 0.087f;
break;
case ColorSpace::MatrixID::YCOCG: {
float data[16] = {0.25f, 0.5f, 0.25f, 0.0f, // Y
-0.25f, 0.5f, -0.25f, chroma_0_5, // Cg
0.5f, 0.0f, -0.5f, chroma_0_5, // Co
0.0f, 0.0f, 0.0f, 1.0f};
return SkM44::RowMajor(data);
}
// BT2020_CL is a special case.
// Basically we return a matrix that transforms RYB values
// to YUV values. (Note that the green component have been replaced
// with the luminance.)
case ColorSpace::MatrixID::BT2020_CL: {
Kr = 0.2627f;
Kb = 0.0593f;
float data[16] = {1.0f, 0.0f, 0.0f, 0.0f, // R
Kr, 1.0f - Kr - Kb, Kb, 0.0f, // Y
0.0f, 0.0f, 1.0f, 0.0f, // B
0.0f, 0.0f, 0.0f, 1.0f};
return SkM44::RowMajor(data);
}
case ColorSpace::MatrixID::BT2020_NCL:
Kr = 0.2627f;
Kb = 0.0593f;
break;
case ColorSpace::MatrixID::YDZDX: {
// clang-format off
float data[16] = {
0.0f, 1.0f, 0.0f, 0.0f, // Y
0.0f, -0.5f, 0.986566f / 2.0f, 0.5f, // DX or DZ
0.5f, -0.991902f / 2.0f, 0.0f, 0.5f, // DZ or DX
0.0f, 0.0f, 0.0f, 1.0f,
};
// clang-format on
return SkM44::RowMajor(data);
}
case ColorSpace::MatrixID::GBR: {
float data[16] = {0.0f, 1.0f, 0.0f, 0.0f, // G
0.0f, 0.0f, 1.0f, 0.0f, // B
1.0f, 0.0f, 0.0f, 0.0f, // R
0.0f, 0.0f, 0.0f, 1.0f};
return SkM44::RowMajor(data);
}
}
float Kg = 1.0f - Kr - Kb;
float u_m = 0.5f / (1.0f - Kb);
float v_m = 0.5f / (1.0f - Kr);
// clang-format off
float data[16] = {
Kr, Kg, Kb, 0.0f, // Y
u_m * -Kr, u_m * -Kg, u_m * (1.0f - Kb), 0.5f, // U
v_m * (1.0f - Kr), v_m * -Kg, v_m * -Kb, 0.5f, // V
0.0f, 0.0f, 0.0f, 1.0f,
};
// clang-format on
return SkM44::RowMajor(data);
}
SkM44 ColorSpace::GetRangeAdjustMatrix(int bit_depth) const {
bit_depth = BitDepthWithWorkaroundApplied(bit_depth);
DCHECK_GE(bit_depth, 8);
switch (range_) {
case RangeID::FULL:
case RangeID::INVALID:
return SkM44();
case RangeID::DERIVED:
case RangeID::LIMITED:
break;
}
// See ITU-T H.273 (2016), Section 8.3. The following is derived from
// Equations 20-31.
const int shift = bit_depth - 8;
const float a_y = 219 << shift;
const float c = (1 << bit_depth) - 1;
const float scale_y = c / a_y;
switch (matrix_) {
case MatrixID::RGB:
case MatrixID::GBR:
case MatrixID::INVALID:
case MatrixID::YCOCG:
return SkM44::Scale(scale_y, scale_y, scale_y)
.postTranslate(-16.0f / 219.0f, -16.0f / 219.0f, -16.0f / 219.0f);
case MatrixID::BT709:
case MatrixID::FCC:
case MatrixID::BT470BG:
case MatrixID::SMPTE170M:
case MatrixID::SMPTE240M:
case MatrixID::BT2020_NCL:
case MatrixID::BT2020_CL:
case MatrixID::YDZDX: {
const float a_uv = 224 << shift;
const float scale_uv = c / a_uv;
const float translate_uv = (a_uv - c) / (2.0f * a_uv);
return SkM44::Scale(scale_y, scale_uv, scale_uv)
.postTranslate(-16.0f / 219.0f, translate_uv, translate_uv);
}
}
NOTREACHED();
return SkM44();
}
bool ColorSpace::ToSkYUVColorSpace(int bit_depth, SkYUVColorSpace* out) const {
bit_depth = BitDepthWithWorkaroundApplied(bit_depth);
switch (matrix_) {
case MatrixID::BT709:
*out = range_ == RangeID::FULL ? kRec709_Full_SkYUVColorSpace
: kRec709_Limited_SkYUVColorSpace;
return true;
case MatrixID::BT470BG:
case MatrixID::SMPTE170M:
*out = range_ == RangeID::FULL ? kJPEG_SkYUVColorSpace
: kRec601_Limited_SkYUVColorSpace;
return true;
case MatrixID::BT2020_NCL:
if (bit_depth == 8) {
*out = range_ == RangeID::FULL ? kBT2020_8bit_Full_SkYUVColorSpace
: kBT2020_8bit_Limited_SkYUVColorSpace;
return true;
}
if (bit_depth == 10) {
*out = range_ == RangeID::FULL ? kBT2020_10bit_Full_SkYUVColorSpace
: kBT2020_10bit_Limited_SkYUVColorSpace;
return true;
}
if (bit_depth == 12) {
*out = range_ == RangeID::FULL ? kBT2020_12bit_Full_SkYUVColorSpace
: kBT2020_12bit_Limited_SkYUVColorSpace;
return true;
}
return false;
default:
break;
}
return false;
}
std::ostream& operator<<(std::ostream& out, const ColorSpace& color_space) {
return out << color_space.ToString();
}
} // namespace gfx