skia2/tests/AnimatedImageTest.cpp
John Stiles 2122f40c4b Update REPORTER_ASSERT/ERRORF to check format strings.
Previously, REPORTER_ASSERT/ERRORF relied on a helper function named
`reporter_string` which papered over zero-argument and one-argument
messages (where one-argument messages are assumed to ignore printf
formatting rules entirely, and just forward the message as-is).

Replacing this helper with a direct call to `SkStringPrintf` allows
the compiler to check format arguments for correctness, but sacrifices
the one-argument special case. In practice the one-argument special
case was very rarely used, so it's not a significant sacrifice,
and this did uncover several real errors in assertion format strings
(including some cases where the wrong number of arguments was passed).

Change-Id: I4378c43b16fd8fdbf4c78d849a9f2f0a254f7abc
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/506617
Auto-Submit: John Stiles <johnstiles@google.com>
Reviewed-by: Brian Osman <brianosman@google.com>
2022-02-10 16:37:05 +00:00

472 lines
17 KiB
C++

/*
* Copyright 2018 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "include/android/SkAnimatedImage.h"
#include "include/codec/SkAndroidCodec.h"
#include "include/codec/SkCodec.h"
#include "include/core/SkBitmap.h"
#include "include/core/SkCanvas.h"
#include "include/core/SkColor.h"
#include "include/core/SkData.h"
#include "include/core/SkImageInfo.h"
#include "include/core/SkPicture.h"
#include "include/core/SkRefCnt.h"
#include "include/core/SkSize.h"
#include "include/core/SkString.h"
#include "include/core/SkTypes.h"
#include "include/core/SkUnPreMultiply.h"
#include "tests/CodecPriv.h"
#include "tests/Test.h"
#include "tools/Resources.h"
#include "tools/ToolUtils.h"
#include <initializer_list>
#include <memory>
#include <utility>
#include <vector>
DEF_TEST(AnimatedImage_simple, r) {
if (GetResourcePath().isEmpty()) {
return;
}
const char* file = "images/stoplight_h.webp";
auto data = GetResourceAsData(file);
if (!data) {
ERRORF(r, "Could not get %s", file);
return;
}
// An animated image with a non-default exif orientation is no longer
// "simple"; verify that the assert has been removed.
auto androidCodec = SkAndroidCodec::MakeFromData(std::move(data));
auto animatedImage = SkAnimatedImage::Make(std::move(androidCodec));
REPORTER_ASSERT(r, animatedImage);
}
DEF_TEST(AnimatedImage_rotation, r) {
if (GetResourcePath().isEmpty()) {
return;
}
// These images use different exif orientations to achieve the same final
// dimensions
const auto expectedBounds = SkRect::MakeIWH(100, 80);
for (int i = 1; i <=8; i++) {
for (const SkString& name : { SkStringPrintf("images/orientation/%d.webp", i),
SkStringPrintf("images/orientation/%d_444.jpg", i) }) {
const char* file = name.c_str();
auto data = GetResourceAsData(file);
if (!data) {
ERRORF(r, "Could not get %s", file);
return;
}
auto androidCodec = SkAndroidCodec::MakeFromData(std::move(data));
auto animatedImage = SkAnimatedImage::Make(std::move(androidCodec));
if (!animatedImage) {
ERRORF(r, "Failed to create animated image from %s", file);
return;
}
auto bounds = animatedImage->getBounds();
if (bounds != expectedBounds) {
ERRORF(r, "Mismatched bounds for %s", file);
bounds.dump();
}
}
}
}
DEF_TEST(AnimatedImage_invalidCrop, r) {
if (GetResourcePath().isEmpty()) {
return;
}
const char* file = "images/alphabetAnim.gif";
auto data = GetResourceAsData(file);
if (!data) {
ERRORF(r, "Could not get %s", file);
return;
}
const struct Rec {
bool valid;
SkISize scaledSize;
SkIRect cropRect;
} gRecs[] = {
// cropRect contained by original dimensions
{ true, {100, 100}, { 0, 0, 100, 100} },
{ true, {100, 100}, { 0, 0, 50, 50} },
{ true, {100, 100}, { 10, 10, 100, 100} },
{ true, {100, 100}, { 0, 0, 100, 100} },
// unsorted cropRect
{ false, {100, 100}, { 0, 100, 100, 0} },
{ false, {100, 100}, { 100, 0, 0, 100} },
// cropRect not contained by original dimensions
{ false, {100, 100}, { 0, 1, 100, 101} },
{ false, {100, 100}, { 0, -1, 100, 99} },
{ false, {100, 100}, { -1, 0, 99, 100} },
{ false, {100, 100}, { 100, 100, 200, 200} },
// cropRect contained by scaled dimensions
{ true, { 50, 50}, { 0, 0, 50, 50} },
{ true, { 50, 50}, { 0, 0, 25, 25} },
{ true, {200, 200}, { 0, 1, 100, 101} },
// cropRect not contained by scaled dimensions
{ false, { 50, 50}, { 0, 0, 75, 25} },
{ false, { 50, 50}, { 0, 0, 25, 75} },
};
for (const auto& rec : gRecs) {
auto codec = SkAndroidCodec::MakeFromData(data);
if (!codec) {
ERRORF(r, "Could not create codec for %s", file);
return;
}
auto info = codec->getInfo();
REPORTER_ASSERT(r, info.dimensions() == SkISize::Make(100, 100));
auto image = SkAnimatedImage::Make(std::move(codec), info.makeDimensions(rec.scaledSize),
rec.cropRect, nullptr);
REPORTER_ASSERT(r, rec.valid == !!image.get());
}
}
DEF_TEST(AnimatedImage_scaled, r) {
if (GetResourcePath().isEmpty()) {
return;
}
const char* file = "images/alphabetAnim.gif";
auto data = GetResourceAsData(file);
if (!data) {
ERRORF(r, "Could not get %s", file);
return;
}
auto codec = SkAndroidCodec::MakeFromCodec(SkCodec::MakeFromData(data));
if (!codec) {
ERRORF(r, "Could not create codec for %s", file);
return;
}
// Force the drawable follow its special case that requires scaling.
auto info = codec->getInfo();
info = info.makeWH(info.width() - 5, info.height() - 5);
auto rect = info.bounds();
auto image = SkAnimatedImage::Make(std::move(codec), info, rect, nullptr);
if (!image) {
ERRORF(r, "Failed to create animated image for %s", file);
return;
}
// Clear a bitmap to non-transparent and draw to it. pixels that are transparent
// in the image should not replace the original non-transparent color.
SkBitmap bm;
bm.allocPixels(SkImageInfo::MakeN32Premul(info.width(), info.height()));
bm.eraseColor(SK_ColorBLUE);
SkCanvas canvas(bm);
image->draw(&canvas);
for (int i = 0; i < info.width(); ++i)
for (int j = 0; j < info.height(); ++j) {
if (*bm.getAddr32(i, j) == SK_ColorTRANSPARENT) {
ERRORF(r, "Erased color underneath!");
return;
}
}
}
static bool compare_bitmaps(skiatest::Reporter* r,
const char* file,
int expectedFrame,
const SkBitmap& expectedBm,
const SkBitmap& actualBm) {
REPORTER_ASSERT(r, expectedBm.colorType() == actualBm.colorType());
REPORTER_ASSERT(r, expectedBm.dimensions() == actualBm.dimensions());
for (int i = 0; i < actualBm.width(); ++i)
for (int j = 0; j < actualBm.height(); ++j) {
SkColor expected = SkUnPreMultiply::PMColorToColor(*expectedBm.getAddr32(i, j));
SkColor actual = SkUnPreMultiply::PMColorToColor(*actualBm .getAddr32(i, j));
if (expected != actual) {
ERRORF(r, "frame %i of %s does not match at pixel %i, %i!"
" expected %x\tactual: %x",
expectedFrame, file, i, j, expected, actual);
SkString expected_name = SkStringPrintf("expected_%c", '0' + expectedFrame);
SkString actual_name = SkStringPrintf("actual_%c", '0' + expectedFrame);
write_bm(expected_name.c_str(), expectedBm);
write_bm(actual_name.c_str(), actualBm);
return false;
}
}
return true;
}
DEF_TEST(AnimatedImage_copyOnWrite, r) {
if (GetResourcePath().isEmpty()) {
return;
}
for (const char* file : { "images/alphabetAnim.gif",
"images/colorTables.gif",
"images/stoplight.webp",
"images/required.webp",
}) {
auto data = GetResourceAsData(file);
if (!data) {
ERRORF(r, "Could not get %s", file);
continue;
}
auto codec = SkCodec::MakeFromData(data);
if (!codec) {
ERRORF(r, "Could not create codec for %s", file);
continue;
}
const auto imageInfo = codec->getInfo().makeAlphaType(kPremul_SkAlphaType);
const int frameCount = codec->getFrameCount();
auto androidCodec = SkAndroidCodec::MakeFromCodec(std::move(codec));
if (!androidCodec) {
ERRORF(r, "Could not create androidCodec for %s", file);
continue;
}
auto animatedImage = SkAnimatedImage::Make(std::move(androidCodec));
if (!animatedImage) {
ERRORF(r, "Could not create animated image for %s", file);
continue;
}
animatedImage->setRepetitionCount(0);
std::vector<SkBitmap> expected(frameCount);
std::vector<sk_sp<SkPicture>> pictures(frameCount);
for (int i = 0; i < frameCount; i++) {
SkBitmap& bm = expected[i];
bm.allocPixels(imageInfo);
bm.eraseColor(SK_ColorTRANSPARENT);
SkCanvas canvas(bm);
pictures[i].reset(animatedImage->newPictureSnapshot());
canvas.drawPicture(pictures[i]);
const auto duration = animatedImage->decodeNextFrame();
// We're attempting to decode i + 1, so decodeNextFrame will return
// kFinished if that is the last frame (or we attempt to decode one
// more).
if (i >= frameCount - 2) {
REPORTER_ASSERT(r, duration == SkAnimatedImage::kFinished);
} else {
REPORTER_ASSERT(r, duration != SkAnimatedImage::kFinished);
}
}
for (int i = 0; i < frameCount; i++) {
SkBitmap test;
test.allocPixels(imageInfo);
test.eraseColor(SK_ColorTRANSPARENT);
SkCanvas canvas(test);
canvas.drawPicture(pictures[i]);
compare_bitmaps(r, file, i, expected[i], test);
}
}
}
DEF_TEST(AnimatedImage, r) {
if (GetResourcePath().isEmpty()) {
return;
}
for (const char* file : { "images/alphabetAnim.gif",
"images/colorTables.gif",
"images/stoplight.webp",
"images/required.webp",
}) {
auto data = GetResourceAsData(file);
if (!data) {
ERRORF(r, "Could not get %s", file);
continue;
}
auto codec = SkCodec::MakeFromData(data);
if (!codec) {
ERRORF(r, "Could not create codec for %s", file);
continue;
}
const int defaultRepetitionCount = codec->getRepetitionCount();
std::vector<SkCodec::FrameInfo> frameInfos = codec->getFrameInfo();
std::vector<SkBitmap> frames(frameInfos.size());
// Used down below for our test image.
const auto imageInfo = codec->getInfo().makeAlphaType(kPremul_SkAlphaType);
for (size_t i = 0; i < frameInfos.size(); ++i) {
auto info = codec->getInfo().makeAlphaType(frameInfos[i].fAlphaType);
auto& bm = frames[i];
SkCodec::Options options;
options.fFrameIndex = (int) i;
options.fPriorFrame = frameInfos[i].fRequiredFrame;
if (options.fPriorFrame == SkCodec::kNoFrame) {
bm.allocPixels(info);
bm.eraseColor(0);
} else {
const SkBitmap& priorFrame = frames[options.fPriorFrame];
if (!ToolUtils::copy_to(&bm, priorFrame.colorType(), priorFrame)) {
ERRORF(r, "Failed to copy %s frame %i", file, options.fPriorFrame);
options.fPriorFrame = SkCodec::kNoFrame;
}
REPORTER_ASSERT(r, bm.setAlphaType(frameInfos[i].fAlphaType));
}
auto result = codec->getPixels(info, bm.getPixels(), bm.rowBytes(), &options);
if (result != SkCodec::kSuccess) {
ERRORF(r, "error in %s frame %zu: %s", file, i, SkCodec::ResultToString(result));
}
}
auto androidCodec = SkAndroidCodec::MakeFromCodec(std::move(codec));
if (!androidCodec) {
ERRORF(r, "Could not create androidCodec for %s", file);
continue;
}
auto animatedImage = SkAnimatedImage::Make(std::move(androidCodec));
if (!animatedImage) {
ERRORF(r, "Could not create animated image for %s", file);
continue;
}
REPORTER_ASSERT(r, defaultRepetitionCount == animatedImage->getRepetitionCount());
auto testDraw = [r, &frames, &imageInfo, file](const sk_sp<SkAnimatedImage>& animatedImage,
int expectedFrame) {
SkBitmap test;
test.allocPixels(imageInfo);
test.eraseColor(0);
SkCanvas c(test);
animatedImage->draw(&c);
const SkBitmap& frame = frames[expectedFrame];
return compare_bitmaps(r, file, expectedFrame, frame, test);
};
REPORTER_ASSERT(r, animatedImage->currentFrameDuration() == frameInfos[0].fDuration);
if (!testDraw(animatedImage, 0)) {
ERRORF(r, "Did not start with frame 0");
continue;
}
// Start at an arbitrary time.
bool failed = false;
for (size_t i = 1; i < frameInfos.size(); ++i) {
const int frameTime = animatedImage->decodeNextFrame();
REPORTER_ASSERT(r, frameTime == animatedImage->currentFrameDuration());
if (i == frameInfos.size() - 1 && defaultRepetitionCount == 0) {
REPORTER_ASSERT(r, frameTime == SkAnimatedImage::kFinished);
REPORTER_ASSERT(r, animatedImage->isFinished());
} else {
REPORTER_ASSERT(r, frameTime == frameInfos[i].fDuration);
REPORTER_ASSERT(r, !animatedImage->isFinished());
}
if (!testDraw(animatedImage, i)) {
ERRORF(r, "Did not update to %zu properly", i);
failed = true;
break;
}
}
if (failed) {
continue;
}
animatedImage->reset();
REPORTER_ASSERT(r, !animatedImage->isFinished());
if (!testDraw(animatedImage, 0)) {
ERRORF(r, "reset failed");
continue;
}
// Test reset from all the frames.
// j is the frame to call reset on.
for (int j = 0; j < (int) frameInfos.size(); ++j) {
if (failed) {
break;
}
// i is the frame to decode.
for (int i = 0; i <= j; ++i) {
if (i == j) {
animatedImage->reset();
if (!testDraw(animatedImage, 0)) {
ERRORF(r, "reset failed for image %s from frame %i",
file, i);
failed = true;
break;
}
} else if (i != 0) {
animatedImage->decodeNextFrame();
if (!testDraw(animatedImage, i)) {
ERRORF(r, "failed to match frame %i in %s on iteration %i",
i, file, j);
failed = true;
break;
}
}
}
}
if (failed) {
continue;
}
for (int loopCount : { 0, 1, 2, 5 }) {
animatedImage = SkAnimatedImage::Make(SkAndroidCodec::MakeFromCodec(
SkCodec::MakeFromData(data)));
animatedImage->setRepetitionCount(loopCount);
REPORTER_ASSERT(r, animatedImage->getRepetitionCount() == loopCount);
for (int loops = 0; loops <= loopCount; loops++) {
if (failed) {
break;
}
REPORTER_ASSERT(r, !animatedImage->isFinished());
for (size_t i = 1; i <= frameInfos.size(); ++i) {
const int frameTime = animatedImage->decodeNextFrame();
if (frameTime == SkAnimatedImage::kFinished) {
if (loops != loopCount) {
ERRORF(r, "%s animation stopped early: loops: %i\tloopCount: %i",
file, loops, loopCount);
failed = true;
}
if (i != frameInfos.size() - 1) {
ERRORF(r, "%s animation stopped early: i: %zu\tsize: %zu",
file, i, frameInfos.size());
failed = true;
}
break;
}
}
}
if (!animatedImage->isFinished()) {
ERRORF(r, "%s animation should have finished with specified loop count (%i)",
file, loopCount);
}
}
}
}