| /* | |
| * Copyright 2006 The Android Open Source Project | |
| * | |
| * Use of this source code is governed by a BSD-style license that can be | |
| * found in the LICENSE file. | |
| */ | |
| #include "src/encode/SkPngEncoderImpl.h" | |
| #include <optional> | |
| #include "include/core/SkBitmap.h" | |
| #include "include/core/SkColorSpace.h" | |
| #include "include/core/SkColorType.h" | |
| #include "include/core/SkData.h" | |
| #include "include/core/SkDataTable.h" | |
| #include "include/core/SkImageInfo.h" | |
| #include "include/core/SkPixmap.h" | |
| #include "include/core/SkRefCnt.h" | |
| #include "include/core/SkSpan.h" | |
| #include "include/core/SkStream.h" | |
| #include "include/core/SkString.h" | |
| #include "include/encode/SkEncoder.h" | |
| #include "include/encode/SkPngEncoder.h" | |
| #include "include/private/SkAssert.h" | |
| #include "include/private/SkDebug.h" | |
| #include "include/private/SkEncodedInfo.h" | |
| #include "include/private/SkGainmapInfo.h" | |
| #include "include/private/SkNoncopyable.h" | |
| #include "modules/skcms/skcms.h" | |
| #include "src/codec/SkPngPriv.h" | |
| #include "src/encode/SkImageEncoderFns.h" | |
| #include "src/encode/SkImageEncoderPriv.h" | |
| #include "src/encode/SkPngEncoderBase.h" | |
| #include "src/image/SkImage_Base.h" | |
| #include <algorithm> | |
| #include <array> | |
| #include <setjmp.h> | |
| #include <cstdint> | |
| #include <cstring> | |
| #include <memory> | |
| #include <utility> | |
| #include <vector> | |
| #include <png.h> | |
| #include <pngconf.h> | |
| class GrDirectContext; | |
| class SkImage; | |
| static_assert(PNG_FILTER_NONE == (int)SkPngEncoder::FilterFlag::kNone, "Skia libpng filter err."); | |
| static_assert(PNG_FILTER_SUB == (int)SkPngEncoder::FilterFlag::kSub, "Skia libpng filter err."); | |
| static_assert(PNG_FILTER_UP == (int)SkPngEncoder::FilterFlag::kUp, "Skia libpng filter err."); | |
| static_assert(PNG_FILTER_AVG == (int)SkPngEncoder::FilterFlag::kAvg, "Skia libpng filter err."); | |
| static_assert(PNG_FILTER_PAETH == (int)SkPngEncoder::FilterFlag::kPaeth, "Skia libpng filter err."); | |
| static_assert(PNG_ALL_FILTERS == (int)SkPngEncoder::FilterFlag::kAll, "Skia libpng filter err."); | |
| static constexpr bool kSuppressPngEncodeWarnings = false; | |
| static void sk_error_fn(png_structp png_ptr, png_const_charp msg) { | |
| if (!kSuppressPngEncodeWarnings) { | |
| SkDebugf("libpng encode error: %s\n", msg); | |
| } | |
| longjmp(png_jmpbuf(png_ptr), 1); | |
| } | |
| static void sk_write_fn(png_structp png_ptr, png_bytep data, png_size_t len) { | |
| SkWStream* stream = (SkWStream*)png_get_io_ptr(png_ptr); | |
| if (!stream->write(data, len)) { | |
| png_error(png_ptr, "sk_write_fn cannot write to stream"); | |
| } | |
| } | |
| class SkPngEncoderMgr final : SkNoncopyable { | |
| public: | |
| /* | |
| * Create the decode manager | |
| * Does not take ownership of stream | |
| */ | |
| static std::unique_ptr<SkPngEncoderMgr> Make(SkWStream* stream); | |
| bool setHeader(const SkPngEncoderBase::TargetInfo& targetInfo, | |
| const SkImageInfo& srcInfo, | |
| const SkPngEncoder::Options& options); | |
| bool setColorSpace(const SkImageInfo& info, const SkPngEncoder::Options& options); | |
| bool setHdrMetadata(const SkPngEncoder::Options& options); | |
| bool writeInfo(const SkImageInfo& srcInfo,const SkPngEncoderBase::TargetInfo& targetInfo); | |
| png_structp pngPtr() { return fPngPtr; } | |
| png_infop infoPtr() { return fInfoPtr; } | |
| transform_scanline_proc proc() const { return fProc; } | |
| ~SkPngEncoderMgr() { png_destroy_write_struct(&fPngPtr, &fInfoPtr); } | |
| private: | |
| SkPngEncoderMgr(png_structp pngPtr, png_infop infoPtr) : fPngPtr(pngPtr), fInfoPtr(infoPtr) {} | |
| png_structp fPngPtr; | |
| png_infop fInfoPtr; | |
| transform_scanline_proc fProc = nullptr; | |
| }; | |
| std::unique_ptr<SkPngEncoderMgr> SkPngEncoderMgr::Make(SkWStream* stream) { | |
| png_structp pngPtr = | |
| png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, sk_error_fn, nullptr); | |
| if (!pngPtr) { | |
| return nullptr; | |
| } | |
| png_infop infoPtr = png_create_info_struct(pngPtr); | |
| if (!infoPtr) { | |
| png_destroy_write_struct(&pngPtr, nullptr); | |
| return nullptr; | |
| } | |
| png_set_write_fn(pngPtr, (void*)stream, sk_write_fn, nullptr); | |
| return std::unique_ptr<SkPngEncoderMgr>(new SkPngEncoderMgr(pngPtr, infoPtr)); | |
| } | |
| bool SkPngEncoderMgr::setHeader(const SkPngEncoderBase::TargetInfo& targetInfo, | |
| const SkImageInfo& srcInfo, | |
| const SkPngEncoder::Options& options) { | |
| if (setjmp(png_jmpbuf(fPngPtr))) { | |
| return false; | |
| } | |
| const SkEncodedInfo& dstInfo = targetInfo.fDstInfo; | |
| const std::optional<SkImageInfo>& dstRowInfo = targetInfo.fDstRowInfo; | |
| int pngColorType; | |
| switch (dstInfo.color()) { | |
| case SkEncodedInfo::kRGB_Color: | |
| pngColorType = PNG_COLOR_TYPE_RGB; | |
| break; | |
| case SkEncodedInfo::kRGBA_Color: | |
| SkASSERT(dstRowInfo); | |
| pngColorType = dstRowInfo->isOpaque() ? PNG_COLOR_TYPE_RGB | |
| : PNG_COLOR_TYPE_RGB_ALPHA; | |
| break; | |
| case SkEncodedInfo::kGray_Color: | |
| pngColorType = PNG_COLOR_TYPE_GRAY; | |
| break; | |
| case SkEncodedInfo::kGrayAlpha_Color: | |
| pngColorType = PNG_COLOR_TYPE_GRAY_ALPHA; | |
| break; | |
| default: | |
| SkDEBUGFAIL("`getTargetInfo` returned unexpected `SkEncodedInfo::Color`"); | |
| return false; | |
| } | |
| png_color_8 sigBit; | |
| bool sigBitSet = true; | |
| switch (srcInfo.colorType()) { | |
| case kRGBA_F16Norm_SkColorType: | |
| case kRGBA_F16_SkColorType: | |
| case kRGBA_F32_SkColorType: | |
| sigBit.red = 16; | |
| sigBit.green = 16; | |
| sigBit.blue = 16; | |
| sigBit.alpha = 16; | |
| break; | |
| case kRGB_F16F16F16x_SkColorType: | |
| sigBit.red = 16; | |
| sigBit.green = 16; | |
| sigBit.blue = 16; | |
| break; | |
| case kGray_8_SkColorType: | |
| sigBit.gray = 8; | |
| break; | |
| case kRGB_888x_SkColorType: | |
| sigBit.red = 8; | |
| sigBit.green = 8; | |
| sigBit.blue = 8; | |
| break; | |
| case kARGB_4444_SkColorType: | |
| sigBit.red = 4; | |
| sigBit.green = 4; | |
| sigBit.blue = 4; | |
| sigBit.alpha = 4; | |
| break; | |
| case kRGB_565_SkColorType: | |
| sigBit.red = 5; | |
| sigBit.green = 6; | |
| sigBit.blue = 5; | |
| break; | |
| case kAlpha_8_SkColorType: // store as gray+alpha, but ignore gray | |
| sigBit.gray = kGraySigBit_GrayAlphaIsJustAlpha; | |
| sigBit.alpha = 8; | |
| break; | |
| case kRGBA_1010102_SkColorType: | |
| case kBGRA_1010102_SkColorType: | |
| sigBit.red = 10; | |
| sigBit.green = 10; | |
| sigBit.blue = 10; | |
| sigBit.alpha = 2; | |
| break; | |
| case kBGR_101010x_XR_SkColorType: | |
| case kRGB_101010x_SkColorType: | |
| case kBGR_101010x_SkColorType: | |
| sigBit.red = 10; | |
| sigBit.green = 10; | |
| sigBit.blue = 10; | |
| break; | |
| case kBGRA_10101010_XR_SkColorType: | |
| sigBit.red = 10; | |
| sigBit.green = 10; | |
| sigBit.blue = 10; | |
| sigBit.alpha = 10; | |
| break; | |
| case kRGBA_8888_SkColorType: | |
| case kBGRA_8888_SkColorType: | |
| sigBit.red = 8; | |
| sigBit.green = 8; | |
| sigBit.blue = 8; | |
| sigBit.alpha = 8; | |
| break; | |
| default: | |
| SkDEBUGFAIL("Unable to set sigBit for src colortype, unhandled value\n"); | |
| sigBitSet = false; | |
| } | |
| png_set_IHDR(fPngPtr, | |
| fInfoPtr, | |
| srcInfo.width(), | |
| srcInfo.height(), | |
| dstInfo.bitsPerComponent(), | |
| pngColorType, | |
| PNG_INTERLACE_NONE, | |
| PNG_COMPRESSION_TYPE_BASE, | |
| PNG_FILTER_TYPE_BASE); | |
| if (sigBitSet) { | |
| png_set_sBIT(fPngPtr, fInfoPtr, &sigBit); | |
| } | |
| int filters = (int)options.fFilterFlags & (int)SkPngEncoder::FilterFlag::kAll; | |
| SkASSERT(filters == (int)options.fFilterFlags); | |
| png_set_filter(fPngPtr, PNG_FILTER_TYPE_BASE, filters); | |
| int zlibLevel = std::min(std::max(0, options.fZLibLevel), 9); | |
| SkASSERT(zlibLevel == options.fZLibLevel); | |
| png_set_compression_level(fPngPtr, zlibLevel); | |
| // Set comments in tEXt chunk | |
| const sk_sp<SkDataTable>& comments = options.fComments; | |
| if (comments != nullptr) { | |
| if (comments->count() % 2 != 0) { | |
| return false; | |
| } | |
| std::vector<png_text> png_texts(comments->count()); | |
| std::vector<SkString> clippedKeys; | |
| for (int i = 0; i < comments->count() / 2; ++i) { | |
| const char* keyword; | |
| const char* originalKeyword = comments->atStr(2 * i); | |
| const char* text = comments->atStr(2 * i + 1); | |
| if (strlen(originalKeyword) <= PNG_KEYWORD_MAX_LENGTH) { | |
| keyword = originalKeyword; | |
| } else { | |
| SkDEBUGFAILF("PNG tEXt keyword should be no longer than %d.", | |
| PNG_KEYWORD_MAX_LENGTH); | |
| clippedKeys.emplace_back(originalKeyword, PNG_KEYWORD_MAX_LENGTH); | |
| keyword = clippedKeys.back().c_str(); | |
| } | |
| // It seems safe to convert png_const_charp to png_charp for key/text, | |
| // and we don't have to provide text_length and other fields as we're providing | |
| // 0-terminated c_str with PNG_TEXT_COMPRESSION_NONE (no compression, no itxt). | |
| png_texts[i].compression = PNG_TEXT_COMPRESSION_NONE; | |
| png_texts[i].key = const_cast<png_charp>(keyword); | |
| png_texts[i].text = const_cast<png_charp>(text); | |
| } | |
| png_set_text(fPngPtr, fInfoPtr, png_texts.data(), png_texts.size()); | |
| } | |
| return true; | |
| } | |
| static void set_icc(png_structp png_ptr, png_infop info_ptr, const SkImageInfo& info) { | |
| sk_sp<SkData> icc = icc_from_color_space(info); | |
| if (!icc) { | |
| return; | |
| } | |
| #if PNG_LIBPNG_VER_MAJOR > 1 || (PNG_LIBPNG_VER_MAJOR == 1 && PNG_LIBPNG_VER_MINOR >= 5) | |
| const char* name = "Skia"; | |
| png_const_bytep iccPtr = icc->bytes(); | |
| #else | |
| SkString str("Skia"); | |
| char* name = str.data(); | |
| png_charp iccPtr = (png_charp)icc->writable_data(); | |
| #endif | |
| png_set_iCCP(png_ptr, info_ptr, name, 0, iccPtr, icc->size()); | |
| } | |
| bool SkPngEncoderMgr::setColorSpace(const SkImageInfo& info, const SkPngEncoder::Options& options) { | |
| if (setjmp(png_jmpbuf(fPngPtr))) { | |
| return false; | |
| } | |
| if (info.colorSpace() && info.colorSpace()->isSRGB()) { | |
| png_set_sRGB(fPngPtr, fInfoPtr, PNG_sRGB_INTENT_PERCEPTUAL); | |
| } else { | |
| set_icc(fPngPtr, fInfoPtr, info); | |
| } | |
| return true; | |
| } | |
| bool SkPngEncoderMgr::setHdrMetadata(const SkPngEncoder::Options& options) { | |
| #ifdef PNG_STORE_UNKNOWN_CHUNKS_SUPPORTED | |
| if (setjmp(png_jmpbuf(fPngPtr))) { | |
| return false; | |
| } | |
| // List all chunks that might be included. | |
| const char* hdrChunkNames = "gmAP\0" | |
| "gdAT\0" | |
| "mDCV\0" | |
| "cLLI\0"; | |
| constexpr int numHdrChunkNames = 4; | |
| png_set_keep_unknown_chunks(fPngPtr, PNG_HANDLE_CHUNK_ALWAYS, | |
| (png_const_bytep)hdrChunkNames, numHdrChunkNames); | |
| // In the below `png_unknown_chunk` structures, the `data` member is a non-const pointer, | |
| // even though it will not be written to. In fact, `data` will be copied by the call to | |
| // `png_set_unknown_chunks`, so it is safe for it to be deallocated immediately after | |
| // the call. | |
| skhdr::ContentLightLevelInformation clli; | |
| if (options.fHdrMetadata.getContentLightLevelInformation(&clli)) { | |
| auto data = clli.serializePngChunk(); | |
| png_unknown_chunk chunk = { | |
| {'c', 'L', 'L', 'I', 0}, | |
| reinterpret_cast<png_byte*>(data->writable_data()), | |
| data->size(), | |
| PNG_HAVE_IHDR, | |
| }; | |
| png_set_unknown_chunks(fPngPtr, fInfoPtr, &chunk, 1); | |
| } | |
| skhdr::MasteringDisplayColorVolume mdcv; | |
| if (options.fHdrMetadata.getMasteringDisplayColorVolume(&mdcv)) { | |
| auto data = mdcv.serialize(); | |
| png_unknown_chunk chunk = { | |
| {'m', 'D', 'C', 'V', 0}, | |
| reinterpret_cast<png_byte*>(data->writable_data()), | |
| data->size(), | |
| PNG_HAVE_IHDR, | |
| }; | |
| png_set_unknown_chunks(fPngPtr, fInfoPtr, &chunk, 1); | |
| } | |
| if (options.fGainmapInfo && options.fGainmap) { | |
| sk_sp<SkData> gainmapVersion = SkGainmapInfo::SerializeVersion(); | |
| // When we encode the gainmap, we need to remove the gainmap from its | |
| // own encoding options, so that we don't recurse. | |
| auto modifiedOptions = options; | |
| modifiedOptions.fGainmap = nullptr; | |
| auto gainmapInfo = *(options.fGainmapInfo); | |
| auto gainmapPixels = *(options.fGainmap); | |
| auto targetInfo = SkPngEncoderBase::getTargetInfo(gainmapPixels.info()); | |
| if (targetInfo && targetInfo->fDstInfo.color() != SkEncodedInfo::kGray_Color && | |
| targetInfo->fDstInfo.color() != SkEncodedInfo::kGrayAlpha_Color) { | |
| // Encode the alternate image colorspace directly in the gainmap profile, | |
| // since the ISO gainmap payload does not contain the actual alternative | |
| // image primaries. | |
| const auto& gainmapColorSpace = options.fGainmapInfo->fGainmapMathColorSpace; | |
| gainmapPixels.setColorSpace(gainmapColorSpace); | |
| } else { | |
| // Scrub the gainmap colorspace, since grayscale PNGs don't support | |
| // RGB ICC profiles | |
| gainmapInfo.fGainmapMathColorSpace = nullptr; | |
| modifiedOptions.fGainmapInfo = &gainmapInfo; | |
| } | |
| sk_sp<SkData> gainmapData = SkPngEncoder::Encode(gainmapPixels, modifiedOptions); | |
| if (!gainmapData) { | |
| return false; | |
| } | |
| // The base image contains chunks for both the gainmap versioning (for possible | |
| // forward-compat, and as a cheap way to check a gainmap might exist) as | |
| // well as the gainmap data. | |
| png_unknown_chunk gmapChunk = { | |
| {'g', 'm', 'A', 'P', 0}, | |
| reinterpret_cast<png_byte*>(gainmapVersion->writable_data()), | |
| gainmapVersion->size(), | |
| PNG_HAVE_IHDR, | |
| }; | |
| png_set_unknown_chunks(fPngPtr, fInfoPtr, &gmapChunk, 1); | |
| png_unknown_chunk gdatChunk = { | |
| {'g', 'd', 'A', 'T', 0}, | |
| reinterpret_cast<png_byte*>(gainmapData->writable_data()), | |
| gainmapData->size(), | |
| PNG_HAVE_IHDR, | |
| }; | |
| png_set_unknown_chunks(fPngPtr, fInfoPtr, &gdatChunk, 1); | |
| } else if (options.fGainmapInfo) { | |
| // If there is no gainmap provided for encoding, but we have info, then | |
| // we're currently encoding the gainmap pixels, so we need to encode the | |
| // gainmap metadata to interpret those pixels. | |
| sk_sp<SkData> data = options.fGainmapInfo->serialize(); | |
| png_unknown_chunk chunk = { | |
| {'g', 'm', 'A', 'P', 0}, | |
| reinterpret_cast<png_byte*>(data->writable_data()), | |
| data->size(), | |
| PNG_HAVE_IHDR, | |
| }; | |
| png_set_unknown_chunks(fPngPtr, fInfoPtr, &chunk, 1); | |
| } | |
| #endif | |
| return true; | |
| } | |
| bool SkPngEncoderMgr::writeInfo(const SkImageInfo& srcInfo, const SkPngEncoderBase::TargetInfo& targetInfo) { | |
| if (setjmp(png_jmpbuf(fPngPtr))) { | |
| return false; | |
| } | |
| png_write_info(fPngPtr, fInfoPtr); | |
| const SkEncodedInfo& dstInfo = targetInfo.fDstInfo; | |
| const std::optional<SkImageInfo>& dstRowInfo = targetInfo.fDstRowInfo; | |
| // Strip input data that has 4 or 8 bytes per pixel down to 3 or 6 bytes if we don't want alpha. | |
| if (dstInfo.color() == SkEncodedInfo::kRGBA_Color) { | |
| SkASSERT(dstRowInfo); | |
| if (dstRowInfo->isOpaque()) { | |
| png_set_filler(fPngPtr, 0, PNG_FILLER_AFTER); | |
| } | |
| } | |
| return true; | |
| } | |
| SkPngEncoderImpl::SkPngEncoderImpl(TargetInfo targetInfo, | |
| std::unique_ptr<SkPngEncoderMgr> encoderMgr, | |
| const SkPixmap& src) | |
| : SkPngEncoderBase(std::move(targetInfo), src), fEncoderMgr(std::move(encoderMgr)) {} | |
| SkPngEncoderImpl::~SkPngEncoderImpl() {} | |
| bool SkPngEncoderImpl::onEncodeRow(SkSpan<const uint8_t> row) { | |
| if (setjmp(png_jmpbuf(fEncoderMgr->pngPtr()))) { | |
| return false; | |
| } | |
| // `png_bytep` is `uint8_t*` rather than `const uint8_t*`. | |
| png_bytep rowPtr = const_cast<png_bytep>(row.data()); | |
| // Swap to big endian if we are storing more than a byte per color channel | |
| // (SkColorTypes are little endian by default). | |
| // By this point our data will either be 8888 or 16161616, so we only check that case. | |
| if (png_get_bit_depth(fEncoderMgr->pngPtr(), fEncoderMgr->infoPtr()) == 16) { | |
| png_set_swap(fEncoderMgr->pngPtr()); | |
| } | |
| png_write_rows(fEncoderMgr->pngPtr(), &rowPtr, 1); | |
| return true; | |
| } | |
| bool SkPngEncoderImpl::onFinishEncoding() { | |
| if (setjmp(png_jmpbuf(fEncoderMgr->pngPtr()))) { | |
| return false; | |
| } | |
| png_write_end(fEncoderMgr->pngPtr(), fEncoderMgr->infoPtr()); | |
| return true; | |
| } | |
| namespace SkPngEncoder { | |
| std::unique_ptr<SkEncoder> Make(SkWStream* dst, const SkPixmap& src, const Options& options) { | |
| if (!SkPixmapIsValid(src)) { | |
| return nullptr; | |
| } | |
| std::unique_ptr<SkPngEncoderMgr> encoderMgr = SkPngEncoderMgr::Make(dst); | |
| if (!encoderMgr) { | |
| return nullptr; | |
| } | |
| std::optional<SkPngEncoderBase::TargetInfo> targetInfo = | |
| SkPngEncoderBase::getTargetInfo(src.info()); | |
| if (!targetInfo.has_value()) { | |
| return nullptr; | |
| } | |
| if (!encoderMgr->setHeader(targetInfo.value(), src.info(), options)) { | |
| return nullptr; | |
| } | |
| if (!encoderMgr->setColorSpace(src.info(), options)) { | |
| return nullptr; | |
| } | |
| if (!encoderMgr->setHdrMetadata(options)) { | |
| return nullptr; | |
| } | |
| if (!encoderMgr->writeInfo(src.info(), targetInfo.value())) { | |
| return nullptr; | |
| } | |
| return std::make_unique<SkPngEncoderImpl>(std::move(*targetInfo), std::move(encoderMgr), src); | |
| } | |
| bool Encode(SkWStream* dst, const SkPixmap& src, const Options& options) { | |
| auto encoder = Make(dst, src, options); | |
| return encoder.get() && encoder->encodeRows(src.height()); | |
| } | |
| sk_sp<SkData> Encode(const SkPixmap& src, const Options& options) { | |
| SkDynamicMemoryWStream stream; | |
| return Encode(&stream, src, options) ? stream.detachAsData() : nullptr; | |
| } | |
| sk_sp<SkData> Encode(GrDirectContext* ctx, const SkImage* img, const Options& options) { | |
| if (!img) { | |
| return nullptr; | |
| } | |
| SkBitmap bm; | |
| if (!as_IB(img)->getROPixels(ctx, &bm)) { | |
| return nullptr; | |
| } | |
| return Encode(bm.pixmap(), options); | |
| } | |
| } // namespace SkPngEncoder |