Просмотр исходного кода

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.
Jelle Raaijmakers 3 лет назад
Родитель
Сommit
b17fb76ace

+ 1 - 0
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; }

+ 1 - 0
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;

+ 8 - 0
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;
     }
 }
 

+ 166 - 3
Userland/Libraries/LibGfx/TrueTypeFont/Font.cpp

@@ -1,6 +1,7 @@
 /*
  * Copyright (c) 2020, Srimanta Barua <srimanta.barua1@gmail.com>
  * Copyright (c) 2021, Andreas Kling <kling@serenityos.org>
+ * Copyright (c) 2022, Jelle Raaijmakers <jelle@gmta.nl>
  *
  * SPDX-License-Identifier: BSD-2-Clause
  */
@@ -167,6 +168,137 @@ Optional<Name> Name::from_slice(ReadonlyBytes slice)
     return Name(slice);
 }
 
+ErrorOr<Kern> 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<size_t>::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<i16> 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<i16> 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<NonnullRefPtr<Font>> Font::try_load_from_offset(ReadonlyBytes buffer, u3
     Optional<ReadonlyBytes> opt_loca_slice = {};
     Optional<ReadonlyBytes> opt_glyf_slice = {};
     Optional<ReadonlyBytes> opt_os2_slice = {};
+    Optional<ReadonlyBytes> opt_kern_slice = {};
 
     Optional<Head> opt_head = {};
     Optional<Name> opt_name = {};
@@ -283,6 +416,7 @@ ErrorOr<NonnullRefPtr<Font>> Font::try_load_from_offset(ReadonlyBytes buffer, u3
     Optional<Cmap> opt_cmap = {};
     Optional<Loca> opt_loca = {};
     Optional<OS2> opt_os2 = {};
+    Optional<Kern> 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<NonnullRefPtr<Font>> 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<NonnullRefPtr<Font>> 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> 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<NonnullRefPtr<Font>> 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<Gfx::Bitmap> 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;

+ 6 - 2
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<Gfx::Bitmap> 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<NonnullRefPtr<Font>> 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>&& 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<Kern> 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(); }

+ 26 - 0
Userland/Libraries/LibGfx/TrueTypeFont/Tables.h

@@ -1,11 +1,14 @@
 /*
  * Copyright (c) 2020, Srimanta Barua <srimanta.barua1@gmail.com>
+ * Copyright (c) 2022, Jelle Raaijmakers <jelle@gmta.nl>
  *
  * SPDX-License-Identifier: BSD-2-Clause
  */
 
 #pragma once
 
+#include <AK/Error.h>
+#include <AK/FixedArray.h>
 #include <AK/Span.h>
 #include <AK/String.h>
 
@@ -201,4 +204,27 @@ private:
     ReadonlyBytes m_slice;
 };
 
+class Kern {
+public:
+    static ErrorOr<Kern> 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<size_t> subtable_offsets)
+        : m_slice(slice)
+        , m_subtable_offsets(move(subtable_offsets))
+    {
+    }
+
+    static Optional<i16> read_glyph_kerning_format0(ReadonlyBytes slice, u16 left_glyph_id, u16 right_glyph_id);
+
+    ReadonlyBytes m_slice;
+    FixedArray<size_t> m_subtable_offsets;
+};
+
 }