From 0e6274f54084d816abd70d445dded8657eb01abd Mon Sep 17 00:00:00 2001 From: msarett Date: Mon, 21 Mar 2016 08:04:40 -0700 Subject: Parse icc profiles and exif orientation from jpeg markers 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 | 26 +++++- resources/exif-orientation-2-ur.jpg | Bin 0 -> 1948 bytes resources/icc-v2-gbr.jpg | Bin 0 -> 43834 bytes src/codec/SkCodec.cpp | 4 +- src/codec/SkCodecPriv.h | 24 +++++ src/codec/SkJpegCodec.cpp | 175 +++++++++++++++++++++++++++++++++++- src/codec/SkJpegCodec.h | 3 +- src/codec/SkRawCodec.cpp | 9 +- tests/ColorSpaceTest.cpp | 38 +++++++- tests/ExifTest.cpp | 30 +++++++ 10 files changed, 294 insertions(+), 15 deletions(-) create mode 100644 resources/exif-orientation-2-ur.jpg create mode 100644 resources/icc-v2-gbr.jpg create mode 100644 tests/ExifTest.cpp diff --git a/include/codec/SkCodec.h b/include/codec/SkCodec.h index 3855064f61..629274d21c 100644 --- a/include/codec/SkCodec.h +++ b/include/codec/SkCodec.h @@ -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 = nullptr); + SkCodec(const SkImageInfo&, + SkStream*, + sk_sp = nullptr, + Origin = kTopLeft_Origin); virtual SkISize onGetScaledDimensions(float /*desiredScale*/) const { // By default, scaling is not supported. @@ -625,6 +646,7 @@ private: SkAutoTDelete fStream; bool fNeedsRewind; sk_sp 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 index 0000000000..70c14d4272 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 index 0000000000..0984e9b3fd Binary files /dev/null and b/resources/icc-v2-gbr.jpg differ diff --git a/src/codec/SkCodec.cpp b/src/codec/SkCodec.cpp index fc57e94f89..7bc831a6ac 100644 --- a/src/codec/SkCodec.cpp +++ b/src/codec/SkCodec.cpp @@ -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 colorSpace) +SkCodec::SkCodec(const SkImageInfo& info, SkStream* stream, sk_sp colorSpace, + Origin origin) : fSrcInfo(info) , fStream(stream) , fNeedsRewind(false) , fColorSpace(colorSpace) + , fOrigin(origin) , fDstInfo() , fOptions() , fCurrScanline(-1) diff --git a/src/codec/SkCodecPriv.h b/src/codec/SkCodecPriv.h index 68be58b0b0..2b22922b0c 100644 --- a/src/codec/SkCodecPriv.h +++ b/src/codec/SkCodecPriv.h @@ -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 diff --git a/src/codec/SkJpegCodec.cpp b/src/codec/SkJpegCodec.cpp index a342cd8b6a..e920de1956 100644 --- a/src/codec/SkJpegCodec.cpp +++ b/src/codec/SkJpegCodec.cpp @@ -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', ''. + 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 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(marker->data, kICCHeaderSize); + size_t bytes = marker->data_length - kICCHeaderSize; + memcpy(dst, src, bytes); + dst = SkTAddOffset(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 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 colorSpace, Origin origin) + : INHERITED(srcInfo, stream, colorSpace, origin) , fDecoderMgr(decoderMgr) , fReadyState(decoderMgr->dinfo()->global_state) , fSwizzlerSubset(SkIRect::MakeEmpty()) diff --git a/src/codec/SkJpegCodec.h b/src/codec/SkJpegCodec.h index 049c3c956a..d3ea132da7 100644 --- a/src/codec/SkJpegCodec.h +++ b/src/codec/SkJpegCodec.h @@ -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 colorSpace, Origin origin); /* * Checks if the conversion between the input image and the requested output diff --git a/src/codec/SkRawCodec.cpp b/src/codec/SkRawCodec.cpp index 3b7b9a96ee..762e82364c 100644 --- a/src/codec/SkRawCodec.cpp +++ b/src/codec/SkRawCodec.cpp @@ -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) { diff --git a/tests/ColorSpaceTest.cpp b/tests/ColorSpaceTest.cpp index 3361aa3420..37785e238b 100644 --- a/tests/ColorSpaceTest.cpp +++ b/tests/ColorSpaceTest.cpp @@ -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 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 stream(resource("icc-v2-gbr.jpg")); + REPORTER_ASSERT(r, nullptr != stream); + + SkAutoTDelete 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 index 0000000000..c49de0f6fe --- /dev/null +++ b/tests/ExifTest.cpp @@ -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 stream(resource("exif-orientation-2-ur.jpg")); + REPORTER_ASSERT(r, nullptr != stream); + SkAutoTDelete 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); +} -- cgit v1.2.3