#[cfg(alloc_frugal)]
use crate::compat::MiniToString;
use crate::compat::{Arc, HashMap, Rc, RefCell, Vec, VecDeque};
use crate::core::Font;
use crate::render::text::font_assets::{active_faces, FaceBytes};
use crate::render::{ShapedGlyphRun, TextShaper};
pub(crate) fn face_for_family(family: &str) -> Option<FaceBytes> {
#[cfg(feature = "runtime-fonts")]
{
let (runtime, len) = crate::render::text::runtime_fonts::active_runtime_faces();
for face in &runtime[..len] {
if face.name.eq_ignore_ascii_case(family) {
return Some(*face);
}
}
}
active_faces().iter().copied().find(|face| face.name.eq_ignore_ascii_case(family))
}
fn face_for_metrics(text: &str, font: &Font) -> Option<FaceBytes> {
if let Some(named) = face_for_family(font.family()) {
return Some(named);
}
let probe = text.chars().find(|ch| !ch.is_whitespace())?;
crate::render::text::font_assets::face_for_char(probe)
}
fn face_for_text(text: &str) -> Option<FaceBytes> {
let probe = text.chars().find(|ch| !ch.is_whitespace())?;
crate::render::text::font_assets::face_for_char(probe)
}
pub(crate) fn cluster_advances(
text: &str,
ranges: &[(usize, usize)],
font: &Font,
scale: f32,
) -> Option<Rc<[f32]>> {
let face = face_for_metrics(text, font)?;
let key = ShapeKey::new(face.name, text, font.size(), scale, Some(font.weight()), false);
if let Some(advances) = cached_advances(&key) {
return Some(advances);
}
let advances = super::variable::with_shaper_face_at_weight(face, font.weight(), |parsed| {
cluster_advances_in_face(text, ranges, font, scale, parsed)
})
.flatten();
if let Some(advances) = advances {
let advances: Rc<[f32]> = advances.into();
store_advances(key, Rc::clone(&advances));
Some(advances)
} else {
None
}
}
fn cluster_advances_in_face(
text: &str,
ranges: &[(usize, usize)],
font: &Font,
scale: f32,
parsed: &rustybuzz::Face<'_>,
) -> Option<Vec<f32>> {
let units_per_em = parsed.units_per_em() as f32;
if units_per_em <= 0.0 {
return None;
}
let mut buffer = take_shaping_buffer();
buffer.clear();
buffer.push_str(text);
buffer.guess_segment_properties();
#[cfg(test)]
note_shaping_pass();
let output = rustybuzz::shape(parsed, ligature_free_features(), buffer);
let unit_px = font.size() * scale / units_per_em;
let mut advances = vec![0.0f32; ranges.len()];
for (info, position) in output.glyph_infos().iter().zip(output.glyph_positions()) {
let byte = info.cluster as usize;
let index = ranges.partition_point(|&(start, _)| start <= byte);
if let Some(slot) = advances.get_mut(index.saturating_sub(1)) {
*slot += position.x_advance as f32 * unit_px;
}
}
recycle_shaping_buffer(output.clear());
Some(advances)
}
fn ligature_free_features() -> &'static [rustybuzz::Feature] {
const TAG_LIGA: rustybuzz::ttf_parser::Tag = rustybuzz::ttf_parser::Tag::from_bytes(b"liga");
const TAG_CLIG: rustybuzz::ttf_parser::Tag = rustybuzz::ttf_parser::Tag::from_bytes(b"clig");
const fn off(tag: rustybuzz::ttf_parser::Tag) -> rustybuzz::Feature {
rustybuzz::Feature { tag, value: 0, start: 0, end: u32::MAX }
}
const FEATURES: [rustybuzz::Feature; 2] = [off(TAG_LIGA), off(TAG_CLIG)];
&FEATURES
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct ShapeKey {
face: &'static str,
text: Arc<str>,
font_size_bits: u32,
scale_bits: u32,
weight: Option<u16>,
ligatures: bool,
}
impl ShapeKey {
fn new(
face: &'static str,
text: &str,
font_size: f32,
scale: f32,
weight: Option<u16>,
ligatures: bool,
) -> Self {
Self {
face,
text: Arc::from(text),
font_size_bits: font_size.to_bits(),
scale_bits: scale.to_bits(),
weight,
ligatures,
}
}
}
const SHAPE_CACHE_LIMIT: usize = 512;
struct FifoCache<V> {
entries: HashMap<ShapeKey, V>,
order: VecDeque<ShapeKey>,
}
impl<V> FifoCache<V> {
fn new() -> Self {
Self { entries: HashMap::new(), order: VecDeque::new() }
}
fn get(&self, key: &ShapeKey) -> Option<&V> {
self.entries.get(key)
}
fn insert(&mut self, key: ShapeKey, value: V) {
if self.entries.insert(key.clone(), value).is_none() {
self.order.push_back(key);
}
while self.order.len() > SHAPE_CACHE_LIMIT {
if let Some(oldest) = self.order.pop_front() {
self.entries.remove(&oldest);
}
}
}
#[cfg(test)]
fn clear(&mut self) {
self.entries.clear();
self.order.clear();
}
}
thread_local! {
static SHAPED_RUNS: RefCell<FifoCache<Option<Rc<ShapedGlyphRun>>>> =
RefCell::new(FifoCache::new());
static CLUSTER_ADVANCES: RefCell<FifoCache<Rc<[f32]>>> = RefCell::new(FifoCache::new());
static BIDI_LEVELS: RefCell<FifoCache<Rc<[u8]>>> = RefCell::new(FifoCache::new());
static SHAPING_BUFFER: RefCell<Option<rustybuzz::UnicodeBuffer>> = const { RefCell::new(None) };
}
fn take_shaping_buffer() -> rustybuzz::UnicodeBuffer {
SHAPING_BUFFER.with(|slot| slot.borrow_mut().take()).unwrap_or_default()
}
fn recycle_shaping_buffer(buffer: rustybuzz::UnicodeBuffer) {
let _ = SHAPING_BUFFER.try_with(|slot| {
*slot.borrow_mut() = Some(buffer);
});
}
#[cfg(test)]
thread_local! {
static SHAPING_PASSES: core::cell::Cell<usize> = const { core::cell::Cell::new(0) };
}
#[cfg(test)]
fn note_shaping_pass() {
SHAPING_PASSES.with(|passes| passes.set(passes.get() + 1));
}
#[cfg(test)]
fn shaping_pass_count() -> usize {
SHAPING_PASSES.with(core::cell::Cell::get)
}
#[cfg(test)]
fn clear_shape_caches() {
let _ = SHAPED_RUNS.try_with(|cache| cache.borrow_mut().clear());
let _ = CLUSTER_ADVANCES.try_with(|cache| cache.borrow_mut().clear());
let _ = BIDI_LEVELS.try_with(|cache| cache.borrow_mut().clear());
BIDI_LEVEL_HITS.with(|hits| hits.set(0));
}
fn cached_run(key: &ShapeKey) -> Option<Option<Rc<ShapedGlyphRun>>> {
SHAPED_RUNS.try_with(|cache| cache.borrow().get(key).cloned()).ok().flatten()
}
fn store_run(key: ShapeKey, run: Option<Rc<ShapedGlyphRun>>) {
let _ = SHAPED_RUNS.try_with(|cache| cache.borrow_mut().insert(key, run));
}
fn cached_advances(key: &ShapeKey) -> Option<Rc<[f32]>> {
CLUSTER_ADVANCES.try_with(|cache| cache.borrow().get(key).cloned()).ok().flatten()
}
fn store_advances(key: ShapeKey, advances: Rc<[f32]>) {
let _ = CLUSTER_ADVANCES.try_with(|cache| cache.borrow_mut().insert(key, advances));
}
pub(crate) fn cached_bidi_levels(
text: &str,
ranges: &[(usize, usize)],
font: &Font,
scale: f32,
) -> Option<Rc<[u8]>> {
let face = face_for_metrics(text, font)?;
let key = ShapeKey::new(face.name, text, font.size(), scale, Some(font.weight()), false);
if let Some(levels) =
BIDI_LEVELS.try_with(|cache| cache.borrow().get(&key).cloned()).ok().flatten()
{
#[cfg(test)]
BIDI_LEVEL_HITS.with(|hits| hits.set(hits.get() + 1));
return Some(levels);
}
let levels: Rc<[u8]> = Rc::from(super::bidi::levels_for(text, ranges));
let _ = BIDI_LEVELS.try_with(|cache| cache.borrow_mut().insert(key, levels.clone()));
Some(levels)
}
#[cfg(test)]
thread_local! {
static BIDI_LEVEL_HITS: core::cell::Cell<usize> = const { core::cell::Cell::new(0) };
}
#[cfg(test)]
pub(crate) fn bidi_level_cache_hits() -> usize {
BIDI_LEVEL_HITS.with(core::cell::Cell::get)
}
pub struct RustybuzzShaper {
face: FaceBytes,
}
impl RustybuzzShaper {
pub fn new(face: FaceBytes) -> Option<Self> {
super::font_assets::with_shaper_face(face, |_| ())?;
Some(Self { face })
}
pub fn from_active_faces(text: &str) -> Option<Self> {
Self::new(face_for_text(text)?)
}
pub fn face_name(&self) -> &'static str {
self.face.name
}
fn trait_shape(&self, text: &str, font_size: f32) -> Option<Rc<ShapedGlyphRun>> {
self.shape_face(text, font_size, 1.0)
}
pub fn shape_face(&self, text: &str, font_size: f32, scale: f32) -> Option<Rc<ShapedGlyphRun>> {
self.shape_face_with(text, font_size, scale, ligature_free_features(), None)
}
pub fn shape_face_at_weight(
&self,
text: &str,
font_size: f32,
scale: f32,
weight: u16,
) -> Option<Rc<ShapedGlyphRun>> {
self.shape_face_with(text, font_size, scale, ligature_free_features(), Some(weight))
}
pub fn shape_face_with_ligatures(
&self,
text: &str,
font_size: f32,
scale: f32,
) -> Option<Rc<ShapedGlyphRun>> {
self.shape_face_with(text, font_size, scale, &[], None)
}
pub fn shape_face_with_ligatures_at_weight(
&self,
text: &str,
font_size: f32,
scale: f32,
weight: u16,
) -> Option<Rc<ShapedGlyphRun>> {
self.shape_face_with(text, font_size, scale, &[], Some(weight))
}
fn shape_face_with(
&self,
text: &str,
font_size: f32,
scale: f32,
features: &[rustybuzz::Feature],
weight: Option<u16>,
) -> Option<Rc<ShapedGlyphRun>> {
let key =
ShapeKey::new(self.face.name, text, font_size, scale, weight, features.is_empty());
if let Some(hit) = cached_run(&key) {
return hit;
}
let shape = |parsed: &rustybuzz::Face<'_>| {
let units_per_em = parsed.units_per_em() as f32;
if units_per_em <= 0.0 {
return None;
}
let mut buffer = take_shaping_buffer();
buffer.clear();
buffer.push_str(text);
buffer.guess_segment_properties();
#[cfg(test)]
note_shaping_pass();
let output = rustybuzz::shape(parsed, features, buffer);
let unit_px = font_size * scale / units_per_em;
let mut glyph_ids = Vec::with_capacity(output.len());
let mut positions = Vec::with_capacity(output.len());
let mut pen_x = 0.0f32;
for (info, position) in output.glyph_infos().iter().zip(output.glyph_positions()) {
glyph_ids.push(info.glyph_id);
pen_x += position.x_offset as f32 * unit_px;
positions.push((pen_x, position.y_offset as f32 * unit_px));
pen_x += position.x_advance as f32 * unit_px;
}
let run = ShapedGlyphRun {
width: pen_x,
height: font_size * 1.2,
glyph_ids,
positions,
font_size,
};
recycle_shaping_buffer(output.clear());
Some(run)
};
let shaped = match weight {
Some(weight) => super::variable::with_shaper_face_at_weight(self.face, weight, shape),
None => super::font_assets::with_shaper_face(self.face, shape),
}
.flatten();
let shaped = shaped.map(Rc::from);
store_run(key, shaped.clone());
shaped
}
}
pub fn glyph_run_for_family_at_weight(
text: &str,
font: &Font,
scale: f32,
weight: u16,
) -> Option<Rc<ShapedGlyphRun>> {
let face = face_for_family(font.family())?;
RustybuzzShaper::new(face)?.shape_face_with_ligatures_at_weight(
text,
font.size(),
scale,
weight,
)
}
impl TextShaper for RustybuzzShaper {
fn shape(&self, text: &str, font_size: f32) -> Vec<ShapedGlyphRun> {
self.trait_shape(text, font_size).map(|run| (*run).clone()).into_iter().collect()
}
fn measure_width(&self, text: &str, font_size: f32) -> f32 {
self.trait_shape(text, font_size).map(|run| run.width).unwrap_or(0.0)
}
fn cache_identity(&self) -> u64 {
let mut hash = 0xcbf2_9ce4_8422_2325u64;
for byte in self.face.name.as_bytes() {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
hash | 1
}
fn measure_height(&self, _text: &str, font_size: f32) -> f32 {
font_size * 1.2
}
fn char_advance(&self, c: char, font_size: f32) -> f32 {
self.measure_width(&c.to_string(), font_size)
}
}
#[cfg(any(feature = "fonts-vector-latin", feature = "fonts-complex"))]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_face_is_chosen_by_the_first_strong_character() {
let face = face_for_text("hello").expect("a Latin face is enabled");
assert!(face.bytes.len() > 1000);
#[cfg(feature = "fonts-complex")]
assert_eq!(
face_for_text("\u{628}\u{64a}\u{62a}").expect("an Arabic face is enabled").name,
"Noto Naskh Arabic"
);
}
#[cfg(feature = "fonts-vector-latin")]
#[test]
fn the_face_a_family_names_is_the_face_that_is_used() {
assert_eq!(
face_for_family("open sans").map(|face| face.name),
Some("Open Sans"),
"a family name is matched case-insensitively"
);
assert!(
face_for_family("Arial").is_none(),
"a family this build does not carry leaves the model in charge"
);
}
#[test]
fn shaping_a_ligature_produces_fewer_glyphs_than_characters() {
let shaper = RustybuzzShaper::from_active_faces("ffi").expect("a face is enabled");
let shaped = shaper.shape_face("ffi", 16.0, 1.0).expect("the run shapes");
assert!(
shaped.glyph_ids.len() <= 3,
"a ligature may not produce more glyphs than characters: {:?}",
shaped.glyph_ids
);
assert!(shaped.width > 0.0, "a shaped run has a width");
}
#[test]
fn arabic_joining_selects_contextual_forms() {
#[cfg(feature = "fonts-complex")]
{
let shaper =
RustybuzzShaper::from_active_faces("\u{628}\u{64a}\u{62a}").expect("a face");
let shaped = shaper.shape_face("\u{628}\u{64a}\u{62a}", 16.0, 1.0).expect("shapes");
let faces = crate::render::text::font_assets::active_faces();
let arabic = faces
.iter()
.find(|face| face.name == "Noto Naskh Arabic")
.expect("the Arabic face is enabled");
let parsed = ttf_parser::Face::parse(arabic.bytes, 0).expect("parses");
let isolated: Vec<u32> = "\u{628}\u{64a}\u{62a}"
.chars()
.filter_map(|ch| parsed.glyph_index(ch).map(|id| id.0 as u32))
.collect();
assert_eq!(isolated.len(), 3, "the face has all three letters");
assert_ne!(
shaped.glyph_ids, isolated,
"joining must substitute the contextual forms, not the isolated ones"
);
}
}
#[test]
fn real_advances_differ_from_the_models_flat_factor() {
let shaper = RustybuzzShaper::from_active_faces("iI").expect("a face is enabled");
let thin = shaper.char_advance('i', 16.0);
let thick = shaper.char_advance('W', 16.0);
assert!(thick > thin, "W ({thick}) must be wider than i ({thin}) in a proportional face");
}
#[test]
fn the_trait_path_reuses_the_cached_shape_result() {
let shaper = RustybuzzShaper::from_active_faces("cache-probe").expect("a face is enabled");
clear_shape_caches();
let before = shaping_pass_count();
let first = shaper.shape("cache-probe", 13.0);
let after_first = shaping_pass_count();
let second = shaper.shape("cache-probe", 13.0);
let after_second = shaping_pass_count();
assert_eq!(after_first - before, 1, "the first call shapes once");
assert_eq!(after_second, after_first, "the second call is a cache hit");
assert_eq!(first.len(), second.len());
assert_eq!(first[0].glyph_ids, second[0].glyph_ids);
let _measured = shaper.measure_width("cache-probe", 13.0);
assert_eq!(shaping_pass_count(), after_second, "measurement reuses the shaped run");
}
#[test]
fn recycled_shaping_buffers_do_not_leak_glyphs_between_texts() {
let shaper = RustybuzzShaper::from_active_faces("WWW").expect("a face is enabled");
clear_shape_caches();
let long = shaper.shape_face("WWW", 16.0, 1.0).expect("the long run shapes");
let short = shaper.shape_face("i", 16.0, 1.0).expect("the short run shapes");
assert_eq!(long.glyph_ids.len(), long.positions.len());
assert_eq!(short.glyph_ids.len(), short.positions.len());
assert!(
short.glyph_ids.len() < long.glyph_ids.len(),
"a recycled buffer must start empty: {:?} vs {:?}",
short.glyph_ids,
long.glyph_ids
);
assert!(short.width < long.width, "the shorter text stays shorter");
}
#[test]
fn a_build_with_no_vector_face_says_so_rather_than_guessing() {
if active_faces().is_empty() {
assert!(RustybuzzShaper::from_active_faces("abc").is_none());
}
}
#[test]
fn the_shaping_cache_evicts_the_oldest_entry_instead_of_clearing() {
clear_shape_caches();
for index in 0..=SHAPE_CACHE_LIMIT {
let text = index.to_string();
let key = ShapeKey::new("probe", &text, 12.0, 1.0, None, false);
store_run(key, None);
}
let oldest = ShapeKey::new("probe", "0", 12.0, 1.0, None, false);
let newest = ShapeKey::new("probe", &SHAPE_CACHE_LIMIT.to_string(), 12.0, 1.0, None, false);
SHAPED_RUNS.with(|cache| {
let cache = cache.borrow();
assert_eq!(cache.entries.len(), SHAPE_CACHE_LIMIT, "the cache stays at its bound");
assert!(!cache.entries.contains_key(&oldest), "the oldest entry was evicted");
assert!(cache.entries.contains_key(&newest), "the newest entry is resident");
});
}
#[cfg(feature = "fonts-vector-latin")]
#[test]
fn bidi_levels_are_memoised_with_the_shaping_cache() {
let font = crate::core::Font::new("Open Sans", 12.0, false, false);
let text = "Hello";
let ranges: Vec<(usize, usize)> =
text.char_indices().map(|(i, ch)| (i, i + ch.len_utf8())).collect();
clear_shape_caches();
let first = cached_bidi_levels(text, &ranges, &font, 1.0).expect("a Latin face is enabled");
let after_first = bidi_level_cache_hits();
let second =
cached_bidi_levels(text, &ranges, &font, 1.0).expect("a Latin face is enabled");
let after_second = bidi_level_cache_hits();
assert_eq!(after_first, 0, "the first request is a miss");
assert_eq!(after_second, 1, "the second request is served from the cache");
assert_eq!(&*first, &*second, "the cached levels are the same answer");
assert!(first.iter().all(|level| level % 2 == 0), "Latin is all even levels");
}
}