#!/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 Any, List, Type class EnumWithExportName(Enum): @classmethod def export_name(cls) -> str: return cls.__name__ class TIFFType(EnumWithExportName): @classmethod def export_name(cls) -> str: return "Type" def __new__(cls, *args): obj = object.__new__(cls) obj._value_ = args[0] obj.size = args[1] return obj # First value is the underlying u16, second one is the size in bytes Byte = 1, 1 ASCII = 2, 1 UnsignedShort = 3, 2 UnsignedLong = 4, 4 UnsignedRational = 5, 8 Undefined = 7, 1 SignedLong = 9, 4 SignedRational = 10, 8 Float = 11, 4 Double = 12, 8 IFD = 13, 4 UTF8 = 129, 1 class Predictor(EnumWithExportName): NoPrediction = 1 HorizontalDifferencing = 2 class Compression(EnumWithExportName): NoCompression = 1 CCITTRLE = 2 Group3Fax = 3 Group4Fax = 4 LZW = 5 OldJPEG = 6 JPEG = 7 AdobeDeflate = 8 PackBits = 32773 PixarDeflate = 32946 # This is the old (and deprecated) code for AdobeDeflate class PhotometricInterpretation(EnumWithExportName): WhiteIsZero = 0 BlackIsZero = 1 RGB = 2 RGBPalette = 3 TransparencyMask = 4 CMYK = 5 YCbCr = 6 CIELab = 8 class FillOrder(EnumWithExportName): LeftToRight = 1 RightToLeft = 2 class Orientation(EnumWithExportName): Default = 1 FlipHorizontally = 2 Rotate180 = 3 FlipVertically = 4 Rotate90ClockwiseThenFlipHorizontally = 5 Rotate90Clockwise = 6 FlipHorizontallyThenRotate90Clockwise = 7 Rotate90CounterClockwise = 8 class PlanarConfiguration(EnumWithExportName): Chunky = 1 Planar = 2 class ResolutionUnit(EnumWithExportName): NoAbsolute = 1 Inch = 2 Centimeter = 3 class SampleFormat(EnumWithExportName): Unsigned = 1 Signed = 2 Float = 3 Undefined = 4 class ExtraSample(EnumWithExportName): Unspecified = 0 AssociatedAlpha = 1 UnassociatedAlpha = 2 tag_fields = ['id', 'types', 'counts', 'default', 'name', 'associated_enum', 'is_required'] 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", is_required=True), Tag('257', [TIFFType.UnsignedShort, TIFFType.UnsignedLong], [1], None, "ImageLength", is_required=True), Tag('258', [TIFFType.UnsignedShort], [], None, "BitsPerSample", is_required=False), Tag('259', [TIFFType.UnsignedShort], [1], None, "Compression", Compression, is_required=True), Tag('262', [TIFFType.UnsignedShort], [1], None, "PhotometricInterpretation", PhotometricInterpretation, is_required=True), Tag('266', [TIFFType.UnsignedShort], [1], FillOrder.LeftToRight, "FillOrder", FillOrder), Tag('271', [TIFFType.ASCII], [], None, "Make"), Tag('272', [TIFFType.ASCII], [], None, "Model"), Tag('273', [TIFFType.UnsignedShort, TIFFType.UnsignedLong], [], None, "StripOffsets", is_required=False), Tag('274', [TIFFType.UnsignedShort], [1], Orientation.Default, "Orientation", Orientation), Tag('277', [TIFFType.UnsignedShort], [1], None, "SamplesPerPixel", is_required=False), Tag('278', [TIFFType.UnsignedShort, TIFFType.UnsignedLong], [1], None, "RowsPerStrip", is_required=False), Tag('279', [TIFFType.UnsignedShort, TIFFType.UnsignedLong], [], None, "StripByteCounts", is_required=False), Tag('282', [TIFFType.UnsignedRational], [1], None, "XResolution"), Tag('283', [TIFFType.UnsignedRational], [1], None, "YResolution"), Tag('284', [TIFFType.UnsignedShort], [1], PlanarConfiguration.Chunky, "PlanarConfiguration", PlanarConfiguration), Tag('285', [TIFFType.ASCII], [], None, "PageName"), Tag('292', [TIFFType.UnsignedLong], [1], 0, "T4Options"), Tag('296', [TIFFType.UnsignedShort], [1], ResolutionUnit.Inch, "ResolutionUnit", ResolutionUnit), Tag('305', [TIFFType.ASCII], [], None, "Software"), Tag('306', [TIFFType.ASCII], [20], None, "DateTime"), Tag('315', [TIFFType.ASCII], [], None, "Artist"), Tag('317', [TIFFType.UnsignedShort], [1], Predictor.NoPrediction, "Predictor", Predictor), Tag('320', [TIFFType.UnsignedShort], [], None, "ColorMap"), Tag('322', [TIFFType.UnsignedShort, TIFFType.UnsignedLong], [1], None, "TileWidth"), Tag('323', [TIFFType.UnsignedShort, TIFFType.UnsignedLong], [1], None, "TileLength"), Tag('324', [TIFFType.UnsignedShort, TIFFType.UnsignedLong], [], None, "TileOffsets"), Tag('325', [TIFFType.UnsignedShort, TIFFType.UnsignedLong], [], None, "TileByteCounts"), Tag('338', [TIFFType.UnsignedShort], [], None, "ExtraSamples", ExtraSample), Tag('339', [TIFFType.UnsignedShort], [], SampleFormat.Unsigned, "SampleFormat", SampleFormat), Tag('34665', [TIFFType.UnsignedLong, TIFFType.IFD], [1], None, "ExifIFD"), Tag('34675', [TIFFType.Undefined], [], None, "ICCProfile"), ] HANDLE_TAG_SIGNATURE_TEMPLATE = ("ErrorOr {namespace}handle_tag(Function(u32)>&& subifd_handler, " "ExifMetadata& 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::") ENSURE_BASELINE_TAG_PRESENCE = "ErrorOr ensure_baseline_tags_are_present(ExifMetadata const& metadata)" TIFF_TYPE_FROM_U16 = "ErrorOr tiff_type_from_u16(u16 type)" SIZE_OF_TIFF_TYPE = "u8 size_of_type(Type type)" LICENSE = R"""/* * Copyright (c) 2023, Lucas Chollet * * SPDX-License-Identifier: BSD-2-Clause */""" def export_enum_to_cpp(e: Type[EnumWithExportName]) -> str: output = f'enum class {e.export_name()} {{\n' for entry in e: output += f' {entry.name} = {entry.value},\n' output += "};\n" return output def export_enum_to_string_converter(enums: List[Type[EnumWithExportName]]) -> str: stringifier_internals = [] for e in enums: single_stringifier = fR""" if constexpr (IsSame) {{ switch (value) {{ default: return "Invalid value for {e.export_name()}"sv;""" for entry in e: single_stringifier += fR""" case {e.export_name()}::{entry.name}: return "{entry.name}"sv;""" single_stringifier += R""" } }""" stringifier_internals.append(single_stringifier) stringifier_internals_str = '\n'.join(stringifier_internals) out = fR"""template StringView name_for_enum_tag_value(E value) {{ {stringifier_internals_str} VERIFY_NOT_REACHED(); }}""" return out def export_tag_related_enums(tags: List[Tag]) -> str: exported_enums = [] for tag in tags: if tag.associated_enum: exported_enums.append(export_enum_to_cpp(tag.associated_enum)) return '\n'.join(exported_enums) def promote_type(t: TIFFType) -> TIFFType: if t == TIFFType.UnsignedShort: return TIFFType.UnsignedLong if t == TIFFType.Float: return TIFFType.Double return t def tiff_type_to_cpp(t: TIFFType, with_promotion: bool = True) -> str: # To simplify the code generator and the ExifMetadata class API, all u16 are promoted to u32 # Note that the Value<> type doesn't include u16 for this reason if with_promotion: t = promote_type(t) if t in [TIFFType.ASCII, TIFFType.UTF8]: return 'String' if t == TIFFType.Undefined: return 'ByteBuffer' if t == TIFFType.UnsignedShort: return 'u16' if t == TIFFType.UnsignedLong or t == TIFFType.IFD: return 'u32' if t == TIFFType.UnsignedRational: return 'TIFF::Rational' if t == TIFFType.Float: return 'float' if t == TIFFType.Double: return 'double' raise RuntimeError(f'Type "{t}" not recognized, please update tiff_type_to_read_only_cpp()') def is_container(t: TIFFType) -> bool: """ Some TIFF types are defined on the unit scale but are intended to be used within a collection. An example of that are ASCII strings defined as N * byte. Let's intercept that and generate a nice API instead of Vector. """ return t in [TIFFType.ASCII, TIFFType.Byte, TIFFType.Undefined, TIFFType.UTF8] 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, with_promotion=False), 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: name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) return re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).lower() def default_value_to_cpp(value: Any) -> str: if isinstance(value, EnumWithExportName): return f'TIFF::{value.export_name()}::{value.name}' return str(value) def generate_getter(tag: Tag) -> str: biggest_type = retrieve_biggest_type(tag.types) variant_inner_type = tiff_type_to_cpp(biggest_type) 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})" single_count = len(tag.counts) == 1 and tag.counts[0] == 1 or is_container(biggest_type) if single_count: return_type = tag_final_type if is_container(biggest_type): return_type += ' const&' 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" if tag.default is not None and single_count: return_if_empty = f'{default_value_to_cpp(tag.default)}' else: return_if_empty = 'OptionalNone {}' body = fR""" {{ auto const& possible_value = m_data.get("{tag.name}"sv); if (!possible_value.has_value()) return {return_if_empty}; {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 ExifMetadata : public Metadata {{ public: virtual ~ExifMetadata() = default; {getters} private: friend {HANDLE_TAG_SIGNATURE_TIFF_NAMESPACE}; virtual void fill_main_tags() const override {{ if (model().has_value()) m_main_tags.set("Model"sv, model().value()); if (make().has_value()) m_main_tags.set("Manufacturer"sv, make().value()); if (software().has_value()) m_main_tags.set("Software"sv, software().value()); if (date_time().has_value()) m_main_tags.set("Creation Time"sv, date_time().value()); if (artist().has_value()) m_main_tags.set("Author"sv, artist().value()); }} 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 #include namespace Gfx {{ class ExifMetadata; namespace TIFF {{ {export_enum_to_cpp(TIFFType)} template x32> struct Rational {{ using Type = x32; x32 numerator; x32 denominator; double as_double() const {{ return static_cast(numerator) / denominator; }} }}; {export_promoter()} // Note that u16 is not include on purpose using Value = Variant, i32, Rational, double>; {export_tag_related_enums(known_tags)} {export_enum_to_string_converter([tag.associated_enum for tag in known_tags if tag.associated_enum] + [TIFFType])} {HANDLE_TAG_SIGNATURE}; {ENSURE_BASELINE_TAG_PRESENCE}; {TIFF_TYPE_FROM_U16}; {SIZE_OF_TIFF_TYPE}; }} {generate_metadata_class(tags)} }} template struct AK::Formatter> : Formatter {{ ErrorOr format(FormatBuilder& builder, Gfx::TIFF::Rational value) {{ return Formatter::format(builder, "{{}} ({{}}/{{}})"sv, value.as_double(), value.numerator, value.denominator); }} }}; template<> struct AK::Formatter : Formatter {{ ErrorOr format(FormatBuilder& builder, Gfx::TIFF::Value const& value) {{ String content; value.visit( [&](ByteBuffer const& buffer) {{ content = MUST(String::formatted("Buffer of size: {{}}"sv, buffer.size())); }}, [&](auto const& other) {{ content = MUST(String::formatted("{{}}", other)); }} ); return Formatter::format(builder, "{{}}"sv, content); }} }}; """ 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""" for (u32 i = 0; i < value.size(); ++i) {{ TRY(value[i].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(); }}) ); }} """ handle_subifd = '' if TIFFType.IFD in tag.types: if tag.counts != [1]: raise RuntimeError("Accessing `value[0]` in the C++ code might fail!") handle_subifd = f'TRY(subifd_handler(value[0].get<{tiff_type_to_cpp(TIFFType.IFD)}>()));' output = fR""" case {tag.id}: // {tag.name} dbgln_if(TIFF_DEBUG, "{tag.name}({{}}): {{}}", name_for_enum_tag_value(type), format_tiff_value(tag, value)); {pre_condition} {check_value} {handle_subifd} metadata.add_entry("{tag.name}"sv, move(value)); break; """ return output def generate_tag_handler_file(tags: List[Tag]) -> str: formatter_for_tag_with_enum = '\n'.join([fR""" case {tag.id}: return MUST(String::from_utf8( name_for_enum_tag_value(static_cast<{tag.associated_enum.export_name()}>(v.get()))));""" for tag in tags if tag.associated_enum]) ensure_tags_are_present = '\n'.join([fR""" if (!metadata.{pascal_case_to_snake_case(tag.name)}().has_value()) return Error::from_string_literal("Unable to decode image, missing required tag {tag.name}."); """ for tag in filter(lambda tag: tag.is_required, known_tags)]) tiff_type_from_u16_cases = '\n'.join([fR""" case to_underlying(Type::{t.name}): return Type::{t.name};""" for t in TIFFType]) size_of_tiff_type_cases = '\n'.join([fR""" case Type::{t.name}: return {t.size};""" for t in TIFFType]) output = fR"""{LICENSE} #include #include #include namespace Gfx::TIFF {{ static String value_formatter(u32 tag_id, Value const& v) {{ switch (tag_id) {{ {formatter_for_tag_with_enum} default: return MUST(String::formatted("{{}}", v)); }} }} [[maybe_unused]] static String format_tiff_value(u32 tag_id, Vector const& values) {{ if (values.size() == 1) return MUST(String::formatted("{{}}", value_formatter(tag_id, values[0]))); StringBuilder builder; builder.append('['); for (u32 i = 0; i < values.size(); ++i) {{ builder.appendff("{{}}", value_formatter(tag_id, values[i])); if (i != values.size() - 1) builder.append(", "sv); }} builder.append(']'); return MUST(builder.to_string()); }} {ENSURE_BASELINE_TAG_PRESENCE} {{ {ensure_tags_are_present} return {{}}; }} {TIFF_TYPE_FROM_U16} {{ switch (type) {{ {tiff_type_from_u16_cases} default: return Error::from_string_literal("TIFFImageDecoderPlugin: Unknown type"); }} }} {SIZE_OF_TIFF_TYPE} {{ switch (type) {{ {size_of_tiff_type_cases} default: VERIFY_NOT_REACHED(); }} }} {HANDLE_TAG_SIGNATURE} {{ switch (tag) {{ """ output += '\n'.join([generate_tag_handler(t) for t in tags]) output += R""" default: dbgln_if(TIFF_DEBUG, "UnknownTag({}, {}): {}", tag, name_for_enum_tag_value(type), format_tiff_value(tag, value)); } 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()