| /* | |
| * 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 "src/codec/SkPngRustCodec.h" | |
| #include <cstring> | |
| #include <limits> | |
| #include <memory> | |
| #include <tuple> | |
| #include <utility> | |
| #include "include/core/SkColorSpace.h" | |
| #include "include/core/SkStream.h" | |
| #include "include/private/SkAssert.h" | |
| #include "include/private/SkEncodedInfo.h" | |
| #include "include/private/SkHdrMetadata.h" | |
| #include "include/private/SkSafe32.h" | |
| #include "include/private/SkTemplates.h" | |
| #include "modules/skcms/skcms.h" | |
| #include "rust/common/SpanUtils.h" | |
| #include "rust/png/FFI.rs.h" | |
| #include "src/codec/SkCodecPriv.h" | |
| #include "src/codec/SkFrameHolder.h" | |
| #include "src/codec/SkParseEncodedOrigin.h" | |
| #include "src/codec/SkPngCompositeChunkReader.h" | |
| #include "src/codec/SkPngPriv.h" | |
| #include "src/codec/SkSwizzler.h" | |
| #include "src/core/SkRasterPipeline.h" | |
| #include "src/core/SkRasterPipelineOpList.h" | |
| #include "src/core/SkSafeMath.h" | |
| #include "third_party/rust/cxx/v1/cxx.h" | |
| #ifdef __clang__ | |
| #pragma clang diagnostic error "-Wconversion" | |
| #endif | |
| namespace { | |
| SkEncodedInfo::Color ToColor(rust_png::ColorType colorType, const rust_png::Reader& reader) { | |
| switch (colorType) { | |
| case rust_png::ColorType::Grayscale: | |
| return SkEncodedInfo::kGray_Color; | |
| case rust_png::ColorType::Rgb: | |
| if (reader.has_sbit_chunk()) { | |
| SkSpan<const uint8_t> sBit = ToSkSpan(reader.get_sbit_chunk()); | |
| // Restricting to 8-bit images, because of `SkEncodedInfo` | |
| // asserts. See also https://crbug.com/458852444. | |
| if (sBit.size() == 3 && reader.output_bits_per_component() == 8) { | |
| if (sBit[0] == 5 && sBit[1] == 6 && sBit[2] == 5) { | |
| return SkEncodedInfo::k565_Color; | |
| } | |
| } | |
| } | |
| return SkEncodedInfo::kRGB_Color; | |
| case rust_png::ColorType::GrayscaleAlpha: | |
| if (reader.has_sbit_chunk()) { | |
| SkSpan<const uint8_t> sBit = ToSkSpan(reader.get_sbit_chunk()); | |
| // Restricting to 8-bit images, because of `SkEncodedInfo` | |
| // asserts. See also https://crbug.com/458852444. | |
| if (sBit.size() == 2 && reader.output_bits_per_component() == 8) { | |
| if (sBit[0] == kGraySigBit_GrayAlphaIsJustAlpha && sBit[1] == 8) { | |
| return SkEncodedInfo::kXAlpha_Color; | |
| } | |
| } | |
| } | |
| return SkEncodedInfo::kGrayAlpha_Color; | |
| case rust_png::ColorType::Rgba: | |
| return SkEncodedInfo::kRGBA_Color; | |
| case rust_png::ColorType::Indexed: | |
| return SkEncodedInfo::kPalette_Color; | |
| } | |
| SK_ABORT("Unexpected `rust_png::ColorType`: %d", static_cast<int>(colorType)); | |
| } | |
| SkEncodedInfo::Alpha ToAlpha(rust_png::ColorType colorType, const rust_png::Reader& reader) { | |
| switch (colorType) { | |
| case rust_png::ColorType::Grayscale: | |
| case rust_png::ColorType::Rgb: | |
| return SkEncodedInfo::kOpaque_Alpha; | |
| case rust_png::ColorType::GrayscaleAlpha: | |
| case rust_png::ColorType::Rgba: | |
| return SkEncodedInfo::kUnpremul_Alpha; | |
| case rust_png::ColorType::Indexed: | |
| if (reader.has_trns_chunk()) { | |
| return SkEncodedInfo::kUnpremul_Alpha; | |
| } else { | |
| return SkEncodedInfo::kOpaque_Alpha; | |
| } | |
| } | |
| SK_ABORT("Unexpected `rust_png::ColorType`: %d", static_cast<int>(colorType)); | |
| } | |
| SkCodecAnimation::DisposalMethod ToDisposalMethod(rust_png::DisposeOp op) { | |
| switch (op) { | |
| case rust_png::DisposeOp::None: | |
| return SkCodecAnimation::DisposalMethod::kKeep; | |
| case rust_png::DisposeOp::Background: | |
| return SkCodecAnimation::DisposalMethod::kRestoreBGColor; | |
| case rust_png::DisposeOp::Previous: | |
| return SkCodecAnimation::DisposalMethod::kRestorePrevious; | |
| } | |
| SK_ABORT("Unexpected `rust_png::DisposeOp`: %d", static_cast<int>(op)); | |
| } | |
| SkCodecAnimation::Blend ToBlend(rust_png::BlendOp op) { | |
| switch (op) { | |
| case rust_png::BlendOp::Source: | |
| return SkCodecAnimation::Blend::kSrc; | |
| case rust_png::BlendOp::Over: | |
| return SkCodecAnimation::Blend::kSrcOver; | |
| } | |
| SK_ABORT("Unexpected `rust_png::BlendOp`: %d", static_cast<int>(op)); | |
| } | |
| SkColorSpacePrimaries ToSkColorSpacePrimaries(const rust_png::ColorSpacePrimaries& p) { | |
| return SkColorSpacePrimaries({p.fRX, p.fRY, p.fGX, p.fGY, p.fBX, p.fBY, p.fWX, p.fWY}); | |
| } | |
| skhdr::MasteringDisplayColorVolume ToSkMDCV(const rust_png::MasteringDisplayColorVolume& mdcv) { | |
| return skhdr::MasteringDisplayColorVolume({ | |
| ToSkColorSpacePrimaries(mdcv.fDisplayPrimaries), | |
| mdcv.fMaximumDisplayMasteringLuminance, | |
| mdcv.fMinimumDisplayMasteringLuminance}); | |
| } | |
| skhdr::ContentLightLevelInformation ToSkCLLI(const rust_png::ContentLightLevelInfo& clli) { | |
| return skhdr::ContentLightLevelInformation({clli.fMaxCLL, clli.fMaxFALL}); | |
| } | |
| std::unique_ptr<SkCodecs::ColorProfile> CreateColorProfile(const rust_png::Reader& reader) { | |
| // NOTE: This method is based on `read_color_profile` in | |
| // `src/codec/SkPngCodec.cpp` but has been refactored to use Rust inputs | |
| // instead of `libpng`. | |
| // Considering the `cICP` chunk first, because the spec at | |
| // https://www.w3.org/TR/png-3/#cICP-chunk says: "This chunk, if understood | |
| // by the decoder, is the highest-precedence color chunk." | |
| uint8_t cicpPrimariesId = 0; | |
| uint8_t cicpTransferId = 0; | |
| uint8_t cicpMatrixId = 0; | |
| bool cicpIsFullRange = false; | |
| if (reader.try_get_cicp_chunk(cicpPrimariesId, cicpTransferId, cicpMatrixId, cicpIsFullRange)) { | |
| // https://www.w3.org/TR/png-3/#cICP-chunk says "RGB is currently the | |
| // only supported color model in PNG, and as such Matrix Coefficients | |
| // shall be set to 0." | |
| // | |
| // According to SkColorSpace::MakeCICP narrow range images are rare and | |
| // therefore not supported. | |
| if (cicpMatrixId == 0 && cicpIsFullRange) { | |
| sk_sp<SkColorSpace> colorSpace = | |
| SkColorSpace::MakeCICP(static_cast<SkNamedPrimaries::CicpId>(cicpPrimariesId), | |
| static_cast<SkNamedTransferFn::CicpId>(cicpTransferId)); | |
| if (colorSpace) { | |
| return SkCodecs::ColorProfile::Make(colorSpace); | |
| } | |
| } | |
| } | |
| if (reader.has_iccp_chunk()) { | |
| // `SkData::MakeWithCopy` is resilient against 0-sized inputs, so | |
| // no need to check `rust_slice.empty()` here. | |
| rust::Slice<const uint8_t> rust_slice = reader.get_iccp_chunk(); | |
| sk_sp<SkData> owned_data = SkData::MakeWithCopy(rust_slice.data(), rust_slice.size()); | |
| std::unique_ptr<SkCodecs::ColorProfile> parsed_data = | |
| SkCodecs::ColorProfile::MakeICCProfile(std::move(owned_data)); | |
| if (parsed_data) { | |
| return parsed_data; | |
| } | |
| } | |
| if (reader.is_srgb()) { | |
| // TODO(https://crbug.com/362304558): Consider the intent field from the | |
| // `sRGB` chunk. | |
| return nullptr; | |
| } | |
| // Next, check for presence of `gAMA` and `cHRM` chunks. | |
| float gamma = 0; | |
| // Unlike libpng, image-rs does not seem to validate the gamma value. | |
| // So we check explicitly for zero, which is an invalid value encountered in the wild. | |
| // TODO: upstream? | |
| if (!reader.try_get_gama(gamma) || gamma <= 0) { | |
| // We ignore whether `chRM` is present or not. | |
| // | |
| // This preserves the behavior decided in Chromium's 83587041dc5f1428c09 | |
| // (https://codereview.chromium.org/2469473002). The PNG spec states | |
| // that cHRM is valid even without gAMA but we cannot apply the cHRM | |
| // without guessing a gAMA. Color correction is not a guessing game, | |
| // so we match the behavior of Safari and Firefox instead (compat). | |
| return nullptr; | |
| } | |
| rust_png::ColorSpacePrimaries chrm; | |
| const bool got_chrm = reader.try_get_chrm(chrm); | |
| if (!got_chrm) { | |
| // If there is no `cHRM` chunk then check if `gamma` is neutral (in PNG | |
| // / `SkNamedTransferFn::k2Dot2` sense). `kPngGammaThreshold` mimics | |
| // `PNG_GAMMA_THRESHOLD_FIXED` from `libpng`. | |
| constexpr float kPngGammaThreshold = 0.05f; | |
| constexpr float kMinNeutralValue = 1.0f - kPngGammaThreshold; | |
| constexpr float kMaxNeutralValue = 1.0f + kPngGammaThreshold; | |
| float tmp = gamma * 2.2f; | |
| bool is_neutral = kMinNeutralValue < tmp && tmp < kMaxNeutralValue; | |
| if (is_neutral) { | |
| // Don't construct a custom color profile if the only encoded color | |
| // space information is a "neutral" gamma. This is primarily needed | |
| // for correctness (see // https://crbug.com/388025081), but may | |
| // also help with performance (using a slightly more direct | |
| // `SkSwizzler` instead of `skcms_Transform`). | |
| return nullptr; | |
| } | |
| } | |
| // Construct a color profile based on `cHRM` and `gAMA` chunks. | |
| skcms_Matrix3x3 toXYZD50; | |
| if (got_chrm) { | |
| if (!ToSkColorSpacePrimaries(chrm).toXYZD50(&toXYZD50)) { | |
| return nullptr; | |
| } | |
| } else { | |
| // `blink::PNGImageDecoder` returns a null color profile when `gAMA` is | |
| // present without `cHRM`. We fall back to the sRGB profile instead | |
| // because we do gamma correction via `skcms_Transform` (rather than | |
| // relying on `libpng` gamma correction as the legacy Blink decoder does | |
| // in this scenario). | |
| toXYZD50 = SkNamedGamut::kSRGB; | |
| } | |
| skcms_TransferFunction fn; | |
| fn.a = 1.0f; | |
| fn.b = fn.c = fn.d = fn.e = fn.f = 0.0f; | |
| fn.g = 1.0f / gamma; | |
| return SkCodecs::ColorProfile::Make(fn, toXYZD50); | |
| } | |
| // Returns `nullopt` when input errors are encountered. | |
| std::optional<SkEncodedInfo> CreateEncodedInfo(const rust_png::Reader& reader) { | |
| rust_png::ColorType rustColor = reader.output_color_type(); | |
| SkEncodedInfo::Color skColor = ToColor(rustColor, reader); | |
| std::unique_ptr<SkCodecs::ColorProfile> profile = CreateColorProfile(reader); | |
| if (!SkPngCodecBase::isCompatibleColorProfileAndType(profile.get(), skColor)) { | |
| profile = nullptr; | |
| } | |
| skhdr::Metadata hdrMetadata; | |
| { | |
| rust_png::MasteringDisplayColorVolume rust_mdcv; | |
| if (reader.try_get_mdcv_chunk(rust_mdcv)) { | |
| hdrMetadata.setMasteringDisplayColorVolume(ToSkMDCV(rust_mdcv)); | |
| } | |
| rust_png::ContentLightLevelInfo rust_clli; | |
| if (reader.try_get_clli_chunk(rust_clli)) { | |
| hdrMetadata.setContentLightLevelInformation(ToSkCLLI(rust_clli)); | |
| } | |
| } | |
| // Protect against large PNGs. See http://bugzil.la/251381 for more details. | |
| constexpr uint32_t kMaxPNGSize = 1000000; | |
| if ((reader.width() > kMaxPNGSize) || (reader.height() > kMaxPNGSize)) { | |
| return std::nullopt; | |
| } | |
| // We checked image dimensions above, so here we can just assert that casts | |
| // from `uint32_t` to `int` work ok. | |
| // | |
| // We don't use a saturating cast, because this could invalidate `fcTL` | |
| // checks done within the `png` crate. For example, the new / truncated | |
| // image width could end up smaller than `fcTL.frameWidth`. | |
| SkSafeMath safe; | |
| int width = safe.castTo<int>(reader.width()); | |
| int height = safe.castTo<int>(reader.height()); | |
| SkASSERT_RELEASE(safe.ok()); | |
| return SkEncodedInfo::Make(width, | |
| height, | |
| skColor, | |
| ToAlpha(rustColor, reader), | |
| reader.output_bits_per_component(), // bitsPerComponent | |
| reader.output_bits_per_component(), // colorDepth | |
| std::move(profile), | |
| hdrMetadata); | |
| } | |
| SkCodec::Result ToSkCodecResult(rust_png::DecodingResult rustResult) { | |
| switch (rustResult) { | |
| case rust_png::DecodingResult::Success: | |
| case rust_png::DecodingResult::EndOfFrame: | |
| return SkCodec::kSuccess; | |
| case rust_png::DecodingResult::FormatError: | |
| return SkCodec::kErrorInInput; | |
| case rust_png::DecodingResult::ParameterError: | |
| return SkCodec::kInvalidParameters; | |
| case rust_png::DecodingResult::OtherIoError: | |
| case rust_png::DecodingResult::LimitsExceededError: | |
| return SkCodec::kInternalError; | |
| case rust_png::DecodingResult::IncompleteInput: | |
| return SkCodec::kIncompleteInput; | |
| } | |
| SK_ABORT("Unexpected `rust_png::DecodingResult`: %d", static_cast<int>(rustResult)); | |
| } | |
| void blendRow(SkSpan<uint8_t> dstRow, | |
| SkSpan<const uint8_t> srcRow, | |
| SkColorType color, | |
| SkAlphaType alpha) { | |
| SkASSERT_RELEASE(dstRow.size() >= srcRow.size()); | |
| SkRasterPipeline_<256> p; | |
| SkRasterPipelineContexts::MemoryCtx dstCtx = {dstRow.data(), 0}; | |
| p.appendLoadDst(color, &dstCtx); | |
| if (kUnpremul_SkAlphaType == alpha) { | |
| p.append(SkRasterPipelineOp::premul_dst); | |
| } | |
| SkRasterPipelineContexts::MemoryCtx srcCtx = { | |
| const_cast<void*>(static_cast<const void*>(srcRow.data())), | |
| 0, | |
| }; | |
| p.appendLoad(color, &srcCtx); | |
| if (kUnpremul_SkAlphaType == alpha) { | |
| p.append(SkRasterPipelineOp::premul); | |
| } | |
| p.append(SkRasterPipelineOp::srcover); | |
| if (kUnpremul_SkAlphaType == alpha) { | |
| p.append(SkRasterPipelineOp::unpremul); | |
| } | |
| p.appendStore(color, &dstCtx); | |
| SkSafeMath safe; | |
| size_t bpp = safe.castTo<size_t>(SkColorTypeBytesPerPixel(color)); | |
| SkASSERT_RELEASE(safe.ok()); | |
| size_t width = srcRow.size() / bpp; | |
| p.run(0, 0, width, 1); | |
| } | |
| void blendAllRows(SkSpan<uint8_t> dstFrame, | |
| SkSpan<const uint8_t> srcFrame, | |
| size_t rowSize, | |
| size_t rowStride, | |
| SkColorType color, | |
| SkAlphaType alpha) { | |
| while (srcFrame.size() >= rowSize) { | |
| blendRow(dstFrame, srcFrame.first(rowSize), color, alpha); | |
| srcFrame = srcFrame.subspan(std::min(rowStride, srcFrame.size())); | |
| dstFrame = dstFrame.subspan(std::min(rowStride, dstFrame.size())); | |
| } | |
| } | |
| SkEncodedOrigin GetEncodedOrigin(const rust_png::Reader& reader) { | |
| if (reader.has_exif_chunk()) { | |
| rust::Slice<const uint8_t> rust_slice = reader.get_exif_chunk(); | |
| SkEncodedOrigin origin; | |
| if (SkParseEncodedOrigin(rust_slice.data(), rust_slice.size(), &origin)) { | |
| return origin; | |
| } | |
| } | |
| return kTopLeft_SkEncodedOrigin; | |
| } | |
| bool IsValidFctlIfAny(const SkEncodedInfo& imageInfo, | |
| const rust_png::Reader& reader) { | |
| // Enforce that if an `fcTL` appears before an `IDAT` chunk, then it has the | |
| // same dimensions as the ones in the earlier `IHDR` chunk. This | |
| // corresponds to the restrictions that the spec at | |
| // https://www.w3.org/TR/png-3/#fcTL-chunk places on "fcTL chunk | |
| // corresponding to the default image". | |
| // | |
| // Doing this check here is more robust than doing it inside | |
| // `setFrameInfoFromCurrentFctlChunk`, because the code elsewhere in | |
| // `SkPngRustCodec` may ignore the `fcTL` chunk (e.g. if | |
| // `idatIsNotPartOfAnimation` and/or if `acTL` chunk is missing). | |
| // | |
| // Reporting a hard error is more robust than trying to ignore the `fcTL` | |
| // chunk and falling back to decoding a static image, because such a | |
| // fallback would risk discrepancies between dimensions used in different | |
| // layers of the software stack (see https://crbug.com/428205250). | |
| // | |
| // This check is kind of a defense-in-depth - in the long-term the | |
| // dimensions should be checked in the Rust `png` crate itself | |
| // (see https://github.com/image-rs/image-png/pull/614 which hasn't yet been | |
| // released in a new crates.io version). | |
| if (reader.has_fctl_chunk()) { | |
| uint32_t width = 0; | |
| uint32_t height = 0; | |
| uint32_t xOffset = 0; | |
| uint32_t yOffset = 0; | |
| auto ignoredDisposeOp = rust_png::DisposeOp::None; | |
| auto ignoredBlendOp = rust_png::BlendOp::Source; | |
| uint32_t ignoredDurationMs = 0; | |
| reader.get_fctl_info(width, height, xOffset, yOffset, | |
| ignoredDisposeOp, ignoredBlendOp, ignoredDurationMs); | |
| SkSafeMath safe; | |
| int frameWidth = safe.castTo<int>(width); | |
| int frameHeight = safe.castTo<int>(height); | |
| if (!safe.ok()) { | |
| return false; | |
| } | |
| if (xOffset != 0 || frameWidth != imageInfo.width() || | |
| yOffset != 0 || frameHeight != imageInfo.height()) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| void ReadUnknownChunks(const rust_png::Reader& reader, SkPngCompositeChunkReader* chunkReader) { | |
| if (!chunkReader) { | |
| return; | |
| } | |
| size_t unknownChunksCount = reader.get_unknown_chunks_count(); | |
| for (size_t i = 0; i < unknownChunksCount; ++i) { | |
| auto name = reader.get_unknown_chunk_name(i); | |
| auto data = reader.get_unknown_chunk_data(i); | |
| std::string tag(reinterpret_cast<const char*>(name.data()), 4); | |
| chunkReader->readChunk(tag.c_str(), data.data(), data.size()); | |
| } | |
| } | |
| } // namespace | |
| // static | |
| std::unique_ptr<SkPngRustCodec> SkPngRustCodec::MakeFromStream(std::unique_ptr<SkStream> stream, | |
| Result* result, | |
| SkPngChunkReader* chunkReader) { | |
| SkASSERT_RELEASE(stream); | |
| SkASSERT_RELEASE(result); | |
| auto inputAdapter = std::make_unique<rust::stream::SkStreamAdapter>(stream.get()); | |
| rust::Box<rust_png::ResultOfReader> resultOfReader = | |
| rust_png::new_reader(std::move(inputAdapter)); | |
| *result = ToSkCodecResult(resultOfReader->err()); | |
| if (*result != kSuccess) { | |
| return nullptr; | |
| } | |
| rust::Box<rust_png::Reader> reader = resultOfReader->unwrap(); | |
| std::optional<SkEncodedInfo> maybeImageInfo = CreateEncodedInfo(*reader); | |
| if (!maybeImageInfo.has_value()) { | |
| *result = kErrorInInput; | |
| return nullptr; | |
| } | |
| SkEncodedInfo& imageInfo = *maybeImageInfo; | |
| if (!IsValidFctlIfAny(imageInfo, *reader)) { | |
| *result = kErrorInInput; | |
| return nullptr; | |
| } | |
| auto compositeReader = sk_make_sp<SkPngCompositeChunkReader>(chunkReader); | |
| ReadUnknownChunks(*reader, compositeReader.get()); | |
| std::unique_ptr<SkStream> gainmapStream = compositeReader->takeGainmapStream(); | |
| std::optional<SkGainmapInfo> gainmapInfo = compositeReader->getGainmapInfo(); | |
| return std::make_unique<SkPngRustCodec>(std::move(imageInfo), | |
| std::move(stream), | |
| std::move(reader), | |
| std::move(compositeReader), | |
| std::move(gainmapStream), | |
| std::move(gainmapInfo)); | |
| } | |
| SkPngRustCodec::SkPngRustCodec(SkEncodedInfo&& encodedInfo, | |
| std::unique_ptr<SkStream> stream, | |
| rust::Box<rust_png::Reader> reader, | |
| sk_sp<SkPngCompositeChunkReader> chunkReader, | |
| std::unique_ptr<SkStream> gainmapStream, | |
| std::optional<SkGainmapInfo> gainmapInfo) | |
| : SkPngCodecBase(std::move(encodedInfo), | |
| // TODO(https://crbug.com/370522089): If/when `SkCodec` can | |
| // avoid unnecessary rewinding, then stop "hiding" our stream | |
| // from it. | |
| /* stream = */ nullptr, | |
| GetEncodedOrigin(*reader), | |
| std::move(chunkReader), | |
| std::move(gainmapStream), | |
| gainmapInfo) | |
| , fReader(std::move(reader)) | |
| , fPrivStream(std::move(stream)) | |
| , fFrameHolder(encodedInfo.width(), encodedInfo.height()) { | |
| SkASSERT_RELEASE(fPrivStream); | |
| bool idatIsNotPartOfAnimation = fReader->has_actl_chunk() && !fReader->has_fctl_chunk(); | |
| fFrameAtCurrentStreamPosition = idatIsNotPartOfAnimation ? -1 : 0; | |
| fStreamIsPositionedAtStartOfFrameData = true; | |
| if (!idatIsNotPartOfAnimation) { | |
| // This `appendNewFrame` call should always succeed because: | |
| // * `fFrameHolder.size()` is 0 at this point | |
| // * Width and height are already capped when calling `SkEncodedInfo::Make` | |
| // * `!fReader->has_fctl_chunk()` means that we don't need to worry | |
| // about validating other frame metadata. | |
| Result result = fFrameHolder.appendNewFrame(*fReader, this->getEncodedInfo()); | |
| SkASSERT_RELEASE(result == kSuccess); | |
| } | |
| } | |
| SkPngRustCodec::~SkPngRustCodec() = default; | |
| SkCodec::Result SkPngRustCodec::readToStartOfNextFrame() { | |
| SkASSERT_RELEASE(fFrameAtCurrentStreamPosition < this->getRawFrameCount()); | |
| Result result = ToSkCodecResult(fReader->next_frame_info()); | |
| if (result != kSuccess) { | |
| fStreamIsPositionedAtStartOfFrameData = false; | |
| return result; | |
| } | |
| fStreamIsPositionedAtStartOfFrameData = true; | |
| fFrameAtCurrentStreamPosition++; | |
| if (fFrameAtCurrentStreamPosition == fFrameHolder.size()) { | |
| result = fFrameHolder.appendNewFrame(*fReader, this->getEncodedInfo()); | |
| } | |
| return result; | |
| } | |
| SkCodec::Result SkPngRustCodec::seekToStartOfFrame(int index) { | |
| // Callers of this `private` method should provide a valid `index`. | |
| // | |
| // `index == fFrameHolder.size()` means that we are seeking to the next | |
| // frame (i.e. to the first frame for which an `fcTL` chunk wasn't parsed | |
| // yet). | |
| SkASSERT_RELEASE((0 <= index) && (index <= fFrameHolder.size())); | |
| // TODO(https://crbug.com/371060427): Improve runtime performance by seeking | |
| // directly to the right offset in the stream, rather than calling `rewind` | |
| // here and moving one-frame-at-a-time via `readToStartOfNextFrame` below. | |
| if ((index < fFrameAtCurrentStreamPosition) || | |
| (index == fFrameAtCurrentStreamPosition && !fStreamIsPositionedAtStartOfFrameData)) { | |
| if (!fPrivStream->rewind()) { | |
| return kCouldNotRewind; | |
| } | |
| auto inputAdapter = | |
| std::make_unique<rust::stream::SkStreamAdapter>(fPrivStream.get()); | |
| rust::Box<rust_png::ResultOfReader> resultOfReader = | |
| rust_png::new_reader(std::move(inputAdapter)); | |
| // `SkPngRustCodec` constructor must have run before, and the | |
| // constructor got a successfully created reader - we therefore also | |
| // expect success here. | |
| SkASSERT_RELEASE(kSuccess == ToSkCodecResult(resultOfReader->err())); | |
| fReader = resultOfReader->unwrap(); | |
| this->processUnknownChunks(); | |
| bool idatIsNotPartOfAnimation = fReader->has_actl_chunk() && !fReader->has_fctl_chunk(); | |
| fFrameAtCurrentStreamPosition = idatIsNotPartOfAnimation ? -1 : 0; | |
| fStreamIsPositionedAtStartOfFrameData = true; | |
| } | |
| while (fFrameAtCurrentStreamPosition < index) { | |
| Result result = this->readToStartOfNextFrame(); | |
| if (result != kSuccess) { | |
| return result; | |
| } | |
| } | |
| return kSuccess; | |
| } | |
| int SkPngRustCodec::getRawFrameCount() const { | |
| if (!fReader->has_actl_chunk()) { | |
| return 1; | |
| } | |
| static_assert(sizeof(int) >= sizeof(int32_t), "Is it ok to use Sk64_pin_to_s32 below?"); | |
| uint32_t num_frames = fReader->get_actl_num_frames(); | |
| return Sk64_pin_to_s32(num_frames); | |
| } | |
| SkCodec::Result SkPngRustCodec::parseAdditionalFrameInfos() { | |
| while (fFrameHolder.size() < this->getRawFrameCount()) { | |
| int oldFrameCount = fFrameHolder.size(); | |
| Result result = this->seekToStartOfFrame(fFrameHolder.size()); | |
| if (result != kSuccess) { | |
| return result; | |
| } | |
| SkASSERT_RELEASE(fFrameHolder.size() == (oldFrameCount + 1)); | |
| } | |
| return kSuccess; | |
| } | |
| void SkPngRustCodec::getSubsetOrSampleFromFullImage(SkSpan<const uint8_t> fullImageBuffer, | |
| SkSpan<uint8_t> dst, | |
| size_t dstRowStride, | |
| size_t offset, | |
| int srcHeight, | |
| int dstHeight, | |
| size_t dstRowSize) { | |
| // This only needs to be used in the case of interlaced images that need a subset | |
| // or sampling, otherwise we can decode row by row. | |
| SkASSERT_RELEASE(fReader->interlaced()); | |
| SkASSERT_RELEASE(this->options().fSubset || this->isSampling()); | |
| // Verify we have enough source rows | |
| SkASSERT_RELEASE(fullImageBuffer.size() >= offset); // Preventing subtraction underflow. | |
| SkASSERT_RELEASE(srcHeight >= 0); // Preventing `static_cast<size_t>` overflow | |
| const size_t encodedRowBytes = this->getEncodedRowBytes(); | |
| size_t fullRowsInFullImageBuffer = (fullImageBuffer.size() - offset) / encodedRowBytes; | |
| SkASSERT_RELEASE(fullRowsInFullImageBuffer >= static_cast<size_t>(srcHeight)); | |
| // We want the whole row and applyXformRow does the rest, so only offset to correct y value. | |
| fullImageBuffer = fullImageBuffer.subspan(offset); | |
| int rowsWritten = 0; | |
| for (int y = 0; y < srcHeight && rowsWritten < dstHeight; ++y) { | |
| SkSpan<const uint8_t> srcRow = fullImageBuffer.first(encodedRowBytes); | |
| fullImageBuffer = fullImageBuffer.subspan(encodedRowBytes); | |
| // Skip rows not needed by the vertical sampler | |
| if (this->swizzler() && !this->swizzler()->rowNeeded(y)) { | |
| continue; | |
| } | |
| SkSpan<uint8_t> dstRow = dst.first(dstRowSize); | |
| // Safe advancement: robust if fDst lacks stride padding on the last row. | |
| dst = dst.subspan(std::min(dstRowStride, dst.size())); | |
| // Copy the source row into the correct position in the destination. | |
| this->applyXformRow(dstRow, srcRow); | |
| rowsWritten++; | |
| } | |
| } | |
| SkCodec::Result SkPngRustCodec::startDecoding(const SkImageInfo& dstInfo, | |
| void* pixels, | |
| size_t rowBytes, | |
| const Options& options, | |
| std::optional<DecodingState>& decodingState) { | |
| if (options.fFrameIndex == 0 && fFrameHolder.size() == 0) { | |
| Result result = this->seekToStartOfFrame(0); | |
| if (result != kSuccess) { | |
| return result; | |
| } | |
| } | |
| if (options.fFrameIndex < 0 || options.fFrameIndex >= fFrameHolder.size()) { | |
| return kInvalidParameters; | |
| } | |
| const SkFrame* frame = fFrameHolder.getFrame(options.fFrameIndex); | |
| SkASSERT_RELEASE(frame); | |
| // TODO(https://crbug.com/362830091): Consider handling `fSubset` for APNG. | |
| if (options.fSubset && !this->supportsSamplingOrSubsetting()) { | |
| return kUnimplemented; | |
| } | |
| Result result = this->seekToStartOfFrame(options.fFrameIndex); | |
| if (result != kSuccess) { | |
| return result; | |
| } | |
| // https://www.w3.org/TR/png-3/#11PLTE says that for color type 3 | |
| // (indexed-color), the PLTE chunk is required. OTOH, `Codec_InvalidImages` | |
| // expects that we will succeed in this case and produce *some* output. | |
| // | |
| // This check needs to happen after `seekToStartOfFrame`, because the act | |
| // of seeking-and/or-rewinding may reset whether `fReader` has encountered | |
| // an PLTE chunk yet or not. | |
| if (this->getEncodedInfo().color() == SkEncodedInfo::kPalette_Color && | |
| !fReader->has_plte_chunk()) { | |
| return kInvalidInput; | |
| } | |
| result = this->initializeXforms(dstInfo, options, frame->width()); | |
| if (result != kSuccess) { | |
| return result; | |
| } | |
| SkSafeMath safe; | |
| DecodingState state; | |
| state.fDst = UninitializedDstInfo{ | |
| .fDstPtr = static_cast<uint8_t*>(pixels), | |
| .fDstRowStride = rowBytes, | |
| }; | |
| if (options.fSubset) { | |
| state.fYByteOffset = safe.mul(safe.castTo<size_t>(this->getEncodedRowBytes()), | |
| safe.castTo<size_t>(options.fSubset->top())); | |
| state.fFirstRow = options.fSubset->top(); | |
| state.fLastRow = options.fSubset->bottom() - 1; | |
| } else { | |
| state.fYByteOffset = 0; | |
| state.fFirstRow = 0; | |
| state.fLastRow = frame->height() - 1; | |
| } | |
| if (!safe.ok()) { | |
| return kErrorInInput; | |
| } | |
| decodingState = std::move(state); | |
| return kSuccess; | |
| } | |
| SkCodec::Result SkPngRustCodec::initializeDecodingDst(DecodingState& decodingState) { | |
| if (std::holds_alternative<DecodingDstInfo>(decodingState.fDst)) { | |
| return kSuccess; | |
| } | |
| const SkFrame* frame = fFrameHolder.getFrame(this->options().fFrameIndex); | |
| SkASSERT_RELEASE(frame); | |
| const UninitializedDstInfo uninit = std::get<UninitializedDstInfo>(decodingState.fDst); | |
| SkSafeMath safe; | |
| uint8_t dstBytesPerPixel = safe.castTo<uint8_t>(this->dstInfo().bytesPerPixel()); | |
| if (dstBytesPerPixel >= 32u) { | |
| return kInvalidParameters; | |
| } | |
| size_t dstRowCount = safe.castTo<size_t>(frame->height()); | |
| size_t dstRowSize = 0; | |
| if (!this->canReadRow()) { | |
| this->initializeXformParams(); | |
| dstRowSize = this->getDstRowSize(); | |
| } else { | |
| size_t frameWidth = safe.castTo<size_t>(frame->width()); | |
| dstRowSize = safe.mul(dstBytesPerPixel, frameWidth); | |
| if (!safe.ok()) { | |
| return kErrorInInput; | |
| } | |
| } | |
| SkSpan<uint8_t> dstSpan; | |
| if (this->isSampling() || this->options().fSubset) { | |
| if (!this->supportsSamplingOrSubsetting()) { | |
| return kInvalidParameters; | |
| } | |
| if (uninit.fDstRowStride < dstRowSize) { | |
| return kInvalidParameters; | |
| } | |
| const int srcHeight = decodingState.fLastRow - decodingState.fFirstRow + 1; | |
| const int sampleY = this->swizzler() ? this->swizzler()->sampleY() : 1; | |
| const size_t dstHeight = | |
| safe.castTo<size_t>(SkCodecPriv::GetSampledDimension(srcHeight, sampleY)); | |
| const size_t dstBufferSize = safe.mul(dstHeight, uninit.fDstRowStride); | |
| if (!safe.ok()) { | |
| return kErrorInInput; | |
| } | |
| dstSpan = SkSpan(uninit.fDstPtr, dstBufferSize); | |
| } else { | |
| if (uninit.fDstRowStride < this->dstInfo().minRowBytes()) { | |
| return kInvalidParameters; | |
| } | |
| size_t imageHeight = safe.castTo<size_t>(this->dstInfo().height()); | |
| size_t imageSize = safe.mul(uninit.fDstRowStride, imageHeight); | |
| size_t xPixelOffset = safe.castTo<size_t>(frame->xOffset()); | |
| size_t xByteOffsetFrame = safe.mul(dstBytesPerPixel, xPixelOffset); | |
| size_t yPixelOffset = safe.castTo<size_t>(frame->yOffset()); | |
| size_t yByteOffsetFrame = safe.mul(uninit.fDstRowStride, yPixelOffset); | |
| size_t frameHeightTimesRowStride = | |
| safe.mul(dstRowCount, uninit.fDstRowStride); | |
| if (!safe.ok()) { | |
| return kErrorInInput; | |
| } | |
| dstSpan = SkSpan(uninit.fDstPtr, imageSize) | |
| .subspan(xByteOffsetFrame) | |
| .subspan(yByteOffsetFrame); | |
| if (frameHeightTimesRowStride < dstSpan.size()) { | |
| dstSpan = dstSpan.first(frameHeightTimesRowStride); | |
| } | |
| } | |
| size_t preblendBufferSize = 0; | |
| if (fReader->interlaced()) { | |
| // Use fPreblendBuffer to decode whole image untransformed, then truncate/xform later. | |
| if (this->options().fSubset || this->isSampling()) { | |
| preblendBufferSize = | |
| safe.mul(this->getEncodedRowBytes(), | |
| safe.castTo<size_t>(this->getEncodedInfo().height())); | |
| if (!safe.ok()) { | |
| return kErrorInInput; | |
| } | |
| } else if (frame->getBlend() == SkCodecAnimation::Blend::kSrcOver) { | |
| preblendBufferSize = dstSpan.size(); | |
| } | |
| } else if (frame->getBlend() == SkCodecAnimation::Blend::kSrcOver) { | |
| preblendBufferSize = dstRowSize; | |
| } | |
| if (Result result = decodingState.fPreblendBuffer.allocateFromBudget(this, preblendBufferSize); | |
| result != kSuccess) { | |
| return result; | |
| } | |
| if (fReader->interlaced() && !this->options().fSubset && !this->isSampling()) { | |
| if (Result result = decodingState.fDecodedInterlacedFullWidthRow.allocateFromBudget( | |
| this, this->getEncodedRowBytes()); | |
| result != kSuccess) { | |
| return result; | |
| } | |
| if (!this->canReadRow()) { | |
| if (Result result = | |
| decodingState.fXformedInterlacedRow.allocateFromBudget(this, dstRowSize); | |
| result != kSuccess) { | |
| return result; | |
| } | |
| } | |
| } | |
| decodingState.fDst = DecodingDstInfo{ | |
| .fDstRowStride = uninit.fDstRowStride, | |
| .fDst = dstSpan, | |
| .fDstRowSize = dstRowSize, | |
| .fDstRowCount = dstRowCount, | |
| .fDstBytesPerPixel = dstBytesPerPixel, | |
| }; | |
| return kSuccess; | |
| } | |
| void SkPngRustCodec::expandDecodedInterlacedRow(DecodingState& decodingState, | |
| SkSpan<uint8_t> dstFrame, | |
| SkSpan<const uint8_t> srcRow, | |
| const DecodingDstInfo& decodingDst, | |
| bool xFormNeeded) { | |
| SkASSERT_RELEASE(fReader->interlaced()); | |
| const size_t dstRowStride = decodingDst.fDstRowStride; | |
| if (xFormNeeded) { | |
| // Copy (potentially shorter for initial Adam7 passes) `srcRow` into a | |
| // full-frame-width `fDecodedInterlacedFullWidthRow`. This is needed because | |
| // `applyXformRow` requires full-width rows as input (can't change | |
| // `SkSwizzler::fSrcWidth` after `initializeXforms`). | |
| SkSpan<uint8_t> decodedInterlacedFullWidthRow = | |
| decodingState.fDecodedInterlacedFullWidthRow.span(); | |
| SkASSERT_RELEASE(decodedInterlacedFullWidthRow.size() == this->getEncodedRowBytes()); | |
| SkASSERT_RELEASE(decodedInterlacedFullWidthRow.size() >= srcRow.size()); | |
| memcpy(decodedInterlacedFullWidthRow.data(), srcRow.data(), srcRow.size()); | |
| memset(decodedInterlacedFullWidthRow.data() + srcRow.size(), | |
| 0, | |
| decodedInterlacedFullWidthRow.size() - srcRow.size()); | |
| // With `kYes_ZeroInitialized`, `fSwizzler` skips writing leading transparent pixels on the | |
| // assumption that the destination is already zeroed, so `fXformedInterlacedRow` must be | |
| // re-zeroed before each row. Otherwise, every byte of the row gets overwritten. | |
| SkSpan<uint8_t> xformedInterlacedRow = decodingState.fXformedInterlacedRow.span(); | |
| SkASSERT_RELEASE(xformedInterlacedRow.size() == decodingDst.fDstRowSize); | |
| if (this->options().fZeroInitialized == kYes_ZeroInitialized) { | |
| memset(xformedInterlacedRow.data(), 0, xformedInterlacedRow.size()); | |
| } | |
| this->applyXformRow(xformedInterlacedRow, decodedInterlacedFullWidthRow); | |
| } | |
| const uint8_t dstBytesPerPixel = decodingDst.fDstBytesPerPixel; | |
| SkASSERT_RELEASE(dstBytesPerPixel < 32u); // Checked in `initializeDecodingDst`. | |
| rust::Slice<const uint8_t> expandedRow = | |
| xFormNeeded ? rust::Slice<const uint8_t>(decodingState.fXformedInterlacedRow) | |
| : rust::Slice<const uint8_t>(srcRow); | |
| const uint8_t bitsPerPixel = | |
| xFormNeeded ? dstBytesPerPixel * 8u : this->getEncodedInfo().bitsPerPixel(); | |
| // `splat_last_interlaced_row` infers the number of rows of the frame from | |
| // `dstFrame.size() / dstRowStride`, so it can only be used when `dstFrame` | |
| // spans whole rows for the full height of the frame. This is not the case | |
| // when an APNG frame is offset horizontally - the tail of `dstFrame` then | |
| // stops short of a whole row. | |
| SkSafeMath safe; | |
| size_t wholeFrameSize = safe.mul(dstRowStride, decodingDst.fDstRowCount); | |
| if (safe.ok() && dstFrame.size() >= wholeFrameSize) { | |
| SkSpan<uint8_t> wholeFrame = dstFrame.first(wholeFrameSize); | |
| // Replicating each decoded sample over the pixels that later Adam7 | |
| // passes will overwrite means that `dstFrame` holds a complete (if | |
| // blocky) image after each pass, rather than a mostly-transparent one. | |
| // This matters because partially decoded images are presented to the | |
| // user. The final image is not affected. | |
| fReader->splat_last_interlaced_row( | |
| rust::Slice<uint8_t>(wholeFrame), dstRowStride, expandedRow, bitsPerPixel); | |
| } else { | |
| fReader->expand_last_interlaced_row( | |
| rust::Slice<uint8_t>(dstFrame), dstRowStride, expandedRow, bitsPerPixel); | |
| } | |
| } | |
| bool SkPngRustCodec::isLastFrame() { | |
| return (this->isAnimated() == IsAnimated::kNo) || | |
| (this->options().fFrameIndex + 1 == this->getRawFrameCount()); | |
| } | |
| bool SkPngRustCodec::isSampling() const { | |
| return this->swizzler() && (this->swizzler()->sampleX() > 1 || this->swizzler()->sampleY() > 1); | |
| } | |
| bool SkPngRustCodec::supportsSamplingOrSubsetting() { | |
| if (this->isAnimated() != IsAnimated::kNo || fFrameHolder.size() == 0) { | |
| return false; | |
| } | |
| const SkFrame* frame = fFrameHolder.getFrame(0); | |
| return frame && frame->frameRect() == this->bounds(); | |
| } | |
| void SkPngRustCodec::finishStreamAfterLastFrame() { std::ignore = fReader->finish_decoding(); } | |
| // Given the dstInfo and the rust colortype/bits per component, determines if we | |
| // can use rust_png::Reader::read_row to decode directly into dst. | |
| bool SkPngRustCodec::canReadRow() { | |
| // Check alpha types | |
| if (this->dstInfo().alphaType() != kUnpremul_SkAlphaType) { | |
| return false; | |
| } | |
| // We cannot decode directly when subsetting or sub-sampling. | |
| if (this->options().fSubset || this->isSampling()) { | |
| return false; | |
| } | |
| // Check color types | |
| rust_png::ColorType color_type = fReader->output_color_type(); | |
| uint8_t bits_per_component = fReader->output_bits_per_component(); | |
| switch (this->dstInfo().colorType()) { | |
| case kRGBA_8888_SkColorType: | |
| if (color_type != rust_png::ColorType::Rgba | |
| || bits_per_component != 8) { | |
| return false; | |
| } | |
| break; | |
| case kGray_8_SkColorType: | |
| if (color_type != rust_png::ColorType::Grayscale | |
| || bits_per_component != 8) { | |
| return false; | |
| } | |
| break; | |
| default: | |
| return false; | |
| } | |
| // Check profiles | |
| if (!!this->getEncodedInfo().profile() != !!this->dstInfo().colorSpace()) { | |
| return false; | |
| } | |
| if (this->getEncodedInfo().profile()) { | |
| SkASSERT_RELEASE(this->dstInfo().colorSpace()); | |
| skcms_ICCProfile dstProfile; | |
| this->dstInfo().colorSpace()->toProfile(&dstProfile); | |
| if (!skcms_ApproximatelyEqualProfiles(this->getEncodedInfo().profile(), &dstProfile)) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| SkCodec::Result SkPngRustCodec::incrementalDecodeXForm(DecodingState& decodingState) { | |
| SkASSERT_RELEASE(!this->canReadRow()); | |
| const bool isSampling = this->isSampling(); | |
| const bool interlaced = fReader->interlaced(); | |
| const bool subset = this->options().fSubset; | |
| DecodingDstInfo& decodingDst = decodingState.dst(); | |
| const int srcHeight = decodingState.fLastRow - decodingState.fFirstRow + 1; | |
| const int sampleY = this->swizzler() ? this->swizzler()->sampleY() : 1; | |
| const int dstHeight = SkCodecPriv::GetSampledDimension(srcHeight, sampleY); | |
| // A non-interlaced partial decode stops once it has written the rows it needs, leaving the | |
| // rest of the frame (and everything after it) unread. Interlaced decodes always walk the whole | |
| // frame, because every Adam7 pass has to be read before any row is final. | |
| const bool partialDecode = subset || isSampling; | |
| const bool stopsBeforeEndOfFrame = partialDecode && !interlaced; | |
| while (true) { | |
| rust::Slice<const uint8_t> decodedRow; | |
| fStreamIsPositionedAtStartOfFrameData = false; | |
| if (!stopsBeforeEndOfFrame || decodingState.fRowsWrittenToOutput < dstHeight) { | |
| Result result = ToSkCodecResult(fReader->next_interlaced_row(decodedRow)); | |
| if (result != kSuccess) { | |
| return result; | |
| } | |
| } | |
| // This is how FFI layer says "no more rows", or if we have decoded all the rows | |
| // we need for our subset or sampling. | |
| if (decodedRow.empty()) { | |
| if (interlaced && !decodingState.fPreblendBuffer.empty()) { | |
| if (partialDecode) { | |
| this->getSubsetOrSampleFromFullImage( | |
| SkSpan<uint8_t>(decodingState.fPreblendBuffer), | |
| decodingDst.fDst, | |
| decodingDst.fDstRowStride, | |
| decodingState.fYByteOffset, | |
| srcHeight, | |
| dstHeight, | |
| decodingDst.fDstRowSize); | |
| } else { | |
| blendAllRows(decodingDst.fDst, | |
| decodingState.fPreblendBuffer, | |
| decodingDst.fDstRowSize, | |
| decodingDst.fDstRowStride, | |
| this->dstInfo().colorType(), | |
| this->dstInfo().alphaType()); | |
| } | |
| } | |
| if (!interlaced && !partialDecode) { | |
| // All of the original `fDst` should be filled out at this point. | |
| SkASSERT_RELEASE(decodingDst.fDst.empty()); | |
| } | |
| if (!stopsBeforeEndOfFrame) { | |
| // `static_cast` is ok, because `startDecoding` already validated `fFrameIndex`. | |
| fFrameHolder.markFrameAsFullyReceived( | |
| static_cast<size_t>(this->options().fFrameIndex)); | |
| } | |
| if (!partialDecode && this->isLastFrame()) { | |
| this->finishStreamAfterLastFrame(); | |
| } | |
| return kSuccess; | |
| } | |
| if (interlaced) { | |
| if (decodingState.fPreblendBuffer.empty()) { | |
| this->expandDecodedInterlacedRow(decodingState, | |
| decodingDst.fDst, | |
| decodedRow, | |
| decodingDst, | |
| /*xFormNeeded=*/true); | |
| } else { | |
| if (partialDecode) { | |
| SkSafeMath safe; | |
| uint8_t encodedBytesPerPixel = safe.castTo<uint8_t>(this->getEncodedInfo() | |
| .makeImageInfo() | |
| .bytesPerPixel()); | |
| SkASSERT_RELEASE(safe.ok()); // Checked in `startDecoding`. | |
| DecodingDstInfo fullImageDecodingDst = { | |
| .fDstRowStride = this->getEncodedRowBytes(), | |
| .fDst = decodingState.fPreblendBuffer, | |
| .fDstRowSize = this->getEncodedRowBytes(), | |
| .fDstRowCount = static_cast<size_t>( | |
| this->getEncodedInfo().height()), | |
| .fDstBytesPerPixel = encodedBytesPerPixel}; | |
| this->expandDecodedInterlacedRow(decodingState, | |
| decodingState.fPreblendBuffer, | |
| decodedRow, | |
| fullImageDecodingDst, | |
| /*xFormNeeded=*/false); | |
| } else { | |
| this->expandDecodedInterlacedRow(decodingState, | |
| decodingState.fPreblendBuffer, | |
| decodedRow, | |
| decodingDst, | |
| /*xFormNeeded=*/true); | |
| } | |
| } | |
| // `fRowsWrittenToOutput` is not incremented, because full, contiguous rows | |
| // are not decoded until pass 6 (or 7 depending on how you look) of | |
| // Adam7 interlacing scheme. | |
| } else { | |
| int y = decodingState.fCurrentSourceRow - decodingState.fFirstRow; | |
| decodingState.fCurrentSourceRow++; | |
| if (y < 0) { | |
| continue; | |
| } | |
| if (this->swizzler() && !this->swizzler()->rowNeeded(y)) { | |
| continue; | |
| } | |
| if (decodingState.fPreblendBuffer.empty()) { | |
| this->applyXformRow(decodingDst.fDst, decodedRow); | |
| } else { | |
| // Non-empty `fPreblendBuffer` implies 2nd or later frame of | |
| // a APNG. But `SkPngRustCodec::getSampler` returns nullptr | |
| // for APNGs. So we don't need to worry about sub-sampling | |
| // here (maybe it works, but when enabling it we should be | |
| // careful, add tests, etc.). Note that `this->swizzler()` | |
| // may be non-null when no sub-sampling happens, because we | |
| // may still need to "swizzle" between color formats. | |
| SkASSERT_RELEASE(!isSampling); | |
| this->applyXformRow(decodingState.fPreblendBuffer, decodedRow); | |
| blendRow(decodingDst.fDst, | |
| decodingState.fPreblendBuffer, | |
| this->dstInfo().colorType(), | |
| this->dstInfo().alphaType()); | |
| } | |
| decodingDst.fDst = decodingDst.fDst.subspan( | |
| std::min(decodingDst.fDstRowStride, decodingDst.fDst.size())); | |
| decodingState.fRowsWrittenToOutput++; | |
| } | |
| } | |
| } | |
| SkCodec::Result SkPngRustCodec::incrementalDecode(DecodingState& decodingState) { | |
| SkASSERT_RELEASE(this->canReadRow()); | |
| const bool interlaced = fReader->interlaced(); | |
| rust::Slice<uint8_t> dstSlice; | |
| if (interlaced) { | |
| SkSpan<uint8_t> fullWidthRow = decodingState.fDecodedInterlacedFullWidthRow.span(); | |
| SkASSERT_RELEASE(fullWidthRow.size() == this->getEncodedRowBytes()); | |
| memset(fullWidthRow.data(), 0, fullWidthRow.size()); | |
| dstSlice = rust::Slice<uint8_t>(fullWidthRow); | |
| } | |
| DecodingDstInfo& decodingDst = decodingState.dst(); | |
| while (true) { | |
| if (!interlaced) { | |
| dstSlice = decodingState.fPreblendBuffer.empty() | |
| ? rust::Slice<uint8_t>(decodingDst.fDst) | |
| : rust::Slice<uint8_t>(decodingState.fPreblendBuffer); | |
| } | |
| fStreamIsPositionedAtStartOfFrameData = false; | |
| rust_png::DecodingResult rustResult = fReader->read_row(dstSlice); | |
| Result result = ToSkCodecResult(rustResult); | |
| if (result != kSuccess) { | |
| return result; | |
| } | |
| // No more rows. | |
| if (rustResult == rust_png::DecodingResult::EndOfFrame) { | |
| if (interlaced && !decodingState.fPreblendBuffer.empty()) { | |
| blendAllRows(decodingDst.fDst, | |
| decodingState.fPreblendBuffer, | |
| decodingDst.fDstRowSize, | |
| decodingDst.fDstRowStride, | |
| this->dstInfo().colorType(), | |
| this->dstInfo().alphaType()); | |
| } | |
| if (!interlaced) { | |
| // All of the original `fDst` should be filled out at this point. | |
| SkASSERT_RELEASE(decodingDst.fDst.empty()); | |
| } | |
| // `static_cast` is ok, because `startDecoding` already validated `fFrameIndex`. | |
| fFrameHolder.markFrameAsFullyReceived(static_cast<size_t>(this->options().fFrameIndex)); | |
| if (this->isLastFrame()) { | |
| // No partial-decode check here: `canReadRow` rules out subsets and sampling. | |
| this->finishStreamAfterLastFrame(); | |
| } | |
| return kSuccess; | |
| } | |
| // Expand interlaced rows or blend into previous frame if needed. | |
| if (interlaced) { | |
| if (decodingState.fPreblendBuffer.empty()) { | |
| this->expandDecodedInterlacedRow(decodingState, | |
| decodingDst.fDst, | |
| dstSlice, | |
| decodingDst, | |
| /*xFormNeeded=*/false); | |
| } else { | |
| this->expandDecodedInterlacedRow(decodingState, | |
| decodingState.fPreblendBuffer, | |
| dstSlice, | |
| decodingDst, | |
| /*xFormNeeded=*/false); | |
| } | |
| // `fRowsWrittenToOutput` is not incremented, because full, contiguous rows | |
| // are not decoded until pass 6 (or 7 depending on how you look) of | |
| // Adam7 interlacing scheme. | |
| } else { | |
| if (!decodingState.fPreblendBuffer.empty()) { | |
| blendRow(decodingDst.fDst, | |
| decodingState.fPreblendBuffer, | |
| this->dstInfo().colorType(), | |
| this->dstInfo().alphaType()); | |
| } | |
| // Increment our pointer to dst memory. | |
| decodingDst.fDst = decodingDst.fDst.subspan( | |
| std::min(decodingDst.fDstRowStride, decodingDst.fDst.size())); | |
| decodingState.fRowsWrittenToOutput++; | |
| } | |
| } | |
| } | |
| void SkPngRustCodec::reportRowsDecoded(Result result, | |
| const DecodingState& decodingState, | |
| int* rowsDecoded) const { | |
| if (result == kSuccess || !rowsDecoded) { | |
| return; | |
| } | |
| *rowsDecoded = decodingState.fRowsWrittenToOutput; | |
| } | |
| SkCodec::Result SkPngRustCodec::onGetPixels(const SkImageInfo& dstInfo, | |
| void* pixels, | |
| size_t rowBytes, | |
| const Options& options, | |
| int* rowsDecoded) { | |
| std::optional<DecodingState> decodingState; | |
| Result result = this->startDecoding(dstInfo, pixels, rowBytes, options, decodingState); | |
| if (result != kSuccess) { | |
| return result; | |
| } | |
| result = this->initializeDecodingDst(*decodingState); | |
| if (result == kSuccess) { | |
| if (this->canReadRow()) { | |
| result = this->incrementalDecode(*decodingState); | |
| } else { | |
| result = this->incrementalDecodeXForm(*decodingState); | |
| } | |
| } | |
| this->reportRowsDecoded(result, *decodingState, rowsDecoded); | |
| return result; | |
| } | |
| SkCodec::Result SkPngRustCodec::onStartIncrementalDecode(const SkImageInfo& dstInfo, | |
| void* pixels, | |
| size_t rowBytes, | |
| const Options& options) { | |
| // It is okay if `fIncrementalDecodingState` contains state of another, | |
| // partially decoded frame - in this case we want to clobber it. | |
| fIncrementalDecodingState.reset(); | |
| return this->startDecoding(dstInfo, pixels, rowBytes, options, fIncrementalDecodingState); | |
| } | |
| SkCodec::Result SkPngRustCodec::onIncrementalDecode(int* rowsDecoded) { | |
| if (!fIncrementalDecodingState.has_value()) { | |
| return kInvalidParameters; | |
| } | |
| Result result = this->initializeDecodingDst(*fIncrementalDecodingState); | |
| if (result == kSuccess) { | |
| if (this->canReadRow()) { | |
| result = this->incrementalDecode(*fIncrementalDecodingState); | |
| } else { | |
| result = this->incrementalDecodeXForm(*fIncrementalDecodingState); | |
| } | |
| } | |
| this->reportRowsDecoded(result, *fIncrementalDecodingState, rowsDecoded); | |
| if (result != kIncompleteInput) { | |
| // After successfully reading the whole row (`kSuccess`), and after a | |
| // fatal error (only recoverable error is `kIncompleteInput`) our client | |
| // should not call `onIncrementalDecode` again. This means that the | |
| // incremental decoding state can be discarded at this point. | |
| fIncrementalDecodingState.reset(); | |
| } | |
| return result; | |
| } | |
| int SkPngRustCodec::onGetFrameCount() { | |
| do { | |
| if (!fCanParseAdditionalFrameInfos || fIncrementalDecodingState.has_value()) { | |
| break; | |
| } | |
| if (fPrivStream->hasLength()) { | |
| size_t currentLength = fPrivStream->getLength(); | |
| if (fMaxStreamLengthSeenWhenParsingAdditionalFrameInfos.has_value()) { | |
| size_t oldLength = *fMaxStreamLengthSeenWhenParsingAdditionalFrameInfos; | |
| // We use `>=` instead of `==`, because the underlying stream | |
| // can be "cleared" - see https://crbug.com/431273809#comment4. | |
| if (oldLength >= currentLength) { | |
| // Don't retry `parseAdditionalFrameInfos` if the input | |
| // didn't change (or is smaller than last time). | |
| break; | |
| } | |
| } | |
| fMaxStreamLengthSeenWhenParsingAdditionalFrameInfos = currentLength; | |
| } | |
| switch (this->parseAdditionalFrameInfos()) { | |
| case kIncompleteInput: | |
| fCanParseAdditionalFrameInfos = true; | |
| break; | |
| case kSuccess: | |
| SkASSERT_RELEASE(fFrameHolder.size() == this->getRawFrameCount()); | |
| fCanParseAdditionalFrameInfos = false; | |
| break; | |
| default: | |
| fCanParseAdditionalFrameInfos = false; | |
| break; | |
| } | |
| } while (false); | |
| return fFrameHolder.size(); | |
| } | |
| bool SkPngRustCodec::onGetFrameInfo(int index, FrameInfo* info) const { | |
| return fFrameHolder.getFrameInfo(index, info); | |
| } | |
| int SkPngRustCodec::onGetRepetitionCount() { | |
| if (!fReader->has_actl_chunk()) { | |
| return 0; | |
| } | |
| uint32_t numFrames = fReader->get_actl_num_frames(); | |
| if (numFrames <= 1) { | |
| return 0; | |
| } | |
| // APNG spec says that "`num_plays` indicates the number of times that this | |
| // animation should play; if it is 0, the animation should play | |
| // indefinitely." | |
| SkSafeMath safe; | |
| int numPlays = safe.castTo<int>(fReader->get_actl_num_plays()); | |
| if ((numPlays == 0) || !safe.ok()) { | |
| return kRepetitionCountInfinite; | |
| } | |
| // Subtracting 1, because `SkCodec::onGetRepetitionCount` doc comment says | |
| // that "This number does not include the first play through of each frame. | |
| // For example, a repetition count of 4 means that each frame is played 5 | |
| // times and then the animation stops." | |
| return numPlays - 1; | |
| } | |
| SkCodec::IsAnimated SkPngRustCodec::onIsAnimated() { | |
| if (fReader->has_actl_chunk() && fReader->get_actl_num_frames() > 1) { | |
| return IsAnimated::kYes; | |
| } | |
| return IsAnimated::kNo; | |
| } | |
| std::unique_ptr<SkCodec> SkPngRustCodec::onDecodeGainmap(std::unique_ptr<SkStream> stream, | |
| SkCodec::Result* result) { | |
| return SkPngRustCodec::MakeFromStream(std::move(stream), result, fPngChunkReader.get()); | |
| } | |
| std::optional<SkSpan<const SkPngCodecBase::PaletteColorEntry>> SkPngRustCodec::onTryGetPlteChunk() { | |
| if (fReader->output_color_type() != rust_png::ColorType::Indexed) { | |
| return std::nullopt; | |
| } | |
| SkASSERT_RELEASE(fReader->has_plte_chunk()); // Checked in `startDecoding`. | |
| SkSpan<const uint8_t> bytes = ToSkSpan(fReader->get_plte_chunk()); | |
| // Make sure that `bytes.size()` is a multiple of | |
| // `sizeof(PaletteColorEntry)`. | |
| constexpr size_t kEntrySize = sizeof(PaletteColorEntry); | |
| bytes = bytes.first((bytes.size() / kEntrySize) * kEntrySize); | |
| // Alignment of `PaletteColorEntry` is 1, because its size is 3, and size | |
| // has to be a multiple of alignment (every element of an array has to be | |
| // aligned) + alignment is always a power of 2. And this means that | |
| // `bytes.data()` is already aligned. | |
| static_assert(kEntrySize == 3, ""); | |
| static_assert(std::alignment_of<PaletteColorEntry>::value == 1, ""); | |
| static_assert(std::alignment_of<uint8_t>::value == 1, ""); | |
| SkSpan<const PaletteColorEntry> palette = SkSpan( | |
| reinterpret_cast<const PaletteColorEntry*>(bytes.data()), bytes.size() / kEntrySize); | |
| return palette; | |
| } | |
| std::optional<SkSpan<const uint8_t>> SkPngRustCodec::onTryGetTrnsChunk() { | |
| if (fReader->output_color_type() != rust_png::ColorType::Indexed) { | |
| return std::nullopt; | |
| } | |
| if (!fReader->has_trns_chunk()) { | |
| return std::nullopt; | |
| } | |
| return ToSkSpan(fReader->get_trns_chunk()); | |
| } | |
| class SkPngRustCodec::FrameHolder::PngFrame final : public SkFrame { | |
| public: | |
| PngFrame(int id, SkEncodedInfo::Alpha alpha) : SkFrame(id), fReportedAlpha(alpha) {} | |
| bool isFullyReceived() const { return fFullyReceived; } | |
| void markAsFullyReceived() { fFullyReceived = true; } | |
| private: | |
| SkEncodedInfo::Alpha onReportedAlpha() const override { return fReportedAlpha; } | |
| const SkEncodedInfo::Alpha fReportedAlpha; | |
| bool fFullyReceived = false; | |
| }; | |
| SkPngRustCodec::FrameHolder::FrameHolder(int width, int height) : SkFrameHolder() { | |
| fScreenWidth = width; | |
| fScreenHeight = height; | |
| } | |
| const SkFrameHolder* SkPngRustCodec::getFrameHolder() const { return &fFrameHolder; } | |
| // We cannot use the SkCodec implementation since we pass nullptr to the superclass out of | |
| // an abundance of caution w/r to rewinding the stream. | |
| // | |
| // TODO(https://crbug.com/370522089): See if `SkCodec` can be tweaked to avoid | |
| // the need to hide the stream from it. | |
| sk_sp<const SkData> SkPngRustCodec::getEncodedData() const { | |
| SkASSERT_RELEASE(fPrivStream); | |
| sk_sp<const SkData> data = fPrivStream->getData(); | |
| if (data) { | |
| return data; | |
| } | |
| auto dStream = fPrivStream->duplicate(); | |
| if (!dStream->hasLength()) { | |
| return nullptr; | |
| } | |
| return SkData::MakeFromStream(dStream.get(), dStream->getLength()); | |
| } | |
| SkPngRustCodec::FrameHolder::~FrameHolder() = default; | |
| const SkFrame* SkPngRustCodec::FrameHolder::onGetFrame(int unverifiedIndex) const { | |
| SkSafeMath safe; | |
| size_t index = safe.castTo<size_t>(unverifiedIndex); | |
| if (safe.ok() && (index < fFrames.size())) { | |
| return &fFrames[index]; | |
| } | |
| return nullptr; | |
| } | |
| int SkPngRustCodec::FrameHolder::size() const { | |
| // This invariant is maintained in `appendNewFrame`. | |
| SkASSERT_RELEASE(SkTFitsIn<int>(fFrames.size())); | |
| return static_cast<int>(fFrames.size()); | |
| } | |
| void SkPngRustCodec::FrameHolder::markFrameAsFullyReceived(size_t index) { | |
| SkASSERT_RELEASE(index < fFrames.size()); | |
| fFrames[index].markAsFullyReceived(); | |
| } | |
| bool SkPngRustCodec::FrameHolder::getFrameInfo(int index, FrameInfo* info) const { | |
| const SkFrame* frame = this->getFrame(index); | |
| if (frame && info) { | |
| bool isFullyReceived = static_cast<const PngFrame*>(frame)->isFullyReceived(); | |
| frame->fillIn(info, isFullyReceived); | |
| } | |
| return !!frame; | |
| } | |
| SkCodec::Result SkPngRustCodec::FrameHolder::appendNewFrame(const rust_png::Reader& reader, | |
| const SkEncodedInfo& info) { | |
| // Ensure that `this->size()` fits into an `int`. `+ 1u` is used to account | |
| // for `push_back` / `emplace_back` below. | |
| if (!SkTFitsIn<int>(fFrames.size() + 1u)) { | |
| return kErrorInInput; | |
| } | |
| int id = static_cast<int>(fFrames.size()); | |
| if (reader.has_actl_chunk() && reader.has_fctl_chunk()) { | |
| if (!fFrames.empty()) { | |
| // Having `fcTL` for a new frame means that the previous frame has been | |
| // fully received (since all of the previous frame's `fdAT` / `IDAT` | |
| // chunks must have come before the new frame's `fcTL` chunk). | |
| fFrames.back().markAsFullyReceived(); | |
| } | |
| PngFrame frame(id, info.alpha()); | |
| SkCodec::Result result = this->setFrameInfoFromCurrentFctlChunk(reader, &frame); | |
| if (result == SkCodec::kSuccess) { | |
| fFrames.push_back(std::move(frame)); | |
| } | |
| return result; | |
| } | |
| SkASSERT_RELEASE(!reader.has_actl_chunk() || !reader.has_fctl_chunk()); | |
| SkASSERT_RELEASE(id == 0); | |
| fFrames.emplace_back(id, info.alpha()); | |
| SkFrame& frame = fFrames.back(); | |
| frame.setXYWH(0, 0, info.width(), info.height()); | |
| frame.setBlend(SkCodecAnimation::Blend::kSrc); | |
| this->setAlphaAndRequiredFrame(&frame); | |
| return kSuccess; | |
| } | |
| SkCodec::Result SkPngRustCodec::FrameHolder::setFrameInfoFromCurrentFctlChunk( | |
| const rust_png::Reader& reader, PngFrame* frame) { | |
| SkASSERT_RELEASE(reader.has_fctl_chunk()); // Caller should guarantee this | |
| SkASSERT_RELEASE(frame); | |
| uint32_t width = 0; | |
| uint32_t height = 0; | |
| uint32_t xOffset = 0; | |
| uint32_t yOffset = 0; | |
| auto disposeOp = rust_png::DisposeOp::None; | |
| auto blendOp = rust_png::BlendOp::Source; | |
| uint32_t durationMs = 0; | |
| reader.get_fctl_info(width, height, xOffset, yOffset, disposeOp, blendOp, durationMs); | |
| { | |
| SkSafeMath safe; | |
| frame->setXYWH(safe.castTo<int>(xOffset), | |
| safe.castTo<int>(yOffset), | |
| safe.castTo<int>(width), | |
| safe.castTo<int>(height)); | |
| frame->setDuration(safe.castTo<int>(durationMs)); | |
| if (!safe.ok()) { | |
| return kErrorInInput; | |
| } | |
| } | |
| frame->setDisposalMethod(ToDisposalMethod(disposeOp)); | |
| // https://wiki.mozilla.org/APNG_Specification#.60fcTL.60:_The_Frame_Control_Chunk | |
| // points out that "for the first frame the two blend modes are functionally | |
| // equivalent" so we use `BlendOp::Source` because it has better performance | |
| // characteristics. | |
| if (frame->frameId() == 0) { | |
| blendOp = rust_png::BlendOp::Source; | |
| } | |
| frame->setBlend(ToBlend(blendOp)); | |
| // Note: `setAlphaAndRequiredFrame` needs to be called last, because it | |
| // depends on the other properties set above. | |
| this->setAlphaAndRequiredFrame(frame); | |
| return kSuccess; | |
| } | |
| void SkPngRustCodec::processUnknownChunks() { ReadUnknownChunks(*fReader, fPngChunkReader.get()); } | |
| SkSampler* SkPngRustCodec::getSampler(bool createIfNecessary) { | |
| if (!this->supportsSamplingOrSubsetting()) { | |
| if (createIfNecessary) { | |
| // Clean up tentative decoding state set up by startIncrementalDecode. | |
| fIncrementalDecodingState.reset(); | |
| } | |
| return nullptr; | |
| } | |
| return SkPngCodecBase::getSampler(createIfNecessary); | |
| } |