diff --git a/crates/xfa/src/data/build.rs b/crates/xfa/src/data/build.rs index 68e8a2a364..98e34f0f49 100644 --- a/crates/xfa/src/data/build.rs +++ b/crates/xfa/src/data/build.rs @@ -1,5 +1,5 @@ use super::values::{FieldDatum, Want, wanted, with_template}; -use super::*; +use super::{Document, HashMap, MAX_DEPTH, XFA_DATA_NS}; pub(super) fn xml_name(s: &str) -> String { let mut out: String = s.chars().map(|c| if c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.' { c } else { '_' }).collect(); diff --git a/crates/xfa/src/data/dates.rs b/crates/xfa/src/data/dates.rs index 029d91b273..565c44117d 100644 --- a/crates/xfa/src/data/dates.rs +++ b/crates/xfa/src/data/dates.rs @@ -1,5 +1,5 @@ use super::path::template_field; -use super::*; +use super::{Field, Template}; /// The date letters (`yyyy-mm-dd`, `yyyymmdd`) of an XFA date picture clause such as /// `date{YYYY-MM-DD}`, or `None` when it has none this reads (empty, or with symbols beyond diff --git a/crates/xfa/src/data/import.rs b/crates/xfa/src/data/import.rs index cec30b11b3..7582d86763 100644 --- a/crates/xfa/src/data/import.rs +++ b/crates/xfa/src/data/import.rs @@ -2,7 +2,7 @@ use super::build::{escape, fresh_datasets, xml_name}; use super::merge::{Op, WRAP_CLOSE, WRAP_OPEN, qname, start_tag_end}; use super::values::Want; use super::write::{DatasetsWrite, rewrite}; -use super::*; +use super::{DataPath, Document, MAX_DEPTH, MAX_NODES, ObjRef, Object, XFA_DATA_NS, XfaError}; /// Data read from a file, held as a tree so it can be written into any packet with the /// prefix that packet uses, never as the file's own markup. diff --git a/crates/xfa/src/data/merge.rs b/crates/xfa/src/data/merge.rs index 6888b1439a..48b4cedabf 100644 --- a/crates/xfa/src/data/merge.rs +++ b/crates/xfa/src/data/merge.rs @@ -2,7 +2,7 @@ use super::build::{Build, CONFLICT, CONTROL, NewNodes, STRUCTURED, TOO_MANY, bui use super::import::{ImportNode, serialize_nodes}; use super::values::Want; use super::write::MAX_PACKET_WRITE; -use super::*; +use super::{DataPath, HashMap, MAX_NODES, XFA_DATA_NS}; /// What a rewrite of the datasets packet does. #[derive(Clone, Copy)] diff --git a/crates/xfa/src/data/passwords.rs b/crates/xfa/src/data/passwords.rs index 78503bb53c..0f98a3f2b4 100644 --- a/crates/xfa/src/data/passwords.rs +++ b/crates/xfa/src/data/passwords.rs @@ -3,7 +3,7 @@ use super::merge::Op; use super::path::{data_path, parse_datasets}; use super::values::{Want, path_of, text_of, with_template}; use super::write::{datasets_stream_ref, rewrite}; -use super::*; +use super::{DataNode, DataPath, Document, MAX_DEPTH, Node, ObjRef, Object, SomIndices, Template, Ui, XfaError}; /// The SOM path and default value of every AcroForm text field with the Password flag (bit 14, /// `1 << 13`) in `doc`, for static XFA forms whose password fields set the flag in the AcroForm diff --git a/crates/xfa/src/data/path.rs b/crates/xfa/src/data/path.rs index 90cc4346f2..ef61cdbd59 100644 --- a/crates/xfa/src/data/path.rs +++ b/crates/xfa/src/data/path.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{DataNode, DataPath, Field, MAX_DEPTH, MAX_NODES, Node, SomIndices, Template}; fn to_node(n: roxmltree::Node, depth: usize) -> DataNode { let name = n.tag_name().name().to_string(); diff --git a/crates/xfa/src/data/values.rs b/crates/xfa/src/data/values.rs index 41fb6644e9..8042e774fe 100644 --- a/crates/xfa/src/data/values.rs +++ b/crates/xfa/src/data/values.rs @@ -1,6 +1,6 @@ use super::dates::{data_date_pattern, data_to_iso, iso_to_data, iso_to_pattern, pattern_to_iso}; use super::path::{data_path, parse_datasets, som_to_path}; -use super::*; +use super::{DataPath, Dict, Document, ObjRef, Object, Template}; /// What a field holds, for writing the datasets. #[derive(Clone, Debug, PartialEq)] diff --git a/crates/xfa/src/data/write.rs b/crates/xfa/src/data/write.rs index 282dd33ef5..4667036fe5 100644 --- a/crates/xfa/src/data/write.rs +++ b/crates/xfa/src/data/write.rs @@ -2,7 +2,7 @@ use super::build::{MAX_INDEX, build_children, fresh_datasets}; use super::merge::{Op, Within, merge_all, readable}; use super::path::path_som; use super::values::{FieldDatum, Want, wanted, with_template}; -use super::*; +use super::{DataOp, DataPath, Dict, Document, Encoding, HashMap, ObjRef, Object, PdfString, Stream, Template, XfaError, encode}; /// What [`write_datasets`] did. #[derive(Clone, Debug, Default, PartialEq)] diff --git a/crates/xfa/src/layout/common.rs b/crates/xfa/src/layout/common.rs index 3b60f8a6a0..625f305a6b 100644 --- a/crates/xfa/src/layout/common.rs +++ b/crates/xfa/src/layout/common.rs @@ -1,5 +1,5 @@ use super::items::{Action, Item, Rounded}; -use super::*; +use super::{HashMap, Layout, MAX_DEPTH, Node, Rect, Script, date_letters, iso_to_pattern}; pub(super) fn collect_values(nodes: &[Node], out: &mut HashMap, depth: usize) { if depth > MAX_DEPTH { diff --git a/crates/xfa/src/layout/draw.rs b/crates/xfa/src/layout/draw.rs index 4f318a73d6..f6520c1628 100644 --- a/crates/xfa/src/layout/draw.rs +++ b/crates/xfa/src/layout/draw.rs @@ -1,6 +1,9 @@ use super::common::clean_name; use super::items::{Item, Rounded}; -use super::*; +use super::{ + Arc, Border, Ctx, Draw, ExclGroup, Face, Font, Layout, Layouter, MAX_DEPTH, MAX_IMAGE_BYTES, Node, Para, Presence, Rect, SomIndices, Stroke, + Subform, Value, XfaError, data_path, text, +}; impl Layouter<'_> { // ───────────────────────────────────────────────────────────────────────── drawing diff --git a/crates/xfa/src/layout/field.rs b/crates/xfa/src/layout/field.rs index 40ec1497de..cc15f16788 100644 --- a/crates/xfa/src/layout/field.rs +++ b/crates/xfa/src/layout/field.rs @@ -1,6 +1,9 @@ use super::common::{button_action, date_pattern}; use super::items::{BorderShape, Item, Widget, WidgetBorder, WidgetKind}; -use super::*; +use super::{ + Access, Border, Ctx, Engine, Face, Field, HAlign, Layouter, MAX_DATA_IMAGE, Margin, Para, Placement, Presence, Rect, Ui, VAlign, Value, XfaError, + data_date_letters, data_path, data_to_iso, is_on, iso_to_pattern, text, +}; impl Layouter<'_> { /// Draw field `f` at `rect`: its border (a button's is its widget's box), its caption, then its diff --git a/crates/xfa/src/layout/flow.rs b/crates/xfa/src/layout/flow.rs index 48c57cd888..8f656d7eaf 100644 --- a/crates/xfa/src/layout/flow.rs +++ b/crates/xfa/src/layout/flow.rs @@ -1,6 +1,6 @@ use super::common::{clamp_opt, divide, is_row, moved, solid_extent, splittable}; use super::items::Item; -use super::*; +use super::{Ctx, Frame, Layout, Layouter, MAX_DEPTH, Node, Rect, SomIndices, Subform, XfaError, data_path, instance_count}; impl Layouter<'_> { /// Place `node` as one block of size (`w`, `h`) at the cursor: in this content area when it fits, diff --git a/crates/xfa/src/layout/items.rs b/crates/xfa/src/layout/items.rs index 51383792c1..8a68388684 100644 --- a/crates/xfa/src/layout/items.rs +++ b/crates/xfa/src/layout/items.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{Arc, Face, HAlign, Rect, Span}; /// How far below its baseline an underline is drawn, in the text's size: an underline is the /// level line pushed just before its span, this far below it (the PDF writer tells it apart from diff --git a/crates/xfa/src/layout/measure.rs b/crates/xfa/src/layout/measure.rs index cd004df982..2fffb3dfa0 100644 --- a/crates/xfa/src/layout/measure.rs +++ b/crates/xfa/src/layout/measure.rs @@ -1,6 +1,9 @@ use super::common::clamp_opt; use super::field::auto_check_reserve; -use super::*; +use super::{ + Access, Area, Ctx, ExclGroup, Face, Field, Font, Layout, Layouter, MAX_DEPTH, MAX_MEASURES, Measured, Node, Para, Placement, Rect, Subform, Ui, + Value, data_path, text, +}; impl Layouter<'_> { /// The width and height `node` takes in `avail_w`: its own size, else what its content needs diff --git a/crates/xfa/src/layout/pages.rs b/crates/xfa/src/layout/pages.rs index 13047058b3..dac17c1169 100644 --- a/crates/xfa/src/layout/pages.rs +++ b/crates/xfa/src/layout/pages.rs @@ -1,5 +1,10 @@ +#[cfg(test)] +use super::EMBED_COPIED; use super::items::{Item, Page, UNDERLINE_DROP}; -use super::*; +use super::{ + Block, Border, Ctx, Cursor, FlowMark, Font, Frame, Layouter, MAX_ITEMS, MAX_MEASURES, MAX_PAGES, PAGE_COUNT_MARK, PageRole, Para, Rc, Rect, + SomIndices, XfaError, text, +}; impl Layouter<'_> { pub(super) fn warn_once(&mut self, msg: impl Into) { diff --git a/crates/xfa/src/pdf.rs b/crates/xfa/src/pdf.rs index ab5d94bb45..0c21507abf 100644 --- a/crates/xfa/src/pdf.rs +++ b/crates/xfa/src/pdf.rs @@ -163,38 +163,8 @@ impl Emitter<'_> { format!("/{} {} Tf {} rg", face.resource_name(), n(face.size), rgb(face.color)).into_bytes() } - /// Write laid-out widget `w` on `page` as an AcroForm widget annotation (and its field), with - /// its border, background and kind, and return the annotation's object. - fn widget(&mut self, w: &Widget, page: &Page, page_ref: ObjRef) -> Result, XfaError> { + fn populate_widget_kind(&mut self, w: &Widget, d: &mut Dict, mk: &mut Dict) -> (i64, bool, Option) { let r = w.rect; - let (x0, y0, x1, y1) = (r.x, page.height - r.bottom(), r.right(), page.height - r.y); - let mut d = Dict::new(); - d.set(b"Type".to_vec(), Object::name("Annot")); - d.set(b"Subtype".to_vec(), Object::name("Widget")); - d.set(b"F".to_vec(), Object::Int(4)); - d.set(b"P".to_vec(), Object::Ref(page_ref)); - d.set(b"Rect".to_vec(), Object::Array([x0, y0, x1, y1].iter().map(|v| Object::Real(*v)).collect())); - d.set(b"DA".to_vec(), Object::String(PdfString::literal(Self::da(&w.face)))); - if let Some(t) = &w.tooltip { - d.set(b"TU".to_vec(), Object::String(PdfString::text(t))); - } - d.set(SOM_KEY.to_vec(), Object::String(PdfString::text(&w.som))); - let mut mk = Dict::new(); - if let Some(c) = w.border.color - && w.border.shape != BorderShape::None - { - mk.set(b"BC".to_vec(), rgb_array(c)); - } - if let Some(c) = w.border.fill { - mk.set(b"BG".to_vec(), rgb_array(c)); - } - let mut bs = Dict::new(); - bs.set(b"W".to_vec(), Object::Real(if w.border.shape == BorderShape::None { 0.0 } else { w.border.width.clamp(0.1, 12.0) })); - bs.set(b"S".to_vec(), Object::name(if w.border.shape == BorderShape::Underline { "U" } else { "S" })); - d.set(b"BS".to_vec(), Object::Dict(bs)); - if matches!(w.kind, WidgetKind::CheckBox { round: true, .. } | WidgetKind::Radio { round: true, .. }) { - d.set(ROUND_KEY.to_vec(), Object::Bool(true)); - } let mut ff: i64 = 0; if w.read_only { ff |= 1; @@ -383,34 +353,45 @@ impl Emitter<'_> { d.set(b"FT".to_vec(), Object::name("Btn")); ff |= 1 << 16; mk.set(b"CA".to_vec(), Object::String(PdfString::text(caption))); - if let Some(Action::Script(_)) = &w.action { - // An XFA click script is no Acrobat JavaScript: no PDF action, which other - // viewers would run; PdfCraft finds the script by the field's SOM path. - d.set(CLICK_KEY.to_vec(), Object::Bool(true)); - } else if let Some(a) = &w.action { - let mut ad = Dict::new(); - match a { - Action::Reset => { - ad.set(b"S".to_vec(), Object::name("ResetForm")); - } - Action::Print => { - ad.set(b"S".to_vec(), Object::name("Named")); - ad.set(b"N".to_vec(), Object::name("Print")); - } - Action::SaveAs => { - ad.set(b"S".to_vec(), Object::name("Named")); - ad.set(b"N".to_vec(), Object::name("SaveAs")); - } - Action::Url(u) => { - ad.set(b"S".to_vec(), Object::name("URI")); - ad.set(b"URI".to_vec(), Object::String(PdfString::literal(u.as_bytes().to_vec()))); - } - Action::Script(_) => {} - } - d.set(b"A".to_vec(), Object::Dict(ad)); - } + write_button_action(w.action.as_ref(), d); } } + (ff, top_level, under) + } + + /// Write laid-out widget `w` on `page` as an AcroForm widget annotation (and its field), with + /// its border, background and kind, and return the annotation's object. + fn widget(&mut self, w: &Widget, page: &Page, page_ref: ObjRef) -> Result, XfaError> { + let r = w.rect; + let (x0, y0, x1, y1) = (r.x, page.height - r.bottom(), r.right(), page.height - r.y); + let mut d = Dict::new(); + d.set(b"Type".to_vec(), Object::name("Annot")); + d.set(b"Subtype".to_vec(), Object::name("Widget")); + d.set(b"F".to_vec(), Object::Int(4)); + d.set(b"P".to_vec(), Object::Ref(page_ref)); + d.set(b"Rect".to_vec(), Object::Array([x0, y0, x1, y1].iter().map(|v| Object::Real(*v)).collect())); + d.set(b"DA".to_vec(), Object::String(PdfString::literal(Self::da(&w.face)))); + if let Some(t) = &w.tooltip { + d.set(b"TU".to_vec(), Object::String(PdfString::text(t))); + } + d.set(SOM_KEY.to_vec(), Object::String(PdfString::text(&w.som))); + let mut mk = Dict::new(); + if let Some(c) = w.border.color + && w.border.shape != BorderShape::None + { + mk.set(b"BC".to_vec(), rgb_array(c)); + } + if let Some(c) = w.border.fill { + mk.set(b"BG".to_vec(), rgb_array(c)); + } + let mut bs = Dict::new(); + bs.set(b"W".to_vec(), Object::Real(if w.border.shape == BorderShape::None { 0.0 } else { w.border.width.clamp(0.1, 12.0) })); + bs.set(b"S".to_vec(), Object::name(if w.border.shape == BorderShape::Underline { "U" } else { "S" })); + d.set(b"BS".to_vec(), Object::Dict(bs)); + if matches!(w.kind, WidgetKind::CheckBox { round: true, .. } | WidgetKind::Radio { round: true, .. }) { + d.set(ROUND_KEY.to_vec(), Object::Bool(true)); + } + let (ff, top_level, under) = self.populate_widget_kind(w, &mut d, &mut mk); if !mk.is_empty() { d.set(b"MK".to_vec(), Object::Dict(mk)); } @@ -751,6 +732,39 @@ impl Emitter<'_> { } } +fn write_button_action(action: Option<&Action>, d: &mut Dict) { + match action { + Some(Action::Script(_)) => { + // An XFA click script is no Acrobat JavaScript: no PDF action, which other + // viewers would run; PdfCraft finds the script by the field's SOM path. + d.set(CLICK_KEY.to_vec(), Object::Bool(true)); + } + Some(action) => { + let mut ad = Dict::new(); + match action { + Action::Reset => { + ad.set(b"S".to_vec(), Object::name("ResetForm")); + } + Action::Print => { + ad.set(b"S".to_vec(), Object::name("Named")); + ad.set(b"N".to_vec(), Object::name("Print")); + } + Action::SaveAs => { + ad.set(b"S".to_vec(), Object::name("Named")); + ad.set(b"N".to_vec(), Object::name("SaveAs")); + } + Action::Url(url) => { + ad.set(b"S".to_vec(), Object::name("URI")); + ad.set(b"URI".to_vec(), Object::String(PdfString::literal(url.as_bytes().to_vec()))); + } + Action::Script(_) => {} + } + d.set(b"A".to_vec(), Object::Dict(ad)); + } + None => {} + } +} + /// The link annotation a page's text last ended in: the next piece of the same link on the same /// line widens it. struct LinkPiece { diff --git a/crates/xfa/src/pdf/common.rs b/crates/xfa/src/pdf/common.rs index e1325b19ae..7cbb1ce7a1 100644 --- a/crates/xfa/src/pdf/common.rs +++ b/crates/xfa/src/pdf/common.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{Dict, Document, Family, Object, PdfString, Rect, Rounded, Stream}; /// Width and height of a JPEG from its first frame header (SOF0–SOF15, not the DNL/JPG/DAC/DHT /// markers). `None` for anything else. diff --git a/crates/xfa/src/pdf/strip.rs b/crates/xfa/src/pdf/strip.rs index de395e37e9..2f3084ec93 100644 --- a/crates/xfa/src/pdf/strip.rs +++ b/crates/xfa/src/pdf/strip.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{CLICK_KEY, Dict, Document, IMAGE_FIELD_KEY, LAYOUT_KEY, NODE_KEY, ObjRef, Object, SOM_KEY, XfaError}; /// What laying a form out says when it took the usage rights off (see [`drop_usage_rights`]). pub(crate) const USAGE_RIGHTS_REMOVED: &str = diff --git a/crates/xfa/src/tests.rs b/crates/xfa/src/tests.rs index 39a1c93483..ca129abceb 100644 --- a/crates/xfa/src/tests.rs +++ b/crates/xfa/src/tests.rs @@ -1512,6 +1512,11 @@ fn top_fields(doc: &Document) -> Vec<(String, pdfcraft_cos::Dict)> { /// widget, and their AcroForm fields and image XObjects once written. #[test] fn choice_lists_signatures_passwords_and_pictures_become_fields_and_images() { + assert_choice_widget_layout(); + assert_choice_fields_and_picture_streams(); +} + +fn assert_choice_widget_layout() { let form = layout_xml(&fields_template("")).unwrap(); let p1 = &form.pages[0]; let w = widgets(p1); @@ -1546,7 +1551,9 @@ fn choice_lists_signatures_passwords_and_pictures_become_fields_and_images() { assert!(t.iter().any(|s| s == "12345"), "the barcode's value as text: {t:?}"); assert!(form.warnings.iter().any(|w| w.contains("barcode")), "{:?}", form.warnings); assert!(!form.warnings.iter().any(|w| w.contains("can't be chosen")), "pictures can be chosen now: {:?}", form.warnings); +} +fn assert_choice_fields_and_picture_streams() { // Written into the PDF: /Ch fields with their options and flags, a /Sig field, a password // field, and Flate images (the PNG with a soft mask). let mut doc = Document::open(Arc::new(shell(&fields_template("")))).unwrap(); @@ -3556,6 +3563,11 @@ fn laying_a_form_out_takes_off_the_usage_rights_it_voids() { #[test] fn stripping_takes_the_xfa_out_and_leaves_an_ordinary_document() { + assert_stripping_leaves_ordinary_document(); + assert_stripping_handles_unbound_data_and_shared_field_tree(); +} + +fn assert_stripping_leaves_ordinary_document() { use crate::pdf::strip; let mut doc = Document::open(Arc::new(shell(&fields_template("
JP
")))).unwrap(); render_into(&mut doc).unwrap(); @@ -3606,6 +3618,10 @@ fn stripping_takes_the_xfa_out_and_leaves_an_ordinary_document() { let r = strip(&mut doc).unwrap(); assert_eq!(r, crate::Stripped::default()); assert!(!doc.is_modified()); +} + +fn assert_stripping_handles_unbound_data_and_shared_field_tree() { + use crate::pdf::strip; // Data no field holds is counted; a form whose click buttons ran scripts too; the shape // check needs no packet decoded and tells a never-laid-out dynamic form from one to strip. let mut doc = Document::open(Arc::new(shell(&scripted_template().replace("", "
3kept nowherex
")))).unwrap();