From 66c9696687ce654d64eb9368ac305444655dd859 Mon Sep 17 00:00:00 2001 From: Zaggy1024 Date: Sat, 18 Feb 2023 17:46:27 -0600 Subject: [PATCH] LibGfx: Add initial ISO BMFF parsing and a utility to print file info Currently, the `isobmff` utility will only print the media file type info from the FileTypeBox (major brand and compatible brands), as well as the names and sizes of top-level boxes. --- Meta/Lagom/CMakeLists.txt | 3 + Tests/LibGfx/CMakeLists.txt | 1 + Tests/LibGfx/TestParseISOBMFF.cpp | 37 ++++++ Tests/LibGfx/test-inputs/loop_forever.avif | Bin 0 -> 2967 bytes Userland/Libraries/LibGfx/CMakeLists.txt | 2 + .../LibGfx/ImageFormats/ISOBMFF/BoxStream.h | 48 +++++++ .../LibGfx/ImageFormats/ISOBMFF/Boxes.cpp | 102 +++++++++++++++ .../LibGfx/ImageFormats/ISOBMFF/Boxes.h | 96 ++++++++++++++ .../LibGfx/ImageFormats/ISOBMFF/Enums.h | 117 ++++++++++++++++++ .../LibGfx/ImageFormats/ISOBMFF/Reader.cpp | 39 ++++++ .../LibGfx/ImageFormats/ISOBMFF/Reader.h | 36 ++++++ Userland/Utilities/CMakeLists.txt | 1 + Userland/Utilities/isobmff.cpp | 28 +++++ 13 files changed, 510 insertions(+) create mode 100644 Tests/LibGfx/TestParseISOBMFF.cpp create mode 100644 Tests/LibGfx/test-inputs/loop_forever.avif create mode 100644 Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/BoxStream.h create mode 100644 Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Boxes.cpp create mode 100644 Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Boxes.h create mode 100644 Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Enums.h create mode 100644 Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Reader.cpp create mode 100644 Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Reader.h create mode 100644 Userland/Utilities/isobmff.cpp diff --git a/Meta/Lagom/CMakeLists.txt b/Meta/Lagom/CMakeLists.txt index 407cd6065b..71499572ef 100644 --- a/Meta/Lagom/CMakeLists.txt +++ b/Meta/Lagom/CMakeLists.txt @@ -534,6 +534,9 @@ if (BUILD_LAGOM) add_executable(image ../../Userland/Utilities/image.cpp) target_link_libraries(image LibCore LibGfx LibMain) + add_executable(isobmff ../../Userland/Utilities/isobmff.cpp) + target_link_libraries(isobmff LibCore LibGfx LibMain) + add_executable(ttfdisasm ../../Userland/Utilities/ttfdisasm.cpp) target_link_libraries(ttfdisasm LibGfx LibMain) diff --git a/Tests/LibGfx/CMakeLists.txt b/Tests/LibGfx/CMakeLists.txt index 027aed5d86..12c3bf68ed 100644 --- a/Tests/LibGfx/CMakeLists.txt +++ b/Tests/LibGfx/CMakeLists.txt @@ -6,6 +6,7 @@ set(TEST_SOURCES TestGfxBitmap.cpp TestICCProfile.cpp TestImageDecoder.cpp + TestParseISOBMFF.cpp TestRect.cpp TestScalingFunctions.cpp ) diff --git a/Tests/LibGfx/TestParseISOBMFF.cpp b/Tests/LibGfx/TestParseISOBMFF.cpp new file mode 100644 index 0000000000..b936794f6c --- /dev/null +++ b/Tests/LibGfx/TestParseISOBMFF.cpp @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2023, Gregory Bertilson + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include + +#include +#include +#include + +TEST_CASE(parse_animated_avif) +{ + auto file = MUST(Core::MappedFile::map("./test-inputs/loop_forever.avif"sv)); + auto reader = MUST(Gfx::ISOBMFF::Reader::create(MUST(try_make(file->bytes())))); + auto boxes = MUST(reader.read_entire_file()); + + for (auto& box : boxes) + box->dump(); + + VERIFY(boxes.size() == 4); + VERIFY(boxes[0]->box_type() == Gfx::ISOBMFF::BoxType::FileTypeBox); + auto& file_type_box = static_cast(*boxes[0]); + VERIFY(file_type_box.major_brand == Gfx::ISOBMFF::BrandIdentifier::avis); + VERIFY(file_type_box.minor_version == 0); + Vector expected_compatible_brands = { + Gfx::ISOBMFF::BrandIdentifier::avif, + Gfx::ISOBMFF::BrandIdentifier::avis, + Gfx::ISOBMFF::BrandIdentifier::msf1, + Gfx::ISOBMFF::BrandIdentifier::iso8, + Gfx::ISOBMFF::BrandIdentifier::mif1, + Gfx::ISOBMFF::BrandIdentifier::miaf, + Gfx::ISOBMFF::BrandIdentifier::MA1A, + }; + VERIFY(file_type_box.compatible_brands == expected_compatible_brands); +} diff --git a/Tests/LibGfx/test-inputs/loop_forever.avif b/Tests/LibGfx/test-inputs/loop_forever.avif new file mode 100644 index 0000000000000000000000000000000000000000..b24b4f29d3c3b87798e224d63ec628da88013fa9 GIT binary patch literal 2967 zcmZQzV9-e`sVqn=%PeMKU|>ir%S;2YbBogqGmG;rax>Emb2Agud>suP85kIQb5lza zLFyP77&J0ca*CjAhJwuG5*Qmq=VT^`QmFfcIiB$gREH!6Tc7=)AabBgkkb1E1Z8F(4kKw9`<${7)g z8Dv1JEfPyBoI#AzqCBhIg4A>{;bfJZnOtC1Tv=R_np^=hi?BkSHzz2Qr8Uaug_#K(tJ8NijIzLLCPZLzb%oX@Fu-;)rHoVEhHjSH&gZ z`~{MN=5z*;;*w%eL_)rki{v;@4RI~gAjueuL1Lc1_lPV21bUN$_YXY zT)YY})r`Ds6O4|t2rx7^un4knIPH49V2fA5&43xHOGP?(9_4(Gaa<_4`}&7lnqHYN zEyXwPNvw(byuM&Pzx~(Z==J;kIX>0z&tugzRG(n@BQ9v?&FlP=n$ypm+{kNJvr0K| zir9_+kM)m#x2leEI2*R{l5MKV|(+>r^hD+IUe}iEZl* zdDcH!GWJHT&huH1>r1D`xPQ9QW0+x3^!>Kb!70066%=Tv!_;Oy%NRu)Q z&sCX`xopA0Z0VQMAH;TttTXYQS}L>j*d(d!FWXN37TT(~JTAvfbYsONp|#KRIt4Qq z0@ZCG=@Nj6YI z&8pYtQBaM_g)q&d+%q@nsn_4_KJ!-E`gwi9!IqLE43>UH4X2ycbt*rw{r0bMSIq9) zm~MQt_snXWZ!?&W8ZVvra{l&=v*!-vpDCPq>*!6_Jnbc2&DSN(j{b9=c{8?uZIF_N z!$*fn|AJ@yTpzpty6$9Eu@aFv1^Oy$y5G2TTt4p2`dP-}NwRx@vg?G& zKb>_y>Um$W#;dv4@0g$J%Kyx9eP?d*S=Us{v)60QttVYvJN?763!-lt1+?9t-AE}3 z4R?ICGw`6<1I0thH8-sH{tV#T@ztQDXwtmk>k}q3XwN_Tm*aUY=dPgY1%(^ltc&lH z;pq@!F#5_2O>8|4Dw8-k^>#HdT;OWCJhO=5{K*}6UF_A=Gc(^AT4Y6oCudoocWd^hf2I=e&g2)e-n+2V zQ@p2Bz3=&l-RVrC5%t&iHGk2lRPh(;7x14u)h159+0L<3IrV98oQnRfdS?Au^#?`& z7EJG)6;X1o?XcO5`K5E?&rUkyYq`B%2Ip> ze)a$6=ZB5X-l|l&8FQ_ZUGArF(@EQV4V`Mt z8z+AKIDJ|BTh#?Sw@4T+75mB9_*ml6Rg+1*;qM(+?YVL9AlH;#C$ly<-7xt3@z-fa zxf{#w@QJFL?M$1U(^&T5N{Zj}<%T7Pw + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace Gfx::ISOBMFF { + +class BoxStream final : public Stream { +public: + explicit BoxStream(Stream& stream, size_t size) + : m_stream(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 ErrorOr read_some(Bytes bytes) override + { + auto read_bytes = TRY(m_stream.read_some(bytes)); + m_data_left -= min(read_bytes.size(), m_data_left); + return read_bytes; + } + + virtual ErrorOr write_some(ReadonlyBytes) override { VERIFY_NOT_REACHED(); } + virtual ErrorOr write_until_depleted(ReadonlyBytes) override { VERIFY_NOT_REACHED(); } + + size_t remaining() const + { + return m_data_left; + } + ErrorOr discard_remaining() + { + return discard(remaining()); + } + +private: + Stream& 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 new file mode 100644 index 0000000000..014fcd7cd7 --- /dev/null +++ b/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Boxes.cpp @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2023, Gregory Bertilson + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "Boxes.h" + +namespace Gfx::ISOBMFF { + +ErrorOr read_box_header(Stream& stream) +{ + BoxHeader header; + u64 total_size = TRY(stream.read_value>()); + header.type = TRY(stream.read_value()); + + u64 data_size_read = sizeof(u32) + sizeof(BoxType); + + if (total_size == 1) { + total_size = TRY(stream.read_value>()); + data_size_read += sizeof(u64); + } + + header.contents_size = total_size - data_size_read; + return header; +} + +void Box::dump(String const& prepend) const +{ + outln("{}{}", prepend, box_type()); +} + +ErrorOr FullBox::read_from_stream(BoxStream& stream) +{ + u32 data = TRY(stream.read_value>()); + // unsigned int(8) version + version = static_cast(data >> 24); + // unsigned int(24) flags + flags = data & 0xFFF; + return {}; +} + +void FullBox::dump(String const& prepend) const +{ + outln("{}{} (version = {}, flags = 0x{:x})", prepend, box_type(), version, flags); +} + +static String add_indent(String const& string) +{ + return MUST(String::formatted("{} ", string)); +} + +ErrorOr UnknownBox::read_from_stream(BoxStream& stream) +{ + m_contents_size = stream.remaining(); + TRY(stream.discard_remaining()); + return {}; +} + +void UnknownBox::dump(String const& prepend) const +{ + Box::dump(prepend); + + auto indented_prepend = add_indent(prepend); + outln("{}[ {} bytes ]", prepend, m_contents_size); +} + +ErrorOr FileTypeBox::read_from_stream(BoxStream& stream) +{ + // unsigned int(32) major_brand; + major_brand = TRY(stream.read_value()); + // unsigned int(32) minor_version; + minor_version = TRY(stream.read_value>()); + + // unsigned int(32) compatible_brands[]; // to end of the box + if (stream.remaining() % sizeof(BrandIdentifier) != 0) + return Error::from_string_literal("FileTypeBox compatible_brands contains a partial brand"); + + for (auto minor_brand_count = stream.remaining() / sizeof(BrandIdentifier); minor_brand_count > 0; minor_brand_count--) + TRY(compatible_brands.try_append(TRY(stream.read_value()))); + + return {}; +} + +void FileTypeBox::dump(String const& prepend) const +{ + FullBox::dump(prepend); + + auto indented_prepend = add_indent(prepend); + + outln("{}- major_brand = {}", prepend, major_brand); + outln("{}- minor_version = {}", prepend, minor_version); + + StringBuilder compatible_brands_string; + compatible_brands_string.append("- compatible_brands = { "sv); + for (size_t i = 0; i < compatible_brands.size() - 1; i++) + compatible_brands_string.appendff("{}, ", compatible_brands[i]); + compatible_brands_string.appendff("{} }}", compatible_brands[compatible_brands.size() - 1]); + outln("{}{}", prepend, compatible_brands_string.string_view()); +} + +} diff --git a/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Boxes.h b/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Boxes.h new file mode 100644 index 0000000000..c5234b826f --- /dev/null +++ b/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Boxes.h @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2023, Gregory Bertilson + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "BoxStream.h" +#include "Enums.h" + +namespace Gfx::ISOBMFF { + +// ISO/IEC 14496-12 Fifth Edition + +// 4.2 Object Structure +struct BoxHeader { + BoxType type { BoxType::None }; + u64 contents_size { 0 }; +}; + +ErrorOr read_box_header(Stream& 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; +}; + +using BoxList = Vector>; + +struct FullBox : public Box { + virtual ErrorOr read_from_stream(BoxStream& stream) override; + virtual void dump(String const& prepend = {}) const override; + + u8 version { 0 }; + u32 flags { 0 }; +}; + +struct UnknownBox final : public Box { + static ErrorOr> create_from_stream(BoxType type, BoxStream& stream) + { + auto box = TRY(try_make(type, stream.remaining())); + TRY(box->read_from_stream(stream)); + return box; + } + UnknownBox(BoxType type, size_t contents_size) + : m_box_type(type) + , m_contents_size(contents_size) + { + } + virtual ~UnknownBox() override = default; + virtual ErrorOr read_from_stream(BoxStream&) override; + virtual BoxType box_type() const override { return m_box_type; } + virtual void dump(String const& prepend = {}) const override; + +private: + BoxType m_box_type { BoxType::None }; + size_t m_contents_size { 0 }; +}; + +#define BOX_SUBTYPE(BoxName) \ + static ErrorOr> create_from_stream(BoxStream& stream) \ + { \ + auto box = TRY(try_make()); \ + TRY(box->read_from_stream(stream)); \ + return box; \ + } \ + BoxName() = default; \ + virtual ~BoxName() override = default; \ + virtual ErrorOr read_from_stream(BoxStream& stream) override; \ + virtual BoxType box_type() const override \ + { \ + return BoxType::BoxName; \ + } \ + virtual void dump(String const& prepend = {}) const override; + +// 4.3 File Type Box +struct FileTypeBox final : public FullBox { + BOX_SUBTYPE(FileTypeBox); + + BrandIdentifier major_brand { BrandIdentifier::None }; + u32 minor_version; + Vector compatible_brands; +}; + +} diff --git a/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Enums.h b/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Enums.h new file mode 100644 index 0000000000..b53076954f --- /dev/null +++ b/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Enums.h @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2023, Gregory Bertilson + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +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) + +constexpr u32 fourcc_to_number(char const fourcc[4]) +{ + return AK::convert_between_host_and_big_endian((fourcc[0] << 24) | (fourcc[1] << 16) | (fourcc[2] << 8) | fourcc[3]); +} + +enum class BoxType : u32 { + None = 0, + +#define ENUMERATE_ONE(box_name, box_4cc) box_name = fourcc_to_number(#box_4cc), + + ENUMERATE_ALL() + +#undef ENUMERATE_ONE +}; + +static Optional box_type_to_string(BoxType type) +{ + switch (type) { +#define ENUMERATE_ONE(box_name, box_4cc) \ + case BoxType::box_name: \ + return #box_name " ('" #box_4cc "')"sv; + + ENUMERATE_ALL() + +#undef ENUMERATE_ONE + + default: + return {}; + } +} + +#undef ENUMERATE_ALL + +// Define all FileTypeBox brand identifiers: +#define ENUMERATE_ALL() \ + ENUMERATE_ONE(iso8) \ + ENUMERATE_ONE(avif) \ + ENUMERATE_ONE(avis) \ + ENUMERATE_ONE(mif1) \ + ENUMERATE_ONE(msf1) \ + ENUMERATE_ONE(miaf) \ + ENUMERATE_ONE(MA1A) + +enum class BrandIdentifier : u32 { + None = 0, + +#define ENUMERATE_ONE(brand_4cc) brand_4cc = fourcc_to_number(#brand_4cc), + + ENUMERATE_ALL() + +#undef ENUMERATE_ONE +}; + +static Optional brand_identifier_to_string(BrandIdentifier type) +{ + switch (type) { +#define ENUMERATE_ONE(brand_4cc) \ + case BrandIdentifier::brand_4cc: \ + return #brand_4cc##sv; + + ENUMERATE_ALL() + +#undef ENUMERATE_ONE + + default: + return {}; + } +} + +#undef ENUMERATE_ALL + +} + +template<> +struct AK::Formatter : Formatter { + ErrorOr format(FormatBuilder& builder, Gfx::ISOBMFF::BoxType const& box_type) + { + auto string = Gfx::ISOBMFF::box_type_to_string(box_type); + if (string.has_value()) { + return Formatter::format(builder, "{}"sv, string.release_value()); + } + return Formatter::format(builder, "Unknown Box ('{}')"sv, StringView((char const*)&box_type, 4)); + } +}; + +template<> +struct AK::Formatter : Formatter { + ErrorOr format(FormatBuilder& builder, Gfx::ISOBMFF::BrandIdentifier const& brand_identifier) + { + auto string = Gfx::ISOBMFF::brand_identifier_to_string(brand_identifier); + if (string.has_value()) { + return Formatter::format(builder, "{}"sv, string.release_value()); + } + return Formatter::format(builder, "{}"sv, StringView((char const*)&brand_identifier, 4)); + } +}; diff --git a/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Reader.cpp b/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Reader.cpp new file mode 100644 index 0000000000..231405af8c --- /dev/null +++ b/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Reader.cpp @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023, Gregory Bertilson + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "Reader.h" + +namespace Gfx::ISOBMFF { + +ErrorOr Reader::create(MaybeOwned stream) +{ + return Reader(move(stream)); +} + +ErrorOr Reader::read_entire_file() +{ + BoxList top_level_boxes; + + while (!m_stream->is_eof()) { + auto box_header = TRY(read_box_header(*m_stream)); + BoxStream box_stream { *m_stream, 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: + TRY(top_level_boxes.try_append(TRY(UnknownBox::create_from_stream(box_header.type, box_stream)))); + break; + } + + if (!box_stream.is_eof()) + return Error::from_string_literal("Reader did not consume all data"); + } + return top_level_boxes; +} + +} diff --git a/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Reader.h b/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Reader.h new file mode 100644 index 0000000000..196fdb226c --- /dev/null +++ b/Userland/Libraries/LibGfx/ImageFormats/ISOBMFF/Reader.h @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023, Gregory Bertilson + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +#include "Boxes.h" + +namespace Gfx::ISOBMFF { + +class Reader { +public: + static ErrorOr create(MaybeOwned stream); + + ErrorOr read_entire_file(); + + ErrorOr get_major_brand(); + ErrorOr> get_minor_brands(); + +private: + Reader(MaybeOwned stream) + : m_stream(move(stream)) + { + } + + ErrorOr parse_initial_data(); + + MaybeOwned m_stream; +}; + +} diff --git a/Userland/Utilities/CMakeLists.txt b/Userland/Utilities/CMakeLists.txt index 6c2c30f898..361a580b9f 100644 --- a/Userland/Utilities/CMakeLists.txt +++ b/Userland/Utilities/CMakeLists.txt @@ -102,6 +102,7 @@ target_link_libraries(image PRIVATE LibGfx) target_link_libraries(image2bin PRIVATE LibGfx) target_link_libraries(ini PRIVATE LibFileSystem) target_link_libraries(install-bin PRIVATE LibFileSystem) +target_link_libraries(isobmff PRIVATE LibGfx) target_link_libraries(jail-attach PRIVATE LibCore LibMain) target_link_libraries(jail-create PRIVATE LibCore LibMain) target_link_libraries(js PRIVATE LibCrypto LibJS LibLine LibLocale LibTextCodec) diff --git a/Userland/Utilities/isobmff.cpp b/Userland/Utilities/isobmff.cpp new file mode 100644 index 0000000000..6178933e2a --- /dev/null +++ b/Userland/Utilities/isobmff.cpp @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023, Gregory Bertilson + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include + +ErrorOr serenity_main(Main::Arguments arguments) +{ + Core::ArgsParser args_parser; + + StringView path; + args_parser.add_positional_argument(path, "Path to ISO Base Media File Format file", "FILE"); + + args_parser.parse(arguments); + + auto file = TRY(Core::MappedFile::map(path)); + auto reader = TRY(Gfx::ISOBMFF::Reader::create(TRY(try_make(file->bytes())))); + auto boxes = TRY(reader.read_entire_file()); + + for (auto& box : boxes) + box->dump(); + return 0; +}