From 534eeb6c4b73709181b4525270b31247d34e2878 Mon Sep 17 00:00:00 2001 From: Nicolas Ramz Date: Tue, 16 Jan 2024 15:29:44 +0100 Subject: [PATCH] LibGfx/ILBMLoader: Properly display images with a bitplane mask Images with a display mask ("stencil" as it's called in DPaint) add an extra bitplane which acts as a mask. For now, at least skip it properly. Later we should render masked pixels as transparent, but this requires some refactoring. --- Tests/LibGfx/TestImageDecoder.cpp | 11 +++++++++++ Tests/LibGfx/test-inputs/ilbm/test-stencil.iff | Bin 0 -> 9832 bytes .../Libraries/LibGfx/ImageFormats/ILBMLoader.cpp | 15 ++++++++++----- 3 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 Tests/LibGfx/test-inputs/ilbm/test-stencil.iff diff --git a/Tests/LibGfx/TestImageDecoder.cpp b/Tests/LibGfx/TestImageDecoder.cpp index 00a3a58b32..dec10f5141 100644 --- a/Tests/LibGfx/TestImageDecoder.cpp +++ b/Tests/LibGfx/TestImageDecoder.cpp @@ -265,6 +265,17 @@ TEST_CASE(test_small_24bit) EXPECT_EQ(frame.image->get_pixel(0, 4), Gfx::Color(1, 0, 1, 255)); } +TEST_CASE(test_stencil_mask) +{ + auto file = TRY_OR_FAIL(Core::MappedFile::map(TEST_INPUT("ilbm/test-stencil.iff"sv))); + EXPECT(Gfx::ILBMImageDecoderPlugin::sniff(file->bytes())); + auto plugin_decoder = TRY_OR_FAIL(Gfx::ILBMImageDecoderPlugin::create(file->bytes())); + + auto frame = TRY_OR_FAIL(expect_single_frame_of_size(*plugin_decoder, { 320, 200 })); + + EXPECT_EQ(frame.image->get_pixel(0, 4), Gfx::Color(0, 0, 0, 255)); +} + TEST_CASE(test_ilbm_malformed_header) { Array test_inputs = { diff --git a/Tests/LibGfx/test-inputs/ilbm/test-stencil.iff b/Tests/LibGfx/test-inputs/ilbm/test-stencil.iff new file mode 100644 index 0000000000000000000000000000000000000000..233ceac178c38b2b1991f607fb584add4e11eead GIT binary patch literal 9832 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 literal 0 HcmV?d00001 diff --git a/Userland/Libraries/LibGfx/ImageFormats/ILBMLoader.cpp b/Userland/Libraries/LibGfx/ImageFormats/ILBMLoader.cpp index 3300ae649d..7dfed023fe 100644 --- a/Userland/Libraries/LibGfx/ImageFormats/ILBMLoader.cpp +++ b/Userland/Libraries/LibGfx/ImageFormats/ILBMLoader.cpp @@ -182,7 +182,8 @@ static ErrorOr planar_to_chunky(ReadonlyBytes bitplanes, ILBMLoading u16 pitch = context.pitch; u16 width = context.bm_header.width; u16 height = context.bm_header.height; - u8 planes = context.bm_header.planes; + // mask is added as an extra plane + u8 planes = context.bm_header.mask == MaskType::HasMask ? context.bm_header.planes + 1 : context.bm_header.planes; size_t buffer_size = static_cast(width) * height; // If planes number is 24 we'll store R,G,B components so buffer needs to be 3 times width*height // otherwise we'll store a single 8bit index to the CMAP. @@ -209,17 +210,17 @@ static ErrorOr planar_to_chunky(ReadonlyBytes bitplanes, ILBMLoading // when enough data for current bitplane row has been read for (u8 b = 0; b < 8 && (i * 8) + b < width; b++) { u8 mask = 1 << (7 - b); - // get current plane - if (bit & mask) { + // get current plane: simply skip mask plane for now + if (bit & mask && p < context.bm_header.planes) { u16 x = (i * 8) + b; size_t offset = (scanline * pixel_size) + (x * pixel_size) + rgb_shift; // Only throw an error if we would actually attempt to write // outside of the chunky buffer. Some apps like PPaint produce // malformed bitplane data but files are still accepted by most readers // since they do not cause writing past the chunky buffer. - if (offset >= chunky.size()) { + if (offset >= chunky.size()) return Error::from_string_literal("Malformed bitplane data"); - } + chunky[offset] |= plane_mask; } } @@ -239,6 +240,10 @@ static ErrorOr uncompress_byte_run(ReadonlyBytes data, ILBMLoadingCo size_t plane_data_size = context.pitch * context.bm_header.height * context.bm_header.planes; + // The mask is encoded as an extra bitplane but is not counted in the bm_header planes + if (context.bm_header.mask == MaskType::HasMask) + plane_data_size += context.pitch * context.bm_header.height; + // The maximum run length of this compression method is 127 bytes, so the uncompressed size // cannot be more than 127 times the size of the chunk we are decompressing. if (plane_data_size > NumericLimits::max() || ceil_div(plane_data_size, 127ul) > length)