1 | /* |
2 | * Copyright 2016 Google Inc. |
3 | * |
4 | * Use of this source code is governed by a BSD-style license that can be |
5 | * found in the LICENSE file. |
6 | */ |
7 | |
8 | #include "include/core/SkColorSpace.h" |
9 | #include "include/core/SkData.h" |
10 | #include "include/third_party/skcms/skcms.h" |
11 | #include "src/core/SkColorSpacePriv.h" |
12 | #include "src/core/SkOpts.h" |
13 | |
14 | bool SkColorSpacePrimaries::toXYZD50(skcms_Matrix3x3* toXYZ_D50) const { |
15 | return skcms_PrimariesToXYZD50(fRX, fRY, fGX, fGY, fBX, fBY, fWX, fWY, toXYZ_D50); |
16 | } |
17 | |
18 | SkColorSpace::SkColorSpace(const skcms_TransferFunction& transferFn, |
19 | const skcms_Matrix3x3& toXYZD50) |
20 | : fTransferFn(transferFn) |
21 | , fToXYZD50(toXYZD50) { |
22 | fTransferFnHash = SkOpts::hash_fn(&fTransferFn, 7*sizeof(float), 0); |
23 | fToXYZD50Hash = SkOpts::hash_fn(&fToXYZD50, 9*sizeof(float), 0); |
24 | } |
25 | |
26 | static bool xyz_almost_equal(const skcms_Matrix3x3& mA, const skcms_Matrix3x3& mB) { |
27 | for (int r = 0; r < 3; ++r) { |
28 | for (int c = 0; c < 3; ++c) { |
29 | if (!color_space_almost_equal(mA.vals[r][c], mB.vals[r][c])) { |
30 | return false; |
31 | } |
32 | } |
33 | } |
34 | |
35 | return true; |
36 | } |
37 | |
38 | sk_sp<SkColorSpace> SkColorSpace::MakeRGB(const skcms_TransferFunction& transferFn, |
39 | const skcms_Matrix3x3& toXYZ) { |
40 | if (classify_transfer_fn(transferFn) == Bad_TF) { |
41 | return nullptr; |
42 | } |
43 | |
44 | const skcms_TransferFunction* tf = &transferFn; |
45 | |
46 | if (is_almost_srgb(transferFn)) { |
47 | if (xyz_almost_equal(toXYZ, SkNamedGamut::kSRGB)) { |
48 | return SkColorSpace::MakeSRGB(); |
49 | } |
50 | tf = &SkNamedTransferFn::kSRGB; |
51 | } else if (is_almost_2dot2(transferFn)) { |
52 | tf = &SkNamedTransferFn::k2Dot2; |
53 | } else if (is_almost_linear(transferFn)) { |
54 | if (xyz_almost_equal(toXYZ, SkNamedGamut::kSRGB)) { |
55 | return SkColorSpace::MakeSRGBLinear(); |
56 | } |
57 | tf = &SkNamedTransferFn::kLinear; |
58 | } |
59 | |
60 | return sk_sp<SkColorSpace>(new SkColorSpace(*tf, toXYZ)); |
61 | } |
62 | |
63 | class SkColorSpaceSingletonFactory { |
64 | public: |
65 | static SkColorSpace* Make(const skcms_TransferFunction& transferFn, |
66 | const skcms_Matrix3x3& to_xyz) { |
67 | return new SkColorSpace(transferFn, to_xyz); |
68 | } |
69 | }; |
70 | |
71 | SkColorSpace* sk_srgb_singleton() { |
72 | static SkColorSpace* cs = SkColorSpaceSingletonFactory::Make(SkNamedTransferFn::kSRGB, |
73 | SkNamedGamut::kSRGB); |
74 | return cs; |
75 | } |
76 | |
77 | SkColorSpace* sk_srgb_linear_singleton() { |
78 | static SkColorSpace* cs = SkColorSpaceSingletonFactory::Make(SkNamedTransferFn::kLinear, |
79 | SkNamedGamut::kSRGB); |
80 | return cs; |
81 | } |
82 | |
83 | sk_sp<SkColorSpace> SkColorSpace::MakeSRGB() { |
84 | return sk_ref_sp(sk_srgb_singleton()); |
85 | } |
86 | |
87 | sk_sp<SkColorSpace> SkColorSpace::MakeSRGBLinear() { |
88 | return sk_ref_sp(sk_srgb_linear_singleton()); |
89 | } |
90 | |
91 | void SkColorSpace::computeLazyDstFields() const { |
92 | fLazyDstFieldsOnce([this] { |
93 | |
94 | // Invert 3x3 gamut, defaulting to sRGB if we can't. |
95 | { |
96 | if (!skcms_Matrix3x3_invert(&fToXYZD50, &fFromXYZD50)) { |
97 | SkAssertResult(skcms_Matrix3x3_invert(&skcms_sRGB_profile()->toXYZD50, |
98 | &fFromXYZD50)); |
99 | } |
100 | } |
101 | |
102 | // Invert transfer function, defaulting to sRGB if we can't. |
103 | { |
104 | if (!skcms_TransferFunction_invert(&fTransferFn, &fInvTransferFn)) { |
105 | fInvTransferFn = *skcms_sRGB_Inverse_TransferFunction(); |
106 | } |
107 | } |
108 | |
109 | }); |
110 | } |
111 | |
112 | bool SkColorSpace::isNumericalTransferFn(skcms_TransferFunction* coeffs) const { |
113 | // TODO: Change transferFn/invTransferFn to just operate on skcms_TransferFunction (all callers |
114 | // already pass pointers to an skcms struct). Then remove this function, and update the two |
115 | // remaining callers to do the right thing with transferFn and classify. |
116 | this->transferFn(coeffs); |
117 | return classify_transfer_fn(*coeffs) == sRGBish_TF; |
118 | } |
119 | |
120 | void SkColorSpace::transferFn(float gabcdef[7]) const { |
121 | memcpy(gabcdef, &fTransferFn, 7*sizeof(float)); |
122 | } |
123 | |
124 | void SkColorSpace::transferFn(skcms_TransferFunction* fn) const { |
125 | *fn = fTransferFn; |
126 | } |
127 | |
128 | void SkColorSpace::invTransferFn(skcms_TransferFunction* fn) const { |
129 | this->computeLazyDstFields(); |
130 | *fn = fInvTransferFn; |
131 | } |
132 | |
133 | bool SkColorSpace::toXYZD50(skcms_Matrix3x3* toXYZD50) const { |
134 | *toXYZD50 = fToXYZD50; |
135 | return true; |
136 | } |
137 | |
138 | void SkColorSpace::gamutTransformTo(const SkColorSpace* dst, skcms_Matrix3x3* src_to_dst) const { |
139 | dst->computeLazyDstFields(); |
140 | *src_to_dst = skcms_Matrix3x3_concat(&dst->fFromXYZD50, &fToXYZD50); |
141 | } |
142 | |
143 | bool SkColorSpace::isSRGB() const { |
144 | return sk_srgb_singleton() == this; |
145 | } |
146 | |
147 | bool SkColorSpace::gammaCloseToSRGB() const { |
148 | // Nearly-equal transfer functions were snapped at construction time, so just do an exact test |
149 | return memcmp(&fTransferFn, &SkNamedTransferFn::kSRGB, 7*sizeof(float)) == 0; |
150 | } |
151 | |
152 | bool SkColorSpace::gammaIsLinear() const { |
153 | // Nearly-equal transfer functions were snapped at construction time, so just do an exact test |
154 | return memcmp(&fTransferFn, &SkNamedTransferFn::kLinear, 7*sizeof(float)) == 0; |
155 | } |
156 | |
157 | sk_sp<SkColorSpace> SkColorSpace::makeLinearGamma() const { |
158 | if (this->gammaIsLinear()) { |
159 | return sk_ref_sp(const_cast<SkColorSpace*>(this)); |
160 | } |
161 | return SkColorSpace::MakeRGB(SkNamedTransferFn::kLinear, fToXYZD50); |
162 | } |
163 | |
164 | sk_sp<SkColorSpace> SkColorSpace::makeSRGBGamma() const { |
165 | if (this->gammaCloseToSRGB()) { |
166 | return sk_ref_sp(const_cast<SkColorSpace*>(this)); |
167 | } |
168 | return SkColorSpace::MakeRGB(SkNamedTransferFn::kSRGB, fToXYZD50); |
169 | } |
170 | |
171 | sk_sp<SkColorSpace> SkColorSpace::makeColorSpin() const { |
172 | skcms_Matrix3x3 spin = {{ |
173 | { 0, 0, 1 }, |
174 | { 1, 0, 0 }, |
175 | { 0, 1, 0 }, |
176 | }}; |
177 | |
178 | skcms_Matrix3x3 spun = skcms_Matrix3x3_concat(&fToXYZD50, &spin); |
179 | |
180 | return sk_sp<SkColorSpace>(new SkColorSpace(fTransferFn, spun)); |
181 | } |
182 | |
183 | void SkColorSpace::toProfile(skcms_ICCProfile* profile) const { |
184 | skcms_Init (profile); |
185 | skcms_SetTransferFunction(profile, &fTransferFn); |
186 | skcms_SetXYZD50 (profile, &fToXYZD50); |
187 | } |
188 | |
189 | sk_sp<SkColorSpace> SkColorSpace::Make(const skcms_ICCProfile& profile) { |
190 | // TODO: move below ≈sRGB test? |
191 | if (!profile.has_toXYZD50 || !profile.has_trc) { |
192 | return nullptr; |
193 | } |
194 | |
195 | if (skcms_ApproximatelyEqualProfiles(&profile, skcms_sRGB_profile())) { |
196 | return SkColorSpace::MakeSRGB(); |
197 | } |
198 | |
199 | // TODO: can we save this work and skip lazily inverting the matrix later? |
200 | skcms_Matrix3x3 inv; |
201 | if (!skcms_Matrix3x3_invert(&profile.toXYZD50, &inv)) { |
202 | return nullptr; |
203 | } |
204 | |
205 | // We can't work with tables or mismatched parametric curves, |
206 | // but if they all look close enough to sRGB, that's fine. |
207 | // TODO: should we maybe do this unconditionally to snap near-sRGB parametrics to sRGB? |
208 | const skcms_Curve* trc = profile.trc; |
209 | if (trc[0].table_entries != 0 || |
210 | trc[1].table_entries != 0 || |
211 | trc[2].table_entries != 0 || |
212 | 0 != memcmp(&trc[0].parametric, &trc[1].parametric, sizeof(trc[0].parametric)) || |
213 | 0 != memcmp(&trc[0].parametric, &trc[2].parametric, sizeof(trc[0].parametric))) |
214 | { |
215 | if (skcms_TRCs_AreApproximateInverse(&profile, skcms_sRGB_Inverse_TransferFunction())) { |
216 | return SkColorSpace::MakeRGB(SkNamedTransferFn::kSRGB, profile.toXYZD50); |
217 | } |
218 | return nullptr; |
219 | } |
220 | |
221 | return SkColorSpace::MakeRGB(profile.trc[0].parametric, profile.toXYZD50); |
222 | } |
223 | |
224 | /////////////////////////////////////////////////////////////////////////////////////////////////// |
225 | |
226 | enum Version { |
227 | k0_Version, // Initial version, header + flags for matrix and profile |
228 | k1_Version, // Simple header (version tag) + 16 floats |
229 | |
230 | kCurrent_Version = k1_Version, |
231 | }; |
232 | |
233 | enum NamedColorSpace { |
234 | kSRGB_NamedColorSpace, |
235 | kAdobeRGB_NamedColorSpace, |
236 | kSRGBLinear_NamedColorSpace, |
237 | }; |
238 | |
239 | enum NamedGamma { |
240 | kLinear_NamedGamma, |
241 | kSRGB_NamedGamma, |
242 | k2Dot2_NamedGamma, |
243 | }; |
244 | |
245 | struct { |
246 | // Flag values, only used by old (k0_Version) serialization |
247 | static constexpr uint8_t = 1 << 0; |
248 | static constexpr uint8_t = 1 << 1; |
249 | static constexpr uint8_t = 1 << 3; |
250 | |
251 | uint8_t = kCurrent_Version; |
252 | |
253 | // Other fields are only used by k0_Version. Could be re-purposed in future versions. |
254 | uint8_t = 0; |
255 | uint8_t = 0; |
256 | uint8_t = 0; |
257 | }; |
258 | |
259 | size_t SkColorSpace::writeToMemory(void* memory) const { |
260 | if (memory) { |
261 | *((ColorSpaceHeader*) memory) = ColorSpaceHeader(); |
262 | memory = SkTAddOffset<void>(memory, sizeof(ColorSpaceHeader)); |
263 | |
264 | memcpy(memory, &fTransferFn, 7 * sizeof(float)); |
265 | memory = SkTAddOffset<void>(memory, 7 * sizeof(float)); |
266 | |
267 | memcpy(memory, &fToXYZD50, 9 * sizeof(float)); |
268 | } |
269 | |
270 | return sizeof(ColorSpaceHeader) + 16 * sizeof(float); |
271 | } |
272 | |
273 | sk_sp<SkData> SkColorSpace::serialize() const { |
274 | sk_sp<SkData> data = SkData::MakeUninitialized(this->writeToMemory(nullptr)); |
275 | this->writeToMemory(data->writable_data()); |
276 | return data; |
277 | } |
278 | |
279 | sk_sp<SkColorSpace> SkColorSpace::Deserialize(const void* data, size_t length) { |
280 | if (length < sizeof(ColorSpaceHeader)) { |
281 | return nullptr; |
282 | } |
283 | |
284 | ColorSpaceHeader = *((const ColorSpaceHeader*) data); |
285 | data = SkTAddOffset<const void>(data, sizeof(ColorSpaceHeader)); |
286 | length -= sizeof(ColorSpaceHeader); |
287 | if (k1_Version == header.fVersion) { |
288 | if (length < 16 * sizeof(float)) { |
289 | return nullptr; |
290 | } |
291 | |
292 | skcms_TransferFunction transferFn; |
293 | memcpy(&transferFn, data, 7 * sizeof(float)); |
294 | data = SkTAddOffset<const void>(data, 7 * sizeof(float)); |
295 | |
296 | skcms_Matrix3x3 toXYZ; |
297 | memcpy(&toXYZ, data, 9 * sizeof(float)); |
298 | return SkColorSpace::MakeRGB(transferFn, toXYZ); |
299 | } else if (k0_Version == header.fVersion) { |
300 | if (0 == header.fFlags) { |
301 | switch ((NamedColorSpace)header.fNamed) { |
302 | case kSRGB_NamedColorSpace: |
303 | return SkColorSpace::MakeSRGB(); |
304 | case kSRGBLinear_NamedColorSpace: |
305 | return SkColorSpace::MakeSRGBLinear(); |
306 | case kAdobeRGB_NamedColorSpace: |
307 | return SkColorSpace::MakeRGB(SkNamedTransferFn::k2Dot2, |
308 | SkNamedGamut::kAdobeRGB); |
309 | } |
310 | } |
311 | |
312 | auto make_named_tf = [=](const skcms_TransferFunction& tf) { |
313 | if (ColorSpaceHeader::kMatrix_Flag != header.fFlags || length < 12 * sizeof(float)) { |
314 | return sk_sp<SkColorSpace>(nullptr); |
315 | } |
316 | |
317 | // Version 0 matrix is row-major 3x4 |
318 | skcms_Matrix3x3 toXYZ; |
319 | memcpy(&toXYZ.vals[0][0], (const float*)data + 0, 3 * sizeof(float)); |
320 | memcpy(&toXYZ.vals[1][0], (const float*)data + 4, 3 * sizeof(float)); |
321 | memcpy(&toXYZ.vals[2][0], (const float*)data + 8, 3 * sizeof(float)); |
322 | return SkColorSpace::MakeRGB(tf, toXYZ); |
323 | }; |
324 | |
325 | switch ((NamedGamma) header.fGammaNamed) { |
326 | case kSRGB_NamedGamma: |
327 | return make_named_tf(SkNamedTransferFn::kSRGB); |
328 | case k2Dot2_NamedGamma: |
329 | return make_named_tf(SkNamedTransferFn::k2Dot2); |
330 | case kLinear_NamedGamma: |
331 | return make_named_tf(SkNamedTransferFn::kLinear); |
332 | default: |
333 | break; |
334 | } |
335 | |
336 | switch (header.fFlags) { |
337 | case ColorSpaceHeader::kICC_Flag: { |
338 | // Deprecated and unsupported code path |
339 | return nullptr; |
340 | } |
341 | case ColorSpaceHeader::kTransferFn_Flag: { |
342 | if (length < 19 * sizeof(float)) { |
343 | return nullptr; |
344 | } |
345 | |
346 | // Version 0 TF is in abcdefg order |
347 | skcms_TransferFunction transferFn; |
348 | transferFn.a = *(((const float*) data) + 0); |
349 | transferFn.b = *(((const float*) data) + 1); |
350 | transferFn.c = *(((const float*) data) + 2); |
351 | transferFn.d = *(((const float*) data) + 3); |
352 | transferFn.e = *(((const float*) data) + 4); |
353 | transferFn.f = *(((const float*) data) + 5); |
354 | transferFn.g = *(((const float*) data) + 6); |
355 | data = SkTAddOffset<const void>(data, 7 * sizeof(float)); |
356 | |
357 | // Version 0 matrix is row-major 3x4 |
358 | skcms_Matrix3x3 toXYZ; |
359 | memcpy(&toXYZ.vals[0][0], (const float*)data + 0, 3 * sizeof(float)); |
360 | memcpy(&toXYZ.vals[1][0], (const float*)data + 4, 3 * sizeof(float)); |
361 | memcpy(&toXYZ.vals[2][0], (const float*)data + 8, 3 * sizeof(float)); |
362 | return SkColorSpace::MakeRGB(transferFn, toXYZ); |
363 | } |
364 | default: |
365 | return nullptr; |
366 | } |
367 | } else { |
368 | return nullptr; |
369 | } |
370 | } |
371 | |
372 | bool SkColorSpace::Equals(const SkColorSpace* x, const SkColorSpace* y) { |
373 | if (x == y) { |
374 | return true; |
375 | } |
376 | |
377 | if (!x || !y) { |
378 | return false; |
379 | } |
380 | |
381 | if (x->hash() == y->hash()) { |
382 | for (int i = 0; i < 7; i++) { |
383 | SkASSERT((&x->fTransferFn.g)[i] == (&y->fTransferFn.g)[i] && "Hash collsion" ); |
384 | } |
385 | for (int r = 0; r < 3; r++) { |
386 | for (int c = 0; c < 3; ++c) { |
387 | SkASSERT(x->fToXYZD50.vals[r][c] == y->fToXYZD50.vals[r][c] && "Hash collsion" ); |
388 | } |
389 | } |
390 | return true; |
391 | } |
392 | return false; |
393 | } |
394 | |