Skip to content

Commit

Permalink
LibGfx/JPEG2000: Implement conversion of samples to Bitmap
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
nico committed Jan 30, 2025
1 parent f8c96c0 commit a999bf4
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 8 deletions.
17 changes: 11 additions & 6 deletions Tests/LibGfx/TestImageDecoder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
62 changes: 60 additions & 2 deletions Userland/Libraries/LibGfx/ImageFormats/JPEG2000Loader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,8 @@ struct JPEG2000LoadingContext {
Vector<Comment> coms;
Vector<TileData> tiles;

RefPtr<Bitmap> 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
Expand Down Expand Up @@ -2013,6 +2015,62 @@ static ErrorOr<void> postprocess_samples(JPEG2000LoadingContext& context)
return {};
}

static ErrorOr<void> 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<u8>(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<u8>(clamp(tile.components[1].samples[y * w + x], 0.0f, 255.0f));
b = round_to<u8>(clamp(tile.components[2].samples[y * w + x], 0.0f, 255.0f));
} else if (tile.components.size() == 4) {
g = round_to<u8>(clamp(tile.components[1].samples[y * w + x], 0.0f, 255.0f));
b = round_to<u8>(clamp(tile.components[2].samples[y * w + x], 0.0f, 255.0f));
a = round_to<u8>(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<void> decode_image(JPEG2000LoadingContext& context)
{
TRY(parse_codestream_tile_headers(context));
Expand All @@ -2021,7 +2079,7 @@ static ErrorOr<void> 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 {};
}
Expand Down Expand Up @@ -2063,7 +2121,7 @@ ErrorOr<ImageFrameDescriptor> 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<Optional<ReadonlyBytes>> JPEG2000ImageDecoderPlugin::icc_data()
Expand Down

0 comments on commit a999bf4

Please sign in to comment.