use crate::compat::Vec;
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> {
active_faces().iter().copied().find(|face| face.name.eq_ignore_ascii_case(family))
}
fn face_for_text(text: &str) -> Option<FaceBytes> {
let faces = active_faces();
if faces.is_empty() {
return None;
}
let probe = text.chars().find(|ch| !ch.is_whitespace())?;
faces.iter().copied().find(|face| covers(face, probe))
}
pub(crate) fn covers(face: &FaceBytes, ch: char) -> bool {
face_parser(face).map(|parsed| parsed.glyph_index(ch).is_some()).unwrap_or(false)
}
fn face_parser(face: &FaceBytes) -> Option<ttf_parser::Face<'static>> {
ttf_parser::Face::parse(face.bytes, 0).ok()
}
pub(crate) fn cluster_advances(
text: &str,
ranges: &[(usize, usize)],
font: &Font,
scale: f32,
) -> Option<Vec<f32>> {
let face = face_for_family(font.family())?;
let parsed = rustybuzz::Face::from_slice(face.bytes, 0)?;
let units_per_em = parsed.units_per_em() as f32;
if units_per_em <= 0.0 {
return None;
}
let mut buffer = rustybuzz::UnicodeBuffer::new();
buffer.push_str(text);
buffer.guess_segment_properties();
let output = rustybuzz::shape(&parsed, &[], 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;
}
}
Some(advances)
}
pub struct RustybuzzShaper {
face: FaceBytes,
}
impl RustybuzzShaper {
pub fn new(face: FaceBytes) -> Option<Self> {
face_parser(&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
}
pub fn shape_face(&self, text: &str, font_size: f32, scale: f32) -> Option<ShapedGlyphRun> {
let parsed = rustybuzz::Face::from_slice(self.face.bytes, 0)?;
let units_per_em = parsed.units_per_em() as f32;
if units_per_em <= 0.0 {
return None;
}
let mut buffer = rustybuzz::UnicodeBuffer::new();
buffer.push_str(text);
buffer.guess_segment_properties();
let output = rustybuzz::shape(&parsed, &[], 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;
}
Some(ShapedGlyphRun {
width: pen_x,
height: font_size * 1.2,
glyph_ids,
positions,
font_size,
})
}
}
impl TextShaper for RustybuzzShaper {
fn shape(&self, text: &str, font_size: f32) -> Vec<ShapedGlyphRun> {
self.shape_face(text, font_size, 1.0).into_iter().collect()
}
fn measure_width(&self, text: &str, font_size: f32) -> f32 {
self.shape_face(text, font_size, 1.0).map(|run| run.width).unwrap_or(0.0)
}
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 a_build_with_no_vector_face_says_so_rather_than_guessing() {
if active_faces().is_empty() {
assert!(RustybuzzShaper::from_active_faces("abc").is_none());
}
}
}