#[derive(Debug, Clone)]
pub struct ShapedGlyph {
pub glyph_id: u16,
pub cluster: u32,
pub x_advance: i32,
pub y_advance: i32,
pub x_offset: i32,
pub y_offset: i32,
}
pub fn shape_text(text: &str, font_data: &[u8]) -> Option<Vec<ShapedGlyph>> {
shape_text_with_direction(text, font_data, false)
}
pub fn shape_text_with_direction(
text: &str,
font_data: &[u8],
is_rtl: bool,
) -> Option<Vec<ShapedGlyph>> {
let face = rustybuzz::Face::from_slice(font_data, 0)?;
let mut buffer = rustybuzz::UnicodeBuffer::new();
buffer.push_str(text);
if is_rtl {
buffer.set_direction(rustybuzz::Direction::RightToLeft);
}
let output = rustybuzz::shape(&face, &[], buffer);
let infos = output.glyph_infos();
let positions = output.glyph_positions();
let byte_to_char: std::collections::HashMap<usize, usize> = text
.char_indices()
.enumerate()
.map(|(ci, (bi, _))| (bi, ci))
.collect();
let glyphs = infos
.iter()
.zip(positions.iter())
.map(|(info, pos)| {
let char_idx = byte_to_char
.get(&(info.cluster as usize))
.copied()
.unwrap_or(0);
ShapedGlyph {
glyph_id: info.glyph_id as u16,
cluster: char_idx as u32,
x_advance: pos.x_advance,
y_advance: pos.y_advance,
x_offset: pos.x_offset,
y_offset: pos.y_offset,
}
})
.collect();
Some(glyphs)
}
pub fn shape_text_with_offset(
text: &str,
font_data: &[u8],
char_offset: u32,
) -> Option<Vec<ShapedGlyph>> {
let mut glyphs = shape_text(text, font_data)?;
for g in &mut glyphs {
g.cluster += char_offset;
}
Some(glyphs)
}
pub fn shaped_width(glyphs: &[ShapedGlyph], units_per_em: u16, font_size: f64) -> f64 {
let scale = font_size / units_per_em as f64;
glyphs.iter().map(|g| g.x_advance as f64 * scale).sum()
}
pub fn cluster_widths(
glyphs: &[ShapedGlyph],
num_chars: usize,
units_per_em: u16,
font_size: f64,
letter_spacing: f64,
) -> Vec<f64> {
let scale = font_size / units_per_em as f64;
let mut widths = vec![0.0_f64; num_chars];
for glyph in glyphs {
let cluster = glyph.cluster as usize;
if cluster < num_chars {
widths[cluster] += glyph.x_advance as f64 * scale + letter_spacing;
}
}
if !glyphs.is_empty() {
let mut cluster_starts: Vec<bool> = vec![false; num_chars];
for glyph in glyphs {
let c = glyph.cluster as usize;
if c < num_chars {
cluster_starts[c] = true;
}
}
let mut in_ligature = false;
for i in 0..num_chars {
if cluster_starts[i] {
in_ligature = false;
} else if i > 0 {
in_ligature = true;
}
if in_ligature {
widths[i] = 0.0;
}
}
}
widths
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_shape_text_invalid_font() {
let result = shape_text("Hello", &[0, 1, 2, 3]);
assert!(result.is_none());
}
#[test]
fn test_shape_text_empty() {
let result = shape_text("", &[0, 1, 2, 3]);
assert!(result.is_none());
}
#[test]
fn test_shaped_width_empty() {
let width = shaped_width(&[], 1000, 12.0);
assert_eq!(width, 0.0);
}
#[test]
fn test_cluster_widths_empty() {
let widths = cluster_widths(&[], 0, 1000, 12.0, 0.0);
assert!(widths.is_empty());
}
#[test]
fn test_cluster_widths_basic() {
let glyphs = vec![
ShapedGlyph {
glyph_id: 1,
cluster: 0,
x_advance: 500,
y_advance: 0,
x_offset: 0,
y_offset: 0,
},
ShapedGlyph {
glyph_id: 2,
cluster: 1,
x_advance: 600,
y_advance: 0,
x_offset: 0,
y_offset: 0,
},
ShapedGlyph {
glyph_id: 3,
cluster: 2,
x_advance: 500,
y_advance: 0,
x_offset: 0,
y_offset: 0,
},
];
let widths = cluster_widths(&glyphs, 3, 1000, 10.0, 0.0);
assert_eq!(widths.len(), 3);
assert!((widths[0] - 5.0).abs() < 0.001); assert!((widths[1] - 6.0).abs() < 0.001); assert!((widths[2] - 5.0).abs() < 0.001);
}
#[test]
fn test_cluster_widths_ligature() {
let glyphs = vec![
ShapedGlyph {
glyph_id: 100,
cluster: 0,
x_advance: 800,
y_advance: 0,
x_offset: 0,
y_offset: 0,
},
ShapedGlyph {
glyph_id: 3,
cluster: 2,
x_advance: 500,
y_advance: 0,
x_offset: 0,
y_offset: 0,
},
];
let widths = cluster_widths(&glyphs, 3, 1000, 10.0, 0.0);
assert_eq!(widths.len(), 3);
assert!((widths[0] - 8.0).abs() < 0.001); assert!((widths[1] - 0.0).abs() < 0.001); assert!((widths[2] - 5.0).abs() < 0.001);
}
}