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