Handle EXIF orientation in SkAnimCodecPlayer

Bug: skia:10914

SkAnimCodecPlayer:
- Properly handle orientation, whether the image is still or not
- Mark const methods as const
- Fix seek() so that if you seek to the duration of frame 0, it will
  show frame 1
- Fix the SkImageInfo so if the first frame is opaque, but following
  frames are not, those frames can still be decoded

resources:
- Rename "webp-animated.webp" to "stoplight.webp", which better
  describes the animation
  - Update test files accordingly
- Add "stoplight_h.webp", which is the same animation with an EXIF
  that converts it to a horizontal stoplight

AnimCodecPlayer test:
- Test the new image files
- Verify SkAnimCodecPlayer::dimensions behaves as expected
- Remove extra debugging line
- Provide better error messages

AnimCodecPlayerExifGM:
- Add a new GM that shows all frames of the new animation with an EXIF
  orientation
- Add a new GM that shows all frames of an animation with an opaque
  first frame followed by frames with alpha

Change-Id: I43cf91c16d52aa1901eef8e13e1e644eea6058b3
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/332753
Reviewed-by: Derek Sollenberger <djsollen@google.com>
Commit-Queue: Leon Scroggins <scroggo@google.com>
This commit is contained in:
Leon Scroggins 2020-10-27 15:24:18 -04:00 committed by Skia Commit-Bot
parent c4ff4df250
commit bc098ef6d4
7 changed files with 166 additions and 24 deletions

View File

@ -229,3 +229,75 @@ private:
}
};
DEF_GM(return new AnimCodecPlayerGM);
class AnimCodecPlayerExifGM : public skiagm::GM {
const char* fPath;
SkISize fSize = SkISize::MakeEmpty();
std::unique_ptr<SkAnimCodecPlayer> fPlayer;
std::vector<SkCodec::FrameInfo> fFrameInfos;
void init() {
if (!fPlayer) {
auto data = GetResourceAsData(fPath);
if (!data) return;
auto codec = SkCodec::MakeFromData(std::move(data));
fFrameInfos = codec->getFrameInfo();
fPlayer = std::make_unique<SkAnimCodecPlayer>(std::move(codec));
if (!fPlayer) return;
// We'll draw one of each frame, so make it big enough to hold them all
// in a grid. The grid will be roughly square, with "factor" frames per
// row and up to "factor" rows.
const size_t count = fFrameInfos.size();
const float root = sqrt((float) count);
const int factor = sk_float_ceil2int(root);
auto imageSize = fPlayer->dimensions();
fSize.fWidth = imageSize.fWidth * factor;
fSize.fHeight = imageSize.fHeight * sk_float_ceil2int((float) count / (float) factor);
}
}
SkString onShortName() override {
return SkStringPrintf("AnimCodecPlayerExif_%s", strrchr(fPath, '/') + 1);
}
SkISize onISize() override {
this->init();
return fSize;
}
void onDraw(SkCanvas* canvas) override {
this->init();
if (!fPlayer) return;
const float root = sqrt((float) fFrameInfos.size());
const int factor = sk_float_ceil2int(root);
auto dimensions = fPlayer->dimensions();
uint32_t duration = 0;
for (int frame = 0; duration < fPlayer->duration(); frame++) {
SkAutoCanvasRestore acr(canvas, true);
const int xTranslate = (frame % factor) * dimensions.width();
const int yTranslate = (frame / factor) * dimensions.height();
canvas->translate(SkIntToScalar(xTranslate), SkIntToScalar(yTranslate));
auto image = fPlayer->getFrame();
canvas->drawImage(image, 0, 0, nullptr);
duration += fFrameInfos[frame].fDuration;
fPlayer->seek(duration);
}
}
public:
AnimCodecPlayerExifGM(const char* path)
: fPath(path)
{}
~AnimCodecPlayerExifGM() override = default;
};
DEF_GM(return new AnimCodecPlayerExifGM("images/required.webp");)
DEF_GM(return new AnimCodecPlayerExifGM("images/required.gif");)
DEF_GM(return new AnimCodecPlayerExifGM("images/stoplight_h.webp");)

View File

