Set SkColorSpace object for PNGs and parse ICC profiles
authormsarett <msarett@google.com>
Fri, 4 Mar 2016 21:27:35 +0000 (13:27 -0800)
committerCommit bot <commit-bot@chromium.org>
Fri, 4 Mar 2016 21:27:35 +0000 (13:27 -0800)
Code for ICC profile parsing adapted from:
https://codereview.chromium.org/1707033002/

BUG=skia:
GOLD_TRYBOT_URL= https://gold.skia.org/search2?unt=true&query=source_type%3Dgm&master=false&issue=1726823002

Review URL: https://codereview.chromium.org/1726823002

gyp/codec_android.gyp
gyp/tools.gyp
include/codec/SkCodec.h
resources/color_wheel_with_profile.png [new file with mode: 0644]
src/codec/SkCodec.cpp
src/codec/SkPngCodec.cpp
src/codec/SkPngCodec.h
src/core/SkColorSpace.cpp
src/core/SkColorSpace.h
tests/ColorSpaceTest.cpp [new file with mode: 0644]

index ca9c7a36b528b17ff57be2521538cd055f6e9ec9..a303494e435b18a102cdefeca6893a689ec12bb2 100644 (file)
@@ -25,6 +25,7 @@
         '../include/private',
         '../src/android',
         '../src/codec',
