skia2/tests/CodecAnimTest.cpp
Leon Scroggins III 42ee284567 Use SkAndroidCodec in SkAnimatedImage
Bug: b/63909536
Bug: b/63908092

SkAnimatedImage is designed around a specific Android use case, so move
it into the android folders.

Make SkAnimatedImage hold an SkAndroidCodec (instead of an SkCodec).
Expose fCodec so that SkAnimatedImage can animate by using the internal
SkCodec.

Update the sample to use SkAndroidCodec.

Allow webp to decode a scaled down animation. For RestoreBG frames,
adjust the frameRect (which is erased) to account for the scaling. Add
a test to verify that we decode a webp with a RestoreBG frame
successfully. Disable scaling for later frames in other formats (GIF,
for now), since the code for erasing a RestoreBG frame is currently
unaware of the sampling.

Change-Id: I5dd2b86138f2c7f6adcd08dce1bd49040f7dc224
Reviewed-on: https://skia-review.googlesource.com/94621
Commit-Queue: Leon Scroggins <scroggo@google.com>
Reviewed-by: Derek Sollenberger <djsollen@google.com>
2018-01-16 20:36:09 +00:00

439 lines
18 KiB
C++

/*
* Copyright 2016 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "SkAndroidCodec.h"
#include "SkBitmap.h"
#include "SkCodec.h"
#include "SkCommonFlags.h"
#include "SkImageEncoder.h"
#include "SkOSPath.h"
#include "SkStream.h"
#include "Resources.h"
#include "Test.h"
#include "sk_tool_utils.h"
#include <initializer_list>
#include <vector>
static void write_bm(const char* name, const SkBitmap& bm) {
if (FLAGS_writePath.isEmpty()) {
return;
}
SkString filename = SkOSPath::Join(FLAGS_writePath[0], name);
filename.appendf(".png");
SkFILEWStream file(filename.c_str());
if (!SkEncodeImage(&file, bm, SkEncodedImageFormat::kPNG, 100)) {
SkDebugf("failed to write '%s'\n", filename.c_str());
}
}
DEF_TEST(Codec_trunc, r) {
sk_sp<SkData> data(GetResourceAsData("images/box.gif"));
if (!data) {
return;
}
SkCodec::MakeFromData(SkData::MakeSubset(data.get(), 0, 23))->getFrameInfo();
}
// 565 does not support alpha, but there is no reason for it not to support an
// animated image with a frame that has alpha but then blends onto an opaque
// frame making the result opaque. Test that we can decode such a frame.
DEF_TEST(Codec_565, r) {
sk_sp<SkData> data(GetResourceAsData("images/blendBG.webp"));
if (!data) {
return;
}
std::unique_ptr<SkCodec> codec(SkCodec::MakeFromData(std::move(data)));
auto info = codec->getInfo().makeColorType(kRGB_565_SkColorType);
SkBitmap bm;
bm.allocPixels(info);
SkCodec::Options options;
options.fFrameIndex = 1;
options.fPriorFrame = SkCodec::kNone;
const auto result = codec->getPixels(info, bm.getPixels(), bm.rowBytes(),
&options);
REPORTER_ASSERT(r, result == SkCodec::kSuccess);
}
static bool restore_previous(const SkCodec::FrameInfo& info) {
return info.fDisposalMethod == SkCodecAnimation::DisposalMethod::kRestorePrevious;
}
DEF_TEST(Codec_frames, r) {
#define kOpaque kOpaque_SkAlphaType
#define kUnpremul kUnpremul_SkAlphaType
#define kKeep SkCodecAnimation::DisposalMethod::kKeep
#define kRestoreBG SkCodecAnimation::DisposalMethod::kRestoreBGColor
#define kRestorePrev SkCodecAnimation::DisposalMethod::kRestorePrevious
static const struct {
const char* fName;
int fFrameCount;
// One less than fFramecount, since the first frame is always
// independent.
std::vector<int> fRequiredFrames;
// Same, since the first frame should match getInfo
std::vector<SkAlphaType> fAlphas;
// The size of this one should match fFrameCount for animated, empty
// otherwise.
std::vector<int> fDurations;
int fRepetitionCount;
std::vector<SkCodecAnimation::DisposalMethod> fDisposalMethods;
} gRecs[] = {
{ "images/required.gif", 7,
{ 0, 1, 2, 3, 4, 5 },
{ kOpaque, kUnpremul, kUnpremul, kUnpremul, kUnpremul, kUnpremul },
{ 100, 100, 100, 100, 100, 100, 100 },
0,
{ kKeep, kRestoreBG, kKeep, kKeep, kKeep, kRestoreBG, kKeep } },
{ "images/alphabetAnim.gif", 13,
{ SkCodec::kNone, 0, 0, 0, 0, 5, 6, SkCodec::kNone,
SkCodec::kNone, 9, 10, 11 },
{ kUnpremul, kUnpremul, kUnpremul, kUnpremul, kUnpremul, kUnpremul,
kUnpremul, kUnpremul, kUnpremul, kUnpremul, kUnpremul, kUnpremul },
{ 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100 },
0,
{ kKeep, kRestorePrev, kRestorePrev, kRestorePrev, kRestorePrev,
kRestoreBG, kKeep, kRestoreBG, kRestoreBG, kKeep, kKeep,
kRestoreBG, kKeep } },
{ "images/randPixelsAnim2.gif", 4,
// required frames
{ 0, 0, 1 },
// alphas
{ kOpaque, kOpaque, kOpaque },
// durations
{ 0, 1000, 170, 40 },
// repetition count
0,
{ kKeep, kKeep, kRestorePrev, kKeep } },
{ "images/randPixelsAnim.gif", 13,
// required frames
{ 0, 1, 2, 3, 4, 3, 6, 7, 7, 7, 9, 9 },
{ kUnpremul, kUnpremul, kUnpremul, kUnpremul, kUnpremul, kUnpremul,
kUnpremul, kUnpremul, kUnpremul, kUnpremul, kUnpremul, kUnpremul },
// durations
{ 0, 1000, 170, 40, 220, 7770, 90, 90, 90, 90, 90, 90, 90 },
// repetition count
0,
{ kKeep, kKeep, kKeep, kKeep, kRestoreBG, kRestoreBG, kRestoreBG,
kRestoreBG, kRestorePrev, kRestoreBG, kRestorePrev, kRestorePrev,
kRestorePrev, } },
{ "images/box.gif", 1, {}, {}, {}, 0, { kKeep } },
{ "images/color_wheel.gif", 1, {}, {}, {}, 0, { kKeep } },
{ "images/test640x479.gif", 4, { 0, 1, 2 },
{ kOpaque, kOpaque, kOpaque },
{ 200, 200, 200, 200 },
SkCodec::kRepetitionCountInfinite,
{ kKeep, kKeep, kKeep, kKeep } },
{ "images/colorTables.gif", 2, { 0 }, { kOpaque }, { 1000, 1000 }, 5,
{ kKeep, kKeep } },
{ "images/arrow.png", 1, {}, {}, {}, 0, {} },
{ "images/google_chrome.ico", 1, {}, {}, {}, 0, {} },
{ "images/brickwork-texture.jpg", 1, {}, {}, {}, 0, {} },
#if defined(SK_CODEC_DECODES_RAW) && (!defined(_WIN32))
{ "images/dng_with_preview.dng", 1, {}, {}, {}, 0, {} },
#endif
{ "images/mandrill.wbmp", 1, {}, {}, {}, 0, {} },
{ "images/randPixels.bmp", 1, {}, {}, {}, 0, {} },
{ "images/yellow_rose.webp", 1, {}, {}, {}, 0, {} },
{ "images/webp-animated.webp", 3, { 0, 1 }, { kOpaque, kOpaque },
{ 1000, 500, 1000 }, SkCodec::kRepetitionCountInfinite,
{ kKeep, kKeep, kKeep } },
{ "images/blendBG.webp", 7, { 0, SkCodec::kNone, SkCodec::kNone, SkCodec::kNone,
4, 4 },
{ kOpaque, kOpaque, kUnpremul, kOpaque, kUnpremul, kUnpremul },
{ 525, 500, 525, 437, 609, 729, 444 }, 7,
{ kKeep, kKeep, kKeep, kKeep, kKeep, kKeep, kKeep } },
{ "images/required.webp", 7,
{ 0, 1, 1, SkCodec::kNone, 4, 4 },
{ kOpaque, kUnpremul, kUnpremul, kOpaque, kOpaque, kOpaque },
{ 100, 100, 100, 100, 100, 100, 100 },
1,
{ kKeep, kRestoreBG, kKeep, kKeep, kKeep, kRestoreBG, kKeep } },
};
#undef kOpaque
#undef kUnpremul
#undef kKeep
#undef kRestorePrev
#undef kRestoreBG
for (const auto& rec : gRecs) {
sk_sp<SkData> data(GetResourceAsData(rec.fName));
if (!data) {
// Useful error statement, but sometimes people run tests without
// resources, and they do not want to see these messages.
//ERRORF(r, "Missing resources? Could not find '%s'", rec.fName);
continue;
}
std::unique_ptr<SkCodec> codec(SkCodec::MakeFromData(data));
if (!codec) {
ERRORF(r, "Failed to create an SkCodec from '%s'", rec.fName);
continue;
}
{
SkCodec::FrameInfo frameInfo;
REPORTER_ASSERT(r, !codec->getFrameInfo(0, &frameInfo));
}
const int repetitionCount = codec->getRepetitionCount();
if (repetitionCount != rec.fRepetitionCount) {
ERRORF(r, "%s repetition count does not match! expected: %i\tactual: %i",
rec.fName, rec.fRepetitionCount, repetitionCount);
}
const int expected = rec.fFrameCount;
if (rec.fRequiredFrames.size() + 1 != static_cast<size_t>(expected)) {
ERRORF(r, "'%s' has wrong number entries in fRequiredFrames; expected: %i\tactual: %i",
rec.fName, expected - 1, rec.fRequiredFrames.size());
continue;
}
if (expected > 1) {
if (rec.fDurations.size() != static_cast<size_t>(expected)) {
ERRORF(r, "'%s' has wrong number entries in fDurations; expected: %i\tactual: %i",
rec.fName, expected, rec.fDurations.size());
continue;
}
if (rec.fAlphas.size() + 1 != static_cast<size_t>(expected)) {
ERRORF(r, "'%s' has wrong number entries in fAlphas; expected: %i\tactual: %i",
rec.fName, expected - 1, rec.fAlphas.size());
continue;
}
if (rec.fDisposalMethods.size() != static_cast<size_t>(expected)) {
ERRORF(r, "'%s' has wrong number entries in fDisposalMethods; "
"expected %i\tactual: %i",
rec.fName, expected, rec.fDisposalMethods.size());
continue;
}
}
enum class TestMode {
kVector,
kIndividual,
};
for (auto mode : { TestMode::kVector, TestMode::kIndividual }) {
// Re-create the codec to reset state and test parsing.
codec = SkCodec::MakeFromData(data);
int frameCount;
std::vector<SkCodec::FrameInfo> frameInfos;
switch (mode) {
case TestMode::kVector:
frameInfos = codec->getFrameInfo();
// getFrameInfo returns empty set for non-animated.
frameCount = frameInfos.empty() ? 1 : frameInfos.size();
break;
case TestMode::kIndividual:
frameCount = codec->getFrameCount();
break;
}
if (frameCount != expected) {
ERRORF(r, "'%s' expected frame count: %i\tactual: %i",
rec.fName, expected, frameCount);
continue;
}
// From here on, we are only concerned with animated images.
if (1 == frameCount) {
continue;
}
for (int i = 0; i < frameCount; i++) {
SkCodec::FrameInfo frameInfo;
switch (mode) {
case TestMode::kVector:
frameInfo = frameInfos[i];
break;
case TestMode::kIndividual:
REPORTER_ASSERT(r, codec->getFrameInfo(i, nullptr));
REPORTER_ASSERT(r, codec->getFrameInfo(i, &frameInfo));
break;
}
if (rec.fDurations[i] != frameInfo.fDuration) {
ERRORF(r, "%s frame %i's durations do not match! expected: %i\tactual: %i",
rec.fName, i, rec.fDurations[i], frameInfo.fDuration);
}
auto to_string = [](SkAlphaType alpha) {
switch (alpha) {
case kUnpremul_SkAlphaType:
return "unpremul";
case kOpaque_SkAlphaType:
return "opaque";
default:
SkASSERT(false);
return "unknown";
}
};
auto expectedAlpha = 0 == i ? codec->getInfo().alphaType() : rec.fAlphas[i-1];
auto alpha = frameInfo.fAlphaType;
if (expectedAlpha != alpha) {
ERRORF(r, "%s's frame %i has wrong alpha type! expected: %s\tactual: %s",
rec.fName, i, to_string(expectedAlpha), to_string(alpha));
}
if (0 == i) {
REPORTER_ASSERT(r, frameInfo.fRequiredFrame == SkCodec::kNone);
} else if (rec.fRequiredFrames[i-1] != frameInfo.fRequiredFrame) {
ERRORF(r, "%s's frame %i has wrong dependency! expected: %i\tactual: %i",
rec.fName, i, rec.fRequiredFrames[i-1], frameInfo.fRequiredFrame);
}
REPORTER_ASSERT(r, frameInfo.fDisposalMethod == rec.fDisposalMethods[i]);
}
if (TestMode::kIndividual == mode) {
// No need to test decoding twice.
continue;
}
// Compare decoding in multiple ways:
// - Start from scratch for each frame. |codec| will have to decode the required frame
// (and any it depends on) to decode. This is stored in |cachedFrames|.
// - Provide the frame that a frame depends on, so |codec| just has to blend.
// - Provide a frame after the required frame, which will be covered up by the newest
// frame.
// All should look the same.
std::vector<SkBitmap> cachedFrames(frameCount);
const auto info = codec->getInfo().makeColorType(kN32_SkColorType);
auto decode = [&](SkBitmap* bm, int index, int cachedIndex) {
auto decodeInfo = info;
if (index > 0) {
decodeInfo = info.makeAlphaType(frameInfos[index].fAlphaType);
}
bm->allocPixels(decodeInfo);
if (cachedIndex != SkCodec::kNone) {
// First copy the pixels from the cached frame
const bool success = sk_tool_utils::copy_to(bm, kN32_SkColorType,
cachedFrames[cachedIndex]);
REPORTER_ASSERT(r, success);
}
SkCodec::Options opts;
opts.fFrameIndex = index;
opts.fPriorFrame = cachedIndex;
const auto result = codec->getPixels(decodeInfo, bm->getPixels(), bm->rowBytes(),
&opts);
if (cachedIndex != SkCodec::kNone && restore_previous(frameInfos[cachedIndex])) {
if (result == SkCodec::kInvalidParameters) {
return true;
}
ERRORF(r, "Using a kRestorePrevious frame as fPriorFrame should fail");
return false;
}
if (result != SkCodec::kSuccess) {
ERRORF(r, "Failed to decode frame %i from %s when providing prior frame %i, "
"error %i", index, rec.fName, cachedIndex, result);
}
return result == SkCodec::kSuccess;
};
for (int i = 0; i < frameCount; i++) {
SkBitmap& cachedFrame = cachedFrames[i];
if (!decode(&cachedFrame, i, SkCodec::kNone)) {
continue;
}
const auto reqFrame = frameInfos[i].fRequiredFrame;
if (reqFrame == SkCodec::kNone) {
// Nothing to compare against.
continue;
}
for (int j = reqFrame; j < i; j++) {
SkBitmap frame;
if (restore_previous(frameInfos[j])) {
(void) decode(&frame, i, j);
continue;
}
if (!decode(&frame, i, j)) {
continue;
}
// Now verify they're equal.
const size_t rowLen = info.bytesPerPixel() * info.width();
for (int y = 0; y < info.height(); y++) {
const void* cachedAddr = cachedFrame.getAddr(0, y);
SkASSERT(cachedAddr != nullptr);
const void* addr = frame.getAddr(0, y);
SkASSERT(addr != nullptr);
const bool lineMatches = memcmp(cachedAddr, addr, rowLen) == 0;
if (!lineMatches) {
SkString name = SkStringPrintf("cached_%i", i);
write_bm(name.c_str(), cachedFrame);
name = SkStringPrintf("frame_%i", i);
write_bm(name.c_str(), frame);
ERRORF(r, "%s's frame %i is different (starting from line %i) when "
"providing prior frame %i!", rec.fName, i, y, j);
break;
}
}
}
}
}
}
}
// Verify that a webp image can be animated scaled down. This image has a
// kRestoreBG frame, so it is an interesting image to test. After decoding that
// frame, we have to erase its rectangle. The rectangle has to be adjusted
// based on the scaled size.
DEF_TEST(AndroidCodec_animated, r) {
if (GetResourcePath().isEmpty()) {
return;
}
const char* file = "images/required.webp";
sk_sp<SkData> data(GetResourceAsData(file));
if (!data) {
ERRORF(r, "Missing %s", file);
return;
}
auto codec = SkAndroidCodec::MakeFromCodec(SkCodec::MakeFromData(std::move(data)));
if (!codec) {
ERRORF(r, "Failed to decode %s", file);
return;
}
auto info = codec->getInfo().makeAlphaType(kPremul_SkAlphaType);
for (int sampleSize : { 8, 32, 100 }) {
auto dimensions = codec->codec()->getScaledDimensions(1.0f / sampleSize);
info = info.makeWH(dimensions.width(), dimensions.height());
SkBitmap bm;
bm.allocPixels(info);
SkCodec::Options options;
for (int i = 0; i < codec->codec()->getFrameCount(); ++i) {
SkCodec::FrameInfo frameInfo;
REPORTER_ASSERT(r, codec->codec()->getFrameInfo(i, &frameInfo));
if (5 == i) {
REPORTER_ASSERT(r, frameInfo.fDisposalMethod
== SkCodecAnimation::DisposalMethod::kRestoreBGColor);
}
options.fFrameIndex = i;
options.fPriorFrame = i - 1;
info = info.makeAlphaType(frameInfo.fAlphaType);
const auto result = codec->codec()->getPixels(info, bm.getPixels(), bm.rowBytes(),
&options);
REPORTER_ASSERT(r, result == SkCodec::kSuccess);
}
}
}