1use std::collections::HashMap;
24use std::path::PathBuf;
25use ttf_parser::OutlineBuilder;
26
27#[derive(Clone, Debug)]
30enum Seg {
31 Line([f32; 2]),
32 Quad([f32; 2], [f32; 2]), Cubic([f32; 2], [f32; 2], [f32; 2]), }
35
36#[derive(Clone, Debug, Default)]
37struct Contour {
38 start: [f32; 2],
39 segs: Vec<Seg>,
40}
41
42#[derive(Clone, Debug, Default)]
43struct Glyph {
44 contours: Vec<Contour>,
45 advance: f32,
46}
47
48#[derive(Clone)]
52pub struct GlyphOutline {
53 pub polylines: Vec<Vec<[f32; 2]>>,
54 pub advance: f32,
55}
56
57#[derive(Default)]
60struct Collector {
61 contours: Vec<Contour>,
62 cur: Option<Contour>,
63 cp: [f32; 2],
64}
65
66impl Collector {
67 fn finish_cur(&mut self) {
68 if let Some(c) = self.cur.take() {
69 if !c.segs.is_empty() {
70 self.contours.push(c);
71 }
72 }
73 }
74}
75
76impl OutlineBuilder for Collector {
77 fn move_to(&mut self, x: f32, y: f32) {
78 self.finish_cur();
79 self.cp = [x, y];
80 self.cur = Some(Contour { start: [x, y], segs: Vec::new() });
81 }
82
83 fn line_to(&mut self, x: f32, y: f32) {
84 if let Some(c) = &mut self.cur {
85 c.segs.push(Seg::Line([x, y]));
86 }
87 self.cp = [x, y];
88 }
89
90 fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
91 if let Some(c) = &mut self.cur {
92 c.segs.push(Seg::Quad([x1, y1], [x, y]));
93 }
94 self.cp = [x, y];
95 }
96
97 fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
98 if let Some(c) = &mut self.cur {
99 c.segs.push(Seg::Cubic([x1, y1], [x2, y2], [x, y]));
100 }
101 self.cp = [x, y];
102 }
103
104 fn close(&mut self) {
105 self.finish_cur();
106 }
107}
108
109fn unit(d: [f32; 2]) -> Option<[f32; 2]> {
112 let l = (d[0] * d[0] + d[1] * d[1]).sqrt();
113 if l < 1e-9 {
114 None
115 } else {
116 Some([d[0] / l, d[1] / l])
117 }
118}
119
120fn compress_contour(start: [f32; 2], segs: &[Seg]) -> Vec<Seg> {
123 let mut out: Vec<Seg> = Vec::with_capacity(segs.len());
124 let mut cp = start; let mut line_a: Option<[f32; 2]> = None; for seg in segs {
127 match seg {
128 Seg::Line(p) => {
129 if let (Some(a), Some(Seg::Line(_))) = (line_a, out.last()) {
130 let d1 = unit([cp[0] - a[0], cp[1] - a[1]]);
131 let d2 = unit([p[0] - cp[0], p[1] - cp[1]]);
132 if let (Some(d1), Some(d2)) = (d1, d2) {
133 let cross = (d1[0] * d2[1] - d1[1] * d2[0]).abs();
134 let dot = d1[0] * d2[0] + d1[1] * d2[1];
135 if cross < 2.0e-3 && dot > 0.0 {
136 *out.last_mut().unwrap() = Seg::Line(*p); cp = *p;
138 continue;
139 }
140 }
141 }
142 out.push(Seg::Line(*p));
143 line_a = Some(cp);
144 cp = *p;
145 },
146 Seg::Quad(c, p) => {
147 out.push(Seg::Quad(*c, *p));
148 cp = *p;
149 line_a = None;
150 },
151 Seg::Cubic(a, b, p) => {
152 out.push(Seg::Cubic(*a, *b, *p));
153 cp = *p;
154 line_a = None;
155 },
156 }
157 }
158 out
159}
160
161fn flat_quad(p0: [f32; 2], c: [f32; 2], p1: [f32; 2], tol: f32, out: &mut Vec<[f32; 2]>) {
164 let dx = p1[0] - p0[0];
166 let dy = p1[1] - p0[1];
167 let d = ((c[0] - p0[0]) * dy - (c[1] - p0[1]) * dx).abs();
168 let chord2 = dx * dx + dy * dy;
169 if d * d <= tol * tol * chord2 || chord2 < 1e-12 {
170 out.push(p1);
171 return;
172 }
173 let m = |a: [f32; 2], b: [f32; 2]| [(a[0] + b[0]) * 0.5, (a[1] + b[1]) * 0.5];
174 let p01 = m(p0, c);
175 let p12 = m(c, p1);
176 let mid = m(p01, p12);
177 flat_quad(p0, p01, mid, tol, out);
178 flat_quad(mid, p12, p1, tol, out);
179}
180
181fn flat_cubic(
182 p0: [f32; 2],
183 c1: [f32; 2],
184 c2: [f32; 2],
185 p1: [f32; 2],
186 tol: f32,
187 out: &mut Vec<[f32; 2]>,
188) {
189 let dx = p1[0] - p0[0];
190 let dy = p1[1] - p0[1];
191 let d1 = ((c1[0] - p0[0]) * dy - (c1[1] - p0[1]) * dx).abs();
192 let d2 = ((c2[0] - p0[0]) * dy - (c2[1] - p0[1]) * dx).abs();
193 let chord2 = dx * dx + dy * dy;
194 if (d1 + d2) * (d1 + d2) <= tol * tol * chord2 || chord2 < 1e-12 {
195 out.push(p1);
196 return;
197 }
198 let m = |a: [f32; 2], b: [f32; 2]| [(a[0] + b[0]) * 0.5, (a[1] + b[1]) * 0.5];
199 let p01 = m(p0, c1);
200 let p12 = m(c1, c2);
201 let p23 = m(c2, p1);
202 let p012 = m(p01, p12);
203 let p123 = m(p12, p23);
204 let mid = m(p012, p123);
205 flat_cubic(p0, p01, p012, mid, tol, out);
206 flat_cubic(mid, p123, p23, p1, tol, out);
207}
208
209pub struct VectorFont {
212 bytes: Vec<u8>,
213 name: String,
214 upm: f32,
215 ascent: f32, descent: f32, weight: Option<f32>,
220 cache_dir: PathBuf,
221 glyphs: HashMap<char, Glyph>,
222 outline_cache: HashMap<(char, u32), GlyphOutline>,
226}
227
228impl VectorFont {
229 pub fn from_path(path: &str) -> Result<Self, String> {
231 Self::from_path_weight(path, None)
232 }
233
234 pub fn from_path_weight(path: &str, weight: Option<f32>) -> Result<Self, String> {
237 let bytes = std::fs::read(path).map_err(|e| format!("{path}: {e}"))?;
238 let name = std::path::Path::new(path)
239 .file_stem()
240 .map(|s| s.to_string_lossy().into_owned())
241 .unwrap_or_else(|| "font".into());
242 Self::from_bytes(bytes, &name, weight)
243 }
244
245 pub fn from_bytes(bytes: Vec<u8>, name: &str, weight: Option<f32>) -> Result<Self, String> {
246 let face = ttf_parser::Face::parse(&bytes, 0).map_err(|e| format!("{e:?}"))?;
247 let upm = face.units_per_em() as f32;
248 let ascent = face.ascender() as f32 / upm;
249 let descent = face.descender() as f32 / upm;
250 let dir = match weight {
251 Some(w) => format!("{name}@{}", w as i32),
252 None => name.to_string(),
253 };
254 let cache_dir = PathBuf::from("cache").join("fonts").join(dir);
255 Ok(Self {
256 bytes,
257 name: name.to_string(),
258 upm,
259 ascent,
260 descent,
261 weight,
262 cache_dir,
263 glyphs: HashMap::new(),
264 outline_cache: HashMap::new(),
265 })
266 }
267
268 pub fn ascent(&self) -> f32 {
269 self.ascent
270 }
271
272 pub fn descent(&self) -> f32 {
273 self.descent
274 }
275
276 fn ensure(&mut self, ch: char) {
279 if self.glyphs.contains_key(&ch) {
280 return;
281 }
282 let file = self.cache_dir.join(format!("{}.ling", ch as u32));
283 if let Ok(text) = std::fs::read_to_string(&file) {
284 if let Some(g) = parse_glyph_ling(&text) {
285 self.glyphs.insert(ch, g);
286 return;
287 }
288 }
289 let g = self.extract(ch);
290 let _ = std::fs::create_dir_all(&self.cache_dir);
291 let _ = std::fs::write(&file, serialize_glyph_ling(&self.name, ch, &g));
292 self.glyphs.insert(ch, g);
293 }
294
295 fn extract(&self, ch: char) -> Glyph {
297 let mut face = match ttf_parser::Face::parse(&self.bytes, 0) {
298 Ok(f) => f,
299 Err(_) => return Glyph { contours: vec![], advance: 0.5 },
300 };
301 if let Some(w) = self.weight {
303 let _ = face.set_variation(ttf_parser::Tag::from_bytes(b"wght"), w);
304 }
305 let gid = match face.glyph_index(ch) {
306 Some(g) => g,
307 None => return Glyph { contours: vec![], advance: 0.5 },
308 };
309 let advance = face
310 .glyph_hor_advance(gid)
311 .map(|a| a as f32 / self.upm)
312 .unwrap_or(0.5);
313
314 let mut col = Collector::default();
315 face.outline_glyph(gid, &mut col);
316 col.finish_cur();
317
318 let upm = self.upm;
319 let n = |p: [f32; 2]| [p[0] / upm, p[1] / upm];
320 let contours = col
321 .contours
322 .into_iter()
323 .map(|c| {
324 let start = n(c.start);
325 let segs: Vec<Seg> = c
326 .segs
327 .iter()
328 .map(|s| match s {
329 Seg::Line(p) => Seg::Line(n(*p)),
330 Seg::Quad(a, p) => Seg::Quad(n(*a), n(*p)),
331 Seg::Cubic(a, b, p) => Seg::Cubic(n(*a), n(*b), n(*p)),
332 })
333 .collect();
334 let segs = compress_contour(start, &segs);
335 Contour { start, segs }
336 })
337 .collect();
338
339 Glyph { contours, advance }
340 }
341
342 pub fn advance(&mut self, ch: char) -> f32 {
344 self.ensure(ch);
345 self.glyphs[&ch].advance
346 }
347
348 pub fn measure(&mut self, text: &str, px: f32) -> f32 {
350 text.chars().map(|c| self.advance(c)).sum::<f32>() * px
351 }
352
353 pub fn glyph_outline(&mut self, ch: char, tol_em: f32) -> GlyphOutline {
356 let tol = tol_em.max(1e-5);
357 let key = (ch, (tol * 100_000.0) as u32);
360 if let Some(o) = self.outline_cache.get(&key) {
361 return o.clone();
362 }
363 self.ensure(ch);
364 let g = &self.glyphs[&ch];
365 let mut polylines = Vec::with_capacity(g.contours.len());
366 for c in &g.contours {
367 let mut pl = Vec::new();
368 let mut cur = c.start;
369 pl.push(cur);
370 for s in &c.segs {
371 match s {
372 Seg::Line(p) => {
373 pl.push(*p);
374 cur = *p;
375 },
376 Seg::Quad(ctrl, p) => {
377 flat_quad(cur, *ctrl, *p, tol, &mut pl);
378 cur = *p;
379 },
380 Seg::Cubic(a, b, p) => {
381 flat_cubic(cur, *a, *b, *p, tol, &mut pl);
382 cur = *p;
383 },
384 }
385 }
386 if pl.len() > 1 {
388 pl.push(c.start);
389 }
390 polylines.push(pl);
391 }
392 let out = GlyphOutline { polylines, advance: g.advance };
393 self.outline_cache.insert(key, out.clone());
394 out
395 }
396}
397
398fn serialize_glyph_ling(font: &str, ch: char, g: &Glyph) -> String {
401 let mut s = String::new();
402 s.push_str(&format!(
403 "# ling glyph — font={font} cp={} char={} adv={:.4}\n",
404 ch as u32, ch, g.advance
405 ));
406 for c in &g.contours {
407 s.push_str(&format!("M {:.4} {:.4}\n", c.start[0], c.start[1]));
408 for seg in &c.segs {
409 match seg {
410 Seg::Line(p) => s.push_str(&format!("L {:.4} {:.4}\n", p[0], p[1])),
411 Seg::Quad(a, p) => s.push_str(&format!(
412 "Q {:.4} {:.4} {:.4} {:.4}\n",
413 a[0], a[1], p[0], p[1]
414 )),
415 Seg::Cubic(a, b, p) => s.push_str(&format!(
416 "C {:.4} {:.4} {:.4} {:.4} {:.4} {:.4}\n",
417 a[0], a[1], b[0], b[1], p[0], p[1]
418 )),
419 }
420 }
421 s.push_str("Z\n");
422 }
423 s
424}
425
426fn parse_glyph_ling(text: &str) -> Option<Glyph> {
427 let mut advance = 0.5f32;
428 let mut contours: Vec<Contour> = Vec::new();
429 let mut cur: Option<Contour> = None;
430 for line in text.lines() {
431 let line = line.trim();
432 if line.is_empty() {
433 continue;
434 }
435 if let Some(rest) = line.strip_prefix('#') {
436 if let Some(i) = rest.find("adv=") {
437 if let Ok(v) = rest[i + 4..]
438 .split_whitespace()
439 .next()
440 .unwrap_or("")
441 .parse::<f32>()
442 {
443 advance = v;
444 }
445 }
446 continue;
447 }
448 let mut it = line.split_whitespace();
449 let op = it.next()?;
450 let nums: Vec<f32> = it.filter_map(|t| t.parse::<f32>().ok()).collect();
451 match op {
452 "M" => {
453 if let Some(c) = cur.take() {
454 contours.push(c);
455 }
456 cur = Some(Contour { start: [*nums.first()?, *nums.get(1)?], segs: Vec::new() });
457 },
458 "L" => {
459 if let Some(c) = &mut cur {
460 c.segs.push(Seg::Line([*nums.first()?, *nums.get(1)?]));
461 }
462 },
463 "Q" => {
464 if let Some(c) = &mut cur {
465 c.segs.push(Seg::Quad(
466 [*nums.first()?, *nums.get(1)?],
467 [*nums.get(2)?, *nums.get(3)?],
468 ));
469 }
470 },
471 "C" => {
472 if let Some(c) = &mut cur {
473 c.segs.push(Seg::Cubic(
474 [*nums.first()?, *nums.get(1)?],
475 [*nums.get(2)?, *nums.get(3)?],
476 [*nums.get(4)?, *nums.get(5)?],
477 ));
478 }
479 },
480 "Z" => {
481 if let Some(c) = cur.take() {
482 contours.push(c);
483 }
484 },
485 _ => {},
486 }
487 }
488 if let Some(c) = cur.take() {
489 contours.push(c);
490 }
491 Some(Glyph { contours, advance })
492}
493
494#[cfg(test)]
495mod tests {
496 use super::*;
497
498 #[test]
499 fn collinear_lines_merge() {
500 let segs = vec![
501 Seg::Line([0.5, 0.0]),
502 Seg::Line([1.0, 0.0]),
503 Seg::Line([1.0, 1.0]),
504 ];
505 let out = compress_contour([0.0, 0.0], &segs);
506 assert_eq!(out.len(), 2);
508 match out[0] {
509 Seg::Line(p) => assert_eq!(p, [1.0, 0.0]),
510 _ => panic!(),
511 }
512 }
513
514 #[test]
515 fn glyph_ling_roundtrips() {
516 let g = Glyph {
517 advance: 0.6,
518 contours: vec![Contour {
519 start: [0.1, 0.0],
520 segs: vec![
521 Seg::Line([0.4, 0.7]),
522 Seg::Quad([0.5, 0.8], [0.6, 0.7]),
523 Seg::Line([0.9, 0.0]),
524 ],
525 }],
526 };
527 let text = serialize_glyph_ling("Test", 'A', &g);
528 let back = parse_glyph_ling(&text).unwrap();
529 assert!((back.advance - 0.6).abs() < 1e-3);
530 assert_eq!(back.contours.len(), 1);
531 assert_eq!(back.contours[0].segs.len(), 3);
532 assert!(matches!(back.contours[0].segs[1], Seg::Quad(..)));
534 }
535}