| /* | |
| * Copyright 2024 Google LLC | |
| * | |
| * Use of this source code is governed by a BSD-style license that can be | |
| * found in the LICENSE file. | |
| */ | |
| #include "include/codec/SkPngRustDecoder.h" | |
| #include <algorithm> | |
| #include <cstdint> | |
| #include <functional> | |
| #include <memory> | |
| #include <optional> | |
| #include <utility> | |
| #include <vector> | |
| #include "include/codec/SkAndroidCodec.h" | |
| #include "include/codec/SkCodec.h" | |
| #include "include/codec/SkCodecAnimation.h" | |
| #include "include/codec/SkPngChunkReader.h" | |
| #if defined(SK_CODEC_DECODES_PNG_WITH_LIBPNG) | |
| #include "include/codec/SkPngDecoder.h" | |
| #endif | |
| #include "include/core/SkBitmap.h" | |
| #include "include/core/SkColor.h" | |
| #include "include/core/SkColorSpace.h" | |
| #include "include/core/SkColorType.h" | |
| #include "include/core/SkData.h" | |
| #include "include/core/SkImage.h" | |
| #include "include/core/SkImageInfo.h" | |
| #include "include/core/SkPixmap.h" | |
| #include "include/core/SkRect.h" | |
| #include "include/core/SkRefCnt.h" | |
| #include "include/core/SkSize.h" | |
| #include "include/core/SkStream.h" | |
| #include "include/private/SkGainmapInfo.h" | |
| #include "src/codec/SkCodecPriv.h" | |
| #include "tests/FakeStreams.h" | |
| #include "tests/Test.h" | |
| #include "tools/Resources.h" | |
| #define REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, actualResult) \ | |
| REPORTER_ASSERT(r, \ | |
| actualResult == SkCodec::kSuccess, \ | |
| "actualResult=\"%s\" != kSuccess", \ | |
| SkCodec::ResultToString(actualResult)) | |
| namespace { | |
| // This class wraps another SkStream. It does not own the underlying stream, so | |
| // that the underlying stream can be reused starting from where the first | |
| // client left off. This mimics Android's JavaInputStreamAdaptor. | |
| // Replicated from tests/CodecExactReadTest.cpp. | |
| class UnowningStream : public SkStream { | |
| public: | |
| explicit UnowningStream(SkStream* stream) : fStream(stream) {} | |
| size_t read(void* buf, size_t bytes) override { return fStream->read(buf, bytes); } | |
| bool rewind() override { return fStream->rewind(); } | |
| bool isAtEnd() const override { return fStream->isAtEnd(); } | |
| private: | |
| SkStream* fStream; // Unowned. | |
| }; | |
| } // namespace | |
| // Helper wrapping a call to `SkPngRustDecoder::Decode`. | |
| std::unique_ptr<SkCodec> SkPngRustDecoderDecode(skiatest::Reporter* r, const char* path) { | |
| sk_sp<SkData> data = GetResourceAsData(path); | |
| if (!data) { | |
| ERRORF(r, "Missing resource: %s", path); | |
| return nullptr; | |
| } | |
| SkCodec::Result result; | |
| std::unique_ptr<SkCodec> codec = | |
| SkPngRustDecoder::Decode(std::make_unique<SkMemoryStream>(std::move(data)), &result); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result); | |
| return codec; | |
| } | |
| void AssertPixelColor(skiatest::Reporter* r, | |
| const SkPixmap& pixmap, | |
| int x, | |
| int y, | |
| SkColor expectedColor, | |
| const char* description) { | |
| SkASSERT(r); | |
| SkASSERT(x >= 0); | |
| SkASSERT(y >= 0); | |
| SkASSERT(description); | |
| REPORTER_ASSERT(r, x < pixmap.width(), "x=%d >= width=%d", x, pixmap.width()); | |
| REPORTER_ASSERT(r, y < pixmap.height(), "y=%d >= height=%d", y, pixmap.height()); | |
| REPORTER_ASSERT(r, | |
| kN32_SkColorType == pixmap.colorType(), | |
| "kN32_SkColorType != pixmap.ColorType()=%d", | |
| pixmap.colorType()); | |
| SkColor actualColor = pixmap.getColor(x, y); | |
| REPORTER_ASSERT(r, | |
| actualColor == expectedColor, | |
| "actualColor=0x%08X != expectedColor==0x%08X at (%d,%d) (%s)", | |
| actualColor, | |
| expectedColor, | |
| x, | |
| y, | |
| description); | |
| } | |
| void AssertGreenPixel(skiatest::Reporter* r, | |
| const SkPixmap& pixmap, | |
| int x, | |
| int y, | |
| const char* description = "Expecting a green pixel") { | |
| AssertPixelColor(r, pixmap, x, y, SkColorSetRGB(0x00, 0xFF, 0x00), description); | |
| } | |
| void AssertRedPixel(skiatest::Reporter* r, | |
| const SkPixmap& pixmap, | |
| int x, | |
| int y, | |
| const char* description = "Expecting a red pixel") { | |
| AssertPixelColor(r, pixmap, x, y, SkColorSetRGB(0xFF, 0x00, 0x00), description); | |
| } | |
| void AssertBluePixel(skiatest::Reporter* r, | |
| const SkPixmap& pixmap, | |
| int x, | |
| int y, | |
| const char* description = "Expecting a blue pixel") { | |
| AssertPixelColor(r, pixmap, x, y, SkColorSetRGB(0x00, 0x00, 0xFF), description); | |
| } | |
| void AssertSingleGreenFrame(skiatest::Reporter* r, | |
| int expectedWidth, | |
| int expectedHeight, | |
| const char* resourcePath) { | |
| std::unique_ptr<SkCodec> codec = SkPngRustDecoderDecode(r, resourcePath); | |
| if (!codec) { | |
| return; | |
| } | |
| REPORTER_ASSERT(r, codec->getFrameCount() == 1); | |
| REPORTER_ASSERT(r, codec->getRepetitionCount() == 0); | |
| SkCodec::FrameInfo info; | |
| REPORTER_ASSERT(r, codec->getFrameInfo(0, &info)); | |
| REPORTER_ASSERT(r, info.fBlend == SkCodecAnimation::Blend::kSrc); | |
| REPORTER_ASSERT(r, info.fDisposalMethod == SkCodecAnimation::DisposalMethod::kKeep); | |
| REPORTER_ASSERT(r, info.fFrameRect == SkIRect::MakeWH(expectedWidth, expectedHeight)); | |
| REPORTER_ASSERT(r, info.fRequiredFrame == SkCodec::kNoFrame); | |
| auto [image, result] = codec->getImage(); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result); | |
| REPORTER_ASSERT(r, image); | |
| REPORTER_ASSERT(r, | |
| image->width() == expectedWidth, | |
| "actualWidth=%d != expectedWidth=%d", | |
| image->width(), | |
| expectedWidth); | |
| REPORTER_ASSERT(r, | |
| image->height() == expectedHeight, | |
| "actualHeight=%d != expectedHeight=%d", | |
| image->height(), | |
| expectedHeight); | |
| SkPixmap pixmap; | |
| REPORTER_ASSERT(r, image->peekPixels(&pixmap)); | |
| AssertGreenPixel(r, pixmap, 0, 0); | |
| AssertGreenPixel(r, pixmap, expectedWidth / 2, expectedHeight / 2); | |
| } | |
| // Sentinel fill color used to detect pixels left unwritten by the codec. | |
| static constexpr SkColor kUnreadSentinelColor = SK_ColorMAGENTA; | |
| static std::unique_ptr<SkCodec> StartIncrementalDecode( | |
| skiatest::Reporter* r, | |
| std::unique_ptr<SkStream> stream, | |
| SkBitmap* dstBitmap, | |
| std::function<SkImageInfo(const SkImageInfo&)> makeDstInfo = nullptr, | |
| const SkCodec::Options& options = {}) { | |
| SkCodec::Result result; | |
| std::unique_ptr<SkCodec> codec = SkPngRustDecoder::Decode(std::move(stream), &result); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result); | |
| if (!codec) { | |
| return nullptr; | |
| } | |
| const SkImageInfo dstInfo = makeDstInfo ? makeDstInfo(codec->getInfo()) : codec->getInfo(); | |
| SkImageInfo allocInfo = dstInfo; | |
| if (options.fSubset) { | |
| allocInfo = allocInfo.makeDimensions(options.fSubset->size()); | |
| } | |
| REPORTER_ASSERT(r, dstBitmap->tryAllocPixels(allocInfo)); | |
| dstBitmap->eraseColor(options.fZeroInitialized == SkCodec::kYes_ZeroInitialized | |
| ? SK_ColorTRANSPARENT | |
| : kUnreadSentinelColor); | |
| result = codec->startIncrementalDecode( | |
| dstInfo, dstBitmap->getPixels(), dstBitmap->rowBytes(), &options); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result); | |
| if (result != SkCodec::kSuccess) { | |
| return nullptr; | |
| } | |
| return codec; | |
| } | |
| static std::optional<SkBitmap> DecodeIncrementalOneShot( | |
| skiatest::Reporter* r, | |
| sk_sp<SkData> data, | |
| std::function<SkImageInfo(const SkImageInfo&)> makeDstInfo = nullptr, | |
| const SkCodec::Options& options = {}) { | |
| SkBitmap bm; | |
| std::unique_ptr<SkCodec> codec = StartIncrementalDecode( | |
| r, SkMemoryStream::Make(std::move(data)), &bm, std::move(makeDstInfo), options); | |
| if (!codec) { | |
| return std::nullopt; | |
| } | |
| SkCodec::Result result = codec->incrementalDecode(); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result); | |
| if (result != SkCodec::kSuccess) { | |
| return std::nullopt; | |
| } | |
| return bm; | |
| } | |
| static void CompareBitmaps(skiatest::Reporter* r, const SkBitmap& bm1, const SkBitmap& bm2) { | |
| const SkImageInfo& info = bm1.info(); | |
| if (info != bm2.info()) { | |
| ERRORF(r, "Bitmaps have different image infos!"); | |
| return; | |
| } | |
| const size_t rowBytes = info.minRowBytes(); | |
| for (int i = 0; i < info.height(); i++) { | |
| if (0 != memcmp(bm1.getAddr(0, i), bm2.getAddr(0, i), rowBytes)) { | |
| ERRORF(r, "Bitmaps have different pixels, starting on line %i!", i); | |
| return; | |
| } | |
| } | |
| } | |
| // Returns the number of rows in `bm` up to and including the last row that contains at least one | |
| // pixel differing from `sentinelColor`. | |
| static int CountNonSentinelRows(const SkBitmap& bm, SkColor sentinelColor) { | |
| for (int y = bm.height() - 1; y >= 0; --y) { | |
| for (int x = 0; x < bm.width(); ++x) { | |
| if (bm.getColor(x, y) != sentinelColor) { | |
| return y + 1; | |
| } | |
| } | |
| } | |
| return 0; | |
| } | |
| // Returns the decoded height of `bm` (pre-filled with `sentinelColor`), measured as the number of | |
| // rows up to and including the last row that differs from `sentinelColor`, and asserts that every | |
| // pixel in rows `0 .. writtenRows - 1` was overwritten. | |
| static int CountWrittenRows(skiatest::Reporter* r, | |
| const SkBitmap& bm, | |
| SkColor sentinelColor = kUnreadSentinelColor) { | |
| const int writtenRows = CountNonSentinelRows(bm, sentinelColor); | |
| for (int y = 0; y < writtenRows; ++y) { | |
| for (int x = 0; x < bm.width(); ++x) { | |
| if (bm.getColor(x, y) == sentinelColor) { | |
| ERRORF(r, | |
| "Row %d < writtenRows (%d) still has sentinel pixel at (%d, %d)", | |
| y, | |
| writtenRows, | |
| x, | |
| y); | |
| return writtenRows; | |
| } | |
| } | |
| } | |
| return writtenRows; | |
| } | |
| // A valid `IEND` chunk has an empty payload: 4-byte length (0), 4-byte type ("IEND"), 4-byte CRC. | |
| static constexpr size_t kIendChunkSize = 12; | |
| static bool HasTrailingIendChunk(const SkData* data) { | |
| return data->size() > kIendChunkSize && | |
| memcmp(data->bytes() + data->size() - kIendChunkSize, "\0\0\0\0IEND", 8) == 0; | |
| } | |
| // Returns a copy of `data` with the last byte of the trailing `IEND` CRC flipped. | |
| static sk_sp<SkData> WithCorruptIendCrc(const SkData* data) { | |
| SkASSERT(HasTrailingIendChunk(data)); | |
| sk_sp<SkData> copy = SkData::MakeWithCopy(data->data(), data->size()); | |
| static_cast<uint8_t*>(copy->writable_data())[copy->size() - 1] ^= 0xFF; | |
| return copy; | |
| } | |
| // Returns `data` without the trailing 4-byte `IEND` CRC (the 8-byte chunk header is kept). | |
| static sk_sp<SkData> WithoutIendCrc(const SkData* data) { | |
| SkASSERT(HasTrailingIendChunk(data)); | |
| return SkData::MakeSubset(data, 0, data->size() - 4); | |
| } | |
| // Returns `data` without the entire 12-byte trailing `IEND` chunk. | |
| static sk_sp<SkData> WithoutIendChunk(const SkData* data) { | |
| SkASSERT(HasTrailingIendChunk(data)); | |
| return SkData::MakeSubset(data, 0, data->size() - kIendChunkSize); | |
| } | |
| // Returns a copy of `data` with the last byte of the CRC of its `idatIndex`-th (0-based) `IDAT` | |
| // chunk flipped, or nullptr if there is no such chunk. Decoding then fails with `kErrorInInput` | |
| // once that chunk has been read, after the rows from the preceding `IDAT` chunks. | |
| static sk_sp<SkData> WithCorruptIdatCrc(const SkData* data, int idatIndex) { | |
| constexpr size_t kSignatureSize = 8; | |
| constexpr size_t kChunkHeaderSize = 8; // 4-byte length + 4-byte type. | |
| constexpr size_t kCrcSize = 4; | |
| sk_sp<SkData> copy = SkData::MakeWithCopy(data->data(), data->size()); | |
| uint8_t* bytes = static_cast<uint8_t*>(copy->writable_data()); | |
| size_t offset = kSignatureSize; | |
| while (offset + kChunkHeaderSize <= copy->size()) { | |
| const size_t length = (size_t{bytes[offset]} << 24) | (size_t{bytes[offset + 1]} << 16) | | |
| (size_t{bytes[offset + 2]} << 8) | size_t{bytes[offset + 3]}; | |
| const size_t crcEnd = offset + kChunkHeaderSize + length + kCrcSize; | |
| if (crcEnd > copy->size()) { | |
| break; | |
| } | |
| if (memcmp(bytes + offset + 4, "IDAT", 4) == 0 && idatIndex-- == 0) { | |
| bytes[crcEnd - 1] ^= 0xFF; | |
| return copy; | |
| } | |
| offset = crcEnd; | |
| } | |
| return nullptr; | |
| } | |
| static std::optional<SkBitmap> DecodeToBitmap(skiatest::Reporter* r, | |
| const SkImageInfo& dstInfo, | |
| SkCodec::Result expectedResult, | |
| std::function<SkCodec::Result(SkBitmap&)> decode) { | |
| SkBitmap bm; | |
| REPORTER_ASSERT(r, bm.tryAllocPixels(dstInfo)); | |
| SkCodec::Result result = decode(bm); | |
| REPORTER_ASSERT(r, | |
| result == expectedResult, | |
| "Expected %s, got %s", | |
| SkCodec::ResultToString(expectedResult), | |
| SkCodec::ResultToString(result)); | |
| if (result != expectedResult || | |
| (result != SkCodec::kSuccess && result != SkCodec::kIncompleteInput && | |
| result != SkCodec::kErrorInInput)) { | |
| return std::nullopt; | |
| } | |
| return bm; | |
| } | |
| static std::optional<SkBitmap> DecodeAndroidPixels( | |
| skiatest::Reporter* r, | |
| std::unique_ptr<SkCodec> codec, | |
| int sampleSize, | |
| std::function<SkIRect(const SkImageInfo&)> getSubset = nullptr, | |
| size_t maxDecodeMemory = 0, | |
| SkCodec::Result expectedResult = SkCodec::kSuccess) { | |
| REPORTER_ASSERT(r, codec); | |
| if (!codec) { | |
| return std::nullopt; | |
| } | |
| auto androidCodec = SkAndroidCodec::MakeFromCodec(std::move(codec)); | |
| REPORTER_ASSERT(r, androidCodec); | |
| if (!androidCodec) { | |
| return std::nullopt; | |
| } | |
| SkISize sampledDims = androidCodec->getSampledDimensions(sampleSize); | |
| SkImageInfo info = | |
| androidCodec->getInfo().makeDimensions(sampledDims).makeColorType(kN32_SkColorType); | |
| SkIRect subset; | |
| if (getSubset) { | |
| subset = getSubset(androidCodec->getInfo()); | |
| int subsetWidth = SkCodecPriv::GetSampledDimension(subset.width(), sampleSize); | |
| int subsetHeight = SkCodecPriv::GetSampledDimension(subset.height(), sampleSize); | |
| info = info.makeWH(subsetWidth, subsetHeight); | |
| } | |
| return DecodeToBitmap(r, info, expectedResult, [&](SkBitmap& bm) { | |
| SkAndroidCodec::AndroidOptions opts; | |
| opts.fSampleSize = sampleSize; | |
| if (getSubset) { | |
| opts.fSubset = ⊂ | |
| } | |
| opts.fMaxDecodeMemory = maxDecodeMemory; | |
| bm.eraseColor(kUnreadSentinelColor); | |
| return androidCodec->getAndroidPixels(bm.info(), bm.getPixels(), bm.rowBytes(), &opts); | |
| }); | |
| } | |
| // Decodes `codec` with `SkCodec::getPixels`. The plain-`SkCodec` counterpart of | |
| // `DecodeAndroidPixels`. | |
| // | |
| // The destination is zeroed for `kYes_ZeroInitialized` (as that option requires), and otherwise | |
| // pre-filled with `kUnreadSentinelColor` so that pixels left unwritten by the codec are detectable. | |
| static std::optional<SkBitmap> DecodePixels( | |
| skiatest::Reporter* r, | |
| std::unique_ptr<SkCodec> codec, | |
| std::function<SkImageInfo(const SkImageInfo&)> makeDstInfo = nullptr, | |
| const SkCodec::Options& options = {}, | |
| SkCodec::Result expectedResult = SkCodec::kSuccess) { | |
| REPORTER_ASSERT(r, codec); | |
| if (!codec) { | |
| return std::nullopt; | |
| } | |
| SkImageInfo info = makeDstInfo ? makeDstInfo(codec->getInfo()) : codec->getInfo(); | |
| return DecodeToBitmap(r, info, expectedResult, [&](SkBitmap& bm) { | |
| bm.eraseColor(options.fZeroInitialized == SkCodec::kYes_ZeroInitialized | |
| ? SK_ColorTRANSPARENT | |
| : kUnreadSentinelColor); | |
| return codec->getPixels(bm.info(), bm.getPixels(), bm.rowBytes(), &options); | |
| }); | |
| } | |
| // Returns default `SkCodec::Options` with `fMaxDecodeMemory` set to `maxDecodeMemory`. | |
| static SkCodec::Options MakeBudgetOptions(size_t maxDecodeMemory) { | |
| SkCodec::Options options; | |
| options.fMaxDecodeMemory = maxDecodeMemory; | |
| return options; | |
| } | |
| // Verifies that decoding `path` with `expectedBudgetBytes - 1` fails with `kOutOfMemory` and | |
| // decoding with `expectedBudgetBytes` succeeds. | |
| static void AssertDecodeBudget( | |
| skiatest::Reporter* r, | |
| const char* path, | |
| size_t expectedBudgetBytes, | |
| std::function<std::optional<SkBitmap>(std::unique_ptr<SkCodec>, size_t, SkCodec::Result)> | |
| decode) { | |
| REPORTER_ASSERT( | |
| r, | |
| !decode(SkPngRustDecoderDecode(r, path), expectedBudgetBytes - 1, SkCodec::kOutOfMemory) | |
| .has_value()); | |
| REPORTER_ASSERT(r, | |
| decode(SkPngRustDecoderDecode(r, path), expectedBudgetBytes, SkCodec::kSuccess) | |
| .has_value()); | |
| } | |
| static void AssertAndroidDecodeSampling( | |
| skiatest::Reporter* r, | |
| const char* path, | |
| int sampleSize, | |
| std::function<SkIRect(const SkImageInfo&)> getSubset = nullptr) { | |
| sk_sp<SkData> data = GetResourceAsData(path); | |
| if (!data) { | |
| ERRORF(r, "Missing resource: %s", path); | |
| return; | |
| } | |
| std::optional<SkBitmap> rustBm = DecodeAndroidPixels( | |
| r, | |
| SkPngRustDecoder::Decode(std::make_unique<SkMemoryStream>(data), nullptr), | |
| sampleSize, | |
| getSubset); | |
| #if defined(SK_CODEC_DECODES_PNG_WITH_LIBPNG) | |
| std::optional<SkBitmap> libpngBm = DecodeAndroidPixels( | |
| r, | |
| SkPngDecoder::Decode(std::make_unique<SkMemoryStream>(data), nullptr), | |
| sampleSize, | |
| getSubset); | |
| if (!rustBm || !libpngBm) { | |
| return; | |
| } | |
| CompareBitmaps(r, *rustBm, *libpngBm); | |
| #else | |
| REPORTER_ASSERT(r, rustBm.has_value()); | |
| #endif | |
| } | |
| #if defined(SK_CODEC_USES_PNG_WITH_RUST_FOR_ANDROID) | |
| static void AssertAndroidStaticApng(skiatest::Reporter* r, | |
| const char* path, | |
| std::optional<SkColor> expectedTopLeftColor = std::nullopt) { | |
| std::unique_ptr<SkCodec> codec = SkPngRustDecoderDecode(r, path); | |
| if (!codec) { | |
| return; | |
| } | |
| REPORTER_ASSERT(r, codec->isAnimated() == SkCodec::IsAnimated::kNo); | |
| REPORTER_ASSERT(r, codec->getFrameCount() == 1); | |
| REPORTER_ASSERT(r, codec->getRepetitionCount() == 0); | |
| SkCodec::FrameInfo info; | |
| REPORTER_ASSERT(r, codec->getFrameInfo(0, &info)); | |
| REPORTER_ASSERT(r, info.fRequiredFrame == SkCodec::kNoFrame); | |
| if (expectedTopLeftColor.has_value()) { | |
| auto [image, result] = codec->getImage(); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result); | |
| if (image) { | |
| SkPixmap pixmap; | |
| REPORTER_ASSERT(r, image->peekPixels(&pixmap)); | |
| AssertPixelColor(r, pixmap, 0, 0, *expectedTopLeftColor, "IDAT top-left pixel"); | |
| } | |
| } | |
| // Verify full, downsampled, and subset+downsampled decodes succeed and match `libpng`. | |
| AssertAndroidDecodeSampling(r, path, /*sampleSize=*/1); | |
| AssertAndroidDecodeSampling(r, path, /*sampleSize=*/2); | |
| AssertAndroidDecodeSampling(r, path, /*sampleSize=*/2, [](const SkImageInfo& imgInfo) { | |
| return SkIRect::MakeXYWH(0, 0, imgInfo.width() / 2, imgInfo.height() / 2); | |
| }); | |
| } | |
| #endif | |
| #if !defined(SK_CODEC_USES_PNG_WITH_RUST_FOR_ANDROID) | |
| // Decodes into a buffer surrounded by guard bytes to detect out-of-bounds writes | |
| // even in builds without ASAN, and asserts that the decode is refused with | |
| // `SkCodec::kUnimplemented`. | |
| static void AssertAndroidDecodeRefused(skiatest::Reporter* r, | |
| const sk_sp<SkData>& data, | |
| int sampleSize, | |
| bool useSubset, | |
| size_t customRowBytes = 0) { | |
| constexpr size_t kGuardBytes = 64 * 1024; | |
| constexpr uint8_t kGuardValue = 0x5A; | |
| auto codec = SkPngRustDecoder::Decode(std::make_unique<SkMemoryStream>(data), nullptr); | |
| REPORTER_ASSERT(r, codec); | |
| if (!codec) { | |
| return; | |
| } | |
| auto androidCodec = SkAndroidCodec::MakeFromCodec(std::move(codec)); | |
| REPORTER_ASSERT(r, androidCodec); | |
| if (!androidCodec) { | |
| return; | |
| } | |
| const SkISize fullDims = androidCodec->getInfo().dimensions(); | |
| SkIRect subset = SkIRect::MakeWH(fullDims.width(), std::max(1, fullDims.height() / 2)); | |
| if (useSubset) { | |
| REPORTER_ASSERT(r, androidCodec->getSupportedSubset(&subset)); | |
| } | |
| const SkISize dims = | |
| useSubset ? SkISize::Make(SkCodecPriv::GetSampledDimension(subset.width(), sampleSize), | |
| SkCodecPriv::GetSampledDimension(subset.height(), sampleSize)) | |
| : androidCodec->getSampledDimensions(sampleSize); | |
| const SkImageInfo info = androidCodec->getInfo() | |
| .makeDimensions(dims) | |
| .makeColorType(kN32_SkColorType) | |
| .makeAlphaType(kPremul_SkAlphaType); | |
| const size_t rowBytes = customRowBytes ? customRowBytes : info.minRowBytes(); | |
| const size_t pixelBytes = info.computeByteSize(rowBytes); | |
| std::vector<uint8_t> buffer(kGuardBytes + pixelBytes + kGuardBytes, kGuardValue); | |
| SkAndroidCodec::AndroidOptions options; | |
| options.fSampleSize = sampleSize; | |
| if (useSubset) { | |
| options.fSubset = ⊂ | |
| } | |
| SkCodec::Result result = | |
| androidCodec->getAndroidPixels(info, buffer.data() + kGuardBytes, rowBytes, &options); | |
| REPORTER_ASSERT(r, | |
| result == SkCodec::kUnimplemented, | |
| "Expected the decode to be refused with kUnimplemented, got %s " | |
| "(sampleSize=%d, useSubset=%d, rowBytes=%zu)", | |
| SkCodec::ResultToString(result), | |
| sampleSize, | |
| (int)useSubset, | |
| rowBytes); | |
| const bool guardBytesIntact = std::all_of(buffer.begin(), | |
| buffer.begin() + kGuardBytes, | |
| [](uint8_t b) { return b == kGuardValue; }) && | |
| std::all_of(buffer.begin() + kGuardBytes + pixelBytes, | |
| buffer.end(), | |
| [](uint8_t b) { return b == kGuardValue; }); | |
| REPORTER_ASSERT(r, | |
| guardBytesIntact, | |
| "The decode wrote outside of the destination buffer " | |
| "(sampleSize=%d, useSubset=%d, rowBytes=%zu)", | |
| sampleSize, | |
| (int)useSubset, | |
| rowBytes); | |
| } | |
| #endif | |
| // Asserts that `decode` produces the same pixels for `path` as for copies of it with a corrupt | |
| // and with a truncated trailing `IEND` CRC. Both variants leave all of `IDAT` intact. | |
| static void AssertBrokenIendTailMatchesIntact( | |
| skiatest::Reporter* r, | |
| const char* path, | |
| std::function<std::optional<SkBitmap>(sk_sp<SkData>)> decode) { | |
| sk_sp<SkData> fullData = GetResourceAsData(path); | |
| if (!fullData) { | |
| ERRORF(r, "Missing resource: %s", path); | |
| return; | |
| } | |
| std::optional<SkBitmap> refBm = decode(fullData); | |
| REPORTER_ASSERT(r, refBm.has_value()); | |
| if (!refBm) { | |
| return; | |
| } | |
| const struct { | |
| const char* fLabel; | |
| sk_sp<SkData> fData; | |
| } variants[] = { | |
| {"corrupt IEND CRC", WithCorruptIendCrc(fullData.get())}, | |
| {"truncated IEND CRC", WithoutIendCrc(fullData.get())}, | |
| }; | |
| for (const auto& [label, variant] : variants) { | |
| std::optional<SkBitmap> bm = decode(variant); | |
| REPORTER_ASSERT( | |
| r, bm.has_value(), "%s: decode failed for variant '%s'", path, label); | |
| if (bm) { | |
| CompareBitmaps(r, *refBm, *bm); | |
| } | |
| } | |
| } | |
| // Full decodes, in the color configurations that cover both decode paths. | |
| static void AssertDecodesWithBrokenIendTail(skiatest::Reporter* r, const char* path) { | |
| // kRGBA_8888 + kUnpremul on an RGBA8 source with an `iCCP` chunk is the only combination that | |
| // satisfies `canReadRow()`, i.e. the only one decoded by `incrementalDecode` (`read_row`). | |
| // The others go through `incrementalDecodeXForm`. | |
| static constexpr struct { | |
| SkColorType fColorType; | |
| SkAlphaType fAlphaType; | |
| } kConfigs[] = { | |
| {kN32_SkColorType, kUnpremul_SkAlphaType}, | |
| {kN32_SkColorType, kPremul_SkAlphaType}, | |
| {kRGBA_8888_SkColorType, kUnpremul_SkAlphaType}, | |
| }; | |
| for (const auto& config : kConfigs) { | |
| AssertBrokenIendTailMatchesIntact(r, path, [&](sk_sp<SkData> data) { | |
| return DecodePixels(r, | |
| SkPngRustDecoder::Decode( | |
| std::make_unique<SkMemoryStream>(std::move(data)), nullptr), | |
| [&](const SkImageInfo& info) { | |
| return info.makeColorType(config.fColorType) | |
| .makeAlphaType(config.fAlphaType); | |
| }); | |
| }); | |
| } | |
| } | |
| // Same, for a sampled decode. It goes through `SkAndroidCodec`, always decodes to `kN32`, and | |
| // skips `finish_decoding()` altogether rather than ignoring its result. | |
| static void AssertSampledDecodeWithBrokenIendTail(skiatest::Reporter* r, | |
| const char* path, | |
| int sampleSize) { | |
| auto decode = [&](sk_sp<SkData> data) { | |
| return DecodeAndroidPixels( | |
| r, | |
| SkPngRustDecoder::Decode(std::make_unique<SkMemoryStream>(std::move(data)), | |
| nullptr), | |
| sampleSize); | |
| }; | |
| AssertBrokenIendTailMatchesIntact(r, path, decode); | |
| // Because a non-interlaced sampled decode stops after the last needed scanline without reading | |
| // the `IDAT` CRC or the 8-byte `IEND` chunk header (`stopsBeforeEndOfFrame`), it must also | |
| // succeed when the entire 12-byte `IEND` chunk is stripped. | |
| sk_sp<SkData> fullData = GetResourceAsData(path); | |
| std::optional<SkBitmap> refBm = decode(fullData); | |
| std::optional<SkBitmap> withoutIendBm = decode(WithoutIendChunk(fullData.get())); | |
| REPORTER_ASSERT(r, withoutIendBm.has_value()); | |
| if (refBm && withoutIendBm) { | |
| CompareBitmaps(r, *refBm, *withoutIendBm); | |
| } | |
| } | |
| sk_sp<SkImage> DecodeLastFrame(skiatest::Reporter* r, SkCodec* codec) { | |
| int frameCount = codec->getFrameCount(); | |
| sk_sp<SkImage> image; | |
| SkCodec::Result result = SkCodec::kSuccess; | |
| for (int i = 0; i < frameCount; i++) { | |
| SkCodec::FrameInfo info; | |
| REPORTER_ASSERT(r, codec->getFrameInfo(i, &info)); | |
| // This test method only supports `kKeep` disposal method. | |
| SkASSERT(info.fDisposalMethod == SkCodecAnimation::DisposalMethod::kKeep); | |
| if (!image) { | |
| std::tie(image, result) = codec->getImage(); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result); | |
| if (result != SkCodec::kSuccess) { | |
| return nullptr; | |
| } | |
| } else { | |
| SkPixmap pixmap; | |
| REPORTER_ASSERT(r, image->peekPixels(&pixmap)); | |
| SkCodec::Options options; | |
| options.fZeroInitialized = SkCodec::kNo_ZeroInitialized; | |
| options.fSubset = nullptr; | |
| options.fFrameIndex = i; | |
| options.fPriorFrame = i - 1; | |
| result = codec->getPixels(pixmap, &options); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result); | |
| if (result != SkCodec::kSuccess) { | |
| return nullptr; | |
| } | |
| } | |
| } | |
| return image; | |
| } | |
| sk_sp<SkImage> DecodeLastFrame(skiatest::Reporter* r, const char* resourcePath) { | |
| std::unique_ptr<SkCodec> codec = SkPngRustDecoderDecode(r, resourcePath); | |
| if (!codec) { | |
| return nullptr; | |
| } | |
| return DecodeLastFrame(r, codec.get()); | |
| } | |
| void AssertAnimationRepetitionCount(skiatest::Reporter* r, | |
| int expectedRepetitionCount, | |
| const char* resourcePath) { | |
| std::unique_ptr<SkCodec> codec = SkPngRustDecoderDecode(r, resourcePath); | |
| if (!codec) { | |
| return; | |
| } | |
| int actualRepetitionCount = codec->getRepetitionCount(); | |
| REPORTER_ASSERT(r, | |
| actualRepetitionCount == expectedRepetitionCount, | |
| "actualRepetitionCount=%d != expectedRepetitionCount=%d", | |
| actualRepetitionCount, | |
| expectedRepetitionCount); | |
| } | |
| // Test based on | |
| // https://philip.html5.org/tests/apng/tests.html#trivial-static-image | |
| DEF_TEST(RustPngCodec_apng_basic_trivial_static_image, r) { | |
| AssertSingleGreenFrame(r, 128, 64, "images/apng-test-suite--basic--trivial-static-image.png"); | |
| } | |
| // Test based on | |
| // https://philip.html5.org/tests/apng/tests.html#trivial-animated-image-one-frame-using-default-image | |
| DEF_TEST(RustPngCodec_apng_basic_using_default_image, r) { | |
| const char* kResource = "images/apng-test-suite--basic--using-default-image.png"; | |
| #if !defined(SK_CODEC_USES_PNG_WITH_RUST_FOR_ANDROID) | |
| AssertSingleGreenFrame(r, 128, 64, kResource); | |
| #else | |
| AssertAndroidStaticApng(r, kResource, SK_ColorGREEN); | |
| #endif | |
| } | |
| // Test based on | |
| // https://philip.html5.org/tests/apng/tests.html#trivial-animated-image-one-frame-ignoring-default-image | |
| // | |
| // The input file contains the following PNG chunks: IHDR, acTL, IDAT, fcTL, | |
| // fdAT, IEND. Presence of acTL chunk + no fcTL chunk before IDAT means that | |
| // the IDAT chunk is *not* part of the animation: | |
| // * On non-Android (APNG-aware), `SkPngRustCodec` ignores the red `IDAT` default | |
| // image and decodes the green `fdAT` animation frame. | |
| // * On Android (`SK_CODEC_USES_PNG_WITH_RUST_FOR_ANDROID`), `SkPngRustCodec` | |
| // ignores `acTL`/`fcTL` for `SkPngCodec` (libpng) parity and decodes the red | |
| // `IDAT` default image as a static single-frame PNG. | |
| DEF_TEST(RustPngCodec_apng_basic_ignoring_default_image, r) { | |
| const char* kResource = "images/apng-test-suite--basic--ignoring-default-image.png"; | |
| #if !defined(SK_CODEC_USES_PNG_WITH_RUST_FOR_ANDROID) | |
| AssertSingleGreenFrame(r, 128, 64, kResource); | |
| #else | |
| AssertAndroidStaticApng(r, kResource, SK_ColorRED); | |
| #endif | |
| } | |
| // Regression test for b/562862995: When an APNG has no `fcTL` chunk before | |
| // `IDAT` (`IDAT` is the fallback default image and Frame 0 starts at `fdAT`), | |
| // callers that decode Frame 0 directly via `getImage()`, `getPixels()`, | |
| // `startIncrementalDecode()`, or `SkAndroidCodec::getAndroidPixels()` without | |
| // calling `getFrameCount()` first must still succeed rather than failing with | |
| // `kInvalidParameters` (decoding the green `fdAT` frame on non-Android, or the | |
| // red `IDAT` default image on Android where APNG chunks are ignored). | |
| DEF_TEST(RustPngCodec_apng_ignoring_default_image_without_getFrameCount, r) { | |
| const char* kResource = "images/apng-test-suite--basic--ignoring-default-image.png"; | |
| #if !defined(SK_CODEC_USES_PNG_WITH_RUST_FOR_ANDROID) | |
| // 1. Direct `getImage()` / `getPixels()` without `getFrameCount()`. | |
| { | |
| std::unique_ptr<SkCodec> codec = SkPngRustDecoderDecode(r, kResource); | |
| REPORTER_ASSERT(r, codec); | |
| auto [image, result] = codec->getImage(); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result); | |
| if (image) { | |
| SkPixmap pixmap; | |
| REPORTER_ASSERT(r, image->peekPixels(&pixmap)); | |
| AssertGreenPixel(r, pixmap, 0, 0); | |
| } | |
| } | |
| // 2. Direct `startIncrementalDecode()` + `incrementalDecode()` without `getFrameCount()`. | |
| { | |
| std::unique_ptr<SkCodec> codec = SkPngRustDecoderDecode(r, kResource); | |
| REPORTER_ASSERT(r, codec); | |
| SkBitmap bitmap; | |
| REPORTER_ASSERT(r, bitmap.tryAllocPixels(codec->getInfo())); | |
| SkCodec::Result result = | |
| codec->startIncrementalDecode(bitmap.info(), bitmap.getPixels(), bitmap.rowBytes()); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result); | |
| if (result == SkCodec::kSuccess) { | |
| result = codec->incrementalDecode(); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result); | |
| AssertGreenPixel(r, bitmap.pixmap(), 0, 0); | |
| } | |
| } | |
| // 3. Direct `SkAndroidCodec::getAndroidPixels()` (with downsampling) without `getFrameCount()`. | |
| { | |
| std::unique_ptr<SkCodec> codec = SkPngRustDecoderDecode(r, kResource); | |
| REPORTER_ASSERT(r, codec); | |
| std::unique_ptr<SkAndroidCodec> androidCodec = | |
| SkAndroidCodec::MakeFromCodec(std::move(codec)); | |
| REPORTER_ASSERT(r, androidCodec); | |
| SkISize sampledDims = androidCodec->getSampledDimensions(2); | |
| SkImageInfo sampledInfo = androidCodec->getInfo().makeDimensions(sampledDims); | |
| SkBitmap bitmap; | |
| REPORTER_ASSERT(r, bitmap.tryAllocPixels(sampledInfo)); | |
| SkAndroidCodec::AndroidOptions options; | |
| options.fSampleSize = 2; | |
| SkCodec::Result result = androidCodec->getAndroidPixels( | |
| sampledInfo, bitmap.getPixels(), bitmap.rowBytes(), &options); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result); | |
| if (result == SkCodec::kSuccess) { | |
| AssertGreenPixel(r, bitmap.pixmap(), 0, 0); | |
| } | |
| } | |
| #else | |
| AssertAndroidStaticApng(r, kResource, SK_ColorRED); | |
| #endif | |
| } | |
| // Test based on | |
| // https://philip.html5.org/tests/apng/tests.html#apng-dispose-op-none-basic | |
| // | |
| // This test covers two aspects of `SkPngRustCodec` implementation: | |
| // | |
| // * Blink expects that `onGetFrameCount` returns the total frame count when | |
| // the complete image resource is available (i.e. a lower frame count should | |
| // only happen upon `SkCodec::kIncompleteInput`). Before http://review.skia.org/911038 | |
| // `SkPngRustCodec::onGetFrameCount` would not discover additional frames if | |
| // previous frames haven't been decoded yet. | |
| // * Skia client (e.g. Blink; or here the testcase) is expected to handle | |
| // `SkCodecAnimation::DisposalMethod` and populate the target buffer (and | |
| // `SkCodec::Options::fPriorFrame`) with the expected pixels. OTOH, | |
| // `SkPngRustCodec` needs to handle `SkCodecAnimation::Blend` - without this | |
| // the final frame in this test will contain red pixels. | |
| DEF_TEST(RustPngCodec_apng_dispose_op_none_basic, r) { | |
| const char* kResource = "images/apng-test-suite--dispose-ops--none-basic.png"; | |
| #if !defined(SK_CODEC_USES_PNG_WITH_RUST_FOR_ANDROID) | |
| std::unique_ptr<SkCodec> codec = SkPngRustDecoderDecode(r, kResource); | |
| if (!codec) { | |
| return; | |
| } | |
| REPORTER_ASSERT(r, codec->getFrameCount() == 3); | |
| REPORTER_ASSERT(r, codec->getRepetitionCount() == 0); | |
| // We should have `FrameInfo` for all 3 frames. | |
| SkCodec::FrameInfo info[3]; | |
| REPORTER_ASSERT(r, codec->getFrameInfo(0, &info[0])); | |
| REPORTER_ASSERT(r, codec->getFrameInfo(1, &info[1])); | |
| REPORTER_ASSERT(r, codec->getFrameInfo(2, &info[2])); | |
| // The codec should realize that the `SkStream` contains all the data of the | |
| // first 2 frames. Currently `SkPngRustCodec::onGetFrameCount` stops after | |
| // parsing the final, 3rd `fcTL` chunk and therefore it can't tell if the | |
| // subsequent `fdAT` chunk has been fully received or not. | |
| REPORTER_ASSERT(r, info[0].fFullyReceived); | |
| REPORTER_ASSERT(r, info[1].fFullyReceived); | |
| REPORTER_ASSERT(r, !info[2].fFullyReceived); | |
| // Spot-check frame metadata. | |
| REPORTER_ASSERT(r, info[1].fAlphaType == kUnpremul_SkAlphaType); | |
| REPORTER_ASSERT(r, info[1].fBlend == SkCodecAnimation::Blend::kSrcOver); | |
| REPORTER_ASSERT(r, info[1].fDisposalMethod == SkCodecAnimation::DisposalMethod::kKeep); | |
| REPORTER_ASSERT(r, info[1].fDuration == 100, "dur = %d", info[1].fDuration); | |
| REPORTER_ASSERT(r, info[1].fFrameRect == SkIRect::MakeWH(128, 64)); | |
| REPORTER_ASSERT(r, info[1].fHasAlphaWithinBounds); | |
| REPORTER_ASSERT(r, info[1].fRequiredFrame == 0); | |
| // Validate contents of the first frame. | |
| auto [image, result] = codec->getImage(); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result); | |
| REPORTER_ASSERT(r, image); | |
| REPORTER_ASSERT(r, image->width() == 128, "width %d != 128", image->width()); | |
| REPORTER_ASSERT(r, image->height() == 64, "height %d != 64", image->height()); | |
| SkPixmap pixmap; | |
| REPORTER_ASSERT(r, image->peekPixels(&pixmap)); | |
| AssertRedPixel(r, pixmap, 0, 0, "Frame #0 should be red"); | |
| // Validate contents of the second frame. | |
| SkCodec::Options options; | |
| options.fZeroInitialized = SkCodec::kNo_ZeroInitialized; | |
| options.fSubset = nullptr; | |
| options.fFrameIndex = 1; // We want to decode the second frame. | |
| options.fPriorFrame = 0; // `pixmap` contains the first frame before `getPixels` call. | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, codec->getPixels(pixmap, &options)); | |
| AssertGreenPixel(r, pixmap, 0, 0, "Frame #1 should be green"); | |
| // Validate contents of the third frame. | |
| options.fFrameIndex = 2; // We want to decode the second frame. | |
| options.fPriorFrame = 1; // `pixmap` contains the second frame before `getPixels` call. | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, codec->getPixels(pixmap, &options)); | |
| AssertGreenPixel(r, pixmap, 0, 0, "Frame #2 should be green"); | |
| #else | |
| AssertAndroidStaticApng(r, kResource, SK_ColorRED); | |
| #endif | |
| } | |
| // This test covers an incomplete input scenario: | |
| // | |
| // * Only half of 1st frame is available during `onGetFrameCount`. | |
| // In this situation `onGetFrameCount` may consume the whole input in a | |
| // (futile in this case) attempt to discover `fcTL` chunks for 2nd and 3rd | |
| // frame. This will mean that the input stream is in the middle of the 1st | |
| // frame - no longer positioned correctly for decoding the 1st frame. | |
| // * Full input is available when subsequently decoding 1st frame. | |
| DEF_TEST(RustPngCodec_apng_dispose_op_none_basic_incomplete_input1, r) { | |
| const char* path = "images/apng-test-suite--dispose-ops--none-basic.png"; | |
| sk_sp<SkData> data = GetResourceAsData(path); | |
| if (!data) { | |
| ERRORF(r, "Missing resource: %s", path); | |
| return; | |
| } | |
| size_t fullLength = data->size(); | |
| // Initially expose roughly middle of `IDAT` chunk (in this image `fcTL` is | |
| // present before the `IDAT` chunk and therefore the `IDAT` chunk is part of | |
| // the animated image). | |
| constexpr size_t kInitialBytes = 0xAD; | |
| auto streamForCodec = std::make_unique<HaltingStream>(std::move(data), kInitialBytes); | |
| HaltingStream* retainedStream = streamForCodec.get(); | |
| SkCodec::Result result; | |
| std::unique_ptr<SkCodec> codec = SkPngRustDecoder::Decode(std::move(streamForCodec), &result); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result); | |
| if (!codec) { | |
| return; | |
| } | |
| SkBitmap bitmap; | |
| if (!bitmap.tryAllocN32Pixels(codec->dimensions().width(), codec->dimensions().height())) { | |
| ERRORF(r, "Failed to allocate SkBitmap"); | |
| return; | |
| } | |
| // Try to provoke the codec to consume the currently-available part of the | |
| // input stream. | |
| // | |
| // At this point only the metadata for the first frame is available. | |
| int frameCount = codec->getFrameCount(); | |
| REPORTER_ASSERT(r, frameCount == 1); | |
| // Make the rest of the input available to the codec. | |
| retainedStream->addNewData(fullLength); | |
| // Try to decode the first frame and check its contents. | |
| sk_sp<SkImage> image; | |
| std::tie(image, result) = codec->getImage(); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result); | |
| REPORTER_ASSERT(r, image); | |
| REPORTER_ASSERT(r, image->width() == 128, "width %d != 128", image->width()); | |
| REPORTER_ASSERT(r, image->height() == 64, "height %d != 64", image->height()); | |
| SkPixmap pixmap; | |
| REPORTER_ASSERT(r, image->peekPixels(&pixmap)); | |
| AssertRedPixel(r, pixmap, 0, 0, "Frame #0 should be red"); | |
| } | |
| // This test covers an incomplete input scenario: | |
| // | |
| // * Only half of 1st frame is available during the initial `incrementalDecode`. | |
| // * Before retrying, `getFrameCount` is called. This should *not* reposition | |
| // the stream while in the middle of an active incremental decode. | |
| // * Then we retry `incrementalDecode`. | |
| DEF_TEST(RustPngCodec_apng_dispose_op_none_basic_incomplete_input2, r) { | |
| const char* path = "images/apng-test-suite--dispose-ops--none-basic.png"; | |
| sk_sp<SkData> data = GetResourceAsData(path); | |
| if (!data) { | |
| ERRORF(r, "Missing resource: %s", path); | |
| return; | |
| } | |
| size_t fullLength = data->size(); | |
| constexpr size_t kInitialBytes = 0x8D; // Roughly middle of IDAT chunk. | |
| auto streamForCodec = std::make_unique<HaltingStream>(std::move(data), kInitialBytes); | |
| HaltingStream* retainedStream = streamForCodec.get(); | |
| SkCodec::Result result; | |
| std::unique_ptr<SkCodec> codec = SkPngRustDecoder::Decode(std::move(streamForCodec), &result); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result); | |
| if (!codec) { | |
| return; | |
| } | |
| SkBitmap bitmap; | |
| if (!bitmap.tryAllocN32Pixels(codec->dimensions().width(), codec->dimensions().height())) { | |
| ERRORF(r, "Failed to allocate SkBitmap"); | |
| return; | |
| } | |
| const SkPixmap& pixmap = bitmap.pixmap(); | |
| // Fill the `bitmap` with blue pixels to detect which pixels have been filled | |
| // by the codec during a partially-successful `incrementalDecode`. | |
| // | |
| // (The first frame has all red pixels.) | |
| bitmap.erase(SkColorSetRGB(0, 0, 0xFF), bitmap.bounds()); | |
| AssertBluePixel(r, pixmap, 0, 0); | |
| AssertBluePixel(r, pixmap, 40, 29); | |
| AssertBluePixel(r, pixmap, 80, 35); | |
| AssertBluePixel(r, pixmap, 127, 63); | |
| // Decode partially-available, incomplete image. | |
| SkCodec::Options options; | |
| options.fZeroInitialized = SkCodec::kNo_ZeroInitialized; | |
| options.fSubset = nullptr; | |
| options.fFrameIndex = 0; | |
| options.fPriorFrame = SkCodec::kNoFrame; | |
| result = codec->startIncrementalDecode(bitmap.pixmap().info(), | |
| bitmap.pixmap().writable_addr(), | |
| bitmap.pixmap().rowBytes(), | |
| &options); | |
| REPORTER_ASSERT(r, result == SkCodec::kSuccess); | |
| int rowsDecoded = -1; | |
| result = codec->incrementalDecode(&rowsDecoded); | |
| REPORTER_ASSERT(r, result == SkCodec::kIncompleteInput); | |
| REPORTER_ASSERT(r, rowsDecoded == 10, "actual rowsDecoded = %d", rowsDecoded); | |
| AssertRedPixel(r, pixmap, 0, 0); | |
| AssertBluePixel(r, pixmap, 40, 29); | |
| AssertBluePixel(r, pixmap, 80, 35); | |
| AssertBluePixel(r, pixmap, 127, 63); | |
| // Make the rest of the input available to the codec. | |
| retainedStream->addNewData(fullLength); | |
| // Try to provoke the codec to consume further into the input stream (doing | |
| // this would loose the position inside the currently active incremental | |
| // decode). | |
| // | |
| // At this point metadata of all the frames is available, but the codec | |
| // shouldn't read the other two `fcTL` chunks during an active incremental | |
| // decode. | |
| int frameCount = codec->getFrameCount(); | |
| REPORTER_ASSERT(r, frameCount == 1); | |
| // Check that all the pixels of the first frame got decoded. | |
| rowsDecoded = -1; | |
| result = codec->incrementalDecode(&rowsDecoded); | |
| REPORTER_ASSERT(r, result == SkCodec::kSuccess); | |
| REPORTER_ASSERT(r, rowsDecoded == -1); // Not set when `kSuccess`. | |
| AssertRedPixel(r, pixmap, 0, 0); | |
| AssertRedPixel(r, pixmap, 40, 29); | |
| AssertRedPixel(r, pixmap, 80, 35); | |
| AssertRedPixel(r, pixmap, 127, 63); | |
| } | |
| // Test based on | |
| // https://philip.html5.org/tests/apng/tests.html#apng-blend-op-source-on-solid-colour | |
| DEF_TEST(RustPngCodec_apng_blend_ops_source_on_solid, r) { | |
| const char* kResource = "images/apng-test-suite--blend-ops--source-on-solid.png"; | |
| #if !defined(SK_CODEC_USES_PNG_WITH_RUST_FOR_ANDROID) | |
| std::unique_ptr<SkCodec> codec = SkPngRustDecoderDecode(r, kResource); | |
| if (!codec) { | |
| return; | |
| } | |
| sk_sp<SkImage> image = DecodeLastFrame(r, codec.get()); | |
| if (!image) { | |
| return; | |
| } | |
| SkPixmap pixmap; | |
| REPORTER_ASSERT(r, image->peekPixels(&pixmap)); | |
| AssertGreenPixel(r, pixmap, 0, 0); | |
| SkCodec::FrameInfo info; | |
| REPORTER_ASSERT(r, codec->getFrameInfo(1, &info)); | |
| REPORTER_ASSERT(r, info.fBlend == SkCodecAnimation::Blend::kSrc); | |
| REPORTER_ASSERT(r, info.fRequiredFrame == SkCodec::kNoFrame); | |
| #else | |
| AssertAndroidStaticApng(r, kResource); | |
| #endif | |
| } | |
| // Test based on | |
| // https://philip.html5.org/tests/apng/tests.html#apng-blend-op-source-on-nearly-transparent-colour | |
| DEF_TEST(RustPngCodec_apng_blend_ops_source_on_nearly_transparent, r) { | |
| const char* kResource = "images/apng-test-suite--blend-ops--source-on-nearly-transparent.png"; | |
| #if !defined(SK_CODEC_USES_PNG_WITH_RUST_FOR_ANDROID) | |
| sk_sp<SkImage> image = DecodeLastFrame(r, kResource); | |
| if (!image) { | |
| return; | |
| } | |
| SkPixmap pixmap; | |
| REPORTER_ASSERT(r, image->peekPixels(&pixmap)); | |
| AssertPixelColor(r, | |
| pixmap, | |
| 0, | |
| 0, | |
| SkColorSetARGB(0x02, 0x00, 0xFF, 0x00), | |
| "Expecting a nearly transparent pixel"); | |
| #else | |
| AssertAndroidStaticApng(r, kResource); | |
| #endif | |
| } | |
| // Test based on | |
| // https://philip.html5.org/tests/apng/tests.html#apng-blend-op-over-on-solid-and-transparent-colours | |
| DEF_TEST(RustPngCodec_apng_blend_ops_over_on_solid_and_transparent, r) { | |
| const char* kResource = "images/apng-test-suite--blend-ops--over-on-solid-and-transparent.png"; | |
| #if !defined(SK_CODEC_USES_PNG_WITH_RUST_FOR_ANDROID) | |
| sk_sp<SkImage> image = DecodeLastFrame(r, kResource); | |
| if (!image) { | |
| return; | |
| } | |
| SkPixmap pixmap; | |
| REPORTER_ASSERT(r, image->peekPixels(&pixmap)); | |
| AssertGreenPixel(r, pixmap, 0, 0); | |
| #else | |
| AssertAndroidStaticApng(r, kResource); | |
| #endif | |
| } | |
| // Test based on | |
| // https://philip.html5.org/tests/apng/tests.html#apng-blend-op-over-repeatedly-with-nearly-transparent-colours | |
| DEF_TEST(RustPngCodec_apng_blend_ops_over_repeatedly, r) { | |
| const char* kResource = "images/apng-test-suite--blend-ops--over-repeatedly.png"; | |
| #if !defined(SK_CODEC_USES_PNG_WITH_RUST_FOR_ANDROID) | |
| sk_sp<SkImage> image = DecodeLastFrame(r, kResource); | |
| if (!image) { | |
| return; | |
| } | |
| SkPixmap pixmap; | |
| REPORTER_ASSERT(r, image->peekPixels(&pixmap)); | |
| AssertGreenPixel(r, pixmap, 0, 0); | |
| #else | |
| AssertAndroidStaticApng(r, kResource); | |
| #endif | |
| } | |
| // Test based on | |
| // https://philip.html5.org/tests/apng/tests.html#apng-dispose-op-none-in-region | |
| DEF_TEST(RustPngCodec_apng_regions_dispose_op_none, r) { | |
| const char* kResource = "images/apng-test-suite--regions--dispose-op-none.png"; | |
| #if !defined(SK_CODEC_USES_PNG_WITH_RUST_FOR_ANDROID) | |
| sk_sp<SkImage> image = DecodeLastFrame(r, kResource); | |
| if (!image) { | |
| return; | |
| } | |
| // Check all pixels. | |
| // | |
| // * The image (and the first frame) is 128x64 | |
| // * The 2nd frame is 64x32 at offset (32,16) | |
| // * The 3rd frame is 1x1 at offset (0,0) | |
| SkPixmap pixmap; | |
| REPORTER_ASSERT(r, image->peekPixels(&pixmap)); | |
| for (int y = 0; y < pixmap.height(); y++) { | |
| for (int x = 0; x < pixmap.width(); x++) { | |
| AssertGreenPixel(r, pixmap, x, y); | |
| } | |
| } | |
| #else | |
| AssertAndroidStaticApng(r, kResource); | |
| #endif | |
| } | |
| // Test based on | |
| // https://philip.html5.org/tests/apng/tests.html#num-plays-0 | |
| DEF_TEST(RustPngCodec_apng_num_plays_0, r) { | |
| const char* kResource = "images/apng-test-suite--num-plays--0.png"; | |
| #if !defined(SK_CODEC_USES_PNG_WITH_RUST_FOR_ANDROID) | |
| AssertAnimationRepetitionCount(r, SkCodec::kRepetitionCountInfinite, kResource); | |
| #else | |
| AssertAndroidStaticApng(r, kResource); | |
| #endif | |
| } | |
| // Test based on | |
| // https://philip.html5.org/tests/apng/tests.html#num-plays-1 | |
| DEF_TEST(RustPngCodec_apng_num_plays_1, r) { | |
| const char* kResource = "images/apng-test-suite--num-plays--1.png"; | |
| #if !defined(SK_CODEC_USES_PNG_WITH_RUST_FOR_ANDROID) | |
| AssertAnimationRepetitionCount(r, 0, kResource); | |
| #else | |
| AssertAndroidStaticApng(r, kResource); | |
| #endif | |
| } | |
| // Test based on | |
| // https://philip.html5.org/tests/apng/tests.html#num-plays-2 | |
| DEF_TEST(RustPngCodec_apng_num_plays_2, r) { | |
| const char* kResource = "images/apng-test-suite--num-plays--2.png"; | |
| #if !defined(SK_CODEC_USES_PNG_WITH_RUST_FOR_ANDROID) | |
| AssertAnimationRepetitionCount(r, 1, kResource); | |
| #else | |
| AssertAndroidStaticApng(r, kResource); | |
| #endif | |
| } | |
| // Test based on | |
| // https://philip.html5.org/tests/apng/tests.html#num-frames-outside-valid-range | |
| // | |
| // In this test the `acTL` chunk sets `num_frames` to `2147483649u` (or `0x80000001u`): | |
| // | |
| // * AFAICT version 1.0 of the APNG spec only says that "0 is not a valid value" | |
| // * The test suite webpage says that at one point the APNG spec said that | |
| // `num_frames` shall be "limited to the range 0 to (2^31)-1" | |
| DEF_TEST(RustPngCodec_apng_invalid_num_frames_outside_valid_range, r) { | |
| std::unique_ptr<SkCodec> codec = SkPngRustDecoderDecode( | |
| r, "images/apng-test-suite--invalid--num-frames-outside-valid-range.png"); | |
| if (!codec) { | |
| return; | |
| } | |
| // Calling `codec->getFrameCount` exercises the code used to discover and | |
| // parse `fcTL` chunks on non-Android (where `onGetFrameCount` returns the | |
| // number of successfully parsed `fcTL` chunks, 1, rather than the raw | |
| // `acTL.num_frames`), and returns 1 immediately on Android where `acTL` is | |
| // ignored. | |
| REPORTER_ASSERT(r, codec->getFrameCount() == 1); | |
| } | |
| DEF_TEST(RustPngCodec_png_swizzling_target_unimplemented, r) { | |
| std::unique_ptr<SkCodec> codec = | |
| SkPngRustDecoderDecode(r, "images/apng-test-suite--basic--ignoring-default-image.png"); | |
| if (!codec) { | |
| return; | |
| } | |
| REPORTER_ASSERT(r, codec->getFrameCount() == 1); | |
| // Ask to decode into an esoteric `SkColorType`: | |
| // | |
| // * Unsupported by `SkSwizzler`. | |
| // * Supported by `SkCodec::conversionSupported`. | |
| SkImageInfo dstInfo = SkImageInfo::Make( | |
| codec->dimensions(), kBGRA_10101010_XR_SkColorType, kPremul_SkAlphaType); | |
| auto [image, result] = codec->getImage(dstInfo); | |
| REPORTER_ASSERT(r, result == SkCodec::kUnimplemented); | |
| REPORTER_ASSERT(r, !image); | |
| } | |
| DEF_TEST(RustPngCodec_png_was_encoded_with_16_bits_or_more_per_component, r) { | |
| struct Test { | |
| const char* fFilename; | |
| bool fEncodedWith16bits; | |
| }; | |
| const std::array<Test, 4> kTests = { | |
| Test{"images/pngsuite/basn0g04.png", false}, // 4 bit (16 level) grayscale | |
| Test{"images/pngsuite/basn2c08.png", false}, // 3x8 bits rgb color | |
| Test{"images/pngsuite/basn2c16.png", true}, // 3x16 bits rgb color | |
| Test{"images/pngsuite/basn3p01.png", false} // 1 bit (2 color) paletted | |
| }; | |
| for (const auto& test : kTests) { | |
| std::unique_ptr<SkCodec> codec = SkPngRustDecoderDecode(r, test.fFilename); | |
| if (codec) { | |
| REPORTER_ASSERT(r, codec->hasHighBitDepthEncodedData() == test.fEncodedWith16bits); | |
| } | |
| } | |
| } | |
| DEF_TEST(RustPngCodec_png_cicp, r) { | |
| std::unique_ptr<SkCodec> codec = SkPngRustDecoderDecode(r, "images/cicp_pq.png"); | |
| if (!codec) { | |
| return; | |
| } | |
| const skcms_ICCProfile* profile = codec->getICCProfile(); | |
| REPORTER_ASSERT(r, profile); | |
| if (!profile) { | |
| return; | |
| } | |
| auto cs = SkColorSpace::Make(*profile); | |
| skcms_TransferFunction tf; | |
| cs->transferFn(&tf); | |
| REPORTER_ASSERT(r, skcms_TransferFunction_isPQish(&tf) || | |
| skcms_TransferFunction_isPQ(&tf)); | |
| } | |
| DEF_TEST(RustPngCodec_green15x15, r) { | |
| std::unique_ptr<SkCodec> codec = SkPngRustDecoderDecode(r, "images/green15x15.png"); | |
| if (!codec) { | |
| return; | |
| } | |
| SkImageInfo dstInfo = codec->getInfo(); | |
| dstInfo = dstInfo.makeColorSpace(SkColorSpace::MakeSRGB()); | |
| auto [image, result] = codec->getImage(dstInfo); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result); | |
| if (result != SkCodec::kSuccess) { | |
| return; | |
| } | |
| SkPixmap pixmap; | |
| REPORTER_ASSERT(r, image->peekPixels(&pixmap)); | |
| const SkColor kExpectedColor = SkColorSetARGB(0xFF, 0x00, 0x80, 0x00); | |
| AssertPixelColor(r, pixmap, 0, 0, kExpectedColor, "Expecting a dark green pixel"); | |
| } | |
| DEF_TEST(RustPngCodec_exif_orientation, r) { | |
| std::unique_ptr<SkCodec> codec = SkPngRustDecoderDecode(r, "images/F-exif-chunk-early.png"); | |
| if (!codec) { | |
| return; | |
| } | |
| REPORTER_ASSERT(r, codec->getOrigin() == kRightTop_SkEncodedOrigin); | |
| } | |
| DEF_TEST(RustPngCodec_f16_trc_tables, r) { | |
| std::unique_ptr<SkCodec> codec = SkPngRustDecoderDecode(r, "images/f16-trc-tables.png"); | |
| REPORTER_ASSERT(r, codec); | |
| const SkImageInfo info = codec->getInfo(); | |
| REPORTER_ASSERT(r, info.colorSpace()); | |
| // Decoding to F16 without color space conversion. | |
| const SkImageInfo dstInfo = info.makeColorType(kRGBA_F16_SkColorType) | |
| .makeColorSpace(nullptr); | |
| // This should not crash. | |
| auto [image, result] = codec->getImage(dstInfo); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result); | |
| } | |
| DEF_TEST(RustPngCodec_crbug445556737, r) { | |
| const char* kResource = "images/crbug445556737.png"; | |
| #if !defined(SK_CODEC_USES_PNG_WITH_RUST_FOR_ANDROID) | |
| sk_sp<SkImage> image = DecodeLastFrame(r, kResource); | |
| if (!image) { | |
| return; | |
| } | |
| // The main test verification is that there are no assertion failures nor | |
| // other crashes. Cursory verification below is supplementary/secondary. | |
| REPORTER_ASSERT(r, image->height() == 5); | |
| REPORTER_ASSERT(r, image->width() == 5); | |
| #else | |
| AssertAndroidStaticApng(r, kResource); | |
| #endif | |
| } | |
| DEF_TEST(RustPngCodec_invalid_profile, r) { | |
| // This image has an gamma value of 0. For parity with Blink, we want to disregard | |
| // the ICC profile in this case and create the codec without it. This is different | |
| // than libpng SkPngCodec behavior, which will default to an SRGB icc profile. | |
| std::unique_ptr<SkCodec> codec = | |
| SkPngRustDecoderDecode(r, "images/png-zero-gamma-color-profile.png"); | |
| REPORTER_ASSERT(r, codec); | |
| // There should be no ICC profile. | |
| REPORTER_ASSERT(r, !codec->getICCProfile()); | |
| // This should not crash. | |
| auto [image, result] = codec->getImage(codec->getInfo()); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result); | |
| } | |
| static void test_subset_decode(skiatest::Reporter* r, const char* resource) { | |
| skiatest::ReporterContext context(r, resource); | |
| std::unique_ptr<SkStream> stream(GetResourceAsStream(resource)); | |
| REPORTER_ASSERT(r, stream); | |
| std::unique_ptr<SkCodec> codec = SkCodec::MakeFromStream(std::move(stream)); | |
| REPORTER_ASSERT(r, codec); | |
| const SkImageInfo info = codec->getInfo(); | |
| SkBitmap bm; | |
| bm.allocPixels(info); | |
| codec->getPixels(info, bm.getPixels(), bm.rowBytes()); | |
| SkBitmap tiledBM; | |
| tiledBM.allocPixels(info); | |
| const int height = info.height(); | |
| const int width = info.width(); | |
| // Note that if numStripes does not evenly divide height there will be an extra | |
| // stripe. | |
| const int numStripes = 4; | |
| const int numVerticalStripes = 2; | |
| if (numStripes > height || numVerticalStripes > width) { | |
| // Image is too small. | |
| return; | |
| } | |
| const int stripeHeight = height / numStripes; | |
| const int stripeWidth = width / numVerticalStripes; | |
| // Iterate through the image twice. Once to decode odd stripes, and once for even. | |
| for (int oddEven = 1; oddEven >= 0; oddEven--) { | |
| for (int y = oddEven * stripeHeight; y < height; y += 2 * stripeHeight) { | |
| for (int xStripe = 0; xStripe < numVerticalStripes; xStripe++) { | |
| // Calculate all four bounds for the grid section | |
| const int top = y; | |
| const int bottom = std::min(y + stripeHeight, height); | |
| const int left = xStripe * stripeWidth; | |
| const int right = std::min((xStripe + 1) * stripeWidth, width); | |
| SkIRect subset = SkIRect::MakeLTRB(left, top, right, bottom); | |
| SkCodec::Options options; | |
| options.fSubset = ⊂ | |
| // Decode each subset tile into a tightly allocated temporary bitmap | |
| // (sized exactly to the subset). This is a valid use case that | |
| // replicates how clients (like Android) perform subset decodes. | |
| // Doing so explicitly exercises the tight-allocation path where the | |
| // stride is exactly the subset row size (lacking full-image stride | |
| // padding), ensuring fDstRowBytes is calculated correctly. | |
| SkBitmap subsetBM; | |
| subsetBM.allocPixels(info.makeDimensions(subset.size())); | |
| REPORTER_ASSERT( | |
| r, | |
| SkCodec::kSuccess == | |
| codec->startIncrementalDecode( | |
| info, subsetBM.getPixels(), subsetBM.rowBytes(), &options)); | |
| REPORTER_ASSERT(r, SkCodec::kSuccess == codec->incrementalDecode()); | |
| // Copy the decoded subset into the full tiled bitmap | |
| REPORTER_ASSERT(r, tiledBM.writePixels(subsetBM.pixmap(), left, top)); | |
| } | |
| } | |
| } | |
| CompareBitmaps(r, bm, tiledBM); | |
| } | |
| DEF_TEST(RustPngCodec_subset, r) { | |
| // Tests subsets by splitting the image into 8 or 10 different tiles and decoding | |
| // those each separately, then comparing to the full image decoded. | |
| test_subset_decode(r, "images/baby_tux.png"); | |
| test_subset_decode(r, "images/plane_interlaced.png"); | |
| test_subset_decode(r, "images/basi3p01.png"); | |
| } | |
| DEF_TEST(RustPngCodec_interlaced_animated_blending, r) { | |
| const char* kResource = "images/interlaced-multiframe-with-blending.png"; | |
| #if !defined(SK_CODEC_USES_PNG_WITH_RUST_FOR_ANDROID) | |
| std::unique_ptr<SkCodec> codec = SkPngRustDecoderDecode(r, kResource); | |
| REPORTER_ASSERT(r, codec); | |
| // Use incrementalDecode for each frame of this image. This should not crash. | |
| SkBitmap bm; | |
| SkImageInfo info = codec->getInfo(); | |
| bm.allocPixels(info); | |
| REPORTER_ASSERT(r, codec->getFrameCount() == 4); | |
| for (int i = 0; i < codec->getFrameCount(); ++i) { | |
| SkCodec::Options options; | |
| options.fFrameIndex = i; | |
| options.fPriorFrame = i - 1; | |
| SkCodec::Result result; | |
| result = codec->startIncrementalDecode(info, bm.getPixels(), bm.rowBytes(), &options); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result); | |
| std::ignore = codec->incrementalDecode(); | |
| } | |
| #else | |
| AssertAndroidStaticApng(r, kResource); | |
| #endif | |
| } | |
| DEF_TEST(RustPngCodec_sbit565_ihdr16bits, r) { | |
| std::unique_ptr<SkCodec> codec = SkPngRustDecoderDecode(r, "images/basn2c16-sbit565.png"); | |
| REPORTER_ASSERT(r, codec); | |
| SkBitmap bm; | |
| SkImageInfo info = codec->getInfo(); | |
| bm.allocPixels(info); | |
| REPORTER_ASSERT(r, codec->getFrameCount() == 1); | |
| SkCodec::Result result; | |
| result = codec->startIncrementalDecode(info, bm.getPixels(), bm.rowBytes()); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result); | |
| result = codec->incrementalDecode(); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result); | |
| } | |
| #ifdef SK_CODEC_USES_PNG_WITH_RUST_FOR_ANDROID | |
| class MockChunkReader : public SkPngChunkReader { | |
| public: | |
| bool readChunk(const char tag[], const void* data, size_t length) override { | |
| fChunks.push_back({tag, std::string((const char*)data, length)}); | |
| return true; | |
| } | |
| std::vector<std::pair<std::string, std::string>> fChunks; | |
| }; | |
| DEF_TEST(RustPngCodec_gainmapDecode, r) { | |
| auto stream = GetResourceAsStream("images/gainmap.png", false); | |
| REPORTER_ASSERT(r, stream); | |
| SkCodec::Result result = SkCodec::kSuccess; | |
| std::unique_ptr<SkCodec> baseCodec = SkPngRustDecoder::Decode(std::move(stream), &result); | |
| REPORTER_ASSERT(r, baseCodec); | |
| std::unique_ptr<SkAndroidCodec> androidCodec = | |
| SkAndroidCodec::MakeFromCodec(std::move(baseCodec)); | |
| REPORTER_ASSERT(r, androidCodec); | |
| SkGainmapInfo gainmapInfo; | |
| std::unique_ptr<SkAndroidCodec> gainmapCodec; | |
| bool hasGainmap = androidCodec->getGainmapAndroidCodec(&gainmapInfo, &gainmapCodec); | |
| REPORTER_ASSERT(r, hasGainmap); | |
| REPORTER_ASSERT(r, gainmapCodec); | |
| // Decode the gainmap bitmap. | |
| SkBitmap gainmapBitmap; | |
| gainmapBitmap.allocPixels(gainmapCodec->getInfo()); | |
| REPORTER_ASSERT(r, | |
| SkCodec::kSuccess == gainmapCodec->getAndroidPixels(gainmapBitmap.info(), | |
| gainmapBitmap.getPixels(), | |
| gainmapBitmap.rowBytes())); | |
| // Spot-check the image size and pixels (dimensions should be 32x32 for gainmap.png) | |
| REPORTER_ASSERT(r, gainmapBitmap.dimensions() == SkISize::Make(32, 32)); | |
| REPORTER_ASSERT(r, gainmapBitmap.getColor(0, 0) == 0xffffffff); | |
| REPORTER_ASSERT(r, gainmapBitmap.getColor(31, 31) == 0xff000000); | |
| // Verify some gainmap info values (matching recs in PngGainmapTest.cpp) | |
| REPORTER_ASSERT(r, gainmapInfo.fType == SkGainmapInfo::Type::kDefault); | |
| REPORTER_ASSERT(r, gainmapInfo.fBaseImageType == SkGainmapInfo::BaseImageType::kHDR); | |
| REPORTER_ASSERT(r, gainmapInfo.fDisplayRatioSdr == 2.f); | |
| REPORTER_ASSERT(r, gainmapInfo.fDisplayRatioHdr == 4.f); | |
| } | |
| DEF_TEST(RustPngCodec_gainmapRewind, r) { | |
| auto stream = GetResourceAsStream("images/gainmap.png", false); | |
| REPORTER_ASSERT(r, stream); | |
| MockChunkReader chunkReader; | |
| SkCodec::Result result = SkCodec::kSuccess; | |
| std::unique_ptr<SkCodec> baseCodec = | |
| SkPngRustDecoder::Decode(std::move(stream), &result, &chunkReader); | |
| REPORTER_ASSERT(r, baseCodec); | |
| REPORTER_ASSERT(r, chunkReader.fChunks.size() == 2); | |
| // Decode pixels the first time. | |
| SkImageInfo info = baseCodec->getInfo(); | |
| SkBitmap bm; | |
| bm.allocPixels(info); | |
| result = baseCodec->getPixels(info, bm.getPixels(), bm.rowBytes()); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result); | |
| // Should still be 2 (no rewind was needed for first decode). | |
| REPORTER_ASSERT(r, chunkReader.fChunks.size() == 2); | |
| // Decode pixels again (forces rewind). | |
| result = baseCodec->getPixels(info, bm.getPixels(), bm.rowBytes()); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result); | |
| // Should be 4 now because rewind recreated the reader and processed chunks again. | |
| REPORTER_ASSERT(r, chunkReader.fChunks.size() == 4); | |
| } | |
| DEF_TEST(RustPngCodec_ninepatchPngChunkReader, r) { | |
| auto stream = GetResourceAsStream("images/ninepatch.png", false); | |
| REPORTER_ASSERT(r, stream); | |
| MockChunkReader chunkReader; | |
| SkCodec::Result result; | |
| std::unique_ptr<SkCodec> codec( | |
| SkPngRustDecoder::Decode(std::move(stream), &result, &chunkReader)); | |
| REPORTER_ASSERT(r, codec); | |
| if (!codec) { | |
| return; | |
| } | |
| // Now compare to the original. | |
| SkBitmap decodedBm; | |
| decodedBm.setInfo(codec->getInfo()); | |
| decodedBm.allocPixels(); | |
| result = codec->getPixels(codec->getInfo(), decodedBm.getPixels(), decodedBm.rowBytes()); | |
| REPORTER_ASSERT(r, SkCodec::kSuccess == result); | |
| REPORTER_ASSERT(r, decodedBm.getColor(0, 0) == SK_ColorBLUE); | |
| REPORTER_ASSERT(r, chunkReader.fChunks.size() == 3); | |
| REPORTER_ASSERT(r, | |
| chunkReader.fChunks[0] == | |
| std::make_pair(std::string("npOl"), std::string("outline", 8))); | |
| REPORTER_ASSERT(r, | |
| chunkReader.fChunks[1] == | |
| std::make_pair(std::string("npLb"), std::string("layoutBounds", 13))); | |
| REPORTER_ASSERT(r, | |
| chunkReader.fChunks[2] == | |
| std::make_pair(std::string("npTc"), std::string("ninePatchData", 14))); | |
| } | |
| DEF_TEST(RustPngCodec_exactRead, r) { | |
| // Replicates the Codec_end test from CodecExactReadTest.cpp. | |
| // Verifies that with the limit reader enabled, we don't overshoot | |
| // and can decode subsequent images from the same stream. | |
| for (const char* path : { | |
| "images/plane.png", | |
| "images/yellow_rose.png", | |
| "images/plane_interlaced.png", | |
| }) { | |
| sk_sp<SkData> data = GetResourceAsData(path); | |
| if (!data) { | |
| continue; | |
| } | |
| const int kNumImages = 2; | |
| const size_t size = data->size(); | |
| sk_sp<SkData> multiData = SkData::MakeUninitialized(size * kNumImages); | |
| void* dst = multiData->writable_data(); | |
| for (int i = 0; i < kNumImages; i++) { | |
| memcpy(SkTAddOffset<void>(dst, size * i), data->data(), size); | |
| } | |
| data.reset(); | |
| SkMemoryStream stream(std::move(multiData)); | |
| for (int i = 0; i < kNumImages; ++i) { | |
| SkCodec::Result result; | |
| std::unique_ptr<SkCodec> codec = | |
| SkPngRustDecoder::Decode(std::make_unique<UnowningStream>(&stream), &result); | |
| if (!codec) { | |
| ERRORF(r, "Failed to create a codec from %s, iteration %i", path, i); | |
| continue; | |
| } | |
| auto info = codec->getInfo().makeColorType(kN32_SkColorType); | |
| SkBitmap bm; | |
| bm.allocPixels(info); | |
| result = codec->getPixels(bm.info(), bm.getPixels(), bm.rowBytes()); | |
| if (result != SkCodec::kSuccess) { | |
| ERRORF(r, "Failed to getPixels from %s, iteration %i error %i", path, i, result); | |
| continue; | |
| } | |
| // A full decode must drain the stream through `IEND`, leaving it exactly at the start | |
| // of the next image. | |
| REPORTER_ASSERT(r, | |
| stream.getPosition() == size * (i + 1), | |
| "%s: decode %i left the stream at %zu, expected %zu", | |
| path, | |
| i, | |
| stream.getPosition(), | |
| size * (i + 1)); | |
| } | |
| } | |
| } | |
| #else | |
| DEF_TEST(RustPngCodec_gainmapNoOp, r) { | |
| auto stream = GetResourceAsStream("images/gainmap.png", false); | |
| REPORTER_ASSERT(r, stream); | |
| SkCodec::Result result = SkCodec::kSuccess; | |
| std::unique_ptr<SkCodec> baseCodec = SkPngRustDecoder::Decode(std::move(stream), &result); | |
| REPORTER_ASSERT(r, baseCodec); | |
| std::unique_ptr<SkAndroidCodec> androidCodec = | |
| SkAndroidCodec::MakeFromCodec(std::move(baseCodec)); | |
| REPORTER_ASSERT(r, androidCodec); | |
| SkGainmapInfo gainmapInfo; | |
| std::unique_ptr<SkAndroidCodec> gainmapCodec; | |
| bool hasGainmap = androidCodec->getGainmapAndroidCodec(&gainmapInfo, &gainmapCodec); | |
| // When the build flag is off, it should return false (no gainmap). | |
| REPORTER_ASSERT(r, !hasGainmap); | |
| REPORTER_ASSERT(r, !gainmapCodec); | |
| } | |
| DEF_TEST(RustPngCodec_exactRead_overshoot, r) { | |
| // Replicates the Codec_end test from CodecExactReadTest.cpp. | |
| // Verifies that without the limit reader, the decoder overshoots | |
| // and fails to decode the second image because the stream is misaligned. | |
| const char* path = "images/plane.png"; | |
| sk_sp<SkData> data = GetResourceAsData(path); | |
| if (!data) { | |
| return; | |
| } | |
| const int kNumImages = 2; | |
| const size_t size = data->size(); | |
| sk_sp<SkData> multiData = SkData::MakeUninitialized(size * kNumImages); | |
| void* dst = multiData->writable_data(); | |
| for (int i = 0; i < kNumImages; i++) { | |
| memcpy(SkTAddOffset<void>(dst, size * i), data->data(), size); | |
| } | |
| data.reset(); | |
| SkMemoryStream stream(std::move(multiData)); | |
| for (int i = 0; i < kNumImages; ++i) { | |
| SkCodec::Result result; | |
| std::unique_ptr<SkCodec> codec = | |
| SkPngRustDecoder::Decode(std::make_unique<UnowningStream>(&stream), &result); | |
| if (i == 0) { | |
| if (!codec) { | |
| ERRORF(r, "Failed to create a codec from %s, iteration %i", path, i); | |
| return; | |
| } | |
| auto info = codec->getInfo().makeColorType(kN32_SkColorType); | |
| SkBitmap bm; | |
| bm.allocPixels(info); | |
| result = codec->getPixels(bm.info(), bm.getPixels(), bm.rowBytes()); | |
| if (result != SkCodec::kSuccess) { | |
| ERRORF(r, "Failed to getPixels from %s, iteration %i error %i", path, i, result); | |
| return; | |
| } | |
| } else { | |
| // We expect failure on the second iteration because the first decode overshot. | |
| REPORTER_ASSERT(r, !codec); | |
| } | |
| } | |
| } | |
| #endif | |
| DEF_TEST(RustPngCodec_subset_halting, r) { | |
| sk_sp<SkData> data = GetResourceAsData("images/mandrill_128.png"); | |
| if (!data) { | |
| ERRORF(r, "Missing resource: images/mandrill_128.png"); | |
| return; | |
| } | |
| // Mandrill is 128x128. We target a center 64x64 subset. | |
| SkIRect subset = SkIRect::MakeXYWH(32, 32, 64, 64); | |
| SkCodec::Options options; | |
| options.fSubset = ⊂ | |
| auto makeN32 = [](const SkImageInfo& info) { return info.makeColorType(kN32_SkColorType); }; | |
| std::optional<SkBitmap> bmOneShot = DecodeIncrementalOneShot(r, data, makeN32, options); | |
| if (!bmOneShot) { | |
| return; | |
| } | |
| const size_t initialLimit = data->size() / 2; | |
| auto haltingStream = std::make_unique<HaltingStream>(data, initialLimit); | |
| HaltingStream* retainedStream = haltingStream.get(); | |
| SkBitmap bmHalting; | |
| std::unique_ptr<SkCodec> codec = | |
| StartIncrementalDecode(r, std::move(haltingStream), &bmHalting, makeN32, options); | |
| if (!codec) { | |
| return; | |
| } | |
| int rowsDecoded = -1; | |
| REPORTER_ASSERT(r, codec->incrementalDecode(&rowsDecoded) == SkCodec::kIncompleteInput); | |
| REPORTER_ASSERT(r, rowsDecoded > 0 && rowsDecoded < subset.height()); | |
| REPORTER_ASSERT(r, CountWrittenRows(r, bmHalting) == rowsDecoded); | |
| retainedStream->addNewData(data->size() - initialLimit); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, codec->incrementalDecode()); | |
| CompareBitmaps(r, *bmOneShot, bmHalting); | |
| } | |
| DEF_TEST(RustPngCodec_subsampling, r) { | |
| for (int sampleSize : {2, 3, 5, 8, 100, 1000}) { | |
| AssertAndroidDecodeSampling(r, "images/plane.png", sampleSize); | |
| } | |
| } | |
| DEF_TEST(RustPngCodec_subsampling_interlaced, r) { | |
| for (int sampleSize : {2, 3, 5, 8, 100, 1000}) { | |
| AssertAndroidDecodeSampling(r, "images/plane_interlaced.png", sampleSize); | |
| } | |
| } | |
| DEF_TEST(RustPngCodec_subsampling_subset, r) { | |
| for (int sampleSize : {2, 3, 5, 8, 100, 1000}) { | |
| AssertAndroidDecodeSampling(r, "images/plane.png", sampleSize, [](const SkImageInfo& info) { | |
| return SkIRect::MakeXYWH(info.width() / 2, 0, info.width() / 2, info.height()); | |
| }); | |
| } | |
| } | |
| DEF_TEST(RustPngCodec_subsampling_subset_interlaced, r) { | |
| for (int sampleSize : {2, 3, 5, 8, 100, 1000}) { | |
| AssertAndroidDecodeSampling( | |
| r, "images/plane_interlaced.png", sampleSize, [](const SkImageInfo& info) { | |
| return SkIRect::MakeXYWH(0, 1, info.width(), info.height() - 1); | |
| }); | |
| } | |
| } | |
| // Regression test for a heap buffer overflow. | |
| // | |
| // In `apng-single-frame-with-offset.png`, `IDAT` is a full-canvas 64x64 default | |
| // image, followed by a single `fcTL`/`fdAT` animation frame of size 64x32 at | |
| // y-origin 32: | |
| // * On non-Android (`!SK_CODEC_USES_PNG_WITH_RUST_FOR_ANDROID`), `onIsAnimated` | |
| // reports `kNo` for a 1-frame APNG, while Frame 0 uses the `fcTL` sub-rect | |
| // `(0, 32, 64, 64)`. Since that origin is only valid in a full-canvas | |
| // destination, the codec must refuse sampled or subset decodes (`kUnimplemented`) | |
| // rather than writing outside the caller's buffer. | |
| // * On Android (`SK_CODEC_USES_PNG_WITH_RUST_FOR_ANDROID`), `acTL`/`fcTL` are | |
| // ignored for `libpng` parity, so the full-canvas 64x64 `IDAT` default image | |
| // is decoded and sampled/subset decodes succeed. | |
| DEF_TEST(RustPngCodec_apng_offset_frame_does_not_overflow_dst, r) { | |
| // 64x64 canvas. The single frame covers the bottom half: y-origin of 32. | |
| const char* path = "images/apng-single-frame-with-offset.png"; | |
| #if !defined(SK_CODEC_USES_PNG_WITH_RUST_FOR_ANDROID) | |
| sk_sp<SkData> data = GetResourceAsData(path); | |
| if (!data) { | |
| ERRORF(r, "Missing resource: %s", path); | |
| return; | |
| } | |
| struct Case { | |
| int fSampleSize; | |
| bool fUseSubset; | |
| // `0` means `info.minRowBytes()`. A larger value gives the destination | |
| // buffer row padding, which makes `rowBytes >= dstInfo.minRowBytes()` | |
| // even though the buffer holds fewer rows than the full canvas. | |
| size_t fRowBytes; | |
| }; | |
| static constexpr Case kCases[] = { | |
| // Sampling only. `sampleSize` of 2 halves both dimensions, while a | |
| // `sampleSize` of 64 makes the destination a single pixel. | |
| {2, false, 0}, | |
| {64, false, 0}, | |
| {2, false, 256}, | |
| // Subset only. | |
| {1, true, 0}, | |
| // Subset and sampling. | |
| {2, true, 0}, | |
| }; | |
| for (const Case& testCase : kCases) { | |
| AssertAndroidDecodeRefused( | |
| r, data, testCase.fSampleSize, testCase.fUseSubset, testCase.fRowBytes); | |
| } | |
| #else | |
| AssertAndroidStaticApng(r, path); | |
| #endif | |
| } | |
| // A caller that drives `SkCodec` directly (rather than through | |
| // `SkAndroidCodec`) can pass a `rowBytes` that is too small for a full-canvas | |
| // destination. `SkSampledCodec` does this and then narrows the destination | |
| // with a sampler, but a caller that never asks for a sampler must get a clean | |
| // error instead of a decode into an undersized buffer. | |
| DEF_TEST(RustPngCodec_apng_offset_frame_small_row_bytes, r) { | |
| const char* path = "images/apng-single-frame-with-offset.png"; | |
| sk_sp<SkData> data = GetResourceAsData(path); | |
| if (!data) { | |
| ERRORF(r, "Missing resource: %s", path); | |
| return; | |
| } | |
| auto codec = SkPngRustDecoder::Decode(std::make_unique<SkMemoryStream>(data), nullptr); | |
| REPORTER_ASSERT(r, codec); | |
| if (!codec) { | |
| return; | |
| } | |
| const SkImageInfo info = codec->getInfo().makeColorType(kN32_SkColorType); | |
| std::vector<uint8_t> buffer(info.computeMinByteSize(), 0x00); | |
| SkCodec::Result result = | |
| codec->startIncrementalDecode(info, buffer.data(), info.minRowBytes() / 2); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result); | |
| result = codec->incrementalDecode(); | |
| REPORTER_ASSERT(r, | |
| result == SkCodec::kInvalidParameters, | |
| "expected kInvalidParameters, got %s", | |
| SkCodec::ResultToString(result)); | |
| } | |
| // On non-Android (`!SK_CODEC_USES_PNG_WITH_RUST_FOR_ANDROID`), the refusal above | |
| // must not change a full-canvas decode: the `fdAT` frame still goes to its | |
| // y=32 origin. On Android (`SK_CODEC_USES_PNG_WITH_RUST_FOR_ANDROID`), the | |
| // 64x64 `IDAT` default image is decoded instead, matching `libpng`. | |
| DEF_TEST(RustPngCodec_apng_offset_frame_full_canvas_placement, r) { | |
| const char* path = "images/apng-single-frame-with-offset.png"; | |
| #if !defined(SK_CODEC_USES_PNG_WITH_RUST_FOR_ANDROID) | |
| sk_sp<SkData> data = GetResourceAsData(path); | |
| if (!data) { | |
| ERRORF(r, "Missing resource: %s", path); | |
| return; | |
| } | |
| auto codec = SkPngRustDecoder::Decode(std::make_unique<SkMemoryStream>(data), nullptr); | |
| if (!codec) { | |
| ERRORF(r, "Failed to create a codec for %s", path); | |
| return; | |
| } | |
| for (SkAlphaType alphaType : {kUnpremul_SkAlphaType, kPremul_SkAlphaType}) { | |
| SkBitmap bm; | |
| bm.allocPixels(codec->getInfo().makeColorType(kN32_SkColorType).makeAlphaType(alphaType)); | |
| // The APNG spec says the output buffer starts as transparent black. | |
| bm.eraseColor(SK_ColorTRANSPARENT); | |
| SkCodec::Result result = codec->getPixels(bm.pixmap()); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result); | |
| // Rows above the frame stay transparent. The frame fills rows 32..63. | |
| REPORTER_ASSERT(r, bm.getColor(0, 0) == SK_ColorTRANSPARENT); | |
| REPORTER_ASSERT(r, bm.getColor(63, 31) == SK_ColorTRANSPARENT); | |
| REPORTER_ASSERT(r, bm.getColor(0, 32) == SK_ColorMAGENTA); | |
| REPORTER_ASSERT(r, bm.getColor(63, 63) == SK_ColorMAGENTA); | |
| } | |
| #else | |
| AssertAndroidStaticApng(r, path); | |
| #endif | |
| } | |
| #ifdef SK_CODEC_USES_PNG_WITH_RUST_FOR_ANDROID | |
| // Decodes a top subset of `path` and asserts that the decode stopped short of the end of the | |
| // stream, i.e. that `finish_decoding()` was not called. libpng stops early the same way: its row | |
| // callbacks longjmp out once the requested rows have been written, and only a whole-image decode | |
| // reads through `IEND` (`SkPngCodec.cpp`). | |
| // A non-interlaced decode stops inside `IDAT`. An interlaced one has to read every Adam7 pass, and | |
| // the `png` crate consumes `IEND`'s 8-byte length+type header while detecting the end of the | |
| // frame, so it stops with only the 4-byte CRC left. | |
| // Only `for_android` builds limit reads to chunk boundaries; elsewhere the `BufReader` inside the | |
| // `png` crate may have buffered the rest of the file, so the position says nothing. | |
| static void AssertPartialDecodeStopsBeforeIend( | |
| skiatest::Reporter* r, | |
| const char* path, | |
| bool interlaced, | |
| int sampleSize, | |
| std::function<SkIRect(const SkImageInfo&)> getSubset = nullptr) { | |
| sk_sp<SkData> data = GetResourceAsData(path); | |
| if (!data) { | |
| ERRORF(r, "Missing resource: %s", path); | |
| return; | |
| } | |
| SkMemoryStream memStream(data); | |
| std::optional<SkBitmap> bm = DecodeAndroidPixels( | |
| r, | |
| SkPngRustDecoder::Decode(std::make_unique<UnowningStream>(&memStream), nullptr), | |
| sampleSize, | |
| std::move(getSubset)); | |
| REPORTER_ASSERT(r, bm.has_value()); | |
| // A non-interlaced subset/sampled decode never reads into `IEND` (at most the end of `IDAT`, | |
| // i.e. `<= data->size() - kIendChunkSize`). An interlaced one reads the 8-byte `IEND` header | |
| // while detecting end-of-frame, but stops before the 4-byte `IEND` CRC (`< data->size()`). | |
| const size_t limit = interlaced ? data->size() : data->size() - kIendChunkSize + 1; | |
| REPORTER_ASSERT(r, | |
| memStream.getPosition() < limit, | |
| "%s: decode read too far, pos=%zu (expected < %zu, file is %zu, IEND at %zu)", | |
| path, | |
| memStream.getPosition(), | |
| limit, | |
| data->size(), | |
| data->size() - kIendChunkSize); | |
| } | |
| // Regression tests for b/562939096: | |
| // A subset or subsampled decode must stop after the last row it needs, rather than reading and | |
| // CRC-checking the rest of the PNG stream through `IEND` via `finish_decoding()`. | |
| DEF_TEST(RustPngCodec_subsetDoesNotReadToIend, r) { | |
| AssertPartialDecodeStopsBeforeIend( | |
| r, | |
| "images/mandrill_128.png", | |
| /*interlaced=*/false, | |
| /*sampleSize=*/1, | |
| [](const SkImageInfo& info) { return SkIRect::MakeWH(info.width(), 16); }); | |
| } | |
| DEF_TEST(RustPngCodec_subsetDoesNotReadToIend_interlaced, r) { | |
| AssertPartialDecodeStopsBeforeIend( | |
| r, | |
| "images/plane_interlaced.png", | |
| /*interlaced=*/true, | |
| /*sampleSize=*/1, | |
| [](const SkImageInfo& info) { return SkIRect::MakeWH(info.width(), 16); }); | |
| } | |
| DEF_TEST(RustPngCodec_subsamplingDoesNotReadToIend, r) { | |
| AssertPartialDecodeStopsBeforeIend( | |
| r, "images/mandrill_128.png", /*interlaced=*/false, /*sampleSize=*/2); | |
| } | |
| DEF_TEST(RustPngCodec_subsamplingDoesNotReadToIend_interlaced, r) { | |
| AssertPartialDecodeStopsBeforeIend( | |
| r, "images/plane_interlaced.png", /*interlaced=*/true, /*sampleSize=*/2); | |
| } | |
| #endif | |
| // A top subset must decode even if everything after the rows it needs is missing. | |
| DEF_TEST(RustPngCodec_subsetOfTruncatedFile, r) { | |
| static constexpr char kPath[] = "images/mandrill_128.png"; | |
| sk_sp<SkData> data = GetResourceAsData(kPath); | |
| if (!data) { | |
| ERRORF(r, "Missing resource: %s", kPath); | |
| return; | |
| } | |
| auto topSubset = [](const SkImageInfo& info) { return SkIRect::MakeWH(info.width(), 16); }; | |
| std::optional<SkBitmap> refBm = DecodeAndroidPixels( | |
| r, | |
| SkPngRustDecoder::Decode(std::make_unique<SkMemoryStream>(data), nullptr), | |
| /*sampleSize=*/1, | |
| topSubset); | |
| REPORTER_ASSERT(r, refBm.has_value()); | |
| sk_sp<SkData> truncatedHalf = SkData::MakeSubset(data.get(), 0, data->size() / 2); | |
| std::optional<SkBitmap> bm = DecodeAndroidPixels( | |
| r, | |
| SkPngRustDecoder::Decode(std::make_unique<SkMemoryStream>(truncatedHalf), nullptr), | |
| /*sampleSize=*/1, | |
| topSubset); | |
| REPORTER_ASSERT(r, bm.has_value()); | |
| if (refBm && bm) { | |
| CompareBitmaps(r, *refBm, *bm); | |
| } | |
| } | |
| // Regression tests for b/562802947: | |
| // When all scanlines and the IDAT chunk have been decoded, a corrupt or truncated trailing `IEND` | |
| // CRC in `finish_decoding()` must not cause the decode to fail or zero-fill the output bitmap | |
| // (matching `SkPngCodec` / libpng `png_read_end` behavior and Chrome's `SkPngRustCodec`). | |
| DEF_TEST(RustPngCodec_missingOrCorruptIendSucceeds, r) { | |
| AssertDecodesWithBrokenIendTail(r, "images/mandrill_128.png"); | |
| } | |
| DEF_TEST(RustPngCodec_missingOrCorruptIendSucceeds_interlaced, r) { | |
| AssertDecodesWithBrokenIendTail(r, "images/plane_interlaced.png"); | |
| } | |
| // `images/color_wheel_with_profile.png` is RGBA8 with an `iCCP` chunk, so this covers the | |
| // `read_row` decode path (see `AssertDecodesWithBrokenIendTail`). | |
| DEF_TEST(RustPngCodec_missingOrCorruptIendSucceeds_readRow, r) { | |
| AssertDecodesWithBrokenIendTail(r, "images/color_wheel_with_profile.png"); | |
| } | |
| DEF_TEST(RustPngCodec_missingOrCorruptIendSucceeds_sampled, r) { | |
| AssertSampledDecodeWithBrokenIendTail(r, "images/mandrill_128.png", /*sampleSize=*/2); | |
| } | |
| // Regression tests for b/565484678 on `images/plane_interlaced.png` (250x126, 8-bit RGB with | |
| // `tRNS`, expanded to RGBA8 -> 4 bytes per encoded pixel = 1000 bytes/row). | |
| // | |
| // First two tests verify that `fPreblendBuffer` in `SkPngRustCodec` is charged against | |
| // `fMaxDecodeMemory` (`allocateFromBudget`) for interlaced sampled/subset decodes, which buffer | |
| // all 126 rows in `fPreblendBuffer`. | |
| DEF_TEST(RustPngCodec_interlacedPreblendBufferBudget_sampled, r) { | |
| constexpr size_t kEncodedRowBytes = 250 * 4; | |
| constexpr size_t kFullBufferBytes = 126 * kEncodedRowBytes; | |
| AssertDecodeBudget(r, | |
| "images/plane_interlaced.png", | |
| kFullBufferBytes, | |
| [&](std::unique_ptr<SkCodec> codec, | |
| size_t maxDecodeMemory, | |
| SkCodec::Result expectedResult) { | |
| return DecodeAndroidPixels(r, | |
| std::move(codec), | |
| /*sampleSize=*/2, | |
| /*getSubset=*/nullptr, | |
| maxDecodeMemory, | |
| expectedResult); | |
| }); | |
| } | |
| DEF_TEST(RustPngCodec_interlacedPreblendBufferBudget_subset, r) { | |
| constexpr size_t kEncodedRowBytes = 250 * 4; | |
| constexpr size_t kFullBufferBytes = 126 * kEncodedRowBytes; | |
| AssertDecodeBudget(r, | |
| "images/plane_interlaced.png", | |
| kFullBufferBytes, | |
| [&](std::unique_ptr<SkCodec> codec, | |
| size_t maxDecodeMemory, | |
| SkCodec::Result expectedResult) { | |
| return DecodeAndroidPixels( | |
| r, | |
| std::move(codec), | |
| /*sampleSize=*/1, | |
| [](const SkImageInfo& info) { | |
| return SkIRect::MakeXYWH( | |
| 0, 1, info.width(), info.height() - 1); | |
| }, | |
| maxDecodeMemory, | |
| expectedResult); | |
| }); | |
| } | |
| // Next tests verify that the single-row interlaced scratch buffers | |
| // (`fDecodedInterlacedFullWidthRow`, `fXformedInterlacedRow`, and `SkPngCodecBase::fStorage`) are | |
| // charged against `fMaxDecodeMemory` even when `fPreblendBuffer` is empty (full-image decode of a | |
| // non-blended interlaced PNG). | |
| // | |
| // Note: `plane_interlaced.png` has no embedded ICC profile (`getEncodedInfo().profile() == | |
| // nullptr`), so `makeColorSpace(nullptr)` is required for `canReadRow()` to match when | |
| // `alphaType == kUnpremul_SkAlphaType`. | |
| DEF_TEST(RustPngCodec_interlacedRowBufferBudget_readRow, r) { | |
| // `kUnpremul_SkAlphaType` + `kRGBA_8888_SkColorType` + `nullptr` colorSpace takes the | |
| // `canReadRow()` path, which only allocates `fDecodedInterlacedFullWidthRow` | |
| // (250 * 4 = 1000 bytes). | |
| constexpr size_t kEncodedRowBytes = 250 * 4; | |
| AssertDecodeBudget(r, | |
| "images/plane_interlaced.png", | |
| kEncodedRowBytes, | |
| [&](std::unique_ptr<SkCodec> codec, | |
| size_t maxDecodeMemory, | |
| SkCodec::Result expectedResult) { | |
| return DecodePixels( | |
| r, | |
| std::move(codec), | |
| [](const SkImageInfo& info) { | |
| return info.makeColorType(kRGBA_8888_SkColorType) | |
| .makeAlphaType(kUnpremul_SkAlphaType) | |
| .makeColorSpace(nullptr); | |
| }, | |
| MakeBudgetOptions(maxDecodeMemory), | |
| expectedResult); | |
| }); | |
| } | |
| DEF_TEST(RustPngCodec_interlacedRowBufferBudget_xform, r) { | |
| // `kPremul_SkAlphaType` + `nullptr` colorSpace takes the `!canReadRow()` | |
| // (`incrementalDecodeXForm`) path with `kSwizzleOnly_XformMode`, which allocates both | |
| // `fDecodedInterlacedFullWidthRow` (1000 bytes) and `fXformedInterlacedRow` (1000 bytes). | |
| constexpr size_t kEncodedRowBytes = 250 * 4; | |
| constexpr size_t kDstRowBytes = 250 * 4; | |
| AssertDecodeBudget(r, | |
| "images/plane_interlaced.png", | |
| kEncodedRowBytes + kDstRowBytes, | |
| [&](std::unique_ptr<SkCodec> codec, | |
| size_t maxDecodeMemory, | |
| SkCodec::Result expectedResult) { | |
| return DecodePixels( | |
| r, | |
| std::move(codec), | |
| [](const SkImageInfo& info) { | |
| return info.makeColorType(kRGBA_8888_SkColorType) | |
| .makeAlphaType(kPremul_SkAlphaType) | |
| .makeColorSpace(nullptr); | |
| }, | |
| MakeBudgetOptions(maxDecodeMemory), | |
| expectedResult); | |
| }); | |
| } | |
| // With `kYes_ZeroInitialized`, `fSwizzler` skips writing leading transparent pixels, so | |
| // `fXformedInterlacedRow` must be re-zeroed before each row, or pixels from the previous row | |
| // leak into them. `plane_interlaced.png` has `tRNS` transparency, and `kPremul_SkAlphaType` | |
| // takes the `incrementalDecodeXForm` path. | |
| DEF_TEST(RustPngCodec_interlacedXformZeroInitialized, r) { | |
| auto decode = [&](SkCodec::ZeroInitialized zeroInit) { | |
| SkCodec::Options options; | |
| options.fZeroInitialized = zeroInit; | |
| return DecodePixels( | |
| r, | |
| SkPngRustDecoderDecode(r, "images/plane_interlaced.png"), | |
| [](const SkImageInfo& info) { | |
| return info.makeColorType(kRGBA_8888_SkColorType) | |
| .makeAlphaType(kPremul_SkAlphaType) | |
| .makeColorSpace(nullptr); | |
| }, | |
| options); | |
| }; | |
| std::optional<SkBitmap> zeroInitBm = decode(SkCodec::kYes_ZeroInitialized); | |
| std::optional<SkBitmap> noZeroInitBm = decode(SkCodec::kNo_ZeroInitialized); | |
| if (zeroInitBm && noZeroInitBm) { | |
| CompareBitmaps(r, *noZeroInitBm, *zeroInitBm); | |
| } | |
| } | |
| DEF_TEST(RustPngCodec_interlacedRowBufferBudget_colorXform, r) { | |
| // Decoding into a non-sRGB color space (`SkColorSpace::MakeSRGBLinear()`) activates | |
| // `SkPngCodecBase::allocateStorage` (`fStorage`, 1000 bytes) in addition to | |
| // `fDecodedInterlacedFullWidthRow` (1000 bytes) and `fXformedInterlacedRow` (1000 bytes). | |
| constexpr size_t kEncodedRowBytes = 250 * 4; | |
| constexpr size_t kDstRowBytes = 250 * 4; | |
| constexpr size_t kColorXformBytes = 250 * 4; | |
| AssertDecodeBudget(r, | |
| "images/plane_interlaced.png", | |
| kEncodedRowBytes + kDstRowBytes + kColorXformBytes, | |
| [&](std::unique_ptr<SkCodec> codec, | |
| size_t maxDecodeMemory, | |
| SkCodec::Result expectedResult) { | |
| return DecodePixels( | |
| r, | |
| std::move(codec), | |
| [](const SkImageInfo& info) { | |
| return info.makeColorType(kRGBA_8888_SkColorType) | |
| .makeAlphaType(kPremul_SkAlphaType) | |
| .makeColorSpace(SkColorSpace::MakeSRGBLinear()); | |
| }, | |
| MakeBudgetOptions(maxDecodeMemory), | |
| expectedResult); | |
| }); | |
| } | |
| // Verifies that the top `rowsDecoded` rows of `actualBm` match `expectedBm`, and that any | |
| // remaining rows (`rowsDecoded .. height - 1`) were zero-filled by `SkCodec::fillIncompleteImage`. | |
| static void AssertPartialBitmapMatches(skiatest::Reporter* r, | |
| const SkBitmap& actualBm, | |
| const SkBitmap& expectedBm, | |
| int rowsDecoded) { | |
| const SkIRect decodedRect = SkIRect::MakeWH(actualBm.width(), rowsDecoded); | |
| SkBitmap actualDecodedRows, expectedDecodedRows; | |
| REPORTER_ASSERT(r, actualBm.extractSubset(&actualDecodedRows, decodedRect)); | |
| REPORTER_ASSERT(r, expectedBm.extractSubset(&expectedDecodedRows, decodedRect)); | |
| CompareBitmaps(r, actualDecodedRows, expectedDecodedRows); | |
| if (rowsDecoded < actualBm.height()) { | |
| SkBitmap remainingRows; | |
| REPORTER_ASSERT( | |
| r, | |
| actualBm.extractSubset( | |
| &remainingRows, | |
| SkIRect::MakeLTRB(0, rowsDecoded, actualBm.width(), actualBm.height()))); | |
| REPORTER_ASSERT(r, CountWrittenRows(r, remainingRows, SK_ColorTRANSPARENT) == 0); | |
| } | |
| } | |
| // Regression test helper for b/562804783: like `SkPngCodec`, `SkPngRustCodec` must report | |
| // `rowsDecoded` for `kErrorInInput` (not just `kIncompleteInput`), so that callers such as | |
| // `SkCodec::getPixels` keep the rows decoded before the error instead of filling the whole image. | |
| static void AssertRowsDecodedReportedOnErrorInInput(skiatest::Reporter* r, | |
| SkColorType colorType, | |
| SkAlphaType alphaType, | |
| sk_sp<SkColorSpace> colorSpace) { | |
| // Non-interlaced RGBA8 image whose image data is split across 12 `IDAT` chunks. | |
| static constexpr char kPath[] = "images/text.png"; | |
| sk_sp<SkData> data = GetResourceAsData(kPath); | |
| if (!data) { | |
| ERRORF(r, "Missing resource: %s", kPath); | |
| return; | |
| } | |
| sk_sp<SkData> corruptData = WithCorruptIdatCrc(data.get(), /*idatIndex=*/5); | |
| if (!corruptData) { | |
| ERRORF(r, "Failed to corrupt IDAT CRC in %s", kPath); | |
| return; | |
| } | |
| auto makeDstInfo = [&](const SkImageInfo& info) { | |
| return info.makeColorType(colorType).makeAlphaType(alphaType).makeColorSpace(colorSpace); | |
| }; | |
| std::optional<SkBitmap> refBitmap = DecodePixels( | |
| r, SkPngRustDecoder::Decode(SkMemoryStream::Make(data), nullptr), makeDstInfo); | |
| if (!refBitmap) { | |
| return; | |
| } | |
| SkBitmap bitmap; | |
| std::unique_ptr<SkCodec> codec = | |
| StartIncrementalDecode(r, SkMemoryStream::Make(corruptData), &bitmap, makeDstInfo); | |
| if (!codec) { | |
| return; | |
| } | |
| const SkImageInfo& info = bitmap.info(); | |
| int rowsDecoded = -1; | |
| SkCodec::Result result = codec->incrementalDecode(&rowsDecoded); | |
| REPORTER_ASSERT(r, | |
| result == SkCodec::kErrorInInput, | |
| "actualResult=\"%s\" != kErrorInInput", | |
| SkCodec::ResultToString(result)); | |
| REPORTER_ASSERT(r, | |
| rowsDecoded > 0 && rowsDecoded < info.height(), | |
| "rowsDecoded=%d, height=%d", | |
| rowsDecoded, | |
| info.height()); | |
| REPORTER_ASSERT(r, CountWrittenRows(r, bitmap) == rowsDecoded); | |
| if (rowsDecoded <= 0 || rowsDecoded >= info.height()) { | |
| return; | |
| } | |
| // `getPixels` keeps the decoded rows, and only fills the rows after them. | |
| if (std::optional<SkBitmap> getPixelsBm = | |
| DecodePixels(r, | |
| SkPngRustDecoder::Decode(SkMemoryStream::Make(corruptData), nullptr), | |
| makeDstInfo, | |
| /*options=*/{}, | |
| SkCodec::kErrorInInput)) { | |
| AssertPartialBitmapMatches(r, *getPixelsBm, *refBitmap, rowsDecoded); | |
| } | |
| } | |
| // kRGBA_8888 + kUnpremul without a color space on an RGBA8 source without `iCCP` satisfies | |
| // `canReadRow()`, i.e. goes through `incrementalDecode` (`read_row`). | |
| DEF_TEST(RustPngCodec_rowsDecodedReportedOnErrorInInput_readRow, r) { | |
| AssertRowsDecodedReportedOnErrorInInput( | |
| r, kRGBA_8888_SkColorType, kUnpremul_SkAlphaType, /*colorSpace=*/nullptr); | |
| } | |
| DEF_TEST(RustPngCodec_rowsDecodedReportedOnErrorInInput_xform, r) { | |
| AssertRowsDecodedReportedOnErrorInInput( | |
| r, kN32_SkColorType, kPremul_SkAlphaType, SkColorSpace::MakeSRGB()); | |
| } | |
| static int AssertInterlacedIncrementalDecodeHalting( | |
| skiatest::Reporter* r, | |
| SkColorType colorType, | |
| SkAlphaType alphaType, | |
| std::optional<int> expectedRowsAtHalt = std::nullopt, | |
| std::optional<size_t> initialLimit = std::nullopt, | |
| const SkIRect* subset = nullptr, | |
| SkCodec::ZeroInitialized zeroInit = SkCodec::kNo_ZeroInitialized) { | |
| static constexpr char kPath[] = "images/plane_interlaced.png"; | |
| sk_sp<SkData> data = GetResourceAsData(kPath); | |
| if (!data) { | |
| ERRORF(r, "Missing resource: %s", kPath); | |
| return 0; | |
| } | |
| const size_t limit = initialLimit.value_or(data->size() / 2); | |
| // `plane_interlaced.png` has no embedded ICC profile, so `makeColorSpace(nullptr)` is required | |
| // for `kRGBA_8888_SkColorType` + `kUnpremul_SkAlphaType` to satisfy `canReadRow()`. | |
| auto makeDstInfo = [colorType, alphaType](const SkImageInfo& info) { | |
| return info.makeColorType(colorType).makeAlphaType(alphaType).makeColorSpace(nullptr); | |
| }; | |
| SkCodec::Options options; | |
| options.fSubset = subset; | |
| options.fZeroInitialized = zeroInit; | |
| auto stream = std::make_unique<HaltingStream>(data, limit); | |
| HaltingStream* retainedStream = stream.get(); | |
| SkBitmap bm; | |
| std::unique_ptr<SkCodec> codec = | |
| StartIncrementalDecode(r, std::move(stream), &bm, makeDstInfo, options); | |
| if (!codec) { | |
| return 0; | |
| } | |
| int rowsDecoded = -1; | |
| SkCodec::Result result = codec->incrementalDecode(&rowsDecoded); | |
| REPORTER_ASSERT(r, result == SkCodec::kIncompleteInput); | |
| if (zeroInit == SkCodec::kNo_ZeroInitialized) { | |
| const int writtenRows = CountWrittenRows(r, bm); | |
| REPORTER_ASSERT(r, | |
| rowsDecoded == writtenRows, | |
| "API rowsDecoded (%d) != written rows (%d)", | |
| rowsDecoded, | |
| writtenRows); | |
| } | |
| if (expectedRowsAtHalt.has_value()) { | |
| REPORTER_ASSERT(r, | |
| rowsDecoded == *expectedRowsAtHalt, | |
| "rowsDecoded (%d) != expectedRowsAtHalt (%d)", | |
| rowsDecoded, | |
| *expectedRowsAtHalt); | |
| } else { | |
| REPORTER_ASSERT(r, | |
| rowsDecoded > 0 && rowsDecoded < bm.height() && rowsDecoded % 8 == 0, | |
| "Unexpected rowsDecoded (%d) for height %d", | |
| rowsDecoded, | |
| bm.height()); | |
| } | |
| // For full-image decodes, also verify `SkCodec::getPixels` (`onGetPixels`) on the truncated | |
| // prefix: the top `rowsDecoded` rows must match `bm` and any remaining rows must be | |
| // zero-filled by `SkCodec::fillIncompleteImage`. | |
| if (!subset && zeroInit == SkCodec::kNo_ZeroInitialized && rowsDecoded > 0) { | |
| sk_sp<SkData> truncated = SkData::MakeSubset(data.get(), 0, limit); | |
| if (std::optional<SkBitmap> getPixelsBm = DecodePixels( | |
| r, | |
| SkPngRustDecoder::Decode(std::make_unique<SkMemoryStream>(std::move(truncated)), | |
| nullptr), | |
| makeDstInfo, | |
| options, | |
| SkCodec::kIncompleteInput)) { | |
| AssertPartialBitmapMatches(r, *getPixelsBm, bm, rowsDecoded); | |
| } | |
| } | |
| retainedStream->addNewData(data->size() - limit); | |
| result = codec->incrementalDecode(); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result); | |
| if (std::optional<SkBitmap> refBm = DecodeIncrementalOneShot(r, data, makeDstInfo, options)) { | |
| CompareBitmaps(r, *refBm, bm); | |
| } | |
| return rowsDecoded; | |
| } | |
| static void AssertInterlacedAndroidDecodeHalting( | |
| skiatest::Reporter* r, | |
| int sampleSize, | |
| std::function<SkIRect(const SkImageInfo&)> getSubset = nullptr) { | |
| static constexpr char kPath[] = "images/plane_interlaced.png"; | |
| sk_sp<SkData> data = GetResourceAsData(kPath); | |
| if (!data) { | |
| ERRORF(r, "Missing resource: %s", kPath); | |
| return; | |
| } | |
| const struct { | |
| size_t fLimit; | |
| bool fExpectPartialHeight; | |
| } kLimits[] = { | |
| {300, true}, | |
| {data->size() / 2, false}, | |
| }; | |
| for (const auto& [limit, expectPartialHeight] : kLimits) { | |
| std::optional<SkBitmap> rustBm = DecodeAndroidPixels( | |
| r, | |
| SkPngRustDecoder::Decode(std::make_unique<HaltingStream>(data, limit), nullptr), | |
| sampleSize, | |
| getSubset, | |
| /*maxDecodeMemory=*/0, | |
| SkCodec::kIncompleteInput); | |
| if (!rustBm) { | |
| continue; | |
| } | |
| // Every pixel must be overwritten (rows `0 .. decodedRows - 1` by the codec and the | |
| // remaining rows by `SkCodec::fillIncompleteImage` with `SK_ColorTRANSPARENT`). Counting | |
| // up to the last non-`SK_ColorTRANSPARENT` row measures `decodedRows` because, for | |
| // `plane_interlaced.png` (full image and the `(10, 10, 100, 50)` subset), the last decoded | |
| // row at both halting points contains non-transparent airplane pixels in the sampled | |
| // columns (earlier rows may be fully transparent, which is fine). | |
| REPORTER_ASSERT(r, CountWrittenRows(r, *rustBm) == rustBm->height()); | |
| const int decodedRows = CountNonSentinelRows(*rustBm, SK_ColorTRANSPARENT); | |
| if (expectPartialHeight) { | |
| REPORTER_ASSERT(r, decodedRows > 0 && decodedRows < rustBm->height()); | |
| } else { | |
| REPORTER_ASSERT(r, decodedRows == rustBm->height()); | |
| } | |
| #if defined(SK_CODEC_DECODES_PNG_WITH_LIBPNG) | |
| std::optional<SkBitmap> libpngBm = DecodeAndroidPixels( | |
| r, | |
| SkPngDecoder::Decode(std::make_unique<HaltingStream>(data, limit), nullptr), | |
| sampleSize, | |
| getSubset, | |
| /*maxDecodeMemory=*/0, | |
| SkCodec::kIncompleteInput); | |
| if (libpngBm) { | |
| REPORTER_ASSERT(r, CountWrittenRows(r, *libpngBm) == libpngBm->height()); | |
| const int libpngDecodedRows = CountNonSentinelRows(*libpngBm, SK_ColorTRANSPARENT); | |
| REPORTER_ASSERT(r, | |
| decodedRows == libpngDecodedRows, | |
| "decodedRows mismatch at halt: Rust %d != Libpng %d", | |
| decodedRows, | |
| libpngDecodedRows); | |
| } | |
| #endif | |
| } | |
| } | |
| static constexpr int kPlaneInterlacedHeight = 126; | |
| // Tests progressive (halting + resume) decoding of an interlaced PNG across both decode paths: | |
| // * `kRGBA_8888_SkColorType` + `kUnpremul_SkAlphaType` (`canReadRow()` -> `incrementalDecode`) | |
| // * `kN32_SkColorType` + `kPremul_SkAlphaType` (`!canReadRow()` -> `incrementalDecodeXForm`) | |
| DEF_TEST(RustPngCodec_interlacedHalting, r) { | |
| for (const auto& [colorType, alphaType] : { | |
| std::make_pair(kRGBA_8888_SkColorType, kUnpremul_SkAlphaType), | |
| std::make_pair(kN32_SkColorType, kPremul_SkAlphaType), | |
| }) { | |
| // At 300 bytes, Pass 1 has partially completed (`0 < rowsDecoded < 126`, multiple of 8). | |
| AssertInterlacedIncrementalDecodeHalting( | |
| r, colorType, alphaType, /*expectedRowsAtHalt=*/std::nullopt, /*initialLimit=*/300); | |
| // At half the file (`initialLimit = std::nullopt`), Pass 1's 16 scanlines have completed | |
| // and splatted all 126 rows (clamped from 16 * 8 = 128), so every pixel in the destination | |
| // has been approximated by the closest already-decoded Adam7 sample. | |
| AssertInterlacedIncrementalDecodeHalting( | |
| r, colorType, alphaType, /*expectedRowsAtHalt=*/kPlaneInterlacedHeight); | |
| } | |
| } | |
| DEF_TEST(RustPngCodec_interlacedSubsetHalting, r) { | |
| const int fullImageRowsAtHalt = | |
| AssertInterlacedIncrementalDecodeHalting(r, | |
| kN32_SkColorType, | |
| kPremul_SkAlphaType, | |
| /*expectedRowsAtHalt=*/std::nullopt, | |
| /*initialLimit=*/300); | |
| if (fullImageRowsAtHalt <= 0) { | |
| return; | |
| } | |
| const SkIRect kSubset = SkIRect::MakeXYWH(10, 10, 100, 50); | |
| const int expectedSubsetRowsAtHalt = | |
| std::clamp(fullImageRowsAtHalt - kSubset.top(), 0, kSubset.height()); | |
| for (SkCodec::ZeroInitialized zeroInit : | |
| {SkCodec::kNo_ZeroInitialized, SkCodec::kYes_ZeroInitialized}) { | |
| AssertInterlacedIncrementalDecodeHalting(r, | |
| kN32_SkColorType, | |
| kPremul_SkAlphaType, | |
| expectedSubsetRowsAtHalt, | |
| /*initialLimit=*/300, | |
| &kSubset, | |
| zeroInit); | |
| // At half the file, all 50 subset rows are flushed. | |
| AssertInterlacedIncrementalDecodeHalting(r, | |
| kN32_SkColorType, | |
| kPremul_SkAlphaType, | |
| /*expectedRowsAtHalt=*/kSubset.height(), | |
| /*initialLimit=*/std::nullopt, | |
| &kSubset, | |
| zeroInit); | |
| } | |
| // A subset starting below the decoded rows at halt reports 0 rows decoded. | |
| const SkIRect kLowerSubset = | |
| SkIRect::MakeLTRB(10, fullImageRowsAtHalt, 110, kPlaneInterlacedHeight); | |
| AssertInterlacedIncrementalDecodeHalting(r, | |
| kN32_SkColorType, | |
| kPremul_SkAlphaType, | |
| /*expectedRowsAtHalt=*/0, | |
| /*initialLimit=*/300, | |
| &kLowerSubset); | |
| } | |
| DEF_TEST(RustPngCodec_interlacedSamplingHalting, r) { | |
| for (int sampleSize : {2, 3, 8}) { | |
| AssertInterlacedAndroidDecodeHalting(r, sampleSize); | |
| } | |
| } | |
| DEF_TEST(RustPngCodec_interlacedSamplingSubsetHalting, r) { | |
| for (int sampleSize : {1, 2, 3, 8}) { | |
| AssertInterlacedAndroidDecodeHalting(r, sampleSize, [](const SkImageInfo&) { | |
| return SkIRect::MakeXYWH(10, 10, 100, 50); | |
| }); | |
| } | |
| } |