Parse icc profiles and exif orientation from jpeg markers
authormsarett <msarett@google.com>
Mon, 21 Mar 2016 15:04:40 +0000 (08:04 -0700)
committerCommit bot <commit-bot@chromium.org>
Mon, 21 Mar 2016 15:04:40 +0000 (08:04 -0700)
New resources should be fine to add since they are already
checked into chromium.

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

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

include/codec/SkCodec.h
resources/exif-orientation-2-ur.jpg [new file with mode: 0644]
resources/icc-v2-gbr.jpg [new file with mode: 0644]
src/codec/SkCodec.cpp
src/codec/SkCodecPriv.h
src/codec/SkJpegCodec.cpp
src/codec/SkJpegCodec.h
src/codec/SkRawCodec.cpp
tests/ColorSpaceTest.cpp
tests/ExifTest.cpp [new file with mode: 0644]

index 3855064..629274d 100644 (file)
@@ -107,6 +107,25 @@ public:
      */
     SkColorSpace* getColorSpace() const { return fColorSpace.get(); }
 
+    enum Origin {
+        kTopLeft_Origin     = 1, // Default
+        kTopRight_Origin    = 2, // Reflected across y-axis
+        kBottomRight_Origin = 3, // Rotated 180
+        kBottomLeft_Origin  = 4, // Reflected across x-axis
+        kLeftTop_Origin     = 5, // Reflected across x-axis, Rotated 90 CCW
+        kRightTop_Origin    = 6, // Rotated 90 CW
+        kRightBottom_Origin = 7, // Reflected across x-axis, Rotated 90 CW
+        kLeftBottom_Origin  = 8, // Rotated 90 CCW
+        kDefault_Origin     = kTopLeft_Origin,
+        kLast_Origin        = kLeftBottom_Origin,
+    };
+
+    /**
+     *  Returns the image orientation stored in the EXIF data.
+     *  If there is no EXIF data, or if we cannot read the EXIF data, returns kTopLeft.
+     */
+    Origin getOrigin() const { return fOrigin; }
+
     /**
      *  Return a size that approximately supports the desired scale factor.
      *  The codec may not be able to scale efficiently to the exact scale
@@ -491,9 +510,11 @@ public:
 protected:
     /**
      *  Takes ownership of SkStream*
-     *  Does not affect ownership of SkColorSpace*
      */
