## What this changes
Batch processing now keeps the caller's "Enable JavaScript" setting. Before, each file was processed in a fresh `Session::new()`, where JavaScript is on, so open-time XFA scripts and "Run a JavaScript" steps ran and their results were saved.
- `actions::run_on` and `ocr::recognize_file` take a `javascript: bool` and call `set_javascript` before opening the file. In `recognize_file` the open now goes through a small private helper, `ocr::open_file`, so the open path can be tested without the recognition models.
- `doc_batch`'s `Recipe` carries the setting into its per-file session and its OCR step.
- Callers pass their own `session.javascript()`: the desktop Action Wizard, desktop multi-file OCR, `action_run`, `ocr_recognize_files` and `doc_batch`.
Also relevant to #5054: an administrator's `DisableJavaScript` policy now reaches these sessions.
Fixes#5080
## How you verified it
Regression tests, adopted from the reviewers' patches, plus one for the OCR open path:
- `crates/engine/src/tests.rs::an_action_runs_a_javascript_step_only_when_javascript_is_on` (croquete1): a "Run a JavaScript" step writes its field with JavaScript on, and `run_on(.., false, ..)` refuses it instead of saving a scripted value. croquete1 reported it failing with the `set_javascript` call in `run_on` removed.
- `crates/automation/tests/it/automation.rs::file_processing_keeps_javascript_off_for_xfa_open_scripts` (Harrison-Booth): with `js_enabled` off, an XFA initialize script that sets `qty` to 42 doesn't reach the output of `action_run`, `doc_batch` or (only when its models are installed) `ocr_recognize_files`. The contributor reported that each propagation removed on its own makes it fail.
- `crates/engine/src/ocr.rs::tests::files_open_for_recognition_with_the_callers_javascript_preference`: opens the scripted XFA fixture through `recognize_file`'s open path with JavaScript off (the initialize script leaves `qty` empty) and on (it sets `qty` to 2). This covers the OCR propagation in runs without the OCR models.
What was run locally, and what wasn't:
- `cargo fmt --check` and `cargo clippy --all-targets -- -D warnings` were clean on `pdfcraft-engine`, `pdfcraft-automation` and `pdfcraft-ui-egui` at the earlier heads. On the latest commit only `cargo fmt --all` was run; clippy was not re-run.
- No tests were run locally, including the new `ocr::tests` one: test builds time out on this machine, and the run was stopped before it compiled. The new test has not been seen failing either. If `set_javascript` is removed, the session keeps its JavaScript-on default, so the off case would read `qty` = 2 like the on case, and the first assertion would fail. Seeing that happen is still owed.
- The tests are left to the maintainers' CI. No CI result is claimed for this PR.
- [x] `cargo fmt --check`
- [ ] clippy with `-D warnings` on the crates I changed (clean at the earlier heads; not re-run on the latest commit)
- [ ] tests pass on the crates I changed (not run locally)
- [x] `cargo xtask layers` and `cargo xtask assets`: N/A. No crate boundaries, dependencies or assets change.
## Never-crash
- [x] No new `unwrap`/`expect`/`panic!`/`unreachable!`/`todo!` outside tests. The new non-test code only passes a `bool` and propagates errors with `?`.
- [x] No indexing or slicing with input-derived positions, no new arithmetic, allocations or recursion.
- [x] No `unsafe`
- [x] Any crash this fixes has a regression test: N/A, this fixes no crash.
## Provenance
- [x] No asset from an Adobe product, and no Adobe icon recreated, traced or imitated (no assets change)
- [x] No font files committed here
- [x] No GPL/AGPL code copied in
- [x] Behaviour comes from the project's own JavaScript preference (`Session::set_javascript`) and issue #5080, not from another implementation's source
- [x] Every new asset has an `ATTRIBUTION.toml` entry: N/A, no new assets
## Security
- [x] `--root` confinement still holds: `action_run` and `ocr_recognize_files` still resolve every input and output through `resolve`, and `doc_batch`'s path handling is unchanged. No new paths are read or written.
- [x] MCP and the UI control channel are untouched.
- [x] No new outbound network access.
- This narrows script execution: with JavaScript off, the per-file sessions of the Action Wizard, multi-file OCR, `action_run`, `ocr_recognize_files` and `doc_batch` no longer run document or step scripts.
## Parity and reach
- [ ] `parity/acrobat-features.toml` not updated: this is a bug fix, and the `misc.batch-processing` entry's status (`partial`) and notes stay true.
- [x] Reachable headlessly: `action_run`, `ocr_recognize_files` and `doc_batch` take the setting from the session, which `js_enabled` sets.
- [x] Docs: N/A. No documented behaviour changes (`docs/cli-batch.md` doesn't describe JavaScript handling).
## Anything a reviewer should look at first
- The existing `run_on` tests pass `true` to keep their behaviour; the tests above cover the JavaScript-off path.
- `recognize_file` now has 7 parameters, the `too_many_arguments` limit.
- The OCR branch of the end-to-end test runs only where the OCR models are installed. The engine unit test covers the OCR open path without them. Provisioning the models in CI is left as a separate decision.
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
<!-- coderabbit-ai-generated:v1 -->
## Summary by CodeRabbit
* **Bug Fixes**
* File actions, batch processing, and OCR now honor the JavaScript setting from your current session for each file.
* When JavaScript is disabled, document scripts—including scripts that run when a document opens—won’t execute during processing. When enabled, JavaScript steps can run as expected.
* This behavior applies across automated and in-app workflows. Per-file results and OCR output handling remain unchanged.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
* Fix#5211: persist JavaScript field colours
* Read current document state in field scripts
* Redraw form fields after validation changes
* Format validation redraw regression test
* Keep field list alive in redraw test
* Refresh fields only after same-field scripts
* Format conditional field refresh
* Borrow accepted field value for storage
* Redraw calculated fields from current appearance
* Format JavaScript field state merge
* Restore JavaScript runner document state
* Group JavaScript runner form inputs
* Read current field display state in scripts
---------
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* Fix#5211: make calculateNow synchronous
* Bound and cache calculation execution
* fix(engine): invalidate field calculation cache
* perf(js): build calculation scripts lazily
* Refresh calculation cache after page insertion
* Load calculations for document script helpers
* Assert imported calculation result
* Initialize formatted field-change state
* Update formatted calculation test expectations
* Include the referenced field in calculation test
---------
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* feat(js): expose document page boxes
* Normalize page boxes for getPageBox
* Correct page box test expectations and formatting
* Use local page helper in box test
---------
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* Print in grayscale prints on a Windows driver with no grey option (#5806)
Windows refused Print in grayscale ("the printer's driver did not retain the requested
grayscale; nothing was sent") on drivers that list no grey option the ticket can name:
Microsoft Print to PDF, and an HP Photosmart driver whose grey is its own vendor setting.
- On Windows, a grayscale job's copy for the spooler is made grey first with Convert colours
(all pages, comments and form fields, black kept black, pictures re-encoded where they can
be), and the job says so (Job::grey_pages). The driver is still asked for grey where it has
it; one without prints the grey pages instead of refusing them. A copy that can't be read
goes as it is and asks the driver strictly, as before.
- craft-print 970644b7 (storytold/craft-libs#774) adds Job::grey_pages and asks a driver that
lists only psk:Grayscale for that, not psk:Monochrome.
- Both the Print dialog and doc_print go through spooler_copy, which now takes the job.
* craft-print 1146cdc4: craft-libs main with #774 (grey pages) and #773 (the CUPS printer list waits at most 15 s, storytold/pdfcraft#6761)
* M10: Windows prints a large image whole: drawn as tiles under Windows.Data.Pdf's cap (#5131)
Windows prints each page through Windows.Data.Pdf, which draws any image
of more than 2^22 pixels from a copy shrunk to that size. With the page
at the printer's resolution (craft-libs#396, #5023), a 6000 x 6000 scan
still printed as 2048 x 2048, its one-pixel detail gone.
pdfcraft_optimize::tile_large_images replaces each such image XObject
with a form XObject of the same object number that draws it as tiles of
at most 2000 pixels a side, so whatever drew the image draws the tiles.
Samples are copied as stored (any bit depth and colour space, decode
array, interpolation, colour key); a grey or RGB JPEG is decoded and
stored with Flate; a soft or stencil mask of the image's size is cut
into the same tiles. Tiles overlap by a pixel against drivers that
soften image edges, except under a soft mask, where the overlap would
blend see-through pixels twice. JPEG 2000, CMYK or ambiguous JPEGs,
masks of another size, more than 80 million pixels, short data and
images past the decode allowance are left whole.
pdfcraft_engine::spooler::spooler_copy applies it to the copy that goes
to the spooler on Windows, from the Print dialog's worker and doc_print;
elsewhere, or when the copy can't be read or written, the bytes go as
they are. The saved print-ready PDF is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M10: a test that the spooler gets the tiles on Windows; tiles are up to 2001 px (#5131)
spooler_copy was only tested with prints that pass through unchanged, so
one that skipped tiling on Windows would have passed. A test now checks
that on Windows it sends the tiled copy and elsewhere the bytes as they
are. The optimize README said tiles have at most 2000 pixels a side;
with the overlap they have 2001.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M10: the tiles' samples are StoredSamples, apart from the crate's Samples (#5131)
main has since added a public `Samples` (8-bit grey or RGB, for tools
that repaint an image), so two different things in one crate shared a
name. The tiles keep an image's samples as stored, in its own colour
space and depth, so each tile redraws exactly what it replaces; the
type now says so in its name and doc comment.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: austin62jake <340374338+austin62jake@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
One conflict, in redact lib.rs: the module list, where main added `elsewhere` and
`identification` (#6714) and this branch `globals`. All three stay, in order.
Main's #2448 work also added a sixth step to `apply`, after the verification and the pattern
guard: `metadata::scrub` removes the `/Info` entries and the catalog's and pages' XMP that repeat
the removed text. An image that only such a packet refers to leaves the file then, after
`unshare` had looked for what left, so its shared dictionary stayed. `unshare` now runs after
the scrub, last of what can take something out. Storing images again changes no page's content
and no pattern cell, so the verification and the pattern guard before it check the same pages
they did. The crate's doc says the dictionary goes last. Remove Hidden Information's order is
unchanged: main's changes to `sanitize.rs` only read (`owned_values`).
Test: `an_image_only_the_scrubbed_metadata_refers_to_takes_a_shared_jbig2_dictionary_too`. An
image under the mark is also named by the catalog's XMP, which repeats the text removed beside
it. After the redaction the packet is gone, the image on the other page is stored again, and
the dictionary is gone too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
One conflict, in redact sanitize.rs. Main's #6021 (930b44131) split `remove_hidden` in two:
- it now counts only what the chosen categories need (`scan_only`);
- it hands the removal to `remove_counted`, which tests also call with `every_page`.
This branch had added one line to the old body: `kept_before`, what a full save keeps before
anything is removed. Main's side is taken whole, and that line is now the first thing
`remove_counted` does after finding the catalog, before any change to the document. So every
caller of `remove_counted` has it. `unshare_unmarked` stays last, after `unlink_undrawn`, and
merged cleanly there. The only early return after a change is `NothingToApply` when nothing
was removed and nothing unlinked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
- engine: the on_changes test's XfaRun literals set highlight; the script
runner reads its template before borrowing the live form for to_js; has_event
reads the paged live form, as the rest of the runner does, so focus scripts
find a page-master copy by its own SOM path.
- js: property reads compare and copy the shared (Arc<str>) node text, and the
property tests build XHost with its current signature.
- xfa: the Emitter test literal sets groups and used; a field split that is
undone also restores the names handed out; rustfmt on a merged import.
- parity: drop the use/usehref gap from form.xfa-dynamic, as prototypes are now
resolved before the template is parsed.