Commit Graph
1205 Commits
Author SHA1 Message Date
FredAppsandDWAA1660 059dbd6e00 feat(forms): open, Will Save and Will Print actions, widget pointer/focus scripts, setItems/setAction/toSource (port of #344) (#6969)
* feat(js, forms): field setItems, setAction and a bounded toSource (#269)

Ported from #344 by @DWAA1660 onto the per-document script session.

- `field.setItems(items)` replaces a combo or list box's items (text, or
  [text, export] pairs). The value stays when an item still has it, and
  otherwise becomes the first item's. Written as the field's /Opt (pairs
  only where the export value differs), /I dropped, field redrawn.
- `field.setAction(trigger, script)` installs a JavaScript action for one
  trigger (MouseUp/Down/Enter/Exit, OnFocus/OnBlur, Keystroke, Validate,
  Calculate, Format), replacing only that trigger's action.
- `Object.prototype.toSource()` writes plain data (strings, numbers,
  booleans, null, undefined, arrays, objects) that eval reads back. It
  refuses functions, cycles, more than 64 levels, 10,000 values or 1 MiB
  with an error the script can catch.

Co-authored-by: DWAA1660 <dwatnip123@gmail.com>

* feat(engine): run a form's open, Will Save and Will Print actions and its widgets' pointer and focus scripts (#269)

Ported from #344 by @DWAA1660 onto the per-document script session: each
runs in the document's retained realm, so state the document scripts and
earlier events left is kept.

- The catalog /OpenAction's JavaScript (and its /Next chain, bounded and
  cycle-safe; other action types skipped) runs once on opening, after the
  document scripts, for background opens and reloads too, and again on
  Revert in a fresh realm. Event Doc/Open.
- `Session::run_document_actions` runs /AA /WS (Will Save) or /AA /WP
  (Will Print). A failing action is reported and never stops the save or
  print. doc_save and doc_print run them first and report what they said
  (`will_save`, `will_print`).
- Scripts that run unasked (open, Will Save, Will Print, pointer over,
  focus) may only ask for highlight, zoom, focus or a page; a click's
  Mouse Down / Mouse Up keep their requests.
- `Session::field_trigger` runs a widget's Mouse Enter, Mouse Exit, Mouse
  Down, Mouse Up, Focus or Blur JavaScript (one undoable edit), reading only
  that widget's dictionary. `field_focus` now runs an AcroForm widget's On
  Focus / On Blur as well as XFA enter/exit.
- New tool `js_event`; form_focus's description says AcroForm focus
  scripts run too.

Co-authored-by: DWAA1660 <dwatnip123@gmail.com>

* feat(ui): Will Save before saving, Will Print before printing, and widget pointer scripts in the app (#269, #247)

Ported from #344 by @DWAA1660.

- Save (both paths) runs the document's Will Save actions first, and Print
  its Will Print actions; what they change is saved or printed, and what
  they say is shown.
- The pointer moving onto or off a form widget queues its Mouse Enter /
  Mouse Exit script, a press its Mouse Down. A click on a check box or radio
  button queues its Mouse Up after the toggle, so the script reads the value
  the click stored (#247). The queue runs after the frame's edit.

Co-authored-by: DWAA1660 <dwatnip123@gmail.com>

* docs(parity): document actions and widget triggers are partly supported (#269)

Co-authored-by: DWAA1660 <dwatnip123@gmail.com>

* test: check toSource's text (scripts can't eval it) and the trigger event names (#269)

Co-authored-by: DWAA1660 <dwatnip123@gmail.com>

* test: a check box's Mouse Up reads its new value; keep the fixtures on screen and the realm across an in-place save (#247, #269)

Co-authored-by: DWAA1660 <dwatnip123@gmail.com>

* fix(engine, forms, js, ui): review fixes for document actions and field triggers

- widget trigger actions follow /Next, each action its own edit; errors don't stop the chain
- setAction installs on the field the object stands for when names repeat
- toSource refuses non-plain objects and a __proto__ key
- doc_print checks the destination before Will Print; js_event returns only its own output
- combo/list clicks run Mouse Up; a refused click's Mouse Up doesn't run
- label crops are re-read when Will Print changed the document

Co-authored-by: DWAA1660 <dwatnip123@gmail.com>

* forms: run a pointer trigger on the field it hit and refresh the edited field's draft

A trigger now carries the field's object, so of fields sharing a name the one under the
pointer runs its own script (Session::field_trigger_target); js_event passes the field it
resolved. A trigger script changing the field being edited shows in its editor unless the
user already typed there, as focus scripts do.

Co-authored-by: DWAA1660 <dwatnip123@gmail.com>

* forms: carry the field's identity through focus scripts and js_event

Focus transitions now track the field object, so moving between fields sharing a name is a
move and the field that gained or lost focus runs its own script
(Session::field_focus_target). js_event takes an optional field_id from form_fields to pick
one of fields sharing a name.

Co-authored-by: DWAA1660 <dwatnip123@gmail.com>

---------

Co-authored-by: DWAA1660 <dwatnip123@gmail.com>
2026-11-11 13:09:37 +01:00
Max Abramowitz 3a117c3c0e M10: open Word, Excel and PowerPoint files through LibreOffice
## What this changes

**File ▸ Open opens Word, Excel, PowerPoint, OpenDocument and RTF files** as new, unsaved PDFs (`Report.docx` → `Report.pdf`), the "LibreOffice sidecar" that `docs/gaps.md` (gap 8) and `docs/file-format-parity.md` planned for `create.from-office`.

- **Conversion** (`crates/create/src/office.rs`): an installed LibreOffice runs headless as a separate process, in a private temporary folder (the input is always `document.<ext>`, so the user's file name never reaches its command line). It gets a fresh profile with `DisableMacrosExecution`, `MacroSecurityLevel` 3 and `BlockUntrustedRefererLinks` set, and Writer's and Calc's `Content/Update/Link` set to *Never* (2 for Writer and 1 for Calc: the two number their values differently), no console window on Windows (`CREATE_NO_WINDOW`), and is killed after 120 s. The output is size-capped (512 MB), must start `%PDF-`, and must open as a PDF before it is used. LibreOffice is found through `PDFCRAFT_SOFFICE`, the `PATH`, then the usual install folders per OS. Without it, the user is told it's needed rather than "not a readable PDF".
- **Off the frame thread:** File ▸ Open converts on the existing open worker (`ResultKind::Converted`); the frame thread only opens the result.
- **Detection:** `SourceKind::Office` requires an Office extension *and* an OLE, ZIP or RTF signature, so `photos.zip`, or a PDF saved with a `.docx` name, are not misread.
- **Open dialog:** PDF (still the default), Images, Office documents, Text, All files. All files is left out on macOS, where rfd merges every filter into one allowed-types list, and on the web; Office documents are left out on the web. The extension lists are now shared constants in `pdfcraft-create` (`IMAGE_EXTENSIONS`, `TEXT_EXTENSIONS`, `OFFICE_EXTENSIONS`), used by both detection and the picker. The duplicate filter list in `open_dialog` is gone. The two new labels are in all 22 catalogues, with their `ATTRIBUTION.toml` hashes updated.
- **Automation:** `doc_create_multiple`, `doc_combine` and batch `convert` go through `convert_to_pdf`, so they take Office documents too; their descriptions say so.

**Deliberately not in this PR:** Insert pages, Combine files and Create ▸ Multiple files convert on the frame thread, so they now refuse Office documents with a pointer to File ▸ Open (`convert_to_pdf_inline` / `as_pdf_inline`) rather than freezing the window for seconds. Moving them onto a worker is the follow-up. `create.from-office` goes planned → **partial**, with that and the rest listed in its notes.

## How you verified it

On Windows 11 Pro with LibreOffice 26.8.1 installed, per crate:

- [x] `cargo fmt --check`
- [x] `cargo clippy -p pdfcraft-create -p pdfcraft-engine -p pdfcraft-ui-egui -p pdfcraft-automation -j 2 --all-targets -- -D warnings`
- [x] `cargo test -p pdfcraft-create -p pdfcraft-engine --lib` (42 and 276 pass); `-p pdfcraft-automation --test automation` (the combine/create tests); `-p pdfcraft-ui-egui --lib open_worker` and `--test editing --test create`
- [x] `cargo xtask layers`, `cargo xtask assets`, `cargo xtask parity --partial`
- [ ] wasm check: the `wasm32-unknown-unknown` target isn't installed here, so I'm leaving it to CI. The web paths are `cfg`'d and were reviewed by eye.

**Tests seen failing before the fix**, by reverting each part:
- Detection off → `office_documents_are_their_own_kind`, `source_kind_tells_pdfs_images_and_text_apart`, and both engine Office tests fail.
- Worker conversion off → `file_open_converts_an_office_document_on_the_worker` fails with the old user-facing message, *"the file is not a readable PDF: no readable page tree or cross-reference data was found"*.

New tests also cover a converter that hangs (it's stopped; the test binary stands in for LibreOffice), one that writes no PDF, a missing converter, temp-folder cleanup, `file://` URL escaping (spaces, drive letters, UTF-8), and the search order.

**Real conversions, end to end:** `crates/automation/tests/it/office.rs` builds a minimal DOCX, an XLSX (with a formula, `=6*7`), a PPTX and an RTF inside the test, so no binary fixtures are committed. It runs them through `doc_create_multiple` in `separate` mode (each output reopened and its text checked, including `42`) and in `combine` mode (page order and bookmarks), and through `doc_combine` beside a PDF, with a page range on one file. These tests, `libreoffice_converts_a_document_when_installed` and the process-tree cleanup test (5 out of 5 reruns) pass with LibreOffice 26.8.1. The Office tests skip where LibreOffice isn't installed, which includes CI.

Three existing tests used a 4-byte `report.docx` as their "file that can't be converted" example. That's no longer true, so they now use `archive.zip`, which keeps what they test.

## Never-crash

- [x] No new `unwrap`/`expect`/`panic!`/`unreachable!`/`todo!` outside tests
- [x] No input-derived indexing or slicing (`rsplit_once` for the extension); the PDF read back is size-capped
- [x] No `unsafe`. The Windows flag uses `std::os::windows::process::CommandExt`
- [x] A crashing, hanging or misbehaving LibreOffice is an error, never a panic or a stuck app: the worker's `catch_unwind`, a 120 s kill, and output checks

## Provenance

- [x] No Adobe assets; no fonts; no copied code
- [x] LibreOffice (MPL-2.0) is only *run* as an external process, as the docs planned; nothing of it is linked or copied. Its command-line options and the profile keys come from LibreOffice's public documentation.
- [x] The translations are contributor-original, written from the plain meaning of "Office documents" and "All files" ("Office" kept as a product name); hashes updated, `cargo xtask assets` passes

## Security

- [x] `--root` confinement unchanged: automation still resolves and reads paths before converting; only the private temp folder is written outside it
- [x] MCP and the control channel untouched
- [ ] **No new outbound network: please decide.** PdfCraft itself opens no connection. The profile blocks untrusted referer links and sets Writer and Calc never to update linked content on load. I converted eight synthetic files that link to a listener on 127.0.0.1: RTF `INCLUDETEXT` and `INCLUDEPICTURE`, a DOCX external picture, an ODT linked section and linked picture, an ODS table source and an external cell reference, and an ODP linked picture. None made a connection, with or without the link settings. Two things remain open: Impress has no link-update setting, and the process's network access isn't sandboxed (documented in `crates/create/README.md` and `docs/gaps.md`).

## Parity and reach

- [x] `create.from-office` planned → partial, with the evidence tests and what's still missing
- [x] Reachable headlessly (`doc_create_multiple`, `doc_combine`, batch `convert`)
- [x] Docs: `docs/file-format-parity.md` (Word/Excel/PowerPoint and RTF rows), `docs/gaps.md` gap 8, `docs/target-app-parity.md` row J (numbers not re-measured), `docs/roadmap.md` M10, the `ROADMAP.md` log, and the `crates/create` README

## Anything a reviewer should look at first

1. **The network question** above. It's the owner's call.
2. **The frame-thread restraint:** Insert, Combine and Create Multiple refuse Office documents rather than block. Is a worker for those wanted in this PR, or as a follow-up?
2026-11-11 11:06:01 +01:00
Michael Higgins 4efcfa6ea3 fix(crypt, engine): refuse page extraction when Protect allows any change except extracting pages (#5081)
## What this changes

Adds `Permissions::extract_pages()`: `assemble()` **and** copying (bit 5), as #5081 asks, since extracted pages carry their text and graphics into a file without the source's protection. It is used for Extract, Split, Split by size and every Combine/Insert/Replace/paste source check (`sources::source_document`). Changes within the document (rotate, delete, move, insert) keep `assemble()`.

Since this PR was opened, #6309 made `assemble()` revision-aware on `main`: bit 11 alone at revision 3 and later, bit 4 at revision 2. That already refuses "Any except extracting pages" at revision 3+. This PR builds on it:

- At revision 2, `assemble()` reads bit 4, which "Any except extracting pages" sets, so such a file could still be extracted and split into unprotected copies. Requiring copying closes that.
- New `Needs::PageExtraction` greys out Extract and Split up front. It reuses the existing security-refusal string, so no catalogue changes.
- `Document::allows_page_extraction()`, and an `extract_pages` key in the automation security JSON.
- `page_extract` with `separate: true` extracts and writes one page at a time (`extract_separate`), so it no longer buffers every page. The first page is extracted before `out_dir` is created, so a refused extraction leaves no empty directory. If a later page fails, the files this call wrote are removed, and `out_dir` too if this call created it (review on this PR).
- Insert Pages checks a PDF through the same path as the insert itself: new `Session::insert_source(dest, name, bytes)`, which the UI's insert now calls (review on this PR).
- The `organize.combine` parity notes now say "page extraction".

Fixes #5081

## How you verified it

`cargo fmt --all` is clean. Regression tests (adopted from Harrison-Booth's review patch, plus CodeRabbit's follow-ups):

- `crates/crypt/src/tests.rs::page_extraction_needs_assembly_and_copying`: unit test for `extract_pages()`.
- `crates/engine/src/tests.rs::extraction_permissions_cover_revision_three_to_six_and_leave_no_partial_output`: AnyExceptExtract on RC4-128, AES-128 and AES-256 refuses Extract, Split, Split by size and Combine/Insert/Replace sources without the permissions password, leaves source and destination untouched, and the owner can do everything. The authorized combine output has 2 pages, and a differently encrypted or unprotected destination is refused.
- `crates/engine/src/tests.rs::page_extraction_needs_the_copy_permission_on_every_revision`: a file that withholds only copying (R2 RC4-40 and AES-256) refuses extraction; the owner password lifts it.
- `crates/engine/src/tests.rs::pages_under_the_same_protection_are_inserted_without_page_extraction`: a separately built file under matching protection is inserted, and the result stays restricted.
- `crates/automation/tests/it/automation.rs::refused_extraction_writes_no_split_or_page_output`, and the `extract_pages` checks in `protecting_through_tools`, which now cover three presets, including a user (not the owner) for whom extraction is allowed.

**Only the crypt unit test has been run locally, and it passes.** The engine and automation tests timed out while compiling on this machine and have not been run, nor seen failing without the fix. The one `pdfcraft-ui-egui` call-site change has not been compiled. CI is the first place these build.

- [x] `cargo fmt --check`
- [ ] clippy with `-D warnings` on the crates I changed (not re-run after the rebase)
- [ ] tests pass on the crates I changed (crypt only)

## Anything a reviewer should look at first

- **Rebased onto #6309.** The tests no longer expect "Any except extracting pages" to allow assembly at revision 3+. The same-protection paste tests now use the "Inserting, deleting and rotating pages" preset without copying.
- **Copy (bit 5) is now required**, as the issue suggests. `crates/ui-egui/tests/it/editing.rs::protected_files_that_can_be_combined_are_flagged` used a copy-withheld file as "protected but combinable"; it now uses a print-withheld file, since a no-copy file is no longer a Combine source without the permissions password.
- **Same-document paste**: Paste re-opens the document's own bytes as an Insert source, which the new check would refuse. `sources::open_source_into` accepts a source that withholds extraction but allows assembly only when the destination will be saved under exactly the same protection (file key and `/Encrypt` values), so cut/copy and paste within one protected document still works. Pasting into an unprotected or differently protected document is refused. One limit: after changing security in the same session without saving, a cut can't be pasted back until you save.
- **Not in this PR:** CodeRabbit's `disabled_reason` suggestion for read-only documents predates this change and covers other commands too, so it belongs in a separate UI change.


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

- **New Features**
  - Page extraction and splitting now follow a dedicated permission check, distinct from page assembly.
  - Pages can be inserted from certain protected PDFs when their protection settings match the destination document.

- **Bug Fixes**
  - Separate-page extraction now cleans up files created during a failed operation and avoids creating an output directory when the first page cannot be extracted.
  - Security details now report whether page extraction is permitted.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-11 09:06:48 +01:00
Erkan DogruandFredApps f6c59c0b93 fix: integrate persistent AcroForm script workflows with native prompts and appearance writeback (#5699)
* AcroForm: integrate persistent timesheet script workflows

* AcroForm: address review caching, worker stacks and live metadata

* M6: address second review of AcroForm script workflows

* M6: handle top-level ASI boundaries and verify cyclic calculation results

* M6: bound script workers and isolate pending completions across edits

* M6: make form resets visible before subsequent script reads

* M6: include calculated dependents in reset comparison baselines

* M6: bound reset writeback work before native completion

* M6: charge expanded reset selections and field name bytes

* M6: bound string conversion bytes and charge reset widgets

* M6: preserve unread defaults and bound prompt text conversion

* M6: bound reset calls early and preserve calculated field identity

* AcroForm: address grouped calculations, field identity and widget ownership

* AcroForm: retire field instances and address native review edge cases

* AcroForm: route comparison Save As and Print to the source document

* M6: dispatch text widget mouse-up scripts

* M6: honor fit bounds for form button icons

* AcroForm: bound calculation work and preserve reset and save integrity

* Harden AcroForm script requests and worker recovery

* Keep form callbacks and focus consistent across edits

* AcroForm: preserve reset ordering and bound edit callbacks

* Fix reset appearance ordering and XFA focus after row removal

* Keep XFA focus indexes and stopped reset appearances consistent

* Document form script execution and reset helpers

* Keep script workers bounded and reject partial callback edits

* Keep form events available after a document script reaches VM limits

* Keep main's first-value Simple calculation and restore the calculateNow helper test

AFSimple_Calculate (JavaScript) and the native Simple calculation count each field's first
value again, as #5304 landed them; summing every selection of a multiple-selection list is
a separate behaviour change. Restores the document-helper calculateNow test and the
AFSimple_Calculate doc comment, and adds native and JavaScript multiple-selection cases.

* Patch the callback field snapshot in place instead of rebuilding it

Each field callback rebuilt all field states (3,129 on the benchmark form), and the
retained realm held the previous snapshot, so stamping instance tokens copied it again.
The runner now keeps its last snapshot and patches only the values and flags that
changed; ScriptSession::stamp_fields stamps it before dispatch, the worker copies only
when a token changes, and the realm drops the job's snapshot when the job ends (field
identity is checked against the cached keys instead).

* An oversized job script is that script's error, not a refused edit

Realm::run refused a script too long or too deep to run as an execution refusal, which
made the engine reject the whole edit (setting a 300 KiB calculation script failed). As on
main it is now a plain script error that keeps the value; the realm is not touched.

* Read a leading #! line as a comment in the chain scan; follow main's refusal and conditional limits in tests

The nesting guard from #5079 now checks brackets, prefixes and conditionals; the document
worker's preflight still bounds arrow-function and right-associative chains and skips a
leading #! line as the guard does. A refused script is an error, not a refused edit, so
the preflight test now checks the edit goes through with the error reported.

---------

Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-11 08:42:04 +01:00
FredApps 235ede4896 Print: flatten transparent sheets for GDI (v3) printer drivers (#6155, #5838, #5940) (#6955)
* Print: flatten transparent sheets for GDI (v3) printer drivers

Windows sends a job for a GDI (v3) driver through its XPS-to-GDI
conversion, which fails on soft masks (#6155), prints halos around them
(#5838) or prints transparency as black boxes (#5940). XPS (v4) drivers
print the same pages correctly.

spooler_copy takes a flatten_transparency flag. With it, after greying
and before tiling, each sheet that uses transparency is replaced by an
opaque picture of itself (spooler::flattened): rendered at 300 dpi, lower
for a sheet past 80 MP or 16000 px a side, in tiles of at most
2000 x 2000 px, each an RGB Flate image over white, so no tile reaches
Windows' 2^22 px image cap and tiling leaves them alone. The sheet keeps
its boxes and rotation; its group and annotations go. Sheets without
transparency stay vector.

pdfcraft-preflight gets page_uses_transparency, the PDF/A transparency
search without counting a transparency group on its own, which Word
puts on every page.

Both callers pass false until the GDI-driver check lands.

* Print: flatten transparent sheets only for a GDI (v3) printer driver

spool::xps_driver / spool::gdi_driver read whether the printer's driver renders XPS itself,
through craft-print's new xps_driver (storytold/craft-libs#804, winprint
storytold/winprint.rs#21). The Print dialog and doc_print pass gdi_driver to spooler_copy's
flatten_transparency, so XPS (v4) drivers, PDF printers among them, keep vector sheets.

craft-print pinned to the craft-libs#804 branch, winprint to #21's, until they merge.

* craft-print 60ede99985ef1e5df87a2aca66ed538eb23dec99: craft-libs main with #804 (xps_driver), winprint bc06a7c4 (#21)

* Cargo.lock: the two winprint entries in Cargo's order (by source, after the re-pin)
2026-11-11 08:41:07 +01:00
Michael Higgins 3be126f9bb fix(engine, automation): Action Wizard and doc_batch honour JavaScript being off (#5080)
## 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 -->
2026-11-11 07:32:14 +01:00
Harrison BoothandFredApps 832a51e6d7 Fix #5211: persist JavaScript field colours (#5499)
* 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>
2026-11-10 23:43:44 +01:00
Harrison BoothandFredApps d0b17ef95f Add JavaScript push-button caption methods (#5505)
* Add push-button caption script methods

* Apply scripted caption with other field changes

* Complete caption field property initializers

* Apply scripted captions to every button widget

* Format caption regression test

* Cover caption getter errors and redo state

* Assert button caption undo state

* Reject button caption writes during format events

* Test empty push-button captions

* Validate push-button caption updates

* Keep no-widget caption test data alive

* Check caption clipping marker

---------

Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-10 23:21:48 +01:00
Harrison BoothandFredApps c73a53705b Fix #5211: make calculateNow synchronous (#5304)
* 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>
2026-11-10 22:48:50 +01:00
Harrison BoothandFredApps 3df6be4eff feat(js): expose document page boxes (#5501)
* 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>
2026-11-10 21:37:10 +01:00
FredApps fe70ecce73 Print in grayscale prints on a Windows driver with no grey option (#5806) (#6863)
* 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)
2026-11-10 17:31:12 +01:00
905e9abf6f fix(forms): ignore print requests from automatic field scripts (#4166)
* fix(forms): ignore print requests from automatic field scripts

* docs(contributors): opt in to GitHub profile name sync

---------

Co-authored-by: enriquemelzer-bit <319226743+enriquemelzer-bit@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-10 15:50:16 +01:00
huppifluppandFredApps 8864903795 M10: Recognize text no longer clips the first and last letters of a line (#5058)
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-10 19:23:21 +07:00
4bff30cf62 M10: Windows prints a large image whole: drawn as tiles under Windows.Data.Pdf's cap (#5131) (#5897)
* 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>
2026-11-10 10:25:54 +01:00
regendorf-ux b7cfbd3980 Find and replace text in Edit text mode: a Replace row in the Find box, Replace all as one undo step (#2467) 2026-11-10 15:19:55 +07:00
BFlat 2b46f8bf8c Merge pull request #5698 from Piohrun/fix/jbig2-dictionary-any-route
fix(redact): a shared JBIG2 dictionary leaves with whatever used it, by any route
2026-11-10 14:24:09 +07:00
Hazel Granados 45d51d8c46 M3: a file named with open parameters opens at their destination (#3425) (#5049) 2026-11-10 14:23:54 +07:00
Eric Kaden ea2e13370c M12: dynamic XFA forms tab in their template's traversal order (#4149) 2026-11-10 14:22:02 +07:00
mtlcl410 67cd69050e M7: paragraph edits keep the advance of the text they drop (edit.text-edit) (#2968) 2026-11-10 14:21:29 +07:00
mtlcl410 d9ec80243b M12: XFA forms with many repeated rows lay out in linear time (form.xfa-dynamic) (#6776) 2026-11-10 10:42:57 +07:00
Eric Kaden 5af40c5515 M6: form buttons open the files attached to their document (exportDataObject) (#3575) 2026-11-10 10:02:13 +07:00
jonathanwxh-cell c6d9c13002 fix(ui): keep added image corner resize responsive and clip out-of-bounds drag preview (#4477, #3938) (#4625) 2026-11-10 10:00:18 +07:00
Grzegorz MartensandClaude Opus 5.5 650419766b Merge main again into fix/jbig2-dictionary-any-route
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
2026-11-09 16:41:19 +01:00
bflatastic c45ac1a873 Merge main into merge/ui-print-optimize2
# Conflicts:
#	crates/ui-egui/src/app/files.rs
#	crates/ui-egui/src/tools/forms_ui.rs
2026-11-09 22:03:08 +07:00
bflatastic 302b61c7e3 Fit the landed changes together: the highlight test batches value edits, since a form-script event can't be batched 2026-11-09 21:43:02 +07:00
bflatastic 36e7121ab0 Merge main into merge/ui-print-optimize2
# Conflicts:
#	crates/render/src/inspect.rs
#	crates/render/src/lib.rs
2026-11-09 21:16:23 +07:00
bflatastic 16b57d6a91 Merge main into merge/xfa2
# Conflicts:
#	crates/engine/src/tests.rs
2026-11-09 21:15:35 +07:00
bflatastic 2af11131ef Merge main into merge/xfa2
# Conflicts:
#	crates/automation/src/tools.rs
#	crates/automation/tests/it/automation.rs
#	crates/forms/src/tests.rs
#	crates/ui-egui/src/tools/forms_ui.rs
#	parity/acrobat-features.toml
2026-11-09 20:57:05 +07:00
bflatastic 2ce2d3cf81 Merge main into merge/ui-print-optimize2
# Conflicts:
#	crates/automation/src/redact.rs
#	crates/automation/src/tools.rs
#	crates/print/src/lib.rs
#	crates/render/src/inspect.rs
#	crates/render/src/lib.rs
2026-11-09 20:56:15 +07:00
bflatastic 856d3fb0ac Merge main into merge/comments-annot2
# Conflicts:
#	crates/render/src/inspect.rs
#	crates/render/src/lib.rs
#	crates/ui-egui/src/canvas/page_pointer/tests.rs
#	crates/ui-egui/tests/it/comments.rs
2026-11-09 20:56:09 +07:00
Grzegorz MartensandClaude Opus 5.5 b9e83d8b67 Merge main again into fix/jbig2-dictionary-any-route
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
2026-11-09 13:05:41 +01:00
bflatastic 28585c01a5 Fit the landed changes together
- 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.
2026-11-09 18:46:50 +07:00
BFlat b6cbf9bb30 Merge pull request #6710 from storytold/merge/forms-links-text2
Land forms, links, find and redaction-save fixes (#4496, #4532, #3567, #3773, #4470, #4474, #4497, #5658)
2026-11-09 19:45:39 +08:00
bflatastic ab2d4382ff Merge #5368: M12: XFA master-page fields keep their own value on each page (#5365)
# Conflicts:
#	crates/engine/src/xfa.rs
#	crates/xfa/src/layout/pages.rs
#	crates/xfa/src/lib.rs
#	crates/xfa/src/script.rs
#	crates/xfa/src/tests.rs
2026-11-09 18:38:21 +07:00
bflatastic 50f174afb7 Merge #5010: M12: XFA scripting object model (1/3): scripts read template properties as XFA 3.3 defines them
# Conflicts:
#	crates/engine/src/xfa.rs
#	crates/js/src/xfa/host.rs
#	crates/xfa/src/packets.rs
#	crates/xfa/src/parse.rs
#	crates/xfa/src/tests.rs
#	parity/acrobat-features.toml
2026-11-09 18:30:30 +07:00
bflatastic c1d9dd1db1 Merge #4791: M12: a dynamic XFA form runs all its opening scripts and is laid out once, so a save no longer keeps a half-initialized form (#4790)
# Conflicts:
#	crates/engine/src/xfa.rs
#	crates/forms/src/tests.rs
#	crates/xfa/src/data.rs
#	crates/xfa/src/lib.rs
#	crates/xfa/src/tests.rs
#	parity/acrobat-features.toml
2026-11-09 18:26:29 +07:00
bflatastic 3ec0a629d4 Merge #5508: M12: edit attachment descriptions
# Conflicts:
#	crates/automation/src/dispatch.rs
#	crates/automation/src/json.rs
#	crates/automation/src/tools.rs
#	crates/engine/src/edit.rs
#	crates/engine/src/edit/labels.rs
#	crates/engine/src/edit/run.rs
#	crates/organize/src/import/carry.rs
#	crates/ui-egui/src/dialogs.rs
2026-11-09 18:25:40 +07:00
bflatastic a0b791853c Merge #4784: M6: a radio option whose export value is a number selects its own button, and its XFA data holds the item (form.fill-checkbox-radio) 2026-11-09 18:24:54 +07:00
bflatastic dd72e2e672 Merge #2345: M6: custom stamps can carry a line filled in when they are placed (#1741)
# Conflicts:
#	ROADMAP.md
#	crates/annot/src/lib.rs
2026-11-09 18:23:14 +07:00
bflatastic 3b3721405c Merge #4401: M6: a turned field keeps its place and its text runs along the turn (#4296) 2026-11-09 18:23:10 +07:00
bflatastic 6d1efb7646 Merge #4378: M3: documents that hide the toolbar, menu bar or window controls open that way, with a key to bring them back (organize.initial-view)
# Conflicts:
#	crates/engine/src/commands.rs
#	crates/ui-egui/src/commands.rs
2026-11-09 18:23:10 +07:00
bflatastic f0be783bd0 Merge #4608: M12: XFA repeating rows lay out past 50, and tables in positioned containers repeat their rows (#4607, #4615) 2026-11-09 18:22:55 +07:00
bflatastic c638edf9c7 Merge #2137: M5: comment pop-up windows on the page, with connector lines (comment.popups, comment.connector-lines)
# Conflicts:
#	ROADMAP.md
#	crates/annot/src/info.rs
#	crates/annot/src/lib.rs
#	crates/annot/src/modify.rs
#	crates/annot/src/tests.rs
#	crates/automation/src/comments.rs
#	crates/automation/tests/it/automation.rs
#	crates/engine/src/comments.rs
#	crates/render/src/inspect.rs
#	crates/render/src/inspect/annotations.rs
#	crates/render/src/inspect/model.rs
#	crates/render/src/inspect/tests.rs
#	crates/render/src/lib.rs
#	crates/ui-egui/src/tools/comments/input.rs
#	crates/ui-egui/src/tools/comments/paint.rs
#	crates/ui-egui/src/tools/comments/tests.rs
#	crates/ui-egui/tests/it/comments.rs
2026-11-09 18:22:50 +07:00
bflatastic 8183f96b37 Merge #5973: fix(print,engine): read the page tree once when finding which pages' text reads up
# Conflicts:
#	crates/print/src/tests.rs
2026-11-09 18:22:45 +07:00
bflatastic 697227f901 Merge #4517: M12: run an XFA signature field's preSign scripts before it is signed and postSign after
# Conflicts:
#	crates/engine/src/js.rs
#	crates/engine/src/tests.rs
#	crates/xfa/src/fixtures.rs
#	parity/acrobat-features.toml
2026-11-09 18:21:41 +07:00
bflatastic 117f3b1102 Merge #4497: M8: after Apply redactions, Save asks where and proposes the name with _Redacted (protect.redact-filename-suffix) 2026-11-09 18:21:20 +07:00
bflatastic 70aa8e5c0c Merge #4459: M2: the standard 14 fonts fit the Euro to its standard width (view.render-pages)
# Conflicts:
#	vendor/README.md
2026-11-09 18:19:55 +07:00
bflatastic db5fdcf9bc Merge #4359: M12: XFA enter and exit scripts run whenever a field gains or loses focus
# Conflicts:
#	crates/engine/src/edit/run.rs
#	crates/engine/src/js.rs
#	crates/ui-egui/src/editing.rs
#	crates/ui-egui/src/tools/forms_ui.rs
#	parity/acrobat-features.toml
2026-11-09 18:19:39 +07:00
bflatastic 88a2ab97ec Merge #4094: M5: a callout's arrow point, knee and text box change on their own (#1152)
# Conflicts:
#	crates/annot/src/info.rs
#	crates/annot/src/modify.rs
#	crates/annot/src/tests.rs
#	crates/automation/src/tools.rs
#	crates/automation/tests/it/automation.rs
#	crates/ui-egui/tests/it/comments.rs
2026-11-09 18:19:26 +07:00
bflatastic 0dda66820c Merge #4396: M2: layers open in the state the view auto state sets, and the Layers panel follows /Order (view.layers-panel)
# Conflicts:
#	crates/render/src/inspect.rs
#	crates/render/src/lib.rs
#	vendor/README.md
2026-11-09 18:19:07 +07:00