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)]
128struct Located {
129 id: ObjectId,
130 fqn: String,
131 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
150fn 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
174fn 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
253fn 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
276fn 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(
426 doc: &mut Document,
427 name: &str,
428 values: &[String],
429) -> Result<WriteOutcome, WritebackError> {
430 ensure_indirect_acroform(doc);
431 let fields = collect_fields(doc)?;
432 let located = fields
433 .iter()
434 .find(|l| l.fqn == name)
435 .cloned()
436 .ok_or_else(|| WritebackError::FieldNotFound(name.to_string()))?;
437
438 let dict = doc
439 .get_object(located.id)
440 .and_then(|o| o.as_dict())
441 .map_err(|_| WritebackError::Malformed("field object is not a dictionary".into()))?;
442
443 if effective_flags(doc, dict) & 0x1 != 0 {
444 return Err(WritebackError::ReadOnly(name.to_string()));
445 }
446
447 let ft = effective_field_type(doc, dict).unwrap_or_else(|| b"Tx".to_vec());
448 let flags = effective_flags(doc, dict);
449 if ft.as_slice() != b"Ch" || flags & 0x200000 == 0 {
451 return Err(WritebackError::WrongType {
452 name: name.to_string(),
453 actual: match ft.as_slice() {
454 b"Ch" => "single-select choice",
455 b"Tx" => "text",
456 b"Btn" => "button",
457 _ => "unknown",
458 },
459 requested: "multi-select choice",
460 });
461 }
462
463 let options = {
465 let d = field_dict(doc, &located)?;
466 choice_options(doc, d)
467 };
468 let editable = flags & 0x40000 != 0;
469 if !editable {
470 for v in values {
471 let known = options
472 .iter()
473 .any(|(export, display)| export == v || display == v);
474 if !known {
475 return Err(WritebackError::InvalidOption {
476 name: name.to_string(),
477 value: v.clone(),
478 });
479 }
480 }
481 }
482
483 let v_obj = Object::Array(values.iter().map(|s| lopdf::text_string(s)).collect());
485 set_field_v(doc, located.id, v_obj)?;
486
487 let mut indices: Vec<i64> = values
491 .iter()
492 .filter_map(|v| {
493 options
494 .iter()
495 .position(|(export, display)| export == v || display == v)
496 .map(|i| i as i64)
497 })
498 .collect();
499 indices.sort_unstable();
500 indices.dedup();
501 if let Ok(Object::Dictionary(d)) = doc.get_object_mut(located.id) {
502 if indices.is_empty() {
503 d.remove(b"I");
506 } else {
507 d.set(
508 b"I".to_vec(),
509 Object::Array(indices.into_iter().map(Object::Integer).collect()),
510 );
511 }
512 }
513 set_need_appearances(doc, true)?;
515
516 Ok(WriteOutcome {
517 appearances_generated: 0,
518 appearance_states_set: 0,
519 need_appearances_fallback: true,
520 })
521}
522
523pub fn regenerate_appearances(doc: &mut Document) -> Result<WriteOutcome, WritebackError> {
532 ensure_indirect_acroform(doc);
533 let fields = collect_fields(doc)?;
534 let mut outcome = WriteOutcome::default();
535 for located in &fields {
536 let Ok(dict) = doc.get_object(located.id).and_then(|o| o.as_dict()) else {
537 continue;
538 };
539 let has_field_kids = located.kids.iter().any(|&k| {
541 doc.get_object(k)
542 .and_then(|o| o.as_dict())
543 .map(|d| d.get(b"T").is_ok())
544 .unwrap_or(false)
545 });
546 if has_field_kids {
547 continue;
548 }
549 let ft = effective_field_type(doc, dict).unwrap_or_default();
550 if ft != b"Tx" && ft != b"Ch" {
551 continue;
552 }
553 let value = match inherited(doc, dict, b"V") {
554 Some(v @ Object::String(..)) => lopdf::decode_text_string(&v).unwrap_or_default(),
555 _ => continue,
556 };
557 if value.is_empty() {
558 continue;
559 }
560 match generate_text_widget_appearances(doc, located, &value) {
561 Ok(n) => outcome.appearances_generated += n,
562 Err(_) => outcome.need_appearances_fallback = true,
563 }
564 }
565 Ok(outcome)
566}
567
568fn apply_text(
573 doc: &mut Document,
574 located: &Located,
575 text: &str,
576) -> Result<WriteOutcome, WritebackError> {
577 let max_len = {
579 let dict = field_dict(doc, located)?;
580 match inherited(doc, dict, b"MaxLen") {
581 Some(Object::Integer(n)) if n >= 0 => Some(n as usize),
582 _ => None,
583 }
584 };
585 let text: String = match max_len {
586 Some(n) => text.chars().take(n).collect(),
587 None => text.to_string(),
588 };
589
590 set_field_v(doc, located.id, lopdf::text_string(&text))?;
591
592 let mut outcome = WriteOutcome::default();
593 match generate_text_widget_appearances(doc, located, &text) {
594 Ok(n) => outcome.appearances_generated = n,
595 Err(NotWinAnsi) => {
596 remove_widget_appearances(doc, located);
599 set_need_appearances(doc, true)?;
600 outcome.need_appearances_fallback = true;
601 }
602 }
603 Ok(outcome)
604}
605
606fn apply_checkbox(
607 doc: &mut Document,
608 located: &Located,
609 on: bool,
610) -> Result<WriteOutcome, WritebackError> {
611 let widgets = widget_ids(doc, located);
612 let on_state = widgets
613 .iter()
614 .find_map(|&w| on_state_of_widget(doc, w))
615 .unwrap_or_else(|| b"Yes".to_vec());
616 let state: &[u8] = if on { &on_state } else { b"Off" };
617
618 set_field_v(doc, located.id, Object::Name(state.to_vec()))?;
619
620 let mut outcome = WriteOutcome::default();
621 for &w in &widgets {
622 let has_substates = {
628 let dict = doc.get_object(w).and_then(|o| o.as_dict()).ok();
629 dict.and_then(|d| appearance_normal_dict(doc, d)).is_some()
630 };
631 let on_here = on && (!has_substates || widget_has_state(doc, w, state));
632 let widget_state: &[u8] = if on_here { state } else { b"Off" };
633 if set_widget_as(doc, w, widget_state) {
634 outcome.appearance_states_set += 1;
635 }
636 }
637 Ok(outcome)
638}
639
640fn apply_radio(
641 doc: &mut Document,
642 located: &Located,
643 name: &str,
644 export: &str,
645) -> Result<WriteOutcome, WritebackError> {
646 let widgets = widget_ids(doc, located);
647
648 let candidates: Vec<Vec<u8>> = {
652 let mut c = vec![export.as_bytes().to_vec()];
653 if let Some(w) = encode_winansi(export) {
654 if w != export.as_bytes() {
655 c.push(w);
656 }
657 }
658 c
659 };
660 let any_substates = widgets.iter().any(|&w| {
666 doc.get_object(w)
667 .and_then(|o| o.as_dict())
668 .ok()
669 .and_then(|d| appearance_normal_dict(doc, d))
670 .is_some()
671 });
672 let chosen = if any_substates {
673 candidates
674 .into_iter()
675 .find(|cand| widgets.iter().any(|&w| widget_has_state(doc, w, cand)))
676 .ok_or_else(|| WritebackError::InvalidOption {
677 name: name.to_string(),
678 value: export.to_string(),
679 })?
680 } else {
681 export.as_bytes().to_vec()
682 };
683
684 set_field_v(doc, located.id, Object::Name(chosen.clone()))?;
686
687 let mut outcome = WriteOutcome::default();
688 for &w in &widgets {
689 let state: &[u8] = if widget_has_state(doc, w, &chosen) {
690 &chosen
691 } else {
692 b"Off"
693 };
694 if set_widget_as(doc, w, state) {
695 outcome.appearance_states_set += 1;
696 }
697 }
698 Ok(outcome)
699}
700
701fn apply_choice(
702 doc: &mut Document,
703 located: &Located,
704 name: &str,
705 text: &str,
706 flags: i64,
707) -> Result<WriteOutcome, WritebackError> {
708 let editable = flags & 0x40000 != 0;
710 let options = {
711 let dict = field_dict(doc, located)?;
712 choice_options(doc, dict)
713 };
714 if !editable && !options.is_empty() {
715 let known = options
716 .iter()
717 .any(|(export, display)| export == text || display == text);
718 if !known {
719 return Err(WritebackError::InvalidOption {
720 name: name.to_string(),
721 value: text.to_string(),
722 });
723 }
724 }
725
726 set_field_v(doc, located.id, lopdf::text_string(text))?;
727 if let Ok(Object::Dictionary(d)) = doc.get_object_mut(located.id) {
729 d.remove(b"I");
730 }
731
732 let mut outcome = WriteOutcome::default();
733 let display = options
735 .iter()
736 .find(|(export, _)| export == text)
737 .map(|(_, d)| d.clone())
738 .unwrap_or_else(|| text.to_string());
739 match generate_text_widget_appearances(doc, located, &display) {
740 Ok(n) => outcome.appearances_generated = n,
741 Err(NotWinAnsi) => {
742 remove_widget_appearances(doc, located);
743 set_need_appearances(doc, true)?;
744 outcome.need_appearances_fallback = true;
745 }
746 }
747 Ok(outcome)
748}
749
750fn field_dict<'a>(doc: &'a Document, located: &Located) -> Result<&'a Dictionary, WritebackError> {
755 doc.get_object(located.id)
756 .and_then(|o| o.as_dict())
757 .map_err(|_| WritebackError::Malformed("field object vanished".into()))
758}
759
760fn set_field_v(doc: &mut Document, id: ObjectId, value: Object) -> Result<(), WritebackError> {
761 match doc.get_object_mut(id) {
762 Ok(Object::Dictionary(d)) => {
763 d.set("V", value);
764 Ok(())
765 }
766 _ => Err(WritebackError::Malformed(
767 "field object is not mutable".into(),
768 )),
769 }
770}
771
772fn set_need_appearances(doc: &mut Document, value: bool) -> Result<(), WritebackError> {
773 let af_id = acroform_id(doc)?;
774 if let Ok(Object::Dictionary(d)) = doc.get_object_mut(af_id) {
775 d.set("NeedAppearances", Object::Boolean(value));
776 }
777 Ok(())
778}
779
780fn remove_widget_appearances(doc: &mut Document, located: &Located) {
781 for w in widget_ids(doc, located) {
782 if let Ok(Object::Dictionary(d)) = doc.get_object_mut(w) {
783 d.remove(b"AP");
784 }
785 }
786}
787
788fn choice_options(doc: &Document, dict: &Dictionary) -> Vec<(String, String)> {
790 let Some(Object::Array(arr)) = inherited(doc, dict, b"Opt") else {
791 return Vec::new();
792 };
793 arr.iter()
794 .filter_map(|o| {
795 let resolved = match o {
796 Object::Reference(id) => doc.get_object(*id).ok()?,
797 other => other,
798 };
799 match resolved {
800 Object::String(..) => {
801 let s = lopdf::decode_text_string(resolved).ok()?;
802 Some((s.clone(), s))
803 }
804 Object::Array(pair) if pair.len() >= 2 => {
805 let export = lopdf::decode_text_string(&pair[0]).ok()?;
806 let display = lopdf::decode_text_string(&pair[1]).ok()?;
807 Some((export, display))
808 }
809 _ => None,
810 }
811 })
812 .collect()
813}
814
815struct NotWinAnsi;
821
822fn generate_text_widget_appearances(
825 doc: &mut Document,
826 located: &Located,
827 text: &str,
828) -> Result<usize, NotWinAnsi> {
829 let encoded_lines: Vec<Vec<u8>> = {
832 let mut lines = Vec::new();
833 for line in text.split('\n') {
834 let line = line.strip_suffix('\r').unwrap_or(line);
835 lines.push(encode_winansi(line).ok_or(NotWinAnsi)?);
836 }
837 lines
838 };
839
840 let Ok(dict) = doc.get_object(located.id).and_then(|o| o.as_dict()) else {
841 return Ok(0);
842 };
843 let flags = effective_flags(doc, dict);
844 let da_string = match inherited(doc, dict, b"DA") {
845 Some(v @ Object::String(..)) => lopdf::decode_text_string(&v).unwrap_or_default(),
846 _ => acroform_da(doc).unwrap_or_default(),
847 };
848 let da = parse_da(if da_string.is_empty() {
849 "/Helv 0 Tf 0 g"
850 } else {
851 &da_string
852 });
853 let quadding = match inherited(doc, dict, b"Q") {
854 Some(Object::Integer(1)) => 1u8,
855 Some(Object::Integer(2)) => 2u8,
856 _ => 0u8,
857 };
858 let max_len = match inherited(doc, dict, b"MaxLen") {
859 Some(Object::Integer(n)) if n > 0 => Some(n as u32),
860 _ => None,
861 };
862 let multiline = flags & 0x1000 != 0;
863 let comb = flags & 0x100_0000 != 0 && max_len.is_some() && !multiline;
864 let password = flags & 0x2000 != 0;
865
866 let widgets = widget_ids(doc, located);
867 let mut updated = 0;
868 for &w in &widgets {
869 let rect = match widget_rect(doc, w) {
870 Some(r) => r,
871 None => continue,
872 };
873 let content = build_text_appearance_content(
874 &encoded_lines,
875 rect,
876 &da,
877 quadding,
878 comb,
879 max_len,
880 multiline,
881 password,
882 );
883 let font_alias = da.font_name.clone().unwrap_or_else(|| "Helv".to_string());
884 install_widget_appearance(doc, w, rect, content, &font_alias);
885 updated += 1;
886 }
887 Ok(updated)
888}
889
890fn acroform_da(doc: &Document) -> Option<String> {
891 let af_id = acroform_id(doc).ok()?;
892 let af = doc.get_object(af_id).and_then(|o| o.as_dict()).ok()?;
893 let v = af.get(b"DA").ok()?;
894 lopdf::decode_text_string(v).ok()
895}
896
897fn widget_rect(doc: &Document, widget: ObjectId) -> Option<[f32; 4]> {
898 let d = doc.get_object(widget).and_then(|o| o.as_dict()).ok()?;
899 let Ok(Object::Array(arr)) = d.get(b"Rect") else {
900 return None;
901 };
902 if arr.len() != 4 {
903 return None;
904 }
905 let mut r = [0f32; 4];
906 for (i, o) in arr.iter().enumerate() {
907 r[i] = match o {
908 Object::Integer(n) => *n as f32,
909 Object::Real(f) => *f,
910 _ => return None,
911 };
912 }
913 Some([
915 r[0].min(r[2]),
916 r[1].min(r[3]),
917 r[0].max(r[2]),
918 r[1].max(r[3]),
919 ])
920}
921
922#[allow(clippy::too_many_arguments)]
924fn build_text_appearance_content(
925 lines: &[Vec<u8>],
926 rect: [f32; 4],
927 da: &DefaultAppearance,
928 quadding: u8,
929 comb: bool,
930 max_len: Option<u32>,
931 multiline: bool,
932 password: bool,
933) -> Vec<u8> {
934 let w = rect[2] - rect[0];
935 let h = rect[3] - rect[1];
936 let face = StandardFace::from_font_name(da.font_name.as_deref().unwrap_or("Helv"));
937 let inset = TEXT_INSET;
938 let inner_w = (w - 2.0 * inset).max(1.0);
939 let inner_h = (h - 2.0 * inset).max(1.0);
940
941 let display_lines: Vec<Vec<u8>> = if password {
943 lines
944 .iter()
945 .map(|l| vec![b'*'; l.iter().filter(|&&b| b != b'\r').count()])
946 .collect()
947 } else {
948 lines.to_vec()
949 };
950
951 let font_size = if da.font_size > 0.0 {
953 da.font_size
954 } else if multiline {
955 12.0_f32.min(inner_h)
957 } else {
958 let longest_units: u32 = display_lines
960 .iter()
961 .map(|l| l.iter().map(|&b| face.glyph_width(b) as u32).sum())
962 .max()
963 .unwrap_or(0);
964 let by_height = inner_h / 1.35;
965 let by_width = if longest_units > 0 {
966 inner_w * 1000.0 / longest_units as f32
967 } else {
968 by_height
969 };
970 by_height.min(by_width).clamp(2.0, 144.0)
971 };
972
973 let mut buf = Vec::with_capacity(256);
974 let _ = writeln!(buf, "/Tx BMC");
975 buf.extend_from_slice(b"q\n");
976 let _ = writeln!(buf, "{inset} {inset} {inner_w} {inner_h} re W n");
979 buf.extend_from_slice(b"BT\n");
980 write_da_color(&mut buf, da);
981 let font_alias = da.font_name.as_deref().unwrap_or("Helv");
982 let _ = writeln!(buf, "/{font_alias} {font_size} Tf");
983
984 if comb {
985 let cells = max_len.unwrap_or(1).max(1);
988 let cell_w = w / cells as f32;
989 let baseline = (h - font_size) / 2.0 + font_size * 0.22;
990 let line = display_lines.first().cloned().unwrap_or_default();
991 let mut prev_x = 0.0_f32;
992 let mut prev_y = 0.0_f32;
993 for (i, &byte) in line.iter().take(cells as usize).enumerate() {
994 let glyph_w = face.glyph_width(byte) as f32 * font_size / 1000.0;
995 let x = cell_w * i as f32 + (cell_w - glyph_w) / 2.0;
996 let _ = writeln!(buf, "{} {} Td", x - prev_x, baseline - prev_y);
997 prev_x = x;
998 prev_y = baseline;
999 let esc = escape_string_bytes(&[byte]);
1000 buf.extend_from_slice(b"(");
1001 buf.extend_from_slice(&esc);
1002 buf.extend_from_slice(b") Tj\n");
1003 }
1004 } else if multiline {
1005 let leading = font_size * 1.2;
1006 let _ = writeln!(buf, "{leading} TL");
1007 let wrapped = wrap_lines(&display_lines, face, font_size, inner_w);
1009 let first_baseline = h - inset - font_size;
1010 let _ = writeln!(buf, "{inset} {first_baseline} Td");
1011 for (i, line) in wrapped.iter().enumerate() {
1012 if i > 0 {
1013 buf.extend_from_slice(b"T*\n");
1014 }
1015 let esc = escape_string_bytes(line);
1016 buf.extend_from_slice(b"(");
1017 buf.extend_from_slice(&esc);
1018 buf.extend_from_slice(b") Tj\n");
1019 }
1020 } else {
1021 let line = display_lines.first().cloned().unwrap_or_default();
1022 let text_w = face.text_width(&line, font_size);
1023 let x = match quadding {
1024 1 => inset + (inner_w - text_w) / 2.0,
1025 2 => inset + inner_w - text_w,
1026 _ => inset,
1027 }
1028 .max(inset);
1029 let baseline = (h - font_size) / 2.0 + font_size * 0.22;
1030 let _ = writeln!(buf, "{x} {baseline} Td");
1031 let esc = escape_string_bytes(&line);
1032 buf.extend_from_slice(b"(");
1033 buf.extend_from_slice(&esc);
1034 buf.extend_from_slice(b") Tj\n");
1035 }
1036
1037 buf.extend_from_slice(b"ET\nQ\nEMC\n");
1038 buf
1039}
1040
1041fn wrap_lines(lines: &[Vec<u8>], face: StandardFace, font_size: f32, width: f32) -> Vec<Vec<u8>> {
1043 let mut out = Vec::new();
1044 for line in lines {
1045 if line.is_empty() {
1046 out.push(Vec::new());
1047 continue;
1048 }
1049 let mut current: Vec<u8> = Vec::new();
1050 for word in line.split(|&b| b == b' ') {
1051 let candidate_len = if current.is_empty() {
1052 face.text_width(word, font_size)
1053 } else {
1054 face.text_width(¤t, font_size)
1055 + face.glyph_width(b' ') as f32 * font_size / 1000.0
1056 + face.text_width(word, font_size)
1057 };
1058 if !current.is_empty() && candidate_len > width {
1059 out.push(std::mem::take(&mut current));
1060 }
1061 if !current.is_empty() {
1062 current.push(b' ');
1063 }
1064 current.extend_from_slice(word);
1065 }
1066 out.push(current);
1067 }
1068 out
1069}
1070
1071fn write_da_color(buf: &mut Vec<u8>, da: &DefaultAppearance) {
1072 let op = match (da.color.len(), da.color_op.as_deref()) {
1073 (1, Some("g")) => "g",
1074 (3, Some("rg")) => "rg",
1075 (4, Some("k")) => "k",
1076 _ => {
1077 buf.extend_from_slice(b"0 g\n");
1078 return;
1079 }
1080 };
1081 for c in &da.color {
1082 let _ = write!(buf, "{c} ");
1083 }
1084 let _ = writeln!(buf, "{op}");
1085}
1086
1087fn install_widget_appearance(
1090 doc: &mut Document,
1091 widget: ObjectId,
1092 rect: [f32; 4],
1093 content: Vec<u8>,
1094 font_alias: &str,
1095) {
1096 let w = rect[2] - rect[0];
1097 let h = rect[3] - rect[1];
1098
1099 let face = StandardFace::from_font_name(font_alias);
1109 let font_obj = Object::Dictionary(dictionary! {
1110 "Type" => Object::Name(b"Font".to_vec()),
1111 "Subtype" => Object::Name(b"Type1".to_vec()),
1112 "BaseFont" => Object::Name(face.base_font_name().as_bytes().to_vec()),
1113 "Encoding" => Object::Name(b"WinAnsiEncoding".to_vec()),
1114 });
1115 let mut fonts = Dictionary::new();
1116 fonts.set(font_alias.as_bytes().to_vec(), font_obj);
1117 let resources = dictionary! {
1118 "Font" => Object::Dictionary(fonts),
1119 };
1120 let xobj = Stream::new(
1121 dictionary! {
1122 "Type" => Object::Name(b"XObject".to_vec()),
1123 "Subtype" => Object::Name(b"Form".to_vec()),
1124 "BBox" => Object::Array(vec![
1125 Object::Real(0.0), Object::Real(0.0), Object::Real(w), Object::Real(h),
1126 ]),
1127 "Resources" => Object::Dictionary(resources),
1128 },
1129 content,
1130 );
1131 let ap_ref = doc.add_object(Object::Stream(xobj));
1132 if let Ok(Object::Dictionary(d)) = doc.get_object_mut(widget) {
1133 let mut ap = Dictionary::new();
1134 ap.set("N", Object::Reference(ap_ref));
1135 d.set("AP", Object::Dictionary(ap));
1136 }
1137}