Files
moli/moli-layout/src/text.rs

1117 lines
38 KiB
Rust

// SPDX-License-Identifier: MIT OR Apache-2.0
//
// The Parley shaping/projection path is narrowly adapted from DioxusLabs/blitz
// commit d788124ab881f9bb537cb452ec1d837604a374a8:
// - packages/blitz-dom/src/node/text.rs
// - packages/blitz-paint/src/text.rs
use std::{borrow::Cow, collections::BTreeMap, sync::Arc};
use parley::{
FontContext, FontFamily, FontFamilyName, LayoutContext, TextStyle,
fontique::{
Attributes, Blob, Collection, CollectionOptions, FontInfoOverride, FontStyle, FontWeight,
FontWidth, QueryFamily, QueryStatus,
},
};
use thiserror::Error;
use crate::stylo_to_parley::TextBrush;
use crate::system_fonts::SystemFontFamilyResolver;
pub(crate) struct ParleyDocumentServices {
pub(crate) font_context: FontContext,
pub(crate) layout_context: LayoutContext<TextBrush>,
system_font_family_resolver: Option<SystemFontFamilyResolver>,
web_font_families: BTreeMap<String, SegmentedWebFontFamily>,
inline_font_metrics_cache: Vec<(
TextStyle<'static, 'static, TextBrush>,
Option<InlineFontMetrics>,
)>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
struct WebFontCapabilities {
width: FontWidth,
style: FontStyle,
weight: FontWeight,
}
#[derive(Clone, Debug)]
struct SegmentedWebFontFace {
internal_family_name: String,
unicode_ranges: Vec<WebFontUnicodeRange>,
}
impl SegmentedWebFontFace {
fn contains(&self, character: char) -> bool {
self.unicode_ranges.is_empty()
|| self
.unicode_ranges
.iter()
.any(|range| range.contains(character))
}
}
#[derive(Clone, Debug)]
struct SegmentedWebFontCapabilityGroup {
capabilities: WebFontCapabilities,
selector_font_identities: Vec<(u64, u32)>,
faces: Vec<SegmentedWebFontFace>,
}
#[derive(Clone, Debug, Default)]
struct SegmentedWebFontFamily {
groups: Vec<SegmentedWebFontCapabilityGroup>,
}
/// Primary-font metrics attached to one resolved Parley text style.
///
/// A shaped run may use a fallback font for its glyphs. CSSOM text geometry,
/// like Blink's `FragmentItem`, instead uses the primary font metrics of the
/// style that owns the run.
#[derive(Clone, Copy, Debug)]
pub(crate) struct InlineFontMetrics {
pub(crate) ascent: f32,
pub(crate) descent: f32,
pub(crate) line_height: f32,
pub(crate) x_height: f32,
}
fn resolved_inline_x_height(ascent: f32, x_height: Option<f32>) -> f32 {
x_height.unwrap_or(ascent * 0.56).max(0.0)
}
impl ParleyDocumentServices {
fn clear_inline_font_metrics_cache(&mut self) {
self.inline_font_metrics_cache.clear();
}
/// Resolves CSS downloadable-font families into the selected segmented
/// face, then applies explicit platform-family substitutions.
///
/// Blink first chooses one font-capability group (width/style/weight),
/// then considers only that group's faces whose `unicode-range` contains
/// the character. Fontique has no segmented-face abstraction, so each
/// physical face is registered under an internal family and this method
/// preserves that capability-before-range behavior before shaping.
pub(crate) fn resolve_font_families(
&mut self,
style: &mut TextStyle<'static, 'static, TextBrush>,
character: Option<char>,
) {
self.resolve_segmented_web_font_families(style, character);
let Some(resolver) = self.system_font_family_resolver.as_mut() else {
return;
};
resolver.resolve_text_style(&mut self.font_context.collection, style);
}
pub(crate) fn requires_character_font_resolution(
&self,
style: &TextStyle<'static, 'static, TextBrush>,
) -> bool {
let family_uses_ranges = |family: &FontFamilyName<'_>| {
let FontFamilyName::Named(name) = family else {
return false;
};
self.web_font_families
.get(&normalized_web_font_family_name(name))
.is_some_and(|family| {
family.groups.iter().any(|group| {
group
.faces
.iter()
.any(|face| !face.unicode_ranges.is_empty())
})
})
};
match &style.font_family {
FontFamily::Single(family) => family_uses_ranges(family),
FontFamily::List(families) => families.iter().any(family_uses_ranges),
FontFamily::Source(source) => FontFamilyName::parse_css_list(source)
.filter_map(Result::ok)
.any(|family| family_uses_ranges(&family)),
}
}
fn resolve_segmented_web_font_families(
&mut self,
style: &mut TextStyle<'static, 'static, TextBrush>,
character: Option<char>,
) {
let parsed_source;
let families = match &style.font_family {
FontFamily::Single(family) => vec![family.clone()],
FontFamily::List(families) => families.iter().cloned().collect(),
FontFamily::Source(source) => {
parsed_source = FontFamilyName::parse_css_list(source)
.filter_map(Result::ok)
.map(FontFamilyName::into_owned)
.collect::<Vec<_>>();
parsed_source
}
};
let attributes = Attributes::new(style.font_width, style.font_style, style.font_weight);
let mut resolved = Vec::with_capacity(families.len());
for family in families {
let FontFamilyName::Named(name) = &family else {
resolved.push(family);
continue;
};
let family_key = normalized_web_font_family_name(name);
if !self.web_font_families.contains_key(&family_key) {
resolved.push(family);
continue;
}
let selected_identity = {
let FontContext {
collection,
source_cache,
} = &mut self.font_context;
let mut query = collection.query(source_cache);
query.set_families([QueryFamily::Named(name)]);
query.set_attributes(attributes);
let mut identity = None;
query.matches_with(|font| {
identity = Some((font.blob.id(), font.index));
QueryStatus::Stop
});
identity
};
let Some(selected_identity) = selected_identity else {
resolved.push(family);
continue;
};
let Some(group) = self.web_font_families[&family_key]
.groups
.iter()
.find(|group| group.selector_font_identities.contains(&selected_identity))
else {
resolved.push(family);
continue;
};
let default_uses_latin_metrics =
character.is_none() && group.faces.iter().any(|face| face.contains('x'));
resolved.extend(
group
.faces
.iter()
.rev()
.filter(|face| match character {
Some(character) => face.contains(character),
None if default_uses_latin_metrics => face.contains('x'),
None => true,
})
.map(|face| {
FontFamilyName::Named(Cow::Owned(face.internal_family_name.clone()))
}),
);
}
style.font_family = FontFamily::List(Cow::Owned(resolved));
}
pub(crate) fn inline_font_metrics(
&mut self,
style: &TextStyle<'static, 'static, TextBrush>,
sample: Option<char>,
) -> Option<InlineFontMetrics> {
if let Some((_, metrics)) = self
.inline_font_metrics_cache
.iter()
.find(|(cached, _)| cached == style)
{
// A font such as Baidu's icon font may not contain `x`. A later
// call carrying one of the style's real characters must be able
// to retry a previous sample-free miss.
if metrics.is_some() || sample.is_none() {
return *metrics;
}
}
// Shape a character from the selected primary face rather than
// borrowing metrics from an arbitrary fallback run. Most fonts cover
// `x`; icon fonts often do not, so the owning text contributes one
// additional candidate and the font identity verifies the result.
let primary_font = self.primary_font_identity(style);
let metrics = ['x'].into_iter().chain(sample).find_map(|candidate| {
let candidate = candidate.to_string();
let mut builder = self.layout_context.style_run_builder(
&mut self.font_context,
&candidate,
1.0,
true,
);
let style_index = builder.push_style(style.clone());
builder.push_style_run(style_index, ..);
let mut layout = builder.build(&candidate);
layout.break_all_lines(None);
let run = layout.lines().next()?.runs().next()?;
if primary_font
.is_some_and(|identity| identity != (run.font().data.id(), run.font().index))
{
return None;
}
let metrics = *run.metrics();
Some(InlineFontMetrics {
ascent: metrics.ascent,
descent: metrics.descent,
line_height: metrics.line_height,
x_height: resolved_inline_x_height(metrics.ascent, metrics.x_height),
})
});
if let Some((_, cached)) = self
.inline_font_metrics_cache
.iter_mut()
.find(|(cached, _)| cached == style)
{
*cached = metrics;
} else {
self.inline_font_metrics_cache
.push((style.clone(), metrics));
}
metrics
}
fn primary_font_identity(
&mut self,
style: &TextStyle<'static, 'static, TextBrush>,
) -> Option<(u64, u32)> {
let parsed_source;
let families = match &style.font_family {
FontFamily::Single(family) => vec![family],
FontFamily::List(families) => families.iter().collect(),
FontFamily::Source(source) => {
parsed_source = FontFamilyName::parse_css_list(source)
.filter_map(Result::ok)
.collect::<Vec<_>>();
parsed_source.iter().collect()
}
};
let query_families = families.iter().map(|family| match family {
FontFamilyName::Named(name) => QueryFamily::Named(name),
FontFamilyName::Generic(family) => QueryFamily::Generic(*family),
});
let FontContext {
collection,
source_cache,
} = &mut self.font_context;
let mut query = collection.query(source_cache);
query.set_families(query_families);
query.set_attributes(Attributes::new(
style.font_width,
style.font_style,
style.font_weight,
));
let mut identity = None;
query.matches_with(|font| {
identity = Some((font.blob.id(), font.index));
QueryStatus::Stop
});
identity
}
}
/// Whether a document's font collection may discover platform fonts.
///
/// Tests and deterministic differential runners disable this and register a
/// fixed web font set. Product documents enable it by default so CSS generic
/// families still have platform fallback when no downloadable font covers a
/// character.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum SystemFontPolicy {
/// Discover fonts through Fontique's platform backend.
#[default]
Enabled,
/// Restrict shaping to explicitly registered web fonts.
Disabled,
}
impl SystemFontPolicy {
const fn is_enabled(self) -> bool {
matches!(self, Self::Enabled)
}
}
/// CSS `font-style` metadata attached to one downloadable font face.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum WebFontStyle {
Normal,
Italic,
Oblique(Option<f32>),
}
/// One inclusive CSS `unicode-range` interval attached to a downloadable
/// font face.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct WebFontUnicodeRange {
start: u32,
end: u32,
}
impl WebFontUnicodeRange {
pub const fn new(start: u32, end: u32) -> Self {
Self { start, end }
}
pub const fn start(self) -> u32 {
self.start
}
pub const fn end(self) -> u32 {
self.end
}
pub const fn contains(self, character: char) -> bool {
let codepoint = character as u32;
self.start <= codepoint && codepoint <= self.end
}
const fn is_valid(self) -> bool {
self.start <= self.end && self.end <= char::MAX as u32
}
}
impl WebFontStyle {
fn to_fontique(self) -> FontStyle {
match self {
Self::Normal => FontStyle::Normal,
Self::Italic => FontStyle::Italic,
Self::Oblique(angle) => FontStyle::Oblique(angle),
}
}
}
/// Fontique metadata overrides derived from a CSS `@font-face` rule.
///
/// `weight` uses the CSS numeric range and `stretch` is a percentage where
/// `100.0` is normal width. Missing descriptors leave the font's own metadata
/// intact.
#[derive(Clone, Debug, PartialEq)]
pub struct WebFontFace {
family_name: String,
weight: Option<f32>,
stretch: Option<f32>,
style: Option<WebFontStyle>,
unicode_ranges: Vec<WebFontUnicodeRange>,
}
impl WebFontFace {
pub fn new(family_name: impl Into<String>) -> Self {
Self {
family_name: family_name.into(),
weight: None,
stretch: None,
style: None,
unicode_ranges: Vec::new(),
}
}
pub fn with_weight(mut self, weight: f32) -> Self {
self.weight = Some(weight);
self
}
pub fn with_stretch(mut self, percentage: f32) -> Self {
self.stretch = Some(percentage);
self
}
pub fn with_style(mut self, style: WebFontStyle) -> Self {
self.style = Some(style);
self
}
/// Sets the face's CSS `unicode-range` intervals. An empty list means the
/// full Unicode range, matching the descriptor's initial value.
pub fn with_unicode_ranges(
mut self,
ranges: impl IntoIterator<Item = WebFontUnicodeRange>,
) -> Self {
self.unicode_ranges = ranges.into_iter().collect();
self
}
pub fn family_name(&self) -> &str {
&self.family_name
}
pub const fn weight(&self) -> Option<f32> {
self.weight
}
pub const fn stretch(&self) -> Option<f32> {
self.stretch
}
pub const fn style(&self) -> Option<WebFontStyle> {
self.style
}
pub fn unicode_ranges(&self) -> &[WebFontUnicodeRange] {
&self.unicode_ranges
}
fn validate(&self) -> Result<(), WebFontRegistrationError> {
if self.family_name.trim().is_empty() {
return Err(WebFontRegistrationError::InvalidDescriptor {
detail: "font-family must not be empty".to_owned(),
});
}
if self
.weight
.is_some_and(|value| !value.is_finite() || !(1.0..=1000.0).contains(&value))
{
return Err(WebFontRegistrationError::InvalidDescriptor {
detail: "font-weight must be a finite value from 1 through 1000".to_owned(),
});
}
if self
.stretch
.is_some_and(|value| !value.is_finite() || value <= 0.0)
{
return Err(WebFontRegistrationError::InvalidDescriptor {
detail: "font-stretch must be a positive finite percentage".to_owned(),
});
}
if self.style.is_some_and(
|style| matches!(style, WebFontStyle::Oblique(Some(angle)) if !angle.is_finite()),
) {
return Err(WebFontRegistrationError::InvalidDescriptor {
detail: "font-style oblique angle must be finite".to_owned(),
});
}
if self.unicode_ranges.iter().any(|range| !range.is_valid()) {
return Err(WebFontRegistrationError::InvalidDescriptor {
detail: "unicode-range intervals must be ordered Unicode scalar bounds".to_owned(),
});
}
Ok(())
}
fn fontique_override_for_family<'a>(&self, family_name: &'a str) -> FontInfoOverride<'a> {
FontInfoOverride {
family_name: Some(family_name),
width: self.stretch.map(FontWidth::from_percentage),
style: self.style.map(WebFontStyle::to_fontique),
weight: self.weight.map(FontWeight::new),
axes: None,
}
}
}
/// One owner-validated downloadable font response.
///
/// `slot` is chosen by the stylesheet/resource owner. Reusing it replaces the
/// old face atomically after the new payload has decoded and validated.
#[derive(Clone, Debug, PartialEq)]
pub struct WebFontRegistration {
slot: String,
face: WebFontFace,
bytes: Vec<u8>,
}
impl WebFontRegistration {
pub fn new(slot: impl Into<String>, face: WebFontFace, bytes: Vec<u8>) -> Self {
Self {
slot: slot.into(),
face,
bytes,
}
}
pub fn slot(&self) -> &str {
&self.slot
}
pub fn face(&self) -> &WebFontFace {
&self.face
}
pub fn bytes(&self) -> &[u8] {
&self.bytes
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WebFontRegistrationOutcome {
Added,
Replaced,
Unchanged,
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
pub enum WebFontRegistrationError {
#[error("web font slot must not be empty")]
EmptySlot,
#[error("invalid web font descriptor: {detail}")]
InvalidDescriptor { detail: String },
#[error("failed to decode {format} web font")]
DecodeFailed { format: &'static str },
#[error("web font payload contains no supported OpenType font")]
UnsupportedPayload,
}
#[derive(Clone, Debug, PartialEq)]
struct RegisteredWebFont {
face: WebFontFace,
sfnt_bytes: Arc<[u8]>,
}
/// Lazily initialized text resources reused by successive layout demands for
/// one committed Document.
///
/// The renderer owns this sidecar. A one-shot [`LayoutWorld`] only borrows the
/// contexts while building pass-local Parley layouts; neither context escapes
/// in [`crate::PaintSnapshot`].
pub struct DocumentLayoutServices {
// FontContext and LayoutContext are both large. Keep them off the stack so
// embedding this sidecar in ScriptVm does not inflate every VM frame.
parley: Option<Box<ParleyDocumentServices>>,
system_font_policy: SystemFontPolicy,
web_fonts: BTreeMap<String, RegisteredWebFont>,
pub(crate) text_layout_passes: u64,
}
impl Default for DocumentLayoutServices {
fn default() -> Self {
Self::new()
}
}
impl DocumentLayoutServices {
/// Creates an uninitialized document sidecar.
pub const fn new() -> Self {
Self {
parley: None,
system_font_policy: SystemFontPolicy::Enabled,
web_fonts: BTreeMap::new(),
text_layout_passes: 0,
}
}
/// Creates a document sidecar with an explicit platform-font policy.
pub const fn with_system_font_policy(system_font_policy: SystemFontPolicy) -> Self {
Self {
parley: None,
system_font_policy,
web_fonts: BTreeMap::new(),
text_layout_passes: 0,
}
}
pub const fn system_font_policy(&self) -> SystemFontPolicy {
self.system_font_policy
}
/// Returns whether a demand with non-empty text has initialized Parley.
pub const fn is_initialized(&self) -> bool {
self.parley.is_some()
}
/// Counts text-bearing one-shot passes served by these reused contexts.
pub const fn text_layout_passes(&self) -> u64 {
self.text_layout_passes
}
pub fn web_font_count(&self) -> usize {
self.web_fonts.len()
}
pub(crate) fn begin_inline_layout_pass(&mut self) {
if let Some(parley) = self.parley.as_deref_mut() {
parley.clear_inline_font_metrics_cache();
}
}
/// Adds or replaces one owner-validated font face.
///
/// This API performs no document/generation check: the resource owner must
/// call it only after matching its stable document, rule/slot, and request
/// identity. Invalid new bytes leave an existing slot untouched.
pub fn register_web_font(
&mut self,
registration: WebFontRegistration,
) -> Result<WebFontRegistrationOutcome, WebFontRegistrationError> {
if registration.slot.trim().is_empty() {
return Err(WebFontRegistrationError::EmptySlot);
}
registration.face.validate()?;
let sfnt_bytes = decode_web_font_bytes(&registration.bytes)?;
validate_registered_font(&registration.face, Arc::clone(&sfnt_bytes))?;
let font = RegisteredWebFont {
face: registration.face,
sfnt_bytes,
};
let outcome = match self.web_fonts.get(&registration.slot) {
Some(current) if current == &font => WebFontRegistrationOutcome::Unchanged,
Some(_) => WebFontRegistrationOutcome::Replaced,
None => WebFontRegistrationOutcome::Added,
};
if outcome == WebFontRegistrationOutcome::Unchanged {
return Ok(outcome);
}
self.web_fonts.insert(registration.slot, font);
if self.parley.is_some() {
self.parley = Some(Box::new(build_parley_services(
self.system_font_policy,
&self.web_fonts,
)));
}
Ok(outcome)
}
/// Removes a font slot. Returns whether a registered face was removed.
pub fn remove_web_font(&mut self, slot: &str) -> bool {
if self.web_fonts.remove(slot).is_none() {
return false;
}
if self.parley.is_some() {
self.parley = Some(Box::new(build_parley_services(
self.system_font_policy,
&self.web_fonts,
)));
}
true
}
pub(crate) fn parley_mut(&mut self) -> &mut ParleyDocumentServices {
if self.parley.is_none() {
self.parley = Some(Box::new(build_parley_services(
self.system_font_policy,
&self.web_fonts,
)));
}
self.parley.as_deref_mut().expect("Parley was initialized")
}
}
fn build_parley_services(
system_font_policy: SystemFontPolicy,
web_fonts: &BTreeMap<String, RegisteredWebFont>,
) -> ParleyDocumentServices {
let mut collection = Collection::new(CollectionOptions {
shared: false,
system_fonts: system_font_policy.is_enabled(),
});
let system_font_family_resolver = system_font_policy
.is_enabled()
.then(|| SystemFontFamilyResolver::new(&mut collection));
let mut font_context = FontContext {
collection,
source_cache: Default::default(),
};
let mut web_font_families = BTreeMap::<String, SegmentedWebFontFamily>::new();
for (source_order, font) in web_fonts.values().enumerate() {
let data: Arc<dyn AsRef<[u8]> + Send + Sync> = Arc::new(Arc::clone(&font.sfnt_bytes));
let blob = Blob::new(data);
let selector_fonts = font_context.collection.register_fonts(
blob.clone(),
Some(
font.face
.fontique_override_for_family(font.face.family_name()),
),
);
if selector_fonts.is_empty() {
continue;
}
let internal_family_name = format!("\0moli-web-font:{source_order}");
if font_context
.collection
.register_fonts(
blob.clone(),
Some(
font.face
.fontique_override_for_family(&internal_family_name),
),
)
.is_empty()
{
continue;
}
let family = web_font_families
.entry(normalized_web_font_family_name(font.face.family_name()))
.or_default();
for (_, fonts) in selector_fonts {
for selector_font in fonts {
let capabilities = WebFontCapabilities {
width: selector_font.width(),
style: selector_font.style(),
weight: selector_font.weight(),
};
let group_index = family
.groups
.iter()
.position(|group| group.capabilities == capabilities)
.unwrap_or_else(|| {
let index = family.groups.len();
family.groups.push(SegmentedWebFontCapabilityGroup {
capabilities,
selector_font_identities: Vec::new(),
faces: Vec::new(),
});
index
});
let group = &mut family.groups[group_index];
group
.selector_font_identities
.push((blob.id(), selector_font.index()));
if !group
.faces
.iter()
.any(|face| face.internal_family_name == internal_family_name)
{
group.faces.push(SegmentedWebFontFace {
internal_family_name: internal_family_name.clone(),
unicode_ranges: font.face.unicode_ranges.clone(),
});
}
}
}
}
ParleyDocumentServices {
font_context,
layout_context: LayoutContext::new(),
system_font_family_resolver,
web_font_families,
inline_font_metrics_cache: Vec::new(),
}
}
fn register_font(font_context: &mut FontContext, font: &RegisteredWebFont) -> bool {
let data: Arc<dyn AsRef<[u8]> + Send + Sync> = Arc::new(Arc::clone(&font.sfnt_bytes));
!font_context
.collection
.register_fonts(
Blob::new(data),
Some(
font.face
.fontique_override_for_family(font.face.family_name()),
),
)
.is_empty()
}
fn normalized_web_font_family_name(name: &str) -> String {
name.chars().flat_map(char::to_lowercase).collect()
}
fn validate_registered_font(
face: &WebFontFace,
sfnt_bytes: Arc<[u8]>,
) -> Result<(), WebFontRegistrationError> {
let mut font_context = FontContext {
collection: Collection::new(CollectionOptions {
shared: false,
system_fonts: false,
}),
source_cache: Default::default(),
};
let font = RegisteredWebFont {
face: face.clone(),
sfnt_bytes,
};
register_font(&mut font_context, &font)
.then_some(())
.ok_or(WebFontRegistrationError::UnsupportedPayload)
}
fn decode_web_font_bytes(bytes: &[u8]) -> Result<Arc<[u8]>, WebFontRegistrationError> {
let decoded = match bytes.get(..4) {
Some(b"wOFF") => wuff::decompress_woff1(bytes)
.map_err(|_| WebFontRegistrationError::DecodeFailed { format: "WOFF" })?,
Some(b"wOF2") => wuff::decompress_woff2(bytes)
.map_err(|_| WebFontRegistrationError::DecodeFailed { format: "WOFF2" })?,
_ => bytes.to_vec(),
};
Ok(Arc::from(decoded))
}
#[cfg(test)]
mod tests {
use super::*;
const TEST_TTF: &[u8] = include_bytes!("../tests/fixtures/moli-ahem.ttf");
const TEST_CJK_TTF: &[u8] = include_bytes!("../tests/fixtures/moli-cjk.ttf");
const TEST_WOFF: &[u8] = include_bytes!("../tests/fixtures/moli-ahem.woff");
const TEST_WOFF2: &[u8] = include_bytes!("../tests/fixtures/moli-ahem.woff2");
#[test]
fn missing_x_height_uses_the_blink_ascent_fallback() {
assert!((resolved_inline_x_height(10.0, None) - 5.6).abs() < f32::EPSILON);
assert_eq!(resolved_inline_x_height(10.0, Some(4.25)), 4.25);
}
#[test]
fn primary_metrics_retry_with_an_owning_character_when_x_is_missing() {
let mut services =
DocumentLayoutServices::with_system_font_policy(SystemFontPolicy::Disabled);
services
.register_web_font(WebFontRegistration::new(
"cjk-face",
WebFontFace::new("Moli CJK"),
TEST_CJK_TTF.to_vec(),
))
.unwrap();
services
.register_web_font(WebFontRegistration::new(
"latin-face",
WebFontFace::new("Moli Latin"),
TEST_TTF.to_vec(),
))
.unwrap();
let style = TextStyle {
font_family: FontFamily::List(std::borrow::Cow::Owned(vec![
FontFamilyName::Named(std::borrow::Cow::Borrowed("Moli CJK")),
FontFamilyName::Named(std::borrow::Cow::Borrowed("Moli Latin")),
])),
..TextStyle::default()
};
let parley = services.parley_mut();
assert!(parley.inline_font_metrics(&style, None).is_none());
assert!(
parley.inline_font_metrics(&style, Some('中')).is_some(),
"the style's actual glyph should recover metrics from a primary font without x"
);
}
fn web_font_style(family: &'static str, weight: f32) -> TextStyle<'static, 'static, TextBrush> {
TextStyle {
font_family: FontFamily::Single(FontFamilyName::Named(Cow::Borrowed(family))),
font_weight: FontWeight::new(weight),
..TextStyle::default()
}
}
fn shape_one_character(
parley: &mut ParleyDocumentServices,
mut style: TextStyle<'static, 'static, TextBrush>,
character: char,
) -> Vec<u8> {
parley.resolve_font_families(&mut style, Some(character));
let text = character.to_string();
let mut builder =
parley
.layout_context
.style_run_builder(&mut parley.font_context, &text, 1.0, true);
let style_index = builder.push_style(style);
builder.push_style_run(style_index, ..);
let mut layout = builder.build(&text);
layout.break_all_lines(None);
layout
.lines()
.next()
.expect("one line")
.runs()
.next()
.expect("one shaped run")
.font()
.data
.as_ref()
.to_vec()
}
#[test]
fn segmented_web_font_faces_select_the_range_for_each_character() {
let mut services =
DocumentLayoutServices::with_system_font_policy(SystemFontPolicy::Disabled);
services
.register_web_font(WebFontRegistration::new(
"latin",
WebFontFace::new("Moli Segmented")
.with_unicode_ranges([WebFontUnicodeRange::new(0x0000, 0x00ff)]),
TEST_TTF.to_vec(),
))
.unwrap();
services
.register_web_font(WebFontRegistration::new(
"cjk",
WebFontFace::new("Moli Segmented")
.with_unicode_ranges([WebFontUnicodeRange::new(0x4e00, 0x9fff)]),
TEST_CJK_TTF.to_vec(),
))
.unwrap();
let parley = services.parley_mut();
assert_eq!(
shape_one_character(parley, web_font_style("Moli Segmented", 400.0), 'R'),
TEST_TTF
);
assert_eq!(
shape_one_character(parley, web_font_style("moli segmented", 400.0), '中'),
TEST_CJK_TTF,
"CSS family matching and segmented range selection must both be case-insensitive"
);
}
#[test]
fn capability_matching_precedes_unicode_range_selection_like_blink() {
let mut services =
DocumentLayoutServices::with_system_font_policy(SystemFontPolicy::Disabled);
services
.register_web_font(WebFontRegistration::new(
"regular-latin",
WebFontFace::new("Moli Capability First")
.with_weight(400.0)
.with_unicode_ranges([WebFontUnicodeRange::new(0x0000, 0x00ff)]),
TEST_TTF.to_vec(),
))
.unwrap();
services
.register_web_font(WebFontRegistration::new(
"bold-cjk",
WebFontFace::new("Moli Capability First")
.with_weight(700.0)
.with_unicode_ranges([WebFontUnicodeRange::new(0x4e00, 0x9fff)]),
TEST_CJK_TTF.to_vec(),
))
.unwrap();
let parley = services.parley_mut();
let mut style = web_font_style("Moli Capability First", 700.0);
parley.resolve_font_families(&mut style, Some('R'));
assert!(
matches!(&style.font_family, FontFamily::List(families) if families.is_empty()),
"the regular face must not be used after the bold capability group wins but does not cover the character"
);
}
fn registration(slot: &str, family: &str, bytes: &[u8]) -> WebFontRegistration {
WebFontRegistration::new(
slot,
WebFontFace::new(family)
.with_weight(625.0)
.with_stretch(87.5)
.with_style(WebFontStyle::Italic),
bytes.to_vec(),
)
}
fn has_family(services: &mut DocumentLayoutServices, family: &str) -> bool {
services
.parley_mut()
.font_context
.collection
.family_id(family)
.is_some()
}
#[test]
fn fixed_font_policy_registers_ttf_under_css_alias() {
let mut services =
DocumentLayoutServices::with_system_font_policy(SystemFontPolicy::Disabled);
assert_eq!(services.system_font_policy(), SystemFontPolicy::Disabled);
assert!(!services.is_initialized());
assert_eq!(
services.register_web_font(registration("face-1", "Phase Three Alias", TEST_TTF)),
Ok(WebFontRegistrationOutcome::Added)
);
assert_eq!(services.web_font_count(), 1);
assert!(!services.is_initialized());
assert!(has_family(&mut services, "Phase Three Alias"));
}
#[test]
fn woff_and_woff2_are_decoded_before_fontique_registration() {
for (slot, family, bytes) in [
("woff", "Moli WOFF", TEST_WOFF),
("woff2", "Moli WOFF2", TEST_WOFF2),
] {
let mut services =
DocumentLayoutServices::with_system_font_policy(SystemFontPolicy::Disabled);
assert_eq!(
services.register_web_font(registration(slot, family, bytes)),
Ok(WebFontRegistrationOutcome::Added),
"{slot} should decode and register"
);
assert!(has_family(&mut services, family));
}
}
#[test]
fn stable_slot_replacement_rebuilds_initialized_font_collection() {
let mut services =
DocumentLayoutServices::with_system_font_policy(SystemFontPolicy::Disabled);
services
.register_web_font(registration("rule-7", "Old Alias", TEST_TTF))
.unwrap();
assert!(has_family(&mut services, "Old Alias"));
assert_eq!(
services.register_web_font(registration("rule-7", "New Alias", TEST_WOFF2)),
Ok(WebFontRegistrationOutcome::Replaced)
);
assert!(!has_family(&mut services, "Old Alias"));
assert!(has_family(&mut services, "New Alias"));
assert_eq!(services.web_font_count(), 1);
}
#[test]
fn invalid_replacement_does_not_poison_existing_slot() {
let mut services =
DocumentLayoutServices::with_system_font_policy(SystemFontPolicy::Disabled);
services
.register_web_font(registration("rule-9", "Stable Alias", TEST_TTF))
.unwrap();
assert!(has_family(&mut services, "Stable Alias"));
assert_eq!(
services.register_web_font(registration("rule-9", "Broken Alias", b"not a font")),
Err(WebFontRegistrationError::UnsupportedPayload)
);
assert!(has_family(&mut services, "Stable Alias"));
assert!(!has_family(&mut services, "Broken Alias"));
assert_eq!(services.web_font_count(), 1);
}
#[test]
fn unchanged_registration_does_not_rebuild_and_remove_is_explicit() {
let mut services =
DocumentLayoutServices::with_system_font_policy(SystemFontPolicy::Disabled);
let registration = registration("rule-11", "Stable Alias", TEST_TTF);
services.register_web_font(registration.clone()).unwrap();
let font_context_address = std::ptr::from_ref(&services.parley_mut().font_context);
assert_eq!(
services.register_web_font(registration),
Ok(WebFontRegistrationOutcome::Unchanged)
);
assert_eq!(
font_context_address,
std::ptr::from_ref(&services.parley_mut().font_context)
);
assert!(services.remove_web_font("rule-11"));
assert!(!services.remove_web_font("rule-11"));
assert!(!has_family(&mut services, "Stable Alias"));
}
#[test]
fn descriptor_validation_rejects_non_css_metadata() {
let mut services = DocumentLayoutServices::new();
for face in [
WebFontFace::new(""),
WebFontFace::new("Bad Weight").with_weight(0.0),
WebFontFace::new("Bad Stretch").with_stretch(f32::NAN),
WebFontFace::new("Bad Style").with_style(WebFontStyle::Oblique(Some(f32::INFINITY))),
WebFontFace::new("Bad Range")
.with_unicode_ranges([WebFontUnicodeRange::new(0x100, 0xff)]),
WebFontFace::new("Too High")
.with_unicode_ranges([WebFontUnicodeRange::new(0, 0x11_0000)]),
] {
let error = services
.register_web_font(WebFontRegistration::new("slot", face, TEST_TTF.to_vec()))
.unwrap_err();
assert!(matches!(
error,
WebFontRegistrationError::InvalidDescriptor { .. }
));
}
assert_eq!(services.web_font_count(), 0);
}
}