Conflicts:
- ATTRIBUTION.toml: ours (sha256 lines only); hashes recomputed at the end of the track
- crates/engine/Cargo.toml: main's workspace chrono entry, plus the PR's pdfcraft-testkit dev-dependency
ROADMAP.md, docs/roadmap.md: all status lines and progress-log lines kept; M12 row takes both 'Done since' insertions.
parity/acrobat-features.toml: form.xfa evidence lists unioned, notes take both sides' insertions.
ROADMAP.md, docs/roadmap.md: both status lines and progress-log lines kept; M12 row takes both 'Done since' insertions.
crates/automation/tests/automation.rs, crates/xfa/src/fixtures.rs: union of #2624's and #2619's tests and fixtures.
parity/acrobat-features.toml: form.xfa evidence lists unioned, notes take both sides' insertions.
ROADMAP.md: kept main's stage line and status lines, added this PR's status and log lines.
docs/roadmap.md: kept main's status lines and M8/M9/M10 rows (main had merged the duplicate M8
row), added this PR's status line and its M10 sentence.
crates/ui-egui/src/lib.rs: persist keeps main's `v` and its separately set keys; print_paper
set the same way.
crates/ui-egui/tests/print.rs: kept both sides' tests.
ROADMAP.md: kept both sides' progress-log lines.
crates/edit/src/lib.rs: re-export list is the union (TextSession from #3942; BlockKey, block_overlapping from #2756).
parity/acrobat-features.toml: form.highlight-fields evidence and notes from both sides (the renamed remembered test, the read-only test)
docs/roadmap.md: M6 row keeps both added sentences
ROADMAP.md: union of the progress-log lines
crates/xfa/src/script.rs: template_field and named_in moved to crate::data in #3397; field_grows_with_text calls crate::data::template_field.
parity/acrobat-features.toml: form.xfa evidence lists unioned, notes take both sides' insertions.
ROADMAP.md: union of progress-log lines.
docs/roadmap.md: union of status lines and revision-history rows.
crates/ui-egui/src/comments_panel.rs: card() takes main's `info` argument and the PR's borrowed reply slice.
Conflicts:
- crates/ui-egui/src/i18n/*.tsv: union of both sides' new keys
- ATTRIBUTION.toml: ours; hashes recomputed at the end of the track
- ROADMAP.md, docs/roadmap.md: both progress-log entries, newest first
- parity/acrobat-features.toml (organize page grid): main's evidence and remembered-size note, with the PR's Pages inspector sentence
* M6: renaming a field renames it in the calculations that read it
A rename silently broke every formula that used the field: a sum dropped
it, notation read it as 0 and a script kept a stale value. set_props now
rewrites the name in each field's Calculate script, in the same edit:
AFSimple_Calculate field lists, simplified field notation (the BVCALC
comment and its translation) and getField calls naming it in one string
literal. The rest of each script is kept as written; names built at run
time, non-UTF-8 script streams and a name another field still has are
left alone, and a self-check keeps a script whose meaning would change.
Notation is read by one lexer shared by notation_js, eval_notation and
the rename. form_set_props returns formulas_updated; Field Properties
says how many calculations now use the new name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
* M6: form_set_props leaves the renamed field out of formulas_updated by object
The renamed field was matched by the name given, but set_props stores it trimmed and under
its parent's prefix. When the same call changed the field's own formula, formulas_updated
listed the field as one the rename had rewritten.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
* M6: calculation order: read, check, fix and set the order fields calculate in
A form's calculations run in its calculation order (/CO), which a field
joins when it gets a calculation, so a chain defined in the wrong order
shows stale values. calculation_order reads the order against the fields,
with what each formula reads (field lists, notation names, literal getField
names) and what is wrong: a field reading one that calculates later, a loop
(strongly connected components, found without recursion), a field /CO
leaves out, and /CO entries that calculate nothing. Fix order is a stable
topological sort that moves only what must move and leaves loops alone.
set_calculation_order writes /CO afresh; a name two fields share (a
malformed form) stands for the next of them in the current order.
Edit::SetCalculationOrder sets it in one undo step and recalculates, and
form_calculation_order reads, sets, moves one field or fixes the order
headlessly. An edited formula now keeps its field's place in /CO, a field
/CO lists twice calculates once, and recalculation runs each field by
object, so two fields sharing a name both calculate. The /CO code of the
Calculate tab, field scripts and recalculation goes through one place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
* M6: Prepare a form ▸ Calculation order: a page to reorder, check and fix the order fields calculate in
The calculated fields in the order they calculate (/CO), each with its
formula and the value it shows, moved with Up/Down arrows (as Acrobat's Set
Field Calculation Order), Alt+Up/Down or by dragging. A card says which
fields read one calculated later, which read each other in a loop and which
/CO leaves out, and holds Fix order; a loop's card opens one of its
formulas. On the page, arrows show what the field in focus reads and what
reads it, with each linked field's step. The order is a page of the Prepare
a form panel, opened from its Fields tools, with a back arrow to Prepare a
form; opening it ends Preview and closes the page grid.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
* M6: form.calculation_order runs form_calculation_order headlessly
The command had no tool linked, so command_list showed none for it and
command_run refused it. form_calculation_order is now its tool: run without
a change, it reads the order.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Retain the calculation-order update and inline-image repair entry.
Position the repair entry away from concurrent top-of-log additions;
parser and render regression code remain unchanged.
* M6: renaming a field renames it in the calculations that read it
A rename silently broke every formula that used the field: a sum dropped
it, notation read it as 0 and a script kept a stale value. set_props now
rewrites the name in each field's Calculate script, in the same edit:
AFSimple_Calculate field lists, simplified field notation (the BVCALC
comment and its translation) and getField calls naming it in one string
literal. The rest of each script is kept as written; names built at run
time, non-UTF-8 script streams and a name another field still has are
left alone, and a self-check keeps a script whose meaning would change.
Notation is read by one lexer shared by notation_js, eval_notation and
the rename. form_set_props returns formulas_updated; Field Properties
says how many calculations now use the new name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
* M6: form_set_props leaves the renamed field out of formulas_updated by object
The renamed field was matched by the name given, but set_props stores it trimmed and under
its parent's prefix. When the same call changed the field's own formula, formulas_updated
listed the field as one the rename had rewritten.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
* M6: calculation order: read, check, fix and set the order fields calculate in
A form's calculations run in its calculation order (/CO), which a field
joins when it gets a calculation, so a chain defined in the wrong order
shows stale values. calculation_order reads the order against the fields,
with what each formula reads (field lists, notation names, literal getField
names) and what is wrong: a field reading one that calculates later, a loop
(strongly connected components, found without recursion), a field /CO
leaves out, and /CO entries that calculate nothing. Fix order is a stable
topological sort that moves only what must move and leaves loops alone.
set_calculation_order writes /CO afresh; a name two fields share (a
malformed form) stands for the next of them in the current order.
Edit::SetCalculationOrder sets it in one undo step and recalculates, and
form_calculation_order reads, sets, moves one field or fixes the order
headlessly. An edited formula now keeps its field's place in /CO, a field
/CO lists twice calculates once, and recalculation runs each field by
object, so two fields sharing a name both calculate. The /CO code of the
Calculate tab, field scripts and recalculation goes through one place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Fall back only when missing sample or terminator bytes cannot fit the
content-window budget, including retained header and separator overhead.
Preserve available deterministic boundaries and ordinary large streams.
Add oversized, exact-budget, overflow and large-stream regressions.
* M6: renaming a field renames it in the calculations that read it
A rename silently broke every formula that used the field: a sum dropped
it, notation read it as 0 and a script kept a stale value. set_props now
rewrites the name in each field's Calculate script, in the same edit:
AFSimple_Calculate field lists, simplified field notation (the BVCALC
comment and its translation) and getField calls naming it in one string
literal. The rest of each script is kept as written; names built at run
time, non-UTF-8 script streams and a name another field still has are
left alone, and a self-check keeps a script whose meaning would change.
Notation is read by one lexer shared by notation_js, eval_notation and
the rename. form_set_props returns formulas_updated; Field Properties
says how many calculations now use the new name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
* M6: form_set_props leaves the renamed field out of formulas_updated by object
The renamed field was matched by the name given, but set_props stores it trimmed and under
its parent's prefix. When the same call changed the field's own formula, formulas_updated
listed the field as one the rename had rewritten.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
* fix(render): read each object stream's index once (vendored hayro-syntax patch 22)
`XRef::get_with` parsed an object stream's whole index of `objnum offset`
pairs again for every object it fetched from that stream, so reading all of a
stream of N objects took N² steps. Firefox keeps every object of a page in one
stream: the pdf.js corpus file bug1978317.pdf (65,542 objects in one stream)
took 37 s to draw and 40 s to extract its text, and was a known timeout in the
robustness baseline.
The parsed index is now kept per stream, beside the decoded stream data it
indexes (`ObjectStreamIndexes`), and never cleared, as that data isn't. A
malformed index is remembered as malformed; `/N` or `/First` that can't be
read (a reference that can't be followed while another stream loads) is not
remembered. An offset past `usize` is malformed instead of wrapping. A poisoned
lock is still used: entries are inserted whole.
Release build, Linux: bug1978317.pdf draws in 0.5 s (was 37 s) and extracts
its text in 1.9 s (was 40 s), and leaves the baseline's known timeouts;
40,000 pages in one stream open in 0.2 s (was 54 s).
`raster::tests::objects_in_one_large_object_stream_are_read_in_linear_time`
times out at 20 s with the fix reverted and passes in 0.2 s with it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MoEx9Nj8KC4WKDcf5YaokQ
* test(render): an object stream offset past usize is malformed, not a panic
Review follow-up on #2427. Mutation testing by mtlcl410 found that replacing
patch (22)'s `checked_add` in `read_object_stream_index` with a plain `+`
survived every test, although in a debug build that `+` panics on the very
input it guards against (and wraps in a release build). Their test pins it:
it fails with "attempt to add with overflow" under that mutant and passes on
this branch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(engine): PNG export writes opaque pages as RGB at deflate level 3
`encode_png` wrote every raster as RGBA at the png crate's default level
(zlib level 6). A rendered page is always opaque, so the alpha channel only
made the file bigger and slower to write. An opaque raster is now written as
RGB, at deflate level 3 with adaptive filtering; a raster with translucent
pixels is still written as RGBA with straight alpha.
The files come out the same size, 3-5x faster (release build, Linux):
300-dpi scan drawn at 220 dpi 805 -> 167 ms 4.46 -> 4.47 MB
text page at 220 dpi 70 -> 40 ms 0.85 -> 0.89 MB
annual-report page at 150 dpi 179 -> 64 ms 1.41 -> 1.38 MB
This is the encoder behind Export > Image (PNG), `pdfcraft-cli render` and
Print as Image; the automation tools (`page_render` and the other page
previews, MCP included) had a copy of it and now call it instead, so png is
only a dev-dependency of pdfcraft-automation. Two automation tests that read
`page_render` output as RGBA now accept the RGB a page renders as.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MoEx9Nj8KC4WKDcf5YaokQ
* fix(engine): PNG export rejects a raster of the wrong length
encode_png now checks that the buffer is exactly width × height × 4 bytes
(checked arithmetic) before choosing RGB or RGBA. The opaque path's
as_chunks::<4>() dropped a partial trailing pixel, so encode_png(1, 1,
&[10, 20, 30, 255, 0]) wrote a valid 1×1 PNG where the old RGBA encoder
returned an error.
Regression test export::png_tests::rasters_of_the_wrong_length_are_rejected
covers opaque and translucent buffers with a partial pixel, a pixel short,
a pixel too many, an empty buffer and a size whose byte count overflows; it
fails on the previous head at the reviewer's case.
Review: https://github.com/storytold/pdfcraft/pull/2425#discussion_r4281603405
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MoEx9Nj8KC4WKDcf5YaokQ
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>