+        '../src/core',
       ],
       'sources': [
         '../src/android/SkBitmapRegionCanvas.cpp',
index e60879bc5c0566991fb708e37a2d389ce322ac15..49e441b9e237ba0c63064216437f4d243b55d495 100644 (file)
       ],
     },
     {
-        'target_name': 'get_images_from_skps',
-        'type': 'executable',
-        'sources': [
-            '../tools/get_images_from_skps.cpp',
-         ],
-         'include_dirs': [
-             '../include/private',
-             '../src/core',
-         ],
-         'dependencies': [
-            'flags.gyp:flags',
-            'skia_lib.gyp:skia_lib',
-         ],
+      'target_name': 'get_images_from_skps',
+      'type': 'executable',
+      'sources': [
+        '../tools/get_images_from_skps.cpp',
+      ],
+      'include_dirs': [
+        '../src/core',
+        '../include/private',
+      ],
+      'dependencies': [
+        'flags.gyp:flags',
+        'skia_lib.gyp:skia_lib',
+      ],
     },
     {
       'target_name': 'gpuveto',
       },
       'include_dirs': [
         '<@(includes_to_test)',
+        '../src/core',
       ],
       'sources': [
         # unused_param_test.cpp is generated by the action below.
index c3a5873d7f9fb77e03027f684d573061bc9c3dd2..78295eb1fc5a393216bf6e856071ab0da41cf4f6 100644 (file)
@@ -10,6 +10,7 @@
 
 #include "../private/SkTemplates.h"
 #include "SkColor.h"
+#include "SkColorSpace.h"
 #include "SkEncodedFormat.h"
 #include "SkImageInfo.h"
 #include "SkSize.h"
@@ -98,6 +99,13 @@ public:
      */
     const SkImageInfo& getInfo() const { return fSrcInfo; }
 
+    /**
+     *  Returns the color space associated with the codec.
+     *  Does not affect ownership.
+     *  Might be NULL.
+     */
+    SkColorSpace* getColorSpace() const { return fColorSpace; }
+
     /**
      *  Return a size that approximately supports the desired scale factor.
      *  The codec may not be able to scale efficiently to the exact scale
@@ -502,7 +510,11 @@ public:
     int outputScanline(int inputScanline) const;
 
 protected:
-    SkCodec(const SkImageInfo&, SkStream*);
+    /**
+     *  Takes ownership of SkStream*
+     *  Does not affect ownership of SkColorSpace*
+     */
+    SkCodec(const SkImageInfo&, SkStream*, SkColorSpace* = nullptr);
 
     virtual SkISize onGetScaledDimensions(float /*desiredScale*/) const {
         // By default, scaling is not supported.
@@ -630,13 +642,15 @@ protected:
     virtual int onOutputScanline(int inputScanline) const;
 
 private:
-    const SkImageInfo       fSrcInfo;
-    SkAutoTDelete<SkStream> fStream;
-    bool                    fNeedsRewind;
+    const SkImageInfo           fSrcInfo;
+    SkAutoTDelete<SkStream>     fStream;
+    bool                        fNeedsRewind;
+    SkAutoTUnref<SkColorSpace>  fColorSpace;
+
     // These fields are only meaningful during scanline decodes.
-    SkImageInfo             fDstInfo;
-    SkCodec::Options        fOptions;
-    int                     fCurrScanline;
+    SkImageInfo                 fDstInfo;
+    SkCodec::Options            fOptions;
+    int                         fCurrScanline;
 
     /**
      *  Return whether these dimensions are supported as a scale.
diff --git a/resources/color_wheel_with_profile.png b/resources/color_wheel_with_profile.png
new file mode 100644 (file)
index 0000000..56f2385
Binary files /dev/null and b/resources/color_wheel_with_profile.png differ
index d7cfb3749708f04e4f3d2adba765c82205dce4f3..4a1902dc53faf4b089c88fe01459491b4557d475 100644 (file)
@@ -113,10 +113,11 @@ SkCodec* SkCodec::NewFromData(SkData* data, SkPngChunkReader* reader) {
     return NewFromStream(new SkMemoryStream(data), reader);
 }
 
-SkCodec::SkCodec(const SkImageInfo& info, SkStream* stream)
+SkCodec::SkCodec(const SkImageInfo& info, SkStream* stream, SkColorSpace* colorSpace)
     : fSrcInfo(info)
     , fStream(stream)
     , fNeedsRewind(false)
+    , fColorSpace(colorSpace ? SkRef(colorSpace) : nullptr)
     , fDstInfo()
     , fOptions()
     , fCurrScanline(-1)
index f946876af2734dd7f660066d68efaa0e91585363..1b51432e5ec1c1bb143ca69972d08968ce1a8c5f 100644 (file)
@@ -165,6 +165,97 @@ bool SkPngCodec::IsPng(const char* buf, size_t bytesRead) {
     return !png_sig_cmp((png_bytep) buf, (png_size_t)0, bytesRead);
 }
 
+static float png_fixed_point_to_float(png_fixed_point x) {
+    // We multiply by the same factor that libpng used to convert
+    // fixed point -> double.  Since we want floats, we choose to
+    // do the conversion ourselves rather than convert
+    // fixed point -> double -> float.
+    return ((float) x) * 0.00001f;
+}
+
+// Returns a colorSpace object that represents any color space information in
+// the encoded data.  If the encoded data contains no color space, this will
+// return NULL.
+SkColorSpace* read_color_space(png_structp png_ptr, png_infop info_ptr) {
+
+    // First check for an ICC profile
+    png_bytep profile;
+    png_uint_32 length;
+    // The below variables are unused, however, we need to pass them in anyway or
+    // png_get_iCCP() will return nothing.
+    // Could knowing the |name| of the profile ever be interesting?  Maybe for debugging?
+    png_charp name;
+    // The |compression| is uninteresting since:
+    //   (1) libpng has already decompressed the profile for us.
+    //   (2) "deflate" is the only mode of decompression that libpng supports.
+    int compression;
+    if (PNG_INFO_iCCP == png_get_iCCP(png_ptr, info_ptr, &name, &compression, &profile,
+            &length)) {
+        return SkColorSpace::NewICC(profile, length);
+    }
+
+    // Second, check for sRGB.
+    if (png_get_valid(png_ptr, info_ptr, PNG_INFO_sRGB)) {
+
+        // sRGB chunks also store a rendering intent: Absolute, Relative,
+        // Perceptual, and Saturation.
+        // FIXME (msarett): Extract this information from the sRGB chunk once
+        //                  we are able to handle this information in
+        //                  SkColorSpace.
+        return SkColorSpace::NewNamed(SkColorSpace::kSRGB_Named);
+    }
+
+    // Next, check for chromaticities.
+    png_fixed_point XYZ[9];
+    SkFloat3x3 toXYZD50;
+    png_fixed_point gamma;
+    SkFloat3 gammas;
+    if (png_get_cHRM_XYZ_fixed(png_ptr, info_ptr, &XYZ[0], &XYZ[1], &XYZ[2], &XYZ[3], &XYZ[4],
+            &XYZ[5], &XYZ[6], &XYZ[7], &XYZ[8])) {
+
+        // FIXME (msarett): Here we are treating XYZ values as D50 even though the color
+        //                  temperature is unspecified.  I suspect that this assumption
+        //                  is most often ok, but we could also calculate the color
+        //                  temperature (D value) and then convert the XYZ to D50.  Maybe
+        //                  we should add a new constructor to SkColorSpace that accepts
+        //                  XYZ with D-Unkown?
+        for (int i = 0; i < 9; i++) {
+            toXYZD50.fMat[i] = png_fixed_point_to_float(XYZ[i]);
+        }
+
+        if (PNG_INFO_gAMA != png_get_gAMA_fixed(png_ptr, info_ptr, &gamma)) {
+
+            // If the image does not specify gamma, let's choose linear.  Should we default
+            // to sRGB?  Most images are intended to be sRGB (gamma = 2.2f).
+            gamma = PNG_GAMMA_LINEAR;
+        }
+        gammas.fVec[0] = gammas.fVec[1] = gammas.fVec[2] = png_fixed_point_to_float(gamma);
+
+        return SkColorSpace::NewRGB(toXYZD50, gammas);
+    }
+
+    // Last, check for gamma.
+    if (PNG_INFO_gAMA == png_get_gAMA_fixed(png_ptr, info_ptr, &gamma)) {
+
+        // Guess a default value for cHRM?  Or should we just give up?
+        // Here we use the identity matrix as a default.
+        // FIXME (msarett): Should SkFloat3x3 have a method to set the identity matrix?
+        memset(toXYZD50.fMat, 0, 9 * sizeof(float));
+        toXYZD50.fMat[0] = toXYZD50.fMat[4] = toXYZD50.fMat[8] = 1.0f;
+
+        // Set the gammas.
+        gammas.fVec[0] = gammas.fVec[1] = gammas.fVec[2] = png_fixed_point_to_float(gamma);
+
+        return SkColorSpace::NewRGB(toXYZD50, gammas);
+    }
+
+    // Finally, what should we do if there is no color space information in the PNG?
+    // The specification says that this indicates "gamma is unknown" and that the
+    // "color is device dependent".  I'm assuming we can represent this with NULL.
+    // But should we guess sRGB?  Most images are sRGB, even if they don't specify.
+    return nullptr;
+}
+
 // Reads the header and initializes the output fields, if not NULL.
 //
 // @param stream Input data. Will be read to get enough information to properly
@@ -330,8 +421,9 @@ static bool read_header(SkStream* stream, SkPngChunkReader* chunkReader,
 }
 
 SkPngCodec::SkPngCodec(const SkImageInfo& info, SkStream* stream, SkPngChunkReader* chunkReader,
-                       png_structp png_ptr, png_infop info_ptr, int bitDepth, int numberPasses)
-    : INHERITED(info, stream)
+                       png_structp png_ptr, png_infop info_ptr, int bitDepth, int numberPasses,
+                       SkColorSpace* colorSpace)
+    : INHERITED(info, stream, colorSpace)
     , fPngChunkReader(SkSafeRef(chunkReader))
     , fPng_ptr(png_ptr)
     , fInfo_ptr(info_ptr)
@@ -541,8 +633,9 @@ uint32_t SkPngCodec::onGetFillValue(SkColorType colorType) const {
 class SkPngScanlineDecoder : public SkPngCodec {
 public:
     SkPngScanlineDecoder(const SkImageInfo& srcInfo, SkStream* stream,
-            SkPngChunkReader* chunkReader, png_structp png_ptr, png_infop info_ptr, int bitDepth)
-        : INHERITED(srcInfo, stream, chunkReader, png_ptr, info_ptr, bitDepth, 1)
+            SkPngChunkReader* chunkReader, png_structp png_ptr, png_infop info_ptr, int bitDepth,
+            SkColorSpace* colorSpace)
+        : INHERITED(srcInfo, stream, chunkReader, png_ptr, info_ptr, bitDepth, 1, colorSpace)
         , fSrcRow(nullptr)
     {}
 
@@ -607,8 +700,9 @@ class SkPngInterlacedScanlineDecoder : public SkPngCodec {
 public:
     SkPngInterlacedScanlineDecoder(const SkImageInfo& srcInfo, SkStream* stream,
             SkPngChunkReader* chunkReader, png_structp png_ptr, png_infop info_ptr,
-            int bitDepth, int numberPasses)
-        : INHERITED(srcInfo, stream, chunkReader, png_ptr, info_ptr, bitDepth, numberPasses)
+            int bitDepth, int numberPasses, SkColorSpace* colorSpace)
+        : INHERITED(srcInfo, stream, chunkReader, png_ptr, info_ptr, bitDepth, numberPasses,
+                    colorSpace)
         , fHeight(-1)
         , fCanSkipRewind(false)
     {
@@ -739,11 +833,14 @@ SkCodec* SkPngCodec::NewFromStream(SkStream* stream, SkPngChunkReader* chunkRead
         return nullptr;
     }
 
+    SkAutoTUnref<SkColorSpace> colorSpace(read_color_space(png_ptr, info_ptr));
+
     if (1 == numberPasses) {
         return new SkPngScanlineDecoder(imageInfo, streamDeleter.detach(), chunkReader,
-                                        png_ptr, info_ptr, bitDepth);
+                                        png_ptr, info_ptr, bitDepth, colorSpace);
     }
 
     return new SkPngInterlacedScanlineDecoder(imageInfo, streamDeleter.detach(), chunkReader,
-                                              png_ptr, info_ptr, bitDepth, numberPasses);
+                                              png_ptr, info_ptr, bitDepth, numberPasses,
+                                              colorSpace);
 }
index 95fd61316342f5f46dd875f724e3df755b34b6ca..9e6628c86b71923900b2aefaaf25be19c5543ba3 100644 (file)
@@ -41,7 +41,8 @@ protected:
         return fSwizzler;
     }
 
-    SkPngCodec(const SkImageInfo&, SkStream*, SkPngChunkReader*, png_structp, png_infop, int, int);
+    SkPngCodec(const SkImageInfo&, SkStream*, SkPngChunkReader*, png_structp, png_infop, int, int,
+               SkColorSpace*);
 
     png_structp png_ptr() { return fPng_ptr; }
     SkSwizzler* swizzler() { return fSwizzler; }
index 6651abb3fca864086418a36be5bfb066e42dd3b0..b852d2dca2c179f09c5147d2fbfddb1e3c5f4d16 100644 (file)
@@ -158,6 +158,310 @@ SkColorSpace* SkColorSpace::NewNamed(Named named) {
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
+#include "SkFixed.h"
+#include "SkTemplates.h"
+
+#define SkColorSpacePrintf(...)
+
+#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 {                                                             \
+        SkDebugf("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_int(const uint8_t* ptr) {
+    return ptr[0] << 24 | ptr[1] << 16 | ptr[2] << 8 | ptr[3];
+}
+
+// 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 const size_t kICCHeaderSize = 132;
+
+// Contains a signature (4), offset (4), and size (4).
+static const size_t kICCTagTableEntrySize = 12;
+
+static const uint32_t kRGB_ColorSpace  = SkSetFourByteTag('R', 'G', 'B', ' ');
+static const uint32_t kGray_ColorSpace = SkSetFourByteTag('G', 'R', 'A', 'Y');
+
+struct ICCProfileHeader {
+    // TODO (msarett):
+    // Can we ignore less of these fields?
+    uint32_t fSize;
+    uint32_t fCMMType_ignored;
+    uint32_t fVersion;
+    uint32_t fClassProfile;
+    uint32_t fColorSpace;
+    uint32_t fPCS;
+    uint32_t fDateTime_ignored[3];
+    uint32_t fSignature;
+    uint32_t fPlatformTarget_ignored;
+    uint32_t fFlags_ignored;
+    uint32_t fManufacturer_ignored;
+    uint32_t fDeviceModel_ignored;
+    uint32_t fDeviceAttributes_ignored[2];
+    uint32_t fRenderingIntent;
+    uint32_t fIlluminantXYZ_ignored[3];
+    uint32_t fCreator_ignored;
+    uint32_t fProfileId_ignored[4];
+    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_int(src);
+        }
+    }
+
+    bool valid() const {
+        // TODO (msarett):
+        // For now it's nice to fail loudly on invalid inputs.  But, can we
+        // recover from some of these errors?
+
+        return_if_false(fSize >= kICCHeaderSize, "Size is too small");
+
+        uint8_t majorVersion = fVersion >> 24;
+        return_if_false(majorVersion <= 4, "Unsupported version");
+
+        const uint32_t kDisplay_Profile = SkSetFourByteTag('m', 'n', 't', 'r');
+        const uint32_t kInput_Profile   = SkSetFourByteTag('s', 'c', 'n', 'r');
+        const uint32_t kOutput_Profile  = SkSetFourByteTag('p', 'r', 't', 'r');
+        // TODO (msarett):
+        // Should we also support DeviceLink, ColorSpace, Abstract, or NamedColor?
+        return_if_false(fClassProfile == kDisplay_Profile ||
+                        fClassProfile == kInput_Profile ||
+                        fClassProfile == kOutput_Profile,
+                        "Unsupported class profile");
+
+        // TODO (msarett):
+        // There are many more color spaces that we could try to support.
+        return_if_false(fColorSpace == kRGB_ColorSpace || fColorSpace == kGray_ColorSpace,
+                        "Unsupported color space");
+
+        const uint32_t kXYZ_PCSSpace = SkSetFourByteTag('X', 'Y', 'Z', ' ');
+        // TODO (msarett):
+        // Can we support PCS LAB as well?
+        return_if_false(fPCS == kXYZ_PCSSpace, "Unsupported PCS space");
+
+        return_if_false(fSignature == SkSetFourByteTag('a', 'c', 's', 'p'), "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(fTagCount <= 100, "Too many tags");
+
+        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_int(src);
+        fOffset = read_big_endian_int(src + 4);
+        fLength = read_big_endian_int(src + 8);
+        return src + 12;
+    }
+
+    bool valid(size_t len) {
+        return_if_false(fOffset + fLength <= 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;
+    }
+};
+
+// TODO (msarett):
+// Should we recognize more tags?
+static const uint32_t kTAG_rXYZ = SkSetFourByteTag('r', 'X', 'Y', 'Z');
+static const uint32_t kTAG_gXYZ = SkSetFourByteTag('g', 'X', 'Y', 'Z');
+static const uint32_t kTAG_bXYZ = SkSetFourByteTag('b', 'X', 'Y', 'Z');
+static const uint32_t kTAG_rTRC = SkSetFourByteTag('r', 'T', 'R', 'C');
+static const uint32_t kTAG_gTRC = SkSetFourByteTag('g', 'T', 'R', 'C');
+static const uint32_t kTAG_bTRC = SkSetFourByteTag('b', 'T', 'R', 'C');
+
+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 const uint32_t kTAG_CurveType     = SkSetFourByteTag('c', 'u', 'r', 'v');
+static const uint32_t kTAG_ParaCurveType = SkSetFourByteTag('p', 'a', 'r', 'a');
+
+static bool load_gamma(float* gamma, const uint8_t* src, size_t len) {
+    if (len < 14) {
+        SkColorSpacePrintf("gamma tag is too small (%d bytes)", len);
+        return false;
+    }
+
+    uint32_t type = read_big_endian_int(src);
+    switch (type) {
+        case kTAG_CurveType: {
+            uint32_t count = read_big_endian_int(src + 8);
+            if (0 == count) {
+                return false;
+            }
+
+            const uint16_t* table = (const uint16_t*) (src + 12);
+            if (1 == count) {
+                // Table entry is the exponent (bias 256).
+                uint16_t value = read_big_endian_short((const uint8_t*) table);
+                *gamma = value / 256.0f;
+                SkColorSpacePrintf("gamma %d %g\n", value, *gamma);
+                return true;
+            }
+
+            // Check length again if we have a table.
+            if (len < 12 + 2 * count) {
+                SkColorSpacePrintf("gamma tag is too small (%d bytes)", len);
+                return false;
+            }
+
+            // Print the interpolation table.  For now, we ignore this and guess 2.2f.
+            for (uint32_t i = 0; i < count; i++) {
+                SkColorSpacePrintf("curve[%d] %d\n", i,
+                        read_big_endian_short((const uint8_t*) &table[i]));
+            }
+
+            *gamma = 2.2f;
+            return true;
+        }
+        case kTAG_ParaCurveType:
+            // Guess 2.2f.
+            SkColorSpacePrintf("parametric curve\n");
+            *gamma = 2.2f;
+            return true;
+        default:
+            SkColorSpacePrintf("Unsupported gamma tag type %d\n", type);
+            return false;
+    }
+}
+
+SkColorSpace* SkColorSpace::NewICC(const void* base, size_t len) {
+    const uint8_t* ptr = (const uint8_t*) base;
+
+    if (len < kICCHeaderSize) {
+        return_null("Data is not large enough to contain an ICC profile");
+    }
+
+    // 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");
+        }
+    }
+
+    // Load our XYZ and gamma matrices.
+    SkFloat3x3 toXYZ;
+    SkFloat3 gamma {{ 1.0f, 1.0f, 1.0f }};
+    switch (header.fColorSpace) {
+        case kRGB_ColorSpace: {
+            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) {
+                return_null("Need rgb tags for XYZ space");
+            }
+
+            if (!load_xyz(&toXYZ.fMat[0], r->addr((const uint8_t*) base), r->fLength) ||
+                !load_xyz(&toXYZ.fMat[3], g->addr((const uint8_t*) base), g->fLength) ||
+                !load_xyz(&toXYZ.fMat[6], b->addr((const uint8_t*) base), b->fLength))
+            {
+                return_null("Need valid rgb tags for XYZ space");
+            }
+
+            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_gamma(&gamma.fVec[0], r->addr((const uint8_t*) base), r->fLength)) {
+                SkColorSpacePrintf("Failed to read R gamma tag.\n");
+            }
+            if (!g || !load_gamma(&gamma.fVec[1], g->addr((const uint8_t*) base), g->fLength)) {
+                SkColorSpacePrintf("Failed to read G gamma tag.\n");
+            }
+            if (!b || !load_gamma(&gamma.fVec[2], b->addr((const uint8_t*) base), b->fLength)) {
+                SkColorSpacePrintf("Failed to read B gamma tag.\n");
+            }
+            return SkColorSpace::NewRGB(toXYZ, gamma);
+        }
+        default:
+            break;
+    }
+
+    return_null("ICC profile contains unsupported colorspace");
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
 SkColorSpace::Result SkColorSpace::Concat(const SkColorSpace* src, const SkColorSpace* dst,
                                           SkFloat3x3* result) {
     if (!src || !dst || (src->named() == kDevice_Named) || (src->named() == dst->named())) {
index 7e81b5f335c8ec53cf6352feeef6aaf735125588..ec6ab43c3cdd28d3865f2162bb9d84483241f72f 100644 (file)
@@ -56,6 +56,7 @@ public:
     static SkColorSpace* NewICC(const void*, size_t);
 
     SkFloat3 gamma() const { return fGamma; }
+    SkFloat3x3 xyz() const { return fToXYZD50; }
     Named named() const { return fNamed; }
     uint32_t uniqueID() const { return fUniqueID; }
 
diff --git a/tests/ColorSpaceTest.cpp b/tests/ColorSpaceTest.cpp
new file mode 100644 (file)
index 0000000..cf75602
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * 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 "Resources.h"
+#include "SkCodec.h"
+#include "SkColorSpace.h"
+#include "Test.h"
+
+static SkStreamAsset* resource(const char path[]) {
+    SkString fullPath = GetResourcePath(path);
+    return SkStream::NewFromFile(fullPath.c_str());
+}
+
+static bool almost_equal(float a, float b) {
+    return SkTAbs(a - b) < 0.0001f;
+}
+
+DEF_TEST(ColorSpaceParseICCProfile, r) {
+    SkAutoTDelete<SkStream> stream(resource("color_wheel_with_profile.png"));
+    REPORTER_ASSERT(r, nullptr != stream);
+
+    SkAutoTDelete<SkCodec> codec(SkCodec::NewFromStream(stream.detach()));
+    REPORTER_ASSERT(r, nullptr != codec);
+
+    SkColorSpace* colorSpace = codec->getColorSpace();
+    REPORTER_ASSERT(r, nullptr != colorSpace);
+
+    // No need to use almost equal here.  The color profile that we have extracted
+    // actually has a table of gammas.  And our current implementation guesses 2.2f.
+    SkFloat3 gammas = colorSpace->gamma();
+    REPORTER_ASSERT(r, 2.2f == gammas.fVec[0]);
+    REPORTER_ASSERT(r, 2.2f == gammas.fVec[1]);
+    REPORTER_ASSERT(r, 2.2f == gammas.fVec[2]);
+
+    // These nine values were extracted from the color profile in isolation (before
+    // we embedded it in the png).  Here we check that we still extract the same values.
+    SkFloat3x3 xyz = colorSpace->xyz();
+    REPORTER_ASSERT(r, almost_equal(0.436066f, xyz.fMat[0]));
+    REPORTER_ASSERT(r, almost_equal(0.222488f, xyz.fMat[1]));
+    REPORTER_ASSERT(r, almost_equal(0.013916f, xyz.fMat[2]));
+    REPORTER_ASSERT(r, almost_equal(0.385147f, xyz.fMat[3]));
+    REPORTER_ASSERT(r, almost_equal(0.716873f, xyz.fMat[4]));
+    REPORTER_ASSERT(r, almost_equal(0.0970764f, xyz.fMat[5]));
+    REPORTER_ASSERT(r, almost_equal(0.143066f, xyz.fMat[6]));
+    REPORTER_ASSERT(r, almost_equal(0.0606079f, xyz.fMat[7]));
+    REPORTER_ASSERT(r, almost_equal(0.714096f, xyz.fMat[8]));
+}