1#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub struct EdgeColor(pub u8);
10
11impl EdgeColor {
12 pub const RED: Self = Self(1);
14 pub const GREEN: Self = Self(2);
16 pub const BLUE: Self = Self(4);
18 pub const YELLOW: Self = Self(3);
20 pub const CYAN: Self = Self(6);
22 pub const MAGENTA: Self = Self(5);
24 pub const WHITE: Self = Self(7);
26
27 pub fn has_red(self) -> bool {
29 self.0 & 1 != 0
30 }
31
32 pub fn has_green(self) -> bool {
34 self.0 & 2 != 0
35 }
36
37 pub fn has_blue(self) -> bool {
39 self.0 & 4 != 0
40 }
41}
42
43#[derive(Debug, Clone, Copy)]
45pub struct Point {
46 pub x: f64,
48 pub y: f64,
50}
51
52#[derive(Debug, Clone, Copy)]
54pub enum Segment {
55 Line(Point, Point),
57 Quad(Point, Point, Point),
59 Cubic(Point, Point, Point, Point),
61}
62
63#[derive(Debug, Clone)]
65pub struct ColoredSegment {
66 pub segment: Segment,
68 pub color: EdgeColor,
70}
71
72#[derive(Debug, Clone, Default)]
74pub struct Contour {
75 pub segments: Vec<ColoredSegment>,
77}
78
79#[derive(Debug, Clone)]
81pub struct GlyphShape {
82 pub contours: Vec<Contour>,
84 pub units_per_em: f32,
86}
87
88#[derive(Debug, Clone)]
90pub struct MsdfTile {
91 pub glyph_id: u16,
93 pub width: u32,
95 pub height: u32,
97 pub data: Vec<u8>,
99 pub bearing_x: f32,
101 pub bearing_y: f32,
103 pub advance_x: f32,
105}
106
107pub struct OutlineCollector {
111 shape: GlyphShape,
112 current: Option<Contour>,
113 current_pos: Point,
114}
115
116impl OutlineCollector {
117 pub fn new(units_per_em: f32) -> Self {
119 Self {
120 shape: GlyphShape {
121 contours: Vec::new(),
122 units_per_em,
123 },
124 current: None,
125 current_pos: Point { x: 0.0, y: 0.0 },
126 }
127 }
128
129 pub fn finish(mut self) -> GlyphShape {
131 if let Some(c) = self.current.take() {
132 if !c.segments.is_empty() {
133 self.shape.contours.push(c);
134 }
135 }
136 self.shape
137 }
138
139 fn push_segment(&mut self, segment: Segment) {
141 let color = EdgeColor::WHITE; self.current
143 .get_or_insert_with(Contour::default)
144 .segments
145 .push(ColoredSegment { segment, color });
146 }
147}
148
149impl ttf_parser::OutlineBuilder for OutlineCollector {
150 fn move_to(&mut self, x: f32, y: f32) {
151 if let Some(c) = self.current.take() {
153 if !c.segments.is_empty() {
154 self.shape.contours.push(c);
155 }
156 }
157 self.current = Some(Contour::default());
158 self.current_pos = Point {
159 x: x as f64,
160 y: y as f64,
161 };
162 }
163
164 fn line_to(&mut self, x: f32, y: f32) {
165 let p1 = Point {
166 x: x as f64,
167 y: y as f64,
168 };
169 let p0 = self.current_pos;
170 self.push_segment(Segment::Line(p0, p1));
171 self.current_pos = p1;
172 }
173
174 fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
175 let ctrl = Point {
176 x: x1 as f64,
177 y: y1 as f64,
178 };
179 let p1 = Point {
180 x: x as f64,
181 y: y as f64,
182 };
183 let p0 = self.current_pos;
184 self.push_segment(Segment::Quad(p0, ctrl, p1));
185 self.current_pos = p1;
186 }
187
188 fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
189 let c1 = Point {
190 x: x1 as f64,
191 y: y1 as f64,
192 };
193 let c2 = Point {
194 x: x2 as f64,
195 y: y2 as f64,
196 };
197 let p1 = Point {
198 x: x as f64,
199 y: y as f64,
200 };
201 let p0 = self.current_pos;
202 self.push_segment(Segment::Cubic(p0, c1, c2, p1));
203 self.current_pos = p1;
204 }
205
206 fn close(&mut self) {
207 if let Some(c) = self.current.take() {
208 if !c.segments.is_empty() {
209 self.shape.contours.push(c);
210 }
211 }
212 }
213}
214
215pub fn extract_glyph_shape(face_data: &[u8], glyph_id: u16) -> Option<GlyphShape> {
220 let face = ttf_parser::Face::parse(face_data, 0).ok()?;
221 let gid = ttf_parser::GlyphId(glyph_id);
222 let units_per_em = face.units_per_em() as f32;
223 let mut collector = OutlineCollector::new(units_per_em);
224 face.outline_glyph(gid, &mut collector)?;
225 Some(collector.finish())
226}
227
228fn normalize(p: Point) -> Point {
232 let len = (p.x * p.x + p.y * p.y).sqrt();
233 if len < 1e-10 {
234 Point { x: 1.0, y: 0.0 }
235 } else {
236 Point {
237 x: p.x / len,
238 y: p.y / len,
239 }
240 }
241}
242
243fn segment_start_tangent(seg: &Segment) -> Point {
245 match seg {
246 Segment::Line(p0, p1) => normalize(Point {
247 x: p1.x - p0.x,
248 y: p1.y - p0.y,
249 }),
250 Segment::Quad(p0, p1, _) => normalize(Point {
251 x: p1.x - p0.x,
252 y: p1.y - p0.y,
253 }),
254 Segment::Cubic(p0, p1, _, _) => normalize(Point {
255 x: p1.x - p0.x,
256 y: p1.y - p0.y,
257 }),
258 }
259}
260
261fn segment_end_tangent(seg: &Segment) -> Point {
263 match seg {
264 Segment::Line(p0, p1) => normalize(Point {
265 x: p1.x - p0.x,
266 y: p1.y - p0.y,
267 }),
268 Segment::Quad(_, p1, p2) => normalize(Point {
269 x: p2.x - p1.x,
270 y: p2.y - p1.y,
271 }),
272 Segment::Cubic(_, _, p2, p3) => normalize(Point {
273 x: p3.x - p2.x,
274 y: p3.y - p2.y,
275 }),
276 }
277}
278
279pub fn color_edges(shape: &mut GlyphShape) {
286 let corner_dot_threshold: f64 = 3.0_f64.to_radians().cos();
288
289 for contour in &mut shape.contours {
290 if contour.segments.is_empty() {
291 continue;
292 }
293 let n = contour.segments.len();
294
295 let mut corners: Vec<usize> = Vec::new();
297 for i in 0..n {
298 let prev = &contour.segments[(i + n - 1) % n].segment;
299 let curr = &contour.segments[i].segment;
300 let t_prev = segment_end_tangent(prev);
301 let t_curr = segment_start_tangent(curr);
302 let dot = t_prev.x * t_curr.x + t_prev.y * t_curr.y;
303 let cross = t_prev.x * t_curr.y - t_prev.y * t_curr.x;
304 if cross.abs() > corner_dot_threshold.acos().sin() || dot < 0.0 {
307 corners.push(i);
308 }
309 }
310
311 if corners.is_empty() {
312 let seg_per_third = (n.max(3) / 3).max(1);
314 let colors = [EdgeColor::RED, EdgeColor::GREEN, EdgeColor::BLUE];
315 for (i, seg) in contour.segments.iter_mut().enumerate() {
316 seg.color = colors[(i / seg_per_third).min(2)];
317 }
318 } else {
319 let nc = corners.len();
322 let palette = [EdgeColor::RED, EdgeColor::GREEN, EdgeColor::BLUE];
323 let mut color_idx = 0usize;
324
325 for ci in 0..nc {
326 let start = corners[ci];
327 let end = corners[(ci + 1) % nc];
328 let span_len = if end > start {
329 end - start
330 } else {
331 n - start + end
332 };
333
334 let color = if span_len == 1 {
335 let c1 = palette[color_idx % 3];
337 let c2 = palette[(color_idx + 1) % 3];
338 color_idx += 1;
339 EdgeColor(c1.0 | c2.0)
340 } else {
341 let c = palette[color_idx % 3];
342 color_idx += 1;
343 c
344 };
345
346 for j in 0..span_len {
347 contour.segments[(start + j) % n].color = color;
348 }
349 }
350 }
351 }
352}
353
354fn signed_pseudo_dist_line(px: f64, py: f64, ax: f64, ay: f64, bx: f64, by: f64) -> f64 {
360 let dx = bx - ax;
361 let dy = by - ay;
362 let len2 = dx * dx + dy * dy;
363 if len2 < 1e-12 {
364 return ((px - ax) * (px - ax) + (py - ay) * (py - ay)).sqrt();
365 }
366 let t = ((px - ax) * dx + (py - ay) * dy) / len2;
367 let t = t.clamp(0.0, 1.0);
368 let cx = ax + t * dx - px;
369 let cy = ay + t * dy - py;
370 let dist = (cx * cx + cy * cy).sqrt();
371 let cross = (bx - ax) * (py - ay) - (by - ay) * (px - ax);
372 if cross < 0.0 {
373 -dist
374 } else {
375 dist
376 }
377}
378
379fn newton_raphson_quad(px: f64, py: f64, p0: Point, p1: Point, p2: Point, t_init: f64) -> f64 {
381 let mut t = t_init.clamp(0.0, 1.0);
382 for _ in 0..8 {
383 let qx = (1.0 - t) * (1.0 - t) * p0.x + 2.0 * t * (1.0 - t) * p1.x + t * t * p2.x;
384 let qy = (1.0 - t) * (1.0 - t) * p0.y + 2.0 * t * (1.0 - t) * p1.y + t * t * p2.y;
385 let dqx = 2.0 * (1.0 - t) * (p1.x - p0.x) + 2.0 * t * (p2.x - p1.x);
387 let dqy = 2.0 * (1.0 - t) * (p1.y - p0.y) + 2.0 * t * (p2.y - p1.y);
388 let ddqx = 2.0 * (p2.x - 2.0 * p1.x + p0.x);
390 let ddqy = 2.0 * (p2.y - 2.0 * p1.y + p0.y);
391 let fx = dqx * (qx - px) + dqy * (qy - py);
393 let dfx = dqx * dqx + dqy * dqy + ddqx * (qx - px) + ddqy * (qy - py);
395 if dfx.abs() < 1e-12 {
396 break;
397 }
398 let dt = fx / dfx;
399 t = (t - dt).clamp(0.0, 1.0);
400 if dt.abs() < 1e-9 {
401 break;
402 }
403 }
404 t
405}
406
407fn signed_pseudo_dist_quad(px: f64, py: f64, p0: Point, p1: Point, p2: Point) -> f64 {
409 let mut best_dist = f64::MAX;
411 let mut best_t = 0.0f64;
412 for i in 0..=8 {
413 let t = i as f64 / 8.0;
414 let qx = (1.0 - t) * (1.0 - t) * p0.x + 2.0 * t * (1.0 - t) * p1.x + t * t * p2.x;
415 let qy = (1.0 - t) * (1.0 - t) * p0.y + 2.0 * t * (1.0 - t) * p1.y + t * t * p2.y;
416 let d = ((qx - px) * (qx - px) + (qy - py) * (qy - py)).sqrt();
417 if d < best_dist {
418 best_dist = d;
419 best_t = t;
420 }
421 }
422
423 let t = newton_raphson_quad(px, py, p0, p1, p2, best_t);
425 let qx = (1.0 - t) * (1.0 - t) * p0.x + 2.0 * t * (1.0 - t) * p1.x + t * t * p2.x;
426 let qy = (1.0 - t) * (1.0 - t) * p0.y + 2.0 * t * (1.0 - t) * p1.y + t * t * p2.y;
427 let dist = ((qx - px) * (qx - px) + (qy - py) * (qy - py)).sqrt();
428
429 let dtx = 2.0 * (1.0 - t) * (p1.x - p0.x) + 2.0 * t * (p2.x - p1.x);
431 let dty = 2.0 * (1.0 - t) * (p1.y - p0.y) + 2.0 * t * (p2.y - p1.y);
432 let cross = dtx * (py - qy) - dty * (px - qx);
433 if cross < 0.0 {
434 -dist
435 } else {
436 dist
437 }
438}
439
440fn newton_raphson_cubic(
442 px: f64,
443 py: f64,
444 p0: Point,
445 p1: Point,
446 p2: Point,
447 p3: Point,
448 t_init: f64,
449) -> f64 {
450 let mut t = t_init.clamp(0.0, 1.0);
451 for _ in 0..8 {
452 let u = 1.0 - t;
453 let qx =
454 u * u * u * p0.x + 3.0 * u * u * t * p1.x + 3.0 * u * t * t * p2.x + t * t * t * p3.x;
455 let qy =
456 u * u * u * p0.y + 3.0 * u * u * t * p1.y + 3.0 * u * t * t * p2.y + t * t * t * p3.y;
457 let dqx =
459 3.0 * u * u * (p1.x - p0.x) + 6.0 * u * t * (p2.x - p1.x) + 3.0 * t * t * (p3.x - p2.x);
460 let dqy =
461 3.0 * u * u * (p1.y - p0.y) + 6.0 * u * t * (p2.y - p1.y) + 3.0 * t * t * (p3.y - p2.y);
462 let ddqx = 6.0 * u * (p2.x - 2.0 * p1.x + p0.x) + 6.0 * t * (p3.x - 2.0 * p2.x + p1.x);
464 let ddqy = 6.0 * u * (p2.y - 2.0 * p1.y + p0.y) + 6.0 * t * (p3.y - 2.0 * p2.y + p1.y);
465
466 let fx = dqx * (qx - px) + dqy * (qy - py);
467 let dfx = dqx * dqx + dqy * dqy + ddqx * (qx - px) + ddqy * (qy - py);
468 if dfx.abs() < 1e-12 {
469 break;
470 }
471 let dt = fx / dfx;
472 t = (t - dt).clamp(0.0, 1.0);
473 if dt.abs() < 1e-9 {
474 break;
475 }
476 }
477 t
478}
479
480fn signed_pseudo_dist_cubic(px: f64, py: f64, p0: Point, p1: Point, p2: Point, p3: Point) -> f64 {
482 let mut best_dist = f64::MAX;
483 let mut best_t = 0.0f64;
484 for i in 0..=8 {
485 let t = i as f64 / 8.0;
486 let u = 1.0 - t;
487 let qx =
488 u * u * u * p0.x + 3.0 * u * u * t * p1.x + 3.0 * u * t * t * p2.x + t * t * t * p3.x;
489 let qy =
490 u * u * u * p0.y + 3.0 * u * u * t * p1.y + 3.0 * u * t * t * p2.y + t * t * t * p3.y;
491 let d = ((qx - px) * (qx - px) + (qy - py) * (qy - py)).sqrt();
492 if d < best_dist {
493 best_dist = d;
494 best_t = t;
495 }
496 }
497
498 let t = newton_raphson_cubic(px, py, p0, p1, p2, p3, best_t);
499 let u = 1.0 - t;
500 let qx = u * u * u * p0.x + 3.0 * u * u * t * p1.x + 3.0 * u * t * t * p2.x + t * t * t * p3.x;
501 let qy = u * u * u * p0.y + 3.0 * u * u * t * p1.y + 3.0 * u * t * t * p2.y + t * t * t * p3.y;
502 let dist = ((qx - px) * (qx - px) + (qy - py) * (qy - py)).sqrt();
503
504 let dtx =
505 3.0 * u * u * (p1.x - p0.x) + 6.0 * u * t * (p2.x - p1.x) + 3.0 * t * t * (p3.x - p2.x);
506 let dty =
507 3.0 * u * u * (p1.y - p0.y) + 6.0 * u * t * (p2.y - p1.y) + 3.0 * t * t * (p3.y - p2.y);
508 let cross = dtx * (py - qy) - dty * (px - qx);
509 if cross < 0.0 {
510 -dist
511 } else {
512 dist
513 }
514}
515
516pub(crate) fn segment_signed_dist(seg: &Segment, px: f64, py: f64) -> f64 {
518 match seg {
519 Segment::Line(a, b) => signed_pseudo_dist_line(px, py, a.x, a.y, b.x, b.y),
520 Segment::Quad(p0, p1, p2) => signed_pseudo_dist_quad(px, py, *p0, *p1, *p2),
521 Segment::Cubic(p0, p1, p2, p3) => signed_pseudo_dist_cubic(px, py, *p0, *p1, *p2, *p3),
522 }
523}
524
525pub fn compute_msdf(
545 shape: &GlyphShape,
546 width: u32,
547 height: u32,
548 spread: f32,
549 scale: f32,
550 offset_x: f32,
551 offset_y: f32,
552) -> Result<Vec<u8>, crate::edt::SdfError> {
553 if width == 0 || height == 0 {
554 return Err(crate::edt::SdfError::ZeroSize);
555 }
556 let mut output = vec![0u8; width as usize * height as usize * 3];
557 let spread_f64 = spread as f64;
558
559 for py in 0..height {
560 for px in 0..width {
561 let fx = (px as f32 + 0.5) / scale + offset_x;
563 let fy = (py as f32 + 0.5) / scale + offset_y;
565
566 let mut r_dist = f64::MAX;
567 let mut g_dist = f64::MAX;
568 let mut b_dist = f64::MAX;
569
570 for contour in &shape.contours {
571 for colored_seg in &contour.segments {
572 let d = segment_signed_dist(&colored_seg.segment, fx as f64, fy as f64);
573 if colored_seg.color.has_red() && d.abs() < r_dist.abs() {
574 r_dist = d;
575 }
576 if colored_seg.color.has_green() && d.abs() < g_dist.abs() {
577 g_dist = d;
578 }
579 if colored_seg.color.has_blue() && d.abs() < b_dist.abs() {
580 b_dist = d;
581 }
582 }
583 }
584
585 let norm = |d: f64| -> u8 {
587 ((d / spread_f64 + 1.0) * 0.5 * 255.0)
588 .clamp(0.0, 255.0)
589 .round() as u8
590 };
591
592 let idx = (py as usize * width as usize + px as usize) * 3;
593 output[idx] = if r_dist == f64::MAX { 0 } else { norm(r_dist) };
594 output[idx + 1] = if g_dist == f64::MAX { 0 } else { norm(g_dist) };
595 output[idx + 2] = if b_dist == f64::MAX { 0 } else { norm(b_dist) };
596 }
597 }
598 Ok(output)
599}
600
601pub fn glyph_to_msdf_tile(
611 face_data: &[u8],
612 glyph_id: u16,
613 px_size: f32,
614 tile_width: u32,
615 tile_height: u32,
616 spread: f32,
617 padding: u32,
618) -> Result<Option<MsdfTile>, crate::edt::SdfError> {
619 let Some(mut shape) = extract_glyph_shape(face_data, glyph_id) else {
620 return Ok(None);
621 };
622 if shape.contours.is_empty() {
623 return Ok(None);
624 }
625
626 color_edges(&mut shape);
627
628 let face =
629 ttf_parser::Face::parse(face_data, 0).map_err(|_| crate::edt::SdfError::InvalidFont)?;
630 let gid = ttf_parser::GlyphId(glyph_id);
631 let scale_full = px_size / shape.units_per_em;
632
633 let bearing_x = face.glyph_hor_side_bearing(gid).unwrap_or(0) as f32 * scale_full;
634 let bearing_y = face
635 .glyph_hor_advance(gid)
636 .map(|_| face.ascender() as f32 * scale_full)
637 .unwrap_or(px_size);
638 let advance_x = face.glyph_hor_advance(gid).unwrap_or(0) as f32 * scale_full;
639
640 let bbox = face
642 .glyph_bounding_box(gid)
643 .ok_or(crate::edt::SdfError::ZeroSize)?;
644 let shape_w = (bbox.x_max - bbox.x_min) as f32;
645 let shape_h = (bbox.y_max - bbox.y_min) as f32;
646
647 let eff_w = tile_width.saturating_sub(2 * padding).max(1) as f32;
648 let eff_h = tile_height.saturating_sub(2 * padding).max(1) as f32;
649 let fit_scale = (eff_w / shape_w).min(eff_h / shape_h);
650 let offset_x = bbox.x_min as f32 - padding as f32 / fit_scale;
651 let offset_y = bbox.y_min as f32 - padding as f32 / fit_scale;
652
653 let data = compute_msdf(
654 &shape,
655 tile_width,
656 tile_height,
657 spread,
658 fit_scale,
659 offset_x,
660 offset_y,
661 )?;
662
663 Ok(Some(MsdfTile {
664 glyph_id,
665 width: tile_width,
666 height: tile_height,
667 data,
668 bearing_x,
669 bearing_y,
670 advance_x,
671 }))
672}
673
674#[derive(Debug, Clone)]
681pub struct MtsdfTile {
682 pub glyph_id: u16,
684 pub width: u32,
686 pub height: u32,
688 pub data: Vec<u8>,
690 pub bearing_x: f32,
692 pub bearing_y: f32,
694 pub advance_x: f32,
696}
697
698pub fn compute_mtsdf(
717 shape: &GlyphShape,
718 width: u32,
719 height: u32,
720 spread: f32,
721 scale: f32,
722 offset_x: f32,
723 offset_y: f32,
724) -> Result<Vec<u8>, crate::edt::SdfError> {
725 if width == 0 || height == 0 {
726 return Err(crate::edt::SdfError::ZeroSize);
727 }
728
729 let rgb = compute_msdf(shape, width, height, spread, scale, offset_x, offset_y)?;
731
732 let spread_f64 = spread as f64;
733 let n = width as usize * height as usize;
734 let mut alpha = vec![0u8; n];
735
736 for py in 0..height {
737 for px in 0..width {
738 let fx = (px as f32 + 0.5) / scale + offset_x;
739 let fy = (py as f32 + 0.5) / scale + offset_y;
740
741 let mut min_dist = f64::MAX;
743 for contour in &shape.contours {
744 for seg in &contour.segments {
745 let d = segment_signed_dist(&seg.segment, fx as f64, fy as f64);
746 if d.abs() < min_dist.abs() {
747 min_dist = d;
748 }
749 }
750 }
751
752 let v = if min_dist == f64::MAX {
753 0u8
754 } else {
755 ((min_dist / spread_f64 + 1.0) * 0.5 * 255.0)
756 .clamp(0.0, 255.0)
757 .round() as u8
758 };
759 alpha[py as usize * width as usize + px as usize] = v;
760 }
761 }
762
763 let mut rgba = vec![0u8; n * 4];
765 for i in 0..n {
766 rgba[i * 4] = rgb[i * 3];
767 rgba[i * 4 + 1] = rgb[i * 3 + 1];
768 rgba[i * 4 + 2] = rgb[i * 3 + 2];
769 rgba[i * 4 + 3] = alpha[i];
770 }
771 Ok(rgba)
772}
773
774pub fn glyph_to_mtsdf_tile(
782 face_data: &[u8],
783 glyph_id: u16,
784 px_size: f32,
785 tile_width: u32,
786 tile_height: u32,
787 spread: f32,
788 padding: u32,
789) -> Result<Option<MtsdfTile>, crate::edt::SdfError> {
790 let Some(mut shape) = extract_glyph_shape(face_data, glyph_id) else {
791 return Ok(None);
792 };
793 if shape.contours.is_empty() {
794 return Ok(None);
795 }
796
797 color_edges(&mut shape);
798
799 let face =
800 ttf_parser::Face::parse(face_data, 0).map_err(|_| crate::edt::SdfError::InvalidFont)?;
801 let gid = ttf_parser::GlyphId(glyph_id);
802 let scale_full = px_size / shape.units_per_em;
803
804 let bearing_x = face.glyph_hor_side_bearing(gid).unwrap_or(0) as f32 * scale_full;
805 let bearing_y = face
806 .glyph_hor_advance(gid)
807 .map(|_| face.ascender() as f32 * scale_full)
808 .unwrap_or(px_size);
809 let advance_x = face.glyph_hor_advance(gid).unwrap_or(0) as f32 * scale_full;
810
811 let bbox = face
812 .glyph_bounding_box(gid)
813 .ok_or(crate::edt::SdfError::ZeroSize)?;
814 let shape_w = (bbox.x_max - bbox.x_min) as f32;
815 let shape_h = (bbox.y_max - bbox.y_min) as f32;
816
817 let eff_w = tile_width.saturating_sub(2 * padding).max(1) as f32;
818 let eff_h = tile_height.saturating_sub(2 * padding).max(1) as f32;
819 let fit_scale = (eff_w / shape_w).min(eff_h / shape_h);
820 let offset_x = bbox.x_min as f32 - padding as f32 / fit_scale;
821 let offset_y = bbox.y_min as f32 - padding as f32 / fit_scale;
822
823 let data = compute_mtsdf(
824 &shape,
825 tile_width,
826 tile_height,
827 spread,
828 fit_scale,
829 offset_x,
830 offset_y,
831 )?;
832
833 Ok(Some(MtsdfTile {
834 glyph_id,
835 width: tile_width,
836 height: tile_height,
837 data,
838 bearing_x,
839 bearing_y,
840 advance_x,
841 }))
842}
843
844#[cfg(test)]
847mod tests {
848 use super::*;
849
850 fn square_shape(size: f64) -> GlyphShape {
852 let segs = vec![
853 ColoredSegment {
854 segment: Segment::Line(Point { x: 0.0, y: 0.0 }, Point { x: size, y: 0.0 }),
855 color: EdgeColor::RED,
856 },
857 ColoredSegment {
858 segment: Segment::Line(Point { x: size, y: 0.0 }, Point { x: size, y: size }),
859 color: EdgeColor::GREEN,
860 },
861 ColoredSegment {
862 segment: Segment::Line(Point { x: size, y: size }, Point { x: 0.0, y: size }),
863 color: EdgeColor::BLUE,
864 },
865 ColoredSegment {
866 segment: Segment::Line(Point { x: 0.0, y: size }, Point { x: 0.0, y: 0.0 }),
867 color: EdgeColor::RED,
868 },
869 ];
870 GlyphShape {
871 contours: vec![Contour { segments: segs }],
872 units_per_em: 1000.0,
873 }
874 }
875
876 #[test]
877 fn color_bits() {
878 assert!(EdgeColor::RED.has_red());
879 assert!(!EdgeColor::RED.has_green());
880 assert!(EdgeColor::YELLOW.has_red() && EdgeColor::YELLOW.has_green());
881 assert!(
882 EdgeColor::WHITE.has_red()
883 && EdgeColor::WHITE.has_green()
884 && EdgeColor::WHITE.has_blue()
885 );
886 }
887
888 #[test]
889 fn msdf_output_has_three_channels() {
890 let shape = square_shape(500.0);
891 let result = compute_msdf(&shape, 16, 16, 4.0, 0.016, -50.0, -50.0);
892 assert!(result.is_ok());
893 let data = result.unwrap();
894 assert_eq!(data.len(), 16 * 16 * 3);
895 }
896
897 #[test]
898 fn sdf_midpoint_near_128() {
899 let shape = square_shape(1000.0);
900 let result = compute_msdf(&shape, 32, 32, 8.0, 0.032, -16.0, -16.0);
901 let data = result.unwrap();
902 let has_nonzero = data.iter().any(|&v| v > 10 && v < 245);
903 assert!(has_nonzero, "SDF should have values spanning the range");
904 }
905
906 #[test]
907 fn empty_glyph_returns_ok() {
908 let empty = GlyphShape {
909 contours: vec![],
910 units_per_em: 1000.0,
911 };
912 let result = compute_msdf(&empty, 8, 8, 4.0, 1.0, 0.0, 0.0);
913 assert!(result.is_ok());
915 }
916
917 #[test]
918 fn edge_color_hashable() {
919 let mut map = std::collections::HashMap::new();
920 map.insert(EdgeColor::RED, 1u32);
921 map.insert(EdgeColor::GREEN, 2u32);
922 assert_eq!(map.get(&EdgeColor::RED), Some(&1));
923 }
924
925 #[test]
926 fn color_edges_smooth_assigns_three_colors() {
927 let mut shape = square_shape(500.0);
928 color_edges(&mut shape);
929 let colors: std::collections::HashSet<u8> = shape.contours[0]
930 .segments
931 .iter()
932 .map(|s| s.color.0)
933 .collect();
934 assert!(
936 colors.len() >= 2,
937 "expected multi-color assignment, got {colors:?}"
938 );
939 }
940
941 #[test]
942 fn zero_size_returns_error() {
943 let shape = square_shape(500.0);
944 assert!(compute_msdf(&shape, 0, 16, 4.0, 1.0, 0.0, 0.0).is_err());
945 assert!(compute_msdf(&shape, 16, 0, 4.0, 1.0, 0.0, 0.0).is_err());
946 }
947
948 #[test]
949 fn mtsdf_four_channel_output() {
950 let shape = GlyphShape {
951 contours: vec![Contour {
952 segments: vec![ColoredSegment {
953 segment: Segment::Line(Point { x: 0.0, y: 0.0 }, Point { x: 100.0, y: 0.0 }),
954 color: EdgeColor::WHITE,
955 }],
956 }],
957 units_per_em: 1000.0,
958 };
959 let result = compute_mtsdf(&shape, 8, 8, 4.0, 0.08, -50.0, -50.0);
960 assert!(result.is_ok(), "compute_mtsdf failed: {:?}", result);
961 let data = result.unwrap();
962 assert_eq!(
963 data.len(),
964 8 * 8 * 4,
965 "MTSDF output should be RGBA (4 bytes/pixel)"
966 );
967 }
968
969 #[test]
970 fn mtsdf_zero_size_errors() {
971 let shape = square_shape(500.0);
972 assert!(compute_mtsdf(&shape, 0, 8, 4.0, 1.0, 0.0, 0.0).is_err());
973 assert!(compute_mtsdf(&shape, 8, 0, 4.0, 1.0, 0.0, 0.0).is_err());
974 }
975
976 const FONT: &[u8] = include_bytes!("../../../tests/fixtures/test-font.ttf");
990
991 fn glyph_id_for_char(ch: char) -> u16 {
993 let face = ttf_parser::Face::parse(FONT, 0).expect("parse test font");
994 face.glyph_index(ch).expect("char not found in test font").0
995 }
996
997 #[test]
999 fn msdf_edge_coloring_assigns_distinct_channels() {
1000 let gid = glyph_id_for_char('H');
1003
1004 let tile_w = 32u32;
1005 let tile_h = 32u32;
1006 let px_size = 24.0f32;
1007 let spread = 4.0f32;
1008 let padding = 2u32;
1009
1010 let tile = glyph_to_msdf_tile(FONT, gid, px_size, tile_w, tile_h, spread, padding)
1011 .expect("glyph_to_msdf_tile should not error")
1012 .expect("'H' should have a non-empty outline");
1013
1014 assert_eq!(
1016 tile.data.len(),
1017 (tile_w * tile_h * 3) as usize,
1018 "MSDF tile should have 3 bytes/pixel (RGB)"
1019 );
1020
1021 let n_pixels = (tile_w * tile_h) as usize;
1023 let mut foreground_pixels = 0usize;
1024 let mut distinct_pixels = 0usize;
1025
1026 for i in 0..n_pixels {
1027 let r = tile.data[i * 3];
1028 let g = tile.data[i * 3 + 1];
1029 let b = tile.data[i * 3 + 2];
1030 if r > 5 || g > 5 || b > 5 {
1032 foreground_pixels += 1;
1033 if r != g || g != b || r != b {
1034 distinct_pixels += 1;
1035 }
1036 }
1037 }
1038
1039 assert!(
1040 foreground_pixels > 0,
1041 "MSDF tile for 'H' should have at least some foreground pixels"
1042 );
1043
1044 let ratio = distinct_pixels as f64 / foreground_pixels as f64;
1046 assert!(
1047 ratio >= 0.05,
1048 "Expected ≥5% of foreground pixels to have distinct R/G/B channels \
1049 (confirms edge coloring), got {:.1}% ({distinct_pixels}/{foreground_pixels})",
1050 ratio * 100.0
1051 );
1052 }
1053}