| /* |
| * 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/encode/SkPngRustEncoderImpl.h" |
| |
| #include <limits> |
| #include <memory> |
| #include <optional> |
| #include <utility> |
| |
| #include "include/core/SkSpan.h" |
| #include "include/core/SkStream.h" |
| #include "include/encode/SkPngRustEncoder.h" |
| #include "include/private/SkEncodedInfo.h" |
| #include "include/private/base/SkAssert.h" |
| #include "rust/png/FFI.rs.h" |
| #include "rust/png/UtilsForFFI.h" |
| #include "src/base/SkSafeMath.h" |
| #include "src/encode/SkImageEncoderFns.h" |
| #include "src/encode/SkImageEncoderPriv.h" |
| #include "third_party/rust/cxx/v1/cxx.h" |
| |
| #ifdef __clang__ |
| #pragma clang diagnostic error "-Wconversion" |
| #endif |
| |
| namespace { |
| |
| rust_png::Compression ToCompression(SkPngRustEncoder::CompressionLevel level) { |
| switch (level) { |
| case SkPngRustEncoder::CompressionLevel::kLow: |
| return rust_png::Compression::Level1WithUpFilter; |
| case SkPngRustEncoder::CompressionLevel::kMedium: |
| #ifdef SK_RUST_PNG_MAP_MEDIUM_COMPRESSION_LEVEL_TO_FDEFLATE_FAST |
| // TODO(https://crbug.com/406072770): Consider using `Fast` instead |
| // of `Balanced` compression here. See the bug for details. |
| return rust_png::Compression::Fast; |
| #else |
| return rust_png::Compression::Balanced; |
| #endif |
| case SkPngRustEncoder::CompressionLevel::kHigh: |
| return rust_png::Compression::High; |
| } |
| SkUNREACHABLE; |
| } |
| |
| rust::Slice<const uint8_t> getDataTableEntry(const SkDataTable& table, int index) { |
| SkASSERT_RELEASE((0 <= index) && (index < table.count())); |
| |
| size_t size = 0; |
| const uint8_t* entry = table.atT<uint8_t>(index, &size); |
| while (size > 0 && entry[size - 1] == 0) { |
| // Ignore trailing NUL characters - these are *not* part of Rust `&str`. |
| size--; |
| } |
| |
| return rust::Slice<const uint8_t>(entry, size); |
| } |
| |
| rust_png::EncodingResult EncodeComments(rust_png::Writer& writer, |
| const sk_sp<SkDataTable>& comments) { |
| if (comments != nullptr) { |
| if (comments->count() % 2 != 0) { |
| return rust_png::EncodingResult::ParameterError; |
| } |
| |
| for (int i = 0; i < comments->count() / 2; ++i) { |
| rust::Slice<const uint8_t> keyword = getDataTableEntry(*comments, 2 * i); |
| rust::Slice<const uint8_t> text = getDataTableEntry(*comments, 2 * i + 1); |
| rust_png::EncodingResult result = writer.write_text_chunk(keyword, text); |
| if (result != rust_png::EncodingResult::Success) { |
| return result; |
| } |
| } |
| } |
| |
| return rust_png::EncodingResult::Success; |
| } |
| |
| // This helper class adapts `SkWStream` to expose the API required by Rust FFI |
| // (i.e. the `WriteTrait` API). |
| class WriteTraitAdapterForSkWStream final : public rust_png::WriteTrait { |
| public: |
| // SAFETY: The caller needs to guarantee that `stream` will be alive for |
| // as long as `WriteTraitAdapterForSkWStream`. |
| explicit WriteTraitAdapterForSkWStream(SkWStream* stream) : fStream(stream) { |
| SkASSERT_RELEASE(fStream); |
| } |
| |
| ~WriteTraitAdapterForSkWStream() override = default; |
| |
| // Non-copyable and non-movable. |
| WriteTraitAdapterForSkWStream(const WriteTraitAdapterForSkWStream&) = delete; |
| WriteTraitAdapterForSkWStream& operator=(const WriteTraitAdapterForSkWStream&) = delete; |
| WriteTraitAdapterForSkWStream(WriteTraitAdapterForSkWStream&&) = delete; |
| WriteTraitAdapterForSkWStream& operator=(WriteTraitAdapterForSkWStream&&) = delete; |
| |
| // Implementation of the `std::io::Read::read` method. See `RustTrait`'s |
| // doc comments and |
| // https://doc.rust-lang.org/nightly/std/io/trait.Read.html#tymethod.read |
| // for guidance on the desired implementation and behavior of this method. |
| bool write(rust::Slice<const uint8_t> buffer) override { |
| SkSpan<const uint8_t> span = ToSkSpan(buffer); |
| return fStream->write(span.data(), span.size()); |
| } |
| |
| void flush() override { fStream->flush(); } |
| |
| private: |
| SkWStream* fStream = nullptr; // Non-owning pointer. |
| }; |
| |
| } // namespace |
| |
| // static |
| std::unique_ptr<SkEncoder> SkPngRustEncoderImpl::Make(SkWStream* dst, |
| const SkPixmap& src, |
| const SkPngRustEncoder::Options& options) { |
| if (!SkPixmapIsValid(src)) { |
| return nullptr; |
| } |
| |
| std::optional<TargetInfo> maybeTargetInfo = SkPngEncoderBase::getTargetInfo(src.info()); |
| if (!maybeTargetInfo.has_value()) { |
| return nullptr; |
| } |
| const SkEncodedInfo& dstInfo = maybeTargetInfo->fDstInfo; |
| const std::optional<SkImageInfo>& maybeDstRowInfo = maybeTargetInfo->fDstRowInfo; |
| |
| SkSafeMath safe; |
| uint32_t width = safe.castTo<uint32_t>(dstInfo.width()); |
| uint32_t height = safe.castTo<uint32_t>(dstInfo.height()); |
| if (!safe.ok()) { |
| return nullptr; |
| } |
| |
| sk_sp<SkData> encodedProfile; |
| rust::Slice<const uint8_t> encodedProfileSlice; |
| if (const SkColorSpace* colorSpace = src.colorSpace(); colorSpace && !colorSpace->isSRGB()) { |
| encodedProfile = icc_from_color_space(colorSpace); |
| if (encodedProfile) { |
| encodedProfileSlice = |
| rust::Slice<const uint8_t>(encodedProfile->bytes(), encodedProfile->size()); |
| } |
| } |
| |
| rust_png::ColorType rustEncoderColorType; |
| ExtraRowTransform extraRowTransform = kNone_ExtraRowTransform; |
| switch (dstInfo.color()) { |
| case SkEncodedInfo::kRGB_Color: |
| rustEncoderColorType = rust_png::ColorType::Rgb; |
| break; |
| case SkEncodedInfo::kRGBA_Color: |
| rustEncoderColorType = rust_png::ColorType::Rgba; |
| if (maybeDstRowInfo) { |
| if (maybeDstRowInfo->isOpaque()) { |
| rustEncoderColorType = rust_png::ColorType::Rgb; |
| if (maybeDstRowInfo->colorType() == kR16G16B16A16_unorm_SkColorType) { |
| extraRowTransform = kRgba16leToRgb16be_ExtraRowTransform; |
| } else { |
| SkASSERT_RELEASE(maybeDstRowInfo->colorType() == kRGB_888x_SkColorType); |
| extraRowTransform = kRgba8ToRgb8_ExtraRowTransform; |
| } |
| } else if (maybeDstRowInfo->colorType() == kR16G16B16A16_unorm_SkColorType) { |
| extraRowTransform = kRgba16leToRgba16be_ExtraRowTransform; |
| } |
| } |
| break; |
| case SkEncodedInfo::kGray_Color: |
| rustEncoderColorType = rust_png::ColorType::Grayscale; |
| break; |
| case SkEncodedInfo::kGrayAlpha_Color: |
| rustEncoderColorType = rust_png::ColorType::GrayscaleAlpha; |
| break; |
| default: |
| SkUNREACHABLE; |
| } |
| |
| auto writeTraitAdapter = std::make_unique<WriteTraitAdapterForSkWStream>(dst); |
| rust::Box<rust_png::ResultOfWriter> resultOfWriter = |
| rust_png::new_writer(std::move(writeTraitAdapter), |
| width, |
| height, |
| rustEncoderColorType, |
| dstInfo.bitsPerComponent(), |
| ToCompression(options.fCompressionLevel), |
| encodedProfileSlice); |
| if (resultOfWriter->err() != rust_png::EncodingResult::Success) { |
| return nullptr; |
| } |
| rust::Box<rust_png::Writer> writer = resultOfWriter->unwrap(); |
| |
| if (EncodeComments(*writer, options.fComments) != rust_png::EncodingResult::Success) { |
| return nullptr; |
| } |
| |
| rust::Box<rust_png::ResultOfStreamWriter> resultOfStreamWriter = |
| rust_png::convert_writer_into_stream_writer(std::move(writer)); |
| if (resultOfStreamWriter->err() != rust_png::EncodingResult::Success) { |
| return nullptr; |
| } |
| rust::Box<rust_png::StreamWriter> stream_writer = resultOfStreamWriter->unwrap(); |
| |
| return std::make_unique<SkPngRustEncoderImpl>( |
| std::move(*maybeTargetInfo), src, std::move(stream_writer), extraRowTransform); |
| } |
| |
| SkPngRustEncoderImpl::SkPngRustEncoderImpl(TargetInfo targetInfo, |
| const SkPixmap& src, |
| rust::Box<rust_png::StreamWriter> stream_writer, |
| ExtraRowTransform extraRowTransform) |
| : SkPngEncoderBase(std::move(targetInfo), src) |
| , fStreamWriter(std::move(stream_writer)) |
| , fExtraRowTransform(extraRowTransform) {} |
| |
| SkPngRustEncoderImpl::~SkPngRustEncoderImpl() = default; |
| |
| bool SkPngRustEncoderImpl::onEncodeRow(SkSpan<const uint8_t> row) { |
| rust::Slice<const uint8_t> rustRow; |
| if (this->fExtraRowTransform != kNone_ExtraRowTransform) { |
| skcms_PixelFormat srcFmt, dstFmt; |
| size_t srcRowBytes = this->targetInfo().fDstRowInfo->minRowBytes(); |
| size_t dstRowBytes; |
| switch (this->fExtraRowTransform) { |
| case kRgba8ToRgb8_ExtraRowTransform: |
| srcFmt = skcms_PixelFormat_RGBA_8888; |
| dstFmt = skcms_PixelFormat_RGB_888; |
| dstRowBytes = srcRowBytes - (srcRowBytes / 4); |
| break; |
| case kRgba16leToRgba16be_ExtraRowTransform: |
| srcFmt = skcms_PixelFormat_RGBA_16161616LE; |
| dstFmt = skcms_PixelFormat_RGBA_16161616BE; |
| dstRowBytes = srcRowBytes; |
| break; |
| case kRgba16leToRgb16be_ExtraRowTransform: |
| srcFmt = skcms_PixelFormat_RGBA_16161616LE; |
| dstFmt = skcms_PixelFormat_RGB_161616BE; |
| dstRowBytes = srcRowBytes - (srcRowBytes / 4); |
| break; |
| default: |
| SkUNREACHABLE; |
| } |
| |
| fExtraRowBuffer.resize(dstRowBytes, 0x00); |
| SkSafeMath safe; |
| size_t width = safe.castTo<size_t>(this->targetInfo().fDstRowInfo->width()); |
| if (!safe.ok()) { |
| return false; |
| } |
| |
| bool success = skcms_Transform( |
| row.data(), srcFmt, skcms_AlphaFormat_Unpremul, nullptr, |
| fExtraRowBuffer.data(), dstFmt, skcms_AlphaFormat_Unpremul, nullptr, |
| width); |
| if (!success) { |
| return false; |
| } |
| |
| rustRow = rust::Slice<const uint8_t>(fExtraRowBuffer); |
| } else { |
| SkASSERT(this->fExtraRowTransform == kNone_ExtraRowTransform); |
| rustRow = rust::Slice<const uint8_t>(row); |
| } |
| return fStreamWriter->write(rustRow) == rust_png::EncodingResult::Success; |
| } |
| |
| bool SkPngRustEncoderImpl::onFinishEncoding() { |
| return rust_png::finish_encoding(std::move(fStreamWriter)) == rust_png::EncodingResult::Success; |
| } |