mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-09-30 08:41:15 +00:00
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:
parent
e72f59cd23
commit
b17fb76ace
Notes:
sideshowbarker
2024-07-17 16:50:22 +09:00
Author: https://github.com/gmta Commit: https://github.com/SerenityOS/serenity/commit/b17fb76ace Pull-request: https://github.com/SerenityOS/serenity/pull/13235
|
@ -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; }
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(); }
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue