Files
pdfcraft/crates/forms
6427357fcb Forms: date fields name months in the date language, not always English (#5303) (#5464)
* Forms: date fields name months in the date language, not always English (#5303)

`AFDate_FormatEx("dd-mmm-yy")` wrote "18-Oct-26" in an Italian interface,
where a localized Acrobat writes "18-ott-26". Form date formats,
`util.printd` and `util.scand` now take their month and weekday names
from the date language (Preferences > Date format > Language, which
follows the interface language until one is picked), and still read
English names typed in. The names are process-wide, so their test runs
in its own binary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Forms: dates read back in every date language (#5303)

Trying every date language showed what a field wrote not always reading
back: names holding digits ("10月"), spaces ("tháng 10"), dots ("ม.ค.")
or combining vowel signs (Thai, Hindi, Telugu) fell apart word by word,
and Italian, Spanish, Romanian and Albanian share some short weekday and
month names ("mar"). The language's names are now found whole in the
text first, the longest at each place, and read as their English names;
the format's order of `mmm` and `ddd` settles a shared name. Both
AFDate parsing and util.scand do this, on text up to 1 KB. A test reads
back every language, each name format, and hostile text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Forms: date names no field font can draw are drawn in English (#5303)

With the names in the date language, a Russian dd-mmm-yy field drawn in
Helvetica showed "18-????.-26": no /DR font held "окт." and the WinAnsi
stand-in drew each letter as ?. The same held for Japanese, Chinese,
Greek, Arabic, Thai and others, and for Latin names outside WinAnsi
(Czech "říj"). Before, the names were English and always drew. An
AFDate/AFTime field now formats its value again with English names for
a widget whose fonts can't draw the localized text; a custom Format
script is run again with util.printd writing English names for such a
widget, and its result kept only when it draws whole. A field whose
font holds the names keeps them, and scripts otherwise still print the
date language.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Forms: an English weekday before the month keeps the month (#5303)

With Italian dates, "Tue 17 Mar 2026" in a "ddd d mmm yyyy" field read
as no date: the language's names were found in the text, but not
English ones, so "Tue" left the format's weekday unfilled and "Mar",
which Italian shares between Tuesday and March, was taken as the
weekday. Both AFDate parsing and util.scand lost the month, and
Spanish, Romanian and Albanian ("mar") did the same. English names are
now found too and fill their field of the format as localized ones do.
A test reads English and mixed weekdays before the month in those
languages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Forms: an English date run is drawn even when it prints the value (#5303)

A Helvetica field storing "18-Oct-26" whose Format script prints it
again as dd-mmm-yy still showed "18-???.-26" in Russian. The English
run printed "18-Oct-26", but it was dropped for equalling the stored
value, which was how a failed run was told apart: a failing script
handed back the value unchanged. The English run now says whether it
ran (`None` when the script failed or the runner can't write English
dates), and what it made is drawn whenever it draws whole. A test
reprints the stored value under Russian, and checks that a failing
script keeps its text and is reported undrawn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Forms: a short weekday typed without its dot is the weekday (#5303)

With French dates, "mar 6 janv 2026" in a "ddd d mmm yyyy" field read
as no date through util.scand, and "mar 06/01/2026" in "ddd dd/mm/yyyy"
through AFDate too. French writes Tuesday as "mar.", which a name
search only found with its dot; since English names are found as well,
the English "Mar" (March) was the only name of that length, so the
weekday became a month. Romanian "mar" did the same. A short name
ending in a dot is now also found typed without it, as the same kind of
name, so the format's order of name fields decides between it and an
equal name of the other kind. Both copies of the name search change. A
test reads French and Romanian weekdays typed without their dot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Forms: a shared name is the month when no weekday is typed (#5303)

With Italian dates, "17 mar 2026" in a "ddd d mmm yyyy" field read as
no date, nor did "mar 17, 2026" in "dddd, mmmm d, yyyy". AFDate leaves
weekdays out of what it reads (they are shown, not typed), but the name
search still expected the format's weekday first, so "mar", which
Italian shares between Tuesday and March, filled it and left no month.
Spanish and Albanian "mar" and Albanian "sht" (Saturday, September) did
the same, and the keystroke check refused those dates. Where such a
read finds no date, the names are now read again with no weekday
expected, so the shared name is the month; a typed weekday still comes
first ("mar 17 mar 2026"). util.scand reads its fields in order and
needs the weekday, so it is unchanged. A test types those dates with the
weekday left out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Forms: Turkish names typed in capitals are read (#5303)

With Turkish dates, "1 MAYIS 2026" and "1 EKİM 2026" in a d mmmm yyyy
field read as no date through AFDate and util.scand, and "SALI 6 OCAK
2026" through util.scand. Names were compared lower-cased, which turns
I into i, not the dotless ı of "Mayıs" and "Salı", and İ into i with a
combining dot above, which "Ekim" doesn't have. Names and typed text
are now folded alike, the capital I and the dotted and dotless i all as
i and the combining dot dropped, one piece for each char so positions
in the text stay aligned: in both copies of the name search, the AFDate
month reader and util.scand's name matcher. English reads as before. A
test types Turkish names in capitals and English ones as a check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Forms: Greek names typed in capitals are read again (#5303)

Folding names char by char turned a capital Σ into σ, so "ΙΑΝΟΥΆΡΙΟΣ"
no longer matched "Ιανουάριος", whose last letter is the final ς. The
fold now makes ς a σ on both sides, in the forms and JS parsers alike.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-05 16:12:04 +01:00
..

pdfcraft-forms

Interactive forms (AcroForm, ISO 32000-2 §12.7). Layer L3; depends on pdfcraft-cos and pdfcraft-fonts.

API

let all: Vec<Field> = fields(&doc);                         // terminal fields with widgets, options, flags, /DA
let snapshot = fields_with_adopted(&doc);                     // fields and page-only adoption count in one pass
let all = snapshot.items;
let adopted = snapshot.adopted_page_fields;
set_value(&mut doc, "name", &FieldValue::Text("Ada".into()))?;
set_value(&mut doc, "agree", &FieldValue::Check(true))?;
set_value(&mut doc, "size", &FieldValue::Radio(Some("L".into())))?;
set_value(&mut doc, "toppings", &FieldValue::Choice(vec!["Ham".into()]))?;
// Many values: as set_value one by one, but reading the form once and recalculating once.
let results: Vec<Result<(), FormError>> = set_values(&mut doc, &[("name".into(), FieldValue::Text("Ada".into()))])?;
reset(&mut doc, None)?;                                      // Clear form (back to /DV)

// Prepare a form
let name = add_field(&mut doc, 0, [72.0, 700.0, 216.0, 722.0], &NewField::Text { multiline: false }, None)?; // "Text1"
set_props(&mut doc, &name, &FieldProps { name: Some("email".into()), required: Some(true), ..Default::default() })?;
// A partial appearance edit keeps each widget's other styles and custom /DA font.
set_props(&mut doc, "email", &FieldProps {
    appearance: Some(LookPatch { width: Some(3.0), fill: Some(None), ..Default::default() }),
    ..Default::default()
})?;
delete_field(&mut doc, "email")?;

// A table of text fields (view space, y down), laid out and named before any of it is added:
// refused whole for a cell under MIN_FIELD_SIDE or a name with a period, repeated or taken.
let names = all.iter().map(|f| f.name.as_str());
let base = table::free_base(names.clone());                    // "Table1", or the next one free
let mut grid = table::FieldTable::new([40.0, 240.0, 240.0, 360.0], &base);
grid.set_columns(2, &base);
let cells: Vec<table::Cell> = grid.cells_for(names)?;      // "Table1_1", "Table1_c2_1", …

Behaviour

  • Inheritable attributes (/FT, /Ff, /V, /DV, /DA, /Q, /MaxLen) are resolved down the field tree; the form's /DA and /Q are the last fallback.
  • Text and choice fields get new appearance streams (/Tx BMC … EMC): auto-size (0 Tf), comb (cells parted in the border colour), multiline wrapping, password masking, quadding, /MK background and border, list-box selection. The /DA font is used when /DR has it as a simple font or a composite font with a predefined Unicode CMap; otherwise Helvetica (WinAnsi). A value or button caption the font can't hold (Chinese in a Helvetica field, a character beyond U+FFFF in a UCS-2 CMap) is kept, as Acrobat keeps it: /V is stored as typed, the appearance draws ? for those characters, the widget is marked with the first one (pdfcraft_fonts::UNDRAWN_MARK, cleared once all of it can be drawn) and the form sets /NeedAppearances, so other viewers draw the field from its value with fonts that have them. Not in a document that declares PDF/A, which forbids that flag (declares_pdfa: the bytes pdfaid:part in XMP metadata each filter stage of which decodes within 8 MiB, the cap preflight's Standards panel reads it with; preflight reads the identifier by its namespace, so a packet that gives that namespace another prefix declares PDF/A there and not here). undrawn_fields lists the marked fields; the engine reports each edit that draws one, and flattening refuses to make that ? permanent.
  • Displaying a form that leaves its appearances to the viewer (/NeedAppearances, or a text or choice widget with no /AP /N): with_field_appearances returns a copy with those widgets drawn from their values, as typing them would draw them; the engine shows and prints that copy and saves the file as it was (#3470). with_field_appearances_cached keeps each widget's drawing while the objects it was drawn from are the same (FieldAppearanceCache), so typing in one field redraws that field, not the form. Check boxes and radio buttons with no appearance for their state are drawn from /MK (border, background, and when on the /MK /CA ZapfDingbats symbol in the /DA colour), and with /NeedAppearances so is every one that has an /MK, over the appearance the file carries; push buttons with none are drawn from /MK, /BS and their caption.
  • Check boxes and radio buttons switch /V and each widget's /AS. Widgets with no appearances get PdfCraft's own (a drawn check mark or dot; no symbol font needed).
  • Imported /MaxLen 0 is treated as unlimited (also clearing an inherited limit), while the original entry is preserved. Positive limits still apply.
  • Values are validated (options, MaxLen, single vs multi-select, NoToggleToOff, read-only) and a failed call changes nothing.
  • FieldProps::appearance applies a LookPatch independently to each widget. None keeps a property; Some(None) removes a border/fill colour. Unchanged /DA, custom font resources, widget-specific /DA overrides, indirect /MK and /BS contents, dash arrays and unknown dictionary keys are preserved. It is mutually exclusive with the complete look replacement. Use the engine's Edit::Batch to change several fields atomically in one undo step.
  • /RV (rich text) is removed when a value is set so it can't contradict /V.
  • New fields follow Acrobat: names Text1, Check Box1, Group1 (radio groups), Dropdown1, List Box1, Button1, Date1, Signature1; /Helv is added to /DR; appearances are drawn at once. Radio buttons joining a group become kid widgets with their own export state. Renaming changes only the last part of a dotted name.
  • Renaming a field renames it in every Calculate script that reads it (af::rename_in_calculation): AFSimple_Calculate field lists, simplified field notation (the BVCALC comment and its translation), and getField calls whose argument is the name as one string literal. The rest of each script is kept as written. Names built at run time, a script stream that isn't UTF-8, other events' scripts and a name another field still has are left alone. calculations_changed compares two readings of a form to say which formulas an edit rewrote.
  • The calculation order (/CO, §12.7.3): calculation_order lists the calculated fields in the order recalculate runs them, each with its field object (two can share a name in a malformed form), with the fields each formula reads (Calculate::reads, af::calculation_reads; names built at run time aren't known) and what is wrong with the order: a field reading one that calculates later, a loop, a field /CO leaves out, and /CO entries that calculate nothing. Two issues are about the formula itself: it reads a field the form doesn't have (MissingField; the field of that name and a field under it count as there, as recalculation and getField find them, but not an Acrobat widget name such as price.0, which neither resolves), or its script builds names at run time (BuiltNames, af::reads_built_names), so its reads are incomplete. CalculationOrder::fixed is Fix order: a stable topological sort that keeps the current order wherever it can and leaves loops as they are. set_calculation_order writes /CO afresh. A field that gets a calculation joins the end of the order; one whose calculation changes keeps its place.
  • Widget::caption reads a push button's normal label (/MK /CA, including indirect values). FieldProps { caption: Some((widget_index, text)), ..Default::default() } changes or clears that label, independently of the field name. The index is 0-based. A label-only edit redraws just that widget, copying shared/indirect /MK and appearance dictionaries before editing. Other widgets, actions, icons, rotation and unknown entries are preserved. Existing icon-only layouts keep hiding the caption; caption rendering retains the existing Helvetica/WinAnsi limits and reports unavailable characters as above.

Not yet

Rich-text values and exact appearance metrics/layout remain incomplete. JavaScript execution and FDF/XFDF exchange are integrated through the engine; this crate provides field actions and native AF rules. /MK /R rotation (0, 90, 180, 270) is drawn into the appearance.

Button export values

FieldProps::export_value edits a zero-based widget's /Opt value while preserving appearance names, /AP, /AS, /V and /DV. Widgets sharing an on-state remain consistent. Export-only changes do not regenerate appearances. Invalid indices, empty/Off values and ambiguous/conflicting mappings are rejected. Field::export_values resolves selected states for data export.