Phase 3 of REVIEWS/DART_PDF_VS_MAKEPAD_PDF_GAP_ANALYSIS.md. The previous
form.rs was keyed by name strings, had no inheritance and no way to edit
anything, so the review item "delete PdfFormFilling and replace it with a
DocumentFormEditor that returns real errors" had no implementation.
Step 3.1 object identity:
- Fields are keyed by ObjRef, not by name. Widget-to-field identity is
preserved across parse, and a field defined as a direct object is skipped
rather than given an invented identity.
- PdfDocument::page_index_of() maps a page reference back to its index and
returns None for a stranger.
Step 3.2 annotations:
- PdfDocument::page_annotations() reads /Annots and records the real page on
every annotation; the hardcoded page_index: None is gone.
- Annotations expose a typed action() (OpenUri / GoToNamed / GoToPage). The
document reports intent only and never opens anything itself (rule 5).
- contains_point() normalises the rectangle: a PDF /Rect is any two opposite
corners, so an inverted one previously never hit-tested.
Step 3.3 form model:
- /Parent chain inheritance for FT, Ff, V, DV, DA, MaxLen and Opt, with the
child key overriding the ancestor.
- /Ff resolved into concrete types: checkbox vs radio vs pushbutton, combo
vs list box.
- DocumentFormEditor takes typed edits and returns FormError. Read-only,
type-mismatched, over-MaxLen, non-option and unknown-state edits are all
refused, and a refused edit leaves the field untouched and not dirty.
- MaxLen counts characters, not bytes.
- Checking a box uses the on state the widget declares, not an assumed /Yes.
- The field tree walk is depth-bounded so a cyclic /Kids cannot recurse
until the stack dies.
Step 3.4 appearance generation (new appearance.rs):
- Document-level, not widget code. Generates text, multiline, choice and
checkbox appearances as Form XObjects with correct /BBox and /Length.
- /DA parsing resolves font, size and colour, converting gray and CMYK.
- Auto-size (0 Tf) resolves to a size that fits the widget.
- Values are escaped, so a parenthesis in a value cannot terminate the
string and corrupt the stream; a single-line value cannot break out via
newlines; content is clipped to the widget box.
- Unsupported kinds (pushbutton, signature) and degenerate rectangles return
AppearanceError instead of a blank stream that would erase the field.
Wiring: PdfDocument::acroform() parses the form against the real page tree,
which is what makes widget page resolution meaningful.
Tests: adds tests/acroform.pdf, a hand-written two-page AcroForm fixture
with an inherited field, a checkbox with /On and /Off states and a URI link,
all on page index 1 so any code defaulting to page 0 fails. 12 integration
tests assert the Phase 3 exit criterion against that real parsed file.
Validation: TEST_TARGET=pdf ./tools/test-rust-clean.sh
118 tests pass; rustfmt and clippy -D warnings clean.