Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213//! Text shaping through Parley, a linked subsystem like HarfBuzz and ICU//! (`roadmap.md` -> "Layering"): the three-browser vote cannot score proportional-font//! advances (`docs/text-metrics.md`), so this is hand-wired code, never a generated//! function. What the rest of layout gets from it is the advance width of a run of text//! in a given font at a given size.//!//! Fonts are never discovered on the machine: Parley is built without its `system`//! feature, and a [`TextShaper`] knows only the font files given to//! [`TextShaper::register_font`], so a width is the same on every machine that registers//! the same bytes. Shaping runs at scale 1 with unquantized coordinates, so a width is//! the font's own advance sum in CSS pixels, with no rounding applied here.
use parley::fontique::{Blob, CollectionOptions};use parley::{FontContext, FontFamily, FontFamilyName, FontStyle, FontWeight, LayoutContext, StyleProperty};use std::sync::{Arc, Mutex, MutexGuard};
/// What laying a text run out on one line needs from its font, in CSS pixels.#[derive(Debug, Clone, Copy, PartialEq)]pub struct RunMetrics { /// The unrounded advance width of the run. pub width: f32, /// The font's ascent above the baseline at this size. pub ascent: f32, /// The font's descent below the baseline at this size. pub descent: f32, /// The line height `line-height: normal` gives: ascent, descent and the font's line gap. pub normal_line_height: f32,}
/// The fonts one document is laid out with, held by the box tree built over it and read/// by the layout of that tree. A clone is another handle on the same fonts./// `Fonts::default()` holds none, and then no text is measured: the box tree has no text/// run (`crate::boxtree::TextRun`) and layout is what it was before text had a width./// Fonts are passed in by whoever builds the tree/// ([`crate::boxtree::BoxTree::build_with_fonts`]) and never discovered on the machine.#[derive(Clone, Default)]pub struct Fonts(Arc<Mutex<TextShaper>>);
impl std::fmt::Debug for Fonts { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("Fonts") }}
impl Fonts { fn shaper(&self) -> MutexGuard<'_, TextShaper> { self.0.lock().unwrap_or_else(|poisoned| poisoned.into_inner()) }
/// Registers a font file and returns the family names it was filed under /// ([`TextShaper::register_font`]). pub fn register_font(&self, data: Vec<u8>) -> Vec<String> { self.shaper().register_font(data) }
/// Whether a family called `name` is registered. pub fn has_family(&self, name: &str) -> bool { self.shaper().has_family(name) }
/// The first family of `font` that is registered, or `None` when none is. A family /// that is not registered is skipped, as in the CSS font fallback list. pub fn resolve_family(&self, font: &dom::computed::FontRequest) -> Option<String> { self.shaper().first_registered(&font.families).map(str::to_string) }
/// Measures `text` in the first registered family of `font` (`None` when there is none). pub fn measure_run(&self, text: &str, font: &dom::computed::FontRequest) -> Option<RunMetrics> { let mut shaper = self.shaper(); let family = shaper.first_registered(&font.families)?.to_string(); Some(shaper.measure(text, &family, font.size_px as f32, font.weight, font.italic)) }}
/// The characters of the text node `node`, or the empty string for any other node.pub fn node_text(document: &dom::Document, node: dom::node::NodeId) -> &str { match &document.nodes()[node.index()].data { dom::node::NodeData::Text(text) => text, _ => "", }}
/// Shapes runs of text with the fonts registered on it.pub struct TextShaper { fonts: FontContext, layouts: LayoutContext<()>,}
impl Default for TextShaper { fn default() -> Self { Self::new() }}
impl TextShaper { /// A shaper with no fonts at all, not even the machine's. pub fn new() -> Self { let collection = parley::fontique::Collection::new(CollectionOptions { shared: false, system_fonts: false, }); Self { fonts: FontContext { collection, source_cache: Default::default(), }, layouts: LayoutContext::new(), } }
/// Registers every face in `data` (a TrueType, OpenType or collection file) and /// returns the family names it was filed under, taken from the font's own name table. pub fn register_font(&mut self, data: Vec<u8>) -> Vec<String> { let registered = self.fonts.collection.register_fonts(Blob::from(data), None); let mut names = Vec::new(); for (id, _) in registered { if let Some(name) = self.fonts.collection.family_name(id) { names.push(name.to_string()); } } names }
/// Whether a font family called `name` is registered. pub fn has_family(&mut self, name: &str) -> bool { self.fonts.collection.family_id(name).is_some() }
/// The first of `families` that is registered, in the order written. pub fn first_registered<'a>(&mut self, families: &'a [String]) -> Option<&'a str> { families.iter().map(String::as_str).find(|name| self.has_family(name)) }
/// [`RunMetrics`] of `text` on one unwrapped line in the registered family `family`, /// at `font_size_px`, in the given weight and style. Like /// [`advance_width`](TextShaper::advance_width), a family that is not registered /// shapes with no font. pub fn measure(&mut self, text: &str, family: &str, font_size_px: f32, weight: f32, italic: bool) -> RunMetrics { let mut builder = self .layouts .ranged_builder(&mut self.fonts, text, 1.0, false); builder.push_default(StyleProperty::FontFamily(FontFamily::Single( FontFamilyName::named(family), ))); builder.push_default(StyleProperty::FontSize(font_size_px)); builder.push_default(StyleProperty::FontWeight(FontWeight::new(weight))); builder.push_default(StyleProperty::FontStyle(if italic { FontStyle::Italic } else { FontStyle::Normal })); let mut layout = builder.build(text); layout.break_all_lines(None); let (ascent, descent, line_height) = layout.lines().next().map_or((0.0, 0.0, 0.0), |line| { let m = line.metrics(); (m.baseline - m.content_block_min_coord, m.content_block_max_coord - m.baseline, m.line_height) }); RunMetrics { width: layout.full_width(), ascent, descent, normal_line_height: line_height } }
/// The advance width, in CSS pixels, of `text` on one unwrapped line in the registered /// family `family` at `font_size_px`, with kerning and the font's other default /// features applied. A `family` that was not registered shapes with no font and /// returns 0. pub fn advance_width(&mut self, text: &str, family: &str, font_size_px: f32) -> f32 { let mut builder = self .layouts .ranged_builder(&mut self.fonts, text, 1.0, false); builder.push_default(StyleProperty::FontFamily(FontFamily::Single( FontFamilyName::named(family), ))); builder.push_default(StyleProperty::FontSize(font_size_px)); let mut layout = builder.build(text); layout.break_all_lines(None); layout.full_width() }}
#[cfg(test)]mod tests { use super::*;
/// The pinned WPT checkout's font directory, when this checkout has it (the lanes and /// the development box fetch it through the mirror; a bare clone does not). fn wpt_font(file: &str) -> Option<Vec<u8>> { let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); for up in ["../..", "../../../.."] { let path = root .join(up) .join("oracle/suites/checkouts/wpt/fonts") .join(file); if let Ok(bytes) = std::fs::read(path) { return Some(bytes); } } None }
#[test] fn ahem_advance_is_one_em_per_glyph() { let Some(bytes) = wpt_font("Ahem.ttf") else { return; }; let mut shaper = TextShaper::new(); let names = shaper.register_font(bytes); assert_eq!(names, vec!["Ahem".to_string()]); assert_eq!(shaper.advance_width("xxxx", "Ahem", 16.0), 64.0); assert_eq!(shaper.advance_width("xxxx", "Ahem", 13.5), 54.0); }
#[test] fn unregistered_family_has_no_width() { let mut shaper = TextShaper::new(); assert_eq!(shaper.advance_width("hello", "Nothing", 16.0), 0.0); }}