From a999bf4218d421495253e7a49ab5bf834df284dc Mon Sep 17 00:00:00 2001 From: Nico Weber Date: Mon, 27 Jan 2025 15:08:04 -0500 Subject: [PATCH] LibGfx/JPEG2000: Implement conversion of samples to Bitmap This is fairly ad-hoc for now, but with this `image` can read many JPEG2000 files and save them in another format -- jpeg2000 decoding now works end-to-end :^) It's not yet hooked up in LibPDF, it doesn't yet handle somewhat more exotic color formats well, it misses support for all kinds of relatively rare other JPEG2000 features, but the basics are working. This also doesn't do any of the clever things that are possible with jpeg2000, such as only decoding image components that are needed, only decoding as many bits per channel as needed, only decoding as much resolution as needed, or decoding a subrect of the image. Maybe later. --- Tests/LibGfx/TestImageDecoder.cpp | 17 +++-- .../LibGfx/ImageFormats/JPEG2000Loader.cpp | 62 ++++++++++++++++++- 2 files changed, 71 insertions(+), 8 deletions(-) diff --git a/Tests/LibGfx/TestImageDecoder.cpp b/Tests/LibGfx/TestImageDecoder.cpp index 7f2a53b9cae035..814cdb36be3715 100644 --- a/Tests/LibGfx/TestImageDecoder.cpp +++ b/Tests/LibGfx/TestImageDecoder.cpp @@ -672,12 +672,17 @@ TEST_CASE(test_jpeg2000_spec_annex_j_10) // clang-format on auto plugin_decoder = TRY_OR_FAIL(Gfx::JPEG2000ImageDecoderPlugin::create(data)); - EXPECT_EQ(plugin_decoder->size(), Gfx::IntSize(1, 9)); - - // FIXME: Do something with this. - // For now, this is useful for debugging: - // `Build/lagom/bin/TestImageDecoder test_jpeg2000_spec_annex_j_10` prints internal state with JPEG2000_DEBUG=1. - (void)plugin_decoder->frame(0); + auto frame = TRY_OR_FAIL(expect_single_frame_of_size(*plugin_decoder, { 1, 9 })); + + // "After the inverse 5-3 reversible filter and level shifting, the component samples in decimal are:" + Array expected_values = { 101, 103, 104, 105, 96, 97, 96, 102, 109 }; + for (int i = 0; i < 9; ++i) { + auto pixel = frame.image->get_pixel(0, i); + EXPECT_EQ(pixel.red(), expected_values[i]); + EXPECT_EQ(pixel.green(), expected_values[i]); + EXPECT_EQ(pixel.blue(), expected_values[i]); + EXPECT_EQ(pixel.alpha(), 0xff); + } } TEST_CASE(test_jpeg2000_simple) diff --git a/Userland/Libraries/LibGfx/ImageFormats/JPEG2000Loader.cpp b/Userland/Libraries/LibGfx/ImageFormats/JPEG2000Loader.cpp index c551abce6ffabc..6c39a119c9f5e1 100644 --- a/Userland/Libraries/LibGfx/ImageFormats/JPEG2000Loader.cpp +++ b/Userland/Libraries/LibGfx/ImageFormats/JPEG2000Loader.cpp @@ -780,6 +780,8 @@ struct JPEG2000LoadingContext { Vector coms; Vector tiles; + RefPtr bitmap; + CodingStyleParameters const& coding_style_parameters_for_component(TileData const& tile, size_t component_index) const { // Tile-part COC > Tile-part COD > Main COC > Main COD @@ -2013,6 +2015,62 @@ static ErrorOr postprocess_samples(JPEG2000LoadingContext& context) return {}; } +static ErrorOr convert_to_bitmap(JPEG2000LoadingContext& context) +{ + // FIXME: This is pretty ad-hoc. It should look at JPEG2000ChannelDefinitionBox, + // JPEG2000ColorSpecificationBox::method, and if present at + // JPEG2000PaletteBox and JPEG2000ComponentMappingBox + // to figure out mapping from components to bitmap channels (and optionally return a CMYKBitmap instead). + + auto bitmap = TRY(Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, { context.siz.width, context.siz.height })); + + for (auto& tile : context.tiles) { + // compute_decoding_metadata currently rejects images with horizontal_separation or vertical_separation != 1. + for (auto& component : tile.components) { + if (component.rect.size() != tile.components[0].rect.size()) + return Error::from_string_literal("JPEG2000ImageDecoderPlugin: Components with differing sizes not yet supported"); + } + + int w = tile.components[0].rect.width(); + int h = tile.components[0].rect.height(); + + for (int y = 0; y < h; ++y) { + for (int x = 0; x < w; ++x) { + float value = tile.components[0].samples[y * w + x]; + + // FIXME: This is wrong for palettized images. + u8 byte_value = round_to(clamp(value, 0.0f, 255.0f)); + u8 r = byte_value; + u8 g = byte_value; + u8 b = byte_value; + u8 a = 255; + + if (tile.components.size() == 3) { + g = round_to(clamp(tile.components[1].samples[y * w + x], 0.0f, 255.0f)); + b = round_to(clamp(tile.components[2].samples[y * w + x], 0.0f, 255.0f)); + } else if (tile.components.size() == 4) { + g = round_to(clamp(tile.components[1].samples[y * w + x], 0.0f, 255.0f)); + b = round_to(clamp(tile.components[2].samples[y * w + x], 0.0f, 255.0f)); + a = round_to(clamp(tile.components[3].samples[y * w + x], 0.0f, 255.0f)); + } + + Color pixel; + pixel.set_red(r); + pixel.set_green(g); + pixel.set_blue(b); + pixel.set_alpha(a); + bitmap->set_pixel(x + tile.components[0].rect.left(), y + tile.components[0].rect.top(), pixel); + } + } + } + + // FIXME: Could release sample data here, to reduce peak memory use. + + context.bitmap = move(bitmap); + + return {}; +} + static ErrorOr decode_image(JPEG2000LoadingContext& context) { TRY(parse_codestream_tile_headers(context)); @@ -2021,7 +2079,7 @@ static ErrorOr decode_image(JPEG2000LoadingContext& context) TRY(decode_bitplanes_to_coefficients(context)); TRY(run_inverse_discrete_wavelet_transform(context)); TRY(postprocess_samples(context)); - // FIXME: Convert transformed coefficients to pixel values. + TRY(convert_to_bitmap(context)); return {}; } @@ -2063,7 +2121,7 @@ ErrorOr JPEG2000ImageDecoderPlugin::frame(size_t index, Op m_context->state = JPEG2000LoadingContext::State::DecodedImage; } - return Error::from_string_literal("JPEG2000ImageDecoderPlugin: Draw the rest of the owl"); + return ImageFrameDescriptor { m_context->bitmap, 0 }; } ErrorOr> JPEG2000ImageDecoderPlugin::icc_data()