1adcac52d6
Bug: skia:10178 These functions can be performed generically using SkRasterPipeline or skcms. Further, the reason we used a function pointer anyway was so that we could call the same function on each row separately. But libwebp's API doesn't let us do a single row at a time anyway. Simplify this method by using readPixels when necessary and skipping conversion entirely when possible. Add support for encoding from unpremul 4444. It is simpler to support it, and it's not obvious why we didn't support it before. Keep the behavior of not supporting A8, and apply the same to the other alpha-only formats. Note that we could support encoding such an image to alpha, r=0, g=0, b=0, but I'd rather leave adding that feature to a separate change, which enables it for all encoders (and accounts for the internal use of PNGs as a round-trip for kAlpha_8_SkColorType). Add GMs to test the newly supported SkColorTypes. Change-Id: I4d86c5621792fb6dc3cb68b736a1eb35d577e3a6 Reviewed-on: https://skia-review.googlesource.com/c/skia/+/292962 Commit-Queue: Leon Scroggins <scroggo@google.com> Reviewed-by: Mike Klein <mtklein@google.com>
458 lines
15 KiB
C++
458 lines
15 KiB
C++
/*
|
|
* Copyright 2017 Google Inc.
|
|
*
|
|
* Use of this source code is governed by a BSD-style license that can be
|
|
* found in the LICENSE file.
|
|
*/
|
|
|
|
#include "tests/Test.h"
|
|
#include "tools/Resources.h"
|
|
|
|
#include "include/core/SkBitmap.h"
|
|
#include "include/core/SkCanvas.h"
|
|
#include "include/core/SkColorPriv.h"
|
|
#include "include/core/SkEncodedImageFormat.h"
|
|
#include "include/core/SkImage.h"
|
|
#include "include/core/SkStream.h"
|
|
#include "include/core/SkSurface.h"
|
|
#include "include/encode/SkJpegEncoder.h"
|
|
#include "include/encode/SkPngEncoder.h"
|
|
#include "include/encode/SkWebpEncoder.h"
|
|
#include "include/private/SkImageInfoPriv.h"
|
|
|
|
#include "png.h"
|
|
|
|
#include <algorithm>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
// FIXME: Update the Google3 build's dependencies so it can run this test.
|
|
#ifndef SK_BUILD_FOR_GOOGLE3
|
|
#include "webp/decode.h"
|
|
#endif
|
|
|
|
static bool encode(SkEncodedImageFormat format, SkWStream* dst, const SkPixmap& src) {
|
|
switch (format) {
|
|
case SkEncodedImageFormat::kJPEG:
|
|
return SkJpegEncoder::Encode(dst, src, SkJpegEncoder::Options());
|
|
case SkEncodedImageFormat::kPNG:
|
|
return SkPngEncoder::Encode(dst, src, SkPngEncoder::Options());
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static std::unique_ptr<SkEncoder> make(SkEncodedImageFormat format, SkWStream* dst,
|
|
const SkPixmap& src) {
|
|
switch (format) {
|
|
case SkEncodedImageFormat::kJPEG:
|
|
return SkJpegEncoder::Make(dst, src, SkJpegEncoder::Options());
|
|
case SkEncodedImageFormat::kPNG:
|
|
return SkPngEncoder::Make(dst, src, SkPngEncoder::Options());
|
|
default:
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
static void test_encode(skiatest::Reporter* r, SkEncodedImageFormat format) {
|
|
SkBitmap bitmap;
|
|
bool success = GetResourceAsBitmap("images/mandrill_128.png", &bitmap);
|
|
if (!success) {
|
|
return;
|
|
}
|
|
|
|
SkPixmap src;
|
|
success = bitmap.peekPixels(&src);
|
|
REPORTER_ASSERT(r, success);
|
|
if (!success) {
|
|
return;
|
|
}
|
|
|
|
SkDynamicMemoryWStream dst0, dst1, dst2, dst3;
|
|
success = encode(format, &dst0, src);
|
|
REPORTER_ASSERT(r, success);
|
|
|
|
auto encoder1 = make(format, &dst1, src);
|
|
for (int i = 0; i < src.height(); i++) {
|
|
success = encoder1->encodeRows(1);
|
|
REPORTER_ASSERT(r, success);
|
|
}
|
|
|
|
auto encoder2 = make(format, &dst2, src);
|
|
for (int i = 0; i < src.height(); i+=3) {
|
|
success = encoder2->encodeRows(3);
|
|
REPORTER_ASSERT(r, success);
|
|
}
|
|
|
|
auto encoder3 = make(format, &dst3, src);
|
|
success = encoder3->encodeRows(200);
|
|
REPORTER_ASSERT(r, success);
|
|
|
|
sk_sp<SkData> data0 = dst0.detachAsData();
|
|
sk_sp<SkData> data1 = dst1.detachAsData();
|
|
sk_sp<SkData> data2 = dst2.detachAsData();
|
|
sk_sp<SkData> data3 = dst3.detachAsData();
|
|
REPORTER_ASSERT(r, data0->equals(data1.get()));
|
|
REPORTER_ASSERT(r, data0->equals(data2.get()));
|
|
REPORTER_ASSERT(r, data0->equals(data3.get()));
|
|
}
|
|
|
|
DEF_TEST(Encode, r) {
|
|
test_encode(r, SkEncodedImageFormat::kJPEG);
|
|
test_encode(r, SkEncodedImageFormat::kPNG);
|
|
}
|
|
|
|
static inline bool almost_equals(SkPMColor a, SkPMColor b, int tolerance) {
|
|
if (SkTAbs((int)SkGetPackedR32(a) - (int)SkGetPackedR32(b)) > tolerance) {
|
|
return false;
|
|
}
|
|
|
|
if (SkTAbs((int)SkGetPackedG32(a) - (int)SkGetPackedG32(b)) > tolerance) {
|
|
return false;
|
|
}
|
|
|
|
if (SkTAbs((int)SkGetPackedB32(a) - (int)SkGetPackedB32(b)) > tolerance) {
|
|
return false;
|
|
}
|
|
|
|
if (SkTAbs((int)SkGetPackedA32(a) - (int)SkGetPackedA32(b)) > tolerance) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static inline bool almost_equals(const SkBitmap& a, const SkBitmap& b, int tolerance) {
|
|
if (a.info() != b.info()) {
|
|
return false;
|
|
}
|
|
|
|
SkASSERT(kN32_SkColorType == a.colorType());
|
|
for (int y = 0; y < a.height(); y++) {
|
|
for (int x = 0; x < a.width(); x++) {
|
|
if (!almost_equals(*a.getAddr32(x, y), *b.getAddr32(x, y), tolerance)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
DEF_TEST(Encode_JPG, r) {
|
|
auto image = GetResourceAsImage("images/mandrill_128.png");
|
|
if (!image) {
|
|
return;
|
|
}
|
|
|
|
for (auto ct : { kRGBA_8888_SkColorType,
|
|
kBGRA_8888_SkColorType,
|
|
kRGB_565_SkColorType,
|
|
kARGB_4444_SkColorType,
|
|
kGray_8_SkColorType,
|
|
kRGBA_F16_SkColorType }) {
|
|
for (auto at : { kPremul_SkAlphaType, kUnpremul_SkAlphaType, kOpaque_SkAlphaType }) {
|
|
auto info = SkImageInfo::Make(image->width(), image->height(), ct, at);
|
|
auto surface = SkSurface::MakeRaster(info);
|
|
auto canvas = surface->getCanvas();
|
|
canvas->drawImage(image, 0, 0);
|
|
|
|
SkBitmap bm;
|
|
bm.allocPixels(info);
|
|
if (!surface->makeImageSnapshot()->readPixels(bm.pixmap(), 0, 0)) {
|
|
ERRORF(r, "failed to readPixels! ct: %i\tat: %i\n", ct, at);
|
|
continue;
|
|
}
|
|
for (auto alphaOption : { SkJpegEncoder::AlphaOption::kIgnore,
|
|
SkJpegEncoder::AlphaOption::kBlendOnBlack }) {
|
|
SkJpegEncoder::Options opts;
|
|
opts.fAlphaOption = alphaOption;
|
|
SkNullWStream dummy;
|
|
if (!SkJpegEncoder::Encode(&dummy, bm.pixmap(), opts)) {
|
|
REPORTER_ASSERT(r, ct == kARGB_4444_SkColorType
|
|
&& alphaOption == SkJpegEncoder::AlphaOption::kBlendOnBlack);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
DEF_TEST(Encode_JpegDownsample, r) {
|
|
SkBitmap bitmap;
|
|
bool success = GetResourceAsBitmap("images/mandrill_128.png", &bitmap);
|
|
if (!success) {
|
|
return;
|
|
}
|
|
|
|
SkPixmap src;
|
|
success = bitmap.peekPixels(&src);
|
|
REPORTER_ASSERT(r, success);
|
|
if (!success) {
|
|
return;
|
|
}
|
|
|
|
SkDynamicMemoryWStream dst0, dst1, dst2;
|
|
SkJpegEncoder::Options options;
|
|
success = SkJpegEncoder::Encode(&dst0, src, options);
|
|
REPORTER_ASSERT(r, success);
|
|
|
|
options.fDownsample = SkJpegEncoder::Downsample::k422;
|
|
success = SkJpegEncoder::Encode(&dst1, src, options);
|
|
REPORTER_ASSERT(r, success);
|
|
|
|
options.fDownsample = SkJpegEncoder::Downsample::k444;
|
|
success = SkJpegEncoder::Encode(&dst2, src, options);
|
|
REPORTER_ASSERT(r, success);
|
|
|
|
sk_sp<SkData> data0 = dst0.detachAsData();
|
|
sk_sp<SkData> data1 = dst1.detachAsData();
|
|
sk_sp<SkData> data2 = dst2.detachAsData();
|
|
REPORTER_ASSERT(r, data0->size() < data1->size());
|
|
REPORTER_ASSERT(r, data1->size() < data2->size());
|
|
|
|
SkBitmap bm0, bm1, bm2;
|
|
SkImage::MakeFromEncoded(data0)->asLegacyBitmap(&bm0);
|
|
SkImage::MakeFromEncoded(data1)->asLegacyBitmap(&bm1);
|
|
SkImage::MakeFromEncoded(data2)->asLegacyBitmap(&bm2);
|
|
REPORTER_ASSERT(r, almost_equals(bm0, bm1, 60));
|
|
REPORTER_ASSERT(r, almost_equals(bm1, bm2, 60));
|
|
}
|
|
|
|
static inline void pushComment(
|
|
std::vector<std::string>& comments, const char* keyword, const char* text) {
|
|
comments.push_back(keyword);
|
|
comments.push_back(text);
|
|
}
|
|
|
|
static void testPngComments(const SkPixmap& src, SkPngEncoder::Options& options,
|
|
skiatest::Reporter* r) {
|
|
std::vector<std::string> commentStrings;
|
|
pushComment(commentStrings, "key", "text");
|
|
pushComment(commentStrings, "test", "something");
|
|
pushComment(commentStrings, "have some", "spaces in both");
|
|
|
|
std::string longKey(PNG_KEYWORD_MAX_LENGTH, 'x');
|
|
#ifdef SK_DEBUG
|
|
commentStrings.push_back(longKey);
|
|
#else
|
|
// We call SkDEBUGFAILF it the key is too long so we'll only test this in release mode.
|
|
commentStrings.push_back(longKey + "x");
|
|
#endif
|
|
commentStrings.push_back("");
|
|
|
|
std::vector<const char*> commentPointers;
|
|
std::vector<size_t> commentSizes;
|
|
for(auto& str : commentStrings) {
|
|
commentPointers.push_back(str.c_str());
|
|
commentSizes.push_back(str.length() + 1);
|
|
}
|
|
|
|
options.fComments = SkDataTable::MakeCopyArrays((void const *const *)commentPointers.data(),
|
|
commentSizes.data(), commentStrings.size());
|
|
|
|
|
|
SkDynamicMemoryWStream dst;
|
|
bool success = SkPngEncoder::Encode(&dst, src, options);
|
|
REPORTER_ASSERT(r, success);
|
|
|
|
std::vector<char> output(dst.bytesWritten());
|
|
dst.copyTo(output.data());
|
|
|
|
// Each chunk is of the form length (4 bytes), chunk type (tEXt), data,
|
|
// checksum (4 bytes). Make sure we find all of them in the encoded
|
|
// results.
|
|
const char kExpected1[] =
|
|
"\x00\x00\x00\x08tEXtkey\x00text\x9e\xe7\x66\x51";
|
|
const char kExpected2[] =
|
|
"\x00\x00\x00\x0etEXttest\x00something\x29\xba\xef\xac";
|
|
const char kExpected3[] =
|
|
"\x00\x00\x00\x18tEXthave some\x00spaces in both\x8d\x69\x34\x2d";
|
|
std::string longKeyRecord = "tEXt" + longKey; // A snippet of our long key comment
|
|
std::string tooLongRecord = "tExt" + longKey + "x"; // A snippet whose key is too long
|
|
|
|
auto search1 = std::search(output.begin(), output.end(),
|
|
kExpected1, kExpected1 + sizeof(kExpected1));
|
|
auto search2 = std::search(output.begin(), output.end(),
|
|
kExpected2, kExpected2 + sizeof(kExpected2));
|
|
auto search3 = std::search(output.begin(), output.end(),
|
|
kExpected3, kExpected3 + sizeof(kExpected3));
|
|
auto search4 = std::search(output.begin(), output.end(),
|
|
longKeyRecord.begin(), longKeyRecord.end());
|
|
auto search5 = std::search(output.begin(), output.end(),
|
|
tooLongRecord.begin(), tooLongRecord.end());
|
|
|
|
REPORTER_ASSERT(r, search1 != output.end());
|
|
REPORTER_ASSERT(r, search2 != output.end());
|
|
REPORTER_ASSERT(r, search3 != output.end());
|
|
REPORTER_ASSERT(r, search4 != output.end());
|
|
REPORTER_ASSERT(r, search5 == output.end());
|
|
// Comments test ends
|
|
}
|
|
|
|
DEF_TEST(Encode_PngOptions, r) {
|
|
SkBitmap bitmap;
|
|
bool success = GetResourceAsBitmap("images/mandrill_128.png", &bitmap);
|
|
if (!success) {
|
|
return;
|
|
}
|
|
|
|
SkPixmap src;
|
|
success = bitmap.peekPixels(&src);
|
|
REPORTER_ASSERT(r, success);
|
|
if (!success) {
|
|
return;
|
|
}
|
|
|
|
SkDynamicMemoryWStream dst0, dst1, dst2;
|
|
SkPngEncoder::Options options;
|
|
success = SkPngEncoder::Encode(&dst0, src, options);
|
|
REPORTER_ASSERT(r, success);
|
|
|
|
options.fFilterFlags = SkPngEncoder::FilterFlag::kUp;
|
|
success = SkPngEncoder::Encode(&dst1, src, options);
|
|
REPORTER_ASSERT(r, success);
|
|
|
|
options.fZLibLevel = 3;
|
|
success = SkPngEncoder::Encode(&dst2, src, options);
|
|
REPORTER_ASSERT(r, success);
|
|
|
|
testPngComments(src, options, r);
|
|
|
|
sk_sp<SkData> data0 = dst0.detachAsData();
|
|
sk_sp<SkData> data1 = dst1.detachAsData();
|
|
sk_sp<SkData> data2 = dst2.detachAsData();
|
|
REPORTER_ASSERT(r, data0->size() < data1->size());
|
|
REPORTER_ASSERT(r, data1->size() < data2->size());
|
|
|
|
SkBitmap bm0, bm1, bm2;
|
|
SkImage::MakeFromEncoded(data0)->asLegacyBitmap(&bm0);
|
|
SkImage::MakeFromEncoded(data1)->asLegacyBitmap(&bm1);
|
|
SkImage::MakeFromEncoded(data2)->asLegacyBitmap(&bm2);
|
|
REPORTER_ASSERT(r, almost_equals(bm0, bm1, 0));
|
|
REPORTER_ASSERT(r, almost_equals(bm0, bm2, 0));
|
|
}
|
|
|
|
#ifndef SK_BUILD_FOR_GOOGLE3
|
|
DEF_TEST(Encode_WebpQuality, r) {
|
|
SkBitmap bm;
|
|
bm.allocN32Pixels(100, 100);
|
|
bm.eraseColor(SK_ColorBLUE);
|
|
|
|
auto dataLossy = SkEncodeBitmap(bm, SkEncodedImageFormat::kWEBP, 99);
|
|
auto dataLossLess = SkEncodeBitmap(bm, SkEncodedImageFormat::kWEBP, 100);
|
|
|
|
enum Format {
|
|
kMixed = 0,
|
|
kLossy = 1,
|
|
kLossless = 2,
|
|
};
|
|
|
|
auto test = [&r](const sk_sp<SkData>& data, Format expected) {
|
|
auto printFormat = [](int f) {
|
|
switch (f) {
|
|
case kMixed: return "mixed";
|
|
case kLossy: return "lossy";
|
|
case kLossless: return "lossless";
|
|
default: return "unknown";
|
|
}
|
|
};
|
|
|
|
if (!data) {
|
|
ERRORF(r, "Failed to encode. Expected %s", printFormat(expected));
|
|
return;
|
|
}
|
|
|
|
WebPBitstreamFeatures features;
|
|
auto status = WebPGetFeatures(data->bytes(), data->size(), &features);
|
|
if (status != VP8_STATUS_OK) {
|
|
ERRORF(r, "Encode had an error %i. Expected %s", status, printFormat(expected));
|
|
return;
|
|
}
|
|
|
|
if (expected != features.format) {
|
|
ERRORF(r, "Expected %s encode, but got format %s", printFormat(expected),
|
|
printFormat(features.format));
|
|
}
|
|
};
|
|
|
|
test(dataLossy, kLossy);
|
|
test(dataLossLess, kLossless);
|
|
}
|
|
#endif
|
|
|
|
DEF_TEST(Encode_WebpOptions, r) {
|
|
SkBitmap bitmap;
|
|
bool success = GetResourceAsBitmap("images/google_chrome.ico", &bitmap);
|
|
if (!success) {
|
|
return;
|
|
}
|
|
|
|
SkPixmap src;
|
|
success = bitmap.peekPixels(&src);
|
|
REPORTER_ASSERT(r, success);
|
|
if (!success) {
|
|
return;
|
|
}
|
|
|
|
SkDynamicMemoryWStream dst0, dst1, dst2, dst3;
|
|
SkWebpEncoder::Options options;
|
|
options.fCompression = SkWebpEncoder::Compression::kLossless;
|
|
options.fQuality = 0.0f;
|
|
success = SkWebpEncoder::Encode(&dst0, src, options);
|
|
REPORTER_ASSERT(r, success);
|
|
|
|
options.fQuality = 100.0f;
|
|
success = SkWebpEncoder::Encode(&dst1, src, options);
|
|
REPORTER_ASSERT(r, success);
|
|
|
|
options.fCompression = SkWebpEncoder::Compression::kLossy;
|
|
options.fQuality = 100.0f;
|
|
success = SkWebpEncoder::Encode(&dst2, src, options);
|
|
REPORTER_ASSERT(r, success);
|
|
|
|
options.fCompression = SkWebpEncoder::Compression::kLossy;
|
|
options.fQuality = 50.0f;
|
|
success = SkWebpEncoder::Encode(&dst3, src, options);
|
|
REPORTER_ASSERT(r, success);
|
|
|
|
sk_sp<SkData> data0 = dst0.detachAsData();
|
|
sk_sp<SkData> data1 = dst1.detachAsData();
|
|
sk_sp<SkData> data2 = dst2.detachAsData();
|
|
sk_sp<SkData> data3 = dst3.detachAsData();
|
|
REPORTER_ASSERT(r, data0->size() > data1->size());
|
|
REPORTER_ASSERT(r, data1->size() > data2->size());
|
|
REPORTER_ASSERT(r, data2->size() > data3->size());
|
|
|
|
SkBitmap bm0, bm1, bm2, bm3;
|
|
SkImage::MakeFromEncoded(data0)->asLegacyBitmap(&bm0);
|
|
SkImage::MakeFromEncoded(data1)->asLegacyBitmap(&bm1);
|
|
SkImage::MakeFromEncoded(data2)->asLegacyBitmap(&bm2);
|
|
SkImage::MakeFromEncoded(data3)->asLegacyBitmap(&bm3);
|
|
REPORTER_ASSERT(r, almost_equals(bm0, bm1, 0));
|
|
REPORTER_ASSERT(r, almost_equals(bm0, bm2, 90));
|
|
REPORTER_ASSERT(r, almost_equals(bm2, bm3, 50));
|
|
}
|
|
|
|
DEF_TEST(Encode_Alpha, r) {
|
|
// These formats have no sensible way to encode alpha images.
|
|
for (auto format : { SkEncodedImageFormat::kJPEG,
|
|
SkEncodedImageFormat::kPNG,
|
|
SkEncodedImageFormat::kWEBP }) {
|
|
for (int ctAsInt = kUnknown_SkColorType + 1; ctAsInt <= kLastEnum_SkColorType; ctAsInt++) {
|
|
auto ct = static_cast<SkColorType>(ctAsInt);
|
|
// Non-alpha-only colortypes are tested elsewhere.
|
|
if (!SkColorTypeIsAlphaOnly(ct)) continue;
|
|
SkBitmap bm;
|
|
bm.allocPixels(SkImageInfo::Make(10, 10, ct, kPremul_SkAlphaType));
|
|
sk_bzero(bm.getPixels(), bm.computeByteSize());
|
|
auto data = SkEncodeBitmap(bm, format, 100);
|
|
if (format == SkEncodedImageFormat::kPNG && ct == kAlpha_8_SkColorType) {
|
|
// We support encoding alpha8 to png with our own private meaning.
|
|
REPORTER_ASSERT(r, data != nullptr);
|
|
} else {
|
|
REPORTER_ASSERT(r, data == nullptr);
|
|
}
|
|
}
|
|
}
|
|
}
|