LibGfx+LibWeb: Make emoji font fallback presentation-aware
Previously, font selection ignored the Unicode emoji presentation of a code point. Emoji-capable code points were always resolved through pre-baked color emoji and symbol fonts. Text-default code points which lacked the `Emoji_Presentation` property, were therefore rendered as color emoji rather than text. We now classify each code point's default presentation from its `Emoji_Presentation` property and any trailing variation selector.
This commit is contained in:
parent
bebcf95883
commit
57103da08d
10 changed files with 124 additions and 46 deletions
|
|
@ -50,11 +50,11 @@ RefPtr<Gfx::Font> FontDatabase::get(FlyString const& family, float point_size, u
|
|||
return m_system_font_provider->get_font(family, point_size, weight, width, slope, font_variation_settings, shape_features);
|
||||
}
|
||||
|
||||
RefPtr<Gfx::Font> FontDatabase::get_font_for_code_point(u32 code_point, float point_size, u16 weight, u16 width, u8 slope)
|
||||
RefPtr<Gfx::Font> FontDatabase::get_font_for_code_point(u32 code_point, float point_size, u16 weight, u16 width, u8 slope, bool prefer_color_emoji)
|
||||
{
|
||||
CodePointFallbackKey key { code_point, weight, width, slope };
|
||||
CodePointFallbackKey key { code_point, weight, width, slope, prefer_color_emoji };
|
||||
auto& entry = m_code_point_fallback_cache.ensure(key, [&]() -> CodePointFallbackEntry {
|
||||
auto typeface_or_error = TypefaceSkia::find_typeface_for_code_point(code_point, weight, width, slope);
|
||||
auto typeface_or_error = TypefaceSkia::find_typeface_for_code_point(code_point, weight, width, slope, prefer_color_emoji);
|
||||
if (typeface_or_error.is_error() || !typeface_or_error.value())
|
||||
return { {}, nullptr };
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ struct CodePointFallbackKey {
|
|||
u16 weight { 0 };
|
||||
u16 width { 0 };
|
||||
u8 slope { 0 };
|
||||
bool prefer_color_emoji { false };
|
||||
|
||||
bool operator==(CodePointFallbackKey const&) const = default;
|
||||
|
||||
|
|
@ -28,7 +29,7 @@ struct CodePointFallbackKey {
|
|||
{
|
||||
return pair_int_hash(
|
||||
pair_int_hash(code_point, weight),
|
||||
pair_int_hash(width, slope));
|
||||
pair_int_hash(pair_int_hash(width, slope), prefer_color_emoji));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -47,7 +48,7 @@ public:
|
|||
SystemFontProvider& install_system_font_provider(NonnullOwnPtr<SystemFontProvider>);
|
||||
|
||||
RefPtr<Gfx::Font> get(FlyString const& family, float point_size, unsigned weight, unsigned width, unsigned slope, Optional<FontVariationSettings> const& font_variation_settings = {}, Optional<Gfx::ShapeFeatures> const& shape_features = {});
|
||||
RefPtr<Gfx::Font> get_font_for_code_point(u32 code_point, float point_size, u16 weight, u16 width, u8 slope);
|
||||
RefPtr<Gfx::Font> get_font_for_code_point(u32 code_point, float point_size, u16 weight, u16 width, u8 slope, bool prefer_color_emoji);
|
||||
void for_each_typeface_with_family_name(FlyString const& family_name, Function<void(Typeface const&)>);
|
||||
[[nodiscard]] StringView system_font_provider_name() const;
|
||||
|
||||
|
|
|
|||
|
|
@ -305,12 +305,15 @@ ErrorOr<RefPtr<TypefaceSkia>> TypefaceSkia::match_family_style(StringView family
|
|||
return typeface_from_skia_typeface(move(skia_typeface));
|
||||
}
|
||||
|
||||
ErrorOr<RefPtr<TypefaceSkia>> TypefaceSkia::find_typeface_for_code_point(u32 code_point, u16 weight, u16 width, u8 slope)
|
||||
ErrorOr<RefPtr<TypefaceSkia>> TypefaceSkia::find_typeface_for_code_point(u32 code_point, u16 weight, u16 width, u8 slope, bool prefer_color_emoji)
|
||||
{
|
||||
SkFontStyle style(weight, width, slope_to_skia_slant(slope));
|
||||
|
||||
// The "und-Zsye" language tag steers the font matcher towards a color emoji font. Without it, a text-presentation
|
||||
// font is preferred for emoji-capable code points.
|
||||
char const* emoji_locale[] = { "und-Zsye" };
|
||||
auto skia_typeface = font_manager().matchFamilyStyleCharacter(
|
||||
nullptr, style, nullptr, 0, code_point);
|
||||
nullptr, style, prefer_color_emoji ? emoji_locale : nullptr, prefer_color_emoji ? 1 : 0, code_point);
|
||||
|
||||
if (!skia_typeface)
|
||||
return RefPtr<TypefaceSkia> {};
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ public:
|
|||
static ErrorOr<NonnullRefPtr<TypefaceSkia>> load_from_buffer(ReadonlyBytes, u32 ttc_index = 0);
|
||||
static ErrorOr<RefPtr<TypefaceSkia>> match_system_ui(SystemUIFontKind, float point_size, u16 weight, double width, u8 slope);
|
||||
static ErrorOr<RefPtr<TypefaceSkia>> match_family_style(StringView family_name, u16 weight, u16 width, u8 slope);
|
||||
static ErrorOr<RefPtr<TypefaceSkia>> find_typeface_for_code_point(u32 code_point, u16 weight, u16 width, u8 slope);
|
||||
static ErrorOr<RefPtr<TypefaceSkia>> find_typeface_for_code_point(u32 code_point, u16 weight, u16 width, u8 slope, bool prefer_color_emoji);
|
||||
static Optional<FlyString> resolve_generic_family(StringView family_name, u16 weight, u8 slope);
|
||||
|
||||
RefPtr<TypefaceSkia const> clone_with_variations(Vector<FontVariationAxis> const& axes) const;
|
||||
|
|
|
|||
|
|
@ -5,9 +5,24 @@
|
|||
*/
|
||||
|
||||
#include <LibGfx/FontCascadeList.h>
|
||||
#include <LibUnicode/CharacterTypes.h>
|
||||
|
||||
namespace Gfx {
|
||||
|
||||
EmojiPresentationResult emoji_presentation_for_code_point(u32 code_point, Optional<u32> next_code_point)
|
||||
{
|
||||
// VARIATION SELECTOR-16 (emoji)
|
||||
if (next_code_point == 0xFE0Fu)
|
||||
return { EmojiPresentation::Emoji, ForcedPresentation::Yes };
|
||||
// VARIATION SELECTOR-15 (text)
|
||||
if (next_code_point == 0xFE0Eu)
|
||||
return { EmojiPresentation::Text, ForcedPresentation::Yes };
|
||||
|
||||
if (Unicode::code_point_has_emoji_presentation_property(code_point))
|
||||
return { EmojiPresentation::Emoji, ForcedPresentation::No };
|
||||
return { EmojiPresentation::Text, ForcedPresentation::No };
|
||||
}
|
||||
|
||||
void FontCascadeList::add(NonnullRefPtr<Font const> font)
|
||||
{
|
||||
m_fonts.append({ move(font), {} });
|
||||
|
|
@ -58,7 +73,12 @@ void FontCascadeList::extend(FontCascadeList const& other)
|
|||
m_pending_faces.extend(other.m_pending_faces);
|
||||
}
|
||||
|
||||
Gfx::Font const& FontCascadeList::font_for_code_point(u32 code_point, TriggerPendingLoads trigger_pending_loads) const
|
||||
void FontCascadeList::extend_fallback(FontCascadeList const& other)
|
||||
{
|
||||
m_fallback_fonts.extend(other.m_fonts);
|
||||
}
|
||||
|
||||
Gfx::Font const& FontCascadeList::font_for_code_point(u32 code_point, TriggerPendingLoads trigger_pending_loads, EmojiPresentationResult emoji_presentation) const
|
||||
{
|
||||
// Only the text-shaping paths pass TriggerPendingLoads::Yes. Probes that don't
|
||||
// lead to a glyph being drawn (the U+0020 check used to compute first-available-
|
||||
|
|
@ -80,37 +100,68 @@ Gfx::Font const& FontCascadeList::font_for_code_point(u32 code_point, TriggerPen
|
|||
});
|
||||
}
|
||||
|
||||
if (code_point < m_ascii_cache.size()) {
|
||||
auto use_ascii_cache = code_point < m_ascii_cache.size() && emoji_presentation.presentation == EmojiPresentation::Text && emoji_presentation.forced == ForcedPresentation::No;
|
||||
if (use_ascii_cache) {
|
||||
if (auto const* cached = m_ascii_cache[code_point])
|
||||
return *cached;
|
||||
}
|
||||
|
||||
auto cache_and_return = [&](Font const& font) -> Font const& {
|
||||
if (code_point < m_ascii_cache.size())
|
||||
if (use_ascii_cache)
|
||||
m_ascii_cache[code_point] = &font;
|
||||
return font;
|
||||
};
|
||||
|
||||
for (auto const& entry : m_fonts) {
|
||||
if (entry.range_data.has_value()) {
|
||||
if (!entry.range_data->enclosing_range.contains(code_point))
|
||||
continue;
|
||||
for (auto const& range : entry.range_data->unicode_ranges) {
|
||||
if (range.contains(code_point) && entry.font->contains_glyph(code_point))
|
||||
return cache_and_return(*entry.font);
|
||||
}
|
||||
} else if (entry.font->contains_glyph(code_point)) {
|
||||
return cache_and_return(*entry.font);
|
||||
auto presentation_matches = [wants_emoji = emoji_presentation.presentation == EmojiPresentation::Emoji](Font const& font) {
|
||||
return font.is_emoji_font() == wants_emoji;
|
||||
};
|
||||
|
||||
auto entry_contains_glyph = [code_point](Entry const& entry) {
|
||||
if (!entry.range_data.has_value())
|
||||
return entry.font->contains_glyph(code_point);
|
||||
if (!entry.range_data->enclosing_range.contains(code_point))
|
||||
return false;
|
||||
for (auto const& range : entry.range_data->unicode_ranges) {
|
||||
if (range.contains(code_point) && entry.font->contains_glyph(code_point))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
Font const* author_glyph_match = nullptr;
|
||||
for (auto const& entry : m_fonts) {
|
||||
if (!entry_contains_glyph(entry))
|
||||
continue;
|
||||
if (emoji_presentation.forced == ForcedPresentation::No || presentation_matches(*entry.font))
|
||||
return cache_and_return(*entry.font);
|
||||
if (!author_glyph_match)
|
||||
author_glyph_match = entry.font.ptr();
|
||||
}
|
||||
|
||||
Font const* fallback_glyph_match = nullptr;
|
||||
for (auto const& entry : m_fallback_fonts) {
|
||||
if (!entry_contains_glyph(entry))
|
||||
continue;
|
||||
if (presentation_matches(*entry.font))
|
||||
return cache_and_return(*entry.font);
|
||||
if (!fallback_glyph_match)
|
||||
fallback_glyph_match = entry.font.ptr();
|
||||
}
|
||||
|
||||
if (m_system_font_fallback_callback) {
|
||||
if (auto fallback = m_system_font_fallback_callback(code_point, first())) {
|
||||
m_fonts.append({ fallback.release_nonnull(), {} });
|
||||
return cache_and_return(*m_fonts.last().font);
|
||||
if (auto fallback = m_system_font_fallback_callback(code_point, emoji_presentation.presentation, first())) {
|
||||
if (presentation_matches(*fallback) || (!author_glyph_match && !fallback_glyph_match)) {
|
||||
m_fallback_fonts.append({ fallback.release_nonnull(), {} });
|
||||
return cache_and_return(*m_fallback_fonts.last().font);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (author_glyph_match)
|
||||
return cache_and_return(*author_glyph_match);
|
||||
if (fallback_glyph_match)
|
||||
return cache_and_return(*fallback_glyph_match);
|
||||
|
||||
return cache_and_return(*m_last_resort_font);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,9 +14,26 @@
|
|||
|
||||
namespace Gfx {
|
||||
|
||||
enum class EmojiPresentation : u8 {
|
||||
Text,
|
||||
Emoji,
|
||||
};
|
||||
|
||||
enum class ForcedPresentation : u8 {
|
||||
No,
|
||||
Yes,
|
||||
};
|
||||
|
||||
struct EmojiPresentationResult {
|
||||
EmojiPresentation presentation { EmojiPresentation::Text };
|
||||
ForcedPresentation forced { ForcedPresentation::No };
|
||||
};
|
||||
|
||||
EmojiPresentationResult emoji_presentation_for_code_point(u32 code_point, Optional<u32> next_code_point);
|
||||
|
||||
class FontCascadeList : public RefCounted<FontCascadeList> {
|
||||
public:
|
||||
using SystemFontFallbackCallback = Function<RefPtr<Font const>(u32, Font const&)>;
|
||||
using SystemFontFallbackCallback = Function<RefPtr<Font const>(u32, EmojiPresentation, Font const&)>;
|
||||
|
||||
static NonnullRefPtr<FontCascadeList> create()
|
||||
{
|
||||
|
|
@ -42,6 +59,8 @@ public:
|
|||
|
||||
void extend(FontCascadeList const& other);
|
||||
|
||||
void extend_fallback(FontCascadeList const& other);
|
||||
|
||||
// A pending-face fetch should only be initiated for codepoints that are actually
|
||||
// being shaped into glyph runs. Callers that merely probe the cascade (e.g. the
|
||||
// U+0020 check in "first available font" metrics) pass No so that probing does
|
||||
|
|
@ -50,7 +69,7 @@ public:
|
|||
No,
|
||||
Yes,
|
||||
};
|
||||
Gfx::Font const& font_for_code_point(u32 code_point, TriggerPendingLoads = TriggerPendingLoads::No) const;
|
||||
Gfx::Font const& font_for_code_point(u32 code_point, TriggerPendingLoads = TriggerPendingLoads::No, EmojiPresentationResult = {}) const;
|
||||
|
||||
bool equals(FontCascadeList const& other) const;
|
||||
|
||||
|
|
@ -99,6 +118,7 @@ public:
|
|||
private:
|
||||
RefPtr<Font const> m_last_resort_font;
|
||||
mutable Vector<Entry> m_fonts;
|
||||
mutable Vector<Entry> m_fallback_fonts;
|
||||
mutable Vector<NonnullRefPtr<PendingFace>> m_pending_faces;
|
||||
SystemFontFallbackCallback m_system_font_fallback_callback;
|
||||
|
||||
|
|
|
|||
|
|
@ -655,26 +655,25 @@ NonnullRefPtr<Gfx::FontCascadeList const> FontComputer::compute_font_for_style_v
|
|||
font_list->add(*default_font);
|
||||
}
|
||||
|
||||
// Add emoji and symbol fonts
|
||||
for (auto font_name : Platform::FontPlugin::the().symbol_font_names()) {
|
||||
if (auto other_font_list = find_font(font_name)) {
|
||||
font_list->extend(*other_font_list);
|
||||
}
|
||||
}
|
||||
|
||||
// The default font is already included in the font list, but we explicitly set it
|
||||
// as the last-resort font. This ensures that if none of the specified fonts contain
|
||||
// the requested code point, there is still a font available to provide a fallback glyph.
|
||||
font_list->set_last_resort_font(*default_font);
|
||||
|
||||
if (!Platform::FontPlugin::the().is_layout_test_mode()) {
|
||||
font_list->set_system_font_fallback_callback([](u32 code_point, Gfx::Font const& reference_font) -> RefPtr<Gfx::Font const> {
|
||||
if (Platform::FontPlugin::the().is_layout_test_mode()) {
|
||||
for (auto font_name : Platform::FontPlugin::the().symbol_font_names()) {
|
||||
if (auto other_font_list = find_font(font_name))
|
||||
font_list->extend_fallback(*other_font_list);
|
||||
}
|
||||
} else {
|
||||
font_list->set_system_font_fallback_callback([](u32 code_point, Gfx::EmojiPresentation presentation, Gfx::Font const& reference_font) -> RefPtr<Gfx::Font const> {
|
||||
return Gfx::FontDatabase::the().get_font_for_code_point(
|
||||
code_point,
|
||||
reference_font.point_size(),
|
||||
reference_font.weight(),
|
||||
reference_font.typeface().width(),
|
||||
reference_font.slope());
|
||||
reference_font.slope(),
|
||||
presentation == Gfx::EmojiPresentation::Emoji);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -758,7 +758,7 @@ Gfx::Font const& TextNode::ChunkIterator::font_for_space(size_t at_index, u32 sp
|
|||
for (size_t i = at_index; i < m_view.length_in_code_units();) {
|
||||
auto cp = m_view.code_point_at(i);
|
||||
if (!is_interword_space(cp) && cp != '\t' && cp != '\n') {
|
||||
auto const& font = m_font_cascade_list.font_for_code_point(cp, Gfx::FontCascadeList::TriggerPendingLoads::Yes);
|
||||
auto const& font = m_font_cascade_list.font_for_code_point(cp, Gfx::FontCascadeList::TriggerPendingLoads::Yes, emoji_presentation_at(i, cp));
|
||||
if (!font.is_emoji_font() && has_glyph(font))
|
||||
return font;
|
||||
// Text is coming from an emoji face; we'll fall back to (3).
|
||||
|
|
@ -771,6 +771,16 @@ Gfx::Font const& TextNode::ChunkIterator::font_for_space(size_t at_index, u32 sp
|
|||
return m_font_cascade_list.font_for_code_point(space_code_point, Gfx::FontCascadeList::TriggerPendingLoads::Yes);
|
||||
}
|
||||
|
||||
Gfx::EmojiPresentationResult TextNode::ChunkIterator::emoji_presentation_at(size_t code_unit_offset, u32 code_point) const
|
||||
{
|
||||
auto next_offset = code_unit_offset + AK::UnicodeUtils::code_unit_length_for_code_point(code_point);
|
||||
Optional<u32> next_code_point;
|
||||
if (next_offset < m_view.length_in_code_units())
|
||||
next_code_point = m_view.code_point_at(next_offset);
|
||||
|
||||
return Gfx::emoji_presentation_for_code_point(code_point, next_code_point);
|
||||
}
|
||||
|
||||
Optional<TextNode::Chunk> TextNode::ChunkIterator::next_without_peek()
|
||||
{
|
||||
if (m_current_index >= m_view.length_in_code_units())
|
||||
|
|
@ -795,7 +805,7 @@ Optional<TextNode::Chunk> TextNode::ChunkIterator::next_without_peek()
|
|||
auto const& expected_font_for = [&](u32 cp) -> Gfx::Font const& {
|
||||
return is_interword_space(cp)
|
||||
? font_for_space(m_current_index, cp)
|
||||
: m_font_cascade_list.font_for_code_point(cp, Gfx::FontCascadeList::TriggerPendingLoads::Yes);
|
||||
: m_font_cascade_list.font_for_code_point(cp, Gfx::FontCascadeList::TriggerPendingLoads::Yes, emoji_presentation_at(m_current_index, cp));
|
||||
};
|
||||
|
||||
auto const& font = expected_font_for(current_code_point());
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ public:
|
|||
|
||||
[[nodiscard]] bool is_at_line_break_opportunity() const;
|
||||
[[nodiscard]] Gfx::Font const& font_for_space(size_t at_index, u32 space_code_point) const;
|
||||
[[nodiscard]] Gfx::EmojiPresentationResult emoji_presentation_at(size_t code_unit_offset, u32 code_point) const;
|
||||
|
||||
bool const m_should_wrap_lines;
|
||||
bool const m_should_respect_linebreaks;
|
||||
|
|
|
|||
|
|
@ -52,15 +52,8 @@ FontPlugin::FontPlugin(bool is_layout_test_mode, Gfx::SystemFontProvider* font_p
|
|||
m_default_fixed_width_font = Gfx::FontDatabase::the().get(default_fixed_width_font_name, 12.0, 400, Gfx::FontWidth::Normal, 0);
|
||||
VERIFY(m_default_fixed_width_font);
|
||||
|
||||
if (is_layout_test_mode) {
|
||||
if (is_layout_test_mode)
|
||||
m_symbol_font_names = { "Noto Emoji"_fly_string };
|
||||
} else {
|
||||
#ifdef AK_OS_MACOS
|
||||
m_symbol_font_names = { "Apple Color Emoji"_fly_string, "Apple Symbols"_fly_string };
|
||||
#else
|
||||
m_symbol_font_names = { "Noto Color Emoji"_fly_string, "Noto Sans Symbols"_fly_string };
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
FontPlugin::~FontPlugin() = default;
|
||||
|
|
|
|||
Loading…
Reference in a new issue