From ab7da32d253f0bdcd7bf001b716de77bbd2b67f4 Mon Sep 17 00:00:00 2001 From: Nico Weber Date: Fri, 29 Mar 2024 19:52:20 -0400 Subject: [PATCH] LibGfx/JPEG2000: Support jpx extended 'colr' boxes The T.800 spec says there should only be one 'colr' box, but the extended jpx file format spec in T.801 annex M allows having multiple. Method 2 is a basic ICC profile, while method 3 (jpx-only) allows full ICC profiles. Support that. For the test, I opened buggie.png in Photoshop, converted it to grayscale, and saved it as a JPEG2000, with "JP2 Compatible" checked and "Include Transparency" unchecked. I also unchecked "Include Metadata", and "Lossless". I left "Fast Mode" checked and the quality at the default 50. --- Tests/LibGfx/TestImageDecoder.cpp | 14 ++++++++++++++ .../test-inputs/jpeg2000/buggie-gray.jpf | Bin 0 -> 4355 bytes .../ImageFormats/ISOBMFF/JPEG2000Boxes.cpp | 15 ++++++++++++++- .../LibGfx/ImageFormats/JPEG2000Loader.cpp | 17 +++++++++++++---- 4 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 Tests/LibGfx/test-inputs/jpeg2000/buggie-gray.jpf diff --git a/Tests/LibGfx/TestImageDecoder.cpp b/Tests/LibGfx/TestImageDecoder.cpp index 7321ea11b32..75592ee97d2 100644 --- a/Tests/LibGfx/TestImageDecoder.cpp +++ b/Tests/LibGfx/TestImageDecoder.cpp @@ -584,6 +584,20 @@ TEST_CASE(test_jpeg2000_simple) EXPECT_EQ(icc_bytes->size(), 3144u); } +TEST_CASE(test_jpeg2000_gray) +{ + auto file = TRY_OR_FAIL(Core::MappedFile::map(TEST_INPUT("jpeg2000/buggie-gray.jpf"sv))); + EXPECT(Gfx::JPEG2000ImageDecoderPlugin::sniff(file->bytes())); + auto plugin_decoder = TRY_OR_FAIL(Gfx::JPEG2000ImageDecoderPlugin::create(file->bytes())); + + EXPECT_EQ(plugin_decoder->size(), Gfx::IntSize(64, 138)); + + // The file contains both a simple and a real profile. Make sure we get the bigger one. + auto icc_bytes = MUST(plugin_decoder->icc_data()); + EXPECT(icc_bytes.has_value()); + EXPECT_EQ(icc_bytes->size(), 912u); +} + TEST_CASE(test_pam_rgb) { auto file = TRY_OR_FAIL(Core::MappedFile::map(TEST_INPUT("pnm/2x1.pam"sv))); diff --git a/Tests/LibGfx/test-inputs/jpeg2000/buggie-gray.jpf b/Tests/LibGfx/test-inputs/jpeg2000/buggie-gray.jpf new file mode 100644 index 0000000000000000000000000000000000000000..9203eaf6a049c851e87b6a8ea30be7cd8ede8644 GIT binary patch literal 4355 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 literal 0 HcmV?d00001 diff --git a/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/JPEG2000Boxes.cpp b/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/JPEG2000Boxes.cpp index 2fa8760b7b5..f44731e5167 100644 --- a/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/JPEG2000Boxes.cpp +++ b/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/JPEG2000Boxes.cpp @@ -76,6 +76,19 @@ ErrorOr JPEG2000ColorSpecificationBox::read_from_stream(BoxStream& stream) TRY(stream.read_until_filled(local_icc_data)); icc_data = move(local_icc_data); } + + // T.801 JPX extended file format syntax, + // Table M.22 – Legal METH values + if (method == 3) { + ByteBuffer local_icc_data = TRY(ByteBuffer::create_uninitialized(stream.remaining())); + TRY(stream.read_until_filled(local_icc_data)); + icc_data = move(local_icc_data); + } + if (method == 4) + return Error::from_string_literal("Method 4 is not yet implemented"); + if (method == 5) + return Error::from_string_literal("Method 5 is not yet implemented"); + return {}; } @@ -87,7 +100,7 @@ void JPEG2000ColorSpecificationBox::dump(String const& prepend) const outln("{}- approximation = {}", prepend, approximation); if (method == 1) outln("{}- enumerated_color_space = {}", prepend, enumerated_color_space); - if (method == 2) + if (method == 2 || method == 3) outln("{}- icc_data = {} bytes", prepend, icc_data.size()); } diff --git a/Userland/Libraries/LibGfx/ImageFormats/JPEG2000Loader.cpp b/Userland/Libraries/LibGfx/ImageFormats/JPEG2000Loader.cpp index fc1236f5f87..9860094663b 100644 --- a/Userland/Libraries/LibGfx/ImageFormats/JPEG2000Loader.cpp +++ b/Userland/Libraries/LibGfx/ImageFormats/JPEG2000Loader.cpp @@ -108,9 +108,18 @@ static ErrorOr decode_jpeg2000_header(JPEG2000LoadingContext& context, Rea image_header_box_index = i; } if (subbox->box_type() == ISOBMFF::BoxType::JPEG2000ColorSpecificationBox) { - if (color_header_box_index.has_value()) - return Error::from_string_literal("JPEG2000ImageDecoderPlugin: Multiple Color Specification boxes"); - color_header_box_index = i; + // T.800 says there should be just one 'colr' box, but T.801 allows several and says to pick the one with highest precedence. + bool use_this_color_box; + if (!color_header_box_index.has_value()) { + use_this_color_box = true; + } else { + auto const& new_header_box = static_cast(*header_box.child_boxes()[i]); + auto const& current_color_box = static_cast(*header_box.child_boxes()[color_header_box_index.value()]); + use_this_color_box = new_header_box.precedence > current_color_box.precedence; + } + + if (use_this_color_box) + color_header_box_index = i; } } @@ -123,7 +132,7 @@ static ErrorOr decode_jpeg2000_header(JPEG2000LoadingContext& context, Rea context.size = { image_header_box.width, image_header_box.height }; auto const& color_header_box = static_cast(*header_box.child_boxes()[color_header_box_index.value()]); - if (color_header_box.method == 2) + if (color_header_box.method == 2 || color_header_box.method == 3) context.icc_data = color_header_box.icc_data.bytes(); return {};