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.
This commit is contained in:
Jelle Raaijmakers 2022-03-24 16:20:43 +01:00 committed by Andreas Kling
parent e72f59cd23
commit b17fb76ace
Notes: sideshowbarker 2024-07-17 16:50:22 +09:00
6 changed files with 208 additions and 5 deletions

View file

@ -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; }

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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(); }

View file

@ -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;
};
}