mirror of
https://github.com/servo/servo.git
synced 2025-08-03 20:50:07 +01:00
fonts: Respect emoji variation selector when selecting fonts (#32493)
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>
This commit is contained in:
parent
79cd87a3c3
commit
57b64d8123
9 changed files with 87 additions and 28 deletions
|
@ -32,7 +32,7 @@ use crate::platform::font::{FontTable, PlatformFont};
|
||||||
pub use crate::platform::font_list::fallback_font_families;
|
pub use crate::platform::font_list::fallback_font_families;
|
||||||
use crate::text::glyph::{ByteIndex, GlyphData, GlyphId, GlyphStore};
|
use crate::text::glyph::{ByteIndex, GlyphData, GlyphId, GlyphStore};
|
||||||
use crate::text::shaping::ShaperMethods;
|
use crate::text::shaping::ShaperMethods;
|
||||||
use crate::text::{FallbackFontSelectionOptions, Shaper};
|
use crate::text::{EmojiPresentationPreference, FallbackFontSelectionOptions, Shaper};
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! ot_tag {
|
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
|
/// 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.
|
/// to the consumer of this font to properly use this reference.
|
||||||
pub synthesized_small_caps: Option<FontRef>,
|
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 {
|
impl malloc_size_of::MallocSizeOf for Font {
|
||||||
|
@ -250,6 +255,7 @@ impl Font {
|
||||||
cached_shape_data: Default::default(),
|
cached_shape_data: Default::default(),
|
||||||
font_key: FontInstanceKey::default(),
|
font_key: FontInstanceKey::default(),
|
||||||
synthesized_small_caps,
|
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 {
|
pub fn webrender_font_instance_flags(&self) -> FontInstanceFlags {
|
||||||
self.handle.webrender_font_instance_flags()
|
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! {
|
bitflags! {
|
||||||
|
@ -503,25 +517,45 @@ impl FontGroup {
|
||||||
Some(font)
|
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 =
|
let char_in_template =
|
||||||
|template: FontTemplateRef| template.char_in_unicode_range(options.character);
|
|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);
|
return font_or_synthesized_small_caps(font);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ref last_matching_fallback) = self.last_matching_fallback {
|
if let Some(ref last_matching_fallback) = self.last_matching_fallback {
|
||||||
if char_in_template(last_matching_fallback.template.clone()) &&
|
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());
|
return font_or_synthesized_small_caps(last_matching_fallback.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(font) =
|
if let Some(font) = self.find_fallback(
|
||||||
self.find_fallback(font_context, options, char_in_template, glyph_in_font)
|
font_context,
|
||||||
{
|
options,
|
||||||
|
char_in_template,
|
||||||
|
font_has_glyph_and_presentation,
|
||||||
|
) {
|
||||||
self.last_matching_fallback = Some(font.clone());
|
self.last_matching_fallback = Some(font.clone());
|
||||||
return font_or_synthesized_small_caps(font);
|
return font_or_synthesized_small_caps(font);
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,8 +89,8 @@ impl FontTemplateDescriptor {
|
||||||
// a mismatch between the desired and actual glyph presentation (emoji vs text)
|
// a mismatch between the desired and actual glyph presentation (emoji vs text)
|
||||||
// will take precedence over any of the style attributes.
|
// will take precedence over any of the style attributes.
|
||||||
//
|
//
|
||||||
// TODO: Take into account Unicode presentation preferences here, in order to properly
|
// Also relevant for font selection is the emoji presentation preference, but this
|
||||||
// choose a font for emoji clusters that start with non-emoji characters.
|
// is handled later when filtering fonts based on the glyphs they contain.
|
||||||
const STRETCH_FACTOR: f32 = 1.0e8;
|
const STRETCH_FACTOR: f32 = 1.0e8;
|
||||||
const STYLE_FACTOR: f32 = 1.0e4;
|
const STYLE_FACTOR: f32 = 1.0e4;
|
||||||
const WEIGHT_FACTOR: f32 = 1.0e0;
|
const WEIGHT_FACTOR: f32 = 1.0e0;
|
||||||
|
|
|
@ -35,7 +35,7 @@ use super::c_str_to_string;
|
||||||
use crate::font::map_platform_values_to_style_values;
|
use crate::font::map_platform_values_to_style_values;
|
||||||
use crate::font_template::{FontTemplate, FontTemplateDescriptor};
|
use crate::font_template::{FontTemplate, FontTemplateDescriptor};
|
||||||
use crate::platform::add_noto_fallback_families;
|
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.
|
/// An identifier for a local font on systems using Freetype.
|
||||||
#[derive(Clone, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize)]
|
#[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
|
// Based on gfxPlatformGtk::GetCommonFallbackFonts() in Gecko
|
||||||
pub fn fallback_font_families(options: FallbackFontSelectionOptions) -> Vec<&'static str> {
|
pub fn fallback_font_families(options: FallbackFontSelectionOptions) -> Vec<&'static str> {
|
||||||
let mut families = Vec::new();
|
let mut families = Vec::new();
|
||||||
if options.prefer_emoji_presentation {
|
if options.presentation_preference == EmojiPresentationPreference::Emoji {
|
||||||
families.push("Noto Color Emoji");
|
families.push("Noto Color Emoji");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ use webrender_api::NativeFontHandle;
|
||||||
use crate::font_template::{FontTemplate, FontTemplateDescriptor};
|
use crate::font_template::{FontTemplate, FontTemplateDescriptor};
|
||||||
use crate::platform::add_noto_fallback_families;
|
use crate::platform::add_noto_fallback_families;
|
||||||
use crate::platform::font::CoreTextFontTraitsMapping;
|
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
|
/// 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
|
/// 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<String> {
|
||||||
/// <https://searchfox.org/mozilla-central/source/gfx/thebes/gfxPlatformMac.cpp>.
|
/// <https://searchfox.org/mozilla-central/source/gfx/thebes/gfxPlatformMac.cpp>.
|
||||||
pub fn fallback_font_families(options: FallbackFontSelectionOptions) -> Vec<&'static str> {
|
pub fn fallback_font_families(options: FallbackFontSelectionOptions) -> Vec<&'static str> {
|
||||||
let mut families = Vec::new();
|
let mut families = Vec::new();
|
||||||
if options.prefer_emoji_presentation {
|
if options.presentation_preference == EmojiPresentationPreference::Emoji {
|
||||||
families.push("Apple Color Emoji");
|
families.push("Apple Color Emoji");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ use style::values::computed::{FontStyle as StyleFontStyle, FontWeight as StyleFo
|
||||||
use style::values::specified::font::FontStretchKeyword;
|
use style::values::specified::font::FontStretchKeyword;
|
||||||
|
|
||||||
use crate::font_template::{FontTemplate, FontTemplateDescriptor};
|
use crate::font_template::{FontTemplate, FontTemplateDescriptor};
|
||||||
use crate::text::FallbackFontSelectionOptions;
|
use crate::text::{EmojiPresentationPreference, FallbackFontSelectionOptions};
|
||||||
|
|
||||||
pub static SANS_SERIF_FONT_FAMILY: &str = "Arial";
|
pub static SANS_SERIF_FONT_FAMILY: &str = "Arial";
|
||||||
|
|
||||||
|
@ -92,8 +92,7 @@ where
|
||||||
// Based on gfxWindowsPlatform::GetCommonFallbackFonts() in Gecko
|
// Based on gfxWindowsPlatform::GetCommonFallbackFonts() in Gecko
|
||||||
pub fn fallback_font_families(options: FallbackFontSelectionOptions) -> Vec<&'static str> {
|
pub fn fallback_font_families(options: FallbackFontSelectionOptions) -> Vec<&'static str> {
|
||||||
let mut families = Vec::new();
|
let mut families = Vec::new();
|
||||||
|
if options.presentation_preference == EmojiPresentationPreference::Emoji {
|
||||||
if options.prefer_emoji_presentation {
|
|
||||||
families.push("Segoe UI Emoji");
|
families.push("Segoe UI Emoji");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* 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/. */
|
* 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 use crate::text::shaping::Shaper;
|
||||||
|
|
||||||
|
@ -10,31 +10,59 @@ pub mod glyph;
|
||||||
pub mod shaping;
|
pub mod shaping;
|
||||||
pub mod util;
|
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)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub struct FallbackFontSelectionOptions {
|
pub struct FallbackFontSelectionOptions {
|
||||||
pub character: char,
|
pub character: char,
|
||||||
pub prefer_emoji_presentation: bool,
|
pub presentation_preference: EmojiPresentationPreference,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for FallbackFontSelectionOptions {
|
impl Default for FallbackFontSelectionOptions {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
character: ' ',
|
character: ' ',
|
||||||
prefer_emoji_presentation: false,
|
presentation_preference: EmojiPresentationPreference::None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FallbackFontSelectionOptions {
|
impl FallbackFontSelectionOptions {
|
||||||
pub fn new(character: char, next_character: Option<char>) -> Self {
|
pub fn new(character: char, next_character: Option<char>) -> Self {
|
||||||
let prefer_emoji_presentation = match next_character {
|
let presentation_preference = match next_character {
|
||||||
Some(next_character) if emoji::is_emoji_presentation_selector(next_character) => true,
|
Some(next_character) if emoji::is_emoji_presentation_selector(next_character) => {
|
||||||
Some(next_character) if emoji::is_text_presentation_selector(next_character) => false,
|
EmojiPresentationPreference::Emoji
|
||||||
_ => character.is_emoji_char(),
|
},
|
||||||
|
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 {
|
Self {
|
||||||
character,
|
character,
|
||||||
prefer_emoji_presentation,
|
presentation_preference,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
[font-variant-emoji-1.html]
|
|
||||||
expected: FAIL
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
[font-variant-emoji-2.html]
|
||||||
|
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
||||||
[text-transform-capitalize-026.html]
|
|
||||||
expected: FAIL
|
|
Loading…
Add table
Add a link
Reference in a new issue