1use crate::error::{Result, XfaError};
34use lopdf::ObjectId;
35use std::collections::HashMap;
36use std::path::PathBuf;
37use std::sync::OnceLock;
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub struct PdfSourceFont {
42 pub object_id: ObjectId,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct EmbeddedFontData {
49 pub name: String,
51 pub data: Vec<u8>,
53 pub pdf_widths: Option<(u16, Vec<u16>)>,
55 pub pdf_encoding: Option<PdfSimpleEncoding>,
57 pub pdf_source_font: Option<PdfSourceFont>,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum PdfBaseEncoding {
64 WinAnsi,
66 Standard,
68 MacRoman,
70}
71
72impl PdfBaseEncoding {
73 pub fn from_pdf_name(name: &[u8]) -> Option<Self> {
75 match name {
76 b"WinAnsiEncoding" => Some(Self::WinAnsi),
77 b"StandardEncoding" => Some(Self::Standard),
78 b"MacRomanEncoding" => Some(Self::MacRoman),
79 _ => None,
80 }
81 }
82
83 fn code_to_unicode_table(self) -> &'static [Option<u16>; 256] {
84 match self {
85 Self::WinAnsi => &base_encoding_tables().win_ansi,
86 Self::Standard => &base_encoding_tables().standard,
87 Self::MacRoman => &base_encoding_tables().mac_roman,
88 }
89 }
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct PdfSimpleEncoding {
95 pub base_encoding: PdfBaseEncoding,
97 pub differences: Vec<(u8, u16)>,
99}
100
101impl PdfSimpleEncoding {
102 pub fn code_to_unicode_table(&self) -> Vec<Option<u16>> {
104 let mut table = self.base_encoding.code_to_unicode_table().to_vec();
105 for (code, unicode) in &self.differences {
106 table[*code as usize] = Some(*unicode);
107 }
108 table
109 }
110}
111
112#[derive(Debug, Clone, PartialEq, Eq)]
113struct PdfWidthData {
114 widths: (u16, Vec<u16>),
115 encoding: Option<PdfSimpleEncoding>,
116 source_font: Option<PdfSourceFont>,
117}
118
119#[derive(Debug)]
120struct PdfBaseEncodingTables {
121 win_ansi: [Option<u16>; 256],
122 standard: [Option<u16>; 256],
123 mac_roman: [Option<u16>; 256],
124}
125
126#[derive(Debug, Clone)]
128pub struct ResolvedFont {
129 pub name: String,
131 pub data: Vec<u8>,
133 pub face_index: u32,
135 pub units_per_em: u16,
137 pub ascender: i16,
139 pub descender: i16,
141 pub pdf_widths: Option<(u16, Vec<u16>)>,
143 pub pdf_encoding: Option<PdfSimpleEncoding>,
145 pub pdf_source_font: Option<PdfSourceFont>,
147}
148
149impl ResolvedFont {
150 pub fn measure_string(&self, text: &str, font_size: f64) -> f64 {
152 if let Some(widths) = self.pdf_unicode_widths() {
153 let mut total = 0.0;
154 for ch in text.chars() {
155 let code = ch as usize;
156 if code < widths.len() && widths[code] != 0 {
157 total += widths[code] as f64;
158 } else {
159 total += self.measure_char_fallback(ch);
160 }
161 }
162 return total * font_size / 1000.0;
163 }
164 if let Ok(face) = ttf_parser::Face::parse(&self.data, self.face_index) {
165 let upem = face.units_per_em() as f64;
166 let scale = font_size / upem;
167 let mut width = 0.0;
168 for ch in text.chars() {
169 if let Some(gid) = face.glyph_index(ch) {
170 width += face.glyph_hor_advance(gid).unwrap_or(0) as f64 * scale;
171 } else {
172 width += font_size * 0.5;
173 }
174 }
175 width
176 } else {
177 text.len() as f64 * font_size * 0.5
178 }
179 }
180
181 fn measure_char_fallback(&self, ch: char) -> f64 {
182 if let Ok(face) = ttf_parser::Face::parse(&self.data, self.face_index) {
183 if let Some(gid) = face.glyph_index(ch) {
184 let upem = face.units_per_em() as f64;
185 let scale = 1.0 / upem;
186 face.glyph_hor_advance(gid).unwrap_or(0) as f64 * scale * 1000.0
187 } else {
188 500.0
189 }
190 } else {
191 500.0
192 }
193 }
194
195 pub fn line_height(&self, font_size: f64) -> f64 {
197 let upem = self.units_per_em as f64;
198 if upem > 0.0 {
199 (self.ascender as f64 - self.descender as f64) / upem * font_size
200 } else {
201 font_size * 1.2
202 }
203 }
204
205 pub fn ascender_pt(&self, font_size: f64) -> f64 {
207 let upem = self.units_per_em as f64;
208 if upem > 0.0 {
209 self.ascender as f64 / upem * font_size
210 } else {
211 font_size * 0.8
212 }
213 }
214
215 pub fn descender_pt(&self, font_size: f64) -> f64 {
217 let upem = self.units_per_em as f64;
218 if upem > 0.0 {
219 self.descender as f64 / upem * font_size
220 } else {
221 font_size * -0.2
222 }
223 }
224
225 pub fn pdf_glyph_widths(&self) -> (u16, Vec<u16>) {
237 if let Some(widths) = self.pdf_unicode_widths() {
238 return (0, widths);
239 }
240 if let Ok(face) = ttf_parser::Face::parse(&self.data, self.face_index) {
241 let upem = face.units_per_em() as f64;
242 let scale = 1000.0 / upem;
243 let mut widths = Vec::with_capacity(256);
244 for code in 0u16..256 {
245 let w = if let Some(gid) = face.glyph_index(char::from(code as u8)) {
246 (face.glyph_hor_advance(gid).unwrap_or(0) as f64 * scale) as u16
247 } else {
248 0
249 };
250 widths.push(w);
251 }
252 (0, widths)
253 } else {
254 (0, vec![500; 256])
255 }
256 }
257
258 fn pdf_unicode_widths(&self) -> Option<Vec<u16>> {
259 let (first_char, widths) = self.pdf_widths.as_ref()?;
260 Some(unicode_widths_from_pdf_widths(
261 *first_char,
262 widths,
263 self.pdf_encoding.as_ref(),
264 ))
265 }
266
267 pub fn cid_font_info(&self) -> Option<CidFontInfo> {
272 let face = ttf_parser::Face::parse(&self.data, self.face_index).ok()?;
273 let upem = face.units_per_em() as f64;
274 let scale = 1000.0 / upem;
275 let num_glyphs = face.number_of_glyphs();
276
277 let mut widths = Vec::with_capacity(num_glyphs as usize);
278 for gid_val in 0..num_glyphs {
279 let w = face
280 .glyph_hor_advance(ttf_parser::GlyphId(gid_val))
281 .map(|a| (a as f64 * scale) as u16)
282 .unwrap_or(0);
283 widths.push(w);
284 }
285
286 let mut gid_to_unicode = Vec::new();
287 for cp in 0x0020u32..=0xFFFDu32 {
288 if let Some(ch) = char::from_u32(cp) {
289 if let Some(gid) = face.glyph_index(ch) {
290 gid_to_unicode.push((gid.0, ch));
291 }
292 }
293 }
294
295 Some(CidFontInfo {
296 widths,
297 gid_to_unicode,
298 })
299 }
300}
301
302pub struct CidFontInfo {
304 pub widths: Vec<u16>,
306 pub gid_to_unicode: Vec<(u16, char)>,
308}
309
310pub fn font_variant_key(typeface: &str, weight: Option<&str>, posture: Option<&str>) -> String {
315 let w = match weight {
316 Some("bold") => "_Bold",
317 _ => "_Normal",
318 };
319 let p = match posture {
320 Some("italic") => "_Italic",
321 _ => "_Normal",
322 };
323 format!("{}{}{}", typeface, w, p)
324}
325
326#[derive(Debug, Clone, Copy, PartialEq, Eq)]
331pub enum GenericFamily {
332 Serif,
334 SansSerif,
336 Monospaced,
338 Decorative,
340 Fantasy,
342 Cursive,
344}
345
346impl GenericFamily {
347 pub fn parse(s: &str) -> Option<Self> {
349 match s {
350 "serif" => Some(Self::Serif),
351 "sansSerif" => Some(Self::SansSerif),
352 "monospaced" => Some(Self::Monospaced),
353 "decorative" => Some(Self::Decorative),
354 "fantasy" => Some(Self::Fantasy),
355 "cursive" => Some(Self::Cursive),
356 _ => None,
357 }
358 }
359}
360
361#[derive(Debug, Clone)]
363pub struct XfaFontSpec {
364 pub typeface: String,
366 pub weight: FontWeight,
368 pub posture: FontPosture,
370 pub size_pt: f64,
372 pub generic_family: Option<GenericFamily>,
374}
375#[derive(Debug, Clone, Copy, PartialEq, Eq)]
378pub enum FontWeight {
379 Normal,
381 Bold,
383}
384#[derive(Debug, Clone, Copy, PartialEq, Eq)]
387pub enum FontPosture {
388 Normal,
390 Italic,
392}
393
394impl XfaFontSpec {
395 pub fn from_xfa_attrs(
397 typeface: &str,
398 weight: Option<&str>,
399 posture: Option<&str>,
400 size: Option<&str>,
401 generic_family: Option<&str>,
402 ) -> Self {
403 Self {
404 typeface: typeface.to_string(),
405 weight: match weight {
406 Some("bold") => FontWeight::Bold,
407 _ => FontWeight::Normal,
408 },
409 posture: match posture {
410 Some("italic") => FontPosture::Italic,
411 _ => FontPosture::Normal,
412 },
413 size_pt: size
414 .and_then(|s| s.strip_suffix("pt").or(Some(s)))
415 .and_then(|s| s.parse::<f64>().ok())
416 .unwrap_or(10.0),
417 generic_family: generic_family.and_then(GenericFamily::parse),
418 }
419 }
420}
421
422pub struct XfaFontResolver {
424 embedded: HashMap<String, ResolvedFont>,
425 embedded_pdf_widths: HashMap<String, PdfWidthData>,
426 system_fonts: HashMap<String, PathBuf>,
427 cache: HashMap<String, ResolvedFont>,
428 font_cache_fonts: HashMap<String, PathBuf>,
431}
432
433fn normalize_font_name(name: &str) -> String {
439 let stripped = if name.len() > 7 && name.as_bytes()[6] == b'+' {
441 let prefix = &name[..6];
442 if prefix.chars().all(|c| c.is_ascii_uppercase()) {
443 &name[7..]
444 } else {
445 name
446 }
447 } else {
448 name
449 };
450
451 let stripped = stripped
456 .strip_suffix("PSMT")
457 .or_else(|| stripped.strip_suffix("MT"))
458 .unwrap_or(stripped);
459
460 stripped.to_lowercase()
461}
462
463fn canonical_font_key(name: &str) -> Option<String> {
479 let normalized = normalize_font_name(name);
480 let canonical: String = normalized
481 .chars()
482 .filter(|c| c.is_ascii_alphanumeric())
483 .collect();
484 if canonical.is_empty() {
485 None
486 } else {
487 Some(canonical)
488 }
489}
490
491fn font_family_aliases(name: &str) -> &'static [&'static str] {
495 match name {
496 "arial" | "arialmt" => &["liberationsans", "arimo", "freesans"],
497 "times new roman" | "timesnewroman" | "timesnewromanpsmt" => {
498 &["liberationserif", "tinos", "freeserif"]
499 }
500 "courier new" | "couriernew" | "couriernewpsmt" => {
501 &["liberationmono", "cousine", "freemono"]
502 }
503 "helvetica" => &["liberationsans", "arimo", "arial"],
504 "myriad pro" | "myriadpro" => &["liberationsans", "arimo", "dejavusans"],
505 "liberationsans" | "liberation sans" => &["arial", "arimo", "freesans", "helvetica"],
507 "liberationserif" | "liberation serif" => &["times new roman", "tinos", "freeserif"],
508 "liberationmono" | "liberation mono" => &["courier new", "cousine", "freemono"],
509 _ => &[],
510 }
511}
512
513#[derive(Debug, Clone, Copy, PartialEq, Eq)]
515enum FontFamily {
516 Serif,
517 SansSerif,
518 Monospace,
519 Unknown,
520}
521
522fn classify_font_family(name: &str) -> FontFamily {
524 let lower = name.to_lowercase();
525
526 if lower.contains("mono")
528 || lower.contains("courier")
529 || lower.contains("consolas")
530 || lower.contains("menlo")
531 || lower.contains("fixed")
532 || lower.contains("code")
533 {
534 return FontFamily::Monospace;
535 }
536
537 if lower.contains("serif") && !lower.contains("sans") {
539 return FontFamily::Serif;
540 }
541 if lower.contains("times")
542 || lower.contains("garamond")
543 || lower.contains("georgia")
544 || lower.contains("palatino")
545 || lower.contains("bodoni")
546 || lower.contains("cambria")
547 || lower.contains("tinos")
548 {
549 return FontFamily::Serif;
550 }
551
552 if lower.contains("sans")
554 || lower.contains("arial")
555 || lower.contains("helvetica")
556 || lower.contains("verdana")
557 || lower.contains("tahoma")
558 || lower.contains("calibri")
559 || lower.contains("arimo")
560 || lower.contains("myriad")
561 || lower.contains("segoe")
562 {
563 return FontFamily::SansSerif;
564 }
565
566 FontFamily::Unknown
567}
568
569fn family_fallback_chain(family: FontFamily) -> &'static [&'static str] {
571 match family {
572 FontFamily::SansSerif | FontFamily::Unknown => &[
573 "liberationsans",
574 "arimo",
575 "dejavusans",
576 "freesans",
577 "helvetica",
578 "arial",
579 ],
580 FontFamily::Serif => &["liberationserif", "tinos", "dejavuserif", "freeserif"],
581 FontFamily::Monospace => &["liberationmono", "cousine", "dejavusansmono", "freemono"],
582 }
583}
584
585fn font_name_substring_match(requested: &str, available: &str) -> bool {
586 let requested_lower = requested.to_lowercase();
587 let available_lower = available.to_lowercase();
588
589 if requested_lower == available_lower {
590 return true;
591 }
592
593 if requested_lower.contains(&available_lower) || available_lower.contains(&requested_lower) {
594 return true;
595 }
596
597 let requested_stripped = normalize_font_name(requested);
598 let available_stripped = normalize_font_name(available);
599
600 if requested_stripped == available_stripped {
601 return true;
602 }
603
604 if requested_stripped.contains(&available_stripped)
605 || available_stripped.contains(&requested_stripped)
606 {
607 return true;
608 }
609
610 false
611}
612
613fn generic_family_fallback_chain(gf: GenericFamily) -> &'static [&'static str] {
616 match gf {
617 GenericFamily::Serif | GenericFamily::Decorative => &[
618 "liberationserif",
619 "tinos",
620 "dejavuserif",
621 "freeserif",
622 "times new roman",
623 "times",
624 ],
625 GenericFamily::SansSerif | GenericFamily::Fantasy => &[
626 "liberationsans",
627 "arimo",
628 "dejavusans",
629 "freesans",
630 "helvetica",
631 "arial",
632 ],
633 GenericFamily::Monospaced => &[
634 "liberationmono",
635 "cousine",
636 "dejavusansmono",
637 "freemono",
638 "courier new",
639 "courier",
640 ],
641 GenericFamily::Cursive => {
642 &[
644 "liberationserif",
645 "tinos",
646 "dejavuserif",
647 "freeserif",
648 "times new roman",
649 ]
650 }
651 }
652}
653
654impl XfaFontResolver {
655 pub fn new(embedded_fonts: Vec<EmbeddedFontData>) -> Self {
657 let mut embedded = HashMap::new();
658 let mut embedded_pdf_widths = HashMap::new();
659 for font_data in embedded_fonts {
660 let EmbeddedFontData {
661 name,
662 data,
663 pdf_widths,
664 pdf_encoding,
665 pdf_source_font,
666 } = font_data;
667 if let Some(ref widths) = pdf_widths {
668 remember_pdf_widths(
669 &mut embedded_pdf_widths,
670 &name,
671 widths,
672 pdf_encoding.clone(),
673 pdf_source_font,
674 );
675 }
676 if let Some(font) = parse_font_data_with_widths(
677 &name,
678 &data,
679 pdf_widths.clone(),
680 pdf_encoding.clone(),
681 pdf_source_font,
682 )
683 .or_else(|| build_pdf_only_font(&name, pdf_widths, pdf_encoding, pdf_source_font))
684 {
685 let normalized = normalize_font_name(&name);
686 embedded.insert(name.to_lowercase(), font.clone());
687 if normalized != name.to_lowercase() {
688 embedded.insert(normalized, font);
689 }
690 }
691 }
692 let system_fonts = scan_system_fonts();
693 let font_cache_fonts = scan_font_cache_dir();
694 Self {
695 embedded,
696 embedded_pdf_widths,
697 system_fonts,
698 cache: HashMap::new(),
699 font_cache_fonts,
700 }
701 }
702
703 pub fn with_font_cache(mut self, dir: &std::path::Path) -> Self {
705 self.font_cache_fonts = scan_font_dir(dir);
706 self
707 }
708
709 pub fn resolve(&mut self, spec: &XfaFontSpec) -> Result<ResolvedFont> {
721 let cache_key = format!("{}_{:?}_{:?}", spec.typeface, spec.weight, spec.posture);
722 if let Some(cached) = self.cache.get(&cache_key) {
723 return Ok(cached.clone());
724 }
725 let normalized = normalize_font_name(&spec.typeface);
726
727 let variant_names = build_variant_names(&spec.typeface, spec.weight, spec.posture);
729
730 let font = variant_names
732 .iter()
733 .find_map(|vn| {
734 self.try_embedded(vn)
735 .or_else(|| self.try_font_cache(vn))
736 .or_else(|| self.try_system(vn))
737 .or_else(|| {
738 let norm = normalize_font_name(vn);
739 self.try_embedded(&norm)
740 .or_else(|| self.try_font_cache(&norm))
741 .or_else(|| self.try_system(&norm))
742 })
743 })
744 .or_else(|| self.try_embedded(&spec.typeface))
746 .or_else(|| self.try_embedded(&normalized))
747 .or_else(|| self.try_font_cache(&spec.typeface))
748 .or_else(|| self.try_font_cache(&normalized))
749 .or_else(|| self.try_system(&spec.typeface))
750 .or_else(|| self.try_system(&normalized))
751 .or_else(|| self.try_base_name(&spec.typeface))
752 .or_else(|| self.try_aliases(&spec.typeface))
753 .or_else(|| self.try_substring_match(&spec.typeface))
754 .or_else(|| self.try_generic_family_fallback(spec.generic_family))
756 .or_else(|| self.try_family_fallback(&spec.typeface))
757 .or_else(|| self.try_fallbacks())
759 .ok_or_else(|| {
760 XfaError::FontError(format!("cannot resolve font: {}", spec.typeface))
761 })?;
762 let mut font = self.attach_pdf_widths(font, spec, &variant_names);
763 if font.data.is_empty() && font.pdf_source_font.is_some() {
768 let sys = self
769 .try_system(&font.name)
770 .or_else(|| self.try_system(&spec.typeface))
771 .or_else(|| self.try_aliases(&spec.typeface));
772 if let Some(sys_font) = sys {
773 font.data = sys_font.data;
774 font.face_index = sys_font.face_index;
775 }
776 }
777 self.cache.insert(cache_key, font.clone());
778 Ok(font)
779 }
780
781 fn try_embedded(&self, name: &str) -> Option<ResolvedFont> {
782 self.embedded.get(&name.to_lowercase()).cloned()
783 }
784
785 fn try_font_cache(&self, name: &str) -> Option<ResolvedFont> {
786 let path = self.font_cache_fonts.get(&name.to_lowercase())?;
787 load_system_font(path, name)
788 }
789
790 fn try_system(&self, name: &str) -> Option<ResolvedFont> {
791 let path = self.system_fonts.get(&name.to_lowercase())?;
792 load_system_font(path, name)
793 }
794
795 fn try_base_name(&self, name: &str) -> Option<ResolvedFont> {
796 let base = name
797 .replace("-Bold", "")
798 .replace("-Italic", "")
799 .replace("-BoldItalic", "")
800 .replace(",Bold", "")
801 .replace(",Italic", "");
802 if base != name {
803 let normalized_base = normalize_font_name(&base);
804 self.try_embedded(&base)
805 .or_else(|| self.try_embedded(&normalized_base))
806 .or_else(|| self.try_system(&base))
807 .or_else(|| self.try_system(&normalized_base))
808 } else {
809 None
810 }
811 }
812
813 fn try_aliases(&self, name: &str) -> Option<ResolvedFont> {
815 let normalized = normalize_font_name(name);
816
817 for lookup in &[name.to_lowercase(), normalized] {
818 let no_spaces = lookup.replace(' ', "");
819 for candidate in [lookup.as_str(), no_spaces.as_str()] {
820 let aliases = font_family_aliases(candidate);
821 for alias in aliases {
822 if let Some(font) = self.try_system(alias) {
823 return Some(font);
824 }
825 }
826 }
827 }
828 None
829 }
830
831 fn try_substring_match(&self, name: &str) -> Option<ResolvedFont> {
832 let name_lower = name.to_lowercase();
833 let name_normalized = normalize_font_name(name);
834
835 for (available_key, font) in &self.embedded {
836 if font_name_substring_match(&name_lower, available_key)
837 || font_name_substring_match(&name_normalized, available_key)
838 {
839 return Some(font.clone());
840 }
841 }
842
843 for (available_key, path) in &self.system_fonts {
844 if font_name_substring_match(&name_lower, available_key)
845 || font_name_substring_match(&name_normalized, available_key)
846 {
847 if let Some(font) = load_system_font(path, available_key) {
848 return Some(font);
849 }
850 }
851 }
852
853 None
854 }
855
856 fn try_generic_family_fallback(&self, gf: Option<GenericFamily>) -> Option<ResolvedFont> {
858 let gf = gf?;
859 let chain = generic_family_fallback_chain(gf);
860 for candidate in chain {
861 if let Some(font) = self.try_system(candidate) {
862 return Some(font);
863 }
864 }
865 None
866 }
867
868 fn try_family_fallback(&self, name: &str) -> Option<ResolvedFont> {
870 let family = classify_font_family(name);
871 let chain = family_fallback_chain(family);
872 for candidate in chain {
873 if let Some(font) = self.try_system(candidate) {
874 return Some(font);
875 }
876 }
877 None
878 }
879
880 fn try_fallbacks(&self) -> Option<ResolvedFont> {
881 #[cfg(target_os = "macos")]
882 let fallback_chain = ["Arial", "Helvetica.ttc", "DejaVuSans", "LiberationSans"];
883 #[cfg(not(target_os = "macos"))]
884 let fallback_chain = [
885 "LiberationSans",
886 "DejaVuSans",
887 "Arial",
888 "Helvetica",
889 "FreeSans",
890 ];
891
892 for name in &fallback_chain {
893 if let Some(font) = self.try_system(name) {
894 return Some(font);
895 }
896 }
897 None
898 }
899
900 fn style_matched_pdf_source(&self, spec: &XfaFontSpec) -> Option<PdfWidthData> {
909 if spec.weight == FontWeight::Normal && spec.posture == FontPosture::Normal {
910 return None;
911 }
912 let want_italic = spec.posture == FontPosture::Italic;
913 let want_bold = spec.weight == FontWeight::Bold;
914 let spec_base = canonical_font_key(&spec.typeface)?;
915 let strip_style = |s: &str| -> String {
916 s.replace("italic", "")
917 .replace("oblique", "")
918 .replace("bolditalic", "")
919 .replace("bold", "")
920 };
921 let mut best: Option<(usize, &PdfWidthData)> = None;
922 for (key, data) in &self.embedded_pdf_widths {
923 if data.source_font.is_none() {
924 continue;
925 }
926 let k = key.to_lowercase();
927 let has_italic = k.contains("italic") || k.contains("oblique");
928 let has_bold = k.contains("bold");
929 if has_italic != want_italic || has_bold != want_bold {
930 continue;
931 }
932 let Some(key_canon) = canonical_font_key(key) else {
933 continue;
934 };
935 let key_family = strip_style(&key_canon);
936 if key_family.contains(&spec_base) || spec_base.contains(&key_family) {
937 let score = key_family.len().abs_diff(spec_base.len());
938 if best.is_none_or(|(s, _)| score < s) {
939 best = Some((score, data));
940 }
941 }
942 }
943 best.map(|(_, d)| d.clone())
944 }
945
946 fn attach_pdf_widths(
947 &self,
948 mut font: ResolvedFont,
949 spec: &XfaFontSpec,
950 variant_names: &[String],
951 ) -> ResolvedFont {
952 if std::env::var("XFA_PDF_SOURCE_STYLE_MATCH").as_deref() != Ok("0") {
959 if let Some(width_data) = self.style_matched_pdf_source(spec) {
960 font.pdf_widths = Some(width_data.widths);
961 font.pdf_encoding = width_data.encoding;
962 font.pdf_source_font = width_data.source_font;
963 return font;
964 }
965 }
966
967 if font.pdf_widths.is_some() && font.pdf_source_font.is_some() {
968 return font;
969 }
970
971 let mut matched = false;
972 for name in variant_names
973 .iter()
974 .map(String::as_str)
975 .chain([spec.typeface.as_str(), font.name.as_str()])
976 {
977 if let Some(width_data) = lookup_pdf_widths(&self.embedded_pdf_widths, name) {
978 if font.pdf_widths.is_none() {
979 font.pdf_widths = Some(width_data.widths);
980 }
981 if font.pdf_encoding.is_none() {
982 font.pdf_encoding = width_data.encoding;
983 }
984 if font.pdf_source_font.is_none() {
985 font.pdf_source_font = width_data.source_font;
986 }
987 matched = true;
988 break;
989 }
990 }
991
992 if !matched
997 && font.pdf_widths.is_none()
998 && !self.embedded_pdf_widths.is_empty()
999 && std::env::var("XFA_FONT_BRIDGE_DEBUG").is_ok()
1000 {
1001 eprintln!(
1002 "attach_pdf_widths: no /Widths match for typeface '{}' (resolved='{}', variants={:?}); available keys (first 20): {:?}",
1003 spec.typeface,
1004 font.name,
1005 variant_names,
1006 self.embedded_pdf_widths
1007 .keys()
1008 .take(20)
1009 .collect::<Vec<_>>(),
1010 );
1011 }
1012
1013 font
1014 }
1015}
1016
1017fn remember_pdf_widths(
1018 widths_map: &mut HashMap<String, PdfWidthData>,
1019 name: &str,
1020 widths: &(u16, Vec<u16>),
1021 encoding: Option<PdfSimpleEncoding>,
1022 source_font: Option<PdfSourceFont>,
1023) {
1024 let record = PdfWidthData {
1025 widths: widths.clone(),
1026 encoding,
1027 source_font,
1028 };
1029 let lower = name.to_lowercase();
1030 widths_map.insert(lower.clone(), record.clone());
1031
1032 let normalized = normalize_font_name(name);
1033 if normalized != lower {
1034 widths_map.insert(normalized.clone(), record.clone());
1035 }
1036
1037 let no_spaces = lower.replace(' ', "");
1038 if no_spaces != lower {
1039 widths_map.insert(no_spaces, record.clone());
1040 }
1041
1042 if let Some(canonical) = canonical_font_key(name) {
1046 widths_map.entry(canonical).or_insert(record);
1047 }
1048}
1049
1050fn lookup_pdf_widths(
1051 widths_map: &HashMap<String, PdfWidthData>,
1052 name: &str,
1053) -> Option<PdfWidthData> {
1054 let lower = name.to_lowercase();
1055 if let Some(hit) = widths_map.get(&lower) {
1056 return Some(hit.clone());
1057 }
1058 let normalized = normalize_font_name(name);
1059 if let Some(hit) = widths_map.get(&normalized) {
1060 return Some(hit.clone());
1061 }
1062 let no_spaces = lower.replace(' ', "");
1063 if let Some(hit) = widths_map.get(&no_spaces) {
1064 return Some(hit.clone());
1065 }
1066 if let Some(canonical) = canonical_font_key(name) {
1069 if let Some(hit) = widths_map.get(&canonical) {
1070 return Some(hit.clone());
1071 }
1072 let mut best: Option<(&String, &PdfWidthData)> = None;
1076 for (key, data) in widths_map {
1077 if (key.contains(&canonical) || canonical.contains(key.as_str()))
1078 && best.is_none_or(|(b, _)| key.len() > b.len())
1079 {
1080 best = Some((key, data));
1081 }
1082 }
1083 if let Some((_, data)) = best {
1084 return Some(data.clone());
1085 }
1086 }
1087 None
1088}
1089
1090fn build_variant_names(typeface: &str, weight: FontWeight, posture: FontPosture) -> Vec<String> {
1096 let suffix = match (weight, posture) {
1097 (FontWeight::Bold, FontPosture::Italic) => "BoldItalic",
1098 (FontWeight::Bold, FontPosture::Normal) => "Bold",
1099 (FontWeight::Normal, FontPosture::Italic) => "Italic",
1100 (FontWeight::Normal, FontPosture::Normal) => return Vec::new(),
1101 };
1102
1103 let mut names = Vec::with_capacity(6);
1104 names.push(format!("{}-{}", typeface, suffix));
1106 names.push(format!("{}{}", typeface, suffix));
1108 names.push(format!("{} {}", typeface, suffix));
1110 names.push(format!("{},{}", typeface, suffix));
1112
1113 if weight == FontWeight::Bold && posture == FontPosture::Italic {
1116 names.push(format!("{}-Bold Italic", typeface));
1117 names.push(format!("{} Bold Italic", typeface));
1118 }
1119
1120 names
1121}
1122
1123fn _parse_font_data(name: &str, data: &[u8]) -> Option<ResolvedFont> {
1124 let face = ttf_parser::Face::parse(data, 0).ok()?;
1125 Some(ResolvedFont {
1126 name: name.to_string(),
1127 data: data.to_vec(),
1128 face_index: 0,
1129 units_per_em: face.units_per_em(),
1130 ascender: face.ascender(),
1131 descender: face.descender(),
1132 pdf_widths: None,
1133 pdf_encoding: None,
1134 pdf_source_font: None,
1135 })
1136}
1137
1138fn parse_font_data_with_widths(
1139 name: &str,
1140 data: &[u8],
1141 pdf_widths: Option<(u16, Vec<u16>)>,
1142 pdf_encoding: Option<PdfSimpleEncoding>,
1143 pdf_source_font: Option<PdfSourceFont>,
1144) -> Option<ResolvedFont> {
1145 let face = ttf_parser::Face::parse(data, 0).ok()?;
1146 Some(ResolvedFont {
1147 name: name.to_string(),
1148 data: data.to_vec(),
1149 face_index: 0,
1150 units_per_em: face.units_per_em(),
1151 ascender: face.ascender(),
1152 descender: face.descender(),
1153 pdf_widths,
1154 pdf_encoding,
1155 pdf_source_font,
1156 })
1157}
1158
1159fn build_pdf_only_font(
1160 name: &str,
1161 pdf_widths: Option<(u16, Vec<u16>)>,
1162 pdf_encoding: Option<PdfSimpleEncoding>,
1163 pdf_source_font: Option<PdfSourceFont>,
1164) -> Option<ResolvedFont> {
1165 let pdf_widths = pdf_widths?;
1166 let pdf_source_font = pdf_source_font?;
1167 Some(ResolvedFont {
1168 name: name.to_string(),
1169 data: Vec::new(),
1170 face_index: 0,
1171 units_per_em: 1000,
1178 ascender: 800,
1179 descender: -200,
1180 pdf_widths: Some(pdf_widths),
1181 pdf_encoding,
1182 pdf_source_font: Some(pdf_source_font),
1183 })
1184}
1185
1186fn load_system_font(path: &PathBuf, name: &str) -> Option<ResolvedFont> {
1187 let data = std::fs::read(path).ok()?;
1188 let num_fonts = ttf_parser::fonts_in_collection(&data).unwrap_or(1);
1189 for idx in 0..num_fonts {
1190 if let Ok(face) = ttf_parser::Face::parse(&data, idx) {
1191 let name_id_matches = |name_id: u16| {
1192 name_id == ttf_parser::name_id::FULL_NAME
1193 || name_id == ttf_parser::name_id::POST_SCRIPT_NAME
1194 || name_id == ttf_parser::name_id::FAMILY
1195 };
1196 let matches = face.names().into_iter().any(|n| {
1197 name_id_matches(n.name_id)
1198 && n.to_string().is_some_and(|s| s.eq_ignore_ascii_case(name))
1199 });
1200 if matches || idx == 0 {
1201 return Some(ResolvedFont {
1202 name: name.to_string(),
1203 data: data.clone(),
1204 face_index: idx,
1205 units_per_em: face.units_per_em(),
1206 ascender: face.ascender(),
1207 descender: face.descender(),
1208 pdf_widths: None,
1209 pdf_encoding: None,
1210 pdf_source_font: None,
1211 });
1212 }
1213 }
1214 }
1215 None
1216}
1217
1218fn unicode_widths_from_pdf_widths(
1219 first_char: u16,
1220 widths: &[u16],
1221 encoding: Option<&PdfSimpleEncoding>,
1222) -> Vec<u16> {
1223 let code_to_unicode = encoding
1224 .map(PdfSimpleEncoding::code_to_unicode_table)
1225 .unwrap_or_else(|| PdfBaseEncoding::WinAnsi.code_to_unicode_table().to_vec());
1226
1227 let mut max_codepoint = 255usize;
1228 for (offset, _) in widths.iter().enumerate() {
1229 let code = first_char as usize + offset;
1230 if code >= 256 {
1231 break;
1232 }
1233 if let Some(unicode) = code_to_unicode[code] {
1234 max_codepoint = max_codepoint.max(unicode as usize);
1235 }
1236 }
1237
1238 let mut projected = vec![0u16; max_codepoint + 1];
1239 for (offset, &width) in widths.iter().enumerate() {
1240 let code = first_char as usize + offset;
1241 if code >= 256 {
1242 break;
1243 }
1244 if let Some(unicode) = code_to_unicode[code] {
1245 projected[unicode as usize] = width;
1246 }
1247 }
1248 projected
1249}
1250
1251pub(crate) fn pdf_glyph_name_to_unicode(name: &str) -> Option<u16> {
1252 if let Some(base) = name.split('.').next() {
1253 if base != name {
1254 return pdf_glyph_name_to_unicode(base);
1255 }
1256 }
1257
1258 if let Some(cp) = glyph_name_map().get(name) {
1259 return Some(*cp);
1260 }
1261
1262 if let Some(hex) = name.strip_prefix("uni") {
1263 if hex.len() == 4 && hex.chars().all(|ch| ch.is_ascii_hexdigit()) {
1264 return u16::from_str_radix(hex, 16).ok();
1265 }
1266 }
1267
1268 if let Some(hex) = name.strip_prefix('u') {
1269 if (4..=6).contains(&hex.len()) && hex.chars().all(|ch| ch.is_ascii_hexdigit()) {
1270 let codepoint = u32::from_str_radix(hex, 16).ok()?;
1271 return u16::try_from(codepoint).ok();
1272 }
1273 }
1274
1275 None
1276}
1277
1278fn glyph_name_map() -> &'static HashMap<&'static str, u16> {
1279 static MAP: OnceLock<HashMap<&'static str, u16>> = OnceLock::new();
1280 MAP.get_or_init(|| {
1281 let mut map = HashMap::new();
1282 for line in include_str!("encodings/glyphnames.rs").lines() {
1283 let line = line.trim();
1284 let Some(rest) = line.strip_prefix("pub const ") else {
1285 continue;
1286 };
1287 let Some((name, hex)) = rest.split_once(": u16 = 0x") else {
1288 continue;
1289 };
1290 let Some(hex) = hex.strip_suffix(';') else {
1291 continue;
1292 };
1293 if let Ok(codepoint) = u16::from_str_radix(hex, 16) {
1294 map.insert(name, codepoint);
1295 }
1296 }
1297 map
1298 })
1299}
1300
1301fn base_encoding_tables() -> &'static PdfBaseEncodingTables {
1302 static TABLES: OnceLock<PdfBaseEncodingTables> = OnceLock::new();
1303 TABLES.get_or_init(|| PdfBaseEncodingTables {
1304 win_ansi: parse_base_encoding_table("WIN_ANSI_ENCODING"),
1305 standard: parse_base_encoding_table("STANDARD_ENCODING"),
1306 mac_roman: parse_base_encoding_table("MAC_ROMAN_ENCODING"),
1307 })
1308}
1309
1310fn parse_base_encoding_table(const_name: &str) -> [Option<u16>; 256] {
1311 let src = include_str!("encodings/mappings.rs");
1317 let marker = format!("pub const {const_name}: CodedCharacterSet = [");
1318 let start = src
1319 .find(&marker)
1320 .unwrap_or_else(|| panic!("missing {const_name} in compile-time-embedded lopdf mappings"));
1322 let body = &src[start + marker.len()..];
1323 let end = body
1324 .find("];")
1325 .unwrap_or_else(|| {
1327 panic!("unterminated {const_name} in compile-time-embedded lopdf mappings")
1328 });
1329
1330 let mut entries = Vec::with_capacity(256);
1331 for line in body[..end].lines() {
1332 let line = line.trim();
1333 if line.is_empty() {
1334 continue;
1335 }
1336 if line == "None," {
1337 entries.push(None);
1338 continue;
1339 }
1340 if let Some(name) = line
1341 .strip_prefix("Some(Glyph::")
1342 .and_then(|rest| rest.strip_suffix("),"))
1343 {
1344 entries.push(pdf_glyph_name_to_unicode(name));
1345 }
1346 }
1347
1348 let entry_count = entries.len();
1349 entries
1350 .try_into()
1351 .unwrap_or_else(|_| panic!("expected 256 entries in {const_name}, got {entry_count}; compile-time data invariant violated"))
1353}
1354
1355fn scan_system_fonts() -> HashMap<String, PathBuf> {
1356 let mut fonts = HashMap::new();
1357 let mut font_files = Vec::new();
1358
1359 for dir in system_font_dirs() {
1362 collect_font_files(&dir, &mut font_files, 0);
1363 }
1364
1365 for path in &font_files {
1366 if let Some(name) = path.file_stem().and_then(|s| s.to_str()) {
1368 fonts.insert(name.to_lowercase(), path.clone());
1369 }
1370
1371 if let Ok(data) = std::fs::read(path) {
1373 let num_faces = ttf_parser::fonts_in_collection(&data).unwrap_or(1);
1374 for face_idx in 0..num_faces {
1375 if let Ok(face) = ttf_parser::Face::parse(&data, face_idx) {
1376 for name_record in face.names() {
1377 let dominated = matches!(
1380 name_record.name_id,
1381 ttf_parser::name_id::FAMILY
1382 | ttf_parser::name_id::FULL_NAME
1383 | ttf_parser::name_id::POST_SCRIPT_NAME
1384 );
1385 if dominated {
1386 if let Some(s) = name_record.to_string() {
1387 let key = s.to_lowercase();
1388 fonts.entry(key).or_insert_with(|| path.clone());
1390
1391 let no_spaces = s.replace(' ', "").to_lowercase();
1394 if no_spaces != s.to_lowercase() {
1395 fonts.entry(no_spaces).or_insert_with(|| path.clone());
1396 }
1397 }
1398 }
1399 }
1400 }
1401 }
1402 }
1403 }
1404
1405 fonts
1406}
1407
1408fn collect_font_files(dir: &std::path::Path, out: &mut Vec<PathBuf>, depth: u32) {
1410 if depth > 3 {
1411 return;
1412 }
1413 if let Ok(entries) = std::fs::read_dir(dir) {
1414 for entry in entries.flatten() {
1415 let path = entry.path();
1416 if path.is_dir() {
1417 collect_font_files(&path, out, depth + 1);
1418 } else {
1419 let ext = path
1420 .extension()
1421 .and_then(|e| e.to_str())
1422 .unwrap_or("")
1423 .to_lowercase();
1424 if matches!(ext.as_str(), "ttf" | "otf" | "ttc" | "otc") {
1425 out.push(path);
1426 }
1427 }
1428 }
1429 }
1430}
1431
1432fn system_font_dirs() -> Vec<PathBuf> {
1433 #[cfg_attr(target_arch = "wasm32", allow(unused_mut))]
1436 let mut dirs = Vec::new();
1437 #[cfg(target_os = "macos")]
1438 {
1439 dirs.push(PathBuf::from("/System/Library/Fonts"));
1440 dirs.push(PathBuf::from("/Library/Fonts"));
1441 if let Ok(home) = std::env::var("HOME") {
1442 dirs.push(PathBuf::from(format!("{home}/Library/Fonts")));
1443 }
1444 }
1445 #[cfg(target_os = "linux")]
1446 {
1447 dirs.push(PathBuf::from("/usr/share/fonts"));
1448 dirs.push(PathBuf::from("/usr/local/share/fonts"));
1449 if let Ok(home) = std::env::var("HOME") {
1450 dirs.push(PathBuf::from(format!("{home}/.local/share/fonts")));
1451 dirs.push(PathBuf::from(format!("{home}/.fonts")));
1452 }
1453 }
1454 #[cfg(target_os = "windows")]
1455 {
1456 if let Ok(windir) = std::env::var("WINDIR") {
1457 dirs.push(PathBuf::from(format!("{windir}\\Fonts")));
1458 }
1459 if let Ok(local) = std::env::var("LOCALAPPDATA") {
1460 dirs.push(PathBuf::from(format!("{local}\\Microsoft\\Windows\\Fonts")));
1461 }
1462 }
1463 dirs
1464}
1465
1466fn scan_font_dir(dir: &std::path::Path) -> HashMap<String, PathBuf> {
1471 let mut fonts = HashMap::new();
1472 let entries = match std::fs::read_dir(dir) {
1473 Ok(e) => e,
1474 Err(_) => return fonts,
1475 };
1476 for entry in entries.flatten() {
1477 let path = entry.path();
1478 if path.is_dir() {
1479 continue;
1480 }
1481 let ext = path
1482 .extension()
1483 .and_then(|e| e.to_str())
1484 .unwrap_or("")
1485 .to_lowercase();
1486 if !matches!(ext.as_str(), "ttf" | "otf" | "ttc" | "otc" | "cff") {
1487 continue;
1488 }
1489 if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
1491 fonts
1492 .entry(stem.to_lowercase())
1493 .or_insert_with(|| path.clone());
1494 }
1495 if let Ok(data) = std::fs::read(&path) {
1497 let num_faces = ttf_parser::fonts_in_collection(&data).unwrap_or(1);
1498 for face_idx in 0..num_faces {
1499 if let Ok(face) = ttf_parser::Face::parse(&data, face_idx) {
1500 for name_record in face.names() {
1501 let dominated = matches!(
1502 name_record.name_id,
1503 ttf_parser::name_id::FAMILY
1504 | ttf_parser::name_id::FULL_NAME
1505 | ttf_parser::name_id::POST_SCRIPT_NAME
1506 );
1507 if dominated {
1508 if let Some(s) = name_record.to_string() {
1509 let key = s.to_lowercase();
1510 fonts.entry(key).or_insert_with(|| path.clone());
1511 let no_spaces = s.replace(' ', "").to_lowercase();
1512 if no_spaces != s.to_lowercase() {
1513 fonts.entry(no_spaces).or_insert_with(|| path.clone());
1514 }
1515 }
1516 }
1517 }
1518 }
1519 }
1520 }
1521 }
1522 fonts
1523}
1524
1525fn scan_font_cache_dir() -> HashMap<String, PathBuf> {
1527 match std::env::var("XFA_FONT_CACHE") {
1528 Ok(dir) => {
1529 let path = PathBuf::from(&dir);
1530 if path.is_dir() {
1531 scan_font_dir(&path)
1532 } else {
1533 HashMap::new()
1534 }
1535 }
1536 Err(_) => HashMap::new(),
1537 }
1538}
1539
1540#[cfg(test)]
1541mod tests {
1542 use super::*;
1543
1544 #[test]
1545 fn font_spec_parsing() {
1546 let spec = XfaFontSpec::from_xfa_attrs("Helvetica", Some("bold"), None, Some("12pt"), None);
1547 assert_eq!(spec.typeface, "Helvetica");
1548 assert_eq!(spec.weight, FontWeight::Bold);
1549 assert_eq!(spec.posture, FontPosture::Normal);
1550 assert!((spec.size_pt - 12.0).abs() < 0.001);
1551 assert_eq!(spec.generic_family, None);
1552 }
1553
1554 #[test]
1555 fn font_spec_defaults() {
1556 let spec = XfaFontSpec::from_xfa_attrs("Arial", None, None, None, None);
1557 assert_eq!(spec.weight, FontWeight::Normal);
1558 assert_eq!(spec.posture, FontPosture::Normal);
1559 assert!((spec.size_pt - 10.0).abs() < 0.001);
1560 }
1561
1562 #[test]
1563 fn font_spec_generic_family() {
1564 let spec = XfaFontSpec::from_xfa_attrs("FancyFont", None, None, None, Some("serif"));
1565 assert_eq!(spec.generic_family, Some(GenericFamily::Serif));
1566
1567 let spec = XfaFontSpec::from_xfa_attrs("FancyFont", None, None, None, Some("sansSerif"));
1568 assert_eq!(spec.generic_family, Some(GenericFamily::SansSerif));
1569
1570 let spec = XfaFontSpec::from_xfa_attrs("FancyFont", None, None, None, Some("monospaced"));
1571 assert_eq!(spec.generic_family, Some(GenericFamily::Monospaced));
1572
1573 let spec = XfaFontSpec::from_xfa_attrs("FancyFont", None, None, None, Some("bogus"));
1574 assert_eq!(spec.generic_family, None);
1575 }
1576
1577 #[test]
1578 fn resolver_empty() {
1579 let mut resolver = XfaFontResolver::new(vec![]);
1580 let spec = XfaFontSpec::from_xfa_attrs("NonExistentFont12345", None, None, None, None);
1581 let _ = resolver.resolve(&spec);
1582 }
1583
1584 #[test]
1585 fn system_font_dirs_not_empty() {
1586 let dirs = system_font_dirs();
1587 assert!(!dirs.is_empty());
1588 }
1589
1590 #[test]
1591 fn cid_font_info_with_system_font() {
1592 let mut resolver = XfaFontResolver::new(vec![]);
1594 let spec = XfaFontSpec::from_xfa_attrs("Helvetica", None, None, None, None);
1595 if let Ok(font) = resolver.resolve(&spec) {
1596 let info = font.cid_font_info();
1597 assert!(
1598 info.is_some(),
1599 "cid_font_info should succeed for a valid font"
1600 );
1601 let info = info.unwrap();
1602 assert!(!info.widths.is_empty(), "widths should not be empty");
1603 assert!(
1604 !info.gid_to_unicode.is_empty(),
1605 "gid_to_unicode should not be empty"
1606 );
1607 let has_a = info.gid_to_unicode.iter().any(|&(_, ch)| ch == 'A');
1609 assert!(has_a, "font should have a mapping for 'A'");
1610 }
1611 }
1612
1613 #[test]
1614 fn normalize_font_name_strips_subset_prefix() {
1615 assert_eq!(normalize_font_name("ABCDEF+Arial"), "arial");
1616 assert_eq!(
1617 normalize_font_name("XYZABC+TimesNewRomanPSMT"),
1618 "timesnewroman"
1619 );
1620 }
1621
1622 #[test]
1623 fn normalize_font_name_strips_ps_suffixes() {
1624 assert_eq!(normalize_font_name("ArialMT"), "arial");
1625 assert_eq!(normalize_font_name("TimesNewRomanPSMT"), "timesnewroman");
1626 assert_eq!(normalize_font_name("CourierNewPSMT"), "couriernew");
1627 }
1628
1629 #[test]
1630 fn normalize_font_name_preserves_normal_names() {
1631 assert_eq!(normalize_font_name("Helvetica"), "helvetica");
1632 assert_eq!(normalize_font_name("DejaVuSans"), "dejavusans");
1633 }
1634
1635 #[test]
1636 fn normalize_font_name_no_false_prefix_strip() {
1637 assert_eq!(normalize_font_name("abcdef+Arial"), "abcdef+arial");
1639 assert_eq!(normalize_font_name("AB+Arial"), "ab+arial");
1641 }
1642
1643 #[test]
1644 fn font_family_aliases_known_fonts() {
1645 assert!(!font_family_aliases("arial").is_empty());
1646 assert!(!font_family_aliases("helvetica").is_empty());
1647 assert!(!font_family_aliases("courier new").is_empty());
1648 assert!(!font_family_aliases("times new roman").is_empty());
1649 assert!(!font_family_aliases("myriad pro").is_empty());
1650 }
1651
1652 #[test]
1653 fn font_family_aliases_unknown_font() {
1654 assert!(font_family_aliases("some_unknown_font_xyz").is_empty());
1655 }
1656
1657 #[test]
1658 fn classify_font_family_sans() {
1659 assert_eq!(classify_font_family("Arial"), FontFamily::SansSerif);
1660 assert_eq!(classify_font_family("Helvetica"), FontFamily::SansSerif);
1661 assert_eq!(classify_font_family("DejaVuSans"), FontFamily::SansSerif);
1662 assert_eq!(
1663 classify_font_family("LiberationSans"),
1664 FontFamily::SansSerif
1665 );
1666 }
1667
1668 #[test]
1669 fn classify_font_family_serif() {
1670 assert_eq!(classify_font_family("Times New Roman"), FontFamily::Serif);
1671 assert_eq!(classify_font_family("Georgia"), FontFamily::Serif);
1672 assert_eq!(classify_font_family("LiberationSerif"), FontFamily::Serif);
1673 }
1674
1675 #[test]
1676 fn classify_font_family_mono() {
1677 assert_eq!(classify_font_family("Courier New"), FontFamily::Monospace);
1678 assert_eq!(
1679 classify_font_family("LiberationMono"),
1680 FontFamily::Monospace
1681 );
1682 assert_eq!(classify_font_family("Consolas"), FontFamily::Monospace);
1683 }
1684
1685 #[test]
1686 fn classify_font_family_unknown() {
1687 assert_eq!(classify_font_family("FancyFont"), FontFamily::Unknown);
1688 }
1689
1690 #[test]
1691 fn scan_system_fonts_has_name_table_entries() {
1692 let fonts = scan_system_fonts();
1693 assert!(!fonts.is_empty(), "system fonts map should not be empty");
1696 }
1697
1698 #[test]
1699 fn build_variant_names_normal() {
1700 let names = build_variant_names("Arial", FontWeight::Normal, FontPosture::Normal);
1701 assert!(names.is_empty(), "normal/normal should produce no variants");
1702 }
1703
1704 #[test]
1705 fn build_variant_names_bold() {
1706 let names = build_variant_names("Arial", FontWeight::Bold, FontPosture::Normal);
1707 assert!(names.contains(&"Arial-Bold".to_string()));
1708 assert!(names.contains(&"ArialBold".to_string()));
1709 assert!(names.contains(&"Arial Bold".to_string()));
1710 assert!(names.contains(&"Arial,Bold".to_string()));
1711 }
1712
1713 #[test]
1714 fn build_variant_names_italic() {
1715 let names = build_variant_names("Helvetica", FontWeight::Normal, FontPosture::Italic);
1716 assert!(names.contains(&"Helvetica-Italic".to_string()));
1717 assert!(names.contains(&"HelveticaItalic".to_string()));
1718 assert!(names.contains(&"Helvetica Italic".to_string()));
1719 }
1720
1721 #[test]
1722 fn build_variant_names_bold_italic() {
1723 let names = build_variant_names("Arial", FontWeight::Bold, FontPosture::Italic);
1724 assert!(names.contains(&"Arial-BoldItalic".to_string()));
1725 assert!(names.contains(&"ArialBoldItalic".to_string()));
1726 assert!(names.contains(&"Arial BoldItalic".to_string()));
1727 assert!(names.contains(&"Arial-Bold Italic".to_string()));
1728 assert!(names.contains(&"Arial Bold Italic".to_string()));
1729 }
1730
1731 #[test]
1732 fn font_variant_key_encoding() {
1733 assert_eq!(font_variant_key("Arial", None, None), "Arial_Normal_Normal");
1734 assert_eq!(
1735 font_variant_key("Arial", Some("bold"), None),
1736 "Arial_Bold_Normal"
1737 );
1738 assert_eq!(
1739 font_variant_key("Arial", None, Some("italic")),
1740 "Arial_Normal_Italic"
1741 );
1742 assert_eq!(
1743 font_variant_key("Arial", Some("bold"), Some("italic")),
1744 "Arial_Bold_Italic"
1745 );
1746 }
1747
1748 #[test]
1749 fn resolve_uses_bold_variant_cache_key() {
1750 let mut resolver = XfaFontResolver::new(vec![]);
1751 let spec_normal = XfaFontSpec::from_xfa_attrs("Arial", None, None, None, None);
1752 let spec_bold = XfaFontSpec::from_xfa_attrs("Arial", Some("bold"), None, None, None);
1753 let _ = resolver.resolve(&spec_normal);
1755 let _ = resolver.resolve(&spec_bold);
1756 }
1757
1758 #[test]
1759 fn resolver_preserves_pdf_widths_when_embedded_font_data_is_unparseable() {
1760 let embedded = vec![EmbeddedFontData {
1761 name: "Helvetica".to_string(),
1762 data: vec![0_u8, 1, 2, 3],
1763 pdf_widths: Some((32, vec![278, 333, 444])),
1764 pdf_encoding: Some(PdfSimpleEncoding {
1765 base_encoding: PdfBaseEncoding::WinAnsi,
1766 differences: vec![(32, 0x0020)],
1767 }),
1768 pdf_source_font: None,
1769 }];
1770 let mut resolver = XfaFontResolver::new(embedded);
1771 let spec = XfaFontSpec::from_xfa_attrs("Helvetica", None, None, None, None);
1772 let resolved = resolver
1773 .resolve(&spec)
1774 .expect("resolver should fall back to a system font");
1775 assert_eq!(resolved.pdf_widths, Some((32, vec![278, 333, 444])));
1776 assert_eq!(
1777 resolved.pdf_encoding,
1778 Some(PdfSimpleEncoding {
1779 base_encoding: PdfBaseEncoding::WinAnsi,
1780 differences: vec![(32, 0x0020)],
1781 })
1782 );
1783 }
1784
1785 #[test]
1786 fn canonical_font_key_collapses_variant_separators() {
1787 let keys: Vec<_> = ["Arial-Bold", "Arial,Bold", "Arial Bold", "ArialBold"]
1788 .iter()
1789 .map(|n| canonical_font_key(n).unwrap())
1790 .collect();
1791 assert!(keys.iter().all(|k| k == "arialbold"), "got {:?}", keys);
1793 }
1794
1795 #[test]
1796 fn canonical_font_key_strips_mt_and_subset_prefix() {
1797 assert_eq!(
1798 canonical_font_key("Arial-BoldMT").as_deref(),
1799 Some("arialbold")
1800 );
1801 assert_eq!(
1802 canonical_font_key("ABCDEF+Arial-Bold").as_deref(),
1803 Some("arialbold")
1804 );
1805 assert_eq!(
1806 canonical_font_key("TimesNewRomanPSMT").as_deref(),
1807 Some("timesnewroman")
1808 );
1809 assert_eq!(canonical_font_key("").as_deref(), None);
1810 assert_eq!(canonical_font_key("-,_").as_deref(), None);
1811 }
1812
1813 #[test]
1814 fn lookup_pdf_widths_matches_across_separator_variants() {
1815 let mut map = HashMap::new();
1816 let widths = (32_u16, vec![278_u16, 333, 611]);
1817 remember_pdf_widths(&mut map, "Arial,Bold", &widths, None, None);
1819
1820 for probe in &[
1822 "Arial,Bold",
1823 "Arial-Bold",
1824 "ArialBold",
1825 "Arial Bold",
1826 "Arial-BoldMT",
1827 "ABCDEF+Arial-Bold",
1828 ] {
1829 assert!(
1830 lookup_pdf_widths(&map, probe).is_some(),
1831 "lookup should resolve '{probe}' via the canonical key"
1832 );
1833 }
1834 }
1835
1836 #[test]
1837 fn lookup_pdf_widths_substring_fallback() {
1838 let mut map = HashMap::new();
1839 let widths = (32_u16, vec![500_u16; 3]);
1840 remember_pdf_widths(&mut map, "Helvetica", &widths, None, None);
1842
1843 assert!(lookup_pdf_widths(&map, "HelveticaNeue").is_some());
1845 }
1846
1847 #[test]
1848 fn attach_pdf_widths_binds_arial_bold_mt_across_separator_variants() {
1849 let widths = (32_u16, vec![278_u16, 333, 611]);
1853 let embedded = vec![EmbeddedFontData {
1854 name: "Arial,Bold".to_string(),
1855 data: Vec::new(),
1856 pdf_widths: Some(widths.clone()),
1857 pdf_encoding: None,
1858 pdf_source_font: Some(PdfSourceFont { object_id: (7, 0) }),
1859 }];
1860 let mut resolver = XfaFontResolver::new(embedded);
1861 let spec = XfaFontSpec::from_xfa_attrs("Arial-BoldMT", Some("bold"), None, None, None);
1862 let resolved = resolver
1863 .resolve(&spec)
1864 .expect("resolver should bind /Widths for the variant spelling");
1865 assert_eq!(resolved.pdf_widths, Some(widths));
1866 assert_eq!(
1867 resolved.pdf_source_font,
1868 Some(PdfSourceFont { object_id: (7, 0) })
1869 );
1870 }
1871
1872 #[test]
1873 fn style_matched_pdf_source_binds_same_style_dr_object() {
1874 let mk = |name: &str, obj: u32| EmbeddedFontData {
1880 name: name.to_string(),
1881 data: Vec::new(),
1882 pdf_widths: Some((32_u16, vec![278_u16, 333, 420])),
1883 pdf_encoding: None,
1884 pdf_source_font: Some(PdfSourceFont {
1885 object_id: (obj, 0),
1886 }),
1887 };
1888 let resolver = XfaFontResolver::new(vec![
1889 mk("TimesNewRomanPSMT", 5),
1890 mk("TimesNewRomanPS-ItalicMT", 8),
1891 mk("TimesNewRomanPS-BoldMT", 9),
1892 mk("TimesNewRomanPS-BoldItalicMT", 10),
1893 ]);
1894 let src = |w: Option<&str>, p: Option<&str>| {
1895 resolver
1896 .style_matched_pdf_source(&XfaFontSpec::from_xfa_attrs(
1897 "Times New Roman",
1898 w,
1899 p,
1900 None,
1901 None,
1902 ))
1903 .and_then(|d| d.source_font)
1904 };
1905 assert_eq!(
1906 src(None, Some("italic")),
1907 Some(PdfSourceFont { object_id: (8, 0) }),
1908 "italic spec must bind the italic /DR object"
1909 );
1910 assert_eq!(
1911 src(Some("bold"), None),
1912 Some(PdfSourceFont { object_id: (9, 0) }),
1913 "bold spec must bind the bold /DR object"
1914 );
1915 assert_eq!(
1916 src(Some("bold"), Some("italic")),
1917 Some(PdfSourceFont { object_id: (10, 0) }),
1918 "bold-italic spec must bind the bold-italic /DR object"
1919 );
1920 assert!(
1921 resolver
1922 .style_matched_pdf_source(&XfaFontSpec::from_xfa_attrs(
1923 "Times New Roman",
1924 None,
1925 None,
1926 None,
1927 None
1928 ))
1929 .is_none(),
1930 "normal/normal must not be style-matched (regular path is untouched)"
1931 );
1932 }
1933
1934 #[test]
1941 #[cfg_attr(target_os = "linux", ignore)]
1942 fn resolver_prefers_reusable_pdf_font_over_system_fallback() {
1943 let embedded = vec![EmbeddedFontData {
1944 name: "Myriad Pro".to_string(),
1945 data: Vec::new(),
1946 pdf_widths: Some((32, vec![278, 333, 612])),
1947 pdf_encoding: None,
1948 pdf_source_font: Some(PdfSourceFont { object_id: (42, 0) }),
1949 }];
1950 let mut resolver = XfaFontResolver::new(embedded);
1951 let spec = XfaFontSpec::from_xfa_attrs("Myriad Pro", None, None, None, None);
1952 let resolved = resolver
1953 .resolve(&spec)
1954 .expect("resolver should reuse the original PDF font object");
1955
1956 assert_eq!(resolved.pdf_widths, Some((32, vec![278, 333, 612])));
1957 assert_eq!(
1958 resolved.pdf_source_font,
1959 Some(PdfSourceFont { object_id: (42, 0) })
1960 );
1961 }
1965
1966 #[test]
1967 fn scan_font_dir_empty_dir() {
1968 let dir = std::env::temp_dir().join("xfa_font_cache_test_empty");
1969 let _ = std::fs::create_dir_all(&dir);
1970 let fonts = scan_font_dir(&dir);
1971 assert!(fonts.len() < 10000);
1973 let _ = std::fs::remove_dir_all(&dir);
1974 }
1975
1976 #[test]
1977 fn scan_font_dir_nonexistent() {
1978 let fonts = scan_font_dir(std::path::Path::new("/nonexistent/font/cache/dir"));
1979 assert!(fonts.is_empty());
1980 }
1981
1982 #[test]
1983 fn scan_font_cache_dir_unset() {
1984 std::env::remove_var("XFA_FONT_CACHE");
1986 let fonts = scan_font_cache_dir();
1987 assert!(fonts.is_empty());
1988 }
1989
1990 #[test]
1991 fn resolver_with_font_cache_dir() {
1992 let dir = std::env::temp_dir().join("xfa_font_cache_test_resolver");
1993 let _ = std::fs::create_dir_all(&dir);
1994 let resolver = XfaFontResolver::new(vec![]).with_font_cache(&dir);
1995 assert!(resolver.font_cache_fonts.len() < 10000);
1997 let _ = std::fs::remove_dir_all(&dir);
1998 }
1999
2000 #[test]
2001 fn font_cache_resolves_before_system_fallback() {
2002 let dir = std::env::temp_dir().join("xfa_font_cache_test_resolve_order");
2004 let _ = std::fs::remove_dir_all(&dir);
2005 std::fs::create_dir_all(&dir).unwrap();
2006
2007 let source = {
2010 #[cfg(target_os = "macos")]
2011 {
2012 PathBuf::from("/System/Library/Fonts/Helvetica.ttc")
2013 }
2014 #[cfg(not(target_os = "macos"))]
2015 {
2016 PathBuf::from("/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf")
2017 }
2018 };
2019 if !source.exists() {
2020 let _ = std::fs::remove_dir_all(&dir);
2022 return;
2023 }
2024 let dest = dir.join("TestCacheFont.ttf");
2025 std::fs::copy(&source, &dest).unwrap();
2026
2027 let resolver = XfaFontResolver::new(vec![]).with_font_cache(&dir);
2028 assert!(
2030 resolver.font_cache_fonts.contains_key("testcachefont"),
2031 "font cache should contain the test font by stem name"
2032 );
2033
2034 let _ = std::fs::remove_dir_all(&dir);
2035 }
2036}