| /* | |
| * Copyright 2025 Google LLC | |
| * | |
| * Use of this source code is governed by a BSD-style license that can be | |
| * found in the LICENSE file. | |
| */ | |
| #include "include/codec/SkBmpRustDecoder.h" | |
| #include <cstring> | |
| #include <memory> | |
| #include <utility> | |
| #include <vector> | |
| #include "include/codec/SkBmpDecoder.h" | |
| #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 "modules/skcms/skcms.h" | |
| #include "tests/ComparePixels.h" | |
| #include "tests/FakeStreams.h" | |
| #include "tests/Test.h" | |
| #include "tools/Resources.h" | |
| // Helper to verify pixel color at a specific location. | |
| static void assert_pixel_color(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); | |
| skiatest::ReporterContext ctx(r, 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)", | |
| actualColor, expectedColor, x, y); | |
| } | |
| #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 `SkBmpRustDecoder::Decode`. | |
| static std::unique_ptr<SkCodec> decode_bmp(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 = | |
| SkBmpRustDecoder::Decode(SkMemoryStream::Make(std::move(data)), &result); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result); | |
| return codec; | |
| } | |
| static void write_u16_le(std::vector<uint8_t>& bytes, size_t offset, uint16_t value) { | |
| bytes[offset] = value & 0xFF; | |
| bytes[offset + 1] = value >> 8; | |
| } | |
| static void write_u32_le(std::vector<uint8_t>& bytes, size_t offset, uint32_t value) { | |
| bytes[offset] = value & 0xFF; | |
| bytes[offset + 1] = (value >> 8) & 0xFF; | |
| bytes[offset + 2] = (value >> 16) & 0xFF; | |
| bytes[offset + 3] = value >> 24; | |
| } | |
| static sk_sp<SkData> make_bmp_with_icc_profile(const SkData& profile) { | |
| constexpr size_t kFileHeaderSize = 14; | |
| constexpr size_t kDibHeaderSize = 124; | |
| constexpr size_t kPixelDataOffset = kFileHeaderSize + kDibHeaderSize; | |
| constexpr size_t kPixelDataSize = 8; | |
| constexpr size_t kProfileOffset = kPixelDataOffset + kPixelDataSize; | |
| std::vector<uint8_t> bmp(kProfileOffset + profile.size(), 0); | |
| bmp[0] = 'B'; | |
| bmp[1] = 'M'; | |
| write_u32_le(bmp, 2, bmp.size()); | |
| write_u32_le(bmp, 10, kPixelDataOffset); | |
| write_u32_le(bmp, 14, kDibHeaderSize); | |
| write_u32_le(bmp, 18, 2); // Width. | |
| write_u32_le(bmp, 22, 1); // Height. | |
| write_u16_le(bmp, 26, 1); // Planes. | |
| write_u16_le(bmp, 28, 24); | |
| write_u32_le(bmp, 34, kPixelDataSize); | |
| write_u32_le(bmp, 70, 0x4D424544); // PROFILE_EMBEDDED ("MBED"). | |
| write_u32_le(bmp, 126, kProfileOffset - kFileHeaderSize); | |
| write_u32_le(bmp, 130, profile.size()); | |
| // Two BGR pixels followed by row padding: red, then green. | |
| const uint8_t pixels[kPixelDataSize] = {0, 0, 255, 0, 255, 0, 0, 0}; | |
| std::memcpy(bmp.data() + kPixelDataOffset, pixels, sizeof(pixels)); | |
| std::memcpy(bmp.data() + kProfileOffset, profile.data(), profile.size()); | |
| return SkData::MakeWithCopy(bmp.data(), bmp.size()); | |
| } | |
| // Table-based test for decoding valid BMP files. | |
| DEF_TEST(RustBmpCodec_decode_valid_bmp, r) { | |
| auto test = [&r](const char* description, const char* file, SkISize expectedSize, | |
| SkColorType expectedColorType, | |
| SkCodec::SkScanlineOrder expectedScanlineOrder) { | |
| skiatest::ReporterContext ctx(r, description); | |
| std::unique_ptr<SkCodec> codec = decode_bmp(r, file); | |
| if (!codec) { | |
| return; | |
| } | |
| REPORTER_ASSERT(r, codec->dimensions() == expectedSize, | |
| "dimensions=%dx%d != expected=%dx%d", | |
| codec->dimensions().width(), codec->dimensions().height(), | |
| expectedSize.width(), expectedSize.height()); | |
| // Verify color type | |
| SkImageInfo info = codec->getInfo(); | |
| REPORTER_ASSERT(r, info.colorType() == expectedColorType, | |
| "colorType=%d != expected=%d", | |
| info.colorType(), expectedColorType); | |
| auto [image, result] = codec->getImage(); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result); | |
| REPORTER_ASSERT(r, image); | |
| REPORTER_ASSERT(r, image->width() == expectedSize.width()); | |
| REPORTER_ASSERT(r, image->height() == expectedSize.height()); | |
| }; | |
| test("basic 24-bit", "images/randPixels.bmp", {8, 8}, kN32_SkColorType, | |
| SkCodec::kBottomUp_SkScanlineOrder); | |
| test("rle compression", "images/rle.bmp", {320, 240}, kN32_SkColorType, | |
| SkCodec::kBottomUp_SkScanlineOrder); | |
| test("8 bit indexed", "images/bmp-size-32x32-8bpp.bmp", {32, 32}, kN32_SkColorType, | |
| SkCodec::kBottomUp_SkScanlineOrder); | |
| test("32 bit top-down", "images/32bpp-topdown-320x240.bmp", {320, 240}, kN32_SkColorType, | |
| SkCodec::kTopDown_SkScanlineOrder); | |
| } | |
| // Test that Decode handles nullptr for the Result parameter. | |
| DEF_TEST(RustBmpCodec_nullptr_result, r) { | |
| sk_sp<SkData> data = GetResourceAsData("images/randPixels.bmp"); | |
| REPORTER_ASSERT(r, data); | |
| // This should not crash even when result is nullptr | |
| std::unique_ptr<SkCodec> codec = | |
| SkBmpRustDecoder::Decode(SkMemoryStream::Make(std::move(data)), nullptr); | |
| REPORTER_ASSERT(r, codec); | |
| REPORTER_ASSERT(r, codec->dimensions() == SkISize::Make(8, 8)); | |
| } | |
| // Test that SkBmpRustDecoder correctly rejects non-BMP data. | |
| DEF_TEST(RustBmpCodec_reject_non_bmp, 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 = | |
| SkBmpRustDecoder::Decode(SkMemoryStream::Make(std::move(data)), &result); | |
| // Should fail to decode PNG data as BMP | |
| REPORTER_ASSERT(r, !codec, "SkBmpRustDecoder should reject PNG data"); | |
| } | |
| // Test codec reuse functionality. | |
| DEF_TEST(RustBmpCodec_rewind, r) { | |
| std::unique_ptr<SkCodec> codec = decode_bmp(r, "images/randPixels.bmp"); | |
| if (!codec) { | |
| return; | |
| } | |
| // Decode first time | |
| auto [image1, result1] = codec->getImage(); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result1); | |
| REPORTER_ASSERT(r, image1); | |
| // Decode again using the same codec (tests reuse) | |
| auto [image2, result2] = codec->getImage(); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result2); | |
| REPORTER_ASSERT(r, image2); | |
| // Both images should have the same dimensions | |
| REPORTER_ASSERT(r, | |
| image1->dimensions() == image2->dimensions(), | |
| "Images after rewind should have same dimensions"); | |
| // Verify pixel data is identical | |
| SkBitmap bm1, bm2; | |
| REPORTER_ASSERT(r, bm1.tryAllocPixels(image1->imageInfo())); | |
| REPORTER_ASSERT(r, bm2.tryAllocPixels(image2->imageInfo())); | |
| REPORTER_ASSERT(r, image1->readPixels(nullptr, bm1.pixmap(), 0, 0)); | |
| REPORTER_ASSERT(r, image2->readPixels(nullptr, bm2.pixmap(), 0, 0)); | |
| // Use zero tolerance for exact pixel match | |
| const float tols[4] = {0, 0, 0, 0}; | |
| auto error = std::function<ComparePixmapsErrorReporter>( | |
| [&](int x, int y, const float diffs[4]) { | |
| ERRORF(r, "Pixels differ at (%d, %d) after rewind. Diffs: (%f, %f, %f, %f)", | |
| x, y, diffs[0], diffs[1], diffs[2], diffs[3]); | |
| }); | |
| ComparePixels(bm1.pixmap(), bm2.pixmap(), tols, error); | |
| } | |
| // Test getPixels with a pre-allocated bitmap. | |
| DEF_TEST(RustBmpCodec_getPixels, r) { | |
| std::unique_ptr<SkCodec> codec = decode_bmp(r, "images/randPixels.bmp"); | |
| if (!codec) { | |
| return; | |
| } | |
| SkImageInfo info = codec->getInfo().makeColorType(kN32_SkColorType); | |
| SkBitmap bitmap; | |
| bitmap.allocPixels(info); | |
| SkCodec::Result result = codec->getPixels(bitmap.pixmap()); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result); | |
| // Verify bitmap dimensions | |
| REPORTER_ASSERT(r, bitmap.width() == 8); | |
| REPORTER_ASSERT(r, bitmap.height() == 8); | |
| // Spot check pixel values to verify correct decoding | |
| SkPixmap pixmap = bitmap.pixmap(); | |
| assert_pixel_color(r, pixmap, 0, 0, SkColorSetARGB(0xFF, 0xBB, 0xA5, 0x70), "pixel (0,0)"); | |
| assert_pixel_color(r, pixmap, 1, 0, SkColorSetARGB(0xFF, 0x39, 0x5F, 0x5D), "pixel (1,0)"); | |
| assert_pixel_color(r, pixmap, 0, 1, SkColorSetARGB(0xFF, 0xBD, 0xA6, 0x0E), "pixel (0,1)"); | |
| assert_pixel_color(r, pixmap, 1, 1, SkColorSetARGB(0xFF, 0xC0, 0x1D, 0xB6), "pixel (1,1)"); | |
| } | |
| // Test that IsBmp correctly identifies BMP data. | |
| DEF_TEST(RustBmpCodec_IsBmp_positive, r) { | |
| sk_sp<SkData> data = GetResourceAsData("images/randPixels.bmp"); | |
| if (!data) { | |
| ERRORF(r, "Missing resource: images/randPixels.bmp"); | |
| return; | |
| } | |
| bool isBmp = SkBmpRustDecoder::IsBmp(data->data(), data->size()); | |
| REPORTER_ASSERT(r, isBmp, "IsBmp should return true for BMP data"); | |
| } | |
| // Test that IsBmp correctly rejects non-BMP data. | |
| DEF_TEST(RustBmpCodec_IsBmp_negative, r) { | |
| sk_sp<SkData> data = GetResourceAsData("images/color_wheel.png"); | |
| if (!data) { | |
| ERRORF(r, "Missing resource: images/color_wheel.png"); | |
| return; | |
| } | |
| bool isBmp = SkBmpRustDecoder::IsBmp(data->data(), data->size()); | |
| REPORTER_ASSERT(r, !isBmp, "IsBmp should return false for PNG data"); | |
| } | |
| // Test IsBmp with insufficient data. | |
| DEF_TEST(RustBmpCodec_IsBmp_insufficient_data, r) { | |
| // BMP signature is "BM" (2 bytes), but header is larger | |
| const uint8_t shortData[] = {'B'}; | |
| bool isBmp = SkBmpRustDecoder::IsBmp(shortData, sizeof(shortData)); | |
| REPORTER_ASSERT(r, !isBmp, "IsBmp should return false for insufficient data"); | |
| } | |
| // Table-based test for handling invalid/corrupted BMP files. | |
| DEF_TEST(RustBmpCodec_invalid_bmp_handling, r) { | |
| auto test = [&r](const char* description, const char* file) { | |
| skiatest::ReporterContext ctx(r, description); | |
| sk_sp<SkData> data = GetResourceAsData(file); | |
| if (!data) { | |
| ERRORF(r, "Missing resource: %s", file); | |
| return; | |
| } | |
| SkCodec::Result result; | |
| std::unique_ptr<SkCodec> codec = | |
| SkBmpRustDecoder::Decode(SkMemoryStream::Make(std::move(data)), &result); | |
| // If we got a codec, try to decode to ensure we don't crash | |
| if (codec) { | |
| auto [image, decodeResult] = codec->getImage(); | |
| // Any result is acceptable as long as we don't crash | |
| (void)image; | |
| (void)decodeResult; | |
| } | |
| }; | |
| test("zero width", "empty_images/zero-width.bmp"); | |
| test("zero height", "empty_images/zero-height.bmp"); | |
| test("corrupted incomplete", "invalid_images/b33251605.bmp"); | |
| test("invalid header", "invalid_images/b33651913.bmp"); | |
| test("extreme dimensions", "invalid_images/b34778578.bmp"); | |
| test("os2 fuzz", "invalid_images/osfuzz6288.bmp"); | |
| } | |
| // Test explicit rewind through multiple getPixels calls. | |
| DEF_TEST(RustBmpCodec_explicit_rewind, r) { | |
| std::unique_ptr<SkCodec> codec = decode_bmp(r, "images/randPixels.bmp"); | |
| if (!codec) { | |
| return; | |
| } | |
| // First decode | |
| SkBitmap bitmap1; | |
| bitmap1.allocPixels(codec->getInfo()); | |
| SkCodec::Result result1 = codec->getPixels(bitmap1.pixmap()); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result1); | |
| // Second decode (implicitly rewinds internally) | |
| SkBitmap bitmap2; | |
| bitmap2.allocPixels(codec->getInfo()); | |
| SkCodec::Result result2 = codec->getPixels(bitmap2.pixmap()); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result2); | |
| // Verify dimensions match | |
| REPORTER_ASSERT(r, bitmap1.dimensions() == bitmap2.dimensions(), | |
| "Dimensions should match after rewind"); | |
| // Verify pixel data matches | |
| REPORTER_ASSERT(r, bitmap1.computeByteSize() == bitmap2.computeByteSize(), | |
| "Byte sizes should match"); | |
| REPORTER_ASSERT(r, | |
| memcmp(bitmap1.getPixels(), bitmap2.getPixels(), | |
| bitmap1.computeByteSize()) == 0, | |
| "Pixel data should match after rewind"); | |
| } | |
| // Test incremental decoding | |
| DEF_TEST(RustBmpCodec_IncrementalDecode, r) { | |
| const char* path = "images/randPixels.bmp"; | |
| sk_sp<SkData> data = GetResourceAsData(path); | |
| REPORTER_ASSERT(r, data, "Missing test image: %s", path); | |
| if (!data) { | |
| return; | |
| } | |
| // Decode with standard BMP decoder for reference | |
| std::unique_ptr<SkCodec> stdCodec = SkBmpDecoder::Decode(SkMemoryStream::Make(data), nullptr); | |
| REPORTER_ASSERT(r, stdCodec, "Failed to create standard BMP codec for %s", path); | |
| if (!stdCodec) { | |
| return; | |
| } | |
| SkBitmap reference; | |
| reference.allocPixels(stdCodec->getInfo()); | |
| SkCodec::Result result = stdCodec->getPixels(reference.pixmap()); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result); | |
| // Now decode using Rust BMP decoder with incremental decode | |
| std::unique_ptr<SkCodec> rustCodec = | |
| SkBmpRustDecoder::Decode(SkMemoryStream::Make(data), nullptr); | |
| REPORTER_ASSERT(r, rustCodec, "Failed to create Rust BMP codec for %s", path); | |
| if (!rustCodec) { | |
| return; | |
| } | |
| // Verify dimensions match before decoding | |
| REPORTER_ASSERT(r, rustCodec->dimensions() == stdCodec->dimensions(), | |
| "Rust codec dimensions should match standard codec"); | |
| SkBitmap incremental; | |
| incremental.allocPixels(rustCodec->getInfo()); | |
| result = rustCodec->startIncrementalDecode(incremental.info(), | |
| incremental.getPixels(), | |
| incremental.rowBytes()); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result); | |
| // Decode all rows incrementally | |
| // For BMP with all data available, this should complete in one call | |
| result = rustCodec->incrementalDecode(); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result); | |
| // Verify that Rust incremental decode produces the same output as standard BMP decoder | |
| REPORTER_ASSERT(r, reference.dimensions() == incremental.dimensions(), | |
| "Dimensions should match"); | |
| REPORTER_ASSERT(r, reference.computeByteSize() == incremental.computeByteSize(), | |
| "Byte sizes should match"); | |
| // Compare pixel data | |
| const uint8_t* refPixels = static_cast<const uint8_t*>(reference.getPixels()); | |
| const uint8_t* incPixels = static_cast<const uint8_t*>(incremental.getPixels()); | |
| size_t byteSize = reference.computeByteSize(); | |
| bool pixelsMatch = memcmp(refPixels, incPixels, byteSize) == 0; | |
| REPORTER_ASSERT(r, pixelsMatch, | |
| "Rust incremental decode should produce identical output to standard BMP " | |
| "decoder"); | |
| } | |
| // Test incremental decode with progressively available data. | |
| // Simulates a streaming scenario where data arrives in chunks (like over a network). | |
| // This tests true resumability: starting with partial data, getting kIncompleteInput, | |
| // adding more data, and resuming until complete. Tests both top-down and bottom-up | |
| // BMP images to ensure correct row ordering in both cases. | |
| DEF_TEST(RustBmpCodec_IncrementalDecode_PartialStreaming, r) { | |
| struct TestCase { | |
| const char* path; | |
| const char* description; | |
| int expectedIterations; | |
| bool isTopDown; // Used for intermediate row verification | |
| int expectedFirstRowsDecoded; // Expected rows after first 8KB chunk | |
| }; | |
| // Test both top-down and bottom-up BMP files. | |
| // expectedFirstRowsDecoded is calculated as: | |
| // (kInitialBytes + kChunkSize - headerSize) / srcRowBytes | |
| // where srcRowBytes is the row size in the source BMP file. | |
| constexpr TestCase testCases[] = { | |
| // Top-down 32bpp: 307254 bytes, 54-byte header, row=320*4=1280 bytes | |
| // After first chunk: (256 + 8192 - 54) / 1280 = 6 rows | |
| {"images/32bpp-topdown-320x240.bmp", "top-down 320x240 32-bit", 39, true, 6}, | |
| // Bottom-up 24bpp with ICC: 138-byte header, row=127*3 padded to 384 bytes | |
| // After first chunk: (256 + 8192 - 138) / 384 = 21 rows | |
| {"images/rgb24prof.bmp", "bottom-up 127x64 24-bit with ICC", 4, false, 21}, | |
| }; | |
| for (const auto& testCase : testCases) { | |
| skiatest::ReporterContext ctx(r, testCase.description); | |
| sk_sp<SkData> data = GetResourceAsData(testCase.path); | |
| REPORTER_ASSERT(r, data, "Missing test image: %s", testCase.path); | |
| if (!data) { | |
| continue; | |
| } | |
| // Create a HaltingStream that starts with enough data for the BMP header. | |
| // Use 256 bytes to accommodate various BMP header sizes (including ICC). | |
| constexpr size_t kInitialBytes = 256; | |
| constexpr size_t kChunkSize = 8 * 1024; // Add 8KB at a time | |
| const size_t fullSize = data->size(); | |
| const size_t initialBytes = std::min(kInitialBytes, fullSize); | |
| // Create stream and retain raw pointer before moving to codec | |
| auto haltingStream = std::make_unique<HaltingStream>(data, initialBytes); | |
| HaltingStream* streamPtr = haltingStream.get(); | |
| // Create codec with Rust BMP decoder | |
| SkCodec::Result result; | |
| std::unique_ptr<SkCodec> codec = | |
| SkBmpRustDecoder::Decode(std::move(haltingStream), &result); | |
| REPORTER_ASSERT(r, codec, "Failed to create codec with %zu bytes, result: %s", | |
| initialBytes, SkCodec::ResultToString(result)); | |
| if (!codec) { | |
| continue; | |
| } | |
| // Get the image info from the streaming codec | |
| const SkImageInfo streamingInfo = codec->getInfo(); | |
| // Decode the full image for comparison using standard BMP decoder, | |
| // requesting the same image info for valid comparison. | |
| std::unique_ptr<SkCodec> stdCodec = | |
| SkBmpDecoder::Decode(SkMemoryStream::Make(data), nullptr); | |
| REPORTER_ASSERT(r, stdCodec, "Failed to create standard BMP codec"); | |
| if (!stdCodec) { | |
| continue; | |
| } | |
| // Verify dimensions match | |
| REPORTER_ASSERT(r, codec->dimensions() == stdCodec->dimensions(), | |
| "Streaming codec dimensions should match standard codec"); | |
| SkBitmap reference; | |
| reference.allocPixels(streamingInfo); // Use streaming codec's info | |
| SkCodec::Result refResult = stdCodec->getPixels(reference.pixmap()); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, refResult); | |
| SkBitmap bitmap; | |
| bitmap.allocPixels(streamingInfo); | |
| // Fill with non-zero pattern to verify decoder properly writes all pixels | |
| memset(bitmap.getPixels(), 0xAB, bitmap.computeByteSize()); | |
| SkCodec::Options options; | |
| options.fZeroInitialized = SkCodec::kNo_ZeroInitialized; | |
| result = codec->startIncrementalDecode( | |
| bitmap.info(), bitmap.getPixels(), bitmap.rowBytes(), &options); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result); | |
| // Resumable decode loop | |
| int totalRowsDecoded = 0; | |
| int iterations = 0; | |
| constexpr int kMaxIterations = 1000; | |
| bool sawProgressiveRows = false; // Track if we saw partial row decoding | |
| bool verifiedDecodedRows = false; // Track if we verified decoded rows | |
| while (iterations < kMaxIterations) { | |
| iterations++; | |
| int rowsDecoded = 0; | |
| result = codec->incrementalDecode(&rowsDecoded); | |
| if (result == SkCodec::kSuccess) { | |
| totalRowsDecoded = rowsDecoded; | |
| break; | |
| } else if (result == SkCodec::kIncompleteInput) { | |
| // Track if we got partial rows (progressive rendering) | |
| if (rowsDecoded > 0 && rowsDecoded < codec->getInfo().height()) { | |
| sawProgressiveRows = true; | |
| // Verify decoded rows match reference image (not garbage 0xAB). | |
| // Only check once to avoid slowing down the test. | |
| if (!verifiedDecodedRows) { | |
| verifiedDecodedRows = true; | |
| // Verify rowsDecoded matches our expected value based on | |
| // chunk size, header size, and row bytes. | |
| REPORTER_ASSERT(r, rowsDecoded == testCase.expectedFirstRowsDecoded, | |
| "Expected %d rows decoded after first chunk, got %d", | |
| testCase.expectedFirstRowsDecoded, rowsDecoded); | |
| const int height = codec->getInfo().height(); | |
| const size_t rowBytes = bitmap.rowBytes(); | |
| const uint8_t* bitmapPixels = | |
| static_cast<const uint8_t*>(bitmap.getPixels()); | |
| const uint8_t* refPixels = | |
| static_cast<const uint8_t*>(reference.getPixels()); | |
| // For top-down images: decoded rows are at positions 0 to | |
| // rowsDecoded-1. | |
| // For bottom-up images: decoded rows are placed at the bottom | |
| // of the buffer, positions (height - rowsDecoded) to (height - 1). | |
| int startRow = testCase.isTopDown ? 0 : (height - rowsDecoded); | |
| bool decodedRowsMatch = true; | |
| for (int i = 0; i < rowsDecoded && decodedRowsMatch; ++i) { | |
| int y = startRow + i; | |
| if (memcmp(bitmapPixels + y * rowBytes, | |
| refPixels + y * rowBytes, | |
| rowBytes) != 0) { | |
| decodedRowsMatch = false; | |
| ERRORF(r, "Decoded row %d doesn't match reference", y); | |
| } | |
| } | |
| REPORTER_ASSERT(r, decodedRowsMatch, | |
| "Decoded rows should match reference, not garbage"); | |
| } | |
| } | |
| totalRowsDecoded = rowsDecoded; | |
| if (streamPtr->isAllDataReceived()) { | |
| ERRORF(r, "Got kIncompleteInput with all data available"); | |
| break; | |
| } | |
| streamPtr->addNewData(kChunkSize); | |
| } else { | |
| ERRORF(r, "Unexpected result: %s", SkCodec::ResultToString(result)); | |
| break; | |
| } | |
| } | |
| REPORTER_ASSERT(r, iterations < kMaxIterations, | |
| "Decode loop exceeded max iterations"); | |
| // Verify the exact iteration count for deterministic behavior | |
| REPORTER_ASSERT(r, iterations == testCase.expectedIterations, | |
| "Expected exactly %d iterations, got %d", | |
| testCase.expectedIterations, iterations); | |
| REPORTER_ASSERT(r, result == SkCodec::kSuccess, | |
| "Should complete successfully, got %s", | |
| SkCodec::ResultToString(result)); | |
| REPORTER_ASSERT(r, totalRowsDecoded == codec->getInfo().height(), | |
| "Should decode all %d rows, decoded %d", | |
| codec->getInfo().height(), totalRowsDecoded); | |
| // Verify progressive rendering occurred | |
| REPORTER_ASSERT(r, sawProgressiveRows, | |
| "Should see progressive row decoding with small chunk sizes"); | |
| // Verify we checked decoded rows during partial decode | |
| REPORTER_ASSERT(r, verifiedDecodedRows, | |
| "Should have verified decoded rows during partial decode"); | |
| // Verify final image matches standard decoder output | |
| REPORTER_ASSERT(r, bitmap.dimensions() == reference.dimensions(), | |
| "Dimensions should match reference"); | |
| // Compare row by row for detailed error reporting. | |
| // Both decoders output rows in logical (top-down) order. | |
| const size_t rowBytes = bitmap.rowBytes(); | |
| const uint8_t* bitmapPixels = static_cast<const uint8_t*>(bitmap.getPixels()); | |
| const uint8_t* refPixels = static_cast<const uint8_t*>(reference.getPixels()); | |
| const int height = codec->getInfo().height(); | |
| bool allRowsMatch = true; | |
| for (int y = 0; y < height; ++y) { | |
| if (memcmp(bitmapPixels + y * rowBytes, | |
| refPixels + y * rowBytes, rowBytes) != 0) { | |
| allRowsMatch = false; | |
| ERRORF(r, "Row %d doesn't match between Rust streaming and " | |
| "standard decoder", y); | |
| } | |
| } | |
| REPORTER_ASSERT(r, allRowsMatch, | |
| "Streaming decode should match standard BMP decoder output"); | |
| } | |
| } | |
| // Test incomplete metadata - stream with partial BMP header | |
| DEF_TEST(RustBmpCodec_IncompleteMetadata_PartialHeader, r) { | |
| // Load a valid BMP | |
| sk_sp<SkData> data = GetResourceAsData("images/randPixels.bmp"); | |
| if (!data) { | |
| ERRORF(r, "Failed to load test image"); | |
| return; | |
| } | |
| // Create stream with only partial header (30 bytes of 54-byte header) | |
| constexpr size_t kPartialSize = 30; | |
| sk_sp<SkData> partialData = SkData::MakeSubset(data.get(), 0, kPartialSize); | |
| std::unique_ptr<SkStream> partialStream = SkMemoryStream::Make(partialData); | |
| SkCodec::Result result; | |
| std::unique_ptr<SkCodec> codec = | |
| SkBmpRustDecoder::Decode(std::move(partialStream), &result); | |
| // Should fail with incomplete input (not enough for metadata) | |
| REPORTER_ASSERT(r, codec == nullptr, | |
| "Should not create codec with incomplete metadata"); | |
| REPORTER_ASSERT(r, result == SkCodec::kIncompleteInput, | |
| "Should return kIncompleteInput, got %d", result); | |
| } | |
| // Test incomplete row data - stream with partial pixel data | |
| DEF_TEST(RustBmpCodec_IncompleteRowData, r) { | |
| // Load a valid BMP | |
| sk_sp<SkData> data = GetResourceAsData("images/randPixels.bmp"); | |
| if (!data) { | |
| ERRORF(r, "Failed to load test image"); | |
| return; | |
| } | |
| // Get codec to determine dimensions | |
| std::unique_ptr<SkCodec> fullCodec = | |
| SkBmpRustDecoder::Decode(SkMemoryStream::Make(data), nullptr); | |
| if (!fullCodec) { | |
| ERRORF(r, "Failed to create codec from full data"); | |
| return; | |
| } | |
| SkImageInfo info = fullCodec->getInfo(); | |
| // BMP header is 54 bytes, calculate size for header + half of first row | |
| constexpr size_t kHeaderSize = 54; | |
| size_t rowBytes = info.width() * 3; // Assuming 24-bit RGB | |
| rowBytes = ((rowBytes + 3) / 4) * 4; // Row padding to 4-byte boundary | |
| size_t partialSize = kHeaderSize + (rowBytes / 2); | |
| // Ensure we don't exceed data size | |
| if (partialSize >= data->size()) { | |
| ERRORF(r, "Image too small for this test"); | |
| return; | |
| } | |
| sk_sp<SkData> partialData = SkData::MakeSubset(data.get(), 0, partialSize); | |
| std::unique_ptr<SkStream> partialStream = SkMemoryStream::Make(partialData); | |
| SkCodec::Result result; | |
| std::unique_ptr<SkCodec> codec = | |
| SkBmpRustDecoder::Decode(std::move(partialStream), &result); | |
| // With header data available, codec creation should succeed | |
| REPORTER_ASSERT(r, codec, "Codec should be created with header data available"); | |
| REPORTER_ASSERT(r, result == SkCodec::kSuccess, | |
| "Expected kSuccess for codec creation, got %s", | |
| SkCodec::ResultToString(result)); | |
| if (!codec) { | |
| return; | |
| } | |
| // Try to decode - should fail with kIncompleteInput since we don't have all pixel data | |
| SkBitmap bitmap; | |
| bitmap.allocPixels(info); | |
| SkCodec::Result decodeResult = codec->getPixels(info, bitmap.getPixels(), | |
| bitmap.rowBytes()); | |
| REPORTER_ASSERT(r, decodeResult == SkCodec::kIncompleteInput, | |
| "Expected kIncompleteInput with partial pixel data, got %s", | |
| SkCodec::ResultToString(decodeResult)); | |
| } | |
| // Test that embedded ICC profiles in BMP V5 headers are correctly extracted and applied. | |
| // This uses rgb24prof.bmp from the image-rs test suite, which contains an embedded ICC profile. | |
| DEF_TEST(RustBmpCodec_ICCProfile, r) { | |
| const char* path = "images/rgb24prof.bmp"; | |
| sk_sp<SkData> data = GetResourceAsData(path); | |
| REPORTER_ASSERT(r, data, "Missing test image: %s", path); | |
| if (!data) { | |
| return; | |
| } | |
| std::unique_ptr<SkCodec> codec = | |
| SkBmpRustDecoder::Decode(SkMemoryStream::Make(data), nullptr); | |
| REPORTER_ASSERT(r, codec, "Failed to create Rust BMP codec"); | |
| if (!codec) { | |
| return; | |
| } | |
| // Verify that the codec extracted an ICC profile | |
| const skcms_ICCProfile* profile = codec->getICCProfile(); | |
| REPORTER_ASSERT(r, profile, | |
| "BMP with embedded ICC profile should have getICCProfile() != null"); | |
| if (!profile) { | |
| return; | |
| } | |
| // Verify the profile can be used to create a color space | |
| sk_sp<SkColorSpace> colorSpace = SkColorSpace::Make(*profile); | |
| REPORTER_ASSERT(r, colorSpace, "ICC profile should be valid and create a color space"); | |
| if (!colorSpace) { | |
| return; | |
| } | |
| // Verify the color space from getInfo() matches | |
| SkImageInfo info = codec->getInfo(); | |
| REPORTER_ASSERT(r, info.colorSpace(), "getInfo() should have a color space"); | |
| if (info.colorSpace()) { | |
| REPORTER_ASSERT(r, SkColorSpace::Equals(info.colorSpace(), colorSpace.get()), | |
| "Color space from getInfo() should match ICC profile"); | |
| } | |
| // Verify decoding works with the ICC profile | |
| auto [image, result] = codec->getImage(); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, result); | |
| REPORTER_ASSERT(r, image, "Should be able to decode BMP with ICC profile"); | |
| } | |
| DEF_TEST(RustBmpCodec_ICCProfileTransform, r) { | |
| sk_sp<SkData> profile = GetResourceAsData("icc_profiles/AdobeRGB1998.icc"); | |
| REPORTER_ASSERT(r, profile, "Missing Adobe RGB ICC profile"); | |
| if (!profile) { | |
| return; | |
| } | |
| sk_sp<SkData> data = make_bmp_with_icc_profile(*profile); | |
| std::unique_ptr<SkCodec> codec = | |
| SkBmpRustDecoder::Decode(SkMemoryStream::Make(data), nullptr); | |
| std::unique_ptr<SkCodec> referenceCodec = | |
| SkBmpDecoder::Decode(SkMemoryStream::Make(data), nullptr); | |
| REPORTER_ASSERT(r, codec && referenceCodec, "Failed to create BMP codecs"); | |
| if (!codec || !referenceCodec) { | |
| return; | |
| } | |
| SkImageInfo dstInfo = codec->getInfo() | |
| .makeColorType(kN32_SkColorType) | |
| .makeAlphaType(kPremul_SkAlphaType) | |
| .makeColorSpace(SkColorSpace::MakeSRGB()); | |
| SkBitmap actual; | |
| SkBitmap expected; | |
| REPORTER_ASSERT(r, actual.tryAllocPixels(dstInfo)); | |
| REPORTER_ASSERT(r, expected.tryAllocPixels(dstInfo)); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, codec->getPixels(actual.pixmap())); | |
| REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT(r, referenceCodec->getPixels(expected.pixmap())); | |
| const float tolerances[4] = {0, 0, 0, 0}; | |
| auto reportError = std::function<ComparePixmapsErrorReporter>( | |
| [&](int x, int y, const float differences[4]) { | |
| ERRORF(r, | |
| "ICC-transformed pixels differ at (%d, %d): (%f, %f, %f, %f)", | |
| x, y, differences[0], differences[1], differences[2], differences[3]); | |
| }); | |
| ComparePixels(expected.pixmap(), actual.pixmap(), tolerances, reportError); | |
| } | |
| #undef REPORTER_ASSERT_SUCCESSFUL_CODEC_RESULT |