|  | /* | 
|  | * Copyright 2023 Google Inc. | 
|  | * | 
|  | * Use of this source code is governed by a BSD-style license that can be | 
|  | * found in the LICENSE file. | 
|  | */ | 
|  |  | 
|  | #include "include/codec/SkAndroidCodec.h" | 
|  | #include "include/codec/SkCodec.h" | 
|  | #include "include/core/SkBitmap.h" | 
|  | #include "include/core/SkCanvas.h" | 
|  | #include "include/core/SkColor.h" | 
|  | #include "include/core/SkImageEncoder.h" | 
|  | #include "include/core/SkShader.h" | 
|  | #include "include/core/SkSize.h" | 
|  | #include "include/core/SkStream.h" | 
|  | #include "include/core/SkTypes.h" | 
|  | #include "include/encode/SkJpegEncoder.h" | 
|  | #include "include/private/SkGainmapInfo.h" | 
|  | #include "include/private/SkGainmapShader.h" | 
|  | #include "include/private/SkJpegGainmapEncoder.h" | 
|  | #include "src/codec/SkJpegCodec.h" | 
|  | #include "src/codec/SkJpegConstants.h" | 
|  | #include "src/codec/SkJpegMultiPicture.h" | 
|  | #include "src/codec/SkJpegSegmentScan.h" | 
|  | #include "src/codec/SkJpegSourceMgr.h" | 
|  | #include "src/codec/SkJpegXmp.h" | 
|  | #include "tests/Test.h" | 
|  | #include "tools/Resources.h" | 
|  |  | 
|  | #include <cstdint> | 
|  | #include <cstring> | 
|  | #include <memory> | 
|  | #include <utility> | 
|  | #include <vector> | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | // A test stream to stress the different SkJpegSourceMgr sub-classes. | 
|  | class TestStream : public SkStream { | 
|  | public: | 
|  | enum class Type { | 
|  | kUnseekable,    // SkJpegUnseekableSourceMgr | 
|  | kSeekable,      // SkJpegBufferedSourceMgr | 
|  | kMemoryMapped,  // SkJpegMemorySourceMgr | 
|  | }; | 
|  | TestStream(Type type, SkStream* stream) | 
|  | : fStream(stream) | 
|  | , fSeekable(type != Type::kUnseekable) | 
|  | , fMemoryMapped(type == Type::kMemoryMapped) {} | 
|  | ~TestStream() override {} | 
|  |  | 
|  | size_t read(void* buffer, size_t size) override { return fStream->read(buffer, size); } | 
|  | size_t peek(void* buffer, size_t size) const override { return fStream->peek(buffer, size); } | 
|  | bool isAtEnd() const override { return fStream->isAtEnd(); } | 
|  | bool rewind() override { | 
|  | if (!fSeekable) { | 
|  | return false; | 
|  | } | 
|  | return fStream->rewind(); | 
|  | } | 
|  | bool hasPosition() const override { | 
|  | if (!fSeekable) { | 
|  | return false; | 
|  | } | 
|  | return fStream->hasPosition(); | 
|  | } | 
|  | size_t getPosition() const override { | 
|  | if (!fSeekable) { | 
|  | return 0; | 
|  | } | 
|  | return fStream->hasPosition(); | 
|  | } | 
|  | bool seek(size_t position) override { | 
|  | if (!fSeekable) { | 
|  | return 0; | 
|  | } | 
|  | return fStream->seek(position); | 
|  | } | 
|  | bool move(long offset) override { | 
|  | if (!fSeekable) { | 
|  | return 0; | 
|  | } | 
|  | return fStream->move(offset); | 
|  | } | 
|  | bool hasLength() const override { | 
|  | if (!fMemoryMapped) { | 
|  | return false; | 
|  | } | 
|  | return fStream->hasLength(); | 
|  | } | 
|  | size_t getLength() const override { | 
|  | if (!fMemoryMapped) { | 
|  | return 0; | 
|  | } | 
|  | return fStream->getLength(); | 
|  | } | 
|  | const void* getMemoryBase() override { | 
|  | if (!fMemoryMapped) { | 
|  | return nullptr; | 
|  | } | 
|  | return fStream->getMemoryBase(); | 
|  | } | 
|  |  | 
|  | private: | 
|  | SkStream* const fStream; | 
|  | bool fSeekable = false; | 
|  | bool fMemoryMapped = false; | 
|  | }; | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | DEF_TEST(Codec_jpegSegmentScan, r) { | 
|  | const struct Rec { | 
|  | const char* path; | 
|  | size_t sosSegmentCount; | 
|  | size_t eoiSegmentCount; | 
|  | size_t testSegmentIndex; | 
|  | uint8_t testSegmentMarker; | 
|  | size_t testSegmentOffset; | 
|  | uint16_t testSegmentParameterLength; | 
|  | } recs[] = { | 
|  | {"images/wide_gamut_yellow_224_224_64.jpeg", 11, 15, 10, 0xda, 9768, 12}, | 
|  | {"images/CMYK.jpg", 7, 8, 1, 0xee, 2, 14}, | 
|  | {"images/b78329453.jpeg", 10, 23, 3, 0xe2, 154, 540}, | 
|  | {"images/brickwork-texture.jpg", 8, 28, 12, 0xc4, 34183, 42}, | 
|  | {"images/brickwork_normal-map.jpg", 8, 28, 27, 0xd9, 180612, 0}, | 
|  | {"images/cmyk_yellow_224_224_32.jpg", 19, 23, 2, 0xed, 854, 2828}, | 
|  | {"images/color_wheel.jpg", 10, 11, 2, 0xdb, 20, 67}, | 
|  | {"images/cropped_mandrill.jpg", 10, 11, 4, 0xc0, 158, 17}, | 
|  | {"images/dog.jpg", 10, 11, 5, 0xc4, 177, 28}, | 
|  | {"images/ducky.jpg", 12, 13, 10, 0xc4, 3718, 181}, | 
|  | {"images/exif-orientation-2-ur.jpg", 11, 12, 2, 0xe1, 20, 130}, | 
|  | {"images/flutter_logo.jpg", 9, 27, 21, 0xda, 5731, 8}, | 
|  | {"images/grayscale.jpg", 6, 16, 9, 0xda, 327, 8}, | 
|  | {"images/icc-v2-gbr.jpg", 12, 25, 24, 0xd9, 43832, 0}, | 
|  | {"images/mandrill_512_q075.jpg", 10, 11, 7, 0xc4, 393, 31}, | 
|  | {"images/mandrill_cmyk.jpg", 19, 35, 16, 0xdd, 574336, 4}, | 
|  | {"images/mandrill_h1v1.jpg", 10, 11, 1, 0xe0, 2, 16}, | 
|  | {"images/mandrill_h2v1.jpg", 10, 11, 0, 0xd8, 0, 0}, | 
|  | {"images/randPixels.jpg", 10, 11, 6, 0xc4, 200, 30}, | 
|  | {"images/wide_gamut_yellow_224_224_64.jpeg", 11, 15, 10, 0xda, 9768, 12}, | 
|  | }; | 
|  |  | 
|  | for (const auto& rec : recs) { | 
|  | auto stream = GetResourceAsStream(rec.path); | 
|  | if (!stream) { | 
|  | continue; | 
|  | } | 
|  |  | 
|  | // Scan all the way to EndOfImage. | 
|  | auto sourceMgr = SkJpegSourceMgr::Make(stream.get()); | 
|  | const auto& segments = sourceMgr->getAllSegments(); | 
|  |  | 
|  | // Verify we got the expected number of segments at EndOfImage | 
|  | REPORTER_ASSERT(r, rec.eoiSegmentCount == segments.size()); | 
|  |  | 
|  | // Verify we got the expected number of segments before StartOfScan | 
|  | for (size_t i = 0; i < segments.size(); ++i) { | 
|  | if (segments[i].marker == kJpegMarkerStartOfScan) { | 
|  | REPORTER_ASSERT(r, rec.sosSegmentCount == i + 1); | 
|  | break; | 
|  | } | 
|  | } | 
|  |  | 
|  | // Verify the values for a randomly pre-selected segment index. | 
|  | const auto& segment = segments[rec.testSegmentIndex]; | 
|  | REPORTER_ASSERT(r, rec.testSegmentMarker == segment.marker); | 
|  | REPORTER_ASSERT(r, rec.testSegmentOffset == segment.offset); | 
|  | REPORTER_ASSERT(r, rec.testSegmentParameterLength == segment.parameterLength); | 
|  | } | 
|  | } | 
|  |  | 
|  | static bool find_mp_params_segment(SkStream* stream, | 
|  | std::unique_ptr<SkJpegMultiPictureParameters>* outMpParams, | 
|  | SkJpegSegment* outMpParamsSegment) { | 
|  | auto sourceMgr = SkJpegSourceMgr::Make(stream); | 
|  | for (const auto& segment : sourceMgr->getAllSegments()) { | 
|  | if (segment.marker != kMpfMarker) { | 
|  | continue; | 
|  | } | 
|  | auto parameterData = sourceMgr->getSegmentParameters(segment); | 
|  | if (!parameterData) { | 
|  | continue; | 
|  | } | 
|  | *outMpParams = SkJpegMultiPictureParameters::Make(parameterData); | 
|  | if (*outMpParams) { | 
|  | *outMpParamsSegment = segment; | 
|  | return true; | 
|  | } | 
|  | } | 
|  | return false; | 
|  | } | 
|  |  | 
|  | DEF_TEST(Codec_multiPictureParams, r) { | 
|  | // Little-endian test. | 
|  | { | 
|  | const uint8_t bytes[] = { | 
|  | 0x4d, 0x50, 0x46, 0x00, 0x49, 0x49, 0x2a, 0x00, 0x08, 0x00, 0x00, 0x00, 0x03, | 
|  | 0x00, 0x00, 0xb0, 0x07, 0x00, 0x04, 0x00, 0x00, 0x00, 0x30, 0x31, 0x30, 0x30, | 
|  | 0x01, 0xb0, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, | 
|  | 0xb0, 0x07, 0x00, 0x20, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, | 
|  | 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x20, 0xcf, 0x49, 0x00, 0x00, 0x00, 0x00, | 
|  | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xee, 0x28, 0x01, 0x00, | 
|  | 0xf9, 0xb7, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x00, | 
|  | }; | 
|  | auto mpParams = | 
|  | SkJpegMultiPictureParameters::Make(SkData::MakeWithoutCopy(bytes, sizeof(bytes))); | 
|  | REPORTER_ASSERT(r, mpParams); | 
|  | REPORTER_ASSERT(r, mpParams->images.size() == 2); | 
|  | REPORTER_ASSERT(r, mpParams->images[0].dataOffset == 0); | 
|  | REPORTER_ASSERT(r, mpParams->images[0].size == 4837152); | 
|  | REPORTER_ASSERT(r, mpParams->images[1].dataOffset == 3979257); | 
|  | REPORTER_ASSERT(r, mpParams->images[1].size == 76014); | 
|  | } | 
|  |  | 
|  | // Big-endian test. | 
|  | { | 
|  | const uint8_t bytes[] = { | 
|  | 0x4d, 0x50, 0x46, 0x00, 0x4d, 0x4d, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x08, 0x00, | 
|  | 0x03, 0xb0, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x04, 0x30, 0x31, 0x30, 0x30, | 
|  | 0xb0, 0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0xb0, | 
|  | 0x02, 0x00, 0x07, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, | 
|  | 0x00, 0x00, 0x20, 0x03, 0x00, 0x00, 0x00, 0x56, 0xda, 0x2f, 0x00, 0x00, 0x00, | 
|  | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0xc6, 0x01, | 
|  | 0x00, 0x55, 0x7c, 0x1f, 0x00, 0x00, 0x00, 0x00, | 
|  | }; | 
|  | auto mpParams = | 
|  | SkJpegMultiPictureParameters::Make(SkData::MakeWithoutCopy(bytes, sizeof(bytes))); | 
|  | REPORTER_ASSERT(r, mpParams); | 
|  | REPORTER_ASSERT(r, mpParams->images.size() == 2); | 
|  | REPORTER_ASSERT(r, mpParams->images[0].dataOffset == 0); | 
|  | REPORTER_ASSERT(r, mpParams->images[0].size == 5691951); | 
|  | REPORTER_ASSERT(r, mpParams->images[1].dataOffset == 5602335); | 
|  | REPORTER_ASSERT(r, mpParams->images[1].size == 1361409); | 
|  | } | 
|  |  | 
|  | // Three entry test. | 
|  | { | 
|  | const uint8_t bytes[] = { | 
|  | 0x4d, 0x50, 0x46, 0x00, 0x4d, 0x4d, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x08, 0x00, | 
|  | 0x03, 0xb0, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x04, 0x30, 0x31, 0x30, 0x30, | 
|  | 0xb0, 0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0xb0, | 
|  | 0x02, 0x00, 0x07, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, | 
|  | 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x1f, 0x1c, 0xc2, 0x00, 0x00, 0x00, | 
|  | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x05, 0xb0, | 
|  | 0x00, 0x1f, 0x12, 0xec, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | 
|  | 0x00, 0x96, 0x6b, 0x00, 0x22, 0x18, 0x9c, 0x00, 0x00, 0x00, 0x00, | 
|  | }; | 
|  | auto mpParams = | 
|  | SkJpegMultiPictureParameters::Make(SkData::MakeWithoutCopy(bytes, sizeof(bytes))); | 
|  | REPORTER_ASSERT(r, mpParams); | 
|  | REPORTER_ASSERT(r, mpParams->images.size() == 3); | 
|  | REPORTER_ASSERT(r, mpParams->images[0].dataOffset == 0); | 
|  | REPORTER_ASSERT(r, mpParams->images[0].size == 2038978); | 
|  | REPORTER_ASSERT(r, mpParams->images[1].dataOffset == 2036460); | 
|  | REPORTER_ASSERT(r, mpParams->images[1].size == 198064); | 
|  | REPORTER_ASSERT(r, mpParams->images[2].dataOffset == 2234524); | 
|  | REPORTER_ASSERT(r, mpParams->images[2].size == 38507); | 
|  | } | 
|  | } | 
|  |  | 
|  | DEF_TEST(Codec_jpegMultiPicture, r) { | 
|  | const char* path = "images/iphone_13_pro.jpeg"; | 
|  | auto stream = GetResourceAsStream(path); | 
|  | REPORTER_ASSERT(r, stream); | 
|  |  | 
|  | // Search and parse the MPF header. | 
|  | std::unique_ptr<SkJpegMultiPictureParameters> mpParams; | 
|  | SkJpegSegment mpParamsSegment; | 
|  | REPORTER_ASSERT(r, find_mp_params_segment(stream.get(), &mpParams, &mpParamsSegment)); | 
|  |  | 
|  | // Verify that we get the same parameters when we re-serialize and de-serialize them | 
|  | { | 
|  | auto mpParamsSerialized = mpParams->serialize(); | 
|  | REPORTER_ASSERT(r, mpParamsSerialized); | 
|  | auto mpParamsRoundTripped = SkJpegMultiPictureParameters::Make(mpParamsSerialized); | 
|  | REPORTER_ASSERT(r, mpParamsRoundTripped); | 
|  | REPORTER_ASSERT(r, mpParamsRoundTripped->images.size() == mpParams->images.size()); | 
|  | for (size_t i = 0; i < mpParamsRoundTripped->images.size(); ++i) { | 
|  | REPORTER_ASSERT(r, mpParamsRoundTripped->images[i].size == mpParams->images[i].size); | 
|  | REPORTER_ASSERT( | 
|  | r, | 
|  | mpParamsRoundTripped->images[i].dataOffset == mpParams->images[i].dataOffset); | 
|  | } | 
|  | } | 
|  |  | 
|  | const struct Rec { | 
|  | const TestStream::Type streamType; | 
|  | const bool skipFirstImage; | 
|  | const size_t bufferSize; | 
|  | } recs[] = { | 
|  | {TestStream::Type::kMemoryMapped, false, 1024}, | 
|  | {TestStream::Type::kMemoryMapped, true, 1024}, | 
|  | {TestStream::Type::kSeekable, false, 1024}, | 
|  | {TestStream::Type::kSeekable, true, 1024}, | 
|  | {TestStream::Type::kSeekable, false, 7}, | 
|  | {TestStream::Type::kSeekable, true, 13}, | 
|  | {TestStream::Type::kSeekable, true, 1024 * 1024 * 16}, | 
|  | {TestStream::Type::kUnseekable, false, 1024}, | 
|  | {TestStream::Type::kUnseekable, true, 1024}, | 
|  | {TestStream::Type::kUnseekable, false, 1}, | 
|  | {TestStream::Type::kUnseekable, true, 1}, | 
|  | {TestStream::Type::kUnseekable, false, 7}, | 
|  | {TestStream::Type::kUnseekable, true, 13}, | 
|  | {TestStream::Type::kUnseekable, false, 1024 * 1024 * 16}, | 
|  | {TestStream::Type::kUnseekable, true, 1024 * 1024 * 16}, | 
|  | }; | 
|  | for (const auto& rec : recs) { | 
|  | stream->rewind(); | 
|  | TestStream testStream(rec.streamType, stream.get()); | 
|  | auto sourceMgr = SkJpegSourceMgr::Make(&testStream, rec.bufferSize); | 
|  |  | 
|  | // Decode the images into bitmaps. | 
|  | size_t numberOfImages = mpParams->images.size(); | 
|  | std::vector<SkBitmap> bitmaps(numberOfImages); | 
|  | for (size_t i = 0; i < numberOfImages; ++i) { | 
|  | if (i == 0) { | 
|  | REPORTER_ASSERT(r, mpParams->images[i].dataOffset == 0); | 
|  | continue; | 
|  | } | 
|  | if (i == 1 && rec.skipFirstImage) { | 
|  | continue; | 
|  | } | 
|  | auto imageData = sourceMgr->getSubsetData( | 
|  | SkJpegMultiPictureParameters::GetAbsoluteOffset(mpParams->images[i].dataOffset, | 
|  | mpParamsSegment.offset), | 
|  | mpParams->images[i].size); | 
|  | REPORTER_ASSERT(r, imageData); | 
|  |  | 
|  | std::unique_ptr<SkCodec> codec = | 
|  | SkCodec::MakeFromStream(SkMemoryStream::Make(imageData)); | 
|  | REPORTER_ASSERT(r, codec); | 
|  |  | 
|  | SkBitmap bm; | 
|  | bm.allocPixels(codec->getInfo()); | 
|  | REPORTER_ASSERT(r, | 
|  | SkCodec::kSuccess == | 
|  | codec->getPixels(bm.info(), bm.getPixels(), bm.rowBytes())); | 
|  | bitmaps[i] = bm; | 
|  | } | 
|  |  | 
|  | // Spot-check the image size and pixels. | 
|  | if (!rec.skipFirstImage) { | 
|  | REPORTER_ASSERT(r, bitmaps[1].dimensions() == SkISize::Make(1512, 2016)); | 
|  | REPORTER_ASSERT(r, bitmaps[1].getColor(0, 0) == 0xFF3B3B3B); | 
|  | REPORTER_ASSERT(r, bitmaps[1].getColor(1511, 2015) == 0xFF101010); | 
|  | } | 
|  | REPORTER_ASSERT(r, bitmaps[2].dimensions() == SkISize::Make(576, 768)); | 
|  | REPORTER_ASSERT(r, bitmaps[2].getColor(0, 0) == 0xFF010101); | 
|  | REPORTER_ASSERT(r, bitmaps[2].getColor(575, 767) == 0xFFB5B5B5); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Decode an image and its gainmap. | 
|  | template <typename Reporter> | 
|  | void decode_all(Reporter& r, | 
|  | std::unique_ptr<SkStream> stream, | 
|  | SkBitmap& baseBitmap, | 
|  | SkBitmap& gainmapBitmap, | 
|  | SkGainmapInfo& gainmapInfo) { | 
|  | // Decode the base bitmap. | 
|  | SkCodec::Result result = SkCodec::kSuccess; | 
|  | std::unique_ptr<SkCodec> baseCodec = SkJpegCodec::MakeFromStream(std::move(stream), &result); | 
|  | REPORTER_ASSERT(r, baseCodec); | 
|  | baseBitmap.allocPixels(baseCodec->getInfo()); | 
|  | REPORTER_ASSERT(r, | 
|  | SkCodec::kSuccess == baseCodec->getPixels(baseBitmap.info(), | 
|  | baseBitmap.getPixels(), | 
|  | baseBitmap.rowBytes())); | 
|  | std::unique_ptr<SkAndroidCodec> androidCodec = | 
|  | SkAndroidCodec::MakeFromCodec(std::move(baseCodec)); | 
|  | REPORTER_ASSERT(r, androidCodec); | 
|  |  | 
|  | // Extract the gainmap info and stream. | 
|  | std::unique_ptr<SkStream> gainmapStream; | 
|  | REPORTER_ASSERT(r, androidCodec->getAndroidGainmap(&gainmapInfo, &gainmapStream)); | 
|  | REPORTER_ASSERT(r, gainmapStream); | 
|  |  | 
|  | // Decode the gainmap bitmap. | 
|  | std::unique_ptr<SkCodec> gainmapCodec = SkCodec::MakeFromStream(std::move(gainmapStream)); | 
|  | REPORTER_ASSERT(r, gainmapCodec); | 
|  | SkBitmap bm; | 
|  | bm.allocPixels(gainmapCodec->getInfo()); | 
|  | gainmapBitmap.allocPixels(gainmapCodec->getInfo()); | 
|  | REPORTER_ASSERT(r, | 
|  | SkCodec::kSuccess == gainmapCodec->getPixels(gainmapBitmap.info(), | 
|  | gainmapBitmap.getPixels(), | 
|  | gainmapBitmap.rowBytes())); | 
|  | } | 
|  |  | 
|  | // Render an applied gainmap. | 
|  | SkBitmap render_gainmap(const SkImageInfo& renderInfo, | 
|  | float renderHdrRatio, | 
|  | const SkBitmap& baseBitmap, | 
|  | const SkBitmap& gainmapBitmap, | 
|  | const SkGainmapInfo& gainmapInfo, | 
|  | int x, | 
|  | int y) { | 
|  | SkRect baseRect = SkRect::MakeXYWH(x, y, renderInfo.width(), renderInfo.height()); | 
|  |  | 
|  | float scaleX = gainmapBitmap.width() / static_cast<float>(baseBitmap.width()); | 
|  | float scaleY = gainmapBitmap.height() / static_cast<float>(baseBitmap.height()); | 
|  | SkRect gainmapRect = SkRect::MakeXYWH(baseRect.x() * scaleX, | 
|  | baseRect.y() * scaleY, | 
|  | baseRect.width() * scaleX, | 
|  | baseRect.height() * scaleY); | 
|  |  | 
|  | SkRect dstRect = SkRect::Make(renderInfo.dimensions()); | 
|  |  | 
|  | sk_sp<SkImage> baseImage = SkImage::MakeFromBitmap(baseBitmap); | 
|  | sk_sp<SkImage> gainmapImage = SkImage::MakeFromBitmap(gainmapBitmap); | 
|  | sk_sp<SkShader> shader = SkGainmapShader::Make(baseImage, | 
|  | baseRect, | 
|  | SkSamplingOptions(), | 
|  | gainmapImage, | 
|  | gainmapRect, | 
|  | SkSamplingOptions(), | 
|  | gainmapInfo, | 
|  | dstRect, | 
|  | renderHdrRatio, | 
|  | renderInfo.refColorSpace()); | 
|  |  | 
|  | SkBitmap result; | 
|  | result.allocPixels(renderInfo); | 
|  | result.eraseColor(SK_ColorTRANSPARENT); | 
|  | SkCanvas canvas(result); | 
|  |  | 
|  | SkPaint paint; | 
|  | paint.setShader(shader); | 
|  | canvas.drawRect(dstRect, paint); | 
|  |  | 
|  | return result; | 
|  | } | 
|  |  | 
|  | DEF_TEST(AndroidCodec_xmpHdrgmAsFieldValue, r) { | 
|  | // Expose HDRM values as fields. Also place the HDRGM namespace in the rdf:RDF node. | 
|  | const char xmpData[] = | 
|  | "http://ns.adobe.com/xap/1.0/\0" | 
|  | "<x:xmpmeta xmlns:x=\"adobe:ns:meta/\" x:xmptk=\"XMP Core 6.0.0\">\n" | 
|  | "   <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"\n" | 
|  | "            xmlns:hdrgm=\"http://ns.adobe.com/hdr-gain-map/1.0/\">\n" | 
|  | "      <rdf:Description rdf:about=\"\">\n" | 
|  | "         <hdrgm:Version>1.0</hdrgm:Version>\n" | 
|  | "         <hdrgm:GainMapMax>3</hdrgm:GainMapMax>\n" | 
|  | "         <hdrgm:HDRCapacityMax>4</hdrgm:HDRCapacityMax>\n" | 
|  | "      </rdf:Description>\n" | 
|  | "   </rdf:RDF>\n" | 
|  | "</x:xmpmeta>\n"; | 
|  |  | 
|  | std::vector<sk_sp<SkData>> app1Params; | 
|  | app1Params.push_back(SkData::MakeWithoutCopy(xmpData, sizeof(xmpData) - 1)); | 
|  |  | 
|  | auto xmp = SkJpegXmp::Make(app1Params); | 
|  | REPORTER_ASSERT(r, xmp); | 
|  |  | 
|  | SkGainmapInfo info; | 
|  | REPORTER_ASSERT(r, xmp->getGainmapInfoHDRGM(&info)); | 
|  | REPORTER_ASSERT(r, info.fGainmapRatioMax.fR == 8.f); | 
|  | REPORTER_ASSERT(r, info.fDisplayRatioHdr == 16.f); | 
|  | } | 
|  |  | 
|  | DEF_TEST(AndroidCodec_xmpHdrgmAsDescriptionPropertyAttributes, r) { | 
|  | // Expose HDRGM values as attributes on an rdf:Description node. | 
|  | const char xmpData[] = | 
|  | "http://ns.adobe.com/xap/1.0/\0" | 
|  | "<x:xmpmeta xmlns:x=\"adobe:ns:meta/\" x:xmptk=\"XMP Core 6.0.0\">\n" | 
|  | "   <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">\n" | 
|  | "      <rdf:Description rdf:about=\"\"\n" | 
|  | "            xmlns:hdrgm=\"http://ns.adobe.com/hdr-gain-map/1.0/\"\n" | 
|  | "         hdrgm:Version=\"1.0\"\n" | 
|  | "         hdrgm:GainMapMax=\"3\"\n" | 
|  | "         hdrgm:HDRCapacityMax=\"4\"/>\n" | 
|  | "   </rdf:RDF>\n" | 
|  | "</x:xmpmeta>\n"; | 
|  |  | 
|  | std::vector<sk_sp<SkData>> app1Params; | 
|  | app1Params.push_back(SkData::MakeWithoutCopy(xmpData, sizeof(xmpData) - 1)); | 
|  |  | 
|  | auto xmp = SkJpegXmp::Make(app1Params); | 
|  | REPORTER_ASSERT(r, xmp); | 
|  |  | 
|  | SkGainmapInfo info; | 
|  | REPORTER_ASSERT(r, xmp->getGainmapInfoHDRGM(&info)); | 
|  | REPORTER_ASSERT(r, info.fGainmapRatioMax.fR == 8.f); | 
|  | REPORTER_ASSERT(r, info.fDisplayRatioHdr == 16.f); | 
|  | } | 
|  |  | 
|  | DEF_TEST(AndroidCodec_xmpContainerTypedNode, r) { | 
|  | // Container and Item using a node of type Container:Item. | 
|  | const char xmpData[] = | 
|  | "http://ns.adobe.com/xap/1.0/\0" | 
|  | "<x:xmpmeta xmlns:x=\"adobe:ns:meta/\" x:xmptk=\"XMP Core 5.5.0\">\n" | 
|  | " <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">\n" | 
|  | "  <rdf:Description rdf:about=\"\"\n" | 
|  | "    xmlns:Container=\"http://ns.google.com/photos/1.0/container/\"\n" | 
|  | "    xmlns:Item=\"http://ns.google.com/photos/1.0/container/item/\">\n" | 
|  | "   <Container:Directory>\n" | 
|  | "    <rdf:Seq>\n" | 
|  | "     <rdf:li rdf:parseType=\"Resource\">\n" | 
|  | "      <Container:Item>\n" | 
|  | "       <Item:Mime>image/jpeg</Item:Mime>\n" | 
|  | "       <Item:Semantic>Primary</Item:Semantic>\n" | 
|  | "      </Container:Item>\n" | 
|  | "     </rdf:li>\n" | 
|  | "     <rdf:li rdf:parseType=\"Resource\">\n" | 
|  | "      <Container:Item\n" | 
|  | "         Item:Semantic=\"RecoveryMap\"\n" | 
|  | "         Item:Mime=\"image/jpeg\"\n" | 
|  | "         Item:Length=\"49035\"/>\n" | 
|  | "     </rdf:li>\n" | 
|  | "    </rdf:Seq>\n" | 
|  | "   </Container:Directory>\n" | 
|  | "  </rdf:Description>\n" | 
|  | " </rdf:RDF>\n" | 
|  | "</x:xmpmeta>\n"; | 
|  | std::vector<sk_sp<SkData>> app1Params; | 
|  | app1Params.push_back(SkData::MakeWithoutCopy(xmpData, sizeof(xmpData) - 1)); | 
|  |  | 
|  | auto xmp = SkJpegXmp::Make(app1Params); | 
|  | REPORTER_ASSERT(r, xmp); | 
|  |  | 
|  | size_t offset = 999; | 
|  | size_t size = 999; | 
|  | REPORTER_ASSERT(r, xmp->getContainerGainmapLocation(&offset, &size)); | 
|  | REPORTER_ASSERT(r, size == 49035); | 
|  | } | 
|  |  | 
|  | DEF_TEST(AndroidCodec_xmpContainerTypedNodeRdfEquivalent, r) { | 
|  | // Container and Item using rdf:value and rdf:type pairs. | 
|  | const char xmpData[] = | 
|  | "http://ns.adobe.com/xap/1.0/\0" | 
|  | "<x:xmpmeta xmlns:x=\"adobe:ns:meta/\" x:xmptk=\"XMP Core 5.5.0\">\n" | 
|  | " <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">\n" | 
|  | "  <rdf:Description rdf:about=\"\"\n" | 
|  | "    xmlns:Container=\"http://ns.google.com/photos/1.0/container/\"\n" | 
|  | "    xmlns:Item=\"http://ns.google.com/photos/1.0/container/item/\">\n" | 
|  | "   <Container:Directory>\n" | 
|  | "    <rdf:Seq>\n" | 
|  | "     <rdf:li rdf:parseType=\"Resource\">\n" | 
|  | "      <rdf:value rdf:parseType=\"Resource\">\n" | 
|  | "       <Item:Mime>image/jpeg</Item:Mime>\n" | 
|  | "       <Item:Semantic>Primary</Item:Semantic>\n" | 
|  | "      </rdf:value>\n" | 
|  | "      <rdf:type rdf:resource=\"Item\"/>\n" | 
|  | "     </rdf:li>\n" | 
|  | "     <rdf:li rdf:parseType=\"Resource\">\n" | 
|  | "      <rdf:value rdf:parseType=\"Resource\">\n" | 
|  | "       <Item:Semantic>RecoveryMap</Item:Semantic>\n" | 
|  | "       <Item:Mime>image/jpeg</Item:Mime>\n" | 
|  | "       <Item:Length>49035</Item:Length>\n" | 
|  | "      </rdf:value>\n" | 
|  | "      <rdf:type rdf:resource=\"Item\"/>\n" | 
|  | "     </rdf:li>\n" | 
|  | "    </rdf:Seq>\n" | 
|  | "   </Container:Directory>\n" | 
|  | "  </rdf:Description>\n" | 
|  | " </rdf:RDF>\n" | 
|  | "</x:xmpmeta>\n"; | 
|  | std::vector<sk_sp<SkData>> app1Params; | 
|  | app1Params.push_back(SkData::MakeWithoutCopy(xmpData, sizeof(xmpData) - 1)); | 
|  |  | 
|  | auto xmp = SkJpegXmp::Make(app1Params); | 
|  | REPORTER_ASSERT(r, xmp); | 
|  |  | 
|  | size_t offset = 999; | 
|  | size_t size = 999; | 
|  | REPORTER_ASSERT(r, xmp->getContainerGainmapLocation(&offset, &size)); | 
|  | REPORTER_ASSERT(r, size == 49035); | 
|  | } | 
|  |  | 
|  | // Render a single pixel of an applied gainmap and return it. | 
|  | SkColor4f render_gainmap_pixel(float renderHdrRatio, | 
|  | const SkBitmap& baseBitmap, | 
|  | const SkBitmap& gainmapBitmap, | 
|  | const SkGainmapInfo& gainmapInfo, | 
|  | int x, | 
|  | int y) { | 
|  | SkImageInfo testPixelInfo = SkImageInfo::Make( | 
|  | 1, 1, kRGBA_F16_SkColorType, kPremul_SkAlphaType, SkColorSpace::MakeSRGB()); | 
|  | SkBitmap testPixelBitmap = render_gainmap( | 
|  | testPixelInfo, renderHdrRatio, baseBitmap, gainmapBitmap, gainmapInfo, x, y); | 
|  | return testPixelBitmap.getColor4f(0, 0); | 
|  | } | 
|  |  | 
|  | static bool approx_eq(float x, float y, float epsilon) { return std::abs(x - y) < epsilon; } | 
|  |  | 
|  | static bool approx_eq_rgb(const SkColor4f& x, const SkColor4f& y, float epsilon) { | 
|  | return approx_eq(x.fR, y.fR, epsilon) && approx_eq(x.fG, y.fG, epsilon) && | 
|  | approx_eq(x.fB, y.fB, epsilon); | 
|  | } | 
|  |  | 
|  | DEF_TEST(AndroidCodec_jpegGainmapDecode, r) { | 
|  | const struct Rec { | 
|  | const char* path; | 
|  | SkISize dimensions; | 
|  | SkColor originColor; | 
|  | SkColor farCornerColor; | 
|  | float logRatioMin; | 
|  | float logRatioMax; | 
|  | float hdrRatioMin; | 
|  | float hdrRatioMax; | 
|  | SkGainmapInfo::Type type; | 
|  | } recs[] = { | 
|  | {"images/iphone_13_pro.jpeg", | 
|  | SkISize::Make(1512, 2016), | 
|  | 0xFF3B3B3B, | 
|  | 0xFF101010, | 
|  | 0.f, | 
|  | 1.f, | 
|  | 1.f, | 
|  | 2.71828f, | 
|  | SkGainmapInfo::Type::kMultiPicture}, | 
|  | {"images/hdrgm.jpg", | 
|  | SkISize::Make(188, 250), | 
|  | 0xFFE9E9E9, | 
|  | 0xFFAAAAAA, | 
|  | -2.209409f, | 
|  | 2.209409f, | 
|  | 1.f, | 
|  | 9.110335f, | 
|  | SkGainmapInfo::Type::kHDRGM}, | 
|  | }; | 
|  |  | 
|  | TestStream::Type kStreamTypes[] = { | 
|  | TestStream::Type::kUnseekable, | 
|  | TestStream::Type::kSeekable, | 
|  | TestStream::Type::kMemoryMapped, | 
|  | }; | 
|  | for (const auto& streamType : kStreamTypes) { | 
|  | bool useFileStream = streamType != TestStream::Type::kMemoryMapped; | 
|  | for (const auto& rec : recs) { | 
|  | auto stream = GetResourceAsStream(rec.path, useFileStream); | 
|  | REPORTER_ASSERT(r, stream); | 
|  | auto testStream = std::make_unique<TestStream>(streamType, stream.get()); | 
|  |  | 
|  | SkBitmap baseBitmap; | 
|  | SkBitmap gainmapBitmap; | 
|  | SkGainmapInfo gainmapInfo; | 
|  | decode_all(r, std::move(testStream), baseBitmap, gainmapBitmap, gainmapInfo); | 
|  |  | 
|  | // Spot-check the image size and pixels. | 
|  | REPORTER_ASSERT(r, gainmapBitmap.dimensions() == rec.dimensions); | 
|  | REPORTER_ASSERT(r, gainmapBitmap.getColor(0, 0) == rec.originColor); | 
|  | REPORTER_ASSERT( | 
|  | r, | 
|  | gainmapBitmap.getColor(rec.dimensions.fWidth - 1, rec.dimensions.fHeight - 1) == | 
|  | rec.farCornerColor); | 
|  |  | 
|  | // Verify the gainmap rendering parameters. | 
|  | constexpr float kEpsilon = 1e-3f; | 
|  | REPORTER_ASSERT(r, approx_eq(gainmapInfo.fLogRatioMin.fR, rec.logRatioMin, kEpsilon)); | 
|  | REPORTER_ASSERT(r, approx_eq(gainmapInfo.fLogRatioMin.fG, rec.logRatioMin, kEpsilon)); | 
|  | REPORTER_ASSERT(r, approx_eq(gainmapInfo.fLogRatioMin.fB, rec.logRatioMin, kEpsilon)); | 
|  |  | 
|  | REPORTER_ASSERT(r, approx_eq(gainmapInfo.fLogRatioMax.fR, rec.logRatioMax, kEpsilon)); | 
|  | REPORTER_ASSERT(r, approx_eq(gainmapInfo.fLogRatioMax.fG, rec.logRatioMax, kEpsilon)); | 
|  | REPORTER_ASSERT(r, approx_eq(gainmapInfo.fLogRatioMax.fB, rec.logRatioMax, kEpsilon)); | 
|  |  | 
|  | REPORTER_ASSERT(r, approx_eq(gainmapInfo.fHdrRatioMin, rec.hdrRatioMin, kEpsilon)); | 
|  | REPORTER_ASSERT(r, approx_eq(gainmapInfo.fHdrRatioMax, rec.hdrRatioMax, kEpsilon)); | 
|  |  | 
|  | REPORTER_ASSERT(r, gainmapInfo.fType == rec.type); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | DEF_TEST(AndroidCodec_jpegNoGainmap, r) { | 
|  | // This test image has a large APP16 segment that will stress the various SkJpegSourceMgrs' | 
|  | // data skipping paths. | 
|  | const char* path = "images/icc-v2-gbr.jpg"; | 
|  |  | 
|  | TestStream::Type kStreamTypes[] = { | 
|  | TestStream::Type::kUnseekable, | 
|  | TestStream::Type::kSeekable, | 
|  | TestStream::Type::kMemoryMapped, | 
|  | }; | 
|  | for (const auto& streamType : kStreamTypes) { | 
|  | bool useFileStream = streamType != TestStream::Type::kMemoryMapped; | 
|  | auto stream = GetResourceAsStream(path, useFileStream); | 
|  | REPORTER_ASSERT(r, stream); | 
|  | auto testStream = std::make_unique<TestStream>(streamType, stream.get()); | 
|  |  | 
|  | // Decode the base bitmap. | 
|  | SkCodec::Result result = SkCodec::kSuccess; | 
|  | std::unique_ptr<SkCodec> baseCodec = | 
|  | SkJpegCodec::MakeFromStream(std::move(testStream), &result); | 
|  | REPORTER_ASSERT(r, baseCodec); | 
|  | SkBitmap baseBitmap; | 
|  | baseBitmap.allocPixels(baseCodec->getInfo()); | 
|  | REPORTER_ASSERT(r, | 
|  | SkCodec::kSuccess == baseCodec->getPixels(baseBitmap.info(), | 
|  | baseBitmap.getPixels(), | 
|  | baseBitmap.rowBytes())); | 
|  |  | 
|  | std::unique_ptr<SkAndroidCodec> androidCodec = | 
|  | SkAndroidCodec::MakeFromCodec(std::move(baseCodec)); | 
|  | REPORTER_ASSERT(r, androidCodec); | 
|  |  | 
|  | // Try to extract the gainmap info and stream. It should fail. | 
|  | SkGainmapInfo gainmapInfo; | 
|  | std::unique_ptr<SkStream> gainmapStream; | 
|  | REPORTER_ASSERT(r, !androidCodec->getAndroidGainmap(&gainmapInfo, &gainmapStream)); | 
|  | } | 
|  | } | 
|  |  | 
|  | #ifdef SK_ENCODE_JPEG | 
|  | DEF_TEST(AndroidCodec_jpegGainmapTranscode, r) { | 
|  | const char* path = "images/iphone_13_pro.jpeg"; | 
|  | SkBitmap baseBitmap[2]; | 
|  | SkBitmap gainmapBitmap[2]; | 
|  | SkGainmapInfo gainmapInfo[2]; | 
|  |  | 
|  | // Decode an MPF-based gainmap image. | 
|  | decode_all(r, GetResourceAsStream(path), baseBitmap[0], gainmapBitmap[0], gainmapInfo[0]); | 
|  |  | 
|  | constexpr float kEpsilon = 1e-2f; | 
|  | for (size_t i = 0; i < 2; ++i) { | 
|  | SkDynamicMemoryWStream encodeStream; | 
|  | bool encodeResult = false; | 
|  |  | 
|  | if (i == 0) { | 
|  | // Transcode to JpegR. | 
|  | encodeResult = SkJpegGainmapEncoder::EncodeJpegR(&encodeStream, | 
|  | baseBitmap[0].pixmap(), | 
|  | SkJpegEncoder::Options(), | 
|  | gainmapBitmap[0].pixmap(), | 
|  | SkJpegEncoder::Options(), | 
|  | gainmapInfo[0]); | 
|  | } else { | 
|  | // Transcode to HDRGM. | 
|  | encodeResult = SkJpegGainmapEncoder::EncodeHDRGM(&encodeStream, | 
|  | baseBitmap[0].pixmap(), | 
|  | SkJpegEncoder::Options(), | 
|  | gainmapBitmap[0].pixmap(), | 
|  | SkJpegEncoder::Options(), | 
|  | gainmapInfo[0]); | 
|  | } | 
|  | REPORTER_ASSERT(r, encodeResult); | 
|  | auto encodeData = encodeStream.detachAsData(); | 
|  |  | 
|  | // Decode the just-encoded image. | 
|  | auto decodeStream = std::make_unique<SkMemoryStream>(encodeData); | 
|  | decode_all(r, std::move(decodeStream), baseBitmap[1], gainmapBitmap[1], gainmapInfo[1]); | 
|  |  | 
|  | // HDRGM will have the same rendering parameters. | 
|  | REPORTER_ASSERT( | 
|  | r, | 
|  | approx_eq_rgb(gainmapInfo[0].fLogRatioMin, gainmapInfo[1].fLogRatioMin, kEpsilon)); | 
|  | REPORTER_ASSERT( | 
|  | r, | 
|  | approx_eq_rgb(gainmapInfo[0].fLogRatioMax, gainmapInfo[1].fLogRatioMax, kEpsilon)); | 
|  | REPORTER_ASSERT( | 
|  | r, | 
|  | approx_eq_rgb( | 
|  | gainmapInfo[0].fGainmapGamma, gainmapInfo[1].fGainmapGamma, kEpsilon)); | 
|  | REPORTER_ASSERT( | 
|  | r, | 
|  | approx_eq(gainmapInfo[0].fEpsilonSdr.fR, gainmapInfo[1].fEpsilonSdr.fR, kEpsilon)); | 
|  | REPORTER_ASSERT( | 
|  | r, | 
|  | approx_eq(gainmapInfo[0].fEpsilonHdr.fR, gainmapInfo[1].fEpsilonHdr.fR, kEpsilon)); | 
|  | REPORTER_ASSERT( | 
|  | r, approx_eq(gainmapInfo[0].fHdrRatioMin, gainmapInfo[1].fHdrRatioMin, kEpsilon)); | 
|  | REPORTER_ASSERT( | 
|  | r, approx_eq(gainmapInfo[0].fHdrRatioMax, gainmapInfo[1].fHdrRatioMax, kEpsilon)); | 
|  |  | 
|  | #ifdef SK_ENABLE_SKSL | 
|  | // Render a few pixels and verify that they come out the same. Rendering requires SkSL. | 
|  | const struct Rec { | 
|  | int x; | 
|  | int y; | 
|  | float hdrRatio; | 
|  | SkColor4f expectedColor; | 
|  | SkColorType forcedColorType; | 
|  | } recs[] = { | 
|  | {1446, 1603, 1.05f, {0.984375f, 1.004883f, 1.008789f, 1.f}, kUnknown_SkColorType}, | 
|  | {1446, 1603, 100.f, {1.147461f, 1.170898f, 1.174805f, 1.f}, kUnknown_SkColorType}, | 
|  | {1446, 1603, 100.f, {1.147461f, 1.170898f, 1.174805f, 1.f}, kGray_8_SkColorType}, | 
|  | {1446, 1603, 100.f, {1.147461f, 1.170898f, 1.174805f, 1.f}, kAlpha_8_SkColorType}, | 
|  | {1446, 1603, 100.f, {1.147461f, 1.170898f, 1.174805f, 1.f}, kR8_unorm_SkColorType}, | 
|  | }; | 
|  |  | 
|  | for (const auto& rec : recs) { | 
|  | SkBitmap gainmapBitmap0; | 
|  | SkASSERT(gainmapBitmap[0].colorType() == kGray_8_SkColorType); | 
|  |  | 
|  | // Force various different single-channel formats, to ensure that they all work. Note | 
|  | // that when the color type is forced to kAlpha_8_SkColorType, the shader will always | 
|  | // read (0,0,0,1) if the alpha type is kOpaque_SkAlphaType. | 
|  | if (rec.forcedColorType == kUnknown_SkColorType) { | 
|  | gainmapBitmap0 = gainmapBitmap[0]; | 
|  | } else { | 
|  | gainmapBitmap0.installPixels(gainmapBitmap[0] | 
|  | .info() | 
|  | .makeColorType(rec.forcedColorType) | 
|  | .makeAlphaType(kPremul_SkAlphaType), | 
|  | gainmapBitmap[0].getPixels(), | 
|  | gainmapBitmap[0].rowBytes()); | 
|  | } | 
|  | SkColor4f p0 = render_gainmap_pixel( | 
|  | rec.hdrRatio, baseBitmap[0], gainmapBitmap0, gainmapInfo[0], rec.x, rec.y); | 
|  | SkColor4f p1 = render_gainmap_pixel( | 
|  | rec.hdrRatio, baseBitmap[1], gainmapBitmap[1], gainmapInfo[1], rec.x, rec.y); | 
|  |  | 
|  | REPORTER_ASSERT(r, approx_eq_rgb(p0, p1, kEpsilon)); | 
|  | } | 
|  | #endif  // SK_ENABLE_SKSL | 
|  | } | 
|  | } | 
|  | #endif  // SK_ENCODE_JPEG |