1use crate::appearance::{parse_da, DefaultAppearance};
32use crate::encoding::{encode_winansi, escape_string_bytes};
33use crate::metrics::StandardFace;
34use lopdf::{dictionary, Dictionary, Document, Object, ObjectId, Stream};
35use std::io::Write as _;
36
37const MAX_DEPTH: usize = 100;
39
40const TEXT_INSET: f32 = 2.0;
43
44#[derive(Debug, thiserror::Error)]
46pub enum WritebackError {
47 #[error("form field '{0}' not found")]
49 FieldNotFound(String),
50 #[error("form field '{0}' is read-only")]
52 ReadOnly(String),
53 #[error("form field '{name}' is a {actual} field; cannot apply a {requested} value")]
55 WrongType {
56 name: String,
58 actual: &'static str,
60 requested: &'static str,
62 },
63 #[error("value '{value}' is not a valid option for field '{name}'")]
66 InvalidOption {
67 name: String,
69 value: String,
71 },
72 #[error("malformed form structure: {0}")]
74 Malformed(String),
75}
76
77#[derive(Debug, Clone, Copy)]
79pub enum WriteValue<'a> {
80 Text(&'a str),
82 Checkbox(bool),
85 Radio(&'a str),
90 Choice(&'a str),
92}
93
94impl WriteValue<'_> {
95 fn kind(&self) -> &'static str {
96 match self {
97 WriteValue::Text(_) => "text",
98 WriteValue::Checkbox(_) => "checkbox",
99 WriteValue::Radio(_) => "radio",
100 WriteValue::Choice(_) => "choice",
101 }
102 }
103}
104
105#[derive(Debug, Default, Clone)]
107pub struct WriteOutcome {
108 pub appearances_generated: usize,
110 pub appearance_states_set: usize,
112 pub need_appearances_fallback: bool,
114}
115
116#[derive(Debug, Clone)]
122struct Located {
123 id: ObjectId,
124 fqn: String,
125 kids: Vec<ObjectId>,
127}
128
129fn acroform_id(doc: &Document) -> Result<ObjectId, WritebackError> {
130 let catalog = doc
131 .catalog()
132 .map_err(|_| WritebackError::Malformed("document has no catalog".into()))?;
133 match catalog.get(b"AcroForm") {
134 Ok(Object::Reference(id)) => Ok(*id),
135 Ok(Object::Dictionary(_)) => Err(WritebackError::Malformed(
136 "inline /AcroForm dictionary; promote it first via ensure_indirect_acroform".into(),
137 )),
138 _ => Err(WritebackError::Malformed(
139 "document has no /AcroForm dictionary".into(),
140 )),
141 }
142}
143
144fn ensure_indirect_acroform(doc: &mut Document) {
153 let inline = match doc.catalog() {
154 Ok(catalog) => match catalog.get(b"AcroForm") {
155 Ok(Object::Dictionary(d)) => Some(d.clone()),
156 _ => None,
157 },
158 Err(_) => None,
159 };
160 if let Some(dict) = inline {
161 let id = doc.add_object(Object::Dictionary(dict));
162 if let Ok(catalog) = doc.catalog_mut() {
163 catalog.set("AcroForm", Object::Reference(id));
164 }
165 }
166}
167
168fn collect_fields(doc: &Document) -> Result<Vec<Located>, WritebackError> {
171 let af_id = acroform_id(doc)?;
172 let af = doc
173 .get_object(af_id)
174 .and_then(|o| o.as_dict())
175 .map_err(|_| WritebackError::Malformed("/AcroForm is not a dictionary".into()))?;
176 let fields = match af.get(b"Fields") {
177 Ok(Object::Array(arr)) => arr.clone(),
178 Ok(Object::Reference(id)) => match doc.get_object(*id) {
179 Ok(Object::Array(arr)) => arr.clone(),
180 _ => return Err(WritebackError::Malformed("/Fields is not an array".into())),
181 },
182 _ => return Err(WritebackError::Malformed("/AcroForm has no /Fields".into())),
183 };
184
185 let mut out = Vec::new();
186 let mut visited = std::collections::BTreeSet::new();
187 for entry in &fields {
188 if let Object::Reference(id) = entry {
189 walk_field(doc, *id, String::new(), 0, &mut visited, &mut out);
190 }
191 }
192 Ok(out)
193}
194
195fn walk_field(
196 doc: &Document,
197 id: ObjectId,
198 prefix: String,
199 depth: usize,
200 visited: &mut std::collections::BTreeSet<ObjectId>,
201 out: &mut Vec<Located>,
202) {
203 if depth >= MAX_DEPTH || !visited.insert(id) {
204 return;
205 }
206 let Ok(dict) = doc.get_object(id).and_then(|o| o.as_dict()) else {
207 return;
208 };
209 let partial = dict
210 .get(b"T")
211 .ok()
212 .and_then(|o| lopdf::decode_text_string(o).ok())
213 .unwrap_or_default();
214 let fqn = match (prefix.is_empty(), partial.is_empty()) {
215 (true, _) => partial.clone(),
216 (false, true) => prefix.clone(),
217 (false, false) => format!("{prefix}.{partial}"),
218 };
219
220 let mut kid_ids = Vec::new();
221 if let Ok(Object::Array(kids)) = dict.get(b"Kids") {
222 for kid in kids {
223 if let Object::Reference(kid_id) = kid {
224 kid_ids.push(*kid_id);
225 let is_nested_field = doc
230 .get_object(*kid_id)
231 .and_then(|o| o.as_dict())
232 .map(|d| d.get(b"T").is_ok())
233 .unwrap_or(false);
234 if is_nested_field {
235 walk_field(doc, *kid_id, fqn.clone(), depth + 1, visited, out);
236 }
237 }
238 }
239 }
240 out.push(Located {
241 id,
242 fqn,
243 kids: kid_ids,
244 });
245}
246
247fn inherited<'a>(doc: &'a Document, dict: &'a Dictionary, key: &[u8]) -> Option<Object> {
249 let mut current: Option<&Dictionary> = Some(dict);
250 for _ in 0..MAX_DEPTH {
251 let d = current?;
252 if let Ok(v) = d.get(key) {
253 return Some(v.clone());
254 }
255 current = match d.get(b"Parent") {
256 Ok(Object::Reference(pid)) => doc.get_object(*pid).and_then(|o| o.as_dict()).ok(),
257 _ => None,
258 };
259 }
260 None
261}
262
263fn effective_flags(doc: &Document, dict: &Dictionary) -> i64 {
264 match inherited(doc, dict, b"Ff") {
265 Some(Object::Integer(i)) => i,
266 _ => 0,
267 }
268}
269
270fn effective_field_type(doc: &Document, dict: &Dictionary) -> Option<Vec<u8>> {
271 match inherited(doc, dict, b"FT") {
272 Some(Object::Name(n)) => Some(n),
273 _ => None,
274 }
275}
276
277fn widget_ids(doc: &Document, located: &Located) -> Vec<ObjectId> {
280 let mut out = Vec::new();
281 for &kid in &located.kids {
282 if let Ok(d) = doc.get_object(kid).and_then(|o| o.as_dict()) {
283 if d.get(b"T").is_err() {
288 out.push(kid);
289 }
290 }
291 }
292 if out.is_empty() {
293 out.push(located.id);
294 }
295 out
296}
297
298fn on_state_of_widget(doc: &Document, widget: ObjectId) -> Option<Vec<u8>> {
300 let dict = doc.get_object(widget).and_then(|o| o.as_dict()).ok()?;
301 let n = appearance_normal_dict(doc, dict)?;
302 n.iter()
303 .map(|(k, _)| k.clone())
304 .find(|k| k.as_slice() != b"Off")
305}
306
307fn appearance_normal_dict<'a>(doc: &'a Document, dict: &'a Dictionary) -> Option<&'a Dictionary> {
309 let ap = match dict.get(b"AP").ok()? {
310 Object::Reference(id) => doc.get_object(*id).and_then(|o| o.as_dict()).ok()?,
311 Object::Dictionary(d) => d,
312 _ => return None,
313 };
314 match ap.get(b"N").ok()? {
315 Object::Reference(id) => doc.get_object(*id).and_then(|o| o.as_dict()).ok(),
316 Object::Dictionary(d) => Some(d),
317 _ => None,
318 }
319}
320
321fn widget_has_state(doc: &Document, widget: ObjectId, state: &[u8]) -> bool {
323 let Ok(dict) = doc.get_object(widget).and_then(|o| o.as_dict()) else {
324 return false;
325 };
326 match appearance_normal_dict(doc, dict) {
327 Some(n) => n.iter().any(|(k, _)| k.as_slice() == state),
328 None => false,
329 }
330}
331
332fn set_widget_as(doc: &mut Document, widget: ObjectId, state: &[u8]) -> bool {
334 if let Ok(Object::Dictionary(d)) = doc.get_object_mut(widget) {
335 d.set("AS", Object::Name(state.to_vec()));
336 true
337 } else {
338 false
339 }
340}
341
342pub fn apply_field_value(
352 doc: &mut Document,
353 name: &str,
354 value: WriteValue<'_>,
355) -> Result<WriteOutcome, WritebackError> {
356 ensure_indirect_acroform(doc);
357 let fields = collect_fields(doc)?;
358 let located = fields
359 .iter()
360 .find(|l| l.fqn == name)
361 .cloned()
362 .ok_or_else(|| WritebackError::FieldNotFound(name.to_string()))?;
363
364 let dict = doc
365 .get_object(located.id)
366 .and_then(|o| o.as_dict())
367 .map_err(|_| WritebackError::Malformed("field object is not a dictionary".into()))?;
368
369 if effective_flags(doc, dict) & 0x1 != 0 {
371 return Err(WritebackError::ReadOnly(name.to_string()));
372 }
373
374 let ft = effective_field_type(doc, dict).unwrap_or_else(|| b"Tx".to_vec());
375 let flags = effective_flags(doc, dict);
376
377 match (ft.as_slice(), value) {
378 (b"Tx", WriteValue::Text(text)) => apply_text(doc, &located, text),
379 (b"Btn", WriteValue::Checkbox(on)) if flags & 0x18000 == 0 => {
380 apply_checkbox(doc, &located, on)
381 }
382 (b"Btn", WriteValue::Radio(export)) if flags & 0x8000 != 0 => {
383 apply_radio(doc, &located, name, export)
384 }
385 (b"Ch", WriteValue::Choice(text)) => apply_choice(doc, &located, name, text, flags),
386 (b"Sig", _) => Err(WritebackError::WrongType {
387 name: name.to_string(),
388 actual: "signature",
389 requested: value.kind(),
390 }),
391 (actual_ft, v) => Err(WritebackError::WrongType {
392 name: name.to_string(),
393 actual: match actual_ft {
394 b"Tx" => "text",
395 b"Btn" if flags & 0x10000 != 0 => "push-button",
396 b"Btn" if flags & 0x8000 != 0 => "radio",
397 b"Btn" => "checkbox",
398 b"Ch" => "choice",
399 _ => "unknown",
400 },
401 requested: v.kind(),
402 }),
403 }
404}
405
406pub fn apply_choice_multi(
420 doc: &mut Document,
421 name: &str,
422 values: &[String],
423) -> Result<WriteOutcome, WritebackError> {
424 ensure_indirect_acroform(doc);
425 let fields = collect_fields(doc)?;
426 let located = fields
427 .iter()
428 .find(|l| l.fqn == name)
429 .cloned()
430 .ok_or_else(|| WritebackError::FieldNotFound(name.to_string()))?;
431
432 let dict = doc
433 .get_object(located.id)
434 .and_then(|o| o.as_dict())
435 .map_err(|_| WritebackError::Malformed("field object is not a dictionary".into()))?;
436
437 if effective_flags(doc, dict) & 0x1 != 0 {
438 return Err(WritebackError::ReadOnly(name.to_string()));
439 }
440
441 let ft = effective_field_type(doc, dict).unwrap_or_else(|| b"Tx".to_vec());
442 let flags = effective_flags(doc, dict);
443 if ft.as_slice() != b"Ch" || flags & 0x200000 == 0 {
445 return Err(WritebackError::WrongType {
446 name: name.to_string(),
447 actual: match ft.as_slice() {
448 b"Ch" => "single-select choice",
449 b"Tx" => "text",
450 b"Btn" => "button",
451 _ => "unknown",
452 },
453 requested: "multi-select choice",
454 });
455 }
456
457 let options = {
459 let d = field_dict(doc, &located)?;
460 choice_options(doc, d)
461 };
462 let editable = flags & 0x40000 != 0;
463 if !editable {
464 for v in values {
465 let known = options
466 .iter()
467 .any(|(export, display)| export == v || display == v);
468 if !known {
469 return Err(WritebackError::InvalidOption {
470 name: name.to_string(),
471 value: v.clone(),
472 });
473 }
474 }
475 }
476
477 let v_obj = Object::Array(values.iter().map(|s| lopdf::text_string(s)).collect());
479 set_field_v(doc, located.id, v_obj)?;
480
481 let mut indices: Vec<i64> = values
485 .iter()
486 .filter_map(|v| {
487 options
488 .iter()
489 .position(|(export, display)| export == v || display == v)
490 .map(|i| i as i64)
491 })
492 .collect();
493 indices.sort_unstable();
494 indices.dedup();
495 if let Ok(Object::Dictionary(d)) = doc.get_object_mut(located.id) {
496 if indices.is_empty() {
497 d.remove(b"I");
500 } else {
501 d.set(
502 b"I".to_vec(),
503 Object::Array(indices.into_iter().map(Object::Integer).collect()),
504 );
505 }
506 }
507 set_need_appearances(doc, true)?;
509
510 Ok(WriteOutcome {
511 appearances_generated: 0,
512 appearance_states_set: 0,
513 need_appearances_fallback: true,
514 })
515}
516
517pub fn regenerate_appearances(doc: &mut Document) -> Result<WriteOutcome, WritebackError> {
526 ensure_indirect_acroform(doc);
527 let fields = collect_fields(doc)?;
528 let mut outcome = WriteOutcome::default();
529 for located in &fields {
530 let Ok(dict) = doc.get_object(located.id).and_then(|o| o.as_dict()) else {
531 continue;
532 };
533 let has_field_kids = located.kids.iter().any(|&k| {
535 doc.get_object(k)
536 .and_then(|o| o.as_dict())
537 .map(|d| d.get(b"T").is_ok())
538 .unwrap_or(false)
539 });
540 if has_field_kids {
541 continue;
542 }
543 let ft = effective_field_type(doc, dict).unwrap_or_default();
544 if ft != b"Tx" && ft != b"Ch" {
545 continue;
546 }
547 let value = match inherited(doc, dict, b"V") {
548 Some(v @ Object::String(..)) => lopdf::decode_text_string(&v).unwrap_or_default(),
549 _ => continue,
550 };
551 if value.is_empty() {
552 continue;
553 }
554 match generate_text_widget_appearances(doc, located, &value) {
555 Ok(n) => outcome.appearances_generated += n,
556 Err(_) => outcome.need_appearances_fallback = true,
557 }
558 }
559 Ok(outcome)
560}
561
562fn apply_text(
567 doc: &mut Document,
568 located: &Located,
569 text: &str,
570) -> Result<WriteOutcome, WritebackError> {
571 let max_len = {
573 let dict = field_dict(doc, located)?;
574 match inherited(doc, dict, b"MaxLen") {
575 Some(Object::Integer(n)) if n >= 0 => Some(n as usize),
576 _ => None,
577 }
578 };
579 let text: String = match max_len {
580 Some(n) => text.chars().take(n).collect(),
581 None => text.to_string(),
582 };
583
584 set_field_v(doc, located.id, lopdf::text_string(&text))?;
585
586 let mut outcome = WriteOutcome::default();
587 match generate_text_widget_appearances(doc, located, &text) {
588 Ok(n) => outcome.appearances_generated = n,
589 Err(NotWinAnsi) => {
590 remove_widget_appearances(doc, located);
593 set_need_appearances(doc, true)?;
594 outcome.need_appearances_fallback = true;
595 }
596 }
597 Ok(outcome)
598}
599
600fn apply_checkbox(
601 doc: &mut Document,
602 located: &Located,
603 on: bool,
604) -> Result<WriteOutcome, WritebackError> {
605 let widgets = widget_ids(doc, located);
606 let on_state = widgets
607 .iter()
608 .find_map(|&w| on_state_of_widget(doc, w))
609 .unwrap_or_else(|| b"Yes".to_vec());
610 let state: &[u8] = if on { &on_state } else { b"Off" };
611
612 set_field_v(doc, located.id, Object::Name(state.to_vec()))?;
613
614 let mut outcome = WriteOutcome::default();
615 for &w in &widgets {
616 let has_substates = {
622 let dict = doc.get_object(w).and_then(|o| o.as_dict()).ok();
623 dict.and_then(|d| appearance_normal_dict(doc, d)).is_some()
624 };
625 let on_here = on && (!has_substates || widget_has_state(doc, w, state));
626 let widget_state: &[u8] = if on_here { state } else { b"Off" };
627 if set_widget_as(doc, w, widget_state) {
628 outcome.appearance_states_set += 1;
629 }
630 }
631 Ok(outcome)
632}
633
634fn apply_radio(
635 doc: &mut Document,
636 located: &Located,
637 name: &str,
638 export: &str,
639) -> Result<WriteOutcome, WritebackError> {
640 let widgets = widget_ids(doc, located);
641
642 let candidates: Vec<Vec<u8>> = {
646 let mut c = vec![export.as_bytes().to_vec()];
647 if let Some(w) = encode_winansi(export) {
648 if w != export.as_bytes() {
649 c.push(w);
650 }
651 }
652 c
653 };
654 let any_substates = widgets.iter().any(|&w| {
660 doc.get_object(w)
661 .and_then(|o| o.as_dict())
662 .ok()
663 .and_then(|d| appearance_normal_dict(doc, d))
664 .is_some()
665 });
666 let chosen = if any_substates {
667 candidates
668 .into_iter()
669 .find(|cand| widgets.iter().any(|&w| widget_has_state(doc, w, cand)))
670 .ok_or_else(|| WritebackError::InvalidOption {
671 name: name.to_string(),
672 value: export.to_string(),
673 })?
674 } else {
675 export.as_bytes().to_vec()
676 };
677
678 set_field_v(doc, located.id, Object::Name(chosen.clone()))?;
680
681 let mut outcome = WriteOutcome::default();
682 for &w in &widgets {
683 let state: &[u8] = if widget_has_state(doc, w, &chosen) {
684 &chosen
685 } else {
686 b"Off"
687 };
688 if set_widget_as(doc, w, state) {
689 outcome.appearance_states_set += 1;
690 }
691 }
692 Ok(outcome)
693}
694
695fn apply_choice(
696 doc: &mut Document,
697 located: &Located,
698 name: &str,
699 text: &str,
700 flags: i64,
701) -> Result<WriteOutcome, WritebackError> {
702 let editable = flags & 0x40000 != 0;
704 let options = {
705 let dict = field_dict(doc, located)?;
706 choice_options(doc, dict)
707 };
708 if !editable && !options.is_empty() {
709 let known = options
710 .iter()
711 .any(|(export, display)| export == text || display == text);
712 if !known {
713 return Err(WritebackError::InvalidOption {
714 name: name.to_string(),
715 value: text.to_string(),
716 });
717 }
718 }
719
720 set_field_v(doc, located.id, lopdf::text_string(text))?;
721 if let Ok(Object::Dictionary(d)) = doc.get_object_mut(located.id) {
723 d.remove(b"I");
724 }
725
726 let mut outcome = WriteOutcome::default();
727 let display = options
729 .iter()
730 .find(|(export, _)| export == text)
731 .map(|(_, d)| d.clone())
732 .unwrap_or_else(|| text.to_string());
733 match generate_text_widget_appearances(doc, located, &display) {
734 Ok(n) => outcome.appearances_generated = n,
735 Err(NotWinAnsi) => {
736 remove_widget_appearances(doc, located);
737 set_need_appearances(doc, true)?;
738 outcome.need_appearances_fallback = true;
739 }
740 }
741 Ok(outcome)
742}
743
744fn field_dict<'a>(doc: &'a Document, located: &Located) -> Result<&'a Dictionary, WritebackError> {
749 doc.get_object(located.id)
750 .and_then(|o| o.as_dict())
751 .map_err(|_| WritebackError::Malformed("field object vanished".into()))
752}
753
754fn set_field_v(doc: &mut Document, id: ObjectId, value: Object) -> Result<(), WritebackError> {
755 match doc.get_object_mut(id) {
756 Ok(Object::Dictionary(d)) => {
757 d.set("V", value);
758 Ok(())
759 }
760 _ => Err(WritebackError::Malformed(
761 "field object is not mutable".into(),
762 )),
763 }
764}
765
766fn set_need_appearances(doc: &mut Document, value: bool) -> Result<(), WritebackError> {
767 let af_id = acroform_id(doc)?;
768 if let Ok(Object::Dictionary(d)) = doc.get_object_mut(af_id) {
769 d.set("NeedAppearances", Object::Boolean(value));
770 }
771 Ok(())
772}
773
774fn remove_widget_appearances(doc: &mut Document, located: &Located) {
775 for w in widget_ids(doc, located) {
776 if let Ok(Object::Dictionary(d)) = doc.get_object_mut(w) {
777 d.remove(b"AP");
778 }
779 }
780}
781
782fn choice_options(doc: &Document, dict: &Dictionary) -> Vec<(String, String)> {
784 let Some(Object::Array(arr)) = inherited(doc, dict, b"Opt") else {
785 return Vec::new();
786 };
787 arr.iter()
788 .filter_map(|o| {
789 let resolved = match o {
790 Object::Reference(id) => doc.get_object(*id).ok()?,
791 other => other,
792 };
793 match resolved {
794 Object::String(..) => {
795 let s = lopdf::decode_text_string(resolved).ok()?;
796 Some((s.clone(), s))
797 }
798 Object::Array(pair) if pair.len() >= 2 => {
799 let export = lopdf::decode_text_string(&pair[0]).ok()?;
800 let display = lopdf::decode_text_string(&pair[1]).ok()?;
801 Some((export, display))
802 }
803 _ => None,
804 }
805 })
806 .collect()
807}
808
809struct NotWinAnsi;
815
816fn generate_text_widget_appearances(
819 doc: &mut Document,
820 located: &Located,
821 text: &str,
822) -> Result<usize, NotWinAnsi> {
823 let encoded_lines: Vec<Vec<u8>> = {
826 let mut lines = Vec::new();
827 for line in text.split('\n') {
828 let line = line.strip_suffix('\r').unwrap_or(line);
829 lines.push(encode_winansi(line).ok_or(NotWinAnsi)?);
830 }
831 lines
832 };
833
834 let Ok(dict) = doc.get_object(located.id).and_then(|o| o.as_dict()) else {
835 return Ok(0);
836 };
837 let flags = effective_flags(doc, dict);
838 let da_string = match inherited(doc, dict, b"DA") {
839 Some(v @ Object::String(..)) => lopdf::decode_text_string(&v).unwrap_or_default(),
840 _ => acroform_da(doc).unwrap_or_default(),
841 };
842 let da = parse_da(if da_string.is_empty() {
843 "/Helv 0 Tf 0 g"
844 } else {
845 &da_string
846 });
847 let quadding = match inherited(doc, dict, b"Q") {
848 Some(Object::Integer(1)) => 1u8,
849 Some(Object::Integer(2)) => 2u8,
850 _ => 0u8,
851 };
852 let max_len = match inherited(doc, dict, b"MaxLen") {
853 Some(Object::Integer(n)) if n > 0 => Some(n as u32),
854 _ => None,
855 };
856 let multiline = flags & 0x1000 != 0;
857 let comb = flags & 0x100_0000 != 0 && max_len.is_some() && !multiline;
858 let password = flags & 0x2000 != 0;
859
860 let widgets = widget_ids(doc, located);
861 let mut updated = 0;
862 for &w in &widgets {
863 let rect = match widget_rect(doc, w) {
864 Some(r) => r,
865 None => continue,
866 };
867 let content = build_text_appearance_content(
868 &encoded_lines,
869 rect,
870 &da,
871 quadding,
872 comb,
873 max_len,
874 multiline,
875 password,
876 );
877 let font_alias = da.font_name.clone().unwrap_or_else(|| "Helv".to_string());
878 install_widget_appearance(doc, w, rect, content, &font_alias);
879 updated += 1;
880 }
881 Ok(updated)
882}
883
884fn acroform_da(doc: &Document) -> Option<String> {
885 let af_id = acroform_id(doc).ok()?;
886 let af = doc.get_object(af_id).and_then(|o| o.as_dict()).ok()?;
887 let v = af.get(b"DA").ok()?;
888 lopdf::decode_text_string(v).ok()
889}
890
891fn widget_rect(doc: &Document, widget: ObjectId) -> Option<[f32; 4]> {
892 let d = doc.get_object(widget).and_then(|o| o.as_dict()).ok()?;
893 let Ok(Object::Array(arr)) = d.get(b"Rect") else {
894 return None;
895 };
896 if arr.len() != 4 {
897 return None;
898 }
899 let mut r = [0f32; 4];
900 for (i, o) in arr.iter().enumerate() {
901 r[i] = match o {
902 Object::Integer(n) => *n as f32,
903 Object::Real(f) => *f,
904 _ => return None,
905 };
906 }
907 Some([
909 r[0].min(r[2]),
910 r[1].min(r[3]),
911 r[0].max(r[2]),
912 r[1].max(r[3]),
913 ])
914}
915
916#[allow(clippy::too_many_arguments)]
918fn build_text_appearance_content(
919 lines: &[Vec<u8>],
920 rect: [f32; 4],
921 da: &DefaultAppearance,
922 quadding: u8,
923 comb: bool,
924 max_len: Option<u32>,
925 multiline: bool,
926 password: bool,
927) -> Vec<u8> {
928 let w = rect[2] - rect[0];
929 let h = rect[3] - rect[1];
930 let face = StandardFace::from_font_name(da.font_name.as_deref().unwrap_or("Helv"));
931 let inset = TEXT_INSET;
932 let inner_w = (w - 2.0 * inset).max(1.0);
933 let inner_h = (h - 2.0 * inset).max(1.0);
934
935 let display_lines: Vec<Vec<u8>> = if password {
937 lines
938 .iter()
939 .map(|l| vec![b'*'; l.iter().filter(|&&b| b != b'\r').count()])
940 .collect()
941 } else {
942 lines.to_vec()
943 };
944
945 let font_size = if da.font_size > 0.0 {
947 da.font_size
948 } else if multiline {
949 12.0_f32.min(inner_h)
951 } else {
952 let longest_units: u32 = display_lines
954 .iter()
955 .map(|l| l.iter().map(|&b| face.glyph_width(b) as u32).sum())
956 .max()
957 .unwrap_or(0);
958 let by_height = inner_h / 1.35;
959 let by_width = if longest_units > 0 {
960 inner_w * 1000.0 / longest_units as f32
961 } else {
962 by_height
963 };
964 by_height.min(by_width).clamp(2.0, 144.0)
965 };
966
967 let mut buf = Vec::with_capacity(256);
968 let _ = writeln!(buf, "/Tx BMC");
969 buf.extend_from_slice(b"q\n");
970 let _ = writeln!(buf, "{inset} {inset} {inner_w} {inner_h} re W n");
973 buf.extend_from_slice(b"BT\n");
974 write_da_color(&mut buf, da);
975 let font_alias = da.font_name.as_deref().unwrap_or("Helv");
976 let _ = writeln!(buf, "/{font_alias} {font_size} Tf");
977
978 if comb {
979 let cells = max_len.unwrap_or(1).max(1);
982 let cell_w = w / cells as f32;
983 let baseline = (h - font_size) / 2.0 + font_size * 0.22;
984 let line = display_lines.first().cloned().unwrap_or_default();
985 let mut prev_x = 0.0_f32;
986 let mut prev_y = 0.0_f32;
987 for (i, &byte) in line.iter().take(cells as usize).enumerate() {
988 let glyph_w = face.glyph_width(byte) as f32 * font_size / 1000.0;
989 let x = cell_w * i as f32 + (cell_w - glyph_w) / 2.0;
990 let _ = writeln!(buf, "{} {} Td", x - prev_x, baseline - prev_y);
991 prev_x = x;
992 prev_y = baseline;
993 let esc = escape_string_bytes(&[byte]);
994 buf.extend_from_slice(b"(");
995 buf.extend_from_slice(&esc);
996 buf.extend_from_slice(b") Tj\n");
997 }
998 } else if multiline {
999 let leading = font_size * 1.2;
1000 let _ = writeln!(buf, "{leading} TL");
1001 let wrapped = wrap_lines(&display_lines, face, font_size, inner_w);
1003 let first_baseline = h - inset - font_size;
1004 let _ = writeln!(buf, "{inset} {first_baseline} Td");
1005 for (i, line) in wrapped.iter().enumerate() {
1006 if i > 0 {
1007 buf.extend_from_slice(b"T*\n");
1008 }
1009 let esc = escape_string_bytes(line);
1010 buf.extend_from_slice(b"(");
1011 buf.extend_from_slice(&esc);
1012 buf.extend_from_slice(b") Tj\n");
1013 }
1014 } else {
1015 let line = display_lines.first().cloned().unwrap_or_default();
1016 let text_w = face.text_width(&line, font_size);
1017 let x = match quadding {
1018 1 => inset + (inner_w - text_w) / 2.0,
1019 2 => inset + inner_w - text_w,
1020 _ => inset,
1021 }
1022 .max(inset);
1023 let baseline = (h - font_size) / 2.0 + font_size * 0.22;
1024 let _ = writeln!(buf, "{x} {baseline} Td");
1025 let esc = escape_string_bytes(&line);
1026 buf.extend_from_slice(b"(");
1027 buf.extend_from_slice(&esc);
1028 buf.extend_from_slice(b") Tj\n");
1029 }
1030
1031 buf.extend_from_slice(b"ET\nQ\nEMC\n");
1032 buf
1033}
1034
1035fn wrap_lines(lines: &[Vec<u8>], face: StandardFace, font_size: f32, width: f32) -> Vec<Vec<u8>> {
1037 let mut out = Vec::new();
1038 for line in lines {
1039 if line.is_empty() {
1040 out.push(Vec::new());
1041 continue;
1042 }
1043 let mut current: Vec<u8> = Vec::new();
1044 for word in line.split(|&b| b == b' ') {
1045 let candidate_len = if current.is_empty() {
1046 face.text_width(word, font_size)
1047 } else {
1048 face.text_width(¤t, font_size)
1049 + face.glyph_width(b' ') as f32 * font_size / 1000.0
1050 + face.text_width(word, font_size)
1051 };
1052 if !current.is_empty() && candidate_len > width {
1053 out.push(std::mem::take(&mut current));
1054 }
1055 if !current.is_empty() {
1056 current.push(b' ');
1057 }
1058 current.extend_from_slice(word);
1059 }
1060 out.push(current);
1061 }
1062 out
1063}
1064
1065fn write_da_color(buf: &mut Vec<u8>, da: &DefaultAppearance) {
1066 let op = match (da.color.len(), da.color_op.as_deref()) {
1067 (1, Some("g")) => "g",
1068 (3, Some("rg")) => "rg",
1069 (4, Some("k")) => "k",
1070 _ => {
1071 buf.extend_from_slice(b"0 g\n");
1072 return;
1073 }
1074 };
1075 for c in &da.color {
1076 let _ = write!(buf, "{c} ");
1077 }
1078 let _ = writeln!(buf, "{op}");
1079}
1080
1081fn install_widget_appearance(
1084 doc: &mut Document,
1085 widget: ObjectId,
1086 rect: [f32; 4],
1087 content: Vec<u8>,
1088 font_alias: &str,
1089) {
1090 let w = rect[2] - rect[0];
1091 let h = rect[3] - rect[1];
1092
1093 let face = StandardFace::from_font_name(font_alias);
1103 let font_obj = Object::Dictionary(dictionary! {
1104 "Type" => Object::Name(b"Font".to_vec()),
1105 "Subtype" => Object::Name(b"Type1".to_vec()),
1106 "BaseFont" => Object::Name(face.base_font_name().as_bytes().to_vec()),
1107 "Encoding" => Object::Name(b"WinAnsiEncoding".to_vec()),
1108 });
1109 let mut fonts = Dictionary::new();
1110 fonts.set(font_alias.as_bytes().to_vec(), font_obj);
1111 let resources = dictionary! {
1112 "Font" => Object::Dictionary(fonts),
1113 };
1114 let xobj = Stream::new(
1115 dictionary! {
1116 "Type" => Object::Name(b"XObject".to_vec()),
1117 "Subtype" => Object::Name(b"Form".to_vec()),
1118 "BBox" => Object::Array(vec![
1119 Object::Real(0.0), Object::Real(0.0), Object::Real(w), Object::Real(h),
1120 ]),
1121 "Resources" => Object::Dictionary(resources),
1122 },
1123 content,
1124 );
1125 let ap_ref = doc.add_object(Object::Stream(xobj));
1126 if let Ok(Object::Dictionary(d)) = doc.get_object_mut(widget) {
1127 let mut ap = Dictionary::new();
1128 ap.set("N", Object::Reference(ap_ref));
1129 d.set("AP", Object::Dictionary(ap));
1130 }
1131}