diff options
-rw-r--r-- | gn/tests.gni | 1 | ||||
-rw-r--r-- | include/android/SkAnimatedImage.h | 18 | ||||
-rw-r--r-- | samplecode/SampleAnimatedImage.cpp | 6 | ||||
-rw-r--r-- | src/android/SkAnimatedImage.cpp | 85 | ||||
-rw-r--r-- | tests/AnimatedImageTest.cpp | 229 | ||||
-rw-r--r-- | tests/CodecAnimTest.cpp | 17 | ||||
-rw-r--r-- | tests/CodecPriv.h | 19 |
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 |