From d9ec80243b3e42e512989bcb444615bc88a6092f Mon Sep 17 00:00:00 2001 From: mtlcl410 <2368091+mtlcl410@users.noreply.github.com> Date: Tue, 10 Nov 2026 11:42:57 +0800 Subject: [PATCH] M12: XFA forms with many repeated rows lay out in linear time (form.xfa-dynamic) (#6776) --- crates/engine/src/xfa_form.rs | 11 ++++------- crates/forms/src/author.rs | 2 +- crates/forms/src/author/look.rs | 21 ++++++++++++++++++++- crates/forms/src/lib.rs | 2 +- crates/forms/src/tests.rs | 28 ++++++++++++++++++++++++++++ crates/redact/src/sanitize.rs | 6 +----- 6 files changed, 55 insertions(+), 15 deletions(-) diff --git a/crates/engine/src/xfa_form.rs b/crates/engine/src/xfa_form.rs index 5739c57160..be2ff1883e 100644 --- a/crates/engine/src/xfa_form.rs +++ b/crates/engine/src/xfa_form.rs @@ -15,13 +15,10 @@ pub(crate) fn xfa_layout(doc: &mut pdfcraft_cos::Document) -> Result Result<(), String> { - for f in pdfcraft_forms::fields(doc) { - if f.kind == pdfcraft_forms::FieldKind::Signature && doc.get(f.obj).as_dict().is_some_and(|d| d.contains(b"V")) { - continue; - } - pdfcraft_forms::redraw_field(doc, &f.name).map_err(|e| format!("{}: {e}", f.name))?; - } - Ok(()) + let signed = |doc: &pdfcraft_cos::Document, f: &pdfcraft_forms::Field| { + f.kind == pdfcraft_forms::FieldKind::Signature && doc.get(f.obj).as_dict().is_some_and(|d| d.contains(b"V")) + }; + pdfcraft_forms::redraw_fields(doc, |doc, f| !signed(doc, f)).map_err(|e| e.to_string()) } /// A warning for each field whose appearance PdfCraft drew with `?` for a character its font diff --git a/crates/forms/src/author.rs b/crates/forms/src/author.rs index 715db946e4..ee6eef518b 100644 --- a/crates/forms/src/author.rs +++ b/crates/forms/src/author.rs @@ -21,7 +21,7 @@ pub(crate) use tree::ancestor_sets; pub use edit::set_props; pub use lifecycle::{add_field, delete_field, delete_field_object, delete_widget, duplicate_field}; pub(crate) use look::button_appearance; -pub use look::{BorderStyle, CheckStyle, FieldFont, Look, LookPatch, check_style, look, redraw_field, set_button_icon}; +pub use look::{BorderStyle, CheckStyle, FieldFont, Look, LookPatch, check_style, look, redraw_field, redraw_fields, set_button_icon}; pub(crate) use tree::{ensure_form, form_fonts_as_objects}; /// The smallest width and height of a field's widget, in points: a smaller one is refused. diff --git a/crates/forms/src/author/look.rs b/crates/forms/src/author/look.rs index 325a1ec3ad..01e78f8dc1 100644 --- a/crates/forms/src/author/look.rs +++ b/crates/forms/src/author/look.rs @@ -315,6 +315,25 @@ pub fn redraw_field(doc: &mut Document, name: &str) -> Result<(), FormError> { /// `FormError::NoSuchField` when no field is named `name`. pub(super) fn redraw_widgets(doc: &mut Document, name: &str, only: Option) -> Result<(), FormError> { let f = fields(doc).into_iter().find(|f| f.name == name).ok_or_else(|| FormError::NoSuchField(name.into()))?; + redraw_widgets_of(doc, &f, only) +} + +/// [`redraw_field`] for each field [`fields`] lists that `keep` accepts. The form's fields are +/// read once: drawing a field changes only its own widgets' appearances, so the list stays true +/// for the rest. Looking each one up by name instead reads the whole form again for every field, +/// which grows with the square of the field count (a laid out XFA form with a thousand rows holds +/// thousands of fields). A failure names the field it stopped at. +pub fn redraw_fields(doc: &mut Document, mut keep: impl FnMut(&Document, &Field) -> bool) -> Result<(), FormError> { + for f in fields(doc) { + if keep(doc, &f) { + redraw_widgets_of(doc, &f, None).map_err(|e| FormError::Invalid(format!("{}: {e}", f.name)))?; + } + } + Ok(()) +} + +/// [`redraw_widgets`] for the field `f`, as `fields(doc)` read it. +fn redraw_widgets_of(doc: &mut Document, f: &Field, only: Option) -> Result<(), FormError> { let mut undrawn = false; for (index, w) in f.widgets.iter().enumerate() { if only.is_some_and(|only| only != index) { @@ -323,7 +342,7 @@ pub(super) fn redraw_widgets(doc: &mut Document, name: &str, only: Option let mut missing = None; let ap = match f.kind { FieldKind::Text | FieldKind::Combo | FieldKind::List => { - let (mut s, m) = appearance::field_appearance_drawn(doc, &f, w, &f.value, true); + let (mut s, m) = appearance::field_appearance_drawn(doc, f, w, &f.value, true); missing = m; // Thai glyphs are streams of their own. pdfcraft_fonts::shaped_text::store_streams(doc, &mut s.dict); diff --git a/crates/forms/src/lib.rs b/crates/forms/src/lib.rs index 236310f268..6a35956ed0 100644 --- a/crates/forms/src/lib.rs +++ b/crates/forms/src/lib.rs @@ -27,7 +27,7 @@ pub mod table; pub use actions::{FieldAction, Trigger, field_action_chains, field_actions, set_field_actions}; pub use author::{ BorderStyle, CheckStyle, FieldFont, FieldProps, Look, LookPatch, MIN_FIELD_SIDE, NewField, add_field, check_style, delete_field, - delete_field_object, delete_widget, duplicate_field, look, redraw_field, set_button_icon, set_props, + delete_field_object, delete_widget, duplicate_field, look, redraw_field, redraw_fields, set_button_icon, set_props, }; pub use calc_order::{CalculatedField, CalculationIssue, CalculationOrder, calculation_order, set_calculation_order}; pub use scripting::{ diff --git a/crates/forms/src/tests.rs b/crates/forms/src/tests.rs index defdf6a71f..6f3eda2be4 100644 --- a/crates/forms/src/tests.rs +++ b/crates/forms/src/tests.rs @@ -5785,6 +5785,34 @@ fn set_values_reads_the_form_once_not_per_value() { assert_eq!(reads, 3, "one to fill, one when f0 comes again, one to recalculate"); } +/// Redrawing every field of a large form reads the form once, not once per field: a laid out XFA +/// form with a thousand rows holds thousands of fields, and reading them all again for each one +/// took 15 s a relayout. The fields `keep` turns down keep the appearance they had. +#[test] +fn redraw_fields_reads_the_form_once_however_many_fields_it_holds() { + for n in [250u32, 1000] { + let mut objs: Vec = vec![ + "<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>".into(), + "<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 600 800] >>".into(), + ]; + let kids: Vec = (0..n).map(|i| format!("{} 0 R", i + 5)).collect(); + objs.push(format!("<< /Type /Page /Parent 2 0 R /Annots [{}] >>", kids.join(" "))); + objs.push(format!("<< /Fields [{}] /DA (/Helv 10 Tf 0 g) >>", kids.join(" "))); + for i in 0..n { + objs.push(format!("<< /Type /Annot /Subtype /Widget /FT /Tx /T (f{i}) /V (v{i}) /Rect [50 0 250 10] /P 3 0 R >>")); + } + let mut doc = document(&objs); + let before = form_reads(); + redraw_fields(&mut doc, |_, f| f.name != "f0").unwrap(); + assert_eq!(form_reads() - before, 1, "{n} fields"); + let all = fields(&doc); + let has_ap = |name: &str| field(&all, name).widgets.iter().all(|w| doc.get(w.obj).as_dict().is_some_and(|d| d.contains(b"AP"))); + assert!(!has_ap("f0"), "turned down: left as it was"); + assert!((1..n).all(|i| has_ap(&format!("f{i}"))), "{n} fields"); + assert!(ap(&doc, &field(&all, &format!("f{}", n - 1)).widgets[0]).contains(&format!("(v{})", n - 1))); + } +} + /// A radio group `size` with the buttons Small (on), Medium and Large, a per-widget `/Opt`, and /// the group's buttons in the calculation order; and a text field `name` with two widgets. fn radio_group_form() -> Document { diff --git a/crates/redact/src/sanitize.rs b/crates/redact/src/sanitize.rs index 2734dcd7ee..f51197d16d 100644 --- a/crates/redact/src/sanitize.rs +++ b/crates/redact/src/sanitize.rs @@ -929,11 +929,7 @@ pub(crate) fn remove_counted( let listed_before = if annotations { listed_annotations(doc) } else { std::collections::HashSet::new() }; if fields { // Fields without appearances get one first, so their values stay visible. - for f in pdfcraft_forms::fields(doc) { - if f.widgets.iter().any(|w| !doc.get(w.obj).as_dict().is_some_and(|d| d.contains(b"AP"))) { - pdfcraft_forms::redraw_field(doc, &f.name)?; - } - } + pdfcraft_forms::redraw_fields(doc, |doc, f| f.widgets.iter().any(|w| !doc.get(w.obj).as_dict().is_some_and(|d| d.contains(b"AP"))))?; let n = pdfcraft_model::pages(doc).len(); pdfcraft_edit::flatten(doc, &(0..n).collect::>(), false, true)?; doc.update_dict(root, |d| {