use skrifa::raw::tables::glyf::{CurvePoint, SimpleGlyph};
use skrifa::raw::{FontData, FontRead};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DecodeError {
Composite,
Hinted,
Malformed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Point {
pub x: i32,
pub y: i32,
pub on_curve: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Outline {
pub contours: Vec<Vec<Point>>,
pub x_min: i32,
pub y_min: i32,
pub x_max: i32,
pub y_max: i32,
}
pub fn decode(data: &[u8]) -> Result<Outline, DecodeError> {
if data.len() < 10 {
return Err(DecodeError::Malformed);
}
let num_contours = i16::from_be_bytes([data[0], data[1]]);
if num_contours < 0 {
return Err(DecodeError::Composite);
}
let glyph =
SimpleGlyph::read(FontData::new(data)).map_err(|_| DecodeError::Malformed)?;
if glyph.instruction_length() != 0 {
return Err(DecodeError::Hinted);
}
let x_min = glyph.x_min() as i32;
let y_min = glyph.y_min() as i32;
let x_max = glyph.x_max() as i32;
let y_max = glyph.y_max() as i32;
let end_pts: Vec<usize> = glyph
.end_pts_of_contours()
.iter()
.map(|v| v.get() as usize)
.collect();
if end_pts.is_empty() {
return Ok(Outline {
contours: Vec::new(),
x_min,
y_min,
x_max,
y_max,
});
}
for w in end_pts.windows(2) {
if w[1] <= w[0] {
return Err(DecodeError::Malformed);
}
}
let num_points = end_pts[end_pts.len() - 1] + 1;
let pts: Vec<CurvePoint> = glyph.points().collect();
if pts.len() != num_points {
return Err(DecodeError::Malformed);
}
let mut contours = Vec::with_capacity(end_pts.len());
let mut start = 0usize;
for &end in &end_pts {
let mut contour = Vec::with_capacity(end - start + 1);
for p in &pts[start..=end] {
contour.push(Point {
x: p.x as i32,
y: p.y as i32,
on_curve: p.on_curve,
});
}
contours.push(contour);
start = end + 1;
}
Ok(Outline {
contours,
x_min,
y_min,
x_max,
y_max,
})
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PathCmd {
MoveTo { x: f32, y: f32 },
LineTo { x: f32, y: f32 },
QuadTo { cx: f32, cy: f32, x: f32, y: f32 },
Close,
}
impl Outline {
pub fn walk(&self, upm: u16, pixel_size: f32) -> Vec<PathCmd> {
if self.contours.is_empty() || upm == 0 {
return Vec::new();
}
let scale = pixel_size / upm as f32;
let fx = |x: i32| x as f32 * scale;
let y_base = self.y_max as f32 * scale;
let fy = |y: i32| y_base - y as f32 * scale;
let mut out = Vec::new();
for contour in &self.contours {
walk_contour(contour, &fx, &fy, &mut out);
}
out
}
}
fn walk_contour(
pts: &[Point],
fx: &impl Fn(i32) -> f32,
fy: &impl Fn(i32) -> f32,
out: &mut Vec<PathCmd>,
) {
if pts.is_empty() {
return;
}
let n = pts.len();
let (start_x, start_y, start_idx, steps) = if pts[0].on_curve {
(fx(pts[0].x), fy(pts[0].y), 1usize, n - 1)
} else if pts[n - 1].on_curve {
(fx(pts[n - 1].x), fy(pts[n - 1].y), 0usize, n - 1)
} else {
let mx = (fx(pts[0].x) + fx(pts[n - 1].x)) / 2.0;
let my = (fy(pts[0].y) + fy(pts[n - 1].y)) / 2.0;
(mx, my, 0usize, n)
};
out.push(PathCmd::MoveTo {
x: start_x,
y: start_y,
});
let mut i = start_idx;
let mut pending_off: Option<(f32, f32)> = None;
let mut visited = 0;
while visited < steps {
let p = pts[i];
let px = fx(p.x);
let py = fy(p.y);
if p.on_curve {
match pending_off.take() {
Some((cx, cy)) => out.push(PathCmd::QuadTo {
cx,
cy,
x: px,
y: py,
}),
None => out.push(PathCmd::LineTo { x: px, y: py }),
}
} else {
match pending_off.take() {
Some((cx, cy)) => {
let mx = (cx + px) / 2.0;
let my = (cy + py) / 2.0;
out.push(PathCmd::QuadTo {
cx,
cy,
x: mx,
y: my,
});
pending_off = Some((px, py));
}
None => {
pending_off = Some((px, py));
}
}
}
i = (i + 1) % n;
visited += 1;
}
if let Some((cx, cy)) = pending_off.take() {
out.push(PathCmd::QuadTo {
cx,
cy,
x: start_x,
y: start_y,
});
}
out.push(PathCmd::Close);
}
#[cfg(test)]
mod tests {
use super::*;
const FLAG_ON_CURVE: u8 = 0x01;
const FLAG_REPEAT: u8 = 0x08;
fn triangle_bytes() -> Vec<u8> {
let mut v = Vec::new();
v.extend_from_slice(&1i16.to_be_bytes()); v.extend_from_slice(&100i16.to_be_bytes()); v.extend_from_slice(&100i16.to_be_bytes()); v.extend_from_slice(&900i16.to_be_bytes()); v.extend_from_slice(&900i16.to_be_bytes()); v.extend_from_slice(&2u16.to_be_bytes()); v.extend_from_slice(&0u16.to_be_bytes()); v.push(FLAG_ON_CURVE);
v.push(FLAG_ON_CURVE);
v.push(FLAG_ON_CURVE);
v.extend_from_slice(&500i16.to_be_bytes());
v.extend_from_slice(&(-400i16).to_be_bytes());
v.extend_from_slice(&800i16.to_be_bytes());
v.extend_from_slice(&900i16.to_be_bytes());
v.extend_from_slice(&(-800i16).to_be_bytes());
v.extend_from_slice(&0i16.to_be_bytes());
v
}
#[test]
fn decodes_triangle() {
let out = decode(&triangle_bytes()).unwrap();
assert_eq!(out.contours.len(), 1);
let c = &out.contours[0];
assert_eq!(c.len(), 3);
assert_eq!(
c[0],
Point {
x: 500,
y: 900,
on_curve: true
}
);
assert_eq!(
c[1],
Point {
x: 100,
y: 100,
on_curve: true
}
);
assert_eq!(
c[2],
Point {
x: 900,
y: 100,
on_curve: true
}
);
}
#[test]
fn rejects_composite() {
let mut v = Vec::new();
v.extend_from_slice(&(-1i16).to_be_bytes());
v.extend_from_slice(&[0u8; 8]);
assert_eq!(decode(&v), Err(DecodeError::Composite));
}
#[test]
fn rejects_hinting() {
let mut v = Vec::new();
v.extend_from_slice(&1i16.to_be_bytes()); v.extend_from_slice(&[0u8; 8]); v.extend_from_slice(&0u16.to_be_bytes()); v.extend_from_slice(&1u16.to_be_bytes()); v.push(0x00); v.push(FLAG_ON_CURVE); v.extend_from_slice(&0i16.to_be_bytes()); v.extend_from_slice(&0i16.to_be_bytes()); assert_eq!(decode(&v), Err(DecodeError::Hinted));
}
#[test]
fn rejects_truncated() {
assert_eq!(decode(&[]), Err(DecodeError::Malformed));
let mut v = Vec::new();
v.extend_from_slice(&1i16.to_be_bytes());
v.extend_from_slice(&[0u8; 8]);
assert_eq!(decode(&v), Err(DecodeError::Malformed));
}
#[test]
fn handles_repeat_flag() {
let mut v = Vec::new();
v.extend_from_slice(&1i16.to_be_bytes());
v.extend_from_slice(&[0u8; 8]);
v.extend_from_slice(&3u16.to_be_bytes()); v.extend_from_slice(&0u16.to_be_bytes()); v.push(FLAG_ON_CURVE | FLAG_REPEAT);
v.push(3); for dx in [10i16, 10, 10, 10] {
v.extend_from_slice(&dx.to_be_bytes());
}
for dy in [0i16, 10, 0, -10] {
v.extend_from_slice(&dy.to_be_bytes());
}
let out = decode(&v).unwrap();
assert_eq!(out.contours[0].len(), 4);
assert_eq!(out.contours[0][3].x, 40);
assert_eq!(out.contours[0][3].y, 0);
}
#[test]
fn walk_triangle_produces_move_line_line_close() {
let out = decode(&triangle_bytes()).unwrap();
let cmds = out.walk(1000, 100.0);
assert!(matches!(cmds[0], PathCmd::MoveTo { .. }));
assert_eq!(cmds.len(), 4);
assert!(matches!(cmds[1], PathCmd::LineTo { .. }));
assert!(matches!(cmds[2], PathCmd::LineTo { .. }));
assert!(matches!(cmds[3], PathCmd::Close));
}
#[test]
fn walk_handles_two_off_curves_in_a_row() {
let mut v = Vec::new();
v.extend_from_slice(&1i16.to_be_bytes());
v.extend_from_slice(&[0u8; 8]);
v.extend_from_slice(&3u16.to_be_bytes());
v.extend_from_slice(&0u16.to_be_bytes());
v.push(FLAG_ON_CURVE);
v.push(0);
v.push(0);
v.push(FLAG_ON_CURVE);
for dx in [0i16, 100, 100, 0] {
v.extend_from_slice(&dx.to_be_bytes());
}
for dy in [0i16, 100, -100, -100] {
v.extend_from_slice(&dy.to_be_bytes());
}
let out = decode(&v).unwrap();
let cmds = out.walk(1000, 1000.0);
assert!(cmds.iter().any(|c| matches!(c, PathCmd::QuadTo { .. })));
assert!(matches!(cmds[0], PathCmd::MoveTo { .. }));
assert!(matches!(cmds.last().unwrap(), PathCmd::Close));
}
#[test]
fn scale_and_flip_are_correct() {
let out = decode(&triangle_bytes()).unwrap();
let cmds = out.walk(1000, 100.0);
if let PathCmd::MoveTo { x, y } = cmds[0] {
assert!((x - 50.0).abs() < 1e-3);
assert!((y - 0.0).abs() < 1e-3);
} else {
panic!("expected MoveTo");
}
}
}