mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-12 13:54:09 +00:00
c73cccfb33a9146470060bf692578fbabb543607
5649
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c73cccfb33 |
fix(redact): hidden-layer removal reads the default state as viewers do (#5867)
* fix(redact): hidden-layer removal reads the default state as viewers do hidden_layers ignored an indirect /BaseState and, with /BaseState /OFF, let /ON override /OFF, so a group both lists named survived as visible. The OCMD and OCG classifier also read /Type directly, so a membership dictionary with an indirect /Type was never evaluated and its content kept. The default state is now /BaseState (resolved), then /ON, then /OFF, and /Type is resolved before classifying a group or membership dictionary. * address review: follow chained /Type and /BaseState references, hash-set ON/OFF lists * address review: a visibility expression naming a group through an alias reference fails closed |
||
|
|
1e91e8a93e |
fix(forms): a read-only radio button is not picked or left through set_value (#6038)
* fix(forms): a read-only radio button is not picked or left through set_value * address review: use /V as the selection in the read-only radio check, keep reset going * address review: radio read-only checks judge each button by the state it would get, and cover FieldValue::Widget |
||
|
|
ee367a4a1c |
fix(xfa): bound the name and path bytes data nodes and queued instances copy (#6032)
* fix(xfa): bound the name and path bytes data nodes and queued instances copy * review: bound the name bytes of a whole data path and report each cap separately * address review: the queued-instance allowance counts path segment buffers and the dedup copy * address review: the queued path's spare capacity is counted |
||
|
|
b52bc30328 |
fix(annot): count ink points as strokes resolve and cap the eraser's samples (#6001)
* fix(annot): count ink points as strokes resolve and cap the eraser's samples Resizing and erasing a drawing resolved every /InkList entry without a total, so one stroke array listed many times expanded freely; the eraser densified every segment into up to 2,000 samples with no overall limit. * review: refuse an eraser result that exceeds the stored ink point cap * review: say the erasure would make the drawing too large * address review: the eraser counts each stroke's final point against its sample total |
||
|
|
13d6367385 |
fix(redact): read each Type 3 glyph program once per cleanup (#5993)
clean_type3_font decoded and parsed every surviving glyph program of a font, for every font. Many small font dictionaries sharing one compressed CharProc multiplied that work without multiplying the stored data, with a fresh allowance for each decode. The resource names each indirect glyph program uses are now cached by object across fonts (resource-name interpretation stays per font, since each font prunes its own /Resources), and all decoding spends one cleanup-wide DecodeBudget; past it the cleanup fails closed. |
||
|
|
87aebd1b24 |
fix(preflight): follow every /Next action and bound colour and intent work (#5926)
* fix(preflight): follow every /Next action and bound colour and intent work Action chains followed an indirect /Next only when it named an array, and skipped references inside a /Next array, so a forbidden action one indirect hop behind an allowed one survived both verification and conversion; an untyped dictionary with /P and /K was exempt from action checks whatever its /S. Both are now followed and checked, and arrays are borrowed instead of deep-copied per chain. Conversion treats a /Subtype /Widget without /Type as an annotation, and takes forbidden annotations off their pages in one pass. Device-colour inspection decoded each content stream with the 1 GiB per-stream default and no total; it now shares one 1 GiB DecodeBudget and reports unread content instead of treating it as clean. Type 3 CharProcs shared by many fonts are read once, and declared() reads each output intent once (at most 8). * address review: no sRGB intent when colour couldn't all be read; count array reads in action chains; declare every output intent * address review: cut only an over-long identifier; test with a filter that can't be read * review: leave untyped structure elements out of the forbidden-action check * merge main, keep both * address review: a wide direct /Next array is refused before it is queued; queued references are resolved through aliases |
||
|
|
fba22fc738 |
fix(optimize): the fragment search charges every operator of a group it reads (#6095)
* fix(optimize): the fragment search charges every operator of a group it reads * address review: conflict markers removed; the fragment search charges every operator it reads to the production allowance * address review: group reads are charged before a parse failure and count all four operators |
||
|
|
683ccc9fcc |
fix(optimize): walk shared resource dictionaries and name-tree kids once (#5956)
* fix(optimize): walk shared resource dictionaries and name-tree kids once * test(optimize): restore the file around the appended tests * address review: import ObjRef in the optimize audit module |
||
|
|
d46d9daea5 | fix(edit): editor font enumeration reads a face name without building font metrics (#6086) | ||
|
|
583ae29599 | fix(sign): a widget removed from a page's inline Annots is not a comment change (#6013) | ||
|
|
f44ea8380d | redact: a covered clip-and-paint path leaves an empty clip, not its outline (#6996) | ||
|
|
0b0519f5df |
fix(forms): deleting a field removes ancestors it leaves without kids (#6997)
* forms: deleting a field removes ancestors it leaves without kids * forms test: full-save check, widget only in the sibling case |
||
|
|
a23b7c7f73 |
fix(redact): apply marks with an indirect /Subtype and judge inline annotations (#5849)
* fix(redact): apply marks with an indirect /Subtype and judge inline annotations A Redact annotation whose /Subtype is an indirect reference was passed over by apply although the audit, which resolves it, reports it as a mark: the other marks applied and the content under that one stayed. Annotation dictionaries written in place in /Annots were kept whatever they covered, and an inline reply or pop-up kept a removed comment reachable through /IRT or /Parent. /Subtype is now resolved wherever redaction classifies an annotation. Inline entries are judged by their rectangle and relationships like referenced ones, on the marked page and across pages, and the pop-up an annotation names (/Popup) goes with it. * address review: an inline widget's /Parent field goes even when no referenced widget names it * fmt |
||
|
|
2c39b1d625 |
Merge pull request #6882 from Piohrun/fix/redact-unparsed-bytes
M8: redaction refuses a page whose content it can't fully parse |
||
|
|
96590430d4 |
Merge pull request #6935 from Piohrun/feat/optimize-mono-past-80-megapixels
M11: Optimize stores one-bit images and masks past 80 million pixels as monochrome images |
||
|
|
203c4fd8fa |
Merge pull request #6927 from Piohrun/fix/redact-marked-content-last-operands
M8: redaction reads an operator's operands from the end, as the renderer does |
||
|
|
209bb40d88 |
Merge pull request #6886 from Piohrun/fix/redact-form-matrix-by-reference
M8: the redaction walk reads a form's /Matrix entries by reference |
||
|
|
825ba80eac |
Merge pull request #6860 from Piohrun/fix/redact-type3-check-first
M8: the Type 3 cleanup charges the page only when there is a Type 3 font |
||
|
|
b363f7a717 |
Merge pull request #6825 from Piohrun/feat/optimize-mono-masks
M11: Optimize stores a picture's one-bit masks as monochrome images |
||
|
|
22974ca874 |
Merge pull request #6813 from Piohrun/fix/fonts-refusal-counted
M7: a font's metrics are past their allowance only when it refused a read |
||
|
|
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> |
||
|
|
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?
|
||
|
|
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 --> |
||
|
|
c04925c580 |
fix(cos): a clone that edits an object stream or a /Length stops reading what other clones cached (#6910)
* fix(cos): a clone that edits an object stream or a /Length stops reading what other clones cached Clones share the object cache and the parsed object streams. #5802 keeps results that read a clone's edits out of those shared maps, but a clone could still read entries cached before its edit, by another clone or by itself: after replacing object stream 4, a member of it still read as the file's copy, and a stream or object stream framed by an indirect /Length kept the file's framing after the length was edited. Each cached load now records the other objects it read (a member's object stream, an indirect /Length, and what those read in turn), and a document whose edits include one of them reads the object again. Documents with no edits read the shared entries as before. This also replaces forgetting a stream's cached member failures on every edit, which the same check covers for the editing document without costing other clones their entries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cos): count a reference read as null in what a load read A reference whose generation doesn't match the file reads as null, but an edit that sets that generation makes it match. A load that read null there (an object stream's /DecodeParms by reference, say) now counts the object as read, so a clone that sets it decodes the stream again instead of taking the one another clone cached. Reads keep a shared slice, and the read bound has its own test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(optimize): edit the object stream without breaking it space_audit_shares_an_object_stream_..._edited_members set the stream's /First to 999999 and still expected its members to read, which only held while a stale shared-cache entry answered for them. Mark the stream with an unrelated key instead: it is still edited, so its members are sized on their own, and they stay readable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cos): keep an object stream decoded by edited parameters out of the shared cache Whether an object stream is the loading document's own was settled before its indirect /DecodeParms were read. A clone that edited them decoded the stream by its edit and put the result in the cache its clones share, so the document it came from read the clone's data. The check now runs after decoding. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: KenM76 <KenM76> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6b993d9738 |
perf(organize): decode the XMP packet once for repeated document-information reads (#6918)
`info` falls back to the catalog's XMP packet, and the tab and window title read it several times per frame when the document asks for its title to be shown. A packet (up to 16 MiB decoded) was inflated on every read. The last decoded packet is now kept per thread by the identity of its stream object: an edit or another document hands out another object, so it reads afresh. Co-authored-by: KenM76 <KenM76> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
532bdc8fa6 |
feat(prc): PrcFile::units, the model's unit from its model file (#6919)
The model file's units_in_mm and units_from_cad_file were read and dropped. Keep them on ModelFile and expose PrcFile::units() -> Result<Option<ModelUnits>, PrcError>: millimetres per unit and whether the unit is the CAD system's own or the exporter's default. None when no tree is stored or the unit is not a positive finite number. Co-authored-by: KenM76 <KenM76> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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) |
||
|
|
495cba8efc |
fix(js): follow every reading of a script in the nesting guard, so a regex can't put it out of phase (#5079)
## What this changes
The nesting guard that keeps hostile scripts from overflowing the stack in boa's parser could be put out of phase, and this replaces it with one that can't be. boa parses by recursive descent with no depth limit of its own (boa_parser 0.22 has none to turn on), and a stack overflow aborts the process, since `catch_unwind` can't catch it.
**The problem.** `refuse()` scanned bytes and guessed where strings begin. A quote inside a regular expression put it out of phase with the real strings. The first version of this PR counted opening brackets inside skipped text, which closed the reported `/'/` reproduction. It still let closing brackets inside *real* strings count as code. `x=` followed by 5,000 lines of `(/"/,"))))",` nests 5,000 deep in 65 KB (under the 256 KiB limit), while that scan never went past depth 1. The same works with `'`, and with template literals, which the scan didn't read at all.
**The fix.** The guard now lives in `crates/js/src/nesting.rs`.
- **It doesn't guess.** Whether a `/` is a division or starts a regular expression depends on where the parser is: boa's parser, not its lexer, sets that. So the scan follows every reading at once. At each `/` that isn't a comment, it splits into a division reading and a regular-expression reading, and counts nesting on each. boa's reading is always one of them, so the deepest count is at least boa's real depth. That makes it a sound bound, not a mitigation for one reproduction.
- **It drops readings boa rejects,** at the byte where boa stops with a syntax error: a string or regular expression running into a line break, or a regular expression whose groups don't match. boa runs `regress::try_parse` on each literal as it lexes it. This keeps ordinary divisions such as `Math.round(a / b * 100) / 100` from leaving stray readings that add up to false depth.
- **It reads the rest of the lexical grammar that changes what's code:** strings with escapes and line continuations, comments, template text and `${}` substitutions (which count as a level), the Annex B `<!--` and `-->` comments (followed both ways), and a leading `#!`.
- **It caps the work.** At most 16 readings are followed at once. Readings in the same place are merged first, keeping the larger counts, and past that the script is refused.
The limits are unchanged: 64 brackets, a run of 64 prefix operators, 4,096 conditionals. Every path that hands source to boa goes through `refuse()`: AcroForm scripts, XFA JavaScript, and `CheckedCodeFromStrings`.
Brackets inside strings and comments no longer count at all. That drops the trade-off in this PR's first version, which refused a script whose strings held more than 64 opening brackets in total.
Fixes #5079
## How you verified it
Locally, only formatting was run. I didn't build or test this commit, and I'm relying on CI for the build, clippy and the tests.
- [x] `cargo fmt --check -p pdfcraft-js` passes.
- [ ] clippy with `-D warnings` on `pdfcraft-js`: not run locally.
- [ ] `cargo test -p pdfcraft-js`: not run locally. A local test build was stopped before it compiled anything, so I have no local results for this commit.
What the tests check:
- `a_quote_in_a_regex_cannot_put_the_guard_out_of_phase`: the 5,000-line bypass above, and the same with `'` and backticks. It also runs a script nested exactly to the limit, closed properly.
- `nesting_guard_reads_templates_regexes_and_comments` and `divisions_do_not_add_up_to_a_refusal`: controls that must run.
- `boa_rejects_what_the_nesting_guard_drops`: the boa behaviour the guard relies on.
- The XFA-open test (`session::a_quote_in_a_regex_cannot_hide_nesting_from_an_initialize_script`) now runs the 5,000-line bypass too.
- `ordinary_nesting_still_runs` again expects 1,000 brackets inside strings and comments to run.
Not yet shown: that these tests fail on the previous head (
|
||
|
|
e51654bc83 |
fix: anchor comments to highlighted text
## Summary Fixed selected-text comments that were previously shown as separate sticky notes, often covering text and making their target unclear. Comments are now attached directly to highlight annotations, with a small marker and connector beside the highlighted word. Hovering shows only the comment text, while normal highlights and other annotation types keep their existing behavior. Fixes #3759 ## What this changes Fixes the confusing behavior when adding comments to selected text. - Selected-text comments are stored as highlight annotations. - Commented highlights display a small message marker and connector. - Hovering a commented highlight shows only the comment text. - Normal highlights and other annotation types retain their existing display. - Cancelling a selected-text comment clears its pending selection state. ### Screenshots ### Before <img width="1763" height="293" alt="Screenshot from 2026-10-21 04-18-37" src="https://github.com/user-attachments/assets/b88cb60a-82e9-4dd6-81ae-053b482c1e81" /> ### After <img width="2474" height="930" alt="image" src="https://github.com/user-attachments/assets/ee5c6c31-6a6a-4dd8-a3c4-baa04d9908f5" /> ## How you verified it - [x] `git diff --check` - [x] `cargo fmt --check` - [x] clippy with `-D warnings` on the changed crate - [x] tests pass on the changed crate - [x] `cargo xtask layers` and `cargo xtask assets` — not applicable Manual verification was performed with a PDF containing selectable text: - Added a comment to a selected word. - Confirmed the word remains highlighted. - Confirmed the comment marker appears near the highlighted word. - Confirmed the popup displays the comment text. - Confirmed ordinary highlights remain visually distinct. Cargo tests could not be run in the agent environment. ## Never-crash - [x] No new `unwrap`/`expect`/`panic!`/`unsafe` - [x] No new input-derived indexing or unsafe arithmetic - [x] No crash fix is included in this PR ## Provenance - [x] No assets added - [x] No fonts added - [x] No GPL/AGPL code copied - [x] Behavior was implemented from the existing PDF annotation model and project behavior - [x] No new asset requires attribution ## Security - [x] No path or automation behavior changed - [x] No MCP or UI control-channel behavior changed - [x] No outbound network access added ## Parity and reach - [ ] `parity/acrobat-features.toml` updated — not applicable - [ ] New automation tool — not applicable - [ ] Documentation update — not required for this UI bug fix ## Anything a reviewer should look at first The main behavior change is in the selected-text comment path: it now creates a highlighted annotation carrying the comment instead of an independent sticky note. This preserves the selected text as the comment’s visual anchor while keeping normal sticky-note placement unchanged. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **New Features** * Comments on selected text are saved as highlights, preserving the text location. * Choosing the sticky-note tool and clicking elsewhere replaces the selection-anchored composer and places a comment at the new location. * Comments with text markup display a marker on the page, including near page edges. Hovering over markers shows the author and comment, and markers remain selectable across page rotations and zoom levels. * **Bug Fixes** * Markers take priority when selecting or hovering over overlapping annotations. Markers are not shown for comments without text or outside the page. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
f51ce3cfcc |
fix(render): bound the objects lopdf's fallback parses from object streams and arrays (#5083)
## What this changes lopdf's fallback loader (used by `inspect` when the lazy reader fails, and always by `attachment_data`) parsed every object stream eagerly, capping only decoded bytes. A stream of `0 0 0 …` inside the 256 MiB limit built ~120M `Object`s (10+ GB) and aborted. This bounds what it builds: - **Per top-level object:** at most 4,194,304 objects (`parser::MAX_OBJECT_NODES`, the same as cos's `MAX_NODES`), counting every array element and dictionary value at any depth. One budget is threaded through `array` / `_dictionary` / `_direct_object`. An object is charged once it is built, so failed `alt` branches cost nothing, and an exhausted budget is a nom `Failure`. - **Per object stream:** at most 1,000,000 index entries (cos's `/N` clamp), and the index list is no longer allocated from the whole block. - **Per load:** at most 8,388,608 objects across all object streams, in an atomic `NodeBudget` shared by rayon workers. A member reserves up to its own byte length (every object takes at least one byte) and refunds what it doesn't use, so parallel members can't hold the budget between them. Once the budget is spent, the remaining members are skipped with a warning. - **`convert` in `crates/render/src/structure.rs`:** the depth limit is now 128, one level per array or dictionary, matching cos's parser. Before, it was 100 with two levels per dictionary, so ~51 nested dictionaries alone forced the lopdf fallback. Logged as lopdf patch (7) in `vendor/README.md`. Fixes #5083 ## How you verified it `cargo fmt --check` on `pdfcraft-render` and `cargo clippy -p pdfcraft-render --all-targets -- -D warnings` (which compiles the vendored lopdf). Regression tests in `crates/render/src/inspect/tests.rs`, where lopdf patches are tested in-tree (the vendored lopdf's own tests need its missing `assets/example.pdf`): - `object_stream_members_over_the_node_cap_are_skipped` (croquete1): a member that is one array of 4.3M elements is skipped at the per-object cap; a small member is kept. - `object_stream_index_entries_past_the_clamp_are_ignored`: of 1,000,001 distinct members, the one past the 1,000,000-entry clamp is never built. - `object_streams_of_one_load_share_their_node_budget`, `object_streams_loaded_in_parallel_share_their_node_budget` and `a_malformed_object_stream_member_refunds_its_reservation` (Harrison-Booth): a small shared `NodeBudget` bounds the members of several streams, serially and across threads, and a malformed member refunds what it didn't build. `NodeBudget` and `ObjectStream::new_within` are now public for this. - [x] `cargo fmt --check` - [x] clippy with `-D warnings` on the crates I changed - [x] tests pass on the crates I changed (`cargo test -p pdfcraft-render --lib object_stream`: 9 passed, including the five new tests) ## Never-crash - [x] No new `unwrap`/`expect`/`panic!`/`unreachable!`/`todo!` outside tests - [x] No indexing or slicing with input-derived positions; the budget uses `checked_sub` and a `min`-bounded reservation; index entries and objects built are capped; nesting stays bounded (128 in `convert`) - [x] No `unsafe` - [x] The abort this fixes has synthetic regression tests (above) ## Provenance - [x] No assets, fonts or copied code; the limits follow `pdfcraft_cos`'s own caps ## Security - [x] No change to `--root`, MCP, the control channel or network access ## Parity and reach - Not applicable: an internal robustness fix to the fallback loader, with no user-facing feature or parity entry. The vendored-patch log (`vendor/README.md`, lopdf (7)) is updated. ## Anything a reviewer should look at first - `vendor/README.md` now lists the tests above for patch (7). - Ordinary (not compressed) indirect objects are capped at 4M objects each but aren't charged to the per-load budget; their total is bounded only by file size. - `get_compressed_object` still decodes and re-parses the whole stream per lookup (pre-existing); it now gets one object's budget. - Objects nested 101–128 deep now reach the Inspector through the lazy reader rather than being skipped by the fallback. The Inspector's own walkers cap depth at 32 or less. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **Bug Fixes** * Added limits to PDF object parsing and object-stream processing. Loading now parses only selected members, while lookups parse only the requested member. Members that exceed available limits are skipped in a deterministic order. * Increased the supported nesting depth for dictionary and object conversion, allowing more deeply nested content to be processed. * Documented that conversion failures can trigger eager loading as a fallback. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
e317ccae44 |
Print: the printer's own paper sizes in the Paper list, named as the job's media (#2672)
* Print: the printer's own paper sizes in the Paper list, named as the job's media The work of #2672 on current main as one commit (its earlier four commits are on MaxFairwinds/pdfcraft `feat/printer-paper-sizes-pre-squash`), moved to the split Print dialog (workflows/print_ui/{draft,body}.rs) and the single integration-test binaries. - spool::printer_papers: on CUPS the queue PPD's *PaperDimension entries (named by their labels, the *DefaultPageSize one marked), read with the same queue checks and capped PPD read as printer_options; paper_of_size finds one by size, as the driver feeds it. - The Paper list offers the printer's sizes after the standard ones, read each time the dialog opens and when the printer changes. It starts on the printer's default size (the standard entry when that is a standard size as the driver feeds it), unless a paper was chosen, in the dialog or remembered from the last session (#2156), and the printer lists it. - The job names the chosen sheet as its media by the printer's own keyword, also for a standard size the printer has, else by main's media_option. - printer_papers tool; doc_print takes one of the printer's sizes as paper (paper_choice), and with none named uses the printer's default, for "default" the default queue's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Print: name the driver's default keyword for a size it offers twice; fix a parity evidence path A standard size chosen in the Paper list is sent as the printer's own keyword for it. When the driver offers that size under two keywords (Letter and Letter.Fullbleed), the job now names the driver's default one rather than the first. The printer_papers_are_read_from_the_queue_ppd evidence entry named a module path that xtask parity cannot resolve. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Print: a chosen printer paper keeps its keyword when the sizes are read again When the chosen paper was one of the printer's own entries and its size is a standard one (Letter.Fullbleed), reading the printer's sizes again (the dialog reopened) matched it by size alone: it became the standard Letter and the job named the driver's default keyword for that size. The entry is now kept by its keyword when the printer still lists it at that size. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Print: test that a PPD keyword that would break a CUPS option is not offered as a paper A paper's keyword becomes the job's `media` option. parse_ppd_papers already leaves out keywords with a space, `=` or a quote; the PPD test now has lines with each, and fails when that check is loosened. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Print: on CUPS a job on a standard size names the PPD keyword of that size PdfCraft's 4 x 6 in preset goes out as w288h432, which a driver with its own keyword for that stock (a 102 x 152 mm label) didn't get mapped. A cups_media name, a w<w>h<h> name or a PWG self-describing name now maps to the nearest PPD paper within 5 pt a side, the epsilon CUPS's _ppdCacheGetPageSize uses. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
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 --> |
||
|
|
de1fe40da5 |
fix(cos): cap the total entries a materialized xref-stream chain merges (#5082)
## What this changes
`read_xref_chain_materialized` in `crates/cos/src/document/reader.rs` keeps a running total of the rows it merges across the cross-reference chain. Past `MAX_XREF_OBJECTS` (8,388,607) it returns `CosError::Syntax` ("cross-reference chain lists more than 8388607 entries"). The open path then rebuilds the index from object headers and logs the repair, as it already does when a single section decodes past that cap. Before this, the per-section cap and the 10,000-section chain limit multiplied, so the merged map had no overall bound.
Each section adds `sect.len() + odd.len()`: its entries plus the rows it set aside, including those from a hybrid `/XRefStm`. Set-aside rows are kept for the whole chain, and a section can set aside nearly everything it lists. The count is of rows merged, not the map's size, because a chain that relists the same objects costs as much to merge. A section's rows never outnumber what the compact reader counts for it, and that reader stops at 2,000,000 rows. So no chain the compact reader accepts is refused here.
The limit itself lives in `read_xref_chain_materialized_with_limit` (`pub(super)`). The existing entry point, `read_xref_chain_materialized`, calls it with `MAX_XREF_OBJECTS`, so tests can reach the limit in a few rows. Both functions have doc comments.
Fixes #5082
## How you verified it
Three regression tests in `crates/cos/src/document/tests.rs`. The two small-limit tests were suggested by @Harrison-Booth in review.
1. `a_cross_reference_chain_past_the_total_cap_is_refused_and_the_file_reconstructed` runs end to end at the production cap. It builds a synthetic file:
- a catalog, a page tree with one page, and a chain of three cross-reference streams;
- the newest stream locates objects 0 to 6;
- two older Flate-compressed streams each list 4,200,000 rows (`/W [1 0 0]`, disjoint `/Index` ranges). Each is half the per-section cap and too large for the compact reader. Their rows are in use at offset 0, so they are set aside, which keeps the test at a few bytes a row.
It asserts that `Document::open` logs the cap with "rebuilding from object headers" and then "rebuilt cross-reference index ...". The rebuilt document must keep `/Root 1 0 R` and the `/Page`. `read_xref_chain_materialized` must return `CosError::Syntax` at the oldest stream's offset.
2. `the_chain_row_limit_counts_disjoint_and_relisted_rows` uses three classic tables of 2, 1 and 1 rows. A limit of 4 passes, and a limit of 3 refuses the chain at the oldest table. This holds when the sections list different objects, and when one relists an object a newer section has: that is a map of three entries but four rows merged.
3. `the_chain_row_limit_counts_hybrid_rows_set_aside` uses a hybrid file whose table puts object 3 at an offset while its `/XRefStm` puts it in an object stream. It merges six rows and sets object 3 aside. A limit of 7 passes, and a limit of 6 refuses it.
Seen failing, each by reverting part of the fix in the working tree:
| Revert | Test 1 | Test 2 | Test 3 |
|---|---|---|---|
| no chain check at all | fails (empty repair log) | fails | fails |
| count without `sect.len()` | passes | fails | fails |
| count without `odd.len()` (state before
|
||
|
|
41d959d94c | M7: replacing a line keeps both of two dropped quote copies' line move and spacing (edit.text-edit) (#2961) | ||
|
|
9345b5caaa |
cos: drop the second decoded_strict_within that #6046 merged in (#6933)
main fails to compile (E0592): the merge of #6046 added Stream::decoded_strict_within next to the one already there. Both decode the whole chain bounded by max and map the error to CosError::Filter; the earlier one stays, with #6046's test. |
||
|
|
6b53325f28 |
M11: Optimize stores one-bit images and masks past 80 million pixels as monochrome images
The image steps leave an image over 80 million pixels alone, for memory: decoded at eight bits a pixel or more, it would hold 80 MB a component and more. An image already of one bit a pixel holds a byte for each 8 samples of a row, so a large black-and-white scan (an A0 page at 600 ppi has about 560 million pixels) was left as stored though its samples would take less than 80 MB. The monochrome step now takes an image of one bit a pixel as stored (an image mask, or one bit in one colour component), a picture's one-bit mask included, while its rows hold no more bytes than 80 million pixels of eight bits (`one_bit_bytes`): up to 640 million pixels when it is wide, and a strip one sample wide no taller than before. One it would pack from eight bits keeps the 80 million limit, as it is decoded at eight bits first. A fax image's code is allowed no more than 80 MB for its pixels however large the page, and four bytes a row, where it was allowed about a byte a pixel (`fax_code_allowance`). When planning how many pictures run at once, a one-bit image counts as the bytes its rows hold and one more a row, as decoding allows each row's end a few bytes besides its samples. The optimize README and the parity note say which images the monochrome step now takes, and the note states JBIG2's limit of 2^28 pixels in all, as the README does. `group_4_takes_images_of_one_bit_a_pixel_up_to_the_pixel_limit` now expects its one-bit image of 10,000 by 8,001 stored again, where it was left alone. The packing path counts what it decodes, for the tests, as the one-bit path does. Tests: `one_bit_images_past_80_million_pixels_are_stored_as_group_4` (a one-bit image, a picture's stencil mask and a picture's one-bit soft mask of 9,000 by 8,900), `a_one_bit_image_past_80_million_pixels_is_averaged_with_the_default_settings`, `an_image_too_large_for_the_jbig2_decoder_stays_as_it_is` (2^28 pixels stored as JBIG2, a row more left alone, and averaged), and the unit tests `one_bit_images_are_decoded_up_to_their_own_limit` and `a_one_bit_picture_counts_as_the_bytes_of_its_rows` (each with a strip one sample wide, at its limit and past it) and `a_fax_images_code_is_allowed_80_million_bytes_and_four_a_row` and `a_zero_wide_image_is_not_decoded`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY |
||
|
|
ad3e6d4b85 | fix(optimize): bound each unused-resource decode by the remaining shared budget (#6046) | ||
|
|
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> |
||
|
|
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> |
||
|
|
3ad707d07b |
Test: wait for the paper row before clicking Custom… while discovery is pending (#6925)
adding_custom_paper_overrides_pending_driver_default clicked Custom… as soon as the background paper read started; on hosted macOS the dialog hadn't drawn its paper row by then and the test failed (no node "Custom…"). It now waits for the button, up to 10 s, before clicking. Found by the first nightly macOS run. |
||
|
|
042371bda9 |
M8: a test of its own for each operand read the fix changes
Review of the whole change found reads it fixes that no test failed without: each was put back to main's read alone, and the tests passed. Each now has a case that fails without it: - the proof's `Tf` size (text starting outside the region reaches into it only at its size) and font (an undecodable font's text leaves the region unverifiable), and the strings a `TJ` carries; - the proof's `l`, `c`, `v` and `y` points after a surplus, from a point outside the region, and the walk's `m`, `l`, `c`, `v` and `y` points beside a mark, each alone; - the property list a second page sharing the resources still names; - a Type 3 font an ExtGState's `/Font` sets, kept by the page and its glyph by the font, and one without resources of its own, which keeps the page's pattern its glyph may paint with; - a Type 3 font only a soft mask's group selects, the mask's ExtGState named after a surplus; - the named property lists read once, with each `BDC` naming another entry, so that it is the lists, not each name's answer, that are kept. And the walk's `cs` says why it tells a Pattern space by the name `/Pattern` alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY |
||
|
|
5e2552dcb0 |
M8: named property lists and their MCIDs are read once for a stream
The tags cleanup resolved the page's `/Properties` at every named `BDC`, on main too: 20,000 of them with 5,000 entries took 28 s, as measured in review (optimized), against 0.07 s with one entry. And the second commit's `MCID` by reference was read again at every `BDC`: the document keeps an object larger than its cache only until it reads another, so reading the `MCID` evicted a property list that large, which was parsed again, all of it, at the next `BDC` (50 `BDC` took 12.3 s against 0.44 s with the `MCID` given directly). Now the named property lists are read once for the stream, in place when they are direct, and each name's `MCID` once. Tests (`crates/redact/tests/operator_work.rs`, a binary of its own whose tests run one at a time), each the least of three runs against one entry or operator, allowing ten times as long plus a second: `named_property_lists_are_read_once_for_a_page` (5,000 `BDC`, 2,000 entries) and `an_oversized_property_list_is_read_once_for_a_stream` (100 `BDC` naming a list too large to keep). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY |
||
|
|
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> |
||
|
|
7c78222612 |
M8: path points and an MCID by reference are read as the renderer does
Review of the operand reads. Path construction was still read from the front, in the walk (`construct_path`) and in the proof (`extend_path`): after a surplus number, `9 100 100 m 9 140 100 l ...` placed the square at x = 9, so a mark over it removed nothing, and an even surplus placed a square beside a mark under it, which was removed. `m` and `l` now take their last two numbers, `c` six, `v` and `y` four, and the proof's `re` four, as the walk's already did. The tags cleanup read an `MCID` with `Dict::int`, so one given by reference in a named property list, or in a structure element's `/MCR`, was missed and the element kept the removed text in `/ActualText` and `/Alt`. It is read through the reference now. Tests: `a_paths_points_after_a_surplus_number_are_where_the_renderer_draws_them` (lines, and curves by `c`, `v` and `y`), `an_mcid_given_by_reference_loses_what_redaction_removed` (and an `/MCR` of a form's stream that stays), and `the_proof_reads_a_paths_points_from_the_end` (lines, `re`, and each of `c`, `v` and `y` from a point outside the region). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY |
||
|
|
b16f0c0a77 |
M8: redaction reads an operator's operands from the end, as the renderer does
An operator takes its operands from the end, and the renderer ignores a surplus before them: `(x) /OC /MC0 BDC` marks a layer's block, and `(x) /GS1 gs` selects `/GS1`. Redaction read some of them from the front, so a surplus stood in for them and what they named went unseen. Found in review of #6882, where Remove Hidden Information left the text of a hidden layer in `(x) /OC /MC0 BDC (LAYERSECRET) Tj EMC`, which both the app's renderer and Poppler hide. In what Apply Redactions and Remove Hidden Information decide: - the layers walk read a block's tag and property list from the front, and kept a hidden layer's block; - the cleanup of `/ActualText`, `/Alt`, `/E` and `/T` in a block whose paint was removed read (and wrote) the property list from the front, and kept the name that still read the removed word; - the tags cleanup read a block's `MCID` from the front, and left the removed text in its structure element's `/ActualText` and `/Alt`; and a `TJ`'s array, so a block that still drew was taken for empty and its structure element lost the content the page still shows; - the property cleanups read the name of a `BDC` or `DP` property list from the front, and retired a property list the page still draws with; - the walk read the ExtGState a `gs` selects from the front, so a font set only that way was not set, and `cs`/`CS` the same for a pattern space; - the Type 3 cleanup's reads of `cs`, `gs`, `Tf`, `BDC` and `DP`, which decide which resources and fonts are still used. The redaction proof read `Tc`, `Tw`, `Tz`, `TL`, `Ts`, `Tf`, `"`, `TJ` and `Do` from the front too. `pdfcraft_content::Op` gains `back`, `name_back` and `num_back`: operand `k` counted from the end. Tests (`crates/redact/src/tests/operands_from_the_end.rs`): a hidden layer's block, `/ActualText` on removed paint, a tag's `MCID`, a tagged `TJ` that still draws, a property list still drawn with, a font from an ExtGState, the pattern a `/Pattern cs` keeps, and a Type 3 font with the ExtGState, colour space, property list and font its glyph uses, each after a surplus operand. In `tests_verify.rs`, `the_proof_reads_operands_from_the_end` (`Tf`, `TJ`, and `Do` of a form and of images), `the_proof_reads_the_text_states_operands_from_the_end` (`Tc`, `Tw`, `Tz`, `TL`, `Ts` and `"`) and `an_image_a_type3_glyph_draws_after_a_surplus_operand_is_found`. And `operands_are_counted_from_the_end` in the content crate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY |
||
|
|
85987baef0 |
Test: at the minimum width on macOS the main menu may be the folded Menu button (#6920)
The menu bar folds titles that don't fit into "⋯", or all of them into one "Menu" when not even the first fits. On hosted macOS, at MIN_WINDOW_SIZE, the bar is the Menu button (the system font is wider), so the test's File menu item wasn't there and it failed; the main menu is still reachable. A real macOS window uses the native menu bar. |
||
|
|
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> |
||
|
|
1fc9501cf3 | fix(platform): preserve file visibility when saving replaced files (#5001) (#5048) | ||
|
|
b10f85262c |
Test: the non-UTF-8 output-name probe test doesn't run on macOS (#6913)
output_probe_paths_preserve_non_utf8_names creates files named with bytes that aren't UTF-8. APFS keeps only UTF-8 file names, so on macOS creating the probe fails before the check runs and the test fails; no output can have such a name there. It runs on the other unix systems as before. Found running the automation tests on hosted macOS. |