| 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 | |