blob: 9942406a0eff9b358cefb4f8cbc3a5e522c03f07 [file] [log] [blame]
 /* * Copyright 2016 Google Inc. * * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #include "SkColorSpace.h" #include "SkColorSpace_Base.h" #include "SkColorSpace_XYZ.h" #include "SkColorSpacePriv.h" #include "SkOnce.h" #include "SkPoint3.h" bool SkColorSpacePrimaries::toXYZD50(SkMatrix44* toXYZ_D50) const { if (!is_zero_to_one(fRX) || !is_zero_to_one(fRY) || !is_zero_to_one(fGX) || !is_zero_to_one(fGY) || !is_zero_to_one(fBX) || !is_zero_to_one(fBY) || !is_zero_to_one(fWX) || !is_zero_to_one(fWY)) { return false; } // First, we need to convert xy values (primaries) to XYZ. SkMatrix primaries; primaries.setAll( fRX, fGX, fBX, fRY, fGY, fBY, 1.0f - fRX - fRY, 1.0f - fGX - fGY, 1.0f - fBX - fBY); SkMatrix primariesInv; if (!primaries.invert(&primariesInv)) { return false; } // Assumes that Y is 1.0f. SkVector3 wXYZ = SkVector3::Make(fWX / fWY, 1.0f, (1.0f - fWX - fWY) / fWY); SkVector3 XYZ; XYZ.fX = primariesInv[0] * wXYZ.fX + primariesInv[1] * wXYZ.fY + primariesInv[2] * wXYZ.fZ; XYZ.fY = primariesInv[3] * wXYZ.fX + primariesInv[4] * wXYZ.fY + primariesInv[5] * wXYZ.fZ; XYZ.fZ = primariesInv[6] * wXYZ.fX + primariesInv[7] * wXYZ.fY + primariesInv[8] * wXYZ.fZ; SkMatrix toXYZ; toXYZ.setAll(XYZ.fX, 0.0f, 0.0f, 0.0f, XYZ.fY, 0.0f, 0.0f, 0.0f, XYZ.fZ); toXYZ.postConcat(primaries); // Now convert toXYZ matrix to toXYZD50. SkVector3 wXYZD50 = SkVector3::Make(0.96422f, 1.0f, 0.82521f); // Calculate the chromatic adaptation matrix. We will use the Bradford method, thus // the matrices below. The Bradford method is used by Adobe and is widely considered // to be the best. SkMatrix mA, mAInv; mA.setAll(+0.8951f, +0.2664f, -0.1614f, -0.7502f, +1.7135f, +0.0367f, +0.0389f, -0.0685f, +1.0296f); mAInv.setAll(+0.9869929f, -0.1470543f, +0.1599627f, +0.4323053f, +0.5183603f, +0.0492912f, -0.0085287f, +0.0400428f, +0.9684867f); SkVector3 srcCone; srcCone.fX = mA[0] * wXYZ.fX + mA[1] * wXYZ.fY + mA[2] * wXYZ.fZ; srcCone.fY = mA[3] * wXYZ.fX + mA[4] * wXYZ.fY + mA[5] * wXYZ.fZ; srcCone.fZ = mA[6] * wXYZ.fX + mA[7] * wXYZ.fY + mA[8] * wXYZ.fZ; SkVector3 dstCone; dstCone.fX = mA[0] * wXYZD50.fX + mA[1] * wXYZD50.fY + mA[2] * wXYZD50.fZ; dstCone.fY = mA[3] * wXYZD50.fX + mA[4] * wXYZD50.fY + mA[5] * wXYZD50.fZ; dstCone.fZ = mA[6] * wXYZD50.fX + mA[7] * wXYZD50.fY + mA[8] * wXYZD50.fZ; SkMatrix DXToD50; DXToD50.setIdentity(); DXToD50[0] = dstCone.fX / srcCone.fX; DXToD50[4] = dstCone.fY / srcCone.fY; DXToD50[8] = dstCone.fZ / srcCone.fZ; DXToD50.postConcat(mAInv); DXToD50.preConcat(mA); toXYZ.postConcat(DXToD50); toXYZ_D50->set3x3(toXYZ[0], toXYZ[3], toXYZ[6], toXYZ[1], toXYZ[4], toXYZ[7], toXYZ[2], toXYZ[5], toXYZ[8]); return true; } /////////////////////////////////////////////////////////////////////////////////////////////////// SkColorSpace_Base::SkColorSpace_Base(sk_sp profileData) : fProfileData(std::move(profileData)) {} /** * Checks if our toXYZ matrix is a close match to a known color gamut. * * @param toXYZD50 transformation matrix deduced from profile data * @param standard 3x3 canonical transformation matrix */ static bool xyz_almost_equal(const SkMatrix44& toXYZD50, const float* standard) { return color_space_almost_equal(toXYZD50.getFloat(0, 0), standard[0]) && color_space_almost_equal(toXYZD50.getFloat(0, 1), standard[1]) && color_space_almost_equal(toXYZD50.getFloat(0, 2), standard[2]) && color_space_almost_equal(toXYZD50.getFloat(1, 0), standard[3]) && color_space_almost_equal(toXYZD50.getFloat(1, 1), standard[4]) && color_space_almost_equal(toXYZD50.getFloat(1, 2), standard[5]) && color_space_almost_equal(toXYZD50.getFloat(2, 0), standard[6]) && color_space_almost_equal(toXYZD50.getFloat(2, 1), standard[7]) && color_space_almost_equal(toXYZD50.getFloat(2, 2), standard[8]) && color_space_almost_equal(toXYZD50.getFloat(0, 3), 0.0f) && color_space_almost_equal(toXYZD50.getFloat(1, 3), 0.0f) && color_space_almost_equal(toXYZD50.getFloat(2, 3), 0.0f) && color_space_almost_equal(toXYZD50.getFloat(3, 0), 0.0f) && color_space_almost_equal(toXYZD50.getFloat(3, 1), 0.0f) && color_space_almost_equal(toXYZD50.getFloat(3, 2), 0.0f) && color_space_almost_equal(toXYZD50.getFloat(3, 3), 1.0f); } sk_sp SkColorSpace_Base::MakeRGB(SkGammaNamed gammaNamed, const SkMatrix44& toXYZD50) { switch (gammaNamed) { case kSRGB_SkGammaNamed: if (xyz_almost_equal(toXYZD50, gSRGB_toXYZD50)) { return SkColorSpace_Base::MakeNamed(kSRGB_Named); } break; case k2Dot2Curve_SkGammaNamed: if (xyz_almost_equal(toXYZD50, gAdobeRGB_toXYZD50)) { return SkColorSpace_Base::MakeNamed(kAdobeRGB_Named); } break; case kLinear_SkGammaNamed: if (xyz_almost_equal(toXYZD50, gSRGB_toXYZD50)) { return SkColorSpace_Base::MakeNamed(kSRGBLinear_Named); } break; case kNonStandard_SkGammaNamed: // This is not allowed. return nullptr; default: break; } return sk_sp(new SkColorSpace_XYZ(gammaNamed, toXYZD50)); } sk_sp SkColorSpace::MakeRGB(RenderTargetGamma gamma, const SkMatrix44& toXYZD50) { switch (gamma) { case kLinear_RenderTargetGamma: return SkColorSpace_Base::MakeRGB(kLinear_SkGammaNamed, toXYZD50); case kSRGB_RenderTargetGamma: return SkColorSpace_Base::MakeRGB(kSRGB_SkGammaNamed, toXYZD50); default: return nullptr; } } sk_sp SkColorSpace::MakeRGB(const SkColorSpaceTransferFn& coeffs, const SkMatrix44& toXYZD50) { if (!is_valid_transfer_fn(coeffs)) { return nullptr; } if (is_almost_srgb(coeffs)) { return SkColorSpace::MakeRGB(kSRGB_RenderTargetGamma, toXYZD50); } if (is_almost_2dot2(coeffs)) { return SkColorSpace_Base::MakeRGB(k2Dot2Curve_SkGammaNamed, toXYZD50); } if (is_almost_linear(coeffs)) { return SkColorSpace_Base::MakeRGB(kLinear_SkGammaNamed, toXYZD50); } void* memory = sk_malloc_throw(sizeof(SkGammas) + sizeof(SkColorSpaceTransferFn)); sk_sp gammas = sk_sp(new (memory) SkGammas(3)); SkColorSpaceTransferFn* fn = SkTAddOffset(memory, sizeof(SkGammas)); *fn = coeffs; SkGammas::Data data; data.fParamOffset = 0; for (int channel = 0; channel < 3; ++channel) { gammas->fType[channel] = SkGammas::Type::kParam_Type; gammas->fData[channel] = data; } return sk_sp(new SkColorSpace_XYZ(kNonStandard_SkGammaNamed, std::move(gammas), toXYZD50, nullptr)); } sk_sp SkColorSpace::MakeRGB(RenderTargetGamma gamma, Gamut gamut) { SkMatrix44 toXYZD50(SkMatrix44::kUninitialized_Constructor); to_xyz_d50(&toXYZD50, gamut); return SkColorSpace::MakeRGB(gamma, toXYZD50); } sk_sp SkColorSpace::MakeRGB(const SkColorSpaceTransferFn& coeffs, Gamut gamut) { SkMatrix44 toXYZD50(SkMatrix44::kUninitialized_Constructor); to_xyz_d50(&toXYZD50, gamut); return SkColorSpace::MakeRGB(coeffs, toXYZD50); } static SkColorSpace* gAdobeRGB; static SkColorSpace* gSRGB; static SkColorSpace* gSRGBLinear; sk_sp SkColorSpace_Base::MakeNamed(Named named) { static SkOnce sRGBOnce; static SkOnce adobeRGBOnce; static SkOnce sRGBLinearOnce; switch (named) { case kSRGB_Named: { sRGBOnce([] { SkMatrix44 srgbToxyzD50(SkMatrix44::kUninitialized_Constructor); srgbToxyzD50.set3x3RowMajorf(gSRGB_toXYZD50); // Force the mutable type mask to be computed. This avoids races. (void)srgbToxyzD50.getType(); gSRGB = new SkColorSpace_XYZ(kSRGB_SkGammaNamed, srgbToxyzD50); }); return sk_ref_sp(gSRGB); } case kAdobeRGB_Named: { adobeRGBOnce([] { SkMatrix44 adobergbToxyzD50(SkMatrix44::kUninitialized_Constructor); adobergbToxyzD50.set3x3RowMajorf(gAdobeRGB_toXYZD50); // Force the mutable type mask to be computed. This avoids races. (void)adobergbToxyzD50.getType(); gAdobeRGB = new SkColorSpace_XYZ(k2Dot2Curve_SkGammaNamed, adobergbToxyzD50); }); return sk_ref_sp(gAdobeRGB); } case kSRGBLinear_Named: { sRGBLinearOnce([] { SkMatrix44 srgbToxyzD50(SkMatrix44::kUninitialized_Constructor); srgbToxyzD50.set3x3RowMajorf(gSRGB_toXYZD50); // Force the mutable type mask to be computed. This avoids races. (void)srgbToxyzD50.getType(); gSRGBLinear = new SkColorSpace_XYZ(kLinear_SkGammaNamed, srgbToxyzD50); }); return sk_ref_sp(gSRGBLinear); } default: break; } return nullptr; } sk_sp SkColorSpace::MakeSRGB() { return SkColorSpace_Base::MakeNamed(SkColorSpace_Base::kSRGB_Named); } sk_sp SkColorSpace::MakeSRGBLinear() { return SkColorSpace_Base::MakeNamed(SkColorSpace_Base::kSRGBLinear_Named); } /////////////////////////////////////////////////////////////////////////////////////////////////// bool SkColorSpace::gammaCloseToSRGB() const { return as_CSB(this)->onGammaCloseToSRGB(); } bool SkColorSpace::gammaIsLinear() const { return as_CSB(this)->onGammaIsLinear(); } bool SkColorSpace::isNumericalTransferFn(SkColorSpaceTransferFn* fn) const { return as_CSB(this)->onIsNumericalTransferFn(fn); } bool SkColorSpace::toXYZD50(SkMatrix44* toXYZD50) const { const SkMatrix44* matrix = as_CSB(this)->toXYZD50(); if (matrix) { *toXYZD50 = *matrix; return true; } return false; } bool SkColorSpace::isSRGB() const { return gSRGB == this; } /////////////////////////////////////////////////////////////////////////////////////////////////// enum Version { k0_Version, // Initial version, header + flags for matrix and profile }; struct ColorSpaceHeader { /** * It is only valid to set zero or one flags. * Setting multiple flags is invalid. */ /** * If kMatrix_Flag is set, we will write 12 floats after the header. */ static constexpr uint8_t kMatrix_Flag = 1 << 0; /** * If kICC_Flag is set, we will write an ICC profile after the header. * The ICC profile will be written as a uint32 size, followed immediately * by the data (padded to 4 bytes). */ static constexpr uint8_t kICC_Flag = 1 << 1; /** * If kTransferFn_Flag is set, we will write 19 floats after the header. * The first seven represent the transfer fn, and the next twelve are the * matrix. */ static constexpr uint8_t kTransferFn_Flag = 1 << 3; static ColorSpaceHeader Pack(Version version, uint8_t named, uint8_t gammaNamed, uint8_t flags) { ColorSpaceHeader header; SkASSERT(k0_Version == version); header.fVersion = (uint8_t) version; SkASSERT(named <= SkColorSpace_Base::kSRGBLinear_Named); header.fNamed = (uint8_t) named; SkASSERT(gammaNamed <= kNonStandard_SkGammaNamed); header.fGammaNamed = (uint8_t) gammaNamed; SkASSERT(flags <= kTransferFn_Flag); header.fFlags = flags; return header; } uint8_t fVersion; // Always zero uint8_t fNamed; // Must be a SkColorSpace::Named uint8_t fGammaNamed; // Must be a SkGammaNamed uint8_t fFlags; }; size_t SkColorSpace::writeToMemory(void* memory) const { // Start by trying the serialization fast path. If we haven't saved ICC profile data, // we must have a profile that we can serialize easily. if (!as_CSB(this)->fProfileData) { // Profile data is mandatory for A2B0 color spaces. SkASSERT(SkColorSpace_Base::Type::kXYZ == as_CSB(this)->type()); const SkColorSpace_XYZ* thisXYZ = static_cast(this); // If we have a named profile, only write the enum. const SkGammaNamed gammaNamed = thisXYZ->gammaNamed(); if (this == gSRGB) { if (memory) { *((ColorSpaceHeader*) memory) = ColorSpaceHeader::Pack( k0_Version, SkColorSpace_Base::kSRGB_Named, gammaNamed, 0); } return sizeof(ColorSpaceHeader); } else if (this == gAdobeRGB) { if (memory) { *((ColorSpaceHeader*) memory) = ColorSpaceHeader::Pack( k0_Version, SkColorSpace_Base::kAdobeRGB_Named, gammaNamed, 0); } return sizeof(ColorSpaceHeader); } else if (this == gSRGBLinear) { if (memory) { *((ColorSpaceHeader*) memory) = ColorSpaceHeader::Pack( k0_Version, SkColorSpace_Base::kSRGBLinear_Named, gammaNamed, 0); } return sizeof(ColorSpaceHeader); } // If we have a named gamma, write the enum and the matrix. switch (gammaNamed) { case kSRGB_SkGammaNamed: case k2Dot2Curve_SkGammaNamed: case kLinear_SkGammaNamed: { if (memory) { *((ColorSpaceHeader*) memory) = ColorSpaceHeader::Pack(k0_Version, 0, gammaNamed, ColorSpaceHeader::kMatrix_Flag); memory = SkTAddOffset(memory, sizeof(ColorSpaceHeader)); thisXYZ->toXYZD50()->as3x4RowMajorf((float*) memory); } return sizeof(ColorSpaceHeader) + 12 * sizeof(float); } default: { const SkGammas* gammas = thisXYZ->gammas(); SkASSERT(gammas); SkASSERT(gammas->isParametric(0)); SkASSERT(gammas->isParametric(1)); SkASSERT(gammas->isParametric(2)); SkASSERT(gammas->data(0) == gammas->data(1)); SkASSERT(gammas->data(0) == gammas->data(2)); if (memory) { *((ColorSpaceHeader*) memory) = ColorSpaceHeader::Pack(k0_Version, 0, thisXYZ->fGammaNamed, ColorSpaceHeader::kTransferFn_Flag); memory = SkTAddOffset(memory, sizeof(ColorSpaceHeader)); *(((float*) memory) + 0) = gammas->params(0).fA; *(((float*) memory) + 1) = gammas->params(0).fB; *(((float*) memory) + 2) = gammas->params(0).fC; *(((float*) memory) + 3) = gammas->params(0).fD; *(((float*) memory) + 4) = gammas->params(0).fE; *(((float*) memory) + 5) = gammas->params(0).fF; *(((float*) memory) + 6) = gammas->params(0).fG; memory = SkTAddOffset(memory, 7 * sizeof(float)); thisXYZ->fToXYZD50.as3x4RowMajorf((float*) memory); } return sizeof(ColorSpaceHeader) + 19 * sizeof(float); } } } // Otherwise, serialize the ICC data. size_t profileSize = as_CSB(this)->fProfileData->size(); if (SkAlign4(profileSize) != (uint32_t) SkAlign4(profileSize)) { return 0; } if (memory) { *((ColorSpaceHeader*) memory) = ColorSpaceHeader::Pack(k0_Version, 0, kNonStandard_SkGammaNamed, ColorSpaceHeader::kICC_Flag); memory = SkTAddOffset(memory, sizeof(ColorSpaceHeader)); *((uint32_t*) memory) = (uint32_t) SkAlign4(profileSize); memory = SkTAddOffset(memory, sizeof(uint32_t)); memcpy(memory, as_CSB(this)->fProfileData->data(), profileSize); memset(SkTAddOffset(memory, profileSize), 0, SkAlign4(profileSize) - profileSize); } return sizeof(ColorSpaceHeader) + sizeof(uint32_t) + SkAlign4(profileSize); } sk_sp SkColorSpace::serialize() const { size_t size = this->writeToMemory(nullptr); if (0 == size) { return nullptr; } sk_sp data = SkData::MakeUninitialized(size); this->writeToMemory(data->writable_data()); return data; } sk_sp SkColorSpace::Deserialize(const void* data, size_t length) { if (length < sizeof(ColorSpaceHeader)) { return nullptr; } ColorSpaceHeader header = *((const ColorSpaceHeader*) data); data = SkTAddOffset(data, sizeof(ColorSpaceHeader)); length -= sizeof(ColorSpaceHeader); if (0 == header.fFlags) { return SkColorSpace_Base::MakeNamed((SkColorSpace_Base::Named) header.fNamed); } switch ((SkGammaNamed) header.fGammaNamed) { case kSRGB_SkGammaNamed: case k2Dot2Curve_SkGammaNamed: case kLinear_SkGammaNamed: { if (ColorSpaceHeader::kMatrix_Flag != header.fFlags || length < 12 * sizeof(float)) { return nullptr; } SkMatrix44 toXYZ(SkMatrix44::kUninitialized_Constructor); toXYZ.set3x4RowMajorf((const float*) data); return SkColorSpace_Base::MakeRGB((SkGammaNamed) header.fGammaNamed, toXYZ); } default: break; } switch (header.fFlags) { case ColorSpaceHeader::kICC_Flag: { if (length < sizeof(uint32_t)) { return nullptr; } uint32_t profileSize = *((uint32_t*) data); data = SkTAddOffset(data, sizeof(uint32_t)); length -= sizeof(uint32_t); if (length < profileSize) { return nullptr; } return MakeICC(data, profileSize); } case ColorSpaceHeader::kTransferFn_Flag: { if (length < 19 * sizeof(float)) { return nullptr; } SkColorSpaceTransferFn transferFn; transferFn.fA = *(((const float*) data) + 0); transferFn.fB = *(((const float*) data) + 1); transferFn.fC = *(((const float*) data) + 2); transferFn.fD = *(((const float*) data) + 3); transferFn.fE = *(((const float*) data) + 4); transferFn.fF = *(((const float*) data) + 5); transferFn.fG = *(((const float*) data) + 6); data = SkTAddOffset(data, 7 * sizeof(float)); SkMatrix44 toXYZ(SkMatrix44::kUninitialized_Constructor); toXYZ.set3x4RowMajorf((const float*) data); return SkColorSpace::MakeRGB(transferFn, toXYZ); } default: return nullptr; } } bool SkColorSpace::Equals(const SkColorSpace* src, const SkColorSpace* dst) { if (src == dst) { return true; } if (!src || !dst) { return false; } SkData* srcData = as_CSB(src)->fProfileData.get(); SkData* dstData = as_CSB(dst)->fProfileData.get(); if (srcData || dstData) { if (srcData && dstData) { return srcData->size() == dstData->size() && 0 == memcmp(srcData->data(), dstData->data(), srcData->size()); } return false; } // profiles are mandatory for A2B0 color spaces SkASSERT(as_CSB(src)->type() == SkColorSpace_Base::Type::kXYZ); const SkColorSpace_XYZ* srcXYZ = static_cast(src); const SkColorSpace_XYZ* dstXYZ = static_cast(dst); if (srcXYZ->gammaNamed() != dstXYZ->gammaNamed()) { return false; } switch (srcXYZ->gammaNamed()) { case kSRGB_SkGammaNamed: case k2Dot2Curve_SkGammaNamed: case kLinear_SkGammaNamed: if (srcXYZ->toXYZD50Hash() == dstXYZ->toXYZD50Hash()) { SkASSERT(*srcXYZ->toXYZD50() == *dstXYZ->toXYZD50() && "Hash collision"); return true; } return false; default: // It is unlikely that we will reach this case. sk_sp serializedSrcData = src->serialize(); sk_sp serializedDstData = dst->serialize(); return serializedSrcData->size() == serializedDstData->size() && 0 == memcmp(serializedSrcData->data(), serializedDstData->data(), serializedSrcData->size()); } } SkColorSpaceTransferFn SkColorSpaceTransferFn::invert() const { // Original equation is: y = (ax + b)^g + e for x >= d // y = cx + f otherwise // // so 1st inverse is: (y - e)^(1/g) = ax + b // x = ((y - e)^(1/g) - b) / a // // which can be re-written as: x = (1/a)(y - e)^(1/g) - b/a // x = ((1/a)^g)^(1/g) * (y - e)^(1/g) - b/a // x = ([(1/a)^g]y + [-((1/a)^g)e]) ^ [1/g] + [-b/a] // // and 2nd inverse is: x = (y - f) / c // which can be re-written as: x = [1/c]y + [-f/c] // // and now both can be expressed in terms of the same parametric form as the // original - parameters are enclosed in square brackets. SkColorSpaceTransferFn inv = { 0, 0, 0, 0, 0, 0, 0 }; // find inverse for linear segment (if possible) if (!transfer_fn_almost_equal(0.f, fC)) { inv.fC = 1.f / fC; inv.fF = -fF / fC; } else { // otherwise assume it should be 0 as it is the lower segment // as y = f is a constant function } // find inverse for the other segment (if possible) if (transfer_fn_almost_equal(0.f, fA) || transfer_fn_almost_equal(0.f, fG)) { // otherwise assume it should be 1 as it is the top segment // as you can't invert the constant functions y = b^g + c, or y = 1 + c inv.fG = 1.f; inv.fE = 1.f; } else { inv.fG = 1.f / fG; inv.fA = powf(1.f / fA, fG); inv.fB = -inv.fA * fE; inv.fE = -fB / fA; } inv.fD = fC * fD + fF; return inv; }