LibGfx: Correctly handle OS/2 BMPs with 3-byte color entries

We now properly handle OS/2 format BMPs that use 3 bytes per color
entry instead of 4. While OS/2 2.x officially specified 4 bytes per
color, some tools still produce files with 3-byte entries. We can
identify such files by checking the available color table space.
This commit is contained in:
aplefull 2025-05-06 21:32:35 +02:00 committed by Jelle Raaijmakers
commit e5944a4d9e
Notes: github-actions[bot] 2025-05-09 19:49:19 +00:00
3 changed files with 33 additions and 3 deletions

View file

@ -958,7 +958,26 @@ static ErrorOr<void> decode_bmp_color_table(BMPLoadingContext& context)
return {}; return {};
} }
auto bytes_per_color = context.dib_type == DIBType::Core ? 3 : 4; // OS/2 1.x (Core) uses 3 bytes per color
// OS/2 2.x (OSV2) can use 3 or 4 bytes per color
u8 bytes_per_color;
if (context.dib_type == DIBType::Core) {
bytes_per_color = 3;
} else if (context.dib_type == DIBType::OSV2 || context.dib_type == DIBType::OSV2Short) {
// For OS/2 2.x we need to determine the number of bytes per color based on the size of the color table
u8 header_size = context.is_included_in_ico ? 0 : bmp_header_size;
u32 available_size = context.data_offset - header_size - context.dib_size();
u32 max_colors = 1 << context.dib.core.bpp;
if (available_size == 3 * max_colors) {
bytes_per_color = 3;
} else {
bytes_per_color = 4;
}
} else {
bytes_per_color = 4;
}
u32 max_colors = 1 << context.dib.core.bpp; u32 max_colors = 1 << context.dib.core.bpp;
u8 header_size = !context.is_included_in_ico ? bmp_header_size : 0; u8 header_size = !context.is_included_in_ico ? bmp_header_size : 0;
@ -974,9 +993,9 @@ static ErrorOr<void> decode_bmp_color_table(BMPLoadingContext& context)
if (context.dib_type <= DIBType::OSV2) { if (context.dib_type <= DIBType::OSV2) {
// Partial color tables are not supported, so the space of the color // Partial color tables are not supported, so the space of the color
// table must be at least enough for the maximum amount of colors // table must be at least enough for the maximum amount of colors
if (size_of_color_table < 3 * max_colors) { if (size_of_color_table < bytes_per_color * max_colors) {
// This is against the spec, but most viewers process it anyways // This is against the spec, but most viewers process it anyways
dbgln("BMP with CORE header does not have enough colors. Has: {}, expected: {}", size_of_color_table, (3 * max_colors)); dbgln("BMP with CORE header does not have enough colors. Has: {}, expected: {}", size_of_color_table, (bytes_per_color * max_colors));
} }
} }

View file

@ -94,6 +94,17 @@ TEST_CASE(test_bmp_v4)
EXPECT_EQ(frame.image->get_pixel(0, 0), Gfx::Color::NamedColor::Red); EXPECT_EQ(frame.image->get_pixel(0, 0), Gfx::Color::NamedColor::Red);
} }
TEST_CASE(test_bmp_os2_3bit)
{
auto file = TRY_OR_FAIL(Core::MappedFile::map(TEST_INPUT("bmp/os2_3bpc.bmp"sv)));
EXPECT(Gfx::BMPImageDecoderPlugin::sniff(file->bytes()));
auto plugin_decoder = TRY_OR_FAIL(Gfx::BMPImageDecoderPlugin::create(file->bytes()));
auto frame = TRY_OR_FAIL(expect_single_frame_of_size(*plugin_decoder, { 300, 200 }));
EXPECT_EQ(frame.image->get_pixel(150, 100), Gfx::Color::NamedColor::Black);
EXPECT_EQ(frame.image->get_pixel(152, 100), Gfx::Color::NamedColor::White);
}
TEST_CASE(test_ico_malformed_frame) TEST_CASE(test_ico_malformed_frame)
{ {
Array test_inputs = { Array test_inputs = {

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB