LibGfx+LibWeb: Support per-glyph font fallbacks in canvas text painting

Instead of using the first font from the FontCascadeList for all glyphs
in a text, we perform a text shaping process that finds a suitable font
for each glyph and returns a list of glyph runs, where each glyph run
represents consecutive glyphs using the same font.
This commit is contained in:
Aliaksandr Kalenik 2025-04-19 21:36:26 +02:00 committed by Andreas Kling
commit f6b0851c38
Notes: github-actions[bot] 2025-04-21 07:52:18 +00:00
3 changed files with 49 additions and 4 deletions

View file

@ -1,6 +1,7 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2021, sin-ack <sin-ack@protonmail.com>
* Copyright (c) 2024-2025, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -12,6 +13,45 @@
namespace Gfx {
Vector<NonnullRefPtr<GlyphRun>> shape_text(FloatPoint baseline_start, Utf8View string, FontCascadeList const& font_cascade_list)
{
if (string.length() == 0)
return {};
Vector<NonnullRefPtr<GlyphRun>> runs;
auto it = string.begin();
auto substring_begin_offset = string.iterator_offset(it);
Font const* last_font = &font_cascade_list.font_for_code_point(*it);
FloatPoint last_position = baseline_start;
auto add_run = [&runs, &last_position](Utf8View string, Font const& font) {
auto run = shape_text(last_position, 0, string, font, GlyphRun::TextType::Common, {});
last_position.translate_by(run->width(), 0);
runs.append(*run);
};
while (it != string.end()) {
auto code_point = *it;
auto const* font = &font_cascade_list.font_for_code_point(code_point);
if (font != last_font) {
auto substring = string.substring_view(substring_begin_offset, string.iterator_offset(it) - substring_begin_offset);
add_run(substring, *last_font);
last_font = font;
substring_begin_offset = string.iterator_offset(it);
}
++it;
}
auto end_offset = string.iterator_offset(it);
if (substring_begin_offset < end_offset) {
auto substring = string.substring_view(substring_begin_offset, end_offset - substring_begin_offset);
add_run(substring, *last_font);
}
return runs;
}
RefPtr<GlyphRun> shape_text(FloatPoint baseline_start, float letter_spacing, Utf8View string, Gfx::Font const& font, GlyphRun::TextType text_type, ShapeFeatures const& features)
{
static hb_buffer_t* buffer = hb_buffer_create();

View file

@ -1,6 +1,7 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2021, sin-ack <sin-ack@protonmail.com>
* Copyright (c) 2024-2025, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -12,6 +13,7 @@
#include <AK/Utf8View.h>
#include <AK/Vector.h>
#include <LibGfx/Font/Font.h>
#include <LibGfx/FontCascadeList.h>
#include <LibGfx/Forward.h>
#include <LibGfx/Point.h>
@ -69,6 +71,7 @@ private:
};
RefPtr<GlyphRun> shape_text(FloatPoint baseline_start, float letter_spacing, Utf8View string, Gfx::Font const& font, GlyphRun::TextType, ShapeFeatures const& features);
Vector<NonnullRefPtr<GlyphRun>> shape_text(FloatPoint baseline_start, Utf8View string, FontCascadeList const&);
float measure_text_width(Utf8View const& string, Gfx::Font const& font, ShapeFeatures const& features);
}

View file

@ -232,11 +232,13 @@ Gfx::Path CanvasRenderingContext2D::text_path(StringView text, float x, float y,
auto& drawing_state = this->drawing_state();
auto const& font = font_cascade_list()->first();
auto const& font_cascade_list = this->font_cascade_list();
auto const& font = font_cascade_list->first();
auto glyph_runs = Gfx::shape_text({ x, y }, Utf8View(text), *font_cascade_list);
Gfx::Path path;
auto glyph_run = Gfx::shape_text({ x, y }, 0, Utf8View(text), font, Gfx::GlyphRun::TextType::Ltr, {});
path.glyph_run(*glyph_run);
for (auto const& glyph_run : glyph_runs) {
path.glyph_run(glyph_run);
}
auto text_width = path.bounding_box().width();
Gfx::AffineTransform transform = {};