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