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.
This commit is contained in:
Nico Weber 2024-03-29 19:52:20 -04:00 committed by Andreas Kling
parent 3f740fc727
commit ab7da32d25
Notes: sideshowbarker 2024-07-17 20:33:50 +09:00
4 changed files with 41 additions and 5 deletions

View file

@ -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)));

Binary file not shown.

View file

@ -76,6 +76,19 @@ ErrorOr<void> 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());
}

View file

@ -108,9 +108,18 @@ static ErrorOr<void> 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<ISOBMFF::JPEG2000ColorSpecificationBox const&>(*header_box.child_boxes()[i]);
auto const& current_color_box = static_cast<ISOBMFF::JPEG2000ColorSpecificationBox const&>(*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<void> decode_jpeg2000_header(JPEG2000LoadingContext& context, Rea
context.size = { image_header_box.width, image_header_box.height };
auto const& color_header_box = static_cast<ISOBMFF::JPEG2000ColorSpecificationBox const&>(*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 {};