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:
parent
3c1f6c9e8a
commit
2610c869f6
7 changed files with 221 additions and 121 deletions
193
src/font/mod.rs
193
src/font/mod.rs
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>(())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
76
src/shape.rs
76
src/shape.rs
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue