Skip to content

Commit

Permalink
fonts: Respect emoji variation selector when selecting fonts (#32493)
Browse files Browse the repository at this point in the history
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 <mrobinson@igalia.com>
Co-authored-by: Rakhi Sharma <atbrakhi@igalia.com>
Co-authored-by: Mukilan Thiyagarajan <mukilan@igalia.com>
  • Loading branch information
3 people committed Jun 18, 2024
1 parent 79cd87a commit 57b64d8
Show file tree
Hide file tree
Showing 9 changed files with 87 additions and 28 deletions.
48 changes: 41 additions & 7 deletions components/gfx/font.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<FontRef>,

/// 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<bool>,
}

impl malloc_size_of::MallocSizeOf for Font {
Expand Down Expand Up @@ -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(),
})
}

Expand All @@ -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! {
Expand Down Expand Up @@ -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);
}
Expand Down
4 changes: 2 additions & 2 deletions components/gfx/font_template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions components/gfx/platform/freetype/font_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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");
}

Expand Down
4 changes: 2 additions & 2 deletions components/gfx/platform/macos/font_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -97,7 +97,7 @@ pub fn system_default_family(_generic_name: &str) -> Option<String> {
/// <https://searchfox.org/mozilla-central/source/gfx/thebes/gfxPlatformMac.cpp>.
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");
}

Expand Down
5 changes: 2 additions & 3 deletions components/gfx/platform/windows/font_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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");
}

Expand Down
44 changes: 36 additions & 8 deletions components/gfx/text/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,67 @@
* 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;

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<char>) -> 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,
}
}
}
2 changes: 0 additions & 2 deletions tests/wpt/meta/css/css-fonts/font-variant-emoji-1.html.ini

This file was deleted.

2 changes: 2 additions & 0 deletions tests/wpt/meta/css/css-fonts/font-variant-emoji-2.html.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[font-variant-emoji-2.html]
expected: FAIL

This file was deleted.

0 comments on commit 57b64d8

Please sign in to comment.