1use std::collections::HashMap;
11
12use agg_rust::basics::{VertexD, VertexSource, PATH_CMD_STOP};
13use agg_rust::path_storage::PathStorage;
14use agg_rust::rasterizer_cells_aa::CellAa;
15
16use crate::font::parsed::{build_glyph_path, OwnedGlyph, ParsedFont};
17
18pub(crate) struct SliceVertexSource<'a> {
24 verts: &'a [VertexD],
25 pos: usize,
26}
27
28impl<'a> SliceVertexSource<'a> {
29 pub(crate) const fn new(verts: &'a [VertexD]) -> Self {
30 Self { verts, pos: 0 }
31 }
32}
33
34impl VertexSource for SliceVertexSource<'_> {
35 fn rewind(&mut self, _path_id: u32) {
36 self.pos = 0;
37 }
38
39 fn vertex(&mut self, x: &mut f64, y: &mut f64) -> u32 {
40 match self.verts.get(self.pos) {
41 Some(v) => {
42 self.pos += 1;
43 *x = v.x;
44 *y = v.y;
45 v.cmd
46 }
47 None => PATH_CMD_STOP,
48 }
49 }
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
55struct GlyphPathKey {
56 font_hash: u64,
57 glyph_id: u16,
58 ppem: u16,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
64struct GlyphCellKey {
65 font_hash: u64,
66 glyph_id: u16,
67 ppem: u16,
68 scale_fixed: u32,
71 subpx_x: u8,
73 subpx_y: u8,
75}
76
77pub struct CachedGlyph<'a> {
79 pub path: &'a PathStorage,
80 pub is_hinted: bool,
81}
82
83impl core::fmt::Debug for CachedGlyph<'_> {
84 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
86 f.debug_struct("CachedGlyph")
87 .field("is_hinted", &self.is_hinted)
88 .finish_non_exhaustive()
89 }
90}
91
92struct CachedCells {
96 cells: Vec<CellAa>,
97}
98
99const MAX_PATH_ENTRIES: usize = 8192;
102const MAX_CELL_ENTRIES: usize = 16384;
105
106pub struct GlyphCache {
108 paths: HashMap<GlyphPathKey, Option<(PathStorage, bool)>>,
109 cells: HashMap<GlyphCellKey, Option<CachedCells>>,
110}
111
112impl core::fmt::Debug for GlyphCache {
113 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
115 f.debug_struct("GlyphCache")
116 .field("paths", &self.paths.len())
117 .field("cells", &self.cells.len())
118 .finish_non_exhaustive()
119 }
120}
121
122#[inline]
124#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] fn quantize_subpx(frac: f32) -> u8 {
126 let f = frac - frac.floor();
127 (f * 4.0).min(3.0) as u8
128}
129
130impl Default for GlyphCache {
131 fn default() -> Self {
132 Self::new()
133 }
134}
135
136impl GlyphCache {
137 #[must_use]
138 pub fn new() -> Self {
139 Self {
140 paths: HashMap::new(),
141 cells: HashMap::new(),
142 }
143 }
144
145 #[must_use] pub fn paths_len(&self) -> usize { self.paths.len() }
147
148 #[must_use] pub fn cells_len(&self) -> usize { self.cells.len() }
150
151 pub fn get_or_build(
154 &mut self,
155 font_hash: u64,
156 glyph_id: u16,
157 glyph_data: &OwnedGlyph,
158 parsed_font: &ParsedFont,
159 ppem: u16,
160 ) -> Option<CachedGlyph<'_>> {
161 if self.paths.len() >= MAX_PATH_ENTRIES {
162 self.paths.clear();
163 }
164 let key = GlyphPathKey { font_hash, glyph_id, ppem };
165 let entry = self
166 .paths
167 .entry(key)
168 .or_insert_with(|| {
169 if ppem > 0 {
171 if let Some(path) = build_hinted_path(glyph_id, glyph_data, parsed_font, ppem) {
172 return Some((path, true));
173 }
174 }
175 build_glyph_path(glyph_data).map(|p| (p, false))
177 });
178 entry.as_ref().map(|(path, is_hinted)| CachedGlyph {
179 path,
180 is_hinted: *is_hinted,
181 })
182 }
183
184 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] pub fn get_or_build_cells(
200 &mut self,
201 font_hash: u64,
202 glyph_id: u16,
203 ppem: u16,
204 glyph_x: f32,
205 glyph_y: f32,
206 scale: f32,
207 is_hinted: bool,
208 hint_correction: f32,
209 ) -> Option<(&[CellAa], i32, i32)> {
210 if self.cells.len() >= MAX_CELL_ENTRIES {
211 self.cells.clear();
212 }
213 let rescale_hinted = is_hinted && (hint_correction - 1.0).abs() > 1e-4;
217 let grid_snapped = is_hinted && !rescale_hinted;
218
219 let subpx_x_snap = grid_snapped && !text_subpixel_enabled();
229 let subpx_x = if subpx_x_snap { 0 } else { quantize_subpx(glyph_x) };
230 let subpx_y = if grid_snapped { 0 } else { quantize_subpx(glyph_y) };
231 debug_assert!((0.0..65536.0).contains(&scale), "scale out of range for fixed-point: {scale}");
232 let scale_fixed = if is_hinted {
233 if rescale_hinted { (hint_correction * 65536.0) as u32 } else { 0 }
234 } else {
235 (scale * 65536.0) as u32
236 };
237
238 let cell_key = GlyphCellKey {
239 font_hash, glyph_id, ppem, scale_fixed, subpx_x, subpx_y,
240 };
241
242 let int_x = if subpx_x_snap { glyph_x.round() as i32 } else { glyph_x.floor() as i32 };
246 let int_y = if grid_snapped { glyph_y.round() as i32 } else { glyph_y.floor() as i32 };
247
248 if !self.cells.contains_key(&cell_key) {
249 let path_key = GlyphPathKey { font_hash, glyph_id, ppem };
251 let path_entry = self.paths.get(&path_key);
252 let cached_cells = path_entry.and_then(|entry| {
253 use agg_rust::trans_affine::TransAffine;
254 use agg_rust::basics::FillingRule;
255 use agg_rust::rasterizer_scanline_aa::RasterizerScanlineAa;
256 let (path, _) = entry.as_ref()?;
257 let frac_x = f64::from(subpx_x) * 0.25;
258 let frac_y = f64::from(subpx_y) * 0.25;
259
260 let mut ras = RasterizerScanlineAa::new();
261 ras.filling_rule(FillingRule::NonZero);
262
263 let transform = if is_hinted {
264 if rescale_hinted {
265 let mut t = TransAffine::new_scaling_uniform(f64::from(hint_correction));
266 t.multiply(&TransAffine::new_translation(frac_x, frac_y));
267 t
268 } else {
269 TransAffine::new_translation(frac_x, frac_y)
270 }
271 } else {
272 let mut t = TransAffine::new_scaling_uniform(f64::from(scale));
273 t.multiply(&TransAffine::new_translation(frac_x, frac_y));
274 t
275 };
276
277 let mut src = agg_rust::conv_transform::ConvTransform::new(
280 SliceVertexSource::new(path.vertices()),
281 transform,
282 );
283 ras.add_path(&mut src, 0);
284 let cells = ras.outline_cells();
285 if cells.is_empty() { None } else { Some(CachedCells { cells }) }
286 });
287 self.cells.insert(cell_key, cached_cells);
288 }
289
290 let entry = self.cells.get(&cell_key)?;
291 entry.as_ref().map(|cc| (cc.cells.as_slice(), int_x, int_y))
292 }
293}
294
295fn glyph_lsb(parsed_font: &ParsedFont, glyph_id: u16) -> Option<i16> {
303 let (off, len) = parsed_font.hmtx_range;
304 if len == 0 {
305 return None;
306 }
307 let bytes = parsed_font.original_bytes.as_ref()?;
308 let hmtx = bytes.as_ref().get(off..off + len)?;
309 let num = usize::from(parsed_font.hhea_table.num_h_metrics);
310 if num == 0 {
311 return None;
312 }
313 let gid = usize::from(glyph_id);
314 let lsb_off = if gid < num {
317 gid * 4 + 2
318 } else {
319 num * 4 + (gid - num) * 2
320 };
321 let b = hmtx.get(lsb_off..lsb_off + 2)?;
322 Some(i16::from_be_bytes([b[0], b[1]]))
323}
324
325fn build_hinted_path(
326 glyph_id: u16,
327 glyph: &OwnedGlyph,
328 parsed_font: &ParsedFont,
329 ppem: u16,
330) -> Option<PathStorage> {
331 use allsorts::hinting::f26dot6::F26Dot6;
332 let raw_points = glyph.raw_points.as_ref()?;
333 let raw_on_curve = glyph.raw_on_curve.as_ref()?;
334 let raw_contour_ends = glyph.raw_contour_ends.as_ref()?;
335 let instructions = glyph.instructions.as_ref()?;
336
337 if raw_points.is_empty() || raw_contour_ends.is_empty() {
338 return None;
339 }
340
341 let hint_mutex = parsed_font.hint_instance.as_ref()?;
342 let mut hint = hint_mutex.lock().ok()?;
343
344 let upem = parsed_font.font_metrics.units_per_em;
345 if upem == 0 {
346 return None;
347 }
348
349 if hint.set_ppem(ppem, f64::from(ppem)).is_err() {
351 return None;
352 }
353
354 let scale = allsorts::hinting::f26dot6::compute_scale(ppem, upem);
356
357 let points_f26dot6: Vec<(i32, i32)> = raw_points
358 .iter()
359 .map(|&(x, y)| {
360 let sx = F26Dot6::from_funits(i32::from(x), scale);
361 let sy = F26Dot6::from_funits(i32::from(y), scale);
362 (sx.to_bits(), sy.to_bits())
363 })
364 .collect();
365
366 let adv_f26dot6 = F26Dot6::from_funits(i32::from(glyph.horz_advance), scale).to_bits();
368
369 let x_min = i32::from(glyph.bounding_box.min_x);
374 let lsb = glyph_lsb(parsed_font, glyph_id).map_or(x_min, i32::from);
375 let pp1_x_f26dot6 = F26Dot6::from_funits(x_min - lsb, scale).to_bits();
376
377 let Ok((hinted, hinted_on_curve)) = hint.hint_glyph_with_flags_pp1(
383 &points_f26dot6,
384 raw_on_curve,
385 raw_contour_ends,
386 instructions,
387 adv_f26dot6,
388 pp1_x_f26dot6,
389 ) else {
390 return None;
391 };
392 drop(hint);
393
394 if hint_light_enabled() {
405 let light: Vec<(i32, i32)> = hinted
406 .iter()
407 .enumerate()
408 .map(|(i, &(hx, hy))| (points_f26dot6.get(i).map_or(hx, |p| p.0), hy))
409 .collect();
410 return build_path_from_contours(&light, &hinted_on_curve, raw_contour_ends);
411 }
412
413 build_path_from_contours(&hinted, &hinted_on_curve, raw_contour_ends)
415}
416
417fn hint_light_enabled() -> bool {
421 static V: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
422 *V.get_or_init(|| {
423 std::env::var("AZ_HINT_LIGHT")
424 .map(|s| !(s == "0" || s.eq_ignore_ascii_case("false")))
425 .unwrap_or(true)
426 })
427}
428
429pub(crate) fn text_subpixel_enabled() -> bool {
440 static V: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
441 *V.get_or_init(|| {
442 std::env::var("AZ_TEXT_SUBPIXEL")
443 .map(|s| !(s == "0" || s.eq_ignore_ascii_case("false")))
444 .unwrap_or(true)
445 })
446}
447
448#[must_use] pub fn build_path_from_contours(
457 points: &[(i32, i32)],
458 on_curve: &[bool],
459 contour_ends: &[u16],
460) -> Option<PathStorage> {
461 use agg_rust::basics::PATH_FLAGS_NONE;
462
463 let mut path = PathStorage::new();
464 let mut has_ops = false;
465 let mut contour_start = 0usize;
466
467 for &end_idx in contour_ends {
468 let end = end_idx as usize;
469 if end >= points.len() || contour_start > end {
470 contour_start = end + 1;
471 continue;
472 }
473
474 let pts = &points[contour_start..=end];
475 let flags = &on_curve[contour_start..=end];
476 let n = pts.len();
477 if n < 2 {
478 contour_start = end + 1;
479 continue;
480 }
481
482 let px = |i: usize| -> (f64, f64) {
484 (f64::from(f26_to_px(pts[i].0)), f64::from(-f26_to_px(pts[i].1)))
485 };
486 let mid = |a: (f64, f64), b: (f64, f64)| -> (f64, f64) {
487 ((a.0 + b.0) * 0.5, (a.1 + b.1) * 0.5)
488 };
489
490 let (origin, start, until) = if flags[0] {
492 (px(0), 1usize, n)
493 } else if flags[n - 1] {
494 (px(n - 1), 0usize, n - 1)
495 } else {
496 (mid(px(0), px(n - 1)), 0usize, n)
497 };
498
499 path.move_to(origin.0, origin.1);
500 has_ops = true;
501
502 let mut i = start;
503 while i < until {
504 if flags[i] {
505 let to = px(i);
507 path.line_to(to.0, to.1);
508 i += 1;
509 } else {
510 let ctrl = px(i);
512 let next = i + 1;
513 if next < until {
514 if flags[next] {
515 let to = px(next);
517 path.curve3(ctrl.0, ctrl.1, to.0, to.1);
518 i = next + 1;
519 } else {
520 let m = mid(ctrl, px(next));
522 path.curve3(ctrl.0, ctrl.1, m.0, m.1);
523 i = next;
524 }
525 } else {
526 path.curve3(ctrl.0, ctrl.1, origin.0, origin.1);
528 i = next;
529 }
530 }
531 }
532 path.close_polygon(PATH_FLAGS_NONE);
533
534 contour_start = end + 1;
535 }
536
537 if !has_ops {
538 return None;
539 }
540 Some(path)
541}
542
543#[inline]
545#[allow(clippy::cast_precision_loss)] fn f26_to_px(v: i32) -> f32 {
547 v as f32 / 64.0
548}
549
550#[cfg(test)]
551#[allow(
552 clippy::float_cmp,
555 clippy::cast_precision_loss,
556 clippy::cast_possible_truncation,
557 clippy::cast_lossless
558)]
559mod autotest_generated {
560 use std::{
561 panic::{catch_unwind, AssertUnwindSafe},
562 sync::Arc,
563 };
564
565 use agg_rust::basics::{
566 PATH_CMD_CURVE3, PATH_CMD_END_POLY, PATH_CMD_LINE_TO, PATH_CMD_MOVE_TO, PATH_FLAGS_CLOSE,
567 };
568 use azul_core::resources::OwnedGlyphBoundingBox;
569
570 use super::*;
571
572 fn test_font() -> Option<ParsedFont> {
579 let candidates = [
580 "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
581 "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
582 "/System/Library/Fonts/Supplemental/Times New Roman.ttf",
583 "C:/Windows/Fonts/arial.ttf",
584 concat!(
585 env!("CARGO_MANIFEST_DIR"),
586 "/../examples/assets/fonts/SourceSerifPro-Regular.ttf"
587 ),
588 ];
589 for path in candidates {
590 let Ok(bytes) = std::fs::read(path) else {
591 continue;
592 };
593 let arc = Arc::new(rust_fontconfig::FontBytes::Owned(Arc::from(
594 bytes.as_slice(),
595 )));
596 if let Some(font) =
597 ParsedFont::from_bytes(&bytes, 0, &mut Vec::new()).map(|f| f.with_source_bytes(arc))
598 {
599 return Some(font);
600 }
601 }
602 None
603 }
604
605 fn empty_glyph() -> OwnedGlyph {
608 OwnedGlyph {
609 bounding_box: OwnedGlyphBoundingBox {
610 max_x: 0,
611 max_y: 0,
612 min_x: 0,
613 min_y: 0,
614 },
615 horz_advance: 0,
616 outline: Vec::new(),
617 phantom_points: None,
618 raw_points: None,
619 raw_on_curve: None,
620 raw_contour_ends: None,
621 instructions: None,
622 }
623 }
624
625 fn glyph_a(font: &ParsedFont) -> Option<(u16, Arc<OwnedGlyph>)> {
627 let gid = font.lookup_glyph_index('A' as u32)?;
628 let glyph = font.get_or_decode_glyph(gid)?;
629 Some((gid, glyph))
630 }
631
632 #[test]
637 fn quantize_subpx_zero_and_quarter_buckets() {
638 assert_eq!(quantize_subpx(0.0), 0);
639 assert_eq!(quantize_subpx(0.24), 0);
640 assert_eq!(quantize_subpx(0.25), 1);
641 assert_eq!(quantize_subpx(0.5), 2);
642 assert_eq!(quantize_subpx(0.75), 3);
643 assert_eq!(quantize_subpx(0.999), 3);
645 assert_eq!(quantize_subpx(1.0), 0);
647 assert_eq!(quantize_subpx(2.25), 1);
648 assert_eq!(quantize_subpx(1024.5), 2);
649 }
650
651 #[test]
652 fn quantize_subpx_negative_inputs_use_floor_not_truncation() {
653 assert_eq!(quantize_subpx(-0.25), 3);
656 assert_eq!(quantize_subpx(-0.5), 2);
657 assert_eq!(quantize_subpx(-0.75), 1);
658 assert_eq!(quantize_subpx(-1.0), 0);
659 assert_eq!(quantize_subpx(-0.0), 0);
660 assert_eq!(quantize_subpx(-7.25), 3);
661 }
662
663 #[test]
664 fn quantize_subpx_nan_and_infinities_are_defined() {
665 assert_eq!(quantize_subpx(f32::NAN), 3);
669 assert_eq!(quantize_subpx(f32::INFINITY), 3);
670 assert_eq!(quantize_subpx(f32::NEG_INFINITY), 3);
671 }
672
673 #[test]
674 fn quantize_subpx_never_exceeds_three() {
675 let extremes = [
676 f32::MAX,
677 f32::MIN,
678 f32::MIN_POSITIVE,
679 -f32::MIN_POSITIVE,
680 f32::EPSILON,
681 1e30,
682 -1e30,
683 16_777_216.0, -16_777_217.0,
685 f32::NAN,
686 f32::INFINITY,
687 f32::NEG_INFINITY,
688 ];
689 for v in extremes {
690 assert!(
691 quantize_subpx(v) <= 3,
692 "quantize_subpx({v}) escaped the 0..=3 bucket range"
693 );
694 }
695 for i in -2000..2000 {
696 let v = f64_to_f32(f64::from(i) * 0.017);
697 assert!(quantize_subpx(v) <= 3, "quantize_subpx({v}) > 3");
698 }
699 }
700
701 #[allow(clippy::cast_possible_truncation)]
702 fn f64_to_f32(v: f64) -> f32 {
703 v as f32
704 }
705
706 #[test]
711 fn f26_to_px_zero_and_exact_fractions() {
712 assert_eq!(f26_to_px(0), 0.0);
713 assert_eq!(f26_to_px(64), 1.0);
714 assert_eq!(f26_to_px(-64), -1.0);
715 assert_eq!(f26_to_px(32), 0.5);
716 assert_eq!(f26_to_px(16), 0.25);
717 assert_eq!(f26_to_px(1), 1.0 / 64.0);
718 assert_eq!(f26_to_px(-1), -1.0 / 64.0);
719 }
720
721 #[test]
722 fn f26_to_px_min_max_stay_finite() {
723 assert_eq!(f26_to_px(i32::MAX), 33_554_432.0);
725 assert_eq!(f26_to_px(i32::MIN), -33_554_432.0);
726 assert!(f26_to_px(i32::MAX).is_finite());
727 assert!(f26_to_px(i32::MIN).is_finite());
728 }
729
730 #[test]
731 fn f26_to_px_round_trips_for_exactly_representable_inputs() {
732 for v in [
734 0,
735 1,
736 -1,
737 63,
738 64,
739 -64,
740 4096,
741 -4096,
742 8_388_607,
743 -8_388_607,
744 16_777_216,
745 -16_777_216,
746 ] {
747 let px = f26_to_px(v);
748 assert_eq!(px * 64.0, v as f32, "f26_to_px({v}) did not round-trip");
749 }
750 }
751
752 #[test]
753 fn f26_to_px_is_monotonic() {
754 let ladder = [
755 i32::MIN,
756 -1_000_000,
757 -64,
758 -1,
759 0,
760 1,
761 64,
762 1_000_000,
763 i32::MAX,
764 ];
765 for w in ladder.windows(2) {
766 assert!(
767 f26_to_px(w[0]) <= f26_to_px(w[1]),
768 "f26_to_px is not monotonic between {} and {}",
769 w[0],
770 w[1]
771 );
772 }
773 }
774
775 #[test]
780 fn build_path_from_contours_empty_inputs_return_none() {
781 assert!(build_path_from_contours(&[], &[], &[]).is_none());
782 assert!(build_path_from_contours(&[(0, 0), (64, 0)], &[true, true], &[]).is_none());
784 assert!(build_path_from_contours(&[], &[], &[0]).is_none());
786 }
787
788 #[test]
789 fn build_path_from_contours_single_point_contour_is_skipped() {
790 assert!(build_path_from_contours(&[(0, 0)], &[true], &[0]).is_none());
792 }
793
794 #[test]
795 fn build_path_from_contours_out_of_range_contour_end_is_skipped_not_indexed() {
796 let pts = [(0, 0), (64, 64)];
797 let oc = [true, true];
798 assert!(build_path_from_contours(&pts, &oc, &[10]).is_none());
799 assert!(build_path_from_contours(&pts, &oc, &[u16::MAX]).is_none());
801 assert!(
805 build_path_from_contours(&pts, &oc, &[99, 1]).is_none(),
806 "a bogus contour end poisons the cursor for the rest of the glyph"
807 );
808 }
809
810 #[test]
811 fn build_path_from_contours_line_contour_emits_move_line_close() {
812 let path = build_path_from_contours(&[(0, 0), (128, 64)], &[true, true], &[1])
813 .expect("two on-curve points form a contour");
814 let v = path.vertices();
815 assert_eq!(v.len(), 3, "expected move_to + line_to + close");
816 assert_eq!(v[0].cmd, PATH_CMD_MOVE_TO);
817 assert_eq!(v[0].x, 0.0);
818 assert_eq!(v[0].y, 0.0);
819 assert_eq!(v[1].cmd, PATH_CMD_LINE_TO);
820 assert_eq!(v[1].x, 2.0, "128 F26Dot6 units == 2 px");
821 assert_eq!(v[1].y, -1.0, "Y must be negated for screen coords");
822 assert_eq!(v[2].cmd, PATH_CMD_END_POLY | PATH_FLAGS_CLOSE);
823 }
824
825 #[test]
826 fn build_path_from_contours_offcurve_point_becomes_curve3() {
827 let path = build_path_from_contours(
828 &[(0, 0), (64, 64), (128, 0)],
829 &[true, false, true],
830 &[2],
831 )
832 .expect("on/off/on contour");
833 let v = path.vertices();
834 assert_eq!(v.len(), 4, "move_to + curve3(ctrl,to) + close");
835 assert_eq!(v[0].cmd, PATH_CMD_MOVE_TO);
836 assert_eq!(v[1].cmd, PATH_CMD_CURVE3);
837 assert_eq!((v[1].x, v[1].y), (1.0, -1.0), "control point");
838 assert_eq!(v[2].cmd, PATH_CMD_CURVE3);
839 assert_eq!((v[2].x, v[2].y), (2.0, 0.0), "curve endpoint");
840 assert_eq!(v[3].cmd, PATH_CMD_END_POLY | PATH_FLAGS_CLOSE);
841 }
842
843 #[test]
844 fn build_path_from_contours_two_offcurve_points_insert_implicit_midpoint() {
845 let path = build_path_from_contours(
846 &[(0, 0), (64, 64), (128, 64), (192, 0)],
847 &[true, false, false, true],
848 &[3],
849 )
850 .expect("on/off/off/on contour");
851 let v = path.vertices();
852 assert_eq!(v.len(), 6, "move_to + 2 curve3 + close");
853 assert_eq!((v[1].x, v[1].y), (1.0, -1.0));
856 assert_eq!((v[2].x, v[2].y), (1.5, -1.0), "implicit on-curve midpoint");
857 assert_eq!((v[3].x, v[3].y), (2.0, -1.0));
858 assert_eq!((v[4].x, v[4].y), (3.0, 0.0));
859 }
860
861 #[test]
862 fn build_path_from_contours_all_offcurve_closes_back_to_the_synthetic_origin() {
863 let path = build_path_from_contours(
866 &[(0, 0), (64, 64), (128, 0)],
867 &[false, false, false],
868 &[2],
869 )
870 .expect("all-off-curve contour");
871 let v = path.vertices();
872 assert_eq!(v.len(), 8, "move_to + 3 curve3 + close");
873 assert_eq!(v[0].cmd, PATH_CMD_MOVE_TO);
874 assert_eq!((v[0].x, v[0].y), (1.0, 0.0), "origin = mid(first, last)");
875 let last_pt = v[6];
876 assert_eq!(
877 (last_pt.x, last_pt.y),
878 (v[0].x, v[0].y),
879 "final curve3 must land back on the origin"
880 );
881 assert_eq!(v[7].cmd, PATH_CMD_END_POLY | PATH_FLAGS_CLOSE);
882 }
883
884 #[test]
885 fn build_path_from_contours_leading_offcurve_uses_trailing_oncurve_as_origin() {
886 let path = build_path_from_contours(&[(64, 64), (128, 0)], &[false, true], &[1])
888 .expect("off/on contour");
889 let v = path.vertices();
890 assert_eq!(v.len(), 4);
891 assert_eq!(v[0].cmd, PATH_CMD_MOVE_TO);
892 assert_eq!((v[0].x, v[0].y), (2.0, 0.0), "origin is the on-curve point");
893 assert_eq!(v[1].cmd, PATH_CMD_CURVE3);
894 assert_eq!((v[2].x, v[2].y), (2.0, 0.0), "curve returns to the origin");
895 }
896
897 #[test]
898 fn build_path_from_contours_multiple_contours_emit_multiple_subpaths() {
899 let pts = [(0, 0), (64, 0), (0, 64), (64, 64)];
900 let oc = [true; 4];
901 let path = build_path_from_contours(&pts, &oc, &[1, 3]).expect("two contours");
902 let v = path.vertices();
903 assert_eq!(v.len(), 6, "two × (move_to + line_to + close)");
904 let moves = v.iter().filter(|x| x.cmd == PATH_CMD_MOVE_TO).count();
905 assert_eq!(moves, 2, "each contour must start its own subpath");
906 }
907
908 #[test]
909 fn build_path_from_contours_non_monotonic_contour_ends_do_not_panic() {
910 let pts = [(0, 0), (64, 0), (0, 64), (64, 64)];
911 let oc = [true; 4];
912 let path = build_path_from_contours(&pts, &oc, &[1, 0]).expect("first contour builds");
914 assert_eq!(path.vertices().len(), 3);
915 let dup = build_path_from_contours(&pts, &oc, &[1, 1]).expect("first contour builds");
917 assert_eq!(dup.vertices().len(), 3);
918 }
919
920 #[test]
921 fn build_path_from_contours_extra_on_curve_flags_are_harmless() {
922 let path = build_path_from_contours(&[(0, 0), (64, 0)], &[true; 8], &[1])
924 .expect("contour builds with a too-long flag slice");
925 assert_eq!(path.vertices().len(), 3);
926 }
927
928 #[test]
929 #[should_panic(expected = "out of range")]
930 fn build_path_from_contours_short_on_curve_slice_panics() {
931 let _ = build_path_from_contours(&[(0, 0), (64, 0)], &[true], &[1]);
937 }
938
939 #[test]
940 fn build_path_from_contours_extreme_coordinates_stay_finite() {
941 let pts = [(i32::MIN, i32::MAX), (i32::MAX, i32::MIN), (0, 0)];
942 let oc = [true, false, true];
943 let path = build_path_from_contours(&pts, &oc, &[2]).expect("extreme contour still builds");
944 for v in path.vertices() {
945 assert!(
946 v.x.is_finite() && v.y.is_finite(),
947 "F26Dot6 extremes produced a non-finite vertex: ({}, {})",
948 v.x,
949 v.y
950 );
951 }
952 }
953
954 #[test]
959 fn hint_light_enabled_is_stable_across_calls() {
960 let first = hint_light_enabled();
962 assert_eq!(first, hint_light_enabled());
963 assert_eq!(first, hint_light_enabled());
964 }
965
966 #[test]
967 fn text_subpixel_enabled_is_stable_across_calls() {
968 let first = text_subpixel_enabled();
969 assert_eq!(first, text_subpixel_enabled());
970 assert_eq!(first, text_subpixel_enabled());
971 }
972
973 #[test]
978 fn new_cache_is_empty_and_default_matches_new() {
979 let cache = GlyphCache::new();
980 assert_eq!(cache.paths_len(), 0);
981 assert_eq!(cache.cells_len(), 0);
982
983 let def = GlyphCache::default();
984 assert_eq!(def.paths_len(), 0);
985 assert_eq!(def.cells_len(), 0);
986 }
987
988 #[test]
989 fn debug_impl_reports_entry_counts_without_touching_agg_types() {
990 let cache = GlyphCache::new();
991 let s = format!("{cache:?}");
992 assert!(s.contains("GlyphCache"), "{s}");
993 assert!(s.contains("paths"), "{s}");
994 assert!(s.contains("cells"), "{s}");
995 }
996
997 #[test]
1003 fn get_or_build_cells_without_a_cached_path_returns_none_and_negative_caches() {
1004 let mut cache = GlyphCache::new();
1005 let got = cache
1006 .get_or_build_cells(1, 2, 16, 0.0, 0.0, 1.0, false, 1.0)
1007 .map(|(cells, x, y)| (cells.len(), x, y));
1008 assert_eq!(got, None, "no path cached => no cells");
1009 assert_eq!(cache.cells_len(), 1, "the miss must be negative-cached");
1010
1011 let again = cache
1013 .get_or_build_cells(1, 2, 16, 0.0, 0.0, 1.0, false, 1.0)
1014 .map(|(cells, _, _)| cells.len());
1015 assert_eq!(again, None);
1016 assert_eq!(cache.cells_len(), 1);
1017 }
1018
1019 #[test]
1020 fn get_or_build_cells_keys_on_the_quarter_pixel_bucket_not_the_raw_position() {
1021 let mut cache = GlyphCache::new();
1022 for x in [0.0_f32, 0.1, 0.2, 5.24, 100.0] {
1024 let _ = cache.get_or_build_cells(7, 3, 16, x, 0.0, 1.0, false, 1.0);
1025 }
1026 assert_eq!(cache.cells_len(), 1, "same bucket must reuse one entry");
1027
1028 let _ = cache.get_or_build_cells(7, 3, 16, 0.5, 0.0, 1.0, false, 1.0);
1030 assert_eq!(cache.cells_len(), 2);
1031
1032 let _ = cache.get_or_build_cells(7, 3, 16, 0.5, 0.0, 2.0, false, 1.0);
1034 assert_eq!(cache.cells_len(), 3);
1035 }
1036
1037 #[test]
1038 fn get_or_build_cells_extreme_arguments_do_not_panic() {
1039 let mut cache = GlyphCache::new();
1040 for x in [
1043 0.0_f32,
1044 f32::MAX,
1045 f32::MIN,
1046 f32::NAN,
1047 f32::INFINITY,
1048 f32::NEG_INFINITY,
1049 -0.75,
1050 ] {
1051 let got = cache
1052 .get_or_build_cells(u64::MAX, u16::MAX, u16::MAX, x, x, 0.0, true, 1.0)
1053 .map(|(cells, _, _)| cells.len());
1054 assert_eq!(got, None, "empty path cache must yield None for x = {x}");
1055 }
1056 let zeroed = cache
1058 .get_or_build_cells(0, 0, 0, 0.0, 0.0, 0.0, false, 0.0)
1059 .map(|(cells, _, _)| cells.len());
1060 assert_eq!(zeroed, None);
1061 let big = cache
1063 .get_or_build_cells(0, 0, 0, 0.0, 0.0, 65_535.0, false, 1.0)
1064 .map(|(cells, _, _)| cells.len());
1065 assert_eq!(big, None);
1066 }
1067
1068 #[test]
1069 fn get_or_build_cells_nan_hint_correction_falls_back_to_grid_snapped() {
1070 let mut cache = GlyphCache::new();
1074 let _ = cache.get_or_build_cells(9, 1, 12, 3.5, 4.5, 0.0, true, 1.0);
1075 assert_eq!(cache.cells_len(), 1);
1076 let _ = cache.get_or_build_cells(9, 1, 12, 3.5, 4.5, 0.0, true, f32::NAN);
1077 assert_eq!(
1078 cache.cells_len(),
1079 1,
1080 "NaN hint_correction must collapse onto the grid-snapped key"
1081 );
1082 }
1083
1084 #[test]
1085 fn get_or_build_cells_negative_scale_is_debug_asserted() {
1086 let mut cache = GlyphCache::new();
1087 let res = catch_unwind(AssertUnwindSafe(|| {
1088 cache
1089 .get_or_build_cells(1, 1, 16, 0.0, 0.0, -1.0, false, 1.0)
1090 .map(|(cells, _, _)| cells.len())
1091 }));
1092 if cfg!(debug_assertions) {
1093 assert!(
1094 res.is_err(),
1095 "a negative scale must trip the fixed-point range debug_assert"
1096 );
1097 } else {
1098 assert_eq!(res.ok().flatten(), None);
1100 }
1101 }
1102
1103 #[test]
1104 fn get_or_build_cells_evicts_wholesale_at_the_entry_limit() {
1105 let mut cache = GlyphCache::new();
1106 for i in 0..MAX_CELL_ENTRIES as u64 {
1107 let _ = cache.get_or_build_cells(i, 0, 16, 0.0, 0.0, 1.0, false, 1.0);
1108 }
1109 assert_eq!(cache.cells_len(), MAX_CELL_ENTRIES);
1110 let _ = cache.get_or_build_cells(u64::MAX, 0, 16, 0.0, 0.0, 1.0, false, 1.0);
1112 assert_eq!(cache.cells_len(), 1, "cell cache must not grow unbounded");
1113 }
1114
1115 #[test]
1120 fn get_or_build_outlineless_glyph_returns_none_and_caches_the_miss() {
1121 let Some(font) = test_font() else {
1122 return; };
1124 let mut cache = GlyphCache::new();
1125 let glyph = empty_glyph();
1126 let got = cache
1127 .get_or_build(1, 0, &glyph, &font, 0)
1128 .map(|c| c.is_hinted);
1129 assert_eq!(got, None, "a glyph with no outline must yield None");
1130 assert_eq!(cache.paths_len(), 1, "the miss must be negative-cached");
1131 }
1132
1133 #[test]
1134 fn get_or_build_extreme_ids_and_ppem_do_not_panic() {
1135 let Some(font) = test_font() else {
1136 return;
1137 };
1138 let mut cache = GlyphCache::new();
1139 let glyph = empty_glyph();
1140 for (hash, gid, ppem) in [
1141 (0_u64, 0_u16, 0_u16),
1142 (u64::MAX, u16::MAX, u16::MAX),
1143 (u64::MAX, 0, 1),
1144 (0, u16::MAX, u16::MAX),
1145 ] {
1146 let got = cache
1147 .get_or_build(hash, gid, &glyph, &font, ppem)
1148 .map(|c| c.is_hinted);
1149 assert_eq!(got, None, "outline-less glyph at ppem {ppem}");
1150 }
1151 assert_eq!(cache.paths_len(), 4, "each (hash, gid, ppem) is its own key");
1152 }
1153
1154 #[test]
1155 fn get_or_build_is_idempotent_and_ppem_is_part_of_the_key() {
1156 let Some(font) = test_font() else {
1157 return;
1158 };
1159 let Some((gid, glyph)) = glyph_a(&font) else {
1160 return;
1161 };
1162 let mut cache = GlyphCache::new();
1163
1164 let first = cache
1166 .get_or_build(font.hash, gid, &glyph, &font, 0)
1167 .map(|c| (c.is_hinted, c.path.total_vertices()));
1168 let Some((is_hinted, verts)) = first else {
1169 return; };
1171 assert!(!is_hinted, "ppem == 0 must not produce a hinted path");
1172 assert!(verts > 0, "an outlined glyph must emit vertices");
1173 assert_eq!(cache.paths_len(), 1);
1174
1175 let second = cache
1177 .get_or_build(font.hash, gid, &glyph, &font, 0)
1178 .map(|c| (c.is_hinted, c.path.total_vertices()));
1179 assert_eq!(second, Some((is_hinted, verts)));
1180 assert_eq!(cache.paths_len(), 1, "a hit must not insert a second entry");
1181
1182 let _ = cache.get_or_build(font.hash, gid, &glyph, &font, 16);
1184 assert_eq!(cache.paths_len(), 2);
1185 }
1186
1187 #[test]
1188 fn get_or_build_evicts_wholesale_at_the_entry_limit() {
1189 let Some(font) = test_font() else {
1190 return;
1191 };
1192 let mut cache = GlyphCache::new();
1193 let glyph = empty_glyph(); for i in 0..MAX_PATH_ENTRIES as u64 {
1195 let _ = cache.get_or_build(i, 0, &glyph, &font, 0);
1196 }
1197 assert_eq!(cache.paths_len(), MAX_PATH_ENTRIES);
1198 let _ = cache.get_or_build(u64::MAX, 0, &glyph, &font, 0);
1199 assert_eq!(cache.paths_len(), 1, "path cache must not grow unbounded");
1200 }
1201
1202 #[test]
1207 fn glyph_lsb_without_source_bytes_is_none() {
1208 let Some(font) = test_font() else {
1209 return;
1210 };
1211 let mut stripped = font;
1212 stripped.original_bytes = None;
1213 assert_eq!(
1214 glyph_lsb(&stripped, 0),
1215 None,
1216 "no retained font bytes => no hmtx to read"
1217 );
1218 }
1219
1220 #[test]
1221 fn glyph_lsb_zero_h_metrics_is_none() {
1222 let Some(mut font) = test_font() else {
1223 return;
1224 };
1225 font.hhea_table.num_h_metrics = 0;
1226 assert_eq!(glyph_lsb(&font, 0), None, "num_h_metrics == 0 must bail out");
1227 }
1228
1229 #[test]
1230 fn glyph_lsb_reads_gid_zero_and_rejects_out_of_range_gids() {
1231 let Some(font) = test_font() else {
1232 return;
1233 };
1234 let (_, len) = font.hmtx_range;
1235 let num = usize::from(font.hhea_table.num_h_metrics);
1236 if len > 0 && num > 0 && font.original_bytes.is_some() {
1237 assert!(
1238 glyph_lsb(&font, 0).is_some(),
1239 "gid 0 sits inside hmtx and must read back"
1240 );
1241 }
1242
1243 let gid = usize::from(u16::MAX);
1247 let lsb_off = if gid < num {
1248 gid * 4 + 2
1249 } else {
1250 num * 4 + (gid - num) * 2
1251 };
1252 if lsb_off + 2 > len {
1253 assert_eq!(
1254 glyph_lsb(&font, u16::MAX),
1255 None,
1256 "an out-of-table gid must return None, not panic"
1257 );
1258 }
1259
1260 for gid in [0_u16, 1, u16::MAX] {
1263 let _ = glyph_lsb(&font, gid);
1264 }
1265 }
1266
1267 #[test]
1273 fn build_hinted_path_without_raw_hinting_data_is_none() {
1274 let Some(font) = test_font() else {
1275 return;
1276 };
1277 let glyph = empty_glyph(); assert!(build_hinted_path(0, &glyph, &font, 16).is_none());
1279 assert!(
1280 build_hinted_path(u16::MAX, &glyph, &font, u16::MAX).is_none(),
1281 "missing raw data must short-circuit before any hinting arithmetic"
1282 );
1283 }
1284
1285 #[test]
1286 fn build_hinted_path_with_empty_contours_is_none() {
1287 let Some(font) = test_font() else {
1288 return;
1289 };
1290 let mut glyph = empty_glyph();
1291 glyph.raw_points = Some(Vec::new());
1292 glyph.raw_on_curve = Some(Vec::new());
1293 glyph.raw_contour_ends = Some(Vec::new());
1294 glyph.instructions = Some(Vec::new());
1295 assert!(
1296 build_hinted_path(0, &glyph, &font, 16).is_none(),
1297 "an empty point/contour list must bail out, not hint an empty glyph"
1298 );
1299 }
1300
1301 #[test]
1302 fn build_hinted_path_without_a_hint_instance_is_none() {
1303 let Some(mut font) = test_font() else {
1304 return;
1305 };
1306 let Some((gid, glyph)) = glyph_a(&font) else {
1307 return;
1308 };
1309 if glyph.raw_points.is_none() || glyph.instructions.is_none() {
1310 return; }
1312 font.hint_instance = None;
1313 assert!(
1314 build_hinted_path(gid, &glyph, &font, 16).is_none(),
1315 "no interpreter => no hinted path"
1316 );
1317 }
1318
1319 #[test]
1320 fn build_hinted_path_with_zero_upem_is_none() {
1321 let Some(mut font) = test_font() else {
1322 return;
1323 };
1324 let Some((gid, glyph)) = glyph_a(&font) else {
1325 return;
1326 };
1327 if font.hint_instance.is_none() || glyph.raw_points.is_none() {
1328 return; }
1330 font.font_metrics.units_per_em = 0;
1331 assert!(
1332 build_hinted_path(gid, &glyph, &font, 16).is_none(),
1333 "upem == 0 must bail out before the divide-by-upem scale"
1334 );
1335 }
1336
1337 #[test]
1338 fn build_hinted_path_at_a_realistic_ppem_produces_a_finite_path() {
1339 let Some(font) = test_font() else {
1340 return;
1341 };
1342 let Some((gid, glyph)) = glyph_a(&font) else {
1343 return;
1344 };
1345 let Some(path) = build_hinted_path(gid, &glyph, &font, 16) else {
1346 return; };
1348 assert!(path.total_vertices() > 0, "hinted path must have vertices");
1349 for v in path.vertices() {
1350 assert!(
1351 v.x.is_finite() && v.y.is_finite(),
1352 "hinting produced a non-finite vertex: ({}, {})",
1353 v.x,
1354 v.y
1355 );
1356 }
1357 }
1358}