skia2/tests/AndroidCodecTest.cpp
Leon Scroggins 1340dbde91 Reland "SkAndroidCodec: Support decoding all frames"
This is a reland of fc4fdc5b25

Original change's description:
> SkAndroidCodec: Support decoding all frames
>
> Bug: b/160984428
> Bug: b/163595585
>
> Add support to SkAndroidCodec for decoding all frames with an
> fSampleSize, so that an entire animation can be decoded to a smaller
> size.
>
> dm/:
> - Test scaled + animated decodes
>
> SkAndroidCodec:
> - Make AndroidOptions inherit from SkCodec::Options. This allows
>   SkAndroidCodec to use fFrameIndex. (It also combines the two versions
>   of fSubset, which is now const for both.)
> - Respect fFrameIndex, and call SkCodec::handleFrameIndex to decode
>   the required frame.
> - Disallow decoding with kRespect + fFrameIndex > 0 if there is a
>   non-default orientation. As currently written (except without
>   disabling this combination), SkPixmapPriv::Orient would draw the new
>   portion of the frame on top of uninitialized pixels, instead of the
>   prior frame. This could be fixed by
>   - If SkAndroidCodec needs to decode the required frame, it could do so
>     without applying the orientation, then decode fFrameIndex, and then
>     apply the orientation.
>   - If the client provided the required frame, SkAndroidCodec would need
>     to un-apply the orientation to get the proper starting state, then
>     decode and apply.
>   I think it is simpler to force the client to handle the orientation
>   externally.
>
> SkCodec:
> - Allow SkAndroidCodec to call its private method handleFrameIndex. This
>   method handles decoding a required frame, if necessary. When called by
>   SkAndroidCodec, it now uses the SkAndroidCodec to check for/decode the
>   required frame, so that it will scale properly.
> - Call rewindIfNeeded inside handleFrameIndex. handleFrameIndex calls a
>   virtual method which may set some state (e.g. in SkJpegCodec). Without
>   this change, that state would be reset by rewindIfNeeded.
> - Simplify handling a kRestoreBGColor frame. Whether provided or not,
>   take the same path to calling zero_rect.
> - Updates to zero_rect:
>   - Intersect after scaling, which will also check for empty.
>   - Round out instead of in - this ensures we don't under-erase
>   - Use kFill_ScaleToFit, which better matches the intent.
>
> Change-Id: Ibe1951980a0dca8f5b7b1f20192432d395681683
> Reviewed-on: https://skia-review.googlesource.com/c/skia/+/333225
> Commit-Queue: Leon Scroggins <scroggo@google.com>
> Reviewed-by: Derek Sollenberger <djsollen@google.com>

Bug: b/160984428
Bug: b/163595585
Change-Id: I7c1e79e0f92c75b4840eef65c8fc2b8497189e81
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/334842
Auto-Submit: Leon Scroggins <scroggo@google.com>
Commit-Queue: Derek Sollenberger <djsollen@google.com>
Reviewed-by: Derek Sollenberger <djsollen@google.com>
2020-11-16 16:55:48 +00:00

361 lines
14 KiB
C++

