mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 13:01:29 +00:00
M12: XFA forms with many repeated rows lay out in linear time (form.xfa-dynamic) (#6776)
This commit is contained in:
1 parent
85e2ebfac8
commit
d9ec80243b
6 files changed
+55
-15
No files matched your search
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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::{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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| {
|
||||
|
||||
Reference in new issue
Block a user