1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-05-14 11:34:59 +00:00
serenity/Userland/Libraries/LibGfx/TIFFGenerator.py
Lucas CHOLLET 82d40aab18 LibGfx/TIFF: Don't try to check non-existent values
We were previously only checking the first value, this is wrong for tags
that accept multiple values (e.g. ExtraSamples) and can lead to crashes
on malformed images containing tags with a count of 0.
2023-12-30 23:24:05 +01:00

537 lines
16 KiB
Python
Executable file

#!/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 EnumWithExportName(Enum):
@classmethod
def export_name(cls) -> str:
return cls.__name__
class TIFFType(EnumWithExportName):
@classmethod
def export_name(cls) -> str:
return "Type"
Byte = 1
ASCII = 2
UnsignedShort = 3
UnsignedLong = 4
UnsignedRational = 5
Undefined = 7
SignedLong = 9
SignedRational = 10
Float = 11
Double = 12
UTF8 = 129
class Predictor(EnumWithExportName):
NoPrediction = 1
HorizontalDifferencing = 2
class Compression(EnumWithExportName):
NoCompression = 1
CCITT = 2
Group3Fax = 3
Group4Fax = 4
LZW = 5
JPEG = 6
AdobeDeflate = 8
PackBits = 32773
class PhotometricInterpretation(EnumWithExportName):
WhiteIsZero = 0
BlackIsZero = 1
RGB = 2
RGBPalette = 3
TransparencyMask = 4
CMYK = 5
YCbCr = 6
CIELab = 8
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, "ImageHeight", is_required=True),
Tag('258', [TIFFType.UnsignedShort], [], None, "BitsPerSample", is_required=True),
Tag('259', [TIFFType.UnsignedShort], [1], None, "Compression", Compression, is_required=True),
Tag('262', [TIFFType.UnsignedShort], [1], None, "PhotometricInterpretation",
PhotometricInterpretation, is_required=True),
Tag('273', [TIFFType.UnsignedShort, TIFFType.UnsignedLong], [], None, "StripOffsets", is_required=True),
Tag('274', [TIFFType.UnsignedShort], [1], Orientation.Default, "Orientation", Orientation),
Tag('277', [TIFFType.UnsignedShort], [1], None, "SamplesPerPixel", is_required=True),
Tag('278', [TIFFType.UnsignedShort, TIFFType.UnsignedLong], [1], None, "RowsPerStrip", is_required=True),
Tag('279', [TIFFType.UnsignedShort, TIFFType.UnsignedLong], [], None, "StripByteCounts", is_required=True),
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('296', [TIFFType.UnsignedShort], [1], ResolutionUnit.Inch, "ResolutionUnit", ResolutionUnit),
Tag('339', [TIFFType.UnsignedShort], [], SampleFormat.Unsigned, "SampleFormat", SampleFormat),
Tag('317', [TIFFType.UnsignedShort], [1], Predictor.NoPrediction, "Predictor", Predictor),
Tag('320', [TIFFType.UnsignedShort], [], None, "ColorMap"),
Tag('338', [TIFFType.UnsignedShort], [], None, "ExtraSamples", ExtraSample),
Tag('34675', [TIFFType.Undefined], [], None, "ICCProfile"),
]
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::")
ENSURE_BASELINE_TAG_PRESENCE = "ErrorOr<void> ensure_baseline_tags_presence(Metadata const& metadata)"
LICENSE = R"""/*
* Copyright (c) 2023, Lucas Chollet <lucas.chollet@serenityos.org>
*
* 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<E, {e.export_name()}>) {{
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<Enum E>
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
return t
def tiff_type_to_cpp(t: TIFFType, with_promotion: bool = True) -> 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 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:
return 'u32'
if t == TIFFType.UnsignedRational:
return 'TIFF::Rational<u32>'
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<u8>.
"""
return t in [TIFFType.ASCII, TIFFType.Byte, TIFFType.Undefined, TIFFType.UTF8]
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, 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 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})"
if len(tag.counts) == 1 and tag.counts[0] == 1 or is_container(biggest_type):
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"
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)}
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<ByteBuffer, String, u32, Rational<u32>, i32, Rational<i32>>;
{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};
}}
{generate_metadata_class(tags)}
}}
template<typename T>
struct AK::Formatter<Gfx::TIFF::Rational<T>> : Formatter<FormatString> {{
ErrorOr<void> format(FormatBuilder& builder, Gfx::TIFF::Rational<T> value)
{{
return Formatter<FormatString>::format(builder, "{{}} ({{}}/{{}})"sv,
static_cast<double>(value.numerator) / value.denominator, value.numerator, value.denominator);
}}
}};
template<>
struct AK::Formatter<Gfx::TIFF::Value> : Formatter<FormatString> {{
ErrorOr<void> 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<FormatString>::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<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}
dbgln_if(TIFF_DEBUG, "{tag.name}({{}}): {{}}", name_for_enum_tag_value(type), format_tiff_value(tag, value));
{pre_condition}
{check_value}
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<u32>()))));"""
for tag in tags if tag.associated_enum])
ensure_tag_presence = '\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)])
output = fR"""{LICENSE}
#include <AK/Debug.h>
#include <AK/String.h>
#include <LibGfx/ImageFormats/TIFFMetadata.h>
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<Value> 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_tag_presence}
return {{}};
}}
{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()