Merge #4537: XFA: fields and exclusion groups that must be filled in are Required

# Conflicts:
#	crates/xfa/src/layout/draw.rs
#	crates/xfa/src/layout/field.rs
#	crates/xfa/src/layout/items.rs
#	parity/acrobat-features.toml
This commit is contained in:
bflatastic committed 2026-11-09 18:22:54 +07:00
commit 3394f3077b
9 files changed
+159 -30

No files matched your search

+3 -2
View File
@@ -25,7 +25,7 @@ that warning and keeps the existing layout and accepted widget values.
margins, borders (edge order rules), captions with reserves, check-button items, pictures,
rich text (`exData` XHTML: paragraphs, `br`, bold/italic/underline/size spans, embedded fields),
text-field limits (`text/maxChars` and `exData/maxLength`, carried into AcroForm `/MaxLen`),
inline JPEG, PNG and GIF images, lines, rectangles, occurrences, `breakBefore`/`breakAfter`, overflow
required fields and exclusion groups (`<validate nullTest="error">`), inline JPEG, PNG and GIF images, lines, rectangles, occurrences, `breakBefore`/`breakAfter`, overflow
leaders, `columnWidths` and `colSpan`.
- **Layout** (`layout`): positioned, `tb`, `lr-tb`/`rl-tb`, `table` and `row` layouts; a
container without a width is as wide as its content; flow across content areas and pages with
@@ -50,7 +50,8 @@ that warning and keeps the existing layout and accepted widget values.
scripts, and signature fields, each named by its SOM path (`form1[0].page1[0].sign[0]`, as
fields under one for each step above it) because other viewers lay a dynamic form out again
and find a signature by that name. A box whose path, or a part of it, the file already gives
another field keeps its own name, and a new layout keeps a signature a generated box holds. Appearance streams are left empty for the forms layer to
another field keeps its own name, and a new layout keeps a signature a generated box holds. A field or exclusion group that must be filled in gets the `Required` flag (a radio
group on its group field), which viewers outline when they highlight fields. Appearance streams are left empty for the forms layer to
generate. Each field keeps its SOM path in `/PCSom`; the AcroForm gets `/PCXfaLayout` so a saved form is not laid out
twice. The XFA packets stay, so Adobe's viewers keep rendering the form from them.
+16 -3
View File
@@ -1,8 +1,8 @@
use super::common::clean_name;
use super::items::{Item, Rounded, Turned};
use super::{
Arc, Border, Ctx, Draw, ExclGroup, Face, Font, Layout, Layouter, MAX_DEPTH, MAX_IMAGE_BYTES, Node, Para, Rect, SomIndices, Stroke, Subform,
Value, XfaError, data_path, text,
Arc, Border, Ctx, Draw, ExclGroup, Face, Field, Font, Layout, Layouter, MAX_DEPTH, MAX_IMAGE_BYTES, Node, Para, Rect, SomIndices, Stroke,
Subform, Value, XfaError, data_path, text,
};
impl Layouter<'_> {
@@ -304,6 +304,8 @@ impl Layouter<'_> {
self.push_block(block)
}
/// Draw a `draw` element (static text, an image, a line, a rectangle or an arc) in `rect`,
/// inside its border and margin.
fn draw_draw(&mut self, d: &Draw, rect: Rect, ctx: &Ctx) -> Result<(), XfaError> {
if let Some(b) = &d.common.border {
self.draw_border(b, rect)?;
@@ -330,7 +332,9 @@ impl Layouter<'_> {
}
/// Draw exclusion group `g` at `rect`: its border, then each of its buttons where its layout
/// puts it, as one radio group, the one its data selects turned on.
/// puts it, as one radio group, the one its data selects turned on. A group that must be
/// answered makes each button required. `sib` is the group's index among same-named
/// siblings, for its SOM path.
fn draw_group(&mut self, g: &ExclGroup, rect: Rect, ctx: &Ctx, depth: usize, sib: usize) -> Result<(), XfaError> {
if let Some(b) = &g.common.border {
self.draw_border(b, rect)?;
@@ -382,6 +386,15 @@ impl Layouter<'_> {
let on = f.items.first().cloned().filter(|s| !s.trim().is_empty()).unwrap_or_else(|| (i + 1).to_string());
let tip = f.tooltip.clone().or_else(|| g.tooltip.clone());
let chosen = selected.as_deref() == Some(on.as_str());
// A group that must be answered makes each of its buttons required, as the group
// field they make is.
let required;
let f = if g.required && !f.required {
required = Field { required: true, ..f.clone() };
&required
} else {
f
};
let outer = std::mem::replace(&mut self.views, views);
let drawn = self.draw_field(f, Rect::new(inner.x + r.x, inner.y + r.y, r.w, r.h), &ctx, Some((&group, &on, tip, chosen)), sibs.at(i));
self.views = outer;
+1
View File
@@ -267,6 +267,7 @@ impl Layouter<'_> {
no_scroll: f.no_scroll,
// `push` sets where it shows.
views: Views::ALL,
required: f.required,
})))
}
}
+2
View File
@@ -152,6 +152,8 @@ pub struct Widget {
pub no_scroll: bool,
/// Where the field is shown: on screen, in print, or both (its annotation flags).
pub views: Views,
/// The field must be filled in (`Required`); a radio button, when its group must be.
pub required: bool,
}
#[derive(Clone, Debug, Default, PartialEq)]
+4
View File
@@ -517,6 +517,8 @@ pub struct ExclGroup {
pub fields: Vec<Field>,
pub tooltip: Option<String>,
pub scripts: Vec<Script>,
/// One of the buttons must be chosen (`<validate nullTest="error">` on the group).
pub required: bool,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
@@ -577,6 +579,8 @@ pub struct Field {
/// The field doesn't scroll in the direction its text runs out (`hScrollPolicy="off"` for one
/// line, `vScrollPolicy="off"` for several): it takes no more text than it shows.
pub no_scroll: bool,
/// The field must be filled in (`<validate nullTest="error">`).
pub required: bool,
pub caption: Option<Caption>,
pub value: Value,
/// Check buttons: the on value then the off value; choice lists: the items as shown.
+13
View File
@@ -585,12 +585,19 @@ fn parse_field(n: XmlNode, warnings: &mut Vec<String>) -> Field {
calculate: child(n, "calculate").and_then(|c| child(c, "script")).map(|s| parse_script(s, "calculate")),
validate: validate_el.and_then(|v| child(v, "script")).map(|s| parse_script(s, "validate")),
validate_message,
required: must_be_filled(validate_el),
numeric,
display_picture: child(n, "format").and_then(|f| child(f, "picture")).map(text_of).map(|p| p.trim().to_string()).filter(|p| !p.is_empty()),
keep_intact: child(n, "keep").and_then(|k| k.attribute("intact")).is_some_and(|v| v == "contentArea" || v == "pageArea"),
}
}
/// Whether a `<validate>` element makes its object one that must be filled in: `nullTest="error"`
/// (`warning` only recommends it, and `disabled`, the default, leaves it optional).
fn must_be_filled(validate: Option<XmlNode>) -> bool {
validate.is_some_and(|v| v.attribute("nullTest").is_some_and(|t| t.trim() == "error"))
}
fn parse_script(s: XmlNode, activity: &str) -> Script {
let ct = s.attribute("contentType").unwrap_or("");
Script { activity: activity.to_string(), text: text_of(s).trim().to_string(), formcalc: !ct.to_ascii_lowercase().contains("javascript") }
@@ -672,6 +679,8 @@ fn parse_page_set(n: XmlNode, warnings: &mut Vec<String>, depth: usize) -> PageS
PageSet { areas }
}
/// A `subform` element: its own attributes, its page set if it has one, and the nodes it holds,
/// `depth` levels down (for the nesting limit).
fn parse_subform(n: XmlNode, warnings: &mut Vec<String>, depth: usize) -> Subform {
let common = parse_common(n);
let mut children_out = Vec::new();
@@ -706,6 +715,9 @@ fn parse_subform(n: XmlNode, warnings: &mut Vec<String>, depth: usize) -> Subfor
}
}
/// The template object element `c` stands for (a subform, area, field, draw or exclusion group,
/// with what it holds), or `None` for anything else and past [`MAX_DEPTH`] levels. What can't be
/// read faithfully is noted in `warnings`.
fn parse_node(c: XmlNode, warnings: &mut Vec<String>, depth: usize) -> Option<Node> {
if depth > MAX_DEPTH {
return None;
@@ -730,6 +742,7 @@ fn parse_node(c: XmlNode, warnings: &mut Vec<String>, depth: usize) -> Option<No
fields: children(c, "field").map(|f| parse_field(f, warnings)).collect(),
tooltip: child(c, "assist").and_then(|a| child(a, "toolTip")).map(text_of).map(|t| t.trim().to_string()).filter(|t| !t.is_empty()),
scripts: parse_events(c),
required: must_be_filled(child(c, "validate")),
})),
_ => return None,
})
+49 -23
View File
@@ -179,17 +179,62 @@ fn existing_fields(doc: &Document, catalog: &Dict) -> (HashMap<String, Signed>,
(out, names)
}
/// Required (`Ff` bit 2), which viewers outline when they highlight a form's fields, for widget
/// `w` when it must be filled in. A radio button's flags are its group's (the group takes it,
/// [`Emitter::radio_group`]), and a push button keeps no value (ISO 32000-2 §12.7.5.2.2), so
/// there is nothing it could be required to hold.
fn required_flag(w: &Widget) -> i64 {
if w.required && !matches!(w.kind, WidgetKind::Radio { .. } | WidgetKind::Button { .. }) { 1 << 1 } else { 0 }
}
impl Emitter<'_> {
fn da(face: &Face) -> Vec<u8> {
format!("/{} {} Tf {} rg", face.resource_name(), n(face.size), rgb(face.color)).into_bytes()
}
fn populate_widget_kind(&mut self, w: &Widget, d: &mut Dict, mk: &mut Dict) -> (i64, bool, Option<String>) {
/// The field of radio group `group`, which button `w` is a kid of: the one an earlier button
/// made (required too when `w` must be chosen), or a new field of the form, with `w`'s text
/// look and the group's SOM path (the button's without its last step).
fn radio_group(&mut self, w: &Widget, group: &str) -> Result<ObjRef, XfaError> {
if let Some((p, _)) = self.radio_groups.get(group) {
let p = *p;
// A later button that must be chosen makes the group required too.
if w.required {
self.doc.update_dict(p, |g| {
let ff = g.int(b"Ff").unwrap_or(0);
g.set(b"Ff".to_vec(), Object::Int(ff | (1 << 1)));
})?;
}
return Ok(p);
}
let mut g = Dict::new();
g.set(b"FT".to_vec(), Object::name("Btn"));
let required = if w.required { 1 << 1 } else { 0 };
g.set(b"Ff".to_vec(), Object::Int((1 << 15) | (1 << 14) | required));
g.set(b"T".to_vec(), Object::String(PdfString::text(group)));
g.set(b"V".to_vec(), Object::name("Off"));
g.set(b"DA".to_vec(), Object::String(PdfString::literal(Self::da(&w.face))));
g.set(b"Kids".to_vec(), Object::Array(Vec::new()));
let group_som = w.som.rsplit_once('.').map(|(g, _)| g.to_string()).unwrap_or_else(|| w.som.clone());
g.set(SOM_KEY.to_vec(), Object::String(PdfString::text(&group_som)));
let p = self.doc.add(Object::Dict(g));
self.radio_groups.insert(group.to_string(), (p, Vec::new()));
self.fields.push(p);
self.field_count += 1;
Ok(p)
}
/// Fill in widget `w`'s field by its kind (field type, flags, value, appearance states, items,
/// action) in `d` and `mk`. Returns its field flags, whether it is a top-level field (not a kid
/// of its radio group's field, nor a signature field under the fields generated for its SOM
/// path), and for such a signature field the SOM path of the generated field above it.
fn populate_widget_kind(&mut self, w: &Widget, d: &mut Dict, mk: &mut Dict) -> Result<(i64, bool, Option<String>), XfaError> {
let r = w.rect;
let mut ff: i64 = 0;
if w.read_only {
ff |= 1;
}
ff |= required_flag(w);
let mut top_level = true;
// The SOM path of the generated field above this one, for a signature field named by its
// SOM path.
@@ -334,26 +379,7 @@ impl Emitter<'_> {
}
WidgetKind::Radio { group, on, .. } => {
top_level = false;
let parent = match self.radio_groups.get(group) {
Some((p, _)) => *p,
None => {
let mut g = Dict::new();
g.set(b"FT".to_vec(), Object::name("Btn"));
g.set(b"Ff".to_vec(), Object::Int((1 << 15) | (1 << 14)));
g.set(b"T".to_vec(), Object::String(PdfString::text(group)));
g.set(b"V".to_vec(), Object::name("Off"));
g.set(b"DA".to_vec(), Object::String(PdfString::literal(Self::da(&w.face))));
g.set(b"Kids".to_vec(), Object::Array(Vec::new()));
// The group's SOM path: the button's without its last step.
let group_som = w.som.rsplit_once('.').map(|(g, _)| g.to_string()).unwrap_or_else(|| w.som.clone());
g.set(SOM_KEY.to_vec(), Object::String(PdfString::text(&group_som)));
let p = self.doc.add(Object::Dict(g));
self.radio_groups.insert(group.clone(), (p, Vec::new()));
self.fields.push(p);
self.field_count += 1;
p
}
};
let parent = self.radio_group(w, group)?;
d.set(b"Parent".to_vec(), Object::Ref(parent));
// Appearance states are PDF names, so a space becomes an underscore. The datasets
// still hold the template's text (`/PCOn`); writing the name back would drop the selection.
@@ -379,7 +405,7 @@ impl Emitter<'_> {
write_button_action(w.action.as_ref(), d);
}
}
(ff, top_level, under)
Ok((ff, top_level, under))
}
/// Write laid-out widget `w` on `page` as an AcroForm widget annotation (and its field), with
@@ -423,7 +449,7 @@ impl Emitter<'_> {
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);
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));
}
+69
View File
@@ -504,6 +504,10 @@ fn tables_break_across_pages_and_repeat_their_header_row() {
assert_eq!(all.len(), all.iter().collect::<std::collections::HashSet<_>>().len(), "field names are unique");
}
/// Laying a dynamic form out into its PDF replaces the placeholder page with the template's pages
/// and adds its fields (a radio group once), keeps the XFA packets and `/NeedsRendering` for Adobe's
/// viewers, writes each `/DR` font as an object of its own, and marks the form so it isn't laid out
/// twice.
#[test]
fn rendering_into_the_pdf_replaces_the_placeholder_page_and_adds_fields() {
let mut doc = Document::open(Arc::new(shell(&template(2)))).unwrap();
@@ -542,6 +546,71 @@ fn rendering_into_the_pdf_replaces_the_placeholder_page_and_adds_fields() {
assert!(!is_dynamic(&doc));
}
/// A field or exclusion group that must be filled in (`<validate nullTest="error">`) becomes a
/// Required field, which viewers outline when they highlight a form's fields: a radio group on the
/// group field, its buttons' flags being the group's (a button of its own that must be chosen
/// makes the group required too). `nullTest="warning"` only recommends a value,
/// so it, like no test, leaves the field optional. A push button keeps no value (ISO 32000-2
/// §12.7.5.2.2), so it is never required.
#[test]
fn fields_that_must_be_filled_in_are_required() {
let xdp = r##"<?xml version="1.0" encoding="UTF-8"?>
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/"><template xmlns="http://www.xfa.org/schema/xfa-template/3.3/"><subform name="form" layout="tb">
<pageSet><pageArea name="p"><contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/><medium short="8.5in" long="11in"/></pageArea></pageSet>
<field name="must" w="2in" h="0.3in"><ui><textEdit/></ui><validate nullTest="error"/></field>
<field name="should" w="2in" h="0.3in"><ui><textEdit/></ui><validate nullTest="warning"/></field>
<field name="may" w="2in" h="0.3in"><ui><textEdit/></ui></field>
<field name="agree" w="2in" h="0.3in"><ui><checkButton/></ui><items><integer>1</integer><integer>0</integer></items><validate nullTest="error"/></field>
<field name="go" w="1in" h="0.3in"><ui><button/></ui><caption><value><text>Go</text></value></caption><validate nullTest="error"/></field>
<exclGroup name="answer" layout="lr-tb"><validate nullTest="error"/>
<field name="yes" w="1in" h="0.3in"><ui><checkButton shape="round"/></ui><items><text>Y</text></items></field>
<field name="no" w="1in" h="0.3in"><ui><checkButton shape="round"/></ui><items><text>N</text></items></field>
</exclGroup>
<exclGroup name="optional" layout="lr-tb">
<field name="a" w="1in" h="0.3in"><ui><checkButton shape="round"/></ui><items><text>A</text></items></field>
</exclGroup>
<exclGroup name="second" layout="lr-tb">
<field name="b" w="1in" h="0.3in"><ui><checkButton shape="round"/></ui><items><text>B</text></items></field>
<field name="c" w="1in" h="0.3in"><ui><checkButton shape="round"/></ui><items><text>C</text></items><validate nullTest="error"/></field>
</exclGroup>
</subform></template></xdp:xdp>"##;
let mut doc = Document::open(Arc::new(shell(xdp))).unwrap();
render_into(&mut doc).unwrap();
let catalog = doc.get(doc.root().unwrap());
let acro = doc.resolve(catalog.as_dict().unwrap().get(b"AcroForm").unwrap());
let fields = doc.resolve(acro.as_dict().unwrap().get(b"Fields").unwrap()).as_array().unwrap().clone();
let required: std::collections::BTreeMap<String, bool> = fields
.iter()
.map(|f| {
let d = doc.resolve(f).as_dict().unwrap().clone();
let name = d.get(b"T").and_then(|t| doc.resolve(t).as_string().map(|s| s.to_text())).unwrap_or_default();
(name, d.int(b"Ff").unwrap_or(0) & 2 != 0)
})
.collect();
let leaf = |n: &str| required.iter().find(|(k, _)| k.contains(n)).map(|(_, r)| *r);
assert_eq!(leaf("must"), Some(true), "{required:?}");
assert_eq!(leaf("should"), Some(false), "a warning only recommends a value");
assert_eq!(leaf("may"), Some(false));
assert_eq!(leaf("agree"), Some(true), "a check box too");
assert_eq!(leaf("go"), Some(false), "a push button holds no value to fill in");
assert_eq!(leaf("answer"), Some(true), "the radio group field");
assert_eq!(leaf("optional"), Some(false));
assert_eq!(leaf("second"), Some(true), "a later button that must be chosen makes its group required");
// The buttons themselves carry no Required flag: theirs are the group's.
for group in ["answer", "second"] {
let field = fields
.iter()
.map(|f| doc.resolve(f).as_dict().unwrap().clone())
.find(|d| d.get(b"T").and_then(|t| doc.resolve(t).as_string().map(|s| s.to_text())).is_some_and(|t| t.contains(group)));
let kids = field.and_then(|d| d.get(b"Kids").map(|k| doc.resolve(k).as_array().cloned().unwrap_or_default())).unwrap_or_default();
assert!(!kids.is_empty(), "{group} has its buttons as kids");
for kid in &kids {
let ff = doc.resolve(kid).as_dict().and_then(|d| d.int(b"Ff")).unwrap_or(0);
assert_eq!(ff & 2, 0, "{group}: a button is not Required itself");
}
}
}
/// A dynamic form's own text that the standard fonts can't draw is laid out anyway and reported
/// once, never written as `?` silently.
#[test]
+2 -2
View File
@@ -5447,8 +5447,8 @@ milestone = "M12"
status = "partial"
commands = ["file.open"]
tools = ["doc_open", "doc_info", "form_fields", "form_fill", "js_run", "form_focus"]
evidence = ["crates/xfa/src/tests.rs::a_draw_turns_counter_clockwise_about_its_anchor", "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", "crates/xfa/src/tests.rs::faces_measure_text_at_their_standard_14_widths", "crates/xfa/src/tests.rs::rich_text_links_keep_their_address", "crates/xfa/src/tests.rs::links_in_rich_text_become_link_annotations_one_per_line", "crates/xfa/src/tests.rs::a_links_pieces_share_one_address", "crates/xfa/src/tests.rs::a_rule_between_two_pieces_of_a_link_keeps_them_apart", "crates/xfa/src/tests.rs::links_to_anything_but_web_and_email_addresses_stay_plain_text", "crates/automation/tests/it/automation.rs::links_in_xfa_rich_text_are_listed_through_tools", "crates/xfa/src/testsLine truncated
notes = "A draw placed by position turns about its anchor (rotate); fields, subforms and draws in a flow don't. 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, borders and fills of flowed subforms (a page break between their children gives each page's part a closed border, the margins repeated at the cut, as measured on a synthetic form in another viewer, and a child's own page break breaks them the same way; one holding an item that doesn't fit on its page, cut across pages or placed past the content area's bottom, is drawn without its border, with a layout warning), captions, objects shown only on screen or only in print (relevant -print and +print, inside containers too: fields by their annotation flags, draws in optional content groups whose view and print usage say so; PdfCraft's own printing doesn't yet apply a layer's print usage, so a screen-only or print-only draw prints as it shows on screen, and a hidden object relevant prints isn't laid out), rounded and inverted border corners, rich text (its http, https and mailto links clickable), JPEG/PNG/GIF/BMP/TIFF images) into real pages and AcroForm fields (text, password, date with AFDate formats (a date one of the edit picture's patterns reads is kept in its first, and in the data as an ISO date; text none of them reads, ISO text included, is kept and shown as typed rather than refused, as another viewer keeps it on a synthetic form; a display picture that differs from the edit picture isn't used to show the date yet), check boxes and radio groups (each box placed by its field's margins, caption and paragraph alignment as measured on two synthetic forms in another viewer: with no caption at the right of the content area, beside a reserved caption where the alignment puts it, and a caption without a reserve taking the rest of the field; a check box's own margin larger than the box draws a bigger box there, which isn't copied), drop-downs and list boxes with shown/saved item pairs and multi-select, signature fields (each named by its SOM path, under a generated field for each step above it, so that another viewer, which lays the form out again, finds a signature made here, as seen on a synthetic form, and a box signed here keeps its signature, and that name, when a script lays the form out again; a box whose path, or a part of it, the file already gives another field keeps its own name, and its signature isn't found there yet), 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), enter and exit each tiLine truncated
evidence = ["crates/xfa/src/tests.rs::a_draw_turns_counter_clockwise_about_its_anchor", "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", "crates/xfa/src/tests.rs::faces_measure_text_at_their_standard_14_widths", "crates/xfa/src/tests.rs::rich_text_links_keep_their_address", "crates/xfa/src/tests.rs::links_in_rich_text_become_link_annotations_one_per_line", "crates/xfa/src/tests.rs::a_links_pieces_share_one_address", "crates/xfa/src/tests.rs::a_rule_between_two_pieces_of_a_link_keeps_them_apart", "crates/xfa/src/tests.rs::links_to_anything_but_web_and_email_addresses_stay_plain_text", "crates/automation/tests/it/automation.rs::links_in_xfa_rich_text_are_listed_through_tools", "crates/xfa/src/testsLine truncated
notes = "A draw placed by position turns about its anchor (rotate); fields, subforms and draws in a flow don't. 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, borders and fills of flowed subforms (a page break between their children gives each page's part a closed border, the margins repeated at the cut, as measured on a synthetic form in another viewer, and a child's own page break breaks them the same way; one holding an item that doesn't fit on its page, cut across pages or placed past the content area's bottom, is drawn without its border, with a layout warning), captions, objects shown only on screen or only in print (relevant -print and +print, inside containers too: fields by their annotation flags, draws in optional content groups whose view and print usage say so; PdfCraft's own printing doesn't yet apply a layer's print usage, so a screen-only or print-only draw prints as it shows on screen, and a hidden object relevant prints isn't laid out), rounded and inverted border corners, rich text (its http, https and mailto links clickable), JPEG/PNG/GIF/BMP/TIFF images) into real pages and AcroForm fields (text, password, date with AFDate formats (a date one of the edit picture's patterns reads is kept in its first, and in the data as an ISO date; text none of them reads, ISO text included, is kept and shown as typed rather than refused, as another viewer keeps it on a synthetic form; a display picture that differs from the edit picture isn't used to show the date yet), check boxes and radio groups (each box placed by its field's margins, caption and paragraph alignment as measured on two synthetic forms in another viewer: with no caption at the right of the content area, beside a reserved caption where the alignment puts it, and a caption without a reserve taking the rest of the field; a check box's own margin larger than the box draws a bigger box there, which isn't copied), drop-downs and list boxes with shown/saved item pairs and multi-select, signature fields (each named by its SOM path, under a generated field for each step above it, so that another viewer, which lays the form out again, finds a signature made here, as seen on a synthetic form, and a box signed here keeps its signature, and that name, when a script lays the form out again; a box whose path, or a part of it, the file already gives another field keeps its own name, and its signature isn't found there yet), 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; a field or exclusion group that must be filled in, validate nullTest=\"error\", is Required, so field highlighting outlines it as another viewer does on a real form; a push button, which keeps no value, is not), 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, accessLine truncated
[[feature]]
id = "form.xfa-formcalc"