blob: 92dc18450cc3d4717079f4a525176ab08259c8ae [file]
/*
* Copyright 2026 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_jpeg/decoder/SkJpegRustDecoder.h"
#include "experimental/rust_jpeg/decoder/impl/SkJpegRustCodec.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstring>
#include <functional>
#include <memory>
#include <utility>
#include <vector>
#include "include/codec/SkCodec.h"
#include "include/core/SkBitmap.h"
#include "include/core/SkColor.h"
#include "include/core/SkColorSpace.h"
#include "include/core/SkColorType.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 "include/core/SkYUVAInfo.h"
#include "include/core/SkYUVAPixmaps.h"
#include "include/private/SkFeatures.h"
#include "rust/common/SkStreamAdapter.h"
#include "tests/ComparePixels.h"
#include "tests/FakeStreams.h"
#include "tests/Test.h"
#include "tools/DecodeUtils.h"
#include "tools/Resources.h"
#if defined(SK_CODEC_ENCODES_JPEG_WITH_RUST)
#include "experimental/rust_jpeg/encoder/SkJpegRustEncoder.h"
#include "include/encode/SkJpegEncoder.h"
#include "tools/ToolUtils.h"
#endif
#if defined(SK_CODEC_DECODES_JPEG_GAINMAPS)
#include "include/codec/SkAndroidCodec.h"
#include "include/private/SkGainmapInfo.h"
#endif
#if defined(SK_CODEC_DECODES_JPEG)
#include "include/codec/SkEncodedOrigin.h"
#include "include/codec/SkJpegDecoder.h"
#include "modules/skcms/skcms.h"
#endif
#define REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, actualResult) \
REPORTER_ASSERT(r, \
(actualResult) == SkCodec::kSuccess, \
"actualResult=\"%s\" != kSuccess", \
SkCodec::ResultToString(actualResult))
// Helper wrapping a call to `SkJpegRustDecoder::Decode`.
static std::unique_ptr<SkCodec> decode_rust_jpeg(skiatest::Reporter* r, const char* path) {
skiatest::ReporterContext ctx(r, 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 =
SkJpegRustDecoder::Decode(SkMemoryStream::Make(std::move(data)), &result);
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result);
return codec;
}
static std::unique_ptr<SkCodec> decode_rust_jpeg_data(sk_sp<SkData> data,
SkCodec::Result* result = nullptr) {
return SkJpegRustDecoder::Decode(SkMemoryStream::Make(std::move(data)), result);
}
static rust::Box<rust_jpeg::Reader> make_rust_jpeg_reader_for_test(SkStream* stream,
uint64_t streamLength) {
auto reader = rust_jpeg::new_reader(std::make_unique<rust::stream::SkStreamAdapter>(stream),
streamLength);
SkASSERT_RELEASE(reader->initialization_result() == rust_jpeg::DecodingResult::Success);
return reader;
}
struct RustJpegReaderForTest {
explicit RustJpegReaderForTest(std::unique_ptr<SkStream> stream, uint64_t streamLength)
: fStream(std::move(stream))
, fReader(make_rust_jpeg_reader_for_test(fStream.get(), streamLength)) {}
std::unique_ptr<SkStream> fStream;
rust::Box<rust_jpeg::Reader> fReader;
};
// ========== Decoder Tests ==========
DEF_TEST(RustJpegCodec_IsJpeg, r) {
sk_sp<SkData> jpegData = GetResourceAsData("images/color_wheel.jpg");
if (!jpegData) {
ERRORF(r, "Missing resource: images/color_wheel.jpg");
return;
}
REPORTER_ASSERT(r, SkJpegRustDecoder::IsJpeg(jpegData->data(), jpegData->size()));
sk_sp<SkData> pngData = GetResourceAsData("images/color_wheel.png");
if (!pngData) {
ERRORF(r, "Missing resource: images/color_wheel.png");
return;
}
REPORTER_ASSERT(r, !SkJpegRustDecoder::IsJpeg(pngData->data(), pngData->size()));
}
// Decode a variety of JPEG images covering baseline, progressive, grayscale,
// different subsampling modes, ICC profiles, and other edge cases.
DEF_TEST(RustJpegCodec_decode_valid_jpgs, r) {
struct TestCase {
const char* path;
SkISize expectedDimensions;
};
static const TestCase kTestCases[] = {
{"images/color_wheel.jpg", SkISize::Make(128, 128)},
{"images/grayscale.jpg", SkISize::Make(128, 128)},
{"images/dog.jpg", SkISize::Make(180, 180)},
{"images/b78329453.jpeg", SkISize::Make(635, 760)},
{"images/mandrill_512_q075.jpg", SkISize::Make(512, 512)},
{"images/ducky.jpg", SkISize::Make(489, 537)},
{"images/mandrill_h1v1.jpg", SkISize::Make(512, 512)},
{"images/mandrill_h2v1.jpg", SkISize::Make(512, 512)},
{"images/cropped_mandrill.jpg", SkISize::Make(439, 154)},
{"images/flutter_logo.jpg", SkISize::Make(400, 400)},
{"images/icc-v2-gbr.jpg", SkISize::Make(275, 207)},
{"images/randPixels.jpg", SkISize::Make(8, 8)},
{"images/crbug999986.jpeg", SkISize::Make(2000, 1500)},
{"images/CMYK.jpg", SkISize::Make(642, 516)},
};
for (const auto& tc : kTestCases) {
skiatest::ReporterContext ctx(r, tc.path);
std::unique_ptr<SkCodec> codec = decode_rust_jpeg(r, tc.path);
if (!codec) {
continue;
}
REPORTER_ASSERT(r, codec->getFrameCount() == 1);
REPORTER_ASSERT(r,
codec->dimensions() == tc.expectedDimensions,
"dimensions=%dx%d != expected=%dx%d",
codec->dimensions().width(),
codec->dimensions().height(),
tc.expectedDimensions.width(),
tc.expectedDimensions.height());
auto [image, result] = codec->getImage();
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result);
REPORTER_ASSERT(r, image);
}
}
// Verify true incremental resumption with a HaltingStream: start decoding with
// incomplete input, observe kIncompleteInput, progressively expose more of the
// same stream, and complete without restarting the decoder.
DEF_TEST(RustJpeg_IncrementalDecode_PartialStreaming, r) {
struct TestCase {
const char* path;
const char* description;
bool expectProgressivePreview;
};
static const TestCase kTestCases[] = {
{"images/color_wheel.jpg", "baseline RGB 128x128", false},
{"images/dog.jpg", "RGB 180x180", false},
{"images/mandrill_512_q075.jpg", "RGB 512x512", false},
{"images/b78329453.jpeg", "progressive RGB 635x760", true},
};
// JPEG headers (SOS, DQT, DHT tables...) can exceed 1 KB. This leaves
// enough bytes to construct the codec while withholding image data.
constexpr size_t kInitialBytes = 4 * 1024;
constexpr size_t kChunkSize = 4 * 1024;
constexpr int kMaxIterations = 1000;
for (const auto& testCase : kTestCases) {
skiatest::ReporterContext ctx(r, testCase.description);
sk_sp<SkData> data = GetResourceAsData(testCase.path);
if (!data) {
ERRORF(r, "Missing resource: %s", testCase.path);
continue;
}
// Decode the full image first so we can verify the incremental result.
std::unique_ptr<SkCodec> refCodec =
SkJpegRustDecoder::Decode(SkMemoryStream::Make(data), nullptr);
if (!refCodec) {
ERRORF(r, "Failed to create reference codec for %s", testCase.path);
continue;
}
SkBitmap reference;
reference.allocPixels(refCodec->getInfo().makeColorType(kN32_SkColorType));
{
SkCodec::Result refResult = refCodec->getPixels(reference.pixmap());
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, refResult);
if (refResult != SkCodec::kSuccess) {
continue;
}
}
REPORTER_ASSERT(r,
data->size() > kInitialBytes,
"%s: test image must exceed the initial stream window",
testCase.description);
if (data->size() <= kInitialBytes) {
continue;
}
// Create a HaltingStream that starts with the headers and partial image data.
const size_t initialBytes = kInitialBytes;
auto haltingStream = std::make_unique<HaltingStream>(data, initialBytes);
HaltingStream* streamPtr = haltingStream.get();
SkCodec::Result result;
std::unique_ptr<SkCodec> codec =
SkJpegRustDecoder::Decode(std::move(haltingStream), &result);
if (!codec) {
ERRORF(r,
"%s: failed to create streaming codec with %zu bytes: %s",
testCase.description,
initialBytes,
SkCodec::ResultToString(result));
continue;
}
const SkImageInfo info = codec->getInfo().makeColorType(kN32_SkColorType);
SkBitmap bitmap;
if (!bitmap.tryAllocPixels(info)) {
ERRORF(r, "%s: failed to allocate bitmap", testCase.description);
continue;
}
memset(bitmap.getPixels(), 0xAB, bitmap.computeByteSize());
result = codec->startIncrementalDecode(info, bitmap.getPixels(), bitmap.rowBytes());
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result);
if (result != SkCodec::kSuccess) {
continue;
}
int rowsDecoded = 0;
int previousRowsDecoded = 0;
int iterations = 0;
bool sawIncompleteInput = false;
bool sawPartialOutput = false;
bool checkedNoNewDataRetry = false;
int progressivePreviewCount = 0;
std::vector<uint8_t> previousPixels(bitmap.computeByteSize(), 0xAB);
while (iterations++ < kMaxIterations) {
result = codec->incrementalDecode(&rowsDecoded);
REPORTER_ASSERT(r,
rowsDecoded >= previousRowsDecoded,
"%s: decoded row count regressed from %d to %d",
testCase.description,
previousRowsDecoded,
rowsDecoded);
const uint8_t* pixels = static_cast<const uint8_t*>(bitmap.getPixels());
if (!testCase.expectProgressivePreview && previousRowsDecoded > 0) {
const size_t stableBytes =
static_cast<size_t>(previousRowsDecoded) * bitmap.rowBytes();
REPORTER_ASSERT(r,
memcmp(pixels, previousPixels.data(), stableBytes) == 0,
"%s: previously reported stable rows changed",
testCase.description);
}
if (result == SkCodec::kSuccess) {
break;
}
REPORTER_ASSERT(r,
result == SkCodec::kIncompleteInput,
"%s: partial input should return kIncompleteInput, got %s",
testCase.description,
SkCodec::ResultToString(result));
if (result != SkCodec::kIncompleteInput) {
break;
}
sawIncompleteInput = true;
const bool pixelsChanged = std::any_of(pixels,
pixels + bitmap.computeByteSize(),
[](uint8_t value) { return value != 0xAB; });
if (testCase.expectProgressivePreview) {
if (memcmp(pixels, previousPixels.data(), bitmap.computeByteSize()) != 0) {
progressivePreviewCount++;
sawPartialOutput = true;
REPORTER_ASSERT(r,
rowsDecoded > 0,
"%s: preview pixels were not reported as initialized",
testCase.description);
}
} else if (rowsDecoded > 0) {
sawPartialOutput = true;
REPORTER_ASSERT(r,
pixelsChanged,
"%s: reported stable rows were not written",
testCase.description);
}
for (int y = 0; y < rowsDecoded; ++y) {
for (int x = 0; x < info.width(); ++x) {
REPORTER_ASSERT(r,
SkColorGetA(bitmap.getColor(x, y)) == SK_AlphaOPAQUE,
"%s: initialized pixel (%d,%d) is not opaque",
testCase.description,
x,
y);
}
}
previousRowsDecoded = rowsDecoded;
memcpy(previousPixels.data(), pixels, bitmap.computeByteSize());
if (!checkedNoNewDataRetry && sawPartialOutput) {
int retryRowsDecoded = -1;
const SkCodec::Result retryResult = codec->incrementalDecode(&retryRowsDecoded);
REPORTER_ASSERT(r,
retryResult == SkCodec::kIncompleteInput,
"%s: retry without new data returned %s",
testCase.description,
SkCodec::ResultToString(retryResult));
REPORTER_ASSERT(r,
retryRowsDecoded == rowsDecoded,
"%s: retry changed initialized rows from %d to %d",
testCase.description,
rowsDecoded,
retryRowsDecoded);
REPORTER_ASSERT(
r,
memcmp(pixels, previousPixels.data(), bitmap.computeByteSize()) == 0,
"%s: retry without new data changed output",
testCase.description);
checkedNoNewDataRetry = true;
}
if (streamPtr->isAllDataReceived()) {
ERRORF(r,
"%s: still incomplete after all data was available",
testCase.description);
break;
}
// Do not call startIncrementalDecode() again. Each subsequent call
// must resume the existing Rust/zune decoder after one more chunk
// becomes available.
streamPtr->addNewData(kChunkSize);
}
REPORTER_ASSERT(r,
iterations < kMaxIterations,
"%s: decode loop exceeded max iterations",
testCase.description);
REPORTER_ASSERT(r,
sawIncompleteInput,
"%s: decode never observed incomplete input",
testCase.description);
REPORTER_ASSERT(r,
sawPartialOutput,
"%s: no partial output was exposed before all input arrived",
testCase.description);
REPORTER_ASSERT(r,
checkedNoNewDataRetry,
"%s: retry without new input was not exercised",
testCase.description);
if (testCase.expectProgressivePreview) {
REPORTER_ASSERT(r,
progressivePreviewCount >= 2,
"%s: expected multiple progressive preview replacements, got %d",
testCase.description,
progressivePreviewCount);
}
REPORTER_ASSERT(r,
result == SkCodec::kSuccess,
"%s: resumed decode expected kSuccess, got %s",
testCase.description,
SkCodec::ResultToString(result));
REPORTER_ASSERT(r,
rowsDecoded == info.height(),
"%s: resumed decode reported %d of %d rows",
testCase.description,
rowsDecoded,
info.height());
// Verify dimensions match the reference.
REPORTER_ASSERT(r,
bitmap.dimensions() == reference.dimensions(),
"%s: incremental dims %dx%d != reference %dx%d",
testCase.description,
bitmap.width(),
bitmap.height(),
reference.width(),
reference.height());
// Both paths used the same Rust decoder, so pixel output should be
// identical (zero tolerance).
if (bitmap.dimensions() == reference.dimensions()) {
const float tols[4] = {0, 0, 0, 0};
auto error = std::function<ComparePixmapsErrorReporter>(
[&](int x, int y, const float diffs[4]) {
ERRORF(r,
"%s: pixel differs at (%d,%d): diffs=(%f,%f,%f,%f)",
testCase.description,
x,
y,
diffs[0],
diffs[1],
diffs[2],
diffs[3]);
});
ComparePixels(bitmap.pixmap(), reference.pixmap(), tols, error);
}
}
}
DEF_TEST(RustJpeg_IncrementalDecode_RejectsSubset, r) {
std::unique_ptr<SkCodec> codec = decode_rust_jpeg(r, "images/color_wheel.jpg");
if (!codec) {
return;
}
const SkImageInfo info = codec->getInfo().makeColorType(kN32_SkColorType);
const SkIRect subset = SkIRect::MakeXYWH(0, 1, info.width(), info.height() - 1);
SkBitmap bitmap;
bitmap.allocPixels(info.makeWH(subset.width(), subset.height()));
SkCodec::Options options;
options.fSubset = &subset;
const SkCodec::Result result =
codec->startIncrementalDecode(info, bitmap.getPixels(), bitmap.rowBytes(), &options);
REPORTER_ASSERT(r,
result == SkCodec::kInvalidParameters,
"incremental subset decode returned %s",
SkCodec::ResultToString(result));
}
DEF_TEST(RustJpeg_IncrementalDecode_RejectsShortRowBytes, r) {
std::unique_ptr<SkCodec> codec = decode_rust_jpeg(r, "images/color_wheel.jpg");
if (!codec) {
return;
}
const SkImageInfo info = codec->getInfo().makeColorType(kN32_SkColorType);
SkBitmap bitmap;
bitmap.allocPixels(info);
const SkCodec::Result result =
codec->startIncrementalDecode(info, bitmap.getPixels(), info.minRowBytes() - 1);
REPORTER_ASSERT(r,
result == SkCodec::kInvalidParameters,
"short incremental row bytes returned %s",
SkCodec::ResultToString(result));
}
DEF_TEST(RustJpeg_IncrementalDecode_AfterRewind, r) {
std::unique_ptr<SkCodec> codec = decode_rust_jpeg(r, "images/color_wheel.jpg");
if (!codec) {
return;
}
const SkImageInfo info = codec->getInfo().makeColorType(kN32_SkColorType);
SkBitmap firstIncremental;
firstIncremental.allocPixels(info);
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(
r,
codec->startIncrementalDecode(
info, firstIncremental.getPixels(), firstIncremental.rowBytes()));
int rowsDecoded = 0;
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, codec->incrementalDecode(&rowsDecoded));
REPORTER_ASSERT(r, rowsDecoded == info.height());
auto [fullImage, fullResult] = codec->getImage();
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, fullResult);
REPORTER_ASSERT(r, fullImage);
SkBitmap secondIncremental;
secondIncremental.allocPixels(info);
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(
r,
codec->startIncrementalDecode(
info, secondIncremental.getPixels(), secondIncremental.rowBytes()));
rowsDecoded = 0;
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, codec->incrementalDecode(&rowsDecoded));
REPORTER_ASSERT(r, rowsDecoded == info.height());
REPORTER_ASSERT(r, firstIncremental.computeByteSize() == secondIncremental.computeByteSize());
REPORTER_ASSERT(r,
memcmp(firstIncremental.getPixels(),
secondIncremental.getPixels(),
firstIncremental.computeByteSize()) == 0);
}
DEF_TEST(RustJpeg_IncrementalDecode_ColorTransformPaddedStride, r) {
// This file has more than 32 KiB of metadata before its decodable scan data.
constexpr size_t kInitialBytes = 34 * 1024;
constexpr size_t kChunkSize = 4 * 1024;
constexpr int kMaxIterations = 1000;
sk_sp<SkData> data = GetResourceAsData("images/icc-v2-gbr.jpg");
if (!data) {
ERRORF(r, "Missing resource: images/icc-v2-gbr.jpg");
return;
}
std::unique_ptr<SkCodec> referenceCodec =
SkJpegRustDecoder::Decode(SkMemoryStream::Make(data), nullptr);
REPORTER_ASSERT(r, referenceCodec);
if (!referenceCodec) {
return;
}
const SkImageInfo dstInfo = referenceCodec->getInfo()
.makeColorType(kRGBA_8888_SkColorType)
.makeColorSpace(SkColorSpace::MakeSRGB());
SkBitmap reference;
reference.allocPixels(dstInfo);
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(
r, referenceCodec->getPixels(dstInfo, reference.getPixels(), reference.rowBytes()));
auto haltingStream = std::make_unique<HaltingStream>(data, kInitialBytes);
HaltingStream* streamPtr = haltingStream.get();
SkCodec::Result result;
std::unique_ptr<SkCodec> codec = SkJpegRustDecoder::Decode(std::move(haltingStream), &result);
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result);
REPORTER_ASSERT(r, codec);
if (!codec) {
return;
}
const size_t paddedRowBytes = dstInfo.minRowBytes() + 32;
SkBitmap incremental;
incremental.allocPixels(dstInfo, paddedRowBytes);
memset(incremental.getPixels(), 0xAB, incremental.computeByteSize());
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(
r,
codec->startIncrementalDecode(
dstInfo, incremental.getPixels(), incremental.rowBytes()));
int rowsDecoded = 0;
int iterations = 0;
bool sawIncompleteInput = false;
do {
result = codec->incrementalDecode(&rowsDecoded);
if (result == SkCodec::kIncompleteInput) {
sawIncompleteInput = true;
REPORTER_ASSERT(r, !streamPtr->isAllDataReceived());
streamPtr->addNewData(kChunkSize);
}
} while (result == SkCodec::kIncompleteInput && iterations++ < kMaxIterations);
REPORTER_ASSERT(r, sawIncompleteInput);
REPORTER_ASSERT(r, iterations < kMaxIterations);
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result);
REPORTER_ASSERT(r, rowsDecoded == dstInfo.height());
const float tolerances[4] = {0, 0, 0, 0};
auto reportError =
std::function<ComparePixmapsErrorReporter>([&](int x, int y, const float diffs[4]) {
ERRORF(r,
"incremental transformed pixel differs at (%d,%d): "
"diffs=(%f,%f,%f,%f)",
x,
y,
diffs[0],
diffs[1],
diffs[2],
diffs[3]);
});
ComparePixels(reference.pixmap(), incremental.pixmap(), tolerances, reportError);
const size_t pixelBytes = dstInfo.minRowBytes();
for (int y = 0; y + 1 < dstInfo.height(); ++y) {
const uint8_t* row = static_cast<const uint8_t*>(incremental.getAddr(0, y));
REPORTER_ASSERT(r,
std::all_of(row + pixelBytes,
row + paddedRowBytes,
[](uint8_t value) { return value == 0xAB; }),
"incremental decode modified row %d padding",
y);
}
}
DEF_TEST(RustJpegCodec_rewind, r) {
std::unique_ptr<SkCodec> codec = decode_rust_jpeg(r, "images/color_wheel.jpg");
if (!codec) {
return;
}
auto [image1, result1] = codec->getImage();
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result1);
REPORTER_ASSERT(r, image1);
auto [image2, result2] = codec->getImage();
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result2);
REPORTER_ASSERT(r, image2);
REPORTER_ASSERT(r, image1->dimensions() == image2->dimensions());
}
DEF_TEST(RustJpegCodec_incomplete_input, r) {
sk_sp<SkData> data = GetResourceAsData("images/color_wheel.jpg");
if (!data) {
ERRORF(r, "Missing resource: images/color_wheel.jpg");
return;
}
// Keep only the first third of the data.
sk_sp<SkData> truncated = SkData::MakeSubset(data.get(), 0, data->size() / 3);
SkCodec::Result result;
std::unique_ptr<SkCodec> codec =
SkJpegRustDecoder::Decode(SkMemoryStream::Make(std::move(truncated)), &result);
if (!codec) {
// Acceptable: codec construction failed.
REPORTER_ASSERT(r, result != SkCodec::kSuccess);
return;
}
// If codec constructed, decoding may succeed (Rust decoder is lenient with
// truncated data) or fail - either outcome is acceptable.
auto [image, decodeResult] = codec->getImage();
(void)decodeResult;
}
DEF_TEST(RustJpegCodec_short_input_classification, r) {
const auto decode = [](sk_sp<SkData> data, SkCodec::Result* result) {
return SkJpegRustDecoder::Decode(SkMemoryStream::Make(std::move(data)), result);
};
SkCodec::Result result;
std::unique_ptr<SkCodec> codec = decode(SkData::MakeEmpty(), &result);
REPORTER_ASSERT(r, !codec);
REPORTER_ASSERT(r, result == SkCodec::kIncompleteInput);
constexpr uint8_t kPartialSignature[] = {0xFF};
codec = decode(SkData::MakeWithCopy(kPartialSignature, sizeof(kPartialSignature)), &result);
REPORTER_ASSERT(r, !codec);
REPORTER_ASSERT(r, result == SkCodec::kIncompleteInput);
constexpr uint8_t kInvalidSignature[] = {0x00};
codec = decode(SkData::MakeWithCopy(kInvalidSignature, sizeof(kInvalidSignature)), &result);
REPORTER_ASSERT(r, !codec);
REPORTER_ASSERT(r, result == SkCodec::kErrorInInput);
constexpr uint8_t kStartOfImage[] = {0xFF, 0xD8};
codec = decode(SkData::MakeWithCopy(kStartOfImage, sizeof(kStartOfImage)), &result);
REPORTER_ASSERT(r, !codec);
REPORTER_ASSERT(r, result == SkCodec::kIncompleteInput);
sk_sp<SkData> data = GetResourceAsData("images/color_wheel.jpg");
REPORTER_ASSERT(r, data);
if (!data) {
return;
}
codec = decode(std::move(data), &result);
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result);
REPORTER_ASSERT(r, codec);
}
DEF_TEST(RustJpegCodec_reject_non_jpeg, r) {
sk_sp<SkData> data = GetResourceAsData("images/color_wheel.png");
if (!data) {
ERRORF(r, "Missing resource: images/color_wheel.png");
return;
}
SkCodec::Result result;
std::unique_ptr<SkCodec> codec =
SkJpegRustDecoder::Decode(SkMemoryStream::Make(std::move(data)), &result);
REPORTER_ASSERT(r, !codec, "Should reject PNG data as JPEG");
}
// Decode an ICC-profiled JPEG into an explicit sRGB color space, exercising the
// applyColorXform() path (xformOnDecode == true).
DEF_TEST(RustJpegCodec_decode_with_color_transform, r) {
std::unique_ptr<SkCodec> codec = decode_rust_jpeg(r, "images/icc-v2-gbr.jpg");
if (!codec) {
return;
}
// The image has a non-sRGB ICC profile. Requesting sRGB forces a color transform.
SkImageInfo dstInfo = codec->getInfo()
.makeColorType(kRGBA_8888_SkColorType)
.makeColorSpace(SkColorSpace::MakeSRGB());
SkBitmap bitmap;
bitmap.allocPixels(dstInfo);
SkCodec::Result result = codec->getPixels(dstInfo, bitmap.getPixels(), bitmap.rowBytes());
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result);
// Sanity: the decoded pixels should not be all zero (would indicate garbled output).
bool allZero = true;
const uint32_t* pixels = static_cast<const uint32_t*>(bitmap.getPixels());
for (int i = 0; i < bitmap.width() * bitmap.height() && allZero; ++i) {
if (pixels[i] != 0) {
allZero = false;
}
}
REPORTER_ASSERT(r, !allZero, "Color-transformed decode produced all-zero pixels");
}
#if defined(SK_CODEC_DECODES_JPEG)
DEF_TEST(RustJpegCodec_RawYUVAParity, r) {
struct TestCase {
const char* path;
SkYUVAInfo::Subsampling subsampling;
};
const TestCase testCases[] = {
{"images/orientation/1_444.jpg", SkYUVAInfo::Subsampling::k444},
{"images/orientation/1_422.jpg", SkYUVAInfo::Subsampling::k422},
{"images/orientation/1_420.jpg", SkYUVAInfo::Subsampling::k420},
{"images/orientation/1_440.jpg", SkYUVAInfo::Subsampling::k440},
{"images/orientation/1_411.jpg", SkYUVAInfo::Subsampling::k411},
{"images/orientation/1_410.jpg", SkYUVAInfo::Subsampling::k410},
{"images/cropped_mandrill.jpg", SkYUVAInfo::Subsampling::k420},
{"images/brickwork-texture.jpg", SkYUVAInfo::Subsampling::k444},
{"images/non_interleaved_420_64x64.jpg", SkYUVAInfo::Subsampling::k420},
{"images/four-colors-limited-range-420-8bpc.jpg", SkYUVAInfo::Subsampling::k420},
{"images/b78329453.jpeg", SkYUVAInfo::Subsampling::k420},
};
constexpr auto kAllTypes = SkYUVAPixmapInfo::SupportedDataTypes::All();
int maximumDeviation = 0;
for (const TestCase& testCase : testCases) {
skiatest::ReporterContext context(r, testCase.path);
sk_sp<SkData> data = GetResourceAsData(testCase.path);
REPORTER_ASSERT(r, data);
if (!data) {
continue;
}
std::unique_ptr<SkCodec> rustCodec = decode_rust_jpeg_data(data);
std::unique_ptr<SkCodec> jpegCodec =
SkJpegDecoder::Decode(SkMemoryStream::Make(data), nullptr);
REPORTER_ASSERT(r, rustCodec && jpegCodec);
if (!rustCodec || !jpegCodec) {
continue;
}
SkYUVAPixmapInfo rustInfo;
SkYUVAPixmapInfo jpegInfo;
REPORTER_ASSERT(r, rustCodec->queryYUVAInfo(kAllTypes, &rustInfo));
REPORTER_ASSERT(r, jpegCodec->queryYUVAInfo(kAllTypes, &jpegInfo));
if (!rustInfo.isValid() || !jpegInfo.isValid()) {
continue;
}
REPORTER_ASSERT(r, rustInfo.yuvaInfo() == jpegInfo.yuvaInfo());
REPORTER_ASSERT(r, rustInfo.yuvaInfo().subsampling() == testCase.subsampling);
REPORTER_ASSERT(r, rustInfo.yuvaInfo().yuvColorSpace() == kJPEG_Full_SkYUVColorSpace);
REPORTER_ASSERT(r, rustInfo.yuvaInfo().origin() == rustCodec->getOrigin());
std::array<std::vector<uint8_t>, SkYUVAPixmaps::kMaxPlanes> storage;
std::array<SkPixmap, SkYUVAPixmaps::kMaxPlanes> pixmaps;
for (int plane = 0; plane < rustInfo.numPlanes(); ++plane) {
const SkImageInfo& info = rustInfo.planeInfo(plane);
const size_t rowBytes = static_cast<size_t>(info.width()) + 13;
const size_t exactSize = (static_cast<size_t>(info.height()) - 1) * rowBytes +
static_cast<size_t>(info.width());
storage[plane].assign(exactSize, 0xCD);
pixmaps[plane] = SkPixmap(info, storage[plane].data(), rowBytes);
}
SkYUVAPixmaps rustPlanes =
SkYUVAPixmaps::FromExternalPixmaps(rustInfo.yuvaInfo(), pixmaps.data());
SkYUVAPixmaps jpegPlanes = SkYUVAPixmaps::Allocate(jpegInfo);
REPORTER_ASSERT(r, rustPlanes.isValid() && jpegPlanes.isValid());
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, rustCodec->getYUVAPlanes(rustPlanes));
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, jpegCodec->getYUVAPlanes(jpegPlanes));
if (&testCase == &testCases[0] ||
!strcmp(testCase.path, "images/four-colors-limited-range-420-8bpc.jpg")) {
SkYUVAPixmaps replay = SkYUVAPixmaps::Allocate(rustInfo);
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, rustCodec->getYUVAPlanes(replay));
for (int plane = 0; plane < rustPlanes.numPlanes(); ++plane) {
for (int y = 0; y < rustPlanes.plane(plane).height(); ++y) {
REPORTER_ASSERT(
r,
memcmp(rustPlanes.plane(plane).addr(0, y),
replay.plane(plane).addr(0, y),
static_cast<size_t>(rustPlanes.plane(plane).width())) == 0);
}
}
}
for (int plane = 0; plane < rustPlanes.numPlanes(); ++plane) {
const SkPixmap& actual = rustPlanes.plane(plane);
const SkPixmap& expected = jpegPlanes.plane(plane);
REPORTER_ASSERT(r, actual.dimensions() == expected.dimensions());
for (int y = 0; y < actual.height(); ++y) {
const auto* actualRow = static_cast<const uint8_t*>(actual.addr(0, y));
const auto* expectedRow = static_cast<const uint8_t*>(expected.addr(0, y));
for (int x = 0; x < actual.width(); ++x) {
const int deviation = std::abs(static_cast<int>(actualRow[x]) -
static_cast<int>(expectedRow[x]));
maximumDeviation = std::max(maximumDeviation, deviation);
REPORTER_ASSERT(r,
deviation <= 2,
"%s plane %d sample (%d,%d) deviation %d exceeds 2",
testCase.path,
plane,
x,
y,
deviation);
}
if (y + 1 < actual.height()) {
REPORTER_ASSERT(r,
std::all_of(actualRow + actual.width(),
actualRow + actual.rowBytes(),
[](uint8_t value) { return value == 0xCD; }),
"%s plane %d row %d padding was modified",
testCase.path,
plane,
y);
}
}
}
}
INFOF(r, "Rust JPEG raw-plane maximum libjpeg-turbo deviation: %d", maximumDeviation);
for (const char* path : {"images/grayscale.jpg", "images/CMYK.jpg"}) {
std::unique_ptr<SkCodec> codec = decode_rust_jpeg(r, path);
SkYUVAPixmapInfo info;
REPORTER_ASSERT(r, codec && !codec->queryYUVAInfo(kAllTypes, &info));
}
sk_sp<SkData> source = GetResourceAsData("images/orientation/1_444.jpg");
REPORTER_ASSERT(r, source);
if (source) {
auto makeMutatedSOF = [&](size_t componentOffset, uint8_t value) {
sk_sp<SkData> mutated = SkData::MakeUninitialized(source->size());
memcpy(mutated->writable_data(), source->data(), source->size());
auto* bytes = static_cast<uint8_t*>(mutated->writable_data());
for (size_t index = 0; index + 16 < mutated->size(); ++index) {
if (bytes[index] == 0xFF && bytes[index + 1] == 0xC0) {
bytes[index + componentOffset] = value;
break;
}
}
if (componentOffset == 10) {
for (size_t index = 0; index + 6 < mutated->size(); ++index) {
if (bytes[index] == 0xFF && bytes[index + 1] == 0xDA) {
bytes[index + 5] = value;
break;
}
}
}
return mutated;
};
struct Mutation {
const char* description;
sk_sp<SkData> data;
};
Mutation mutations[] = {{"component ID", makeMutatedSOF(10, 4)},
{"chroma sampling", makeMutatedSOF(14, 0x21)}};
for (Mutation& mutation : mutations) {
skiatest::ReporterContext context(r, mutation.description);
std::unique_ptr<SkCodec> codec = decode_rust_jpeg_data(std::move(mutation.data));
SkYUVAPixmapInfo info;
REPORTER_ASSERT(r, codec && !codec->queryYUVAInfo(kAllTypes, &info));
}
}
}
#endif
DEF_TEST(RustJpegCodec_ProgressiveRawYUVRetry, r) {
sk_sp<SkData> data = GetResourceAsData("images/four-colors-limited-range-420-8bpc.jpg");
REPORTER_ASSERT(r, data);
if (!data) {
return;
}
auto stream = std::make_unique<HaltingStream>(data, data->size() / 2);
HaltingStream* streamPtr = stream.get();
SkCodec::Result result;
std::unique_ptr<SkCodec> codec = SkJpegRustDecoder::Decode(std::move(stream), &result);
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result);
REPORTER_ASSERT(r, codec);
if (!codec) {
return;
}
constexpr auto kAllTypes = SkYUVAPixmapInfo::SupportedDataTypes::All();
SkYUVAPixmapInfo info;
REPORTER_ASSERT(r, codec->queryYUVAInfo(kAllTypes, &info));
REPORTER_ASSERT(r, info.yuvaInfo().planeConfig() == SkYUVAInfo::PlaneConfig::kY_U_V);
REPORTER_ASSERT(r, info.yuvaInfo().subsampling() == SkYUVAInfo::Subsampling::k420);
std::array<std::vector<uint8_t>, SkYUVAPixmaps::kMaxPlanes> storage;
std::array<SkPixmap, SkYUVAPixmaps::kMaxPlanes> pixmaps;
for (int plane = 0; plane < info.numPlanes(); ++plane) {
const SkImageInfo& planeInfo = info.planeInfo(plane);
const size_t rowBytes = static_cast<size_t>(planeInfo.width()) + 13;
storage[plane].assign(rowBytes * static_cast<size_t>(planeInfo.height()), 0xCD);
pixmaps[plane] = SkPixmap(planeInfo, storage[plane].data(), rowBytes);
}
SkYUVAPixmaps planes = SkYUVAPixmaps::FromExternalPixmaps(info.yuvaInfo(), pixmaps.data());
REPORTER_ASSERT(r, planes.isValid());
REPORTER_ASSERT(r, codec->getYUVAPlanes(planes) == SkCodec::kIncompleteInput);
for (int plane = 0; plane < planes.numPlanes(); ++plane) {
REPORTER_ASSERT(
r, std::all_of(storage[plane].begin(), storage[plane].end(), [](uint8_t value) {
return value == 0xCD;
}));
}
streamPtr->addNewData(data->size());
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, codec->getYUVAPlanes(planes));
for (int plane = 0; plane < planes.numPlanes(); ++plane) {
REPORTER_ASSERT(
r, std::any_of(storage[plane].begin(), storage[plane].end(), [](uint8_t value) {
return value != 0xCD;
}));
}
}
DEF_TEST(RustJpegCodec_RawDirectPlaneContracts, r) {
constexpr uint8_t kSentinel = 0xCD;
sk_sp<SkData> data = GetResourceAsData("images/color_wheel.jpg");
REPORTER_ASSERT(r, data);
if (!data) {
return;
}
auto makeReader = [&]() {
return RustJpegReaderForTest(SkMemoryStream::Make(data), data->size());
};
auto reader = makeReader();
REPORTER_ASSERT(r, reader.fReader->read_metadata() == rust_jpeg::DecodingResult::Success);
REPORTER_ASSERT(r, reader.fReader->raw_component_count() == 3);
std::array<rust_jpeg::RawPlaneInfo, 3> infos = {
reader.fReader->raw_plane_info(0),
reader.fReader->raw_plane_info(1),
reader.fReader->raw_plane_info(2),
};
std::array<size_t, 3> strides;
std::array<size_t, 3> rows;
std::array<size_t, 3> required;
for (int plane = 0; plane < 3; ++plane) {
strides[plane] = static_cast<size_t>(infos[plane].width) + 7 + plane * 3;
rows[plane] = std::min(static_cast<size_t>(infos[plane].height),
static_cast<size_t>(infos[plane].vertical_sampling_factor) * 8);
required[plane] =
rows[plane] == 0 ? 0 : (rows[plane] - 1) * strides[plane] + infos[plane].width;
}
std::array<std::vector<uint8_t>, 3> expected;
for (int plane = 0; plane < 3; ++plane) {
expected[plane].assign(required[plane], kSentinel);
}
rust_jpeg::RawOutputResult expectedResult = reader.fReader->read_raw_imcu_row(
rust::Slice<uint8_t>(expected[0].data(), expected[0].size()),
static_cast<uint32_t>(strides[0]),
rust::Slice<uint8_t>(expected[1].data(), expected[1].size()),
static_cast<uint32_t>(strides[1]),
rust::Slice<uint8_t>(expected[2].data(), expected[2].size()),
static_cast<uint32_t>(strides[2]));
REPORTER_ASSERT(r, expectedResult.status == rust_jpeg::OutputStatus::Ready);
const std::array<uint32_t, 3> expectedRows = {
expectedResult.y_rows, expectedResult.u_rows, expectedResult.v_rows};
for (int plane = 0; plane < 3; ++plane) {
REPORTER_ASSERT(r, expectedRows[plane] == rows[plane]);
for (size_t row = 0; row + 1 < rows[plane]; ++row) {
REPORTER_ASSERT(
r,
std::all_of(expected[plane].begin() + row * strides[plane] + infos[plane].width,
expected[plane].begin() + (row + 1) * strides[plane],
[](uint8_t value) { return value == kSentinel; }));
}
}
for (int shortPlane = 0; shortPlane < 3; ++shortPlane) {
auto shortReader = makeReader();
REPORTER_ASSERT(r,
shortReader.fReader->read_metadata() == rust_jpeg::DecodingResult::Success);
std::array<std::vector<uint8_t>, 3> storage;
for (int plane = 0; plane < 3; ++plane) {
storage[plane].assign(required[plane] - (plane == shortPlane), kSentinel);
}
rust_jpeg::RawOutputResult result = shortReader.fReader->read_raw_imcu_row(
rust::Slice<uint8_t>(storage[0].data(), storage[0].size()),
static_cast<uint32_t>(strides[0]),
rust::Slice<uint8_t>(storage[1].data(), storage[1].size()),
static_cast<uint32_t>(strides[1]),
rust::Slice<uint8_t>(storage[2].data(), storage[2].size()),
static_cast<uint32_t>(strides[2]));
REPORTER_ASSERT(r, result.status == rust_jpeg::OutputStatus::Error);
for (const auto& plane : storage) {
REPORTER_ASSERT(r, std::all_of(plane.begin(), plane.end(), [](uint8_t value) {
return value == kSentinel;
}));
}
}
size_t entropyStart = 0;
const uint8_t* bytes = data->bytes();
for (size_t offset = 0; offset + 4 < data->size(); ++offset) {
if (bytes[offset] == 0xFF && bytes[offset + 1] == 0xDA) {
const size_t markerLength = static_cast<size_t>(bytes[offset + 2]) << 8 |
static_cast<size_t>(bytes[offset + 3]);
entropyStart = offset + 2 + markerLength;
break;
}
}
REPORTER_ASSERT(r, entropyStart > 0 && entropyStart < data->size());
int suspensionsTested = 0;
for (size_t visible = entropyStart;
visible < std::min(data->size(), entropyStart + 128) && suspensionsTested < 3;
++visible) {
auto stream = std::make_unique<HaltingStream>(data, visible);
HaltingStream* streamPtr = stream.get();
RustJpegReaderForTest partialReader(std::move(stream), 0);
REPORTER_ASSERT(
r, partialReader.fReader->read_metadata() == rust_jpeg::DecodingResult::Success);
std::array<std::vector<uint8_t>, 3> storage;
for (int plane = 0; plane < 3; ++plane) {
storage[plane].assign(required[plane], kSentinel);
}
rust_jpeg::RawOutputResult result = partialReader.fReader->read_raw_imcu_row(
rust::Slice<uint8_t>(storage[0].data(), storage[0].size()),
static_cast<uint32_t>(strides[0]),
rust::Slice<uint8_t>(storage[1].data(), storage[1].size()),
static_cast<uint32_t>(strides[1]),
rust::Slice<uint8_t>(storage[2].data(), storage[2].size()),
static_cast<uint32_t>(strides[2]));
if (result.status != rust_jpeg::OutputStatus::NeedMoreInput) {
continue;
}
++suspensionsTested;
for (const auto& plane : storage) {
REPORTER_ASSERT(r, std::all_of(plane.begin(), plane.end(), [](uint8_t value) {
return value == kSentinel;
}));
}
streamPtr->addNewData(data->size());
result = partialReader.fReader->read_raw_imcu_row(
rust::Slice<uint8_t>(storage[0].data(), storage[0].size()),
static_cast<uint32_t>(strides[0]),
rust::Slice<uint8_t>(storage[1].data(), storage[1].size()),
static_cast<uint32_t>(strides[1]),
rust::Slice<uint8_t>(storage[2].data(), storage[2].size()),
static_cast<uint32_t>(strides[2]));
REPORTER_ASSERT(r, result.status == rust_jpeg::OutputStatus::Ready);
REPORTER_ASSERT(r, result.y_rows == expectedRows[0]);
REPORTER_ASSERT(r, result.u_rows == expectedRows[1]);
REPORTER_ASSERT(r, result.v_rows == expectedRows[2]);
REPORTER_ASSERT(r, storage == expected);
}
REPORTER_ASSERT(r, suspensionsTested == 3);
}
// ========== Encoder Tests ==========
#if defined(SK_CODEC_ENCODES_JPEG_WITH_RUST)
// Encode smoke test: decode a JPEG, re-encode it, verify the output is
// non-empty, re-decodable, and preserves dimensions.
DEF_TEST(RustJpegEncode_smoke_test, r) {
struct TestCase {
const char* path;
int expectedWidth;
int expectedHeight;
int quality;
};
static const TestCase kTestCases[] = {
{"images/mandrill_512_q075.jpg", 512, 512, 85},
{"images/dog.jpg", 180, 180, 90},
};
for (const auto& tc : kTestCases) {
skiatest::ReporterContext ctx(r, tc.path);
sk_sp<SkData> data = GetResourceAsData(tc.path);
if (!data) {
ERRORF(r, "Missing resource: %s", tc.path);
continue;
}
SkCodec::Result result;
std::unique_ptr<SkCodec> codec =
SkJpegRustDecoder::Decode(SkMemoryStream::Make(data), &result);
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result);
if (!codec) continue;
auto [image, decodeResult] = codec->getImage();
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, decodeResult);
REPORTER_ASSERT(r, image);
if (!image) continue;
SkPixmap src;
REPORTER_ASSERT(r, image->peekPixels(&src));
SkDynamicMemoryWStream dst;
SkJpegEncoder::Options options;
options.fQuality = tc.quality;
REPORTER_ASSERT(r, SkJpegRustEncoder::Encode(&dst, src, options));
REPORTER_ASSERT(r, dst.bytesWritten() > 0);
sk_sp<SkData> encoded = dst.detachAsData();
REPORTER_ASSERT(r, SkJpegRustDecoder::IsJpeg(encoded->data(), encoded->size()));
SkCodec::Result reDecodeResult;
std::unique_ptr<SkCodec> reCodec =
SkJpegRustDecoder::Decode(SkMemoryStream::Make(encoded), &reDecodeResult);
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, reDecodeResult);
REPORTER_ASSERT(r, reCodec);
if (!reCodec) continue;
auto [reImage, reImageResult] = reCodec->getImage();
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, reImageResult);
REPORTER_ASSERT(r, reImage);
if (!reImage) continue;
REPORTER_ASSERT(r, reImage->width() == tc.expectedWidth);
REPORTER_ASSERT(r, reImage->height() == tc.expectedHeight);
}
}
DEF_TEST(RustJpegEncode_quality_affects_size, r) {
SkBitmap bitmap;
if (!ToolUtils::GetResourceAsBitmap("images/mandrill_512_q075.jpg", &bitmap)) {
ERRORF(r, "Failed to decode images/mandrill_512_q075.jpg");
return;
}
SkPixmap src;
REPORTER_ASSERT(r, bitmap.peekPixels(&src));
SkDynamicMemoryWStream dstLow, dstHigh;
SkJpegEncoder::Options lowOptions, highOptions;
lowOptions.fQuality = 10;
highOptions.fQuality = 95;
REPORTER_ASSERT(r, SkJpegRustEncoder::Encode(&dstLow, src, lowOptions));
REPORTER_ASSERT(r, SkJpegRustEncoder::Encode(&dstHigh, src, highOptions));
REPORTER_ASSERT(r,
dstLow.bytesWritten() < dstHigh.bytesWritten(),
"Low quality (%zu bytes) should be smaller than high quality (%zu bytes)",
dstLow.bytesWritten(),
dstHigh.bytesWritten());
}
// Table-based test for encoding different pixel formats. Verifies that RGB,
// RGBA (alpha stripped or blended), BGRA, and grayscale all round-trip through
// the encoder without crashing and produce non-empty output.
DEF_TEST(RustJpegEncode_color_formats, r) {
struct TestCase {
const char* description;
SkColorType colorType;
SkAlphaType alphaType;
SkColor fillColor;
SkJpegEncoder::AlphaOption alphaOption;
};
static const TestCase kTestCases[] = {
{"grayscale",
kGray_8_SkColorType,
kOpaque_SkAlphaType,
SK_ColorGRAY,
SkJpegEncoder::AlphaOption::kIgnore},
{"RGBA ignore alpha",
kRGBA_8888_SkColorType,
kUnpremul_SkAlphaType,
0x80FF0000,
SkJpegEncoder::AlphaOption::kIgnore},
{"RGBA blend on black",
kRGBA_8888_SkColorType,
kUnpremul_SkAlphaType,
0x8000FF00,
SkJpegEncoder::AlphaOption::kBlendOnBlack},
{"BGRA",
kBGRA_8888_SkColorType,
kUnpremul_SkAlphaType,
SK_ColorBLUE,
SkJpegEncoder::AlphaOption::kIgnore},
{"RGB_888x (4-byte opaque)",
kRGB_888x_SkColorType,
kOpaque_SkAlphaType,
SK_ColorRED,
SkJpegEncoder::AlphaOption::kIgnore},
};
for (const auto& tc : kTestCases) {
skiatest::ReporterContext ctx(r, tc.description);
SkImageInfo info = SkImageInfo::Make(64, 64, tc.colorType, tc.alphaType);
SkBitmap bitmap;
REPORTER_ASSERT(r, bitmap.tryAllocPixels(info));
bitmap.eraseColor(tc.fillColor);
SkPixmap src;
REPORTER_ASSERT(r, bitmap.peekPixels(&src));
SkDynamicMemoryWStream dst;
SkJpegEncoder::Options options;
options.fQuality = 85;
options.fAlphaOption = tc.alphaOption;
REPORTER_ASSERT(r, SkJpegRustEncoder::Encode(&dst, src, options));
REPORTER_ASSERT(r, dst.bytesWritten() > 0);
}
}
#endif // defined(SK_CODEC_ENCODES_JPEG_WITH_RUST)
// ========== Gainmap / MPF Tests ==========
#if defined(SK_CODEC_DECODES_JPEG_GAINMAPS)
// Verify the Rust codec's gainmap pipeline: Rust segment scanning → MPF parsing
// → C++ metadata interpretation → gainmap codec extraction. This exercises the
// ~190-line onGetGainmapInfo code path that is otherwise untested.
DEF_TEST(RustJpeg_gainmap_extraction, r) {
// gainmap_iso21496_1.jpg is a synthetic test image with an ISO 21496-1
// gainmap embedded via MPF. The C++ codec's JpegGainmapTest suite already
// validates the detailed SkGainmapInfo fields, so here we only verify that
// the Rust codec successfully extracts the gainmap and can decode it.
static const char* kGainmapImages[] = {
"images/gainmap_iso21496_1.jpg",
"images/gainmap_iso21496_1_adobe_gcontainer.jpg",
};
for (const char* path : kGainmapImages) {
skiatest::ReporterContext ctx(r, path);
sk_sp<SkData> data = GetResourceAsData(path);
if (!data) {
ERRORF(r, "Missing resource: %s", path);
continue;
}
SkCodec::Result result;
std::unique_ptr<SkCodec> codec =
SkJpegRustDecoder::Decode(SkMemoryStream::Make(data), &result);
REPORTER_ASSERT(r, codec);
if (!codec) continue;
auto androidCodec = SkAndroidCodec::MakeFromCodec(std::move(codec));
REPORTER_ASSERT(r, androidCodec);
if (!androidCodec) continue;
SkGainmapInfo gainmapInfo;
std::unique_ptr<SkAndroidCodec> gainmapCodec;
bool hasGainmap = androidCodec->getGainmapAndroidCodec(&gainmapInfo, &gainmapCodec);
REPORTER_ASSERT(r, hasGainmap, "Expected gainmap in %s", path);
if (!hasGainmap) continue;
REPORTER_ASSERT(r, gainmapCodec);
// The gainmap image should have valid non-zero dimensions.
REPORTER_ASSERT(r, gainmapCodec->getInfo().width() > 0);
REPORTER_ASSERT(r, gainmapCodec->getInfo().height() > 0);
// Actually decode the gainmap to verify the full pipeline.
SkBitmap gainmapBitmap;
gainmapBitmap.allocPixels(gainmapCodec->getInfo());
REPORTER_ASSERT(
r,
SkCodec::kSuccess == gainmapCodec->getAndroidPixels(gainmapBitmap.info(),
gainmapBitmap.getPixels(),
gainmapBitmap.rowBytes()));
}
}
#endif // defined(SK_CODEC_DECODES_JPEG_GAINMAPS)
// ========== Rust vs C++ JPEG Comparison Tests ==========
#if defined(SK_CODEC_DECODES_JPEG)
// Decode the same JPEG with both the Rust decoder and the C++ libjpeg-turbo
// decoder, then compare the outputs pixel-by-pixel within a tolerance.
// JPEG decoding is inherently lossy and different DCT implementations may
// produce slightly different values; a tolerance of ~8/255 per channel covers
// the typical inter-implementation variance.
static void compare_rust_vs_cpp_jpeg(skiatest::Reporter* r,
const char* path,
float tolerancePerChannel = 8.0f / 255.0f) {
skiatest::ReporterContext ctx(r, path);
sk_sp<SkData> data = GetResourceAsData(path);
if (!data) {
ERRORF(r, "Missing resource: %s", path);
return;
}
SkCodec::Result rustResult;
std::unique_ptr<SkCodec> rustCodec =
SkJpegRustDecoder::Decode(SkMemoryStream::Make(data), &rustResult);
if (!rustCodec) {
// Some images may not be supported by the Rust decoder.
return;
}
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, rustResult);
SkCodec::Result cppResult;
std::unique_ptr<SkCodec> cppCodec =
SkJpegDecoder::Decode(SkMemoryStream::Make(data), &cppResult);
if (!cppCodec) {
return;
}
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, cppResult);
auto [rustImage, rustDecodeResult] = rustCodec->getImage();
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, rustDecodeResult);
REPORTER_ASSERT(r, rustImage);
auto [cppImage, cppDecodeResult] = cppCodec->getImage();
REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, cppDecodeResult);
REPORTER_ASSERT(r, cppImage);
if (!rustImage || !cppImage) {
return;
}
REPORTER_ASSERT(r,
rustImage->dimensions() == cppImage->dimensions(),
"Rust dims %dx%d != C++ dims %dx%d",
rustImage->width(),
rustImage->height(),
cppImage->width(),
cppImage->height());
if (rustImage->dimensions() != cppImage->dimensions()) {
return;
}
SkBitmap rustBitmap, cppBitmap;
REPORTER_ASSERT(r, rustBitmap.tryAllocPixels(rustImage->imageInfo()));
REPORTER_ASSERT(r, cppBitmap.tryAllocPixels(cppImage->imageInfo()));
REPORTER_ASSERT(r, rustImage->readPixels(nullptr, rustBitmap.pixmap(), 0, 0));
REPORTER_ASSERT(r, cppImage->readPixels(nullptr, cppBitmap.pixmap(), 0, 0));
const float tols[4] = {tolerancePerChannel, tolerancePerChannel, tolerancePerChannel, 0.0f};
auto error =
std::function<ComparePixmapsErrorReporter>([&](int x, int y, const float diffs[4]) {
ERRORF(r,
"%s: pixel differs at (%d, %d): diffs=(%f, %f, %f, %f)",
path,
x,
y,
diffs[0],
diffs[1],
diffs[2],
diffs[3]);
});
ComparePixels(rustBitmap.pixmap(), cppBitmap.pixmap(), tols, error);
}
// Table-based test comparing Rust and C++ JPEG decoders across many images.
// Each entry specifies a description, path, and optional per-channel tolerance
// (default 8/255). Unsupported formats are gracefully skipped when the Rust
// decoder returns nullptr.
DEF_TEST(RustJpeg_vs_CppJpeg, r) {
struct TestCase {
const char* description;
const char* path;
float tolerance;
};
static constexpr float kDefaultTol = 8.0f / 255.0f;
static const TestCase kTestCases[] = {
{"color wheel (baseline RGB)", "images/color_wheel.jpg", kDefaultTol},
{"dog (180x180, RGB)", "images/dog.jpg", kDefaultTol},
{"ducky", "images/ducky.jpg", kDefaultTol},
{"mandrill 512 q075", "images/mandrill_512_q075.jpg", kDefaultTol},
{"mandrill h1v1 subsampling", "images/mandrill_h1v1.jpg", kDefaultTol},
{"mandrill h2v1 subsampling", "images/mandrill_h2v1.jpg", kDefaultTol},
{"grayscale", "images/grayscale.jpg", kDefaultTol},
{"progressive (b78329453)", "images/b78329453.jpeg", kDefaultTol},
// Progressive JPEGs can have slightly larger inter-implementation diffs.
{"progressive (crbug999986)", "images/crbug999986.jpeg", 10.0f / 255.0f},
{"cropped mandrill", "images/cropped_mandrill.jpg", kDefaultTol},
{"flutter logo", "images/flutter_logo.jpg", kDefaultTol},
{"ICC v2 GBR profile", "images/icc-v2-gbr.jpg", kDefaultTol},
// randPixels has small DCT rounding differences between implementations.
{"random pixels", "images/randPixels.jpg", 16.0f / 255.0f},
// CMYK: different CMYK→RGB conversion paths (zune-jpeg vs libjpeg-turbo)
// may produce slightly larger per-channel differences.
{"CMYK", "images/CMYK.jpg", 16.0f / 255.0f},
};
for (const TestCase& tc : kTestCases) {
compare_rust_vs_cpp_jpeg(r, tc.path, tc.tolerance);
}
}
// Compare EXIF orientation reported by Rust and C++ JPEG decoders for the same
// image. Both codecs ultimately use SkExif::Parse, so the values must match
// exactly.
DEF_TEST(RustJpeg_vs_CppJpeg_Orientation, r) {
static const char* kImages[] = {
"images/orientation/1_444.jpg",
"images/orientation/2_444.jpg",
"images/orientation/3_444.jpg",
"images/orientation/4_444.jpg",
"images/orientation/5_444.jpg",
"images/orientation/6_444.jpg",
"images/orientation/7_444.jpg",
"images/orientation/8_444.jpg",
"images/exif-orientation-2-ur.jpg",
"images/orientation/exif.jpg", // large EXIF block
"images/orientation/subifd.jpg", // EXIF with sub-IFD structure
"images/ducky.jpg",
"images/color_wheel.jpg", // no EXIF → default orientation
};
for (const char* path : kImages) {
skiatest::ReporterContext ctx(r, path);
sk_sp<SkData> data = GetResourceAsData(path);
if (!data) {
ERRORF(r, "Missing resource: %s", path);
continue;
}
SkCodec::Result rustResult;
auto rustCodec = SkJpegRustDecoder::Decode(SkMemoryStream::Make(data), &rustResult);
if (!rustCodec) continue;
SkCodec::Result cppResult;
auto cppCodec = SkJpegDecoder::Decode(SkMemoryStream::Make(data), &cppResult);
if (!cppCodec) continue;
REPORTER_ASSERT(r,
rustCodec->getOrigin() == cppCodec->getOrigin(),
"%s: Rust orientation %d != C++ orientation %d",
path,
static_cast<int>(rustCodec->getOrigin()),
static_cast<int>(cppCodec->getOrigin()));
}
}
// Compare ICC profile data reported by Rust and C++ JPEG decoders. Both
// profiles should round-trip to the same parsed representation.
DEF_TEST(RustJpeg_vs_CppJpeg_ICCProfile, r) {
static const char* kImages[] = {
"images/icc-v2-gbr.jpg", // ICC v2 with GBR channel order
"images/ducky.jpg", // large ICC profile
"images/color_wheel.jpg", // no ICC → both should be nullptr
"images/grayscale.jpg", // no ICC
};
for (const char* path : kImages) {
skiatest::ReporterContext ctx(r, path);
sk_sp<SkData> data = GetResourceAsData(path);
if (!data) {
ERRORF(r, "Missing resource: %s", path);
continue;
}
SkCodec::Result rustResult;
auto rustCodec = SkJpegRustDecoder::Decode(SkMemoryStream::Make(data), &rustResult);
if (!rustCodec) continue;
SkCodec::Result cppResult;
auto cppCodec = SkJpegDecoder::Decode(SkMemoryStream::Make(data), &cppResult);
if (!cppCodec) continue;
const skcms_ICCProfile* rustICC = rustCodec->getICCProfile();
const skcms_ICCProfile* cppICC = cppCodec->getICCProfile();
// Both should agree on presence.
if (!rustICC && !cppICC) {
continue; // both null — OK
}
if (!rustICC || !cppICC) {
ERRORF(r,
"%s: ICC presence mismatch (Rust %s, C++ %s)",
path,
rustICC ? "present" : "absent",
cppICC ? "present" : "absent");
continue;
}
// Compare data class and color space.
REPORTER_ASSERT(r,
rustICC->data_color_space == cppICC->data_color_space,
"%s: ICC data_color_space mismatch",
path);
// Compare the TRC (transfer function) for each channel.
for (int ch = 0; ch < 3; ++ch) {
REPORTER_ASSERT(r,
skcms_TransferFunction_isSRGBish(&rustICC->trc[ch].parametric) ==
skcms_TransferFunction_isSRGBish(&cppICC->trc[ch].parametric),
"%s: ICC TRC[%d] sRGB-ish mismatch",
path,
ch);
}
// Compare the 3x3 toXYZD50 matrix.
for (int row = 0; row < 3; ++row) {
for (int col = 0; col < 3; ++col) {
float rustVal = rustICC->toXYZD50.vals[row][col];
float cppVal = cppICC->toXYZD50.vals[row][col];
float diff = std::abs(rustVal - cppVal);
REPORTER_ASSERT(r,
diff < 1e-5f,
"%s: ICC toXYZD50[%d][%d] differs: Rust=%f C++=%f",
path,
row,
col,
rustVal,
cppVal);
}
}
}
}
#endif // defined(SK_CODEC_DECODES_JPEG)