From 9836a9ad0ef93bdace6620be715766805cc4aed4 Mon Sep 17 00:00:00 2001 From: Lucas CHOLLET Date: Sun, 12 Nov 2023 23:59:52 -0500 Subject: [PATCH] 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. --- Userland/Libraries/LibGfx/CMakeLists.txt | 20 +- .../LibGfx/ImageFormats/TIFFLoader.cpp | 27 +- .../LibGfx/ImageFormats/TIFFMetadata.h | 73 ---- .../LibGfx/ImageFormats/TIFFTagHandler.cpp | 165 --------- Userland/Libraries/LibGfx/TIFFGenerator.py | 338 ++++++++++++++++++ 5 files changed, 372 insertions(+), 251 deletions(-) delete mode 100644 Userland/Libraries/LibGfx/ImageFormats/TIFFMetadata.h delete mode 100644 Userland/Libraries/LibGfx/ImageFormats/TIFFTagHandler.cpp create mode 100755 Userland/Libraries/LibGfx/TIFFGenerator.py diff --git a/Userland/Libraries/LibGfx/CMakeLists.txt b/Userland/Libraries/LibGfx/CMakeLists.txt index 7915ad4520..9271ecfab8 100644 --- a/Userland/Libraries/LibGfx/CMakeLists.txt +++ b/Userland/Libraries/LibGfx/CMakeLists.txt @@ -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") diff --git a/Userland/Libraries/LibGfx/ImageFormats/TIFFLoader.cpp b/Userland/Libraries/LibGfx/ImageFormats/TIFFLoader.cpp index 24ccc6ebe8..45c96baf6a 100644 --- a/Userland/Libraries/LibGfx/ImageFormats/TIFFLoader.cpp +++ b/Userland/Libraries/LibGfx/ImageFormats/TIFFLoader.cpp @@ -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 ErrorOr loop_over_pixels(ByteReader&& byte_reader, Function(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(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 last_color {}; - for (u32 column = 0; column < static_cast(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 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(); })); break; @@ -341,7 +344,7 @@ private: result.empend(T { TRY(read_value()), TRY(read_value()) }); } else { for (u32 i = 0; i < count; ++i) - result.empend(TRY(read_value())); + result.empend(typename TypePromoter::Type(TRY(read_value()))); } return result; }; diff --git a/Userland/Libraries/LibGfx/ImageFormats/TIFFMetadata.h b/Userland/Libraries/LibGfx/ImageFormats/TIFFMetadata.h deleted file mode 100644 index 56ce7c6dad..0000000000 --- a/Userland/Libraries/LibGfx/ImageFormats/TIFFMetadata.h +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (c) 2023, Lucas Chollet - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#pragma once - -#include -#include -#include - -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 x32> -struct Rational { - using Type = x32; - x32 numerator; - x32 denominator; -}; - -using Value = Variant, i32, Rational>; - -// 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 handle_tag(Metadata& metadata, u16 tag, Type type, u32 count, Vector&& value); - -} - -struct Metadata { - IntSize size {}; - Array bits_per_sample {}; - TIFF::Compression compression {}; - TIFF::Predictor predictor {}; - Vector strip_offsets {}; - u32 rows_per_strip {}; - Vector strip_bytes_count {}; -}; - -} diff --git a/Userland/Libraries/LibGfx/ImageFormats/TIFFTagHandler.cpp b/Userland/Libraries/LibGfx/ImageFormats/TIFFTagHandler.cpp deleted file mode 100644 index eabe8c2a98..0000000000 --- a/Userland/Libraries/LibGfx/ImageFormats/TIFFTagHandler.cpp +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright (c) 2023, Lucas Chollet - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#include -#include -#include - -namespace Gfx::TIFF { - -ErrorOr handle_tag(Metadata& metadata, u16 tag, Type type, u32 count, Vector&& 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] 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] 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 { - if (compression > 6 && compression != to_underlying(Compression::PackBits)) - return Error::from_string_literal("TIFFImageDecoderPlugin: Invalid compression value"); - - metadata.compression = static_cast(compression); - return {}; - }, - [&](auto const&) -> ErrorOr { - 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] 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 { - if (samples_per_pixels != 3) - return Error::from_string_literal("TIFFImageDecoderPlugin: Invalid tag 277"); - return {}; - }, - [&](auto const&) -> ErrorOr { - 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] 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] 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 { - if (predictor != 1 && predictor != 2) - return Error::from_string_literal("TIFFImageDecoderPlugin: Invalid predictor value"); - - metadata.predictor = static_cast(predictor); - return {}; - }, - [&](auto const&) -> ErrorOr { - VERIFY_NOT_REACHED(); - })); - break; - default: - dbgln_if(TIFF_DEBUG, "Unknown tag: {}", tag); - } - - return {}; -} - -} diff --git a/Userland/Libraries/LibGfx/TIFFGenerator.py b/Userland/Libraries/LibGfx/TIFFGenerator.py new file mode 100755 index 0000000000..b37d723683 --- /dev/null +++ b/Userland/Libraries/LibGfx/TIFFGenerator.py @@ -0,0 +1,338 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2023, Lucas Chollet +# +# 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 {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 + * + * 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 +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'(? 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&& value) {{ + m_data.set(key, move(value)); + }} + + HashMap> m_data; +}}; +""" + + return output + + +def generate_metadata_file(tags: List[Tag]) -> str: + output = fR"""{LICENSE} + +#pragma once + +#include +#include +#include +#include + +namespace Gfx {{ + +class Metadata; + +namespace TIFF {{ + +{export_enum_to_cpp(TIFFType, 'Type')} + +template x32> +struct Rational {{ + using Type = x32; + x32 numerator; + x32 denominator; +}}; + +{export_promoter()} + +// Note that u16 is not include on purpose +using Value = Variant, i32, Rational>; + +// 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 {{ + if ({not_in_value_list}) + return Error::from_string_literal("TIFFImageDecoderPlugin: Invalid value for tag {tag.name}"); + return {{}}; + }}, + [&](auto const&) -> ErrorOr {{ + 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 +#include +#include + +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()