1pub fn vmtx_advance_for_glyph(face_data: &[u8], glyph_id: u16, em_size: f32) -> f32 {
11 if face_data.is_empty() || em_size <= 0.0 {
12 return em_size;
13 }
14 let face = match ttf_parser::Face::parse(face_data, 0) {
15 Ok(f) => f,
16 Err(_) => return em_size,
17 };
18 let units_per_em = face.units_per_em();
19 if units_per_em == 0 {
20 return em_size;
21 }
22 match face.glyph_ver_advance(ttf_parser::GlyphId(glyph_id)) {
23 Some(adv) => adv as f32 * em_size / units_per_em as f32,
24 None => em_size,
25 }
26}
27
28pub fn is_upright_in_vertical(c: char) -> bool {
36 let cp = c as u32;
37 (0x4E00..=0x9FFF).contains(&cp)
39 || (0x3400..=0x4DBF).contains(&cp)
41 || (0x2_0000..=0x2_A6DF).contains(&cp)
43 || (0x3040..=0x309F).contains(&cp)
45 || (0x30A0..=0x30FF).contains(&cp)
47 || (0x3000..=0x303F).contains(&cp)
49 || (0x3200..=0x32FF).contains(&cp)
51 || (0xFF01..=0xFF60).contains(&cp)
53 || (0xAC00..=0xD7A3).contains(&cp)
55 || (0x1100..=0x11FF).contains(&cp)
57 || (0x3100..=0x312F).contains(&cp)
59 || (0x2F00..=0x2FDF).contains(&cp)
61}
62
63pub(crate) struct ParsedFaceCache<'a> {
77 faces: std::collections::HashMap<usize, Option<(ttf_parser::Face<'a>, u16)>>,
80}
81
82impl<'a> ParsedFaceCache<'a> {
83 pub(crate) fn new() -> Self {
85 Self {
86 faces: std::collections::HashMap::new(),
87 }
88 }
89
90 pub(crate) fn vmtx_advance_or_default(
99 &mut self,
100 face_data: &'a [u8],
101 glyph_id: u16,
102 em_size: f32,
103 ) -> f32 {
104 if face_data.is_empty() || em_size <= 0.0 {
105 return em_size;
106 }
107 let key = face_data.as_ptr() as usize;
109 let entry = self.faces.entry(key).or_insert_with(|| {
110 let face = ttf_parser::Face::parse(face_data, 0).ok()?;
111 let upem = face.units_per_em();
112 if upem == 0 {
113 return None;
114 }
115 Some((face, upem))
116 });
117 match entry {
118 Some((face, upem)) => match face.glyph_ver_advance(ttf_parser::GlyphId(glyph_id)) {
119 Some(adv) => adv as f32 * em_size / (*upem as f32),
120 None => em_size,
121 },
122 None => em_size,
123 }
124 }
125}
126
127pub struct VerticalMetrics {
134 pub advance: f32,
137 pub upright: bool,
139}
140
141impl VerticalMetrics {
142 pub fn for_char(c: char, em_size: f32) -> Self {
148 Self {
149 advance: em_size,
150 upright: is_upright_in_vertical(c),
151 }
152 }
153
154 pub fn for_glyph(face_data: &[u8], glyph_id: u16, c: char, em_size: f32) -> Self {
157 Self {
158 advance: vmtx_advance_for_glyph(face_data, glyph_id, em_size),
159 upright: is_upright_in_vertical(c),
160 }
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167 use std::path::Path;
168 use std::sync::Arc;
169
170 #[test]
173 fn parsed_face_cache_returns_em_size_for_empty_face() {
174 let mut cache = ParsedFaceCache::new();
175 let font: Arc<[u8]> = Arc::from(&[][..]);
176 assert_eq!(cache.vmtx_advance_or_default(font.as_ref(), 0, 16.0), 16.0);
177 }
178
179 #[test]
180 fn parsed_face_cache_returns_em_size_for_garbage_face() {
181 let mut cache = ParsedFaceCache::new();
182 let font: Arc<[u8]> = Arc::from(b"not a font".as_slice());
183 assert_eq!(cache.vmtx_advance_or_default(font.as_ref(), 0, 16.0), 16.0);
184 }
185
186 #[test]
187 fn parsed_face_cache_returns_em_size_for_zero_em() {
188 let mut cache = ParsedFaceCache::new();
189 let font: Arc<[u8]> = Arc::from(b"not a font".as_slice());
190 assert_eq!(cache.vmtx_advance_or_default(font.as_ref(), 0, 0.0), 0.0);
193 }
194
195 #[test]
196 fn parsed_face_cache_single_parse_per_face() {
197 let mut cache = ParsedFaceCache::new();
198 let font: Arc<[u8]> = Arc::from(b"garbage bytes".as_slice());
199 for _ in 0..100 {
202 let _ = cache.vmtx_advance_or_default(font.as_ref(), 1, 16.0);
203 }
204 assert_eq!(
205 cache.faces.len(),
206 1,
207 "only one cache entry per unique data pointer"
208 );
209 }
210
211 #[test]
212 fn parsed_face_cache_two_fonts_two_entries() {
213 let mut cache = ParsedFaceCache::new();
214 let font_a: Arc<[u8]> = Arc::from(b"garbage_a".as_slice());
215 let font_b: Arc<[u8]> = Arc::from(b"garbage_b".as_slice());
216 let _ = cache.vmtx_advance_or_default(font_a.as_ref(), 0, 16.0);
218 let _ = cache.vmtx_advance_or_default(font_b.as_ref(), 0, 16.0);
219 assert_eq!(
220 cache.faces.len(),
221 2,
222 "two distinct fonts → two cache entries"
223 );
224 }
225
226 #[test]
227 fn parsed_face_cache_matches_uncached_for_invalid_font() {
228 let font: Arc<[u8]> = Arc::from(b"not a valid font".as_slice());
229 let uncached = vmtx_advance_for_glyph(font.as_ref(), 5, 20.0);
230 let mut cache = ParsedFaceCache::new();
231 let cached = cache.vmtx_advance_or_default(font.as_ref(), 5, 20.0);
232 assert_eq!(
233 cached, uncached,
234 "cached and uncached paths must agree for invalid font data"
235 );
236 }
237
238 #[test]
241 fn cjk_ideograph_is_upright() {
242 assert!(is_upright_in_vertical('日'));
243 assert!(is_upright_in_vertical('語'));
244 }
245
246 #[test]
247 fn latin_letter_is_rotated() {
248 assert!(!is_upright_in_vertical('A'));
249 assert!(!is_upright_in_vertical('z'));
250 }
251
252 #[test]
254 fn vertical_metrics_advance_equals_em() {
255 let vm = VerticalMetrics::for_char('日', 16.0);
256 assert!((vm.advance - 16.0).abs() < f32::EPSILON);
257 assert!(vm.upright);
258 }
259
260 #[test]
261 fn vmtx_advance_empty_face_returns_em_size() {
262 assert_eq!(vmtx_advance_for_glyph(&[], 0, 16.0), 16.0);
263 }
264
265 #[test]
266 fn vmtx_advance_invalid_face_returns_em_size() {
267 assert_eq!(vmtx_advance_for_glyph(b"not a font", 0, 16.0), 16.0);
269 }
270
271 #[test]
272 fn vmtx_advance_zero_em_size() {
273 assert_eq!(vmtx_advance_for_glyph(&[], 0, 0.0), 0.0);
275 }
276
277 #[test]
278 fn vmtx_advance_scales_linearly_with_em() {
279 let candidates = [
281 Path::new(env!("CARGO_MANIFEST_DIR"))
282 .join("../../tests/fixtures/test-font.ttf")
283 .to_path_buf(),
284 Path::new("/Library/Fonts/Arial Unicode.ttf").to_path_buf(),
285 Path::new("/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf").to_path_buf(),
286 ];
287 let font_bytes = candidates
288 .iter()
289 .filter(|p| p.exists())
290 .find_map(|p| std::fs::read(p).ok());
291 let bytes = match font_bytes {
292 Some(b) => b,
293 None => return, };
295 let face = match ttf_parser::Face::parse(&bytes, 0) {
297 Ok(f) => f,
298 Err(_) => return,
299 };
300 let gid = (1u16..=100).find(|&g| face.glyph_ver_advance(ttf_parser::GlyphId(g)).is_some());
302 let gid = match gid {
303 Some(g) => g,
304 None => return, };
306 let adv16 = vmtx_advance_for_glyph(&bytes, gid, 16.0);
307 let adv32 = vmtx_advance_for_glyph(&bytes, gid, 32.0);
308 assert!(
309 (adv32 - 2.0 * adv16).abs() < 1e-3,
310 "adv at 32px should be 2× adv at 16px: adv16={adv16}, adv32={adv32}"
311 );
312 }
313
314 #[test]
315 fn for_glyph_upright_cjk() {
316 let vm = VerticalMetrics::for_glyph(&[], 0, '日', 16.0);
318 assert!(vm.upright);
319 assert!((vm.advance - 16.0).abs() < f32::EPSILON);
320 }
321
322 #[test]
323 fn for_glyph_rotated_latin() {
324 let vm = VerticalMetrics::for_glyph(&[], 0, 'A', 16.0);
326 assert!(!vm.upright);
327 assert!((vm.advance - 16.0).abs() < f32::EPSILON);
328 }
329}