blob: c93379a357b2442461d7060d5041b8c4cfb993df [file] [log] [blame]
/*
* 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/SkEncodedInfo.h"
#include "include/private/SkGainmapInfo.h"
#include "include/private/base/SkAssert.h"
#include "include/private/base/SkDebug.h"
#include "include/private/base/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 <csetjmp>
#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 setV0Gainmap(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,
const skcms_ICCProfile* profile,
const char* profile_description) {
sk_sp<SkData> icc = icc_from_color_space(info, profile, profile_description);
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, options.fICCProfile, options.fICCProfileDescription);
}
return true;
}
bool SkPngEncoderMgr::setV0Gainmap(const SkPngEncoder::Options& options) {
#ifdef PNG_STORE_UNKNOWN_CHUNKS_SUPPORTED
if (setjmp(png_jmpbuf(fPngPtr))) {
return false;
}
// We require some gainmap information.
if (!options.fGainmapInfo) {
return false;
}
if (options.fGainmap) {
sk_sp<SkData> gainmapVersion = SkGainmapInfo::SerializeVersion();
SkDynamicMemoryWStream gainmapStream;
// 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;
}
bool result = SkPngEncoder::Encode(&gainmapStream, gainmapPixels, modifiedOptions);
if (!result) {
return false;
}
sk_sp<SkData> gainmapData = gainmapStream.detachAsData();
// 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.
std::array<png_unknown_chunk, 2> chunks;
auto& gmapChunk = chunks.at(0);
std::strcpy(reinterpret_cast<char*>(gmapChunk.name), "gmAP\0");
gmapChunk.data = reinterpret_cast<png_byte*>(gainmapVersion->writable_data());
gmapChunk.size = gainmapVersion->size();
gmapChunk.location = PNG_HAVE_IHDR;
auto& gdatChunk = chunks.at(1);
std::strcpy(reinterpret_cast<char*>(gdatChunk.name), "gdAT\0");
gdatChunk.data = reinterpret_cast<png_byte*>(gainmapData->writable_data());
gdatChunk.size = gainmapData->size();
gdatChunk.location = PNG_HAVE_IHDR;
png_set_keep_unknown_chunks(fPngPtr, PNG_HANDLE_CHUNK_ALWAYS,
(png_const_bytep)"gmAP\0gdAT\0", chunks.size());
png_set_unknown_chunks(fPngPtr, fInfoPtr, chunks.data(), chunks.size());
} else {
// 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;
std::strcpy(reinterpret_cast<char*>(chunk.name), "gmAP\0");
chunk.data = reinterpret_cast<png_byte*>(data->writable_data());
chunk.size = data->size();
chunk.location = PNG_HAVE_IHDR;
png_set_keep_unknown_chunks(fPngPtr, PNG_HANDLE_CHUNK_ALWAYS,
(png_const_bytep)"gmAP\0", 1);
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 (options.fGainmapInfo && !encoderMgr->setV0Gainmap(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(GrDirectContext* ctx, const SkImage* img, const Options& options) {
if (!img) {
return nullptr;
}
SkBitmap bm;
if (!as_IB(img)->getROPixels(ctx, &bm)) {
return nullptr;
}
SkDynamicMemoryWStream stream;
if (Encode(&stream, bm.pixmap(), options)) {
return stream.detachAsData();
}
return nullptr;
}
} // namespace SkPngEncoder