Avi Drissman | 3e1a26c | 2022-09-15 20:26:03 | [diff] [blame^] | 1 | // Copyright 2016 The Chromium Authors |
ccameron | efdab16 | 2016-07-25 23:00:02 | [diff] [blame] | 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | #include "ui/gfx/icc_profile.h" |
| 6 | |
| 7 | #include <list> |
Christopher Cameron | 374b6c4 | 2017-08-31 22:21:23 | [diff] [blame] | 8 | #include <set> |
ccameron | efdab16 | 2016-07-25 23:00:02 | [diff] [blame] | 9 | |
ccameron | 290a91a | 2017-05-13 19:22:33 | [diff] [blame] | 10 | #include "base/command_line.h" |
cfredric | 8fdc2243 | 2021-10-14 03:24:00 | [diff] [blame] | 11 | #include "base/containers/lru_cache.h" |
ccameron | efdab16 | 2016-07-25 23:00:02 | [diff] [blame] | 12 | #include "base/lazy_instance.h" |
Hans Wennborg | 17cd607 | 2020-04-22 15:47:04 | [diff] [blame] | 13 | #include "base/logging.h" |
ccameron | efdab16 | 2016-07-25 23:00:02 | [diff] [blame] | 14 | #include "base/synchronization/lock.h" |
Mike Klein | eb11dca | 2018-09-05 15:47:06 | [diff] [blame] | 15 | #include "third_party/skia/include/core/SkColorSpace.h" |
Kevin Lubick | efd439a | 2022-07-11 13:50:45 | [diff] [blame] | 16 | #include "third_party/skia/modules/skcms/skcms.h" |
ccameron | 24c87c3 | 2017-03-14 21:50:42 | [diff] [blame] | 17 | #include "ui/gfx/skia_color_space_util.h" |
ccameron | efdab16 | 2016-07-25 23:00:02 | [diff] [blame] | 18 | |
| 19 | namespace gfx { |
| 20 | |
Christopher Cameron | 374b6c4 | 2017-08-31 22:21:23 | [diff] [blame] | 21 | namespace { |
| 22 | |
Christopher Cameron | 3bc3d5e | 2017-11-23 03:48:11 | [diff] [blame] | 23 | static const size_t kMaxCachedICCProfiles = 16; |
| 24 | |
cfredric | 8fdc2243 | 2021-10-14 03:24:00 | [diff] [blame] | 25 | // An LRU cache mapping data to ICCProfile objects, to avoid re-parsing |
Christopher Cameron | 3bc3d5e | 2017-11-23 03:48:11 | [diff] [blame] | 26 | // profiles every time they are read. |
cfredric | 8fdc2243 | 2021-10-14 03:24:00 | [diff] [blame] | 27 | using DataToProfileCacheBase = base::LRUCache<std::vector<char>, ICCProfile>; |
Christopher Cameron | 3bc3d5e | 2017-11-23 03:48:11 | [diff] [blame] | 28 | class DataToProfileCache : public DataToProfileCacheBase { |
| 29 | public: |
| 30 | DataToProfileCache() : DataToProfileCacheBase(kMaxCachedICCProfiles) {} |
| 31 | }; |
Christopher Cameron | a9b81c0b13 | 2017-12-21 00:23:54 | [diff] [blame] | 32 | base::LazyInstance<DataToProfileCache>::Leaky g_data_to_profile_cache = |
| 33 | LAZY_INSTANCE_INITIALIZER; |
Christopher Cameron | 3bc3d5e | 2017-11-23 03:48:11 | [diff] [blame] | 34 | |
Christopher Cameron | 20eeb0c | 2019-04-05 23:40:00 | [diff] [blame] | 35 | // Lock that must be held to access |g_data_to_profile_cache|. |
Daniel Bratell | b6c0ab8c | 2017-12-21 13:09:18 | [diff] [blame] | 36 | base::LazyInstance<base::Lock>::Leaky g_icc_profile_lock = |
| 37 | LAZY_INSTANCE_INITIALIZER; |
ccameron | efdab16 | 2016-07-25 23:00:02 | [diff] [blame] | 38 | |
Christopher Cameron | 374b6c4 | 2017-08-31 22:21:23 | [diff] [blame] | 39 | } // namespace |
| 40 | |
Brian Osman | f836de5 | 2020-03-04 02:13:01 | [diff] [blame] | 41 | void ICCProfile::Internals::Initialize() { |
Christopher Cameron | fdc0f87 | 2017-11-09 03:22:16 | [diff] [blame] | 42 | // Start out with no parametric data. |
Christopher Cameron | 3bc3d5e | 2017-11-23 03:48:11 | [diff] [blame] | 43 | if (data_.empty()) |
Brian Osman | f836de5 | 2020-03-04 02:13:01 | [diff] [blame] | 44 | return; |
Christopher Cameron | a4adb41b | 2017-07-13 06:44:25 | [diff] [blame] | 45 | |
Brian Osman | 37233e3 | 2018-05-17 16:53:17 | [diff] [blame] | 46 | // Parse the profile. |
| 47 | skcms_ICCProfile profile; |
| 48 | if (!skcms_Parse(data_.data(), data_.size(), &profile)) { |
| 49 | DLOG(ERROR) << "Failed to parse ICC profile."; |
Brian Osman | f836de5 | 2020-03-04 02:13:01 | [diff] [blame] | 50 | return; |
Christopher Cameron | a4adb41b | 2017-07-13 06:44:25 | [diff] [blame] | 51 | } |
Brian Osman | 37233e3 | 2018-05-17 16:53:17 | [diff] [blame] | 52 | |
Brian Osman | 56adc57 | 2018-07-26 17:34:43 | [diff] [blame] | 53 | // We have seen many users with profiles that don't have a D50 white point. |
| 54 | // Windows appears to detect these profiles, and not use them for OS drawing. |
| 55 | // It still returns them when we query the system for the installed profile. |
| 56 | // For consistency (and to match old behavior) we reject these profiles on |
| 57 | // all platforms. |
| 58 | // https://crbug.com/847024 |
| 59 | const skcms_Matrix3x3& m(profile.toXYZD50); |
| 60 | float wX = m.vals[0][0] + m.vals[0][1] + m.vals[0][2]; |
| 61 | float wY = m.vals[1][0] + m.vals[1][1] + m.vals[1][2]; |
| 62 | float wZ = m.vals[2][0] + m.vals[2][1] + m.vals[2][2]; |
Brian Osman | 56adc57 | 2018-07-26 17:34:43 | [diff] [blame] | 63 | static const float kD50_WhitePoint[3] = { 0.96420f, 1.00000f, 0.82491f }; |
| 64 | if (fabsf(wX - kD50_WhitePoint[0]) > 0.04f || |
| 65 | fabsf(wY - kD50_WhitePoint[1]) > 0.04f || |
| 66 | fabsf(wZ - kD50_WhitePoint[2]) > 0.04f) { |
Brian Osman | f836de5 | 2020-03-04 02:13:01 | [diff] [blame] | 67 | return; |
Brian Osman | 56adc57 | 2018-07-26 17:34:43 | [diff] [blame] | 68 | } |
| 69 | |
Brian Osman | f836de5 | 2020-03-04 02:13:01 | [diff] [blame] | 70 | // At this point, the profile is considered valid. We still need to determine |
| 71 | // if it's representable with a parametric transfer function. |
| 72 | is_valid_ = true; |
| 73 | |
Christopher Cameron | badc002 | 2019-04-08 18:37:02 | [diff] [blame] | 74 | // Extract the primary matrix, and assume that transfer function is sRGB until |
| 75 | // we get something more precise. |
| 76 | to_XYZD50_ = profile.toXYZD50; |
| 77 | transfer_fn_ = SkNamedTransferFn::kSRGB; |
| 78 | |
Christopher Cameron | 20eeb0c | 2019-04-05 23:40:00 | [diff] [blame] | 79 | // Coerce it into a rasterization destination (if possible). If the profile |
| 80 | // can't be approximated accurately, then use an sRGB transfer function and |
| 81 | // return failure. We will continue to use the gamut from this profile. |
| 82 | if (!skcms_MakeUsableAsDestinationWithSingleCurve(&profile)) { |
Christopher Cameron | badc002 | 2019-04-08 18:37:02 | [diff] [blame] | 83 | DLOG(ERROR) << "Parsed ICC profile but can't make usable as destination, " |
| 84 | "using sRGB gamma"; |
Brian Osman | f836de5 | 2020-03-04 02:13:01 | [diff] [blame] | 85 | return; |
Christopher Cameron | 20eeb0c | 2019-04-05 23:40:00 | [diff] [blame] | 86 | } |
Christopher Cameron | a4adb41b | 2017-07-13 06:44:25 | [diff] [blame] | 87 | |
Christopher Cameron | 20eeb0c | 2019-04-05 23:40:00 | [diff] [blame] | 88 | // If SkColorSpace will treat the gamma as that of sRGB, then use the named |
| 89 | // constants. |
| 90 | sk_sp<SkColorSpace> sk_color_space = SkColorSpace::Make(profile); |
Christopher Cameron | badc002 | 2019-04-08 18:37:02 | [diff] [blame] | 91 | if (!sk_color_space) { |
| 92 | DLOG(ERROR) << "Parsed ICC profile but cannot create SkColorSpace from it, " |
| 93 | "using sRGB gamma."; |
Brian Osman | f836de5 | 2020-03-04 02:13:01 | [diff] [blame] | 94 | return; |
Christopher Cameron | 20eeb0c | 2019-04-05 23:40:00 | [diff] [blame] | 95 | } |
Brian Osman | f836de5 | 2020-03-04 02:13:01 | [diff] [blame] | 96 | |
| 97 | // We were able to get a parametric representation of the transfer function. |
| 98 | is_parametric_ = true; |
| 99 | |
Christopher Cameron | badc002 | 2019-04-08 18:37:02 | [diff] [blame] | 100 | if (sk_color_space->gammaCloseToSRGB()) |
Brian Osman | f836de5 | 2020-03-04 02:13:01 | [diff] [blame] | 101 | return; |
ccameron | e35e232 | 2017-11-02 23:33:46 | [diff] [blame] | 102 | |
Brian Osman | 37233e3 | 2018-05-17 16:53:17 | [diff] [blame] | 103 | // We assume that if we accurately approximated the profile, then the |
| 104 | // single-curve version (which may have higher error) is also okay. If we |
| 105 | // want to maintain the distinction between accurate and inaccurate profiles, |
| 106 | // we could check to see if the single-curve version is/ approximately equal |
| 107 | // to the original (or to the multi-channel approximation). |
Christopher Cameron | 20eeb0c | 2019-04-05 23:40:00 | [diff] [blame] | 108 | transfer_fn_ = profile.trc[0].parametric; |
Christopher Cameron | 374b6c4 | 2017-08-31 22:21:23 | [diff] [blame] | 109 | } |
Reilly Grant | c46032eb | 2017-08-30 22:53:52 | [diff] [blame] | 110 | |
ccameron | efdab16 | 2016-07-25 23:00:02 | [diff] [blame] | 111 | ICCProfile::ICCProfile() = default; |
| 112 | ICCProfile::ICCProfile(ICCProfile&& other) = default; |
| 113 | ICCProfile::ICCProfile(const ICCProfile& other) = default; |
| 114 | ICCProfile& ICCProfile::operator=(ICCProfile&& other) = default; |
| 115 | ICCProfile& ICCProfile::operator=(const ICCProfile& other) = default; |
| 116 | ICCProfile::~ICCProfile() = default; |
| 117 | |
| 118 | bool ICCProfile::operator==(const ICCProfile& other) const { |
Christopher Cameron | 43f1705 | 2017-11-11 01:52:01 | [diff] [blame] | 119 | if (!internals_ && !other.internals_) |
| 120 | return true; |
Christopher Cameron | deea252 | 2017-11-16 07:10:52 | [diff] [blame] | 121 | if (internals_ && other.internals_) { |
Christopher Cameron | 20eeb0c | 2019-04-05 23:40:00 | [diff] [blame] | 122 | return internals_->data_ == other.internals_->data_; |
Christopher Cameron | deea252 | 2017-11-16 07:10:52 | [diff] [blame] | 123 | } |
Christopher Cameron | 43f1705 | 2017-11-11 01:52:01 | [diff] [blame] | 124 | return false; |
ccameron | efdab16 | 2016-07-25 23:00:02 | [diff] [blame] | 125 | } |
| 126 | |
ccameron | c21ca23b | 2017-01-20 03:34:01 | [diff] [blame] | 127 | bool ICCProfile::operator!=(const ICCProfile& other) const { |
| 128 | return !(*this == other); |
| 129 | } |
| 130 | |
Christopher Cameron | 43f1705 | 2017-11-11 01:52:01 | [diff] [blame] | 131 | bool ICCProfile::IsValid() const { |
| 132 | return internals_ ? internals_->is_valid_ : false; |
| 133 | } |
| 134 | |
Christopher Cameron | 3bc3d5e | 2017-11-23 03:48:11 | [diff] [blame] | 135 | std::vector<char> ICCProfile::GetData() const { |
| 136 | return internals_ ? internals_->data_ : std::vector<char>(); |
| 137 | } |
| 138 | |
ccameron | efdab16 | 2016-07-25 23:00:02 | [diff] [blame] | 139 | // static |
Christopher Cameron | 20eeb0c | 2019-04-05 23:40:00 | [diff] [blame] | 140 | ICCProfile ICCProfile::FromData(const void* data_as_void, size_t size) { |
Christopher Cameron | 43f1705 | 2017-11-11 01:52:01 | [diff] [blame] | 141 | const char* data_as_byte = reinterpret_cast<const char*>(data_as_void); |
Christopher Cameron | 3bc3d5e | 2017-11-23 03:48:11 | [diff] [blame] | 142 | std::vector<char> data(data_as_byte, data_as_byte + size); |
Reilly Grant | c46032eb | 2017-08-30 22:53:52 | [diff] [blame] | 143 | |
Daniel Bratell | b6c0ab8c | 2017-12-21 13:09:18 | [diff] [blame] | 144 | base::AutoLock lock(g_icc_profile_lock.Get()); |
ccameron | efdab16 | 2016-07-25 23:00:02 | [diff] [blame] | 145 | |
Christopher Cameron | 43f1705 | 2017-11-11 01:52:01 | [diff] [blame] | 146 | // See if there is already an entry with the same data. If so, return that |
Christopher Cameron | 3bc3d5e | 2017-11-23 03:48:11 | [diff] [blame] | 147 | // entry. If not, parse the data. |
| 148 | ICCProfile icc_profile; |
| 149 | auto found_by_data = g_data_to_profile_cache.Get().Get(data); |
| 150 | if (found_by_data != g_data_to_profile_cache.Get().end()) { |
| 151 | icc_profile = found_by_data->second; |
| 152 | } else { |
Christopher Cameron | 20eeb0c | 2019-04-05 23:40:00 | [diff] [blame] | 153 | icc_profile.internals_ = base::MakeRefCounted<Internals>(std::move(data)); |
Christopher Cameron | 43f1705 | 2017-11-11 01:52:01 | [diff] [blame] | 154 | } |
| 155 | |
Christopher Cameron | 3bc3d5e | 2017-11-23 03:48:11 | [diff] [blame] | 156 | // Insert the profile into all caches. |
Christopher Cameron | 3bc3d5e | 2017-11-23 03:48:11 | [diff] [blame] | 157 | g_data_to_profile_cache.Get().Put(icc_profile.internals_->data_, icc_profile); |
Christopher Cameron | deea252 | 2017-11-16 07:10:52 | [diff] [blame] | 158 | |
Christopher Cameron | 3bc3d5e | 2017-11-23 03:48:11 | [diff] [blame] | 159 | return icc_profile; |
ccameron | efdab16 | 2016-07-25 23:00:02 | [diff] [blame] | 160 | } |
| 161 | |
Christopher Cameron | fdc0f87 | 2017-11-09 03:22:16 | [diff] [blame] | 162 | ColorSpace ICCProfile::GetColorSpace() const { |
Zhenyao Mo | 44c8d183 | 2019-12-10 06:43:10 | [diff] [blame] | 163 | if (!internals_ || !internals_->is_valid_) |
Christopher Cameron | 43f1705 | 2017-11-11 01:52:01 | [diff] [blame] | 164 | return ColorSpace(); |
Christopher Cameron | fdc0f87 | 2017-11-09 03:22:16 | [diff] [blame] | 165 | |
Zhenyao Mo | 44c8d183 | 2019-12-10 06:43:10 | [diff] [blame] | 166 | return ColorSpace(ColorSpace::PrimaryID::CUSTOM, |
| 167 | ColorSpace::TransferID::CUSTOM, ColorSpace::MatrixID::RGB, |
| 168 | ColorSpace::RangeID::FULL, &internals_->to_XYZD50_, |
| 169 | &internals_->transfer_fn_); |
Christopher Cameron | 20eeb0c | 2019-04-05 23:40:00 | [diff] [blame] | 170 | } |
| 171 | |
Christopher Cameron | a808f1f | 2019-05-02 20:29:53 | [diff] [blame] | 172 | ColorSpace ICCProfile::GetPrimariesOnlyColorSpace() const { |
Zhenyao Mo | 44c8d183 | 2019-12-10 06:43:10 | [diff] [blame] | 173 | if (!internals_ || !internals_->is_valid_) |
| 174 | return ColorSpace(); |
| 175 | |
Christopher Cameron | 6c9cfff0 | 2022-01-25 01:29:03 | [diff] [blame] | 176 | return ColorSpace(ColorSpace::PrimaryID::CUSTOM, ColorSpace::TransferID::SRGB, |
Zhenyao Mo | 44c8d183 | 2019-12-10 06:43:10 | [diff] [blame] | 177 | ColorSpace::MatrixID::RGB, ColorSpace::RangeID::FULL, |
| 178 | &internals_->to_XYZD50_, nullptr); |
Christopher Cameron | a808f1f | 2019-05-02 20:29:53 | [diff] [blame] | 179 | } |
| 180 | |
Christopher Cameron | 20eeb0c | 2019-04-05 23:40:00 | [diff] [blame] | 181 | bool ICCProfile::IsColorSpaceAccurate() const { |
| 182 | if (!internals_) |
| 183 | return false; |
| 184 | |
| 185 | if (!internals_->is_valid_) |
| 186 | return false; |
| 187 | |
| 188 | return internals_->is_parametric_; |
ccameron | 24c87c3 | 2017-03-14 21:50:42 | [diff] [blame] | 189 | } |
| 190 | |
Christopher Cameron | 3bc3d5e | 2017-11-23 03:48:11 | [diff] [blame] | 191 | // static |
Christopher Cameron | 20eeb0c | 2019-04-05 23:40:00 | [diff] [blame] | 192 | ICCProfile ICCProfile::FromColorSpace(const ColorSpace& color_space) { |
Christopher Cameron | 3bc3d5e | 2017-11-23 03:48:11 | [diff] [blame] | 193 | if (!color_space.IsValid()) { |
| 194 | return ICCProfile(); |
Christopher Cameron | 43f1705 | 2017-11-11 01:52:01 | [diff] [blame] | 195 | } |
Zhenyao Mo | 44c8d183 | 2019-12-10 06:43:10 | [diff] [blame] | 196 | if (color_space.GetMatrixID() != ColorSpace::MatrixID::RGB) { |
Christopher Cameron | deea252 | 2017-11-16 07:10:52 | [diff] [blame] | 197 | DLOG(ERROR) << "Not creating non-RGB ICCProfile"; |
Christopher Cameron | 3bc3d5e | 2017-11-23 03:48:11 | [diff] [blame] | 198 | return ICCProfile(); |
Christopher Cameron | deea252 | 2017-11-16 07:10:52 | [diff] [blame] | 199 | } |
Zhenyao Mo | 44c8d183 | 2019-12-10 06:43:10 | [diff] [blame] | 200 | if (color_space.GetRangeID() != ColorSpace::RangeID::FULL) { |
Christopher Cameron | deea252 | 2017-11-16 07:10:52 | [diff] [blame] | 201 | DLOG(ERROR) << "Not creating non-full-range ICCProfile"; |
Christopher Cameron | 3bc3d5e | 2017-11-23 03:48:11 | [diff] [blame] | 202 | return ICCProfile(); |
| 203 | } |
Brian Osman | b76ea80 | 2019-02-01 19:06:23 | [diff] [blame] | 204 | skcms_Matrix3x3 to_XYZD50_matrix; |
Christopher Cameron | 3bc3d5e | 2017-11-23 03:48:11 | [diff] [blame] | 205 | color_space.GetPrimaryMatrix(&to_XYZD50_matrix); |
Brian Osman | b76ea80 | 2019-02-01 19:06:23 | [diff] [blame] | 206 | skcms_TransferFunction fn; |
Christopher Cameron | 3bc3d5e | 2017-11-23 03:48:11 | [diff] [blame] | 207 | if (!color_space.GetTransferFunction(&fn)) { |
Christopher Cameron | deea252 | 2017-11-16 07:10:52 | [diff] [blame] | 208 | DLOG(ERROR) << "Failed to get ColorSpace transfer function for ICCProfile."; |
Christopher Cameron | 3bc3d5e | 2017-11-23 03:48:11 | [diff] [blame] | 209 | return ICCProfile(); |
Christopher Cameron | deea252 | 2017-11-16 07:10:52 | [diff] [blame] | 210 | } |
Brian Osman | b76ea80 | 2019-02-01 19:06:23 | [diff] [blame] | 211 | sk_sp<SkData> data = SkWriteICCProfile(fn, to_XYZD50_matrix); |
Christopher Cameron | deea252 | 2017-11-16 07:10:52 | [diff] [blame] | 212 | if (!data) { |
| 213 | DLOG(ERROR) << "Failed to create SkICC."; |
Christopher Cameron | 3bc3d5e | 2017-11-23 03:48:11 | [diff] [blame] | 214 | return ICCProfile(); |
Christopher Cameron | deea252 | 2017-11-16 07:10:52 | [diff] [blame] | 215 | } |
Christopher Cameron | 20eeb0c | 2019-04-05 23:40:00 | [diff] [blame] | 216 | return FromData(data->data(), data->size()); |
Christopher Cameron | 3bc3d5e | 2017-11-23 03:48:11 | [diff] [blame] | 217 | } |
| 218 | |
Christopher Cameron | 20eeb0c | 2019-04-05 23:40:00 | [diff] [blame] | 219 | ICCProfile::Internals::Internals(std::vector<char> data) |
| 220 | : data_(std::move(data)) { |
Christopher Cameron | a4adb41b | 2017-07-13 06:44:25 | [diff] [blame] | 221 | // Parse the ICC profile |
Brian Osman | f836de5 | 2020-03-04 02:13:01 | [diff] [blame] | 222 | Initialize(); |
Christopher Cameron | 374b6c4 | 2017-08-31 22:21:23 | [diff] [blame] | 223 | } |
| 224 | |
Christopher Cameron | 43f1705 | 2017-11-11 01:52:01 | [diff] [blame] | 225 | ICCProfile::Internals::~Internals() {} |
| 226 | |
ccameron | efdab16 | 2016-07-25 23:00:02 | [diff] [blame] | 227 | } // namespace gfx |