aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar Leon Scroggins III <scroggo@google.com>2018-01-20 10:33:24 -0500
committerGravatar Skia Commit-Bot <skia-commit-bot@chromium.org>2018-01-22 18:53:47 +0000
commit4c11945a971f18177aa494b773b24d88a942bba6 (patch)
treed413112fe36318bc3dd201fb298413bbf7c5cb27
parent19dd8fbabfbf38644c47af4b0feac2392e9049fc (diff)
Respect repetition count in SkAnimatedImage
Bug: b/63908092 By default use the repetition count stored in the encoded data (if any). Allow setting the repetition count manually, so that the animation will stop after n+1 total cycles (unless -1 is used for infinite). If the animation is complete, make start reset it. When the animation is not running, make update return max double (i.e. no need to update any time soon). Fix a bug where the first call to update returned -1. Share write_bm with CodecAnimTest, for debugging. Update Sample to check isRunning rather than keeping its own record of whether the animation is running. Change-Id: I883e4d7325f7a7b23a422fa9d756f9ea3018f0f8 Reviewed-on: https://skia-review.googlesource.com/97082 Reviewed-by: Derek Sollenberger <djsollen@google.com> Commit-Queue: Leon Scroggins <scroggo@google.com>
-rw-r--r--gn/tests.gni1
-rw-r--r--include/android/SkAnimatedImage.h18
-rw-r--r--samplecode/SampleAnimatedImage.cpp6
-rw-r--r--src/android/SkAnimatedImage.cpp85
-rw-r--r--tests/AnimatedImageTest.cpp229
-rw-r--r--tests/CodecAnimTest.cpp17
-rw-r--r--tests/CodecPriv.h19
7 files changed, 335 insertions, 40 deletions
diff --git a/gn/tests.gni b/gn/tests.gni
index ebe715adeb..e4519d9678 100644
--- a/gn/tests.gni
+++ b/gn/tests.gni
@@ -8,6 +8,7 @@ _tests = get_path_info("../tests", "abspath")
tests_sources = [
"$_tests/AndroidCodecTest.cpp",
+ "$_tests/AnimatedImageTest.cpp",
"$_tests/AAClipTest.cpp",
"$_tests/AnnotationTest.cpp",
"$_tests/ApplyGammaTest.cpp",
diff --git a/include/android/SkAnimatedImage.h b/include/android/SkAnimatedImage.h
index 1c1a2fc039..d74c6bec49 100644
--- a/include/android/SkAnimatedImage.h
+++ b/include/android/SkAnimatedImage.h
@@ -76,6 +76,17 @@ public:
*/
double update(double msecs);
+ /**
+ * Change the repetition count.
+ *
+ * By default, the image will repeat the number of times indicated in the
+ * encoded data.
+ *
+ * Use SkCodec::kRepetitionCountInfinite for infinite, and 0 to show all
+ * frames once and then stop.
+ */
+ void setRepetitionCount(int count);
+
protected:
SkRect onGetBounds() override;
void onDraw(SkCanvas*) override;
@@ -95,6 +106,7 @@ private:
const SkImageInfo fDecodeInfo;
const SkIRect fCropRect;
const sk_sp<SkPicture> fPostProcess;
+ const int fFrameCount;
const bool fSimple; // no crop, scale, or postprocess
SkMatrix fMatrix; // used only if !fSimple
@@ -104,9 +116,15 @@ private:
double fRemainingMS;
Frame fActiveFrame;
Frame fRestoreFrame;
+ int fRepetitionCount;
+ int fRepetitionsCompleted;
SkAnimatedImage(std::unique_ptr<SkAndroidCodec>, SkISize scaledSize,
SkImageInfo decodeInfo, SkIRect cropRect, sk_sp<SkPicture> postProcess);
+ SkAnimatedImage(std::unique_ptr<SkAndroidCodec>);
+
+ int computeNextFrame(int current, bool* animationEnded);
+ double finish();
typedef SkDrawable INHERITED;
};
diff --git a/samplecode/SampleAnimatedImage.cpp b/samplecode/SampleAnimatedImage.cpp
index bb1dd5a3f1..5c02d29bbe 100644
--- a/samplecode/SampleAnimatedImage.cpp
+++ b/samplecode/SampleAnimatedImage.cpp
@@ -25,7 +25,6 @@ class SampleAnimatedImage : public SampleView {
public:
SampleAnimatedImage()
: INHERITED()
- , fRunning(false)
, fYOffset(0)
{}
@@ -95,12 +94,10 @@ protected:
if (fImage && SampleCode::CharQ(*evt, &uni)) {
switch (uni) {
case kPauseKey:
- if (fRunning) {
+ if (fImage->isRunning()) {
fImage->stop();
- fRunning = false;
} else {
fImage->start();
- fRunning = true;
}
return true;
case kResetKey:
@@ -116,7 +113,6 @@ protected:
private:
sk_sp<SkAnimatedImage> fImage;
sk_sp<SkDrawable> fDrawable;
- bool fRunning;
SkScalar fYOffset;
typedef SampleView INHERITED;
};
diff --git a/src/android/SkAnimatedImage.cpp b/src/android/SkAnimatedImage.cpp
index 4c87083957..577aa725d2 100644
--- a/src/android/SkAnimatedImage.cpp
+++ b/src/android/SkAnimatedImage.cpp
@@ -68,12 +68,15 @@ SkAnimatedImage::SkAnimatedImage(std::unique_ptr<SkAndroidCodec> codec, SkISize
, fDecodeInfo(decodeInfo)
, fCropRect(cropRect)
, fPostProcess(std::move(postProcess))
+ , fFrameCount(fCodec->codec()->getFrameCount())
, fSimple(fScaledSize == fDecodeInfo.dimensions() && !fPostProcess
&& fCropRect == fDecodeInfo.bounds())
, fFinished(false)
, fRunning(false)
, fNowMS(kInit)
, fRemainingMS(kInit)
+ , fRepetitionCount(fCodec->codec()->getRepetitionCount())
+ , fRepetitionsCompleted(0)
{
if (!fActiveFrame.fBitmap.tryAllocPixels(fDecodeInfo)) {
return;
@@ -113,6 +116,9 @@ bool SkAnimatedImage::Frame::copyTo(Frame* dst) const {
void SkAnimatedImage::start() {
fRunning = true;
+ if (fFinished) {
+ this->reset();
+ }
}
void SkAnimatedImage::stop() {
@@ -120,6 +126,8 @@ void SkAnimatedImage::stop() {
}
void SkAnimatedImage::reset() {
+ fFinished = false;
+ fRepetitionsCompleted = 0;
this->update(kInit);
}
@@ -127,6 +135,30 @@ static bool is_restore_previous(SkCodecAnimation::DisposalMethod dispose) {
return SkCodecAnimation::DisposalMethod::kRestorePrevious == dispose;
}
+int SkAnimatedImage::computeNextFrame(int current, bool* animationEnded) {
+ SkASSERT(animationEnded != nullptr);
+ *animationEnded = false;
+
+ const int frameToDecode = current + 1;
+ if (frameToDecode == fFrameCount - 1) {
+ // Final frame. Check to determine whether to stop.
+ fRepetitionsCompleted++;
+ if (fRepetitionCount != SkCodec::kRepetitionCountInfinite
+ && fRepetitionsCompleted > fRepetitionCount) {
+ *animationEnded = true;
+ }
+ } else if (frameToDecode == fFrameCount) {
+ return 0;
+ }
+ return frameToDecode;
+}
+
+double SkAnimatedImage::finish() {
+ fFinished = true;
+ fRunning = false;
+ return std::numeric_limits<double>::max();
+}
+
double SkAnimatedImage::update(double msecs) {
if (fFinished) {
return std::numeric_limits<double>::max();
@@ -136,19 +168,25 @@ double SkAnimatedImage::update(double msecs) {
fNowMS = msecs;
const double msSinceLastUpdate = fNowMS - lastUpdateMS;
- const int frameCount = fCodec->codec()->getFrameCount();
+ bool animationEnded = false;
int frameToDecode = SkCodec::kNone;
if (kInit == msecs) {
frameToDecode = 0;
+ fNowMS = lastUpdateMS;
} else {
- if (!fRunning || lastUpdateMS == kInit) {
- return kInit;
+ if (!fRunning) {
+ return std::numeric_limits<double>::max();
}
+
+ if (lastUpdateMS == kInit) {
+ return fRemainingMS + fNowMS;
+ }
+
if (msSinceLastUpdate < fRemainingMS) {
fRemainingMS -= msSinceLastUpdate;
return fRemainingMS + fNowMS;
} else {
- frameToDecode = (fActiveFrame.fIndex + 1) % frameCount;
+ frameToDecode = this->computeNextFrame(fActiveFrame.fIndex, &animationEnded);
}
}
@@ -156,8 +194,7 @@ double SkAnimatedImage::update(double msecs) {
if (fCodec->codec()->getFrameInfo(frameToDecode, &frameInfo)) {
if (!frameInfo.fFullyReceived) {
SkCodecPrintf("Frame %i not fully received\n", frameToDecode);
- fFinished = true;
- return std::numeric_limits<double>::max();
+ return this->finish();
}
if (kInit == msecs) {
@@ -168,22 +205,22 @@ double SkAnimatedImage::update(double msecs) {
while (pastUpdate >= frameInfo.fDuration) {
SkCodecPrintf("Skipping frame %i\n", frameToDecode);
pastUpdate -= frameInfo.fDuration;
- frameToDecode = (frameToDecode + 1) % frameCount;
+ frameToDecode = computeNextFrame(frameToDecode, &animationEnded);
if (!fCodec->codec()->getFrameInfo(frameToDecode, &frameInfo)) {
SkCodecPrintf("Could not getFrameInfo for frame %i",
frameToDecode);
// Prior call to getFrameInfo succeeded, so use that one.
frameToDecode--;
- fFinished = true;
+ animationEnded = true;
if (frameToDecode < 0) {
- return std::numeric_limits<double>::max();
+ return this->finish();
}
}
}
fRemainingMS = frameInfo.fDuration - pastUpdate;
}
} else {
- fFinished = true;
+ animationEnded = true;
if (0 == frameToDecode) {
// Static image. This is okay.
frameInfo.fRequiredFrame = SkCodec::kNone;
@@ -194,16 +231,22 @@ double SkAnimatedImage::update(double msecs) {
} else {
SkCodecPrintf("Error getting frameInfo for frame %i\n",
frameToDecode);
- return std::numeric_limits<double>::max();
+ return this->finish();
}
}
if (frameToDecode == fActiveFrame.fIndex) {
+ if (animationEnded) {
+ return this->finish();
+ }
return fRemainingMS + fNowMS;
}
if (frameToDecode == fRestoreFrame.fIndex) {
SkTSwap(fActiveFrame, fRestoreFrame);
+ if (animationEnded) {
+ return this->finish();
+ }
return fRemainingMS + fNowMS;
}
@@ -245,8 +288,7 @@ double SkAnimatedImage::update(double msecs) {
SkTSwap(fActiveFrame, fRestoreFrame);
} else if (!fRestoreFrame.copyTo(&fActiveFrame)) {
SkCodecPrintf("Failed to restore frame\n");
- fFinished = true;
- return std::numeric_limits<double>::max();
+ return this->finish();
}
options.fPriorFrame = fActiveFrame.fIndex;
}
@@ -260,22 +302,23 @@ double SkAnimatedImage::update(double msecs) {
} else {
auto info = fDecodeInfo.makeAlphaType(alphaType);
if (!dst->tryAllocPixels(info)) {
- fFinished = true;
- return std::numeric_limits<double>::max();
+ return this->finish();
}
}
auto result = fCodec->codec()->getPixels(dst->info(), dst->getPixels(), dst->rowBytes(),
&options);
if (result != SkCodec::kSuccess) {
- SkCodecPrintf("error %i, frame %i of %i\n", result, frameToDecode, frameCount);
- // Reset to the beginning.
- fActiveFrame.fIndex = SkCodec::kNone;
- return 0.0;
+ SkCodecPrintf("error %i, frame %i of %i\n", result, frameToDecode, fFrameCount);
+ return this->finish();
}
fActiveFrame.fIndex = frameToDecode;
fActiveFrame.fDisposalMethod = frameInfo.fDisposalMethod;
+
+ if (animationEnded) {
+ return this->finish();
+ }
return fRemainingMS + fNowMS;
}
@@ -302,3 +345,7 @@ void SkAnimatedImage::onDraw(SkCanvas* canvas) {
canvas->restore();
}
}
+
+void SkAnimatedImage::setRepetitionCount(int newCount) {
+ fRepetitionCount = newCount;
+}
diff --git a/tests/AnimatedImageTest.cpp b/tests/AnimatedImageTest.cpp
new file mode 100644
index 0000000000..c8113d3fb1
--- /dev/null
+++ b/tests/AnimatedImageTest.cpp
@@ -0,0 +1,229 @@
+/*
+ * 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 "SkAndroidCodec.h"
+#include "SkAnimatedImage.h"
+#include "SkCanvas.h"
+#include "SkCodec.h"
+#include "SkUnPreMultiply.h"
+
+#include "CodecPriv.h"
+#include "Resources.h"
+#include "Test.h"
+#include "sk_tool_utils.h"
+
+#include <vector>
+
+DEF_TEST(AnimatedImage, r) {
+ if (GetResourcePath().isEmpty()) {
+ return;
+ }
+ for (const char* file : { "images/alphabetAnim.gif",
+ "images/colorTables.gif",
+ "images/webp-animated.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::kNone) {
+ bm.allocPixels(info);
+ bm.eraseColor(0);
+ } else {
+ const SkBitmap& priorFrame = frames[options.fPriorFrame];
+ if (!sk_tool_utils::copy_to(&bm, priorFrame.colorType(), priorFrame)) {
+ ERRORF(r, "Failed to copy %s frame %i", file, options.fPriorFrame);
+ options.fPriorFrame = SkCodec::kNone;
+ }
+ 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;
+ }
+
+ 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];
+ REPORTER_ASSERT(r, frame.colorType() == test.colorType());
+ REPORTER_ASSERT(r, frame.dimensions() == test.dimensions());
+ for (int i = 0; i < test.width(); ++i)
+ for (int j = 0; j < test.height(); ++j) {
+ SkColor expected = SkUnPreMultiply::PMColorToColor(*frame.getAddr32(i, j));
+ SkColor actual = SkUnPreMultiply::PMColorToColor(*test .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(), frame);
+ write_bm(actual_name.c_str(), test);
+ return false;
+ }
+ }
+ return true;
+ };
+
+ REPORTER_ASSERT(r, !animatedImage->isRunning());
+ if (!testDraw(animatedImage, 0)) {
+ ERRORF(r, "Did not start with frame 0");
+ continue;
+ }
+
+ animatedImage->start();
+ REPORTER_ASSERT(r, animatedImage->isRunning());
+ if (!testDraw(animatedImage, 0)) {
+ ERRORF(r, "After starting, still not on frame 0");
+ continue;
+ }
+
+ // Start at an arbitrary time.
+ double currentTime = 100000;
+ bool failed = false;
+ for (size_t i = 0; i < frameInfos.size(); ++i) {
+ double next = animatedImage->update(currentTime);
+ if (i == frameInfos.size() - 1 && defaultRepetitionCount == 0) {
+ REPORTER_ASSERT(r, next == std::numeric_limits<double>::max());
+ REPORTER_ASSERT(r, !animatedImage->isRunning());
+ } else {
+ REPORTER_ASSERT(r, animatedImage->isRunning());
+ double expectedNext = currentTime + frameInfos[i].fDuration;
+ if (next != expectedNext) {
+ ERRORF(r, "Time did not match for frame %i: next: %g expected: %g",
+ i, next, expectedNext);
+ failed = true;
+ break;
+ }
+ }
+
+ if (!testDraw(animatedImage, i)) {
+ ERRORF(r, "Did not update to %i properly", i);
+ failed = true;
+ break;
+ }
+
+ // Update, but not to the next frame.
+ REPORTER_ASSERT(r, animatedImage->update((next - currentTime) / 2) == next);
+ if (!testDraw(animatedImage, i)) {
+ ERRORF(r, "Should still be on frame %i", i);
+ failed = true;
+ break;
+ }
+
+ currentTime = next;
+ }
+
+ if (failed) {
+ continue;
+ }
+
+ // Create a new animated image and test stop.
+ animatedImage = SkAnimatedImage::Make(SkAndroidCodec::MakeFromCodec(
+ SkCodec::MakeFromData(data)));
+
+ animatedImage->start();
+ currentTime = 100000;
+ // Do not go to the last frame, so it should still be running after.
+ for (size_t i = 0; i < frameInfos.size() - 1; ++i) {
+ double next = animatedImage->update(currentTime);
+ if (!testDraw(animatedImage, i)) {
+ ERRORF(r, "Error during stop tests.");
+ failed = true;
+ break;
+ }
+
+ double interval = next - currentTime;
+ animatedImage->stop();
+ REPORTER_ASSERT(r, !animatedImage->isRunning());
+
+ currentTime = next;
+ double stoppedNext = animatedImage->update(currentTime);
+ REPORTER_ASSERT(r, stoppedNext == std::numeric_limits<double>::max());
+ if (!testDraw(animatedImage, i)) {
+ ERRORF(r, "Advanced the frame while stopped?");
+ failed = true;
+ break;
+ }
+
+ animatedImage->start();
+ currentTime += interval;
+ }
+
+ if (failed) {
+ return;
+ }
+
+ REPORTER_ASSERT(r, animatedImage->isRunning());
+ animatedImage->reset();
+ if (!testDraw(animatedImage, 0)) {
+ ERRORF(r, "reset failed");
+ continue;
+ }
+
+ for (int loopCount : { 0, 1, 2, 5 }) {
+ animatedImage = SkAnimatedImage::Make(SkAndroidCodec::MakeFromCodec(
+ SkCodec::MakeFromData(data)));
+ animatedImage->start();
+ animatedImage->setRepetitionCount(loopCount);
+ for (int loops = 0; loops <= loopCount; loops++) {
+ REPORTER_ASSERT(r, animatedImage->isRunning());
+ for (size_t i = 0; i < frameInfos.size(); ++i) {
+ double next = animatedImage->update(currentTime);
+ if (animatedImage->isRunning()) {
+ currentTime = next;
+ }
+ }
+ }
+ if (animatedImage->isRunning()) {
+ ERRORF(r, "%s animation still running after %i loops", file, loopCount);
+ }
+ }
+ }
+}
diff --git a/tests/CodecAnimTest.cpp b/tests/CodecAnimTest.cpp
index f707f1e04c..c5c65f8468 100644
--- a/tests/CodecAnimTest.cpp
+++ b/tests/CodecAnimTest.cpp
@@ -8,11 +8,9 @@
#include "SkAndroidCodec.h"
#include "SkBitmap.h"
#include "SkCodec.h"
-#include "SkCommonFlags.h"
-#include "SkImageEncoder.h"
-#include "SkOSPath.h"
#include "SkStream.h"
+#include "CodecPriv.h"
#include "Resources.h"
#include "Test.h"
#include "sk_tool_utils.h"
@@ -20,19 +18,6 @@
#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) {
diff --git a/tests/CodecPriv.h b/tests/CodecPriv.h
index 8362599e95..88de79c65e 100644
--- a/tests/CodecPriv.h
+++ b/tests/CodecPriv.h
@@ -9,7 +9,12 @@
#include "SkBitmap.h"
#include "SkCodec.h"
+#include "SkCommonFlags.h"
#include "SkData.h"
+#include "SkEncodedImageFormat.h"
+#include "SkImageEncoder.h"
+#include "SkOSPath.h"
+#include "SkStream.h"
inline bool decode_memory(const void* mem, size_t size, SkBitmap* bm) {
std::unique_ptr<SkCodec> codec(SkCodec::MakeFromData(SkData::MakeWithoutCopy(mem, size)));
@@ -22,4 +27,18 @@ inline bool decode_memory(const void* mem, size_t size, SkBitmap* bm) {
bm->rowBytes());
return result == SkCodec::kSuccess || result == SkCodec::kIncompleteInput;
}
+
+inline 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());
+ }
+}
+
#endif // CodecPriv_DEFINED