1use std::collections::HashMap;
16
17use dotloom_geometry::{HAlign, Text, VAlign};
18use read_fonts::{
19 FontRef, TableProvider,
20 tables::glyf::{Anchor, CompositeGlyphFlags, CurvePoint, Glyph},
21 types::GlyphId,
22};
23
24use crate::RenderError;
25
26pub const DEFAULT_FONT: &[u8] = include_bytes!("../assets/Inter-Regular-subset.ttf");
28
29pub const RASTER_PX: f32 = 48.0;
31pub const SPREAD: usize = 6;
33pub const ATLAS_SIZE: usize = 2048;
35
36const LINE_SPACING: f64 = Text::LINE_SPACING;
38const TOP_ABOVE_BASELINE: f64 = Text::TOP_ABOVE_BASELINE;
40const FLATTEN_TOLERANCE: f32 = 0.1;
42
43#[derive(Debug, Clone, Copy)]
45pub(crate) struct GlyphSlot {
46 pub uv: Option<[f32; 4]>,
48 pub quad: [f32; 4],
51 pub advance: f32,
53}
54
55#[derive(Debug, Clone, Copy)]
57pub(crate) struct PlacedGlyph {
58 pub x0: f64,
59 pub y0: f64,
60 pub x1: f64,
61 pub y1: f64,
62 pub uv: [f32; 4],
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub(crate) struct DirtyRect {
68 pub x: usize,
69 pub y: usize,
70 pub w: usize,
71 pub h: usize,
72}
73
74struct Shelf {
75 y: usize,
76 h: usize,
77 x: usize,
78}
79
80pub struct TextSystem {
82 font: Vec<u8>,
84 shared_layout: bool,
86 height_per_em: f64,
88 glyphs: HashMap<u32, GlyphSlot>,
89 pub(crate) atlas: Vec<u8>,
90 shelves: Vec<Shelf>,
91 pub(crate) dirty: Option<DirtyRect>,
92 pub(crate) generation: u64,
94}
95
96impl std::fmt::Debug for TextSystem {
97 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98 f.debug_struct("TextSystem").field("glyphs", &self.glyphs.len()).field("generation", &self.generation).finish()
99 }
100}
101
102trait OutlinePen {
104 fn move_to(&mut self, x: f32, y: f32);
105 fn line_to(&mut self, x: f32, y: f32);
106 fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32);
107 fn close(&mut self);
108}
109
110#[derive(Default)]
112struct Bounds {
113 min: (f32, f32),
114 max: (f32, f32),
115 any: bool,
116}
117
118impl OutlinePen for Bounds {
119 fn move_to(&mut self, x: f32, y: f32) {
120 self.add(x, y);
121 }
122 fn line_to(&mut self, x: f32, y: f32) {
123 self.add(x, y);
124 }
125 fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
126 self.add(cx0, cy0);
128 self.add(x, y);
129 }
130 fn close(&mut self) {}
131}
132
133impl Bounds {
134 fn add(&mut self, x: f32, y: f32) {
135 if !self.any {
136 self.min = (x, y);
137 self.max = (x, y);
138 self.any = true;
139 }
140 self.min = (self.min.0.min(x), self.min.1.min(y));
141 self.max = (self.max.0.max(x), self.max.1.max(y));
142 }
143}
144
145struct Raster {
149 w: usize,
150 h: usize,
151 acc: Vec<f32>,
153 origin: (f32, f32),
155 start: (f32, f32),
156 cur: (f32, f32),
157}
158
159impl Raster {
160 fn new(w: usize, h: usize, origin: (f32, f32)) -> Self {
161 Self { w, h, acc: vec![0.0; w * h + 2], origin, start: (0.0, 0.0), cur: (0.0, 0.0) }
162 }
163
164 fn to_px(&self, x: f32, y: f32) -> (f32, f32) {
165 (x - self.origin.0, self.origin.1 - y)
166 }
167
168 fn add(&mut self, i: isize, v: f32) {
169 if let Some(cell) = usize::try_from(i).ok().and_then(|i| self.acc.get_mut(i)) {
170 *cell += v;
171 }
172 }
173
174 fn line(&mut self, p0: (f32, f32), p1: (f32, f32)) {
175 if p0.1 == p1.1 || !(p0.0.is_finite() && p0.1.is_finite() && p1.0.is_finite() && p1.1.is_finite()) {
176 return;
177 }
178 let (dir, p0, p1) = if p0.1 < p1.1 { (1.0f32, p0, p1) } else { (-1.0f32, p1, p0) };
179 let dxdy = (p1.0 - p0.0) / (p1.1 - p0.1);
180 let y_start = p0.1.max(0.0);
181 let y_end = p1.1.min(self.h as f32);
182 let mut x = p0.0 + (y_start - p0.1) * dxdy;
183 let mut y = y_start.floor();
184 while y < y_end {
185 let row = y as isize * self.w as isize;
186 let dy = (y + 1.0).min(y_end) - y.max(y_start);
187 let x_next = x + dxdy * dy;
188 let d = dy * dir;
189 let (x0, x1) = if x < x_next { (x, x_next) } else { (x_next, x) };
190 let x0 = x0.clamp(0.0, self.w as f32);
191 let x1 = x1.clamp(0.0, self.w as f32);
192 let x0_floor = x0.floor();
193 let x0i = x0_floor as isize;
194 let x1_ceil = x1.ceil();
195 let x1i = x1_ceil as isize;
196 if x1i <= x0i + 1 {
197 let xm = 0.5 * (x0 + x1) - x0_floor;
199 self.add(row + x0i, d - d * xm);
200 self.add(row + x0i + 1, d * xm);
201 } else {
202 let s = (x1 - x0).recip();
203 let x0f = x0 - x0_floor;
204 let a0 = 0.5 * s * (1.0 - x0f) * (1.0 - x0f);
205 let x1f = x1 - x1_ceil + 1.0;
206 let am = 0.5 * s * x1f * x1f;
207 self.add(row + x0i, d * a0);
208 if x1i == x0i + 2 {
209 self.add(row + x0i + 1, d * (1.0 - a0 - am));
210 } else {
211 let a1 = s * (1.5 - x0f);
212 self.add(row + x0i + 1, d * (a1 - a0));
213 for xi in x0i + 2..x1i - 1 {
214 self.add(row + xi, d * s);
215 }
216 let a2 = a1 + (x1i - x0i - 3) as f32 * s;
217 self.add(row + x1i - 1, d * (1.0 - a2 - am));
218 }
219 self.add(row + x1i, d * am);
220 }
221 x = x_next;
222 y += 1.0;
223 }
224 }
225
226 fn line_to_px(&mut self, p: (f32, f32)) {
227 let from = self.cur;
228 self.line(from, p);
229 self.cur = p;
230 }
231
232 fn coverage(&self) -> Vec<u8> {
233 let mut sum = 0.0f32;
234 self.acc
235 .iter()
236 .take(self.w * self.h)
237 .map(|a| {
238 sum += a;
239 (sum.abs().min(1.0) * 255.0).round() as u8
240 })
241 .collect()
242 }
243}
244
245impl OutlinePen for Raster {
246 fn move_to(&mut self, x: f32, y: f32) {
247 let p = self.to_px(x, y);
248 self.start = p;
249 self.cur = p;
250 }
251 fn line_to(&mut self, x: f32, y: f32) {
252 let p = self.to_px(x, y);
253 self.line_to_px(p);
254 }
255 fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
256 let (p0, c, p2) = (self.cur, self.to_px(cx0, cy0), self.to_px(x, y));
257 let dd = ((p0.0 - 2.0 * c.0 + p2.0).powi(2) + (p0.1 - 2.0 * c.1 + p2.1).powi(2)).sqrt();
258 let n = ((dd / FLATTEN_TOLERANCE).sqrt().ceil() as usize).clamp(1, 64);
259 for i in 1..=n {
260 let t = i as f32 / n as f32;
261 let mt = 1.0 - t;
262 let p = (
263 mt * mt * p0.0 + 2.0 * mt * t * c.0 + t * t * p2.0,
264 mt * mt * p0.1 + 2.0 * mt * t * c.1 + t * t * p2.1,
265 );
266 self.line_to_px(p);
267 }
268 }
269 fn close(&mut self) {
270 let start = self.start;
271 self.line_to_px(start);
272 }
273}
274
275impl TextSystem {
276 pub fn new(font_bytes: &[u8]) -> Result<Self, RenderError> {
281 let font = FontRef::new(font_bytes).map_err(|e| RenderError::Font(e.to_string()))?;
282 let err = |e: read_fonts::ReadError| RenderError::Font(e.to_string());
283 let upm = f32::from(font.head().map_err(err)?.units_per_em());
284 let hhea = font.hhea().map_err(err)?;
285 font.glyf().map_err(err)?;
286 font.hmtx().map_err(err)?;
287 if upm <= 0.0 {
288 return Err(RenderError::Font("font has no units per em".into()));
289 }
290 let ascent = f32::from(hhea.ascender().to_i16());
291 let cap = map_char(&font, 'H').and_then(|g| outline_bounds(&font, g, upm)).map_or(ascent * 0.75, |b| b.max.1);
292 let descent = (-f32::from(hhea.descender().to_i16())).max(0.0);
293 let shared_layout = font_bytes == DEFAULT_FONT;
294 let height_per_em = if shared_layout { 1.0 / Text::em_size(1.0) } else { f64::from((cap + descent) / upm) };
296 if !(height_per_em.is_finite() && height_per_em > 0.1) {
297 return Err(RenderError::Font("implausible font metrics".into()));
298 }
299 Ok(Self {
300 font: font_bytes.to_vec(),
301 shared_layout,
302 height_per_em,
303 glyphs: HashMap::new(),
304 atlas: vec![0; ATLAS_SIZE * ATLAS_SIZE],
305 shelves: Vec::new(),
306 dirty: None,
307 generation: 0,
308 })
309 }
310
311 pub fn with_default_font() -> Result<Self, RenderError> {
316 Self::new(DEFAULT_FONT)
317 }
318
319 #[must_use]
321 pub fn cached_glyphs(&self) -> usize {
322 self.glyphs.len()
323 }
324
325 fn clear_atlas(&mut self) {
326 self.glyphs.clear();
327 self.shelves.clear();
328 self.atlas.fill(0);
329 self.dirty = Some(DirtyRect { x: 0, y: 0, w: ATLAS_SIZE, h: ATLAS_SIZE });
330 self.generation += 1;
331 }
332
333 fn mark_dirty(&mut self, r: DirtyRect) {
334 self.dirty = Some(match self.dirty {
335 None => r,
336 Some(d) => {
337 let x0 = d.x.min(r.x);
338 let y0 = d.y.min(r.y);
339 let x1 = (d.x + d.w).max(r.x + r.w);
340 let y1 = (d.y + d.h).max(r.y + r.h);
341 DirtyRect { x: x0, y: y0, w: x1 - x0, h: y1 - y0 }
342 }
343 });
344 }
345
346 fn allocate(&mut self, w: usize, h: usize) -> Option<(usize, usize)> {
347 if w > ATLAS_SIZE || h > ATLAS_SIZE {
348 return None;
349 }
350 for s in &mut self.shelves {
351 if h <= s.h && s.x + w <= ATLAS_SIZE {
352 let at = (s.x, s.y);
353 s.x += w + 1;
354 return Some(at);
355 }
356 }
357 let y = self.shelves.last().map_or(0, |s| s.y + s.h + 1);
358 if y + h > ATLAS_SIZE {
359 return None;
360 }
361 self.shelves.push(Shelf { y, h, x: w + 1 });
362 Some((0, y))
363 }
364
365 fn glyph_index(&self, c: char) -> u32 {
367 let Ok(font) = FontRef::new(&self.font) else { return 0 };
368 let substitute = match c {
369 '\u{2300}' => Some('\u{2205}'),
370 '\u{00a0}' | '\u{2007}' | '\u{202f}' => Some(' '),
371 _ => None,
372 };
373 map_char(&font, c)
374 .or_else(|| substitute.and_then(|s| map_char(&font, s)))
375 .or_else(|| map_char(&font, '\u{fffd}'))
376 .map_or(0, GlyphId::to_u32)
377 }
378
379 fn glyph(&mut self, index: u32) -> Option<GlyphSlot> {
382 if let Some(g) = self.glyphs.get(&index) {
383 return Some(*g);
384 }
385 let font = FontRef::new(&self.font).ok()?;
386 let gid = GlyphId::new(index);
387 let upm = f32::from(font.head().ok()?.units_per_em());
388 let advance = f32::from(font.hmtx().ok()?.advance(gid).unwrap_or(0)) / upm;
389 let bounds = outline_bounds(&font, gid, RASTER_PX);
390 let Some(b) = bounds.filter(|b| b.max.0 > b.min.0 && b.max.1 > b.min.1) else {
391 let slot = GlyphSlot { uv: None, quad: [0.0; 4], advance };
392 self.glyphs.insert(index, slot);
393 return Some(slot);
394 };
395 let (xmin, ymin) = (b.min.0.floor(), b.min.1.floor());
396 let (gw, gh) = ((b.max.0.ceil() - xmin) as usize, (b.max.1.ceil() - ymin) as usize);
397 let mut raster = Raster::new(gw, gh, (xmin, ymin + gh as f32));
398 let s = RASTER_PX / upm;
399 draw_glyph(&font, gid, [s, 0.0, 0.0, s, 0.0, 0.0], 0, &mut raster)?;
400 let coverage = raster.coverage();
401 let (w, h) = (gw + 2 * SPREAD, gh + 2 * SPREAD);
402 let at = match self.allocate(w, h) {
403 Some(at) => at,
404 None => {
405 self.clear_atlas();
406 self.allocate(w, h)?
407 }
408 };
409 let sdf = signed_distance_field(&coverage, gw, gh, SPREAD);
410 for row in 0..h {
411 let dst = (at.1 + row) * ATLAS_SIZE + at.0;
412 self.atlas[dst..dst + w].copy_from_slice(&sdf[row * w..(row + 1) * w]);
413 }
414 self.mark_dirty(DirtyRect { x: at.0, y: at.1, w, h });
415 let size = ATLAS_SIZE as f32;
416 let pad = SPREAD as f32;
417 let slot = GlyphSlot {
418 uv: Some([at.0 as f32 / size, at.1 as f32 / size, (at.0 + w) as f32 / size, (at.1 + h) as f32 / size]),
419 quad: [
420 (xmin - pad) / RASTER_PX,
421 (ymin - pad) / RASTER_PX,
422 (xmin + gw as f32 + pad) / RASTER_PX,
423 (ymin + gh as f32 + pad) / RASTER_PX,
424 ],
425 advance,
426 };
427 self.glyphs.insert(index, slot);
428 Some(slot)
429 }
430
431 fn pens(&mut self, line: &str) -> Option<(Vec<(char, f64)>, f64)> {
434 if self.shared_layout {
435 let units = 1.0 / Text::FONT_UNITS_PER_EM;
436 let (pens, width) = Text::line_pens(line);
437 return Some((pens.into_iter().map(|(c, x)| (c, x * units)).collect(), width * units));
438 }
439 let mut pens = Vec::with_capacity(line.len());
440 let mut pen = 0.0;
441 for c in line.trim_end_matches('\r').chars().filter_map(normalize_char) {
442 pens.push((c, pen));
443 pen += f64::from(self.glyph(self.glyph_index(c))?.advance);
444 }
445 Some((pens, pen))
446 }
447
448 #[cfg(test)]
450 pub(crate) fn line_width(&mut self, line: &str, height: f64) -> f64 {
451 let em = height / self.height_per_em;
452 self.pens(line).map_or(0.0, |p| p.1) * em
453 }
454
455 pub(crate) fn layout(&mut self, t: &Text) -> Option<Vec<PlacedGlyph>> {
459 let h = t.height;
460 if !(h.is_finite() && h > 0.0) {
461 return Some(Vec::new());
462 }
463 let em = h / self.height_per_em;
464 let lines: Vec<&str> = t.content.split('\n').collect();
465 let n = lines.len().max(1) as f64;
466 let block_h = h * (1.0 + (n - 1.0) * LINE_SPACING);
467 let first_baseline = match t.valign {
468 VAlign::Baseline => 0.0,
469 VAlign::Top => -TOP_ABOVE_BASELINE * h,
470 VAlign::Middle => block_h * 0.5 - TOP_ABOVE_BASELINE * h,
471 VAlign::Bottom => block_h - TOP_ABOVE_BASELINE * h,
472 };
473 let generation = self.generation;
474 let mut out = Vec::with_capacity(t.content.len());
475 for (i, line) in lines.iter().enumerate() {
476 let (pens, width) = self.pens(line)?;
477 let width = width * em;
478 let start = match t.halign {
479 HAlign::Left => 0.0,
480 HAlign::Center => -width * 0.5,
481 HAlign::Right => -width,
482 };
483 let baseline = first_baseline - i as f64 * LINE_SPACING * h;
484 for (c, x) in pens {
485 let g = self.glyph(self.glyph_index(c))?;
486 if self.generation != generation {
487 return None;
488 }
489 if let Some(uv) = g.uv {
490 let pen = start + x * em;
491 out.push(PlacedGlyph {
492 x0: pen + f64::from(g.quad[0]) * em,
493 y0: baseline + f64::from(g.quad[1]) * em,
494 x1: pen + f64::from(g.quad[2]) * em,
495 y1: baseline + f64::from(g.quad[3]) * em,
496 uv,
497 });
498 }
499 }
500 }
501 Some(out)
502 }
503}
504
505fn outline_bounds(font: &FontRef<'_>, gid: GlyphId, size: f32) -> Option<Bounds> {
507 let s = size / f32::from(font.head().ok()?.units_per_em());
508 let mut b = Bounds::default();
509 draw_glyph(font, gid, [s, 0.0, 0.0, s, 0.0, 0.0], 0, &mut b)?;
510 b.any.then_some(b)
511}
512
513fn map_char(font: &FontRef<'_>, c: char) -> Option<GlyphId> {
515 font.cmap().ok()?.map_codepoint(c).filter(|g| g.to_u32() != 0)
516}
517
518fn draw_glyph(font: &FontRef<'_>, gid: GlyphId, m: [f32; 6], depth: u8, pen: &mut impl OutlinePen) -> Option<()> {
522 if depth > 8 {
523 return None;
524 }
525 let glyf = font.glyf().ok()?;
526 let glyph = font.loca(None).ok()?.get(gid, &glyf)?.into_glyph();
527 match glyph {
528 None => Some(()),
529 Some(Glyph::Simple(g)) => {
530 let points: Vec<CurvePoint> = g.points().collect();
531 let mut first = 0usize;
532 for end in g.end_pts_of_contours() {
533 let end = usize::from(end.get());
534 draw_contour(points.get(first..=end)?, m, pen);
535 first = end + 1;
536 }
537 Some(())
538 }
539 Some(Glyph::Composite(g)) => {
540 for c in g.components() {
541 let t = c.transform;
542 let (a, b, cc, d) = (t.xx.to_f32(), t.yx.to_f32(), t.xy.to_f32(), t.yy.to_f32());
543 let (mut ox, mut oy) = match c.anchor {
544 Anchor::Offset { x, y } => (f32::from(x), f32::from(y)),
545 Anchor::Point { .. } => (0.0, 0.0),
547 };
548 if c.flags.contains(CompositeGlyphFlags::SCALED_COMPONENT_OFFSET) {
549 (ox, oy) = (a * ox + cc * oy, b * ox + d * oy);
550 }
551 let child = [
553 m[0] * a + m[2] * b,
554 m[1] * a + m[3] * b,
555 m[0] * cc + m[2] * d,
556 m[1] * cc + m[3] * d,
557 m[0] * ox + m[2] * oy + m[4],
558 m[1] * ox + m[3] * oy + m[5],
559 ];
560 draw_glyph(font, GlyphId::from(c.glyph), child, depth + 1, pen)?;
561 }
562 Some(())
563 }
564 }
565}
566
567fn draw_contour(points: &[CurvePoint], m: [f32; 6], pen: &mut impl OutlinePen) {
570 let n = points.len();
571 if n == 0 {
572 return;
573 }
574 let map = |p: &CurvePoint| {
575 let (x, y) = (f32::from(p.x), f32::from(p.y));
576 (m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5])
577 };
578 let mid = |a: (f32, f32), b: (f32, f32)| (0.5 * (a.0 + b.0), 0.5 * (a.1 + b.1));
579 let (start, first) = match points.iter().position(|p| p.on_curve) {
580 Some(i) => (points.get(i).map_or((0.0, 0.0), map), i),
581 None => (mid(points.get(n - 1).map_or((0.0, 0.0), map), points.first().map_or((0.0, 0.0), map)), n - 1),
582 };
583 pen.move_to(start.0, start.1);
584 let mut control: Option<(f32, f32)> = None;
585 for k in 1..=n {
586 let Some(p) = points.get((first + k) % n) else { continue };
587 let q = map(p);
588 if p.on_curve {
589 match control.take() {
590 Some(c) => pen.quad_to(c.0, c.1, q.0, q.1),
591 None => pen.line_to(q.0, q.1),
592 }
593 } else {
594 if let Some(c) = control {
595 let implied = mid(c, q);
596 pen.quad_to(c.0, c.1, implied.0, implied.1);
597 }
598 control = Some(q);
599 }
600 }
601 if let Some(c) = control {
602 pen.quad_to(c.0, c.1, start.0, start.1);
603 }
604 pen.close();
605}
606
607fn normalize_char(c: char) -> Option<char> {
609 match c {
610 '\t' => Some(' '),
611 c if c.is_control() => None,
612 c => Some(c),
613 }
614}
615
616fn edt_1d(f: &[f32], d: &mut [f32], v: &mut [usize], z: &mut [f32]) {
618 let n = f.len();
619 if n == 0 {
620 return;
621 }
622 let mut k = 0usize;
623 v[0] = 0;
624 z[0] = f32::NEG_INFINITY;
625 z[1] = f32::INFINITY;
626 for q in 1..n {
627 let fq = f[q] + (q * q) as f32;
628 let mut s;
630 loop {
631 let p = v[k];
632 s = (fq - (f[p] + (p * p) as f32)) / (2.0 * q as f32 - 2.0 * p as f32);
633 if s <= z[k] && k > 0 {
634 k -= 1;
635 } else {
636 break;
637 }
638 }
639 k += 1;
640 v[k] = q;
641 z[k] = s;
642 z[k + 1] = f32::INFINITY;
643 }
644 k = 0;
645 for (q, out) in d.iter_mut().enumerate().take(n) {
646 while z[k + 1] < q as f32 {
647 k += 1;
648 }
649 let p = v[k];
650 let dq = q as f32 - p as f32;
651 *out = dq * dq + f[p];
652 }
653}
654
655fn edt_2d(feature: &[bool], w: usize, h: usize) -> Vec<f32> {
657 const INF: f32 = 1e20;
658 let mut grid: Vec<f32> = feature.iter().map(|&b| if b { 0.0 } else { INF }).collect();
659 let n = w.max(h);
660 let (mut f, mut d) = (vec![0.0f32; n], vec![0.0f32; n]);
661 let (mut v, mut z) = (vec![0usize; n], vec![0.0f32; n + 1]);
662 for x in 0..w {
663 for y in 0..h {
664 f[y] = grid[y * w + x];
665 }
666 edt_1d(&f[..h], &mut d[..h], &mut v, &mut z);
667 for y in 0..h {
668 grid[y * w + x] = d[y];
669 }
670 }
671 for y in 0..h {
672 f[..w].copy_from_slice(&grid[y * w..(y + 1) * w]);
673 edt_1d(&f[..w], &mut d[..w], &mut v, &mut z);
674 grid[y * w..(y + 1) * w].copy_from_slice(&d[..w]);
675 }
676 grid
677}
678
679pub(crate) fn signed_distance_field(coverage: &[u8], w: usize, h: usize, spread: usize) -> Vec<u8> {
682 let (pw, ph) = (w + 2 * spread, h + 2 * spread);
683 let mut cov = vec![0u8; pw * ph];
684 for y in 0..h {
685 let dst = (y + spread) * pw + spread;
686 cov[dst..dst + w].copy_from_slice(&coverage[y * w..(y + 1) * w]);
687 }
688 let inside: Vec<bool> = cov.iter().map(|&c| c >= 128).collect();
689 let outside: Vec<bool> = inside.iter().map(|&b| !b).collect();
690 let to_inside = edt_2d(&inside, pw, ph);
691 let to_outside = edt_2d(&outside, pw, ph);
692 let s = spread as f32;
693 cov.iter()
694 .enumerate()
695 .map(|(i, &c)| {
696 let sd = if c > 0 && c < 255 {
698 f32::from(c) / 255.0 - 0.5
699 } else if inside[i] {
700 to_outside[i].sqrt() - 0.5
701 } else {
702 -(to_inside[i].sqrt() - 0.5)
703 };
704 ((0.5 + sd / (2.0 * s)).clamp(0.0, 1.0) * 255.0).round() as u8
705 })
706 .collect()
707}
708
709#[cfg(test)]
710mod tests {
711 use dotloom_geometry::Point;
712
713 use super::*;
714
715 fn raster(w: usize, h: usize, polys: &[&[(f32, f32)]]) -> Vec<u8> {
717 let mut r = Raster::new(w, h, (0.0, 0.0));
718 for poly in polys {
719 r.move_to(poly[0].0, -poly[0].1);
720 for p in &poly[1..] {
721 r.line_to(p.0, -p.1);
722 }
723 r.close();
724 }
725 r.coverage()
726 }
727
728 #[test]
729 fn rasterizer_coverage_area_and_winding() {
730 let sq: &[(f32, f32)] = &[(1.5, 1.5), (8.5, 1.5), (8.5, 8.5), (1.5, 8.5)];
732 let cov = raster(10, 10, &[sq]);
733 let area: f32 = cov.iter().map(|&c| f32::from(c) / 255.0).sum();
734 assert!((area - 49.0).abs() < 0.1, "area {area}");
735 assert_eq!(cov[5 * 10 + 5], 255);
736 assert_eq!(cov[5 * 10 + 1], 128);
737 assert_eq!(cov[0], 0);
738 let twice = raster(10, 10, &[sq, sq]);
741 for i in 0..100 {
742 if cov[i] == 255 || cov[i] == 0 {
743 assert_eq!(twice[i], cov[i], "pixel {i}");
744 }
745 }
746 let hole: &[(f32, f32)] = &[(3.0, 3.0), (3.0, 7.0), (7.0, 7.0), (7.0, 3.0)];
748 let ring = raster(10, 10, &[sq, hole]);
749 assert_eq!(ring[5 * 10 + 5], 0);
750 let ring_area: f32 = ring.iter().map(|&c| f32::from(c) / 255.0).sum();
751 assert!((ring_area - 33.0).abs() < 0.1, "ring {ring_area}");
752 let tri: &[(f32, f32)] = &[(0.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
754 let t: f32 = raster(10, 10, &[tri]).iter().map(|&c| f32::from(c) / 255.0).sum();
755 assert!((t - 50.0).abs() < 0.1, "triangle {t}");
756 }
757
758 #[test]
759 fn outlines_match_the_glyph_boxes_of_the_font() {
760 let font = FontRef::new(DEFAULT_FONT).unwrap();
763 let upm = f32::from(font.head().unwrap().units_per_em());
764 let glyf = font.glyf().unwrap();
765 let loca = font.loca(None).unwrap();
766 let (mut simple, mut composite) = (0, 0);
767 for id in 0..font.maxp().unwrap().num_glyphs() {
768 let gid = GlyphId::new(u32::from(id));
769 let Some(glyph) = loca.get(gid, &glyf).unwrap().into_glyph() else { continue };
770 let header = [glyph.x_min(), glyph.y_min(), glyph.x_max(), glyph.y_max()].map(f32::from);
771 match glyph {
772 Glyph::Simple(_) => simple += 1,
773 Glyph::Composite(_) => composite += 1,
774 }
775 let b = outline_bounds(&font, gid, upm).unwrap();
776 assert_eq!([b.min.0, b.min.1, b.max.0, b.max.1], header, "glyph {id}");
777 }
778 assert!(simple > 300 && composite > 100, "{simple} simple, {composite} composite");
779 }
780
781 #[test]
782 fn glyph_rasters_have_counters_and_plausible_ink() {
783 let font = FontRef::new(DEFAULT_FONT).unwrap();
784 let gid = map_char(&font, 'O').unwrap();
786 let b = outline_bounds(&font, gid, RASTER_PX).unwrap();
787 let (xmin, ymin) = (b.min.0.floor(), b.min.1.floor());
788 let (w, h) = ((b.max.0.ceil() - xmin) as usize, (b.max.1.ceil() - ymin) as usize);
789 let mut r = Raster::new(w, h, (xmin, ymin + h as f32));
790 let s = RASTER_PX / f32::from(font.head().unwrap().units_per_em());
791 draw_glyph(&font, gid, [s, 0.0, 0.0, s, 0.0, 0.0], 0, &mut r).unwrap();
792 let cov = r.coverage();
793 assert_eq!(cov[(h / 2) * w + w / 2], 0, "counter");
794 assert!(cov[(h / 2) * w + 1] > 200, "left stroke");
795 assert!(cov[(h / 2) * w + w - 2] > 200, "right stroke");
796 let mut ts = TextSystem::with_default_font().unwrap();
798 for c in "AgjÇğİ@%∅".chars() {
799 let g = ts.glyph(ts.glyph_index(c)).unwrap();
800 assert!(g.uv.is_some() && g.quad[2] > g.quad[0] && g.quad[3] > g.quad[1], "{c}");
801 }
802 }
803
804 fn text(s: &str) -> Text {
805 Text {
806 position: Point::new(0.0, 0.0),
807 content: s.into(),
808 height: 10.0,
809 rotation: 0.0,
810 halign: HAlign::Left,
811 valign: VAlign::Baseline,
812 }
813 }
814
815 #[test]
816 fn turkish_and_symbols_have_glyphs() {
817 let ts = TextSystem::with_default_font().unwrap();
818 let font = FontRef::new(DEFAULT_FONT).unwrap();
819 for c in "çÇğĞıİöÖşŞüܰ±×∅µ²€ΩπДж".chars() {
820 assert!(map_char(&font, c).is_some(), "missing {c}");
821 assert_ne!(ts.glyph_index(c), 0, "{c}");
822 }
823 assert_eq!(ts.glyph_index('\u{2300}'), ts.glyph_index('\u{2205}'));
825 }
826
827 #[test]
828 fn layout_metrics_follow_the_contract() {
829 let mut ts = TextSystem::with_default_font().unwrap();
830 let g = ts.layout(&text("H")).unwrap();
831 assert_eq!(g.len(), 1);
832 let cap_top = g[0].y1 - (SPREAD as f64 / f64::from(RASTER_PX)) * (10.0 / ts.height_per_em);
834 assert!((cap_top - 7.5).abs() < 0.3, "cap top {cap_top}");
835 let two = ts.layout(&text("H\nH")).unwrap();
837 assert!((two[0].y0 - two[1].y0 - 12.0).abs() < 1e-9);
838 }
839
840 #[test]
841 fn headless_text_metrics_match_the_font() {
842 let mut ts = TextSystem::with_default_font().unwrap();
847 let font = FontRef::new(DEFAULT_FONT).unwrap();
848 let upm = f32::from(font.head().unwrap().units_per_em());
849 assert_eq!(f64::from(upm), Text::FONT_UNITS_PER_EM);
850 let hmtx = font.hmtx().unwrap();
851 let mut checked = 0;
852 for cp in 0..=0xffff_u32 {
853 let Some(c) = char::from_u32(cp).filter(|c| !c.is_control()) else { continue };
854 let Some(gid) = map_char(&font, c) else { continue };
855 let want = f64::from(hmtx.advance(gid).unwrap());
856 assert_eq!(Text::line_pens(&c.to_string()).1, want, "advance of U+{cp:04X}");
857 checked += 1;
858 }
859 assert!(checked > 900, "{checked}");
860 let cap = outline_bounds(&font, map_char(&font, 'H').unwrap(), upm).unwrap().max.1;
861 let descender = -f32::from(font.hhea().unwrap().descender().to_i16());
862 assert!((1.0 / Text::em_size(1.0) - f64::from((cap + descender) / upm)).abs() < 1e-12);
863 let notdef = f64::from(hmtx.advance(GlyphId::NOTDEF).unwrap());
865 assert_eq!(Text::line_pens("\u{e000}").1, notdef);
866 assert_eq!(ts.glyph_index('\u{e000}'), 0);
867 for s in ["Dotloom 0123 ABC xyz", "Ölçü ğüşıİç ÇĞÖŞÜ", "W i\tm", "∅ 40 mm ⌀ ±0,5 €", "Дж Ωπ", "AVATAR"]
869 {
870 let want = ts.line_width(s, 10.0);
871 let got = Text::line_width(s, 10.0);
872 assert!((got - want).abs() <= 1e-9 * want.max(1.0), "{s:?}: geometry {got} vs renderer {want}");
873 }
874 let em = Text::em_size(10.0);
876 let kerned = ts.layout(&text("AV")).unwrap();
877 let a_advance = f64::from(ts.glyph(ts.glyph_index('A')).unwrap().advance) * em;
878 let v_left = f64::from(ts.glyph(ts.glyph_index('V')).unwrap().quad[0]) * em;
879 assert!(kerned[1].x0 < a_advance + v_left - 0.01, "{} vs {}", kerned[1].x0, a_advance + v_left);
880 let t = Text { content: "Ölçü\nÇĞÖŞÜ gjpq".into(), ..text("") };
882 let b = t.layout_box();
883 let pad = SPREAD as f64 / f64::from(RASTER_PX) * (t.height / ts.height_per_em);
884 for g in ts.layout(&t).unwrap() {
885 assert!(g.x0 + pad >= b.min.x - 1e-6 && g.x1 - pad <= b.max.x + 1e-6, "x {g:?} {b:?}");
886 assert!(g.y0 + pad >= b.min.y - 1e-6 && g.y1 - pad <= b.max.y + 1e-6, "y {g:?} {b:?}");
887 }
888 }
889
890 #[test]
891 fn alignment() {
892 let mut ts = TextSystem::with_default_font().unwrap();
893 let mut t = text("İstanbul");
894 let w = ts.line_width(&t.content, t.height);
895 assert!(w > 20.0 && w < 60.0, "{w}");
896 t.halign = HAlign::Right;
897 let g = ts.layout(&t).unwrap();
898 let pad = SPREAD as f64 / f64::from(RASTER_PX) * (t.height / ts.height_per_em);
900 let right = g.last().unwrap().x1 - pad;
901 assert!(right <= 0.1 && right > -2.0, "{right}");
902 t.halign = HAlign::Center;
903 t.valign = VAlign::Middle;
904 let g = ts.layout(&t).unwrap();
905 let min_x = g.iter().map(|q| q.x0).fold(f64::INFINITY, f64::min);
906 assert!((min_x + w * 0.5).abs() < 1.5);
907 }
908
909 #[test]
910 fn sdf_is_monotonic_across_an_edge() {
911 let mut cov = vec![0u8; 100];
913 for y in 3..7 {
914 for x in 3..7 {
915 cov[y * 10 + x] = 255;
916 }
917 }
918 let sdf = signed_distance_field(&cov, 10, 10, 4);
919 let pw = 18;
920 let row = 4 + 5;
921 let vals: Vec<u8> = (0..pw).map(|x| sdf[row * pw + x]).collect();
922 let center = vals[9];
923 assert!(center > 128, "{vals:?}");
924 assert!(vals[0] < 64);
925 for x in 1..9 {
926 assert!(vals[x] >= vals[x - 1], "{vals:?}");
927 }
928 }
929
930 #[test]
931 fn controls_are_skipped_and_empty_text_is_fine() {
932 let mut ts = TextSystem::with_default_font().unwrap();
933 assert!(ts.layout(&text("")).unwrap().is_empty());
934 assert_eq!(ts.layout(&text("a\u{7}b")).unwrap().len(), 2);
935 let mut bad = text("x");
936 bad.height = f64::NAN;
937 assert!(ts.layout(&bad).unwrap().is_empty());
938 }
939}