use std::collections::HashMap;
use std::path::PathBuf;
use ttf_parser::OutlineBuilder;
#[derive(Clone, Debug)]
enum Seg {
Line([f32; 2]),
Quad([f32; 2], [f32; 2]), Cubic([f32; 2], [f32; 2], [f32; 2]), }
#[derive(Clone, Debug, Default)]
struct Contour {
start: [f32; 2],
segs: Vec<Seg>,
}
#[derive(Clone, Debug, Default)]
struct Glyph {
contours: Vec<Contour>,
advance: f32,
}
#[derive(Clone)]
pub struct GlyphOutline {
pub polylines: Vec<Vec<[f32; 2]>>,
pub advance: f32,
}
#[derive(Default)]
struct Collector {
contours: Vec<Contour>,
cur: Option<Contour>,
cp: [f32; 2],
}
impl Collector {
fn finish_cur(&mut self) {
if let Some(c) = self.cur.take() {
if !c.segs.is_empty() {
self.contours.push(c);
}
}
}
}
impl OutlineBuilder for Collector {
fn move_to(&mut self, x: f32, y: f32) {
self.finish_cur();
self.cp = [x, y];
self.cur = Some(Contour { start: [x, y], segs: Vec::new() });
}
fn line_to(&mut self, x: f32, y: f32) {
if let Some(c) = &mut self.cur {
c.segs.push(Seg::Line([x, y]));
}
self.cp = [x, y];
}
fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
if let Some(c) = &mut self.cur {
c.segs.push(Seg::Quad([x1, y1], [x, y]));
}
self.cp = [x, y];
}
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
if let Some(c) = &mut self.cur {
c.segs.push(Seg::Cubic([x1, y1], [x2, y2], [x, y]));
}
self.cp = [x, y];
}
fn close(&mut self) {
self.finish_cur();
}
}
fn unit(d: [f32; 2]) -> Option<[f32; 2]> {
let l = (d[0] * d[0] + d[1] * d[1]).sqrt();
if l < 1e-9 {
None
} else {
Some([d[0] / l, d[1] / l])
}
}
fn compress_contour(start: [f32; 2], segs: &[Seg]) -> Vec<Seg> {
let mut out: Vec<Seg> = Vec::with_capacity(segs.len());
let mut cp = start; let mut line_a: Option<[f32; 2]> = None; for seg in segs {
match seg {
Seg::Line(p) => {
if let (Some(a), Some(Seg::Line(_))) = (line_a, out.last()) {
let d1 = unit([cp[0] - a[0], cp[1] - a[1]]);
let d2 = unit([p[0] - cp[0], p[1] - cp[1]]);
if let (Some(d1), Some(d2)) = (d1, d2) {
let cross = (d1[0] * d2[1] - d1[1] * d2[0]).abs();
let dot = d1[0] * d2[0] + d1[1] * d2[1];
if cross < 2.0e-3 && dot > 0.0 {
*out.last_mut().unwrap() = Seg::Line(*p); cp = *p;
continue;
}
}
}
out.push(Seg::Line(*p));
line_a = Some(cp);
cp = *p;
},
Seg::Quad(c, p) => {
out.push(Seg::Quad(*c, *p));
cp = *p;
line_a = None;
},
Seg::Cubic(a, b, p) => {
out.push(Seg::Cubic(*a, *b, *p));
cp = *p;
line_a = None;
},
}
}
out
}
fn flat_quad(p0: [f32; 2], c: [f32; 2], p1: [f32; 2], tol: f32, out: &mut Vec<[f32; 2]>) {
let dx = p1[0] - p0[0];
let dy = p1[1] - p0[1];
let d = ((c[0] - p0[0]) * dy - (c[1] - p0[1]) * dx).abs();
let chord2 = dx * dx + dy * dy;
if d * d <= tol * tol * chord2 || chord2 < 1e-12 {
out.push(p1);
return;
}
let m = |a: [f32; 2], b: [f32; 2]| [(a[0] + b[0]) * 0.5, (a[1] + b[1]) * 0.5];
let p01 = m(p0, c);
let p12 = m(c, p1);
let mid = m(p01, p12);
flat_quad(p0, p01, mid, tol, out);
flat_quad(mid, p12, p1, tol, out);
}
fn flat_cubic(
p0: [f32; 2],
c1: [f32; 2],
c2: [f32; 2],
p1: [f32; 2],
tol: f32,
out: &mut Vec<[f32; 2]>,
) {
let dx = p1[0] - p0[0];
let dy = p1[1] - p0[1];
let d1 = ((c1[0] - p0[0]) * dy - (c1[1] - p0[1]) * dx).abs();
let d2 = ((c2[0] - p0[0]) * dy - (c2[1] - p0[1]) * dx).abs();
let chord2 = dx * dx + dy * dy;
if (d1 + d2) * (d1 + d2) <= tol * tol * chord2 || chord2 < 1e-12 {
out.push(p1);
return;
}
let m = |a: [f32; 2], b: [f32; 2]| [(a[0] + b[0]) * 0.5, (a[1] + b[1]) * 0.5];
let p01 = m(p0, c1);
let p12 = m(c1, c2);
let p23 = m(c2, p1);
let p012 = m(p01, p12);
let p123 = m(p12, p23);
let mid = m(p012, p123);
flat_cubic(p0, p01, p012, mid, tol, out);
flat_cubic(mid, p123, p23, p1, tol, out);
}
pub struct VectorFont {
bytes: Vec<u8>,
name: String,
upm: f32,
ascent: f32, descent: f32, weight: Option<f32>,
cache_dir: PathBuf,
glyphs: HashMap<char, Glyph>,
outline_cache: HashMap<(char, u32), GlyphOutline>,
}
impl VectorFont {
pub fn from_path(path: &str) -> Result<Self, String> {
Self::from_path_weight(path, None)
}
pub fn from_path_weight(path: &str, weight: Option<f32>) -> Result<Self, String> {
let bytes = std::fs::read(path).map_err(|e| format!("{path}: {e}"))?;
let name = std::path::Path::new(path)
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "font".into());
Self::from_bytes(bytes, &name, weight)
}
pub fn from_bytes(bytes: Vec<u8>, name: &str, weight: Option<f32>) -> Result<Self, String> {
let face = ttf_parser::Face::parse(&bytes, 0).map_err(|e| format!("{e:?}"))?;
let upm = face.units_per_em() as f32;
let ascent = face.ascender() as f32 / upm;
let descent = face.descender() as f32 / upm;
let dir = match weight {
Some(w) => format!("{name}@{}", w as i32),
None => name.to_string(),
};
let cache_dir = PathBuf::from("cache").join("fonts").join(dir);
Ok(Self {
bytes,
name: name.to_string(),
upm,
ascent,
descent,
weight,
cache_dir,
glyphs: HashMap::new(),
outline_cache: HashMap::new(),
})
}
pub fn ascent(&self) -> f32 {
self.ascent
}
pub fn descent(&self) -> f32 {
self.descent
}
fn ensure(&mut self, ch: char) {
if self.glyphs.contains_key(&ch) {
return;
}
let file = self.cache_dir.join(format!("{}.ling", ch as u32));
if let Ok(text) = std::fs::read_to_string(&file) {
if let Some(g) = parse_glyph_ling(&text) {
self.glyphs.insert(ch, g);
return;
}
}
let g = self.extract(ch);
let _ = std::fs::create_dir_all(&self.cache_dir);
let _ = std::fs::write(&file, serialize_glyph_ling(&self.name, ch, &g));
self.glyphs.insert(ch, g);
}
fn extract(&self, ch: char) -> Glyph {
let mut face = match ttf_parser::Face::parse(&self.bytes, 0) {
Ok(f) => f,
Err(_) => return Glyph { contours: vec![], advance: 0.5 },
};
if let Some(w) = self.weight {
let _ = face.set_variation(ttf_parser::Tag::from_bytes(b"wght"), w);
}
let gid = match face.glyph_index(ch) {
Some(g) => g,
None => return Glyph { contours: vec![], advance: 0.5 },
};
let advance = face
.glyph_hor_advance(gid)
.map(|a| a as f32 / self.upm)
.unwrap_or(0.5);
let mut col = Collector::default();
face.outline_glyph(gid, &mut col);
col.finish_cur();
let upm = self.upm;
let n = |p: [f32; 2]| [p[0] / upm, p[1] / upm];
let contours = col
.contours
.into_iter()
.map(|c| {
let start = n(c.start);
let segs: Vec<Seg> = c
.segs
.iter()
.map(|s| match s {
Seg::Line(p) => Seg::Line(n(*p)),
Seg::Quad(a, p) => Seg::Quad(n(*a), n(*p)),
Seg::Cubic(a, b, p) => Seg::Cubic(n(*a), n(*b), n(*p)),
})
.collect();
let segs = compress_contour(start, &segs);
Contour { start, segs }
})
.collect();
Glyph { contours, advance }
}
pub fn advance(&mut self, ch: char) -> f32 {
self.ensure(ch);
self.glyphs[&ch].advance
}
pub fn measure(&mut self, text: &str, px: f32) -> f32 {
text.chars().map(|c| self.advance(c)).sum::<f32>() * px
}
pub fn glyph_outline(&mut self, ch: char, tol_em: f32) -> GlyphOutline {
let tol = tol_em.max(1e-5);
let key = (ch, (tol * 100_000.0) as u32);
if let Some(o) = self.outline_cache.get(&key) {
return o.clone();
}
self.ensure(ch);
let g = &self.glyphs[&ch];
let mut polylines = Vec::with_capacity(g.contours.len());
for c in &g.contours {
let mut pl = Vec::new();
let mut cur = c.start;
pl.push(cur);
for s in &c.segs {
match s {
Seg::Line(p) => {
pl.push(*p);
cur = *p;
},
Seg::Quad(ctrl, p) => {
flat_quad(cur, *ctrl, *p, tol, &mut pl);
cur = *p;
},
Seg::Cubic(a, b, p) => {
flat_cubic(cur, *a, *b, *p, tol, &mut pl);
cur = *p;
},
}
}
if pl.len() > 1 {
pl.push(c.start);
}
polylines.push(pl);
}
let out = GlyphOutline { polylines, advance: g.advance };
self.outline_cache.insert(key, out.clone());
out
}
}
fn serialize_glyph_ling(font: &str, ch: char, g: &Glyph) -> String {
let mut s = String::new();
s.push_str(&format!(
"# ling glyph — font={font} cp={} char={} adv={:.4}\n",
ch as u32, ch, g.advance
));
for c in &g.contours {
s.push_str(&format!("M {:.4} {:.4}\n", c.start[0], c.start[1]));
for seg in &c.segs {
match seg {
Seg::Line(p) => s.push_str(&format!("L {:.4} {:.4}\n", p[0], p[1])),
Seg::Quad(a, p) => s.push_str(&format!(
"Q {:.4} {:.4} {:.4} {:.4}\n",
a[0], a[1], p[0], p[1]
)),
Seg::Cubic(a, b, p) => s.push_str(&format!(
"C {:.4} {:.4} {:.4} {:.4} {:.4} {:.4}\n",
a[0], a[1], b[0], b[1], p[0], p[1]
)),
}
}
s.push_str("Z\n");
}
s
}
fn parse_glyph_ling(text: &str) -> Option<Glyph> {
let mut advance = 0.5f32;
let mut contours: Vec<Contour> = Vec::new();
let mut cur: Option<Contour> = None;
for line in text.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
if let Some(rest) = line.strip_prefix('#') {
if let Some(i) = rest.find("adv=") {
if let Ok(v) = rest[i + 4..]
.split_whitespace()
.next()
.unwrap_or("")
.parse::<f32>()
{
advance = v;
}
}
continue;
}
let mut it = line.split_whitespace();
let op = it.next()?;
let nums: Vec<f32> = it.filter_map(|t| t.parse::<f32>().ok()).collect();
match op {
"M" => {
if let Some(c) = cur.take() {
contours.push(c);
}
cur = Some(Contour { start: [*nums.first()?, *nums.get(1)?], segs: Vec::new() });
},
"L" => {
if let Some(c) = &mut cur {
c.segs.push(Seg::Line([*nums.first()?, *nums.get(1)?]));
}
},
"Q" => {
if let Some(c) = &mut cur {
c.segs.push(Seg::Quad(
[*nums.first()?, *nums.get(1)?],
[*nums.get(2)?, *nums.get(3)?],
));
}
},
"C" => {
if let Some(c) = &mut cur {
c.segs.push(Seg::Cubic(
[*nums.first()?, *nums.get(1)?],
[*nums.get(2)?, *nums.get(3)?],
[*nums.get(4)?, *nums.get(5)?],
));
}
},
"Z" => {
if let Some(c) = cur.take() {
contours.push(c);
}
},
_ => {},
}
}
if let Some(c) = cur.take() {
contours.push(c);
}
Some(Glyph { contours, advance })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn collinear_lines_merge() {
let segs = vec![
Seg::Line([0.5, 0.0]),
Seg::Line([1.0, 0.0]),
Seg::Line([1.0, 1.0]),
];
let out = compress_contour([0.0, 0.0], &segs);
assert_eq!(out.len(), 2);
match out[0] {
Seg::Line(p) => assert_eq!(p, [1.0, 0.0]),
_ => panic!(),
}
}
#[test]
fn glyph_ling_roundtrips() {
let g = Glyph {
advance: 0.6,
contours: vec![Contour {
start: [0.1, 0.0],
segs: vec![
Seg::Line([0.4, 0.7]),
Seg::Quad([0.5, 0.8], [0.6, 0.7]),
Seg::Line([0.9, 0.0]),
],
}],
};
let text = serialize_glyph_ling("Test", 'A', &g);
let back = parse_glyph_ling(&text).unwrap();
assert!((back.advance - 0.6).abs() < 1e-3);
assert_eq!(back.contours.len(), 1);
assert_eq!(back.contours[0].segs.len(), 3);
assert!(matches!(back.contours[0].segs[1], Seg::Quad(..)));
}
}