Skip to main content

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}