1use crate::media::Frame;
7use crate::model::TextStyle;
8use ab_glyph::{point, Font, FontArc, FontVec, Glyph, GlyphId, OutlinedGlyph, PxScale, ScaleFont};
9use fontdb::{Database, Family, Query, Style, Weight, ID};
10use std::collections::HashMap;
11use std::sync::Arc;
12
13const MAX_SIDE: u32 = 8192;
15
16pub struct TextRasterizer {
17 cache: HashMap<(u64, u32), Arc<Frame>>,
18 families: Vec<String>,
19 loaded: bool,
20 db: Database,
21 fonts: HashMap<ID, Option<FontArc>>,
22 user_fonts: Vec<String>,
24}
25
26impl Default for TextRasterizer {
27 fn default() -> Self {
28 Self::new()
29 }
30}
31
32impl TextRasterizer {
33 pub fn new() -> Self {
35 Self {
36 cache: HashMap::new(),
37 families: Vec::new(),
38 loaded: false,
39 db: Database::new(),
40 fonts: HashMap::new(),
41 user_fonts: Vec::new(),
42 }
43 }
44 pub fn load_system_fonts(&mut self) {
46 if self.loaded {
47 return;
48 }
49 self.loaded = true;
50 self.db.load_system_fonts();
51 self.refresh_families();
52 }
53 pub fn load_user_fonts(&mut self, paths: &[String]) {
56 let mut added = false;
57 for p in paths {
58 if self.user_fonts.iter().any(|q| q == p) {
59 continue;
60 }
61 self.user_fonts.push(p.clone());
63 if self.db.load_font_file(p).is_ok() {
64 added = true;
65 }
66 }
67 if added {
68 self.refresh_families();
69 self.cache.clear();
71 }
72 }
73 fn refresh_families(&mut self) {
74 let mut fams: Vec<String> =
75 self.db.faces().filter_map(|f| f.families.first().map(|(n, _)| n.clone())).collect();
76 fams.sort();
77 fams.dedup();
78 self.families = fams;
79 }
80 pub fn is_loaded(&self) -> bool {
81 self.loaded
82 }
83 pub fn families(&self) -> &[String] {
85 &self.families
86 }
87 pub fn render(&mut self, style: &TextStyle, scale: f32) -> Arc<Frame> {
90 let key = (style.cache_key(), scale.to_bits());
91 if let Some(f) = self.cache.get(&key) {
92 return f.clone();
93 }
94 self.load_system_fonts();
95 let frame = Arc::new(self.rasterize(style, scale).unwrap_or_else(|| Frame::new(1, 1)));
96 if self.cache.len() >= 64 {
97 self.cache.clear();
99 }
100 self.cache.insert(key, frame.clone());
101 frame
102 }
103
104 fn font_for(&mut self, style: &TextStyle) -> Option<FontArc> {
105 let q = Query {
106 families: &[Family::Name(&style.font), Family::SansSerif],
107 weight: if style.bold { Weight::BOLD } else { Weight::NORMAL },
108 style: if style.italic { Style::Italic } else { Style::Normal },
109 stretch: Default::default(),
110 };
111 let id = self.db.query(&q).or_else(|| self.db.faces().next().map(|f| f.id))?;
112 let db = &self.db;
113 self.fonts
114 .entry(id)
115 .or_insert_with(|| {
116 db.with_face_data(id, |data, idx| FontVec::try_from_vec_and_index(data.to_vec(), idx).ok())
117 .flatten()
118 .map(FontArc::new)
119 })
120 .clone()
121 }
122
123 fn rasterize(&mut self, style: &TextStyle, scale: f32) -> Option<Frame> {
124 let px = style.size * scale;
125 if style.text.trim().is_empty() || !(px >= 1.0) || !px.is_finite() {
126 return None;
127 }
128 let font = self.font_for(style)?;
129 let upm = font.units_per_em().unwrap_or(font.height_unscaled());
131 let sf = font.as_scaled(PxScale::from(px * font.height_unscaled() / upm));
132 let ps = sf.scale();
133 let ls = style.letter_spacing * scale;
134 let lh = (sf.ascent() - sf.descent() + sf.line_gap()) * style.line_spacing.max(0.1);
135
136 let mut glyphs: Vec<Glyph> = Vec::new();
138 let mut lines: Vec<(usize, usize, f32)> = Vec::new();
139 for line in style.text.split('\n') {
140 let start = glyphs.len();
141 let mut x = 0.0f32;
142 let mut prev: Option<GlyphId> = None;
143 for c in line.chars().filter(|c| *c != '\r') {
144 let id = sf.glyph_id(c);
145 if let Some(p) = prev {
146 x += sf.kern(p, id);
147 }
148 glyphs.push(id.with_scale_and_position(ps, point(x, 0.0)));
149 x += sf.h_advance(id) + ls;
150 prev = Some(id);
151 }
152 if prev.is_some() {
153 x -= ls;
154 }
155 lines.push((start, glyphs.len(), x.max(0.0)));
156 }
157 let block_w = lines.iter().map(|l| l.2).fold(0.0, f32::max);
158 let block_h = (lines.len() - 1) as f32 * lh + sf.ascent() - sf.descent();
159 let align = match style.align {
160 0 => 0.0,
161 2 => 1.0,
162 _ => 0.5,
163 };
164 for (i, (s, e, lw)) in lines.iter().enumerate() {
165 let ox = (block_w - lw) * align;
166 let oy = sf.ascent() + i as f32 * lh;
167 for g in &mut glyphs[*s..*e] {
168 g.position.x += ox;
169 g.position.y += oy;
170 }
171 }
172 let outlined: Vec<OutlinedGlyph> = glyphs.iter().filter_map(|g| font.outline_glyph(g.clone())).collect();
173
174 let (mut bx0, mut by0, mut bx1, mut by1) = (0.0f32, 0.0f32, block_w.ceil(), block_h.ceil());
176 for og in &outlined {
177 let b = og.px_bounds();
178 bx0 = bx0.min(b.min.x);
179 by0 = by0.min(b.min.y);
180 bx1 = bx1.max(b.max.x);
181 by1 = by1.max(b.max.y);
182 }
183 let r = style.outline_width.max(0.0) * scale;
184 let r = if r < 0.5 { 0.0 } else { r };
185 let (shx, shy, blur) = if style.shadow {
186 let s = |v: f32| (v * scale).round();
187 (s(style.shadow_x), s(style.shadow_y), s(style.shadow_blur.max(0.0)))
188 } else {
189 (0.0, 0.0, 0.0)
190 };
191 let has_box = style.box_color[3] > 0;
192 let pad = if has_box { style.box_padding.max(0.0) * scale } else { 0.0 };
193 let m = (r + shx.abs().max(shy.abs()) + blur * 2.0).max(pad).ceil() as i32 + 1;
194 let w = ((bx1 - bx0).ceil() as i32 + 2 * m).clamp(1, MAX_SIDE as i32) as u32;
196 let h = ((by1 - by0).ceil() as i32 + 2 * m).clamp(1, MAX_SIDE as i32) as u32;
197 let (ox, oy) = (m as f32 - bx0, m as f32 - by0);
198 let (wu, n) = (w as usize, (w * h) as usize);
199
200 let mut fill = vec![0u8; n];
202 for og in &outlined {
203 let b = og.px_bounds();
204 let (gx, gy) = ((b.min.x + ox) as i32, (b.min.y + oy) as i32);
205 og.draw(|x, y, c| {
206 let (px, py) = (gx + x as i32, gy + y as i32);
207 if px >= 0 && py >= 0 && (px as u32) < w && (py as u32) < h {
208 let i = py as usize * wu + px as usize;
209 fill[i] = fill[i].max((c.min(1.0) * 255.0 + 0.5) as u8);
210 }
211 });
212 }
213 let outline = if r > 0.0 { dilate(&fill, w, h, r) } else { Vec::new() };
214 let shadow = if style.shadow && style.shadow_color[3] > 0 {
215 let mut s = vec![0u8; n];
216 let (dx, dy) = (shx as i32, shy as i32);
217 for y in 0..h as i32 {
218 let sy = y - dy;
219 if sy < 0 || sy >= h as i32 {
220 continue;
221 }
222 for x in 0..w as i32 {
223 let sx = x - dx;
224 if sx < 0 || sx >= w as i32 {
225 continue;
226 }
227 let j = sy as usize * wu + sx as usize;
228 let v = if outline.is_empty() { fill[j] } else { fill[j].max(outline[j]) };
229 s[y as usize * wu + x as usize] = v;
230 }
231 }
232 box_blur(&mut s, w, h, blur as usize);
233 s
234 } else {
235 Vec::new()
236 };
237
238 let mut out = Frame::new(w, h);
240 if has_box {
241 let x0 = ((ox - pad).floor().max(0.0) as u32).min(w);
242 let y0 = ((oy - pad).floor().max(0.0) as u32).min(h);
243 let x1 = ((block_w + ox + pad).ceil().max(0.0) as u32).min(w);
244 let y1 = ((block_h + oy + pad).ceil().max(0.0) as u32).min(h);
245 for y in y0..y1 {
246 for x in x0..x1 {
247 over(&mut out.rgba[(y as usize * wu + x as usize) * 4..][..4], style.box_color, 255);
248 }
249 }
250 }
251 for (mask, color) in [(&shadow, style.shadow_color), (&outline, style.outline_color), (&fill, style.color)] {
252 if mask.is_empty() || color[3] == 0 {
253 continue;
254 }
255 for (px, &m) in out.rgba.chunks_exact_mut(4).zip(mask.iter()) {
256 if m > 0 {
257 over(px, color, m);
258 }
259 }
260 }
261 Some(out)
262 }
263}
264
265#[inline]
267fn over(d: &mut [u8], c: [u8; 4], cov: u8) {
268 let sa = c[3] as f32 / 255.0 * cov as f32 / 255.0;
269 if sa <= 0.0 {
270 return;
271 }
272 let da = d[3] as f32 / 255.0 * (1.0 - sa);
273 let oa = sa + da;
274 for i in 0..3 {
275 d[i] = ((c[i] as f32 * sa + d[i] as f32 * da) / oa + 0.5) as u8;
276 }
277 d[3] = (oa * 255.0 + 0.5) as u8;
278}
279
280fn dilate(src: &[u8], w: u32, h: u32, r: f32) -> Vec<u8> {
283 const INF: f32 = 1e20;
284 let (w, h) = (w as usize, h as usize);
285 let mut d: Vec<f32> = src.iter().map(|&v| if v >= 128 { 0.0 } else { INF }).collect();
287 let n = w.max(h);
288 let (mut f, mut g, mut v, mut z) = (vec![0f64; n], vec![0f64; n], vec![0usize; n], vec![0f64; n + 1]);
290 for (lines, len, ls, es) in [(w, h, 1, w), (h, w, w, 1)] {
292 for l in 0..lines {
293 for j in 0..len {
294 f[j] = d[l * ls + j * es] as f64;
295 }
296 dt1d(&f[..len], &mut g, &mut v, &mut z);
297 for j in 0..len {
298 d[l * ls + j * es] = g[j] as f32;
299 }
300 }
301 }
302 d.iter().map(|&d2| ((r + 0.5 - d2.sqrt()).clamp(0.0, 1.0) * 255.0 + 0.5) as u8).collect()
303}
304
305fn dt1d(f: &[f64], d: &mut [f64], v: &mut [usize], z: &mut [f64]) {
308 let n = f.len();
309 let mut k = 0usize;
310 v[0] = 0;
311 z[0] = f64::NEG_INFINITY;
312 z[1] = f64::INFINITY;
313 let s_at = |q: usize, p: usize| ((f[q] + (q * q) as f64) - (f[p] + (p * p) as f64)) / (2.0 * (q - p) as f64);
314 for q in 1..n {
315 let mut s = s_at(q, v[k]);
316 while s <= z[k] {
317 k -= 1;
318 s = s_at(q, v[k]);
319 }
320 k += 1;
321 v[k] = q;
322 z[k] = s;
323 z[k + 1] = f64::INFINITY;
324 }
325 k = 0;
326 for (q, dq) in d[..n].iter_mut().enumerate() {
327 while z[k + 1] < q as f64 {
328 k += 1;
329 }
330 let p = v[k];
331 *dq = (q as f64 - p as f64).powi(2) + f[p];
332 }
333}
334
335fn box_blur(buf: &mut [u8], w: u32, h: u32, r: usize) {
337 if r == 0 || buf.is_empty() {
338 return;
339 }
340 let (w, h) = (w as usize, h as usize);
341 let mut tmp = vec![0u8; buf.len()];
342 for _ in 0..2 {
343 blur_lines(buf, &mut tmp, h, w, w, 1, r);
344 blur_lines(&tmp, buf, w, h, 1, w, r);
345 }
346}
347
348fn blur_lines(src: &[u8], dst: &mut [u8], lines: usize, len: usize, lstride: usize, estride: usize, r: usize) {
350 let norm = (2 * r + 1) as u32;
351 for l in 0..lines {
352 let base = l * lstride;
353 let at = |j: usize| base + j * estride;
354 let mut sum: u32 = (0..=r.min(len - 1)).map(|j| src[at(j)] as u32).sum();
355 for j in 0..len {
356 dst[at(j)] = ((sum + norm / 2) / norm) as u8;
357 if j + r + 1 < len {
358 sum += src[at(j + r + 1)] as u32;
359 }
360 if j >= r {
361 sum -= src[at(j - r)] as u32;
362 }
363 }
364 }
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370
371 fn alpha_sum(f: &Frame) -> u64 {
372 f.rgba.chunks_exact(4).map(|p| p[3] as u64).sum()
373 }
374
375 #[test]
376 fn renders_default_style_and_caches() {
377 let mut tr = TextRasterizer::new();
378 let style = TextStyle::default();
379 let a = tr.render(&style, 0.5);
380 if tr.families().is_empty() {
381 eprintln!("no system fonts — skipping");
382 assert_eq!((a.width, a.height), (1, 1));
383 return;
384 }
385 assert!(a.width > 4 && a.height > 4, "{}x{}", a.width, a.height);
386 assert!(alpha_sum(&a) > 0);
387 assert!(a.rgba.chunks_exact(4).filter(|p| p[3] > 0).all(|p| p[0] == 255 && p[1] == 255 && p[2] == 255));
389 let b = tr.render(&style, 0.5);
390 assert!(Arc::ptr_eq(&a, &b), "cache hit expected");
391 let c = tr.render(&style, 1.0);
392 assert!(!Arc::ptr_eq(&a, &c));
393 assert!(c.width > a.width);
394
395 let mut s2 = style.clone();
397 s2.font = "No Such Font 123".into();
398 s2.bold = true;
399 s2.italic = true;
400 assert!(alpha_sum(&tr.render(&s2, 0.5)) > 0);
401
402 let mut s3 = style.clone();
404 s3.outline_width = 4.0;
405 s3.shadow = true;
406 s3.box_color = [0, 0, 255, 255];
407 let d = tr.render(&s3, 0.5);
408 assert!(d.width > a.width && d.height > a.height);
409 assert!(d.rgba.chunks_exact(4).any(|p| p[3] > 0 && p[2] == 255 && p[0] == 0));
410 assert!(d.rgba.chunks_exact(4).any(|p| p[3] > 0 && p[0] == 0 && p[2] == 0));
411
412 let mut s4 = style.clone();
414 s4.text = "One\nTwo\nThree".into();
415 s4.align = 0;
416 let e = tr.render(&s4, 0.5);
417 assert!(e.height > a.height * 2);
418 }
419
420 #[test]
421 fn user_font_load_is_idempotent_and_listed() {
422 let src = std::path::Path::new("C:\\Windows\\Fonts\\arial.ttf");
423 if !src.exists() {
424 eprintln!("no arial.ttf — skipping");
425 return;
426 }
427 let dst = std::env::temp_dir().join(format!("se-userfont-{}.ttf", std::process::id()));
428 std::fs::copy(src, &dst).expect("copy font");
429 let mut tr = TextRasterizer::new();
430 let paths = vec![dst.to_string_lossy().into_owned()];
431 tr.load_user_fonts(&paths);
432 assert!(tr.families().iter().any(|f| f == "Arial"), "{:?}", tr.families());
433 let n = tr.families().len();
434 tr.load_user_fonts(&paths); assert_eq!(tr.families().len(), n);
436 let mut style = TextStyle::default();
439 style.font = "Arial".into();
440 assert!(alpha_sum(&tr.render(&style, 0.5)) > 0);
441 tr.load_user_fonts(&["Z:\\nope\\missing-font.ttf".into()]);
443 let _ = std::fs::remove_file(&dst);
444 }
445
446 #[test]
447 fn empty_text_is_1x1() {
448 let mut tr = TextRasterizer::new();
449 let mut style = TextStyle::default();
450 style.text = " \n ".into();
451 let f = tr.render(&style, 1.0);
452 assert_eq!((f.width, f.height, f.rgba[3]), (1, 1, 0));
453 style.text = "x".into();
454 assert_eq!(tr.render(&style, 0.0).width, 1);
455 }
456
457 #[test]
458 fn blur_and_dilate_basics() {
459 let mut m = vec![0u8; 25];
460 m[12] = 255;
461 let d = dilate(&m, 5, 5, 1.5);
462 assert_eq!(d[12], 255);
463 assert_eq!(d[11], 255);
464 assert_eq!(d[7], 255);
465 assert_eq!(d[0], 0);
466 assert_eq!(d[10], 0);
467 let mut b = vec![0u8; 49];
469 b[24] = 255;
470 box_blur(&mut b, 7, 7, 1);
471 assert!(b[24] < 255 && b[24] > 0);
472 assert!(b[23] > 0 && b[16] > 0 && b[10] > 0);
473 assert_eq!(b[0], 0);
474 assert_eq!(b[3], 0);
475 }
476
477 #[test]
478 fn dilate_matches_brute_force_and_is_fast() {
479 let (w, h) = (40usize, 30usize);
481 let mut m = vec![0u8; w * h];
482 for &(x, y) in &[(3usize, 4usize), (20, 15), (21, 15), (38, 28), (0, 0)] {
483 m[y * w + x] = 255;
484 }
485 m[10 * w + 10] = 100; let r = 3.7f32;
487 let d = dilate(&m, w as u32, h as u32, r);
488 for y in 0..h {
489 for x in 0..w {
490 let mut best = f32::INFINITY;
491 for (i, &v) in m.iter().enumerate() {
492 if v >= 128 {
493 let (xx, yy) = ((i % w) as f32, (i / w) as f32);
494 best = best.min(((x as f32 - xx).powi(2) + (y as f32 - yy).powi(2)).sqrt());
495 }
496 }
497 let want = ((r + 0.5 - best).clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
498 assert_eq!(d[y * w + x], want, "at {x},{y}");
499 }
500 }
501 let (w, h) = (2000u32, 600u32);
503 let mut big = vec![0u8; (w * h) as usize];
504 for y in 200..400 {
505 big[(y * w + 500) as usize..(y * w + 1500) as usize].fill(255);
506 }
507 let t = std::time::Instant::now();
508 let d = dilate(&big, w, h, 100.0);
509 assert!(t.elapsed() < std::time::Duration::from_secs(2), "{:?}", t.elapsed());
510 assert_eq!(d[(300 * w + 1000) as usize], 255);
511 assert_eq!(d[(300 * w + 1550) as usize], 255); assert_eq!(d[(300 * w + 1600) as usize], 0); }
514}