Merge PR #3401: M12: laying out a dynamic XFA form takes off the usage rights it voids

parity/acrobat-features.toml: form.xfa-dynamic evidence list: both sides' entries; notes: both sides' insertions (word-level merge).
This commit is contained in:
aiongg
2026-10-24 06:22:01 +00:00
9 changed files with 133 additions and 21 deletions
+26
View File
@@ -4747,6 +4747,32 @@ fn measurement_edits_are_transactional_and_honour_annotation_permissions() {
);
}
#[test]
fn a_dynamic_xfa_form_is_saved_without_the_usage_rights_its_layout_voided() {
let xdp = pdfcraft_xfa::fixtures::template(3);
let original = Arc::new(pdfcraft_xfa::fixtures::usage_rights_shell(&xdp, "", false));
let mut s = Session::new().with_clock(|| 1_700_000_000);
let id = s.open("form.pdf", None, original.clone(), None).expect("opens");
let doc = s.get(id).unwrap();
let warnings = &doc.xfa.as_ref().expect("laid out from the template").warnings;
assert!(warnings.iter().any(|w| w.contains("usage rights")), "the user is told: {warnings:?}");
assert!(!doc.dirty, "laying out is still not an edit");
s.apply(id, Edit::SetFieldValue { name: "familyName".into(), value: FieldValue::Text("Singh".into()) }).unwrap();
let incremental = s.save_bytes(id).unwrap();
assert!(incremental.starts_with(&original[..]), "still an incremental save");
// Saved either way, the file no longer carries them, and keeps what was filled in.
for saved in [incremental, s.save_full_bytes(id).unwrap()] {
let cos = pdfcraft_cos::Document::open(saved.clone()).unwrap();
let catalog = cos.get(cos.root().unwrap()).as_dict().cloned().unwrap();
assert!(!catalog.contains(b"Perms"), "{catalog:?}");
let again = s.open("saved.pdf", None, saved, None).unwrap();
let d2 = s.get(again).unwrap();
assert_eq!(d2.form.iter().find(|f| f.name == "familyName").unwrap().value, vec!["Singh".to_string()]);
let warnings = &d2.xfa.as_ref().expect("the layout is remembered").warnings;
assert!(!warnings.iter().any(|w| w.contains("usage rights")), "nothing left to say: {warnings:?}");
}
}
#[test]
fn dynamic_xfa_forms_are_laid_out_on_open_filled_and_saved_incrementally() {
let original = Arc::new(pdfcraft_xfa::fixtures::shell(&pdfcraft_xfa::fixtures::template(3)));
+5 -1
View File
@@ -4846,8 +4846,12 @@ fn notices(
let mut text =
crate::i18n::fmt(tl!("Dynamic XFA form laid out from its template: {pages}, {fields}."), &[("pages", &pages), ("fields", &fields)]);
if !x.warnings.is_empty() {
// The notes are clauses ("a picture is left out"); the first starts a sentence here.
let notes = x.warnings.join("; ");
let mut chars = notes.chars();
text.push(' ');
text.push_str(&x.warnings.join("; "));
text.extend(chars.next().into_iter().flat_map(char::to_uppercase));
text.push_str(chars.as_str());
text.push('.');
}
Some(("text-cursor-input", text, true))
+8
View File
@@ -62,6 +62,14 @@ fn a_dynamic_xfa_form_is_laid_out_and_filled() {
h.get_by_label("Hide field highlights");
}
#[test]
fn a_dynamic_xfa_form_says_when_laying_it_out_took_its_usage_rights_off() {
let xdp = pdfcraft_xfa::fixtures::template(2);
let h = open(pdfcraft_xfa::fixtures::usage_rights_shell(&xdp, "", false));
// The note starts its own sentence after the layout's.
h.get_by_label_contains("laid out from its template: 2 pages, 11 fields. The form's usage rights were removed");
}
#[test]
fn a_dynamic_xfa_form_says_its_page_is_a_placeholder() {
// No fields, the form lives in the XFA packets (an array of name/stream pairs here), but
+21
View File
@@ -597,6 +597,27 @@ pub fn shell(xdp: &str) -> Vec<u8> {
assemble(objs)
}
/// [`shell`] with usage rights: the catalog's `/Perms` holds a `/UR3` signature (a stand-in
/// dictionary, nothing verifies it), beside `others` (`/DocMDP 6 0 R`, say: object 6 is a
/// signature dictionary too). `indirect` puts `/Perms` in an object of its own.
pub fn usage_rights_shell(xdp: &str, others: &str, indirect: bool) -> Vec<u8> {
let perms = format!("<< /UR3 6 0 R {others} >>");
let entry = if indirect { "7 0 R".to_string() } else { perms.clone() };
let catalog = format!("<< /Type /Catalog /Pages 2 0 R /NeedsRendering true /AcroForm << /XFA 5 0 R /Fields [] >> /Perms {entry} >>");
let mut objs: Vec<Vec<u8>> = vec![
catalog.into_bytes(),
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R >>".to_vec(),
b"<< /Length 44 >>\nstream\nBT /F1 12 Tf 72 700 Td (Please wait...) Tj ET\nendstream".to_vec(),
format!("<< /Length {} >>\nstream\n{xdp}\nendstream", xdp.len()).into_bytes(),
b"<< /Type /Sig /Reference [<< /Type /SigRef /TransformMethod /UR3 >>] >>".to_vec(),
];
if indirect {
objs.push(perms.into_bytes());
}
assemble(objs)
}
/// A dynamic form with two signature boxes of which `sign` has been signed: the file holds the
/// field a signing application leaves (named by its SOM path, its value a signature dictionary)
/// and the picture drawn in the box, a solid blue rectangle.
+3
View File
@@ -165,6 +165,9 @@ pub fn render_into(doc: &mut Document) -> Result<Report, XfaError> {
let written = pdf::write_form(doc, &form)?;
let mut warnings = form.warnings;
warnings.extend(written.warnings);
if written.usage_rights {
warnings.push(crate::pdf::USAGE_RIGHTS_REMOVED.into());
}
warnings.dedup();
Ok(Report { pages: written.pages, fields: written.fields, template_bytes: packets.xdp.len(), warnings })
}
+33 -17
View File
@@ -150,10 +150,16 @@ fn js_action(js: &str) -> Object {
Object::Dict(a)
}
/// What laying a form out says when it took the usage rights off (see [`drop_usage_rights`]).
pub(crate) const USAGE_RIGHTS_REMOVED: &str =
"the form's usage rights were removed: laying it out voids them, and a form with void usage rights can open with every field locked";
pub struct Written {
pub pages: usize,
pub fields: usize,
pub warnings: Vec<String>,
/// Usage rights were taken off the catalog (laying the form out voided them).
pub usage_rights: bool,
}
/// On a push button generated for an XFA image field: clicking it picks a picture (the forms
@@ -786,10 +792,12 @@ pub fn write_form(doc: &mut Document, form: &Form) -> Result<Written, XfaError>
catalog_changed = true;
}
}
if catalog_changed {
// The pages and fields are new: usage rights signed over the form as it was are void now.
let usage_rights = drop_usage_rights(doc, &mut catalog);
if catalog_changed || usage_rights {
doc.set(catalog_ref, Object::Dict(catalog));
}
Ok(Written { pages: count, fields: field_count, warnings })
Ok(Written { pages: count, fields: field_count, warnings, usage_rights })
}
/// What [`strip`] took out, for the user to be told (the packets held more than the fields).
@@ -975,21 +983,7 @@ pub fn strip(doc: &mut Document) -> Result<Stripped, XfaError> {
None => catalog.set(b"AcroForm".to_vec(), Object::Dict(acro)),
}
catalog.remove(b"NeedsRendering");
// Reader usage rights are void once the form changed: Reader says so on opening the file
// and drops them, so they go now, with a note.
if let Some(perms) = catalog.get(b"Perms").map(|p| doc.resolve(p))
&& let Some(mut perms) = perms.as_dict().cloned()
&& (perms.contains(b"UR3") || perms.contains(b"UR"))
{
perms.remove(b"UR3");
perms.remove(b"UR");
out.usage_rights = true;
if perms.is_empty() {
catalog.remove(b"Perms");
} else {
catalog.set(b"Perms".to_vec(), Object::Dict(perms));
}
}
out.usage_rights = drop_usage_rights(doc, &mut catalog);
doc.set(root, Object::Dict(catalog));
// The packet streams are referenced from nowhere else: freed, so an incremental save
// doesn't carry an unreachable copy of the data.
@@ -1036,6 +1030,28 @@ fn unbound_data(doc: &Document) -> Option<usize> {
Some(count)
}
/// Take the usage rights (`/Perms /UR3`, or the older `/UR`) off a catalog, keeping the rest of
/// `/Perms` (a certification's `/DocMDP`). They are signed over the form as it was: once its
/// pages or fields change they are void, and a viewer may lock every field of a form whose
/// usage rights are void, so they go, and the caller says so. Whether there were any.
fn drop_usage_rights(doc: &mut Document, catalog: &mut Dict) -> bool {
let Some(entry) = catalog.get(b"Perms").cloned() else { return false };
let Some(mut perms) = doc.resolve(&entry).as_dict().cloned() else { return false };
if !perms.contains(b"UR3") && !perms.contains(b"UR") {
return false;
}
perms.remove(b"UR3");
perms.remove(b"UR");
match entry.as_ref() {
_ if perms.is_empty() => {
catalog.remove(b"Perms");
}
Some(r) => doc.set(r, Object::Dict(perms)),
None => catalog.set(b"Perms".to_vec(), Object::Dict(perms)),
}
true
}
/// The layout a document already carries from an earlier pass, if any.
pub fn existing_layout(doc: &Document) -> Option<crate::Report> {
let root = doc.root()?;
+3
View File
@@ -601,6 +601,9 @@ pub fn rerender(doc: &mut Document, tpl: &Template) -> Result<Report, XfaError>
let written = crate::pdf::write_form(doc, &form)?;
let mut warnings = form.warnings;
warnings.extend(written.warnings);
if written.usage_rights {
warnings.push(crate::pdf::USAGE_RIGHTS_REMOVED.into());
}
warnings.dedup();
Ok(Report { pages: written.pages, fields: written.fields, template_bytes: packets.xdp.len(), warnings })
}
+31
View File
@@ -2500,6 +2500,37 @@ fn an_anchor_type_places_the_named_point_of_the_box_at_its_x_and_y() {
assert!((field.rect.x - 6.0 * 72.0).abs() < 0.01 && (field.rect.y - 72.0).abs() < 0.01, "{:?}", field.rect);
}
#[test]
fn laying_a_form_out_takes_off_the_usage_rights_it_voids() {
use crate::fixtures::usage_rights_shell;
let xdp = fields_template("<form><page1><country>JP</country></page1></form>");
// What `/Perms` holds once the layout is saved and the file read again.
let laid_out = |bytes: Vec<u8>| {
let mut doc = Document::open(Arc::new(bytes)).unwrap();
let report = render_into(&mut doc).unwrap();
let doc = Document::open(Arc::new(write_incremental(&doc, &SaveOptions::default()).unwrap())).unwrap();
let catalog = doc.get(doc.root().unwrap()).as_dict().cloned().unwrap();
let indirect = catalog.get(b"Perms").and_then(Object::as_ref).is_some();
let perms = catalog.get(b"Perms").map(|p| doc.resolve(p)).and_then(|p| p.as_dict().cloned());
let said = report.warnings.iter().any(|w| w.contains("usage rights"));
(perms, indirect, said, report.warnings)
};
// Usage rights alone: `/Perms` goes, and the user is told.
let (perms, _, said, warnings) = laid_out(usage_rights_shell(&xdp, "", false));
assert!(perms.is_none(), "{perms:?}");
assert!(said, "{warnings:?}");
// Beside a certification, in an object of its own: only the usage rights go, and the
// object is rewritten where it is.
let (perms, indirect, said, warnings) = laid_out(usage_rights_shell(&xdp, "/DocMDP 6 0 R", true));
let perms = perms.expect("the certification stays");
assert!(perms.contains(b"DocMDP") && !perms.contains(b"UR3"), "{perms:?}");
assert!(indirect, "/Perms is still its own object");
assert!(said, "{warnings:?}");
// A form without usage rights says nothing about them.
let (perms, _, said, warnings) = laid_out(shell(&xdp));
assert!(perms.is_none() && !said, "{perms:?} {warnings:?}");
}
#[test]
fn stripping_takes_the_xfa_out_and_leaves_an_ordinary_document() {
use crate::pdf::strip;
+3 -3
View File
@@ -707,7 +707,7 @@ status = "planned"
commands = []
tools = []
evidence = []
notes = "preserve only"
notes = "preserve only. A dynamic XFA form PdfCraft lays out loses them: the layout voids them, so they are taken off with a note (form.xfa-dynamic)"
# ==================================================================================================
# B. Viewing and navigation
@@ -5249,8 +5249,8 @@ milestone = "M12"
status = "partial"
commands = ["file.open"]
tools = ["doc_open", "doc_info", "form_fields", "form_fill", "js_run"]
evidence = ["crates/xfa/src/tests.rs::layout_places_captions_fields_and_sizes_containers_to_their_content", "crates/xfa/src/tests.rs::tables_break_across_pages_and_repeat_their_header_row", "crates/xfa/src/tests.rs::rendering_into_the_pdf_replaces_the_placeholder_page_and_adds_fields", "crates/engine/src/tests.rs::dynamic_xfa_forms_are_laid_out_on_open_filled_and_saved_incrementally", "crates/automation/tests/it/automation.rs::dynamic_xfa_forms_open_render_fill_and_save_through_tools", "crates/ui-egui/tests/it/xfa.rs::a_dynamic_xfa_form_is_laid_out_and_filled", "crates/xfa/src/tests.rs::nested_width_less_subforms_lay_out_in_polynomial_time", "crates/js/src/tests.rs::instance_managers_add_remove_and_count", "crates/xfa/src/tests.rs::the_form_tree_carries_values_presence_instances_and_the_events", "crates/engine/src/tests.rs::xfa_scripts_initialize_calculate_validate_toggle_and_add_rows", "crates/engine/src/tests.rs::xfa_date_arithmetic_reads_the_date_as_its_display_picture_shows_it", "crates/automation/tests/it/automation.rs::xfa_scripts_run_for_buttons_and_field_changes_through_tools", "crates/ui-egui/tests/it/xfa.rs::xfa_buttons_run_their_scripts_in_the_app", "crates/engine/src/tests.rs::hostile_xfa_open_scripts_are_merged_capped_kept_from_printing_and_noted", "crates/engine/src/tests.rs::runaway_xfa_calculations_at_open_end_quickly_with_a_report", "crates/ui-egui/tests/it/xfa.rs::a_form_whose_scripts_were_turned_off_at_open_says_so", "crates/ui-egui/tests/it/xfa.rs::scripts_turned_off_by_a_click_bring_a_closed_notice_bar_back", "crates/automation/tests/it/automation.rs::doc_info_says_when_a_runaway_script_turned_the_forms_scripts_off", "crates/xfa/src/tests.rs::the_live_form_of_nested_repeating_subforms_stops_at_its_node_budget", "crates/xfa/src/tests.rs::data_ops_of_one_event_are_one_datasets_rewrite", "crates/xfa/src/tests.rs::choice_lists_signatures_passwords_and_pictures_become_fields_and_images", "crates/xfa/src/tests.rs::choice_values_come_from_the_data_as_saved_values_and_several_for_lists", "crates/xfa/src/tests.rs::pictures_are_sized_from_their_headers_and_hostile_ones_are_refused_unallocated", "crates/engine/src/tests.rs::xfa_choice_lists_signature_and_picture_fields_open_fill_script_and_save", "crates/xfa/src/tests.rs::repeating_subforms_inside_positioned_content_get_their_instances", "crates/engine/src/tests.rs::xfa_rows_in_areas_are_laid_out_read_calculated_and_written_where_adobe_binds_them", "crates/xfa/src/tests.rs::page_area_without_occur_serves_every_body_page", "apps/pdfcraft-cli/tests/it/xfa.rs::info_reports_the_laid_out_pages_and_fields", "apps/pdfcraft-cli/tests/it/xfa.rs::text_reads_the_layout_including_a_page_absent_from_the_placeholder", "apps/pdfcraft-cli/tests/it/xfa.rs::render_matches_the_engine_layout_on_the_second_page", "apps/pdfcraft-cli/tests/it/xfa.rs::a_laid_out_form_keeps_the_original_copy_permissions", "crates/engine/src/tests.rs::filling_growable_xfa_text_reflows_and_undo_restores_the_layout", "crates/engine/src/tests.rs::xfa_text_growth_refreshes_widget_geometry_but_fixed_height_fields_keep_their_widgets", "crates/automation/tests/it/automation.rs::growable_xfa_text_paginates_after_fill_save_and_print", "crates/engine/src/tests.rs::an_xfa_script_that_fails_partway_keeps_what_it_changed_before_the_error", "crates/engine/src/tests.rs::repeated_xfa_print_buttons_run_their_click_script", "crates/automation/tests/it/automation.rs::repeated_xfa_print_buttons_work_through_tools_and_after_reopen", "crates/xfa/src/tests.rs::rich_text_max_length_reaches_the_field_and_layout", "crates/automation/tests/it/automation.rs::xfa_rich_text_max_length_is_reported_and_enforced_through_tools", "crates/xfa/src/tests.rs::bmp_and_tiff_pictures_are_decoded_and_drawn", "crates/engine/src/tests.rs::active_xfa_does_not_run_pdf_container_scripts_but_keeps_requested_alerts", "crates/engine/src/tests.rs::active_xfa_without_template_scripts_does_not_initialize_pdf_fallback_scripts", "crates/engine/src/tests.rs::inactive_xfa_and_ordinary_pdfs_keep_document_helpers_and_alerts", "crates/automation/tests/it/automation.rs::active_xfa_skips_container_fallback_scripts_after_save_and_reopen"]
notes = "Active XFA layout skips PDF-container document-script initialization for the Acrobat console/field context, so fallback-viewer scripts do not run there. Scripts remain in the file/listing; explicit console/field and XFA alerts still run. Ordinary PDF, static XFA and unlaid-out shells keep document scripts; static-conversion undo restores the XFA policy. Changed growable multiline text (no fixed h) reflows from final datasets after edits, with or without scripts; pages/widgets refresh and undo/redo, save/reopen and print retain all text. Oversized text still uses the existing read-only page-text fallback with a warning; fixed-height fields do not reflow. Repeated page-master buttons resolve their own PDF field to the shared XFA click script; the numPages-based print script requests printing from every copy, including after save/reopen (#2572). Rich-text exData/maxLength is carried into AcroForm MaxLen, reported by form_fields and enforced by form_fill (Unicode characters); limits and accepted values survive save/reopen (#2570). laid out from the template on open by the xfa crate (positioned, tb, lr-tb, table/row, page masters (a page area with no occur repeats without limit, one with occur max=1 is used once, one with occur min=1 the body never reached gets a page after the body, as Acrobat Pro does on a synthetic form), breaks, repeated header rows, captions, rich text, JPEG/PNG/GIF/BMP/TIFF images) into real pages and AcroForm fields (text, password, date with AFDate formats, check boxes, radio groups, drop-downs and list boxes with shown/saved item pairs and multi-select, signature fields, image fields showing their picture (pictures decoded once per document, at most 20 million pixels, and reused across re-layouts), buttons with reset/print/save/URL), filled from the datasets packet (including rows another viewer added), saved as an incremental revision with the values merged into the datasets so Adobe's viewers show them; doc_info reports xfa_layout. CLI info reports the laid-out page/field counts and dimensions while retaining the input file size and raw counts as pdf_pages/acroform_fields; text and render use those pages too, preserving copy permissions and leaving the source file unchanged (#1645). XFA JavaScript and FormCalc (form.xfa-formcalc) run in the js crate's XFA object model (xfa.form, SOM resolution, rawValue, formattedValue in a date display picture's pattern (number and text display pictures still give the raw value), presence, access, instance managers, xfa.host, xfa.layout, xfa.event): initialize and calculate on open, exit, validate and calculate after a field changes, click for buttons (through js_run and the app), with rows added and removed, subforms shown and hidden, and the form laid out again, all as one undo step and kept in the file. All the scripts of one event run on one thread, each in a fresh context on the form as the engine holds it then (nothing shared between scripts; the live form kept up to date in place for values, rebuilt only when the structure changed), so hundreds of calculations run within the open budget. Hostile scripts are bounded per event (merged and capped effects, messages and console lines, lower loop limits and a time budget on open and calculate, one datasets rewrite per event, print/save/links/focus from clicks only, a runaway script abandoned and the form's scripts turned off, which the app says in a toast and on the notice bar, with a button to the JavaScript console, and doc_info reports as xfa_scripts_off) and what open-time scripts changed is noted. A script that fails partway keeps what it changed before the error, as JavaScript does (forms often trip over an object an earlier version had after their first lines did the work). Missing: a page area's relevant=\"-print\" (on a synthetic form Acrobat still prints the page but leaves it blank; PdfCraft prints its content, #2574); a page break before the first section (Acrobat opens an empty body page first when the first section has breakBefore targetType=pageArea, with or without startNew, as seen on synthetic forms; PdfCraft skips a break on an empty page and lays out one page fewer, #2574); loops inside nested calls are only stopped by abandoning their thread, and script memory is not capped; the change event per keystroke; xfa.template, connections and data descriptions; a hidden instance of a repeating subform hides them all; barcodes are drawn as their text, not bars; keep rules; metrics are approximate and Acrobat fidelity is eyeballed on real forms, not measured (#60)"
evidence = ["crates/xfa/src/tests.rs::layout_places_captions_fields_and_sizes_containers_to_their_content", "crates/xfa/src/tests.rs::tables_break_across_pages_and_repeat_their_header_row", "crates/xfa/src/tests.rs::rendering_into_the_pdf_replaces_the_placeholder_page_and_adds_fields", "crates/engine/src/tests.rs::dynamic_xfa_forms_are_laid_out_on_open_filled_and_saved_incrementally", "crates/automation/tests/it/automation.rs::dynamic_xfa_forms_open_render_fill_and_save_through_tools", "crates/ui-egui/tests/it/xfa.rs::a_dynamic_xfa_form_is_laid_out_and_filled", "crates/xfa/src/tests.rs::nested_width_less_subforms_lay_out_in_polynomial_time", "crates/js/src/tests.rs::instance_managers_add_remove_and_count", "crates/xfa/src/tests.rs::the_form_tree_carries_values_presence_instances_and_the_events", "crates/engine/src/tests.rs::xfa_scripts_initialize_calculate_validate_toggle_and_add_rows", "crates/engine/src/tests.rs::xfa_date_arithmetic_reads_the_date_as_its_display_picture_shows_it", "crates/automation/tests/it/automation.rs::xfa_scripts_run_for_buttons_and_field_changes_through_tools", "crates/ui-egui/tests/it/xfa.rs::xfa_buttons_run_their_scripts_in_the_app", "crates/engine/src/tests.rs::hostile_xfa_open_scripts_are_merged_capped_kept_from_printing_and_noted", "crates/engine/src/tests.rs::runaway_xfa_calculations_at_open_end_quickly_with_a_report", "crates/ui-egui/tests/it/xfa.rs::a_form_whose_scripts_were_turned_off_at_open_says_so", "crates/ui-egui/tests/it/xfa.rs::scripts_turned_off_by_a_click_bring_a_closed_notice_bar_back", "crates/automation/tests/it/automation.rs::doc_info_says_when_a_runaway_script_turned_the_forms_scripts_off", "crates/xfa/src/tests.rs::the_live_form_of_nested_repeating_subforms_stops_at_its_node_budget", "crates/xfa/src/tests.rs::data_ops_of_one_event_are_one_datasets_rewrite", "crates/xfa/src/tests.rs::choice_lists_signatures_passwords_and_pictures_become_fields_and_images", "crates/xfa/src/tests.rs::choice_values_come_from_the_data_as_saved_values_and_several_for_lists", "crates/xfa/src/tests.rs::pictures_are_sized_from_their_headers_and_hostile_ones_are_refused_unallocated", "crates/engine/src/tests.rs::xfa_choice_lists_signature_and_picture_fields_open_fill_script_and_save", "crates/xfa/src/tests.rs::repeating_subforms_inside_positioned_content_get_their_instances", "crates/engine/src/tests.rs::xfa_rows_in_areas_are_laid_out_read_calculated_and_written_where_adobe_binds_them", "crates/xfa/src/tests.rs::page_area_without_occur_serves_every_body_page", "apps/pdfcraft-cli/tests/it/xfa.rs::info_reports_the_laid_out_pages_and_fields", "apps/pdfcraft-cli/tests/it/xfa.rs::text_reads_the_layout_including_a_page_absent_from_the_placeholder", "apps/pdfcraft-cli/tests/it/xfa.rs::render_matches_the_engine_layout_on_the_second_page", "apps/pdfcraft-cli/tests/it/xfa.rs::a_laid_out_form_keeps_the_original_copy_permissions", "crates/engine/src/tests.rs::filling_growable_xfa_text_reflows_and_undo_restores_the_layout", "crates/engine/src/tests.rs::xfa_text_growth_refreshes_widget_geometry_but_fixed_height_fields_keep_their_widgets", "crates/automation/tests/it/automation.rs::growable_xfa_text_paginates_after_fill_save_and_print", "crates/engine/src/tests.rs::an_xfa_script_that_fails_partway_keeps_what_it_changed_before_the_error", "crates/engine/src/tests.rs::repeated_xfa_print_buttons_run_their_click_script", "crates/automation/tests/it/automation.rs::repeated_xfa_print_buttons_work_through_tools_and_after_reopen", "crates/xfa/src/tests.rs::rich_text_max_length_reaches_the_field_and_layout", "crates/automation/tests/it/automation.rs::xfa_rich_text_max_length_is_reported_and_enforced_through_tools", "crates/xfa/src/tests.rs::bmp_and_tiff_pictures_are_decoded_and_drawn", "crates/engine/src/tests.rs::active_xfa_does_not_run_pdf_container_scripts_but_keeps_requested_alerts", "crates/engine/src/tests.rs::active_xfa_without_template_scripts_does_not_initialize_pdf_fallback_scripts", "crates/engine/src/tests.rs::inactive_xfa_and_ordinary_pdfs_keep_document_helpers_and_alerts", "crates/automation/tests/it/automation.rs::active_xfa_skips_container_fallback_scripts_after_save_and_reopen", "crates/xfa/src/tests.rs::laying_a_form_out_takes_off_the_usage_rights_it_voids", "crates/engine/src/tests.rs::a_dynamic_xfa_form_is_saved_without_the_usage_rights_its_layout_voided"]
notes = "Active XFA layout skips PDF-container document-script initialization for the Acrobat console/field context, so fallback-viewer scripts do not run there. Scripts remain in the file/listing; explicit console/field and XFA alerts still run. Ordinary PDF, static XFA and unlaid-out shells keep document scripts; static-conversion undo restores the XFA policy. Changed growable multiline text (no fixed h) reflows from final datasets after edits, with or without scripts; pages/widgets refresh and undo/redo, save/reopen and print retain all text. Oversized text still uses the existing read-only page-text fallback with a warning; fixed-height fields do not reflow. Repeated page-master buttons resolve their own PDF field to the shared XFA click script; the numPages-based print script requests printing from every copy, including after save/reopen (#2572). Rich-text exData/maxLength is carried into AcroForm MaxLen, reported by form_fields and enforced by form_fill (Unicode characters); limits and accepted values survive save/reopen (#2570). laid out from the template on open by the xfa crate (positioned, tb, lr-tb, table/row, page masters (a page area with no occur repeats without limit, one with occur max=1 is used once, one with occur min=1 the body never reached gets a page after the body, as Acrobat Pro does on a synthetic form), breaks, repeated header rows, captions, rich text, JPEG/PNG/GIF/BMP/TIFF images) into real pages and AcroForm fields (text, password, date with AFDate formats, check boxes, radio groups, drop-downs and list boxes with shown/saved item pairs and multi-select, signature fields, image fields showing their picture (pictures decoded once per document, at most 20 million pixels, and reused across re-layouts), buttons with reset/print/save/URL), filled from the datasets packet (including rows another viewer added), saved as an incremental revision with the values merged into the datasets so Adobe's viewers show them; usage rights the form carried (/Perms /UR3) are taken off with a note, as the layout voids them and a form with void usage rights opens with every field locked; doc_info reports xfa_layout. CLI info reports the laid-out page/field counts and dimensions while retaining the input file size and raw counts as pdf_pages/acroform_fields; text and render use those pages too, preserving copy permissions and leaving the source file unchanged (#1645). XFA JavaScript and FormCalc (form.xfa-formcalc) run in the js crate's XFA object model (xfa.form, SOM resolution, rawValue, formattedValue in a date display picture's pattern (number and text display pictures still give the raw value), presence, access, instance managers, xfa.host, xfa.layout, xfa.event): initialize and calculate on open, exit, validate and calculate after a field changes, click for buttons (through js_run and the app), with rows added and removed, subforms shown and hidden, and the form laid out again, all as one undo step and kept in the file. All the scripts of one event run on one thread, each in a fresh context on the form as the engine holds it then (nothing shared between scripts; the live form kept up to date in place for values, rebuilt only when the structure changed), so hundreds of calculations run within the open budget. Hostile scripts are bounded per event (merged and capped effects, messages and console lines, lower loop limits and a time budget on open and calculate, one datasets rewrite per event, print/save/links/focus from clicks only, a runaway script abandoned and the form's scripts turned off, which the app says in a toast and on the notice bar, with a button to the JavaScript console, and doc_info reports as xfa_scripts_off) and what open-time scripts changed is noted. A script that fails partway keeps what it changed before the error, as JavaScript does (forms often trip over an object an earlier version had after their first lines did the work). Missing: a page area's relevant=\"-print\" (on a synthetic form Acrobat still prints the page but leaves it blank; PdfCraft prints its content, #2574); a page break before the first section (Acrobat opens an empty body page first when the first section has breakBefore targetType=pageArea, with or without startNew, as seen on synthetic forms; PdfCraft skips a break on an empty page and lays out one page fewer, #2574); loops inside nested calls are only stopped by abandoning their thread, and script memory is not capped; the change event per keystroke; xfa.template, connections and data descriptions; a hidden instance of a repeating subform hides them all; barcodes are drawn as their text, not bars; keep rules; metrics are approximate and Acrobat fidelity is eyeballed on real forms, not measured (#60)"
[[feature]]
id = "form.xfa-formcalc"