diff --git a/AK/Debug.h.in b/AK/Debug.h.in index edd3e8d297bd6d..e571002d47efbb 100644 --- a/AK/Debug.h.in +++ b/AK/Debug.h.in @@ -246,6 +246,10 @@ # cmakedefine01 JPEG_DEBUG #endif +#ifndef JPEG2000_DEBUG +# cmakedefine01 JPEG2000_DEBUG +#endif + #ifndef JS_BYTECODE_DEBUG # cmakedefine01 JS_BYTECODE_DEBUG #endif diff --git a/AK/MaybeOwned.h b/AK/MaybeOwned.h index 38e3c20d119759..dd87626fb4d7fb 100644 --- a/AK/MaybeOwned.h +++ b/AK/MaybeOwned.h @@ -32,6 +32,12 @@ class MaybeOwned { MaybeOwned(MaybeOwned&&) = default; MaybeOwned& operator=(MaybeOwned&&) = default; + template U> + MaybeOwned(MaybeOwned&& other) + : m_handle(downcast(move(other.m_handle))) + { + } + T* ptr() { if (m_handle.template has()) @@ -57,7 +63,22 @@ class MaybeOwned { bool is_owned() const { return m_handle.template has>(); } private: - Variant, T*> m_handle; + template + friend class MaybeOwned; + + template + using Handle = Variant, HT*>; + + template + Handle downcast(Handle&& variant) + { + if (variant.template has()) + return variant.template get(); + else + return static_cast&&>(move(variant.template get>())); + } + + Handle m_handle; }; } diff --git a/Base/res/apps/ImageViewer.af b/Base/res/apps/ImageViewer.af index e6ec0b1adff970..8771a4176355d2 100644 --- a/Base/res/apps/ImageViewer.af +++ b/Base/res/apps/ImageViewer.af @@ -4,4 +4,4 @@ Executable=/bin/ImageViewer Category=Gra&phics [Launcher] -FileTypes=bmp,dds,gif,ico,iff,jb2,jbig2,jpeg,jpg,jxl,lbm,pbm,pgm,png,ppm,qoi,tga,tiff,tif,tvg +FileTypes=bmp,dds,gif,ico,iff,jb2,jbig2,jp2,jpeg,jpg,jpx,jxl,lbm,pbm,pgm,png,ppm,qoi,tga,tiff,tif,tvg diff --git a/Meta/CMake/all_the_debug_macros.cmake b/Meta/CMake/all_the_debug_macros.cmake index b4465334046994..d8e249ee453a93 100644 --- a/Meta/CMake/all_the_debug_macros.cmake +++ b/Meta/CMake/all_the_debug_macros.cmake @@ -92,6 +92,7 @@ set(ITEM_RECTS_DEBUG ON) set(JBIG2_DEBUG ON) set(JOB_DEBUG ON) set(JPEG_DEBUG ON) +set(JPEG2000_DEBUG ON) set(JS_BYTECODE_DEBUG ON) set(JS_MODULE_DEBUG ON) set(KEYBOARD_DEBUG ON) diff --git a/Meta/Lagom/Fuzzers/FuzzJPEG2000Loader.cpp b/Meta/Lagom/Fuzzers/FuzzJPEG2000Loader.cpp new file mode 100644 index 00000000000000..93ebf66228aaee --- /dev/null +++ b/Meta/Lagom/Fuzzers/FuzzJPEG2000Loader.cpp @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2024, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +extern "C" int LLVMFuzzerTestOneInput(uint8_t const* data, size_t size) +{ + AK::set_debug_enabled(false); + auto decoder_or_error = Gfx::JPEG2000ImageDecoderPlugin::create({ data, size }); + if (decoder_or_error.is_error()) + return 0; + auto decoder = decoder_or_error.release_value(); + (void)decoder->frame(0); + return 0; +} diff --git a/Meta/Lagom/Fuzzers/fuzzers.cmake b/Meta/Lagom/Fuzzers/fuzzers.cmake index bb05b4086632d0..09347d416380d7 100644 --- a/Meta/Lagom/Fuzzers/fuzzers.cmake +++ b/Meta/Lagom/Fuzzers/fuzzers.cmake @@ -20,6 +20,7 @@ set(FUZZER_TARGETS ILBMLoader IMAPParser JBIG2Loader + JPEG2000Loader JPEGLoader Js JsonParser @@ -94,6 +95,7 @@ set(FUZZER_DEPENDENCIES_ICOLoader LibGfx) set(FUZZER_DEPENDENCIES_ILBMLoader LibGfx) set(FUZZER_DEPENDENCIES_IMAPParser LibIMAP) set(FUZZER_DEPENDENCIES_JBIG2Loader LibGfx) +set(FUZZER_DEPENDENCIES_JPEG2000Loader LibGfx) set(FUZZER_DEPENDENCIES_JPEGLoader LibGfx) set(FUZZER_DEPENDENCIES_Js LibJS) set(FUZZER_DEPENDENCIES_LzmaDecompression LibArchive LibCompress) diff --git a/Meta/gn/secondary/AK/BUILD.gn b/Meta/gn/secondary/AK/BUILD.gn index dca87520c75747..1c24d6168ff89a 100644 --- a/Meta/gn/secondary/AK/BUILD.gn +++ b/Meta/gn/secondary/AK/BUILD.gn @@ -289,6 +289,7 @@ write_cmake_config("ak_debug_gen") { "JOB_DEBUG=", "JBIG2_DEBUG=", "JPEG_DEBUG=", + "JPEG2000_DEBUG=", "JS_BYTECODE_DEBUG=", "JS_MODULE_DEBUG=", "KEYBOARD_SHORTCUTS_DEBUG=", diff --git a/Meta/gn/secondary/Userland/Libraries/LibGfx/BUILD.gn b/Meta/gn/secondary/Userland/Libraries/LibGfx/BUILD.gn index f027701b560b2f..f94019cdce42d9 100644 --- a/Meta/gn/secondary/Userland/Libraries/LibGfx/BUILD.gn +++ b/Meta/gn/secondary/Userland/Libraries/LibGfx/BUILD.gn @@ -72,9 +72,11 @@ shared_library("LibGfx") { "ImageFormats/ICOLoader.cpp", "ImageFormats/ILBMLoader.cpp", "ImageFormats/ISOBMFF/Boxes.cpp", + "ImageFormats/ISOBMFF/JPEG2000Boxes.cpp", "ImageFormats/ISOBMFF/Reader.cpp", "ImageFormats/ImageDecoder.cpp", "ImageFormats/JBIG2Loader.cpp", + "ImageFormats/JPEG2000Loader.cpp", "ImageFormats/JPEGLoader.cpp", "ImageFormats/JPEGWriter.cpp", "ImageFormats/JPEGXLLoader.cpp", diff --git a/Tests/LibGfx/TestImageDecoder.cpp b/Tests/LibGfx/TestImageDecoder.cpp index c9eae20a317ea4..22b38e2e67ba25 100644 --- a/Tests/LibGfx/TestImageDecoder.cpp +++ b/Tests/LibGfx/TestImageDecoder.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -568,6 +569,15 @@ TEST_CASE(test_jpeg_malformed_frame) } } +TEST_CASE(test_jpeg2000_simple) +{ + auto file = TRY_OR_FAIL(Core::MappedFile::map(TEST_INPUT("jpeg2000/simple.jp2"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(119, 101)); +} + 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/simple.jp2 b/Tests/LibGfx/test-inputs/jpeg2000/simple.jp2 new file mode 100644 index 00000000000000..d4e4cfc9c53293 Binary files /dev/null and b/Tests/LibGfx/test-inputs/jpeg2000/simple.jp2 differ diff --git a/Userland/Libraries/LibCore/MimeData.cpp b/Userland/Libraries/LibCore/MimeData.cpp index 8398c72a9b5710..a60efaccf2c6c7 100644 --- a/Userland/Libraries/LibCore/MimeData.cpp +++ b/Userland/Libraries/LibCore/MimeData.cpp @@ -113,6 +113,7 @@ static Array const s_registered_mime_type = { MimeType { .name = "image/bmp"sv, .common_extensions = { ".bmp"sv }, .description = "BMP image data"sv, .magic_bytes = Vector { 'B', 'M' } }, MimeType { .name = "image/gif"sv, .common_extensions = { ".gif"sv }, .description = "GIF image data"sv, .magic_bytes = Vector { 'G', 'I', 'F', '8', '7', 'a' } }, MimeType { .name = "image/gif"sv, .common_extensions = { ".gif"sv }, .description = "GIF image data"sv, .magic_bytes = Vector { 'G', 'I', 'F', '8', '9', 'a' } }, + MimeType { .name = "image/jp2"sv, .common_extensions = { ".jp2"sv, ".jpx"sv }, .description = "JPEG2000 image data"sv, .magic_bytes = Vector { 0x00, 0x00, 0x00, 0x0C, 0x6A, 0x50, 0x20, 0x20, 0x0D, 0x0A, 0x87, 0x0A } }, MimeType { .name = "image/jpeg"sv, .common_extensions = { ".jpg"sv, ".jpeg"sv }, .description = "JPEG image data"sv, .magic_bytes = Vector { 0xFF, 0xD8, 0xFF } }, MimeType { .name = "image/jxl"sv, .common_extensions = { ".jxl"sv }, .description = "JPEG XL image data"sv, .magic_bytes = Vector { 0xFF, 0x0A } }, MimeType { .name = "image/png"sv, .common_extensions = { ".png"sv }, .description = "PNG image data"sv, .magic_bytes = Vector { 0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A } }, diff --git a/Userland/Libraries/LibGUI/FileTypeFilter.h b/Userland/Libraries/LibGUI/FileTypeFilter.h index 4097c304c6df0c..870dc5cef932b3 100644 --- a/Userland/Libraries/LibGUI/FileTypeFilter.h +++ b/Userland/Libraries/LibGUI/FileTypeFilter.h @@ -30,7 +30,7 @@ struct FileTypeFilter { static FileTypeFilter image_files() { - return FileTypeFilter { "Image Files", Vector { "png", "gif", "bmp", "dip", "pam", "pbm", "pgm", "ppm", "ico", "iff", "jb2", "jbig2", "jpeg", "jpg", "jxl", "dds", "qoi", "tif", "tiff", "webp", "tvg" } }; + return FileTypeFilter { "Image Files", Vector { "png", "gif", "bmp", "dip", "pam", "pbm", "pgm", "ppm", "ico", "iff", "jb2", "jbig2", "jp2", "jpeg", "jpg", "jpx", "jxl", "dds", "qoi", "tif", "tiff", "webp", "tvg" } }; } static FileTypeFilter video_files() diff --git a/Userland/Libraries/LibGfx/Bitmap.h b/Userland/Libraries/LibGfx/Bitmap.h index 3650b68831b32a..bd050efc9d5253 100644 --- a/Userland/Libraries/LibGfx/Bitmap.h +++ b/Userland/Libraries/LibGfx/Bitmap.h @@ -16,28 +16,30 @@ #include #include -#define ENUMERATE_IMAGE_FORMATS \ - __ENUMERATE_IMAGE_FORMAT(bmp, ".bmp") \ - __ENUMERATE_IMAGE_FORMAT(dds, ".dds") \ - __ENUMERATE_IMAGE_FORMAT(gif, ".gif") \ - __ENUMERATE_IMAGE_FORMAT(ico, ".ico") \ - __ENUMERATE_IMAGE_FORMAT(iff, ".iff") \ - __ENUMERATE_IMAGE_FORMAT(jpeg, ".jb2") \ - __ENUMERATE_IMAGE_FORMAT(jpeg, ".jbig2") \ - __ENUMERATE_IMAGE_FORMAT(jpeg, ".jpeg") \ - __ENUMERATE_IMAGE_FORMAT(jpeg, ".jpg") \ - __ENUMERATE_IMAGE_FORMAT(jxl, ".jxl") \ - __ENUMERATE_IMAGE_FORMAT(iff, ".lbm") \ - __ENUMERATE_IMAGE_FORMAT(pam, ".pam") \ - __ENUMERATE_IMAGE_FORMAT(pbm, ".pbm") \ - __ENUMERATE_IMAGE_FORMAT(pgm, ".pgm") \ - __ENUMERATE_IMAGE_FORMAT(png, ".png") \ - __ENUMERATE_IMAGE_FORMAT(ppm, ".ppm") \ - __ENUMERATE_IMAGE_FORMAT(qoi, ".qoi") \ - __ENUMERATE_IMAGE_FORMAT(tga, ".tga") \ - __ENUMERATE_IMAGE_FORMAT(tiff, ".tif") \ - __ENUMERATE_IMAGE_FORMAT(tiff, ".tiff") \ - __ENUMERATE_IMAGE_FORMAT(tvg, ".tvg") \ +#define ENUMERATE_IMAGE_FORMATS \ + __ENUMERATE_IMAGE_FORMAT(bmp, ".bmp") \ + __ENUMERATE_IMAGE_FORMAT(dds, ".dds") \ + __ENUMERATE_IMAGE_FORMAT(gif, ".gif") \ + __ENUMERATE_IMAGE_FORMAT(ico, ".ico") \ + __ENUMERATE_IMAGE_FORMAT(iff, ".iff") \ + __ENUMERATE_IMAGE_FORMAT(jpeg, ".jb2") \ + __ENUMERATE_IMAGE_FORMAT(jpeg, ".jbig2") \ + __ENUMERATE_IMAGE_FORMAT(jpeg2000, ".jp2") \ + __ENUMERATE_IMAGE_FORMAT(jpeg, ".jpeg") \ + __ENUMERATE_IMAGE_FORMAT(jpeg, ".jpg") \ + __ENUMERATE_IMAGE_FORMAT(jpeg2000, ".jpx") \ + __ENUMERATE_IMAGE_FORMAT(jxl, ".jxl") \ + __ENUMERATE_IMAGE_FORMAT(iff, ".lbm") \ + __ENUMERATE_IMAGE_FORMAT(pam, ".pam") \ + __ENUMERATE_IMAGE_FORMAT(pbm, ".pbm") \ + __ENUMERATE_IMAGE_FORMAT(pgm, ".pgm") \ + __ENUMERATE_IMAGE_FORMAT(png, ".png") \ + __ENUMERATE_IMAGE_FORMAT(ppm, ".ppm") \ + __ENUMERATE_IMAGE_FORMAT(qoi, ".qoi") \ + __ENUMERATE_IMAGE_FORMAT(tga, ".tga") \ + __ENUMERATE_IMAGE_FORMAT(tiff, ".tif") \ + __ENUMERATE_IMAGE_FORMAT(tiff, ".tiff") \ + __ENUMERATE_IMAGE_FORMAT(tvg, ".tvg") \ __ENUMERATE_IMAGE_FORMAT(tvg, ".webp") namespace Gfx { diff --git a/Userland/Libraries/LibGfx/CMakeLists.txt b/Userland/Libraries/LibGfx/CMakeLists.txt index 1fb695f601a81c..54abc6c27464b0 100644 --- a/Userland/Libraries/LibGfx/CMakeLists.txt +++ b/Userland/Libraries/LibGfx/CMakeLists.txt @@ -46,8 +46,10 @@ set(SOURCES ImageFormats/ILBMLoader.cpp ImageFormats/ImageDecoder.cpp ImageFormats/ISOBMFF/Boxes.cpp + ImageFormats/ISOBMFF/JPEG2000Boxes.cpp ImageFormats/ISOBMFF/Reader.cpp ImageFormats/JBIG2Loader.cpp + ImageFormats/JPEG2000Loader.cpp ImageFormats/JPEGLoader.cpp ImageFormats/JPEGXLLoader.cpp ImageFormats/JPEGWriter.cpp diff --git a/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/BoxStream.h b/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/BoxStream.h index 9d17ba23648f4f..5a9783c80d8ef3 100644 --- a/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/BoxStream.h +++ b/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/BoxStream.h @@ -6,24 +6,25 @@ #pragma once +#include #include namespace Gfx::ISOBMFF { class BoxStream final : public Stream { public: - explicit BoxStream(Stream& stream, size_t size) - : m_stream(stream) + explicit BoxStream(MaybeOwned stream, size_t size) + : m_stream(move(stream)) , m_data_left(size) { } - virtual bool is_eof() const override { return m_stream.is_eof() || remaining() == 0; } - virtual bool is_open() const override { return m_stream.is_open(); } - virtual void close() override { m_stream.close(); } + virtual bool is_eof() const override { return m_stream->is_eof() || remaining() == 0; } + virtual bool is_open() const override { return m_stream->is_open(); } + virtual void close() override { m_stream->close(); } virtual ErrorOr read_some(Bytes bytes) override { - auto read_bytes = TRY(m_stream.read_some(bytes)); + auto read_bytes = TRY(m_stream->read_some(bytes)); m_data_left -= min(read_bytes.size(), m_data_left); return read_bytes; } @@ -41,7 +42,7 @@ class BoxStream final : public Stream { } private: - Stream& m_stream; + MaybeOwned m_stream; size_t m_data_left; }; diff --git a/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Boxes.cpp b/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Boxes.cpp index 56d3668af4aef4..3658dd9b085ac7 100644 --- a/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Boxes.cpp +++ b/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Boxes.cpp @@ -5,6 +5,8 @@ */ #include "Boxes.h" +#include "Reader.h" +#include namespace Gfx::ISOBMFF { @@ -88,7 +90,7 @@ ErrorOr FileTypeBox::read_from_stream(BoxStream& stream) void FileTypeBox::dump(String const& prepend) const { - FullBox::dump(prepend); + Box::dump(prepend); auto indented_prepend = add_indent(prepend); @@ -103,4 +105,19 @@ void FileTypeBox::dump(String const& prepend) const outln("{}{}", prepend, compatible_brands_string.string_view()); } +ErrorOr SuperBox::read_from_stream(BoxStream& stream, BoxCallback box_factory) +{ + auto reader = TRY(Gfx::ISOBMFF::Reader::create(MaybeOwned { stream })); + m_child_boxes = TRY(reader.read_entire_file(move(box_factory))); + return {}; +} + +void SuperBox::dump(String const& prepend) const +{ + Box::dump(prepend); + auto indented_prepend = add_indent(prepend); + for (auto const& child_box : m_child_boxes) + child_box->dump(indented_prepend); +} + } diff --git a/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Boxes.h b/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Boxes.h index a01fffcd14b559..ea3935764e979b 100644 --- a/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Boxes.h +++ b/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Boxes.h @@ -31,7 +31,6 @@ ErrorOr read_box_header(BoxStream& stream); struct Box { Box() = default; virtual ~Box() = default; - virtual ErrorOr read_from_stream(BoxStream&) { return {}; } virtual BoxType box_type() const { return BoxType::None; } virtual void dump(String const& prepend = {}) const; }; @@ -39,7 +38,7 @@ struct Box { using BoxList = Vector>; struct FullBox : public Box { - virtual ErrorOr read_from_stream(BoxStream& stream) override; + ErrorOr read_from_stream(BoxStream& stream); virtual void dump(String const& prepend = {}) const override; u8 version { 0 }; @@ -59,7 +58,7 @@ struct UnknownBox final : public Box { { } virtual ~UnknownBox() override = default; - virtual ErrorOr read_from_stream(BoxStream&) override; + ErrorOr read_from_stream(BoxStream&); virtual BoxType box_type() const override { return m_box_type; } virtual void dump(String const& prepend = {}) const override; @@ -77,7 +76,7 @@ struct UnknownBox final : public Box { } \ BoxName() = default; \ virtual ~BoxName() override = default; \ - virtual ErrorOr read_from_stream(BoxStream& stream) override; \ + ErrorOr read_from_stream(BoxStream& stream); \ virtual BoxType box_type() const override \ { \ return BoxType::BoxName; \ @@ -85,7 +84,7 @@ struct UnknownBox final : public Box { virtual void dump(String const& prepend = {}) const override; // 4.3 File Type Box -struct FileTypeBox final : public FullBox { +struct FileTypeBox final : public Box { BOX_SUBTYPE(FileTypeBox); BrandIdentifier major_brand { BrandIdentifier::None }; @@ -93,4 +92,18 @@ struct FileTypeBox final : public FullBox { Vector compatible_brands; }; +// A box that contains other boxes. +struct SuperBox : public Box { + SuperBox() = default; + + using BoxCallback = Function>>(BoxType, BoxStream&)>; + ErrorOr read_from_stream(BoxStream&, BoxCallback); + virtual void dump(String const& prepend = {}) const override; + + BoxList const& child_boxes() const { return m_child_boxes; } + +private: + BoxList m_child_boxes; +}; + } diff --git a/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Enums.h b/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Enums.h index 02073340314096..b6b92643c7b6eb 100644 --- a/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Enums.h +++ b/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Enums.h @@ -12,18 +12,38 @@ namespace Gfx::ISOBMFF { -// Define all Box types: -#define ENUMERATE_ALL() \ - ENUMERATE_ONE(FileTypeBox, ftyp) \ - ENUMERATE_ONE(MetaBox, meta) \ - ENUMERATE_ONE(MovieBox, moov) \ - ENUMERATE_ONE(MediaDataBox, mdat) \ - ENUMERATE_ONE(FreeBox, free) +// Define all Box types. +// In FourCC-asciibetical order. +// The JPEG2000 types are from T-REC-T.800-201511-S!!PDF-E.pdf, Table I.2 – Defined boxes +#define ENUMERATE_ALL() \ + ENUMERATE_ONE(JPEG2000BitsPerComponentBox, "bpcc") \ + ENUMERATE_ONE(JPEG2000ChannelDefinitionBox, "cdef") \ + ENUMERATE_ONE(JPEG2000ComponentMappingBox, "cmap") \ + ENUMERATE_ONE(JPEG2000ColorSpecificationBox, "colr") \ + ENUMERATE_ONE(FreeBox, "free") \ + ENUMERATE_ONE(FileTypeBox, "ftyp") \ + ENUMERATE_ONE(JPEG2000ImageHeaderBox, "ihdr") \ + ENUMERATE_ONE(JPEG2000SignatureBox, "jP ") \ + ENUMERATE_ONE(JPEG2000ContiguousCodestreamBox, "jp2c") \ + ENUMERATE_ONE(JPEG2000HeaderBox, "jp2h") \ + ENUMERATE_ONE(JPEG2000IntellectualPropertyBox, "jp2i") \ + ENUMERATE_ONE(MediaDataBox, "mdat") \ + ENUMERATE_ONE(MetaBox, "meta") \ + ENUMERATE_ONE(MovieBox, "moov") \ + ENUMERATE_ONE(JPEG2000PaletteBox, "pclr") \ + ENUMERATE_ONE(JPEG2000ResolutionBox, "res ") \ + ENUMERATE_ONE(JPEG2000CaptureResolutionBox, "resc") \ + ENUMERATE_ONE(JPEG2000DefaultDisplayResolutionBox, "resd") \ + ENUMERATE_ONE(JPEG2000UUIDInfoBox, "uinf") \ + ENUMERATE_ONE(JPEG2000UUIDListBox, "ulst") \ + ENUMERATE_ONE(JPEG2000URLBox, "url ") \ + ENUMERATE_ONE(UserExtensionBox, "uuid") \ + ENUMERATE_ONE(XMLBox, "xml ") enum class BoxType : u32 { None = 0, -#define ENUMERATE_ONE(box_name, box_4cc) box_name = RIFF::ChunkID(#box_4cc).as_number(), +#define ENUMERATE_ONE(box_name, box_4cc) box_name = RIFF::ChunkID(box_4cc).as_number(), ENUMERATE_ALL() diff --git a/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/JPEG2000Boxes.cpp b/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/JPEG2000Boxes.cpp new file mode 100644 index 00000000000000..9ff4269e726243 --- /dev/null +++ b/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/JPEG2000Boxes.cpp @@ -0,0 +1,271 @@ +/* + * Copyright (c) 2024, Nico Weber + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "JPEG2000Boxes.h" +#include + +namespace Gfx::ISOBMFF { + +ErrorOr JPEG2000HeaderBox::read_from_stream(BoxStream& stream) +{ + auto make_subbox = [](BoxType type, BoxStream& stream) -> ErrorOr>> { + switch (type) { + case BoxType::JPEG2000ChannelDefinitionBox: + return TRY(JPEG2000ChannelDefinitionBox::create_from_stream(stream)); + case BoxType::JPEG2000ColorSpecificationBox: + return TRY(JPEG2000ColorSpecificationBox::create_from_stream(stream)); + case BoxType::JPEG2000ImageHeaderBox: + return TRY(JPEG2000ImageHeaderBox::create_from_stream(stream)); + case BoxType::JPEG2000ResolutionBox: + return TRY(JPEG2000ResolutionBox::create_from_stream(stream)); + default: + return OptionalNone {}; + } + }; + + TRY(SuperBox::read_from_stream(stream, move(make_subbox))); + return {}; +} + +void JPEG2000HeaderBox::dump(String const& prepend) const +{ + SuperBox::dump(prepend); +} + +ErrorOr JPEG2000ImageHeaderBox::read_from_stream(BoxStream& stream) +{ + height = TRY(stream.read_value>()); + width = TRY(stream.read_value>()); + num_components = TRY(stream.read_value>()); + bits_per_component = TRY(stream.read_value()); + compression_type = TRY(stream.read_value()); + is_colorspace_unknown = TRY(stream.read_value()); + contains_intellectual_property_rights = TRY(stream.read_value()); + return {}; +} + +void JPEG2000ImageHeaderBox::dump(String const& prepend) const +{ + Box::dump(prepend); + outln("{}- height = {}", prepend, height); + outln("{}- width = {}", prepend, width); + outln("{}- num_components = {}", prepend, num_components); + if (bits_per_component == 0xFF) { + outln("{}- components vary in bit depth", prepend); + } else { + outln("{}- are_components_signed = {}", prepend, (bits_per_component & 0x80) != 0); + outln("{}- bits_per_component = {}", prepend, (bits_per_component & 0x7f) + 1); + } + outln("{}- compression_type = {}", prepend, compression_type); + outln("{}- is_colorspace_unknown = {}", prepend, is_colorspace_unknown); + outln("{}- contains_intellectual_property_rights = {}", prepend, contains_intellectual_property_rights); +} + +ErrorOr JPEG2000ColorSpecificationBox::read_from_stream(BoxStream& stream) +{ + method = TRY(stream.read_value()); + precedence = TRY(stream.read_value()); + approximation = TRY(stream.read_value()); + if (method == 1) + enumerated_color_space = TRY(stream.read_value>()); + if (method == 2) { + ByteBuffer local_icc_data = TRY(ByteBuffer::create_uninitialized(stream.remaining())); + TRY(stream.read_until_filled(local_icc_data)); + icc_data = move(local_icc_data); + } + return {}; +} + +void JPEG2000ColorSpecificationBox::dump(String const& prepend) const +{ + Box::dump(prepend); + outln("{}- method = {}", prepend, method); + outln("{}- precedence = {}", prepend, precedence); + outln("{}- approximation = {}", prepend, approximation); + if (method == 1) + outln("{}- enumerated_color_space = {}", prepend, enumerated_color_space); + if (method == 2) + outln("{}- icc_data = {} bytes", prepend, icc_data.size()); +} + +ErrorOr JPEG2000ChannelDefinitionBox::read_from_stream(BoxStream& stream) +{ + u16 count = TRY(stream.read_value>()); + for (u32 i = 0; i < count; ++i) { + Channel channel; + channel.channel_index = TRY(stream.read_value>()); + channel.channel_type = TRY(stream.read_value>()); + channel.channel_association = TRY(stream.read_value>()); + channels.append(channel); + } + return {}; +} + +void JPEG2000ChannelDefinitionBox::dump(String const& prepend) const +{ + Box::dump(prepend); + for (auto const& channel : channels) { + outln("{}- channel_index = {}", prepend, channel.channel_index); + + out("{}- channel_type = {}", prepend, channel.channel_type); + if (channel.channel_type == 0) + outln(" (color)"); + else if (channel.channel_type == 1) + outln(" (opacity)"); + else if (channel.channel_type == 2) + outln(" (premultiplied opacity)"); + else + outln(" (unknown)"); + + outln("{}- channel_association = {}", prepend, channel.channel_association); + } +} + +ErrorOr JPEG2000ResolutionBox::read_from_stream(BoxStream& stream) +{ + auto make_subbox = [](BoxType type, BoxStream& stream) -> ErrorOr>> { + switch (type) { + case BoxType::JPEG2000CaptureResolutionBox: + return TRY(JPEG2000CaptureResolutionBox::create_from_stream(stream)); + default: + return OptionalNone {}; + } + }; + + TRY(SuperBox::read_from_stream(stream, move(make_subbox))); + return {}; +} + +void JPEG2000ResolutionBox::dump(String const& prepend) const +{ + SuperBox::dump(prepend); +} + +ErrorOr JPEG2000CaptureResolutionBox::read_from_stream(BoxStream& stream) +{ + vertical_capture_grid_resolution_numerator = TRY(stream.read_value>()); + vertical_capture_grid_resolution_denominator = TRY(stream.read_value>()); + horizontal_capture_grid_resolution_numerator = TRY(stream.read_value>()); + horizontal_capture_grid_resolution_denominator = TRY(stream.read_value>()); + vertical_capture_grid_resolution_exponent = TRY(stream.read_value()); + horizontal_capture_grid_resolution_exponent = TRY(stream.read_value()); + return {}; +} + +void JPEG2000CaptureResolutionBox::dump(String const& prepend) const +{ + Box::dump(prepend); + outln("{}- vertical_capture_grid_resolution = {}/{} * 10^{}", prepend, vertical_capture_grid_resolution_numerator, vertical_capture_grid_resolution_denominator, vertical_capture_grid_resolution_exponent); + outln("{}- horizontal_capture_grid_resolution = {}/{} * 10^{}", prepend, horizontal_capture_grid_resolution_numerator, horizontal_capture_grid_resolution_denominator, horizontal_capture_grid_resolution_exponent); +} + +ErrorOr JPEG2000ContiguousCodestreamBox::read_from_stream(BoxStream& stream) +{ + // FIXME: It's wasteful to make a copy of all the image data here. Having just a ReadonlyBytes + // or streaming it into the jpeg2000 decoder would be nicer. + ByteBuffer local_codestream = TRY(ByteBuffer::create_uninitialized(stream.remaining())); + TRY(stream.read_until_filled(local_codestream)); + codestream = move(local_codestream); + return {}; +} + +void JPEG2000ContiguousCodestreamBox::dump(String const& prepend) const +{ + Box::dump(prepend); + outln("{}- codestream = {} bytes", prepend, codestream.size()); +} + +ErrorOr JPEG2000SignatureBox::read_from_stream(BoxStream& stream) +{ + signature = TRY(stream.read_value>()); + return {}; +} + +void JPEG2000SignatureBox::dump(String const& prepend) const +{ + Box::dump(prepend); + outln("{}- signature = {:#08x}", prepend, signature); +} + +ErrorOr JPEG2000UUIDInfoBox::read_from_stream(BoxStream& stream) +{ + auto make_subbox = [](BoxType type, BoxStream& stream) -> ErrorOr>> { + switch (type) { + case BoxType::JPEG2000UUIDListBox: + return TRY(JPEG2000UUIDListBox::create_from_stream(stream)); + case BoxType::JPEG2000URLBox: + return TRY(JPEG2000URLBox::create_from_stream(stream)); + default: + return OptionalNone {}; + } + }; + + TRY(SuperBox::read_from_stream(stream, move(make_subbox))); + return {}; +} + +void JPEG2000UUIDInfoBox::dump(String const& prepend) const +{ + SuperBox::dump(prepend); +} + +ErrorOr JPEG2000UUIDListBox::read_from_stream(BoxStream& stream) +{ + u16 count = TRY(stream.read_value>()); + for (u32 i = 0; i < count; ++i) { + Array uuid; + TRY(stream.read_until_filled(uuid)); + uuids.append(uuid); + } + return {}; +} + +void JPEG2000UUIDListBox::dump(String const& prepend) const +{ + Box::dump(prepend); + for (auto const& uuid : uuids) { + out("{}- ", prepend); + for (auto byte : uuid) { + out("{:02x}", byte); + } + outln(); + } +} + +ErrorOr JPEG2000URLBox::url_as_string() const +{ + // Zero-terminated UTF-8 per spec. + if (url_bytes.is_empty() || url_bytes.bytes().last() != '\0') + return Error::from_string_literal("URL not zero-terminated"); + return String::from_utf8(StringView { url_bytes.bytes().trim(url_bytes.size() - 1) }); +} + +ErrorOr JPEG2000URLBox::read_from_stream(BoxStream& stream) +{ + version_number = TRY(stream.read_value()); + flag = TRY(stream.read_value()) << 16; + flag |= TRY(stream.read_value>()); + + url_bytes = TRY(ByteBuffer::create_uninitialized(stream.remaining())); + TRY(stream.read_until_filled(url_bytes)); + + return {}; +} + +void JPEG2000URLBox::dump(String const& prepend) const +{ + Box::dump(prepend); + outln("{}- version_number = {}", prepend, version_number); + outln("{}- flag = {:#06x}", prepend, flag); + + auto url_or_err = url_as_string(); + if (url_or_err.is_error()) + outln("{}- url = ", prepend, url_or_err.release_error(), url_bytes.size()); + else + outln("{}- url = {}", prepend, url_or_err.release_value()); +} + +} diff --git a/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/JPEG2000Boxes.h b/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/JPEG2000Boxes.h new file mode 100644 index 00000000000000..da2a09b2575f2f --- /dev/null +++ b/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/JPEG2000Boxes.h @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2024, Nico Weber + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include "Boxes.h" + +namespace Gfx::ISOBMFF { + +struct JPEG2000HeaderBox final : public SuperBox { + BOX_SUBTYPE(JPEG2000HeaderBox); +}; + +// I.5.3.1 Image Header box +struct JPEG2000ImageHeaderBox final : public Box { + BOX_SUBTYPE(JPEG2000ImageHeaderBox); + + u32 height { 0 }; + u32 width { 0 }; + u16 num_components { 0 }; + u8 bits_per_component { 0 }; + u8 compression_type { 0 }; + u8 is_colorspace_unknown { 0 }; + u8 contains_intellectual_property_rights { 0 }; +}; + +// I.5.3.3 Colour Specification box +struct JPEG2000ColorSpecificationBox final : public Box { + BOX_SUBTYPE(JPEG2000ColorSpecificationBox); + + u8 method { 0 }; + i8 precedence { 0 }; + u8 approximation { 0 }; + u32 enumerated_color_space { 0 }; // Only set if method == 1 + ByteBuffer icc_data; // Only set if method == 2 +}; + +// I.5.3.6 Channel Definition box +struct JPEG2000ChannelDefinitionBox final : public Box { + BOX_SUBTYPE(JPEG2000ChannelDefinitionBox); + + struct Channel { + u16 channel_index; + u16 channel_type; + u16 channel_association; + }; + Vector channels; +}; + +// I.5.3.7 Resolution box +struct JPEG2000ResolutionBox final : public SuperBox { + BOX_SUBTYPE(JPEG2000ResolutionBox); +}; + +// I.5.3.7.1 Capture Resolution box +struct JPEG2000CaptureResolutionBox final : public Box { + BOX_SUBTYPE(JPEG2000CaptureResolutionBox); + + u16 vertical_capture_grid_resolution_numerator { 0 }; + u16 vertical_capture_grid_resolution_denominator { 0 }; + u16 horizontal_capture_grid_resolution_numerator { 0 }; + u16 horizontal_capture_grid_resolution_denominator { 0 }; + i8 vertical_capture_grid_resolution_exponent { 0 }; + i8 horizontal_capture_grid_resolution_exponent { 0 }; +}; + +// I.5.4 Contiguous Codestream box +struct JPEG2000ContiguousCodestreamBox final : public Box { + BOX_SUBTYPE(JPEG2000ContiguousCodestreamBox); + + ByteBuffer codestream; +}; + +struct JPEG2000SignatureBox final : public Box { + BOX_SUBTYPE(JPEG2000SignatureBox); + + u32 signature { 0 }; +}; + +// I.7.3 UUID Info boxes (superbox) +struct JPEG2000UUIDInfoBox final : public SuperBox { + BOX_SUBTYPE(JPEG2000UUIDInfoBox); +}; + +// I.7.3.1 UUID List box +struct JPEG2000UUIDListBox final : public Box { + BOX_SUBTYPE(JPEG2000UUIDListBox); + + Vector> uuids; +}; + +// I.7.3.2 Data Entry URL box +struct JPEG2000URLBox final : public Box { + BOX_SUBTYPE(JPEG2000URLBox); + + ErrorOr url_as_string() const; + + u8 version_number; + u32 flag; + ByteBuffer url_bytes; +}; + +} diff --git a/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Reader.cpp b/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Reader.cpp index 11188688b6612a..e5466d45d6ed41 100644 --- a/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Reader.cpp +++ b/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Reader.cpp @@ -5,30 +5,56 @@ */ #include "Reader.h" +#include "JPEG2000Boxes.h" +#include namespace Gfx::ISOBMFF { ErrorOr Reader::create(MaybeOwned stream) { size_t size = TRY(stream->size()); - return Reader(move(stream), size); + return Reader(make(move(stream), size)); +} + +ErrorOr Reader::create(MaybeOwned stream) +{ + return Reader(move(stream)); } ErrorOr Reader::read_entire_file() +{ + auto make_top_level_box = [](BoxType type, BoxStream& stream) -> ErrorOr>> { + switch (type) { + case BoxType::FileTypeBox: + return TRY(FileTypeBox::create_from_stream(stream)); + case BoxType::JPEG2000ContiguousCodestreamBox: + return TRY(JPEG2000ContiguousCodestreamBox::create_from_stream(stream)); + case BoxType::JPEG2000HeaderBox: + return TRY(JPEG2000HeaderBox::create_from_stream(stream)); + case BoxType::JPEG2000SignatureBox: + return TRY(JPEG2000SignatureBox::create_from_stream(stream)); + case BoxType::JPEG2000UUIDInfoBox: + return TRY(JPEG2000UUIDInfoBox::create_from_stream(stream)); + default: + return OptionalNone {}; + } + }; + return read_entire_file((ErrorOr>>(*)(BoxType, BoxStream&))(make_top_level_box)); +} + +ErrorOr Reader::read_entire_file(BoxCallback box_factory) { BoxList top_level_boxes; - while (!m_stream->is_eof()) { - auto box_header = TRY(read_box_header(m_box_stream)); - BoxStream box_stream { m_box_stream, static_cast(box_header.contents_size) }; + while (!m_box_stream->is_eof()) { + auto box_header = TRY(read_box_header(*m_box_stream)); + BoxStream box_stream { MaybeOwned { *m_box_stream }, static_cast(box_header.contents_size) }; - switch (box_header.type) { - case BoxType::FileTypeBox: - TRY(top_level_boxes.try_append(TRY(FileTypeBox::create_from_stream(box_stream)))); - break; - default: + auto maybe_box = TRY(box_factory(box_header.type, box_stream)); + if (maybe_box.has_value()) { + TRY(top_level_boxes.try_append(move(maybe_box.value()))); + } else { TRY(top_level_boxes.try_append(TRY(UnknownBox::create_from_stream(box_header.type, box_stream)))); - break; } if (!box_stream.is_eof()) diff --git a/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Reader.h b/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Reader.h index 9333b3b9605eff..5b03a8758ee7eb 100644 --- a/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Reader.h +++ b/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Reader.h @@ -16,23 +16,20 @@ namespace Gfx::ISOBMFF { class Reader { public: static ErrorOr create(MaybeOwned stream); + static ErrorOr create(MaybeOwned stream); ErrorOr read_entire_file(); - ErrorOr get_major_brand(); - ErrorOr> get_minor_brands(); + using BoxCallback = Function>>(BoxType, BoxStream&)>; + ErrorOr read_entire_file(BoxCallback); private: - Reader(MaybeOwned stream, size_t size) - : m_stream(move(stream)) - , m_box_stream(*m_stream, size) + Reader(MaybeOwned stream) + : m_box_stream(move(stream)) { } - ErrorOr parse_initial_data(); - - MaybeOwned m_stream; - BoxStream m_box_stream; + MaybeOwned m_box_stream; }; } diff --git a/Userland/Libraries/LibGfx/ImageFormats/ImageDecoder.cpp b/Userland/Libraries/LibGfx/ImageFormats/ImageDecoder.cpp index de0977ce37b2a2..8a24bdafa38523 100644 --- a/Userland/Libraries/LibGfx/ImageFormats/ImageDecoder.cpp +++ b/Userland/Libraries/LibGfx/ImageFormats/ImageDecoder.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -41,6 +42,7 @@ static ErrorOr> probe_and_sniff_for_appropriate_plugi { ICOImageDecoderPlugin::sniff, ICOImageDecoderPlugin::create }, { ILBMImageDecoderPlugin::sniff, ILBMImageDecoderPlugin::create }, { JBIG2ImageDecoderPlugin::sniff, JBIG2ImageDecoderPlugin::create }, + { JPEG2000ImageDecoderPlugin::sniff, JPEG2000ImageDecoderPlugin::create }, { JPEGImageDecoderPlugin::sniff, JPEGImageDecoderPlugin::create }, { JPEGXLImageDecoderPlugin::sniff, JPEGXLImageDecoderPlugin::create }, { PAMImageDecoderPlugin::sniff, PAMImageDecoderPlugin::create }, diff --git a/Userland/Libraries/LibGfx/ImageFormats/JPEG2000Loader.cpp b/Userland/Libraries/LibGfx/ImageFormats/JPEG2000Loader.cpp new file mode 100644 index 00000000000000..fc1236f5f871a2 --- /dev/null +++ b/Userland/Libraries/LibGfx/ImageFormats/JPEG2000Loader.cpp @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2024, Nico Weber + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include + +// Core coding system spec (.jp2 format): T-REC-T.800-201511-S!!PDF-E.pdf available here: +// https://www.itu.int/rec/dologin_pub.asp?lang=e&id=T-REC-T.800-201511-S!!PDF-E&type=items + +// Extensions (.jpx format): T-REC-T.801-202106-S!!PDF-E.pdf available here: +// https://handle.itu.int/11.1002/1000/14666-en?locatt=format:pdf&auth + +// rfc3745 lists the MIME type. It only mentions the jp2_id_string as magic number. + +namespace Gfx { + +// A JPEG2000 image can be stored in a codestream with markers, similar to a JPEG image, +// or in a JP2 file, which is a container format based on boxes similar to ISOBMFF. + +// This is the marker for the codestream version. We don't support this yet. +// If we add support, add a second `"image/jp2"` line to MimeData.cpp for this magic number. +// T.800 Annex A, Codestream syntax, A.2 Information in the marker segments and A.3 Construction of the codestream +[[maybe_unused]] static constexpr u8 marker_id_string[] = { 0xFF, 0x4F, 0xFF, 0x51 }; + +// This is the marker for the box version. +// T.800 Annex I, JP2 file format syntax, I.5.1 JPEG 2000 Signature box +static constexpr u8 jp2_id_string[] = { 0x00, 0x00, 0x00, 0x0C, 0x6A, 0x50, 0x20, 0x20, 0x0D, 0x0A, 0x87, 0x0A }; + +struct JPEG2000LoadingContext { + enum class State { + NotDecoded = 0, + Error, + }; + State state { State::NotDecoded }; + ReadonlyBytes codestream_data; + Optional icc_data; + + IntSize size; + + ISOBMFF::BoxList boxes; +}; + +static ErrorOr decode_jpeg2000_header(JPEG2000LoadingContext& context, ReadonlyBytes data) +{ + if (!JPEG2000ImageDecoderPlugin::sniff(data)) + return Error::from_string_literal("JBIG2LoadingContext: Invalid JBIG2 header"); + + auto reader = TRY(Gfx::ISOBMFF::Reader::create(TRY(try_make(data)))); + context.boxes = TRY(reader.read_entire_file()); + + // I.2.2 File organization + // "A particular order of those boxes in the file is not generally implied. However, the JPEG 2000 Signature box + // shall be the first box in a JP2 file, the File Type box shall immediately follow the JPEG 2000 Signature box + // and the JP2 Header box shall fall before the Contiguous Codestream box." + if (context.boxes.size() < 4) + return Error::from_string_literal("JPEG2000ImageDecoderPlugin: Expected at least four boxes"); + + // Required toplevel boxes: signature box, file type box, jp2 header box, contiguous codestream box. + + if (context.boxes[0]->box_type() != ISOBMFF::BoxType::JPEG2000SignatureBox) + return Error::from_string_literal("JPEG2000ImageDecoderPlugin: Expected JPEG2000SignatureBox as first box"); + if (context.boxes[1]->box_type() != ISOBMFF::BoxType::FileTypeBox) + return Error::from_string_literal("JPEG2000ImageDecoderPlugin: Expected FileTypeBox as second box"); + + Optional jp2_header_box_index; + Optional contiguous_codestream_box_index; + for (size_t i = 2; i < context.boxes.size(); ++i) { + if (context.boxes[i]->box_type() == ISOBMFF::BoxType::JPEG2000HeaderBox) { + // "Within a JP2 file, there shall be one and only one JP2 Header box." + if (jp2_header_box_index.has_value()) + return Error::from_string_literal("JPEG2000ImageDecoderPlugin: Multiple JP2 Header boxes"); + jp2_header_box_index = i; + } + if (context.boxes[i]->box_type() == ISOBMFF::BoxType::JPEG2000ContiguousCodestreamBox && !contiguous_codestream_box_index.has_value()) { + // "a conforming reader shall ignore all codestreams after the first codestream found in the file. + // Contiguous Codestream boxes may be found anywhere in the file except before the JP2 Header box." + contiguous_codestream_box_index = i; + if (!jp2_header_box_index.has_value() || contiguous_codestream_box_index.value() < jp2_header_box_index.value()) + return Error::from_string_literal("JPEG2000ImageDecoderPlugin: JP2 Header box must come before Contiguous Codestream box"); + } + } + + if (!jp2_header_box_index.has_value()) + return Error::from_string_literal("JPEG2000ImageDecoderPlugin: Expected JP2 Header box"); + if (!contiguous_codestream_box_index.has_value()) + return Error::from_string_literal("JPEG2000ImageDecoderPlugin: Expected Contiguous Codestream box"); + + // FIXME: JPEG2000ContiguousCodestreamBox makes a copy of the codestream data. That's too heavy for header scanning. + // Add a mode to ISOBMFF::Reader where it only stores offsets for the codestream data and the ICC profile. + auto const& codestream_box = static_cast(*context.boxes[contiguous_codestream_box_index.value()]); + context.codestream_data = codestream_box.codestream.bytes(); + + // Required child boxes of the jp2 header box: image header box, color box. + + Optional image_header_box_index; + Optional color_header_box_index; + auto const& header_box = static_cast(*context.boxes[jp2_header_box_index.value()]); + for (size_t i = 0; i < header_box.child_boxes().size(); ++i) { + auto const& subbox = header_box.child_boxes()[i]; + if (subbox->box_type() == ISOBMFF::BoxType::JPEG2000ImageHeaderBox) { + if (image_header_box_index.has_value()) + return Error::from_string_literal("JPEG2000ImageDecoderPlugin: Multiple Image Header boxes"); + 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; + } + } + + if (!image_header_box_index.has_value()) + return Error::from_string_literal("JPEG2000ImageDecoderPlugin: Expected Image Header box"); + if (!color_header_box_index.has_value()) + return Error::from_string_literal("JPEG2000ImageDecoderPlugin: Expected Color Specification box"); + + auto const& image_header_box = static_cast(*header_box.child_boxes()[image_header_box_index.value()]); + 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) + context.icc_data = color_header_box.icc_data.bytes(); + + return {}; +} + +bool JPEG2000ImageDecoderPlugin::sniff(ReadonlyBytes data) +{ + return data.starts_with(jp2_id_string); +} + +JPEG2000ImageDecoderPlugin::JPEG2000ImageDecoderPlugin() +{ + m_context = make(); +} + +IntSize JPEG2000ImageDecoderPlugin::size() +{ + return m_context->size; +} + +ErrorOr> JPEG2000ImageDecoderPlugin::create(ReadonlyBytes data) +{ + auto plugin = TRY(adopt_nonnull_own_or_enomem(new (nothrow) JPEG2000ImageDecoderPlugin())); + TRY(decode_jpeg2000_header(*plugin->m_context, data)); + return plugin; +} + +ErrorOr JPEG2000ImageDecoderPlugin::frame(size_t index, Optional) +{ + if (index != 0) + return Error::from_string_literal("JPEG2000ImageDecoderPlugin: Invalid frame index"); + + if (m_context->state == JPEG2000LoadingContext::State::Error) + return Error::from_string_literal("JPEG2000ImageDecoderPlugin: Decoding failed"); + + return Error::from_string_literal("JPEG2000ImageDecoderPlugin: Draw the rest of the owl"); +} + +ErrorOr> JPEG2000ImageDecoderPlugin::icc_data() +{ + return m_context->icc_data; +} + +} diff --git a/Userland/Libraries/LibGfx/ImageFormats/JPEG2000Loader.h b/Userland/Libraries/LibGfx/ImageFormats/JPEG2000Loader.h new file mode 100644 index 00000000000000..1a2ec8d2bb7a40 --- /dev/null +++ b/Userland/Libraries/LibGfx/ImageFormats/JPEG2000Loader.h @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024, Nico Weber + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace Gfx { + +struct JPEG2000LoadingContext; + +class JPEG2000ImageDecoderPlugin : public ImageDecoderPlugin { +public: + static bool sniff(ReadonlyBytes); + static ErrorOr> create(ReadonlyBytes); + + virtual ~JPEG2000ImageDecoderPlugin() override = default; + + virtual IntSize size() override; + + virtual ErrorOr frame(size_t index, Optional ideal_size = {}) override; + + virtual ErrorOr> icc_data() override; + +private: + JPEG2000ImageDecoderPlugin(); + + OwnPtr m_context; +}; + +} diff --git a/Userland/Utilities/test-fuzz.cpp b/Userland/Utilities/test-fuzz.cpp index a247e02d9b716c..13a949b057fbd0 100644 --- a/Userland/Utilities/test-fuzz.cpp +++ b/Userland/Utilities/test-fuzz.cpp @@ -34,6 +34,7 @@ T(ILBMLoader) \ T(IMAPParser) \ T(JBIG2Loader) \ + T(JPEG2000Loader) \ T(JPEGLoader) \ T(Js) \ T(JsonParser) \