diff --git a/crates/xfa/README.md b/crates/xfa/README.md index db7f75f60f..ecf0aa47b3 100644 --- a/crates/xfa/README.md +++ b/crates/xfa/README.md @@ -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 (``), 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. diff --git a/crates/xfa/src/layout/draw.rs b/crates/xfa/src/layout/draw.rs index cdf5c81446..b61ee07ff8 100644 --- a/crates/xfa/src/layout/draw.rs +++ b/crates/xfa/src/layout/draw.rs @@ -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; diff --git a/crates/xfa/src/layout/field.rs b/crates/xfa/src/layout/field.rs index 27ef7a8946..403cd85f8f 100644 --- a/crates/xfa/src/layout/field.rs +++ b/crates/xfa/src/layout/field.rs @@ -267,6 +267,7 @@ impl Layouter<'_> { no_scroll: f.no_scroll, // `push` sets where it shows. views: Views::ALL, + required: f.required, }))) } } diff --git a/crates/xfa/src/layout/items.rs b/crates/xfa/src/layout/items.rs index faea3aa014..1a8c6999d0 100644 --- a/crates/xfa/src/layout/items.rs +++ b/crates/xfa/src/layout/items.rs @@ -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)] diff --git a/crates/xfa/src/model.rs b/crates/xfa/src/model.rs index 8b09f2c1a9..a26162eb94 100644 --- a/crates/xfa/src/model.rs +++ b/crates/xfa/src/model.rs @@ -517,6 +517,8 @@ pub struct ExclGroup { pub fields: Vec, pub tooltip: Option, pub scripts: Vec