diff options
Diffstat (limited to 'src/core/SkColorSpace_ICC.cpp')
-rw-r--r-- | src/core/SkColorSpace_ICC.cpp | 1054 |
1 files changed, 1054 insertions, 0 deletions
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 <class T> +static bool safe_add(T arg1, T arg2, size_t* result) { + SkASSERT(arg1 >= 0); + SkASSERT(arg2 >= 0); + if (arg1 >= 0 && arg2 <= std::numeric_limits<T>::max() - arg1) { + T sum = arg1 + arg2; + if (sum <= std::numeric_limits<size_t>::max()) { + *result = static_cast<size_t>(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<float[]>(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<float[]>(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> 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<SkData> 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<ICCTag> 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<SkGammas> gammas = sk_make_sp<SkGammas>(std::move(curves[0]), + std::move(curves[1]), + std::move(curves[2])); + return sk_sp<SkColorSpace>(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<SkColorLookUpTable> colorLUT = sk_make_sp<SkColorLookUpTable>(); + 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<SkGammas> gammas = sk_make_sp<SkGammas>(std::move(curves[0]), + std::move(curves[1]), + std::move(curves[2])); + + return sk_sp<SkColorSpace>(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<SkData> 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); +} |