M6: treat imported MaxLen zero as unlimited

This commit is contained in:
djmushin
2026-10-18 11:20:51 -04:00
parent ec0a965074
commit 39d0b953ca
5 changed files with 141 additions and 3 deletions
+2
View File
@@ -44,6 +44,8 @@ delete_field(&mut doc, "email")?;
flattening refuses to make that `?` permanent.
- 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
+4 -1
View File
@@ -754,7 +754,10 @@ fn walk(
inh.q = Some(q);
}
if let Some(m) = d.get(b"MaxLen").and_then(|o| doc.resolve(o).as_int()) {
inh.max_len = usize::try_from(m).ok();
// Compatibility: some writers emit zero for an unlimited text field. Keep /MaxLen
// untouched in the object graph, but expose only positive limits to both UI and API.
// An explicit zero also clears an inherited limit rather than inheriting it again.
inh.max_len = usize::try_from(m).ok().filter(|limit| *limit > 0);
}
let kids: Vec<ObjRef> =
d.get(b"Kids").map(|k| doc.resolve(k)).and_then(|k| k.as_array().cloned()).unwrap_or_default().iter().filter_map(|k| k.as_ref()).collect();
+16
View File
@@ -2239,6 +2239,22 @@ fn page_shapes_closes_only_last_stroked_subpath_but_all_filled_subpaths() {
assert!(filled.rules.is_empty());
}
#[test]
fn zero_max_len_is_unlimited_and_overrides_inherited_limit() {
let mut doc = fixture();
let city = fields(&doc).into_iter().find(|f| f.name == "address.city").unwrap();
let parent = doc.get(city.obj).as_dict().unwrap().get(b"Parent").unwrap().as_ref().unwrap();
doc.update_dict(parent, |d| d.set(b"MaxLen".to_vec(), Object::Int(3))).unwrap();
assert_eq!(fields(&doc).into_iter().find(|f| f.name == "address.city").unwrap().max_len, Some(3));
doc.update_dict(city.obj, |d| d.set(b"MaxLen".to_vec(), Object::Int(0))).unwrap();
assert_eq!(fields(&doc).into_iter().find(|f| f.name == "address.city").unwrap().max_len, None);
set_value(&mut doc, "address.city", &FieldValue::Text("Absalom".into())).unwrap();
let again = reopen(&doc);
assert_eq!(field(&fields(&again), "address.city").value, ["Absalom"]);
assert_eq!(again.get(city.obj).as_dict().unwrap().int(b"MaxLen"), Some(0), "preserve the original entry");
assert!(set_value(&mut doc, "zip", &FieldValue::Text("123456".into())).is_err(), "positive limits still apply");
}
#[test]
fn auto_text_size_fits_single_and_multiline_text() {
use crate::appearance::auto_text_size;
+117
View File
@@ -0,0 +1,117 @@
# AcroForm `/MaxLen 0` compatibility
Status: implemented; proposed as a separate compatibility PR. Last updated: 2026-10-19.
- `crates/forms/src/lib.rs::walk` reads inherited `/MaxLen`; the UI and value validation both
consume `Field::max_len`.
The parser exposed `/MaxLen 0` as `Some(0)`, so both editor and API blocked input. It now
exposes only positive limits; zero clears an inherited limit. The original object entry is
preserved through save/reopen. This is an interoperability interpretation for zero, **not
a claim that ISO explicitly defines zero as unlimited**. ISO 32000-1 Table 229 defines an
optional inheritable maximum length without defining a special zero meaning:
[public specification](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf).
Positive limits remain enforced; no field names or character-sheet rules enter product code.
Regression: `crates/forms/src/tests.rs::zero_max_len_is_unlimited_and_overrides_inherited_limit`.
The forms README and `form.fill-text` parity entry document this behavior.
Historical validation recorded before this review:
- Forms unit suite: all 68 tests passed. The new zero-limit regression was first run against
the old parser and failed (`Some(0)` instead of `None`), then passed with the compatibility
interpretation. Its positive-limit control and preserved raw entry also passed.
The user reports successful Class text input on the integrated release using their Pathfinder
character sheet. This is a manual functional result, without timings; the private document
was not used for automated regression tests in this review.
## Independent branch validation
Validated on upstream `a12fe8f4116984ebbc870eb514346ea922233316` with an isolated
Cargo target directory and no performance changes:
- The focused regression failed with the original parser (`Some(0)` rather than `None`),
then passed after restoring the compatibility fix.
- `cargo test -p pdfcraft-forms -j 2`: 81 unit tests passed; doc tests passed.
- `cargo clippy -p pdfcraft-forms -j 2 --all-targets -- -D warnings`: passed.
- `cargo check --target wasm32-unknown-unknown -p pdfcraft-forms -j 2`: passed.
- `cargo fmt --check`, `git diff --check`, and `cargo xtask layers`, `assets`, `parity`: passed.
These are local checks, not hosted CI. No private PDF was used.
## Rebase validation
Rebased onto freshly fetched upstream main
`932bf9c6bfdd63e2a21bb831696a3447b9def9cf` on 2026-10-18. The only conflict was
in the M6 roadmap row; upstream CSV/XML updates were retained alongside the compatibility
note. The implementation and original regression are unchanged from the independent branch.
Validation used a fresh Cargo target directory, two build jobs, and locked, offline builds:
- The original regression failed with the upstream parser (`Some(0)` instead of `None`),
then passed with the compatibility fix restored.
- Forms: 83 unit tests passed. Engine: 282 unit and 9 integration tests passed.
JavaScript: 47 unit tests passed. Doc tests passed. The timing probe and two private-file
tests remained ignored.
- The automation `javascript_through_tools` regression passed.
- Scoped Clippy for forms, engine and JavaScript, with `--all-targets -- -D warnings`, passed.
- WASM checks for those three crates passed.
- Rust formatting, whitespace, layering, assets and parity checks passed.
- A freshly built CLI filled synthetic fields with direct and inherited `/MaxLen 0`, rendered
their values, saved/reopened them and accepted longer text after reopening. Its positive-limit
control rejected six characters for `/MaxLen 5`; the synthetic input file stayed unchanged.
No performance optimizations, JavaScript snapshot/diff changes or profiling instrumentation
are included. Existing executables, backups and private PDFs were not modified.
## PR #3323 refresh
Revalidated against freshly fetched upstream main
`4db6ac4ae2448df6389d8bb0bc54db6ba175721c` on 2026-10-18. Conflicts in both
roadmaps were resolved by keeping upstream's button-label and content-editing notes alongside
the `/MaxLen 0` documentation. The original compatibility implementation and regression are
preserved; no performance changes were added to the PR.
Forms: 85 unit tests passed. Engine: 283 unit and 15 integration tests passed. JavaScript:
48 unit tests passed. The automation JavaScript regression, scoped Clippy with warnings denied,
WASM, formatting, whitespace, layering, assets and parity checks all passed. The synthetic CLI
fill/render/save/reopen and positive-limit control passed. The original regression again failed
with the old parser and passed with the fix restored. Private-file tests remained ignored.
## 2026-10-19 revalidation
Rebased onto freshly fetched upstream main
`459379ef9b127648cc30c0b2fe2b20c2b2375f04`. Only `ROADMAP.md` required manual
conflict resolution; all upstream entries and the compatibility note were retained.
The implementation, original regression and forms README remain byte-for-byte identical
to the previously reviewed compatibility head. No performance changes were introduced.
Validation used a fresh isolated build directory, two jobs, and locked offline builds:
- The original regression failed with the old parser and passed with the fix restored.
- Forms: 93 unit tests passed. Engine: 283 unit and 15 integration tests passed.
JavaScript: 48 tests passed. Doc tests passed; the timing probe and two private-file
tests remained ignored.
- Automation JavaScript, scoped all-targets Clippy with warnings denied, WASM, formatting,
whitespace, layering, assets and parity passed.
- Synthetic CLI fill/render/save/reopen checks passed for direct zero and zero overriding
an inherited positive limit. Longer text after reopening was accepted. The `/MaxLen 5`
control rejected six characters, and the synthetic input stayed unchanged.
These are local checks, not hosted CI. Existing executables, backups and private PDFs
were not modified; only the compatibility worktree was changed.
## Revision history
- 2026-10-19: refresh PR #3323 onto upstream `459379ef`, retain all upstream roadmap entries
and independently repeat regression, native/WASM, repository and CLI validation.
- 2026-10-18: refresh PR #3323 onto upstream `4db6ac4a`, retaining both roadmap updates and
independently revalidating compatibility and repository checks.
- 2026-10-18: rebase onto fetched upstream `932bf9c6`; preserve upstream roadmap updates and
independently revalidate the regression, forms/engine/JavaScript suites and quality gates.
- 2026-10-18: isolate the compatibility change on current upstream main; verify the
regression before and after the fix, the forms suite and scoped quality gates.
+2 -2
View File
@@ -4416,8 +4416,8 @@ milestone = "M6"
status = "shipped"
commands = ["form.fields"]
tools = ["form_fill", "form_fields"]
evidence = ["crates/ui-egui/tests/forms.rs::typing_into_a_text_field_and_tabbing_on", "crates/ui-egui/tests/forms.rs::auto_size_fields_shrink_and_wrap_typed_text_to_fit_their_box", "crates/forms/src/tests.rs::text_fields_get_new_appearances", "crates/engine/src/tests.rs::filling_a_form_shows_saves_and_undoes", "crates/automation/tests/automation.rs::orphan_form_fields_through_tools", "crates/forms/src/tests.rs::fields_sharing_a_name_take_the_same_value"]
notes = "in-place editor; Enter or clicking away commits, Escape cancels; auto-size (0 Tf) fields shrink, and multiline ones wrap, the text being typed to fit the box, on screen as on the page (the screen measures in the interface font, so its size can be a little smaller than the page's Helvetica fit); fixed sizes are kept and clip, as in Acrobat; no format/keystroke JavaScript yet (M6.4)"
evidence = ["crates/ui-egui/tests/forms.rs::typing_into_a_text_field_and_tabbing_on", "crates/ui-egui/tests/forms.rs::auto_size_fields_shrink_and_wrap_typed_text_to_fit_their_box", "crates/forms/src/tests.rs::text_fields_get_new_appearances", "crates/engine/src/tests.rs::filling_a_form_shows_saves_and_undoes", "crates/automation/tests/automation.rs::orphan_form_fields_through_tools", "crates/forms/src/tests.rs::fields_sharing_a_name_take_the_same_value", "crates/forms/src/tests.rs::zero_max_len_is_unlimited_and_overrides_inherited_limit"]
notes = "in-place editor; Enter or clicking away commits, Escape cancels; auto-size (0 Tf) fields shrink, and multiline ones wrap, the text being typed to fit the box, on screen as on the page (the screen measures in the interface font, so its size can be a little smaller than the page's Helvetica fit); fixed sizes are kept and clip, as in Acrobat; no format/keystroke JavaScript yet (M6.4); imported MaxLen 0 permits text input and overrides inherited limits; positive limits remain enforced."
[[feature]]
id = "form.fill-checkbox-radio"