blob: 7cf67c3fdd4a6abcdbd2976e508426677101f4a7 [file] [log] [blame]
// Copyright (c) 2016 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 "ui/gfx/color_transform.h"
#include <algorithm>
#include <cmath>
#include <list>
#include <memory>
#include <sstream>
#include "base/logging.h"
#include "base/strings/stringprintf.h"
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/third_party/skcms/skcms.h"
#include "ui/gfx/color_space.h"
#include "ui/gfx/icc_profile.h"
#include "ui/gfx/skia_color_space_util.h"
#include "ui/gfx/transform.h"
using std::abs;
using std::copysign;
using std::exp;
using std::log;
using std::max;
using std::min;
using std::pow;
using std::sqrt;
using std::endl;
namespace gfx {
namespace {
void InitStringStream(std::stringstream* ss) {
ss->imbue(std::locale::classic());
ss->precision(8);
*ss << std::scientific;
}
std::string Str(float f) {
std::stringstream ss;
InitStringStream(&ss);
ss << f;
return ss.str();
}
Transform Invert(const Transform& t) {
Transform ret = t;
if (!t.GetInverse(&ret)) {
LOG(ERROR) << "Inverse should always be possible.";
}
return ret;
}
float FromLinear(ColorSpace::TransferID id, float v) {
switch (id) {
case ColorSpace::TransferID::SMPTEST2084_NON_HDR:
// Should already be handled.
break;
case ColorSpace::TransferID::LOG:
if (v < 0.01f)
return 0.0f;
return 1.0f + log(v) / log(10.0f) / 2.0f;
case ColorSpace::TransferID::LOG_SQRT:
if (v < sqrt(10.0f) / 1000.0f)
return 0.0f;
return 1.0f + log(v) / log(10.0f) / 2.5f;
case ColorSpace::TransferID::IEC61966_2_4: {
float a = 1.099296826809442f;
float b = 0.018053968510807f;
if (v < -b)
return -a * pow(-v, 0.45f) + (a - 1.0f);
else if (v <= b)
return 4.5f * v;
return a * pow(v, 0.45f) - (a - 1.0f);
}
case ColorSpace::TransferID::BT1361_ECG: {
float a = 1.099f;
float b = 0.018f;
float l = 0.0045f;
if (v < -l)
return -(a * pow(-4.0f * v, 0.45f) + (a - 1.0f)) / 4.0f;
else if (v <= b)
return 4.5f * v;
else
return a * pow(v, 0.45f) - (a - 1.0f);
}
case ColorSpace::TransferID::SMPTEST2084: {
// Go from scRGB levels to 0-1.
v *= 80.0f / 10000.0f;
v = max(0.0f, v);
float m1 = (2610.0f / 4096.0f) / 4.0f;
float m2 = (2523.0f / 4096.0f) * 128.0f;
float c1 = 3424.0f / 4096.0f;
float c2 = (2413.0f / 4096.0f) * 32.0f;
float c3 = (2392.0f / 4096.0f) * 32.0f;
float p = powf(v, m1);
return powf((c1 + c2 * p) / (1.0f + c3 * p), m2);
}
// Spec: http://www.arib.or.jp/english/html/overview/doc/2-STD-B67v1_0.pdf
case ColorSpace::TransferID::ARIB_STD_B67: {
const float a = 0.17883277f;
const float b = 0.28466892f;
const float c = 0.55991073f;
v = max(0.0f, v);
if (v <= 1)
return 0.5f * sqrt(v);
return a * log(v - b) + c;
}
default:
// Handled by skcms_TransferFunction.
break;
}
NOTREACHED();
return 0;
}
float ToLinear(ColorSpace::TransferID id, float v) {
switch (id) {
case ColorSpace::TransferID::LOG:
if (v < 0.0f)
return 0.0f;
return pow(10.0f, (v - 1.0f) * 2.0f);
case ColorSpace::TransferID::LOG_SQRT:
if (v < 0.0f)
return 0.0f;
return pow(10.0f, (v - 1.0f) * 2.5f);
case ColorSpace::TransferID::IEC61966_2_4: {
float a = 1.099296826809442f;
// Equal to FromLinear(ColorSpace::TransferID::IEC61966_2_4, -a).
float from_linear_neg_a = -1.047844f;
// Equal to FromLinear(ColorSpace::TransferID::IEC61966_2_4, b).
float from_linear_b = 0.081243f;
if (v < from_linear_neg_a)
return -pow((a - 1.0f - v) / a, 1.0f / 0.45f);
else if (v <= from_linear_b)
return v / 4.5f;
return pow((v + a - 1.0f) / a, 1.0f / 0.45f);
}
case ColorSpace::TransferID::BT1361_ECG: {
float a = 1.099f;
// Equal to FromLinear(ColorSpace::TransferID::BT1361_ECG, -l).
float from_linear_neg_l = -0.020250f;
// Equal to FromLinear(ColorSpace::TransferID::BT1361_ECG, b).
float from_linear_b = 0.081000f;
if (v < from_linear_neg_l)
return -pow((1.0f - a - v * 4.0f) / a, 1.0f / 0.45f) / 4.0f;
else if (v <= from_linear_b)
return v / 4.5f;
return pow((v + a - 1.0f) / a, 1.0f / 0.45f);
}
case ColorSpace::TransferID::SMPTEST2084: {
v = max(0.0f, v);
float m1 = (2610.0f / 4096.0f) / 4.0f;
float m2 = (2523.0f / 4096.0f) * 128.0f;
float c1 = 3424.0f / 4096.0f;
float c2 = (2413.0f / 4096.0f) * 32.0f;
float c3 = (2392.0f / 4096.0f) * 32.0f;
float p = pow(v, 1.0f / m2);
v = powf(max(p - c1, 0.0f) / (c2 - c3 * p), 1.0f / m1);
// This matches the scRGB definition that 1.0 means 80 nits.
// TODO(hubbe): It would be *nice* if 1.0 meant more than that, but
// that might be difficult to do right now.
v *= 10000.0f / 80.0f;
return v;
}
case ColorSpace::TransferID::SMPTEST2084_NON_HDR:
v = max(0.0f, v);
return min(2.3f * pow(v, 2.8f), v / 5.0f + 0.8f);
// Spec: http://www.arib.or.jp/english/html/overview/doc/2-STD-B67v1_0.pdf
case ColorSpace::TransferID::ARIB_STD_B67: {
v = max(0.0f, v);
const float a = 0.17883277f;
const float b = 0.28466892f;
const float c = 0.55991073f;
if (v <= 0.5f)
return (v * 2.0f) * (v * 2.0f);
return exp((v - c) / a) + b;
}
default:
// Handled by skcms_TransferFunction.
break;
}
NOTREACHED();
return 0;
}
Transform GetTransferMatrix(const gfx::ColorSpace& color_space) {
SkMatrix44 transfer_matrix;
color_space.GetTransferMatrix(&transfer_matrix);
return Transform(transfer_matrix);
}
Transform GetRangeAdjustMatrix(const gfx::ColorSpace& color_space) {
SkMatrix44 range_adjust_matrix;
color_space.GetRangeAdjustMatrix(&range_adjust_matrix);
return Transform(range_adjust_matrix);
}
Transform GetPrimaryTransform(const gfx::ColorSpace& color_space) {
SkMatrix44 primary_matrix;
color_space.GetPrimaryMatrix(&primary_matrix);
return Transform(primary_matrix);
}
} // namespace
class ColorTransformMatrix;
class ColorTransformSkTransferFn;
class ColorTransformFromLinear;
class ColorTransformToBT2020CL;
class ColorTransformFromBT2020CL;
class ColorTransformNull;
class SkiaColorTransform;
class ColorTransformStep {
public:
ColorTransformStep() {}
virtual ~ColorTransformStep() {}
virtual ColorTransformFromLinear* GetFromLinear() { return nullptr; }
virtual ColorTransformToBT2020CL* GetToBT2020CL() { return nullptr; }
virtual ColorTransformFromBT2020CL* GetFromBT2020CL() { return nullptr; }
virtual ColorTransformSkTransferFn* GetSkTransferFn() { return nullptr; }
virtual ColorTransformMatrix* GetMatrix() { return nullptr; }
virtual ColorTransformNull* GetNull() { return nullptr; }
virtual SkiaColorTransform* GetSkia() { return nullptr; }
// Join methods, returns true if the |next| transform was successfully
// assimilated into |this|.
// If Join() returns true, |next| is no longer needed and can be deleted.
virtual bool Join(ColorTransformStep* next) { return false; }
// Return true if this is a null transform.
virtual bool IsNull() { return false; }
virtual void Transform(ColorTransform::TriStim* color, size_t num) const = 0;
virtual bool CanAppendShaderSource() { return false; }
// In the shader, |hdr| will appear before |src|, so any helper functions that
// are created should be put in |hdr|. Any helper functions should have
// |step_index| included in the function name, to ensure that there are no
// naming conflicts.
virtual void AppendShaderSource(std::stringstream* hdr,
std::stringstream* src,
size_t step_index) const {
NOTREACHED();
}
private:
DISALLOW_COPY_AND_ASSIGN(ColorTransformStep);
};
class ColorTransformInternal : public ColorTransform {
public:
ColorTransformInternal(const ColorSpace& src,
const ColorSpace& dst,
Intent intent);
~ColorTransformInternal() override;
gfx::ColorSpace GetSrcColorSpace() const override { return src_; }
gfx::ColorSpace GetDstColorSpace() const override { return dst_; }
void Transform(TriStim* colors, size_t num) const override {
for (const auto& step : steps_) {
step->Transform(colors, num);
}
}
bool CanGetShaderSource() const override;
std::string GetShaderSource() const override;
bool IsIdentity() const override { return steps_.empty(); }
size_t NumberOfStepsForTesting() const override { return steps_.size(); }
private:
void AppendColorSpaceToColorSpaceTransform(ColorSpace src,
const ColorSpace& dst,
ColorTransform::Intent intent);
void Simplify();
// Retrieve the SkColorSpace for the ICC profile from which |color_space| was
// created, only if that is a more precise than the parametric representation.
sk_sp<SkColorSpace> GetSkColorSpaceIfNecessary(const ColorSpace& color_space);
std::list<std::unique_ptr<ColorTransformStep>> steps_;
gfx::ColorSpace src_;
gfx::ColorSpace dst_;
};
class ColorTransformNull : public ColorTransformStep {
public:
ColorTransformNull* GetNull() override { return this; }
bool IsNull() override { return true; }
void Transform(ColorTransform::TriStim* color, size_t num) const override {}
bool CanAppendShaderSource() override { return true; }
void AppendShaderSource(std::stringstream* hdr,
std::stringstream* src,
size_t step_index) const override {}
};
class ColorTransformMatrix : public ColorTransformStep {
public:
explicit ColorTransformMatrix(const class Transform& matrix)
: matrix_(matrix) {}
ColorTransformMatrix* GetMatrix() override { return this; }
bool Join(ColorTransformStep* next_untyped) override {
ColorTransformMatrix* next = next_untyped->GetMatrix();
if (!next)
return false;
class Transform tmp = next->matrix_;
tmp *= matrix_;
matrix_ = tmp;
return true;
}
bool IsNull() override {
return SkMatrixIsApproximatelyIdentity(matrix_.matrix());
}
void Transform(ColorTransform::TriStim* colors, size_t num) const override {
for (size_t i = 0; i < num; i++)
matrix_.TransformPoint(colors + i);
}
bool CanAppendShaderSource() override { return true; }
void AppendShaderSource(std::stringstream* hdr,
std::stringstream* src,
size_t step_index) const override {
const SkMatrix44& m = matrix_.matrix();
*src << " color = mat3(";
*src << m.get(0, 0) << ", " << m.get(1, 0) << ", " << m.get(2, 0) << ",";
*src << endl;
*src << " ";
*src << m.get(0, 1) << ", " << m.get(1, 1) << ", " << m.get(2, 1) << ",";
*src << endl;
*src << " ";
*src << m.get(0, 2) << ", " << m.get(1, 2) << ", " << m.get(2, 2) << ")";
*src << " * color;" << endl;
// Only print the translational component if it isn't the identity.
if (m.get(0, 3) != 0.f || m.get(1, 3) != 0.f || m.get(2, 3) != 0.f) {
*src << " color += vec3(";
*src << m.get(0, 3) << ", " << m.get(1, 3) << ", " << m.get(2, 3);
*src << ");" << endl;
}
}
private:
class Transform matrix_;
};
class ColorTransformPerChannelTransferFn : public ColorTransformStep {
public:
explicit ColorTransformPerChannelTransferFn(bool extended)
: extended_(extended) {}
void Transform(ColorTransform::TriStim* colors, size_t num) const override {
for (size_t i = 0; i < num; i++) {
ColorTransform::TriStim& c = colors[i];
if (extended_) {
c.set_x(copysign(Evaluate(abs(c.x())), c.x()));
c.set_y(copysign(Evaluate(abs(c.y())), c.y()));
c.set_z(copysign(Evaluate(abs(c.z())), c.z()));
} else {
c.set_x(Evaluate(c.x()));
c.set_y(Evaluate(c.y()));
c.set_z(Evaluate(c.z()));
}
}
}
void AppendShaderSource(std::stringstream* hdr,
std::stringstream* src,
size_t step_index) const override {
*hdr << "float TransferFn" << step_index << "(float v) {" << endl;
AppendTransferShaderSource(hdr);
*hdr << "}" << endl;
if (extended_) {
*src << " color.r = sign(color.r) * TransferFn" << step_index
<< "(abs(color.r));" << endl;
*src << " color.g = sign(color.g) * TransferFn" << step_index
<< "(abs(color.g));" << endl;
*src << " color.b = sign(color.b) * TransferFn" << step_index
<< "(abs(color.b));" << endl;
} else {
*src << " color.r = TransferFn" << step_index << "(color.r);" << endl;
*src << " color.g = TransferFn" << step_index << "(color.g);" << endl;
*src << " color.b = TransferFn" << step_index << "(color.b);" << endl;
}
}
virtual float Evaluate(float x) const = 0;
// Populate the body of a shader function that takes a float v and returns
// Evaluate(v).
virtual void AppendTransferShaderSource(std::stringstream* src) const = 0;
protected:
// True if the transfer function is extended to be defined for all real
// values by point symmetry.
bool extended_ = false;
};
class ColorTransformSkTransferFn : public ColorTransformPerChannelTransferFn {
public:
explicit ColorTransformSkTransferFn(const skcms_TransferFunction& fn,
bool extended)
: ColorTransformPerChannelTransferFn(extended), fn_(fn) {}
// ColorTransformStep implementation.
ColorTransformSkTransferFn* GetSkTransferFn() override { return this; }
bool Join(ColorTransformStep* next_untyped) override {
ColorTransformSkTransferFn* next = next_untyped->GetSkTransferFn();
if (!next)
return false;
if (!extended_ && !next->extended_ &&
SkTransferFnsApproximatelyCancel(fn_, next->fn_)) {
// Set to be the identity.
fn_.a = 1;
fn_.b = 0;
fn_.c = 1;
fn_.d = 0;
fn_.e = 0;
fn_.f = 0;
fn_.g = 1;
return true;
}
return false;
}
bool CanAppendShaderSource() override { return true; }
bool IsNull() override { return SkTransferFnIsApproximatelyIdentity(fn_); }
// ColorTransformPerChannelTransferFn implementation:
float Evaluate(float v) const override {
// Note that the sign-extension is performed by the caller.
return SkTransferFnEvalUnclamped(fn_, v);
}
void AppendTransferShaderSource(std::stringstream* result) const override {
const float kEpsilon = 1.f / 1024.f;
// Construct the linear segment
// linear = C * x + F
// Elide operations that will be close to the identity.
std::string linear = "v";
if (std::abs(fn_.c - 1.f) > kEpsilon)
linear = Str(fn_.c) + " * " + linear;
if (std::abs(fn_.f) > kEpsilon)
linear = linear + " + " + Str(fn_.f);
// Construct the nonlinear segment.
// nonlinear = pow(A * x + B, G) + E
// Elide operations (especially the pow) that will be close to the
// identity.
std::string nonlinear = "v";
if (std::abs(fn_.a - 1.f) > kEpsilon)
nonlinear = Str(fn_.a) + " * " + nonlinear;
if (std::abs(fn_.b) > kEpsilon)
nonlinear = nonlinear + " + " + Str(fn_.b);
if (std::abs(fn_.g - 1.f) > kEpsilon)
nonlinear = "pow(" + nonlinear + ", " + Str(fn_.g) + ")";
if (std::abs(fn_.e) > kEpsilon)
nonlinear = nonlinear + " + " + Str(fn_.e);
*result << " if (v < " << Str(fn_.d) << ")" << endl;
*result << " return " << linear << ";" << endl;
*result << " return " << nonlinear << ";" << endl;
}
private:
skcms_TransferFunction fn_;
};
class ColorTransformFromLinear : public ColorTransformPerChannelTransferFn {
public:
// ColorTransformStep implementation.
explicit ColorTransformFromLinear(ColorSpace::TransferID transfer)
: ColorTransformPerChannelTransferFn(false), transfer_(transfer) {}
ColorTransformFromLinear* GetFromLinear() override { return this; }
bool CanAppendShaderSource() override { return true; }
bool IsNull() override { return transfer_ == ColorSpace::TransferID::LINEAR; }
// ColorTransformPerChannelTransferFn implementation:
float Evaluate(float v) const override { return FromLinear(transfer_, v); }
void AppendTransferShaderSource(std::stringstream* src) const override {
// This is a string-ized copy-paste from FromLinear.
switch (transfer_) {
case ColorSpace::TransferID::LOG:
*src << " if (v < 0.01)\n"
" return 0.0;\n"
" return 1.0 + log(v) / log(10.0) / 2.0;\n";
return;
case ColorSpace::TransferID::LOG_SQRT:
*src << " if (v < sqrt(10.0) / 1000.0)\n"
" return 0.0;\n"
" return 1.0 + log(v) / log(10.0) / 2.5;\n";
return;
case ColorSpace::TransferID::IEC61966_2_4:
*src << " float a = 1.099296826809442;\n"
" float b = 0.018053968510807;\n"
" if (v < -b)\n"
" return -a * pow(-v, 0.45) + (a - 1.0);\n"
" else if (v <= b)\n"
" return 4.5 * v;\n"
" return a * pow(v, 0.45) - (a - 1.0);\n";
return;
case ColorSpace::TransferID::BT1361_ECG:
*src << " float a = 1.099;\n"
" float b = 0.018;\n"
" float l = 0.0045;\n"
" if (v < -l)\n"
" return -(a * pow(-4.0 * v, 0.45) + (a - 1.0)) / 4.0;\n"
" else if (v <= b)\n"
" return 4.5 * v;\n"
" return a * pow(v, 0.45) - (a - 1.0);\n";
return;
case ColorSpace::TransferID::SMPTEST2084:
*src << " v *= 80.0 / 10000.0;\n"
" v = max(0.0, v);\n"
" float m1 = (2610.0 / 4096.0) / 4.0;\n"
" float m2 = (2523.0 / 4096.0) * 128.0;\n"
" float c1 = 3424.0 / 4096.0;\n"
" float c2 = (2413.0 / 4096.0) * 32.0;\n"
" float c3 = (2392.0 / 4096.0) * 32.0;\n"
" return pow((c1 + c2 * pow(v, m1)) / \n"
" (1.0 + c3 * pow(v, m1)), m2);\n";
return;
case ColorSpace::TransferID::ARIB_STD_B67:
*src << " const float a = 0.17883277;\n"
" const float b = 0.28466892;\n"
" const float c = 0.55991073;\n"
" v = max(0.0, v);\n"
" if (v <= 1.0)\n"
" return 0.5 * sqrt(v);\n"
" return a * log(v - b) + c;\n";
return;
default:
break;
}
NOTREACHED();
}
private:
friend class ColorTransformToLinear;
ColorSpace::TransferID transfer_;
};
class ColorTransformToLinear : public ColorTransformPerChannelTransferFn {
public:
explicit ColorTransformToLinear(ColorSpace::TransferID transfer)
: ColorTransformPerChannelTransferFn(false), transfer_(transfer) {}
// ColorTransformStep implementation:
bool Join(ColorTransformStep* next_untyped) override {
ColorTransformFromLinear* next = next_untyped->GetFromLinear();
if (!next)
return false;
if (transfer_ == next->transfer_) {
transfer_ = ColorSpace::TransferID::LINEAR;
return true;
}
return false;
}
bool CanAppendShaderSource() override { return true; }
bool IsNull() override { return transfer_ == ColorSpace::TransferID::LINEAR; }
// ColorTransformPerChannelTransferFn implementation:
float Evaluate(float v) const override { return ToLinear(transfer_, v); }
void AppendTransferShaderSource(std::stringstream* src) const override {
// This is a string-ized copy-paste from ToLinear.
switch (transfer_) {
case ColorSpace::TransferID::LOG:
*src << " if (v < 0.0)\n"
" return 0.0;\n"
" return pow(10.0, (v - 1.0) * 2.0);\n";
return;
case ColorSpace::TransferID::LOG_SQRT:
*src << " if (v < 0.0)\n"
" return 0.0;\n"
" return pow(10.0, (v - 1.0) * 2.5);\n";
return;
case ColorSpace::TransferID::IEC61966_2_4:
*src << " float a = 1.099296826809442;\n"
" float from_linear_neg_a = -1.047844;\n"
" float from_linear_b = 0.081243;\n"
" if (v < from_linear_neg_a)\n"
" return -pow((a - 1.0 - v) / a, 1.0 / 0.45);\n"
" else if (v <= from_linear_b)\n"
" return v / 4.5;\n"
" return pow((v + a - 1.0) / a, 1.0 / 0.45);\n";
return;
case ColorSpace::TransferID::BT1361_ECG:
*src << " float a = 1.099;\n"
" float from_linear_neg_l = -0.020250;\n"
" float from_linear_b = 0.081000;\n"
" if (v < from_linear_neg_l)\n"
" return -pow((1.0 - a - v * 4.0) / a, 1.0 / 0.45) / 4.0;\n"
" else if (v <= from_linear_b)\n"
" return v / 4.5;\n"
" return pow((v + a - 1.0) / a, 1.0 / 0.45);\n";
return;
case ColorSpace::TransferID::SMPTEST2084:
*src << " v = max(0.0, v);\n"
" float m1 = (2610.0 / 4096.0) / 4.0;\n"
" float m2 = (2523.0 / 4096.0) * 128.0;\n"
" float c1 = 3424.0 / 4096.0;\n"
" float c2 = (2413.0 / 4096.0) * 32.0;\n"
" float c3 = (2392.0 / 4096.0) * 32.0;\n"
" #ifdef GL_FRAGMENT_PRECISION_HIGH\n"
" highp float v2 = v;\n"
" #else\n"
" float v2 = v;\n"
" #endif\n"
" v2 = pow(max(pow(v2, 1.0 / m2) - c1, 0.0) /\n"
" (c2 - c3 * pow(v2, 1.0 / m2)), 1.0 / m1);\n"
" v = v2 * 10000.0 / 80.0;\n"
" return v;\n";
return;
case ColorSpace::TransferID::SMPTEST2084_NON_HDR:
*src << " v = max(0.0, v);\n"
" return min(2.3 * pow(v, 2.8), v / 5.0 + 0.8);\n";
return;
case ColorSpace::TransferID::ARIB_STD_B67:
*src << " v = max(0.0, v);\n"
" float a = 0.17883277;\n"
" float b = 0.28466892;\n"
" float c = 0.55991073;\n"
" if (v <= 0.5)\n"
" return (v * 2.0) * (v * 2.0);\n"
" return exp((v - c) / a) + b;\n";
return;
default:
break;
}
NOTREACHED();
}
private:
ColorSpace::TransferID transfer_;
};
class ColorTransformSMPTEST2048NonHdrToLinear : public ColorTransformStep {
public:
// Assumes BT2020 primaries.
static float Luma(const ColorTransform::TriStim& c) {
return c.x() * 0.2627f + c.y() * 0.6780f + c.z() * 0.0593f;
}
static ColorTransform::TriStim ClipToWhite(ColorTransform::TriStim& c) {
float maximum = max(max(c.x(), c.y()), c.z());
if (maximum > 1.0f) {
float l = Luma(c);
c.Scale(1.0f / maximum);
ColorTransform::TriStim white(1.0f, 1.0f, 1.0f);
white.Scale((1.0f - 1.0f / maximum) * l / Luma(white));
ColorTransform::TriStim black(0.0f, 0.0f, 0.0f);
c += white - black;
}
return c;
}
void Transform(ColorTransform::TriStim* colors, size_t num) const override {
for (size_t i = 0; i < num; i++) {
ColorTransform::TriStim ret(
ToLinear(ColorSpace::TransferID::SMPTEST2084_NON_HDR, colors[i].x()),
ToLinear(ColorSpace::TransferID::SMPTEST2084_NON_HDR, colors[i].y()),
ToLinear(ColorSpace::TransferID::SMPTEST2084_NON_HDR, colors[i].z()));
if (Luma(ret) > 0.0) {
ColorTransform::TriStim smpte2084(
ToLinear(ColorSpace::TransferID::SMPTEST2084, colors[i].x()),
ToLinear(ColorSpace::TransferID::SMPTEST2084, colors[i].y()),
ToLinear(ColorSpace::TransferID::SMPTEST2084, colors[i].z()));
smpte2084.Scale(Luma(ret) / Luma(smpte2084));
ret = ClipToWhite(smpte2084);
}
colors[i] = ret;
}
}
};
// BT2020 Constant Luminance is different than most other
// ways to encode RGB values as YUV. The basic idea is that
// transfer functions are applied on the Y value instead of
// on the RGB values. However, running the transfer function
// on the U and V values doesn't make any sense since they
// are centered at 0.5. To work around this, the transfer function
// is applied to the Y, R and B values, and then the U and V
// values are calculated from that.
// In our implementation, the YUV->RGB matrix is used to
// convert YUV to RYB (the G value is replaced with an Y value.)
// Then we run the transfer function like normal, and finally
// this class is inserted as an extra step which takes calculates
// the U and V values.
class ColorTransformToBT2020CL : public ColorTransformStep {
public:
bool Join(ColorTransformStep* next_untyped) override {
ColorTransformFromBT2020CL* next = next_untyped->GetFromBT2020CL();
if (!next)
return false;
if (null_)
return false;
null_ = true;
return true;
}
bool IsNull() override { return null_; }
void Transform(ColorTransform::TriStim* RYB, size_t num) const override {
for (size_t i = 0; i < num; i++) {
float U, V;
float B_Y = RYB[i].z() - RYB[i].y();
if (B_Y <= 0) {
U = B_Y / (-2.0 * -0.9702);
} else {
U = B_Y / (2.0 * 0.7910);
}
float R_Y = RYB[i].x() - RYB[i].y();
if (R_Y <= 0) {
V = R_Y / (-2.0 * -0.8591);
} else {
V = R_Y / (2.0 * 0.4969);
}
RYB[i] = ColorTransform::TriStim(RYB[i].y(), U + 0.5, V + 0.5);
}
}
private:
bool null_ = false;
};
// Inverse of ColorTransformToBT2020CL, see comment above for more info.
class ColorTransformFromBT2020CL : public ColorTransformStep {
public:
bool Join(ColorTransformStep* next_untyped) override {
ColorTransformToBT2020CL* next = next_untyped->GetToBT2020CL();
if (!next)
return false;
if (null_)
return false;
null_ = true;
return true;
}
bool IsNull() override { return null_; }
void Transform(ColorTransform::TriStim* YUV, size_t num) const override {
if (null_)
return;
for (size_t i = 0; i < num; i++) {
float Y = YUV[i].x();
float U = YUV[i].y() - 0.5;
float V = YUV[i].z() - 0.5;
float B_Y, R_Y;
if (U <= 0) {
B_Y = U * (-2.0 * -0.9702);
} else {
B_Y = U * (2.0 * 0.7910);
}
if (V <= 0) {
R_Y = V * (-2.0 * -0.8591);
} else {
R_Y = V * (2.0 * 0.4969);
}
// Return an RYB value, later steps will fix it.
YUV[i] = ColorTransform::TriStim(R_Y + Y, Y, B_Y + Y);
}
}
bool CanAppendShaderSource() override { return true; }
void AppendShaderSource(std::stringstream* hdr,
std::stringstream* src,
size_t step_index) const override {
*hdr << "vec3 BT2020_YUV_to_RYB_Step" << step_index << "(vec3 color) {"
<< endl;
*hdr << " float Y = color.x;" << endl;
*hdr << " float U = color.y - 0.5;" << endl;
*hdr << " float V = color.z - 0.5;" << endl;
*hdr << " float B_Y = 0.0;" << endl;
*hdr << " float R_Y = 0.0;" << endl;
*hdr << " if (U <= 0.0) {" << endl;
*hdr << " B_Y = U * (-2.0 * -0.9702);" << endl;
*hdr << " } else {" << endl;
*hdr << " B_Y = U * (2.0 * 0.7910);" << endl;
*hdr << " }" << endl;
*hdr << " if (V <= 0.0) {" << endl;
*hdr << " R_Y = V * (-2.0 * -0.8591);" << endl;
*hdr << " } else {" << endl;
*hdr << " R_Y = V * (2.0 * 0.4969);" << endl;
*hdr << " }" << endl;
*hdr << " return vec3(R_Y + Y, Y, B_Y + Y);" << endl;
*hdr << "}" << endl;
*src << " color.rgb = BT2020_YUV_to_RYB_Step" << step_index
<< "(color.rgb);" << endl;
}
private:
bool null_ = false;
};
void ColorTransformInternal::AppendColorSpaceToColorSpaceTransform(
ColorSpace src,
const ColorSpace& dst,
ColorTransform::Intent intent) {
if (intent == ColorTransform::Intent::INTENT_PERCEPTUAL) {
switch (src.transfer_) {
case ColorSpace::TransferID::SMPTEST2084:
if (!dst.IsHDR()) {
// We don't have an HDR display, so replace SMPTE 2084 with
// something that returns ranges more or less suitable for a normal
// display.
src.transfer_ = ColorSpace::TransferID::SMPTEST2084_NON_HDR;
}
break;
case ColorSpace::TransferID::ARIB_STD_B67:
if (!dst.IsHDR()) {
// Interpreting HLG using a gamma 2.4 works reasonably well for SDR
// displays.
src.transfer_ = ColorSpace::TransferID::GAMMA24;
}
break;
default: // Do nothing
break;
}
// TODO(hubbe): shrink gamuts here (never stretch gamuts)
}
steps_.push_back(
std::make_unique<ColorTransformMatrix>(GetRangeAdjustMatrix(src)));
if (src.matrix_ == ColorSpace::MatrixID::BT2020_CL) {
// BT2020 CL is a special case.
steps_.push_back(std::make_unique<ColorTransformFromBT2020CL>());
} else {
steps_.push_back(
std::make_unique<ColorTransformMatrix>(Invert(GetTransferMatrix(src))));
}
// If the target color space is not defined, just apply the adjust and
// tranfer matrices. This path is used by YUV to RGB color conversion
// when full color conversion is not enabled.
if (!dst.IsValid())
return;
skcms_TransferFunction src_to_linear_fn;
if (src.GetTransferFunction(&src_to_linear_fn)) {
steps_.push_back(std::make_unique<ColorTransformSkTransferFn>(
src_to_linear_fn, src.HasExtendedSkTransferFn()));
} else if (src.transfer_ == ColorSpace::TransferID::SMPTEST2084_NON_HDR) {
steps_.push_back(
std::make_unique<ColorTransformSMPTEST2048NonHdrToLinear>());
} else {
steps_.push_back(std::make_unique<ColorTransformToLinear>(src.transfer_));
}
if (src.matrix_ == ColorSpace::MatrixID::BT2020_CL) {
// BT2020 CL is a special case.
steps_.push_back(
std::make_unique<ColorTransformMatrix>(Invert(GetTransferMatrix(src))));
}
steps_.push_back(
std::make_unique<ColorTransformMatrix>(GetPrimaryTransform(src)));
steps_.push_back(
std::make_unique<ColorTransformMatrix>(Invert(GetPrimaryTransform(dst))));
if (dst.matrix_ == ColorSpace::MatrixID::BT2020_CL) {
// BT2020 CL is a special case.
steps_.push_back(
std::make_unique<ColorTransformMatrix>(GetTransferMatrix(dst)));
}
skcms_TransferFunction dst_from_linear_fn;
if (dst.GetInverseTransferFunction(&dst_from_linear_fn)) {
steps_.push_back(std::make_unique<ColorTransformSkTransferFn>(
dst_from_linear_fn, dst.HasExtendedSkTransferFn()));
} else {
steps_.push_back(std::make_unique<ColorTransformFromLinear>(dst.transfer_));
}
if (dst.matrix_ == ColorSpace::MatrixID::BT2020_CL) {
steps_.push_back(std::make_unique<ColorTransformToBT2020CL>());
} else {
steps_.push_back(
std::make_unique<ColorTransformMatrix>(GetTransferMatrix(dst)));
}
steps_.push_back(std::make_unique<ColorTransformMatrix>(
Invert(GetRangeAdjustMatrix(dst))));
}
class SkiaColorTransform : public ColorTransformStep {
public:
// Takes ownership of the profiles
SkiaColorTransform(sk_sp<SkColorSpace> src, sk_sp<SkColorSpace> dst)
: src_(src), dst_(dst) {}
~SkiaColorTransform() override {
src_ = nullptr;
dst_ = nullptr;
}
SkiaColorTransform* GetSkia() override { return this; }
bool Join(ColorTransformStep* next_untyped) override {
SkiaColorTransform* next = next_untyped->GetSkia();
if (!next)
return false;
if (SkColorSpace::Equals(dst_.get(), next->src_.get())) {
dst_ = next->dst_;
return true;
}
return false;
}
bool IsNull() override {
if (SkColorSpace::Equals(src_.get(), dst_.get()))
return true;
return false;
}
void Transform(ColorTransform::TriStim* colors, size_t num) const override {
// We could do this either using Skia or skcms, but since skcms can handle
// TriStim directly as skcms_PixelFormat_RGB_fff, let's use that.
skcms_ICCProfile src_profile, dst_profile;
src_->toProfile(&src_profile);
dst_->toProfile(&dst_profile);
const skcms_PixelFormat kFFF = skcms_PixelFormat_RGB_fff;
const skcms_AlphaFormat kUPM = skcms_AlphaFormat_Unpremul;
bool xform_result = skcms_Transform(colors, kFFF, kUPM, &src_profile,
colors, kFFF, kUPM, &dst_profile, num);
DCHECK(xform_result);
}
private:
sk_sp<SkColorSpace> src_;
sk_sp<SkColorSpace> dst_;
};
sk_sp<SkColorSpace> ColorTransformInternal::GetSkColorSpaceIfNecessary(
const ColorSpace& color_space) {
if (!color_space.icc_profile_id_)
return nullptr;
return ICCProfile::GetSkColorSpaceFromId(color_space.icc_profile_id_);
}
ColorTransformInternal::ColorTransformInternal(const ColorSpace& src,
const ColorSpace& dst,
Intent intent)
: src_(src), dst_(dst) {
// If no source color space is specified, do no transformation.
// TODO(ccameron): We may want dst assume sRGB at some point in the future.
if (!src_.IsValid())
return;
// SMPTEST2084_NON_HDR is not a valid destination.
if (dst.transfer_ == ColorSpace::TransferID::SMPTEST2084_NON_HDR) {
DLOG(ERROR) << "Invalid dst transfer function, returning identity.";
return;
}
// If the target color space is not defined, just apply the adjust and
// tranfer matrices. This path is used by YUV to RGB color conversion
// when full color conversion is not enabled.
sk_sp<SkColorSpace> src_sk_color_space;
sk_sp<SkColorSpace> dst_sk_color_space;
bool has_src_profile = false;
bool has_dst_profile = false;
if (dst.IsValid()) {
src_sk_color_space = GetSkColorSpaceIfNecessary(src_);
dst_sk_color_space = GetSkColorSpaceIfNecessary(dst_);
}
has_src_profile = !!src_sk_color_space;
has_dst_profile = !!dst_sk_color_space;
if (has_src_profile) {
steps_.push_back(std::make_unique<SkiaColorTransform>(
std::move(src_sk_color_space),
ColorSpace::CreateXYZD50().ToSkColorSpace()));
}
AppendColorSpaceToColorSpaceTransform(
has_src_profile ? ColorSpace::CreateXYZD50() : src_,
has_dst_profile ? ColorSpace::CreateXYZD50() : dst_, intent);
if (has_dst_profile) {
steps_.push_back(std::make_unique<SkiaColorTransform>(
ColorSpace::CreateXYZD50().ToSkColorSpace(),
std::move(dst_sk_color_space)));
}
if (intent != Intent::TEST_NO_OPT)
Simplify();
}
std::string ColorTransformInternal::GetShaderSource() const {
std::stringstream hdr;
std::stringstream src;
InitStringStream(&hdr);
InitStringStream(&src);
src << "vec3 DoColorConversion(vec3 color) {" << endl;
size_t step_index = 0;
for (const auto& step : steps_)
step->AppendShaderSource(&hdr, &src, step_index++);
src << " return color;" << endl;
src << "}" << endl;
return hdr.str() + src.str();
}
bool ColorTransformInternal::CanGetShaderSource() const {
for (const auto& step : steps_) {
if (!step->CanAppendShaderSource())
return false;
}
return true;
}
ColorTransformInternal::~ColorTransformInternal() {}
void ColorTransformInternal::Simplify() {
for (auto iter = steps_.begin(); iter != steps_.end();) {
std::unique_ptr<ColorTransformStep>& this_step = *iter;
// Try to Join |next_step| into |this_step|. If successful, re-visit the
// step before |this_step|.
auto iter_next = iter;
iter_next++;
if (iter_next != steps_.end()) {
std::unique_ptr<ColorTransformStep>& next_step = *iter_next;
if (this_step->Join(next_step.get())) {
steps_.erase(iter_next);
if (iter != steps_.begin())
--iter;
continue;
}
}
// If |this_step| step is a no-op, remove it, and re-visit the step before
// |this_step|.
if (this_step->IsNull()) {
iter = steps_.erase(iter);
if (iter != steps_.begin())
--iter;
continue;
}
++iter;
}
}
// static
std::unique_ptr<ColorTransform> ColorTransform::NewColorTransform(
const ColorSpace& src,
const ColorSpace& dst,
Intent intent) {
return std::unique_ptr<ColorTransform>(
new ColorTransformInternal(src, dst, intent));
}
ColorTransform::ColorTransform() {}
ColorTransform::~ColorTransform() {}
} // namespace gfx