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.
This commit is contained in:
Zaggy1024 2023-02-18 17:46:27 -06:00 committed by Sam Atkins
parent 9caa0bda7d
commit 66c9696687
13 changed files with 510 additions and 0 deletions

View file

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

View file

@ -6,6 +6,7 @@ set(TEST_SOURCES
TestGfxBitmap.cpp
TestICCProfile.cpp
TestImageDecoder.cpp
TestParseISOBMFF.cpp
TestRect.cpp
TestScalingFunctions.cpp
)

View file

@ -0,0 +1,37 @@
/*
* Copyright (c) 2023, Gregory Bertilson <zaggy1024@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibTest/TestCase.h>
#include <AK/MemoryStream.h>
#include <LibCore/MappedFile.h>
#include <LibGfx/ImageFormats/ISOBMFF/Reader.h>
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<FixedMemoryStream>(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<Gfx::ISOBMFF::FileTypeBox&>(*boxes[0]);
VERIFY(file_type_box.major_brand == Gfx::ISOBMFF::BrandIdentifier::avis);
VERIFY(file_type_box.minor_version == 0);
Vector<Gfx::ISOBMFF::BrandIdentifier, 7> 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);
}

Binary file not shown.

View file

@ -37,6 +37,8 @@ set(SOURCES
ImageFormats/GIFLoader.cpp
ImageFormats/ICOLoader.cpp
ImageFormats/ImageDecoder.cpp
ImageFormats/ISOBMFF/Boxes.cpp
ImageFormats/ISOBMFF/Reader.cpp
ImageFormats/JPEGLoader.cpp
ImageFormats/JPEGXLLoader.cpp
ImageFormats/JPEGWriter.cpp

View file

@ -0,0 +1,48 @@
/*
* Copyright (c) 2023, Gregory Bertilson <Zaggy1024@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Stream.h>
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<Bytes> 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<size_t> write_some(ReadonlyBytes) override { VERIFY_NOT_REACHED(); }
virtual ErrorOr<void> write_until_depleted(ReadonlyBytes) override { VERIFY_NOT_REACHED(); }
size_t remaining() const
{
return m_data_left;
}
ErrorOr<void> discard_remaining()
{
return discard(remaining());
}
private:
Stream& m_stream;
size_t m_data_left;
};
}

View file

@ -0,0 +1,102 @@
/*
* Copyright (c) 2023, Gregory Bertilson <Zaggy1024@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "Boxes.h"
namespace Gfx::ISOBMFF {
ErrorOr<BoxHeader> read_box_header(Stream& stream)
{
BoxHeader header;
u64 total_size = TRY(stream.read_value<BigEndian<u32>>());
header.type = TRY(stream.read_value<BoxType>());
u64 data_size_read = sizeof(u32) + sizeof(BoxType);
if (total_size == 1) {
total_size = TRY(stream.read_value<BigEndian<u64>>());
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<void> FullBox::read_from_stream(BoxStream& stream)
{
u32 data = TRY(stream.read_value<BigEndian<u32>>());
// unsigned int(8) version
version = static_cast<u8>(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<void> 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<void> FileTypeBox::read_from_stream(BoxStream& stream)
{
// unsigned int(32) major_brand;
major_brand = TRY(stream.read_value<BrandIdentifier>());
// unsigned int(32) minor_version;
minor_version = TRY(stream.read_value<BigEndian<u32>>());
// 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<BrandIdentifier>())));
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());
}
}

View file

@ -0,0 +1,96 @@
/*
* Copyright (c) 2023, Gregory Bertilson <Zaggy1024@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Endian.h>
#include <AK/NonnullOwnPtr.h>
#include <AK/String.h>
#include <AK/Traits.h>
#include <AK/Vector.h>
#include <LibVideo/DecoderError.h>
#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<BoxHeader> read_box_header(Stream& stream);
struct Box {
Box() = default;
virtual ~Box() = default;
virtual ErrorOr<void> read_from_stream(BoxStream&) { return {}; }
virtual BoxType box_type() const { return BoxType::None; }
virtual void dump(String const& prepend = {}) const;
};
using BoxList = Vector<NonnullOwnPtr<Box>>;
struct FullBox : public Box {
virtual ErrorOr<void> 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<NonnullOwnPtr<UnknownBox>> create_from_stream(BoxType type, BoxStream& stream)
{
auto box = TRY(try_make<UnknownBox>(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<void> 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<NonnullOwnPtr<BoxName>> create_from_stream(BoxStream& stream) \
{ \
auto box = TRY(try_make<BoxName>()); \
TRY(box->read_from_stream(stream)); \
return box; \
} \
BoxName() = default; \
virtual ~BoxName() override = default; \
virtual ErrorOr<void> 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<BrandIdentifier> compatible_brands;
};
}

View file

@ -0,0 +1,117 @@
/*
* Copyright (c) 2023, Gregory Bertilson <Zaggy1024@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Endian.h>
#include <AK/Types.h>
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<StringView> 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<StringView> 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<Gfx::ISOBMFF::BoxType> : Formatter<FormatString> {
ErrorOr<void> 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<FormatString>::format(builder, "{}"sv, string.release_value());
}
return Formatter<FormatString>::format(builder, "Unknown Box ('{}')"sv, StringView((char const*)&box_type, 4));
}
};
template<>
struct AK::Formatter<Gfx::ISOBMFF::BrandIdentifier> : Formatter<FormatString> {
ErrorOr<void> 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<FormatString>::format(builder, "{}"sv, string.release_value());
}
return Formatter<FormatString>::format(builder, "{}"sv, StringView((char const*)&brand_identifier, 4));
}
};

View file

@ -0,0 +1,39 @@
/*
* Copyright (c) 2023, Gregory Bertilson <Zaggy1024@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "Reader.h"
namespace Gfx::ISOBMFF {
ErrorOr<Reader> Reader::create(MaybeOwned<SeekableStream> stream)
{
return Reader(move(stream));
}
ErrorOr<BoxList> 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;
}
}

View file

@ -0,0 +1,36 @@
/*
* Copyright (c) 2023, Gregory Bertilson <Zaggy1024@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/MaybeOwned.h>
#include <AK/Stream.h>
#include "Boxes.h"
namespace Gfx::ISOBMFF {
class Reader {
public:
static ErrorOr<Reader> create(MaybeOwned<SeekableStream> stream);
ErrorOr<BoxList> read_entire_file();
ErrorOr<BrandIdentifier> get_major_brand();
ErrorOr<Vector<BrandIdentifier>> get_minor_brands();
private:
Reader(MaybeOwned<SeekableStream> stream)
: m_stream(move(stream))
{
}
ErrorOr<void> parse_initial_data();
MaybeOwned<SeekableStream> m_stream;
};
}

View file

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

View file

@ -0,0 +1,28 @@
/*
* Copyright (c) 2023, Gregory Bertilson <Zaggy1024@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/MemoryStream.h>
#include <LibCore/ArgsParser.h>
#include <LibCore/MappedFile.h>
#include <LibGfx/ImageFormats/ISOBMFF/Reader.h>
ErrorOr<int> 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<FixedMemoryStream>(file->bytes()))));
auto boxes = TRY(reader.read_entire_file());
for (auto& box : boxes)
box->dump();
return 0;
}