From b17fb76ace658a9a8ad3a8399748af3ecf27e7dc Mon Sep 17 00:00:00 2001 From: Jelle Raaijmakers Date: Thu, 24 Mar 2022 16:20:43 +0100 Subject: [PATCH] LibGfx: Implement TTF kerning tables If a TTF font contains kern tables, we now read through all of them and apply any kerning values to character rendering. --- Userland/Libraries/LibGfx/BitmapFont.h | 1 + Userland/Libraries/LibGfx/Font.h | 1 + Userland/Libraries/LibGfx/Painter.cpp | 8 + .../Libraries/LibGfx/TrueTypeFont/Font.cpp | 169 +++++++++++++++++- Userland/Libraries/LibGfx/TrueTypeFont/Font.h | 8 +- .../Libraries/LibGfx/TrueTypeFont/Tables.h | 26 +++ 6 files changed, 208 insertions(+), 5 deletions(-) diff --git a/Userland/Libraries/LibGfx/BitmapFont.h b/Userland/Libraries/LibGfx/BitmapFont.h index a38f361214..b02222489d 100644 --- a/Userland/Libraries/LibGfx/BitmapFont.h +++ b/Userland/Libraries/LibGfx/BitmapFont.h @@ -54,6 +54,7 @@ public: return m_glyph_width; return glyph_or_emoji_width_for_variable_width_font(code_point); } + i32 glyphs_horizontal_kerning(u32, u32) const override { return 0; } u8 glyph_height() const override { return m_glyph_height; } int x_height() const override { return m_x_height; } int preferred_line_height() const override { return glyph_height() + m_line_gap; } diff --git a/Userland/Libraries/LibGfx/Font.h b/Userland/Libraries/LibGfx/Font.h index 441e9dbad9..8596764533 100644 --- a/Userland/Libraries/LibGfx/Font.h +++ b/Userland/Libraries/LibGfx/Font.h @@ -114,6 +114,7 @@ public: virtual u8 glyph_width(u32 code_point) const = 0; virtual int glyph_or_emoji_width(u32 code_point) const = 0; + virtual i32 glyphs_horizontal_kerning(u32 left_code_point, u32 right_code_point) const = 0; virtual u8 glyph_height() const = 0; virtual int x_height() const = 0; virtual int preferred_line_height() const = 0; diff --git a/Userland/Libraries/LibGfx/Painter.cpp b/Userland/Libraries/LibGfx/Painter.cpp index 132b2c18cc..91e12bd240 100644 --- a/Userland/Libraries/LibGfx/Painter.cpp +++ b/Userland/Libraries/LibGfx/Painter.cpp @@ -1378,12 +1378,19 @@ void draw_text_line(IntRect const& a_rect, Utf8View const& text, Font const& fon space_width = -space_width; // Draw spaces backwards } + u32 last_code_point { 0 }; for (auto it = text.begin(); it != text.end(); ++it) { auto code_point = *it; if (code_point == ' ') { point.translate_by(space_width, 0); + last_code_point = code_point; continue; } + + int kerning = font.glyphs_horizontal_kerning(last_code_point, code_point); + if (kerning != 0) + point.translate_by(direction == TextDirection::LTR ? kerning : -kerning, 0); + IntSize glyph_size(font.glyph_or_emoji_width(code_point) + font.glyph_spacing(), font.glyph_height()); if (direction == TextDirection::RTL) point.translate_by(-glyph_size.width(), 0); // If we are drawing right to left, we have to move backwards before drawing the glyph @@ -1393,6 +1400,7 @@ void draw_text_line(IntRect const& a_rect, Utf8View const& text, Font const& fon // The callback function might have exhausted the iterator. if (it == text.end()) break; + last_code_point = code_point; } } diff --git a/Userland/Libraries/LibGfx/TrueTypeFont/Font.cpp b/Userland/Libraries/LibGfx/TrueTypeFont/Font.cpp index 8cdb37d0dd..80123199a5 100644 --- a/Userland/Libraries/LibGfx/TrueTypeFont/Font.cpp +++ b/Userland/Libraries/LibGfx/TrueTypeFont/Font.cpp @@ -1,6 +1,7 @@ /* * Copyright (c) 2020, Srimanta Barua * Copyright (c) 2021, Andreas Kling + * Copyright (c) 2022, Jelle Raaijmakers * * SPDX-License-Identifier: BSD-2-Clause */ @@ -167,6 +168,137 @@ Optional Name::from_slice(ReadonlyBytes slice) return Name(slice); } +ErrorOr Kern::from_slice(ReadonlyBytes slice) +{ + if (slice.size() < sizeof(u32)) + return Error::from_string_literal("Invalid kern table header"sv); + + // We only support the old (2x u16) version of the header + auto version = be_u16(slice.data()); + auto number_of_subtables = be_u16(slice.offset(sizeof(u16))); + if (version != 0) + return Error::from_string_literal("Unsupported kern table version"sv); + if (number_of_subtables == 0) + return Error::from_string_literal("Kern table does not contain any subtables"sv); + + // Read all subtable offsets + auto subtable_offsets = TRY(FixedArray::try_create(number_of_subtables)); + size_t offset = 2 * sizeof(u16); + for (size_t i = 0; i < number_of_subtables; ++i) { + if (slice.size() < offset + Sizes::SubtableHeader) + return Error::from_string_literal("Invalid kern subtable header"sv); + + subtable_offsets[i] = offset; + auto subtable_size = be_u16(slice.offset(offset + sizeof(u16))); + offset += subtable_size; + } + + return Kern(slice, move(subtable_offsets)); +} + +i16 Kern::get_glyph_kerning(u16 left_glyph_id, u16 right_glyph_id) const +{ + VERIFY(left_glyph_id > 0 && right_glyph_id > 0); + + i16 glyph_kerning = 0; + for (auto subtable_offset : m_subtable_offsets) { + auto subtable_slice = m_slice.slice(subtable_offset); + + auto version = be_u16(subtable_slice.data()); + auto length = be_u16(subtable_slice.offset(sizeof(u16))); + auto coverage = be_u16(subtable_slice.offset(2 * sizeof(u16))); + + if (version != 0) { + dbgln("TTF::Kern: unsupported subtable version {}", version); + continue; + } + + if (subtable_slice.size() < length) { + dbgln("TTF::Kern: subtable has an invalid size {}", length); + continue; + } + + auto is_horizontal = (coverage & (1 << 0)) > 0; + auto is_minimum = (coverage & (1 << 1)) > 0; + auto is_cross_stream = (coverage & (1 << 2)) > 0; + auto is_override = (coverage & (1 << 3)) > 0; + auto reserved_bits = (coverage & 0xF0); + auto format = (coverage & 0xFF00) >> 8; + + // FIXME: implement support for these features + if (!is_horizontal || is_minimum || is_cross_stream || (reserved_bits > 0)) { + dbgln("TTF::Kern: FIXME: implement missing feature support for subtable"); + continue; + } + + // FIXME: implement support for subtable formats other than 0 + Optional subtable_kerning; + switch (format) { + case 0: + subtable_kerning = read_glyph_kerning_format0(subtable_slice.slice(Sizes::SubtableHeader), left_glyph_id, right_glyph_id); + break; + default: + dbgln("TTF::Kern: FIXME: subtable format {} is unsupported", format); + continue; + } + if (!subtable_kerning.has_value()) + continue; + auto kerning_value = subtable_kerning.release_value(); + + if (is_override) + glyph_kerning = kerning_value; + else + glyph_kerning += kerning_value; + } + return glyph_kerning; +} + +Optional Kern::read_glyph_kerning_format0(ReadonlyBytes slice, u16 left_glyph_id, u16 right_glyph_id) +{ + if (slice.size() < 4 * sizeof(u16)) + return {}; + + u16 number_of_pairs = be_u16(slice.data()); + u16 search_range = be_u16(slice.offset_pointer(sizeof(u16))); + u16 entry_selector = be_u16(slice.offset_pointer(2 * sizeof(u16))); + u16 range_shift = be_u16(slice.offset_pointer(3 * sizeof(u16))); + + // Sanity checks for this table format + auto pairs_in_search_range = search_range / Sizes::Format0Entry; + if (number_of_pairs == 0) + return {}; + if (pairs_in_search_range > number_of_pairs) + return {}; + if ((1 << entry_selector) * Sizes::Format0Entry != search_range) + return {}; + if ((number_of_pairs - pairs_in_search_range) * Sizes::Format0Entry != range_shift) + return {}; + + // FIXME: implement a possibly slightly more efficient binary search using the parameters above + auto search_slice = slice.slice(4 * sizeof(u16)); + size_t left_idx = 0; + size_t right_idx = number_of_pairs - 1; + for (auto i = 0; i < 16; ++i) { + size_t pivot_idx = (left_idx + right_idx) / 2; + + u16 pivot_left_glyph_id = be_u16(search_slice.offset(pivot_idx * Sizes::Format0Entry + 0)); + u16 pivot_right_glyph_id = be_u16(search_slice.offset(pivot_idx * Sizes::Format0Entry + 2)); + + // Match + if (pivot_left_glyph_id == left_glyph_id && pivot_right_glyph_id == right_glyph_id) + return be_i16(search_slice.offset(pivot_idx * Sizes::Format0Entry + 4)); + + // Narrow search area + if (pivot_left_glyph_id < left_glyph_id || (pivot_left_glyph_id == left_glyph_id && pivot_right_glyph_id < right_glyph_id)) + left_idx = pivot_idx + 1; + else if (pivot_idx == left_idx) + break; + else + right_idx = pivot_idx - 1; + } + return 0; +} + String Name::string_for_id(NameId id) const { auto num_entries = be_u16(m_slice.offset_pointer(2)); @@ -274,6 +406,7 @@ ErrorOr> Font::try_load_from_offset(ReadonlyBytes buffer, u3 Optional opt_loca_slice = {}; Optional opt_glyf_slice = {}; Optional opt_os2_slice = {}; + Optional opt_kern_slice = {}; Optional opt_head = {}; Optional opt_name = {}; @@ -283,6 +416,7 @@ ErrorOr> Font::try_load_from_offset(ReadonlyBytes buffer, u3 Optional opt_cmap = {}; Optional opt_loca = {}; Optional opt_os2 = {}; + Optional opt_kern = {}; auto num_tables = be_u16(buffer.offset_pointer(offset + (u32)Offsets::NumTables)); if (buffer.size() < offset + (u32)Sizes::OffsetTable + num_tables * (u32)Sizes::TableRecord) @@ -321,6 +455,8 @@ ErrorOr> Font::try_load_from_offset(ReadonlyBytes buffer, u3 opt_glyf_slice = buffer_here; } else if (tag == tag_from_str("OS/2")) { opt_os2_slice = buffer_here; + } else if (tag == tag_from_str("kern")) { + opt_kern_slice = buffer_here; } } @@ -360,6 +496,10 @@ ErrorOr> Font::try_load_from_offset(ReadonlyBytes buffer, u3 return Error::from_string_literal("Could not load OS/2"sv); auto os2 = OS2(opt_os2_slice.value()); + Optional kern {}; + if (opt_kern_slice.has_value()) + kern = TRY(Kern::from_slice(opt_kern_slice.value())); + // Select cmap table. FIXME: Do this better. Right now, just looks for platform "Windows" // and corresponding encoding "Unicode full repertoire", or failing that, "Unicode BMP" for (u32 i = 0; i < cmap.num_subtables(); i++) { @@ -384,7 +524,7 @@ ErrorOr> Font::try_load_from_offset(ReadonlyBytes buffer, u3 } } - return adopt_ref(*new Font(move(buffer), move(head), move(name), move(hhea), move(maxp), move(hmtx), move(cmap), move(loca), move(glyf), move(os2))); + return adopt_ref(*new Font(move(buffer), move(head), move(name), move(hhea), move(maxp), move(hmtx), move(cmap), move(loca), move(glyf), move(os2), move(kern))); } ScaledFontMetrics Font::metrics(float x_scale, float y_scale) const @@ -420,6 +560,13 @@ ScaledGlyphMetrics Font::glyph_metrics(u32 glyph_id, float x_scale, float y_scal }; } +i32 Font::glyphs_horizontal_kerning(u32 left_glyph_id, u32 right_glyph_id, float x_scale) const +{ + if (!m_kern.has_value()) + return 0; + return m_kern->get_glyph_kerning(left_glyph_id, right_glyph_id) * x_scale; +} + // FIXME: "loca" and "glyf" are not available for CFF fonts. RefPtr Font::rasterize_glyph(u32 glyph_id, float x_scale, float y_scale) const { @@ -510,15 +657,18 @@ ALWAYS_INLINE int ScaledFont::unicode_view_width(T const& view) const return 0; int width = 0; int longest_width = 0; + u32 last_code_point = 0; for (auto code_point : view) { if (code_point == '\n' || code_point == '\r') { longest_width = max(width, longest_width); width = 0; + last_code_point = code_point; continue; } u32 glyph_id = glyph_id_for_code_point(code_point); - auto metrics = glyph_metrics(glyph_id); - width += metrics.advance_width; + auto kerning = glyphs_horizontal_kerning(last_code_point, code_point); + width += kerning + glyph_metrics(glyph_id).advance_width; + last_code_point = code_point; } longest_width = max(width, longest_width); return longest_width; @@ -557,6 +707,19 @@ int ScaledFont::glyph_or_emoji_width(u32 code_point) const return metrics.advance_width; } +i32 ScaledFont::glyphs_horizontal_kerning(u32 left_code_point, u32 right_code_point) const +{ + if (left_code_point == 0 || right_code_point == 0) + return 0; + + auto left_glyph_id = glyph_id_for_code_point(left_code_point); + auto right_glyph_id = glyph_id_for_code_point(right_code_point); + if (left_glyph_id == 0 || right_glyph_id == 0) + return 0; + + return m_font->glyphs_horizontal_kerning(left_glyph_id, right_glyph_id, m_x_scale); +} + u8 ScaledFont::glyph_fixed_width() const { return glyph_metrics(glyph_id_for_code_point(' ')).advance_width; diff --git a/Userland/Libraries/LibGfx/TrueTypeFont/Font.h b/Userland/Libraries/LibGfx/TrueTypeFont/Font.h index 657c9d2930..7e77053707 100644 --- a/Userland/Libraries/LibGfx/TrueTypeFont/Font.h +++ b/Userland/Libraries/LibGfx/TrueTypeFont/Font.h @@ -50,6 +50,7 @@ public: ScaledFontMetrics metrics(float x_scale, float y_scale) const; ScaledGlyphMetrics glyph_metrics(u32 glyph_id, float x_scale, float y_scale) const; + i32 glyphs_horizontal_kerning(u32 left_glyph_id, u32 right_glyph_id, float x_scale) const; RefPtr rasterize_glyph(u32 glyph_id, float x_scale, float y_scale) const; u32 glyph_count() const; u16 units_per_em() const; @@ -74,7 +75,7 @@ private: static ErrorOr> try_load_from_offset(ReadonlyBytes, unsigned index = 0); - Font(ReadonlyBytes bytes, Head&& head, Name&& name, Hhea&& hhea, Maxp&& maxp, Hmtx&& hmtx, Cmap&& cmap, Loca&& loca, Glyf&& glyf, OS2&& os2) + Font(ReadonlyBytes bytes, Head&& head, Name&& name, Hhea&& hhea, Maxp&& maxp, Hmtx&& hmtx, Cmap&& cmap, Loca&& loca, Glyf&& glyf, OS2&& os2, Optional&& kern) : m_buffer(move(bytes)) , m_head(move(head)) , m_name(move(name)) @@ -85,6 +86,7 @@ private: , m_glyf(move(glyf)) , m_cmap(move(cmap)) , m_os2(move(os2)) + , m_kern(move(kern)) { } @@ -102,6 +104,7 @@ private: Glyf m_glyf; Cmap m_cmap; OS2 m_os2; + Optional m_kern; }; class ScaledFont : public Gfx::Font { @@ -129,6 +132,7 @@ public: virtual bool contains_glyph(u32 code_point) const override { return m_font->glyph_id_for_code_point(code_point) > 0; } virtual u8 glyph_width(u32 code_point) const override; virtual int glyph_or_emoji_width(u32 code_point) const override; + virtual i32 glyphs_horizontal_kerning(u32 left_code_point, u32 right_code_point) const override; virtual int preferred_line_height() const override { return metrics().height() + metrics().line_gap; } virtual u8 glyph_height() const override { return m_point_height; } virtual int x_height() const override { return m_point_height; } // FIXME: Read from font @@ -142,7 +146,7 @@ public: virtual int width(Utf32View const&) const override; virtual String name() const override { return String::formatted("{} {}", family(), variant()); } virtual bool is_fixed_width() const override { return m_font->is_fixed_width(); } - virtual u8 glyph_spacing() const override { return m_x_scale; } // FIXME: Read from font + virtual u8 glyph_spacing() const override { return 0; } virtual size_t glyph_count() const override { return m_font->glyph_count(); } virtual String family() const override { return m_font->family(); } virtual String variant() const override { return m_font->variant(); } diff --git a/Userland/Libraries/LibGfx/TrueTypeFont/Tables.h b/Userland/Libraries/LibGfx/TrueTypeFont/Tables.h index 182e2493ca..73cbf998f9 100644 --- a/Userland/Libraries/LibGfx/TrueTypeFont/Tables.h +++ b/Userland/Libraries/LibGfx/TrueTypeFont/Tables.h @@ -1,11 +1,14 @@ /* * Copyright (c) 2020, Srimanta Barua + * Copyright (c) 2022, Jelle Raaijmakers * * SPDX-License-Identifier: BSD-2-Clause */ #pragma once +#include +#include #include #include @@ -201,4 +204,27 @@ private: ReadonlyBytes m_slice; }; +class Kern { +public: + static ErrorOr from_slice(ReadonlyBytes); + i16 get_glyph_kerning(u16 left_glyph_id, u16 right_glyph_id) const; + +private: + enum Sizes : size_t { + SubtableHeader = 6, + Format0Entry = 6, + }; + + Kern(ReadonlyBytes slice, FixedArray subtable_offsets) + : m_slice(slice) + , m_subtable_offsets(move(subtable_offsets)) + { + } + + static Optional read_glyph_kerning_format0(ReadonlyBytes slice, u16 left_glyph_id, u16 right_glyph_id); + + ReadonlyBytes m_slice; + FixedArray m_subtable_offsets; +}; + }