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
This commit is contained in:
parent
85c9447d4d
commit
0e6274f540
@ -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;
|
||||
|
BIN
resources/exif-orientation-2-ur.jpg
Normal file
BIN
resources/exif-orientation-2-ur.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
BIN
resources/icc-v2-gbr.jpg
Normal file
BIN
resources/icc-v2-gbr.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 43 KiB |
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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]));
|
||||
}
|
||||
|
30
tests/ExifTest.cpp
Normal file
30
tests/ExifTest.cpp
Normal file
@ -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);
|
||||
}
|
Loading…
Reference in New Issue
Block a user