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>(())
});
}
}