use rustybuzz::{Face, UnicodeBuffer};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ShapedGlyph {
pub glyph_id: u16,
pub x_advance: i32,
pub y_advance: i32,
pub x_offset: i32,
pub y_offset: i32,
pub cluster: u32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ShapedRun {
pub glyphs: Vec<ShapedGlyph>,
pub total_x_advance: i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
Ltr,
Rtl,
Ttb,
}
impl From<Direction> for rustybuzz::Direction {
fn from(d: Direction) -> Self {
match d {
Direction::Ltr => rustybuzz::Direction::LeftToRight,
Direction::Rtl => rustybuzz::Direction::RightToLeft,
Direction::Ttb => rustybuzz::Direction::TopToBottom,
}
}
}
pub fn shape(text: &str, face_bytes: &[u8], direction: Direction) -> Option<ShapedRun> {
let face = Face::from_slice(face_bytes, 0)?;
let mut buffer = UnicodeBuffer::new();
buffer.push_str(text);
buffer.set_direction(direction.into());
let glyphs_buffer = rustybuzz::shape(&face, &[], buffer);
let infos = glyphs_buffer.glyph_infos();
let positions = glyphs_buffer.glyph_positions();
debug_assert_eq!(
infos.len(),
positions.len(),
"rustybuzz invariant: infos and positions match in length"
);
let mut glyphs = Vec::with_capacity(infos.len());
let mut total_x_advance = 0i32;
for (info, pos) in infos.iter().zip(positions.iter()) {
total_x_advance = total_x_advance.saturating_add(pos.x_advance);
glyphs.push(ShapedGlyph {
glyph_id: info.glyph_id as u16,
x_advance: pos.x_advance,
y_advance: pos.y_advance,
x_offset: pos.x_offset,
y_offset: pos.y_offset,
cluster: info.cluster,
});
}
Some(ShapedRun {
glyphs,
total_x_advance,
})
}
#[cfg(test)]
mod tests {
use super::*;
const DEJAVU: &[u8] = include_bytes!("../../tests/fixtures/fonts/DejaVuSans.ttf");
#[test]
fn shape_ltr_ascii() {
let run = shape("ABC", DEJAVU, Direction::Ltr).expect("shape must succeed");
assert_eq!(run.glyphs.len(), 3);
for g in &run.glyphs {
assert!(g.x_advance > 0, "ASCII glyph must advance forward");
}
let clusters: Vec<u32> = run.glyphs.iter().map(|g| g.cluster).collect();
assert_eq!(clusters, vec![0, 1, 2]);
}
#[test]
fn shape_empty_string_yields_no_glyphs() {
let run = shape("", DEJAVU, Direction::Ltr).expect("shape must succeed");
assert!(run.glyphs.is_empty());
assert_eq!(run.total_x_advance, 0);
}
#[test]
fn shape_garbage_face_returns_none() {
assert!(shape("abc", b"not a font", Direction::Ltr).is_none());
}
#[test]
fn shape_rtl_arabic() {
let run = shape("مرحبا", DEJAVU, Direction::Rtl).expect("shape must succeed");
assert!(!run.glyphs.is_empty());
assert!(run.total_x_advance > 0);
}
#[test]
fn total_advance_equals_sum_of_glyph_advances() {
let run = shape("Hello", DEJAVU, Direction::Ltr).expect("shape must succeed");
let summed: i32 = run.glyphs.iter().map(|g| g.x_advance).sum();
assert_eq!(summed, run.total_x_advance);
}
}