From 57b64d8123535858f96796602906b4d02c7d4e4a Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Tue, 18 Jun 2024 06:37:47 +0200 Subject: [PATCH] fonts: Respect emoji variation selector when selecting fonts (#32493) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This uses a pretty simple heuristic to select a font likely to contain color emoji. In the future Servo should actually check if the font also contains a color representation of the character in question. For now the code assumes that when a font supports color glyphs of some kind and supports the character in question at all, it supports the color version. This fixes support for rendering keycap emoji clusters such as 1️⃣ . Signed-off-by: Martin Robinson Co-authored-by: Rakhi Sharma Co-authored-by: Mukilan Thiyagarajan --- components/gfx/font.rs | 48 ++++++++++++++++--- components/gfx/font_template.rs | 4 +- components/gfx/platform/freetype/font_list.rs | 4 +- components/gfx/platform/macos/font_list.rs | 4 +- components/gfx/platform/windows/font_list.rs | 5 +- components/gfx/text/mod.rs | 44 +++++++++++++---- .../css-fonts/font-variant-emoji-1.html.ini | 2 - .../css-fonts/font-variant-emoji-2.html.ini | 2 + .../text-transform-capitalize-026.html.ini | 2 - 9 files changed, 87 insertions(+), 28 deletions(-) delete mode 100644 tests/wpt/meta/css/css-fonts/font-variant-emoji-1.html.ini create mode 100644 tests/wpt/meta/css/css-fonts/font-variant-emoji-2.html.ini delete mode 100644 tests/wpt/meta/css/css-text/text-transform/text-transform-capitalize-026.html.ini diff --git a/components/gfx/font.rs b/components/gfx/font.rs index 89553b80990..ef93af1170e 100644 --- a/components/gfx/font.rs +++ b/components/gfx/font.rs @@ -32,7 +32,7 @@ use crate::platform::font::{FontTable, PlatformFont}; pub use crate::platform::font_list::fallback_font_families; use crate::text::glyph::{ByteIndex, GlyphData, GlyphId, GlyphStore}; use crate::text::shaping::ShaperMethods; -use crate::text::{FallbackFontSelectionOptions, Shaper}; +use crate::text::{EmojiPresentationPreference, FallbackFontSelectionOptions, Shaper}; #[macro_export] macro_rules! ot_tag { @@ -219,6 +219,11 @@ pub struct Font { /// the version of the font used to replace lowercase ASCII letters. It's up /// to the consumer of this font to properly use this reference. pub synthesized_small_caps: Option, + + /// Whether or not this font supports color bitmaps or a COLR table. This is + /// essentially equivalent to whether or not we use it for emoji presentation. + /// This is cached, because getting table data is expensive. + has_color_bitmap_or_colr_table: OnceLock, } impl malloc_size_of::MallocSizeOf for Font { @@ -250,6 +255,7 @@ impl Font { cached_shape_data: Default::default(), font_key: FontInstanceKey::default(), synthesized_small_caps, + has_color_bitmap_or_colr_table: OnceLock::new(), }) } @@ -261,6 +267,14 @@ impl Font { pub fn webrender_font_instance_flags(&self) -> FontInstanceFlags { self.handle.webrender_font_instance_flags() } + + pub fn has_color_bitmap_or_colr_table(&self) -> bool { + *self.has_color_bitmap_or_colr_table.get_or_init(|| { + self.table_for_tag(SBIX).is_some() || + self.table_for_tag(CBDT).is_some() || + self.table_for_tag(COLR).is_some() + }) + } } bitflags! { @@ -503,25 +517,45 @@ impl FontGroup { Some(font) }; - let glyph_in_font = |font: &FontRef| font.has_glyph_for(options.character); + let font_has_glyph_and_presentation = |font: &FontRef| { + // Do not select this font if it goes against our emoji preference. + match options.presentation_preference { + EmojiPresentationPreference::Text if font.has_color_bitmap_or_colr_table() => { + return false + }, + EmojiPresentationPreference::Emoji if !font.has_color_bitmap_or_colr_table() => { + return false + }, + _ => {}, + } + font.has_glyph_for(options.character) + }; + let char_in_template = |template: FontTemplateRef| template.char_in_unicode_range(options.character); - if let Some(font) = self.find(font_context, char_in_template, glyph_in_font) { + if let Some(font) = self.find( + font_context, + char_in_template, + font_has_glyph_and_presentation, + ) { return font_or_synthesized_small_caps(font); } if let Some(ref last_matching_fallback) = self.last_matching_fallback { if char_in_template(last_matching_fallback.template.clone()) && - glyph_in_font(last_matching_fallback) + font_has_glyph_and_presentation(last_matching_fallback) { return font_or_synthesized_small_caps(last_matching_fallback.clone()); } } - if let Some(font) = - self.find_fallback(font_context, options, char_in_template, glyph_in_font) - { + if let Some(font) = self.find_fallback( + font_context, + options, + char_in_template, + font_has_glyph_and_presentation, + ) { self.last_matching_fallback = Some(font.clone()); return font_or_synthesized_small_caps(font); } diff --git a/components/gfx/font_template.rs b/components/gfx/font_template.rs index 6c2999aef66..19c71ed83fa 100644 --- a/components/gfx/font_template.rs +++ b/components/gfx/font_template.rs @@ -89,8 +89,8 @@ impl FontTemplateDescriptor { // a mismatch between the desired and actual glyph presentation (emoji vs text) // will take precedence over any of the style attributes. // - // TODO: Take into account Unicode presentation preferences here, in order to properly - // choose a font for emoji clusters that start with non-emoji characters. + // Also relevant for font selection is the emoji presentation preference, but this + // is handled later when filtering fonts based on the glyphs they contain. const STRETCH_FACTOR: f32 = 1.0e8; const STYLE_FACTOR: f32 = 1.0e4; const WEIGHT_FACTOR: f32 = 1.0e0; diff --git a/components/gfx/platform/freetype/font_list.rs b/components/gfx/platform/freetype/font_list.rs index 7fa0187a0cc..73d17ccd49e 100644 --- a/components/gfx/platform/freetype/font_list.rs +++ b/components/gfx/platform/freetype/font_list.rs @@ -35,7 +35,7 @@ use super::c_str_to_string; use crate::font::map_platform_values_to_style_values; use crate::font_template::{FontTemplate, FontTemplateDescriptor}; use crate::platform::add_noto_fallback_families; -use crate::text::FallbackFontSelectionOptions; +use crate::text::{EmojiPresentationPreference, FallbackFontSelectionOptions}; /// An identifier for a local font on systems using Freetype. #[derive(Clone, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize)] @@ -204,7 +204,7 @@ pub static SANS_SERIF_FONT_FAMILY: &str = "DejaVu Sans"; // Based on gfxPlatformGtk::GetCommonFallbackFonts() in Gecko pub fn fallback_font_families(options: FallbackFontSelectionOptions) -> Vec<&'static str> { let mut families = Vec::new(); - if options.prefer_emoji_presentation { + if options.presentation_preference == EmojiPresentationPreference::Emoji { families.push("Noto Color Emoji"); } diff --git a/components/gfx/platform/macos/font_list.rs b/components/gfx/platform/macos/font_list.rs index b590058e14b..c1d9621f61e 100644 --- a/components/gfx/platform/macos/font_list.rs +++ b/components/gfx/platform/macos/font_list.rs @@ -17,7 +17,7 @@ use webrender_api::NativeFontHandle; use crate::font_template::{FontTemplate, FontTemplateDescriptor}; use crate::platform::add_noto_fallback_families; use crate::platform::font::CoreTextFontTraitsMapping; -use crate::text::FallbackFontSelectionOptions; +use crate::text::{EmojiPresentationPreference, FallbackFontSelectionOptions}; /// An identifier for a local font on a MacOS system. These values comes from the CoreText /// CTFontCollection. Note that `path` here is required. We do not load fonts that do not @@ -97,7 +97,7 @@ pub fn system_default_family(_generic_name: &str) -> Option { /// . pub fn fallback_font_families(options: FallbackFontSelectionOptions) -> Vec<&'static str> { let mut families = Vec::new(); - if options.prefer_emoji_presentation { + if options.presentation_preference == EmojiPresentationPreference::Emoji { families.push("Apple Color Emoji"); } diff --git a/components/gfx/platform/windows/font_list.rs b/components/gfx/platform/windows/font_list.rs index 9f3b29cff03..9ac7011e270 100644 --- a/components/gfx/platform/windows/font_list.rs +++ b/components/gfx/platform/windows/font_list.rs @@ -13,7 +13,7 @@ use style::values::computed::{FontStyle as StyleFontStyle, FontWeight as StyleFo use style::values::specified::font::FontStretchKeyword; use crate::font_template::{FontTemplate, FontTemplateDescriptor}; -use crate::text::FallbackFontSelectionOptions; +use crate::text::{EmojiPresentationPreference, FallbackFontSelectionOptions}; pub static SANS_SERIF_FONT_FAMILY: &str = "Arial"; @@ -92,8 +92,7 @@ where // Based on gfxWindowsPlatform::GetCommonFallbackFonts() in Gecko pub fn fallback_font_families(options: FallbackFontSelectionOptions) -> Vec<&'static str> { let mut families = Vec::new(); - - if options.prefer_emoji_presentation { + if options.presentation_preference == EmojiPresentationPreference::Emoji { families.push("Segoe UI Emoji"); } diff --git a/components/gfx/text/mod.rs b/components/gfx/text/mod.rs index ae61aab1031..9a44f743c7a 100644 --- a/components/gfx/text/mod.rs +++ b/components/gfx/text/mod.rs @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use unicode_properties::{emoji, UnicodeEmoji}; +use unicode_properties::{emoji, EmojiStatus, UnicodeEmoji}; pub use crate::text::shaping::Shaper; @@ -10,31 +10,59 @@ pub mod glyph; pub mod shaping; pub mod util; +/// Whether or not font fallback selection prefers the emoji or text representation +/// of a character. If `None` then either presentation is acceptable. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum EmojiPresentationPreference { + None, + Text, + Emoji, +} + #[derive(Clone, Copy, Debug)] pub struct FallbackFontSelectionOptions { pub character: char, - pub prefer_emoji_presentation: bool, + pub presentation_preference: EmojiPresentationPreference, } impl Default for FallbackFontSelectionOptions { fn default() -> Self { Self { character: ' ', - prefer_emoji_presentation: false, + presentation_preference: EmojiPresentationPreference::None, } } } impl FallbackFontSelectionOptions { pub fn new(character: char, next_character: Option) -> Self { - let prefer_emoji_presentation = match next_character { - Some(next_character) if emoji::is_emoji_presentation_selector(next_character) => true, - Some(next_character) if emoji::is_text_presentation_selector(next_character) => false, - _ => character.is_emoji_char(), + let presentation_preference = match next_character { + Some(next_character) if emoji::is_emoji_presentation_selector(next_character) => { + EmojiPresentationPreference::Emoji + }, + Some(next_character) if emoji::is_text_presentation_selector(next_character) => { + EmojiPresentationPreference::Text + }, + // We don't want to select emoji prsentation for any possible character that might be an emoji, because + // that includes characters such as '0' that are also used outside of emoji clusters. Instead, only + // select the emoji font for characters that explicitly have an emoji presentation (in the absence + // of the emoji presentation selectors above). + _ if matches!( + character.emoji_status(), + EmojiStatus::EmojiPresentation | + EmojiStatus::EmojiPresentationAndModifierBase | + EmojiStatus::EmojiPresentationAndEmojiComponent | + EmojiStatus::EmojiPresentationAndModifierAndEmojiComponent + ) => + { + EmojiPresentationPreference::Emoji + }, + _ if character.is_emoji_char() => EmojiPresentationPreference::Text, + _ => EmojiPresentationPreference::None, }; Self { character, - prefer_emoji_presentation, + presentation_preference, } } } diff --git a/tests/wpt/meta/css/css-fonts/font-variant-emoji-1.html.ini b/tests/wpt/meta/css/css-fonts/font-variant-emoji-1.html.ini deleted file mode 100644 index 48f039e7548..00000000000 --- a/tests/wpt/meta/css/css-fonts/font-variant-emoji-1.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[font-variant-emoji-1.html] - expected: FAIL diff --git a/tests/wpt/meta/css/css-fonts/font-variant-emoji-2.html.ini b/tests/wpt/meta/css/css-fonts/font-variant-emoji-2.html.ini new file mode 100644 index 00000000000..68aae843f87 --- /dev/null +++ b/tests/wpt/meta/css/css-fonts/font-variant-emoji-2.html.ini @@ -0,0 +1,2 @@ +[font-variant-emoji-2.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-text/text-transform/text-transform-capitalize-026.html.ini b/tests/wpt/meta/css/css-text/text-transform/text-transform-capitalize-026.html.ini deleted file mode 100644 index 6e52a555638..00000000000 --- a/tests/wpt/meta/css/css-text/text-transform/text-transform-capitalize-026.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[text-transform-capitalize-026.html] - expected: FAIL