/*
* 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 "include/codec/SkAndroidCodec.h"
#include "include/codec/SkCodec.h"
#include "include/core/SkBitmap.h"
#include "include/core/SkColor.h"
#include "include/core/SkColorSpace.h"
#include "include/core/SkData.h"
#include "include/core/SkEncodedImageFormat.h"
#include "include/core/SkImageGenerator.h"
#include "include/core/SkImageInfo.h"
#include "include/core/SkRefCnt.h"
#include "include/core/SkSize.h"
#include "include/core/SkString.h"
#include "include/core/SkTypes.h"
#include "include/third_party/skcms/skcms.h"
#include "src/codec/SkCodecImageGenerator.h"
#include "src/core/SkPixmapPriv.h"
#include "tests/Test.h"
#include "tools/Resources.h"
#include <string.h>
#include <initializer_list>
#include <memory>
#include <utility>
static SkISize times(const SkISize& size, float factor) {
return { (int) (size.width() * factor), (int) (size.height() * factor) };
}
static SkISize plus(const SkISize& size, int term) {
return { size.width() + term, size.height() + term };
}
static bool invalid(const SkISize& size) {
return size.width() < 1 || size.height() < 1;
}
DEF_TEST(AndroidCodec_computeSampleSize, r) {
if (GetResourcePath().isEmpty()) {
return;
}
for (const char* file : { "images/color_wheel.webp",
"images/ship.png",
"images/dog.jpg",
"images/color_wheel.gif",
"images/rle.bmp",
"images/google_chrome.ico",
"images/mandrill.wbmp",
#ifdef SK_CODEC_DECODES_RAW
"images/sample_1mp.dng",
#endif
}) {
auto data = GetResourceAsData(file);
if (!data) {
ERRORF(r, "Could not get %s", file);
continue;
}
auto codec = SkAndroidCodec::MakeFromCodec(SkCodec::MakeFromData(std::move(data)));
if (!codec) {
ERRORF(r, "Could not create codec for %s", file);
continue;
}
const auto dims = codec->getInfo().dimensions();
const SkISize downscales[] = {
plus(dims, -1),
times(dims, .15f),
times(dims, .6f),
{ (int32_t) (dims.width() * .25f), (int32_t) (dims.height() * .75f ) },
{ 1, 1 },
{ 1, 2 },
{ 2, 1 },
{ 0, -1 },
{ dims.width(), dims.height() - 1 },
};
for (SkISize size : downscales) {
const auto requested = size;
const int computedSampleSize = codec->computeSampleSize(&size);
REPORTER_ASSERT(r, size.width() >= 1 && size.height() >= 1);
if (codec->getEncodedFormat() == SkEncodedImageFormat::kWEBP) {
// WebP supports arbitrary down-scaling.
REPORTER_ASSERT(r, size == requested || invalid(requested));
} else if (computedSampleSize == 1) {
REPORTER_ASSERT(r, size == dims);
} else {
REPORTER_ASSERT(r, computedSampleSize > 1);
if (size.width() >= dims.width() || size.height() >= dims.height()) {
ERRORF(r, "File %s's computed sample size (%i) is bigger than"
" original? original: %i x %i\tsampled: %i x %i",
file, computedSampleSize, dims.width(), dims.height(),
size.width(), size.height());
}
REPORTER_ASSERT(r, size.width() >= requested.width() &&
size.height() >= requested.height());
REPORTER_ASSERT(r, size.width() < dims.width() &&
size.height() < dims.height());
}
}
const SkISize upscales[] = {
dims, plus(dims, 5), times(dims, 2),
};
for (SkISize size : upscales) {
const int computedSampleSize = codec->computeSampleSize(&size);
REPORTER_ASSERT(r, computedSampleSize == 1);
REPORTER_ASSERT(r, dims == size);
}
// This mimics how Android's ImageDecoder uses SkAndroidCodec. A client
// can choose their dimensions based on calling getSampledDimensions,
// but the ImageDecoder API takes an arbitrary size. It then uses
// computeSampleSize to determine the best dimensions and sampleSize.
// It should return the same dimensions. the sampleSize may be different
// due to integer division.
for (int sampleSize : { 1, 2, 3, 4, 8, 16, 32 }) {
const SkISize sampledDims = codec->getSampledDimensions(sampleSize);
SkISize size = sampledDims;
const int computedSampleSize = codec->computeSampleSize(&size);
if (sampledDims != size) {
ERRORF(r, "File '%s'->getSampledDimensions(%i) yields computed"
" sample size of %i\n\tsampledDimensions: %i x %i\t"
"computed dimensions: %i x %i",
file, sampleSize, computedSampleSize,
sampledDims.width(), sampledDims.height(),
size.width(), size.height());
}
}
}
}
DEF_TEST(AndroidCodec_wide, r) {
if (GetResourcePath().isEmpty()) {
return;
}
const char* path = "images/wide-gamut.png";
auto data = GetResourceAsData(path);
if (!data) {
ERRORF(r, "Missing file %s", path);
return;
}
auto codec = SkAndroidCodec::MakeFromCodec(SkCodec::MakeFromData(std::move(data)));
if (!codec) {
ERRORF(r, "Failed to create codec from %s", path);
return;
}
auto info = codec->getInfo();
auto cs = codec->computeOutputColorSpace(info.colorType(), nullptr);
if (!cs) {
ERRORF(r, "%s should have a color space", path);
return;
}
auto expected = SkColorSpace::MakeRGB(SkNamedTransferFn::kSRGB, SkNamedGamut::kDisplayP3);
REPORTER_ASSERT(r, SkColorSpace::Equals(cs.get(), expected.get()));
}
DEF_TEST(AndroidCodec_P3, r) {
if (GetResourcePath().isEmpty()) {
return;
}
const char* path = "images/purple-displayprofile.png";
auto data = GetResourceAsData(path);
if (!data) {
ERRORF(r, "Missing file %s", path);
return;
}
auto codec = SkAndroidCodec::MakeFromCodec(SkCodec::MakeFromData(std::move(data)));
if (!codec) {
ERRORF(r, "Failed to create codec from %s", path);
return;
}
auto info = codec->getInfo();
auto cs = codec->computeOutputColorSpace(info.colorType(), nullptr);
if (!cs) {
ERRORF(r, "%s should have a color space", path);
return;
}
REPORTER_ASSERT(r, !cs->isSRGB());
REPORTER_ASSERT(r, cs->gammaCloseToSRGB());
skcms_Matrix3x3 matrix;
cs->toXYZD50(&matrix);
static constexpr skcms_Matrix3x3 kExpected = {{
{ 0.426254272f, 0.369018555f, 0.168914795f },
{ 0.226013184f, 0.685974121f, 0.0880126953f },
{ 0.0116729736f, 0.0950927734f, 0.71812439f },
}};
REPORTER_ASSERT(r, 0 == memcmp(&matrix, &kExpected, sizeof(skcms_Matrix3x3)));
}
DEF_TEST(AndroidCodec_orientation, r) {
if (GetResourcePath().isEmpty()) {
return;
}
for (const char* filePathFormatStr : {"images/orientation/%c_420.jpg",
"images/orientation/%c.webp"})
for (char i = '1'; i <= '8'; ++i) {
SkString path = SkStringPrintf(filePathFormatStr, i);
auto data = GetResourceAsData(path.c_str());
auto gen = SkCodecImageGenerator::MakeFromEncodedCodec(data);
if (!gen) {
ERRORF(r, "failed to decode %s", path.c_str());
return;
}
// Dimensions after adjusting for the origin.
const SkISize expectedDims = { 100, 80 };
// SkCodecImageGenerator automatically adjusts for the origin.
REPORTER_ASSERT(r, gen->getInfo().dimensions() == expectedDims);
auto androidCodec = SkAndroidCodec::MakeFromCodec(SkCodec::MakeFromData(data));
if (!androidCodec) {
ERRORF(r, "failed to decode %s", path.c_str());
return;
}
// SkAndroidCodec does not adjust for the origin by default. Dimensions may be reversed.
if (SkPixmapPriv::ShouldSwapWidthHeight(androidCodec->codec()->getOrigin())) {
auto swappedDims = SkPixmapPriv::SwapWidthHeight(androidCodec->getInfo()).dimensions();
REPORTER_ASSERT(r, expectedDims == swappedDims);
} else {
REPORTER_ASSERT(r, expectedDims == androidCodec->getInfo().dimensions());
}
// Passing kRespect adjusts for the origin.
androidCodec = SkAndroidCodec::MakeFromCodec(SkCodec::MakeFromData(std::move(data)),
SkAndroidCodec::ExifOrientationBehavior::kRespect);
auto info = androidCodec->getInfo();
REPORTER_ASSERT(r, info.dimensions() == expectedDims);
SkBitmap fromGenerator;
fromGenerator.allocPixels(info);
REPORTER_ASSERT(r, gen->getPixels(info, fromGenerator.getPixels(),
fromGenerator.rowBytes()));
SkBitmap fromAndroidCodec;
fromAndroidCodec.allocPixels(info);
auto result = androidCodec->getPixels(info, fromAndroidCodec.getPixels(),
fromAndroidCodec.rowBytes());
REPORTER_ASSERT(r, result == SkCodec::kSuccess);
for (int i = 0; i < info.width(); ++i)
for (int j = 0; j < info.height(); ++j) {
SkColor c1 = *fromGenerator .getAddr32(i, j);
SkColor c2 = *fromAndroidCodec.getAddr32(i, j);
if (c1 != c2) {
ERRORF(r, "Bitmaps for %s do not match starting at position %i, %i\n"
"\tfromGenerator: %x\tfromAndroidCodec: %x", path.c_str(), i, j,
c1, c2);
return;
}
}
}
}
DEF_TEST(AndroidCodec_sampledOrientation, r) {
if (GetResourcePath().isEmpty()) {
return;
}
// kRightTop_SkEncodedOrigin = 6, // Rotated 90 CW
auto path = "images/orientation/6_420.jpg";
auto data = GetResourceAsData(path);
if (!data) {
ERRORF(r, "Failed to get resource %s", path);
return;
}
auto androidCodec = SkAndroidCodec::MakeFromCodec(SkCodec::MakeFromData(std::move(data)),
SkAndroidCodec::ExifOrientationBehavior::kRespect);
constexpr int sampleSize = 7;
auto sampledDims = androidCodec->getSampledDimensions(sampleSize);
SkAndroidCodec::AndroidOptions options;
options.fSampleSize = sampleSize;
SkBitmap bm;
auto info = androidCodec->getInfo().makeDimensions(sampledDims);
bm.allocPixels(info);
auto result = androidCodec->getAndroidPixels(info, bm.getPixels(), bm.rowBytes(), &options);
if (result != SkCodec::kSuccess) {
ERRORF(r, "got result \"%s\"\n", SkCodec::ResultToString(result));
}
}
DEF_TEST(AndroidCodec_animatedOrientation, r) {
if (GetResourcePath().isEmpty()) {
return;
}
static const struct {
const char* path;
SkEncodedOrigin origin;
} gRec[] = {
{ "images/stoplight.webp", kDefault_SkEncodedOrigin },
{ "images/stoplight_h.webp", kLeftBottom_SkEncodedOrigin } // Rotated 90 CCW
};
for (auto rec : gRec) {
auto data = GetResourceAsData(rec.path);
if (!data) {
ERRORF(r, "Failed to get resource %s", rec.path);
return;
}
// SkAndroidCodec now allows decoding frames beyond the first, but combining this with
// kRespect-ing a non-kDefault_SkEncodedOrigin is not supported. If a frame depends on
// a prior frame, we allow a client to provide that prior frame. But in order to respect
// the origin, we would need to transform the prior frame back to the original orientation
// in order to blend (and potentially erase, for a kRestoreBG frame) just to transform it
// back. Instead, force the client to handle the orientation after the fact.
for (auto behavior : { SkAndroidCodec::ExifOrientationBehavior::kRespect,
SkAndroidCodec::ExifOrientationBehavior::kIgnore }) {
auto androidCodec = SkAndroidCodec::MakeFromCodec(SkCodec::MakeFromData(data),
behavior);
REPORTER_ASSERT(r, androidCodec->codec()->getOrigin() == rec.origin);
auto info = androidCodec->getInfo();
SkBitmap bm;
bm.allocPixels(info);
auto result = androidCodec->getAndroidPixels(info, bm.getPixels(), bm.rowBytes());
REPORTER_ASSERT(r, result == SkCodec::kSuccess);
SkAndroidCodec::AndroidOptions options;
options.fFrameIndex = 1;
options.fPriorFrame = 0;
result = androidCodec->getAndroidPixels(info, bm.getPixels(), bm.rowBytes(), &options);
switch (behavior) {
case SkAndroidCodec::ExifOrientationBehavior::kRespect:
if (rec.origin != kDefault_SkEncodedOrigin) {
REPORTER_ASSERT(r, result == SkCodec::kInvalidParameters,
"Should not be able to decode frame 1 with exif orientation"
" directly! Result: %s", SkCodec::ResultToString(result));
break;
}
[[fallthrough]];
case SkAndroidCodec::ExifOrientationBehavior::kIgnore:
REPORTER_ASSERT(r, result == SkCodec::kSuccess);
break;
}
}
}
}