1use crate::appearance::{parse_da, DefaultAppearance};
38use crate::encoding::{encode_winansi, escape_string_bytes};
39use crate::metrics::StandardFace;
40use lopdf::{dictionary, Dictionary, Document, Object, ObjectId, Stream};
41use std::io::Write as _;
42
43const MAX_DEPTH: usize = 100;
45
46const TEXT_INSET: f32 = 2.0;
49
50#[derive(Debug, thiserror::Error)]
52pub enum WritebackError {
53 #[error("form field '{0}' not found")]
55 FieldNotFound(String),
56 #[error("form field '{0}' is read-only")]
58 ReadOnly(String),
59 #[error("form field '{name}' is a {actual} field; cannot apply a {requested} value")]
61 WrongType {
62 name: String,
64 actual: &'static str,
66 requested: &'static str,
68 },
69 #[error("value '{value}' is not a valid option for field '{name}'")]
72 InvalidOption {
73 name: String,
75 value: String,
77 },
78 #[error("malformed form structure: {0}")]
80 Malformed(String),
81}
82
83#[derive(Debug, Clone, Copy)]
85pub enum WriteValue<'a> {
86 Text(&'a str),
88 Checkbox(bool),
91 Radio(&'a str),
96 Choice(&'a str),
98}
99
100impl WriteValue<'_> {
101 fn kind(&self) -> &'static str {
102 match self {
103 WriteValue::Text(_) => "text",
104 WriteValue::Checkbox(_) => "checkbox",
105 WriteValue::Radio(_) => "radio",
106 WriteValue::Choice(_) => "choice",
107 }
108 }
109}
110
111#[derive(Debug, Default, Clone)]
113pub struct WriteOutcome {
114 pub appearances_generated: usize,
116 pub appearance_states_set: usize,
118 pub need_appearances_fallback: bool,
120}
121
122#[derive(Debug, Clone)]
128pub(crate) struct Located {
129 pub(crate) id: ObjectId,
130 pub(crate) fqn: String,
131 pub(crate) kids: Vec<ObjectId>,
133}
134
135fn acroform_id(doc: &Document) -> Result<ObjectId, WritebackError> {
136 let catalog = doc
137 .catalog()
138 .map_err(|_| WritebackError::Malformed("document has no catalog".into()))?;
139 match catalog.get(b"AcroForm") {
140 Ok(Object::Reference(id)) => Ok(*id),
141 Ok(Object::Dictionary(_)) => Err(WritebackError::Malformed(
142 "inline /AcroForm dictionary; promote it first via ensure_indirect_acroform".into(),
143 )),
144 _ => Err(WritebackError::Malformed(
145 "document has no /AcroForm dictionary".into(),
146 )),
147 }
148}
149
150pub(crate) fn ensure_indirect_acroform(doc: &mut Document) {
159 let inline = match doc.catalog() {
160 Ok(catalog) => match catalog.get(b"AcroForm") {
161 Ok(Object::Dictionary(d)) => Some(d.clone()),
162 _ => None,
163 },
164 Err(_) => None,
165 };
166 if let Some(dict) = inline {
167 let id = doc.add_object(Object::Dictionary(dict));
168 if let Ok(catalog) = doc.catalog_mut() {
169 catalog.set("AcroForm", Object::Reference(id));
170 }
171 }
172}
173
174pub(crate) fn collect_fields(doc: &Document) -> Result<Vec<Located>, WritebackError> {
177 let af_id = acroform_id(doc)?;
178 let af = doc
179 .get_object(af_id)
180 .and_then(|o| o.as_dict())
181 .map_err(|_| WritebackError::Malformed("/AcroForm is not a dictionary".into()))?;
182 let fields = match af.get(b"Fields") {
183 Ok(Object::Array(arr)) => arr.clone(),
184 Ok(Object::Reference(id)) => match doc.get_object(*id) {
185 Ok(Object::Array(arr)) => arr.clone(),
186 _ => return Err(WritebackError::Malformed("/Fields is not an array".into())),
187 },
188 _ => return Err(WritebackError::Malformed("/AcroForm has no /Fields".into())),
189 };
190
191 let mut out = Vec::new();
192 let mut visited = std::collections::BTreeSet::new();
193 for entry in &fields {
194 if let Object::Reference(id) = entry {
195 walk_field(doc, *id, String::new(), 0, &mut visited, &mut out);
196 }
197 }
198 Ok(out)
199}
200
201fn walk_field(
202 doc: &Document,
203 id: ObjectId,
204 prefix: String,
205 depth: usize,
206 visited: &mut std::collections::BTreeSet<ObjectId>,
207 out: &mut Vec<Located>,
208) {
209 if depth >= MAX_DEPTH || !visited.insert(id) {
210 return;
211 }
212 let Ok(dict) = doc.get_object(id).and_then(|o| o.as_dict()) else {
213 return;
214 };
215 let partial = dict
216 .get(b"T")
217 .ok()
218 .and_then(|o| lopdf::decode_text_string(o).ok())
219 .unwrap_or_default();
220 let fqn = match (prefix.is_empty(), partial.is_empty()) {
221 (true, _) => partial.clone(),
222 (false, true) => prefix.clone(),
223 (false, false) => format!("{prefix}.{partial}"),
224 };
225
226 let mut kid_ids = Vec::new();
227 if let Ok(Object::Array(kids)) = dict.get(b"Kids") {
228 for kid in kids {
229 if let Object::Reference(kid_id) = kid {
230 kid_ids.push(*kid_id);
231 let is_nested_field = doc
236 .get_object(*kid_id)
237 .and_then(|o| o.as_dict())
238 .map(|d| d.get(b"T").is_ok())
239 .unwrap_or(false);
240 if is_nested_field {
241 walk_field(doc, *kid_id, fqn.clone(), depth + 1, visited, out);
242 }
243 }
244 }
245 }
246 out.push(Located {
247 id,
248 fqn,
249 kids: kid_ids,
250 });
251}
252
253pub(crate) fn inherited<'a>(doc: &'a Document, dict: &'a Dictionary, key: &[u8]) -> Option<Object> {
255 let mut current: Option<&Dictionary> = Some(dict);
256 for _ in 0..MAX_DEPTH {
257 let d = current?;
258 if let Ok(v) = d.get(key) {
259 return Some(v.clone());
260 }
261 current = match d.get(b"Parent") {
262 Ok(Object::Reference(pid)) => doc.get_object(*pid).and_then(|o| o.as_dict()).ok(),
263 _ => None,
264 };
265 }
266 None
267}
268
269fn effective_flags(doc: &Document, dict: &Dictionary) -> i64 {
270 match inherited(doc, dict, b"Ff") {
271 Some(Object::Integer(i)) => i,
272 _ => 0,
273 }
274}
275
276pub(crate) fn effective_field_type(doc: &Document, dict: &Dictionary) -> Option<Vec<u8>> {
277 match inherited(doc, dict, b"FT") {
278 Some(Object::Name(n)) => Some(n),
279 _ => None,
280 }
281}
282
283fn widget_ids(doc: &Document, located: &Located) -> Vec<ObjectId> {
286 let mut out = Vec::new();
287 for &kid in &located.kids {
288 if let Ok(d) = doc.get_object(kid).and_then(|o| o.as_dict()) {
289 if d.get(b"T").is_err() {
294 out.push(kid);
295 }
296 }
297 }
298 if out.is_empty() {
299 out.push(located.id);
300 }
301 out
302}
303
304fn on_state_of_widget(doc: &Document, widget: ObjectId) -> Option<Vec<u8>> {
306 let dict = doc.get_object(widget).and_then(|o| o.as_dict()).ok()?;
307 let n = appearance_normal_dict(doc, dict)?;
308 n.iter()
309 .map(|(k, _)| k.clone())
310 .find(|k| k.as_slice() != b"Off")
311}
312
313fn appearance_normal_dict<'a>(doc: &'a Document, dict: &'a Dictionary) -> Option<&'a Dictionary> {
315 let ap = match dict.get(b"AP").ok()? {
316 Object::Reference(id) => doc.get_object(*id).and_then(|o| o.as_dict()).ok()?,
317 Object::Dictionary(d) => d,
318 _ => return None,
319 };
320 match ap.get(b"N").ok()? {
321 Object::Reference(id) => doc.get_object(*id).and_then(|o| o.as_dict()).ok(),
322 Object::Dictionary(d) => Some(d),
323 _ => None,
324 }
325}
326
327fn widget_has_state(doc: &Document, widget: ObjectId, state: &[u8]) -> bool {
329 let Ok(dict) = doc.get_object(widget).and_then(|o| o.as_dict()) else {
330 return false;
331 };
332 match appearance_normal_dict(doc, dict) {
333 Some(n) => n.iter().any(|(k, _)| k.as_slice() == state),
334 None => false,
335 }
336}
337
338fn set_widget_as(doc: &mut Document, widget: ObjectId, state: &[u8]) -> bool {
340 if let Ok(Object::Dictionary(d)) = doc.get_object_mut(widget) {
341 d.set("AS", Object::Name(state.to_vec()));
342 true
343 } else {
344 false
345 }
346}
347
348pub fn apply_field_value(
358 doc: &mut Document,
359 name: &str,
360 value: WriteValue<'_>,
361) -> Result<WriteOutcome, WritebackError> {
362 ensure_indirect_acroform(doc);
363 let fields = collect_fields(doc)?;
364 let located = fields
365 .iter()
366 .find(|l| l.fqn == name)
367 .cloned()
368 .ok_or_else(|| WritebackError::FieldNotFound(name.to_string()))?;
369
370 let dict = doc
371 .get_object(located.id)
372 .and_then(|o| o.as_dict())
373 .map_err(|_| WritebackError::Malformed("field object is not a dictionary".into()))?;
374
375 if effective_flags(doc, dict) & 0x1 != 0 {
377 return Err(WritebackError::ReadOnly(name.to_string()));
378 }
379
380 let ft = effective_field_type(doc, dict).unwrap_or_else(|| b"Tx".to_vec());
381 let flags = effective_flags(doc, dict);
382
383 match (ft.as_slice(), value) {
384 (b"Tx", WriteValue::Text(text)) => apply_text(doc, &located, text),
385 (b"Btn", WriteValue::Checkbox(on)) if flags & 0x18000 == 0 => {
386 apply_checkbox(doc, &located, on)
387 }
388 (b"Btn", WriteValue::Radio(export)) if flags & 0x8000 != 0 => {
389 apply_radio(doc, &located, name, export)
390 }
391 (b"Ch", WriteValue::Choice(text)) => apply_choice(doc, &located, name, text, flags),
392 (b"Sig", _) => Err(WritebackError::WrongType {
393 name: name.to_string(),
394 actual: "signature",
395 requested: value.kind(),
396 }),
397 (actual_ft, v) => Err(WritebackError::WrongType {
398 name: name.to_string(),
399 actual: match actual_ft {
400 b"Tx" => "text",
401 b"Btn" if flags & 0x10000 != 0 => "push-button",
402 b"Btn" if flags & 0x8000 != 0 => "radio",
403 b"Btn" => "checkbox",
404 b"Ch" => "choice",
405 _ => "unknown",
406 },
407 requested: v.kind(),
408 }),
409 }
410}
411
412pub fn apply_choice_multi(
427 doc: &mut Document,
428 name: &str,
429 values: &[String],
430) -> Result<WriteOutcome, WritebackError> {
431 ensure_indirect_acroform(doc);
432 let fields = collect_fields(doc)?;
433 let located = fields
434 .iter()
435 .find(|l| l.fqn == name)
436 .cloned()
437 .ok_or_else(|| WritebackError::FieldNotFound(name.to_string()))?;
438
439 let dict = doc
440 .get_object(located.id)
441 .and_then(|o| o.as_dict())
442 .map_err(|_| WritebackError::Malformed("field object is not a dictionary".into()))?;
443
444 if effective_flags(doc, dict) & 0x1 != 0 {
445 return Err(WritebackError::ReadOnly(name.to_string()));
446 }
447
448 let ft = effective_field_type(doc, dict).unwrap_or_else(|| b"Tx".to_vec());
449 let flags = effective_flags(doc, dict);
450 if ft.as_slice() != b"Ch" || flags & 0x200000 == 0 {
452 return Err(WritebackError::WrongType {
453 name: name.to_string(),
454 actual: match ft.as_slice() {
455 b"Ch" => "single-select choice",
456 b"Tx" => "text",
457 b"Btn" => "button",
458 _ => "unknown",
459 },
460 requested: "multi-select choice",
461 });
462 }
463
464 let options = {
466 let d = field_dict(doc, &located)?;
467 choice_options(doc, d)
468 };
469 let editable = flags & 0x40000 != 0;
470 if !editable {
471 for v in values {
472 let known = options
473 .iter()
474 .any(|(export, display)| export == v || display == v);
475 if !known {
476 return Err(WritebackError::InvalidOption {
477 name: name.to_string(),
478 value: v.clone(),
479 });
480 }
481 }
482 }
483
484 let v_obj = Object::Array(values.iter().map(|s| lopdf::text_string(s)).collect());
486 set_field_v(doc, located.id, v_obj)?;
487
488 let mut indices: Vec<i64> = values
492 .iter()
493 .filter_map(|v| {
494 options
495 .iter()
496 .position(|(export, display)| export == v || display == v)
497 .map(|i| i as i64)
498 })
499 .collect();
500 indices.sort_unstable();
501 indices.dedup();
502 if let Ok(Object::Dictionary(d)) = doc.get_object_mut(located.id) {
503 if indices.is_empty() {
504 d.remove(b"I");
507 } else {
508 d.set(
509 b"I".to_vec(),
510 Object::Array(indices.into_iter().map(Object::Integer).collect()),
511 );
512 }
513 }
514 let selected: Vec<usize> = values
518 .iter()
519 .filter_map(|v| {
520 options
521 .iter()
522 .position(|(export, display)| export == v || display == v)
523 })
524 .collect();
525 let mut outcome = WriteOutcome::default();
526 match generate_listbox_widget_appearances(doc, &located, &options, &selected) {
527 Ok(n) => outcome.appearances_generated = n,
528 Err(NotWinAnsi) => {
529 remove_widget_appearances(doc, &located);
530 set_need_appearances(doc, true)?;
531 outcome.need_appearances_fallback = true;
532 }
533 }
534 Ok(outcome)
535}
536
537pub fn regenerate_appearances(doc: &mut Document) -> Result<WriteOutcome, WritebackError> {
546 ensure_indirect_acroform(doc);
547 let fields = collect_fields(doc)?;
548 let mut outcome = WriteOutcome::default();
549 for located in &fields {
550 let Ok(dict) = doc.get_object(located.id).and_then(|o| o.as_dict()) else {
551 continue;
552 };
553 let has_field_kids = located.kids.iter().any(|&k| {
555 doc.get_object(k)
556 .and_then(|o| o.as_dict())
557 .map(|d| d.get(b"T").is_ok())
558 .unwrap_or(false)
559 });
560 if has_field_kids {
561 continue;
562 }
563 let ft = effective_field_type(doc, dict).unwrap_or_default();
564 if ft != b"Tx" && ft != b"Ch" {
565 continue;
566 }
567 if ft == b"Ch" && effective_flags(doc, dict) & COMBO_FLAG == 0 {
568 let options = choice_options(doc, dict);
570 let selected = listbox_selection(doc, dict, &options);
571 if options.is_empty() || selected.is_empty() {
572 continue;
573 }
574 match generate_listbox_widget_appearances(doc, located, &options, &selected) {
575 Ok(n) => outcome.appearances_generated += n,
576 Err(_) => outcome.need_appearances_fallback = true,
577 }
578 continue;
579 }
580 let value = match inherited(doc, dict, b"V") {
581 Some(v @ Object::String(..)) => lopdf::decode_text_string(&v).unwrap_or_default(),
582 _ => continue,
583 };
584 if value.is_empty() {
585 continue;
586 }
587 match generate_text_widget_appearances(doc, located, &value) {
588 Ok(n) => outcome.appearances_generated += n,
589 Err(_) => outcome.need_appearances_fallback = true,
590 }
591 }
592 Ok(outcome)
593}
594
595fn apply_text(
600 doc: &mut Document,
601 located: &Located,
602 text: &str,
603) -> Result<WriteOutcome, WritebackError> {
604 apply_text_with_display(doc, located, text, None)
605}
606
607pub(crate) fn apply_text_with_display(
611 doc: &mut Document,
612 located: &Located,
613 text: &str,
614 display: Option<&str>,
615) -> Result<WriteOutcome, WritebackError> {
616 let max_len = {
618 let dict = field_dict(doc, located)?;
619 match inherited(doc, dict, b"MaxLen") {
620 Some(Object::Integer(n)) if n >= 0 => Some(n as usize),
621 _ => None,
622 }
623 };
624 let text: String = match max_len {
625 Some(n) => text.chars().take(n).collect(),
626 None => text.to_string(),
627 };
628
629 set_field_v(doc, located.id, lopdf::text_string(&text))?;
630
631 let mut outcome = WriteOutcome::default();
632 match generate_text_widget_appearances(doc, located, display.unwrap_or(&text)) {
633 Ok(n) => outcome.appearances_generated = n,
634 Err(NotWinAnsi) => {
635 remove_widget_appearances(doc, located);
638 set_need_appearances(doc, true)?;
639 outcome.need_appearances_fallback = true;
640 }
641 }
642 Ok(outcome)
643}
644
645fn apply_checkbox(
646 doc: &mut Document,
647 located: &Located,
648 on: bool,
649) -> Result<WriteOutcome, WritebackError> {
650 let widgets = widget_ids(doc, located);
651 let on_state = widgets
652 .iter()
653 .find_map(|&w| on_state_of_widget(doc, w))
654 .unwrap_or_else(|| b"Yes".to_vec());
655 let state: &[u8] = if on { &on_state } else { b"Off" };
656
657 set_field_v(doc, located.id, Object::Name(state.to_vec()))?;
658
659 let mut outcome = WriteOutcome::default();
660 for &w in &widgets {
661 let has_substates = {
667 let dict = doc.get_object(w).and_then(|o| o.as_dict()).ok();
668 dict.and_then(|d| appearance_normal_dict(doc, d)).is_some()
669 };
670 let on_here = on && (!has_substates || widget_has_state(doc, w, state));
671 let widget_state: &[u8] = if on_here { state } else { b"Off" };
672 if set_widget_as(doc, w, widget_state) {
673 outcome.appearance_states_set += 1;
674 }
675 }
676 Ok(outcome)
677}
678
679fn apply_radio(
680 doc: &mut Document,
681 located: &Located,
682 name: &str,
683 export: &str,
684) -> Result<WriteOutcome, WritebackError> {
685 let widgets = widget_ids(doc, located);
686
687 let candidates: Vec<Vec<u8>> = {
691 let mut c = vec![export.as_bytes().to_vec()];
692 if let Some(w) = encode_winansi(export) {
693 if w != export.as_bytes() {
694 c.push(w);
695 }
696 }
697 c
698 };
699 let any_substates = widgets.iter().any(|&w| {
705 doc.get_object(w)
706 .and_then(|o| o.as_dict())
707 .ok()
708 .and_then(|d| appearance_normal_dict(doc, d))
709 .is_some()
710 });
711 let chosen = if any_substates {
712 candidates
713 .into_iter()
714 .find(|cand| widgets.iter().any(|&w| widget_has_state(doc, w, cand)))
715 .ok_or_else(|| WritebackError::InvalidOption {
716 name: name.to_string(),
717 value: export.to_string(),
718 })?
719 } else {
720 export.as_bytes().to_vec()
721 };
722
723 set_field_v(doc, located.id, Object::Name(chosen.clone()))?;
725
726 let mut outcome = WriteOutcome::default();
727 for &w in &widgets {
728 let state: &[u8] = if widget_has_state(doc, w, &chosen) {
729 &chosen
730 } else {
731 b"Off"
732 };
733 if set_widget_as(doc, w, state) {
734 outcome.appearance_states_set += 1;
735 }
736 }
737 Ok(outcome)
738}
739
740fn apply_choice(
741 doc: &mut Document,
742 located: &Located,
743 name: &str,
744 text: &str,
745 flags: i64,
746) -> Result<WriteOutcome, WritebackError> {
747 let editable = flags & 0x40000 != 0;
749 let options = {
750 let dict = field_dict(doc, located)?;
751 choice_options(doc, dict)
752 };
753 if !editable && !options.is_empty() {
754 let known = options
755 .iter()
756 .any(|(export, display)| export == text || display == text);
757 if !known {
758 return Err(WritebackError::InvalidOption {
759 name: name.to_string(),
760 value: text.to_string(),
761 });
762 }
763 }
764
765 set_field_v(doc, located.id, lopdf::text_string(text))?;
766 if let Ok(Object::Dictionary(d)) = doc.get_object_mut(located.id) {
768 d.remove(b"I");
769 }
770
771 let mut outcome = WriteOutcome::default();
772 if flags & COMBO_FLAG == 0 {
773 let selected: Vec<usize> = options
775 .iter()
776 .position(|(export, display)| export == text || display == text)
777 .into_iter()
778 .collect();
779 match generate_listbox_widget_appearances(doc, located, &options, &selected) {
780 Ok(n) => outcome.appearances_generated = n,
781 Err(NotWinAnsi) => {
782 remove_widget_appearances(doc, located);
783 set_need_appearances(doc, true)?;
784 outcome.need_appearances_fallback = true;
785 }
786 }
787 return Ok(outcome);
788 }
789 let display = options
791 .iter()
792 .find(|(export, _)| export == text)
793 .map(|(_, d)| d.clone())
794 .unwrap_or_else(|| text.to_string());
795 match generate_text_widget_appearances(doc, located, &display) {
796 Ok(n) => outcome.appearances_generated = n,
797 Err(NotWinAnsi) => {
798 remove_widget_appearances(doc, located);
799 set_need_appearances(doc, true)?;
800 outcome.need_appearances_fallback = true;
801 }
802 }
803 Ok(outcome)
804}
805
806fn field_dict<'a>(doc: &'a Document, located: &Located) -> Result<&'a Dictionary, WritebackError> {
811 doc.get_object(located.id)
812 .and_then(|o| o.as_dict())
813 .map_err(|_| WritebackError::Malformed("field object vanished".into()))
814}
815
816fn set_field_v(doc: &mut Document, id: ObjectId, value: Object) -> Result<(), WritebackError> {
817 match doc.get_object_mut(id) {
818 Ok(Object::Dictionary(d)) => {
819 d.set("V", value);
820 Ok(())
821 }
822 _ => Err(WritebackError::Malformed(
823 "field object is not mutable".into(),
824 )),
825 }
826}
827
828fn set_need_appearances(doc: &mut Document, value: bool) -> Result<(), WritebackError> {
829 let af_id = acroform_id(doc)?;
830 if let Ok(Object::Dictionary(d)) = doc.get_object_mut(af_id) {
831 d.set("NeedAppearances", Object::Boolean(value));
832 }
833 Ok(())
834}
835
836fn remove_widget_appearances(doc: &mut Document, located: &Located) {
837 for w in widget_ids(doc, located) {
838 if let Ok(Object::Dictionary(d)) = doc.get_object_mut(w) {
839 d.remove(b"AP");
840 }
841 }
842}
843
844fn choice_options(doc: &Document, dict: &Dictionary) -> Vec<(String, String)> {
846 let Some(Object::Array(arr)) = inherited(doc, dict, b"Opt") else {
847 return Vec::new();
848 };
849 arr.iter()
850 .filter_map(|o| {
851 let resolved = match o {
852 Object::Reference(id) => doc.get_object(*id).ok()?,
853 other => other,
854 };
855 match resolved {
856 Object::String(..) => {
857 let s = lopdf::decode_text_string(resolved).ok()?;
858 Some((s.clone(), s))
859 }
860 Object::Array(pair) if pair.len() >= 2 => {
861 let export = lopdf::decode_text_string(&pair[0]).ok()?;
862 let display = lopdf::decode_text_string(&pair[1]).ok()?;
863 Some((export, display))
864 }
865 _ => None,
866 }
867 })
868 .collect()
869}
870
871struct NotWinAnsi;
877
878fn generate_text_widget_appearances(
881 doc: &mut Document,
882 located: &Located,
883 text: &str,
884) -> Result<usize, NotWinAnsi> {
885 let encoded_lines: Vec<Vec<u8>> = {
888 let mut lines = Vec::new();
889 for line in text.split('\n') {
890 let line = line.strip_suffix('\r').unwrap_or(line);
891 lines.push(encode_winansi(line).ok_or(NotWinAnsi)?);
892 }
893 lines
894 };
895
896 let Ok(dict) = doc.get_object(located.id).and_then(|o| o.as_dict()) else {
897 return Ok(0);
898 };
899 let flags = effective_flags(doc, dict);
900 let da_string = match inherited(doc, dict, b"DA") {
901 Some(v @ Object::String(..)) => lopdf::decode_text_string(&v).unwrap_or_default(),
902 _ => acroform_da(doc).unwrap_or_default(),
903 };
904 let da = parse_da(if da_string.is_empty() {
905 "/Helv 0 Tf 0 g"
906 } else {
907 &da_string
908 });
909 let quadding = match inherited(doc, dict, b"Q") {
910 Some(Object::Integer(1)) => 1u8,
911 Some(Object::Integer(2)) => 2u8,
912 _ => 0u8,
913 };
914 let max_len = match inherited(doc, dict, b"MaxLen") {
915 Some(Object::Integer(n)) if n > 0 => Some(n as u32),
916 _ => None,
917 };
918 let multiline = flags & 0x1000 != 0;
919 let comb = flags & 0x100_0000 != 0 && max_len.is_some() && !multiline;
920 let password = flags & 0x2000 != 0;
921
922 let widgets = widget_ids(doc, located);
923 let mut updated = 0;
924 for &w in &widgets {
925 let rect = match widget_rect(doc, w) {
926 Some(r) => r,
927 None => continue,
928 };
929 let content = build_text_appearance_content(
930 &encoded_lines,
931 rect,
932 &da,
933 quadding,
934 comb,
935 max_len,
936 multiline,
937 password,
938 );
939 let font_alias = da.font_name.clone().unwrap_or_else(|| "Helv".to_string());
940 install_widget_appearance(doc, w, rect, content, &font_alias);
941 updated += 1;
942 }
943 Ok(updated)
944}
945
946const COMBO_FLAG: i64 = 0x20000;
948
949fn listbox_selection(
952 doc: &Document,
953 dict: &Dictionary,
954 options: &[(String, String)],
955) -> Vec<usize> {
956 let mut values: Vec<String> = Vec::new();
957 match inherited(doc, dict, b"V") {
958 Some(v @ Object::String(..)) => {
959 values.extend(lopdf::decode_text_string(&v).ok());
960 }
961 Some(Object::Array(a)) => {
962 for o in &a {
963 values.extend(lopdf::decode_text_string(o).ok());
964 }
965 }
966 _ => {}
967 }
968 let mut out: Vec<usize> = values
969 .iter()
970 .filter_map(|v| options.iter().position(|(e, d)| e == v || d == v))
971 .collect();
972 if out.is_empty() {
973 if let Some(Object::Array(a)) = inherited(doc, dict, b"I") {
974 out = a
975 .iter()
976 .filter_map(|o| match o {
977 Object::Integer(i) if *i >= 0 && (*i as usize) < options.len() => {
978 Some(*i as usize)
979 }
980 _ => None,
981 })
982 .collect();
983 }
984 }
985 out.sort_unstable();
986 out.dedup();
987 out
988}
989
990fn generate_listbox_widget_appearances(
994 doc: &mut Document,
995 located: &Located,
996 options: &[(String, String)],
997 selected: &[usize],
998) -> Result<usize, NotWinAnsi> {
999 let mut rows: Vec<Vec<u8>> = Vec::with_capacity(options.len());
1000 for (_, display) in options {
1001 rows.push(encode_winansi(display).ok_or(NotWinAnsi)?);
1002 }
1003 let Ok(dict) = doc.get_object(located.id).and_then(|o| o.as_dict()) else {
1004 return Ok(0);
1005 };
1006 let da_string = match inherited(doc, dict, b"DA") {
1007 Some(v @ Object::String(..)) => lopdf::decode_text_string(&v).unwrap_or_default(),
1008 _ => acroform_da(doc).unwrap_or_default(),
1009 };
1010 let da = parse_da(if da_string.is_empty() {
1011 "/Helv 0 Tf 0 g"
1012 } else {
1013 &da_string
1014 });
1015 let top_index = match inherited(doc, dict, b"TI") {
1016 Some(Object::Integer(n)) if n > 0 => (n as usize).min(rows.len()),
1017 _ => 0,
1018 };
1019
1020 let font_alias = da.font_name.clone().unwrap_or_else(|| "Helv".to_string());
1021 let mut updated = 0;
1022 for w in widget_ids(doc, located) {
1023 let Some(rect) = widget_rect(doc, w) else {
1024 continue;
1025 };
1026 let width = rect[2] - rect[0];
1027 let height = rect[3] - rect[1];
1028 let font_size = if da.font_size > 0.0 {
1029 da.font_size
1030 } else {
1031 12.0
1032 };
1033 let leading = font_size * 1.2;
1034 let inner_w = (width - 2.0 * TEXT_INSET).max(1.0);
1035 let inner_h = (height - 2.0 * TEXT_INSET).max(1.0);
1036
1037 let mut buf = Vec::with_capacity(256);
1038 let _ = writeln!(buf, "/Tx BMC");
1039 buf.extend_from_slice(b"q\n");
1040 let _ = writeln!(buf, "{TEXT_INSET} {TEXT_INSET} {inner_w} {inner_h} re W n");
1041 let first_top = height - TEXT_INSET;
1042 for (row, bytes) in rows.iter().enumerate().skip(top_index) {
1043 let slot = (row - top_index) as f32;
1044 let row_top = first_top - slot * leading;
1045 if row_top <= 0.0 {
1046 break;
1047 }
1048 if selected.contains(&row) {
1049 let _ = writeln!(
1050 buf,
1051 "0.6 0.75 0.85 rg {TEXT_INSET} {} {inner_w} {leading} re f",
1052 row_top - leading
1053 );
1054 }
1055 buf.extend_from_slice(b"BT\n");
1056 write_da_color(&mut buf, &da);
1057 let _ = writeln!(buf, "/{font_alias} {font_size} Tf");
1058 let _ = writeln!(
1059 buf,
1060 "{} {} Td",
1061 TEXT_INSET + 1.0,
1062 row_top - leading + (leading - font_size) / 2.0 + font_size * 0.2
1063 );
1064 buf.extend_from_slice(b"(");
1065 buf.extend_from_slice(&escape_string_bytes(bytes));
1066 buf.extend_from_slice(b") Tj\nET\n");
1067 }
1068 buf.extend_from_slice(b"Q\nEMC\n");
1069 install_widget_appearance(doc, w, rect, buf, &font_alias);
1070 updated += 1;
1071 }
1072 Ok(updated)
1073}
1074
1075fn acroform_da(doc: &Document) -> Option<String> {
1076 let af_id = acroform_id(doc).ok()?;
1077 let af = doc.get_object(af_id).and_then(|o| o.as_dict()).ok()?;
1078 let v = af.get(b"DA").ok()?;
1079 lopdf::decode_text_string(v).ok()
1080}
1081
1082fn widget_rect(doc: &Document, widget: ObjectId) -> Option<[f32; 4]> {
1083 let d = doc.get_object(widget).and_then(|o| o.as_dict()).ok()?;
1084 let Ok(Object::Array(arr)) = d.get(b"Rect") else {
1085 return None;
1086 };
1087 if arr.len() != 4 {
1088 return None;
1089 }
1090 let mut r = [0f32; 4];
1091 for (i, o) in arr.iter().enumerate() {
1092 r[i] = match o {
1093 Object::Integer(n) => *n as f32,
1094 Object::Real(f) => *f,
1095 _ => return None,
1096 };
1097 }
1098 Some([
1100 r[0].min(r[2]),
1101 r[1].min(r[3]),
1102 r[0].max(r[2]),
1103 r[1].max(r[3]),
1104 ])
1105}
1106
1107#[allow(clippy::too_many_arguments)]
1109fn build_text_appearance_content(
1110 lines: &[Vec<u8>],
1111 rect: [f32; 4],
1112 da: &DefaultAppearance,
1113 quadding: u8,
1114 comb: bool,
1115 max_len: Option<u32>,
1116 multiline: bool,
1117 password: bool,
1118) -> Vec<u8> {
1119 let w = rect[2] - rect[0];
1120 let h = rect[3] - rect[1];
1121 let face = StandardFace::from_font_name(da.font_name.as_deref().unwrap_or("Helv"));
1122 let inset = TEXT_INSET;
1123 let inner_w = (w - 2.0 * inset).max(1.0);
1124 let inner_h = (h - 2.0 * inset).max(1.0);
1125
1126 let display_lines: Vec<Vec<u8>> = if password {
1128 lines
1129 .iter()
1130 .map(|l| vec![b'*'; l.iter().filter(|&&b| b != b'\r').count()])
1131 .collect()
1132 } else {
1133 lines.to_vec()
1134 };
1135
1136 let font_size = if da.font_size > 0.0 {
1138 da.font_size
1139 } else if multiline {
1140 12.0_f32.min(inner_h)
1142 } else {
1143 let longest_units: u32 = display_lines
1145 .iter()
1146 .map(|l| l.iter().map(|&b| face.glyph_width(b) as u32).sum())
1147 .max()
1148 .unwrap_or(0);
1149 let by_height = inner_h / 1.35;
1150 let by_width = if longest_units > 0 {
1151 inner_w * 1000.0 / longest_units as f32
1152 } else {
1153 by_height
1154 };
1155 by_height.min(by_width).clamp(2.0, 144.0)
1156 };
1157
1158 let mut buf = Vec::with_capacity(256);
1159 let _ = writeln!(buf, "/Tx BMC");
1160 buf.extend_from_slice(b"q\n");
1161 let _ = writeln!(buf, "{inset} {inset} {inner_w} {inner_h} re W n");
1164 buf.extend_from_slice(b"BT\n");
1165 write_da_color(&mut buf, da);
1166 let font_alias = da.font_name.as_deref().unwrap_or("Helv");
1167 let _ = writeln!(buf, "/{font_alias} {font_size} Tf");
1168
1169 if comb {
1170 let cells = max_len.unwrap_or(1).max(1);
1173 let cell_w = w / cells as f32;
1174 let baseline = (h - font_size) / 2.0 + font_size * 0.22;
1175 let line = display_lines.first().cloned().unwrap_or_default();
1176 let mut prev_x = 0.0_f32;
1177 let mut prev_y = 0.0_f32;
1178 for (i, &byte) in line.iter().take(cells as usize).enumerate() {
1179 let glyph_w = face.glyph_width(byte) as f32 * font_size / 1000.0;
1180 let x = cell_w * i as f32 + (cell_w - glyph_w) / 2.0;
1181 let _ = writeln!(buf, "{} {} Td", x - prev_x, baseline - prev_y);
1182 prev_x = x;
1183 prev_y = baseline;
1184 let esc = escape_string_bytes(&[byte]);
1185 buf.extend_from_slice(b"(");
1186 buf.extend_from_slice(&esc);
1187 buf.extend_from_slice(b") Tj\n");
1188 }
1189 } else if multiline {
1190 let leading = font_size * 1.2;
1191 let _ = writeln!(buf, "{leading} TL");
1192 let wrapped = wrap_lines(&display_lines, face, font_size, inner_w);
1194 let first_baseline = h - inset - font_size;
1195 let _ = writeln!(buf, "{inset} {first_baseline} Td");
1196 for (i, line) in wrapped.iter().enumerate() {
1197 if i > 0 {
1198 buf.extend_from_slice(b"T*\n");
1199 }
1200 let esc = escape_string_bytes(line);
1201 buf.extend_from_slice(b"(");
1202 buf.extend_from_slice(&esc);
1203 buf.extend_from_slice(b") Tj\n");
1204 }
1205 } else {
1206 let line = display_lines.first().cloned().unwrap_or_default();
1207 let text_w = face.text_width(&line, font_size);
1208 let x = match quadding {
1209 1 => inset + (inner_w - text_w) / 2.0,
1210 2 => inset + inner_w - text_w,
1211 _ => inset,
1212 }
1213 .max(inset);
1214 let baseline = (h - font_size) / 2.0 + font_size * 0.22;
1215 let _ = writeln!(buf, "{x} {baseline} Td");
1216 let esc = escape_string_bytes(&line);
1217 buf.extend_from_slice(b"(");
1218 buf.extend_from_slice(&esc);
1219 buf.extend_from_slice(b") Tj\n");
1220 }
1221
1222 buf.extend_from_slice(b"ET\nQ\nEMC\n");
1223 buf
1224}
1225
1226fn wrap_lines(lines: &[Vec<u8>], face: StandardFace, font_size: f32, width: f32) -> Vec<Vec<u8>> {
1228 let mut out = Vec::new();
1229 for line in lines {
1230 if line.is_empty() {
1231 out.push(Vec::new());
1232 continue;
1233 }
1234 let mut current: Vec<u8> = Vec::new();
1235 for word in line.split(|&b| b == b' ') {
1236 let candidate_len = if current.is_empty() {
1237 face.text_width(word, font_size)
1238 } else {
1239 face.text_width(¤t, font_size)
1240 + face.glyph_width(b' ') as f32 * font_size / 1000.0
1241 + face.text_width(word, font_size)
1242 };
1243 if !current.is_empty() && candidate_len > width {
1244 out.push(std::mem::take(&mut current));
1245 }
1246 if !current.is_empty() {
1247 current.push(b' ');
1248 }
1249 current.extend_from_slice(word);
1250 }
1251 out.push(current);
1252 }
1253 out
1254}
1255
1256fn write_da_color(buf: &mut Vec<u8>, da: &DefaultAppearance) {
1257 let op = match (da.color.len(), da.color_op.as_deref()) {
1258 (1, Some("g")) => "g",
1259 (3, Some("rg")) => "rg",
1260 (4, Some("k")) => "k",
1261 _ => {
1262 buf.extend_from_slice(b"0 g\n");
1263 return;
1264 }
1265 };
1266 for c in &da.color {
1267 let _ = write!(buf, "{c} ");
1268 }
1269 let _ = writeln!(buf, "{op}");
1270}
1271
1272fn install_widget_appearance(
1275 doc: &mut Document,
1276 widget: ObjectId,
1277 rect: [f32; 4],
1278 content: Vec<u8>,
1279 font_alias: &str,
1280) {
1281 let w = rect[2] - rect[0];
1282 let h = rect[3] - rect[1];
1283
1284 let face = StandardFace::from_font_name(font_alias);
1294 let font_obj = Object::Dictionary(dictionary! {
1295 "Type" => Object::Name(b"Font".to_vec()),
1296 "Subtype" => Object::Name(b"Type1".to_vec()),
1297 "BaseFont" => Object::Name(face.base_font_name().as_bytes().to_vec()),
1298 "Encoding" => Object::Name(b"WinAnsiEncoding".to_vec()),
1299 });
1300 let mut fonts = Dictionary::new();
1301 fonts.set(font_alias.as_bytes().to_vec(), font_obj);
1302 let resources = dictionary! {
1303 "Font" => Object::Dictionary(fonts),
1304 };
1305 let xobj = Stream::new(
1306 dictionary! {
1307 "Type" => Object::Name(b"XObject".to_vec()),
1308 "Subtype" => Object::Name(b"Form".to_vec()),
1309 "BBox" => Object::Array(vec![
1310 Object::Real(0.0), Object::Real(0.0), Object::Real(w), Object::Real(h),
1311 ]),
1312 "Resources" => Object::Dictionary(resources),
1313 },
1314 content,
1315 );
1316 let ap_ref = doc.add_object(Object::Stream(xobj));
1317 if let Ok(Object::Dictionary(d)) = doc.get_object_mut(widget) {
1318 let mut ap = Dictionary::new();
1319 ap.set("N", Object::Reference(ap_ref));
1320 d.set("AP", Object::Dictionary(ap));
1321 }
1322}