yggdrasil/api/markdown/mermaid/
metrics.rs1use std::sync::LazyLock;
9
10use merman::svg::{
11 DeterministicTextMeasurer, MeasurementProfileId, SvgEnvironment, TextMeasurementPolicy,
12 TextMeasurementProfile, TextMeasurementProfileIdentity, TextStyle,
13};
14
15struct Run {
17 start: u16,
18 end: u16,
19 milli: u16,
20}
21
22struct FontSet {
24 latin: [Vec<Run>; 5],
25 cjk: Vec<Run>,
26}
27
28struct FontTables {
29 regular: FontSet,
30 bold: FontSet,
31}
32
33static TABLES: LazyLock<FontTables> = LazyLock::new(|| decode(include_bytes!("font_metrics.bin")));
35
36fn decode(bytes: &[u8]) -> FontTables {
37 const BAD: &str = "font_metrics.bin 格式不符,请用 bun run mermaid-font-metrics 重新生成";
38 let u16_at = |at: usize| -> u16 {
39 let b = bytes.get(at..at + 2).expect(BAD);
40 u16::from_le_bytes([b[0], b[1]])
41 };
42 assert!(bytes.get(..6) == Some(b"YMFM\x01\x0c"), "{BAD}");
43 let mut at = 6;
44 let mut tables: Vec<Vec<Run>> = (0..12)
45 .map(|_| {
46 let count = usize::from(u16_at(at));
47 at += 2;
48 let runs = (0..count)
49 .map(|i| {
50 let base = at + 6 * i;
51 Run {
52 start: u16_at(base),
53 end: u16_at(base + 2),
54 milli: u16_at(base + 4),
55 }
56 })
57 .collect();
58 at += 6 * count;
59 runs
60 })
61 .collect();
62 assert_eq!(at, bytes.len(), "{BAD}");
63 let into_set = |mut set: Vec<Vec<Run>>| -> FontSet {
64 let cjk = set.pop().expect(BAD);
65 let Ok(latin) = <[Vec<Run>; 5]>::try_from(set) else {
66 panic!("{BAD}");
67 };
68 FontSet { latin, cjk }
69 };
70 let bold = into_set(tables.split_off(6));
71 FontTables {
72 regular: into_set(tables),
73 bold,
74 }
75}
76
77fn lookup(runs: &[Run], cp: u16) -> Option<u16> {
78 let run = runs.get(runs.partition_point(|r| r.end < cp))?;
79 (run.start <= cp).then_some(run.milli)
80}
81
82fn zero_width(ch: char) -> bool {
84 matches!(
85 ch,
86 '\u{AD}' | '\u{200B}'..='\u{200F}' | '\u{2060}' | '\u{FEFF}' | '\u{FE00}'..='\u{FE0F}'
87 ) || ('\u{0300}'..='\u{036F}').contains(&ch)
88}
89
90fn width_em(set: &FontSet, text: &str) -> f64 {
92 let mut sums = [0u64; 5];
93 for ch in text.chars().filter(|c| !zero_width(*c)) {
94 let Ok(cp) = u16::try_from(u32::from(ch)) else {
95 sums.iter_mut().for_each(|sum| *sum += 1000);
97 continue;
98 };
99 let cjk = lookup(&set.cjk, cp);
100 for (sum, runs) in sums.iter_mut().zip(&set.latin) {
101 *sum += u64::from(lookup(runs, cp).or(cjk).unwrap_or(1000));
102 }
103 }
104 sums.into_iter().max().unwrap_or(0) as f64 / 1000.0
105}
106
107fn is_bold(style: &TextStyle) -> bool {
108 match style.font_weight.as_deref().map(str::trim) {
109 Some("bold" | "bolder") => true,
110 Some(w) => w.parse::<u32>().is_ok_and(|w| w >= 600),
111 None => false,
112 }
113}
114
115pub(super) fn text_width_px(text: &str, style: &TextStyle) -> f64 {
117 let set = if is_bold(style) {
118 &TABLES.bold
119 } else {
120 &TABLES.regular
121 };
122 width_em(set, text) * style.font_size
123}
124
125pub(super) fn environment() -> SvgEnvironment {
127 let measurer = DeterministicTextMeasurer::default().with_width_callback(text_width_px);
128 let id = MeasurementProfileId::new("ygg-ui-fonts").expect("测量配置 id 合法");
129 let identity = TextMeasurementProfileIdentity::new(id, "1").expect("测量配置版本合法");
130 let profile = TextMeasurementProfile::new(identity, measurer);
131 SvgEnvironment::deterministic()
132 .with_text_measurement_policy(TextMeasurementPolicy::uniform(profile))
133}