| /* |
| * 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 <cmath> |
| #include <functional> |
| #include <memory> |
| #include <utility> |
| |
| #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 "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; |
| } |
| |
| // ========== 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; |
| }; |
| |
| static const TestCase kTestCases[] = { |
| {"images/color_wheel.jpg", "baseline RGB 128x128"}, |
| {"images/dog.jpg", "RGB 180x180"}, |
| {"images/mandrill_512_q075.jpg", "RGB 512x512"}, |
| }; |
| |
| // 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; |
| } |
| |
| 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; |
| |
| 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); |
| previousRowsDecoded = rowsDecoded; |
| |
| 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; |
| |
| 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, 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(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_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"); |
| } |
| |
| // ========== 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) |