Replace rustybuzz with HarfRust (#417)

* Use HarfRust for shaping

* Replace ttf-parser with skrifa entirely

* Fix clippy lints

* Add shape plan cache

* Bump harfrust and skrifa

* Fix no_std build

* Simplify the shape plan cache

* Please the paperclip

* Cache font ID with plan

* Tune shape plan cache for "BiDi Processing" bench
This commit is contained in:
valadaptive 2025-09-08 23:15:27 -04:00 committed by GitHub
parent 3c1f6c9e8a
commit 2610c869f6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 221 additions and 121 deletions

View file

@ -1,7 +1,10 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
// re-export ttf_parser
pub use ttf_parser;
use harfrust::Shaper;
use skrifa::raw::{ReadError, TableProvider as _};
use skrifa::{metrics::Metrics, prelude::*};
// re-export skrifa
pub use skrifa;
// re-export peniko::Font;
#[cfg(feature = "peniko")]
pub use peniko::Font as PenikoFont;
@ -12,7 +15,6 @@ use alloc::sync::Arc;
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
use rustybuzz::Face as RustybuzzFace;
use self_cell::self_cell;
pub mod fallback;
@ -21,12 +23,19 @@ pub use fallback::{Fallback, PlatformFallback};
pub use self::system::*;
mod system;
struct OwnedFaceData {
data: Arc<dyn AsRef<[u8]> + Send + Sync>,
shaper_data: harfrust::ShaperData,
shaper_instance: harfrust::ShaperInstance,
metrics: Metrics,
}
self_cell!(
struct OwnedFace {
owner: Arc<dyn AsRef<[u8]> + Send + Sync>,
owner: OwnedFaceData,
#[covariant]
dependent: RustybuzzFace,
dependent: Shaper,
}
);
@ -40,7 +49,7 @@ struct FontMonospaceFallback {
pub struct Font {
#[cfg(feature = "swash")]
swash: (u32, swash::CacheKey),
rustybuzz: OwnedFace,
harfrust: OwnedFace,
#[cfg(not(feature = "peniko"))]
data: Arc<dyn AsRef<[u8]> + Send + Sync>,
#[cfg(feature = "peniko")]
@ -89,8 +98,16 @@ impl Font {
}
}
pub fn rustybuzz(&self) -> &RustybuzzFace<'_> {
self.rustybuzz.borrow_dependent()
pub fn shaper(&self) -> &harfrust::Shaper<'_> {
self.harfrust.borrow_dependent()
}
pub(crate) fn shaper_instance(&self) -> &harfrust::ShaperInstance {
&self.harfrust.borrow_owner().shaper_instance
}
pub fn metrics(&self) -> &Metrics {
&self.harfrust.borrow_owner().metrics
}
#[cfg(feature = "peniko")]
@ -113,59 +130,6 @@ impl Font {
pub fn new(db: &fontdb::Database, id: fontdb::ID, weight: fontdb::Weight) -> Option<Self> {
let info = db.face(id)?;
let monospace_fallback = if cfg!(feature = "monospace_fallback") {
db.with_face_data(id, |font_data, face_index| {
let face = ttf_parser::Face::parse(font_data, face_index).ok()?;
let monospace_em_width = info
.monospaced
.then(|| {
let hor_advance = face.glyph_hor_advance(face.glyph_index(' ')?)?;
let upem = face.units_per_em();
Some(f32::from(hor_advance) / f32::from(upem))
})
.flatten();
if info.monospaced && monospace_em_width.is_none() {
None?;
}
let scripts = face
.tables()
.gpos
.into_iter()
.chain(face.tables().gsub)
.flat_map(|table| table.scripts)
.map(|script| script.tag.to_bytes())
.collect();
let mut unicode_codepoints = Vec::new();
face.tables()
.cmap?
.subtables
.into_iter()
.filter(ttf_parser::cmap::Subtable::is_unicode)
.for_each(|subtable| {
unicode_codepoints.reserve(1024);
subtable.codepoints(|code_point| {
if subtable.glyph_index(code_point).is_some() {
unicode_codepoints.push(code_point);
}
});
});
unicode_codepoints.shrink_to_fit();
Some(FontMonospaceFallback {
monospace_em_width,
scripts,
unicode_codepoints,
})
})?
} else {
None
};
let data = match &info.source {
fontdb::Source::Binary(data) => Arc::clone(data),
#[cfg(feature = "std")]
@ -177,6 +141,77 @@ impl Font {
fontdb::Source::SharedFile(_path, data) => Arc::clone(data),
};
// It's a bit unfortunate but we need to parse the data into a `FontRef`
// twice--once to construct the HarfRust `ShaperInstance` and
// `ShaperData`, and once to create the persistent `FontRef` tied to the
// lifetime of the face data.
let font_ref = FontRef::from_index((*data).as_ref(), info.index).ok()?;
let location = font_ref
.axes()
.location([(Tag::new(b"wght"), weight.0 as f32)]);
let metrics = font_ref.metrics(Size::unscaled(), &location);
let monospace_fallback = if cfg!(feature = "monospace_fallback") {
(|| {
let glyph_metrics = font_ref.glyph_metrics(Size::unscaled(), &location);
let charmap = font_ref.charmap();
let monospace_em_width = info
.monospaced
.then(|| {
let hor_advance = glyph_metrics.advance_width(charmap.map(' ')?)?;
let upem = metrics.units_per_em;
Some(hor_advance / f32::from(upem))
})
.flatten();
if info.monospaced && monospace_em_width.is_none() {
None?;
}
let scripts = font_ref
.gpos()
.ok()?
.script_list()
.ok()?
.script_records()
.iter()
.chain(
font_ref
.gsub()
.ok()?
.script_list()
.ok()?
.script_records()
.iter(),
)
.map(|script| script.script_tag().into_bytes())
.collect();
let mut unicode_codepoints = Vec::new();
for (code_point, _) in charmap.mappings() {
unicode_codepoints.push(code_point);
}
unicode_codepoints.shrink_to_fit();
Some(FontMonospaceFallback {
monospace_em_width,
scripts,
unicode_codepoints,
})
})()
} else {
None
};
let (shaper_instance, shaper_data) = {
(
harfrust::ShaperInstance::from_coords(&font_ref, location.coords().iter().copied()),
harfrust::ShaperData::new(&font_ref),
)
};
Some(Self {
id: info.id,
monospace_fallback,
@ -185,21 +220,27 @@ impl Font {
let swash = swash::FontRef::from_index((*data).as_ref(), info.index as usize)?;
(swash.offset, swash.key)
},
rustybuzz: OwnedFace::try_new(Arc::clone(&data), |data| {
RustybuzzFace::from_slice((**data).as_ref(), info.index)
.ok_or(())
.map(|mut face| {
if let Some(axis) = face
.variation_axes()
.into_iter()
.find(|axis| axis.tag == ttf_parser::Tag::from_bytes(b"wght"))
{
let wght = f32::from(weight.0).clamp(axis.min_value, axis.max_value);
let _ = face.set_variation(ttf_parser::Tag::from_bytes(b"wght"), wght);
}
face
})
})
harfrust: OwnedFace::try_new(
OwnedFaceData {
data: Arc::clone(&data),
shaper_data,
shaper_instance,
metrics,
},
|OwnedFaceData {
data,
shaper_data,
shaper_instance,
..
}| {
let font_ref = FontRef::from_index((**data).as_ref(), info.index)?;
let shaper = shaper_data
.shaper(&font_ref)
.instance(Some(shaper_instance))
.build();
Ok::<_, ReadError>(shaper)
},
)
.ok()?,
#[cfg(not(feature = "peniko"))]
data,

View file

@ -7,10 +7,11 @@ use alloc::vec::Vec;
use core::fmt;
use core::ops::{Deref, DerefMut};
use fontdb::Query;
use skrifa::raw::{ReadError, TableProvider as _};
// re-export fontdb and rustybuzz
// re-export fontdb and harfrust
pub use fontdb;
pub use rustybuzz;
pub use harfrust;
use super::fallback::{Fallback, Fallbacks, MonospaceFallbackInfo, PlatformFallback};
@ -182,19 +183,20 @@ impl FontSystem {
if cfg!(feature = "monospace_fallback") {
for &id in &monospace_font_ids {
db.with_face_data(id, |font_data, face_index| {
let _ = ttf_parser::Face::parse(font_data, face_index).map(|face| {
face.tables()
.gpos
.into_iter()
.chain(face.tables().gsub)
.flat_map(|table| table.scripts)
.inspect(|script| {
per_script_monospace_font_ids
.entry(script.tag.to_bytes())
.or_default()
.insert(id);
})
});
let face = skrifa::FontRef::from_index(font_data, face_index)?;
for script in face
.gpos()?
.script_list()?
.script_records()
.iter()
.chain(face.gsub()?.script_list()?.script_records().iter())
{
per_script_monospace_font_ids
.entry(script.script_tag().into_bytes())
.or_default()
.insert(id);
}
Ok::<_, ReadError>(())
});
}
}

View file

@ -4,7 +4,7 @@
//!
//! This library provides advanced text handling in a generic way. It provides abstractions for
//! shaping, font discovery, font fallback, layout, rasterization, and editing. Shaping utilizes
//! rustybuzz, font discovery utilizes fontdb, and the rasterization is optional and utilizes
//! harfrust, font discovery utilizes fontdb, and the rasterization is optional and utilizes
//! swash. The other features are developed internal to this library.
//!
//! It is recommended that you start by creating a [`FontSystem`], after which you can create a

View file

@ -10,6 +10,7 @@ use crate::{
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
use alloc::collections::VecDeque;
use core::cmp::{max, min};
use core::fmt;
use core::mem;
@ -78,11 +79,17 @@ impl Shaping {
}
}
const NUM_SHAPE_PLANS: usize = 6;
/// A set of buffers containing allocations for shaped text.
#[derive(Default)]
pub struct ShapeBuffer {
/// Cache for harfrust shape plans. Stores up to [`NUM_SHAPE_PLANS`] plans at once. Inserting a new one past that
/// will remove the one that was least recently added (not least recently used).
shape_plan_cache: VecDeque<(fontdb::ID, harfrust::ShapePlan)>,
/// Buffer for holding unicode text.
rustybuzz_buffer: Option<rustybuzz::UnicodeBuffer>,
harfrust_buffer: Option<harfrust::UnicodeBuffer>,
/// Temporary buffers for scripts.
scripts: Vec<Script>,
@ -119,15 +126,15 @@ fn shape_fallback(
) -> Vec<usize> {
let run = &line[start_run..end_run];
let font_scale = font.rustybuzz().units_per_em() as f32;
let ascent = f32::from(font.rustybuzz().ascender()) / font_scale;
let descent = -f32::from(font.rustybuzz().descender()) / font_scale;
let font_scale = font.metrics().units_per_em as f32;
let ascent = font.metrics().ascent / font_scale;
let descent = -font.metrics().descent / font_scale;
let mut buffer = scratch.rustybuzz_buffer.take().unwrap_or_default();
let mut buffer = scratch.harfrust_buffer.take().unwrap_or_default();
buffer.set_direction(if span_rtl {
rustybuzz::Direction::RightToLeft
harfrust::Direction::RightToLeft
} else {
rustybuzz::Direction::LeftToRight
harfrust::Direction::LeftToRight
});
if run.contains('\t') {
// Push string to buffer, replacing tabs with spaces
@ -140,29 +147,56 @@ fn shape_fallback(
}
buffer.guess_segment_properties();
let rtl = matches!(buffer.direction(), rustybuzz::Direction::RightToLeft);
let rtl = matches!(buffer.direction(), harfrust::Direction::RightToLeft);
assert_eq!(rtl, span_rtl);
let attrs = attrs_list.get_span(start_run);
let mut rb_font_features = Vec::new();
// Convert attrs::Feature to rustybuzz::Feature
for feature in attrs.font_features.features {
rb_font_features.push(rustybuzz::Feature::new(
rustybuzz::ttf_parser::Tag::from_bytes(feature.tag.as_bytes()),
// Convert attrs::Feature to harfrust::Feature
for feature in &attrs.font_features.features {
rb_font_features.push(harfrust::Feature::new(
harfrust::Tag::new(feature.tag.as_bytes()),
feature.value,
0..usize::MAX,
));
}
let shape_plan = rustybuzz::ShapePlan::new(
font.rustybuzz(),
buffer.direction(),
Some(buffer.script()),
buffer.language().as_ref(),
&rb_font_features,
);
let glyph_buffer = rustybuzz::shape_with_plan(font.rustybuzz(), &shape_plan, buffer);
let language = buffer.language();
let key = harfrust::ShapePlanKey::new(Some(buffer.script()), buffer.direction())
.features(&rb_font_features)
.instance(Some(font.shaper_instance()))
.language(language.as_ref());
let shape_plan = match scratch
.shape_plan_cache
.iter()
.find(|(id, plan)| *id == font.id() && key.matches(plan))
{
Some((_font_id, plan)) => plan,
None => {
let plan = harfrust::ShapePlan::new(
font.shaper(),
buffer.direction(),
Some(buffer.script()),
buffer.language().as_ref(),
&rb_font_features,
);
if scratch.shape_plan_cache.len() >= NUM_SHAPE_PLANS {
scratch.shape_plan_cache.pop_front();
}
scratch.shape_plan_cache.push_back((font.id(), plan));
&scratch
.shape_plan_cache
.back()
.expect("we just pushed the shape plan")
.1
}
};
let glyph_buffer = font
.shaper()
.shape_with_plan(shape_plan, buffer, &rb_font_features);
let glyph_infos = glyph_buffer.glyph_infos();
let glyph_positions = glyph_buffer.glyph_positions();
@ -230,7 +264,7 @@ fn shape_fallback(
}
// Restore the buffer to save an allocation.
scratch.rustybuzz_buffer = Some(glyph_buffer.clear());
scratch.harfrust_buffer = Some(glyph_buffer.clear());
missing
}