use ttf_parser::{Face, GlyphId, OutlineBuilder};
#[derive(Debug, Clone)]
pub enum PathCommand {
MoveTo {
x: f32,
y: f32,
},
LineTo {
x: f32,
y: f32,
},
QuadTo {
x1: f32,
y1: f32,
x: f32,
y: f32,
},
CubicTo {
x1: f32,
y1: f32,
x2: f32,
y2: f32,
x: f32,
y: f32,
},
Close,
}
#[derive(Debug, Clone, Default)]
pub struct GlyphOutline {
pub commands: Vec<PathCommand>,
pub bounding_box: Option<(f32, f32, f32, f32)>,
}
pub fn extract_glyph_outline(face_data: &[u8], glyph_id: u16, scale: f32) -> Option<GlyphOutline> {
let face = Face::parse(face_data, 0).ok()?;
let gid = GlyphId(glyph_id);
let mut builder = ScaledOutlineBuilder::new(scale);
let _rect = face.outline_glyph(gid, &mut builder)?;
if builder.commands.is_empty() {
return None;
}
let bbox = compute_bbox(&builder.commands);
Some(GlyphOutline {
commands: builder.commands,
bounding_box: bbox,
})
}
struct ScaledOutlineBuilder {
scale: f32,
commands: Vec<PathCommand>,
}
impl ScaledOutlineBuilder {
fn new(scale: f32) -> Self {
Self {
scale,
commands: Vec::new(),
}
}
#[inline]
fn sx(&self, v: f32) -> f32 {
v * self.scale
}
#[inline]
fn sy(&self, v: f32) -> f32 {
v * -self.scale
}
}
impl OutlineBuilder for ScaledOutlineBuilder {
fn move_to(&mut self, x: f32, y: f32) {
self.commands.push(PathCommand::MoveTo {
x: self.sx(x),
y: self.sy(y),
});
}
fn line_to(&mut self, x: f32, y: f32) {
self.commands.push(PathCommand::LineTo {
x: self.sx(x),
y: self.sy(y),
});
}
fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
self.commands.push(PathCommand::QuadTo {
x1: self.sx(x1),
y1: self.sy(y1),
x: self.sx(x),
y: self.sy(y),
});
}
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
self.commands.push(PathCommand::CubicTo {
x1: self.sx(x1),
y1: self.sy(y1),
x2: self.sx(x2),
y2: self.sy(y2),
x: self.sx(x),
y: self.sy(y),
});
}
fn close(&mut self) {
self.commands.push(PathCommand::Close);
}
}
fn compute_bbox(commands: &[PathCommand]) -> Option<(f32, f32, f32, f32)> {
let mut x_min = f32::INFINITY;
let mut y_min = f32::INFINITY;
let mut x_max = f32::NEG_INFINITY;
let mut y_max = f32::NEG_INFINITY;
for cmd in commands {
let coords: &[f32] = match cmd {
PathCommand::MoveTo { x, y } => &[*x, *y],
PathCommand::LineTo { x, y } => &[*x, *y],
PathCommand::QuadTo { x1, y1, x, y } => &[*x1, *y1, *x, *y],
PathCommand::CubicTo {
x1,
y1,
x2,
y2,
x,
y,
} => &[*x1, *y1, *x2, *y2, *x, *y],
PathCommand::Close => &[],
};
let mut i = 0;
while i + 1 < coords.len() {
let cx = coords[i];
let cy = coords[i + 1];
if cx < x_min {
x_min = cx;
}
if cx > x_max {
x_max = cx;
}
if cy < y_min {
y_min = cy;
}
if cy > y_max {
y_max = cy;
}
i += 2;
}
}
if x_min.is_infinite() {
None
} else {
Some((x_min, y_min, x_max, y_max))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bbox_none_for_no_coords() {
let commands = vec![PathCommand::Close];
assert!(compute_bbox(&commands).is_none());
}
#[test]
fn bbox_single_point() {
let commands = vec![PathCommand::MoveTo { x: 1.0, y: 2.0 }];
let bbox = compute_bbox(&commands);
assert!(bbox.is_some());
let (x0, y0, x1, y1) = bbox.unwrap();
assert!((x0 - 1.0).abs() < 1e-6);
assert!((y0 - 2.0).abs() < 1e-6);
assert!((x1 - 1.0).abs() < 1e-6);
assert!((y1 - 2.0).abs() < 1e-6);
}
#[test]
fn outline_none_for_empty_data() {
assert!(extract_glyph_outline(&[], 0, 1.0).is_none());
}
}