1use std::collections::HashMap;
11use std::fs;
12use std::io::BufRead;
13use std::path::{Path, PathBuf};
14use std::sync::OnceLock;
15use std::time::SystemTime;
16
17use crate::truetype::{find_table, read_u16, read_u32};
18
19static SYSTEM_FONT_CACHE: OnceLock<SystemFontCache> = OnceLock::new();
21
22pub fn get_system_font_cache() -> &'static SystemFontCache {
24 SYSTEM_FONT_CACHE.get_or_init(SystemFontCache::load_or_build)
25}
26
27pub struct SystemFontCache {
29 fonts: HashMap<String, PathBuf>,
30}
31
32#[derive(serde::Serialize, serde::Deserialize)]
34struct CacheData {
35 version: u32,
36 dir_mtimes: HashMap<String, u64>,
37 fonts: HashMap<String, String>,
38}
39
40impl SystemFontCache {
41 pub fn get_font_path(&self, ps_name: &str) -> Option<&Path> {
43 self.fonts.get(ps_name).map(|p| p.as_path())
44 }
45
46 pub fn iter(&self) -> impl Iterator<Item = (&str, &Path)> {
48 self.fonts.iter().map(|(k, v)| (k.as_str(), v.as_path()))
49 }
50
51 fn cache_path() -> Option<PathBuf> {
52 dirs_cache().map(|d| d.join("stet").join("system_fonts.json"))
53 }
54
55 fn load_or_build() -> SystemFontCache {
56 if let Some(cache_path) = Self::cache_path()
58 && let Some(cache) = Self::try_load_cache(&cache_path)
59 {
60 return cache;
61 }
62
63 let cache = Self::build();
65
66 if let Some(cache_path) = Self::cache_path() {
68 let _ = cache.save(&cache_path);
69 }
70
71 cache
72 }
73
74 fn try_load_cache(path: &Path) -> Option<SystemFontCache> {
75 let data = fs::read_to_string(path).ok()?;
76 let cached: CacheData = serde_json::from_str(&data).ok()?;
77 if cached.version != 1 {
78 return None;
79 }
80
81 let current_mtimes = get_font_dir_mtimes();
83 for (dir, &cached_mtime) in &cached.dir_mtimes {
84 match current_mtimes.get(dir.as_str()) {
85 Some(¤t) if current == cached_mtime => {}
86 _ => return None, }
88 }
89 for dir in current_mtimes.keys() {
91 if !cached.dir_mtimes.contains_key(*dir) {
92 return None;
93 }
94 }
95
96 let fonts = cached
97 .fonts
98 .into_iter()
99 .map(|(k, v)| (k, PathBuf::from(v)))
100 .collect();
101 Some(SystemFontCache { fonts })
102 }
103
104 fn build() -> SystemFontCache {
105 let mut fonts = HashMap::new();
106
107 for dir in font_directories() {
108 let dir_path = Path::new(dir);
109 if dir_path.is_dir() {
110 scan_directory(dir_path, &mut fonts);
111 }
112 }
113 for dir_path in home_font_directories() {
114 if dir_path.is_dir() {
115 scan_directory(&dir_path, &mut fonts);
116 }
117 }
118
119 SystemFontCache { fonts }
120 }
121
122 fn save(&self, path: &Path) -> std::io::Result<()> {
123 if let Some(parent) = path.parent() {
124 fs::create_dir_all(parent)?;
125 }
126
127 let dir_mtimes = get_font_dir_mtimes();
128 let cache_data = CacheData {
129 version: 1,
130 dir_mtimes: dir_mtimes
131 .into_iter()
132 .map(|(k, v)| (k.to_string(), v))
133 .collect(),
134 fonts: self
135 .fonts
136 .iter()
137 .map(|(k, v)| (k.clone(), v.to_string_lossy().to_string()))
138 .collect(),
139 };
140
141 let json = serde_json::to_string_pretty(&cache_data)?;
142 fs::write(path, json)
143 }
144}
145
146fn font_directories() -> Vec<&'static str> {
148 if cfg!(target_os = "macos") {
149 vec!["/System/Library/Fonts", "/Library/Fonts"]
150 } else if cfg!(target_os = "windows") {
151 vec!["C:\\Windows\\Fonts"]
152 } else {
153 vec!["/usr/share/fonts", "/usr/local/share/fonts"]
155 }
156}
157
158fn home_font_directories() -> Vec<PathBuf> {
160 let mut dirs = Vec::new();
161 if let Some(home) = std::env::var_os("HOME") {
162 let home = PathBuf::from(home);
163 if cfg!(target_os = "macos") {
164 dirs.push(home.join("Library/Fonts"));
165 } else if !cfg!(target_os = "windows") {
166 dirs.push(home.join(".local/share/fonts"));
167 dirs.push(home.join(".fonts"));
168 }
169 }
170 dirs
171}
172
173fn dirs_cache() -> Option<PathBuf> {
175 std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".cache"))
176}
177
178fn get_font_dir_mtimes() -> HashMap<&'static str, u64> {
180 let mut mtimes = HashMap::new();
181 for dir in font_directories() {
182 if let Ok(meta) = fs::metadata(dir)
183 && let Ok(mtime) = meta.modified()
184 && let Ok(dur) = mtime.duration_since(SystemTime::UNIX_EPOCH)
185 {
186 mtimes.insert(dir, dur.as_secs());
187 }
188 }
189 mtimes
190}
191
192fn scan_directory(dir: &Path, fonts: &mut HashMap<String, PathBuf>) {
194 let entries = match fs::read_dir(dir) {
195 Ok(e) => e,
196 Err(_) => return,
197 };
198
199 for entry in entries.flatten() {
200 let path = entry.path();
201 if path.is_dir() {
202 scan_directory(&path, fonts);
203 continue;
204 }
205
206 let ext = path
207 .extension()
208 .and_then(|e| e.to_str())
209 .map(|e| e.to_ascii_lowercase());
210
211 match ext.as_deref() {
212 Some("ttf" | "otf") => {
213 if let Some(name) = extract_ps_name_from_sfnt(&path) {
214 fonts.entry(name).or_insert_with(|| path.clone());
215 }
216 }
217 Some("ttc") => {
218 if let Ok(data) = fs::read(&path)
220 && data.len() > 12
221 && &data[0..4] == b"ttcf"
222 {
223 let num = read_u32(&data, 8) as usize;
224 for i in 0..num {
225 let off_pos = 12 + i * 4;
226 if off_pos + 4 > data.len() {
227 break;
228 }
229 let font_off = read_u32(&data, off_pos) as usize;
230 if let Some(name) = extract_ps_name_at_ttc_offset(&data, font_off) {
231 fonts.entry(name).or_insert_with(|| path.clone());
232 }
233 }
234 }
235 }
236 Some("pfa" | "t1") => {
237 if let Some(name) = extract_ps_name_from_pfa(&path) {
238 fonts.entry(name).or_insert_with(|| path.clone());
239 }
240 }
241 Some("pfb") => {
242 if let Some(name) = extract_ps_name_from_pfb(&path) {
243 fonts.entry(name).or_insert_with(|| path.clone());
244 }
245 }
246 _ => {}
247 }
248 }
249}
250
251fn extract_ps_name_from_pfa(path: &Path) -> Option<String> {
253 let file = fs::File::open(path).ok()?;
254 let mut reader = std::io::BufReader::new(file);
255 let mut buf = String::new();
256 let mut bytes_read = 0usize;
257
258 while bytes_read < 4096 {
259 buf.clear();
260 let n = reader.read_line(&mut buf).ok()?;
261 if n == 0 {
262 break;
263 }
264 bytes_read += n;
265 if let Some(name) = parse_fontname_line(&buf) {
266 return Some(name);
267 }
268 }
269 None
270}
271
272fn extract_ps_name_from_pfb(path: &Path) -> Option<String> {
275 let data = fs::read(path).ok()?;
276 let mut offset = 0;
277 let mut ascii_data = Vec::new();
278
279 while offset + 6 <= data.len() && data[offset] == 0x80 {
281 let seg_type = data[offset + 1];
282 let seg_len = u32::from_le_bytes([
283 data[offset + 2],
284 data[offset + 3],
285 data[offset + 4],
286 data[offset + 5],
287 ]) as usize;
288 offset += 6;
289
290 if seg_type == 1 {
291 let end = (offset + seg_len).min(data.len());
293 ascii_data.extend_from_slice(&data[offset..end]);
294
295 if let Some(name) = find_fontname_in_bytes(&ascii_data) {
297 return Some(name);
298 }
299 if ascii_data.len() > 4096 {
300 break;
301 }
302 } else if seg_type == 3 {
303 break; }
305
306 offset += seg_len;
307 }
308 None
309}
310
311fn extract_ps_name_at_ttc_offset(data: &[u8], font_offset: usize) -> Option<String> {
314 if font_offset + 12 > data.len() {
315 return None;
316 }
317 let is_otto = &data[font_offset..font_offset + 4] == b"OTTO";
319
320 let num_tables = read_u16(data, font_offset + 4) as usize;
322 for i in 0..num_tables {
323 let entry = font_offset + 12 + i * 16;
324 if entry + 16 > data.len() {
325 break;
326 }
327 let tag = &data[entry..entry + 4];
328 if tag == b"name" {
329 let tbl_off = read_u32(data, entry + 8) as usize;
330 let tbl_len = read_u32(data, entry + 12) as usize;
331 if tbl_off + tbl_len <= data.len() {
332 return extract_ps_name_from_name_table_data(&data[tbl_off..tbl_off + tbl_len]);
333 }
334 }
335 if is_otto && tag == b"CFF " {
337 let cff_off = read_u32(data, entry + 8) as usize;
338 let cff_len = read_u32(data, entry + 12) as usize;
339 if cff_off + cff_len <= data.len()
340 && let Some(name) = extract_cff_name_from_data(&data[cff_off..cff_off + cff_len])
341 {
342 return Some(name);
343 }
344 }
345 }
346 None
347}
348
349fn extract_ps_name_from_name_table_data(name_data: &[u8]) -> Option<String> {
351 if name_data.len() < 6 {
352 return None;
353 }
354 let count = read_u16(name_data, 2) as usize;
355 let string_offset = read_u16(name_data, 4) as usize;
356 for i in 0..count {
357 let rec = 6 + i * 12;
358 if rec + 12 > name_data.len() {
359 break;
360 }
361 let platform_id = read_u16(name_data, rec);
362 let name_id = read_u16(name_data, rec + 6);
363 let length = read_u16(name_data, rec + 8) as usize;
364 let str_off = read_u16(name_data, rec + 10) as usize;
365 if name_id == 6 {
366 let start = string_offset + str_off;
367 if start + length <= name_data.len() {
368 let raw = &name_data[start..start + length];
369 if platform_id == 3 || platform_id == 0 {
370 let s: String = raw
372 .chunks(2)
373 .filter_map(|c| {
374 if c.len() == 2 {
375 char::from_u32(u16::from_be_bytes([c[0], c[1]]) as u32)
376 } else {
377 None
378 }
379 })
380 .collect();
381 if !s.is_empty() {
382 return Some(s);
383 }
384 } else {
385 let s = String::from_utf8_lossy(raw).to_string();
386 if !s.is_empty() {
387 return Some(s);
388 }
389 }
390 }
391 }
392 }
393 None
394}
395
396fn extract_cff_name_from_data(cff: &[u8]) -> Option<String> {
398 if cff.len() < 4 {
399 return None;
400 }
401 let hdr_size = cff[2] as usize;
402 if hdr_size + 3 > cff.len() {
403 return None;
404 }
405 let count = u16::from_be_bytes([cff[hdr_size], cff[hdr_size + 1]]) as usize;
406 if count == 0 {
407 return None;
408 }
409 let off_size = cff[hdr_size + 2] as usize;
410 if off_size == 0 || off_size > 4 {
411 return None;
412 }
413 let offsets_start = hdr_size + 3;
414 let read_off = |idx: usize| -> usize {
415 let pos = offsets_start + idx * off_size;
416 let mut val = 0u32;
417 for j in 0..off_size {
418 if pos + j < cff.len() {
419 val = (val << 8) | cff[pos + j] as u32;
420 }
421 }
422 val as usize
423 };
424 let data_start = offsets_start + (count + 1) * off_size;
425 let off1 = read_off(0);
426 let off2 = read_off(1);
427 let start = data_start + off1 - 1;
428 let end = data_start + off2 - 1;
429 if start < cff.len() && end <= cff.len() && end > start {
430 return Some(String::from_utf8_lossy(&cff[start..end]).to_string());
431 }
432 None
433}
434
435fn extract_ps_name_from_sfnt(path: &Path) -> Option<String> {
436 let data = fs::read(path).ok()?;
437 if data.len() < 12 {
438 return None;
439 }
440
441 if &data[0..4] == b"OTTO"
443 && let Some(name) = extract_ps_name_from_cff_table(&data)
444 {
445 return Some(name);
446 }
447
448 extract_ps_name_from_name_table(&data)
450}
451
452fn extract_ps_name_from_cff_table(font_data: &[u8]) -> Option<String> {
454 let (cff_offset, cff_length) = find_table(font_data, b"CFF ")?;
455 if cff_offset + cff_length > font_data.len() {
456 return None;
457 }
458 let cff = &font_data[cff_offset..cff_offset + cff_length];
459
460 if cff.len() < 4 {
462 return None;
463 }
464 let hdr_size = cff[2] as usize;
465
466 if hdr_size >= cff.len() {
468 return None;
469 }
470 let name_idx_offset = hdr_size;
471
472 if name_idx_offset + 3 > cff.len() {
474 return None;
475 }
476 let count = u16::from_be_bytes([cff[name_idx_offset], cff[name_idx_offset + 1]]) as usize;
477 if count == 0 {
478 return None;
479 }
480 let off_size = cff[name_idx_offset + 2] as usize;
481 if off_size == 0 || off_size > 4 {
482 return None;
483 }
484
485 let offsets_start = name_idx_offset + 3;
487 let read_offset = |idx: usize| -> Option<usize> {
488 let pos = offsets_start + idx * off_size;
489 if pos + off_size > cff.len() {
490 return None;
491 }
492 let mut val = 0u32;
493 for b in 0..off_size {
494 val = (val << 8) | cff[pos + b] as u32;
495 }
496 Some(val as usize)
497 };
498
499 let off1 = read_offset(0)?;
500 let off2 = read_offset(1)?;
501 let data_start = offsets_start + (count + 1) * off_size;
502 let start = data_start + off1 - 1; let end = data_start + off2 - 1;
504
505 if end > cff.len() || start >= end {
506 return None;
507 }
508
509 String::from_utf8(cff[start..end].to_vec()).ok()
510}
511
512pub fn extract_ps_name_from_name_table(font_data: &[u8]) -> Option<String> {
514 let (name_off, name_len) = find_table(font_data, b"name")?;
515 if name_off + name_len > font_data.len() || name_len < 6 {
516 return None;
517 }
518 let name_table = &font_data[name_off..name_off + name_len];
519
520 let count = read_u16(name_table, 2) as usize;
521 let string_offset = read_u16(name_table, 4) as usize;
522
523 let mut win_result: Option<String> = None;
525 let mut mac_result: Option<String> = None;
526
527 for i in 0..count {
528 let rec_off = 6 + i * 12;
529 if rec_off + 12 > name_table.len() {
530 break;
531 }
532
533 let platform_id = read_u16(name_table, rec_off);
534 let encoding_id = read_u16(name_table, rec_off + 2);
535 let name_id = read_u16(name_table, rec_off + 6);
536 let length = read_u16(name_table, rec_off + 8) as usize;
537 let offset = read_u16(name_table, rec_off + 10) as usize;
538
539 if name_id != 6 {
540 continue;
541 }
542
543 let data_start = string_offset + offset;
544 if data_start + length > name_table.len() {
545 continue;
546 }
547 let data = &name_table[data_start..data_start + length];
548
549 if platform_id == 3 && encoding_id == 1 && win_result.is_none() {
550 win_result = decode_utf16be(data);
552 } else if platform_id == 1 && encoding_id == 0 && mac_result.is_none() {
553 mac_result = Some(data.iter().map(|&b| b as char).collect());
555 }
556 }
557
558 win_result.or(mac_result)
559}
560
561fn decode_utf16be(data: &[u8]) -> Option<String> {
563 if !data.len().is_multiple_of(2) {
564 return None;
565 }
566 let units: Vec<u16> = data
567 .chunks_exact(2)
568 .map(|c| u16::from_be_bytes([c[0], c[1]]))
569 .collect();
570 String::from_utf16(&units).ok()
571}
572
573fn parse_fontname_line(line: &str) -> Option<String> {
575 let trimmed = line.trim();
576 if let Some(rest) = trimmed.strip_prefix("/FontName") {
577 let rest = rest.trim_start();
578 if let Some(name) = rest.strip_prefix('/') {
579 let end = name
581 .find(|c: char| c.is_whitespace() || c == '/' || c == '{' || c == '(')
582 .unwrap_or(name.len());
583 if end > 0 {
584 return Some(name[..end].to_string());
585 }
586 }
587 }
588 None
589}
590
591fn find_fontname_in_bytes(data: &[u8]) -> Option<String> {
593 let text = String::from_utf8_lossy(data);
594 for line in text.lines() {
595 if let Some(name) = parse_fontname_line(line) {
596 return Some(name);
597 }
598 }
599 None
600}
601
602pub fn parse_cmap_table(font_data: &[u8]) -> Option<HashMap<u32, u16>> {
607 let (cmap_off, cmap_len) = find_table(font_data, b"cmap")?;
608 if cmap_off + cmap_len > font_data.len() {
609 return None;
610 }
611 let cmap = &font_data[cmap_off..];
612
613 let num_subtables = read_u16(cmap, 2) as usize;
614
615 let mut format4_offset: Option<usize> = None;
617 let mut format12_offset: Option<usize> = None;
618
619 for i in 0..num_subtables {
620 let rec = 4 + i * 8;
621 if rec + 8 > cmap_len {
622 break;
623 }
624 let platform = read_u16(cmap, rec);
625 let encoding = read_u16(cmap, rec + 2);
626 let offset = read_u32(cmap, rec + 4) as usize;
627
628 if offset + 2 > cmap_len {
629 continue;
630 }
631 let format = read_u16(cmap, offset);
632
633 if platform == 3 && encoding == 10 && format == 12 {
634 format12_offset = Some(offset);
635 } else if platform == 3 && encoding == 1 && format == 4 && format4_offset.is_none() {
636 format4_offset = Some(offset);
637 }
638 }
639
640 if let Some(off) = format12_offset
642 && let Some(map) = parse_cmap_format12(cmap, off)
643 {
644 return Some(map);
645 }
646
647 if let Some(off) = format4_offset {
649 return parse_cmap_format4(cmap, off);
650 }
651
652 None
653}
654
655fn parse_cmap_format4(cmap: &[u8], offset: usize) -> Option<HashMap<u32, u16>> {
657 if offset + 14 > cmap.len() {
658 return None;
659 }
660 let seg_count_x2 = read_u16(cmap, offset + 6) as usize;
661 let seg_count = seg_count_x2 / 2;
662
663 let end_codes_off = offset + 14;
664 let start_codes_off = end_codes_off + seg_count_x2 + 2; let id_delta_off = start_codes_off + seg_count_x2;
666 let id_range_off = id_delta_off + seg_count_x2;
667
668 if id_range_off + seg_count_x2 > cmap.len() {
669 return None;
670 }
671
672 let mut map = HashMap::new();
673
674 for i in 0..seg_count {
675 let end_code = read_u16(cmap, end_codes_off + i * 2) as u32;
676 let start_code = read_u16(cmap, start_codes_off + i * 2) as u32;
677 let id_delta = read_u16(cmap, id_delta_off + i * 2) as i16;
678 let id_range_offset = read_u16(cmap, id_range_off + i * 2) as usize;
679
680 if start_code == 0xFFFF {
681 break;
682 }
683
684 for code in start_code..=end_code {
685 let gid = if id_range_offset == 0 {
686 (code as i32 + id_delta as i32) as u16
687 } else {
688 let glyph_off =
689 id_range_off + i * 2 + id_range_offset + (code - start_code) as usize * 2;
690 if glyph_off + 2 > cmap.len() {
691 continue;
692 }
693 let gid = read_u16(cmap, glyph_off);
694 if gid == 0 {
695 0
696 } else {
697 (gid as i32 + id_delta as i32) as u16
698 }
699 };
700 if gid != 0 {
701 map.insert(code, gid);
702 }
703 }
704 }
705
706 Some(map)
707}
708
709fn parse_cmap_format12(cmap: &[u8], offset: usize) -> Option<HashMap<u32, u16>> {
711 if offset + 16 > cmap.len() {
712 return None;
713 }
714 let num_groups = read_u32(cmap, offset + 12) as usize;
715 let groups_off = offset + 16;
716
717 if groups_off + num_groups * 12 > cmap.len() {
718 return None;
719 }
720
721 let mut map = HashMap::new();
722
723 for i in 0..num_groups {
724 let g = groups_off + i * 12;
725 let start_code = read_u32(cmap, g);
726 let end_code = read_u32(cmap, g + 4);
727 let start_gid = read_u32(cmap, g + 8);
728
729 for code in start_code..=end_code {
730 let gid = (start_gid + (code - start_code)) as u16;
731 if gid != 0 {
732 map.insert(code, gid);
733 }
734 }
735 }
736
737 Some(map)
738}
739
740pub fn parse_post_table(font_data: &[u8]) -> Option<HashMap<u16, String>> {
745 let (post_off, post_len) = find_table(font_data, b"post")?;
746 if post_off + post_len > font_data.len() || post_len < 34 {
747 return None;
748 }
749 let post = &font_data[post_off..post_off + post_len];
750
751 let format_major = read_u16(post, 0);
753 let format_minor = read_u16(post, 2);
754 if format_major != 2 || format_minor != 0 {
755 return None; }
757
758 let num_glyphs = read_u16(post, 32) as usize;
759 if 34 + num_glyphs * 2 > post.len() {
760 return None;
761 }
762
763 let mut name_indices = Vec::with_capacity(num_glyphs);
765 for i in 0..num_glyphs {
766 name_indices.push(read_u16(post, 34 + i * 2));
767 }
768
769 let mut extra_names = Vec::new();
771 let mut offset = 34 + num_glyphs * 2;
772 while offset < post.len() {
773 let str_len = post[offset] as usize;
774 offset += 1;
775 if offset + str_len > post.len() {
776 break;
777 }
778 let name = String::from_utf8_lossy(&post[offset..offset + str_len]).to_string();
779 extra_names.push(name);
780 offset += str_len;
781 }
782
783 let mut map = HashMap::new();
784 for (gid, &idx) in name_indices.iter().enumerate() {
785 let name = if (idx as usize) < MAC_GLYPH_NAMES.len() {
786 MAC_GLYPH_NAMES[idx as usize].to_string()
787 } else {
788 let extra_idx = idx as usize - 258;
789 if extra_idx < extra_names.len() {
790 extra_names[extra_idx].clone()
791 } else {
792 continue;
793 }
794 };
795 if name != ".notdef" {
796 map.insert(gid as u16, name);
797 }
798 }
799
800 Some(map)
801}
802
803static MAC_GLYPH_NAMES: &[&str] = &[
805 ".notdef",
806 ".null",
807 "nonmarkingreturn",
808 "space",
809 "exclam",
810 "quotedbl",
811 "numbersign",
812 "dollar",
813 "percent",
814 "ampersand",
815 "quotesingle",
816 "parenleft",
817 "parenright",
818 "asterisk",
819 "plus",
820 "comma",
821 "hyphen",
822 "period",
823 "slash",
824 "zero",
825 "one",
826 "two",
827 "three",
828 "four",
829 "five",
830 "six",
831 "seven",
832 "eight",
833 "nine",
834 "colon",
835 "semicolon",
836 "less",
837 "equal",
838 "greater",
839 "question",
840 "at",
841 "A",
842 "B",
843 "C",
844 "D",
845 "E",
846 "F",
847 "G",
848 "H",
849 "I",
850 "J",
851 "K",
852 "L",
853 "M",
854 "N",
855 "O",
856 "P",
857 "Q",
858 "R",
859 "S",
860 "T",
861 "U",
862 "V",
863 "W",
864 "X",
865 "Y",
866 "Z",
867 "bracketleft",
868 "backslash",
869 "bracketright",
870 "asciicircum",
871 "underscore",
872 "grave",
873 "a",
874 "b",
875 "c",
876 "d",
877 "e",
878 "f",
879 "g",
880 "h",
881 "i",
882 "j",
883 "k",
884 "l",
885 "m",
886 "n",
887 "o",
888 "p",
889 "q",
890 "r",
891 "s",
892 "t",
893 "u",
894 "v",
895 "w",
896 "x",
897 "y",
898 "z",
899 "braceleft",
900 "bar",
901 "braceright",
902 "asciitilde",
903 "Adieresis",
904 "Aring",
905 "Ccedilla",
906 "Eacute",
907 "Ntilde",
908 "Odieresis",
909 "Udieresis",
910 "aacute",
911 "agrave",
912 "acircumflex",
913 "adieresis",
914 "atilde",
915 "aring",
916 "ccedilla",
917 "eacute",
918 "egrave",
919 "ecircumflex",
920 "edieresis",
921 "iacute",
922 "igrave",
923 "icircumflex",
924 "idieresis",
925 "ntilde",
926 "oacute",
927 "ograve",
928 "ocircumflex",
929 "odieresis",
930 "otilde",
931 "uacute",
932 "ugrave",
933 "ucircumflex",
934 "udieresis",
935 "dagger",
936 "degree",
937 "cent",
938 "sterling",
939 "section",
940 "bullet",
941 "paragraph",
942 "germandbls",
943 "registered",
944 "copyright",
945 "trademark",
946 "acute",
947 "dieresis",
948 "notequal",
949 "AE",
950 "Oslash",
951 "infinity",
952 "plusminus",
953 "lessequal",
954 "greaterequal",
955 "yen",
956 "mu",
957 "partialdiff",
958 "summation",
959 "product",
960 "pi",
961 "integral",
962 "ordfeminine",
963 "ordmasculine",
964 "Omega",
965 "ae",
966 "oslash",
967 "questiondown",
968 "exclamdown",
969 "logicalnot",
970 "radical",
971 "florin",
972 "approxequal",
973 "Delta",
974 "guillemotleft",
975 "guillemotright",
976 "ellipsis",
977 "nonbreakingspace",
978 "Agrave",
979 "Atilde",
980 "Otilde",
981 "OE",
982 "oe",
983 "endash",
984 "emdash",
985 "quotedblleft",
986 "quotedblright",
987 "quoteleft",
988 "quoteright",
989 "divide",
990 "lozenge",
991 "ydieresis",
992 "Ydieresis",
993 "fraction",
994 "currency",
995 "guilsinglleft",
996 "guilsinglright",
997 "fi",
998 "fl",
999 "daggerdbl",
1000 "periodcentered",
1001 "quotesinglbase",
1002 "quotedblbase",
1003 "perthousand",
1004 "Acircumflex",
1005 "Ecircumflex",
1006 "Aacute",
1007 "Edieresis",
1008 "Egrave",
1009 "Iacute",
1010 "Icircumflex",
1011 "Idieresis",
1012 "Igrave",
1013 "Oacute",
1014 "Ocircumflex",
1015 "apple",
1016 "Ograve",
1017 "Uacute",
1018 "Ucircumflex",
1019 "Ugrave",
1020 "dotlessi",
1021 "circumflex",
1022 "tilde",
1023 "macron",
1024 "breve",
1025 "dotaccent",
1026 "ring",
1027 "cedilla",
1028 "hungarumlaut",
1029 "ogonek",
1030 "caron",
1031 "Lslash",
1032 "lslash",
1033 "Scaron",
1034 "scaron",
1035 "Zcaron",
1036 "zcaron",
1037 "brokenbar",
1038 "Eth",
1039 "eth",
1040 "Yacute",
1041 "yacute",
1042 "Thorn",
1043 "thorn",
1044 "minus",
1045 "multiply",
1046 "onesuperior",
1047 "twosuperior",
1048 "threesuperior",
1049 "onehalf",
1050 "onequarter",
1051 "threequarters",
1052 "franc",
1053 "Gbreve",
1054 "gbreve",
1055 "Idotaccent",
1056 "Scedilla",
1057 "scedilla",
1058 "Cacute",
1059 "cacute",
1060 "Ccaron",
1061 "ccaron",
1062 "dcroat",
1063];
1064
1065pub fn unicode_to_glyph_name(codepoint: u32) -> Option<&'static str> {
1068 AGL_MAP
1069 .binary_search_by_key(&codepoint, |&(cp, _)| cp)
1070 .ok()
1071 .map(|idx| AGL_MAP[idx].1)
1072}
1073
1074static AGL_MAP: &[(u32, &str)] = &[
1077 (0x0020, "space"),
1078 (0x0021, "exclam"),
1079 (0x0022, "quotedbl"),
1080 (0x0023, "numbersign"),
1081 (0x0024, "dollar"),
1082 (0x0025, "percent"),
1083 (0x0026, "ampersand"),
1084 (0x0027, "quotesingle"),
1085 (0x0028, "parenleft"),
1086 (0x0029, "parenright"),
1087 (0x002A, "asterisk"),
1088 (0x002B, "plus"),
1089 (0x002C, "comma"),
1090 (0x002D, "hyphen"),
1091 (0x002E, "period"),
1092 (0x002F, "slash"),
1093 (0x0030, "zero"),
1094 (0x0031, "one"),
1095 (0x0032, "two"),
1096 (0x0033, "three"),
1097 (0x0034, "four"),
1098 (0x0035, "five"),
1099 (0x0036, "six"),
1100 (0x0037, "seven"),
1101 (0x0038, "eight"),
1102 (0x0039, "nine"),
1103 (0x003A, "colon"),
1104 (0x003B, "semicolon"),
1105 (0x003C, "less"),
1106 (0x003D, "equal"),
1107 (0x003E, "greater"),
1108 (0x003F, "question"),
1109 (0x0040, "at"),
1110 (0x0041, "A"),
1111 (0x0042, "B"),
1112 (0x0043, "C"),
1113 (0x0044, "D"),
1114 (0x0045, "E"),
1115 (0x0046, "F"),
1116 (0x0047, "G"),
1117 (0x0048, "H"),
1118 (0x0049, "I"),
1119 (0x004A, "J"),
1120 (0x004B, "K"),
1121 (0x004C, "L"),
1122 (0x004D, "M"),
1123 (0x004E, "N"),
1124 (0x004F, "O"),
1125 (0x0050, "P"),
1126 (0x0051, "Q"),
1127 (0x0052, "R"),
1128 (0x0053, "S"),
1129 (0x0054, "T"),
1130 (0x0055, "U"),
1131 (0x0056, "V"),
1132 (0x0057, "W"),
1133 (0x0058, "X"),
1134 (0x0059, "Y"),
1135 (0x005A, "Z"),
1136 (0x005B, "bracketleft"),
1137 (0x005C, "backslash"),
1138 (0x005D, "bracketright"),
1139 (0x005E, "asciicircum"),
1140 (0x005F, "underscore"),
1141 (0x0060, "grave"),
1142 (0x0061, "a"),
1143 (0x0062, "b"),
1144 (0x0063, "c"),
1145 (0x0064, "d"),
1146 (0x0065, "e"),
1147 (0x0066, "f"),
1148 (0x0067, "g"),
1149 (0x0068, "h"),
1150 (0x0069, "i"),
1151 (0x006A, "j"),
1152 (0x006B, "k"),
1153 (0x006C, "l"),
1154 (0x006D, "m"),
1155 (0x006E, "n"),
1156 (0x006F, "o"),
1157 (0x0070, "p"),
1158 (0x0071, "q"),
1159 (0x0072, "r"),
1160 (0x0073, "s"),
1161 (0x0074, "t"),
1162 (0x0075, "u"),
1163 (0x0076, "v"),
1164 (0x0077, "w"),
1165 (0x0078, "x"),
1166 (0x0079, "y"),
1167 (0x007A, "z"),
1168 (0x007B, "braceleft"),
1169 (0x007C, "bar"),
1170 (0x007D, "braceright"),
1171 (0x007E, "asciitilde"),
1172 (0x00A0, "nonbreakingspace"),
1173 (0x00A1, "exclamdown"),
1174 (0x00A2, "cent"),
1175 (0x00A3, "sterling"),
1176 (0x00A4, "currency"),
1177 (0x00A5, "yen"),
1178 (0x00A6, "brokenbar"),
1179 (0x00A7, "section"),
1180 (0x00A8, "dieresis"),
1181 (0x00A9, "copyright"),
1182 (0x00AA, "ordfeminine"),
1183 (0x00AB, "guillemotleft"),
1184 (0x00AC, "logicalnot"),
1185 (0x00AD, "softhyphen"),
1186 (0x00AE, "registered"),
1187 (0x00AF, "macron"),
1188 (0x00B0, "degree"),
1189 (0x00B1, "plusminus"),
1190 (0x00B2, "twosuperior"),
1191 (0x00B3, "threesuperior"),
1192 (0x00B4, "acute"),
1193 (0x00B5, "mu"),
1194 (0x00B6, "paragraph"),
1195 (0x00B7, "periodcentered"),
1196 (0x00B8, "cedilla"),
1197 (0x00B9, "onesuperior"),
1198 (0x00BA, "ordmasculine"),
1199 (0x00BB, "guillemotright"),
1200 (0x00BC, "onequarter"),
1201 (0x00BD, "onehalf"),
1202 (0x00BE, "threequarters"),
1203 (0x00BF, "questiondown"),
1204 (0x00C0, "Agrave"),
1205 (0x00C1, "Aacute"),
1206 (0x00C2, "Acircumflex"),
1207 (0x00C3, "Atilde"),
1208 (0x00C4, "Adieresis"),
1209 (0x00C5, "Aring"),
1210 (0x00C6, "AE"),
1211 (0x00C7, "Ccedilla"),
1212 (0x00C8, "Egrave"),
1213 (0x00C9, "Eacute"),
1214 (0x00CA, "Ecircumflex"),
1215 (0x00CB, "Edieresis"),
1216 (0x00CC, "Igrave"),
1217 (0x00CD, "Iacute"),
1218 (0x00CE, "Icircumflex"),
1219 (0x00CF, "Idieresis"),
1220 (0x00D0, "Eth"),
1221 (0x00D1, "Ntilde"),
1222 (0x00D2, "Ograve"),
1223 (0x00D3, "Oacute"),
1224 (0x00D4, "Ocircumflex"),
1225 (0x00D5, "Otilde"),
1226 (0x00D6, "Odieresis"),
1227 (0x00D7, "multiply"),
1228 (0x00D8, "Oslash"),
1229 (0x00D9, "Ugrave"),
1230 (0x00DA, "Uacute"),
1231 (0x00DB, "Ucircumflex"),
1232 (0x00DC, "Udieresis"),
1233 (0x00DD, "Yacute"),
1234 (0x00DE, "Thorn"),
1235 (0x00DF, "germandbls"),
1236 (0x00E0, "agrave"),
1237 (0x00E1, "aacute"),
1238 (0x00E2, "acircumflex"),
1239 (0x00E3, "atilde"),
1240 (0x00E4, "adieresis"),
1241 (0x00E5, "aring"),
1242 (0x00E6, "ae"),
1243 (0x00E7, "ccedilla"),
1244 (0x00E8, "egrave"),
1245 (0x00E9, "eacute"),
1246 (0x00EA, "ecircumflex"),
1247 (0x00EB, "edieresis"),
1248 (0x00EC, "igrave"),
1249 (0x00ED, "iacute"),
1250 (0x00EE, "icircumflex"),
1251 (0x00EF, "idieresis"),
1252 (0x00F0, "eth"),
1253 (0x00F1, "ntilde"),
1254 (0x00F2, "ograve"),
1255 (0x00F3, "oacute"),
1256 (0x00F4, "ocircumflex"),
1257 (0x00F5, "otilde"),
1258 (0x00F6, "odieresis"),
1259 (0x00F7, "divide"),
1260 (0x00F8, "oslash"),
1261 (0x00F9, "ugrave"),
1262 (0x00FA, "uacute"),
1263 (0x00FB, "ucircumflex"),
1264 (0x00FC, "udieresis"),
1265 (0x00FD, "yacute"),
1266 (0x00FE, "thorn"),
1267 (0x00FF, "ydieresis"),
1268 (0x0131, "dotlessi"),
1270 (0x0141, "Lslash"),
1271 (0x0142, "lslash"),
1272 (0x0152, "OE"),
1273 (0x0153, "oe"),
1274 (0x0160, "Scaron"),
1275 (0x0161, "scaron"),
1276 (0x0178, "Ydieresis"),
1277 (0x017D, "Zcaron"),
1278 (0x017E, "zcaron"),
1279 (0x0192, "florin"),
1280 (0x02C6, "circumflex"),
1281 (0x02C7, "caron"),
1282 (0x02D8, "breve"),
1283 (0x02D9, "dotaccent"),
1284 (0x02DA, "ring"),
1285 (0x02DB, "ogonek"),
1286 (0x02DC, "tilde"),
1287 (0x02DD, "hungarumlaut"),
1288 (0x2013, "endash"),
1289 (0x2014, "emdash"),
1290 (0x2018, "quoteleft"),
1291 (0x2019, "quoteright"),
1292 (0x201A, "quotesinglbase"),
1293 (0x201C, "quotedblleft"),
1294 (0x201D, "quotedblright"),
1295 (0x201E, "quotedblbase"),
1296 (0x2020, "dagger"),
1297 (0x2021, "daggerdbl"),
1298 (0x2022, "bullet"),
1299 (0x2026, "ellipsis"),
1300 (0x2030, "perthousand"),
1301 (0x2039, "guilsinglleft"),
1302 (0x203A, "guilsinglright"),
1303 (0x2044, "fraction"),
1304 (0x20AC, "Euro"),
1305 (0x2122, "trademark"),
1306 (0x2202, "partialdiff"),
1307 (0x2206, "Delta"),
1308 (0x220F, "product"),
1309 (0x2211, "summation"),
1310 (0x221A, "radical"),
1311 (0x221E, "infinity"),
1312 (0x222B, "integral"),
1313 (0x2248, "approxequal"),
1314 (0x2260, "notequal"),
1315 (0x2264, "lessequal"),
1316 (0x2265, "greaterequal"),
1317 (0x25CA, "lozenge"),
1318 (0xF001, "fi"),
1319 (0xFB01, "fi"),
1320 (0xFB02, "fl"),
1321];
1322
1323#[cfg(test)]
1324mod tests {
1325 use super::*;
1326
1327 #[test]
1328 fn test_parse_fontname_line() {
1329 assert_eq!(
1330 parse_fontname_line("/FontName /Helvetica def"),
1331 Some("Helvetica".to_string())
1332 );
1333 assert_eq!(
1334 parse_fontname_line("/FontName /NimbusSans-Regular def"),
1335 Some("NimbusSans-Regular".to_string())
1336 );
1337 assert_eq!(parse_fontname_line("/FontType 1 def"), None);
1338 }
1339
1340 #[test]
1341 fn test_unicode_to_glyph_name() {
1342 assert_eq!(unicode_to_glyph_name(0x0041), Some("A"));
1343 assert_eq!(unicode_to_glyph_name(0x0020), Some("space"));
1344 assert_eq!(unicode_to_glyph_name(0x00C9), Some("Eacute"));
1345 assert_eq!(unicode_to_glyph_name(0x9999), None);
1346 }
1347
1348 #[test]
1349 fn test_extract_ps_name_from_pfa() {
1350 let font_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
1351 .join("../../resources/Font/NimbusSans-Regular.t1");
1352 if !font_path.exists() {
1353 return;
1354 }
1355 let name = extract_ps_name_from_pfa(&font_path);
1356 assert_eq!(name, Some("NimbusSans-Regular".to_string()));
1357 }
1358
1359 #[test]
1360 fn test_system_font_cache_builds() {
1361 let cache = SystemFontCache::build();
1362 assert!(
1364 !cache.fonts.is_empty(),
1365 "Expected to find system fonts, found none"
1366 );
1367 }
1368
1369 #[test]
1370 fn test_cmap_format4_real_font() {
1371 let test_fonts = [
1373 "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
1374 "/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf",
1375 ];
1376 for path in &test_fonts {
1377 if let Ok(data) = std::fs::read(path) {
1378 let map = parse_cmap_table(&data);
1379 assert!(map.is_some(), "Failed to parse cmap for {}", path);
1380 let map = map.unwrap();
1381 assert!(map.contains_key(&0x41), "cmap missing 'A' for {}", path);
1383 return;
1384 }
1385 }
1386 eprintln!("Skipping cmap test — no test font found");
1387 }
1388}