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
11 for (name, &font_ref) in &page.resources.fonts {
12 match load_single_font(file, font_ref) {
13 Ok(font) => {
14 cache.insert(name.clone(), font);
15 }
16 Err(e) => {
17 tracing::debug!("font {name} ({font_ref}): fallback - {e}");
18 cache.insert(name.clone(), LoadedFont::new_placeholder(name.clone()));
19 }
20 }
21 }
22
23 cache
24}
25
26pub fn load_single_font(file: &PdfFile, font_ref: ObjectId) -> Result<LoadedFont> {
27 let obj = file.resolve(font_ref)?;
28 let dict = obj.as_dict()?;
29 load_single_font_dict(file, dict)
30}
31
32pub fn load_single_font_dict(file: &PdfFile, dict: &zpdf_core::PdfDict) -> Result<LoadedFont> {
36 let subtype = dict.get_name("Subtype").unwrap_or("");
37 let base_font = dict.get_name("BaseFont").unwrap_or("Unknown").to_string();
38
39 let mut font = match subtype {
40 "Type0" => load_type0_font(file, dict, base_font)?,
41 "TrueType" => load_truetype_font(file, dict, base_font)?,
42 "Type3" => load_type3_font(file, dict, base_font)?,
43 "Type1" | "MMType1" => load_type1_font(file, dict, base_font)?,
44 _ => LoadedFont::new_placeholder(base_font),
45 };
46
47 attach_text_mappings(file, dict, subtype, &mut font);
48 font.build_substitute_cid_to_gid();
51 apply_descriptor_variations(file, dict, &mut font);
53 Ok(font)
54}
55
56fn substitute_hints(
58 file: &PdfFile,
59 dict: &zpdf_core::PdfDict,
60) -> zpdf_font::system::SubstituteHints {
61 let mut hints = zpdf_font::system::SubstituteHints::default();
62 if let Ok(fd_ref) = dict.get_ref("FontDescriptor") {
63 if let Ok(fd) = file.resolve(fd_ref) {
64 if let Ok(fd) = fd.as_dict() {
65 if let Ok(flags) = fd.get_i64("Flags") {
66 hints.fixed_pitch = flags & 1 != 0;
67 hints.serif = flags & 2 != 0;
68 hints.italic = flags & 64 != 0;
69 hints.bold = flags & (1 << 18) != 0; }
71 if let Ok(w) = fd.get_f64("StemV") {
72 hints.bold |= w >= 160.0;
73 }
74 }
75 }
76 }
77 hints
78}
79
80fn try_system_substitute_simple(
84 file: &PdfFile,
85 dict: &zpdf_core::PdfDict,
86 base_font: &str,
87 font_type: PdfFontType,
88 mut cid_widths: CidWidths,
89) -> Option<LoadedFont> {
90 let hints = substitute_hints(file, dict);
91 let m = zpdf_font::system::find_system_font(base_font, hints, None)?;
92 if cid_widths.is_empty() {
93 if let Some(metrics) = zpdf_font::standard_fonts::lookup(base_font) {
94 for (code, &w) in metrics.widths.iter().enumerate() {
95 if w > 0 {
96 cid_widths.set(code as u16, w as f64);
97 }
98 }
99 }
100 }
101 LoadedFont::new_substitute(
102 font_type,
103 base_font.to_string(),
104 m.data,
105 m.face_index,
106 cid_widths,
107 )
108}
109
110fn attach_text_mappings(
113 file: &PdfFile,
114 dict: &zpdf_core::PdfDict,
115 subtype: &str,
116 font: &mut LoadedFont,
117) {
118 if let Ok(tu_ref) = dict.get_ref("ToUnicode") {
120 if let Ok(data) = file.resolve_stream_data(tu_ref) {
121 let map = zpdf_font::cmap::ToUnicodeMap::parse(&data);
122 if !map.is_empty() {
123 font.to_unicode = Some(map);
124 }
125 }
126 }
127
128 if subtype == "Type0" {
130 return;
131 }
132
133 font.symbolic = font_descriptor_symbolic(file, dict);
134
135 let encoding = if dict.get("Encoding").is_none() {
136 builtin_symbol_encoding(&font.base_font)
139 .or_else(|| parse_encoding(file, dict, subtype, font.symbolic))
140 } else {
141 parse_encoding(file, dict, subtype, font.symbolic)
142 };
143 if let Some(enc) = encoding {
144 font.encoding = Some(enc);
145 }
146
147 font.map_unencoded_orphans();
150}
151
152fn builtin_symbol_encoding(base_font: &str) -> Option<zpdf_font::encoding::Encoding> {
156 use zpdf_font::encoding::{base_encoding_by_name, Encoding};
157 let name = base_font.rsplit('+').next().unwrap_or(base_font);
158 let canonical = if name.contains("ZapfDingbats") || name.contains("Dingbats") {
159 "ZapfDingbats"
160 } else if name.contains("Symbol") {
161 "Symbol"
162 } else {
163 return None;
164 };
165 base_encoding_by_name(canonical).map(Encoding::from_base)
166}
167
168fn font_descriptor_symbolic(file: &PdfFile, dict: &zpdf_core::PdfDict) -> bool {
171 let fd_ref = match dict.get_ref("FontDescriptor") {
172 Ok(r) => r,
173 Err(_) => return false,
174 };
175 let flags = file
176 .resolve(fd_ref)
177 .ok()
178 .and_then(|o| o.as_dict().ok().and_then(|d| d.get_i64("Flags").ok()));
179 matches!(flags, Some(f) if (f & 4) != 0 && (f & 32) == 0)
180}
181
182fn font_descriptor_dict(file: &PdfFile, dict: &zpdf_core::PdfDict) -> Option<zpdf_core::PdfDict> {
186 let host = if dict.get_name("Subtype").unwrap_or("") == "Type0" {
187 let descendants = resolve_array(file, dict, "DescendantFonts")?;
188 let desc_ref = descendants.first()?.as_ref().ok()?;
189 file.resolve(desc_ref).ok()?.as_dict().ok()?.clone()
190 } else {
191 dict.clone()
192 };
193 let fd_ref = host.get_ref("FontDescriptor").ok()?;
194 file.resolve(fd_ref).ok()?.as_dict().ok().cloned()
195}
196
197fn font_stretch_pct(name: &str) -> Option<f64> {
199 Some(match name {
200 "UltraCondensed" => 50.0,
201 "ExtraCondensed" => 62.5,
202 "Condensed" => 75.0,
203 "SemiCondensed" => 87.5,
204 "Normal" => 100.0,
205 "SemiExpanded" => 112.5,
206 "Expanded" => 125.0,
207 "ExtraExpanded" => 150.0,
208 "UltraExpanded" => 200.0,
209 _ => return None,
210 })
211}
212
213fn apply_descriptor_variations(file: &PdfFile, dict: &zpdf_core::PdfDict, font: &mut LoadedFont) {
218 let Some(fd) = font_descriptor_dict(file, dict) else {
219 return;
220 };
221 let weight = fd.get_f64("FontWeight").ok();
222 let width_pct = fd.get_name("FontStretch").ok().and_then(font_stretch_pct);
223 let italic_angle = fd.get_f64("ItalicAngle").ok();
224 let italic = fd.get_i64("Flags").map(|f| f & 64 != 0).unwrap_or(false);
225 if weight.is_some() || width_pct.is_some() || italic_angle.is_some() || italic {
226 font.set_variations(weight, width_pct, italic_angle, italic);
227 }
228}
229
230fn parse_encoding(
233 file: &PdfFile,
234 dict: &zpdf_core::PdfDict,
235 subtype: &str,
236 symbolic: bool,
237) -> Option<zpdf_font::encoding::Encoding> {
238 use zpdf_font::encoding::{base_encoding_by_name, Encoding};
239
240 let enc_obj = match dict.get("Encoding").cloned() {
241 Some(PdfObject::Ref(r)) => file.resolve(r).ok(),
242 other => other,
243 };
244
245 match enc_obj {
246 Some(PdfObject::Name(n)) => base_encoding_by_name(n.as_str()).map(Encoding::from_base),
247 Some(PdfObject::Dict(enc_dict)) => {
248 let base = enc_dict
249 .get_name("BaseEncoding")
250 .ok()
251 .and_then(base_encoding_by_name)
252 .unwrap_or_else(|| default_simple_base(subtype));
253 let mut encoding = Encoding::from_base(base);
254 apply_differences(&enc_dict, &mut encoding);
255 Some(encoding)
256 }
257 _ if symbolic => None,
259 _ => Some(Encoding::from_base(default_simple_base(subtype))),
260 }
261}
262
263fn default_simple_base(subtype: &str) -> &'static zpdf_font::encoding::EncodingTable {
264 match subtype {
265 "TrueType" => &zpdf_font::encoding::WIN_ANSI_ENCODING,
266 _ => &zpdf_font::encoding::STANDARD_ENCODING,
267 }
268}
269
270fn apply_differences(enc_dict: &zpdf_core::PdfDict, encoding: &mut zpdf_font::encoding::Encoding) {
271 if let Ok(diffs) = enc_dict.get_array("Differences") {
272 let mut code = 0u32;
273 for obj in diffs {
274 match obj {
275 PdfObject::Integer(n) => code = (*n).max(0) as u32,
276 PdfObject::Name(name) => {
277 if code <= 255 {
278 encoding.apply_difference(code as u8, name.as_str());
279 }
280 code += 1;
281 }
282 _ => {}
283 }
284 }
285 }
286}
287
288fn parse_type0_encoding(file: &PdfFile, dict: &zpdf_core::PdfDict) -> zpdf_font::cmap::CidCMap {
292 use zpdf_font::cmap::CidCMap;
293 fn identity_fallback(name: &str) -> CidCMap {
296 let wmode = name.ends_with("-V") as u8;
297 tracing::warn!(
298 "unsupported predefined CMap {name}; using Identity-{}",
299 if wmode == 1 { "V" } else { "H" }
300 );
301 CidCMap::identity(wmode)
302 }
303 match dict.get("Encoding") {
304 Some(PdfObject::Name(n)) => {
305 CidCMap::predefined(n.as_str()).unwrap_or_else(|| identity_fallback(n.as_str()))
306 }
307 Some(PdfObject::Ref(r)) => match file.resolve(*r) {
308 Ok(PdfObject::Name(n)) => {
309 CidCMap::predefined(n.as_str()).unwrap_or_else(|| identity_fallback(n.as_str()))
310 }
311 Ok(PdfObject::Stream(s)) => {
312 let data = file
313 .resolve_stream_data(*r)
314 .or_else(|_| zpdf_parser::filters::decode_stream(&s.data, &s.dict));
315 let mut cmap = match data {
316 Ok(d) => CidCMap::parse(&d),
317 Err(e) => {
318 tracing::warn!("undecodable embedded CMap: {e}; using Identity-H");
319 CidCMap::identity(0)
320 }
321 };
322 if let Ok(1) = s.dict.get_i64("WMode") {
324 cmap.wmode = 1;
325 }
326 cmap
327 }
328 _ => CidCMap::identity(0),
329 },
330 _ => CidCMap::identity(0),
331 }
332}
333
334fn parse_dw2(file: &PdfFile, desc_dict: &zpdf_core::PdfDict) -> (f64, f64) {
336 resolve_array(file, desc_dict, "DW2")
337 .and_then(|arr| {
338 let v: Vec<f64> = arr.iter().filter_map(|o| o.as_f64().ok()).collect();
339 (v.len() >= 2).then(|| (v[0], v[1]))
340 })
341 .unwrap_or((880.0, -1000.0))
342}
343
344fn load_type0_font(
345 file: &PdfFile,
346 dict: &zpdf_core::PdfDict,
347 base_font: String,
348) -> Result<LoadedFont> {
349 let descendants = resolve_array(file, dict, "DescendantFonts")
351 .ok_or_else(|| zpdf_core::Error::MissingKey("DescendantFonts".into()))?;
352 let desc_ref = descendants
353 .first()
354 .ok_or_else(|| zpdf_core::Error::MissingKey("DescendantFonts[0]".into()))?
355 .as_ref()?;
356
357 let desc_obj = file.resolve(desc_ref)?;
358 let desc_dict = desc_obj.as_dict()?;
359
360 let mut cid_widths = parse_cid_widths(file, desc_dict);
361 parse_cid_w2(file, desc_dict, &mut cid_widths);
362 let cmap = parse_type0_encoding(file, dict);
363 let dw2 = parse_dw2(file, desc_dict);
364
365 let font_data = extract_font_file(file, desc_dict);
366
367 let mut font = match font_data {
368 Some(data) => {
369 let mut font = LoadedFont::new_with_data(
370 PdfFontType::Type0CidType2,
371 base_font.clone(),
372 data,
373 cid_widths.clone(),
374 );
375 if let Some(map) = parse_cid_to_gid_stream(file, desc_dict) {
380 let subtype = desc_dict.get_name("Subtype").unwrap_or("");
381 if subtype == "CIDFontType2" || font.cid_to_gid.is_none() {
382 font.cid_to_gid = Some(map);
383 }
384 }
385 let cjk = is_cjk_ordering(desc_ordering(file, desc_dict).as_deref())
392 || zpdf_font::system::cjk_ordering_for(&base_font).is_some();
393 if cjk && font.embedded_outline_failure_rate() > 0.5 {
394 if let Some(sub) = substitute_type0_font(file, desc_dict, &base_font, cid_widths) {
395 font = sub;
396 }
397 }
398 font
399 }
400 None => {
401 substitute_type0_font(file, desc_dict, &base_font, cid_widths)
405 .unwrap_or_else(|| LoadedFont::new_placeholder(base_font))
406 }
407 };
408 font.cid_cmap = Some(cmap);
409 font.dw2 = dw2;
410 font.validate_cid_cmap();
413 Ok(font)
414}
415
416fn desc_ordering(file: &PdfFile, desc_dict: &zpdf_core::PdfDict) -> Option<String> {
418 resolve_dict(file, desc_dict, "CIDSystemInfo").and_then(|csi| match csi.get("Ordering") {
419 Some(PdfObject::String(s)) => Some(s.to_string_lossy()),
420 Some(PdfObject::Name(n)) => Some(n.as_str().to_string()),
421 _ => None,
422 })
423}
424
425fn is_cjk_ordering(ordering: Option<&str>) -> bool {
427 matches!(ordering, Some("GB1" | "CNS1" | "Japan1" | "Korea1" | "KR"))
428}
429
430fn substitute_type0_font(
434 file: &PdfFile,
435 desc_dict: &zpdf_core::PdfDict,
436 base_font: &str,
437 cid_widths: CidWidths,
438) -> Option<LoadedFont> {
439 let ordering = desc_ordering(file, desc_dict);
440 let hints = substitute_hints(file, desc_dict);
441 zpdf_font::system::find_system_font(base_font, hints, ordering.as_deref()).and_then(|m| {
442 LoadedFont::new_substitute(
443 PdfFontType::Type0CidType2,
444 base_font.to_string(),
445 m.data,
446 m.face_index,
447 cid_widths,
448 )
449 })
450}
451
452fn parse_cid_to_gid_stream(
458 file: &PdfFile,
459 desc_dict: &zpdf_core::PdfDict,
460) -> Option<std::collections::HashMap<u16, u16>> {
461 let stream_ref = match desc_dict.get("CIDToGIDMap") {
462 Some(PdfObject::Ref(r)) => *r,
463 _ => return None,
465 };
466 let data = match file.resolve_stream_data(stream_ref) {
467 Ok(d) => d,
468 Err(e) => {
469 tracing::debug!("CIDToGIDMap {stream_ref}: not a decodable stream - {e}");
471 return None;
472 }
473 };
474 let mut map = std::collections::HashMap::new();
475 for (cid, gid_bytes) in data.chunks_exact(2).enumerate().take(u16::MAX as usize + 1) {
476 let gid = u16::from_be_bytes([gid_bytes[0], gid_bytes[1]]);
477 if gid != 0 {
478 map.insert(cid as u16, gid);
479 }
480 }
481 if map.is_empty() {
482 None
483 } else {
484 Some(map)
485 }
486}
487
488fn load_truetype_font(
489 file: &PdfFile,
490 dict: &zpdf_core::PdfDict,
491 base_font: String,
492) -> Result<LoadedFont> {
493 let cid_widths = parse_simple_widths(file, dict);
494 let font_data = extract_font_file_from_descriptor(file, dict);
495
496 match font_data {
497 Some(data) => Ok(LoadedFont::new_with_data(
498 PdfFontType::TrueType,
499 base_font,
500 data,
501 cid_widths,
502 )),
503 None => Ok(try_system_substitute_simple(
504 file,
505 dict,
506 &base_font,
507 PdfFontType::TrueType,
508 cid_widths,
509 )
510 .or_else(|| LoadedFont::new_standard(base_font.clone()))
511 .unwrap_or_else(|| LoadedFont::new_placeholder(base_font))),
512 }
513}
514
515fn load_type3_font(
516 file: &PdfFile,
517 dict: &zpdf_core::PdfDict,
518 base_font: String,
519) -> Result<LoadedFont> {
520 use std::sync::Arc;
521
522 let font_matrix = {
527 let mut m = [0.001, 0.0, 0.0, -0.001, 0.0, 0.0];
528 if let Some(arr) = resolve_array(file, dict, "FontMatrix") {
529 for (i, obj) in arr.iter().enumerate().take(6) {
530 if let Ok(v) = obj.as_f64() {
531 m[i] = v;
532 }
533 }
534 }
535 m
536 };
537
538 let mut encoding = Vec::new();
540 if let Some(enc_dict) = resolve_dict(file, dict, "Encoding") {
541 if let Some(diffs) = resolve_array(file, &enc_dict, "Differences") {
542 let mut current_code = 0usize;
543 for obj in &diffs {
544 match obj {
545 PdfObject::Integer(n) => {
546 current_code = *n as usize;
547 while encoding.len() < current_code {
548 encoding.push(String::new());
549 }
550 }
551 PdfObject::Name(n) => {
552 while encoding.len() <= current_code {
553 encoding.push(String::new());
554 }
555 encoding[current_code] = n.0.clone();
556 current_code += 1;
557 }
558 _ => {}
559 }
560 }
561 }
562 }
563
564 let mut char_procs = std::collections::HashMap::new();
566 if let Some(cp_dict) = resolve_dict(file, dict, "CharProcs") {
567 for (name, obj) in &cp_dict.0 {
568 if let PdfObject::Ref(r) = obj {
569 if let Ok(data) = file.resolve_stream_data(*r) {
570 char_procs.insert(name.0.clone(), Arc::from(data));
571 }
572 }
573 }
574 }
575
576 let first_char = dict.get_i64("FirstChar").unwrap_or(0) as u16;
578 let widths: Vec<f64> = resolve_array(file, dict, "Widths")
579 .unwrap_or_default()
580 .iter()
581 .map(|o| o.as_f64().unwrap_or(0.0))
582 .collect();
583
584 let font = LoadedFont {
585 font_type: zpdf_font::PdfFontType::Type3 {
586 font_matrix,
587 char_procs,
588 encoding,
589 widths,
590 first_char,
591 },
592 base_font,
593 font_data: None,
594 face_index: 0,
595 is_substitute: false,
596 cid_widths: CidWidths::new(1000.0),
597 units_per_em: 1000.0,
598 ascent: 880.0,
599 descent: -120.0,
600 cid_to_gid: None,
601 builtin_encoding_gids: None,
602 orphan_gids: Vec::new(),
603 encoding: None,
604 to_unicode: None,
605 symbolic: false,
606 type1: None,
607 cid_cmap: None,
608 dw2: (880.0, -1000.0),
609 variations: Vec::new(),
610 };
611
612 Ok(font)
613}
614
615fn load_type1_font(
616 file: &PdfFile,
617 dict: &zpdf_core::PdfDict,
618 base_font: String,
619) -> Result<LoadedFont> {
620 let cid_widths = parse_simple_widths(file, dict);
621 let font_data = extract_font_file_from_descriptor(file, dict);
622
623 match font_data {
624 Some(data) => Ok(LoadedFont::new_with_data(
625 PdfFontType::Type1,
626 base_font,
627 data,
628 cid_widths,
629 )),
630 None => Ok(try_system_substitute_simple(
631 file,
632 dict,
633 &base_font,
634 PdfFontType::Type1,
635 cid_widths,
636 )
637 .or_else(|| LoadedFont::new_standard(base_font.clone()))
638 .unwrap_or_else(|| LoadedFont::new_placeholder(base_font))),
639 }
640}
641
642fn extract_font_file(file: &PdfFile, cid_dict: &zpdf_core::PdfDict) -> Option<Vec<u8>> {
644 let fd_ref = cid_dict.get_ref("FontDescriptor").ok()?;
645 let fd_obj = file.resolve(fd_ref).ok()?;
646 let fd_dict = fd_obj.as_dict().ok()?;
647
648 for key in &["FontFile2", "FontFile3", "FontFile"] {
650 if let Ok(ff_ref) = fd_dict.get_ref(key) {
651 if let Ok(data) = file.resolve_stream_data(ff_ref) {
652 if !data.is_empty() {
653 return Some(data);
654 }
655 }
656 }
657 }
658 None
659}
660
661fn extract_font_file_from_descriptor(
662 file: &PdfFile,
663 font_dict: &zpdf_core::PdfDict,
664) -> Option<Vec<u8>> {
665 let fd_ref = font_dict.get_ref("FontDescriptor").ok()?;
666 let fd_obj = file.resolve(fd_ref).ok()?;
667 let fd_dict = fd_obj.as_dict().ok()?;
668
669 for key in &["FontFile2", "FontFile3", "FontFile"] {
670 if let Ok(ff_ref) = fd_dict.get_ref(key) {
671 if let Ok(data) = file.resolve_stream_data(ff_ref) {
672 if !data.is_empty() {
673 return Some(data);
674 }
675 }
676 }
677 }
678 None
679}
680
681fn resolve_array(file: &PdfFile, dict: &zpdf_core::PdfDict, key: &str) -> Option<Vec<PdfObject>> {
685 match dict.get(key) {
686 Some(PdfObject::Array(a)) => Some(a.clone()),
687 Some(PdfObject::Ref(id)) => file
688 .resolve(*id)
689 .ok()
690 .and_then(|o| o.as_array().ok().map(|a| a.to_vec())),
691 _ => None,
692 }
693}
694
695fn resolve_dict(
699 file: &PdfFile,
700 dict: &zpdf_core::PdfDict,
701 key: &str,
702) -> Option<zpdf_core::PdfDict> {
703 match dict.get(key) {
704 Some(PdfObject::Dict(d)) => Some(d.clone()),
705 Some(PdfObject::Ref(id)) => file
706 .resolve(*id)
707 .ok()
708 .and_then(|o| o.as_dict().ok().cloned()),
709 _ => None,
710 }
711}
712
713fn parse_cid_widths(file: &PdfFile, dict: &zpdf_core::PdfDict) -> CidWidths {
715 let dw = dict.get_f64("DW").unwrap_or(1000.0);
716 let mut widths = CidWidths::new(dw);
717
718 let w_array = match resolve_array(file, dict, "W") {
719 Some(arr) => arr,
720 None => return widths,
721 };
722
723 let mut i = 0;
724 while i < w_array.len() {
725 let cid_start = match w_array[i].as_i64() {
726 Ok(v) => v as u16,
727 Err(_) => break,
728 };
729 i += 1;
730 if i >= w_array.len() {
731 break;
732 }
733
734 match &w_array[i] {
735 PdfObject::Array(arr) => {
736 for (j, obj) in arr.iter().enumerate() {
738 let Some(cid) = cid_start.checked_add(j as u16) else {
739 break;
740 };
741 if let Ok(w) = obj.as_f64() {
742 widths.set(cid, w);
743 }
744 }
745 i += 1;
746 }
747 PdfObject::Integer(_) | PdfObject::Real(_) => {
748 let cid_end = w_array[i].as_i64().unwrap_or(cid_start as i64) as u16;
750 i += 1;
751 if i < w_array.len() {
752 let w = w_array[i].as_f64().unwrap_or(dw);
753 for cid in cid_start..=cid_end {
754 widths.set(cid, w);
755 }
756 i += 1;
757 }
758 }
759 _ => {
760 i += 1;
761 }
762 }
763 }
764
765 widths
766}
767
768fn parse_cid_w2(file: &PdfFile, dict: &zpdf_core::PdfDict, widths: &mut CidWidths) {
774 if let Some(arr) = resolve_array(file, dict, "W2") {
775 apply_w2_array(&arr, widths);
776 }
777}
778
779fn apply_w2_array(w2_array: &[PdfObject], widths: &mut CidWidths) {
780 let mut i = 0;
781 while i < w2_array.len() {
782 let cid_start = match w2_array[i].as_i64() {
783 Ok(v) => v as u16,
784 Err(_) => break,
785 };
786 i += 1;
787 if i >= w2_array.len() {
788 break;
789 }
790
791 match &w2_array[i] {
792 PdfObject::Array(arr) => {
793 let mut k = 0;
795 while k + 2 < arr.len() {
796 let (Ok(w1y), Ok(vx), Ok(vy)) =
797 (arr[k].as_f64(), arr[k + 1].as_f64(), arr[k + 2].as_f64())
798 else {
799 break;
800 };
801 let Some(cid) = cid_start.checked_add((k / 3) as u16) else {
802 break;
803 };
804 widths.set_v(cid, w1y, vx, vy);
805 k += 3;
806 }
807 i += 1;
808 }
809 PdfObject::Integer(_) | PdfObject::Real(_) => {
810 let cid_end = w2_array[i].as_i64().unwrap_or(cid_start as i64) as u16;
812 if i + 3 < w2_array.len() {
813 let (Ok(w1y), Ok(vx), Ok(vy)) = (
814 w2_array[i + 1].as_f64(),
815 w2_array[i + 2].as_f64(),
816 w2_array[i + 3].as_f64(),
817 ) else {
818 break;
819 };
820 for cid in cid_start..=cid_end {
821 widths.set_v(cid, w1y, vx, vy);
822 }
823 i += 4;
824 } else {
825 break;
826 }
827 }
828 _ => {
829 i += 1;
830 }
831 }
832 }
833}
834
835fn parse_simple_widths(file: &PdfFile, dict: &zpdf_core::PdfDict) -> CidWidths {
836 let first_char = dict.get_i64("FirstChar").unwrap_or(0) as u16;
837 let mut widths = CidWidths::new(1000.0);
838
839 if let Some(arr) = resolve_array(file, dict, "Widths") {
840 for (j, obj) in arr.iter().enumerate() {
841 let Some(code) = first_char.checked_add(j as u16) else {
842 break;
843 };
844 if let Ok(w) = obj.as_f64() {
845 widths.set(code, w);
846 }
847 }
848 }
849
850 widths
851}
852
853#[cfg(test)]
854mod tests {
855 use super::*;
856
857 fn int(v: i64) -> PdfObject {
858 PdfObject::Integer(v)
859 }
860 fn real(v: f64) -> PdfObject {
861 PdfObject::Real(v)
862 }
863
864 #[test]
865 fn w2_list_form_assigns_consecutive_cids() {
866 let arr = vec![
868 int(120),
869 PdfObject::Array(vec![
870 real(-1000.0),
871 real(500.0),
872 real(880.0),
873 int(-900),
874 int(450),
875 int(820),
876 ]),
877 ];
878 let mut w = CidWidths::new(1000.0);
879 apply_w2_array(&arr, &mut w);
880 assert_eq!(w.get_v(120), Some((-1000.0, 500.0, 880.0)));
881 assert_eq!(w.get_v(121), Some((-900.0, 450.0, 820.0)));
882 assert_eq!(w.get_v(122), None);
883 }
884
885 #[test]
886 fn w2_range_form_assigns_inclusive_range() {
887 let arr = vec![int(10), int(12), int(-1000), int(500), int(880)];
889 let mut w = CidWidths::new(1000.0);
890 apply_w2_array(&arr, &mut w);
891 for cid in 10..=12 {
892 assert_eq!(w.get_v(cid), Some((-1000.0, 500.0, 880.0)));
893 }
894 assert_eq!(w.get_v(9), None);
895 assert_eq!(w.get_v(13), None);
896 }
897
898 #[test]
899 fn w2_truncated_entry_is_ignored_not_panic() {
900 let arr = vec![int(10), int(12), int(-1000)];
902 let mut w = CidWidths::new(1000.0);
903 apply_w2_array(&arr, &mut w);
904 assert_eq!(w.get_v(10), None);
905 }
906}