* Copyright 2024 Google LLC.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "experimental/rust_png/decoder/SkPngRustDecoder.h"
#include "include/codec/SkCodec.h"
#include "include/codec/SkCodecAnimation.h"
#include "include/core/SkBitmap.h"
#include "include/core/SkColor.h"
#include "include/core/SkData.h"
#include "include/core/SkImage.h"
#include "include/core/SkImageInfo.h"
#include "include/core/SkPixmap.h"
#include "include/core/SkRefCnt.h"
#include "include/core/SkStream.h"
#include "tests/FakeStreams.h"
#include "tests/Test.h"
#include "tools/Resources.h"
#include <memory>
#include <utility>
#define REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, actualResult) \
REPORTER_ASSERT(r, \
actualResult == SkCodec::kSuccess, \
"actualResult=\"%s\" != kSuccess", \
SkCodec::ResultToString(actualResult))
std::unique_ptr<SkCodec> SkPngRustDecoderDecode(skiatest::Reporter* r, const char* path) {
sk_sp<SkData> data = GetResourceAsData(path);
if (!data) {
ERRORF(r, "Missing resource: %s", path);
return nullptr;
}
SkCodec::Result result;
std::unique_ptr<SkCodec> codec =
SkPngRustDecoder::Decode(std::make_unique<SkMemoryStream>(std::move(data)), &result);
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result);
return codec;
}
void AssertPixelColor(skiatest::Reporter* r,
const SkPixmap& pixmap,
int x,
int y,
SkColor expectedColor,
const char* description) {
SkASSERT(r);
SkASSERT(x >= 0);
SkASSERT(y >= 0);
SkASSERT(description);
REPORTER_ASSERT(r, x < pixmap.width(), "x=%d >= width=%d", x, pixmap.width());
REPORTER_ASSERT(r, y < pixmap.height(), "y=%d >= height=%d", y, pixmap.height());
REPORTER_ASSERT(r,
kN32_SkColorType == pixmap.colorType(),
"kN32_SkColorType != pixmap.ColorType()=%d",
pixmap.colorType());
SkColor actualColor = pixmap.getColor(x, y);
REPORTER_ASSERT(r,
actualColor == expectedColor,
"actualColor=0x%08X != expectedColor==0x%08X at (%d,%d) (%s)",
actualColor,
expectedColor,
x,
y,
description);
}
void AssertGreenPixel(skiatest::Reporter* r,
const SkPixmap& pixmap,
int x,
int y,
const char* description = "Expecting a green pixel") {
AssertPixelColor(r, pixmap, x, y, SkColorSetRGB(0x00, 0xFF, 0x00), description);
}
void AssertRedPixel(skiatest::Reporter* r,
const SkPixmap& pixmap,
int x,
int y,
const char* description = "Expecting a red pixel") {
AssertPixelColor(r, pixmap, x, y, SkColorSetRGB(0xFF, 0x00, 0x00), description);
}
void AssertBluePixel(skiatest::Reporter* r,
const SkPixmap& pixmap,
int x,
int y,
const char* description = "Expecting a blue pixel") {
AssertPixelColor(r, pixmap, x, y, SkColorSetRGB(0x00, 0x00, 0xFF), description);
}
void AssertSingleGreenFrame(skiatest::Reporter* r,
int expectedWidth,
int expectedHeight,
const char* resourcePath) {
std::unique_ptr<SkCodec> codec = SkPngRustDecoderDecode(r, resourcePath);
if (!codec) {
return;
}
REPORTER_ASSERT(r, codec->getFrameCount() == 1);
REPORTER_ASSERT(r, codec->getRepetitionCount() == 0);
SkCodec::FrameInfo info;
REPORTER_ASSERT(r, codec->getFrameInfo(0, &info));
REPORTER_ASSERT(r, info.fBlend == SkCodecAnimation::Blend::kSrc);
REPORTER_ASSERT(r, info.fDisposalMethod == SkCodecAnimation::DisposalMethod::kKeep);
REPORTER_ASSERT(r, info.fFrameRect == SkIRect::MakeWH(expectedWidth, expectedHeight));
REPORTER_ASSERT(r, info.fRequiredFrame == SkCodec::kNoFrame);
auto [image, result] = codec->getImage();
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result);
REPORTER_ASSERT(r, image);
REPORTER_ASSERT(r,
image->width() == expectedWidth,
"actualWidth=%d != expectedWidth=%d",
image->width(),
expectedWidth);
REPORTER_ASSERT(r,
image->height() == expectedHeight,
"actualHeight=%d != expectedHeight=%d",
image->height(),
expectedHeight);
SkPixmap pixmap;
REPORTER_ASSERT(r, image->peekPixels(&pixmap));
AssertGreenPixel(r, pixmap, 0, 0);
AssertGreenPixel(r, pixmap, expectedWidth / 2, expectedHeight / 2);
}
sk_sp<SkImage> DecodeLastFrame(skiatest::Reporter* r, SkCodec* codec) {
int frameCount = codec->getFrameCount();
sk_sp<SkImage> image;
SkCodec::Result result = SkCodec::kSuccess;
for (int i = 0; i < frameCount; i++) {
SkCodec::FrameInfo info;
REPORTER_ASSERT(r, codec->getFrameInfo(i, &info));
SkASSERT(info.fDisposalMethod == SkCodecAnimation::DisposalMethod::kKeep);
if (!image) {
std::tie(image, result) = codec->getImage();
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result);
if (result != SkCodec::kSuccess) {
return nullptr;
}
} else {
SkPixmap pixmap;
REPORTER_ASSERT(r, image->peekPixels(&pixmap));
SkCodec::Options options;
options.fZeroInitialized = SkCodec::kNo_ZeroInitialized;
options.fSubset = nullptr;
options.fFrameIndex = i;
options.fPriorFrame = i - 1;
result = codec->getPixels(pixmap, &options);
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result);
if (result != SkCodec::kSuccess) {
return nullptr;
}
}
}
return image;
}
sk_sp<SkImage> DecodeLastFrame(skiatest::Reporter* r, const char* resourcePath) {
std::unique_ptr<SkCodec> codec = SkPngRustDecoderDecode(r, resourcePath);
if (!codec) {
return nullptr;
}
return DecodeLastFrame(r, codec.get());
}
void AssertAnimationRepetitionCount(skiatest::Reporter* r,
int expectedRepetitionCount,
const char* resourcePath) {
std::unique_ptr<SkCodec> codec = SkPngRustDecoderDecode(r, resourcePath);
if (!codec) {
return;
}
int actualRepetitionCount = codec->getRepetitionCount();
REPORTER_ASSERT(r,
actualRepetitionCount == expectedRepetitionCount,
"actualRepetitionCount=%d != expectedRepetitionCount=%d",
actualRepetitionCount,
expectedRepetitionCount);
}
DEF_TEST(RustPngCodec_apng_basic_trivial_static_image, r) {
AssertSingleGreenFrame(r, 128, 64, "images/apng-test-suite--basic--trivial-static-image.png");
}
DEF_TEST(RustPngCodec_apng_basic_using_default_image, r) {
AssertSingleGreenFrame(r, 128, 64, "images/apng-test-suite--basic--using-default-image.png");
}
DEF_TEST(RustPngCodec_apng_basic_ignoring_default_image, r) {
AssertSingleGreenFrame(r, 128, 64, "images/apng-test-suite--basic--ignoring-default-image.png");
}
DEF_TEST(RustPngCodec_apng_dispose_op_none_basic, r) {
std::unique_ptr<SkCodec> codec =
SkPngRustDecoderDecode(r, "images/apng-test-suite--dispose-ops--none-basic.png");
if (!codec) {
return;
}
REPORTER_ASSERT(r, codec->getFrameCount() == 3);
REPORTER_ASSERT(r, codec->getRepetitionCount() == 0);
SkCodec::FrameInfo info[3];
REPORTER_ASSERT(r, codec->getFrameInfo(0, &info[0]));
REPORTER_ASSERT(r, codec->getFrameInfo(1, &info[1]));
REPORTER_ASSERT(r, codec->getFrameInfo(2, &info[2]));
REPORTER_ASSERT(r, info[0].fFullyReceived);
REPORTER_ASSERT(r, info[1].fFullyReceived);
REPORTER_ASSERT(r, !info[2].fFullyReceived);
REPORTER_ASSERT(r, info[1].fAlphaType == kUnpremul_SkAlphaType);
REPORTER_ASSERT(r, info[1].fBlend == SkCodecAnimation::Blend::kSrcOver);
REPORTER_ASSERT(r, info[1].fDisposalMethod == SkCodecAnimation::DisposalMethod::kKeep);
REPORTER_ASSERT(r, info[1].fDuration == 100, "dur = %d", info[1].fDuration);
REPORTER_ASSERT(r, info[1].fFrameRect == SkIRect::MakeWH(128, 64));
REPORTER_ASSERT(r, info[1].fHasAlphaWithinBounds);
REPORTER_ASSERT(r, info[1].fRequiredFrame == 0);
auto [image, result] = codec->getImage();
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result);
REPORTER_ASSERT(r, image);
REPORTER_ASSERT(r, image->width() == 128, "width %d != 128", image->width());
REPORTER_ASSERT(r, image->height() == 64, "height %d != 64", image->height());
SkPixmap pixmap;
REPORTER_ASSERT(r, image->peekPixels(&pixmap));
AssertRedPixel(r, pixmap, 0, 0, "Frame #0 should be red");
SkCodec::Options options;
options.fZeroInitialized = SkCodec::kNo_ZeroInitialized;
options.fSubset = nullptr;
options.fFrameIndex = 1;
options.fPriorFrame = 0;
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, codec->getPixels(pixmap, &options));
AssertGreenPixel(r, pixmap, 0, 0, "Frame #1 should be green");
options.fFrameIndex = 2;
options.fPriorFrame = 1;
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, codec->getPixels(pixmap, &options));
AssertGreenPixel(r, pixmap, 0, 0, "Frame #2 should be green");
}
DEF_TEST(RustPngCodec_apng_dispose_op_none_basic_incomplete_input1, r) {
const char* path = "images/apng-test-suite--dispose-ops--none-basic.png";
sk_sp<SkData> data = GetResourceAsData(path);
if (!data) {
ERRORF(r, "Missing resource: %s", path);
return;
}
size_t fullLength = data->size();
constexpr size_t kInitialBytes = 0xAD;
auto streamForCodec = std::make_unique<HaltingStream>(std::move(data), kInitialBytes);
HaltingStream* retainedStream = streamForCodec.get();
SkCodec::Result result;
std::unique_ptr<SkCodec> codec = SkPngRustDecoder::Decode(std::move(streamForCodec), &result);
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result);
if (!codec) {
return;
}
SkBitmap bitmap;
if (!bitmap.tryAllocN32Pixels(codec->dimensions().width(), codec->dimensions().height())) {
ERRORF(r, "Failed to allocate SkBitmap");
return;
}
int frameCount = codec->getFrameCount();
REPORTER_ASSERT(r, frameCount == 1);
retainedStream->addNewData(fullLength);
sk_sp<SkImage> image;
std::tie(image, result) = codec->getImage();
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result);
REPORTER_ASSERT(r, image);
REPORTER_ASSERT(r, image->width() == 128, "width %d != 128", image->width());
REPORTER_ASSERT(r, image->height() == 64, "height %d != 64", image->height());
SkPixmap pixmap;
REPORTER_ASSERT(r, image->peekPixels(&pixmap));
AssertRedPixel(r, pixmap, 0, 0, "Frame #0 should be red");
}
DEF_TEST(RustPngCodec_apng_dispose_op_none_basic_incomplete_input2, r) {
const char* path = "images/apng-test-suite--dispose-ops--none-basic.png";
sk_sp<SkData> data = GetResourceAsData(path);
if (!data) {
ERRORF(r, "Missing resource: %s", path);
return;
}
size_t fullLength = data->size();
constexpr size_t kInitialBytes = 0x8D;
auto streamForCodec = std::make_unique<HaltingStream>(std::move(data), kInitialBytes);
HaltingStream* retainedStream = streamForCodec.get();
SkCodec::Result result;
std::unique_ptr<SkCodec> codec = SkPngRustDecoder::Decode(std::move(streamForCodec), &result);
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result);
if (!codec) {
return;
}
SkBitmap bitmap;
if (!bitmap.tryAllocN32Pixels(codec->dimensions().width(), codec->dimensions().height())) {
ERRORF(r, "Failed to allocate SkBitmap");
return;
}
const SkPixmap& pixmap = bitmap.pixmap();
bitmap.erase(SkColorSetRGB(0, 0, 0xFF), bitmap.bounds());
AssertBluePixel(r, pixmap, 0, 0);
AssertBluePixel(r, pixmap, 40, 29);
AssertBluePixel(r, pixmap, 80, 35);
AssertBluePixel(r, pixmap, 127, 63);
SkCodec::Options options;
options.fZeroInitialized = SkCodec::kNo_ZeroInitialized;
options.fSubset = nullptr;
options.fFrameIndex = 0;
options.fPriorFrame = SkCodec::kNoFrame;
result = codec->startIncrementalDecode(bitmap.pixmap().info(),
bitmap.pixmap().writable_addr(),
bitmap.pixmap().rowBytes(),
&options);
REPORTER_ASSERT(r, result == SkCodec::kSuccess);
int rowsDecoded = -1;
result = codec->incrementalDecode(&rowsDecoded);
REPORTER_ASSERT(r, result == SkCodec::kIncompleteInput);
REPORTER_ASSERT(r, rowsDecoded == 10, "actual rowsDecoded = %d", rowsDecoded);
AssertRedPixel(r, pixmap, 0, 0);
AssertBluePixel(r, pixmap, 40, 29);
AssertBluePixel(r, pixmap, 80, 35);
AssertBluePixel(r, pixmap, 127, 63);
retainedStream->addNewData(fullLength);
int frameCount = codec->getFrameCount();
REPORTER_ASSERT(r, frameCount == 1);
rowsDecoded = -1;
result = codec->incrementalDecode(&rowsDecoded);
REPORTER_ASSERT(r, result == SkCodec::kSuccess);
REPORTER_ASSERT(r, rowsDecoded == -1);
AssertRedPixel(r, pixmap, 0, 0);
AssertRedPixel(r, pixmap, 40, 29);
AssertRedPixel(r, pixmap, 80, 35);
AssertRedPixel(r, pixmap, 127, 63);
}
DEF_TEST(RustPngCodec_apng_blend_ops_source_on_solid, r) {
std::unique_ptr<SkCodec> codec =
SkPngRustDecoderDecode(r, "images/apng-test-suite--blend-ops--source-on-solid.png");
if (!codec) {
return;
}
sk_sp<SkImage> image = DecodeLastFrame(r, codec.get());
if (!image) {
return;
}
SkPixmap pixmap;
REPORTER_ASSERT(r, image->peekPixels(&pixmap));
AssertGreenPixel(r, pixmap, 0, 0);
SkCodec::FrameInfo info;
REPORTER_ASSERT(r, codec->getFrameInfo(1, &info));
REPORTER_ASSERT(r, info.fBlend == SkCodecAnimation::Blend::kSrc);
REPORTER_ASSERT(r, info.fRequiredFrame == SkCodec::kNoFrame);
}
DEF_TEST(RustPngCodec_apng_blend_ops_source_on_nearly_transparent, r) {
sk_sp<SkImage> image = DecodeLastFrame(
r, "images/apng-test-suite--blend-ops--source-on-nearly-transparent.png");
if (!image) {
return;
}
SkPixmap pixmap;
REPORTER_ASSERT(r, image->peekPixels(&pixmap));
AssertPixelColor(r,
pixmap,
0,
0,
SkColorSetARGB(0x02, 0x00, 0xFF, 0x00),
"Expecting a nearly transparent pixel");
}
DEF_TEST(RustPngCodec_apng_blend_ops_over_on_solid_and_transparent, r) {
sk_sp<SkImage> image = DecodeLastFrame(
r, "images/apng-test-suite--blend-ops--over-on-solid-and-transparent.png");
if (!image) {
return;
}
SkPixmap pixmap;
REPORTER_ASSERT(r, image->peekPixels(&pixmap));
AssertGreenPixel(r, pixmap, 0, 0);
}
DEF_TEST(RustPngCodec_apng_blend_ops_over_repeatedly, r) {
sk_sp<SkImage> image =
DecodeLastFrame(r, "images/apng-test-suite--blend-ops--over-repeatedly.png");
if (!image) {
return;
}
SkPixmap pixmap;
REPORTER_ASSERT(r, image->peekPixels(&pixmap));
AssertGreenPixel(r, pixmap, 0, 0);
}
DEF_TEST(RustPngCodec_apng_regions_dispose_op_none, r) {
sk_sp<SkImage> image =
DecodeLastFrame(r, "images/apng-test-suite--regions--dispose-op-none.png");
if (!image) {
return;
}
SkPixmap pixmap;
REPORTER_ASSERT(r, image->peekPixels(&pixmap));
for (int y = 0; y < pixmap.height(); y++) {
for (int x = 0; x < pixmap.width(); x++) {
AssertGreenPixel(r, pixmap, x, y);
}
}
}
DEF_TEST(RustPngCodec_apng_num_plays_0, r) {
AssertAnimationRepetitionCount(
r, SkCodec::kRepetitionCountInfinite, "images/apng-test-suite--num-plays--0.png");
}
DEF_TEST(RustPngCodec_apng_num_plays_1, r) {
AssertAnimationRepetitionCount(r, 0, "images/apng-test-suite--num-plays--1.png");
}
DEF_TEST(RustPngCodec_apng_num_plays_2, r) {
AssertAnimationRepetitionCount(r, 1, "images/apng-test-suite--num-plays--2.png");
}
DEF_TEST(RustPngCodec_apng_invalid_num_frames_outside_valid_range, r) {
std::unique_ptr<SkCodec> codec = SkPngRustDecoderDecode(
r, "images/apng-test-suite--invalid--num-frames-outside-valid-range.png");
if (!codec) {
return;
}
REPORTER_ASSERT(r, codec->getFrameCount() == 1);
}
DEF_TEST(RustPngCodec_png_swizzling_target_unimplemented, r) {
std::unique_ptr<SkCodec> codec =
SkPngRustDecoderDecode(r, "images/apng-test-suite--basic--ignoring-default-image.png");
if (!codec) {
return;
}
REPORTER_ASSERT(r, codec->getFrameCount() == 1);
SkImageInfo dstInfo = SkImageInfo::Make(
codec->dimensions(), kBGRA_10101010_XR_SkColorType, kPremul_SkAlphaType);
auto [image, result] = codec->getImage(dstInfo);
REPORTER_ASSERT(r, result == SkCodec::kUnimplemented);
REPORTER_ASSERT(r, !image);
}
DEF_TEST(RustPngCodec_png_was_encoded_with_16_bits_or_more_per_component, r) {
struct Test {
const char* fFilename;
bool fEncodedWith16bits;
};
const std::array<Test, 4> kTests = {
Test{"images/pngsuite/basn0g04.png", false},
Test{"images/pngsuite/basn2c08.png", false},
Test{"images/pngsuite/basn2c16.png", true},
Test{"images/pngsuite/basn3p01.png", false}
};
for (const auto& test : kTests) {
std::unique_ptr<SkCodec> codec = SkPngRustDecoderDecode(r, test.fFilename);
if (codec) {
REPORTER_ASSERT(r, codec->hasHighBitDepthEncodedData() == test.fEncodedWith16bits);
}
}
}
DEF_TEST(RustPngCodec_png_cicp, r) {
std::unique_ptr<SkCodec> codec = SkPngRustDecoderDecode(r, "images/cicp_pq.png");
if (!codec) {
return;
}
const skcms_ICCProfile* profile = codec->getICCProfile();
REPORTER_ASSERT(r, profile);
if (!profile) {
return;
}
REPORTER_ASSERT(r, skcms_TransferFunction_isPQish(&profile->trc[0].parametric));
}