1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-26 07:17:35 +00:00

LibGfx/TIFF: Introduce a code generator

This will allow us to generate code that handle and provide easy access
to metadata stored in TIFF's tags. The generator is a Python script, and
it output both TIFFMetadata.h and TIFFTagHandler.cpp files.

The generator will definitely need some update to support all TIFF and
EXIF tags, but that will still be easier than writing everything
ourselves.

Some small modifications are needed in TIFFLoader.cpp to make it
compatible with the new `Metadata` class.
This commit is contained in:
Lucas CHOLLET 2023-11-12 23:59:52 -05:00 committed by Andrew Kaster
parent 8c65cc185a
commit 9836a9ad0e
5 changed files with 372 additions and 251 deletions

View file

@ -36,7 +36,6 @@ set(SOURCES
ImageFormats/BMPWriter.cpp
ImageFormats/BooleanDecoder.cpp
ImageFormats/TIFFLoader.cpp
ImageFormats/TIFFTagHandler.cpp
ImageFormats/DDSLoader.cpp
ImageFormats/GIFLoader.cpp
ImageFormats/ICOLoader.cpp
@ -79,3 +78,22 @@ set(SOURCES
serenity_lib(LibGfx gfx)
target_link_libraries(LibGfx PRIVATE LibCompress LibCore LibCrypto LibFileSystem LibTextCodec LibIPC LibUnicode)
set(generated_sources TIFFMetadata.h TIFFTagHandler.cpp)
list(TRANSFORM generated_sources PREPEND "ImageFormats/")
find_package(Python COMPONENTS Interpreter REQUIRED)
add_custom_command(
OUTPUT ${generated_sources}
COMMAND ${Python_EXECUTABLE} "${CMAKE_CURRENT_SOURCE_DIR}/TIFFGenerator.py" -o "${CMAKE_CURRENT_BINARY_DIR}/ImageFormats"
DEPENDS "TIFFGenerator.py"
VERBATIM
)
target_sources(LibGfx PRIVATE ${generated_sources})
add_custom_target(generate_tiff_files_handler DEPENDS ${generated_sources})
add_dependencies(all_generated generate_tiff_files_handler)
add_dependencies(LibGfx generate_tiff_files_handler)
list(TRANSFORM generated_sources PREPEND "${CMAKE_CURRENT_BINARY_DIR}/")
install(FILES ${generated_sources} DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/LibGfx/ImageFormats")

View file

@ -52,7 +52,7 @@ public:
IntSize size() const
{
return m_metadata.size;
return { *m_metadata.image_width(), *m_metadata.image_height() };
}
State state() const
@ -74,21 +74,24 @@ private:
template<typename ByteReader>
ErrorOr<void> loop_over_pixels(ByteReader&& byte_reader, Function<ErrorOr<void>(u32)> initializer = {})
{
for (u32 strip_index = 0; strip_index < m_metadata.strip_offsets.size(); ++strip_index) {
TRY(m_stream->seek(m_metadata.strip_offsets[strip_index]));
auto const strips_offset = *m_metadata.strip_offsets();
auto const strip_byte_counts = *m_metadata.strip_byte_counts();
for (u32 strip_index = 0; strip_index < strips_offset.size(); ++strip_index) {
TRY(m_stream->seek(strips_offset[strip_index]));
if (initializer)
TRY(initializer(m_metadata.strip_bytes_count[strip_index]));
for (u32 row = 0; row < m_metadata.rows_per_strip; row++) {
auto const scanline = row + m_metadata.rows_per_strip * strip_index;
if (scanline >= static_cast<u32>(m_metadata.size.height()))
TRY(initializer(strip_byte_counts[strip_index]));
for (u32 row = 0; row < *m_metadata.rows_per_strip(); row++) {
auto const scanline = row + *m_metadata.rows_per_strip() * strip_index;
if (scanline >= *m_metadata.image_height())
break;
Optional<Color> last_color {};
for (u32 column = 0; column < static_cast<u32>(m_metadata.size.width()); ++column) {
for (u32 column = 0; column < *m_metadata.image_width(); ++column) {
auto color = Color { TRY(byte_reader()), TRY(byte_reader()), TRY(byte_reader()) };
if (m_metadata.predictor == Predictor::HorizontalDifferencing && last_color.has_value()) {
if (m_metadata.predictor() == Predictor::HorizontalDifferencing && last_color.has_value()) {
color.set_red(last_color->red() + color.red());
color.set_green(last_color->green() + color.green());
color.set_blue(last_color->blue() + color.blue());
@ -105,9 +108,9 @@ private:
ErrorOr<void> decode_frame_impl()
{
m_bitmap = TRY(Bitmap::create(BitmapFormat::BGRA8888, m_metadata.size));
m_bitmap = TRY(Bitmap::create(BitmapFormat::BGRA8888, size()));
switch (m_metadata.compression) {
switch (*m_metadata.compression()) {
case Compression::NoCompression:
TRY(loop_over_pixels([this]() { return read_value<u8>(); }));
break;
@ -341,7 +344,7 @@ private:
result.empend(T { TRY(read_value<typename T::Type>()), TRY(read_value<typename T::Type>()) });
} else {
for (u32 i = 0; i < count; ++i)
result.empend(TRY(read_value<T>()));
result.empend(typename TypePromoter<T>::Type(TRY(read_value<T>())));
}
return result;
};

View file

@ -1,73 +0,0 @@
/*
* Copyright (c) 2023, Lucas Chollet <lucas.chollet@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Variant.h>
#include <AK/Vector.h>
#include <LibGfx/Size.h>
namespace Gfx {
struct Metadata;
namespace TIFF {
enum class Type {
Byte = 1,
ASCII = 2,
UnsignedShort = 3,
UnsignedLong = 4,
UnsignedRational = 5,
Undefined = 7,
SignedLong = 9,
SignedRational = 10,
Float = 11,
Double = 12,
UTF8 = 129,
};
template<OneOf<u32, i32> x32>
struct Rational {
using Type = x32;
x32 numerator;
x32 denominator;
};
using Value = Variant<u8, String, u16, u32, Rational<u32>, i32, Rational<i32>>;
// This enum is progessively defined across sections but summarized in:
// Appendix A: TIFF Tags Sorted by Number
enum class Compression {
NoCompression = 1,
CCITT = 2,
Group3Fax = 3,
Group4Fax = 4,
LZW = 5,
JPEG = 6,
PackBits = 32773,
};
enum class Predictor {
None = 1,
HorizontalDifferencing = 2,
};
ErrorOr<void> handle_tag(Metadata& metadata, u16 tag, Type type, u32 count, Vector<Value>&& value);
}
struct Metadata {
IntSize size {};
Array<u16, 3> bits_per_sample {};
TIFF::Compression compression {};
TIFF::Predictor predictor {};
Vector<u32> strip_offsets {};
u32 rows_per_strip {};
Vector<u32> strip_bytes_count {};
};
}

View file

@ -1,165 +0,0 @@
/*
* Copyright (c) 2023, Lucas Chollet <lucas.chollet@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Debug.h>
#include <AK/String.h>
#include <LibGfx/ImageFormats/TIFFMetadata.h>
namespace Gfx::TIFF {
ErrorOr<void> handle_tag(Metadata& metadata, u16 tag, Type type, u32 count, Vector<Value>&& value)
{
// FIXME: Make that easy to extend
switch (tag) {
case 256:
// ImageWidth
if ((type != Type::UnsignedShort && type != Type::UnsignedLong) || count != 1)
return Error::from_string_literal("TIFFImageDecoderPlugin: Invalid tag 256");
value[0].visit(
[&metadata]<OneOf<u16, u32> T>(T const& width) {
metadata.size.set_width(width);
},
[&](auto const&) {
VERIFY_NOT_REACHED();
});
break;
case 257:
// ImageLength
if ((type != Type::UnsignedShort && type != Type::UnsignedLong) || count != 1)
return Error::from_string_literal("TIFFImageDecoderPlugin: Invalid tag 257");
value[0].visit(
[&metadata]<OneOf<u16, u32> T>(T const& width) {
metadata.size.set_height(width);
},
[&](auto const&) {
VERIFY_NOT_REACHED();
});
break;
case 258:
// BitsPerSample
if (type != Type::UnsignedShort || count != 3)
return Error::from_string_literal("TIFFImageDecoderPlugin: Invalid tag 258");
for (u8 i = 0; i < metadata.bits_per_sample.size(); ++i) {
value[i].visit(
[&metadata, i](u16 const& bits_per_sample) {
metadata.bits_per_sample[i] = bits_per_sample;
},
[&](auto const&) {
VERIFY_NOT_REACHED();
});
}
break;
case 259:
// Compression
if (type != Type::UnsignedShort || count != 1)
return Error::from_string_literal("TIFFImageDecoderPlugin: Invalid tag 259");
TRY(value[0].visit(
[&metadata](u16 const& compression) -> ErrorOr<void> {
if (compression > 6 && compression != to_underlying(Compression::PackBits))
return Error::from_string_literal("TIFFImageDecoderPlugin: Invalid compression value");
metadata.compression = static_cast<Compression>(compression);
return {};
},
[&](auto const&) -> ErrorOr<void> {
VERIFY_NOT_REACHED();
}));
break;
case 273:
// StripOffsets
if (type != Type::UnsignedShort && type != Type::UnsignedLong)
return Error::from_string_literal("TIFFImageDecoderPlugin: Invalid tag 273");
TRY(metadata.strip_offsets.try_ensure_capacity(count));
for (u32 i = 0; i < count; ++i) {
value[i].visit(
[&metadata]<OneOf<u16, u32> T>(T const& offset) {
metadata.strip_offsets.append(offset);
},
[&](auto const&) {
VERIFY_NOT_REACHED();
});
}
break;
case 277:
// SamplesPerPixel
if (type != Type::UnsignedShort || count != 1)
return Error::from_string_literal("TIFFImageDecoderPlugin: Invalid tag 277");
TRY(value[0].visit(
[](u16 const& samples_per_pixels) -> ErrorOr<void> {
if (samples_per_pixels != 3)
return Error::from_string_literal("TIFFImageDecoderPlugin: Invalid tag 277");
return {};
},
[&](auto const&) -> ErrorOr<void> {
VERIFY_NOT_REACHED();
}));
break;
case 278:
// RowsPerStrip
if ((type != Type::UnsignedShort && type != Type::UnsignedLong) || count != 1)
return Error::from_string_literal("TIFFImageDecoderPlugin: Invalid tag 278");
value[0].visit(
[&metadata]<OneOf<u16, u32> T>(T const& rows_per_strip) {
metadata.rows_per_strip = rows_per_strip;
},
[&](auto const&) {
VERIFY_NOT_REACHED();
});
break;
case 279:
// StripByteCounts
if (type != Type::UnsignedShort && type != Type::UnsignedLong)
return Error::from_string_literal("TIFFImageDecoderPlugin: Invalid tag 279");
TRY(metadata.strip_bytes_count.try_ensure_capacity(count));
for (u32 i = 0; i < count; ++i) {
value[i].visit(
[&metadata]<OneOf<u16, u32> T>(T const& offset) {
metadata.strip_bytes_count.append(offset);
},
[&](auto const&) {
VERIFY_NOT_REACHED();
});
}
break;
case 317:
// Predictor
if (type != Type::UnsignedShort || count != 1)
return Error::from_string_literal("TIFFImageDecoderPlugin: Invalid tag 317");
TRY(value[0].visit(
[&metadata](u16 const& predictor) -> ErrorOr<void> {
if (predictor != 1 && predictor != 2)
return Error::from_string_literal("TIFFImageDecoderPlugin: Invalid predictor value");
metadata.predictor = static_cast<Predictor>(predictor);
return {};
},
[&](auto const&) -> ErrorOr<void> {
VERIFY_NOT_REACHED();
}));
break;
default:
dbgln_if(TIFF_DEBUG, "Unknown tag: {}", tag);
}
return {};
}
}

View file

@ -0,0 +1,338 @@
#!/usr/bin/env python3
# Copyright (c) 2023, Lucas Chollet <lucas.chollet@serenityos.org>
#
# SPDX-License-Identifier: BSD-2-Clause
import argparse
import re
from enum import Enum
from collections import namedtuple
from pathlib import Path
from typing import List, Type
class TIFFType(Enum):
Byte = 1
ASCII = 2
UnsignedShort = 3
UnsignedLong = 4
UnsignedRational = 5
Undefined = 7
SignedLong = 9
SignedRational = 10
Float = 11
Double = 12
UTF8 = 129
class Predictor(Enum):
NoPrediction = 1
HorizontalDifferencing = 2
class Compression(Enum):
NoCompression = 1
CCITT = 2
Group3Fax = 3
Group4Fax = 4
LZW = 5
JPEG = 6
PackBits = 32773
tag_fields = ['id', 'types', 'counts', 'default', 'name', 'associated_enum']
Tag = namedtuple(
'Tag',
field_names=tag_fields,
defaults=(None,) * len(tag_fields)
)
# FIXME: Some tag have only a few allowed values, we should ensure that
known_tags: List[Tag] = [
Tag('256', [TIFFType.UnsignedShort, TIFFType.UnsignedLong], [1], None, "ImageWidth"),
Tag('257', [TIFFType.UnsignedShort, TIFFType.UnsignedLong], [1], None, "ImageHeight"),
Tag('258', [TIFFType.UnsignedShort], [3], None, "BitPerSample"),
Tag('259', [TIFFType.UnsignedShort], [1], None, "Compression", Compression),
Tag('273', [TIFFType.UnsignedShort, TIFFType.UnsignedLong], [], None, "StripOffsets"),
Tag('278', [TIFFType.UnsignedShort, TIFFType.UnsignedLong], [1], None, "RowsPerStrip"),
Tag('279', [TIFFType.UnsignedShort, TIFFType.UnsignedLong], [], None, "StripByteCounts"),
Tag('317', [TIFFType.UnsignedShort], [1], Predictor.NoPrediction, "Predictor", Predictor),
]
HANDLE_TAG_SIGNATURE_TEMPLATE = ("ErrorOr<void> {namespace}handle_tag(Metadata& metadata, u16 tag,"
" {namespace}Type type, u32 count, Vector<{namespace}Value>&& value)")
HANDLE_TAG_SIGNATURE = HANDLE_TAG_SIGNATURE_TEMPLATE.format(namespace="")
HANDLE_TAG_SIGNATURE_TIFF_NAMESPACE = HANDLE_TAG_SIGNATURE_TEMPLATE.format(namespace="TIFF::")
LICENSE = R"""/*
* Copyright (c) 2023, Lucas Chollet <lucas.chollet@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/"""
def export_enum_to_cpp(e: Type[Enum], special_name: str | None = None) -> str:
output = f'enum class {special_name if special_name else e.__name__} {{\n'
for entry in e:
output += f' {entry.name} = {entry.value},\n'
output += "};"
return output
def promote_type(t: TIFFType) -> TIFFType:
if t == TIFFType.UnsignedShort:
return TIFFType.UnsignedLong
return t
def tiff_type_to_cpp(t: TIFFType, without_promotion: bool = False) -> str:
# To simplify the code generator and the Metadata class API, all u16 are promoted to u32
# Note that the Value<> type doesn't include u16 for this reason
if not without_promotion:
t = promote_type(t)
match t:
case TIFFType.UnsignedShort:
return 'u16'
case TIFFType.UnsignedLong:
return 'u32'
case _:
raise RuntimeError(f'Type "{t}" not recognized, please update tiff_type_to_read_only_cpp()')
def export_promoter() -> str:
output = R"""template<typename T>
struct TypePromoter {
using Type = T;
};
"""
specialization_template = R"""template<>
struct TypePromoter<{}> {{
using Type = {};
}};
"""
for t in TIFFType:
if promote_type(t) != t:
output += specialization_template.format(tiff_type_to_cpp(t, without_promotion=True), tiff_type_to_cpp(t))
return output
def retrieve_biggest_type(types: List[TIFFType]) -> TIFFType:
return TIFFType(max([t.value for t in types]))
def pascal_case_to_snake_case(name: str) -> str:
return re.sub(r'(?<!^)(?=[A-Z])', '_', name).lower()
def generate_getter(tag: Tag) -> str:
variant_inner_type = tiff_type_to_cpp(retrieve_biggest_type(tag.types))
extracted_value_template = f"(*possible_value)[{{}}].get<{variant_inner_type}>()"
tag_final_type = variant_inner_type
if tag.associated_enum:
tag_final_type = f"TIFF::{tag.associated_enum.__name__}"
extracted_value_template = f"static_cast<{tag_final_type}>({extracted_value_template})"
if len(tag.counts) == 1 and tag.counts[0] == 1:
return_type = tag_final_type
unpacked_if_needed = f"return {extracted_value_template.format(0)};"
else:
if len(tag.counts) == 1:
container_type = f'Array<{tag_final_type}, {tag.counts[0]}>'
container_initialization = f'{container_type} tmp{{}};'
else:
container_type = f'Vector<{tag_final_type}>'
container_initialization = fR"""{container_type} tmp{{}};
auto maybe_failure = tmp.try_resize(possible_value->size());
if (maybe_failure.is_error())
return OptionalNone {{}};
"""
return_type = container_type
unpacked_if_needed = fR"""
{container_initialization}
for (u32 i = 0; i < possible_value->size(); ++i)
tmp[i] = {extracted_value_template.format('i')};
return tmp;"""
signature = fR" Optional<{return_type}> {pascal_case_to_snake_case(tag.name)}() const"
body = fR"""
{{
auto const& possible_value = m_data.get("{tag.name}"sv);
if (!possible_value.has_value())
return OptionalNone {{}};
{unpacked_if_needed}
}}
"""
return signature + body
def generate_metadata_class(tags: List[Tag]) -> str:
getters = '\n'.join([generate_getter(tag) for tag in tags])
output = fR"""class Metadata {{
public:
{getters}
private:
friend {HANDLE_TAG_SIGNATURE_TIFF_NAMESPACE};
void add_entry(StringView key, Vector<TIFF::Value>&& value) {{
m_data.set(key, move(value));
}}
HashMap<StringView, Vector<TIFF::Value>> m_data;
}};
"""
return output
def generate_metadata_file(tags: List[Tag]) -> str:
output = fR"""{LICENSE}
#pragma once
#include <AK/HashMap.h>
#include <AK/Variant.h>
#include <AK/Vector.h>
#include <LibGfx/Size.h>
namespace Gfx {{
class Metadata;
namespace TIFF {{
{export_enum_to_cpp(TIFFType, 'Type')}
template<OneOf<u32, i32> x32>
struct Rational {{
using Type = x32;
x32 numerator;
x32 denominator;
}};
{export_promoter()}
// Note that u16 is not include on purpose
using Value = Variant<u8, String, u32, Rational<u32>, i32, Rational<i32>>;
// This enum is progressively defined across sections but summarized in:
// Appendix A: TIFF Tags Sorted by Number
{export_enum_to_cpp(Compression)}
{export_enum_to_cpp(Predictor)}
{HANDLE_TAG_SIGNATURE};
}}
"""
output += generate_metadata_class(tags)
output += '\n}\n'
return output
def generate_tag_handler(tag: Tag) -> str:
not_in_type_list = f"({' && '.join([f'type != Type::{t.name}' for t in tag.types])})"
not_in_count_list = ''
if len(tag.counts) != 0:
not_in_count_list = f"|| ({' && '.join([f'count != {c}' for c in tag.counts])})"
pre_condition = fR"""if ({not_in_type_list}
{not_in_count_list})
return Error::from_string_literal("TIFFImageDecoderPlugin: Tag {tag.name} invalid");"""
check_value = ''
if tag.associated_enum is not None:
not_in_value_list = f"({' && '.join([f'v != {v.value}' for v in tag.associated_enum])})"
check_value = fR"""TRY(value[0].visit(
[]({tiff_type_to_cpp(tag.types[0])} const& v) -> ErrorOr<void> {{
if ({not_in_value_list})
return Error::from_string_literal("TIFFImageDecoderPlugin: Invalid value for tag {tag.name}");
return {{}};
}},
[&](auto const&) -> ErrorOr<void> {{
VERIFY_NOT_REACHED();
}}));
"""
output = fR""" case {tag.id}:
// {tag.name}
{pre_condition}
{check_value}
metadata.add_entry("{tag.name}"sv, move(value));
break;
"""
return output
def generate_tag_handler_file(tags: List[Tag]) -> str:
output = fR"""{LICENSE}
#include <AK/Debug.h>
#include <AK/String.h>
#include <LibGfx/ImageFormats/TIFFMetadata.h>
namespace Gfx::TIFF {{
{HANDLE_TAG_SIGNATURE}
{{
switch (tag) {{
"""
output += '\n'.join([generate_tag_handler(t) for t in tags])
output += R"""
default:
dbgln_if(TIFF_DEBUG, "Unknown tag: {}", tag);
}
return {};
}
}
"""
return output
def update_file(target: Path, new_content: str):
should_update = True
if target.exists():
with target.open('r') as file:
content = file.read()
if content == new_content:
should_update = False
if should_update:
with target.open('w') as file:
file.write(new_content)
def main():
parser = argparse.ArgumentParser()
parser.add_argument('-o', '--output')
args = parser.parse_args()
output_path = Path(args.output)
update_file(output_path / 'TIFFMetadata.h', generate_metadata_file(known_tags))
update_file(output_path / 'TIFFTagHandler.cpp', generate_tag_handler_file(known_tags))
if __name__ == '__main__':
main()