M12: XFA forms with many repeated rows lay out in linear time (form.xfa-dynamic) (#6776)

This commit is contained in:
mtlcl410 authored and GitHub committed 2026-11-10 10:42:57 +07:00
1 parent 85e2ebfac8
commit d9ec80243b
6 files changed
+55 -15

No files matched your search

+4 -7
View File
@@ -15,13 +15,10 @@ pub(crate) fn xfa_layout(doc: &mut pdfcraft_cos::Document) -> Result<XfaLayout,
/// Give the fields of a laid-out XFA form their appearances. A signed signature box keeps the
/// picture its signer's application drew.
pub(crate) fn xfa_redraw(doc: &mut pdfcraft_cos::Document) -> 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
+1 -1
View File
@@ -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.
+20 -1
View File
@@ -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<usize>) -> 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<usize>) -> 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<usize>
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);
+1 -1
View File
@@ -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::{
+28
View File
@@ -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<String> = 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<String> = (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 {
+1 -5
View File
@@ -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::<Vec<_>>(), false, true)?;
doc.update_dict(root, |d| {