smix_annotate/font.rs
1//! v1.0 Phase C1 — bundled default fonts.
2//!
3//! smix-annotate ships two fonts embedded via `include_bytes!` so
4//! text annotations work out of the box without any consumer-supplied
5//! font path:
6//!
7//! - **Inter Regular** (~312 KB) — SIL Open Font License, from
8//! rsms/inter. Covers Latin + extended punctuation + Greek + Cyrillic.
9//! - **Noto Sans SC subset** (~64 KB) — SIL Open Font License, from
10//! googlefonts/noto-cjk. Pre-subsetted to ~250 most-frequent CJK
11//! ideographs + ASCII + CJK punctuation. Covers typical QA/UI
12//! Chinese text in yaml annotations.
13//!
14//! # Routing
15//!
16//! `pick_font_for_codepoint(cp)` returns the appropriate font bytes
17//! based on the Unicode range of `cp`:
18//! - ASCII + Latin extended → Inter
19//! - CJK Unified Ideographs + CJK punctuation → Noto Sans SC subset
20//! - Everything else → Inter (fallback)
21//!
22//! For per-glyph routing, `Annotator::render` iterates chars and
23//! picks the font per-codepoint, ensuring mixed-script text renders
24//! correctly.
25//!
26//! # Overriding
27//!
28//! Callers can supply their own font via `Annotator::font(bytes)` —
29//! that overrides both bundled fonts for the entire annotator's
30//! render pass. Use for corpus-specific glyph coverage.
31
32/// Inter Regular — Latin/Greek/Cyrillic + extended punctuation.
33/// SIL Open Font License. From <https://github.com/rsms/inter>.
34pub const INTER_REGULAR: &[u8] = include_bytes!("../fonts/inter-regular.ttf");
35
36/// Noto Sans SC subset — top ~250 CJK ideographs + ASCII + CJK
37/// punctuation. SIL Open Font License. Subsetted from
38/// <https://github.com/googlefonts/noto-cjk>.
39pub const NOTO_SANS_SC_SUBSET: &[u8] = include_bytes!("../fonts/noto-sans-sc-subset.ttf");
40
41/// v1.0 Phase C1 — pick the bundled font best suited to render `cp`.
42///
43/// Precedence:
44/// 1. CJK Unified Ideographs (U+4E00-U+9FFF), CJK Extension A
45/// (U+3400-U+4DBF), CJK punctuation (U+3000-U+303F), or Halfwidth
46/// and Fullwidth Forms (U+FF00-U+FFEF) → Noto Sans SC subset
47/// 2. Everything else → Inter (fallback for unknown ranges too;
48/// ab_glyph renders missing glyphs as tofu boxes rather than
49/// panicking)
50pub fn pick_font_for_codepoint(cp: u32) -> &'static [u8] {
51 match cp {
52 0x3000..=0x303F | 0x3400..=0x4DBF | 0x4E00..=0x9FFF | 0xFF00..=0xFFEF => {
53 NOTO_SANS_SC_SUBSET
54 }
55 _ => INTER_REGULAR,
56 }
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62
63 #[test]
64 fn inter_font_nonempty() {
65 assert!(INTER_REGULAR.len() > 10_000, "Inter font missing / empty");
66 }
67
68 #[test]
69 fn cjk_font_nonempty() {
70 assert!(
71 NOTO_SANS_SC_SUBSET.len() > 10_000,
72 "Noto Sans SC subset missing / empty"
73 );
74 }
75
76 #[test]
77 fn routing_latin_uses_inter() {
78 assert_eq!(pick_font_for_codepoint('a' as u32), INTER_REGULAR);
79 assert_eq!(pick_font_for_codepoint(' ' as u32), INTER_REGULAR);
80 }
81
82 #[test]
83 fn routing_cjk_uses_noto_sc() {
84 // 中 = U+4E2D — inside CJK Unified Ideographs.
85 assert_eq!(pick_font_for_codepoint('中' as u32), NOTO_SANS_SC_SUBSET);
86 // 登 = U+767B
87 assert_eq!(pick_font_for_codepoint('登' as u32), NOTO_SANS_SC_SUBSET);
88 }
89
90 #[test]
91 fn routing_cjk_punctuation() {
92 // 、 = U+3001 (CJK ideographic comma).
93 assert_eq!(pick_font_for_codepoint('、' as u32), NOTO_SANS_SC_SUBSET);
94 }
95
96 #[test]
97 fn routing_unknown_falls_back_to_inter() {
98 // Latin extended A range still routes to Inter (fallback).
99 assert_eq!(pick_font_for_codepoint(0x1F600), INTER_REGULAR);
100 }
101
102 #[test]
103 fn both_fonts_load_via_ab_glyph() {
104 use ab_glyph::FontRef;
105 FontRef::try_from_slice(INTER_REGULAR).expect("Inter loads");
106 FontRef::try_from_slice(NOTO_SANS_SC_SUBSET).expect("Noto SC loads");
107 }
108}