From d38a3ca9eba03226cae843bf2d25c8a9b990c232 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Thu, 16 Mar 2023 21:00:24 +0100 Subject: [PATCH] LibGfx/OpenType: Add some initial support for GPOS glyph positioning This patch parses enough of GPOS tables to be able to support the kerning information embedded in Inter. Since that specific font only applies positioning offsets to the first glyph in each pair, I was able to get away with not changing our API. Once we start adding support for more sophisticated positioning, we'll need to be able to communicate more than a simple "kerning offset" to the clients of this code. --- AK/Debug.h.in | 4 + Meta/CMake/all_the_debug_macros.cmake | 1 + .../Libraries/LibGfx/Font/OpenType/Font.cpp | 190 +++++++++++++++++- .../Libraries/LibGfx/Font/OpenType/Font.h | 5 +- .../Libraries/LibGfx/Font/OpenType/Tables.h | 98 +++++++++ 5 files changed, 293 insertions(+), 5 deletions(-) diff --git a/AK/Debug.h.in b/AK/Debug.h.in index 5428534534c..79a8e95c321 100644 --- a/AK/Debug.h.in +++ b/AK/Debug.h.in @@ -334,6 +334,10 @@ # cmakedefine01 OCCLUSIONS_DEBUG #endif +#ifndef OPENTYPE_GPOS_DEBUG +# cmakedefine01 OPENTYPE_GPOS_DEBUG +#endif + #ifndef HTML_PARSER_DEBUG # cmakedefine01 HTML_PARSER_DEBUG #endif diff --git a/Meta/CMake/all_the_debug_macros.cmake b/Meta/CMake/all_the_debug_macros.cmake index c1a17b84751..3d8b05cd171 100644 --- a/Meta/CMake/all_the_debug_macros.cmake +++ b/Meta/CMake/all_the_debug_macros.cmake @@ -135,6 +135,7 @@ set(NT_DEBUG ON) set(NVME_DEBUG ON) set(OCCLUSIONS_DEBUG ON) set(OFFD_DEBUG ON) +set(OPENTYPE_GPOS_DEBUG ON) set(PAGE_FAULT_DEBUG ON) set(HTML_PARSER_DEBUG ON) set(PATA_DEBUG ON) diff --git a/Userland/Libraries/LibGfx/Font/OpenType/Font.cpp b/Userland/Libraries/LibGfx/Font/OpenType/Font.cpp index ac36cbfa6b2..e0f27fe5b63 100644 --- a/Userland/Libraries/LibGfx/Font/OpenType/Font.cpp +++ b/Userland/Libraries/LibGfx/Font/OpenType/Font.cpp @@ -8,6 +8,8 @@ #include #include +#include +#include #include #include #include @@ -412,6 +414,7 @@ ErrorOr> Font::try_load_from_offset(ReadonlyBytes buffer, u3 Optional opt_prep = {}; Optional cblc; Optional cbdt; + Optional gpos; auto num_tables = be_u16(buffer.offset_pointer(offset + (u32)Offsets::NumTables)); if (buffer.size() < offset + (u32)Sizes::OffsetTable + num_tables * (u32)Sizes::TableRecord) @@ -460,6 +463,8 @@ ErrorOr> Font::try_load_from_offset(ReadonlyBytes buffer, u3 cblc = TRY(CBLC::from_slice(buffer_here)); } else if (tag == tag_from_str("CBDT")) { cbdt = TRY(CBDT::from_slice(buffer_here)); + } else if (tag == tag_from_str("GPOS")) { + gpos = TRY(GPOS::from_slice(buffer_here)); } } @@ -559,7 +564,8 @@ ErrorOr> Font::try_load_from_offset(ReadonlyBytes buffer, u3 move(fpgm), move(prep), move(cblc), - move(cbdt))); + move(cbdt), + move(gpos))); } Gfx::ScaledFontMetrics Font::metrics([[maybe_unused]] float x_scale, float y_scale) const @@ -667,9 +673,16 @@ Gfx::ScaledGlyphMetrics Font::glyph_metrics(u32 glyph_id, float x_scale, float y float Font::glyphs_horizontal_kerning(u32 left_glyph_id, u32 right_glyph_id, float x_scale) const { - if (!m_kern.has_value()) - return 0.f; - return m_kern->get_glyph_kerning(left_glyph_id, right_glyph_id) * x_scale; + if (m_gpos.has_value()) { + auto kerning = m_gpos->glyph_kerning(left_glyph_id, right_glyph_id); + if (kerning.has_value()) + return kerning.value() * x_scale; + } + + if (m_kern.has_value()) + return m_kern->get_glyph_kerning(left_glyph_id, right_glyph_id) * x_scale; + + return 0.0f; } RefPtr Font::rasterize_glyph(u32 glyph_id, float x_scale, float y_scale, Gfx::GlyphSubpixelOffset subpixel_offset) const @@ -972,4 +985,173 @@ RefPtr Font::color_bitmap(u32 glyph_id) const return nullptr; }); } + +Optional GPOS::glyph_kerning(u16 left_glyph_id, u16 right_glyph_id) const +{ + auto const& header = this->header(); + dbgln_if(OPENTYPE_GPOS_DEBUG, "GPOS header:"); + dbgln_if(OPENTYPE_GPOS_DEBUG, " Version: {}.{}", header.major_version, header.minor_version); + dbgln_if(OPENTYPE_GPOS_DEBUG, " Feature list offset: {}", header.feature_list_offset); + + // FIXME: Make sure everything is bounds-checked appropriately. + + auto feature_list_slice = m_slice.slice(header.feature_list_offset); + if (feature_list_slice.size() < sizeof(FeatureList)) { + dbgln_if(OPENTYPE_GPOS_DEBUG, "GPOS table feature list slice is too small"); + return {}; + } + auto const& feature_list = *bit_cast(feature_list_slice.data()); + + auto lookup_list_slice = m_slice.slice(header.lookup_list_offset); + if (lookup_list_slice.size() < sizeof(LookupList)) { + dbgln_if(OPENTYPE_GPOS_DEBUG, "GPOS table lookup list slice is too small"); + } + auto const& lookup_list = *bit_cast(lookup_list_slice.data()); + + Optional kern_feature_offset; + for (size_t i = 0; i < feature_list.feature_count; ++i) { + auto const& feature_record = feature_list.feature_records[i]; + if (feature_record.feature_tag == tag_from_str("kern")) { + kern_feature_offset = feature_record.feature_offset; + break; + } + } + + if (!kern_feature_offset.has_value()) { + dbgln_if(OPENTYPE_GPOS_DEBUG, "No 'kern' feature found in GPOS table"); + return {}; + } + + auto feature_slice = feature_list_slice.slice(kern_feature_offset.value()); + auto const& feature = *bit_cast(feature_slice.data()); + + dbgln_if(OPENTYPE_GPOS_DEBUG, "Feature:"); + dbgln_if(OPENTYPE_GPOS_DEBUG, " featureParamsOffset: {}", feature.feature_params_offset); + dbgln_if(OPENTYPE_GPOS_DEBUG, " lookupIndexCount: {}", feature.lookup_index_count); + + for (size_t i = 0; i < feature.lookup_index_count; ++i) { + auto lookup_index = feature.lookup_list_indices[i]; + dbgln_if(OPENTYPE_GPOS_DEBUG, "Lookup index: {}", lookup_index); + auto lookup_slice = lookup_list_slice.slice(lookup_list.lookup_offsets[lookup_index]); + auto const& lookup = *bit_cast(lookup_slice.data()); + + dbgln_if(OPENTYPE_GPOS_DEBUG, "Lookup:"); + dbgln_if(OPENTYPE_GPOS_DEBUG, " lookupType: {}", lookup.lookup_type); + dbgln_if(OPENTYPE_GPOS_DEBUG, " lookupFlag: {}", lookup.lookup_flag); + dbgln_if(OPENTYPE_GPOS_DEBUG, " subtableCount: {}", lookup.subtable_count); + + for (size_t j = 0; j < lookup.subtable_count; ++j) { + auto subtable_offset = lookup.subtable_offsets[j]; + auto subtable_slice = lookup_slice.slice(subtable_offset); + + auto const& pair_pos_format = *bit_cast const*>(subtable_slice.data()); + + dbgln_if(OPENTYPE_GPOS_DEBUG, "PairPosFormat{}", pair_pos_format); + + if (pair_pos_format == 1) { + dbgln_if(OPENTYPE_GPOS_DEBUG, "FIXME: Implement PairPosFormat1"); + continue; + } + auto const& pair_pos_format2 = *bit_cast(subtable_slice.data()); + + dbgln_if(OPENTYPE_GPOS_DEBUG, " posFormat: {}", pair_pos_format2.pos_format); + dbgln_if(OPENTYPE_GPOS_DEBUG, " valueFormat1: {}", pair_pos_format2.value_format1); + dbgln_if(OPENTYPE_GPOS_DEBUG, " valueFormat2: {}", pair_pos_format2.value_format2); + dbgln_if(OPENTYPE_GPOS_DEBUG, " class1Count: {}", pair_pos_format2.class1_count); + dbgln_if(OPENTYPE_GPOS_DEBUG, " class2Count: {}", pair_pos_format2.class2_count); + + auto get_class = [&](u16 glyph_id, Offset16 glyph_def_offset) -> Optional { + auto class_def_format_slice = subtable_slice.slice(glyph_def_offset); + + auto const& class_def_format = *bit_cast const*>(class_def_format_slice.data()); + if (class_def_format == 1) { + dbgln_if(OPENTYPE_GPOS_DEBUG, "FIXME: Implement ClassDefFormat1"); + return {}; + } + + auto const& class_def_format2 = *bit_cast(class_def_format_slice.data()); + dbgln_if(OPENTYPE_GPOS_DEBUG, "ClassDefFormat2:"); + dbgln_if(OPENTYPE_GPOS_DEBUG, " classFormat: {}", class_def_format2.class_format); + dbgln_if(OPENTYPE_GPOS_DEBUG, " classRangeCount: {}", class_def_format2.class_range_count); + + for (size_t i = 0; i < class_def_format2.class_range_count; ++i) { + auto const& range = class_def_format2.class_range_records[i]; + if (glyph_id >= range.start_glyph_id && glyph_id <= range.end_glyph_id) { + dbgln_if(OPENTYPE_GPOS_DEBUG, "Found class {} for glyph ID {}", range.class_, glyph_id); + return range.class_; + } + } + + dbgln_if(OPENTYPE_GPOS_DEBUG, "No class found for glyph {}", glyph_id); + return {}; + }; + + auto left_class = get_class(left_glyph_id, pair_pos_format2.class_def1_offset); + auto right_class = get_class(right_glyph_id, pair_pos_format2.class_def2_offset); + + if (!left_class.has_value() || !right_class.has_value()) { + dbgln_if(OPENTYPE_GPOS_DEBUG, "Need glyph class for both sides"); + return {}; + } + + dbgln_if(OPENTYPE_GPOS_DEBUG, "Classes: {}, {}", left_class.value(), right_class.value()); + + size_t value1_size = popcount(static_cast(pair_pos_format2.value_format1 & 0xff)) * sizeof(u16); + size_t value2_size = popcount(static_cast(pair_pos_format2.value_format2 & 0xff)) * sizeof(u16); + dbgln_if(OPENTYPE_GPOS_DEBUG, "ValueSizes: {}, {}", value1_size, value2_size); + size_t class2_record_size = value1_size + value2_size; + dbgln_if(OPENTYPE_GPOS_DEBUG, "Class2RecordSize: {}", class2_record_size); + size_t class1_record_size = pair_pos_format2.class2_count * class2_record_size; + dbgln_if(OPENTYPE_GPOS_DEBUG, "Class1RecordSize: {}", class1_record_size); + size_t item_offset = (left_class.value() * class1_record_size) + (right_class.value() * class2_record_size); + dbgln_if(OPENTYPE_GPOS_DEBUG, "Item offset: {}", item_offset); + + auto item_slice = subtable_slice.slice(sizeof(PairPosFormat2) + item_offset); + FixedMemoryStream stream(item_slice); + + struct ValueRecord { + i16 x_placement = 0; + i16 y_placement = 0; + i16 x_advance = 0; + i16 y_advance = 0; + i16 x_placement_device = 0; + i16 y_placement_device = 0; + i16 x_advance_device = 0; + i16 y_advance_device = 0; + }; + + auto read_value_record = [&](u16 value_format) -> ValueRecord { + ValueRecord value_record; + if (value_format & static_cast(ValueFormat::X_PLACEMENT)) + value_record.x_placement = stream.read_value>().release_value_but_fixme_should_propagate_errors(); + if (value_format & static_cast(ValueFormat::Y_PLACEMENT)) + value_record.y_placement = stream.read_value>().release_value_but_fixme_should_propagate_errors(); + if (value_format & static_cast(ValueFormat::X_ADVANCE)) + value_record.x_advance = stream.read_value>().release_value_but_fixme_should_propagate_errors(); + if (value_format & static_cast(ValueFormat::Y_ADVANCE)) + value_record.y_advance = stream.read_value>().release_value_but_fixme_should_propagate_errors(); + if (value_format & static_cast(ValueFormat::X_PLACEMENT_DEVICE)) + value_record.x_placement_device = stream.read_value>().release_value_but_fixme_should_propagate_errors(); + if (value_format & static_cast(ValueFormat::Y_PLACEMENT_DEVICE)) + value_record.y_placement_device = stream.read_value>().release_value_but_fixme_should_propagate_errors(); + if (value_format & static_cast(ValueFormat::X_ADVANCE_DEVICE)) + value_record.x_advance_device = stream.read_value>().release_value_but_fixme_should_propagate_errors(); + if (value_format & static_cast(ValueFormat::Y_ADVANCE_DEVICE)) + value_record.y_advance_device = stream.read_value>().release_value_but_fixme_should_propagate_errors(); + return value_record; + }; + + [[maybe_unused]] auto value_record1 = read_value_record(pair_pos_format2.value_format1); + [[maybe_unused]] auto value_record2 = read_value_record(pair_pos_format2.value_format2); + + dbgln_if(OPENTYPE_GPOS_DEBUG, "Returning x advance {}", value_record1.x_advance); + return value_record1.x_advance; + } + } + + (void)left_glyph_id; + (void)right_glyph_id; + return {}; +} + } diff --git a/Userland/Libraries/LibGfx/Font/OpenType/Font.h b/Userland/Libraries/LibGfx/Font/OpenType/Font.h index b542fc4d3e0..f38d77ac441 100644 --- a/Userland/Libraries/LibGfx/Font/OpenType/Font.h +++ b/Userland/Libraries/LibGfx/Font/OpenType/Font.h @@ -86,7 +86,8 @@ private: Optional fpgm, Optional prep, Optional cblc, - Optional cbdt) + Optional cbdt, + Optional gpos) : m_buffer(move(bytes)) , m_head(move(head)) , m_name(move(name)) @@ -102,6 +103,7 @@ private: , m_prep(move(prep)) , m_cblc(move(cblc)) , m_cbdt(move(cbdt)) + , m_gpos(move(gpos)) { } @@ -124,6 +126,7 @@ private: Optional m_prep; Optional m_cblc; Optional m_cbdt; + Optional m_gpos; // This cache stores information per code point. // It's segmented into pages with data about 256 code points each. diff --git a/Userland/Libraries/LibGfx/Font/OpenType/Tables.h b/Userland/Libraries/LibGfx/Font/OpenType/Tables.h index 8cc69fc7bcd..bd44387fc00 100644 --- a/Userland/Libraries/LibGfx/Font/OpenType/Tables.h +++ b/Userland/Libraries/LibGfx/Font/OpenType/Tables.h @@ -7,6 +7,8 @@ #pragma once +#include "AK/Endian.h" +#include "AK/Forward.h" #include #include #include @@ -533,4 +535,100 @@ private: ReadonlyBytes m_slice; }; +// https://learn.microsoft.com/en-us/typography/opentype/spec/chapter2#feature-list-table +struct FeatureRecord { + Tag feature_tag; + Offset16 feature_offset; +}; + +// https://learn.microsoft.com/en-us/typography/opentype/spec/chapter2#feature-list-table +struct FeatureList { + BigEndian feature_count; + FeatureRecord feature_records[]; +}; + +// https://learn.microsoft.com/en-us/typography/opentype/spec/chapter2#feature-table +struct Feature { + Offset16 feature_params_offset; + BigEndian lookup_index_count; + BigEndian lookup_list_indices[]; +}; + +// https://learn.microsoft.com/en-us/typography/opentype/spec/chapter2#lookup-table +struct Lookup { + BigEndian lookup_type; + BigEndian lookup_flag; + BigEndian subtable_count; + BigEndian subtable_offsets[]; +}; + +// https://learn.microsoft.com/en-us/typography/opentype/spec/chapter2#lookup-list-table +struct LookupList { + BigEndian lookup_count; + Offset16 lookup_offsets[]; +}; + +// https://learn.microsoft.com/en-us/typography/opentype/spec/chapter2#class-definition-table-format-2 +struct ClassRangeRecord { + BigEndian start_glyph_id; + BigEndian end_glyph_id; + BigEndian class_; +}; + +// https://learn.microsoft.com/en-us/typography/opentype/spec/chapter2#class-definition-table-format-2 +struct ClassDefFormat2 { + BigEndian class_format; + BigEndian class_range_count; + ClassRangeRecord class_range_records[]; +}; + +// https://learn.microsoft.com/en-us/typography/opentype/spec/gpos#gpos-header +class GPOS { +public: + // https://learn.microsoft.com/en-us/typography/opentype/spec/gpos#gpos-header + struct GPOSHeader { + BigEndian major_version; + BigEndian minor_version; + Offset16 script_list_offset; + Offset16 feature_list_offset; + Offset16 lookup_list_offset; + }; + + // https://learn.microsoft.com/en-us/typography/opentype/spec/gpos#pair-adjustment-positioning-format-2-class-pair-adjustment + struct PairPosFormat2 { + BigEndian pos_format; + Offset16 coverage_offset; + BigEndian value_format1; + BigEndian value_format2; + Offset16 class_def1_offset; + Offset16 class_def2_offset; + BigEndian class1_count; + BigEndian class2_count; + }; + + enum class ValueFormat : u16 { + X_PLACEMENT = 0x0001, + Y_PLACEMENT = 0x0002, + X_ADVANCE = 0x0004, + Y_ADVANCE = 0x0008, + X_PLACEMENT_DEVICE = 0x0010, + Y_PLACEMENT_DEVICE = 0x0020, + X_ADVANCE_DEVICE = 0x0040, + Y_ADVANCE_DEVICE = 0x0080, + }; + + GPOSHeader const& header() const { return *bit_cast(m_slice.data()); } + + Optional glyph_kerning(u16 left_glyph_id, u16 right_glyph_id) const; + + static ErrorOr from_slice(ReadonlyBytes slice) { return GPOS { slice }; } + +private: + GPOS(ReadonlyBytes slice) + : m_slice(slice) + { + } + + ReadonlyBytes m_slice; +}; }