1use zpdf_core::{ObjectId, PdfObject, Result};
2use zpdf_font::{CidWidths, FontCache, LoadedFont, PdfFontType};
3use zpdf_parser::PdfFile;
4
5use crate::page::PdfPage;
6
7pub fn load_page_fonts(file: &PdfFile, page: &PdfPage) -> FontCache {
9 let mut cache = FontCache::new();
10 let max_bytes = file.limits().max_font_cache_bytes;
11
12 for (name, &font_ref) in &page.resources.fonts {
13 match load_single_font(file, font_ref) {
14 Ok(font) => {
15 if cache
16 .try_insert_with_limit(name.clone(), font, max_bytes)
17 .is_none()
18 {
19 tracing::warn!(
20 "font cache byte limit ({max_bytes}) or ID capacity reached; using placeholder for {name}"
21 );
22 let _ = cache.try_insert_with_limit(
23 name.clone(),
24 LoadedFont::new_placeholder(name.clone()),
25 max_bytes,
26 );
27 }
28 }
29 Err(e) => {
30 tracing::debug!("font {name} ({font_ref}): fallback - {e}");
31 let _ = cache.try_insert_with_limit(
32 name.clone(),
33 LoadedFont::new_placeholder(name.clone()),
34 max_bytes,
35 );
36 }
37 }
38 }
39
40 cache
41}
42
43pub fn load_single_font(file: &PdfFile, font_ref: ObjectId) -> Result<LoadedFont> {
44 let obj = file.resolve(font_ref)?;
45 let dict = obj.as_dict()?;
46 load_single_font_dict(file, dict)
47}
48
49pub fn load_single_font_dict(file: &PdfFile, dict: &zpdf_core::PdfDict) -> Result<LoadedFont> {
53 let subtype = dict.get_name("Subtype").unwrap_or("");
54 let base_font = dict.get_name("BaseFont").unwrap_or("Unknown").to_string();
55
56 let mut font = match subtype {
57 "Type0" => load_type0_font(file, dict, base_font)?,
58 "TrueType" => load_truetype_font(file, dict, base_font)?,
59 "Type3" => load_type3_font(file, dict, base_font)?,
60 "Type1" | "MMType1" => load_type1_font(file, dict, base_font)?,
61 _ => LoadedFont::new_placeholder(base_font),
62 };
63
64 attach_text_mappings(file, dict, subtype, &mut font);
65 font.build_substitute_cid_to_gid();
68 apply_descriptor_variations(file, dict, &mut font);
70 Ok(font)
71}
72
73fn substitute_hints(
75 file: &PdfFile,
76 dict: &zpdf_core::PdfDict,
77) -> zpdf_font::system::SubstituteHints {
78 let mut hints = zpdf_font::system::SubstituteHints::default();
79 if let Some(fd) = resolve_dict(file, dict, "FontDescriptor") {
82 if let Ok(flags) = fd.get_i64("Flags") {
83 hints.fixed_pitch = flags & 1 != 0;
84 hints.serif = flags & 2 != 0;
85 hints.italic = flags & 64 != 0;
86 hints.bold = flags & (1 << 18) != 0; }
88 if let Ok(w) = fd.get_f64("StemV") {
89 hints.bold |= w >= 160.0;
90 }
91 }
92 hints
93}
94
95fn try_system_substitute_simple(
99 file: &PdfFile,
100 dict: &zpdf_core::PdfDict,
101 base_font: &str,
102 font_type: PdfFontType,
103 mut cid_widths: CidWidths,
104) -> Option<LoadedFont> {
105 let hints = substitute_hints(file, dict);
106 let m = zpdf_font::system::find_system_font(base_font, hints, None)?;
107 if cid_widths.is_empty() {
108 if let Some(metrics) = zpdf_font::standard_fonts::lookup(base_font) {
109 for (code, &w) in metrics.widths.iter().enumerate() {
110 if w > 0 {
111 cid_widths.set(code as u16, w as f64);
112 }
113 }
114 }
115 }
116 LoadedFont::new_substitute(
117 font_type,
118 base_font.to_string(),
119 m.data,
120 m.face_index,
121 cid_widths,
122 )
123}
124
125fn attach_text_mappings(
128 file: &PdfFile,
129 dict: &zpdf_core::PdfDict,
130 subtype: &str,
131 font: &mut LoadedFont,
132) {
133 if let Ok(tu_ref) = dict.get_ref("ToUnicode") {
135 if let Ok(data) = file.resolve_stream_data(tu_ref) {
136 let map = zpdf_font::cmap::ToUnicodeMap::parse(&data);
137 if !map.is_empty() {
138 font.to_unicode = Some(map);
139 }
140 }
141 }
142
143 if subtype == "Type0" {
145 return;
146 }
147
148 font.symbolic = font_descriptor_symbolic(file, dict);
149
150 let encoding = if dict.get("Encoding").is_none() {
151 builtin_symbol_encoding(&font.base_font)
154 .or_else(|| parse_encoding(file, dict, subtype, font.symbolic))
155 } else {
156 parse_encoding(file, dict, subtype, font.symbolic)
157 };
158 if let Some(enc) = encoding {
159 font.encoding = Some(enc);
160 }
161
162 font.map_unencoded_orphans();
165}
166
167fn builtin_symbol_encoding(base_font: &str) -> Option<zpdf_font::encoding::Encoding> {
171 use zpdf_font::encoding::{base_encoding_by_name, Encoding};
172 let name = base_font.rsplit('+').next().unwrap_or(base_font);
173 let canonical = if name.contains("ZapfDingbats") || name.contains("Dingbats") {
174 "ZapfDingbats"
175 } else if name.contains("Symbol") {
176 "Symbol"
177 } else {
178 return None;
179 };
180 base_encoding_by_name(canonical).map(Encoding::from_base)
181}
182
183fn font_descriptor_symbolic(file: &PdfFile, dict: &zpdf_core::PdfDict) -> bool {
186 let flags = resolve_dict(file, dict, "FontDescriptor").and_then(|d| d.get_i64("Flags").ok());
187 matches!(flags, Some(f) if (f & 4) != 0 && (f & 32) == 0)
188}
189
190fn font_descriptor_dict(file: &PdfFile, dict: &zpdf_core::PdfDict) -> Option<zpdf_core::PdfDict> {
194 let host = if dict.get_name("Subtype").unwrap_or("") == "Type0" {
195 let descendants = resolve_array(file, dict, "DescendantFonts")?;
196 descendant_cid_dict(file, &descendants)?
197 } else {
198 dict.clone()
199 };
200 resolve_dict(file, &host, "FontDescriptor")
201}
202
203fn descendant_cid_dict(file: &PdfFile, descendants: &[PdfObject]) -> Option<zpdf_core::PdfDict> {
207 match descendants.first()? {
208 PdfObject::Dict(d) => Some(d.clone()),
209 PdfObject::Ref(r) => file.resolve(*r).ok()?.as_dict().ok().cloned(),
210 _ => None,
211 }
212}
213
214fn font_stretch_pct(name: &str) -> Option<f64> {
216 Some(match name {
217 "UltraCondensed" => 50.0,
218 "ExtraCondensed" => 62.5,
219 "Condensed" => 75.0,
220 "SemiCondensed" => 87.5,
221 "Normal" => 100.0,
222 "SemiExpanded" => 112.5,
223 "Expanded" => 125.0,
224 "ExtraExpanded" => 150.0,
225 "UltraExpanded" => 200.0,
226 _ => return None,
227 })
228}
229
230fn apply_descriptor_variations(file: &PdfFile, dict: &zpdf_core::PdfDict, font: &mut LoadedFont) {
235 let Some(fd) = font_descriptor_dict(file, dict) else {
236 return;
237 };
238 let weight = fd.get_f64("FontWeight").ok();
239 let width_pct = fd.get_name("FontStretch").ok().and_then(font_stretch_pct);
240 let italic_angle = fd.get_f64("ItalicAngle").ok();
241 let italic = fd.get_i64("Flags").map(|f| f & 64 != 0).unwrap_or(false);
242 if weight.is_some() || width_pct.is_some() || italic_angle.is_some() || italic {
243 font.set_variations(weight, width_pct, italic_angle, italic);
244 }
245}
246
247fn parse_encoding(
250 file: &PdfFile,
251 dict: &zpdf_core::PdfDict,
252 subtype: &str,
253 symbolic: bool,
254) -> Option<zpdf_font::encoding::Encoding> {
255 use zpdf_font::encoding::{base_encoding_by_name, Encoding};
256
257 let enc_obj = match dict.get("Encoding").cloned() {
258 Some(PdfObject::Ref(r)) => file.resolve(r).ok(),
259 other => other,
260 };
261
262 match enc_obj {
263 Some(PdfObject::Name(n)) => base_encoding_by_name(n.as_str()).map(Encoding::from_base),
264 Some(PdfObject::Dict(enc_dict)) => {
265 let base = enc_dict
266 .get_name("BaseEncoding")
267 .ok()
268 .and_then(base_encoding_by_name)
269 .unwrap_or_else(|| default_simple_base(subtype));
270 let mut encoding = Encoding::from_base(base);
271 apply_differences(&enc_dict, &mut encoding);
272 Some(encoding)
273 }
274 _ if symbolic => None,
276 _ => Some(Encoding::from_base(default_simple_base(subtype))),
277 }
278}
279
280fn default_simple_base(subtype: &str) -> &'static zpdf_font::encoding::EncodingTable {
281 match subtype {
282 "TrueType" => &zpdf_font::encoding::WIN_ANSI_ENCODING,
283 _ => &zpdf_font::encoding::STANDARD_ENCODING,
284 }
285}
286
287fn apply_differences(enc_dict: &zpdf_core::PdfDict, encoding: &mut zpdf_font::encoding::Encoding) {
288 if let Ok(diffs) = enc_dict.get_array("Differences") {
289 let mut code = Some(0u16);
290 for obj in diffs {
291 match obj {
292 PdfObject::Integer(n) => code = u8::try_from(*n).ok().map(u16::from),
293 PdfObject::Name(name) => {
294 if let Some(current) = code {
295 encoding.apply_difference(current as u8, name.as_str());
296 code = current.checked_add(1).filter(|&next| next <= 255);
297 }
298 }
299 _ => {}
300 }
301 }
302 }
303}
304
305fn parse_type0_encoding(file: &PdfFile, dict: &zpdf_core::PdfDict) -> zpdf_font::cmap::CidCMap {
309 use zpdf_font::cmap::CidCMap;
310 fn identity_fallback(name: &str) -> CidCMap {
313 let wmode = name.ends_with("-V") as u8;
314 tracing::warn!(
315 "unsupported predefined CMap {name}; using Identity-{}",
316 if wmode == 1 { "V" } else { "H" }
317 );
318 CidCMap::identity(wmode)
319 }
320 match dict.get("Encoding") {
321 Some(PdfObject::Name(n)) => {
322 CidCMap::predefined(n.as_str()).unwrap_or_else(|| identity_fallback(n.as_str()))
323 }
324 Some(PdfObject::Ref(r)) => match file.resolve(*r) {
325 Ok(PdfObject::Name(n)) => {
326 CidCMap::predefined(n.as_str()).unwrap_or_else(|| identity_fallback(n.as_str()))
327 }
328 Ok(PdfObject::Stream(s)) => {
329 let data = file.resolve_stream_data(*r).or_else(|_| {
330 zpdf_parser::filters::decode_stream_with_limits(&s.data, &s.dict, file.limits())
331 });
332 let mut cmap = match data {
333 Ok(d) => CidCMap::parse(&d),
334 Err(e) => {
335 tracing::warn!("undecodable embedded CMap: {e}; using Identity-H");
336 CidCMap::identity(0)
337 }
338 };
339 if let Ok(1) = s.dict.get_i64("WMode") {
341 cmap.wmode = 1;
342 }
343 cmap
344 }
345 _ => CidCMap::identity(0),
346 },
347 _ => CidCMap::identity(0),
348 }
349}
350
351fn parse_dw2(file: &PdfFile, desc_dict: &zpdf_core::PdfDict) -> (f64, f64) {
353 resolve_array(file, desc_dict, "DW2")
354 .and_then(|arr| {
355 let v: Vec<f64> = arr.iter().filter_map(|o| o.as_f64().ok()).collect();
356 (v.len() >= 2).then(|| (v[0], v[1]))
357 })
358 .unwrap_or((880.0, -1000.0))
359}
360
361fn load_type0_font(
362 file: &PdfFile,
363 dict: &zpdf_core::PdfDict,
364 base_font: String,
365) -> Result<LoadedFont> {
366 let descendants = resolve_array(file, dict, "DescendantFonts")
368 .ok_or_else(|| zpdf_core::Error::MissingKey("DescendantFonts".into()))?;
369 let desc_dict = descendant_cid_dict(file, &descendants)
372 .ok_or_else(|| zpdf_core::Error::MissingKey("DescendantFonts[0]".into()))?;
373 let desc_dict = &desc_dict;
374
375 let mut cid_widths = parse_cid_widths(file, desc_dict);
376 parse_cid_w2(file, desc_dict, &mut cid_widths);
377 let cmap = parse_type0_encoding(file, dict);
378 let dw2 = parse_dw2(file, desc_dict);
379
380 let font_data = extract_font_file(file, desc_dict);
381
382 let mut font = match font_data {
383 Some(data) => {
384 let mut font = LoadedFont::new_with_data(
385 PdfFontType::Type0CidType2,
386 base_font.clone(),
387 data,
388 cid_widths.clone(),
389 );
390 if let Some(map) = parse_cid_to_gid_stream(file, desc_dict) {
395 let subtype = desc_dict.get_name("Subtype").unwrap_or("");
396 if subtype == "CIDFontType2" || font.cid_to_gid.is_none() {
397 font.cid_to_gid = Some(map);
398 }
399 }
400 let cjk = is_cjk_ordering(desc_ordering(file, desc_dict).as_deref())
407 || zpdf_font::system::cjk_ordering_for(&base_font).is_some();
408 if cjk && font.embedded_outline_failure_rate() > 0.5 {
409 if let Some(sub) = substitute_type0_font(file, desc_dict, &base_font, cid_widths) {
410 font = sub;
411 }
412 }
413 font
414 }
415 None => {
416 substitute_type0_font(file, desc_dict, &base_font, cid_widths)
420 .unwrap_or_else(|| LoadedFont::new_placeholder(base_font))
421 }
422 };
423 font.cid_cmap = Some(cmap);
424 font.dw2 = dw2;
425 font.validate_cid_cmap();
428 Ok(font)
429}
430
431fn desc_ordering(file: &PdfFile, desc_dict: &zpdf_core::PdfDict) -> Option<String> {
433 resolve_dict(file, desc_dict, "CIDSystemInfo").and_then(|csi| match csi.get("Ordering") {
434 Some(PdfObject::String(s)) => Some(s.to_string_lossy()),
435 Some(PdfObject::Name(n)) => Some(n.as_str().to_string()),
436 _ => None,
437 })
438}
439
440fn is_cjk_ordering(ordering: Option<&str>) -> bool {
442 matches!(ordering, Some("GB1" | "CNS1" | "Japan1" | "Korea1" | "KR"))
443}
444
445fn substitute_type0_font(
449 file: &PdfFile,
450 desc_dict: &zpdf_core::PdfDict,
451 base_font: &str,
452 cid_widths: CidWidths,
453) -> Option<LoadedFont> {
454 let ordering = desc_ordering(file, desc_dict);
455 let hints = substitute_hints(file, desc_dict);
456 zpdf_font::system::find_system_font(base_font, hints, ordering.as_deref()).and_then(|m| {
457 LoadedFont::new_substitute(
458 PdfFontType::Type0CidType2,
459 base_font.to_string(),
460 m.data,
461 m.face_index,
462 cid_widths,
463 )
464 })
465}
466
467fn parse_cid_to_gid_stream(
473 file: &PdfFile,
474 desc_dict: &zpdf_core::PdfDict,
475) -> Option<std::collections::HashMap<u16, u16>> {
476 let stream_ref = match desc_dict.get("CIDToGIDMap") {
477 Some(PdfObject::Ref(r)) => *r,
478 _ => return None,
480 };
481 let data = match file.resolve_stream_data(stream_ref) {
482 Ok(d) => d,
483 Err(e) => {
484 tracing::debug!("CIDToGIDMap {stream_ref}: not a decodable stream - {e}");
486 return None;
487 }
488 };
489 let mut map = std::collections::HashMap::new();
490 for (cid, gid_bytes) in data.chunks_exact(2).enumerate().take(u16::MAX as usize + 1) {
491 let gid = u16::from_be_bytes([gid_bytes[0], gid_bytes[1]]);
492 if gid != 0 {
493 map.insert(cid as u16, gid);
494 }
495 }
496 if map.is_empty() {
497 None
498 } else {
499 Some(map)
500 }
501}
502
503fn load_truetype_font(
504 file: &PdfFile,
505 dict: &zpdf_core::PdfDict,
506 base_font: String,
507) -> Result<LoadedFont> {
508 let cid_widths = parse_simple_widths(file, dict);
509 let font_data = extract_font_file_from_descriptor(file, dict);
510
511 match font_data {
512 Some(data) => Ok(LoadedFont::new_with_data(
513 PdfFontType::TrueType,
514 base_font,
515 data,
516 cid_widths,
517 )),
518 None => Ok(try_system_substitute_simple(
519 file,
520 dict,
521 &base_font,
522 PdfFontType::TrueType,
523 cid_widths,
524 )
525 .or_else(|| LoadedFont::new_standard(base_font.clone()))
526 .unwrap_or_else(|| LoadedFont::new_placeholder(base_font))),
527 }
528}
529
530fn type3_differences(diffs: &[PdfObject]) -> Vec<String> {
531 let mut encoding = Vec::new();
532 let mut current = Some(0usize);
533 for obj in diffs {
534 match obj {
535 PdfObject::Integer(n) => {
536 current = usize::try_from(*n).ok().filter(|&code| code <= 255);
537 }
538 PdfObject::Name(name) => {
539 let Some(code) = current else { continue };
540 encoding.resize(code + 1, String::new());
541 encoding[code] = name.0.clone();
542 current = code.checked_add(1).filter(|&next| next <= 255);
543 }
544 _ => {}
545 }
546 }
547 encoding
548}
549
550fn load_type3_font(
551 file: &PdfFile,
552 dict: &zpdf_core::PdfDict,
553 base_font: String,
554) -> Result<LoadedFont> {
555 use std::sync::Arc;
556
557 let font_matrix = {
562 let mut m = [0.001, 0.0, 0.0, -0.001, 0.0, 0.0];
563 if let Some(arr) = resolve_array(file, dict, "FontMatrix") {
564 for (i, obj) in arr.iter().enumerate().take(6) {
565 if let Ok(v) = obj.as_f64() {
566 m[i] = v;
567 }
568 }
569 }
570 m
571 };
572
573 let mut encoding = Vec::new();
575 if let Some(enc_dict) = resolve_dict(file, dict, "Encoding") {
576 if let Some(diffs) = resolve_array(file, &enc_dict, "Differences") {
577 encoding = type3_differences(&diffs);
578 }
579 }
580
581 let mut char_procs = std::collections::HashMap::new();
583 let encoded_names: std::collections::HashSet<&str> = encoding
584 .iter()
585 .map(String::as_str)
586 .filter(|name| !name.is_empty())
587 .collect();
588 if let Some(cp_dict) = resolve_dict(file, dict, "CharProcs") {
589 for (name, obj) in &cp_dict.0 {
590 if char_procs.len() >= encoded_names.len() {
591 break;
592 }
593 if !encoded_names.contains(name.as_str()) {
594 continue;
595 }
596 if let PdfObject::Ref(r) = obj {
597 if let Ok(data) = file.resolve_stream_data(*r) {
598 char_procs.insert(name.0.clone(), Arc::from(data));
599 }
600 }
601 }
602 }
603
604 let first_char = dict
606 .get_i64("FirstChar")
607 .ok()
608 .and_then(|n| u8::try_from(n).ok())
609 .map(u16::from)
610 .unwrap_or(0);
611 let widths: Vec<f64> = resolve_array(file, dict, "Widths")
612 .unwrap_or_default()
613 .iter()
614 .take(256)
615 .map(|o| o.as_f64().unwrap_or(0.0))
616 .collect();
617
618 let font = LoadedFont {
619 font_type: zpdf_font::PdfFontType::Type3 {
620 font_matrix,
621 char_procs,
622 encoding,
623 widths,
624 first_char,
625 },
626 base_font,
627 font_data: None,
628 face_index: 0,
629 is_substitute: false,
630 cid_widths: CidWidths::new(1000.0),
631 units_per_em: 1000.0,
632 ascent: 880.0,
633 descent: -120.0,
634 cid_to_gid: None,
635 builtin_encoding_gids: None,
636 orphan_gids: Vec::new(),
637 encoding: None,
638 to_unicode: None,
639 symbolic: false,
640 type1: None,
641 cid_cmap: None,
642 dw2: (880.0, -1000.0),
643 variations: Vec::new(),
644 };
645
646 Ok(font)
647}
648
649fn load_type1_font(
650 file: &PdfFile,
651 dict: &zpdf_core::PdfDict,
652 base_font: String,
653) -> Result<LoadedFont> {
654 let cid_widths = parse_simple_widths(file, dict);
655 let font_data = extract_font_file_from_descriptor(file, dict);
656
657 match font_data {
658 Some(data) => Ok(LoadedFont::new_with_data(
659 PdfFontType::Type1,
660 base_font,
661 data,
662 cid_widths,
663 )),
664 None => Ok(try_system_substitute_simple(
665 file,
666 dict,
667 &base_font,
668 PdfFontType::Type1,
669 cid_widths,
670 )
671 .or_else(|| LoadedFont::new_standard(base_font.clone()))
672 .unwrap_or_else(|| LoadedFont::new_placeholder(base_font))),
673 }
674}
675
676fn extract_font_file(file: &PdfFile, cid_dict: &zpdf_core::PdfDict) -> Option<Vec<u8>> {
678 let fd_dict = resolve_dict(file, cid_dict, "FontDescriptor")?;
679
680 for key in &["FontFile2", "FontFile3", "FontFile"] {
682 if let Ok(ff_ref) = fd_dict.get_ref(key) {
683 if let Ok(data) = file.resolve_stream_data(ff_ref) {
684 if !data.is_empty() {
685 return Some(data);
686 }
687 }
688 }
689 }
690 None
691}
692
693fn extract_font_file_from_descriptor(
694 file: &PdfFile,
695 font_dict: &zpdf_core::PdfDict,
696) -> Option<Vec<u8>> {
697 let fd_dict = resolve_dict(file, font_dict, "FontDescriptor")?;
698
699 for key in &["FontFile2", "FontFile3", "FontFile"] {
700 if let Ok(ff_ref) = fd_dict.get_ref(key) {
701 if let Ok(data) = file.resolve_stream_data(ff_ref) {
702 if !data.is_empty() {
703 return Some(data);
704 }
705 }
706 }
707 }
708 None
709}
710
711fn resolve_array(file: &PdfFile, dict: &zpdf_core::PdfDict, key: &str) -> Option<Vec<PdfObject>> {
715 match dict.get(key) {
716 Some(PdfObject::Array(a)) => Some(a.clone()),
717 Some(PdfObject::Ref(id)) => file
718 .resolve(*id)
719 .ok()
720 .and_then(|o| o.as_array().ok().map(|a| a.to_vec())),
721 _ => None,
722 }
723}
724
725fn resolve_dict(
729 file: &PdfFile,
730 dict: &zpdf_core::PdfDict,
731 key: &str,
732) -> Option<zpdf_core::PdfDict> {
733 match dict.get(key) {
734 Some(PdfObject::Dict(d)) => Some(d.clone()),
735 Some(PdfObject::Ref(id)) => file
736 .resolve(*id)
737 .ok()
738 .and_then(|o| o.as_dict().ok().cloned()),
739 _ => None,
740 }
741}
742
743fn parse_cid_widths(file: &PdfFile, dict: &zpdf_core::PdfDict) -> CidWidths {
745 let dw = dict.get_f64("DW").unwrap_or(1000.0);
746 let mut widths = CidWidths::new(dw);
747
748 let w_array = match resolve_array(file, dict, "W") {
749 Some(arr) => arr,
750 None => return widths,
751 };
752
753 let mut i = 0;
754 while i < w_array.len() {
755 let cid_start = match w_array[i].as_i64().ok().and_then(|v| u16::try_from(v).ok()) {
756 Some(v) => v,
757 None => break,
758 };
759 i += 1;
760 if i >= w_array.len() {
761 break;
762 }
763
764 let resolved = if let PdfObject::Ref(id) = &w_array[i] {
771 file.resolve(*id).ok()
772 } else {
773 None
774 };
775 let entry = resolved.as_ref().unwrap_or(&w_array[i]);
776
777 match entry {
778 PdfObject::Array(arr) => {
779 for (j, obj) in arr.iter().enumerate() {
781 let Some(cid) = u16::try_from(j)
782 .ok()
783 .and_then(|delta| cid_start.checked_add(delta))
784 else {
785 break;
786 };
787 if let Ok(w) = obj.as_f64() {
788 widths.set(cid, w);
789 }
790 }
791 i += 1;
792 }
793 PdfObject::Integer(_) | PdfObject::Real(_) => {
794 let cid_end = entry
796 .as_i64()
797 .ok()
798 .and_then(|v| u16::try_from(v).ok())
799 .unwrap_or(cid_start);
800 i += 1;
801 if i < w_array.len() {
802 let w = w_array[i].as_f64().unwrap_or(dw);
803 for cid in cid_start..=cid_end {
804 widths.set(cid, w);
805 }
806 i += 1;
807 }
808 }
809 _ => {
810 i += 1;
811 }
812 }
813 }
814
815 widths
816}
817
818fn parse_cid_w2(file: &PdfFile, dict: &zpdf_core::PdfDict, widths: &mut CidWidths) {
824 if let Some(arr) = resolve_array(file, dict, "W2") {
825 apply_w2_array(&arr, widths);
826 }
827}
828
829fn apply_w2_array(w2_array: &[PdfObject], widths: &mut CidWidths) {
830 let mut i = 0;
831 while i < w2_array.len() {
832 let cid_start = match w2_array[i]
833 .as_i64()
834 .ok()
835 .and_then(|v| u16::try_from(v).ok())
836 {
837 Some(v) => v,
838 None => break,
839 };
840 i += 1;
841 if i >= w2_array.len() {
842 break;
843 }
844
845 match &w2_array[i] {
846 PdfObject::Array(arr) => {
847 let mut k = 0;
849 while k + 2 < arr.len() {
850 let (Ok(w1y), Ok(vx), Ok(vy)) =
851 (arr[k].as_f64(), arr[k + 1].as_f64(), arr[k + 2].as_f64())
852 else {
853 break;
854 };
855 let Some(cid) = u16::try_from(k / 3)
856 .ok()
857 .and_then(|delta| cid_start.checked_add(delta))
858 else {
859 break;
860 };
861 widths.set_v(cid, w1y, vx, vy);
862 k += 3;
863 }
864 i += 1;
865 }
866 PdfObject::Integer(_) | PdfObject::Real(_) => {
867 let cid_end = w2_array[i]
869 .as_i64()
870 .ok()
871 .and_then(|v| u16::try_from(v).ok())
872 .unwrap_or(cid_start);
873 if i + 3 < w2_array.len() {
874 let (Ok(w1y), Ok(vx), Ok(vy)) = (
875 w2_array[i + 1].as_f64(),
876 w2_array[i + 2].as_f64(),
877 w2_array[i + 3].as_f64(),
878 ) else {
879 break;
880 };
881 for cid in cid_start..=cid_end {
882 widths.set_v(cid, w1y, vx, vy);
883 }
884 i += 4;
885 } else {
886 break;
887 }
888 }
889 _ => {
890 i += 1;
891 }
892 }
893 }
894}
895
896fn parse_simple_widths(file: &PdfFile, dict: &zpdf_core::PdfDict) -> CidWidths {
897 let first_char = dict
898 .get_i64("FirstChar")
899 .ok()
900 .and_then(|v| u8::try_from(v).ok())
901 .map(u16::from)
902 .unwrap_or(0);
903 let mut widths = CidWidths::new(1000.0);
904
905 if let Some(arr) = resolve_array(file, dict, "Widths") {
906 for (j, obj) in arr.iter().take(256).enumerate() {
907 let Some(code) = u16::try_from(j)
908 .ok()
909 .and_then(|delta| first_char.checked_add(delta))
910 .filter(|&code| code <= 255)
911 else {
912 break;
913 };
914 if let Ok(w) = obj.as_f64() {
915 widths.set(code, w);
916 }
917 }
918 }
919
920 widths
921}
922
923#[cfg(test)]
924mod tests {
925 use super::*;
926
927 fn int(v: i64) -> PdfObject {
928 PdfObject::Integer(v)
929 }
930 fn real(v: f64) -> PdfObject {
931 PdfObject::Real(v)
932 }
933
934 #[test]
935 fn w2_list_form_assigns_consecutive_cids() {
936 let arr = vec![
938 int(120),
939 PdfObject::Array(vec![
940 real(-1000.0),
941 real(500.0),
942 real(880.0),
943 int(-900),
944 int(450),
945 int(820),
946 ]),
947 ];
948 let mut w = CidWidths::new(1000.0);
949 apply_w2_array(&arr, &mut w);
950 assert_eq!(w.get_v(120), Some((-1000.0, 500.0, 880.0)));
951 assert_eq!(w.get_v(121), Some((-900.0, 450.0, 820.0)));
952 assert_eq!(w.get_v(122), None);
953 }
954
955 #[test]
956 fn w2_range_form_assigns_inclusive_range() {
957 let arr = vec![int(10), int(12), int(-1000), int(500), int(880)];
959 let mut w = CidWidths::new(1000.0);
960 apply_w2_array(&arr, &mut w);
961 for cid in 10..=12 {
962 assert_eq!(w.get_v(cid), Some((-1000.0, 500.0, 880.0)));
963 }
964 assert_eq!(w.get_v(9), None);
965 assert_eq!(w.get_v(13), None);
966 }
967
968 #[test]
969 fn w2_truncated_entry_is_ignored_not_panic() {
970 let arr = vec![int(10), int(12), int(-1000)];
972 let mut w = CidWidths::new(1000.0);
973 apply_w2_array(&arr, &mut w);
974 assert_eq!(w.get_v(10), None);
975 }
976
977 #[test]
978 fn type3_differences_reject_out_of_range_codes_without_growth() {
979 let name = |s: &str| PdfObject::Name(zpdf_core::PdfName::new(s));
980 let diffs = vec![
981 int(-1),
982 name("ignored-negative"),
983 int(i64::MAX),
984 name("ignored-huge"),
985 int(255),
986 name("last"),
987 name("past-end"),
988 ];
989 let encoding = type3_differences(&diffs);
990 assert_eq!(encoding.len(), 256);
991 assert_eq!(encoding[255], "last");
992 }
993
994 #[test]
995 fn simple_differences_do_not_overflow_after_huge_code() {
996 let mut dict = zpdf_core::PdfDict::new();
997 dict.insert(
998 zpdf_core::PdfName::new("Differences"),
999 PdfObject::Array(vec![
1000 int(i64::MAX),
1001 PdfObject::Name(zpdf_core::PdfName::new("ignored")),
1002 ]),
1003 );
1004 let mut encoding =
1005 zpdf_font::encoding::Encoding::from_base(&zpdf_font::encoding::STANDARD_ENCODING);
1006 apply_differences(&dict, &mut encoding);
1007 }
1008}