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