-    SkCodec(const SkImageInfo&, SkStream*, sk_sp<SkColorSpace> = nullptr);
+    SkCodec(const SkImageInfo&,
+            SkStream*,
+            sk_sp<SkColorSpace> = nullptr,
+            Origin = kTopLeft_Origin);
 
     virtual SkISize onGetScaledDimensions(float /*desiredScale*/) const {
         // By default, scaling is not supported.
@@ -625,6 +646,7 @@ private:
     SkAutoTDelete<SkStream>     fStream;
     bool                        fNeedsRewind;
     sk_sp<SkColorSpace>         fColorSpace;
+    const Origin                fOrigin;
 
     // These fields are only meaningful during scanline decodes.
     SkImageInfo                 fDstInfo;
diff --git a/resources/exif-orientation-2-ur.jpg b/resources/exif-orientation-2-ur.jpg
new file mode 100644 (file)
index 0000000..70c14d4
Binary files /dev/null and b/resources/exif-orientation-2-ur.jpg differ
diff --git a/resources/icc-v2-gbr.jpg b/resources/icc-v2-gbr.jpg
new file mode 100644 (file)
index 0000000..0984e9b
Binary files /dev/null and b/resources/icc-v2-gbr.jpg differ
index fc57e94..7bc831a 100644 (file)
@@ -114,11 +114,13 @@ SkCodec* SkCodec::NewFromData(SkData* data, SkPngChunkReader* reader) {
     return NewFromStream(new SkMemoryStream(data), reader);
 }
 
-SkCodec::SkCodec(const SkImageInfo& info, SkStream* stream, sk_sp<SkColorSpace> colorSpace)
+SkCodec::SkCodec(const SkImageInfo& info, SkStream* stream, sk_sp<SkColorSpace> colorSpace,
+        Origin origin)
     : fSrcInfo(info)
     , fStream(stream)
     , fNeedsRewind(false)
     , fColorSpace(colorSpace)
+    , fOrigin(origin)
     , fDstInfo()
     , fOptions()
     , fCurrScanline(-1)
index 68be58b..2b22922 100644 (file)
@@ -251,4 +251,28 @@ inline uint32_t get_int(uint8_t* buffer, uint32_t i) {
 #endif
 }
 
+/*
+ * @param data           Buffer to read bytes from
+ * @param isLittleEndian Output parameter
+ *                       Indicates if the data is little endian
+ *                       Is unaffected on false returns
+ */
+inline bool is_valid_endian_marker(const uint8_t* data, bool* isLittleEndian) {
+    // II indicates Intel (little endian) and MM indicates motorola (big endian).
+    if (('I' != data[0] || 'I' != data[1]) && ('M' != data[0] || 'M' != data[1])) {
+        return false;
+    }
+
+    *isLittleEndian = ('I' == data[0]);
+    return true;
+}
+
+inline uint16_t get_endian_short(const uint8_t* data, bool littleEndian) {
+    if (littleEndian) {
+        return (data[1] << 8) | (data[0]);
+    }
+
+    return (data[0] << 8) | (data[1]);
+}
+
 #endif // SkCodecPriv_DEFINED
index a342cd8..e920de1 100644 (file)
@@ -29,6 +29,160 @@ bool SkJpegCodec::IsJpeg(const void* buffer, size_t bytesRead) {
     return bytesRead >= 3 && !memcmp(buffer, jpegSig, sizeof(jpegSig));
 }
 
+static uint32_t get_endian_int(const uint8_t* data, bool littleEndian) {
+    if (littleEndian) {
+        return (data[3] << 24) | (data[2] << 16) | (data[1] << 8) | (data[0]);
+    }
+
+    return (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | (data[3]);
+}
+
+const uint32_t kExifHeaderSize = 14;
+const uint32_t kICCHeaderSize = 14;
+const uint32_t kExifMarker = JPEG_APP0 + 1;
+const uint32_t kICCMarker = JPEG_APP0 + 2;
+
+static bool is_orientation_marker(jpeg_marker_struct* marker, SkCodec::Origin* orientation) {
+    if (kExifMarker != marker->marker || marker->data_length < kExifHeaderSize) {
+        return false;
+    }
+
+    const uint8_t* data = marker->data;
+    static const uint8_t kExifSig[] { 'E', 'x', 'i', 'f', '\0' };
+    if (memcmp(data, kExifSig, sizeof(kExifSig))) {
+        return false;
+    }
+
+    bool littleEndian;
+    if (!is_valid_endian_marker(data + 6, &littleEndian)) {
+        return false;
+    }
+
+    // Get the offset from the start of the marker.
+    // Account for 'E', 'x', 'i', 'f', '\0', '<fill byte>'.
+    uint32_t offset = get_endian_int(data + 10, littleEndian);
+    offset += sizeof(kExifSig) + 1;
+
+    // Require that the marker is at least large enough to contain the number of entries.
+    if (marker->data_length < offset + 2) {
+        return false;
+    }
+    uint32_t numEntries = get_endian_short(data + offset, littleEndian);
+
+    // Tag (2 bytes), Datatype (2 bytes), Number of elements (4 bytes), Data (4 bytes)
+    const uint32_t kEntrySize = 12;
+    numEntries = SkTMin(numEntries, (marker->data_length - offset - 2) / kEntrySize);
+
+    // Advance the data to the start of the entries.
+    data += offset + 2;
+
+    const uint16_t kOriginTag = 0x112;
+    const uint16_t kOriginType = 3;
+    for (uint32_t i = 0; i < numEntries; i++, data += kEntrySize) {
+        uint16_t tag = get_endian_short(data, littleEndian);
+        uint16_t type = get_endian_short(data + 2, littleEndian);
+        uint32_t count = get_endian_int(data + 4, littleEndian);
+        if (kOriginTag == tag && kOriginType == type && 1 == count) {
+            uint16_t val = get_endian_short(data + 8, littleEndian);
+            if (0 < val && val <= SkCodec::kLast_Origin) {
+                *orientation = (SkCodec::Origin) val;
+                return true;
+            }
+        }
+    }
+
+    return false;
+}
+
+static SkCodec::Origin get_exif_orientation(jpeg_decompress_struct* dinfo) {
+    SkCodec::Origin orientation;
+    for (jpeg_marker_struct* marker = dinfo->marker_list; marker; marker = marker->next) {
+        if (is_orientation_marker(marker, &orientation)) {
+            return orientation;
+        }
+    }
+
+    return SkCodec::kDefault_Origin;
+}
+
+static bool is_icc_marker(jpeg_marker_struct* marker) {
+    if (kICCMarker != marker->marker || marker->data_length < kICCHeaderSize) {
+        return false;
+    }
+
+    static const uint8_t kICCSig[] { 'I', 'C', 'C', '_', 'P', 'R', 'O', 'F', 'I', 'L', 'E', '\0' };
+    return !memcmp(marker->data, kICCSig, sizeof(kICCSig));
+}
+
+/*
+ * ICC profiles may be stored using a sequence of multiple markers.  We obtain the ICC profile
+ * in two steps:
+ *     (1) Discover all ICC profile markers and verify that they are numbered properly.
+ *     (2) Copy the data from each marker into a contiguous ICC profile.
+ */
+static sk_sp<SkColorSpace> get_icc_profile(jpeg_decompress_struct* dinfo) {
+    // Note that 256 will be enough storage space since each markerIndex is stored in 8-bits.
+    jpeg_marker_struct* markerSequence[256];
+    memset(markerSequence, 0, sizeof(markerSequence));
+    uint8_t numMarkers = 0;
+    size_t totalBytes = 0;
+
+    // Discover any ICC markers and verify that they are numbered properly.
+    for (jpeg_marker_struct* marker = dinfo->marker_list; marker; marker = marker->next) {
+        if (is_icc_marker(marker)) {
+            // Verify that numMarkers is valid and consistent.
+            if (0 == numMarkers) {
+                numMarkers = marker->data[13];
+                if (0 == numMarkers) {
+                    SkCodecPrintf("ICC Profile Error: numMarkers must be greater than zero.\n");
+                    return nullptr;
+                }
+            } else if (numMarkers != marker->data[13]) {
+                SkCodecPrintf("ICC Profile Error: numMarkers must be consistent.\n");
+                return nullptr;
+            }
+
+            // Verify that the markerIndex is valid and unique.  Note that zero is not
+            // a valid index.
+            uint8_t markerIndex = marker->data[12];
+            if (markerIndex == 0 || markerIndex > numMarkers) {
+                SkCodecPrintf("ICC Profile Error: markerIndex is invalid.\n");
+                return nullptr;
+            }
+            if (markerSequence[markerIndex]) {
+                SkCodecPrintf("ICC Profile Error: Duplicate value of markerIndex.\n");
+                return nullptr;
+            }
+            markerSequence[markerIndex] = marker;
+            SkASSERT(marker->data_length >= kICCHeaderSize);
+            totalBytes += marker->data_length - kICCHeaderSize;
+        }
+    }
+
+    if (0 == totalBytes) {
+        // No non-empty ICC profile markers were found.
+        return nullptr;
+    }
+
+    // Combine the ICC marker data into a contiguous profile.
+    SkAutoMalloc iccData(totalBytes);
+    void* dst = iccData.get();
+    for (uint32_t i = 1; i <= numMarkers; i++) {
+        jpeg_marker_struct* marker = markerSequence[i];
+        if (!marker) {
+            SkCodecPrintf("ICC Profile Error: Missing marker %d of %d.\n", i, numMarkers);
+            return nullptr;
+        }
+
+        void* src = SkTAddOffset<void>(marker->data, kICCHeaderSize);
+        size_t bytes = marker->data_length - kICCHeaderSize;
+        memcpy(dst, src, bytes);
+        dst = SkTAddOffset<void>(dst, bytes);
+    }
+
+    return SkColorSpace::NewICC(iccData.get(), totalBytes);
+}
+
 bool SkJpegCodec::ReadHeader(SkStream* stream, SkCodec** codecOut,
         JpegDecoderMgr** decoderMgrOut) {
 
@@ -43,19 +197,32 @@ bool SkJpegCodec::ReadHeader(SkStream* stream, SkCodec** codecOut,
     // Initialize the decompress info and the source manager
     decoderMgr->init();
 
+    // Instruct jpeg library to save the markers that we care about.  Since
+    // the orientation and color profile will not change, we can skip this
+    // step on rewinds.
+    if (codecOut) {
+        jpeg_save_markers(decoderMgr->dinfo(), kExifMarker, 0xFFFF);
+        jpeg_save_markers(decoderMgr->dinfo(), kICCMarker, 0xFFFF);
+    }
+
     // Read the jpeg header
     if (JPEG_HEADER_OK != jpeg_read_header(decoderMgr->dinfo(), true)) {
         return decoderMgr->returnFalse("read_header");
     }
 
-    if (nullptr != codecOut) {
+    if (codecOut) {
         // Recommend the color type to decode to
         const SkColorType colorType = decoderMgr->getColorType();
 
         // Create image info object and the codec
         const SkImageInfo& imageInfo = SkImageInfo::Make(decoderMgr->dinfo()->image_width,
                 decoderMgr->dinfo()->image_height, colorType, kOpaque_SkAlphaType);
-        *codecOut = new SkJpegCodec(imageInfo, stream, decoderMgr.release());
+
+        Origin orientation = get_exif_orientation(decoderMgr->dinfo());
+        sk_sp<SkColorSpace> colorSpace = get_icc_profile(decoderMgr->dinfo());
+
+        *codecOut = new SkJpegCodec(imageInfo, stream, decoderMgr.release(), colorSpace,
+                orientation);
     } else {
         SkASSERT(nullptr != decoderMgrOut);
         *decoderMgrOut = decoderMgr.release();
@@ -76,8 +243,8 @@ SkCodec* SkJpegCodec::NewFromStream(SkStream* stream) {
 }
 
 SkJpegCodec::SkJpegCodec(const SkImageInfo& srcInfo, SkStream* stream,
-        JpegDecoderMgr* decoderMgr)
-    : INHERITED(srcInfo, stream)
+        JpegDecoderMgr* decoderMgr, sk_sp<SkColorSpace> colorSpace, Origin origin)
+    : INHERITED(srcInfo, stream, colorSpace, origin)
     , fDecoderMgr(decoderMgr)
     , fReadyState(decoderMgr->dinfo()->global_state)
     , fSwizzlerSubset(SkIRect::MakeEmpty())
index 049c3c9..d3ea132 100644 (file)
@@ -91,7 +91,8 @@ private:
      * @param decoderMgr holds decompress struct, src manager, and error manager
      *                   takes ownership
      */
-    SkJpegCodec(const SkImageInfo& srcInfo, SkStream* stream, JpegDecoderMgr* decoderMgr);
+    SkJpegCodec(const SkImageInfo& srcInfo, SkStream* stream, JpegDecoderMgr* decoderMgr,
+            sk_sp<SkColorSpace> colorSpace, Origin origin);
 
     /*
      * Checks if the conversion between the input image and the requested output
index 3b7b9a9..762e823 100644 (file)
@@ -537,9 +537,12 @@ private:
         }
 
         // Check if the header is valid (endian info and magic number "42").
-        return
-            (header[0] == 0x49 && header[1] == 0x49 && header[2] == 0x2A && header[3] == 0x00) ||
-            (header[0] == 0x4D && header[1] == 0x4D && header[2] == 0x00 && header[3] == 0x2A);
+        bool littleEndian;
+        if (!is_valid_endian_marker(header, &littleEndian)) {
+            return false;
+        }
+
+        return 0x2A == get_endian_short(header + 2, littleEndian);
     }
 
     void init(const int width, const int height, const dng_point& cfaPatternSize) {
index 3361aa3..37785e2 100644 (file)
@@ -17,13 +17,11 @@ static SkStreamAsset* resource(const char path[]) {
     return SkStream::NewFromFile(fullPath.c_str());
 }
 
-#if (PNG_LIBPNG_VER_MAJOR > 1) || (PNG_LIBPNG_VER_MAJOR == 1 && PNG_LIBPNG_VER_MINOR >= 6)
 static bool almost_equal(float a, float b) {
-    return SkTAbs(a - b) < 0.0001f;
+    return SkTAbs(a - b) < 0.001f;
 }
-#endif
 
-DEF_TEST(ColorSpaceParseICCProfile, r) {
+DEF_TEST(ColorSpaceParsePngICCProfile, r) {
     SkAutoTDelete<SkStream> stream(resource("color_wheel_with_profile.png"));
     REPORTER_ASSERT(r, nullptr != stream);
 
@@ -55,3 +53,35 @@ DEF_TEST(ColorSpaceParseICCProfile, r) {
     REPORTER_ASSERT(r, almost_equal(0.714096f, xyz.fMat[8]));
 #endif
 }
+
+DEF_TEST(ColorSpaceParseJpegICCProfile, r) {
+    SkAutoTDelete<SkStream> stream(resource("icc-v2-gbr.jpg"));
+    REPORTER_ASSERT(r, nullptr != stream);
+
+    SkAutoTDelete<SkCodec> codec(SkCodec::NewFromStream(stream.release()));
+    REPORTER_ASSERT(r, nullptr != codec);
+
+    SkColorSpace* colorSpace = codec->getColorSpace();
+    REPORTER_ASSERT(r, nullptr != colorSpace);
+
+    // It's important to use almost equal here.  This profile sets gamma as
+    // 563 / 256, which actually comes out to about 2.19922.
+    SkFloat3 gammas = colorSpace->gamma();
+    REPORTER_ASSERT(r, almost_equal(2.2f, gammas.fVec[0]));
+    REPORTER_ASSERT(r, almost_equal(2.2f, gammas.fVec[1]));
+    REPORTER_ASSERT(r, almost_equal(2.2f, gammas.fVec[2]));
+
+    // These nine values were extracted from the color profile.  Until we know any
+    // better, we'll assume these are the right values and test that we continue
+    // to extract them properly.
+    SkFloat3x3 xyz = colorSpace->xyz();
+    REPORTER_ASSERT(r, almost_equal(0.385117f, xyz.fMat[0]));
+    REPORTER_ASSERT(r, almost_equal(0.716904f, xyz.fMat[1]));
+    REPORTER_ASSERT(r, almost_equal(0.0970612f, xyz.fMat[2]));
+    REPORTER_ASSERT(r, almost_equal(0.143051f, xyz.fMat[3]));
+    REPORTER_ASSERT(r, almost_equal(0.0606079f, xyz.fMat[4]));
+    REPORTER_ASSERT(r, almost_equal(0.713913f, xyz.fMat[5]));
+    REPORTER_ASSERT(r, almost_equal(0.436035f, xyz.fMat[6]));
+    REPORTER_ASSERT(r, almost_equal(0.222488f, xyz.fMat[7]));
+    REPORTER_ASSERT(r, almost_equal(0.013916f, xyz.fMat[8]));
+}
diff --git a/tests/ExifTest.cpp b/tests/ExifTest.cpp
new file mode 100644 (file)
index 0000000..c49de0f
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * 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 "Test.h"
+
+static SkStreamAsset* resource(const char path[]) {
+    SkString fullPath = GetResourcePath(path);
+    return SkStream::NewFromFile(fullPath.c_str());
+}
+
+DEF_TEST(ExifOrientation, r) {
+    SkAutoTDelete<SkStream> stream(resource("exif-orientation-2-ur.jpg"));
+    REPORTER_ASSERT(r, nullptr != stream);
+    SkAutoTDelete<SkCodec> codec(SkCodec::NewFromStream(stream.release()));
+    REPORTER_ASSERT(r, nullptr != codec);
+    SkCodec::Origin origin = codec->getOrigin();
+    REPORTER_ASSERT(r, SkCodec::kTopRight_Origin == origin);
+
+    stream.reset(resource("mandrill_512_q075.jpg"));
+    codec.reset(SkCodec::NewFromStream(stream.release()));
+    REPORTER_ASSERT(r, nullptr != codec);
+    origin = codec->getOrigin();
+    REPORTER_ASSERT(r, SkCodec::kTopLeft_Origin == origin);
+}