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 Ok(fd_ref) = dict.get_ref("FontDescriptor") {
80 if let Ok(fd) = file.resolve(fd_ref) {
81 if let Ok(fd) = fd.as_dict() {
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 }
93 }
94 hints
95}
96
97fn try_system_substitute_simple(
101 file: &PdfFile,
102 dict: &zpdf_core::PdfDict,
103 base_font: &str,
104 font_type: PdfFontType,
105 mut cid_widths: CidWidths,
106) -> Option<LoadedFont> {
107 let hints = substitute_hints(file, dict);
108 let m = zpdf_font::system::find_system_font(base_font, hints, None)?;
109 if cid_widths.is_empty() {
110 if let Some(metrics) = zpdf_font::standard_fonts::lookup(base_font) {
111 for (code, &w) in metrics.widths.iter().enumerate() {
112 if w > 0 {
113 cid_widths.set(code as u16, w as f64);
114 }
115 }
116 }
117 }
118 LoadedFont::new_substitute(
119 font_type,
120 base_font.to_string(),
121 m.data,
122 m.face_index,
123 cid_widths,
124 )
125}
126
127fn attach_text_mappings(
130 file: &PdfFile,
131 dict: &zpdf_core::PdfDict,
132 subtype: &str,
133 font: &mut LoadedFont,
134) {
135 if let Ok(tu_ref) = dict.get_ref("ToUnicode") {
137 if let Ok(data) = file.resolve_stream_data(tu_ref) {
138 let map = zpdf_font::cmap::ToUnicodeMap::parse(&data);
139 if !map.is_empty() {
140 font.to_unicode = Some(map);
141 }
142 }
143 }
144
145 if subtype == "Type0" {
147 return;
148 }
149
150 font.symbolic = font_descriptor_symbolic(file, dict);
151
152 let encoding = if dict.get("Encoding").is_none() {
153 builtin_symbol_encoding(&font.base_font)
156 .or_else(|| parse_encoding(file, dict, subtype, font.symbolic))
157 } else {
158 parse_encoding(file, dict, subtype, font.symbolic)
159 };
160 if let Some(enc) = encoding {
161 font.encoding = Some(enc);
162 }
163
164 font.map_unencoded_orphans();
167}
168
169fn builtin_symbol_encoding(base_font: &str) -> Option<zpdf_font::encoding::Encoding> {
173 use zpdf_font::encoding::{base_encoding_by_name, Encoding};
174 let name = base_font.rsplit('+').next().unwrap_or(base_font);
175 let canonical = if name.contains("ZapfDingbats") || name.contains("Dingbats") {
176 "ZapfDingbats"
177 } else if name.contains("Symbol") {
178 "Symbol"
179 } else {
180 return None;
181 };
182 base_encoding_by_name(canonical).map(Encoding::from_base)
183}
184
185fn font_descriptor_symbolic(file: &PdfFile, dict: &zpdf_core::PdfDict) -> bool {
188 let fd_ref = match dict.get_ref("FontDescriptor") {
189 Ok(r) => r,
190 Err(_) => return false,
191 };
192 let flags = file
193 .resolve(fd_ref)
194 .ok()
195 .and_then(|o| o.as_dict().ok().and_then(|d| d.get_i64("Flags").ok()));
196 matches!(flags, Some(f) if (f & 4) != 0 && (f & 32) == 0)
197}
198
199fn font_descriptor_dict(file: &PdfFile, dict: &zpdf_core::PdfDict) -> Option<zpdf_core::PdfDict> {
203 let host = if dict.get_name("Subtype").unwrap_or("") == "Type0" {
204 let descendants = resolve_array(file, dict, "DescendantFonts")?;
205 let desc_ref = descendants.first()?.as_ref().ok()?;
206 file.resolve(desc_ref).ok()?.as_dict().ok()?.clone()
207 } else {
208 dict.clone()
209 };
210 let fd_ref = host.get_ref("FontDescriptor").ok()?;
211 file.resolve(fd_ref).ok()?.as_dict().ok().cloned()
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_ref = descendants
370 .first()
371 .ok_or_else(|| zpdf_core::Error::MissingKey("DescendantFonts[0]".into()))?
372 .as_ref()?;
373
374 let desc_obj = file.resolve(desc_ref)?;
375 let desc_dict = desc_obj.as_dict()?;
376
377 let mut cid_widths = parse_cid_widths(file, desc_dict);
378 parse_cid_w2(file, desc_dict, &mut cid_widths);
379 let cmap = parse_type0_encoding(file, dict);
380 let dw2 = parse_dw2(file, desc_dict);
381
382 let font_data = extract_font_file(file, desc_dict);
383
384 let mut font = match font_data {
385 Some(data) => {
386 let mut font = LoadedFont::new_with_data(
387 PdfFontType::Type0CidType2,
388 base_font.clone(),
389 data,
390 cid_widths.clone(),
391 );
392 if let Some(map) = parse_cid_to_gid_stream(file, desc_dict) {
397 let subtype = desc_dict.get_name("Subtype").unwrap_or("");
398 if subtype == "CIDFontType2" || font.cid_to_gid.is_none() {
399 font.cid_to_gid = Some(map);
400 }
401 }
402 let cjk = is_cjk_ordering(desc_ordering(file, desc_dict).as_deref())
409 || zpdf_font::system::cjk_ordering_for(&base_font).is_some();
410 if cjk && font.embedded_outline_failure_rate() > 0.5 {
411 if let Some(sub) = substitute_type0_font(file, desc_dict, &base_font, cid_widths) {
412 font = sub;
413 }
414 }
415 font
416 }
417 None => {
418 substitute_type0_font(file, desc_dict, &base_font, cid_widths)
422 .unwrap_or_else(|| LoadedFont::new_placeholder(base_font))
423 }
424 };
425 font.cid_cmap = Some(cmap);
426 font.dw2 = dw2;
427 font.validate_cid_cmap();
430 Ok(font)
431}
432
433fn desc_ordering(file: &PdfFile, desc_dict: &zpdf_core::PdfDict) -> Option<String> {
435 resolve_dict(file, desc_dict, "CIDSystemInfo").and_then(|csi| match csi.get("Ordering") {
436 Some(PdfObject::String(s)) => Some(s.to_string_lossy()),
437 Some(PdfObject::Name(n)) => Some(n.as_str().to_string()),
438 _ => None,
439 })
440}
441
442fn is_cjk_ordering(ordering: Option<&str>) -> bool {
444 matches!(ordering, Some("GB1" | "CNS1" | "Japan1" | "Korea1" | "KR"))
445}
446
447fn substitute_type0_font(
451 file: &PdfFile,
452 desc_dict: &zpdf_core::PdfDict,
453 base_font: &str,
454 cid_widths: CidWidths,
455) -> Option<LoadedFont> {
456 let ordering = desc_ordering(file, desc_dict);
457 let hints = substitute_hints(file, desc_dict);
458 zpdf_font::system::find_system_font(base_font, hints, ordering.as_deref()).and_then(|m| {
459 LoadedFont::new_substitute(
460 PdfFontType::Type0CidType2,
461 base_font.to_string(),
462 m.data,
463 m.face_index,
464 cid_widths,
465 )
466 })
467}
468
469fn parse_cid_to_gid_stream(
475 file: &PdfFile,
476 desc_dict: &zpdf_core::PdfDict,
477) -> Option<std::collections::HashMap<u16, u16>> {
478 let stream_ref = match desc_dict.get("CIDToGIDMap") {
479 Some(PdfObject::Ref(r)) => *r,
480 _ => return None,
482 };
483 let data = match file.resolve_stream_data(stream_ref) {
484 Ok(d) => d,
485 Err(e) => {
486 tracing::debug!("CIDToGIDMap {stream_ref}: not a decodable stream - {e}");
488 return None;
489 }
490 };
491 let mut map = std::collections::HashMap::new();
492 for (cid, gid_bytes) in data.chunks_exact(2).enumerate().take(u16::MAX as usize + 1) {
493 let gid = u16::from_be_bytes([gid_bytes[0], gid_bytes[1]]);
494 if gid != 0 {
495 map.insert(cid as u16, gid);
496 }
497 }
498 if map.is_empty() {
499 None
500 } else {
501 Some(map)
502 }
503}
504
505fn load_truetype_font(
506 file: &PdfFile,
507 dict: &zpdf_core::PdfDict,
508 base_font: String,
509) -> Result<LoadedFont> {
510 let cid_widths = parse_simple_widths(file, dict);
511 let font_data = extract_font_file_from_descriptor(file, dict);
512
513 match font_data {
514 Some(data) => Ok(LoadedFont::new_with_data(
515 PdfFontType::TrueType,
516 base_font,
517 data,
518 cid_widths,
519 )),
520 None => Ok(try_system_substitute_simple(
521 file,
522 dict,
523 &base_font,
524 PdfFontType::TrueType,
525 cid_widths,
526 )
527 .or_else(|| LoadedFont::new_standard(base_font.clone()))
528 .unwrap_or_else(|| LoadedFont::new_placeholder(base_font))),
529 }
530}
531
532fn type3_differences(diffs: &[PdfObject]) -> Vec<String> {
533 let mut encoding = Vec::new();
534 let mut current = Some(0usize);
535 for obj in diffs {
536 match obj {
537 PdfObject::Integer(n) => {
538 current = usize::try_from(*n).ok().filter(|&code| code <= 255);
539 }
540 PdfObject::Name(name) => {
541 let Some(code) = current else { continue };
542 encoding.resize(code + 1, String::new());
543 encoding[code] = name.0.clone();
544 current = code.checked_add(1).filter(|&next| next <= 255);
545 }
546 _ => {}
547 }
548 }
549 encoding
550}
551
552fn load_type3_font(
553 file: &PdfFile,
554 dict: &zpdf_core::PdfDict,
555 base_font: String,
556) -> Result<LoadedFont> {
557 use std::sync::Arc;
558
559 let font_matrix = {
564 let mut m = [0.001, 0.0, 0.0, -0.001, 0.0, 0.0];
565 if let Some(arr) = resolve_array(file, dict, "FontMatrix") {
566 for (i, obj) in arr.iter().enumerate().take(6) {
567 if let Ok(v) = obj.as_f64() {
568 m[i] = v;
569 }
570 }
571 }
572 m
573 };
574
575 let mut encoding = Vec::new();
577 if let Some(enc_dict) = resolve_dict(file, dict, "Encoding") {
578 if let Some(diffs) = resolve_array(file, &enc_dict, "Differences") {
579 encoding = type3_differences(&diffs);
580 }
581 }
582
583 let mut char_procs = std::collections::HashMap::new();
585 let encoded_names: std::collections::HashSet<&str> = encoding
586 .iter()
587 .map(String::as_str)
588 .filter(|name| !name.is_empty())
589 .collect();
590 if let Some(cp_dict) = resolve_dict(file, dict, "CharProcs") {
591 for (name, obj) in &cp_dict.0 {
592 if char_procs.len() >= encoded_names.len() {
593 break;
594 }
595 if !encoded_names.contains(name.as_str()) {
596 continue;
597 }
598 if let PdfObject::Ref(r) = obj {
599 if let Ok(data) = file.resolve_stream_data(*r) {
600 char_procs.insert(name.0.clone(), Arc::from(data));
601 }
602 }
603 }
604 }
605
606 let first_char = dict
608 .get_i64("FirstChar")
609 .ok()
610 .and_then(|n| u8::try_from(n).ok())
611 .map(u16::from)
612 .unwrap_or(0);
613 let widths: Vec<f64> = resolve_array(file, dict, "Widths")
614 .unwrap_or_default()
615 .iter()
616 .take(256)
617 .map(|o| o.as_f64().unwrap_or(0.0))
618 .collect();
619
620 let font = LoadedFont {
621 font_type: zpdf_font::PdfFontType::Type3 {
622 font_matrix,
623 char_procs,
624 encoding,
625 widths,
626 first_char,
627 },
628 base_font,
629 font_data: None,
630 face_index: 0,
631 is_substitute: false,
632 cid_widths: CidWidths::new(1000.0),
633 units_per_em: 1000.0,
634 ascent: 880.0,
635 descent: -120.0,
636 cid_to_gid: None,
637 builtin_encoding_gids: None,
638 orphan_gids: Vec::new(),
639 encoding: None,
640 to_unicode: None,
641 symbolic: false,
642 type1: None,
643 cid_cmap: None,
644 dw2: (880.0, -1000.0),
645 variations: Vec::new(),
646 };
647
648 Ok(font)
649}
650
651fn load_type1_font(
652 file: &PdfFile,
653 dict: &zpdf_core::PdfDict,
654 base_font: String,
655) -> Result<LoadedFont> {
656 let cid_widths = parse_simple_widths(file, dict);
657 let font_data = extract_font_file_from_descriptor(file, dict);
658
659 match font_data {
660 Some(data) => Ok(LoadedFont::new_with_data(
661 PdfFontType::Type1,
662 base_font,
663 data,
664 cid_widths,
665 )),
666 None => Ok(try_system_substitute_simple(
667 file,
668 dict,
669 &base_font,
670 PdfFontType::Type1,
671 cid_widths,
672 )
673 .or_else(|| LoadedFont::new_standard(base_font.clone()))
674 .unwrap_or_else(|| LoadedFont::new_placeholder(base_font))),
675 }
676}
677
678fn extract_font_file(file: &PdfFile, cid_dict: &zpdf_core::PdfDict) -> Option<Vec<u8>> {
680 let fd_ref = cid_dict.get_ref("FontDescriptor").ok()?;
681 let fd_obj = file.resolve(fd_ref).ok()?;
682 let fd_dict = fd_obj.as_dict().ok()?;
683
684 for key in &["FontFile2", "FontFile3", "FontFile"] {
686 if let Ok(ff_ref) = fd_dict.get_ref(key) {
687 if let Ok(data) = file.resolve_stream_data(ff_ref) {
688 if !data.is_empty() {
689 return Some(data);
690 }
691 }
692 }
693 }
694 None
695}
696
697fn extract_font_file_from_descriptor(
698 file: &PdfFile,
699 font_dict: &zpdf_core::PdfDict,
700) -> Option<Vec<u8>> {
701 let fd_ref = font_dict.get_ref("FontDescriptor").ok()?;
702 let fd_obj = file.resolve(fd_ref).ok()?;
703 let fd_dict = fd_obj.as_dict().ok()?;
704
705 for key in &["FontFile2", "FontFile3", "FontFile"] {
706 if let Ok(ff_ref) = fd_dict.get_ref(key) {
707 if let Ok(data) = file.resolve_stream_data(ff_ref) {
708 if !data.is_empty() {
709 return Some(data);
710 }
711 }
712 }
713 }
714 None
715}
716
717fn resolve_array(file: &PdfFile, dict: &zpdf_core::PdfDict, key: &str) -> Option<Vec<PdfObject>> {
721 match dict.get(key) {
722 Some(PdfObject::Array(a)) => Some(a.clone()),
723 Some(PdfObject::Ref(id)) => file
724 .resolve(*id)
725 .ok()
726 .and_then(|o| o.as_array().ok().map(|a| a.to_vec())),
727 _ => None,
728 }
729}
730
731fn resolve_dict(
735 file: &PdfFile,
736 dict: &zpdf_core::PdfDict,
737 key: &str,
738) -> Option<zpdf_core::PdfDict> {
739 match dict.get(key) {
740 Some(PdfObject::Dict(d)) => Some(d.clone()),
741 Some(PdfObject::Ref(id)) => file
742 .resolve(*id)
743 .ok()
744 .and_then(|o| o.as_dict().ok().cloned()),
745 _ => None,
746 }
747}
748
749fn parse_cid_widths(file: &PdfFile, dict: &zpdf_core::PdfDict) -> CidWidths {
751 let dw = dict.get_f64("DW").unwrap_or(1000.0);
752 let mut widths = CidWidths::new(dw);
753
754 let w_array = match resolve_array(file, dict, "W") {
755 Some(arr) => arr,
756 None => return widths,
757 };
758
759 let mut i = 0;
760 while i < w_array.len() {
761 let cid_start = match w_array[i].as_i64().ok().and_then(|v| u16::try_from(v).ok()) {
762 Some(v) => v,
763 None => break,
764 };
765 i += 1;
766 if i >= w_array.len() {
767 break;
768 }
769
770 match &w_array[i] {
771 PdfObject::Array(arr) => {
772 for (j, obj) in arr.iter().enumerate() {
774 let Some(cid) = u16::try_from(j)
775 .ok()
776 .and_then(|delta| cid_start.checked_add(delta))
777 else {
778 break;
779 };
780 if let Ok(w) = obj.as_f64() {
781 widths.set(cid, w);
782 }
783 }
784 i += 1;
785 }
786 PdfObject::Integer(_) | PdfObject::Real(_) => {
787 let cid_end = w_array[i]
789 .as_i64()
790 .ok()
791 .and_then(|v| u16::try_from(v).ok())
792 .unwrap_or(cid_start);
793 i += 1;
794 if i < w_array.len() {
795 let w = w_array[i].as_f64().unwrap_or(dw);
796 for cid in cid_start..=cid_end {
797 widths.set(cid, w);
798 }
799 i += 1;
800 }
801 }
802 _ => {
803 i += 1;
804 }
805 }
806 }
807
808 widths
809}
810
811fn parse_cid_w2(file: &PdfFile, dict: &zpdf_core::PdfDict, widths: &mut CidWidths) {
817 if let Some(arr) = resolve_array(file, dict, "W2") {
818 apply_w2_array(&arr, widths);
819 }
820}
821
822fn apply_w2_array(w2_array: &[PdfObject], widths: &mut CidWidths) {
823 let mut i = 0;
824 while i < w2_array.len() {
825 let cid_start = match w2_array[i]
826 .as_i64()
827 .ok()
828 .and_then(|v| u16::try_from(v).ok())
829 {
830 Some(v) => v,
831 None => break,
832 };
833 i += 1;
834 if i >= w2_array.len() {
835 break;
836 }
837
838 match &w2_array[i] {
839 PdfObject::Array(arr) => {
840 let mut k = 0;
842 while k + 2 < arr.len() {
843 let (Ok(w1y), Ok(vx), Ok(vy)) =
844 (arr[k].as_f64(), arr[k + 1].as_f64(), arr[k + 2].as_f64())
845 else {
846 break;
847 };
848 let Some(cid) = u16::try_from(k / 3)
849 .ok()
850 .and_then(|delta| cid_start.checked_add(delta))
851 else {
852 break;
853 };
854 widths.set_v(cid, w1y, vx, vy);
855 k += 3;
856 }
857 i += 1;
858 }
859 PdfObject::Integer(_) | PdfObject::Real(_) => {
860 let cid_end = w2_array[i]
862 .as_i64()
863 .ok()
864 .and_then(|v| u16::try_from(v).ok())
865 .unwrap_or(cid_start);
866 if i + 3 < w2_array.len() {
867 let (Ok(w1y), Ok(vx), Ok(vy)) = (
868 w2_array[i + 1].as_f64(),
869 w2_array[i + 2].as_f64(),
870 w2_array[i + 3].as_f64(),
871 ) else {
872 break;
873 };
874 for cid in cid_start..=cid_end {
875 widths.set_v(cid, w1y, vx, vy);
876 }
877 i += 4;
878 } else {
879 break;
880 }
881 }
882 _ => {
883 i += 1;
884 }
885 }
886 }
887}
888
889fn parse_simple_widths(file: &PdfFile, dict: &zpdf_core::PdfDict) -> CidWidths {
890 let first_char = dict
891 .get_i64("FirstChar")
892 .ok()
893 .and_then(|v| u8::try_from(v).ok())
894 .map(u16::from)
895 .unwrap_or(0);
896 let mut widths = CidWidths::new(1000.0);
897
898 if let Some(arr) = resolve_array(file, dict, "Widths") {
899 for (j, obj) in arr.iter().take(256).enumerate() {
900 let Some(code) = u16::try_from(j)
901 .ok()
902 .and_then(|delta| first_char.checked_add(delta))
903 .filter(|&code| code <= 255)
904 else {
905 break;
906 };
907 if let Ok(w) = obj.as_f64() {
908 widths.set(code, w);
909 }
910 }
911 }
912
913 widths
914}
915
916#[cfg(test)]
917mod tests {
918 use super::*;
919
920 fn int(v: i64) -> PdfObject {
921 PdfObject::Integer(v)
922 }
923 fn real(v: f64) -> PdfObject {
924 PdfObject::Real(v)
925 }
926
927 #[test]
928 fn w2_list_form_assigns_consecutive_cids() {
929 let arr = vec![
931 int(120),
932 PdfObject::Array(vec![
933 real(-1000.0),
934 real(500.0),
935 real(880.0),
936 int(-900),
937 int(450),
938 int(820),
939 ]),
940 ];
941 let mut w = CidWidths::new(1000.0);
942 apply_w2_array(&arr, &mut w);
943 assert_eq!(w.get_v(120), Some((-1000.0, 500.0, 880.0)));
944 assert_eq!(w.get_v(121), Some((-900.0, 450.0, 820.0)));
945 assert_eq!(w.get_v(122), None);
946 }
947
948 #[test]
949 fn w2_range_form_assigns_inclusive_range() {
950 let arr = vec![int(10), int(12), int(-1000), int(500), int(880)];
952 let mut w = CidWidths::new(1000.0);
953 apply_w2_array(&arr, &mut w);
954 for cid in 10..=12 {
955 assert_eq!(w.get_v(cid), Some((-1000.0, 500.0, 880.0)));
956 }
957 assert_eq!(w.get_v(9), None);
958 assert_eq!(w.get_v(13), None);
959 }
960
961 #[test]
962 fn w2_truncated_entry_is_ignored_not_panic() {
963 let arr = vec![int(10), int(12), int(-1000)];
965 let mut w = CidWidths::new(1000.0);
966 apply_w2_array(&arr, &mut w);
967 assert_eq!(w.get_v(10), None);
968 }
969
970 #[test]
971 fn type3_differences_reject_out_of_range_codes_without_growth() {
972 let name = |s: &str| PdfObject::Name(zpdf_core::PdfName::new(s));
973 let diffs = vec![
974 int(-1),
975 name("ignored-negative"),
976 int(i64::MAX),
977 name("ignored-huge"),
978 int(255),
979 name("last"),
980 name("past-end"),
981 ];
982 let encoding = type3_differences(&diffs);
983 assert_eq!(encoding.len(), 256);
984 assert_eq!(encoding[255], "last");
985 }
986
987 #[test]
988 fn simple_differences_do_not_overflow_after_huge_code() {
989 let mut dict = zpdf_core::PdfDict::new();
990 dict.insert(
991 zpdf_core::PdfName::new("Differences"),
992 PdfObject::Array(vec![
993 int(i64::MAX),
994 PdfObject::Name(zpdf_core::PdfName::new("ignored")),
995 ]),
996 );
997 let mut encoding =
998 zpdf_font::encoding::Encoding::from_base(&zpdf_font::encoding::STANDARD_ENCODING);
999 apply_differences(&dict, &mut encoding);
1000 }
1001}