bcfc554fde
Note: The polarity of the staging flag is inverted from usual because a G3 dependency with no SkUserConfig.h relies on the legacy API. Once this lands, we will migrate them and others, then remove the staging API. The inverted staging flag is kind of nice, actually - I may use that pattern in the future. It means less total CLs and it's just as easy to flip the bit on or off during debugging. Bug: skia:104662 Change-Id: I48cba1eeae3e2e6f79918c6d243e0666e68ec71b Reviewed-on: https://skia-review.googlesource.com/c/skia/+/310656 Reviewed-by: Brian Salomon <bsalomon@google.com> Reviewed-by: Robert Phillips <robertphillips@google.com> Commit-Queue: Adlai Holler <adlai@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(nullptr, 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);
|
|
}
|
|
}
|
|
}
|
|
}
|