* M6: forms decodes values, renamed calculations and detected pages within shared budgets
Part of #5755: the forms crate's four unbudgeted stream reads go through a
`DecodeBudget`, so an operation's decoding is bounded however often the file
names the same stream.
- A reading of the form decodes its fields' value streams (/V, /DV) within one
64 MiB budget, as it already does their scripts. Each field got a fresh
1 GiB allowance, so 300 fields naming one stream decoded it 300 times.
- Renaming a field rewrites the calculations that read it within one script
budget; past it, a calculation is left as it is, like one that won't decode.
- Field detection reads a page's content within one page's allowance
(`DecodeBudget::page`), as the content walks do, however often /Contents
lists a stream.
- The PDF/A declaration is read from at most 8 MiB of XMP, as preflight's
Standards panel reads it, and only once /NeedAppearances isn't already set.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
* M6: the forms README says which metadata declares PDF/A
declares_pdfa reads pdfaid:part from XMP metadata that decodes within
8 MiB, as preflight's Standards panel reads it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
* M6: a test pins that metadata without pdfaid:part declares no PDF/A
cargo-mutants found `==` turned to `!=` in declares_pdfa's search unpinned:
no case had metadata within the cap that declares nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
* M6: field detection reads operators one at a time; the PDF/A check reads XMP as preflight does
From the Resources review of 66415d737:
- page_shapes collected every parsed operator, about 80 bytes for each
decoded byte of dense content, so a page within its 256 MiB allowance
could take some 20 GB. It now reads them one at a time
(pdfcraft_content::ops) and drops the pieces once joined.
- declares_pdfa decoded through a DecodeBudget, which counts every filter
stage against the 8 MiB cap, so XMP stored through a chain of filters
stopped declaring PDF/A while preflight's Standards panel, which caps
each stage, still read it. It reads the metadata as preflight does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
* M6: say what declares PDF/A to forms, and why its metadata read stays direct
From the Legal-forms review of c00e3d2fb: the README and doc comments said
forms reads the declaration "as preflight's Standards panel reads it". The
two share the 8 MiB cap, applied to each filter stage, but forms matches the
bytes pdfaid:part and preflight the identifier by its namespace, as on main.
The sweep's direct-/DecodeParms hit is answered at the site: preflight and
organize read metadata the same way, and the readers move together.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
* forms: rename a shared calculation once; keep unread defaults on Clear form; cap q in detection
- A Calculate action that several fields share by reference is read and
rewritten once, in place, so every calculation reads the new name and
the script is decoded once, not skipped past the budget.
- Clear form leaves a field alone when its /DV is a stream the value
budget didn't reach, instead of emptying it.
- Field detection keeps at most 256 saved graphics states; deeper q are
counted so each Q still restores its own.
- The dense-content test checks that the budget is spent; exhausted() was
true from the start with no form visits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* forms: restore the matrix past the save cap; reset a value that wasn't read
- Field detection keeps the matrix of the first save past MAX_SAVES and
restores it when the last of them closes. A shape painted after a Q
that would have undone a cm inside those saves is dropped, as the
matrix there isn't known, instead of being reported scaled.
- Clear form skips a field only when its value is known: one whose /V
is a stream the value budget didn't reach is reset, not kept as if it
equalled an empty default.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* forms: keep detection's saved states in a Saves type
shapes_within went past clippy's cognitive complexity limit (32/30) with
the save-cap tracking inline. The same logic now lives in Saves (save,
restore, moved, unknown).
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>
* chore: resolve SonarCloud maintainability issues
* fix(packaging): use verify_sha256 helper function
* style: cargo fmt
* fix: restore wildcard imports and fix method-reference type errors
Revert the glob-import expansions (they missed SomIndices and left unused
imports), revert char::is_ascii_digit where the iterator yields char by
value, and pass the content slice to prune_doomed_xobjects as &[Vec<u8>].
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep XObject decodability check without binding content in interp
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Dylan Taylor <dylan@craftmaker.dev>
* M6: detect a row of comb boxes as one comb text field (fixes#1887)
* M6: keep empty tables of wide cells out of comb detection (#1887)
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Field detection read a path as a rectangle only when it had exactly four
corners, and gave up on a subpath once it reached five points. The French
income tax form 2042 draws its letter cells with a midpoint on each
vertical side (six points), so most of its cells were not found.
A subpath now keeps up to 16 points, and a point lying on a straight side
between its neighbours is dropped before the four-corner test. A point
that turns back along a side is not between its neighbours and stays, so
a path that doubles back is still not a box.
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Field detection read only vector data: text-layer words and drawn
rectangles and lines. A scanned form is a single picture, so it found
nothing there.
A page that an image covers at least half of, and whose content draws
no lines, is now rendered at 144 dpi and its shapes are read from the
pixels (pdfcraft_forms::detect::raster): ink against a local mean,
long thin runs as lines, enclosed rectangular patches of paper as
boxes, the empty band of a box with a small printed label as its
writing area, and the open-ended horizontal lines as rules. Labels come
from the text layer, else from OCR when the models are installed;
without either the fields get default names. Detection works in the
displayed frame, so a scan stored sideways reads like any other, and
fields on a turned page get /MK /R so typing reads upright.
generated with Claude Code
The app, crates, binaries, bundle id, packaging, icons, screenshots and docs are now
PdfCraft / pdfcraft (repo storytold/pdfcraft). On first launch the desktop app moves the
PrintCraft settings and crash-recovery folders to the new name, and both apps read settings
saved under the old storage key, so upgrades keep recent files, preferences and unsaved work.
The signing test fixtures keep their PrintCraft subject names (they are binary).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>