From 1f8a42c3674a0605bc331c0a8df7797a045e75c1 Mon Sep 17 00:00:00 2001 From: InvalidUsernameException Date: Sun, 23 Nov 2025 13:07:43 +0100 Subject: [PATCH] LibGfx: Add a test for bitmap export The verified pixel output in this test just reflects currently observed behavior, I have not verified that all cases output the correct data wrt what the spec expects. --- Libraries/LibGfx/Bitmap.cpp | 12 ++ Libraries/LibGfx/Bitmap.h | 17 +- Libraries/LibGfx/ImmutableBitmap.cpp | 12 ++ Libraries/LibGfx/ImmutableBitmap.h | 25 +-- Tests/LibGfx/CMakeLists.txt | 1 + Tests/LibGfx/TestImmutableBitmap.cpp | 243 +++++++++++++++++++++++++++ 6 files changed, 294 insertions(+), 16 deletions(-) create mode 100644 Tests/LibGfx/TestImmutableBitmap.cpp diff --git a/Libraries/LibGfx/Bitmap.cpp b/Libraries/LibGfx/Bitmap.cpp index 7ba9fa05de5..f24c1a881cc 100644 --- a/Libraries/LibGfx/Bitmap.cpp +++ b/Libraries/LibGfx/Bitmap.cpp @@ -31,6 +31,18 @@ struct BackingStore { size_t size_in_bytes { 0 }; }; +StringView bitmap_format_name(BitmapFormat format) +{ + switch (format) { +#define ENUMERATE_BITMAP_FORMAT(format) \ + case BitmapFormat::format: \ + return #format##sv; + ENUMERATE_BITMAP_FORMATS(ENUMERATE_BITMAP_FORMAT) +#undef ENUMERATE_BITMAP_FORMAT + } + VERIFY_NOT_REACHED(); +} + size_t Bitmap::minimum_pitch(size_t width, BitmapFormat format) { size_t element_size; diff --git a/Libraries/LibGfx/Bitmap.h b/Libraries/LibGfx/Bitmap.h index f2b959b2265..0a2f3d25725 100644 --- a/Libraries/LibGfx/Bitmap.h +++ b/Libraries/LibGfx/Bitmap.h @@ -20,14 +20,21 @@ namespace Gfx { // A pixel value that does not express any information about its component order using RawPixel = u32; +#define ENUMERATE_BITMAP_FORMATS(X) \ + X(Invalid) \ + X(BGRx8888) \ + X(BGRA8888) \ + X(RGBx8888) \ + X(RGBA8888) + enum class BitmapFormat { - Invalid, - BGRx8888, - BGRA8888, - RGBx8888, - RGBA8888, +#define ENUMERATE_BITMAP_FORMAT(format) format, + ENUMERATE_BITMAP_FORMATS(ENUMERATE_BITMAP_FORMAT) +#undef ENUMERATE_BITMAP_FORMAT }; +[[nodiscard]] StringView bitmap_format_name(BitmapFormat); + inline bool is_valid_bitmap_format(u32 const format) { switch (format) { diff --git a/Libraries/LibGfx/ImmutableBitmap.cpp b/Libraries/LibGfx/ImmutableBitmap.cpp index 3f725decac6..a4de7b1b8e0 100644 --- a/Libraries/LibGfx/ImmutableBitmap.cpp +++ b/Libraries/LibGfx/ImmutableBitmap.cpp @@ -16,6 +16,18 @@ namespace Gfx { +StringView export_format_name(ExportFormat format) +{ + switch (format) { +#define ENUMERATE_EXPORT_FORMAT(format) \ + case Gfx::ExportFormat::format: \ + return #format##sv; + ENUMERATE_EXPORT_FORMATS(ENUMERATE_EXPORT_FORMAT) +#undef ENUMERATE_EXPORT_FORMAT + } + VERIFY_NOT_REACHED(); +} + struct ImmutableBitmapImpl { sk_sp sk_image; SkBitmap sk_bitmap; diff --git a/Libraries/LibGfx/ImmutableBitmap.h b/Libraries/LibGfx/ImmutableBitmap.h index 7496175d241..09f99e3226f 100644 --- a/Libraries/LibGfx/ImmutableBitmap.h +++ b/Libraries/LibGfx/ImmutableBitmap.h @@ -21,20 +21,23 @@ namespace Gfx { struct ImmutableBitmapImpl; +#define ENUMERATE_EXPORT_FORMATS(X) \ + X(Gray8) \ + X(Alpha8) \ + X(RGB565) \ + X(RGBA5551) \ + X(RGBA4444) \ + X(RGB888) \ + X(RGBA8888) + enum class ExportFormat : u8 { - // 8 bit - Gray8, - Alpha8, - // 16 bit - RGB565, - RGBA5551, - RGBA4444, - // 24 bit - RGB888, - // 32 bit - RGBA8888, +#define ENUMERATE_EXPORT_FORMAT(format) format, + ENUMERATE_EXPORT_FORMATS(ENUMERATE_EXPORT_FORMAT) +#undef ENUMERATE_EXPORT_FORMAT }; +[[nodiscard]] StringView export_format_name(ExportFormat); + struct ExportFlags { enum : u8 { PremultiplyAlpha = 1 << 0, diff --git a/Tests/LibGfx/CMakeLists.txt b/Tests/LibGfx/CMakeLists.txt index 38103195afe..0a2cf807116 100644 --- a/Tests/LibGfx/CMakeLists.txt +++ b/Tests/LibGfx/CMakeLists.txt @@ -3,6 +3,7 @@ set(TEST_SOURCES TestColor.cpp TestImageDecoder.cpp TestImageWriter.cpp + TestImmutableBitmap.cpp TestQuad.cpp TestRect.cpp TestWOFF.cpp diff --git a/Tests/LibGfx/TestImmutableBitmap.cpp b/Tests/LibGfx/TestImmutableBitmap.cpp new file mode 100644 index 00000000000..de9050acf85 --- /dev/null +++ b/Tests/LibGfx/TestImmutableBitmap.cpp @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2025, Ladybird contributors + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include + +TEST_CASE(export_to_byte_buffer) +{ + enum class Premultiplied : u8 { + Yes, + No, + }; + + struct TestData { + Vector source_formats_to_test; + Vector source_alpha_cases_to_test; + Gfx::BGRA8888 source_pixels[4]; + Gfx::ExportFormat export_format; + Vector target_alpha_cases_to_test; + Vector expected_result; + }; + + Vector all_bitmap_formats = { + Gfx::BitmapFormat::BGRx8888, + Gfx::BitmapFormat::BGRA8888, + Gfx::BitmapFormat::RGBx8888, + Gfx::BitmapFormat::RGBA8888, + }; + + Vector alpha_bitmap_formats = { + Gfx::BitmapFormat::BGRA8888, + Gfx::BitmapFormat::RGBA8888, + }; + + Vector non_alpha_bitmap_formats = { + Gfx::BitmapFormat::BGRx8888, + Gfx::BitmapFormat::RGBx8888, + }; + + // FIXME: Some of these test cases seem suspect, particularly with regard to alpha-(un)premultiplication. We should + // validate whether these actually have the correct behavior. + TestData subtests[] = { + { + alpha_bitmap_formats, + { Premultiplied::No }, + { 0x00FFFFFF, 0x55FFFFFF, 0xAAFFFFFF, 0xFFFFFFFF }, + Gfx::ExportFormat::Gray8, + { Premultiplied::No, Premultiplied::Yes }, + { 0x00, 0x55, 0xAA, 0xFF }, + }, + { + non_alpha_bitmap_formats, + { Premultiplied::No }, + { 0x00000000, 0x55555555, 0xAAAAAAAA, 0xFFFFFFFF }, + Gfx::ExportFormat::Gray8, + { Premultiplied::No, Premultiplied::Yes }, + { 0x00, 0x55, 0xAA, 0xFF }, + }, + { + all_bitmap_formats, + { Premultiplied::Yes }, + { 0x00000000, 0x55555555, 0xAAAAAAAA, 0xFFFFFFFF }, + Gfx::ExportFormat::Gray8, + { Premultiplied::No, Premultiplied::Yes }, + { 0x00, 0x55, 0xAA, 0xFF }, + }, + { + alpha_bitmap_formats, + { Premultiplied::No, Premultiplied::Yes }, + { 0x00112233, 0x44556677, 0x8899AABB, 0xCCDDEEFF }, + Gfx::ExportFormat::Alpha8, + { Premultiplied::No, Premultiplied::Yes }, + { 0x00, 0x44, 0x88, 0xCC }, + }, + { + non_alpha_bitmap_formats, + { Premultiplied::No, Premultiplied::Yes }, + { 0x00112233, 0x44556677, 0x8899AABB, 0xCCDDEEFF }, + Gfx::ExportFormat::Alpha8, + { Premultiplied::No, Premultiplied::Yes }, + { 0xFF, 0xFF, 0xFF, 0xFF }, + }, + { + non_alpha_bitmap_formats, + { Premultiplied::No, Premultiplied::Yes }, + { 0xFFFF0000, 0xFF00FF00, 0xFF0000FF, 0xFFFF00FF }, + Gfx::ExportFormat::RGB565, + { Premultiplied::No, Premultiplied::Yes }, + { 0x00, 0xF8, 0xE0, 0x07, 0x1F, 0x00, 0x1F, 0xF8 }, + }, + { + alpha_bitmap_formats, + { Premultiplied::No }, + { 0x33FFFFFF, 0x66FFFFFF, 0x99FFFFFF, 0xCCFFFFFF }, + Gfx::ExportFormat::RGB565, + { Premultiplied::No, Premultiplied::Yes }, + { 0xA6, 0x31, 0x2C, 0x63, 0xD3, 0x9C, 0x59, 0xCE }, + }, + { + alpha_bitmap_formats, + { Premultiplied::Yes }, + { 0x33FF0000, 0x6600FF00, 0x990000FF, 0xCCFF00FF }, + Gfx::ExportFormat::RGB565, + { Premultiplied::No, Premultiplied::Yes }, + { 0x00, 0xF8, 0xE0, 0x07, 0x1F, 0x00, 0x1F, 0xF8 }, + }, + { + non_alpha_bitmap_formats, + { Premultiplied::No, Premultiplied::Yes }, + { 0x00112233, 0x44556677, 0x8899AABB, 0xCCDDEEFF }, + Gfx::ExportFormat::RGBA4444, + { Premultiplied::No, Premultiplied::Yes }, + { 0x3f, 0x12, 0x7f, 0x56, 0xBF, 0x9A, 0xFF, 0xDE }, + }, + { + alpha_bitmap_formats, + { Premultiplied::No }, + { 0x33001122, 0x77445566, 0xBB8899AA, 0xFFCCDDEE }, + Gfx::ExportFormat::RGBA4444, + { Premultiplied::No }, + { 0x23, 0x01, 0x67, 0x45, 0xAB, 0x89, 0xEF, 0xCD }, + }, + { + alpha_bitmap_formats, + { Premultiplied::No }, + { 0x3355AAFF, 0x6655AAFF, 0x9955AAFF, 0xCC55AAFF }, + Gfx::ExportFormat::RGBA4444, + { Premultiplied::Yes }, + { 0x33, 0x12, 0x66, 0x24, 0x99, 0x36, 0xCC, 0x48 }, + }, + { + alpha_bitmap_formats, + { Premultiplied::Yes }, + { 0x33112233, 0x66224466, 0x99336699, 0xCC4488CC }, + Gfx::ExportFormat::RGBA4444, + { Premultiplied::No }, + { 0xF3, 0x5A, 0xF6, 0x5A, 0xF9, 0x5A, 0xFC, 0x5A }, + }, + { + alpha_bitmap_formats, + { Premultiplied::Yes }, + { 0x33001122, 0x77445566, 0xBB8899AA, 0xFFCCDDEE }, + Gfx::ExportFormat::RGBA4444, + { Premultiplied::Yes }, + { 0x23, 0x01, 0x67, 0x45, 0xAB, 0x89, 0xEF, 0xCD }, + }, + { + all_bitmap_formats, + { Premultiplied::No, Premultiplied::Yes }, + { 0x00112233, 0x44556677, 0x8899AABB, 0xCCDDEEFF }, + Gfx::ExportFormat::RGB888, + { Premultiplied::No, Premultiplied::Yes }, + { 0x11, 0x22, 0x33, 0x55, 0x66, 0x77, 0x99, 0xAA, 0xBB, 0xDD, 0xEE, 0xFF }, + }, + { + alpha_bitmap_formats, + { Premultiplied::No }, + { 0x33001122, 0x77445566, 0xBB8899AA, 0xFFCCDDEE }, + Gfx::ExportFormat::RGBA8888, + { Premultiplied::No }, + { 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF }, + }, + { + alpha_bitmap_formats, + { Premultiplied::No }, + { 0x3355AAFF, 0x6655AAFF, 0x9955AAFF, 0xCC55AAFF }, + Gfx::ExportFormat::RGBA8888, + { Premultiplied::Yes }, + { 0x11, 0x22, 0x33, 0x33, 0x22, 0x44, 0x66, 0x66, 0x33, 0x66, 0x99, 0x99, 0x44, 0x88, 0xCC, 0xCC }, + }, + { + alpha_bitmap_formats, + { Premultiplied::Yes }, + { 0x33112233, 0x66224466, 0x99336699, 0xCC4488CC }, + Gfx::ExportFormat::RGBA8888, + { Premultiplied::No }, + { 0x55, 0xAA, 0xFF, 0x33, 0x55, 0xAA, 0xFF, 0x66, 0x55, 0xAA, 0xFF, 0x99, 0x55, 0xAA, 0xFF, 0xCC }, + }, + { + alpha_bitmap_formats, + { Premultiplied::Yes }, + { 0x33001122, 0x77445566, 0xBB8899AA, 0xFFCCDDEE }, + Gfx::ExportFormat::RGBA8888, + { Premultiplied::Yes }, + { 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF }, + }, + { + non_alpha_bitmap_formats, + { Premultiplied::No, Premultiplied::Yes }, + { 0x00112233, 0x44556677, 0x8899AABB, 0xCCDDEEFF }, + Gfx::ExportFormat::RGBA8888, + { Premultiplied::No, Premultiplied::Yes }, + { 0x11, 0x22, 0x33, 0xFF, 0x55, 0x66, 0x77, 0xFF, 0x99, 0xAA, 0xBB, 0xFF, 0xDD, 0xEE, 0xFF, 0xFF }, + }, + }; + + auto alpha_case_name = [](Premultiplied premultiplied) -> StringView { + return premultiplied == Premultiplied::Yes ? "premultiplied"sv : "unpremultiplied"sv; + }; + + auto flip_y_name = [](u32 flags) -> StringView { + return flags & Gfx::ExportFlags::FlipY ? "flip Y"sv : "keep Y"sv; + }; + + auto count = 0; + for (auto const& subtest : subtests) { + for (auto source_format : subtest.source_formats_to_test) { + for (auto maybe_flip_y_flag : Vector { 0, Gfx::ExportFlags::FlipY }) { + for (auto source_alpha_case : subtest.source_alpha_cases_to_test) { + for (auto target_alpha_case : subtest.target_alpha_cases_to_test) { + auto export_flags = 0; + export_flags |= maybe_flip_y_flag; + export_flags |= target_alpha_case == Premultiplied::Yes ? Gfx::ExportFlags::PremultiplyAlpha : 0; + + dbgln("Running subtest {}: {} -> {}, {} -> {}, {}", count++, bitmap_format_name(source_format), export_format_name(subtest.export_format), alpha_case_name(source_alpha_case), alpha_case_name(target_alpha_case), flip_y_name(export_flags)); + + auto source_alpha_type = source_alpha_case == Premultiplied::Yes ? Gfx::AlphaType::Premultiplied : Gfx::AlphaType::Unpremultiplied; + auto bitmap = MUST(Gfx::Bitmap::create(source_format, source_alpha_type, { 2, 2 })); + auto logical_y0 = maybe_flip_y_flag ? 1 : 0; + auto logical_y1 = maybe_flip_y_flag ? 0 : 1; + bitmap->set_pixel(0, logical_y0, Color::from_bgra(subtest.source_pixels[0])); + bitmap->set_pixel(1, logical_y0, Color::from_bgra(subtest.source_pixels[1])); + bitmap->set_pixel(0, logical_y1, Color::from_bgra(subtest.source_pixels[2])); + bitmap->set_pixel(1, logical_y1, Color::from_bgra(subtest.source_pixels[3])); + + auto immutable_bitmap = Gfx::ImmutableBitmap::create(bitmap); + auto result = MUST(immutable_bitmap->export_to_byte_buffer(subtest.export_format, export_flags, 2, 2)); + + EXPECT_EQ(result.width, 2); + EXPECT_EQ(result.height, 2); + EXPECT_EQ(result.buffer.bytes(), subtest.expected_result); + } + } + } + } + } +}