Add an SkDrawable for animated images (e.g. GIF)

Bug: b/63909536

SkAnimatedImage is a simple drawable for animating a GIF. Thread-safety
is left up to the client. At most two bitmaps are stored in the
drawable; one for the current frame and one for a frame that may need to
be restored. The backup frame prevents some cases where we would
otherwise have to re-decode from the beginning of the image.

The API lets the client set the time value, and decodes to match that
time.

TODO:
- Callback for when the animation is complete
- Ability to use SkAndroidCodec
- Modify the loop count (or leave that up to client?)
- Better and/or client-specific caching

Other changes:
- Add a sample which animates a GIF
- Reenable SK_CODEC_PRINTF for debug builds and Android

Change-Id: I945ffbccdb6008f2a05ed4d9b2af869a261fb300
Reviewed-on: https://skia-review.googlesource.com/93420
Reviewed-by: Derek Sollenberger <djsollen@google.com>
Commit-Queue: Leon Scroggins <scroggo@google.com>
This commit is contained in:
Leon Scroggins III 2018-01-12 11:24:30 -05:00 committed by Skia Commit-Bot
parent 65fa8ca85e
commit 7a10b332a3
7 changed files with 456 additions and 1 deletions

View File

@ -774,6 +774,7 @@ component("skia") {
"src/android/SkBitmapRegionCodec.cpp",
"src/android/SkBitmapRegionDecoder.cpp",
"src/codec/SkAndroidCodec.cpp",
"src/codec/SkAnimatedImage.cpp",
"src/codec/SkBmpBaseCodec.cpp",
"src/codec/SkBmpCodec.cpp",
"src/codec/SkBmpMaskCodec.cpp",

View File

@ -268,6 +268,7 @@ cflags = cflags.union([
"-DSKIA_DLL",
"-DSKIA_IMPLEMENTATION=1",
"-DATRACE_TAG=ATRACE_TAG_VIEW",
"-DSK_PRINT_CODEC_MESSAGES",
])
cflags_cc.add("-fexceptions")

View File

@ -16,6 +16,7 @@ samples_sources = [
"$_samplecode/SampleAARects.cpp",
"$_samplecode/SampleAll.cpp",
"$_samplecode/SampleAndroidShadows.cpp",
"$_samplecode/SampleAnimatedImage.cpp",
"$_samplecode/SampleAnimatedText.cpp",
"$_samplecode/SampleAnimBlur.cpp",
"$_samplecode/SampleArc.cpp",

View File

@ -0,0 +1,86 @@
/*
* Copyright 2018 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#ifndef SkAnimatedImage_DEFINED
#define SkAnimatedImage_DEFINED
#include "SkBitmap.h"
#include "SkCodecAnimation.h"
#include "SkDrawable.h"
class SkCodec;
/**
* Thread unsafe drawable for drawing animated images (e.g. GIF).
*/
class SK_API SkAnimatedImage : public SkDrawable {
public:
/**
* Create an SkAnimatedImage from the SkCodec.
*
* Returns null on failure to allocate pixels. On success, this will
* decode the first frame. It will not animate until start() is called.
*/
static sk_sp<SkAnimatedImage> MakeFromCodec(std::unique_ptr<SkCodec>);
~SkAnimatedImage() override;
/**
* Start or resume the animation. update() must be called to advance the
* time.
*/
void start();
/**
* Stop the animation. update() has no effect while the animation is
* stopped.
*/
void stop();
/**
* Reset the animation to the beginning.
*/
void reset();
/**
* Update the current time. If the image is animating, this may decode
* a new frame.
*
* @return the time to show the next frame.
* Returns numeric_limits<double>::max() if there is no max frame to
* show, and -1.0 if the animation is not running.
*/
double update(double msecs);
protected:
SkRect onGetBounds() override;
void onDraw(SkCanvas*) override;
private:
struct Frame {
SkBitmap fBitmap;
int fIndex;
SkCodecAnimation::DisposalMethod fDisposalMethod;
Frame();
bool copyTo(Frame*) const;
};
std::unique_ptr<SkCodec> fCodec;
bool fFinished;
bool fRunning;
double fNowMS;
double fRemainingMS;
Frame fActiveFrame;
Frame fRestoreFrame;
SkAnimatedImage(std::unique_ptr<SkCodec>);
typedef SkDrawable INHERITED;
};
#endif // SkAnimatedImage_DEFINED

View File

@ -0,0 +1,130 @@
/*
* 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 "SkAnimatedImage.h"
#include "SkAnimTimer.h"
#include "SkCanvas.h"
#include "SkCodec.h"
#include "SkPaint.h"
#include "SkPictureRecorder.h"
#include "SkRect.h"
#include "SkScalar.h"
#include "SkString.h"
#include "SampleCode.h"
#include "Resources.h"
static constexpr char kPauseKey = 'p';
static constexpr char kResetKey = 'r';
class SampleAnimatedImage : public SampleView {
public:
SampleAnimatedImage()
: INHERITED()
, fRunning(false)
, fYOffset(0)
{}
protected:
void onDrawBackground(SkCanvas* canvas) override {
SkPaint paint;
paint.setAntiAlias(true);
constexpr SkScalar kTextSize = 20;
paint.setTextSize(kTextSize);
SkString str = SkStringPrintf("Press '%c' to start/pause; '%c' to reset.",
kPauseKey, kResetKey);
const char* text = str.c_str();
SkRect bounds;
paint.measureText(text, strlen(text), &bounds);
fYOffset = bounds.height();
canvas->drawText(text, strlen(text), 5, fYOffset, paint);
fYOffset *= 2;
}
void onDrawContent(SkCanvas* canvas) override {
if (!fImage) {
return;
}
canvas->translate(0, fYOffset);
canvas->drawDrawable(fImage.get());
canvas->drawDrawable(fDrawable.get(), fImage->getBounds().width(), 0);
}
bool onAnimate(const SkAnimTimer& animTimer) override {
if (!fImage) {
return false;
}
fImage->update(animTimer.msec());
return true;
}
void onOnceBeforeDraw() override {
sk_sp<SkData> file(GetResourceAsData("images/alphabetAnim.gif"));
std::unique_ptr<SkCodec> codec(SkCodec::MakeFromData(file));
if (!codec) {
return;
}
fImage = SkAnimatedImage::MakeFromCodec(std::move(codec));
if (!fImage) {
return;
}
SkPictureRecorder recorder;
auto canvas = recorder.beginRecording(fImage->getBounds());
canvas->drawDrawable(fImage.get());
fDrawable = recorder.finishRecordingAsDrawable();
}
bool onQuery(SkEvent* evt) override {
if (SampleCode::TitleQ(*evt)) {
SampleCode::TitleR(evt, "AnimatedImage");
return true;
}
SkUnichar uni;
if (fImage && SampleCode::CharQ(*evt, &uni)) {
switch (uni) {
case kPauseKey:
if (fRunning) {
fImage->stop();
fRunning = false;
} else {
fImage->start();
fRunning = true;
}
return true;
case kResetKey:
fImage->reset();
return true;
default:
break;
}
}
return this->INHERITED::onQuery(evt);
}
private:
sk_sp<SkAnimatedImage> fImage;
sk_sp<SkDrawable> fDrawable;
bool fRunning;
SkScalar fYOffset;
typedef SampleView INHERITED;
};
///////////////////////////////////////////////////////////////////////////////
static SkView* MyFactory() {
return new SampleAnimatedImage;
}
static SkViewRegister reg(MyFactory);

View File

@ -0,0 +1,236 @@
/*
* 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 "SkAnimatedImage.h"
#include "SkCanvas.h"
#include "SkCodec.h"
#include "SkCodecPriv.h"
sk_sp<SkAnimatedImage> SkAnimatedImage::MakeFromCodec(std::unique_ptr<SkCodec> codec) {
if (!codec) {
return nullptr;
}
auto image = sk_sp<SkAnimatedImage>(new SkAnimatedImage(std::move(codec)));
if (!image->fActiveFrame.fBitmap.getPixels()) {
// tryAllocPixels failed.
return nullptr;
}
return image;
}
// Sentinel value for starting at the beginning.
static constexpr double kInit = -1.0;
SkAnimatedImage::SkAnimatedImage(std::unique_ptr<SkCodec> codec)
: fCodec(std::move(codec))
, fFinished(false)
, fRunning(false)
, fNowMS(kInit)
, fRemainingMS(kInit)
{
if (!fActiveFrame.fBitmap.tryAllocPixels(fCodec->getInfo())) {
return;
}
this->update(kInit);
}
SkAnimatedImage::~SkAnimatedImage() { }
SkRect SkAnimatedImage::onGetBounds() {
return SkRect::Make(fCodec->getInfo().bounds());
}
void SkAnimatedImage::onDraw(SkCanvas* canvas) {
canvas->drawBitmap(fActiveFrame.fBitmap, 0, 0);
}
SkAnimatedImage::Frame::Frame()
: fIndex(SkCodec::kNone)
{}
bool SkAnimatedImage::Frame::copyTo(Frame* dst) const {
if (dst->fBitmap.getPixels()) {
dst->fBitmap.setAlphaType(fBitmap.alphaType());
} else if (!dst->fBitmap.tryAllocPixels(fBitmap.info())) {
return false;
}
memcpy(dst->fBitmap.getPixels(), fBitmap.getPixels(), fBitmap.computeByteSize());
dst->fIndex = fIndex;
dst->fDisposalMethod = fDisposalMethod;
return true;
}
void SkAnimatedImage::start() {
fRunning = true;
}
void SkAnimatedImage::stop() {
fRunning = false;
}
void SkAnimatedImage::reset() {
this->update(kInit);
}
static bool is_restore_previous(SkCodecAnimation::DisposalMethod dispose) {
return SkCodecAnimation::DisposalMethod::kRestorePrevious == dispose;
}
double SkAnimatedImage::update(double msecs) {
if (fFinished) {
return std::numeric_limits<double>::max();
}
const double lastUpdateMS = fNowMS;
fNowMS = msecs;
const double msSinceLastUpdate = fNowMS - lastUpdateMS;
const int frameCount = fCodec->getFrameCount();
int frameToDecode = SkCodec::kNone;
if (kInit == msecs) {
frameToDecode = 0;
} else {
if (!fRunning || lastUpdateMS == kInit) {
return kInit;
}
if (msSinceLastUpdate < fRemainingMS) {
fRemainingMS -= msSinceLastUpdate;
return fRemainingMS + fNowMS;
} else {
frameToDecode = (fActiveFrame.fIndex + 1) % frameCount;
}
}
SkCodec::FrameInfo frameInfo;
if (fCodec->getFrameInfo(frameToDecode, &frameInfo)) {
if (!frameInfo.fFullyReceived) {
SkCodecPrintf("Frame %i not fully received\n", frameToDecode);
fFinished = true;
return std::numeric_limits<double>::max();
}
if (kInit == msecs) {
fRemainingMS = frameInfo.fDuration;
} else {
// Check to see whether we should skip this frame.
double pastUpdate = msSinceLastUpdate - fRemainingMS;
while (pastUpdate >= frameInfo.fDuration) {
SkCodecPrintf("Skipping frame %i\n", frameToDecode);
pastUpdate -= frameInfo.fDuration;
frameToDecode = (frameToDecode + 1) % frameCount;
if (!fCodec->getFrameInfo(frameToDecode, &frameInfo)) {
SkCodecPrintf("Could not getFrameInfo for frame %i",
frameToDecode);
// Prior call to getFrameInfo succeeded, so use that one.
frameToDecode--;
fFinished = true;
if (frameToDecode < 0) {
return std::numeric_limits<double>::max();
}
}
}
fRemainingMS = frameInfo.fDuration - pastUpdate;
}
} else {
fFinished = true;
if (0 == frameToDecode) {
// Static image. This is okay.
frameInfo.fRequiredFrame = SkCodec::kNone;
frameInfo.fAlphaType = fCodec->getInfo().alphaType();
// These fields won't be read.
frameInfo.fDuration = INT_MAX;
frameInfo.fFullyReceived = true;
} else {
SkCodecPrintf("Error getting frameInfo for frame %i\n",
frameToDecode);
return std::numeric_limits<double>::max();
}
}
if (frameToDecode == fActiveFrame.fIndex) {
return fRemainingMS + fNowMS;
}
if (frameToDecode == fRestoreFrame.fIndex) {
SkTSwap(fActiveFrame, fRestoreFrame);
return fRemainingMS + fNowMS;
}
// The following code makes an effort to avoid overwriting a frame that will
// be used again. If frame |i| is_restore_previous, frame |i+1| will not
// depend on frame |i|, so do not overwrite frame |i-1|, which may be needed
// for frame |i+1|.
// We could be even smarter about which frames to save by looking at the
// entire dependency chain.
SkCodec::Options options;
options.fFrameIndex = frameToDecode;
if (frameInfo.fRequiredFrame == SkCodec::kNone) {
if (is_restore_previous(frameInfo.fDisposalMethod)) {
// frameToDecode will be discarded immediately after drawing, so
// do not overwrite a frame which could possibly be used in the
// future.
if (fActiveFrame.fIndex != SkCodec::kNone &&
!is_restore_previous(fActiveFrame.fDisposalMethod)) {
SkTSwap(fActiveFrame, fRestoreFrame);
}
}
} else {
auto validPriorFrame = [&frameInfo, &frameToDecode](const Frame& frame) {
if (SkCodec::kNone == frame.fIndex || is_restore_previous(frame.fDisposalMethod)) {
return false;
}
return frame.fIndex >= frameInfo.fRequiredFrame && frame.fIndex < frameToDecode;
};
if (validPriorFrame(fActiveFrame)) {
if (is_restore_previous(frameInfo.fDisposalMethod)) {
// fActiveFrame is a good frame to use for this one, but we
// don't want to overwrite it.
fActiveFrame.copyTo(&fRestoreFrame);
}
options.fPriorFrame = fActiveFrame.fIndex;
} else if (validPriorFrame(fRestoreFrame)) {
if (!is_restore_previous(frameInfo.fDisposalMethod)) {
SkTSwap(fActiveFrame, fRestoreFrame);
} else if (!fRestoreFrame.copyTo(&fActiveFrame)) {
SkCodecPrintf("Failed to restore frame\n");
fFinished = true;
return std::numeric_limits<double>::max();
}
options.fPriorFrame = fActiveFrame.fIndex;
}
}
auto alphaType = kOpaque_SkAlphaType == frameInfo.fAlphaType ?
kOpaque_SkAlphaType : kPremul_SkAlphaType;
SkBitmap* dst = &fActiveFrame.fBitmap;
if (dst->getPixels()) {
SkAssertResult(dst->setAlphaType(alphaType));
} else {
auto info = fCodec->getInfo().makeAlphaType(alphaType);
if (!dst->tryAllocPixels(info)) {
fFinished = true;
return std::numeric_limits<double>::max();
}
}
auto result = fCodec->getPixels(dst->info(), dst->getPixels(), dst->rowBytes(), &options);
if (result != SkCodec::kSuccess) {
SkCodecPrintf("error %i, frame %i of %i\n", result, frameToDecode, fCodec->getFrameCount());
// Reset to the beginning.
fActiveFrame.fIndex = SkCodec::kNone;
return 0.0;
}
fActiveFrame.fIndex = frameToDecode;
fActiveFrame.fDisposalMethod = frameInfo.fDisposalMethod;
return fRemainingMS + fNowMS;
}

View File

@ -16,7 +16,7 @@
#include "SkImageInfo.h"
#include "SkTypes.h"
#ifdef SK_PRINT_CODEC_MESSAGES
#if defined(SK_PRINT_CODEC_MESSAGES) || defined(SK_DEBUG)
#define SkCodecPrintf SkDebugf
#else
#define SkCodecPrintf(...)