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:
parent
c4ff4df250
commit
bc098ef6d4
@ -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");)
|
||||
|
@ -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
|
||||
|
Before Width: | Height: | Size: 340 B After Width: | Height: | Size: 340 B |
BIN
resources/images/stoplight_h.webp
Normal file
BIN
resources/images/stoplight_h.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 528 B |
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user