From 085cad4abcca4e10dcc6ba95347c378d4c47fb90 Mon Sep 17 00:00:00 2001 From: msarett Date: Tue, 28 Jun 2016 07:16:40 -0700 Subject: Move SkColorSpace ICC parsing/writing code to its own file BUG=skia: GOLD_TRYBOT_URL= https://gold.skia.org/search?issue=2101823003 Review-Url: https://codereview.chromium.org/2101823003 --- gyp/core.gypi | 1 + src/core/SkColorSpace.cpp | 1071 +---------------------------------------- src/core/SkColorSpacePriv.h | 25 + src/core/SkColorSpace_ICC.cpp | 1054 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 1081 insertions(+), 1070 deletions(-) create mode 100644 src/core/SkColorSpacePriv.h create mode 100644 src/core/SkColorSpace_ICC.cpp diff --git a/gyp/core.gypi b/gyp/core.gypi index 7c89960e66..91387a39d0 100644 --- a/gyp/core.gypi +++ b/gyp/core.gypi @@ -76,6 +76,7 @@ '<(skia_src_path)/core/SkColorShader.cpp', '<(skia_src_path)/core/SkColorShader.h', '<(skia_src_path)/core/SkColorSpace.cpp', + '<(skia_src_path)/core/SkColorSpace_ICC.cpp', '<(skia_src_path)/core/SkColorSpaceXform.cpp', '<(skia_src_path)/core/SkColorTable.cpp', '<(skia_src_path)/core/SkComposeShader.cpp', diff --git a/src/core/SkColorSpace.cpp b/src/core/SkColorSpace.cpp index f988c7d323..894c4b99b7 100644 --- a/src/core/SkColorSpace.cpp +++ b/src/core/SkColorSpace.cpp @@ -7,18 +7,8 @@ #include "SkColorSpace.h" #include "SkColorSpace_Base.h" -#include "SkEndian.h" +#include "SkColorSpacePriv.h" #include "SkOnce.h" -#include "SkReadBuffer.h" -#include "SkWriteBuffer.h" - -#define SkColorSpacePrintf(...) - -static bool color_space_almost_equal(float a, float b) { - return SkTAbs(a - b) < 0.01f; -} - -////////////////////////////////////////////////////////////////////////////////////////////////// SkColorSpace::SkColorSpace(GammaNamed gammaNamed, const SkMatrix44& toXYZD50, Named named) : fGammaNamed(gammaNamed) @@ -77,19 +67,6 @@ static bool xyz_almost_equal(const SkMatrix44& toXYZD50, const float* standard) color_space_almost_equal(toXYZD50.getFloat(3, 3), 1.0f); } -static void set_gamma_value(SkGammaCurve* gamma, float value) { - if (color_space_almost_equal(2.2f, value)) { - gamma->fNamed = SkColorSpace::k2Dot2Curve_GammaNamed; - } else if (color_space_almost_equal(1.0f, value)) { - gamma->fNamed = SkColorSpace::kLinear_GammaNamed; - } else if (color_space_almost_equal(0.0f, value)) { - SkColorSpacePrintf("Treating invalid zero gamma as linear."); - gamma->fNamed = SkColorSpace::kLinear_GammaNamed; - } else { - gamma->fValue = value; - } -} - sk_sp SkColorSpace_Base::NewRGB(float values[3], const SkMatrix44& toXYZD50) { SkGammaCurve curves[3]; set_gamma_value(&curves[0], values[0]); @@ -164,1052 +141,6 @@ sk_sp SkColorSpace::NewNamed(Named named) { /////////////////////////////////////////////////////////////////////////////////////////////////// -#include "SkFixed.h" -#include "SkTemplates.h" - -#define return_if_false(pred, msg) \ - do { \ - if (!(pred)) { \ - SkColorSpacePrintf("Invalid ICC Profile: %s.\n", (msg)); \ - return false; \ - } \ - } while (0) - -#define return_null(msg) \ - do { \ - SkColorSpacePrintf("Invalid ICC Profile: %s.\n", (msg)); \ - return nullptr; \ - } while (0) - -static uint16_t read_big_endian_short(const uint8_t* ptr) { - return ptr[0] << 8 | ptr[1]; -} - -static uint32_t read_big_endian_uint(const uint8_t* ptr) { - return ptr[0] << 24 | ptr[1] << 16 | ptr[2] << 8 | ptr[3]; -} - -static int32_t read_big_endian_int(const uint8_t* ptr) { - return (int32_t) read_big_endian_uint(ptr); -} - -// This is equal to the header size according to the ICC specification (128) -// plus the size of the tag count (4). We include the tag count since we -// always require it to be present anyway. -static constexpr size_t kICCHeaderSize = 132; - -// Contains a signature (4), offset (4), and size (4). -static constexpr size_t kICCTagTableEntrySize = 12; - -static constexpr uint32_t kRGB_ColorSpace = SkSetFourByteTag('R', 'G', 'B', ' '); -static constexpr uint32_t kDisplay_Profile = SkSetFourByteTag('m', 'n', 't', 'r'); -static constexpr uint32_t kInput_Profile = SkSetFourByteTag('s', 'c', 'n', 'r'); -static constexpr uint32_t kOutput_Profile = SkSetFourByteTag('p', 'r', 't', 'r'); -static constexpr uint32_t kXYZ_PCSSpace = SkSetFourByteTag('X', 'Y', 'Z', ' '); -static constexpr uint32_t kACSP_Signature = SkSetFourByteTag('a', 'c', 's', 'p'); - -struct ICCProfileHeader { - uint32_t fSize; - - // No reason to care about the preferred color management module (ex: Adobe, Apple, etc.). - // We're always going to use this one. - uint32_t fCMMType_ignored; - - uint32_t fVersion; - uint32_t fProfileClass; - uint32_t fInputColorSpace; - uint32_t fPCS; - uint32_t fDateTime_ignored[3]; - uint32_t fSignature; - - // Indicates the platform that this profile was created for (ex: Apple, Microsoft). This - // doesn't really matter to us. - uint32_t fPlatformTarget_ignored; - - // Flags can indicate: - // (1) Whether this profile was embedded in a file. This flag is consistently wrong. - // Ex: The profile came from a file but indicates that it did not. - // (2) Whether we are allowed to use the profile independently of the color data. If set, - // this may allow us to use the embedded profile for testing separate from the original - // image. - uint32_t fFlags_ignored; - - // We support many output devices. It doesn't make sense to think about the attributes of - // the device in the context of the image profile. - uint32_t fDeviceManufacturer_ignored; - uint32_t fDeviceModel_ignored; - uint32_t fDeviceAttributes_ignored[2]; - - uint32_t fRenderingIntent; - int32_t fIlluminantXYZ[3]; - - // We don't care who created the profile. - uint32_t fCreator_ignored; - - // This is an MD5 checksum. Could be useful for checking if profiles are equal. - uint32_t fProfileId_ignored[4]; - - // Reserved for future use. - uint32_t fReserved_ignored[7]; - - uint32_t fTagCount; - - void init(const uint8_t* src, size_t len) { - SkASSERT(kICCHeaderSize == sizeof(*this)); - - uint32_t* dst = (uint32_t*) this; - for (uint32_t i = 0; i < kICCHeaderSize / 4; i++, src+=4) { - dst[i] = read_big_endian_uint(src); - } - } - - bool valid() const { - return_if_false(fSize >= kICCHeaderSize, "Size is too small"); - - uint8_t majorVersion = fVersion >> 24; - return_if_false(majorVersion <= 4, "Unsupported version"); - - // These are the three basic classes of profiles that we might expect to see embedded - // in images. Four additional classes exist, but they generally are used as a convenient - // way for CMMs to store calculated transforms. - return_if_false(fProfileClass == kDisplay_Profile || - fProfileClass == kInput_Profile || - fProfileClass == kOutput_Profile, - "Unsupported profile"); - - // TODO (msarett): - // All the profiles we've tested so far use RGB as the input color space. - return_if_false(fInputColorSpace == kRGB_ColorSpace, "Unsupported color space"); - - // TODO (msarett): - // All the profiles we've tested so far use XYZ as the profile connection space. - return_if_false(fPCS == kXYZ_PCSSpace, "Unsupported PCS space"); - - return_if_false(fSignature == kACSP_Signature, "Bad signature"); - - // TODO (msarett): - // Should we treat different rendering intents differently? - // Valid rendering intents include kPerceptual (0), kRelative (1), - // kSaturation (2), and kAbsolute (3). - return_if_false(fRenderingIntent <= 3, "Bad rendering intent"); - - return_if_false(color_space_almost_equal(SkFixedToFloat(fIlluminantXYZ[0]), 0.96420f) && - color_space_almost_equal(SkFixedToFloat(fIlluminantXYZ[1]), 1.00000f) && - color_space_almost_equal(SkFixedToFloat(fIlluminantXYZ[2]), 0.82491f), - "Illuminant must be D50"); - - return_if_false(fTagCount <= 100, "Too many tags"); - - return true; - } -}; - -template -static bool safe_add(T arg1, T arg2, size_t* result) { - SkASSERT(arg1 >= 0); - SkASSERT(arg2 >= 0); - if (arg1 >= 0 && arg2 <= std::numeric_limits::max() - arg1) { - T sum = arg1 + arg2; - if (sum <= std::numeric_limits::max()) { - *result = static_cast(sum); - return true; - } - } - return false; -} - -static bool safe_mul(uint32_t arg1, uint32_t arg2, uint32_t* result) { - uint64_t product64 = (uint64_t) arg1 * (uint64_t) arg2; - uint32_t product32 = (uint32_t) product64; - if (product32 != product64) { - return false; - } - - *result = product32; - return true; -} - -struct ICCTag { - uint32_t fSignature; - uint32_t fOffset; - uint32_t fLength; - - const uint8_t* init(const uint8_t* src) { - fSignature = read_big_endian_uint(src); - fOffset = read_big_endian_uint(src + 4); - fLength = read_big_endian_uint(src + 8); - return src + 12; - } - - bool valid(size_t len) { - size_t tagEnd; - return_if_false(safe_add(fOffset, fLength, &tagEnd), - "Tag too large, overflows integer addition"); - return_if_false(tagEnd <= len, "Tag too large for ICC profile"); - return true; - } - - const uint8_t* addr(const uint8_t* src) const { - return src + fOffset; - } - - static const ICCTag* Find(const ICCTag tags[], int count, uint32_t signature) { - for (int i = 0; i < count; ++i) { - if (tags[i].fSignature == signature) { - return &tags[i]; - } - } - return nullptr; - } -}; - -static constexpr uint32_t kTAG_rXYZ = SkSetFourByteTag('r', 'X', 'Y', 'Z'); -static constexpr uint32_t kTAG_gXYZ = SkSetFourByteTag('g', 'X', 'Y', 'Z'); -static constexpr uint32_t kTAG_bXYZ = SkSetFourByteTag('b', 'X', 'Y', 'Z'); -static constexpr uint32_t kTAG_rTRC = SkSetFourByteTag('r', 'T', 'R', 'C'); -static constexpr uint32_t kTAG_gTRC = SkSetFourByteTag('g', 'T', 'R', 'C'); -static constexpr uint32_t kTAG_bTRC = SkSetFourByteTag('b', 'T', 'R', 'C'); -static constexpr uint32_t kTAG_A2B0 = SkSetFourByteTag('A', '2', 'B', '0'); - -static bool load_xyz(float dst[3], const uint8_t* src, size_t len) { - if (len < 20) { - SkColorSpacePrintf("XYZ tag is too small (%d bytes)", len); - return false; - } - - dst[0] = SkFixedToFloat(read_big_endian_int(src + 8)); - dst[1] = SkFixedToFloat(read_big_endian_int(src + 12)); - dst[2] = SkFixedToFloat(read_big_endian_int(src + 16)); - SkColorSpacePrintf("XYZ %g %g %g\n", dst[0], dst[1], dst[2]); - return true; -} - -static constexpr uint32_t kTAG_CurveType = SkSetFourByteTag('c', 'u', 'r', 'v'); -static constexpr uint32_t kTAG_ParaCurveType = SkSetFourByteTag('p', 'a', 'r', 'a'); - -static bool load_gammas(SkGammaCurve* gammas, uint32_t numGammas, const uint8_t* src, size_t len) { - for (uint32_t i = 0; i < numGammas; i++) { - if (len < 12) { - // FIXME (msarett): - // We could potentially return false here after correctly parsing *some* of the - // gammas correctly. Should we somehow try to indicate a partial success? - SkColorSpacePrintf("gamma tag is too small (%d bytes)", len); - return false; - } - - // We need to count the number of bytes in the tag, so we are able to move to the - // next tag on the next loop iteration. - size_t tagBytes; - - uint32_t type = read_big_endian_uint(src); - switch (type) { - case kTAG_CurveType: { - uint32_t count = read_big_endian_uint(src + 8); - - // tagBytes = 12 + 2 * count - // We need to do safe addition here to avoid integer overflow. - if (!safe_add(count, count, &tagBytes) || - !safe_add((size_t) 12, tagBytes, &tagBytes)) - { - SkColorSpacePrintf("Invalid gamma count"); - return false; - } - - if (0 == count) { - // Some tags require a gamma curve, but the author doesn't actually want - // to transform the data. In this case, it is common to see a curve with - // a count of 0. - gammas[i].fNamed = SkColorSpace::kLinear_GammaNamed; - break; - } else if (len < tagBytes) { - SkColorSpacePrintf("gamma tag is too small (%d bytes)", len); - return false; - } - - const uint16_t* table = (const uint16_t*) (src + 12); - if (1 == count) { - // The table entry is the gamma (with a bias of 256). - float value = (read_big_endian_short((const uint8_t*) table)) / 256.0f; - set_gamma_value(&gammas[i], value); - SkColorSpacePrintf("gamma %g\n", value); - break; - } - - // Check for frequently occurring sRGB curves. - // We do this by sampling a few values and see if they match our expectation. - // A more robust solution would be to compare each value in this curve against - // an sRGB curve to see if we remain below an error threshold. At this time, - // we haven't seen any images in the wild that make this kind of - // calculation necessary. We encounter identical gamma curves over and - // over again, but relatively few variations. - if (1024 == count) { - // The magic values were chosen because they match a very common sRGB - // gamma table and the less common Canon sRGB gamma table (which use - // different rounding rules). - if (0 == read_big_endian_short((const uint8_t*) &table[0]) && - 3366 == read_big_endian_short((const uint8_t*) &table[257]) && - 14116 == read_big_endian_short((const uint8_t*) &table[513]) && - 34318 == read_big_endian_short((const uint8_t*) &table[768]) && - 65535 == read_big_endian_short((const uint8_t*) &table[1023])) { - gammas[i].fNamed = SkColorSpace::kSRGB_GammaNamed; - break; - } - } else if (26 == count) { - // The magic values were chosen because they match a very common sRGB - // gamma table. - if (0 == read_big_endian_short((const uint8_t*) &table[0]) && - 3062 == read_big_endian_short((const uint8_t*) &table[6]) && - 12824 == read_big_endian_short((const uint8_t*) &table[12]) && - 31237 == read_big_endian_short((const uint8_t*) &table[18]) && - 65535 == read_big_endian_short((const uint8_t*) &table[25])) { - gammas[i].fNamed = SkColorSpace::kSRGB_GammaNamed; - break; - } - } else if (4096 == count) { - // The magic values were chosen because they match Nikon, Epson, and - // LCMS sRGB gamma tables (all of which use different rounding rules). - if (0 == read_big_endian_short((const uint8_t*) &table[0]) && - 950 == read_big_endian_short((const uint8_t*) &table[515]) && - 3342 == read_big_endian_short((const uint8_t*) &table[1025]) && - 14079 == read_big_endian_short((const uint8_t*) &table[2051]) && - 65535 == read_big_endian_short((const uint8_t*) &table[4095])) { - gammas[i].fNamed = SkColorSpace::kSRGB_GammaNamed; - break; - } - } - - // Otherwise, fill in the interpolation table. - gammas[i].fTableSize = count; - gammas[i].fTable = std::unique_ptr(new float[count]); - for (uint32_t j = 0; j < count; j++) { - gammas[i].fTable[j] = - (read_big_endian_short((const uint8_t*) &table[j])) / 65535.0f; - } - break; - } - case kTAG_ParaCurveType: { - enum ParaCurveType { - kExponential_ParaCurveType = 0, - kGAB_ParaCurveType = 1, - kGABC_ParaCurveType = 2, - kGABDE_ParaCurveType = 3, - kGABCDEF_ParaCurveType = 4, - }; - - // Determine the format of the parametric curve tag. - uint16_t format = read_big_endian_short(src + 8); - if (kExponential_ParaCurveType == format) { - tagBytes = 12 + 4; - if (len < tagBytes) { - SkColorSpacePrintf("gamma tag is too small (%d bytes)", len); - return false; - } - - // Y = X^g - int32_t g = read_big_endian_int(src + 12); - set_gamma_value(&gammas[i], SkFixedToFloat(g)); - } else { - // Here's where the real parametric gammas start. There are many - // permutations of the same equations. - // - // Y = (aX + b)^g + c for X >= d - // Y = eX + f otherwise - // - // We will fill in with zeros as necessary to always match the above form. - float g = 0.0f, a = 0.0f, b = 0.0f, c = 0.0f, d = 0.0f, e = 0.0f, f = 0.0f; - switch(format) { - case kGAB_ParaCurveType: { - tagBytes = 12 + 12; - if (len < tagBytes) { - SkColorSpacePrintf("gamma tag is too small (%d bytes)", len); - return false; - } - - // Y = (aX + b)^g for X >= -b/a - // Y = 0 otherwise - g = SkFixedToFloat(read_big_endian_int(src + 12)); - a = SkFixedToFloat(read_big_endian_int(src + 16)); - if (0.0f == a) { - return false; - } - - b = SkFixedToFloat(read_big_endian_int(src + 20)); - d = -b / a; - break; - } - case kGABC_ParaCurveType: - tagBytes = 12 + 16; - if (len < tagBytes) { - SkColorSpacePrintf("gamma tag is too small (%d bytes)", len); - return false; - } - - // Y = (aX + b)^g + c for X >= -b/a - // Y = c otherwise - g = SkFixedToFloat(read_big_endian_int(src + 12)); - a = SkFixedToFloat(read_big_endian_int(src + 16)); - if (0.0f == a) { - return false; - } - - b = SkFixedToFloat(read_big_endian_int(src + 20)); - c = SkFixedToFloat(read_big_endian_int(src + 24)); - d = -b / a; - f = c; - break; - case kGABDE_ParaCurveType: - tagBytes = 12 + 20; - if (len < tagBytes) { - SkColorSpacePrintf("gamma tag is too small (%d bytes)", len); - return false; - } - - // Y = (aX + b)^g for X >= d - // Y = cX otherwise - g = SkFixedToFloat(read_big_endian_int(src + 12)); - a = SkFixedToFloat(read_big_endian_int(src + 16)); - b = SkFixedToFloat(read_big_endian_int(src + 20)); - d = SkFixedToFloat(read_big_endian_int(src + 28)); - e = SkFixedToFloat(read_big_endian_int(src + 24)); - break; - case kGABCDEF_ParaCurveType: - tagBytes = 12 + 28; - if (len < tagBytes) { - SkColorSpacePrintf("gamma tag is too small (%d bytes)", len); - return false; - } - - // Y = (aX + b)^g + c for X >= d - // Y = eX + f otherwise - // NOTE: The ICC spec writes "cX" in place of "eX" but I think - // it's a typo. - g = SkFixedToFloat(read_big_endian_int(src + 12)); - a = SkFixedToFloat(read_big_endian_int(src + 16)); - b = SkFixedToFloat(read_big_endian_int(src + 20)); - c = SkFixedToFloat(read_big_endian_int(src + 24)); - d = SkFixedToFloat(read_big_endian_int(src + 28)); - e = SkFixedToFloat(read_big_endian_int(src + 32)); - f = SkFixedToFloat(read_big_endian_int(src + 36)); - break; - default: - SkColorSpacePrintf("Invalid parametric curve type\n"); - return false; - } - - // Recognize and simplify a very common parametric representation of sRGB gamma. - if (color_space_almost_equal(0.9479f, a) && - color_space_almost_equal(0.0521f, b) && - color_space_almost_equal(0.0000f, c) && - color_space_almost_equal(0.0405f, d) && - color_space_almost_equal(0.0774f, e) && - color_space_almost_equal(0.0000f, f) && - color_space_almost_equal(2.4000f, g)) { - gammas[i].fNamed = SkColorSpace::kSRGB_GammaNamed; - } else { - // Fail on invalid gammas. - if (d <= 0.0f) { - // Y = (aX + b)^g + c for always - if (0.0f == a || 0.0f == g) { - SkColorSpacePrintf("A or G is zero, constant gamma function " - "is nonsense"); - return false; - } - } else if (d >= 1.0f) { - // Y = eX + f for always - if (0.0f == e) { - SkColorSpacePrintf("E is zero, constant gamma function is " - "nonsense"); - return false; - } - } else if ((0.0f == a || 0.0f == g) && 0.0f == e) { - SkColorSpacePrintf("A or G, and E are zero, constant gamma function " - "is nonsense"); - return false; - } - - gammas[i].fG = g; - gammas[i].fA = a; - gammas[i].fB = b; - gammas[i].fC = c; - gammas[i].fD = d; - gammas[i].fE = e; - gammas[i].fF = f; - } - } - - break; - } - default: - SkColorSpacePrintf("Unsupported gamma tag type %d\n", type); - return false; - } - - // Ensure that we have successfully read a gamma representation. - SkASSERT(gammas[i].isNamed() || gammas[i].isValue() || gammas[i].isTable() || - gammas[i].isParametric()); - - // Adjust src and len if there is another gamma curve to load. - if (i != numGammas - 1) { - // Each curve is padded to 4-byte alignment. - tagBytes = SkAlign4(tagBytes); - if (len < tagBytes) { - return false; - } - - src += tagBytes; - len -= tagBytes; - } - } - - return true; -} - -static constexpr uint32_t kTAG_AtoBType = SkSetFourByteTag('m', 'A', 'B', ' '); - -bool load_color_lut(SkColorLookUpTable* colorLUT, uint32_t inputChannels, uint32_t outputChannels, - const uint8_t* src, size_t len) { - // 16 bytes reserved for grid points, 2 for precision, 2 for padding. - // The color LUT data follows after this header. - static constexpr uint32_t kColorLUTHeaderSize = 20; - if (len < kColorLUTHeaderSize) { - SkColorSpacePrintf("Color LUT tag is too small (%d bytes).", len); - return false; - } - size_t dataLen = len - kColorLUTHeaderSize; - - SkASSERT(3 == inputChannels && 3 == outputChannels); - colorLUT->fInputChannels = inputChannels; - colorLUT->fOutputChannels = outputChannels; - uint32_t numEntries = 1; - for (uint32_t i = 0; i < inputChannels; i++) { - colorLUT->fGridPoints[i] = src[i]; - if (0 == src[i]) { - SkColorSpacePrintf("Each input channel must have at least one grid point."); - return false; - } - - if (!safe_mul(numEntries, src[i], &numEntries)) { - SkColorSpacePrintf("Too many entries in Color LUT."); - return false; - } - } - - if (!safe_mul(numEntries, outputChannels, &numEntries)) { - SkColorSpacePrintf("Too many entries in Color LUT."); - return false; - } - - // Space is provided for a maximum of the 16 input channels. Now we determine the precision - // of the table values. - uint8_t precision = src[16]; - switch (precision) { - case 1: // 8-bit data - case 2: // 16-bit data - break; - default: - SkColorSpacePrintf("Color LUT precision must be 8-bit or 16-bit.\n"); - return false; - } - - uint32_t clutBytes; - if (!safe_mul(numEntries, precision, &clutBytes)) { - SkColorSpacePrintf("Too many entries in Color LUT."); - return false; - } - - if (dataLen < clutBytes) { - SkColorSpacePrintf("Color LUT tag is too small (%d bytes).", len); - return false; - } - - // Movable struct colorLUT has ownership of fTable. - colorLUT->fTable = std::unique_ptr(new float[numEntries]); - const uint8_t* ptr = src + kColorLUTHeaderSize; - for (uint32_t i = 0; i < numEntries; i++, ptr += precision) { - if (1 == precision) { - colorLUT->fTable[i] = ((float) ptr[i]) / 255.0f; - } else { - colorLUT->fTable[i] = ((float) read_big_endian_short(ptr)) / 65535.0f; - } - } - - return true; -} - -bool load_matrix(SkMatrix44* toXYZ, const uint8_t* src, size_t len) { - if (len < 48) { - SkColorSpacePrintf("Matrix tag is too small (%d bytes).", len); - return false; - } - - // For this matrix to behave like our "to XYZ D50" matrices, it needs to be scaled. - constexpr float scale = 65535.0 / 32768.0; - float array[16]; - array[ 0] = scale * SkFixedToFloat(read_big_endian_int(src)); - array[ 1] = scale * SkFixedToFloat(read_big_endian_int(src + 4)); - array[ 2] = scale * SkFixedToFloat(read_big_endian_int(src + 8)); - array[ 3] = scale * SkFixedToFloat(read_big_endian_int(src + 36)); // translate R - array[ 4] = scale * SkFixedToFloat(read_big_endian_int(src + 12)); - array[ 5] = scale * SkFixedToFloat(read_big_endian_int(src + 16)); - array[ 6] = scale * SkFixedToFloat(read_big_endian_int(src + 20)); - array[ 7] = scale * SkFixedToFloat(read_big_endian_int(src + 40)); // translate G - array[ 8] = scale * SkFixedToFloat(read_big_endian_int(src + 24)); - array[ 9] = scale * SkFixedToFloat(read_big_endian_int(src + 28)); - array[10] = scale * SkFixedToFloat(read_big_endian_int(src + 32)); - array[11] = scale * SkFixedToFloat(read_big_endian_int(src + 44)); // translate B - array[12] = 0.0f; - array[13] = 0.0f; - array[14] = 0.0f; - array[15] = 1.0f; - toXYZ->setColMajorf(array); - return true; -} - -bool load_a2b0(SkColorLookUpTable* colorLUT, SkGammaCurve* gammas, SkMatrix44* toXYZ, - const uint8_t* src, size_t len) { - if (len < 32) { - SkColorSpacePrintf("A to B tag is too small (%d bytes).", len); - return false; - } - - uint32_t type = read_big_endian_uint(src); - if (kTAG_AtoBType != type) { - // FIXME (msarett): Need to support lut8Type and lut16Type. - SkColorSpacePrintf("Unsupported A to B tag type.\n"); - return false; - } - - // Read the number of channels. The four bytes that we skipped are reserved and - // must be zero. - uint8_t inputChannels = src[8]; - uint8_t outputChannels = src[9]; - if (3 != inputChannels || 3 != outputChannels) { - // We only handle (supposedly) RGB inputs and RGB outputs. The numbers of input - // channels and output channels both must be 3. - SkColorSpacePrintf("Input and output channels must equal 3 in A to B tag.\n"); - return false; - } - - // Read the offsets of each element in the A to B tag. With the exception of A curves and - // B curves (which we do not yet support), we will handle these elements in the order in - // which they should be applied (rather than the order in which they occur in the tag). - // If the offset is non-zero it indicates that the element is present. - uint32_t offsetToACurves = read_big_endian_int(src + 28); - uint32_t offsetToBCurves = read_big_endian_int(src + 12); - if ((0 != offsetToACurves) || (0 != offsetToBCurves)) { - // FIXME (msarett): Handle A and B curves. - // Note that the A curve is technically required in order to have a color LUT. - // However, all the A curves I have seen so far have are just placeholders that - // don't actually transform the data. - SkColorSpacePrintf("Ignoring A and/or B curve. Output may be wrong.\n"); - } - - uint32_t offsetToColorLUT = read_big_endian_int(src + 24); - if (0 != offsetToColorLUT && offsetToColorLUT < len) { - if (!load_color_lut(colorLUT, inputChannels, outputChannels, src + offsetToColorLUT, - len - offsetToColorLUT)) { - SkColorSpacePrintf("Failed to read color LUT from A to B tag.\n"); - } - } - - uint32_t offsetToMCurves = read_big_endian_int(src + 20); - if (0 != offsetToMCurves && offsetToMCurves < len) { - if (!load_gammas(gammas, outputChannels, src + offsetToMCurves, len - offsetToMCurves)) { - SkColorSpacePrintf("Failed to read M curves from A to B tag. Using linear gamma.\n"); - gammas[0].fNamed = SkColorSpace::kLinear_GammaNamed; - gammas[1].fNamed = SkColorSpace::kLinear_GammaNamed; - gammas[2].fNamed = SkColorSpace::kLinear_GammaNamed; - } - } - - uint32_t offsetToMatrix = read_big_endian_int(src + 16); - if (0 != offsetToMatrix && offsetToMatrix < len) { - if (!load_matrix(toXYZ, src + offsetToMatrix, len - offsetToMatrix)) { - SkColorSpacePrintf("Failed to read matrix from A to B tag.\n"); - toXYZ->setIdentity(); - } - } - - return true; -} - -sk_sp SkColorSpace::NewICC(const void* input, size_t len) { - if (!input || len < kICCHeaderSize) { - return_null("Data is null or not large enough to contain an ICC profile"); - } - - // Create our own copy of the input. - void* memory = sk_malloc_throw(len); - memcpy(memory, input, len); - sk_sp data = SkData::MakeFromMalloc(memory, len); - const void* base = data->data(); - const uint8_t* ptr = (const uint8_t*) base; - - // Read the ICC profile header and check to make sure that it is valid. - ICCProfileHeader header; - header.init(ptr, len); - if (!header.valid()) { - return nullptr; - } - - // Adjust ptr and len before reading the tags. - if (len < header.fSize) { - SkColorSpacePrintf("ICC profile might be truncated.\n"); - } else if (len > header.fSize) { - SkColorSpacePrintf("Caller provided extra data beyond the end of the ICC profile.\n"); - len = header.fSize; - } - ptr += kICCHeaderSize; - len -= kICCHeaderSize; - - // Parse tag headers. - uint32_t tagCount = header.fTagCount; - SkColorSpacePrintf("ICC profile contains %d tags.\n", tagCount); - if (len < kICCTagTableEntrySize * tagCount) { - return_null("Not enough input data to read tag table entries"); - } - - SkAutoTArray tags(tagCount); - for (uint32_t i = 0; i < tagCount; i++) { - ptr = tags[i].init(ptr); - SkColorSpacePrintf("[%d] %c%c%c%c %d %d\n", i, (tags[i].fSignature >> 24) & 0xFF, - (tags[i].fSignature >> 16) & 0xFF, (tags[i].fSignature >> 8) & 0xFF, - (tags[i].fSignature >> 0) & 0xFF, tags[i].fOffset, tags[i].fLength); - - if (!tags[i].valid(kICCHeaderSize + len)) { - return_null("Tag is too large to fit in ICC profile"); - } - } - - switch (header.fInputColorSpace) { - case kRGB_ColorSpace: { - // Recognize the rXYZ, gXYZ, and bXYZ tags. - const ICCTag* r = ICCTag::Find(tags.get(), tagCount, kTAG_rXYZ); - const ICCTag* g = ICCTag::Find(tags.get(), tagCount, kTAG_gXYZ); - const ICCTag* b = ICCTag::Find(tags.get(), tagCount, kTAG_bXYZ); - if (r && g && b) { - float toXYZ[9]; - if (!load_xyz(&toXYZ[0], r->addr((const uint8_t*) base), r->fLength) || - !load_xyz(&toXYZ[3], g->addr((const uint8_t*) base), g->fLength) || - !load_xyz(&toXYZ[6], b->addr((const uint8_t*) base), b->fLength)) - { - return_null("Need valid rgb tags for XYZ space"); - } - SkMatrix44 mat(SkMatrix44::kUninitialized_Constructor); - mat.set3x3RowMajorf(toXYZ); - - // It is not uncommon to see missing or empty gamma tags. This indicates - // that we should use unit gamma. - SkGammaCurve curves[3]; - r = ICCTag::Find(tags.get(), tagCount, kTAG_rTRC); - g = ICCTag::Find(tags.get(), tagCount, kTAG_gTRC); - b = ICCTag::Find(tags.get(), tagCount, kTAG_bTRC); - if (!r || !load_gammas(&curves[0], 1, r->addr((const uint8_t*) base), r->fLength)) - { - SkColorSpacePrintf("Failed to read R gamma tag.\n"); - curves[0].fNamed = SkColorSpace::kLinear_GammaNamed; - } - if (!g || !load_gammas(&curves[1], 1, g->addr((const uint8_t*) base), g->fLength)) - { - SkColorSpacePrintf("Failed to read G gamma tag.\n"); - curves[1].fNamed = SkColorSpace::kLinear_GammaNamed; - } - if (!b || !load_gammas(&curves[2], 1, b->addr((const uint8_t*) base), b->fLength)) - { - SkColorSpacePrintf("Failed to read B gamma tag.\n"); - curves[2].fNamed = SkColorSpace::kLinear_GammaNamed; - } - - GammaNamed gammaNamed = SkGammas::Named(curves); - if (kNonStandard_GammaNamed == gammaNamed) { - sk_sp gammas = sk_make_sp(std::move(curves[0]), - std::move(curves[1]), - std::move(curves[2])); - return sk_sp(new SkColorSpace_Base(nullptr, std::move(gammas), - mat, std::move(data))); - } else { - return SkColorSpace_Base::NewRGB(gammaNamed, mat); - } - } - - // Recognize color profile specified by A2B0 tag. - const ICCTag* a2b0 = ICCTag::Find(tags.get(), tagCount, kTAG_A2B0); - if (a2b0) { - sk_sp colorLUT = sk_make_sp(); - SkGammaCurve curves[3]; - SkMatrix44 toXYZ(SkMatrix44::kUninitialized_Constructor); - if (!load_a2b0(colorLUT.get(), curves, &toXYZ, a2b0->addr((const uint8_t*) base), - a2b0->fLength)) { - return_null("Failed to parse A2B0 tag"); - } - - GammaNamed gammaNamed = SkGammas::Named(curves); - colorLUT = colorLUT->fTable ? colorLUT : nullptr; - if (colorLUT || kNonStandard_GammaNamed == gammaNamed) { - sk_sp gammas = sk_make_sp(std::move(curves[0]), - std::move(curves[1]), - std::move(curves[2])); - - return sk_sp(new SkColorSpace_Base(std::move(colorLUT), - std::move(gammas), toXYZ, - std::move(data))); - } else { - return SkColorSpace_Base::NewRGB(gammaNamed, toXYZ); - } - } - } - default: - break; - } - - return_null("ICC profile contains unsupported colorspace"); -} - -/////////////////////////////////////////////////////////////////////////////////////////////////// - -// We will write a profile with the minimum nine required tags. -static constexpr uint32_t kICCNumEntries = 9; - -static constexpr uint32_t kTAG_desc = SkSetFourByteTag('d', 'e', 's', 'c'); -static constexpr uint32_t kTAG_desc_Bytes = 12; -static constexpr uint32_t kTAG_desc_Offset = kICCHeaderSize + kICCNumEntries*kICCTagTableEntrySize; - -static constexpr uint32_t kTAG_XYZ_Bytes = 20; -static constexpr uint32_t kTAG_rXYZ_Offset = kTAG_desc_Offset + kTAG_desc_Bytes; -static constexpr uint32_t kTAG_gXYZ_Offset = kTAG_rXYZ_Offset + kTAG_XYZ_Bytes; -static constexpr uint32_t kTAG_bXYZ_Offset = kTAG_gXYZ_Offset + kTAG_XYZ_Bytes; - -static constexpr uint32_t kTAG_TRC_Bytes = 14; -static constexpr uint32_t kTAG_rTRC_Offset = kTAG_bXYZ_Offset + kTAG_XYZ_Bytes; -static constexpr uint32_t kTAG_gTRC_Offset = kTAG_rTRC_Offset + SkAlign4(kTAG_TRC_Bytes); -static constexpr uint32_t kTAG_bTRC_Offset = kTAG_gTRC_Offset + SkAlign4(kTAG_TRC_Bytes); - -static constexpr uint32_t kTAG_wtpt = SkSetFourByteTag('w', 't', 'p', 't'); -static constexpr uint32_t kTAG_wtpt_Offset = kTAG_bTRC_Offset + SkAlign4(kTAG_TRC_Bytes); - -static constexpr uint32_t kTAG_cprt = SkSetFourByteTag('c', 'p', 'r', 't'); -static constexpr uint32_t kTAG_cprt_Bytes = 12; -static constexpr uint32_t kTAG_cprt_Offset = kTAG_wtpt_Offset + kTAG_XYZ_Bytes; - -static constexpr uint32_t kICCProfileSize = kTAG_cprt_Offset + kTAG_cprt_Bytes; - -static constexpr uint32_t gICCHeader[kICCHeaderSize / 4] { - SkEndian_SwapBE32(kICCProfileSize), // Size of the profile - 0, // Preferred CMM type (ignored) - SkEndian_SwapBE32(0x02100000), // Version 2.1 - SkEndian_SwapBE32(kDisplay_Profile), // Display device profile - SkEndian_SwapBE32(kRGB_ColorSpace), // RGB input color space - SkEndian_SwapBE32(kXYZ_PCSSpace), // XYZ profile connection space - 0, 0, 0, // Date and time (ignored) - SkEndian_SwapBE32(kACSP_Signature), // Profile signature - 0, // Platform target (ignored) - 0x00000000, // Flags: not embedded, can be used independently - 0, // Device manufacturer (ignored) - 0, // Device model (ignored) - 0, 0, // Device attributes (ignored) - SkEndian_SwapBE32(1), // Relative colorimetric rendering intent - SkEndian_SwapBE32(0x0000f6d6), // D50 standard illuminant (X) - SkEndian_SwapBE32(0x00010000), // D50 standard illuminant (Y) - SkEndian_SwapBE32(0x0000d32d), // D50 standard illuminant (Z) - 0, // Profile creator (ignored) - 0, 0, 0, 0, // Profile id checksum (ignored) - 0, 0, 0, 0, 0, 0, 0, // Reserved (ignored) - SkEndian_SwapBE32(kICCNumEntries), // Number of tags -}; - -static constexpr uint32_t gICCTagTable[3 * kICCNumEntries] { - // Profile description - SkEndian_SwapBE32(kTAG_desc), - SkEndian_SwapBE32(kTAG_desc_Offset), - SkEndian_SwapBE32(kTAG_desc_Bytes), - - // rXYZ - SkEndian_SwapBE32(kTAG_rXYZ), - SkEndian_SwapBE32(kTAG_rXYZ_Offset), - SkEndian_SwapBE32(kTAG_XYZ_Bytes), - - // gXYZ - SkEndian_SwapBE32(kTAG_gXYZ), - SkEndian_SwapBE32(kTAG_gXYZ_Offset), - SkEndian_SwapBE32(kTAG_XYZ_Bytes), - - // bXYZ - SkEndian_SwapBE32(kTAG_bXYZ), - SkEndian_SwapBE32(kTAG_bXYZ_Offset), - SkEndian_SwapBE32(kTAG_XYZ_Bytes), - - // rTRC - SkEndian_SwapBE32(kTAG_rTRC), - SkEndian_SwapBE32(kTAG_rTRC_Offset), - SkEndian_SwapBE32(kTAG_TRC_Bytes), - - // gTRC - SkEndian_SwapBE32(kTAG_gTRC), - SkEndian_SwapBE32(kTAG_gTRC_Offset), - SkEndian_SwapBE32(kTAG_TRC_Bytes), - - // bTRC - SkEndian_SwapBE32(kTAG_bTRC), - SkEndian_SwapBE32(kTAG_bTRC_Offset), - SkEndian_SwapBE32(kTAG_TRC_Bytes), - - // White point - SkEndian_SwapBE32(kTAG_wtpt), - SkEndian_SwapBE32(kTAG_wtpt_Offset), - SkEndian_SwapBE32(kTAG_XYZ_Bytes), - - // Copyright - SkEndian_SwapBE32(kTAG_cprt), - SkEndian_SwapBE32(kTAG_cprt_Offset), - SkEndian_SwapBE32(kTAG_cprt_Bytes), -}; - -static constexpr uint32_t kTAG_TextType = SkSetFourByteTag('m', 'l', 'u', 'c'); -static constexpr uint32_t gEmptyTextTag[3] { - SkEndian_SwapBE32(kTAG_TextType), // Type signature - 0, // Reserved - 0, // Zero records -}; - -static void write_xyz_tag(uint32_t* ptr, const SkMatrix44& toXYZ, int row) { - ptr[0] = SkEndian_SwapBE32(kXYZ_PCSSpace); - ptr[1] = 0; - ptr[2] = SkEndian_SwapBE32(SkFloatToFixed(toXYZ.getFloat(row, 0))); - ptr[3] = SkEndian_SwapBE32(SkFloatToFixed(toXYZ.getFloat(row, 1))); - ptr[4] = SkEndian_SwapBE32(SkFloatToFixed(toXYZ.getFloat(row, 2))); -} - -static void write_trc_tag(uint32_t* ptr, float value) { - ptr[0] = SkEndian_SwapBE32(kTAG_CurveType); - ptr[1] = 0; - - // Gamma will be specified with a single value. - ptr[2] = SkEndian_SwapBE32(1); - - // Convert gamma to 16-bit fixed point. - uint16_t* ptr16 = (uint16_t*) (ptr + 3); - ptr16[0] = SkEndian_SwapBE16((uint16_t) (value * 256.0f)); - - // Pad tag with zero. - ptr16[1] = 0; -} - -static float get_gamma_value(const SkGammaCurve* curve) { - switch (curve->fNamed) { - case SkColorSpace::kSRGB_GammaNamed: - // FIXME (msarett): - // kSRGB cannot be represented by a value. Here we fall through to 2.2f, - // which is a close guess. To be more accurate, we need to represent sRGB - // gamma with a parametric curve. - case SkColorSpace::k2Dot2Curve_GammaNamed: - return 2.2f; - case SkColorSpace::kLinear_GammaNamed: - return 1.0f; - default: - SkASSERT(curve->isValue()); - return curve->fValue; - } -} - -sk_sp SkColorSpace_Base::writeToICC() const { - // Return if this object was created from a profile, or if we have already serialized - // the profile. - if (fProfileData) { - return fProfileData; - } - - // The client may create an SkColorSpace using an SkMatrix44, but currently we only - // support writing profiles with 3x3 matrices. - // TODO (msarett): Fix this! - if (0.0f != fToXYZD50.getFloat(3, 0) || 0.0f != fToXYZD50.getFloat(3, 1) || - 0.0f != fToXYZD50.getFloat(3, 2) || 0.0f != fToXYZD50.getFloat(0, 3) || - 0.0f != fToXYZD50.getFloat(1, 3) || 0.0f != fToXYZD50.getFloat(2, 3)) - { - return nullptr; - } - - SkAutoMalloc profile(kICCProfileSize); - uint8_t* ptr = (uint8_t*) profile.get(); - - // Write profile header - memcpy(ptr, gICCHeader, sizeof(gICCHeader)); - ptr += sizeof(gICCHeader); - - // Write tag table - memcpy(ptr, gICCTagTable, sizeof(gICCTagTable)); - ptr += sizeof(gICCTagTable); - - // Write profile description tag - memcpy(ptr, gEmptyTextTag, sizeof(gEmptyTextTag)); - ptr += sizeof(gEmptyTextTag); - - // Write XYZ tags - write_xyz_tag((uint32_t*) ptr, fToXYZD50, 0); - ptr += kTAG_XYZ_Bytes; - write_xyz_tag((uint32_t*) ptr, fToXYZD50, 1); - ptr += kTAG_XYZ_Bytes; - write_xyz_tag((uint32_t*) ptr, fToXYZD50, 2); - ptr += kTAG_XYZ_Bytes; - - // Write TRC tags - GammaNamed gammaNamed = this->gammaNamed(); - if (kNonStandard_GammaNamed == gammaNamed) { - write_trc_tag((uint32_t*) ptr, get_gamma_value(&as_CSB(this)->fGammas->fRed)); - ptr += SkAlign4(kTAG_TRC_Bytes); - write_trc_tag((uint32_t*) ptr, get_gamma_value(&as_CSB(this)->fGammas->fGreen)); - ptr += SkAlign4(kTAG_TRC_Bytes); - write_trc_tag((uint32_t*) ptr, get_gamma_value(&as_CSB(this)->fGammas->fBlue)); - ptr += SkAlign4(kTAG_TRC_Bytes); - } else { - switch (gammaNamed) { - case SkColorSpace::kSRGB_GammaNamed: - // FIXME (msarett): - // kSRGB cannot be represented by a value. Here we fall through to 2.2f, - // which is a close guess. To be more accurate, we need to represent sRGB - // gamma with a parametric curve. - case SkColorSpace::k2Dot2Curve_GammaNamed: - write_trc_tag((uint32_t*) ptr, 2.2f); - ptr += SkAlign4(kTAG_TRC_Bytes); - write_trc_tag((uint32_t*) ptr, 2.2f); - ptr += SkAlign4(kTAG_TRC_Bytes); - write_trc_tag((uint32_t*) ptr, 2.2f); - ptr += SkAlign4(kTAG_TRC_Bytes); - break; - case SkColorSpace::kLinear_GammaNamed: - write_trc_tag((uint32_t*) ptr, 1.0f); - ptr += SkAlign4(kTAG_TRC_Bytes); - write_trc_tag((uint32_t*) ptr, 1.0f); - ptr += SkAlign4(kTAG_TRC_Bytes); - write_trc_tag((uint32_t*) ptr, 1.0f); - ptr += SkAlign4(kTAG_TRC_Bytes); - break; - default: - SkASSERT(false); - break; - } - } - - // Write white point tag - uint32_t* ptr32 = (uint32_t*) ptr; - ptr32[0] = SkEndian_SwapBE32(kXYZ_PCSSpace); - ptr32[1] = 0; - // TODO (msarett): These values correspond to the D65 white point. This may not always be - // correct. - ptr32[2] = SkEndian_SwapBE32(0x0000f351); - ptr32[3] = SkEndian_SwapBE32(0x00010000); - ptr32[4] = SkEndian_SwapBE32(0x000116cc); - ptr += kTAG_XYZ_Bytes; - - // Write copyright tag - memcpy(ptr, gEmptyTextTag, sizeof(gEmptyTextTag)); - - // TODO (msarett): Should we try to hold onto the data so we can return immediately if - // the client calls again? - return SkData::MakeFromMalloc(profile.release(), kICCProfileSize); -} - -/////////////////////////////////////////////////////////////////////////////////////////////////// - enum Version { k0_Version, // Initial version, header + flags for matrix and profile }; diff --git a/src/core/SkColorSpacePriv.h b/src/core/SkColorSpacePriv.h new file mode 100644 index 0000000000..7c7b9d0c21 --- /dev/null +++ b/src/core/SkColorSpacePriv.h @@ -0,0 +1,25 @@ +/* + * Copyright 2016 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#define SkColorSpacePrintf(...) + +inline bool color_space_almost_equal(float a, float b) { + return SkTAbs(a - b) < 0.01f; +} + +inline void set_gamma_value(SkGammaCurve* gamma, float value) { + if (color_space_almost_equal(2.2f, value)) { + gamma->fNamed = SkColorSpace::k2Dot2Curve_GammaNamed; + } else if (color_space_almost_equal(1.0f, value)) { + gamma->fNamed = SkColorSpace::kLinear_GammaNamed; + } else if (color_space_almost_equal(0.0f, value)) { + SkColorSpacePrintf("Treating invalid zero gamma as linear."); + gamma->fNamed = SkColorSpace::kLinear_GammaNamed; + } else { + gamma->fValue = value; + } +} diff --git a/src/core/SkColorSpace_ICC.cpp b/src/core/SkColorSpace_ICC.cpp new file mode 100644 index 0000000000..e4be8f4e4e --- /dev/null +++ b/src/core/SkColorSpace_ICC.cpp @@ -0,0 +1,1054 @@ +/* + * 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 "SkColorSpacePriv.h" +#include "SkEndian.h" +#include "SkFixed.h" +#include "SkTemplates.h" + +#define return_if_false(pred, msg) \ + do { \ + if (!(pred)) { \ + SkColorSpacePrintf("Invalid ICC Profile: %s.\n", (msg)); \ + return false; \ + } \ + } while (0) + +#define return_null(msg) \ + do { \ + SkColorSpacePrintf("Invalid ICC Profile: %s.\n", (msg)); \ + return nullptr; \ + } while (0) + +static uint16_t read_big_endian_short(const uint8_t* ptr) { + return ptr[0] << 8 | ptr[1]; +} + +static uint32_t read_big_endian_uint(const uint8_t* ptr) { + return ptr[0] << 24 | ptr[1] << 16 | ptr[2] << 8 | ptr[3]; +} + +static int32_t read_big_endian_int(const uint8_t* ptr) { + return (int32_t) read_big_endian_uint(ptr); +} + +// This is equal to the header size according to the ICC specification (128) +// plus the size of the tag count (4). We include the tag count since we +// always require it to be present anyway. +static constexpr size_t kICCHeaderSize = 132; + +// Contains a signature (4), offset (4), and size (4). +static constexpr size_t kICCTagTableEntrySize = 12; + +static constexpr uint32_t kRGB_ColorSpace = SkSetFourByteTag('R', 'G', 'B', ' '); +static constexpr uint32_t kDisplay_Profile = SkSetFourByteTag('m', 'n', 't', 'r'); +static constexpr uint32_t kInput_Profile = SkSetFourByteTag('s', 'c', 'n', 'r'); +static constexpr uint32_t kOutput_Profile = SkSetFourByteTag('p', 'r', 't', 'r'); +static constexpr uint32_t kXYZ_PCSSpace = SkSetFourByteTag('X', 'Y', 'Z', ' '); +static constexpr uint32_t kACSP_Signature = SkSetFourByteTag('a', 'c', 's', 'p'); + +struct ICCProfileHeader { + uint32_t fSize; + + // No reason to care about the preferred color management module (ex: Adobe, Apple, etc.). + // We're always going to use this one. + uint32_t fCMMType_ignored; + + uint32_t fVersion; + uint32_t fProfileClass; + uint32_t fInputColorSpace; + uint32_t fPCS; + uint32_t fDateTime_ignored[3]; + uint32_t fSignature; + + // Indicates the platform that this profile was created for (ex: Apple, Microsoft). This + // doesn't really matter to us. + uint32_t fPlatformTarget_ignored; + + // Flags can indicate: + // (1) Whether this profile was embedded in a file. This flag is consistently wrong. + // Ex: The profile came from a file but indicates that it did not. + // (2) Whether we are allowed to use the profile independently of the color data. If set, + // this may allow us to use the embedded profile for testing separate from the original + // image. + uint32_t fFlags_ignored; + + // We support many output devices. It doesn't make sense to think about the attributes of + // the device in the context of the image profile. + uint32_t fDeviceManufacturer_ignored; + uint32_t fDeviceModel_ignored; + uint32_t fDeviceAttributes_ignored[2]; + + uint32_t fRenderingIntent; + int32_t fIlluminantXYZ[3]; + + // We don't care who created the profile. + uint32_t fCreator_ignored; + + // This is an MD5 checksum. Could be useful for checking if profiles are equal. + uint32_t fProfileId_ignored[4]; + + // Reserved for future use. + uint32_t fReserved_ignored[7]; + + uint32_t fTagCount; + + void init(const uint8_t* src, size_t len) { + SkASSERT(kICCHeaderSize == sizeof(*this)); + + uint32_t* dst = (uint32_t*) this; + for (uint32_t i = 0; i < kICCHeaderSize / 4; i++, src+=4) { + dst[i] = read_big_endian_uint(src); + } + } + + bool valid() const { + return_if_false(fSize >= kICCHeaderSize, "Size is too small"); + + uint8_t majorVersion = fVersion >> 24; + return_if_false(majorVersion <= 4, "Unsupported version"); + + // These are the three basic classes of profiles that we might expect to see embedded + // in images. Four additional classes exist, but they generally are used as a convenient + // way for CMMs to store calculated transforms. + return_if_false(fProfileClass == kDisplay_Profile || + fProfileClass == kInput_Profile || + fProfileClass == kOutput_Profile, + "Unsupported profile"); + + // TODO (msarett): + // All the profiles we've tested so far use RGB as the input color space. + return_if_false(fInputColorSpace == kRGB_ColorSpace, "Unsupported color space"); + + // TODO (msarett): + // All the profiles we've tested so far use XYZ as the profile connection space. + return_if_false(fPCS == kXYZ_PCSSpace, "Unsupported PCS space"); + + return_if_false(fSignature == kACSP_Signature, "Bad signature"); + + // TODO (msarett): + // Should we treat different rendering intents differently? + // Valid rendering intents include kPerceptual (0), kRelative (1), + // kSaturation (2), and kAbsolute (3). + return_if_false(fRenderingIntent <= 3, "Bad rendering intent"); + + return_if_false(color_space_almost_equal(SkFixedToFloat(fIlluminantXYZ[0]), 0.96420f) && + color_space_almost_equal(SkFixedToFloat(fIlluminantXYZ[1]), 1.00000f) && + color_space_almost_equal(SkFixedToFloat(fIlluminantXYZ[2]), 0.82491f), + "Illuminant must be D50"); + + return_if_false(fTagCount <= 100, "Too many tags"); + + return true; + } +}; + +template +static bool safe_add(T arg1, T arg2, size_t* result) { + SkASSERT(arg1 >= 0); + SkASSERT(arg2 >= 0); + if (arg1 >= 0 && arg2 <= std::numeric_limits::max() - arg1) { + T sum = arg1 + arg2; + if (sum <= std::numeric_limits::max()) { + *result = static_cast(sum); + return true; + } + } + return false; +} + +static bool safe_mul(uint32_t arg1, uint32_t arg2, uint32_t* result) { + uint64_t product64 = (uint64_t) arg1 * (uint64_t) arg2; + uint32_t product32 = (uint32_t) product64; + if (product32 != product64) { + return false; + } + + *result = product32; + return true; +} + +struct ICCTag { + uint32_t fSignature; + uint32_t fOffset; + uint32_t fLength; + + const uint8_t* init(const uint8_t* src) { + fSignature = read_big_endian_uint(src); + fOffset = read_big_endian_uint(src + 4); + fLength = read_big_endian_uint(src + 8); + return src + 12; + } + + bool valid(size_t len) { + size_t tagEnd; + return_if_false(safe_add(fOffset, fLength, &tagEnd), + "Tag too large, overflows integer addition"); + return_if_false(tagEnd <= len, "Tag too large for ICC profile"); + return true; + } + + const uint8_t* addr(const uint8_t* src) const { + return src + fOffset; + } + + static const ICCTag* Find(const ICCTag tags[], int count, uint32_t signature) { + for (int i = 0; i < count; ++i) { + if (tags[i].fSignature == signature) { + return &tags[i]; + } + } + return nullptr; + } +}; + +static constexpr uint32_t kTAG_rXYZ = SkSetFourByteTag('r', 'X', 'Y', 'Z'); +static constexpr uint32_t kTAG_gXYZ = SkSetFourByteTag('g', 'X', 'Y', 'Z'); +static constexpr uint32_t kTAG_bXYZ = SkSetFourByteTag('b', 'X', 'Y', 'Z'); +static constexpr uint32_t kTAG_rTRC = SkSetFourByteTag('r', 'T', 'R', 'C'); +static constexpr uint32_t kTAG_gTRC = SkSetFourByteTag('g', 'T', 'R', 'C'); +static constexpr uint32_t kTAG_bTRC = SkSetFourByteTag('b', 'T', 'R', 'C'); +static constexpr uint32_t kTAG_A2B0 = SkSetFourByteTag('A', '2', 'B', '0'); + +static bool load_xyz(float dst[3], const uint8_t* src, size_t len) { + if (len < 20) { + SkColorSpacePrintf("XYZ tag is too small (%d bytes)", len); + return false; + } + + dst[0] = SkFixedToFloat(read_big_endian_int(src + 8)); + dst[1] = SkFixedToFloat(read_big_endian_int(src + 12)); + dst[2] = SkFixedToFloat(read_big_endian_int(src + 16)); + SkColorSpacePrintf("XYZ %g %g %g\n", dst[0], dst[1], dst[2]); + return true; +} + +static constexpr uint32_t kTAG_CurveType = SkSetFourByteTag('c', 'u', 'r', 'v'); +static constexpr uint32_t kTAG_ParaCurveType = SkSetFourByteTag('p', 'a', 'r', 'a'); + +static bool load_gammas(SkGammaCurve* gammas, uint32_t numGammas, const uint8_t* src, size_t len) { + for (uint32_t i = 0; i < numGammas; i++) { + if (len < 12) { + // FIXME (msarett): + // We could potentially return false here after correctly parsing *some* of the + // gammas correctly. Should we somehow try to indicate a partial success? + SkColorSpacePrintf("gamma tag is too small (%d bytes)", len); + return false; + } + + // We need to count the number of bytes in the tag, so we are able to move to the + // next tag on the next loop iteration. + size_t tagBytes; + + uint32_t type = read_big_endian_uint(src); + switch (type) { + case kTAG_CurveType: { + uint32_t count = read_big_endian_uint(src + 8); + + // tagBytes = 12 + 2 * count + // We need to do safe addition here to avoid integer overflow. + if (!safe_add(count, count, &tagBytes) || + !safe_add((size_t) 12, tagBytes, &tagBytes)) + { + SkColorSpacePrintf("Invalid gamma count"); + return false; + } + + if (0 == count) { + // Some tags require a gamma curve, but the author doesn't actually want + // to transform the data. In this case, it is common to see a curve with + // a count of 0. + gammas[i].fNamed = SkColorSpace::kLinear_GammaNamed; + break; + } else if (len < tagBytes) { + SkColorSpacePrintf("gamma tag is too small (%d bytes)", len); + return false; + } + + const uint16_t* table = (const uint16_t*) (src + 12); + if (1 == count) { + // The table entry is the gamma (with a bias of 256). + float value = (read_big_endian_short((const uint8_t*) table)) / 256.0f; + set_gamma_value(&gammas[i], value); + SkColorSpacePrintf("gamma %g\n", value); + break; + } + + // Check for frequently occurring sRGB curves. + // We do this by sampling a few values and see if they match our expectation. + // A more robust solution would be to compare each value in this curve against + // an sRGB curve to see if we remain below an error threshold. At this time, + // we haven't seen any images in the wild that make this kind of + // calculation necessary. We encounter identical gamma curves over and + // over again, but relatively few variations. + if (1024 == count) { + // The magic values were chosen because they match a very common sRGB + // gamma table and the less common Canon sRGB gamma table (which use + // different rounding rules). + if (0 == read_big_endian_short((const uint8_t*) &table[0]) && + 3366 == read_big_endian_short((const uint8_t*) &table[257]) && + 14116 == read_big_endian_short((const uint8_t*) &table[513]) && + 34318 == read_big_endian_short((const uint8_t*) &table[768]) && + 65535 == read_big_endian_short((const uint8_t*) &table[1023])) { + gammas[i].fNamed = SkColorSpace::kSRGB_GammaNamed; + break; + } + } else if (26 == count) { + // The magic values were chosen because they match a very common sRGB + // gamma table. + if (0 == read_big_endian_short((const uint8_t*) &table[0]) && + 3062 == read_big_endian_short((const uint8_t*) &table[6]) && + 12824 == read_big_endian_short((const uint8_t*) &table[12]) && + 31237 == read_big_endian_short((const uint8_t*) &table[18]) && + 65535 == read_big_endian_short((const uint8_t*) &table[25])) { + gammas[i].fNamed = SkColorSpace::kSRGB_GammaNamed; + break; + } + } else if (4096 == count) { + // The magic values were chosen because they match Nikon, Epson, and + // LCMS sRGB gamma tables (all of which use different rounding rules). + if (0 == read_big_endian_short((const uint8_t*) &table[0]) && + 950 == read_big_endian_short((const uint8_t*) &table[515]) && + 3342 == read_big_endian_short((const uint8_t*) &table[1025]) && + 14079 == read_big_endian_short((const uint8_t*) &table[2051]) && + 65535 == read_big_endian_short((const uint8_t*) &table[4095])) { + gammas[i].fNamed = SkColorSpace::kSRGB_GammaNamed; + break; + } + } + + // Otherwise, fill in the interpolation table. + gammas[i].fTableSize = count; + gammas[i].fTable = std::unique_ptr(new float[count]); + for (uint32_t j = 0; j < count; j++) { + gammas[i].fTable[j] = + (read_big_endian_short((const uint8_t*) &table[j])) / 65535.0f; + } + break; + } + case kTAG_ParaCurveType: { + enum ParaCurveType { + kExponential_ParaCurveType = 0, + kGAB_ParaCurveType = 1, + kGABC_ParaCurveType = 2, + kGABDE_ParaCurveType = 3, + kGABCDEF_ParaCurveType = 4, + }; + + // Determine the format of the parametric curve tag. + uint16_t format = read_big_endian_short(src + 8); + if (kExponential_ParaCurveType == format) { + tagBytes = 12 + 4; + if (len < tagBytes) { + SkColorSpacePrintf("gamma tag is too small (%d bytes)", len); + return false; + } + + // Y = X^g + int32_t g = read_big_endian_int(src + 12); + set_gamma_value(&gammas[i], SkFixedToFloat(g)); + } else { + // Here's where the real parametric gammas start. There are many + // permutations of the same equations. + // + // Y = (aX + b)^g + c for X >= d + // Y = eX + f otherwise + // + // We will fill in with zeros as necessary to always match the above form. + float g = 0.0f, a = 0.0f, b = 0.0f, c = 0.0f, d = 0.0f, e = 0.0f, f = 0.0f; + switch(format) { + case kGAB_ParaCurveType: { + tagBytes = 12 + 12; + if (len < tagBytes) { + SkColorSpacePrintf("gamma tag is too small (%d bytes)", len); + return false; + } + + // Y = (aX + b)^g for X >= -b/a + // Y = 0 otherwise + g = SkFixedToFloat(read_big_endian_int(src + 12)); + a = SkFixedToFloat(read_big_endian_int(src + 16)); + if (0.0f == a) { + return false; + } + + b = SkFixedToFloat(read_big_endian_int(src + 20)); + d = -b / a; + break; + } + case kGABC_ParaCurveType: + tagBytes = 12 + 16; + if (len < tagBytes) { + SkColorSpacePrintf("gamma tag is too small (%d bytes)", len); + return false; + } + + // Y = (aX + b)^g + c for X >= -b/a + // Y = c otherwise + g = SkFixedToFloat(read_big_endian_int(src + 12)); + a = SkFixedToFloat(read_big_endian_int(src + 16)); + if (0.0f == a) { + return false; + } + + b = SkFixedToFloat(read_big_endian_int(src + 20)); + c = SkFixedToFloat(read_big_endian_int(src + 24)); + d = -b / a; + f = c; + break; + case kGABDE_ParaCurveType: + tagBytes = 12 + 20; + if (len < tagBytes) { + SkColorSpacePrintf("gamma tag is too small (%d bytes)", len); + return false; + } + + // Y = (aX + b)^g for X >= d + // Y = cX otherwise + g = SkFixedToFloat(read_big_endian_int(src + 12)); + a = SkFixedToFloat(read_big_endian_int(src + 16)); + b = SkFixedToFloat(read_big_endian_int(src + 20)); + d = SkFixedToFloat(read_big_endian_int(src + 28)); + e = SkFixedToFloat(read_big_endian_int(src + 24)); + break; + case kGABCDEF_ParaCurveType: + tagBytes = 12 + 28; + if (len < tagBytes) { + SkColorSpacePrintf("gamma tag is too small (%d bytes)", len); + return false; + } + + // Y = (aX + b)^g + c for X >= d + // Y = eX + f otherwise + // NOTE: The ICC spec writes "cX" in place of "eX" but I think + // it's a typo. + g = SkFixedToFloat(read_big_endian_int(src + 12)); + a = SkFixedToFloat(read_big_endian_int(src + 16)); + b = SkFixedToFloat(read_big_endian_int(src + 20)); + c = SkFixedToFloat(read_big_endian_int(src + 24)); + d = SkFixedToFloat(read_big_endian_int(src + 28)); + e = SkFixedToFloat(read_big_endian_int(src + 32)); + f = SkFixedToFloat(read_big_endian_int(src + 36)); + break; + default: + SkColorSpacePrintf("Invalid parametric curve type\n"); + return false; + } + + // Recognize and simplify a very common parametric representation of sRGB gamma. + if (color_space_almost_equal(0.9479f, a) && + color_space_almost_equal(0.0521f, b) && + color_space_almost_equal(0.0000f, c) && + color_space_almost_equal(0.0405f, d) && + color_space_almost_equal(0.0774f, e) && + color_space_almost_equal(0.0000f, f) && + color_space_almost_equal(2.4000f, g)) { + gammas[i].fNamed = SkColorSpace::kSRGB_GammaNamed; + } else { + // Fail on invalid gammas. + if (d <= 0.0f) { + // Y = (aX + b)^g + c for always + if (0.0f == a || 0.0f == g) { + SkColorSpacePrintf("A or G is zero, constant gamma function " + "is nonsense"); + return false; + } + } else if (d >= 1.0f) { + // Y = eX + f for always + if (0.0f == e) { + SkColorSpacePrintf("E is zero, constant gamma function is " + "nonsense"); + return false; + } + } else if ((0.0f == a || 0.0f == g) && 0.0f == e) { + SkColorSpacePrintf("A or G, and E are zero, constant gamma function " + "is nonsense"); + return false; + } + + gammas[i].fG = g; + gammas[i].fA = a; + gammas[i].fB = b; + gammas[i].fC = c; + gammas[i].fD = d; + gammas[i].fE = e; + gammas[i].fF = f; + } + } + + break; + } + default: + SkColorSpacePrintf("Unsupported gamma tag type %d\n", type); + return false; + } + + // Ensure that we have successfully read a gamma representation. + SkASSERT(gammas[i].isNamed() || gammas[i].isValue() || gammas[i].isTable() || + gammas[i].isParametric()); + + // Adjust src and len if there is another gamma curve to load. + if (i != numGammas - 1) { + // Each curve is padded to 4-byte alignment. + tagBytes = SkAlign4(tagBytes); + if (len < tagBytes) { + return false; + } + + src += tagBytes; + len -= tagBytes; + } + } + + return true; +} + +static constexpr uint32_t kTAG_AtoBType = SkSetFourByteTag('m', 'A', 'B', ' '); + +bool load_color_lut(SkColorLookUpTable* colorLUT, uint32_t inputChannels, uint32_t outputChannels, + const uint8_t* src, size_t len) { + // 16 bytes reserved for grid points, 2 for precision, 2 for padding. + // The color LUT data follows after this header. + static constexpr uint32_t kColorLUTHeaderSize = 20; + if (len < kColorLUTHeaderSize) { + SkColorSpacePrintf("Color LUT tag is too small (%d bytes).", len); + return false; + } + size_t dataLen = len - kColorLUTHeaderSize; + + SkASSERT(3 == inputChannels && 3 == outputChannels); + colorLUT->fInputChannels = inputChannels; + colorLUT->fOutputChannels = outputChannels; + uint32_t numEntries = 1; + for (uint32_t i = 0; i < inputChannels; i++) { + colorLUT->fGridPoints[i] = src[i]; + if (0 == src[i]) { + SkColorSpacePrintf("Each input channel must have at least one grid point."); + return false; + } + + if (!safe_mul(numEntries, src[i], &numEntries)) { + SkColorSpacePrintf("Too many entries in Color LUT."); + return false; + } + } + + if (!safe_mul(numEntries, outputChannels, &numEntries)) { + SkColorSpacePrintf("Too many entries in Color LUT."); + return false; + } + + // Space is provided for a maximum of the 16 input channels. Now we determine the precision + // of the table values. + uint8_t precision = src[16]; + switch (precision) { + case 1: // 8-bit data + case 2: // 16-bit data + break; + default: + SkColorSpacePrintf("Color LUT precision must be 8-bit or 16-bit.\n"); + return false; + } + + uint32_t clutBytes; + if (!safe_mul(numEntries, precision, &clutBytes)) { + SkColorSpacePrintf("Too many entries in Color LUT."); + return false; + } + + if (dataLen < clutBytes) { + SkColorSpacePrintf("Color LUT tag is too small (%d bytes).", len); + return false; + } + + // Movable struct colorLUT has ownership of fTable. + colorLUT->fTable = std::unique_ptr(new float[numEntries]); + const uint8_t* ptr = src + kColorLUTHeaderSize; + for (uint32_t i = 0; i < numEntries; i++, ptr += precision) { + if (1 == precision) { + colorLUT->fTable[i] = ((float) ptr[i]) / 255.0f; + } else { + colorLUT->fTable[i] = ((float) read_big_endian_short(ptr)) / 65535.0f; + } + } + + return true; +} + +bool load_matrix(SkMatrix44* toXYZ, const uint8_t* src, size_t len) { + if (len < 48) { + SkColorSpacePrintf("Matrix tag is too small (%d bytes).", len); + return false; + } + + // For this matrix to behave like our "to XYZ D50" matrices, it needs to be scaled. + constexpr float scale = 65535.0 / 32768.0; + float array[16]; + array[ 0] = scale * SkFixedToFloat(read_big_endian_int(src)); + array[ 1] = scale * SkFixedToFloat(read_big_endian_int(src + 4)); + array[ 2] = scale * SkFixedToFloat(read_big_endian_int(src + 8)); + array[ 3] = scale * SkFixedToFloat(read_big_endian_int(src + 36)); // translate R + array[ 4] = scale * SkFixedToFloat(read_big_endian_int(src + 12)); + array[ 5] = scale * SkFixedToFloat(read_big_endian_int(src + 16)); + array[ 6] = scale * SkFixedToFloat(read_big_endian_int(src + 20)); + array[ 7] = scale * SkFixedToFloat(read_big_endian_int(src + 40)); // translate G + array[ 8] = scale * SkFixedToFloat(read_big_endian_int(src + 24)); + array[ 9] = scale * SkFixedToFloat(read_big_endian_int(src + 28)); + array[10] = scale * SkFixedToFloat(read_big_endian_int(src + 32)); + array[11] = scale * SkFixedToFloat(read_big_endian_int(src + 44)); // translate B + array[12] = 0.0f; + array[13] = 0.0f; + array[14] = 0.0f; + array[15] = 1.0f; + toXYZ->setColMajorf(array); + return true; +} + +bool load_a2b0(SkColorLookUpTable* colorLUT, SkGammaCurve* gammas, SkMatrix44* toXYZ, + const uint8_t* src, size_t len) { + if (len < 32) { + SkColorSpacePrintf("A to B tag is too small (%d bytes).", len); + return false; + } + + uint32_t type = read_big_endian_uint(src); + if (kTAG_AtoBType != type) { + // FIXME (msarett): Need to support lut8Type and lut16Type. + SkColorSpacePrintf("Unsupported A to B tag type.\n"); + return false; + } + + // Read the number of channels. The four bytes that we skipped are reserved and + // must be zero. + uint8_t inputChannels = src[8]; + uint8_t outputChannels = src[9]; + if (3 != inputChannels || 3 != outputChannels) { + // We only handle (supposedly) RGB inputs and RGB outputs. The numbers of input + // channels and output channels both must be 3. + SkColorSpacePrintf("Input and output channels must equal 3 in A to B tag.\n"); + return false; + } + + // Read the offsets of each element in the A to B tag. With the exception of A curves and + // B curves (which we do not yet support), we will handle these elements in the order in + // which they should be applied (rather than the order in which they occur in the tag). + // If the offset is non-zero it indicates that the element is present. + uint32_t offsetToACurves = read_big_endian_int(src + 28); + uint32_t offsetToBCurves = read_big_endian_int(src + 12); + if ((0 != offsetToACurves) || (0 != offsetToBCurves)) { + // FIXME (msarett): Handle A and B curves. + // Note that the A curve is technically required in order to have a color LUT. + // However, all the A curves I have seen so far have are just placeholders that + // don't actually transform the data. + SkColorSpacePrintf("Ignoring A and/or B curve. Output may be wrong.\n"); + } + + uint32_t offsetToColorLUT = read_big_endian_int(src + 24); + if (0 != offsetToColorLUT && offsetToColorLUT < len) { + if (!load_color_lut(colorLUT, inputChannels, outputChannels, src + offsetToColorLUT, + len - offsetToColorLUT)) { + SkColorSpacePrintf("Failed to read color LUT from A to B tag.\n"); + } + } + + uint32_t offsetToMCurves = read_big_endian_int(src + 20); + if (0 != offsetToMCurves && offsetToMCurves < len) { + if (!load_gammas(gammas, outputChannels, src + offsetToMCurves, len - offsetToMCurves)) { + SkColorSpacePrintf("Failed to read M curves from A to B tag. Using linear gamma.\n"); + gammas[0].fNamed = SkColorSpace::kLinear_GammaNamed; + gammas[1].fNamed = SkColorSpace::kLinear_GammaNamed; + gammas[2].fNamed = SkColorSpace::kLinear_GammaNamed; + } + } + + uint32_t offsetToMatrix = read_big_endian_int(src + 16); + if (0 != offsetToMatrix && offsetToMatrix < len) { + if (!load_matrix(toXYZ, src + offsetToMatrix, len - offsetToMatrix)) { + SkColorSpacePrintf("Failed to read matrix from A to B tag.\n"); + toXYZ->setIdentity(); + } + } + + return true; +} + +sk_sp SkColorSpace::NewICC(const void* input, size_t len) { + if (!input || len < kICCHeaderSize) { + return_null("Data is null or not large enough to contain an ICC profile"); + } + + // Create our own copy of the input. + void* memory = sk_malloc_throw(len); + memcpy(memory, input, len); + sk_sp data = SkData::MakeFromMalloc(memory, len); + const void* base = data->data(); + const uint8_t* ptr = (const uint8_t*) base; + + // Read the ICC profile header and check to make sure that it is valid. + ICCProfileHeader header; + header.init(ptr, len); + if (!header.valid()) { + return nullptr; + } + + // Adjust ptr and len before reading the tags. + if (len < header.fSize) { + SkColorSpacePrintf("ICC profile might be truncated.\n"); + } else if (len > header.fSize) { + SkColorSpacePrintf("Caller provided extra data beyond the end of the ICC profile.\n"); + len = header.fSize; + } + ptr += kICCHeaderSize; + len -= kICCHeaderSize; + + // Parse tag headers. + uint32_t tagCount = header.fTagCount; + SkColorSpacePrintf("ICC profile contains %d tags.\n", tagCount); + if (len < kICCTagTableEntrySize * tagCount) { + return_null("Not enough input data to read tag table entries"); + } + + SkAutoTArray tags(tagCount); + for (uint32_t i = 0; i < tagCount; i++) { + ptr = tags[i].init(ptr); + SkColorSpacePrintf("[%d] %c%c%c%c %d %d\n", i, (tags[i].fSignature >> 24) & 0xFF, + (tags[i].fSignature >> 16) & 0xFF, (tags[i].fSignature >> 8) & 0xFF, + (tags[i].fSignature >> 0) & 0xFF, tags[i].fOffset, tags[i].fLength); + + if (!tags[i].valid(kICCHeaderSize + len)) { + return_null("Tag is too large to fit in ICC profile"); + } + } + + switch (header.fInputColorSpace) { + case kRGB_ColorSpace: { + // Recognize the rXYZ, gXYZ, and bXYZ tags. + const ICCTag* r = ICCTag::Find(tags.get(), tagCount, kTAG_rXYZ); + const ICCTag* g = ICCTag::Find(tags.get(), tagCount, kTAG_gXYZ); + const ICCTag* b = ICCTag::Find(tags.get(), tagCount, kTAG_bXYZ); + if (r && g && b) { + float toXYZ[9]; + if (!load_xyz(&toXYZ[0], r->addr((const uint8_t*) base), r->fLength) || + !load_xyz(&toXYZ[3], g->addr((const uint8_t*) base), g->fLength) || + !load_xyz(&toXYZ[6], b->addr((const uint8_t*) base), b->fLength)) + { + return_null("Need valid rgb tags for XYZ space"); + } + SkMatrix44 mat(SkMatrix44::kUninitialized_Constructor); + mat.set3x3RowMajorf(toXYZ); + + // It is not uncommon to see missing or empty gamma tags. This indicates + // that we should use unit gamma. + SkGammaCurve curves[3]; + r = ICCTag::Find(tags.get(), tagCount, kTAG_rTRC); + g = ICCTag::Find(tags.get(), tagCount, kTAG_gTRC); + b = ICCTag::Find(tags.get(), tagCount, kTAG_bTRC); + if (!r || !load_gammas(&curves[0], 1, r->addr((const uint8_t*) base), r->fLength)) + { + SkColorSpacePrintf("Failed to read R gamma tag.\n"); + curves[0].fNamed = SkColorSpace::kLinear_GammaNamed; + } + if (!g || !load_gammas(&curves[1], 1, g->addr((const uint8_t*) base), g->fLength)) + { + SkColorSpacePrintf("Failed to read G gamma tag.\n"); + curves[1].fNamed = SkColorSpace::kLinear_GammaNamed; + } + if (!b || !load_gammas(&curves[2], 1, b->addr((const uint8_t*) base), b->fLength)) + { + SkColorSpacePrintf("Failed to read B gamma tag.\n"); + curves[2].fNamed = SkColorSpace::kLinear_GammaNamed; + } + + GammaNamed gammaNamed = SkGammas::Named(curves); + if (kNonStandard_GammaNamed == gammaNamed) { + sk_sp gammas = sk_make_sp(std::move(curves[0]), + std::move(curves[1]), + std::move(curves[2])); + return sk_sp(new SkColorSpace_Base(nullptr, std::move(gammas), + mat, std::move(data))); + } else { + return SkColorSpace_Base::NewRGB(gammaNamed, mat); + } + } + + // Recognize color profile specified by A2B0 tag. + const ICCTag* a2b0 = ICCTag::Find(tags.get(), tagCount, kTAG_A2B0); + if (a2b0) { + sk_sp colorLUT = sk_make_sp(); + SkGammaCurve curves[3]; + SkMatrix44 toXYZ(SkMatrix44::kUninitialized_Constructor); + if (!load_a2b0(colorLUT.get(), curves, &toXYZ, a2b0->addr((const uint8_t*) base), + a2b0->fLength)) { + return_null("Failed to parse A2B0 tag"); + } + + GammaNamed gammaNamed = SkGammas::Named(curves); + colorLUT = colorLUT->fTable ? colorLUT : nullptr; + if (colorLUT || kNonStandard_GammaNamed == gammaNamed) { + sk_sp gammas = sk_make_sp(std::move(curves[0]), + std::move(curves[1]), + std::move(curves[2])); + + return sk_sp(new SkColorSpace_Base(std::move(colorLUT), + std::move(gammas), toXYZ, + std::move(data))); + } else { + return SkColorSpace_Base::NewRGB(gammaNamed, toXYZ); + } + } + } + default: + break; + } + + return_null("ICC profile contains unsupported colorspace"); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +// We will write a profile with the minimum nine required tags. +static constexpr uint32_t kICCNumEntries = 9; + +static constexpr uint32_t kTAG_desc = SkSetFourByteTag('d', 'e', 's', 'c'); +static constexpr uint32_t kTAG_desc_Bytes = 12; +static constexpr uint32_t kTAG_desc_Offset = kICCHeaderSize + kICCNumEntries*kICCTagTableEntrySize; + +static constexpr uint32_t kTAG_XYZ_Bytes = 20; +static constexpr uint32_t kTAG_rXYZ_Offset = kTAG_desc_Offset + kTAG_desc_Bytes; +static constexpr uint32_t kTAG_gXYZ_Offset = kTAG_rXYZ_Offset + kTAG_XYZ_Bytes; +static constexpr uint32_t kTAG_bXYZ_Offset = kTAG_gXYZ_Offset + kTAG_XYZ_Bytes; + +static constexpr uint32_t kTAG_TRC_Bytes = 14; +static constexpr uint32_t kTAG_rTRC_Offset = kTAG_bXYZ_Offset + kTAG_XYZ_Bytes; +static constexpr uint32_t kTAG_gTRC_Offset = kTAG_rTRC_Offset + SkAlign4(kTAG_TRC_Bytes); +static constexpr uint32_t kTAG_bTRC_Offset = kTAG_gTRC_Offset + SkAlign4(kTAG_TRC_Bytes); + +static constexpr uint32_t kTAG_wtpt = SkSetFourByteTag('w', 't', 'p', 't'); +static constexpr uint32_t kTAG_wtpt_Offset = kTAG_bTRC_Offset + SkAlign4(kTAG_TRC_Bytes); + +static constexpr uint32_t kTAG_cprt = SkSetFourByteTag('c', 'p', 'r', 't'); +static constexpr uint32_t kTAG_cprt_Bytes = 12; +static constexpr uint32_t kTAG_cprt_Offset = kTAG_wtpt_Offset + kTAG_XYZ_Bytes; + +static constexpr uint32_t kICCProfileSize = kTAG_cprt_Offset + kTAG_cprt_Bytes; + +static constexpr uint32_t gICCHeader[kICCHeaderSize / 4] { + SkEndian_SwapBE32(kICCProfileSize), // Size of the profile + 0, // Preferred CMM type (ignored) + SkEndian_SwapBE32(0x02100000), // Version 2.1 + SkEndian_SwapBE32(kDisplay_Profile), // Display device profile + SkEndian_SwapBE32(kRGB_ColorSpace), // RGB input color space + SkEndian_SwapBE32(kXYZ_PCSSpace), // XYZ profile connection space + 0, 0, 0, // Date and time (ignored) + SkEndian_SwapBE32(kACSP_Signature), // Profile signature + 0, // Platform target (ignored) + 0x00000000, // Flags: not embedded, can be used independently + 0, // Device manufacturer (ignored) + 0, // Device model (ignored) + 0, 0, // Device attributes (ignored) + SkEndian_SwapBE32(1), // Relative colorimetric rendering intent + SkEndian_SwapBE32(0x0000f6d6), // D50 standard illuminant (X) + SkEndian_SwapBE32(0x00010000), // D50 standard illuminant (Y) + SkEndian_SwapBE32(0x0000d32d), // D50 standard illuminant (Z) + 0, // Profile creator (ignored) + 0, 0, 0, 0, // Profile id checksum (ignored) + 0, 0, 0, 0, 0, 0, 0, // Reserved (ignored) + SkEndian_SwapBE32(kICCNumEntries), // Number of tags +}; + +static constexpr uint32_t gICCTagTable[3 * kICCNumEntries] { + // Profile description + SkEndian_SwapBE32(kTAG_desc), + SkEndian_SwapBE32(kTAG_desc_Offset), + SkEndian_SwapBE32(kTAG_desc_Bytes), + + // rXYZ + SkEndian_SwapBE32(kTAG_rXYZ), + SkEndian_SwapBE32(kTAG_rXYZ_Offset), + SkEndian_SwapBE32(kTAG_XYZ_Bytes), + + // gXYZ + SkEndian_SwapBE32(kTAG_gXYZ), + SkEndian_SwapBE32(kTAG_gXYZ_Offset), + SkEndian_SwapBE32(kTAG_XYZ_Bytes), + + // bXYZ + SkEndian_SwapBE32(kTAG_bXYZ), + SkEndian_SwapBE32(kTAG_bXYZ_Offset), + SkEndian_SwapBE32(kTAG_XYZ_Bytes), + + // rTRC + SkEndian_SwapBE32(kTAG_rTRC), + SkEndian_SwapBE32(kTAG_rTRC_Offset), + SkEndian_SwapBE32(kTAG_TRC_Bytes), + + // gTRC + SkEndian_SwapBE32(kTAG_gTRC), + SkEndian_SwapBE32(kTAG_gTRC_Offset), + SkEndian_SwapBE32(kTAG_TRC_Bytes), + + // bTRC + SkEndian_SwapBE32(kTAG_bTRC), + SkEndian_SwapBE32(kTAG_bTRC_Offset), + SkEndian_SwapBE32(kTAG_TRC_Bytes), + + // White point + SkEndian_SwapBE32(kTAG_wtpt), + SkEndian_SwapBE32(kTAG_wtpt_Offset), + SkEndian_SwapBE32(kTAG_XYZ_Bytes), + + // Copyright + SkEndian_SwapBE32(kTAG_cprt), + SkEndian_SwapBE32(kTAG_cprt_Offset), + SkEndian_SwapBE32(kTAG_cprt_Bytes), +}; + +static constexpr uint32_t kTAG_TextType = SkSetFourByteTag('m', 'l', 'u', 'c'); +static constexpr uint32_t gEmptyTextTag[3] { + SkEndian_SwapBE32(kTAG_TextType), // Type signature + 0, // Reserved + 0, // Zero records +}; + +static void write_xyz_tag(uint32_t* ptr, const SkMatrix44& toXYZ, int row) { + ptr[0] = SkEndian_SwapBE32(kXYZ_PCSSpace); + ptr[1] = 0; + ptr[2] = SkEndian_SwapBE32(SkFloatToFixed(toXYZ.getFloat(row, 0))); + ptr[3] = SkEndian_SwapBE32(SkFloatToFixed(toXYZ.getFloat(row, 1))); + ptr[4] = SkEndian_SwapBE32(SkFloatToFixed(toXYZ.getFloat(row, 2))); +} + +static void write_trc_tag(uint32_t* ptr, float value) { + ptr[0] = SkEndian_SwapBE32(kTAG_CurveType); + ptr[1] = 0; + + // Gamma will be specified with a single value. + ptr[2] = SkEndian_SwapBE32(1); + + // Convert gamma to 16-bit fixed point. + uint16_t* ptr16 = (uint16_t*) (ptr + 3); + ptr16[0] = SkEndian_SwapBE16((uint16_t) (value * 256.0f)); + + // Pad tag with zero. + ptr16[1] = 0; +} + +static float get_gamma_value(const SkGammaCurve* curve) { + switch (curve->fNamed) { + case SkColorSpace::kSRGB_GammaNamed: + // FIXME (msarett): + // kSRGB cannot be represented by a value. Here we fall through to 2.2f, + // which is a close guess. To be more accurate, we need to represent sRGB + // gamma with a parametric curve. + case SkColorSpace::k2Dot2Curve_GammaNamed: + return 2.2f; + case SkColorSpace::kLinear_GammaNamed: + return 1.0f; + default: + SkASSERT(curve->isValue()); + return curve->fValue; + } +} + +sk_sp SkColorSpace_Base::writeToICC() const { + // Return if this object was created from a profile, or if we have already serialized + // the profile. + if (fProfileData) { + return fProfileData; + } + + // The client may create an SkColorSpace using an SkMatrix44, but currently we only + // support writing profiles with 3x3 matrices. + // TODO (msarett): Fix this! + if (0.0f != fToXYZD50.getFloat(3, 0) || 0.0f != fToXYZD50.getFloat(3, 1) || + 0.0f != fToXYZD50.getFloat(3, 2) || 0.0f != fToXYZD50.getFloat(0, 3) || + 0.0f != fToXYZD50.getFloat(1, 3) || 0.0f != fToXYZD50.getFloat(2, 3)) + { + return nullptr; + } + + SkAutoMalloc profile(kICCProfileSize); + uint8_t* ptr = (uint8_t*) profile.get(); + + // Write profile header + memcpy(ptr, gICCHeader, sizeof(gICCHeader)); + ptr += sizeof(gICCHeader); + + // Write tag table + memcpy(ptr, gICCTagTable, sizeof(gICCTagTable)); + ptr += sizeof(gICCTagTable); + + // Write profile description tag + memcpy(ptr, gEmptyTextTag, sizeof(gEmptyTextTag)); + ptr += sizeof(gEmptyTextTag); + + // Write XYZ tags + write_xyz_tag((uint32_t*) ptr, fToXYZD50, 0); + ptr += kTAG_XYZ_Bytes; + write_xyz_tag((uint32_t*) ptr, fToXYZD50, 1); + ptr += kTAG_XYZ_Bytes; + write_xyz_tag((uint32_t*) ptr, fToXYZD50, 2); + ptr += kTAG_XYZ_Bytes; + + // Write TRC tags + GammaNamed gammaNamed = this->gammaNamed(); + if (kNonStandard_GammaNamed == gammaNamed) { + write_trc_tag((uint32_t*) ptr, get_gamma_value(&as_CSB(this)->fGammas->fRed)); + ptr += SkAlign4(kTAG_TRC_Bytes); + write_trc_tag((uint32_t*) ptr, get_gamma_value(&as_CSB(this)->fGammas->fGreen)); + ptr += SkAlign4(kTAG_TRC_Bytes); + write_trc_tag((uint32_t*) ptr, get_gamma_value(&as_CSB(this)->fGammas->fBlue)); + ptr += SkAlign4(kTAG_TRC_Bytes); + } else { + switch (gammaNamed) { + case SkColorSpace::kSRGB_GammaNamed: + // FIXME (msarett): + // kSRGB cannot be represented by a value. Here we fall through to 2.2f, + // which is a close guess. To be more accurate, we need to represent sRGB + // gamma with a parametric curve. + case SkColorSpace::k2Dot2Curve_GammaNamed: + write_trc_tag((uint32_t*) ptr, 2.2f); + ptr += SkAlign4(kTAG_TRC_Bytes); + write_trc_tag((uint32_t*) ptr, 2.2f); + ptr += SkAlign4(kTAG_TRC_Bytes); + write_trc_tag((uint32_t*) ptr, 2.2f); + ptr += SkAlign4(kTAG_TRC_Bytes); + break; + case SkColorSpace::kLinear_GammaNamed: + write_trc_tag((uint32_t*) ptr, 1.0f); + ptr += SkAlign4(kTAG_TRC_Bytes); + write_trc_tag((uint32_t*) ptr, 1.0f); + ptr += SkAlign4(kTAG_TRC_Bytes); + write_trc_tag((uint32_t*) ptr, 1.0f); + ptr += SkAlign4(kTAG_TRC_Bytes); + break; + default: + SkASSERT(false); + break; + } + } + + // Write white point tag + uint32_t* ptr32 = (uint32_t*) ptr; + ptr32[0] = SkEndian_SwapBE32(kXYZ_PCSSpace); + ptr32[1] = 0; + // TODO (msarett): These values correspond to the D65 white point. This may not always be + // correct. + ptr32[2] = SkEndian_SwapBE32(0x0000f351); + ptr32[3] = SkEndian_SwapBE32(0x00010000); + ptr32[4] = SkEndian_SwapBE32(0x000116cc); + ptr += kTAG_XYZ_Bytes; + + // Write copyright tag + memcpy(ptr, gEmptyTextTag, sizeof(gEmptyTextTag)); + + // TODO (msarett): Should we try to hold onto the data so we can return immediately if + // the client calls again? + return SkData::MakeFromMalloc(profile.release(), kICCProfileSize); +} -- cgit v1.2.3