@ -27,13 +27,13 @@ public:
/**
* Return the size of the image(s) that will be returned by getFrame().
*/
SkISize dimensions();
SkISize dimensions() const;
/**
* Returns the total duration of the animation in milliseconds. Returns 0 for a single-frame
* image.
*/
uint32_t duration() { return fTotalDuration; }
uint32_t duration() const { return fTotalDuration; }
/**
* Finds the closest frame associated with the time code (in milliseconds) and sets that

View File

Before

Width:  |  Height:  |  Size: 340 B

After

Width:  |  Height:  |  Size: 340 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 B

View File

@ -6,10 +6,12 @@
*/
#include "include/codec/SkCodec.h"
#include "include/core/SkCanvas.h"
#include "include/core/SkData.h"
#include "include/core/SkImage.h"
#include "include/utils/SkAnimCodecPlayer.h"
#include "src/codec/SkCodecImageGenerator.h"
#include "src/core/SkPixmapPriv.h"
#include <algorithm>
SkAnimCodecPlayer::SkAnimCodecPlayer(std::unique_ptr<SkCodec> codec) : fCodec(std::move(codec)) {
@ -36,7 +38,14 @@ SkAnimCodecPlayer::SkAnimCodecPlayer(std::unique_ptr<SkCodec> codec) : fCodec(st
SkAnimCodecPlayer::~SkAnimCodecPlayer() {}
SkISize SkAnimCodecPlayer::dimensions() {
SkISize SkAnimCodecPlayer::dimensions() const {
if (!fCodec) {
auto image = fImages.front();
return image ? image->dimensions() : SkISize::MakeEmpty();
}
if (SkPixmapPriv::ShouldSwapWidthHeight(fCodec->getOrigin())) {
return { fImageInfo.height(), fImageInfo.width() };
}
return { fImageInfo.width(), fImageInfo.height() };
}
@ -54,19 +63,54 @@ sk_sp<SkImage> SkAnimCodecPlayer::getFrameAt(int index) {
SkCodec::Options opts;
opts.fFrameIndex = index;
const auto origin = fCodec->getOrigin();
const auto orientedDims = this->dimensions();
const auto originMatrix = SkEncodedOriginToMatrix(origin, orientedDims.width(),
orientedDims.height());
SkPaint paint;
paint.setBlendMode(SkBlendMode::kSrc);
auto imageInfo = fImageInfo;
if (fFrameInfos[index].fAlphaType != kOpaque_SkAlphaType && imageInfo.isOpaque()) {
imageInfo = imageInfo.makeAlphaType(kPremul_SkAlphaType);
}
const int requiredFrame = fFrameInfos[index].fRequiredFrame;
if (requiredFrame != SkCodec::kNoFrame) {
if (requiredFrame != SkCodec::kNoFrame && fImages[requiredFrame]) {
auto requiredImage = fImages[requiredFrame];
SkPixmap requiredPM;
if (requiredImage && requiredImage->peekPixels(&requiredPM)) {
sk_careful_memcpy(data->writable_data(), requiredPM.addr(), size);
opts.fPriorFrame = requiredFrame;
auto canvas = SkCanvas::MakeRasterDirect(imageInfo, data->writable_data(), rb);
if (origin != kDefault_SkEncodedOrigin) {
// The required frame is stored after applying the origin. Undo that,
// because the codec decodes prior to applying the origin.
// FIXME: Another approach would be to decode the frame's delta on top
// of transparent black, and then draw that through the origin matrix
// onto the required frame. To do that, SkCodec needs to expose the
// rectangle of the delta and the blend mode, so we can handle
// kRestoreBGColor frames and Blend::kBG.
SkMatrix inverse;
SkAssertResult(originMatrix.invert(&inverse));
canvas->concat(inverse);
}
canvas->drawImage(requiredImage, 0, 0, &paint);
opts.fPriorFrame = requiredFrame;
}
if (SkCodec::kSuccess == fCodec->getPixels(fImageInfo, data->writable_data(), rb, &opts)) {
return fImages[index] = SkImage::MakeRasterData(fImageInfo, std::move(data), rb);
if (SkCodec::kSuccess != fCodec->getPixels(imageInfo, data->writable_data(), rb, &opts)) {
return nullptr;
}
return nullptr;
auto image = SkImage::MakeRasterData(imageInfo, std::move(data), rb);
if (origin != kDefault_SkEncodedOrigin) {
imageInfo = imageInfo.makeDimensions(orientedDims);
rb = imageInfo.minRowBytes();
size = imageInfo.computeByteSize(rb);
data = SkData::MakeUninitialized(size);
auto canvas = SkCanvas::MakeRasterDirect(imageInfo, data->writable_data(), rb);
canvas->concat(originMatrix);
canvas->drawImage(image, 0, 0, &paint);
image = SkImage::MakeRasterData(imageInfo, std::move(data), rb);
}
return fImages[index] = image;
}
sk_sp<SkImage> SkAnimCodecPlayer::getFrame() {
@ -86,7 +130,7 @@ bool SkAnimCodecPlayer::seek(uint32_t msec) {
auto lower = std::lower_bound(fFrameInfos.begin(), fFrameInfos.end(), msec,
[](const SkCodec::FrameInfo& info, uint32_t msec) {
return (uint32_t)info.fDuration < msec;
return (uint32_t)info.fDuration <= msec;
});
int prevIndex = fCurrIndex;
fCurrIndex = lower - fFrameInfos.begin();

View File

@ -104,7 +104,7 @@ DEF_TEST(AnimatedImage_copyOnWrite, r) {
}
for (const char* file : { "images/alphabetAnim.gif",
"images/colorTables.gif",
"images/webp-animated.webp",
"images/stoplight.webp",
"images/required.webp",
}) {
auto data = GetResourceAsData(file);
@ -175,7 +175,7 @@ DEF_TEST(AnimatedImage, r) {
}
for (const char* file : { "images/alphabetAnim.gif",
"images/colorTables.gif",
"images/webp-animated.webp",
"images/stoplight.webp",
"images/required.webp",
}) {
auto data = GetResourceAsData(file);

View File

@ -148,7 +148,7 @@ DEF_TEST(Codec_frames, r) {
{ "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 },
{ "images/stoplight.webp", 3, { 0, 1 }, { kOpaque, kOpaque },
{ 1000, 500, 1000 }, SkCodec::kRepetitionCountInfinite,
{ kKeep, kKeep, kKeep } },
{ "images/blendBG.webp", 7,
@ -456,16 +456,42 @@ DEF_TEST(AndroidCodec_animated, r) {
}
}
DEF_TEST(EncodedOriginToMatrixTest, r) {
// SkAnimCodecPlayer relies on the fact that these matrices are invertible.
for (auto origin : { kTopLeft_SkEncodedOrigin ,
kTopRight_SkEncodedOrigin ,
kBottomRight_SkEncodedOrigin ,
kBottomLeft_SkEncodedOrigin ,
kLeftTop_SkEncodedOrigin ,
kRightTop_SkEncodedOrigin ,
kRightBottom_SkEncodedOrigin ,
kLeftBottom_SkEncodedOrigin }) {
// Arbitrary output dimensions.
auto matrix = SkEncodedOriginToMatrix(origin, 100, 80);
REPORTER_ASSERT(r, matrix.invert(nullptr));
}
}
DEF_TEST(AnimCodecPlayer, r) {
static constexpr struct {
const char* fFile;
uint32_t fDuration;
SkISize fSize;
} gTests[] = {
{ "images/alphabetAnim.gif", 1300, {100, 100} },
{ "images/randPixels.gif" , 0, { 8, 8} },
{ "images/randPixels.jpg" , 0, { 8, 8} },
{ "images/randPixels.png" , 0, { 8, 8} },
{ "images/alphabetAnim.gif" , 1300, {100, 100} },
{ "images/randPixels.gif" , 0, { 8, 8} },
{ "images/randPixels.jpg" , 0, { 8, 8} },
{ "images/randPixels.png" , 0, { 8, 8} },
{ "images/stoplight.webp" , 2500, { 11, 29} },
{ "images/stoplight_h.webp" , 2500, { 29, 11} },
{ "images/orientation/1.webp", 0, {100, 80} },
{ "images/orientation/2.webp", 0, {100, 80} },
{ "images/orientation/3.webp", 0, {100, 80} },
{ "images/orientation/4.webp", 0, {100, 80} },
{ "images/orientation/5.webp", 0, {100, 80} },
{ "images/orientation/6.webp", 0, {100, 80} },
{ "images/orientation/7.webp", 0, {100, 80} },
{ "images/orientation/8.webp", 0, {100, 80} },
};
for (const auto& test : gTests) {
@ -473,18 +499,18 @@ DEF_TEST(AnimCodecPlayer, r) {
REPORTER_ASSERT(r, codec);
auto player = std::make_unique<SkAnimCodecPlayer>(std::move(codec));
if (player->duration() != test.fDuration) {
printf("*** %d vs %d\n", player->duration(), test.fDuration);
}
REPORTER_ASSERT(r, player->duration() == test.fDuration);
REPORTER_ASSERT(r, player->dimensions() == test.fSize);
auto f0 = player->getFrame();
REPORTER_ASSERT(r, f0);
REPORTER_ASSERT(r, f0->bounds().size() == test.fSize);
REPORTER_ASSERT(r, f0->bounds().size() == test.fSize,
"Mismatched size for initial frame of %s", test.fFile);
player->seek(500);
auto f1 = player->getFrame();
REPORTER_ASSERT(r, f1);
REPORTER_ASSERT(r, f1->bounds().size() == test.fSize);
REPORTER_ASSERT(r, f1->bounds().size() == test.fSize,
"Mismatched size for frame at 500 ms of %s", test.fFile);
}
}