1use std::collections::{HashMap, HashSet};
23
24use zpdf_core::{Matrix, ObjectId, PdfDict, PdfName, PdfObject, Rect};
25use zpdf_parser::PdfFile;
26
27const MAX_FIELD_DEPTH: usize = 50;
30const MAX_FIELDS: usize = 20_000;
31
32pub const FF_READONLY: i64 = 1 << 0;
36pub const FF_MULTILINE: i64 = 1 << 12;
38pub const FF_PASSWORD: i64 = 1 << 13;
40pub const FF_RADIO: i64 = 1 << 15;
42pub const FF_PUSHBUTTON: i64 = 1 << 16;
44pub const FF_COMBO: i64 = 1 << 17;
46pub const FF_COMB: i64 = 1 << 24;
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum FieldKind {
52 Text,
53 Button,
54 Choice,
55 Signature,
56 Unknown,
57}
58
59impl FieldKind {
60 pub fn as_str(self) -> &'static str {
61 match self {
62 FieldKind::Text => "Tx",
63 FieldKind::Button => "Btn",
64 FieldKind::Choice => "Ch",
65 FieldKind::Signature => "Sig",
66 FieldKind::Unknown => "?",
67 }
68 }
69}
70
71#[derive(Debug, Clone, PartialEq)]
73pub enum FieldValue {
74 Text(String),
76 Name(String),
78 List(Vec<String>),
80}
81
82#[derive(Debug, Clone)]
84pub struct FormField {
85 pub field_id: ObjectId,
88 pub name: String,
91 pub kind: FieldKind,
92 pub flags: i64,
94 pub value: Option<FieldValue>,
96 pub default_appearance: Option<String>,
99 pub quadding: i64,
101 pub max_len: Option<i64>,
103 pub options: Vec<(String, String)>,
106 pub widgets: Vec<ObjectId>,
110}
111
112impl FormField {
113 pub fn display_value(&self) -> Option<String> {
118 let s = match self.value.as_ref()? {
119 FieldValue::Text(s) => self.choice_label(s),
120 FieldValue::Name(n) if n != "Off" => n.clone(),
121 FieldValue::Name(_) => return None,
122 FieldValue::List(v) => v
123 .iter()
124 .map(|s| self.choice_label(s))
125 .collect::<Vec<_>>()
126 .join("\n"),
127 };
128 (!s.is_empty()).then_some(s)
129 }
130
131 fn choice_label(&self, value: &str) -> String {
134 if self.kind == FieldKind::Choice {
135 if let Some((_, display)) = self.options.iter().find(|(export, _)| export == value) {
136 return display.clone();
137 }
138 }
139 value.to_string()
140 }
141
142 pub fn is_multiline(&self) -> bool {
143 self.kind == FieldKind::Text && self.flags & FF_MULTILINE != 0
144 }
145
146 pub fn is_password(&self) -> bool {
147 self.kind == FieldKind::Text && self.flags & FF_PASSWORD != 0
148 }
149
150 pub fn is_comb(&self) -> bool {
151 self.kind == FieldKind::Text
152 && self.flags & (FF_COMB | FF_MULTILINE | FF_PASSWORD) == FF_COMB
154 && self.max_len.unwrap_or(0) > 0
155 }
156}
157
158pub struct AcroForm {
160 pub fields: Vec<FormField>,
162 pub need_appearances: bool,
165 pub dr_fonts: Option<PdfDict>,
167 widget_owner: HashMap<ObjectId, usize>,
169}
170
171impl AcroForm {
172 pub fn parse(file: &PdfFile) -> Option<AcroForm> {
175 let root_ref = file.trailer.get_ref("Root").ok()?;
176 let root = file.resolve(root_ref).ok()?;
177 let root = root.as_dict().ok()?;
178 let af = deref(file, root.get("AcroForm")?);
179 let af = af.as_dict().ok()?;
180
181 let need_appearances = matches!(af.get("NeedAppearances"), Some(PdfObject::Bool(true)));
182 let dr_fonts = deref_opt(file, af.get("DR"))
183 .and_then(|dr| dr.as_dict().ok().cloned())
184 .and_then(|dr| match dr.get("Font") {
185 Some(obj) => deref(file, obj).as_dict().ok().cloned(),
186 None => None,
187 });
188
189 let root_inherited = Inherited {
190 ft: None,
191 flags: 0,
192 value: None,
193 da: af.get("DA").and_then(|o| text_string(file, o)),
194 quadding: int_value(file, af.get("Q")).unwrap_or(0),
195 };
196
197 let mut state = WalkState {
198 file,
199 fields: Vec::new(),
200 widget_owner: HashMap::new(),
201 visited: HashSet::new(),
202 };
203 if let Some(arr) = deref_array(file, af.get("Fields")) {
204 for obj in &arr {
205 if let PdfObject::Ref(r) = obj {
206 walk_field(&mut state, *r, "", &root_inherited, 0);
207 }
208 }
209 }
210
211 Some(AcroForm {
212 fields: state.fields,
213 need_appearances,
214 dr_fonts,
215 widget_owner: state.widget_owner,
216 })
217 }
218
219 pub fn field_for_widget(&self, id: ObjectId) -> Option<&FormField> {
221 self.widget_owner.get(&id).and_then(|&i| self.fields.get(i))
222 }
223}
224
225#[derive(Clone)]
227struct Inherited {
228 ft: Option<String>,
229 flags: i64,
230 value: Option<FieldValue>,
231 da: Option<String>,
232 quadding: i64,
233}
234
235struct WalkState<'a> {
236 file: &'a PdfFile,
237 fields: Vec<FormField>,
238 widget_owner: HashMap<ObjectId, usize>,
239 visited: HashSet<ObjectId>,
240}
241
242fn walk_field(
243 state: &mut WalkState,
244 id: ObjectId,
245 parent_name: &str,
246 inherited: &Inherited,
247 depth: usize,
248) {
249 if depth > MAX_FIELD_DEPTH || state.fields.len() >= MAX_FIELDS {
250 return;
251 }
252 if !state.visited.insert(id) {
253 return; }
255 let file = state.file;
256 let obj = match file.resolve(id) {
257 Ok(o) => o,
258 Err(_) => return,
259 };
260 let Ok(dict) = obj.as_dict() else { return };
261
262 let partial = dict.get("T").and_then(|o| text_string(file, o));
265 let name = match &partial {
266 Some(t) if parent_name.is_empty() => t.clone(),
267 Some(t) => format!("{parent_name}.{t}"),
268 None => parent_name.to_string(),
269 };
270
271 let merged = Inherited {
273 ft: dict
274 .get_name("FT")
275 .ok()
276 .map(String::from)
277 .or_else(|| inherited.ft.clone()),
278 flags: int_value(file, dict.get("Ff")).unwrap_or(inherited.flags),
279 value: field_value(file, dict.get("V")).or_else(|| inherited.value.clone()),
280 da: dict
281 .get("DA")
282 .and_then(|o| text_string(file, o))
283 .or_else(|| inherited.da.clone()),
284 quadding: int_value(file, dict.get("Q")).unwrap_or(inherited.quadding),
285 };
286
287 let kids = deref_array(file, dict.get("Kids")).unwrap_or_default();
290 let mut child_fields = Vec::new();
291 let mut widget_kids = Vec::new();
292 for kid in &kids {
293 if let PdfObject::Ref(r) = kid {
294 let kid_obj = file.resolve(*r).ok();
295 let has_t = kid_obj
296 .as_ref()
297 .and_then(|o| o.as_dict().ok())
298 .map(|d| d.get("T").is_some())
299 .unwrap_or(false);
300 if has_t {
301 child_fields.push(*r);
302 } else {
303 widget_kids.push(*r);
304 }
305 }
306 }
307
308 let has_child_fields = !child_fields.is_empty();
310 for r in child_fields {
311 walk_field(state, r, &name, &merged, depth + 1);
312 }
313
314 let widgets = if !widget_kids.is_empty() {
321 widget_kids
322 } else if has_child_fields {
323 Vec::new()
324 } else {
325 vec![id] };
327 if widgets.is_empty() {
328 return;
329 }
330
331 let kind = field_kind(merged.ft.as_deref());
332 let options = if kind == FieldKind::Choice {
333 parse_options(file, dict)
334 } else {
335 Vec::new()
336 };
337 let max_len = int_value(file, dict.get("MaxLen"));
338
339 let index = state.fields.len();
340 for &w in &widgets {
341 state.widget_owner.entry(w).or_insert(index);
342 }
343 state.fields.push(FormField {
344 field_id: id,
345 name,
346 kind,
347 flags: merged.flags,
348 value: merged.value,
349 default_appearance: merged.da,
350 quadding: merged.quadding,
351 max_len,
352 options,
353 widgets,
354 });
355}
356
357fn field_kind(ft: Option<&str>) -> FieldKind {
358 match ft {
359 Some("Tx") => FieldKind::Text,
360 Some("Btn") => FieldKind::Button,
361 Some("Ch") => FieldKind::Choice,
362 Some("Sig") => FieldKind::Signature,
363 _ => FieldKind::Unknown,
364 }
365}
366
367fn parse_options(file: &PdfFile, dict: &PdfDict) -> Vec<(String, String)> {
370 let as_text = |o: &PdfObject| match o {
371 PdfObject::String(s) => Some(pdf_string_to_unicode(s.as_bytes())),
372 _ => None,
373 };
374 deref_array(file, dict.get("Opt"))
375 .map(|arr| {
376 arr.iter()
377 .map(|o| match deref(file, o) {
378 PdfObject::String(s) => {
379 let t = pdf_string_to_unicode(s.as_bytes());
380 (t.clone(), t)
381 }
382 PdfObject::Array(a) => {
383 let export = a.first().and_then(as_text).unwrap_or_default();
384 let display = a.get(1).and_then(as_text).unwrap_or_else(|| export.clone());
385 (export, display)
386 }
387 _ => (String::new(), String::new()),
388 })
389 .collect()
390 })
391 .unwrap_or_default()
392}
393
394pub(crate) const MAX_APPEARANCE_TEXT_CHARS: usize = 50_000;
404
405#[derive(Debug, Clone)]
409pub struct GeneratedAppearance {
410 pub bbox: Rect,
411 pub matrix: Matrix,
412 pub resources: PdfDict,
413 pub content: Vec<u8>,
414}
415
416pub fn generate_widget_appearance(
421 field: &FormField,
422 rect: Rect,
423 dr_fonts: Option<&PdfDict>,
424) -> Option<GeneratedAppearance> {
425 if !matches!(field.kind, FieldKind::Text | FieldKind::Choice) || field.is_password() {
426 return None;
427 }
428 let text: String = field
431 .display_value()?
432 .chars()
433 .take(MAX_APPEARANCE_TEXT_CHARS)
434 .collect();
435 let rect = rect.normalize();
436 let (w, h) = (rect.width(), rect.height());
437 if w <= 1.0 || h <= 1.0 {
438 return None;
439 }
440
441 let da = field
442 .default_appearance
443 .as_deref()
444 .unwrap_or("/Helv 0 Tf 0 g");
445 let da = parse_da(da);
446 let font_res_name = da
449 .font
450 .as_deref()
451 .filter(|n| is_safe_resource_name(n))
452 .unwrap_or("Helv")
453 .to_string();
454 let base_font = resolve_base_font(dr_fonts, &font_res_name);
455
456 const PAD: f64 = 2.0;
457 let comb = field.is_comb();
458 let mut body: Vec<u8> = Vec::new();
459 push_str(&mut body, "BT\n");
460
461 let stacked =
464 field.is_multiline() || (field.kind == FieldKind::Choice && field.flags & FF_COMBO == 0);
465
466 if comb {
467 comb_layout(
468 &mut body,
469 &one_line(&text),
470 &da,
471 &base_font,
472 &font_res_name,
473 w,
474 h,
475 field,
476 );
477 } else if stacked {
478 multiline_layout(
479 &mut body,
480 &text,
481 &da,
482 &base_font,
483 &font_res_name,
484 w,
485 h,
486 PAD,
487 field.quadding,
488 );
489 } else {
490 single_line_layout(
491 &mut body,
492 &one_line(&text),
493 &da,
494 &base_font,
495 &font_res_name,
496 w,
497 h,
498 PAD,
499 field.quadding,
500 );
501 }
502 push_str(&mut body, "ET\n");
503
504 let inset = if comb { 0.0 } else { PAD };
507 let clip_w = (w - 2.0 * inset).max(0.0);
508 let clip_h = (h - 2.0 * inset).max(0.0);
509 let mut content: Vec<u8> = Vec::new();
510 push_str(&mut content, "/Tx BMC\nq\n");
511 push_str(&mut content, &fmt_num(inset));
512 push_str(&mut content, " ");
513 push_str(&mut content, &fmt_num(inset));
514 push_str(&mut content, " ");
515 push_str(&mut content, &fmt_num(clip_w));
516 push_str(&mut content, " ");
517 push_str(&mut content, &fmt_num(clip_h));
518 push_str(&mut content, " re W n\n");
519 content.extend_from_slice(&body);
520 push_str(&mut content, "Q\nEMC\n");
521
522 Some(GeneratedAppearance {
523 bbox: Rect::new(0.0, 0.0, w, h),
524 matrix: Matrix::identity(),
525 resources: build_resources(dr_fonts, &font_res_name),
526 content,
527 })
528}
529
530#[allow(clippy::too_many_arguments)]
531fn single_line_layout(
532 body: &mut Vec<u8>,
533 text: &str,
534 da: &DaInfo,
535 base_font: &str,
536 font_res_name: &str,
537 w: f64,
538 h: f64,
539 pad: f64,
540 quadding: i64,
541) {
542 let usable = (w - 2.0 * pad).max(1.0);
543 let mut size = if da.size > 0.0 {
544 da.size
545 } else {
546 let mut s = (h * 0.7).clamp(4.0, 12.0);
548 let tw = measure(text, base_font, s);
549 if tw > usable {
550 s *= usable / tw;
551 }
552 s.max(2.0)
553 };
554 if size <= 0.0 {
555 size = 12.0;
556 }
557
558 let tw = measure(text, base_font, size);
559 let x = match quadding {
560 1 => (w - tw) / 2.0, 2 => w - pad - tw, _ => pad, };
564 let y = vertical_baseline(h, size);
565
566 emit_font(body, da, font_res_name, size);
567 emit_line(body, x, y, text);
568}
569
570#[allow(clippy::too_many_arguments)]
571pub(crate) fn multiline_layout(
572 body: &mut Vec<u8>,
573 text: &str,
574 da: &DaInfo,
575 base_font: &str,
576 font_res_name: &str,
577 w: f64,
578 h: f64,
579 pad: f64,
580 quadding: i64,
581) {
582 let usable = (w - 2.0 * pad).max(1.0);
583 let usable_h = (h - 2.0 * pad).max(1.0);
584
585 let size = if da.size > 0.0 {
588 da.size
589 } else {
590 let mut s = 12.0_f64;
591 while s > 4.0 {
592 let lines = wrap_lines(text, base_font, s, usable);
593 if lines.len() as f64 * s * 1.15 <= usable_h {
594 break;
595 }
596 s -= 1.0;
597 }
598 s
599 };
600 let leading = size * 1.15;
601 let lines = wrap_lines(text, base_font, size, usable);
602
603 emit_font(body, da, font_res_name, size);
604 let mut y = h - pad - size * 0.72;
606 for line in &lines {
607 if y < -size {
608 break; }
610 let lw = measure(line, base_font, size);
611 let x = match quadding {
612 1 => (w - lw) / 2.0, 2 => w - pad - lw, _ => pad, };
616 emit_line(body, x, y, line);
617 y -= leading;
618 }
619}
620
621#[allow(clippy::too_many_arguments)]
622fn comb_layout(
623 body: &mut Vec<u8>,
624 text: &str,
625 da: &DaInfo,
626 base_font: &str,
627 font_res_name: &str,
628 w: f64,
629 h: f64,
630 field: &FormField,
631) {
632 let n = field.max_len.unwrap_or(1).max(1) as f64;
633 let cell = w / n;
634 let size = if da.size > 0.0 {
635 da.size
636 } else {
637 ((h - 4.0).min(cell)).clamp(2.0, 12.0)
638 };
639 let y = vertical_baseline(h, size);
640
641 emit_font(body, da, font_res_name, size);
642 for (i, ch) in text.chars().take(n as usize).enumerate() {
643 let s = ch.to_string();
644 let cw = measure(&s, base_font, size);
645 let x = cell * i as f64 + (cell - cw) / 2.0;
646 emit_line(body, x, y, &s);
647 }
648}
649
650fn vertical_baseline(h: f64, size: f64) -> f64 {
653 (h / 2.0 - 0.255 * size).max(0.0)
656}
657
658fn emit_font(body: &mut Vec<u8>, da: &DaInfo, font_res_name: &str, size: f64) {
660 push_str(body, &format!("{}\n", da.color_ops));
661 push_str(body, &format!("/{font_res_name} {} Tf\n", fmt_num(size)));
662}
663
664fn emit_line(body: &mut Vec<u8>, x: f64, y: f64, text: &str) {
666 push_str(body, &format!("1 0 0 1 {} {} Tm\n", fmt_num(x), fmt_num(y)));
667 body.push(b'(');
668 escape_text(text, body);
669 push_str(body, ") Tj\n");
670}
671
672fn fmt_num(v: f64) -> String {
676 if v.is_finite() {
677 format!("{v:.2}")
678 } else {
679 "0".to_string()
680 }
681}
682
683pub(crate) fn is_safe_resource_name(name: &str) -> bool {
686 !name.is_empty()
687 && name.len() <= 64
688 && name
689 .chars()
690 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '+' | '.'))
691}
692
693fn wrap_lines(text: &str, base_font: &str, size: f64, usable: f64) -> Vec<String> {
695 const MAX_LINES: usize = 1000;
698 let mut out = Vec::new();
699 for paragraph in text.split('\n') {
700 if out.len() > MAX_LINES {
701 break;
702 }
703 if paragraph.is_empty() {
704 out.push(String::new());
705 continue;
706 }
707 let mut line = String::new();
708 for word in paragraph.split(' ') {
709 let candidate = if line.is_empty() {
710 word.to_string()
711 } else {
712 format!("{line} {word}")
713 };
714 if measure(&candidate, base_font, size) <= usable || line.is_empty() {
715 line = candidate;
716 } else {
717 out.push(std::mem::take(&mut line));
718 line = word.to_string();
719 }
720 }
721 out.push(line);
722 }
723 out
724}
725
726fn measure(text: &str, base_font: &str, size: f64) -> f64 {
729 let metrics = zpdf_font::standard_fonts::lookup(base_font);
730 let mut total = 0.0;
731 for ch in text.chars() {
732 let w1000 = match metrics {
733 Some(m) => {
734 let code = unicode_to_winansi(ch).unwrap_or(b'?') as usize;
735 m.widths[code] as f64
736 }
737 None => 500.0,
738 };
739 let w1000 = if w1000 == 0.0 { 500.0 } else { w1000 };
740 total += w1000 / 1000.0 * size;
741 }
742 total
743}
744
745pub(crate) struct DaInfo {
749 pub(crate) font: Option<String>,
750 pub(crate) size: f64,
751 pub(crate) color_ops: String,
753}
754
755pub(crate) fn parse_da(da: &str) -> DaInfo {
758 let mut font = None;
759 let mut size: f64 = 0.0;
760 let mut color = String::new();
761 let mut operands: Vec<&str> = Vec::new();
762
763 for tok in da.split_whitespace() {
764 match tok {
765 "Tf" => {
766 if operands.len() >= 2 {
767 if let Some(name) = operands[operands.len() - 2].strip_prefix('/') {
768 font = Some(name.to_string());
769 }
770 size = operands[operands.len() - 1].parse().unwrap_or(0.0);
771 }
772 operands.clear();
773 }
774 "g" if !operands.is_empty() => {
775 if let Some(c) = da_color(&operands, 1, "g") {
776 color = c;
777 }
778 operands.clear();
779 }
780 "rg" if operands.len() >= 3 => {
781 if let Some(c) = da_color(&operands, 3, "rg") {
782 color = c;
783 }
784 operands.clear();
785 }
786 "k" if operands.len() >= 4 => {
787 if let Some(c) = da_color(&operands, 4, "k") {
788 color = c;
789 }
790 operands.clear();
791 }
792 other => operands.push(other),
793 }
794 }
795
796 const MAX_FONT_SIZE: f64 = 1000.0;
799 DaInfo {
800 font,
801 size: if size.is_finite() && size >= 0.0 {
802 size.min(MAX_FONT_SIZE)
803 } else {
804 0.0
805 },
806 color_ops: if color.is_empty() {
807 "0 g".to_string()
808 } else {
809 color
810 },
811 }
812}
813
814fn da_color(operands: &[&str], n: usize, op: &str) -> Option<String> {
819 let vals: Option<Vec<f64>> = operands[operands.len() - n..]
820 .iter()
821 .map(|t| {
822 t.parse::<f64>()
823 .ok()
824 .filter(|v| v.is_finite())
825 .map(|v| v.clamp(0.0, 1.0))
826 })
827 .collect();
828 let parts: Vec<String> = vals?.iter().map(|v| format!("{v:.4}")).collect();
829 Some(format!("{} {op}", parts.join(" ")))
830}
831
832pub(crate) fn resolve_base_font(dr_fonts: Option<&PdfDict>, res_name: &str) -> String {
836 if let Some(dr) = dr_fonts {
837 if let Some(PdfObject::Dict(fd)) = dr.get(res_name) {
838 if let Ok(bf) = fd.get_name("BaseFont") {
839 return strip_subset_prefix(bf).to_string();
840 }
841 }
842 }
843 acrobat_standard_name(res_name).to_string()
844}
845
846fn acrobat_standard_name(res_name: &str) -> &str {
848 match res_name {
849 "Helv" => "Helvetica",
850 "HeBO" | "HeBo" => "Helvetica-Bold",
851 "HeOb" => "Helvetica-Oblique",
852 "Cour" => "Courier",
853 "CoBO" | "CoBo" => "Courier-Bold",
854 "TiRo" => "Times-Roman",
855 "TiBo" => "Times-Bold",
856 "TiIt" => "Times-Italic",
857 "Symb" => "Symbol",
858 "ZaDb" => "ZapfDingbats",
859 other => other,
860 }
861}
862
863fn strip_subset_prefix(name: &str) -> &str {
864 name.rsplit('+').next().unwrap_or(name)
866}
867
868pub fn build_resources(dr_fonts: Option<&PdfDict>, font_res_name: &str) -> PdfDict {
871 let font_entry = dr_fonts
872 .and_then(|dr| dr.get(font_res_name).cloned())
873 .unwrap_or_else(|| PdfObject::Dict(standard_font_dict("Helvetica")));
874
875 let mut fonts = PdfDict::new();
876 fonts.insert(PdfName::new(font_res_name), font_entry);
877 let mut res = PdfDict::new();
878 res.insert(PdfName::new("Font"), PdfObject::Dict(fonts));
879 res
880}
881
882pub fn standard_font_dict(base: &str) -> PdfDict {
886 let mut d = PdfDict::new();
887 d.insert(PdfName::new("Type"), PdfObject::Name(PdfName::new("Font")));
888 d.insert(
889 PdfName::new("Subtype"),
890 PdfObject::Name(PdfName::new("Type1")),
891 );
892 d.insert(
893 PdfName::new("BaseFont"),
894 PdfObject::Name(PdfName::new(base)),
895 );
896 d.insert(
897 PdfName::new("Encoding"),
898 PdfObject::Name(PdfName::new("WinAnsiEncoding")),
899 );
900 d
901}
902
903pub fn escape_text(s: &str, out: &mut Vec<u8>) {
907 for ch in s.chars() {
908 let b = unicode_to_winansi(ch).unwrap_or(b'?');
909 match b {
910 b'\\' => out.extend_from_slice(b"\\\\"),
911 b'(' => out.extend_from_slice(b"\\("),
912 b')' => out.extend_from_slice(b"\\)"),
913 b'\r' => out.extend_from_slice(b"\\r"),
914 _ => out.push(b),
915 }
916 }
917}
918
919pub fn unicode_to_winansi(ch: char) -> Option<u8> {
924 let cp = ch as u32;
925 match cp {
926 0x20..=0x7E | 0xA0..=0xFF => Some(cp as u8),
927 0x20AC => Some(0x80),
928 0x201A => Some(0x82),
929 0x0192 => Some(0x83),
930 0x201E => Some(0x84),
931 0x2026 => Some(0x85),
932 0x2020 => Some(0x86),
933 0x2021 => Some(0x87),
934 0x02C6 => Some(0x88),
935 0x2030 => Some(0x89),
936 0x0160 => Some(0x8A),
937 0x2039 => Some(0x8B),
938 0x0152 => Some(0x8C),
939 0x017D => Some(0x8E),
940 0x2018 => Some(0x91),
941 0x2019 => Some(0x92),
942 0x201C => Some(0x93),
943 0x201D => Some(0x94),
944 0x2022 => Some(0x95),
945 0x2013 => Some(0x96),
946 0x2014 => Some(0x97),
947 0x02DC => Some(0x98),
948 0x2122 => Some(0x99),
949 0x0161 => Some(0x9A),
950 0x203A => Some(0x9B),
951 0x0153 => Some(0x9C),
952 0x017E => Some(0x9E),
953 0x0178 => Some(0x9F),
954 _ => None,
955 }
956}
957
958fn push_str(out: &mut Vec<u8>, s: &str) {
959 out.extend_from_slice(s.as_bytes());
960}
961
962fn one_line(s: &str) -> String {
964 s.chars()
965 .map(|c| {
966 if c == '\n' || c == '\r' || c == '\t' {
967 ' '
968 } else {
969 c
970 }
971 })
972 .collect()
973}
974
975fn deref(file: &PdfFile, obj: &PdfObject) -> PdfObject {
981 match obj {
982 PdfObject::Ref(r) => file.resolve(*r).unwrap_or(PdfObject::Null),
983 other => other.clone(),
984 }
985}
986
987fn deref_opt(file: &PdfFile, obj: Option<&PdfObject>) -> Option<PdfObject> {
988 obj.map(|o| deref(file, o))
989}
990
991fn deref_array(file: &PdfFile, obj: Option<&PdfObject>) -> Option<Vec<PdfObject>> {
992 match deref(file, obj?) {
993 PdfObject::Array(a) => Some(a),
994 _ => None,
995 }
996}
997
998fn text_string(file: &PdfFile, obj: &PdfObject) -> Option<String> {
1000 match deref(file, obj) {
1001 PdfObject::String(s) => Some(pdf_string_to_unicode(s.as_bytes())),
1002 _ => None,
1003 }
1004}
1005
1006fn field_value(file: &PdfFile, obj: Option<&PdfObject>) -> Option<FieldValue> {
1007 match deref(file, obj?) {
1008 PdfObject::String(s) => Some(FieldValue::Text(pdf_string_to_unicode(s.as_bytes()))),
1009 PdfObject::Name(n) => Some(FieldValue::Name(n.0)),
1010 PdfObject::Array(a) => {
1011 let items: Vec<String> = a
1012 .iter()
1013 .filter_map(|o| match o {
1014 PdfObject::String(s) => Some(pdf_string_to_unicode(s.as_bytes())),
1015 _ => None,
1016 })
1017 .collect();
1018 (!items.is_empty()).then_some(FieldValue::List(items))
1019 }
1020 _ => None,
1021 }
1022}
1023
1024fn int_value(file: &PdfFile, obj: Option<&PdfObject>) -> Option<i64> {
1025 match deref(file, obj?) {
1026 PdfObject::Integer(n) => Some(n),
1027 PdfObject::Real(r) => Some(r as i64),
1028 _ => None,
1029 }
1030}
1031
1032pub(crate) fn pdf_string_to_unicode(bytes: &[u8]) -> String {
1035 if bytes.len() >= 2 && bytes[0] == 0xFE && bytes[1] == 0xFF {
1036 let units: Vec<u16> = bytes[2..]
1037 .chunks_exact(2)
1038 .map(|c| u16::from_be_bytes([c[0], c[1]]))
1039 .collect();
1040 String::from_utf16_lossy(&units)
1041 } else {
1042 bytes.iter().map(|&b| b as char).collect()
1043 }
1044}
1045
1046#[cfg(test)]
1047mod tests {
1048 use super::*;
1049 use crate::test_util::build_pdf;
1050 use crate::PdfDocument;
1051
1052 #[test]
1053 fn field_tree_names_inheritance_and_widgets() {
1054 let doc = PdfDocument::open(build_pdf(&[
1055 "<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>",
1056 "<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
1057 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] >>",
1058 "<< /Fields [5 0 R] /DA (/Helv 0 Tf 0 g) /DR << /Font << /Helv 8 0 R >> >> >>",
1059 "<< /T (address) /FT /Tx /Kids [6 0 R 7 0 R] >>",
1061 "<< /T (street) /V (Main St) >>",
1062 "<< /T (city) /V (Springfield) /Q 1 >>",
1063 "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>",
1064 ]))
1065 .expect("open");
1066
1067 let form = doc.acro_form().expect("acroform");
1068 assert!(!form.need_appearances);
1069 assert!(form.dr_fonts.is_some());
1070 assert_eq!(form.fields.len(), 2);
1071
1072 let street = &form.fields[0];
1073 assert_eq!(street.name, "address.street");
1074 assert_eq!(street.kind, FieldKind::Text); assert_eq!(street.value, Some(FieldValue::Text("Main St".into())));
1076 assert_eq!(street.default_appearance.as_deref(), Some("/Helv 0 Tf 0 g")); assert_eq!(street.quadding, 0);
1078 assert_eq!(street.widgets, vec![ObjectId(6, 0)]);
1080 assert_eq!(
1081 form.field_for_widget(ObjectId(6, 0))
1082 .map(|f| f.name.as_str()),
1083 Some("address.street")
1084 );
1085
1086 let city = &form.fields[1];
1087 assert_eq!(city.name, "address.city");
1088 assert_eq!(city.quadding, 1); }
1090
1091 #[test]
1092 fn single_widget_field_and_button_value() {
1093 let doc = PdfDocument::open(build_pdf(&[
1094 "<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>",
1095 "<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
1096 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Annots [5 0 R] >>",
1097 "<< /Fields [5 0 R] /NeedAppearances true >>",
1098 "<< /T (agree) /FT /Btn /V /Yes /AS /Yes /Subtype /Widget /Rect [10 10 30 30] >>",
1100 ]))
1101 .expect("open");
1102
1103 let form = doc.acro_form().expect("acroform");
1104 assert!(form.need_appearances);
1105 assert_eq!(form.fields.len(), 1);
1106 let f = &form.fields[0];
1107 assert_eq!(f.name, "agree");
1108 assert_eq!(f.kind, FieldKind::Button);
1109 assert_eq!(f.value, Some(FieldValue::Name("Yes".into())));
1110 assert!(generate_widget_appearance(f, Rect::new(10.0, 10.0, 30.0, 30.0), None).is_none());
1112 }
1113
1114 #[test]
1115 fn no_acroform_returns_none() {
1116 let doc = PdfDocument::open(build_pdf(&[
1117 "<< /Type /Catalog /Pages 2 0 R >>",
1118 "<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
1119 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] >>",
1120 ]))
1121 .expect("open");
1122 assert!(doc.acro_form().is_none());
1123 }
1124
1125 #[test]
1126 fn da_parsing_extracts_font_size_color() {
1127 let da = parse_da("0 0 1 rg /Helv 12 Tf");
1129 assert_eq!(da.font.as_deref(), Some("Helv"));
1130 assert_eq!(da.size, 12.0);
1131 assert_eq!(da.color_ops, "0.0000 0.0000 1.0000 rg");
1132
1133 let da = parse_da("/Cour 0 Tf 0.2 g");
1134 assert_eq!(da.font.as_deref(), Some("Cour"));
1135 assert_eq!(da.size, 0.0);
1136 assert_eq!(da.color_ops, "0.2000 g");
1137
1138 let da = parse_da("/Helv 10 Tf");
1140 assert_eq!(da.color_ops, "0 g");
1141
1142 let da = parse_da("/Helv 1e308 Tf");
1144 assert_eq!(da.size, 1000.0);
1145 let da = parse_da("1)Tj/Evil 0 0 rg /Helv 10 Tf");
1146 assert_eq!(da.color_ops, "0 g"); }
1148
1149 #[test]
1150 fn winansi_punctuation_round_trips() {
1151 assert_eq!(unicode_to_winansi('\u{2019}'), Some(0x92));
1153 assert_eq!(unicode_to_winansi('\u{2014}'), Some(0x97));
1154 assert_eq!(unicode_to_winansi('\u{20AC}'), Some(0x80));
1155 assert_eq!(unicode_to_winansi('A'), Some(0x41));
1156 assert_eq!(unicode_to_winansi('\u{00E9}'), Some(0xE9)); assert_eq!(unicode_to_winansi('\u{4E2D}'), None); }
1159
1160 #[test]
1161 fn non_finite_numbers_never_reach_output() {
1162 assert_eq!(fmt_num(f64::INFINITY), "0");
1163 assert_eq!(fmt_num(f64::NAN), "0");
1164 assert_eq!(fmt_num(-1.5), "-1.50");
1165 }
1166
1167 #[test]
1168 fn utf16be_value_is_decoded() {
1169 let bytes = [0xFE, 0xFF, 0x00, b'H', 0x00, b'i'];
1171 assert_eq!(pdf_string_to_unicode(&bytes), "Hi");
1172 }
1173
1174 #[test]
1175 fn escape_handles_parens_and_backslash() {
1176 let mut out = Vec::new();
1177 escape_text("a(b)\\c", &mut out);
1178 assert_eq!(out, b"a\\(b\\)\\\\c");
1179 }
1180
1181 #[test]
1182 fn standard_name_mapping() {
1183 assert_eq!(acrobat_standard_name("Helv"), "Helvetica");
1184 assert_eq!(acrobat_standard_name("ZaDb"), "ZapfDingbats");
1185 assert_eq!(acrobat_standard_name("F1"), "F1");
1186 }
1187
1188 #[test]
1189 fn choice_value_maps_export_to_display_label() {
1190 let f = FormField {
1191 field_id: ObjectId(0, 0),
1192 name: "month".into(),
1193 kind: FieldKind::Choice,
1194 flags: 0,
1195 value: Some(FieldValue::Text("01".into())),
1196 default_appearance: None,
1197 quadding: 0,
1198 max_len: None,
1199 options: vec![
1200 ("01".into(), "January".into()),
1201 ("02".into(), "February".into()),
1202 ],
1203 widgets: vec![],
1204 };
1205 assert_eq!(f.display_value().as_deref(), Some("January"));
1207 let f2 = FormField {
1209 value: Some(FieldValue::Text("99".into())),
1210 ..f
1211 };
1212 assert_eq!(f2.display_value().as_deref(), Some("99"));
1213 }
1214
1215 #[test]
1216 fn comb_is_suppressed_when_multiline() {
1217 let base = FormField {
1218 field_id: ObjectId(0, 0),
1219 name: "x".into(),
1220 kind: FieldKind::Text,
1221 flags: FF_COMB | FF_MULTILINE,
1222 value: Some(FieldValue::Text("AB".into())),
1223 default_appearance: None,
1224 quadding: 0,
1225 max_len: Some(4),
1226 options: vec![],
1227 widgets: vec![],
1228 };
1229 assert!(!base.is_comb());
1231 assert!(base.is_multiline());
1232 }
1233
1234 #[test]
1235 fn comb_field_detection() {
1236 let f = FormField {
1237 field_id: ObjectId(0, 0),
1238 name: "x".into(),
1239 kind: FieldKind::Text,
1240 flags: FF_COMB,
1241 value: Some(FieldValue::Text("AB".into())),
1242 default_appearance: None,
1243 quadding: 0,
1244 max_len: Some(4),
1245 options: vec![],
1246 widgets: vec![],
1247 };
1248 assert!(f.is_comb());
1249 let f2 = FormField {
1251 max_len: None,
1252 ..f.clone()
1253 };
1254 assert!(!f2.is_comb());
1255 }
1256
1257 #[test]
1258 fn generated_appearance_draws_value() {
1259 let f = FormField {
1260 field_id: ObjectId(0, 0),
1261 name: "name".into(),
1262 kind: FieldKind::Text,
1263 flags: 0,
1264 value: Some(FieldValue::Text("Test".into())),
1265 default_appearance: Some("/Helv 12 Tf 0 g".into()),
1266 quadding: 0,
1267 max_len: None,
1268 options: vec![],
1269 widgets: vec![],
1270 };
1271 let ap = generate_widget_appearance(&f, Rect::new(0.0, 0.0, 200.0, 40.0), None)
1272 .expect("appearance");
1273 assert_eq!(ap.bbox, Rect::new(0.0, 0.0, 200.0, 40.0));
1274 let s = String::from_utf8_lossy(&ap.content);
1275 assert!(s.contains("/Tx BMC"));
1276 assert!(s.contains("Tf"));
1277 assert!(s.contains("(Test) Tj"));
1278 assert!(ap.resources.get("Font").is_some());
1280 }
1281
1282 #[test]
1283 fn empty_and_button_values_generate_nothing() {
1284 let base = FormField {
1285 field_id: ObjectId(0, 0),
1286 name: "x".into(),
1287 kind: FieldKind::Text,
1288 flags: 0,
1289 value: Some(FieldValue::Text(String::new())),
1290 default_appearance: None,
1291 quadding: 0,
1292 max_len: None,
1293 options: vec![],
1294 widgets: vec![],
1295 };
1296 assert!(
1297 generate_widget_appearance(&base, Rect::new(0.0, 0.0, 100.0, 20.0), None).is_none()
1298 );
1299
1300 let button = FormField {
1301 kind: FieldKind::Button,
1302 value: Some(FieldValue::Name("Yes".into())),
1303 ..base.clone()
1304 };
1305 assert!(
1306 generate_widget_appearance(&button, Rect::new(0.0, 0.0, 100.0, 20.0), None).is_none()
1307 );
1308
1309 let password = FormField {
1310 flags: FF_PASSWORD,
1311 value: Some(FieldValue::Text("secret".into())),
1312 ..base
1313 };
1314 assert!(
1315 generate_widget_appearance(&password, Rect::new(0.0, 0.0, 100.0, 20.0), None).is_none()
1316 );
1317 }
1318}