diff --git a/.cargo/config.toml b/.cargo/config.toml index 5592118f6c..a1caaeaa38 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,2 +1,7 @@ [alias] xtask = "run -p xtask --" + +# WebAssembly SIMD (every current browser has it; there's no runtime detection on wasm). The OCR +# engine's kernels (rten-simd) and other vectorised code only use it when compiled in. +[target.wasm32-unknown-unknown] +rustflags = ["-C", "target-feature=+simd128"] diff --git a/.github/workflows/house-rules.yml b/.github/workflows/house-rules.yml new file mode 100644 index 0000000000..de64ad3ee4 --- /dev/null +++ b/.github/workflows/house-rules.yml @@ -0,0 +1,28 @@ +name: House rules + +on: + push: + branches: [main] + paths: + - AGENTS.md + - CLAUDE.md + - .github/workflows/house-rules.yml + pull_request: + paths: + - AGENTS.md + - CLAUDE.md + - .github/workflows/house-rules.yml + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: house-rules-${{ github.ref }} + cancel-in-progress: true + +jobs: + house-rules: + uses: storytold/craft-common-actions/.github/workflows/house-rules.yml@main + with: + rust: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9403a1e044..4b44d79b68 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,14 @@ concurrency: cancel-in-progress: false jobs: + # Regenerates contributors/contributors.json for the release commit; every build bundles it. + credits: + permissions: + contents: read + pull-requests: read + uses: storytold/craft-common-actions/.github/workflows/refresh-contributors.yml@main release: + needs: credits # Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos. uses: storytold/craft-common-actions/.github/workflows/release.yml@main with: @@ -27,6 +34,7 @@ jobs: app-title: PdfCraft craft-fonts-ref: b076417b5880bd4154bd22f00695b1d28c6bea83 version: ${{ inputs.version }} + contributors-artifact: ${{ needs.credits.outputs.artifact-name }} secrets: APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} diff --git a/AGENTS.md b/AGENTS.md index 3ab900aa95..066cad3597 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,60 @@ + +## House rules + +These apply in every Craft repository. A later rule may be stricter, not weaker. The source is `standards/house-rules-core.md` in `storytold/craft-contrib`. Propose changes there, not in a copy. + +1. **A person is accountable.** Whoever runs an agent answers for what it opens and for its review comments. +2. **Check, then claim.** Before writing code, read the issue comments, search pull requests for `#N` and the keywords, and check recent commits on `main`. Comment on the issue first. Release a claim you will not finish. +3. **Verify, don't guess.** Open a definition before using it. Confirm the cause in the code, even when someone names a location. +4. **Keep fixes narrow and tested.** Fix what the issue describes, where the bug is. Add a test that fails without the fix. +5. **Review the diff before opening a pull request.** Identifiers exist as used, the test fails without the fix, nothing unrelated changed, and the description is true. +6. **Look for a better pull request.** If another fixes the same thing, close yours when it is better or equal, and say why. +7. **Report only what ran.** Say what you tested and what you could not run. Without a local build or test, say so and rely on CI before marking ready. +8. **`main` changes through a pull request.** Do not push to it. Branch protection enforces this. There is no exception. +9. **Stop and ask only about** licensing outside the allowlist, publishing, releases, a new remote, secrets, spending money, destructive git, and anything else hard to undo. Otherwise keep working. `plan/` is a gitignored notebook, not rules. Without it, do not ask for it and do not stop. +10. **Keep secrets and personal details out** of the repository, logs and requests. +11. **CI stays shared.** Workflows call `storytold/craft-common-actions`. Do not change CI in a feature or fix pull request. If CI cannot cover a change, open an issue there. +12. **Guards say why.** A check that looks removable says what it prevents. Read that before changing it. +13. **Instructions must be readable by everyone.** No notes about one machine, and no links to files a reader cannot open. `craftrules` is retired. Do not link to it. +14. **Keep pull requests moving.** Concurrent pull requests are fine; unanswered actionable feedback is not. Respond in 7 days, a reminder at 14, closure eligible at 30, from when an actionable request waits on the author. Suggestions and unconfirmed bot findings do not start the clock. A fix, explanation or agreed plan resets it; unrelated activity does not. Waiting on a reviewer or maintainer pauses it and does not count against the author. A maintainer decides closure; it is never automatic. Reuse search results, do not poll, and stop at a rate limit. +15. **No agent attribution.** A pull request does not say an agent wrote it. A repository rule may not require that. + + +## Rust Craft repositories + +These apply to native Craft apps and to shared Rust libraries. Select them by repository type. A repository that merely contains some Rust does not take this block. Rules later in a repository's file may be stricter, but not weaker. The source is `standards/house-rules-rust.md` in [storytold/craft-contrib](https://github.com/storytold/craft-contrib). + +1. **Never crash.** Non-test code does not panic and has no `unsafe`, except where the repository file names an exception. Return errors, treat input as hostile, and bound recursion and allocations. New production crate roots copy the `deny` attribute from an existing crate: `clippy::unwrap_used`, `clippy::expect_used`, `clippy::panic`, `clippy::unimplemented`, `clippy::todo`, `clippy::unreachable`. Every crash fix comes with a test that panicked before the fix. +2. **Clean room.** Learn from observed behaviour and public specifications. Do not copy another product's code, assets, strings or artwork. Check a dependency's licence before adding it. +3. **Assets and fonts.** Every asset is original or suitably licensed, and is recorded in `ATTRIBUTION.md` in the same change. Font files live in [storytold/craft-fonts](https://github.com/storytold/craft-fonts), not in the app repository. +4. **Rust only for native Craft apps.** No handwritten JavaScript or TypeScript, and no Tauri, Electron or webview shell, in those apps. Shared tooling follows its documented technical scope and maintainer-approved exceptions. Craft PR Review's existing Tauri and TypeScript UI is one such exception. It does not relax this rule for native apps. Preserve any existing web build. +5. **Time goes through `chrono`.** Do not hand-roll calendar conversion. For dates beyond what `chrono` can represent, follow [standards/time.md](https://github.com/storytold/craft-contrib/blob/main/standards/time.md). +6. **Contributor credits.** Add or edit only your own person's entry in `contributors/people.toml`, and only after asking them. Never guess a name or hand-edit `contributors.json`. + +## Licence allowlist + +Dependencies and assets use SPDX identifiers. An expression is allowed when every identifier is in this list. A repository may name a shorter list. It may not add an identifier here by editing its own copy of these rules. + +- `ISC` +- `MIT` +- `BSD-2-Clause` +- `BSD-3-Clause` +- `Apache-2.0` +- `Unicode-3.0` +- `OFL-1.1` +- `Ubuntu-font-1.0` +- `Bitstream-Vera` +- `CC0-1.0` +- `CC-BY-4.0` +- `CC-BY-SA-4.0` +- `LicenseRef-Public-Domain` + +`LicenseRef-Storyteller-Trademark` is not in this list. It covers Storyteller brand marks only, where a repository already says so. + + # AGENTS.md — rules for every agent and contributor -These rules bind every AI agent and every human contributor working in this repository. They come before any task, instruction or deadline. `CLAUDE.md` holds the day-to-day working instructions. If the two ever disagree, **this file wins.** +These rules bind every AI agent and every human contributor working in this repository. They come before any task, instruction or deadline. Day-to-day working instructions are in this file; `CLAUDE.md` imports it. --- @@ -30,7 +84,7 @@ An **asset** is any non-code material that the project includes, ships, embeds, ### 1.2 Only open, provable licences Every asset must be one of: -- **Open source:** ISC, MIT, BSD-2-Clause, BSD-3-Clause, Apache-2.0, OFL-1.1, Ubuntu-Font-1.0 or Bitstream-Vera. +- **Open source:** ISC, MIT, BSD-2-Clause, BSD-3-Clause, Apache-2.0, OFL-1.1, Ubuntu-Font-1.0, Bitstream-Vera or Unicode-3.0 (Unicode, Inc. data such as ICU4X's line-breaking dictionaries; already allowed for code in `deny.toml`). - **Creative Commons:** CC0-1.0, CC-BY-4.0 or CC-BY-SA-4.0. - **Public domain.** - **Contributor-original:** created, photographed or generated by a contributor who owns it and licenses it under the project licence (`MIT OR Apache-2.0`). @@ -85,7 +139,7 @@ If you find an asset that breaks these rules, stop and remove it from the reposi --- -## 2. Clean-room rules (summary; details in `CLAUDE.md` and `plan/adr/0001`) +## 2. Clean-room rules (summary; `plan/adr/0001` is an optional local note) - Never read, disassemble or copy anything inside the Acrobat bundle. Only names and listings are allowed, and **never** open `Contents/Resources/JavaScripts/`. - Behaviour comes only from public specifications (ISO 32000-2, the Arlington model), public documentation and black-box observation. @@ -106,7 +160,7 @@ If you find an asset that breaks these rules, stop and remove it from the reposi PdfCraft opens files from strangers, and people trust it with their work. A malformed PDF, a bad tool argument, a corrupt settings file or a full disk must produce an error the user (or agent) can act on, never a crash and never lost work. **This outranks feature work:** don't ship a feature by adding a panic path, and fix a crash before building on top of it. The rules below are the ones that bind this repository. -- **Fail with `Result`.** Non-test code returns errors through the crate's error type and `?`. Where readers are lenient by design, fall back gracefully and record the repair (see Fidelity in `CLAUDE.md`). +- **Fail with `Result`.** Non-test code returns errors through the crate's error type and `?`. Where readers are lenient by design, fall back gracefully and record the repair (see Fidelity below). - **No panicking shortcuts.** No `unwrap()`, `expect()`, `panic!`, `unreachable!`, `todo!` or `unimplemented!` outside tests, unless the call is provably infallible (e.g. writing to a `Vec`), with a comment saying why. `assert!` only for internal invariants that input cannot reach. - **No `unsafe`.** The workspace sets `unsafe_code = "forbid"`. If a platform API needs it, use a maintained safe wrapper crate; a real exception needs the owner's sign-off. - **Treat every input-derived number as hostile.** Use `get()` instead of `[i]` or `[a..b]`. Slice strings only at char boundaries. Use checked/saturating arithmetic for lengths, offsets and counts. Guard against division by zero and NaN/inf casts, and never cast a negative value to `usize`. Cap allocations sized by the file. @@ -115,10 +169,18 @@ PdfCraft opens files from strangers, and people trust it with their work. A malf - **Prove it.** Every crash fix comes with a small synthetic regression test that panicked before the fix. Turn fuzzer findings (`cargo xtask fuzz`) into such tests before fixing them. - **Enforced by clippy.** Root `clippy.toml` lets tests use `unwrap`/`expect`/`panic!`. A crate that is clean carries `#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]`; new crates start with it. -## 5. Everything else +## 5. Page interaction, fidelity and gates - -Follow `CLAUDE.md`. It covers quality gates, layering, commit rules and how to run and look at the app. +- **Layering.** Nothing below L7 depends on egui/winit/eframe/rfd. `cos`, `filters`, `crypt` and `arlington` stay standalone. +- **Page input.** Page modules read `PagePointer` (`crates/ui-egui/src/page_pointer.rs`), never egui's raw pointer state. `page_pointer::tests::page_modules_read_the_pointer_only_through_page_pointer` enforces it. +- **Page cursor.** Page modules claim the cursor for what is under the pointer (`view.cursor.claim`, `crates/ui-egui/src/canvas/page_cursor.rs`) and never set it themselves. `page_pointer::tests::page_modules_claim_the_cursor_and_never_set_it` enforces it. +- **Page presses.** A page module decides what is under a point in one hit test (`Hit` in `crates/ui-egui/src/canvas/page_router.rs`) and acts on a press only through that router. Modules never read the router; they are told. `page_pointer::tests::page_modules_act_on_a_press_only_through_the_router` enforces it. +- **Fidelity.** The PDF object graph is the model. Preserve unknown data. Saves are incremental unless a full rewrite is required. Never silently drop or repair data without recording it. +- **Fonts** are the optional build input `CRAFT_FONTS_DIR`. Code using `pdfcraft_fonts::CRAFT_FONTS` must work when it is empty. +- **Quality gates** before every commit: `cargo fmt --check`, clippy with `-D warnings`, the tests, `cargo xtask layers`, `cargo xtask assets`, and the wasm check. New workspace crates include `workspace = true` under `[lints]`. Scope `cargo clippy -p --all-targets -- -D warnings` and `cargo test -p ` to the crates you touched; the workspace sweep is CI's job. +- **Review at four zooms** as you go: the clause, the function, the module, and the project (layering, wasm, undo, save fidelity, a matching automation tool, an honest `parity/` entry). Prove a test fails before the fix by reverting the fix once. +- `plan/` is an optional gitignored notebook. Do not ask for it and do not stop when it is absent. Public status is [`ROADMAP.md`](ROADMAP.md), [`docs/gaps.md`](docs/gaps.md) and [`docs/roadmap.md`](docs/roadmap.md). "Shipped" in `parity/` means exists and tested, not parity with Acrobat. +- `cargo run -p pdfcraft -- ` opens the app. `pdfcraft-cli run` drives the engine headlessly. `cargo xtask fuzz` findings stay in `fuzz-out/` and become synthetic tests. For UI work, look at a screenshot from `--control` or the `shot` example. ## Contributor credits (About window) diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index c70d84e624..cfc16f78f4 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -4,10 +4,11 @@ Every asset PdfCraft includes, bundles or uses to build its published material, with its author, source and licence. The policy is in [AGENTS.md](AGENTS.md) §1. The machine-readable list, with the SHA-256 of every asset, is [ATTRIBUTION.toml](ATTRIBUTION.toml). Licence texts are kept beside the assets and summarised in [NOTICE](NOTICE). -## In this repository (256) +## In this repository (258) | Asset | Title | Author | Licence | Source | Used for | |---|---|---|---|---|---| +| `docs/images/pdfcraft-ocr-verify.png` | PdfCraft screenshot: OCR Verify screen | PdfCraft contributors (generated by `cargo xtask screenshots`) | MIT OR Apache-2.0 | this repository | README: OCR review. Shows only PdfCraft UI, its Lucide icons and a synthetic scanned page built from the embedded fonts, with a real recognition (the OCR models) loaded into the Verify screen | | `assets/icons/accessibility.svg` | Lucide icon "accessibility" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/accessibility.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/icons/align-left.svg` | Lucide icon "align-left" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/align-left.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/icons/align-center.svg` | Lucide icon "align-center" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/align-center.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | @@ -44,7 +45,7 @@ Every asset PdfCraft includes, bundles or uses to build its published material, | `assets/icons/chevron-up.svg` | Lucide icon "chevron-up" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/chevron-up.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/icons/chevrons-left.svg` | Lucide icon "chevrons-left" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/chevrons-left.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/icons/chevrons-right.svg` | Lucide icon "chevrons-right" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/chevrons-right.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | -| `assets/icons/circle-check.svg` | Lucide icon "circle-check" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/circle-check.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | +| `assets/icons/circle-check.svg` | Lucide icon "circle-check" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/circle-check.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs; drawn as PDF path data by crates/sign/src/status_layers.rs into displayed signature status layers | | `assets/icons/circle-dashed.svg` | Lucide icon "circle-dashed" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/circle-dashed.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/icons/circle-dot.svg` | Lucide icon "circle-dot" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/circle-dot.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/icons/circle-help.svg` | Lucide icon "circle-help" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/circle-help.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | @@ -173,7 +174,7 @@ Every asset PdfCraft includes, bundles or uses to build its published material, | `assets/icons/text-select.svg` | Lucide icon "text-select" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/text-select.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/icons/trash-2.svg` | Lucide icon "trash-2" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/trash-2.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/icons/trash.svg` | Lucide icon "trash" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/trash.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | -| `assets/icons/triangle-alert.svg` | Lucide icon "triangle-alert" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/triangle-alert.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | +| `assets/icons/triangle-alert.svg` | Lucide icon "triangle-alert" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/triangle-alert.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs; drawn as PDF path data by crates/sign/src/status_layers.rs into displayed signature status layers | | `assets/icons/type.svg` | Lucide icon "type" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/type.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/icons/underline.svg` | Lucide icon "underline" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/underline.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/icons/undo-2.svg` | Lucide icon "undo-2" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/undo-2.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | @@ -183,7 +184,7 @@ Every asset PdfCraft includes, bundles or uses to build its published material, | `assets/icons/x.svg` | Lucide icon "x" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/x.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/icons/zoom-in.svg` | Lucide icon "zoom-in" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/zoom-in.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/icons/zoom-out.svg` | Lucide icon "zoom-out" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/zoom-out.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | -| `assets/fonts/Inter-Regular.ttf` | Inter Regular | The Inter Project Authors (Rasmus Andersson) | OFL-1.1 | https://github.com/rsms/inter | UI font, embedded in the app; demo PDF labels; slanted fallback glyphs in Fill & Sign typed signatures for characters Dancing Script lacks (Cyrillic, Greek), drawn as filled paths | +| `assets/fonts/Inter-Regular.ttf` | Inter Regular | The Inter Project Authors (Rasmus Andersson) | OFL-1.1 | https://github.com/rsms/inter | UI font, embedded in the app; demo PDF labels; slanted fallback glyphs in Fill & Sign typed signatures for characters Dancing Script lacks (Cyrillic, Greek), drawn as filled paths; embedded into displayed signature status captions beyond WinAnsi by crates/sign/src/status_layers.rs | | `assets/fonts/Inter-Medium.ttf` | Inter Medium | The Inter Project Authors (Rasmus Andersson) | OFL-1.1 | https://github.com/rsms/inter | UI font, embedded in the app; demo PDF labels | | `assets/fonts/Inter-SemiBold.ttf` | Inter SemiBold | The Inter Project Authors (Rasmus Andersson) | OFL-1.1 | https://github.com/rsms/inter | UI font, embedded in the app; demo PDF labels | | `assets/fonts/JetBrainsMono-Regular.ttf` | JetBrains Mono Regular | The JetBrains Mono Project Authors | OFL-1.1 | https://github.com/JetBrains/JetBrainsMono | UI monospace font; demo PDF code | @@ -248,7 +249,7 @@ Every asset PdfCraft includes, bundles or uses to build its published material, | `assets/icons/thumbs-up.svg` | Lucide icon "thumbs-up" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/thumbs-up.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/icons/thumbs-down.svg` | Lucide icon "thumbs-down" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/thumbs-down.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/icons/message-square-plus.svg` | Lucide icon "message-square-plus" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/message-square-plus.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | -| `assets/icons/circle-x.svg` | Lucide icon "circle-x" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/circle-x.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | +| `assets/icons/circle-x.svg` | Lucide icon "circle-x" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/circle-x.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs; drawn as PDF path data by crates/sign/src/status_layers.rs into displayed signature status layers | | `crates/ui-egui/tests/data/form.pdf` | One-page AcroForm test fixture (text, check box, radio group, combo box) | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: hand-written PDF objects, no third-party content | Form-filling UI tests (crates/ui-egui/tests/it/forms.rs, commands.rs) | | `assets/icons/pentagon.svg` | Lucide icon "pentagon" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/pentagon.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/icons/message-square-quote.svg` | Lucide icon "message-square-quote" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/message-square-quote.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | @@ -263,9 +264,10 @@ Every asset PdfCraft includes, bundles or uses to build its published material, | `assets/icons/superscript.svg` | Lucide icon "superscript" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/superscript.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/icons/subscript.svg` | Lucide icon "subscript" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/subscript.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/icons/pilcrow.svg` | Lucide icon "pilcrow" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/pilcrow.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | +| `assets/icons/a-large-small.svg` | Lucide icon "a-large-small" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/a-large-small.svg) | UI icon, embedded via crates/ui-egui/src/widgets/icon_data.rs | | `assets/icons/move-horizontal.svg` | Lucide icon "move-horizontal" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/move-horizontal.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | -## Compiled in through dependencies (6) +## Compiled in through dependencies (7) | Crate | File | Title | Author | Licence | Source | Used for | |---|---|---|---|---|---|---| @@ -275,6 +277,7 @@ Every asset PdfCraft includes, bundles or uses to build its published material, | `epaint_default_fonts` 0.37.0 | `fonts/emoji-icon-font.ttf` | emoji-icon-font | John Slegers | MIT | https://github.com/jslegers/emoji-icon-font | egui monochrome fallback font (egui feature monochrome_emoji_fonts) | | `epaint_default_fonts` 0.37.0 | `fonts/egui-icons.ttf` | egui icons (monochrome emoji-icon-font subset) | John Slegers | MIT | https://github.com/jslegers/emoji-icon-font | Small special-icon subset embedded by egui default_fonts; retained in FontDefinitions::default(). Provenance: https://github.com/storytold/egui/blob/6d518fc799681571963842f95b55a6b68431b15a/crates/epaint_default_fonts/fonts/egui-icons.txt; complete MIT copyright and permission text in NOTICE. | | `hayro-interpret` 0.7.0 | `src/font/generated/metrics.rs` | Standard-14 font metrics and encodings (as Rust tables) (non-visual data, AGENTS.md §1.1) | Adobe (Core 14 AFM metrics, PDF specification encodings); tables by The Hayro Authors | MIT OR Apache-2.0 | ISO 32000-2 Annex D; Adobe Core14 AFM files (via vendored hayro-interpret) | Glyph widths and encodings for non-embedded standard fonts. Non-visual data, allowed by AGENTS.md §1.1 | +| `icu_segmenter_data` 2.3.0 | `data/segmenter_dictionary_extended_v1.rs.data` | Southeast Asian word dictionaries for line breaking (Thai, Lao, Khmer, Burmese) | The ICU4X Project Developers; word lists from the ICU project (Unicode, Inc.) | Unicode-3.0 | https://github.com/unicode-org/icu4x (via icu_segmenter_data) | Word boundaries for wrapping Thai text in Edit text and Add text (crates/edit/src/linebreak.rs). Non-visual data; the line segmenter loads only these dictionaries, not the Chinese and Japanese one | ## Downloaded at build time (22) @@ -312,4 +315,4 @@ Not in this repository and never downloaded by it: compiled in only when the bui | Input | Option | Title | Author | Licence | Source | Attribution | Used for | |---|---|---|---|---|---|---|---| | `craft-i18n` | `CRAFT_I18N_DIR` | Interface translations: the craft-i18n catalogs (locales//common.tsv and pdfcraft.tsv) of the 34 languages PdfCraft offers besides English | The craft-i18n translators, including every translator of PdfCraft's former in-repo catalogs (crates/ui-egui/src/i18n/*.tsv, migrated at pdfcraft@ecdd1e6) | MIT OR Apache-2.0 | https://github.com/storytold/craft-i18n (pinned by commit in craft-i18n.rev, which CI, releases and the flatpak manifests read) | https://github.com/storytold/craft-i18n/blob/main/ATTRIBUTION.md | Interface text in languages other than English, embedded by crates/ui-egui/build.rs and looked up through craft-libs' craft-i18n crate; a build without CRAFT_I18N_DIR is English only. Clean-room translations from PdfCraft's English interface labels | -| `craft-fonts` | `CRAFT_FONTS_DIR` | Japanese fonts: BIZ UDPGothic (Regular, Bold), Shippori Mincho, BIZ UDMincho; native Simplified Chinese font: LXGW WenKai Lite; Arabic font: Noto Sans Arabic | The BIZ UDGothic and BIZ UDMincho Project Authors (Morisawa Inc.), The Shippori Mincho Project Authors (FONTDASU), LXGW and the original Klee Project Authors / Fontworks (LXGW WenKai Lite), The Noto Project Authors (Noto Sans Arabic) | OFL-1.1 | https://github.com/storytold/craft-fonts (pinned by commit in .github/workflows/release.yml and .github/workflows/ci.yml); LXGW WenKai Lite from https://github.com/lxgw/LxgwWenKai-Lite/releases/tag/v1.530 (commit b712c83d98acf977bacfb7fb010207802808bda9) | https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md | Japanese fallback in the interface (BIZ UDPGothic first) and Japanese glyphs in edited PDF text (Shippori Mincho); Simplified Chinese interface fallback on native builds only (LXGW WenKai Lite Regular, font SHA-256 f799a538a3355877e597a0ae04050e897932c9ae7689eb9227c2953c5dd549ea, OFL SHA-256 0333a85fe077b6849f8e417acc44e8bd879442ceb681e271acf93e2d57fd4e2e, read from fonts/simplified-chinese-manifest.txt and never embedded on wasm); Arabic interface text, file names and document titles (Noto Sans Arabic); the web build embeds BIZ UDPGothic Regular and the pinned small Arabic, Hebrew, Thai and Devanagari faces; the selected craft-fonts revision contains no Telugu face. Other non-Japanese CJK faces retain the general 4 MiB cap; the reviewed native-only LXGW fallback has a separate 16 MiB cap. Open-source OFL releases are permitted under current AGENTS.md §1.1 | +| `craft-fonts` | `CRAFT_FONTS_DIR` | Japanese fonts: BIZ UDPGothic (Regular, Bold), Shippori Mincho, BIZ UDMincho; native Simplified Chinese font: LXGW WenKai Lite; Arabic font: Noto Sans Arabic | The BIZ UDGothic and BIZ UDMincho Project Authors (Morisawa Inc.), The Shippori Mincho Project Authors (FONTDASU), LXGW and the original Klee Project Authors / Fontworks (LXGW WenKai Lite), The Noto Project Authors (Noto Sans Arabic) | OFL-1.1 | https://github.com/storytold/craft-fonts (pinned by commit in .github/workflows/release.yml and .github/workflows/ci.yml); LXGW WenKai Lite from https://github.com/lxgw/LxgwWenKai-Lite/releases/tag/v1.530 (commit b712c83d98acf977bacfb7fb010207802808bda9) | https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md | Japanese fallback in the interface (BIZ UDPGothic first) and Japanese glyphs in edited PDF text (Shippori Mincho); Simplified Chinese interface fallback on native builds only (LXGW WenKai Lite Regular, font SHA-256 f799a538a3355877e597a0ae04050e897932c9ae7689eb9227c2953c5dd549ea, OFL SHA-256 0333a85fe077b6849f8e417acc44e8bd879442ceb681e271acf93e2d57fd4e2e, read from fonts/simplified-chinese-manifest.txt and never embedded on wasm); Arabic interface text, file names and document titles (Noto Sans Arabic); status captions of named-layer signatures in Arabic, Japanese, Thai and Devanagari, drawn as filled outlines into the display copy by crates/sign/src/status_layers.rs; the web build embeds BIZ UDPGothic Regular and the pinned small Arabic, Hebrew, Thai and Devanagari faces; the selected craft-fonts revision contains no Telugu face. Other non-Japanese CJK faces retain the general 4 MiB cap; the reviewed native-only LXGW fallback has a separate 16 MiB cap. Open-source OFL releases are permitted under current AGENTS.md §1.1 | diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index 9638749929..67b588f25e 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -10,6 +10,17 @@ # licence: SPDX expression from the allowlist in AGENTS.md §1.2. adobe_data = true marks the closed list of # Adobe-authored non-visual technical data allowed by AGENTS.md §1.1. +[[asset]] +path = "docs/images/pdfcraft-ocr-verify.png" +title = "PdfCraft screenshot: OCR Verify screen" +author = "PdfCraft contributors (generated by `cargo xtask screenshots`)" +source = "this repository" +licence = "MIT OR Apache-2.0" +licence_file = "LICENSE-MIT" +kind = "image" +usage = "README: OCR review. Shows only PdfCraft UI, its Lucide icons and a synthetic scanned page built from the embedded fonts, with a real recognition (the OCR models) loaded into the Verify screen" +sha256 = "ea7485b151e27ad3fd2a2dac079436ab11745d42032201a029bb20f819c6af8f" + [[asset]] path = "assets/icons/accessibility.svg" title = "Lucide icon \"accessibility\"" @@ -414,7 +425,7 @@ source = "https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icon licence = "ISC AND MIT" licence_file = "assets/icons/LICENSE-lucide.txt" kind = "icon" -usage = "UI icon, embedded via crates/ui-egui/src/icon_data.rs" +usage = "UI icon, embedded via crates/ui-egui/src/icon_data.rs; drawn as PDF path data by crates/sign/src/status_layers.rs into displayed signature status layers" sha256 = "201e790f3a05c65b2c4b0f952c9b9e1f35cd1c5b21698b3f4be131883fe90bc8" [[asset]] @@ -1833,7 +1844,7 @@ source = "https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icon licence = "ISC AND MIT" licence_file = "assets/icons/LICENSE-lucide.txt" kind = "icon" -usage = "UI icon, embedded via crates/ui-egui/src/icon_data.rs" +usage = "UI icon, embedded via crates/ui-egui/src/icon_data.rs; drawn as PDF path data by crates/sign/src/status_layers.rs into displayed signature status layers" sha256 = "55aa37e0b73e7f8cd906952dbdeb96283c6595ca64aba240a17f3455cb989114" [[asset]] @@ -1943,7 +1954,7 @@ source = "https://github.com/rsms/inter" licence = "OFL-1.1" licence_file = "assets/fonts/OFL-Inter.txt" kind = "font" -usage = "UI font, embedded in the app; demo PDF labels; slanted fallback glyphs in Fill & Sign typed signatures for characters Dancing Script lacks (Cyrillic, Greek), drawn as filled paths" +usage = "UI font, embedded in the app; demo PDF labels; slanted fallback glyphs in Fill & Sign typed signatures for characters Dancing Script lacks (Cyrillic, Greek), drawn as filled paths; embedded into displayed signature status captions beyond WinAnsi by crates/sign/src/status_layers.rs" sha256 = "40d692fce188e4471e2b3cba937be967878f631ad3ebbbdcd587687c7ebe0c82" [[asset]] @@ -2252,6 +2263,18 @@ adobe_data = true usage = "Glyph widths and encodings for non-embedded standard fonts. Non-visual data, allowed by AGENTS.md §1.1" sha256 = "2ec9e608573eda459de1e1d9bda3bb494315e36bee885acc88912a11fe0caa41" +[[bundled]] +crate = "icu_segmenter_data" +version = "2.3.0" +path = "data/segmenter_dictionary_extended_v1.rs.data" +title = "Southeast Asian word dictionaries for line breaking (Thai, Lao, Khmer, Burmese)" +author = "The ICU4X Project Developers; word lists from the ICU project (Unicode, Inc.)" +source = "https://github.com/unicode-org/icu4x (via icu_segmenter_data)" +licence = "Unicode-3.0" +kind = "data" +usage = "Word boundaries for wrapping Thai text in Edit text and Add text (crates/edit/src/linebreak.rs). Non-visual data; the line segmenter loads only these dictionaries, not the Chinese and Japanese one" +sha256 = "cad3821ca44fdb88ed856ff3336444aa200afe7b9d19e34ac28ae6a4459d4c6d" + [[fetched]] file = "PlayfairDisplay[wght].ttf" url = "https://raw.githubusercontent.com/google/fonts/9710da1eacb3be272583c3224dcb70f9da6eadbb/ofl/playfairdisplay/PlayfairDisplay%5Bwght%5D.ttf" @@ -2993,7 +3016,7 @@ source = "https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icon licence = "ISC AND MIT" licence_file = "assets/icons/LICENSE-lucide.txt" kind = "icon" -usage = "UI icon, embedded via crates/ui-egui/src/icon_data.rs" +usage = "UI icon, embedded via crates/ui-egui/src/icon_data.rs; drawn as PDF path data by crates/sign/src/status_layers.rs into displayed signature status layers" sha256 = "ef1fec17d88794ba6c34c5cf63bdadbd234b3d4dffbcc3d55468b47e3f398472" [[asset]] @@ -3131,7 +3154,7 @@ source = "https://github.com/storytold/craft-fonts (pinned by commit in .github/ licence = "OFL-1.1" attribution = "https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md" kind = "font" -usage = "Japanese fallback in the interface (BIZ UDPGothic first) and Japanese glyphs in edited PDF text (Shippori Mincho); Simplified Chinese interface fallback on native builds only (LXGW WenKai Lite Regular, font SHA-256 f799a538a3355877e597a0ae04050e897932c9ae7689eb9227c2953c5dd549ea, OFL SHA-256 0333a85fe077b6849f8e417acc44e8bd879442ceb681e271acf93e2d57fd4e2e, read from fonts/simplified-chinese-manifest.txt and never embedded on wasm); Arabic interface text, file names and document titles (Noto Sans Arabic); the web build embeds BIZ UDPGothic Regular and the pinned small Arabic, Hebrew, Thai and Devanagari faces; the selected craft-fonts revision contains no Telugu face. Other non-Japanese CJK faces retain the general 4 MiB cap; the reviewed native-only LXGW fallback has a separate 16 MiB cap. Open-source OFL releases are permitted under current AGENTS.md §1.1" +usage = "Japanese fallback in the interface (BIZ UDPGothic first) and Japanese glyphs in edited PDF text (Shippori Mincho); Simplified Chinese interface fallback on native builds only (LXGW WenKai Lite Regular, font SHA-256 f799a538a3355877e597a0ae04050e897932c9ae7689eb9227c2953c5dd549ea, OFL SHA-256 0333a85fe077b6849f8e417acc44e8bd879442ceb681e271acf93e2d57fd4e2e, read from fonts/simplified-chinese-manifest.txt and never embedded on wasm); Arabic interface text, file names and document titles (Noto Sans Arabic); status captions of named-layer signatures in Arabic, Japanese, Thai and Devanagari, drawn as filled outlines into the display copy by crates/sign/src/status_layers.rs; the web build embeds BIZ UDPGothic Regular and the pinned small Arabic, Hebrew, Thai and Devanagari faces; the selected craft-fonts revision contains no Telugu face. Other non-Japanese CJK faces retain the general 4 MiB cap; the reviewed native-only LXGW fallback has a separate 16 MiB cap. Open-source OFL releases are permitted under current AGENTS.md §1.1" [[asset]] path = "crates/sign/tests/data/x509-rsa-sha1.pdf" @@ -3200,6 +3223,17 @@ kind = "icon" usage = "UI icon, embedded via crates/ui-egui/src/icon_data.rs" sha256 = "d344d9f9222f6c1b698c0c82a753a2212d9bd7785f06c00cd6e4910fe2fee8f8" +[[asset]] +path = "assets/icons/a-large-small.svg" +title = "Lucide icon \"a-large-small\"" +author = "Lucide Contributors (portions Cole Bemis, Feather)" +source = "https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/a-large-small.svg)" +licence = "ISC AND MIT" +licence_file = "assets/icons/LICENSE-lucide.txt" +kind = "icon" +usage = "UI icon, embedded via crates/ui-egui/src/widgets/icon_data.rs" +sha256 = "d4985fc71174d108e322c3b25cb6abe2837a115fe3f239a1918645db436743ec" + [[asset]] path = "assets/icons/move-horizontal.svg" title = "Lucide icon \"move-horizontal\"" diff --git a/CLAUDE.md b/CLAUDE.md index 5c7034b7a4..43c994c2d3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,65 +1 @@ -# PdfCraft — instructions for agents - -PdfCraft is a clean-room, open-source, Rust-native PDF application targeting Adobe Acrobat Pro parity. It runs natively on macOS, Windows, Linux and FreeBSD, and on the web via WASM. It is the sibling of `../photocraft` (a Photoshop-class editor) and follows the same conventions. - -## Start every session here -1. Read `plan/STATUS.md`: the current milestone, the next unchecked task and any blockers. `ROADMAP.md` is the one-page summary (stage, numbers, progress log) and `docs/roadmap.md` the milestone table; at the end of every session update the milestone row and add a line to the ROADMAP.md progress log. - - Read `docs/gaps.md` (the ranked gaps: Acrobat interop, the open-issue backlog, the renderer and a fidelity harness, editing existing content, Pro workflows, 1.0 polish) and `docs/target-app-parity.md` (the numbers and how they are measured) before choosing work. Prefer the gaps over new P2/P3 features, and keep both documents true when things change (`docs/` progress docs carry a status line and a revision history). - - "Shipped" in `parity/` means "exists and tested", not "as good as Acrobat". Don't mark a feature shipped on a generic test, and say in its notes what is still missing. -2. Read that task in `plan/execution-plan.md` §3, the relevant section of `plan/architecture.md`, and the README of the crate you're touching. -3. Follow the **autonomous operation protocol** in `plan/execution-plan.md` §7 (orient → plan → implement + test → verify → record → commit). Don't stop to ask unless §7 lists the decision as the user's. - -`plan/` is gitignored (local-only, like PhotoCraft). The machine-readable parity checklist lives in `parity/` (committed). - -## Non-negotiables -- **Assets: read `AGENTS.md` §1 before adding or showing any icon, image, font or document.** No assets from Adobe products, ever. Only openly licensed or contributor-original assets are allowed, each with an entry in `ATTRIBUTION.toml`. `cargo xtask assets` enforces this. `AGENTS.md` overrides this file. -- **Fonts live in [storytold/craft-fonts](https://github.com/storytold/craft-fonts).** Never commit font files here (`AGENTS.md` §1.4). It is the optional build input `CRAFT_FONTS_DIR`: `git clone https://github.com/storytold/craft-fonts ../craft-fonts && CRAFT_FONTS_DIR=../craft-fonts cargo test --workspace` embeds the Japanese fonts (UI fallback, Japanese text in edited PDFs) and runs their tests, which otherwise skip. Code using `pdfcraft_fonts::CRAFT_FONTS` must work when it is empty. -- **Clean-room.** - - Never read, disassemble or copy anything inside the Acrobat bundle (names and listings only). **Never open `Contents/Resources/JavaScripts/`.** - - Behaviour comes from public docs, specs (ISO 32000-2, the Arlington model) and black-box observation (`plan/acrobat/`). - - Never copy GPL/AGPL code. MuPDF, Ghostscript, Poppler, veraPDF and DSS run only as external oracle processes. - - See `plan/README.md` §Clean-room and `plan/adr/0001`. -- **Privacy.** When observing Acrobat, use synthetic fixtures only. Never capture the Home view, recent files or account info. Capture by window id (`plan/acrobat/tools/`). Never commit Acrobat outputs, corpus files or personal data. -- **Layering.** Nothing below L7 depends on egui/winit/eframe/rfd (`plan/architecture.md` §3). `cos`/`filters`/`crypt`/`arlington` stay standalone. -- **Page input.** Page modules read `PagePointer` (`crates/ui-egui/src/page_pointer.rs`), never egui's raw pointer state (`hover_pos`, `interact_pos`, `latest_pos`, `press_origin`, button reads); `page_pointer::tests::page_modules_read_the_pointer_only_through_page_pointer` enforces it. -- **Page cursor.** Page modules claim the cursor for what is under the pointer (`view.cursor.claim(Layer::…, icon)`, `crates/ui-egui/src/canvas/page_cursor.rs`), never set it (`set_cursor_icon`, `cursor_icon =`, `on_hover_cursor`); `document_area` sets it once, from the claim on the highest layer the active tool reaches. `page_pointer::tests::page_modules_claim_the_cursor_and_never_set_it` enforces it. -- **Page presses.** A page module decides what of its own is under a point in one hit test, which returns a `Hit` (owner, layer, cursor, tip; `crates/ui-egui/src/canvas/page_router.rs`) and gives its cursor, its tip and its press. It acts on a press only when the press is its own or, for the active tool, nobody's: the page view tells it which (`PagePress`: its own, nobody's, or another page's), and it never tests the press point again or asks another module whether it wants it. Escape cancels any press the pages hold (`cancel_on_escape`), and that press is ignored until the button is up. `page_pointer::tests::page_modules_act_on_a_press_only_through_the_router` enforces it. -- **Fidelity.** The PDF object graph is the model. Preserve unknown data. Saves are incremental unless a full rewrite is required. Never silently drop or repair data without recording it. -- **Never panic.** Every PDF, script, MCP call, settings file and keystroke is untrusted input, and none of it may crash the app or lose the user's work. This outranks feature work. Return `Result` (the crate's error enum) and propagate with `?`, or fall back leniently and record it. In non-test code: - - No `unwrap`/`expect`/`panic!`/`unreachable!`/`todo!` unless it is provably infallible, with a comment saying why. - - No indexing or slicing with input-derived positions (use `get`, and slice strings only at char boundaries). - - Use checked or saturating arithmetic on input-derived numbers, and guard against division by zero and NaN/inf casts. - - Cap allocations sized by input, and bound recursion with depth limits or seen-sets. - - Handle lock poisoning. - - No `unsafe` (`unsafe_code = "forbid"`). - - Every crash fix gets a synthetic regression test. See `AGENTS.md` §4. -- **Rust only** in the product and build (`xtask`). No handwritten JS/TS. -- **Time goes through `chrono`** (workspace dependency). Never hand-roll UTC/calendar conversion (days-to-date algorithms, leap-year math); a crate owns only its output format string. The one exception is range extension: past the years chrono can represent, shift the date by whole 400-year Gregorian cycles (146,097 days, after which the calendar repeats), let chrono compute the month and day, and add the cycles back to the year (splitting seconds into hours, minutes and seconds is plain arithmetic). -- **Review every update as you make it, at four zooms.** Not an end-of-task pass — a per-update habit, so a defect never gets built on. **Clause:** checked arithmetic, `get()` not `[i]`, char-boundary slicing, the right error through `?`, `<` against `<=`, the 1-based/0-based seam, capped allocations. **Function:** is it total — name an input that panics; walk empty, zero, max, malformed, cyclic; is the return type honest. **File/module:** two sources of truth, dead or duplicated code, is the public surface still smallest and the doc comment still true. **Project:** layering, wasm, undo/redo, save fidelity, encryption and permissions, a matching `automation` tool or registry entry, and whether a `parity/` status or note just became untrue. Scale the zoom to the change, and fix what a level turns up before widening. - - Tests mirror the zooms: property → unit → crate integration → end-to-end. - - **Prove a test fails before the fix by actually reverting the fix.** A test nobody has seen fail is not evidence. This has caught two worthless tests in this repo: one confinement test passed with path resolution switched off entirely, because the tools failed on a nonexistent document id before reaching the path. - - **Rebasing is a review step, not a mechanical one.** Read every hunk you resolve, and the auto-merged hunks in the files you touched too — `main` moves fast enough that a file can change by hundreds of lines under a branch. -- **External PRs are held to safety, security and completeness before style.** Provenance first (no Adobe-derived asset, no committed font, no copied GPL/AGPL code, behaviour from the spec and not from reading another implementation), then no new panic path or `unsafe`, then `--root` confinement on every path including ones reached by nested dispatch, opt-in-only MCP and control channel, no new outbound network, then a headless `automation` tool, tests at the right zoom and an honest `parity/` entry. Provenance is the part a contributor cannot be expected to know, and the only part that cannot be undone after merge. -- **Quality gates** before every commit: `cargo fmt --check`, clippy with `-D warnings`, the tests, `cargo xtask layers`, `cargo xtask assets`, and the wasm check once `xtask ci` exists (M0). - - New workspace crates must include `[lints] workspace = true` in `Cargo.toml` to inherit `[workspace.lints]`. Production crate roots must declare the required crate-level lint attributes before modules or other crate items; crate-level `//!` documentation may precede them. - - **Scope builds and tests to the crates you touched** — `cargo clippy -p -j 2 --all-targets` and `cargo test -p -j 2`, under a `timeout`. A full `--workspace` build has locked up a development machine's CPU; it is CI's job and CI runs it on every PR. Run the workspace sweep locally only when you have a reason to and the machine is free. -- **Check for existing work before starting a fix or feature.** Search the issues and PRs, open and closed, with `gh issue list -R storytold/pdfcraft --state all --search ""` and the same with `gh pr list` (try a few phrasings), and check whether a merged fix is already on `main` (`git merge-base --is-ancestor origin/main`). If one is, the report is likely from an older build: say so rather than fixing it again. If an open PR covers it, review or build on that PR instead of opening a competing one. If an issue exists, reference it in the PR (`Fixes #123`). -- **Commits and PRs:** `main` is the only integration branch and is **never pushed to directly**. Branch from an up-to-date `main` as `/` (`feat/`, `fix/`, `docs/`, `chore/`, `refactor/`), one task id per commit (e.g. `M1.4: xref stream reader`), commit only green states, and open a PR into `main` using `.github/pull_request_template.md`. Merge only when CI is green. - - **`main` moves fast enough that PRs rot.** It took 396 commits in the three days to 2026-10-12, and most open PRs are conflicting at any given time. Rebase your own branch the same day `main` moves under it, and ask the author to rebase theirs — never rewrite a contributor's branch. Conflict risk tracks how *contested* the files are, not how large the change is: tooling, tests and docs are nearly free, while `crates/fonts`, `ui-egui` and the i18n catalogues are where several people collide at once. - -## Running and looking at the app -- `cargo run -p pdfcraft -- ` opens the desktop app. -- `cargo run -p pdfcraft-cli -- run --script steps.json --root DIR` drives the engine headlessly through the automation tools (`pdfcraft-cli tools` lists them). Use it, together with `page_render`, to check engine changes. `pdfcraft-cli mcp` is the opt-in MCP server (AGENTS.md §3). -- `cargo xtask fuzz --time 300` mutation-fuzzes open/render/edit/save in child processes. Findings land in `fuzz-out/findings/` (git-ignored; never commit corpus-derived files). Turn every real finding into a small synthetic regression test before fixing it. -- `cargo xtask parity [--partial]` reports Acrobat-parity progress from `parity/acrobat-features.toml`. Update the entry when a feature ships. -- `cargo xtask demo-pdf` builds `dist/demo/pdfcraft-showcase.pdf` (needs Chrome) for visual checks. -- For UI work, **look at the result**. Either launch `pdfcraft --control FILE doc.pdf` and use `pdfcraft-cli ui --control FILE screenshot --out x.png` (plus `inspect`, `click`, `key`, `type`, `command`, `set`), or take a headless shot with `cargo run -p pdfcraft-ui-egui --example shot`. Compare against `plan/acrobat/02-ui-ux.md`. Control-channel tests use kittest (`crates/ui-egui/tests/control.rs`). -- Parallel agents: use a separate `CARGO_TARGET_DIR` per agent and separate git worktrees. - -## Current bootstrap debt (tracked in STATUS.md) -- `pdfcraft-render` renders through the `hayro` crate directly and inspects documents through `lopdf`. Both get replaced by `cos` / `model` / the DisplayList device (M1–M2, ADR-0004). - -## CI workflows -- Workflows in `.github/workflows/` call the shared workflows in [`storytold/craft-common-actions`](https://github.com/storytold/craft-common-actions) at `@main`, with their inputs and secrets. No other jobs: if no shared workflow does what's needed, it doesn't go here. -- Don't add or change CI in a feature or fix PR. If the existing workflows can't build or test a change, link an existing craft-common-actions issue or PR, or open a new issue, or a PR only for a small fix that is safe for every app and does not increase CI time. -- Some workflow files here still run their own jobs. Don't add to them. Moving a job into craft-common-actions is its own task, never part of another PR, and the job stays here until a shared workflow runs it. +@AGENTS.md diff --git a/Cargo.lock b/Cargo.lock index 573b215b7c..ea3a743bf8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -143,6 +143,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + [[package]] name = "aligned-vec" version = "0.6.4" @@ -1234,7 +1243,7 @@ dependencies = [ [[package]] name = "craft-print" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=787a7a0f972fd1d7a7618fdbeca3a5267a0407ff#787a7a0f972fd1d7a7618fdbeca3a5267a0407ff" +source = "git+https://github.com/storytold/craft-libs?rev=7ee35a9fbd47af572d55fd63dbf0333d1f00da17#7ee35a9fbd47af572d55fd63dbf0333d1f00da17" dependencies = [ "craft-winprint-gdi", "log", @@ -1303,10 +1312,30 @@ dependencies = [ "zip", ] +[[package]] +name = "craft-web-api" +version = "0.1.0" +source = "git+https://github.com/storytold/craft-libs?rev=fd5e2feff6ea25bed371a99cc732b3968d621505#fd5e2feff6ea25bed371a99cc732b3968d621505" +dependencies = [ + "js-sys", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-futures", +] + +[[package]] +name = "craft-web-host" +version = "0.1.0" +source = "git+https://github.com/storytold/craft-libs?rev=fd5e2feff6ea25bed371a99cc732b3968d621505#fd5e2feff6ea25bed371a99cc732b3968d621505" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + [[package]] name = "craft-winprint-gdi" version = "0.1.0" -source = "git+https://github.com/storytold/winprint.rs?rev=417ac3c4d14d3da5485a99c7d5ce8f05664ec312#417ac3c4d14d3da5485a99c7d5ce8f05664ec312" +source = "git+https://github.com/storytold/winprint.rs?rev=64e7afd4d1483512385012c340ecaaaf0f3beb7f#64e7afd4d1483512385012c340ecaaaf0f3beb7f" dependencies = [ "windows-sys 0.61.2", ] @@ -3104,6 +3133,28 @@ dependencies = [ "zerovec", ] +[[package]] +name = "icu_segmenter" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d07aafccd67af15d02512a6adf5896fbc5ed00f2e99b471d2efa14016db3db" +dependencies = [ + "icu_collections", + "icu_locale_fallback", + "icu_provider", + "icu_segmenter_data", + "potential_utf", + "smallvec", + "utf8_iter", + "zerovec", +] + +[[package]] +name = "icu_segmenter_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae293c039020f9ec10710af98d29ce6aa2051486638b49c9a6409f3b4a9e98ad" + [[package]] name = "idna" version = "1.1.0" @@ -4236,7 +4287,6 @@ version = "0.18.1" dependencies = [ "pdfcraft-content", "pdfcraft-cos", - "pdfcraft-filters", "pdfcraft-fonts", "pdfcraft-model", "thiserror 2.0.21", @@ -4249,6 +4299,7 @@ dependencies = [ "hayro-syntax", "pdfcraft-cos", "pdfcraft-fonts", + "pdfcraft-geom", "pdfcraft-model", "thiserror 2.0.21", ] @@ -4324,6 +4375,7 @@ version = "0.18.1" dependencies = [ "chrono", "hayro-syntax", + "js-sys", "pdfcraft-crypt", "pdfcraft-filters", "pdfcraft-oracle", @@ -4331,6 +4383,7 @@ dependencies = [ "proptest", "serde_json", "thiserror 2.0.21", + "wasm-bindgen", ] [[package]] @@ -4369,6 +4422,7 @@ name = "pdfcraft-edit" version = "0.18.1" dependencies = [ "hayro-syntax", + "icu_segmenter", "kurbo 0.13.1", "pdfcraft-annot", "pdfcraft-content", @@ -4507,11 +4561,24 @@ version = "0.18.1" dependencies = [ "ocrs", "pdfcraft-fonts", + "png 0.18.1", "rten", + "serde_json", + "sha2 0.11.1", "tempfile", "thiserror 2.0.21", ] +[[package]] +name = "pdfcraft-ocr-worker" +version = "0.18.1" +dependencies = [ + "js-sys", + "pdfcraft-ocr", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "pdfcraft-optimize" version = "0.18.1" @@ -4598,6 +4665,7 @@ dependencies = [ name = "pdfcraft-redact" version = "0.18.1" dependencies = [ + "aho-corasick", "pdfcraft-annot", "pdfcraft-content", "pdfcraft-cos", @@ -4605,9 +4673,13 @@ dependencies = [ "pdfcraft-filters", "pdfcraft-fonts", "pdfcraft-forms", + "pdfcraft-geom", "pdfcraft-model", + "pdfcraft-render", "pdfcraft-testkit", "pdfcraft-xfa", + "regex", + "sha2 0.11.1", "thiserror 2.0.21", ] @@ -4625,6 +4697,7 @@ dependencies = [ "lopdf", "pdfcraft-cos", "pdfcraft-fonts", + "pdfcraft-geom", "serde", "skrifa 0.44.0", "thiserror 2.0.21", @@ -4652,9 +4725,11 @@ dependencies = [ "p521", "pbkdf2", "pdfcraft-annot", + "pdfcraft-content", "pdfcraft-cos", "pdfcraft-fonts", "pdfcraft-model", + "pdfcraft-testkit", "rc2", "ripemd", "rsa", @@ -4702,6 +4777,7 @@ dependencies = [ "pdfcraft-engine", "pdfcraft-fonts", "pdfcraft-model", + "pdfcraft-ocr", "pdfcraft-platform", "pdfcraft-render", "pdfcraft-xfa", @@ -4729,10 +4805,13 @@ dependencies = [ name = "pdfcraft-web" version = "0.18.1" dependencies = [ + "craft-web-api", + "craft-web-host", "eframe", "egui", "js-sys", "pdfcraft-ui-egui", + "serde_json", "wasm-bindgen-futures", "web-sys", ] @@ -5481,6 +5560,29 @@ dependencies = [ "bitflags 2.13.2", ] +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + [[package]] name = "regex-syntax" version = "0.8.11" @@ -8183,7 +8285,7 @@ dependencies = [ [[package]] name = "winprint" version = "0.2.1" -source = "git+https://github.com/storytold/winprint.rs?rev=5f80b9baf6b58937c401656634e42d33f00ddf49#5f80b9baf6b58937c401656634e42d33f00ddf49" +source = "git+https://github.com/storytold/winprint.rs?rev=51934db382f225454cf152a8657eb03789be63c4#51934db382f225454cf152a8657eb03789be63c4" dependencies = [ "flate2", "fmt-derive", diff --git a/Cargo.toml b/Cargo.toml index bf6a4e3146..d919a90383 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ craft-keymap = { git = "https://github.com/storytold/craft-libs", version = "0.1 # the commit in `craft-i18n.rev`). Already in the tree through craft-ui. craft-i18n = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "787a7a0f972fd1d7a7618fdbeca3a5267a0407ff" } # Shared system spooler; document composition remains in pdfcraft-print. -craft-print = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "787a7a0f972fd1d7a7618fdbeca3a5267a0407ff" } +craft-print = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "7ee35a9fbd47af572d55fd63dbf0333d1f00da17" } # Whether a path stored in a document may be opened (GoToR link targets). craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "787a7a0f972fd1d7a7618fdbeca3a5267a0407ff" } # craft-update: app-specific pretend versions apply to startup and manual checks. @@ -70,7 +70,11 @@ pdfcraft-prc-render = { path = "crates/prc-render", version = "0.18.1" } hayro = "0.7.1" hayro-syntax = "0.7.2" lopdf = "0.45" -flate2 = { version = "1.1", default-features = false, features = ["rust_backend"] } +# zlib-rs: miniz_oxide took about 1.7 µs for every fixed-Huffman block, empty or not +# (`pdfcraft_filters::FLATE_READ_WORK`). Other dependencies still build flate2's default +# miniz_oxide backend; flate2 prefers zlib-rs when both are built, as the Windows build already did +# through craft-update. `flate::tests::a_copy_from_before_the_start_is_damage` shows which runs. +flate2 = { version = "1.1", default-features = false, features = ["zlib-rs"] } weezl = "0.2" # CCITT Group 4 encoding (pdfcraft-filters); already in the tree through tiff. fax = "0.2.7" @@ -80,6 +84,9 @@ hayro-jbig2 = "0.3" # foundation thiserror = "2" +# Linear-time matching (no backtracking) for caller-supplied redaction patterns. `perf-dfa` gives +# the lazy-DFA cache that `RegexBuilder::dfa_size_limit` bounds (the limit is inert without it). +regex = { version = "1", default-features = false, features = ["std", "unicode", "perf-dfa"] } serde = { version = "1", features = ["derive", "rc"] } serde_json = "1" log = "0.4" diff --git a/LICENSES.toml b/LICENSES.toml index 0617ca5d57..ac54d9f702 100644 --- a/LICENSES.toml +++ b/LICENSES.toml @@ -16,7 +16,7 @@ sha256 = "cd7032ea33b71427f188e34ffb4aee9d6868a57fd196037c5c8b2be03a2a0352" [[document]] source = "NOTICE" destination = "NOTICE" -sha256 = "9f854f730719af962cd840529324d089e05e68a30bd245c50e33f96e009b4314" +sha256 = "d9ba162ad2bc95d22ab9885065e253bb823bd6795068828e7b39858344b660eb" [[document]] source = "assets/app-icon/LICENSE-document.txt" @@ -60,6 +60,13 @@ destination = "licenses/CMap-LICENSE.txt" sha256 = "742665db9c8e1bc72603c6d319ca3e90b83bd6d95202f0cd4ef11068a07a9c29" origin = "https://static.crates.io/crates/hayro-cmap/hayro-cmap-0.1.0.crate#assets/LICENSE.txt" +[[document]] +source = "licenses/ICU4X-LICENSE.txt" +licences = ["Unicode-3.0"] +destination = "licenses/ICU4X-LICENSE.txt" +sha256 = "f367c1b8e1aa262435251e442901da4607b4650e0e63a026f5044473ecfb90f2" +origin = "https://static.crates.io/crates/icu_segmenter_data/icu_segmenter_data-2.3.0.crate#LICENSE" + [[document]] source = "licenses/egui-Hack-Regular.txt" licences = ["MIT", "Bitstream-Vera"] @@ -164,3 +171,9 @@ crate = "hayro-interpret" path = "src/font/generated/metrics.rs" licence = "MIT OR Apache-2.0" documents = ["vendor/hayro-interpret/LICENSE-MIT", "vendor/hayro-interpret/LICENSE-APACHE"] + +[[bundled]] +crate = "icu_segmenter_data" +path = "data/segmenter_dictionary_extended_v1.rs.data" +licence = "Unicode-3.0" +documents = ["licenses/ICU4X-LICENSE.txt"] diff --git a/NOTICE b/NOTICE index e8f31ade2f..eed2d654b0 100644 --- a/NOTICE +++ b/NOTICE @@ -36,6 +36,9 @@ list (author, source, license) is in ATTRIBUTION.md. - egui default fonts (epaint_default_fonts, via eframe) — Ubuntu Light (Ubuntu Font Licence 1.0), Hack (MIT / Bitstream Vera licence), Noto Emoji (SIL OFL 1.1), emoji-icon-font (MIT). - Colour profile in hayro-interpret — CGATS001Compat-v2-micro.icc (CC0-1.0, Clinton Ingram). +- ICU4X line-breaking data (icu_segmenter_data, via crates.io) — Unicode-3.0. Its Thai, Lao, Khmer + and Burmese word dictionaries are compiled in for wrapping Thai text; notice below, full licence in + licenses/ICU4X-LICENSE.txt. Required notices: @@ -48,6 +51,32 @@ Required notices: (https://github.com/adobe-type-tools/cmap-resources/blob/master/LICENSE.txt). Neither the name of Adobe nor the names of its contributors are used to endorse or promote this product. + ICU4X segmenter data (bundled by icu_segmenter_data 2.3.0; word lists, not visual assets): + Copyright © 2020-2024 Unicode, Inc. Portions adapted from ICU4C and/or ICU4J: + ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. + + Permission is hereby granted, free of charge, to any person obtaining a copy of data files and any + associated documentation (the "Data Files") or software and any associated documentation (the + "Software") to deal in the Data Files or Software without restriction, including without + limitation the rights to use, copy, modify, merge, publish, distribute, and/or sell copies of the + Data Files or Software, and to permit persons to whom the Data Files or Software are furnished to + do so, provided that either (a) this copyright and permission notice appear with all copies of + the Data Files or Software, or (b) this copyright and permission notice appear in associated + Documentation. + + THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. + + IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, + OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF + USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA FILES OR SOFTWARE. + + Except as contained in this notice, the name of a copyright holder shall not be used in + advertising or otherwise to promote the sale, use or other dealings in these Data Files or + Software without prior written authorization of the copyright holder. + Showcase PDF (built by `cargo xtask demo-pdf`, not committed): fonts are downloaded at build time from google/fonts at a pinned commit, verified by SHA-256, all under the SIL Open Font License 1.1 — see the "Downloaded at build time" table in ATTRIBUTION.md. diff --git a/README.md b/README.md index 3c1b67936a..f488d160cc 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ Real-world typography: world scripts, vertical Japanese, colour emoji, gradients ### Fearless -Every save appends your changes and leaves the original bytes untouched. Writes are atomic, undo runs deep, and nothing is lost if you close by mistake. +Ordinary saves append your changes and leave the original bytes untouched. Edits to damaged objects whose xref and header generations disagree use a full rewrite, keeping both accepted references connected to the same repaired object. Writes are atomic, undo runs deep, and nothing is lost if you close by mistake. @@ -253,6 +253,18 @@ Documents restricted by their author show a clear notice, and PdfCraft honours t +## Scan pages, then read the words back before trusting them + +**Scan & OCR ▸ Recognize text** turns a scan into searchable text — the current page, a range, or a folder of files written as searchable copies — with optional cleanup before reading (auto-rotate, deskew, denoise, binarize; word boxes map back to the original scan) and a second engine: an installed Tesseract joins the built-in one, merged per your strategy. A searchable result applies as one undoable step; the **Editable text** output opens the **OCR Verify screen** instead, where each word carries the engine's confidence as a colour, suspects are one keypress away, and corrections, a user dictionary and Re-OCR of a region or a page happen before Accept writes the new document. **Scan & OCR ▸ Correct recognized text** reviews any recognition there before it is applied. + +

+ The OCR Verify screen: a scanned note with each recognized word boxed in its confidence colour, the info strip reporting a 95% average confidence and one low-confidence word, and the confidence legend with its thresholds below +
+ The OCR Verify screen: words coloured by confidence — the legend spells the thresholds out — reviewed and corrected before anything is applied. +

+ +--- + ## Every tool, one keystroke away Press ⌘K to search every tool and command, or browse the **All tools** catalogue. Tools that are still in development are marked with the milestone that will ship them. diff --git a/ROADMAP.md b/ROADMAP.md index 00dd8fdee0..ee91096427 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,8 +1,16 @@ # PdfCraft roadmap +> **Last reviewed:** 2026-11-08 | **Last updated:** 2026-11-08 | **Change:** patch (M10 #6514: refresh craft-print to 7ee35a9f, PDF loader to 51934db3 and public GDI kept at 64e7afd4 after #605 merged; preserve crash fix and stream-test repairs; new-graph CI pending; estimates unchanged) + +> **Last reviewed:** 2026-11-07 | **Last updated:** 2026-11-07 | **Change:** patch (M10: file-backed Windows PDF loading without whole-job buffering or per-chunk async waits; approved dependency chain retains #5006; physical output still unqualified; estimates unchanged) + +> **Last reviewed:** 2026-11-05 | **Last updated:** 2026-11-05 | **Change:** patch (M7 #2964: directly edit Thai source text inside supported Form XObjects; estimates unchanged) + > **Last reviewed:** 2026-11-03 | **Last updated:** 2026-11-03 | **Change:** patch (M5 #5850 review: indirect typewriter intent and composer edits preserve outer whitespace; movement regression commits by outside click; estimates unchanged) > **Last reviewed:** 2026-11-03 | **Last updated:** 2026-11-03 | **Change:** patch (M5 #5850: Enter adds Fill & Sign line breaks; outside clicks commit, Escape cancels, and saved multiline appearances retain their text and height; estimates unchanged) +> **Last reviewed:** 2026-10-31 | **Last updated:** 2026-10-31 | **Change:** minor (M3/M4, #5330: Crop pages is directly available in the document quick toolbar; estimates unchanged) + > **Last reviewed:** 2026-11-01 | **Last updated:** 2026-11-01 | **Change:** patch (M7, PR #2155 further review: inherited drawing/text state, affine image gestures, paste/selection ownership, no-op scripts and menu permissions; estimates unchanged) @@ -21,10 +29,17 @@ > **Last reviewed:** 2026-10-23 · **Last updated:** 2026-10-23 · **Change:** minor (M2, PR #4172: extend on-screen CJK fallback to Combine thumbnails and figure previews; recognise FangSong and subset-prefixed Founder names; estimates unchanged) > **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-15 · **Change:** minor (typed small Edit font sizes and Fill & Sign text/date colour, #2444/#2496; estimates unchanged) +> **Last reviewed:** 2026-10-20 · **Last updated:** 2026-10-20 · **Change:** patch (M1, #1143: repaired aliases survive preflight, snapshots and normalized saves; retained materialized revisions report generation disagreements; estimates unchanged) + +> **Last reviewed:** 2026-10-18 · **Last updated:** 2026-10-18 · **Change:** minor (M1, #1143: explicit live xref/header generation repair policy and bounded diagnostics; estimates unchanged) + +> **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-15 · **Change:** minor (M1: refresh object-generation reference identity against current upstream; estimates unchanged) **Stage: alpha** · next: beta, ~25% (ready for real work 50% → 75%) and ~400–700 h away -> **Last reviewed:** 2026-10-29 | **Last updated:** 2026-10-29 | **Change:** minor (M7 #2517: a narrowed then widened paragraph keeps the typed spaces the wrap consumed, after save and reopen; estimates unchanged) +> **Last reviewed:** 2026-10-29 | **Last updated:** 2026-10-29 | **Change:** minor (M2: scans keep their strokes at screen size and are smoothed when zoomed; M7 #2517: a narrowed then widened paragraph keeps the typed spaces the wrap consumed, after save and reopen; estimates unchanged) +> **Last reviewed:** 2026-10-28 | **Last updated:** 2026-10-28 | **Change:** minor (M10, #4119 partial: Enhance scanned file cleans scanned pages in place: background removal, edge shadow removal, despeckle; estimates unchanged) > **Last reviewed:** 2026-10-27 | **Last updated:** 2026-10-27 | **Change:** minor (M10: OCR model downloads use verified ONNX pairs with legacy discovery fallback; estimates unchanged) +> **Last reviewed:** 2026-10-25 | **Last updated:** 2026-10-25 | **Change:** minor (web: same-document host mode on the canvas, `data-host="page"`; estimates unchanged) > **Last reviewed:** 2026-10-25 | **Last updated:** 2026-10-25 | **Change:** minor (M14 localization current state: Arabic logical catalog, explicit RTL label direction and mirrored stock chrome; native/browser RTL workflows unqualified. Native release Hans face via LXGW WenKai Lite; estimates unchanged) > **Last reviewed:** 2026-10-23 · **Last updated:** 2026-10-23 · **Change:** minor (M7, #2636 partial: pasting added text boxes and images in the engine and through `content_copy`; estimates unchanged) > **Last reviewed:** 2026-10-23 · **Last updated:** 2026-10-23 · **Change:** minor (M11: image optimization accounts for page-local UserUnit; estimates unchanged) @@ -183,27 +198,28 @@ What's strong and missing in each: [target-app-parity.md](docs/target-app-parity ## Languages -Twelve key languages (historical % snapshot vs 2,143 keys, except Vietnamese, Brazilian -Portuguese and Spanish vs the current union). Detail and script notes: [localization-parity.md](docs/localization-parity.md). +Twelve key languages: the share of the 2,897 strings in the i18n report's denominator (the interface's strings plus every catalog key), from +`cargo xtask i18n-report` on 2026-11-03. Detail and script notes: [localization-parity.md](docs/localization-parity.md). | Language | Code | Strings | Status | |---|---|---|---| | English | `en` | 100% | full | -| Simplified Chinese | `zh-hans` | 97.4% | partial (native LXGW WenKai Lite; no Hans face in the web build, #826) | -| Spanish | `es` | 100% of current union | partial (dialogs / tooltips; no help) | -| Hindi | `hi` | 0% | none | -| Arabic | `ar` | 100% | partial (logical catalog, explicit RTL labels, mirrored stock chrome; native/browser/IME workflows unqualified) | -| French | `fr` | 99.3% | partial | -| Portuguese (Brazil) | `pt-br` | 100% of current union | partial | -| Indonesian | `id` | 0% | none | -| Japanese | `ja` | 99.1% | partial | -| German | `de` | 99.3% | partial | -| Korean | `ko` | 0% | none | -| Vietnamese | `vi` | 100% of current union | partial (dialogs / tooltips; no help) | +| Simplified Chinese | `zh-hans` | 88.4% | partial (native LXGW WenKai Lite; no Hans face in the web build, #826) | +| Spanish | `es` | 98.4% | partial (dialogs / tooltips; no help) | +| Hindi | `hi` | 97.5% | partial (Devanagari rendering unqualified) | +| Arabic | `ar` | 97.9% | partial (logical catalog, explicit RTL labels, mirrored stock chrome; native/browser/IME workflows unqualified) | +| French | `fr` | 98.4% | partial | +| Portuguese (Brazil) | `pt-br` | 98.4% | partial | +| Indonesian | `id` | 97.5% | partial | +| Japanese | `ja` | 89.6% | partial | +| German | `de` | 89.7% | partial | +| Korean | `ko` | 97.5% | partial (no Hangul UI face) | +| Vietnamese | `vi` | 98.4% | partial (dialogs / tooltips; no help) | Also shipped: Traditional Chinese, Russian, Bulgarian, Telugu, Hungarian, Ukrainian, Italian, -Kazakh, Polish, Slovenian, Greek, Turkish, Lithuanian, Romanian and Czech: **15 more catalogs** -(24 interface languages including English). +Kazakh, Polish, Slovenian, Greek, Turkish, Lithuanian, Finnish, Albanian, Catalan, Romanian, +Norwegian Bokmål, Persian, Central Kurdish, Thai, Lao and Czech: **23 more catalogs** +(35 interface languages including English). ## Upcoming @@ -222,6 +238,11 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md | 8 | 1.0 polish: performance, right-to-left, missing languages, keyboard-only (M14) | 55–95 | ## Progress log +- **2026-10-13 (M8, redact by regex):** `redact_mark` takes `regex` (caller expression, linear-time engine, length/size/nesting caps, empty-matching refused) with optional `validate` (luhn, mod97, iban). Headless only; issue #1886. + +- **2026-11-07 (M7, #2964 / #3185):** Supported Form text can be prepared for direct editing; groups left grouped (unsafe compositing, text nested past eight levels) are reported. Legacy Thai keeps the original font programs, Regular/Bold and text settings, also across same-face subsets; the narrow THSarabunPSK map repair is checked glyph by glyph against the font program; cross-family replacement is explicit. Builds on #3067 and #2811. Unsafe groups and mixed contextual styles remain partial; estimates unchanged. +- **2026-10-28 (M10, #4119 partial):** Scan & OCR ▸ Enhance scanned file cleans scanned pages in place: background removal (level 0–100, as the camera clean-up), edge shadow removal (book gutters, copier borders) and despeckle. Each scan image is decoded, cleaned and written back into its own object at the same size and colour space (JPEG stays JPEG, others lossless, 1-bit stays 1-bit), so the OCR text layer, comments and links stay; one undo step, shared images once, unsupported images (CMYK, indexed, JPEG 2000, JBIG2, CCITT, broken data) reported. `page_enhance_scan` drives it headlessly. Not yet an option of Recognize text. Estimates unchanged. +- **2026-11-07 (M10, `ocr.ocr-web`, port onto #977):** Web OCR rebuilt on main's engines and preprocessing (#977). `OcrJob::run` is now `OcrJob::steps` a page at a time: the page renders, crops to the region and cleans each page, a Web Worker runs ocrs on the result, and `OcrShot::place` maps the words back through the region and preprocessing. Every strategy reads like this when only one engine is present, and the web build has only ocrs. The downloaded models are installed into the engine cache (`ocr::install`), so the Verify screen and the engine-status checks see them too. The batch keeps main's skipped-files bucket, low-confidence notes and cancel. The translations moved to craft-i18n (storytold/craft-i18n#141). The Verify screen starts the model download itself and runs once the models land; before, on a fresh page it failed with the desktop's "set PDFCRAFT_MODELS" message. Known limits: the cleaning steps, and the Verify screen's Run OCR and re-reads, run on the page. Estimates unchanged. - **2026-11-03 (M11, shared JBIG2 dictionary):** With Monochrome Images set to JBIG2, Optimize has up to 16 JBIG2 images share one symbol dictionary (`/JBIG2Globals`), the letters found on more than one page stored once, when that is smaller (`pdfcraft_filters::encode_jbig2_shared`). Eight pages of 10 pt text rendered at 300 dpi take 41% less in Helvetica and 42% in Times than coded alone, 10% in Courier; groups of 16 save 45%, of 32 only 47%. Only images redaction can store again without the dictionary share one (image masks and DeviceGray named directly, at most 64 Mi pixels), and redacting one stores the rest of its group so, which the group bounds to 15. @@ -229,12 +250,16 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md - **2026-11-03 (M5, #5850):** Fill & Sign Enter inserts a line break in a growing editor; clicking outside commits and Escape cancels. Nonempty text keeps leading, internal and trailing line breaks. The headless fill_sign_add path sizes its typewriter box for the same wrapped lines so saved appearances show every row. Production-order keyboard and engine save/reopen/render regressions exercise the UI and headless callers. Estimates unchanged. +- **2026-11-03 (community, M7, Thai word breaking):** Thai rows end between words, found with ICU4X's dictionary line segmenter (`icu_segmenter` 2.3, compiled dictionary data, no LSTM model; Unicode-3.0), instead of only at spaces when rewrapping and at any cluster in Add text. A row never starts with a following vowel, tone or other mark, Mai Yamok or Paiyannoi, nor ends with a leading vowel; a word wider than the row still breaks between clusters, and the spaces where an Add text row ends are dropped as in Latin text. Rewrapping existing text works out the breaks once per run of non-space characters. Rewriting a paragraph into Thai still needs a source font with the Thai glyphs (there is no Thai fallback face for it yet). The compiled dictionaries (Thai, Lao, Khmer, Burmese) add about 1.8 MB to a stripped release binary. Estimates unchanged. - **2026-11-03 (M7, #5835):** In image crop mode, a right-click on the image, its crop box or its handles keeps the crop and opens no menu, so the menu no longer switches as the pointer moves. A right-click elsewhere clears the selection and opens the page menu. UI tests cover all four page rotations, overlapping images, a 45° image, and a check that the Done/Cancel/Remove Crop pills at a page edge still keep the crop. -- **2026-11-02 (M12, #1297):** `model_add` embeds a U3D or PRC file, recognised by its header, as a 3D annotation with a chosen activation and a poster (an image or PDF page fitted in the box, or a labelled frame); an older file's catalog `/Version` is raised to what the model needs. Views, a rendered poster and a UI command are later steps. Estimates unchanged. +- **2026-11-03 (M14 docs, #5805):** `docs/localization-parity.md` and the Languages table here count every catalog with `cargo xtask i18n-report`: the share of the 2,897 strings in its denominator, on `14ae839a`. The catalog-union measure it replaces had grown to 2,895 keys and left four catalogs at "100%" that were 44 keys short. Hindi, Indonesian and Korean are listed with their catalogs (97.5% each) instead of 0, and Thai, Lao and Central Kurdish join the table. The ≈ 68% localization score is not re-scored. - **2026-11-02 (M10, #5743):** macOS print jobs name their title and owner (`com.apple.print.JobInfo.PMJobName`, `PMJobOwner`), as the system print dialog does, because a Kyocera filter crashes on a job without them. Checked by a test and through `doc_print` against a stand-in `lp`; not yet on a physical printer. +- **2026-11-02 (M3, #5708):** Pages shorter than the window sit in its middle, one page or row at a time and when scrolling continuously with every page in view. +- **2026-11-02 (M14, #5737, #5460):** Preferences can turn off the update check at start. It stays on by default, and Help ▸ Check for updates still asks. + - **2026-11-01 (M7, #5657):** Header/footer, watermark and background additions and replacements stage the complete call before committing. Synthetic later-page failures preserve exact incremental bytes, object count, dirty state and allocation cursor; success controls retain other marks and shared/inherited resources. Estimates unchanged. - **2026-11-01 (M14, #5572):** Preferences ▸ Documents and view sets how many recent files to keep, from 0 to 50 with 12 by default. At 0 Home has no Recent section and File ▸ Open Recent is disabled. The list is cut when Preferences closes, so the number can pass through 0 on its way to another. @@ -246,6 +271,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md - 2026-11-01: PR #2155 review follow-up preserves editor crops through group moves and keeps arranged artwork outside added-item streams; unsafe nested-tag extraction is refused. Image handles and flips follow page orientation, added images accept general transforms, and clipboard/text/Home/font-discovery edge cases have focused regressions. Candidate only; estimates unchanged. - 2026-11-01: PR #2155 maintainer edits remove redundant image/text helpers and add regressions for crop ownership, arrange scope/state fidelity, clipboard precedence, crop-drag bounds, and font scanning. Transform and Unicode-script tools complete the headless paths. +- **2026-10-31 (M3/M4, #5330):** The document quick toolbar offers Crop pages directly through the existing page.crop command, with the command’s assembly-permission check and an active-tool highlight. The existing crop, undo and headless page_set_box paths are unchanged. No toolbar customization or new shortcut. Estimates unchanged. - **2026-10-18 (M3, #1354, local candidate):** Integrated --two-paged with current main, retaining upstream modal/picker ownership and single-tab reuse. Native launch/opening/password regressions, negative checks against the old wrapper, visual inspection, formatting, strict scoped Clippy, layers, assets, parity and scoped WASM compilation pass. Three existing socket tests are unavailable in this workspace; one existing network test remains ignored. No push or GitHub merge; estimates unchanged. - **2026-10-31 (M2, PR #4172 review):** Added same-machine pixel oracles for GBK Song, Hei, Kai and FangSong, including all four subset-prefixed Founder aliases. Reference PDFs use explicit Unicode mappings and independently selected installed faces; font data and rasters remain in memory. All eight cases ran; wrong-glyph and wrong-family production mutations each failed the new assertion and were restored. Validation: 422 renderer unit tests, renderer all-target Clippy with -D warnings, formatting, layers/assets/parity and renderer WASM checks passed; existing optional-patch/WASM warnings and parity notes remain. Estimates unchanged. @@ -256,15 +282,20 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md - **2026-10-27 (M2, PR #4172 module-split conflicts):** Integrated main 1dd2737b, retaining viewer-only CJK fallback in split session modules and Combine thumbnail workers, and moving renderer regressions to the new test module. Validation: 798 scoped tests passed (371 engine, 412 renderer, 15 Combine; two existing engine tests ignored); scoped all-target Clippy with -D warnings, formatting, layers, assets, parity and renderer/engine WASM checks passed. Estimates unchanged. +- **2026-10-27 (M10, `ocr.ocr-web`, rebase fix):** The web build loads OCR models only when their SHA-256 matches `ATTRIBUTION.toml`. With ONNX loading now on, rten's parser spins forever on some bytes that aren't a model (a corrupt or wrong download), so a bad file now fails at once instead of hanging a worker. - **2026-10-27 (M10, OCR models):** Model staging downloads and verifies the ONNX pair after the upstream RTEN URL brownout; runtime discovery prefers complete ONNX pairs per directory, retains complete RTEN fallback and preserves directory precedence. - **2026-10-26 (M2, PR #4172):** Reconciled current main, preserving its Japanese serif/weight selection and both branches' progress records. Installed CJK viewer and preview fallback remains display-only; estimates unchanged. - **2026-10-29 (M7, #2517):** Widening a paragraph rejoins a wrap with the number of spaces that wrap consumed, including a typed run and none for a Chinese or Japanese word split. The count survives an incremental save and a full rewrite, and a width-only `text_edit` uses it. Estimates unchanged. +- **2026-10-29 (M2):** Scanned pages look like the paper again. An image that doesn't ask for /Interpolate was shrunk by nearest-neighbour (since #635), which kept one sample in N per axis, so the text of a 300 dpi scan at 100% lost most of its strokes; it is now area-averaged to whole device pixels (on a 13-page 300 dpi scan every sample at 96–192 dpi is within 1 of the exact area average; it was up to 255 off). Enlarged, an image of page-scan size is smoothed bilinearly, so a zoomed scan has no staircases; icons, QR codes and pixel art keep hard pixels. Estimates unchanged. - **2026-10-26 (M11, JBIG2 symbols):** Lossless JBIG2 now stores each distinct mark of a black-and-white image once, as a symbol, and places its exact copies, when that is smaller than the generic region: on six pages of rendered 300 dpi text 28% of the generic region's size and 17% of Group 4's, and the same bits back through two independent decoders (hayro-jbig2 and Poppler). Scans, whose letters are seldom identical, gain little until refinement coding; a dictionary shared between pages (`/JBIG2Globals`) is next. Estimates unchanged. - **2026-10-25 (M5, comment replies):** One reply can be deleted from the Comments panel without its thread. On the selected card each reply has its own "…" menu (Edit, Copy text, Delete); Delete takes that reply with its pop-up and the replies to it, as `comment_delete` already did headlessly, and the comment and its other replies stay. The card stays selected, and the panel's editor stays open, on what they were on though the delete moved it in `/Annots`. Undo and Redo close the editor, since either can move what it was open on. A locked reply can't be deleted. Estimates unchanged. +- **2026-10-25 (web, same-document host mode):** A page that loads the PdfCraft web module into its own document (no iframe) can opt into the host bridge with `data-host="page"` on the app's canvas — the exact value `page`; any other value keeps the normal app with downloads, unless a same-origin framed `?host=parent` selects the parent bridge (unframed or cross-origin, `?host=parent` also keeps the normal app). It is the `?host=parent` bridge with the transport swapped: the same five messages travel as non-bubbling `CustomEvent`s on the canvas (`detail` holds the parent messages' fields without `type`; the host answers with a `pdfcraft:open` event), so saves reach the host instead of downloading and dirty and start-up-failure reporting are unchanged. Unit tests cover the opt-in rule; no parity or estimate changes. - **2026-10-24 (M10, #4158 partial):** Word, HTML, RTF and PowerPoint export keep a block that continues another on its line (an italic case name, a link, a superscript) as a run in its own style inside that paragraph, with the source's word space, instead of a paragraph of its own. Style changes that wrap onto the next line still split paragraphs (the reading blocks keep style boundaries). Estimates unchanged. - **2026-10-24 (M3, window move on Wayland, #3970 follow-up):** After a move or resize by PdfCraft's own title bar or edges, the button release given back to egui goes before the pointer leaving the window, so hover no longer sticks at the old place until the pointer moves. Found by CodeRabbit in review of #3970; a unit test covers it. Estimates unchanged. +- **2026-10-26 (M10, `ocr.ocr-web`, review fix):** On the web, Edit on a scanned page starts the OCR model download, and the page is read once the models load; it was taken for no scan and left unread until the document changed. +- **2026-10-24 (M10, `ocr.ocr-web`, review fix):** On the web, single-file and multiple-file OCR running at once no longer lose a page: the workers' answers go to the run that sent each page, instead of to whichever run collected first, and a cancelled run's late answers are let go. Regression tests cover both start orders and cancellation. Files chosen for Recognize Text in Multiple Files before the models have downloaded now wait for them instead of having to be chosen again, and idle workers close on time even when nothing is happening on the page, a full minute after their last page comes back rather than after it was handed out (checked by hand in Firefox); estimates unchanged. - **2026-10-24 (M14, #4253):** The annotation quick bar remembers Keep tool selected across restarts, including unpinning. Old and malformed settings keep safe defaults; the opt-in UI control channel reads and changes the same preference. Estimates unchanged. - **2026-10-22 (M7, #2517):** A partial rewrite of the last paragraph closes its marker after that paragraph's last `ET`, so a row left in the source stays in the paragraph. When the next paragraph shares the text object, the marker closes before that neighbour instead of after the shared `ET`. The extra marker is not added across two marked-content regions. Estimates unchanged. - **2026-10-15 (M7, #2517):** A blank line between paragraphs stays a paragraph break. An explicit `TL` is the measured baseline gap. Without one, three or more rows at a repeated step stay one paragraph, and a single wider gap stays a break. Adding a row, even when an earlier row is left in the source, does not absorb the next paragraph on a later edit. Estimates unchanged. @@ -291,6 +322,11 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md Estimates unchanged. - **2026-10-21 (M3, window resize and move on Wayland):** With PdfCraft's own title bar on GNOME/Wayland, a second resize by a window edge, or a second move by the empty tab strip, works straight after the first; it waited for a click inside the window, because the compositor keeps the button's release from egui. A unit test covers the release given back; checked by hand in a test build. Estimates unchanged. - **2026-10-24 (M5, move preview):** A comment being moved is drawn where it goes while it is dragged (a rectangle or oval showed only its blue frame): the comment is rendered alone once as the drag starts and laid over the page, which shows it where it was until the move is applied. Engine `Document::comment_layer`; synthetic engine and UI regressions. Estimates unchanged. +- **2026-10-24 (M5, border shape):** Text boxes and Fill & Sign's typed text take an oval or a circle for their border (Comment Properties ▸ Border shape, Fill & Sign's toolbar): the text box keeps its place and the shape goes around it, the text centred, a circle snug around the text (its box no wider than the text needs); stored as /RD and /PCBorderShape and drawn in the appearance. comment_add / comment_edit / fill_sign_add `border_shape`. One new label in every catalog. Synthetic annot, UI and automation regressions. Estimates unchanged. +- **2026-10-24 (M5, Escape in Fill & Sign):** Escape leaves a Fill & Sign tool as it leaves a comment tool: the first press closes the open type box, the next goes back to Select, so the Text tool no longer opens a box at every click with no way out by the keyboard. A synthetic UI regression covers the Text and Checkmark tools. Estimates unchanged. +- **2026-10-23 (M5, ortho):** Shift while drawing holds a line or arrow level or plumb, whichever is nearer (a CAD drawing's ortho mode), and each new side of connected lines, a polygon or a cloud from the corner before; dragging an end or corner with Shift is held the same way (it turned to 45° steps). `measure::snap::ortho`; `comment_add ortho`. Estimates unchanged. +- **2026-10-24 (M3, arrow keys on a wide page):** → / ← scroll a page wider than the window (a plan zoomed in) sideways a line at a time and turn the page at its edge, as ↓ / ↑ do in single-page view; ⌘→ / ⌘← turn it at once, and a page that fits across turns as before (#185). A synthetic viewer regression covers both. Estimates unchanged. +- **2026-10-24 (M5, part opacity):** Comment Properties sets a text box's text, border and fill, and a shape's line and fill, each at an opacity of its own (a see-through fill with an opaque line on a plan); stored as /CA, /ca (PDF 2.0) and /PCTextOpacity and drawn with separate graphics states, so other viewers show it. Make Properties Default and the settings carry it; comment_add / comment_edit `fill_opacity`, `text_opacity`. Four new labels in every catalog. Synthetic annot, UI and automation regressions. Estimates unchanged. - **2026-10-22 (M4, #3916 partial):** Document edits and Undo/Redo cancel an Organize page drag. Pointer-release regressions check exact page order and save/reopen. - **2026-10-22 (M10, #3752 paper):** CUPS jobs ask for the chosen paper in every size. The 13 sizes the Print dialog sent no paper for (A6, A0–A2, ARCH, ANSI C–E) use PWG 5101.1 names, checked against a synthetic PPD with `cupsfilter`, and `doc_print` to a printer sends the paper too. The macOS tray half (Auto Select instead of the PPD's default tray) is a craft-print change that needs a pin bump here. Estimates unchanged. @@ -298,6 +334,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md - **2026-10-22 (M14, macOS association recovery candidate):** Successful desktop open/save/sign can queue recovery of an existing per-file binding to the running PDFCraft bundle through NSWorkspace. Metadata-only watcher changes retain undo and avoid false overwrite prompts; real same-length/same-mtime edits remain protected. Rebased on 0.12.0 and reuses #3042's shared atomic metadata preservation; the bounded recovery parser does not narrow save limits. Prior native Preview TEST-signature and Finder workflows passed on macOS 27.0.1; other versions remain unqualified. Estimates unchanged. - **2026-10-13 (M7, free image rotation):** A selected page image has a rotate knob (Shift: 15 degree steps) and `image_edit` a `rotate_by` action; the angle is written as one undoable `cm`. Snapping to page edges while moving is in #1740; resize snapping is not done. Estimates unchanged. +- **2026-10-20 (M9 #79 review, M2, M8):** text extraction reads a marked-content /ActualText that marks no glyph where it paints (paths and images, cut to what is visible, along the direction they are drawn), so words drawn as outlines can be found, copied and exported; OCR and scan field detection still count such a page as having no text of its own. Redaction strips /ActualText, /Alt, /E and /T from inline marked content around paint it clips, as it already did around paint it removed, so a partly redacted outlined word leaves no name in the file. Named-layer recognition also needs the layer form to draw n1 and n3. Estimates unchanged. - **2026-10-20 (M4, initial view):** Window and interface options read indirect boolean ViewerPreferences values. Synthetic regressions cover true/false, direct/indirect preference dictionaries and save/reopen; malformed entries retain false defaults. Estimates unchanged. - **2026-10-20 (M3, #3535):** Escape with a menu open closes the menu and keeps the text selection; the next Escape clears it, as before. One keypress used to do both. A synthetic UI regression checks it. Estimates unchanged. - **2026-10-16 (M3, close prompt keyboard):** The "Save changes?" prompt opens with Save chosen (Enter saves, as before) and the arrow keys and Tab / Shift+Tab move the choice to Cancel or Don't save, shown with a focus ring; Enter or Space press the chosen button and Escape still cancels. Before, Enter always saved whatever was Tab-focused. Estimates unchanged. @@ -312,6 +349,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md - **2026-10-20 (M11, #3507):** Image merging keeps trailing comments, split operands and graphics state across content streams. Synthetic tool tests check saved text, rendered regions and exact image rows. - **2026-10-17 (M8, gap 6, #2662):** A filled cover over a long line counts when it hides half of any one word, not only 40% of the line's words or box, so a black box over one number in a long line — or an unapplied redaction mark over it — no longer audits clean while the number is still extractable; a sliver under half of every word it touches and under 40% of the line is still not flagged +- **2026-10-13 (M10, OCR review, batch and two engines):** Scan & OCR gains a review step, a second engine and Unicode output. Recognition can clean the raster first (opt-in auto-rotate, deskew, denoise, binarize; word boxes map back to the scan) and read with the built-in ocrs engine, an installed tesseract program run as a fenced external process (a validated location that fails closed instead of falling back to the PATH, allow-listed languages, a private temp directory, timeout and output caps), or both: Automatic asks tesseract when the ocrs reading is weak or of unknown confidence, and a ROVER merge keeps the more confident word (an unscored ocrs word yields only to a tesseract word of 70 or more). The OCR Verify screen (Scan & OCR ▸ Correct recognized text) reviews a recognition before it is applied: confidence-coloured word boxes, corrections, a per-language user dictionary (a plain-text file in the preferences folder, so a word added from any document stays on disk after it closes), Re-OCR of a page or a region, Accept/Reject per page or whole. Pages accepted one by one each apply once; a region re-read moves the review marks of the words it keeps, and a whole-page re-read clears that page's marks (undoable in the panel). Output is searchable (an invisible layer, one undo step) or a NEW editable document of visible text. Words beyond WinAnsi are drawn in Type3 faces from craft-fonts with a ToUnicode map in the editable output (characters no bundled face has appear as '?'); the searchable layer writes them as Type3 fonts with empty character programs and a ToUnicode map, so extraction reads them back with no fonts and no substitutions; redaction removes that layer like any other text (tests in pdfcraft-redact; the end-to-end engine test needs the craft-fonts build input and is skipped without it). Batch runs skip pages that already have text unless forced, and skip whole files that have text only with the opt-in skip-files flag (off by default; Force OCR overrides both); they refuse two inputs of the same file name and existing outputs before reading anything, and report low-confidence pages at the settings' threshold. Headless: ocr_recognize (engine, strategy, preprocessing, region; the editable output goes to an `out` that must not exist yet, and `out` is refused for a searchable run), ocr_words, ocr_review_apply, ocr_dictionary, ocr_status and ocr_recognize_files. All 21 interface catalogs carry the new strings; the Lithuanian ones were added without native review. Reading with every cleaning step off passes the render through untouched, as on main (the 300 dpi normalization runs only when a cleaning step is on: enlarging a 72 dpi render 4x on its own made the models read "due: €250" as "ue: ?250"). Still missing: the deep scan clean-up options (descreen, background and edge-shadow removal), scan images in the editable output, matching the scanned typeface, and recognition quality beyond the tested English set. - **2026-10-13 (M10, #1757):** Office export keeps each dotted-leader contents entry in one paragraph, with its page number, instead of inferring table cells from its separate text objects. - **2026-10-22 (M6, calculation scripts):** The Calculate tab and a rename read simplified field notation only from a comment, where Acrobat writes it: a string that spells `BVCALC … EVCALC` is no longer taken for the notation, or rewritten by a rename. A `/` after the condition of `if`, `while`, `for` or `with` starts a regular expression, so a rename no longer rewrites a name inside one, and a quote inside one no longer hides the `getField` calls after it. Both found in review of #3550. A `//` comment ends at `\r`, U+2028 or U+2029 as well as `\n`, so in a script with `\r` line endings the code after one is no longer hidden from a rename, or taken for the notation. Estimates unchanged. - **2026-10-20 (M6, calculation order page):** Prepare a form ▸ Calculation order, a page of its own, lists the calculated fields in the order they calculate with their formulas and values, moved with Up/Down arrows (as Acrobat's Set Field Calculation Order), Alt+↑/↓ or by dragging. A card says what is wrong with the order and holds Fix order; a loop has a card of its own that opens one of its formulas. On the page, each calculated field shows its step, and arrows show what the field in focus reads and what reads it. Estimates unchanged. @@ -322,6 +360,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md - **2026-10-20 (M12, #3673):** Character encoding accepts Identity-H/V fonts with Adobe CJK collections and recognizes predefined non-Identity CMaps independently. Unknown CMaps and foreign registries remain refused; synthetic checker and read-only automation regressions passed after failing before the fix. Per-glyph mapping validation and embedded-font extraction remain separate. Estimates unchanged. - **2026-10-19 (M6, renames keep formulas):** Renaming a field renames it in every calculation that reads it, in the same undo step: `AFSimple_Calculate` field lists, simplified field notation (the comment and the JavaScript after it) and `getField` calls naming it in one string literal; the rest of each script is kept as written. A sum used to drop the renamed field, notation read it as 0 and a script kept a stale value. Field Properties says how many formulas changed and `form_set_props` lists them as `formulas_updated`. Names built at run time and other events' scripts are not rewritten. Estimates unchanged. - **2026-10-19 (M6, text field table):** Prepare a form ▸ Text field table drags out an area and adds rows and columns of named text fields as one undoable edit, laid out on the page as displayed, with a live preview, per-column name patterns and widths, and refusal before anything is added when a cell would be too small or a name taken; `form_add_field_table` does the same headlessly. `form.multiple-copies` is partial: copying an existing field into a grid is still missing. Estimates unchanged. +- **2026-10-17 (research):** Added a [PDF workflow research reference](docs/research/pdf-workflows/README.md): dated public-source locators, 20 workflow research domains, a pinned feature mapping and interaction evidence. Reference documentation; no feature statuses, milestone priorities or estimates changed. - **2026-10-16 (M7, #2516):** A paragraph number from `text_paragraphs` keeps naming the paragraph it listed after edits regroup the page: `text_edit`, `text_edit_layout` and `page_render`'s `hide_text_block` find it again by its text and box, and refuse one that was merged into a neighbour (the edited paragraph included) instead of rewriting the paragraph that took its number (`stale_paragraphs` lists the numbers gone). The inline editor's switch-paragraph click holds the same key. No stable identity across save and reopen yet (#2508). - **2026-10-19 (M7, #3591):** A background added to a page that already has one is painted over the old one instead of going under it and staying hidden. Behind the content, backgrounds come first, in the order added, and then behind-content watermarks, so a watermark added after a background no longer disappears. Synthetic regressions in `crates/edit` check the order. Estimates unchanged. @@ -333,9 +372,15 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md - **2026-10-20 (M3, #3541/#3620):** Comment hover follows the individual marked lines instead of the enclosing multiline rectangle, and overlapping comments follow paint order. Synthetic UI regressions cover all four text-markup kinds, page/view rotations, neighboring highlights, legacy rectangle fallback and unchanged PDF bytes. Estimates unchanged. - **2026-10-20 (M10, #3532 partial):** Vector printing retains existing widget artwork when its appearance lacks Subtype. A print-owned copy adds Form dispatch metadata. The source stream and all explicit subtypes stay unchanged. Synthetic source/output raster checks cover resources, selected/shared appearances, placement, flags and content modes. Appearance regeneration and physical printer validation remain outside this fix. Estimates unchanged. +- **2026-10-20 (M5, several comments):** Comment Properties opened on one of several selected comments, the context menu's Colour and the quick bar's colour change all of them as one undo step, each where the property applies (a font size to text boxes, a fill to shapes, a width to lines and shapes); one that stays locked keeps its look. Copied Fill & Sign ticks, crosses, dots and lines hang on the pointer as themselves rather than as empty boxes, and copied text boxes show their text (`FillMark::strokes` is shared by the appearance and the outline). Estimates unchanged. +- **2026-10-19 (M5, comments between documents):** Comments copy and paste, within and between documents: ⌘C with comments selected copies them as XFDF (each with its replies) to the app's and the system clipboard, ⌘V / Edit ▸ Paste comments / the page menu pastes them onto the current page as new comments under names of their own (`Edit::PasteComments`, `pdfcraft_xfdf::import_comments` with a target page and fresh names, so nothing is replaced and pasting twice gives two sets; `export_xfdf_comments` writes a selection with its replies). Several comments select at once (⌘-click, ⇧-click, on the page and in the panel); Delete removes them as one step and a drag moves them together. Connected lines, polygons and clouds finish from the right-click menu's Complete (Undo last point, Cancel drawing beside it), as other editors do. Tools `comments_copy` / `comments_paste`. Estimates unchanged. - **2026-10-20 (M5, arrow keys move comments):** With a comment selected, the arrow keys move it a screen pixel and ⌘/Ctrl or Shift and an arrow ten, instead of scrolling the view or turning the page; up is up on screen at any zoom and rotation, and each press is one undo step. Text markup and locked comments stay put. Estimates unchanged. +- **2026-10-20 (M8, #3647):** Apply prunes fully removed tiling patterns, then checks retained pattern resources for text, images and unreadable content before reporting success. Shared patterns, Form-owned and inherited patterns, and cells kept by another page cause an atomic refusal when safe removal cannot be shown. Synthetic saved-file and automation regressions cover refusal, preserved marks and undo, and vector-only controls. Estimates unchanged. + +- **2026-10-20 (M1, PR #1143 review):** Repair-aware dictionary/preflight edits and snapshot restoration preserve both accepted generations of a damaged live slot. Such edits use the existing full-save fallback; classic and packed rewrites coalesce aliases so page-tree parent links retain one object identity. Explicit generation replacement remains single-generation. Materialized xref diagnostics inspect retained primary and hybrid rows before folding, retaining the 256-object cap plus summary and discarding abandoned-index notes. Synthetic red/green controls cover preflight, undo restoration, equal values, explicit narrowing, encryption packing and four save variants. Estimates unchanged. + - **2026-10-19 (M14, #3373):** The main menu and its long submenus scroll in short windows, keeping their commands reachable. Estimates unchanged. - **2026-10-19 (M3, #3387):** Save-prompt shortcuts yield to later consent and utility dialogs; a synthetic UI regression checks that an edited document stays open. Estimates unchanged. @@ -380,6 +425,8 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md - **2026-10-16 (M10, #1201 partial):** Lossless image export applies each component's Decode pair. Indexed values round and clip before palette lookup. Color-key masks keep raw samples. The existing 16-bit precision limit remains. JPEG and mask Decode, ICC transforms, and calibrated colors remain outside this fix. Estimates unchanged. - **2026-10-16 (M3, #2739):** Wrapped revision buttons stay above Binding in Document Properties. A synthetic regression checks control bounds and opens a saved revision at two viewport widths. Estimates unchanged. - **2026-10-19 (M7, tracked text):** Editable paragraphs and object labels distinguish repeated glyph tracking from word gaps in TJ arrays and separately positioned glyphs. Synthetic regressions cover character spacing, horizontal scaling, rotated baselines, larger word gaps, and rows of one-glyph words or cells (inferred tracking is capped at half an em, so `a b c` and digit or Y/N columns keep their spaces). Estimates unchanged. +- **2026-10-16 (M9, #79):** a signature appearance that follows the named-layer convention (its /N draws one Form that holds and draws n1 and n3) shows the signature's real state: n1, n3 and n4 are redrawn in the display copy with the panel's icons and a caption in the interface language (Helvetica; else shaped in embedded Inter, or in a craft-fonts face for Arabic, Japanese and Telugu; else English, as Chinese always is). Exports, both print modes and the CLI's render show the redrawn state; saves never do. Every signature keeps its badge, since a document can clip or cover its own layers. Estimates unchanged. +- **2026-10-16 (M9):** the page validation overlay moves to the widget's bottom-right corner as it lies on the page (so it turns with the page and the view) and takes a quarter of the widget's shorter side, at most 48 screen points, so it annotates the signer's artwork from a corner instead of covering the middle of it; a field too small for a legible mark keeps its outline, a rectangle the screen cannot place is not drawn at all, and the whole widget answers the pointer. Estimates unchanged. - **2026-10-16 (M5, #2669, item 5):** The Comments panel groups replies by name and counts all page roots once per frame. Estimates unchanged. - **2026-10-16 (M7, #2140):** Header & footer (and Bates numbering, which uses the same dialog) can shrink the page content so the header and footer don't cover it: the original content is scaled evenly into the band they leave free, centred across, also on rotated pages; Remove and Update take the scaling away, and margins that leave no room are refused. Dialog checkbox with a preview of the band, `shrink` on `doc_header_footer`, all catalogs. Annotations and added text and images are not moved yet. Estimates unchanged. - **2026-10-20 (M14, Spanish catalog gaps):** `es.tsv` gains the 143 `(context, English)` keys the other catalogs carried and it was missing, so Spanish covers the whole bundled key union like Lithuanian and Vietnamese: calendar months and weekdays, the `certificate` context, X.509 key usages, signature-revision and digital-ID lines, password validation, eight plural messages, the combine-from-folder strings, the rendering and stale-file notices and the shortcut-spelled tooltips. Counts in `localization-parity.md` re-measured against the current union (`vi` restated against it for the same reason). Status stays `partial`: no translated help, and OS details stay outside the catalog. Estimates unchanged. @@ -399,6 +446,8 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md - **2026-10-15 (community, M6, #2608):** In Prepare a form the selected field moves with the arrow keys: a point per press, ten with Shift, as the object nudge and dragging it do. Each press is one undo step ("Change field properties"), and the keys no longer turn the page while a field is selected. Estimates unchanged. - **2026-10-15 (M3, closable community card):** The ArtCraft community card on Home has a close button that hides it for the rest of the run; it returns on the next launch, and its links stay in the top bar's Discord button and the Help menu. A UI regression covers closing and the fresh-launch default. Estimates unchanged. - **2026-10-13 (community, M3, #1569 review):** Pixel-only Wayland wheel, wheel-tilt and continuous input use standalone moved events, so Ctrl zoom does not remain latched after the modifier is released (#991); vendored winit patch (13), with a policy test. Estimates unchanged. +- **2026-10-18 (M1, PR #1143 repair policy):** A live in-file reference resolves when its generation matches the xref row or a same-number header at that indexed offset. Neither-match and free slots remain null, including when old object bytes remain. Opening records bounded xref/header disagreements without parsing object bodies or expanding compact xref rows. Synthetic controls cover read order, clones, overlays, dictionary edits, full classic/packed saves and an independent reader of incremental output. **Correction to the 2026-10-15 note below:** its async-opening-test sentence was not evidenced by this COS change; no fresh async-opening or GUI validation is claimed. The full-reference stream-length recursion key is defensive hardening, not an independently isolated regression fix. Estimates unchanged. +- **2026-10-15 (M1, PR #1143 refresh):** COS reference lookups and full saves distinguish complete object references, so stale generations cannot expose replacement content. Snapshot restoration also keeps edits to objects with nonzero generations before incremental saves. Synthetic regressions cover caches, overlays, snapshots, stream lengths and classic/packed saves. Current upstream async-opening test corrections are included. Estimates unchanged. - **2026-10-15 (M14, #2383 candidate):** Device-limit requests now clamp unsupported maxima and minimum alignments before enabling the adapter's full 2D texture extent. This addresses the reported V3D request of 16 shader variables on an adapter supporting 15. Synthetic regressions fail before the clamp and pass afterward, along with the existing HiDPI case and Windows GPU-required application tests. Raspberry Pi startup remains unverified; milestone and overall estimates are unchanged. - **2026-10-12 (M6):** XFDF field import and data-file extraction join all text nodes in a value, preserving text after XML comments and processing instructions. Synthetic regressions cover Unicode, CDATA, empty values and PDF save/reopen. Estimates unchanged. - **2026-10-12 (M7, picture right-click menu):** The Edit text & images menu of a picture gains Cut Image, Redact Image (a redaction mark over the picture, applied from Redact a PDF), Create Link… and Recognize Text… (for that page), in all 21 catalogs. The menu was drawn only while the pointer was over the picture, so its items outside the picture (most of a logo's menu, and Delete below a page) could not be clicked; an open menu now stays drawn. Still missing from Acrobat's menu: Arrange (bring to front, send backward), Remove crop, Draw freehand and Edit image using… (opening an external editor). @@ -526,6 +575,8 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md Newest first: the date, what moved, and the new overall estimate. - **2026-10-28 (M10, #1595):** Export to Markdown: `doc_export_office` with a `.md` or `.markdown` path writes CommonMark with GitHub pipe tables (headings, paragraphs with bold and italic, tables with their first row as the header, a rule between pages); text that Markdown would read as markup is escaped. Images, lists and links are not in the export's layout model and are left out. No menu item yet. Estimates unchanged. +- **2026-10-12 (M10, `ocr.ocr-web`):** OCR in the browser, partly. The web zip carries the text recognition models in `models/` beside `index.html`; the first time Recognize Text is needed the web build downloads them (about 12 MB, with a spinner, an error and a retry). Pages are rendered on the page and read in Web Workers (up to two at once, so the page keeps responding), built with WebAssembly SIMD: 8.5 s a page instead of 20.5 s in wasmtime, still about 17× native. Where workers can't start, the page reads the pages itself. No automated browser test yet. Estimates unchanged. +- **2026-10-12 (M10, `ocr.ocr-web`):** Web OCR shipped: reading scanned pages in Web Workers was checked by hand in a browser (4-page synthetic scan). Still no automated browser test, and a page reads about 17× slower than native. - **2026-10-12 (M3, GNOME title bar, #553):** On GNOME's Wayland session, whose compositor draws no window frames (it offers no `zxdg_decoration_manager_v1`), PdfCraft no longer shows winit's fallback title bar: it turns the frame off and the tab strip becomes the header bar, as in GNOME's own apps. Minimize, maximize/restore and close are drawn in Adwaita's style, in the order and on the side GNOME's `button-layout` names (Fedora's default shows only close); the window's edges and corners resize it, and a hairline outlines it while it isn't maximized. Preferences ▸ Use system title bar (shown only there) gives the window back winit's frame at once, without a restart (part of #1001), and `PDFCRAFT_TITLEBAR=app|system` overrides the choice. KDE, Sway, Hyprland, COSMIC and X11 keep the frame their desktop draws. Not done: right-clicking the header bar doesn't open GNOME's window menu (egui can't ask for it), the window has no shadow or rounded corners, the resize edges lie just inside the window rather than outside it, and Windows still uses its system frame (the rest of #1001). Estimates unchanged. - **2026-10-12 (M0, build label):** A local or patched build can carry a label, set at compile time with `PDFCRAFT_BUILD_LABEL`: `pdfcraft --version`, `pdfcraft-cli --version`, About and the log show `0.7.1 (titlebar)`, and `packaging/linux/package.sh` names the files `pdfcraft-0.7.1-titlebar-…` and makes the .rpm/.deb release `1.titlebar`, which upgrades over the release of the same version (and the next release over it). The version itself is unchanged, so the update check still compares it with published releases. Unlabelled builds are as before. Estimates unchanged. - **2026-10-11 (M6, #1208):** Dropdown and list-box Field Properties expose the existing Left/Center/Right alignment picker. Existing alignment stays intact until changed; cancel, undo/redo and save/reopen are covered through the real UI without changing options, values or flags. Estimates unchanged. @@ -761,11 +812,15 @@ Newest first: the date, what moved, and the new overall estimate. | Date | Change | Summary | |---|---|---| +| 2026-11-08 | patch | M10 #6514/#636: retain main's public GDI Git revision (64e7afd4) to preserve downstream Rust type identity, update the internal PDF loader independently, and refresh the application dependency/lockfile. New-graph CI pending; estimates unchanged. | +| 2026-11-08 | patch | M10 #6514: refresh the craft-print and winprint/GDI pins after #605 merged; retain the Direct2D crash fix and stream-test cleanup repairs, record single-page HUAWEI output with its diagnostic limits, and rerun CI for the new dependency graph. Estimates unchanged. | +| 2026-11-07 | patch | M10 #6514: load Windows PDF print jobs on demand through a read-only file stream, avoiding StorageFile access denial, whole-file buffering and per-chunk async waits; approved dependencies retain the #5006 crash fix. Regression tests pass; physical output remains unqualified; estimates unchanged. | | 2026-11-03 | patch | M5 #5850 review: indirect typewriter intent and composer edits preserve outer whitespace; movement regression commits by outside click; estimates unchanged. | | 2026-11-03 | patch | M5 #5850: Enter adds Fill & Sign line breaks; outside clicks commit, Escape cancels, and saved multiline appearances retain their text and height; estimates unchanged. | | 2026-11-01 | patch | M7 #5657: header/footer, watermark and background operations commit all selected pages together; later-page errors preserve marks, content, shared/inherited resources and pending edits; estimates unchanged. | | 2026-10-28 | patch | M2, PR #4172: integrate main 5af10276 and retain both roadmap histories; viewer-only CJK and print/output font policies and estimates unchanged. | | 2026-10-27 | minor | M10: OCR stages SHA-256-verified ONNX models after the upstream RTEN URL brownout; discovery prefers a complete ONNX pair per directory, falls back to a complete RTEN pair, and never mixes formats or directories; language/web limits and estimates unchanged. | +| 2026-10-20 | minor | M9 #79 review, M2, M8: /ActualText that marks no glyph extracts where it paints, as far as it is visible; redaction strips /ActualText around paint it clips; named-layer recognition needs the layer form to draw n1 and n3. Estimates unchanged. | | 2026-10-23 | minor | M8, #3205 follow-up: phone-pattern redaction includes the +39 country code on labelled Italian geographic numbers with a slash; dates, protocol identifiers and whole-run guards remain. Estimates unchanged. | | 2026-10-23 | minor | M11: image downsampling measures physical resolution after page-local UserUnit scaling, including shared nested forms; synthetic measurement and optimization regressions. Estimates unchanged. | | 2026-10-23 | minor | M10: Word table cells use the grid columns they span, including cells after a merged cell; synthetic export regression and CLI reproduction. Estimates unchanged. | @@ -777,8 +832,11 @@ Newest first: the date, what moved, and the new overall estimate. | 2026-10-20 | minor | M6, #3502: form detection preserves comments across content streams; estimates unchanged. | | 2026-10-20 | minor | M10, #3532 partial: vector printing retains existing appearances with absent Subtype; estimates unchanged. | | 2026-10-20 | minor | M11 #3507: image merging preserves comments and state across content streams. Estimates unchanged. | +| 2026-10-18 | minor | M1, #1143: a live reference may match the xref generation or a same-number header at its indexed offset; neither-match and free slots remain null. Bounded repair diagnostics and synthetic cache, free-slot, edit and classic/packed save regressions; estimates unchanged. | | 2026-10-18 | minor | M5, PR #1971: nested source `/Page` and `/Pages` dictionaries export as null without traversing page content or the page tree. Synthetic saved/reopened three-page regression retains unrelated metadata and aliases. FDF import semantics, copied-graph limits and estimates unchanged. | | 2026-10-15 | minor | M5: budget retained FDF entries before cloning; discovery keeps lazy array owners and annotation positions, with alias/cache-eviction stress fixtures. Limits cover the copied graph; source parsing/page discovery retain existing COS/parser limits, with no total source-memory bound. FDF import semantics and estimates unchanged. | +| 2026-10-16 | minor | M9 #79: named-layer signature appearances show the signature's real state in the display copy, exports and both print modes, never in saves; captions follow the interface language. Estimates unchanged. | +| 2026-10-16 | minor | M9: the page validation overlay moves to the widget's bottom-right corner as it lies on the page and stops growing past 48 screen points, so it annotates the signer's artwork from a corner instead of covering the middle of it; a field under 32 screen points keeps its outline. Estimates unchanged. | | 2026-10-15 | minor | M7: the Edit text caret is as tall as the row it is on in a region with several sizes; estimates unchanged. | | 2026-10-18 | minor | M4, PR #1919 review: repeated page indices apply each rotation to the latest page-local value, preserving indirect/inherited angles. Synthetic adjacent/interleaved duplicates retain siblings and page contents through incremental and full saves. Estimates unchanged. | | 2026-10-18 | minor | M3, #3149: page replacement cancels render demand collected for the previous geometry; all viewer raster upload paths reject empty or oversized textures before cache eviction. Synthetic regressions reproduce the reported 1880×2350 upload at a 2048 limit, retain cached images and unsaved edits, and resume bounded backdrops and tiles. Estimates unchanged. | @@ -787,6 +845,7 @@ Newest first: the date, what moved, and the new overall estimate. | 2026-10-14 | minor | M1: raw-deflate fallback preserves decoding-limit errors in strict and tolerant decoding; synthetic filter and COS stream regressions. Estimates unchanged. | | 2026-10-16 | minor | M14: Czech catalog completed (2,674 entries) and "tag" terminology unified. Estimates unchanged. | | 2026-10-13 | minor | M9: separate page validation overlays preserve signature artwork, and snapshot tabs reuse instead of multiplying; baked captions remain unchanged (#79 open); estimates unchanged | +| 2026-10-15 | minor | M1: refresh object-generation reference identity, retaining current upstream progress and regression tests. Estimates unchanged. | | 2026-10-16 | minor | M10 #1201 partial: lossless image export applies component Decode pairs and Indexed mapping. Estimates unchanged. | | 2026-10-15 | minor | M6: CSV export and spreadsheet merging quote tabs, and import distinguishes quoted tabs from TSV delimiters without changing field identities. Estimates unchanged. | | 2026-10-13 | minor | M4: rotation queries and edits resolve indirect page-tree values; inherited, negative and large rotations verified through both saves. Estimates unchanged. | diff --git a/apps/pdfcraft-cli/src/commands.rs b/apps/pdfcraft-cli/src/commands.rs index 9d0b2057f0..c64f9e1c0f 100644 --- a/apps/pdfcraft-cli/src/commands.rs +++ b/apps/pdfcraft-cli/src/commands.rs @@ -8,6 +8,9 @@ pub(crate) fn read(path: &str) -> Result>, String> { std::fs::read(path).map(Arc::new).map_err(|e| format!("{path}: {e}")) } +/// `info FILE`: a JSON summary of the document as the app opens it (pages, fields, metadata, the +/// standards it declares, and the notes about damage repaired while opening it), with none of +/// its scripts run. pub(crate) fn info(args: &[String]) -> Result<(), CliError> { validate_options("info", args, INFO_OPTIONS)?; let path = *positional(args, INFO_OPTIONS).first().ok_or("info: missing file")?; @@ -18,24 +21,33 @@ pub(crate) fn info(args: &[String]) -> Result<(), CliError> { let mut info = inspect(bytes.clone(), render_password.as_deref()).map_err(|e| e.to_string())?; let pdf_pages = info.pages.len(); let acroform_fields = info.fields.len(); + // The document as the app opens it: the notes about damage repaired while opening, which the + // app shows in Document Properties, and for a dynamic XFA form the laid-out pages and fields. + // Inspection runs none of the document's scripts: an XFA form's initialize and calculate + // scripts, which the app runs on opening, are left alone, as with JavaScript turned off. + let mut session = pdfcraft_engine::Session::new(); + session.set_javascript(false); + let opened = session.open("info.pdf", None, bytes.clone(), password).ok().and_then(|id| session.get(id)); // Dynamic XFA forms: say what laying the template out gives (what the app shows). - let xfa_layout = (info.xfa == Some(pdfcraft_render::Xfa::Dynamic)) - .then(|| { - let mut s = pdfcraft_engine::Session::new(); - let id = s.open("info.pdf", None, bytes.clone(), password).ok()?; - let d = s.get(id)?; - d.xfa.as_ref().map(|x| { - info = d.info.clone(); - // The in-memory layout is an appended revision, not the file being inspected. - info.file_size = bytes.len(); - serde_json::json!({ "pages": x.pages, "fields": x.fields, "warnings": x.warnings }) - }) + let xfa_layout = opened.filter(|_| info.xfa == Some(pdfcraft_render::Xfa::Dynamic)).and_then(|d| { + d.xfa.as_ref().map(|x| { + info = d.info.clone(); + // The in-memory layout is an appended revision, not the file being inspected. + info.file_size = bytes.len(); + serde_json::json!({ "pages": x.pages, "fields": x.fields, "warnings": x.warnings }) }) - .flatten(); - // What the document declares as a standard, read the way the Standards panel reads it, so - // `info` and the app agree. A file that declares nothing leaves these null and empty. - let declared = - pdfcraft_cos::Document::open_with_password(bytes.clone(), password).map(|doc| pdfcraft_preflight::declared(&doc)).unwrap_or_default(); + }); + // What the document declares as a standard, as the Standards panel reads it, and the notes + // Document Properties shows, which include what the app repaired after reading the objects (a + // page tree rebuilt from the pages found by scanning), both from the session's own reading. A + // file the app cannot open is read once more for what the object reader declares and repaired. + // A file that declares nothing leaves the standards null and empty. + let (declared, repairs) = match opened { + Some(d) => (d.standards(), d.repair_log()), + None => pdfcraft_cos::Document::open_with_password(bytes.clone(), password) + .map(|cos| (pdfcraft_preflight::declared(&cos), cos.repair_log().to_vec())) + .unwrap_or_default(), + }; let json = serde_json::json!({ "file": path, "pdf_version": info.pdf_version, @@ -56,6 +68,7 @@ pub(crate) fn info(args: &[String]) -> Result<(), CliError> { "custom_properties": info.custom.len(), "page_labels": info.pages.iter().take(8).map(|p| p.label.clone()).collect::>(), "warnings": info.warnings, + "repairs": repairs, }); stdout_line(format_args!("{}", serde_json::to_string_pretty(&json).unwrap_or_default()))?; Ok(()) @@ -366,8 +379,9 @@ pub(crate) fn render(args: &[String]) -> Result<(), CliError> { Some("pam") => None, _ => return Err(format!("render: --out {out}: use a .png, .jpg, .tif or .pam name").into()), }; - // The app renders a display copy that draws comments missing their /AP streams; the CLI - // must agree with it (and with independent viewers), while keeping the file it reads intact. + // The app renders a display copy that draws comments missing their /AP streams and the state + // of named-layer signatures (here with no trusted certificates); the CLI must agree with it + // (and with independent viewers), while keeping the file it reads intact. let bytes = read(path)?; let password = flag(args, "--password"); // The renderer takes only the user password of R2–R4 files; `Session::open` and the display diff --git a/apps/pdfcraft-cli/tests/it/batch_exit.rs b/apps/pdfcraft-cli/tests/it/batch_exit.rs new file mode 100644 index 0000000000..6dfb9f61e9 --- /dev/null +++ b/apps/pdfcraft-cli/tests/it/batch_exit.rs @@ -0,0 +1,111 @@ +//! The exit codes the CLI has today: `ExitCode::SUCCESS` (0) or `ExitCode::FAILURE` (1). +//! +//! Pinned here so that the exit-code contract is a visible, reviewed change when it comes: a +//! failed file, an outside-root path and a usage error all exit 1 today. + +use serde_json::Value; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; + +const BIN: &str = env!("CARGO_BIN_EXE_pdfcraft-cli"); + +/// A fresh folder for one test, so the tests can run in parallel and leave nothing behind. +fn workdir(name: &str) -> PathBuf { + let dir = std::env::temp_dir().join(format!("pdfcraft-cli-exit-{}-{name}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + dir +} + +/// `batch` with `args`, run from the working directory `dir`. +fn batch(dir: &Path, args: &[&str]) -> Output { + Command::new(BIN).current_dir(dir).arg("batch").args(args).output().unwrap() +} + +/// `run --script --report` from the working directory `dir`: one JSON document for the run. +fn run_report(dir: &Path, script: &Path) -> Output { + Command::new(BIN).current_dir(dir).args(["run", "--script"]).arg(script).arg("--report").output().unwrap() +} + +/// The one JSON document a command prints on stdout. +fn reported(output: &Output) -> Value { + serde_json::from_slice(&output.stdout).unwrap() +} + +#[test] +fn batch_exits_0_when_every_file_succeeds() { + let dir = workdir("batch-ok"); + std::fs::write(dir.join("page.txt"), "Exit code page").unwrap(); + let output = batch(&dir, &["page.txt", "--steps", "convert", "--out-dir", "out"]); + assert_eq!(output.status.code(), Some(0), "{}", String::from_utf8_lossy(&output.stderr)); + let report = reported(&output); + assert_eq!(report["completed"], 1, "{report}"); + assert_eq!(report["failed"], 0, "{report}"); + assert!(dir.join("out/page.pdf").exists(), "the converted page is written"); + std::fs::remove_dir_all(&dir).unwrap(); +} + +#[test] +fn batch_exits_1_when_one_file_fails() { + let dir = workdir("batch-fail"); + std::fs::write(dir.join("bad.pdf"), "broken").unwrap(); + let output = batch(&dir, &["bad.pdf", "--steps", "convert", "--out-dir", "out"]); + assert_eq!(output.status.code(), Some(1), "{}", String::from_utf8_lossy(&output.stderr)); + let report = reported(&output); + assert_eq!(report["failed"], 1, "{report}"); + std::fs::remove_dir_all(&dir).unwrap(); +} + +#[test] +fn batch_usage_error_exits_1_and_creates_nothing() { + let dir = workdir("batch-usage"); + std::fs::write(dir.join("page.txt"), "Exit code page").unwrap(); + // Baseline: a usage error exits 1 today. A later change to the exit-code contract will + // update this assertion on purpose. + let output = batch(&dir, &["page.txt", "--out-dir", "out"]); + assert_eq!(output.status.code(), Some(1), "{}", String::from_utf8_lossy(&output.stderr)); + assert!(output.stdout.is_empty(), "a usage error prints to stderr, not a report on stdout"); + assert!(!output.stderr.is_empty(), "a usage error says what is wrong"); + assert!(!dir.join("out").exists(), "a refused batch creates no output"); + std::fs::remove_dir_all(&dir).unwrap(); +} + +#[test] +fn batch_outside_root_exits_1_and_creates_nothing() { + let dir = workdir("batch-outside-root"); + let output = Command::new(BIN) + .current_dir(&dir) + .args(["batch", "../outside.pdf", "--steps", "convert", "--out-dir", "out", "--root"]) + .arg(&dir) + .output() + .unwrap(); + assert_eq!(output.status.code(), Some(1), "{}", String::from_utf8_lossy(&output.stderr)); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("is outside the allowed directory"), "the input is refused by the root: {stderr}"); + assert!(output.stdout.is_empty(), "a refused batch prints no report"); + assert!(!dir.join("out").exists(), "a refused batch creates no output"); + std::fs::remove_dir_all(&dir).unwrap(); +} + +#[test] +fn run_script_report_exits_0_when_ok_and_1_when_a_step_fails() { + let dir = workdir("run-report"); + let script = dir.join("steps.json"); + let blank = serde_json::json!({ "tool": "doc_create", "args": { "from": "blank", "width": 100, "height": 100 } }); + let steps = serde_json::json!([blank.clone(), { "tool": "no_such_tool", "args": {} }]); + std::fs::write(&script, steps.to_string()).unwrap(); + + let failing = run_report(&dir, &script); + assert_eq!(failing.status.code(), Some(1), "{}", String::from_utf8_lossy(&failing.stderr)); + let report = reported(&failing); + assert_eq!(report["ok"], false, "{report}"); + assert_eq!(report["steps"][1]["status"], "error", "{report}"); + + std::fs::write(&script, serde_json::json!([blank]).to_string()).unwrap(); + let good = run_report(&dir, &script); + assert_eq!(good.status.code(), Some(0), "{}", String::from_utf8_lossy(&good.stderr)); + let report = reported(&good); + assert_eq!(report["ok"], true, "{report}"); + assert_eq!(report["steps"][0]["status"], "ok", "{report}"); + std::fs::remove_dir_all(&dir).unwrap(); +} diff --git a/apps/pdfcraft-cli/tests/it/info_repairs.rs b/apps/pdfcraft-cli/tests/it/info_repairs.rs new file mode 100644 index 0000000000..72e8bed1a3 --- /dev/null +++ b/apps/pdfcraft-cli/tests/it/info_repairs.rs @@ -0,0 +1,75 @@ +//! `info` lists the notes about damage repaired while opening, as the app shows them in Document +//! Properties and as `doc_info` reports them in `repairs`: a rebuilt cross-reference index, and a +//! page tree rebuilt from the pages found by scanning (a note the app adds after reading). + +use std::process::Command; + +const BIN: &str = env!("CARGO_BIN_EXE_pdfcraft-cli"); + +/// A PDF with `pages` pages, each drawing a rectangle (a page found by scanning has content), and +/// `catalog` as object 1; `startxref` points at its cross-reference table, or at `bad_startxref`. +fn fixture(catalog: &str, pages: usize, bad_startxref: Option) -> Vec { + let content = "0 0 0 rg 10 10 40 20 re f"; + let kids: String = (0..pages).map(|i| format!("{} 0 R ", 4 + i)).collect(); + let mut objs = vec![ + catalog.to_string(), + format!("<< /Type /Pages /Kids [{kids}] /Count {pages} >>"), + format!("<< /Length {} >>\nstream\n{content}\nendstream", content.len()), + ]; + objs.extend((0..pages).map(|_| "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 120 60] /Contents 3 0 R >>".to_string())); + let mut out = b"%PDF-1.7\n".to_vec(); + let mut offsets = Vec::new(); + for (i, o) in objs.iter().enumerate() { + offsets.push(out.len()); + out.extend_from_slice(format!("{} 0 obj\n{o}\nendobj\n", i + 1).as_bytes()); + } + let xref = out.len(); + out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes()); + for off in &offsets { + out.extend_from_slice(format!("{off:010} 00000 n \n").as_bytes()); + } + let start = bad_startxref.unwrap_or(xref); + out.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{start}\n%%EOF\n", objs.len() + 1).as_bytes()); + out +} + +const CATALOG: &str = "<< /Type /Catalog /Pages 2 0 R >>"; + +fn info_of(bytes: &[u8], name: &str) -> serde_json::Value { + let dir = std::env::temp_dir().join(format!("pdfcraft-info-repairs-{}-{name}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("f.pdf"); + std::fs::write(&path, bytes).unwrap(); + let out = Command::new(BIN).args(["info", path.to_str().unwrap()]).output().unwrap(); + assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr)); + let json = serde_json::from_slice(&out.stdout).expect("info prints JSON"); + let _ = std::fs::remove_dir_all(&dir); + json +} + +fn repairs(json: &serde_json::Value) -> Vec { + json["repairs"].as_array().unwrap_or_else(|| panic!("no repairs array: {json}")).iter().map(|l| l.as_str().unwrap().to_string()).collect() +} + +#[test] +fn info_lists_a_rebuilt_cross_reference_index() { + // `startxref` points into the middle of an object, so the index is rebuilt from the headers. + let json = info_of(&fixture(CATALOG, 1, Some(20)), "xref"); + assert_eq!(json["pages"], serde_json::json!(1), "{json}"); + let lines = repairs(&json); + assert!(lines.iter().any(|l| l.contains("rebuilt cross-reference index")), "{lines:?}"); +} + +#[test] +fn info_lists_a_page_tree_the_app_rebuilt_by_scanning() { + // The catalog names a page tree object the file does not have (#1656). + let json = info_of(&fixture("<< /Type /Catalog /Pages 9 0 R >>", 2, None), "pagetree"); + let lines = repairs(&json); + assert!(lines.iter().any(|l| l.contains("page tree is missing")), "{lines:?}"); +} + +#[test] +fn info_lists_no_repairs_for_a_sound_file() { + let json = info_of(&fixture(CATALOG, 1, None), "sound"); + assert_eq!(json["repairs"], serde_json::json!([]), "{json}"); +} diff --git a/apps/pdfcraft-cli/tests/it/main.rs b/apps/pdfcraft-cli/tests/it/main.rs index f0b87c97a7..7b7292fe45 100644 --- a/apps/pdfcraft-cli/tests/it/main.rs +++ b/apps/pdfcraft-cli/tests/it/main.rs @@ -3,10 +3,12 @@ mod audit; mod automation_roots; mod batch; +mod batch_exit; mod check; mod edit; mod extract; mod help; +mod info_repairs; mod info_standards; mod mcp_compact; mod options; diff --git a/apps/pdfcraft-cli/tests/it/xfa.rs b/apps/pdfcraft-cli/tests/it/xfa.rs index 1b5ff7bc9d..4d5df67535 100644 --- a/apps/pdfcraft-cli/tests/it/xfa.rs +++ b/apps/pdfcraft-cli/tests/it/xfa.rs @@ -7,6 +7,11 @@ use std::sync::Arc; const BIN: &str = env!("CARGO_BIN_EXE_pdfcraft-cli"); fn fixture(test: &str) -> PathBuf { + fixture_with(test, "") +} + +/// The two-page form, with `event` inside its `name` field. +fn fixture_with(test: &str, event: &str) -> PathBuf { let xdp = r#" "#; +"# + .replace("{event}", event); let objects = [ "<< /Type /Catalog /Pages 2 0 R /NeedsRendering true /AcroForm << /XFA 5 0 R /Fields [] >> >>".to_string(), "<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(), @@ -67,6 +73,21 @@ fn info_reports_the_laid_out_pages_and_fields() { assert_eq!(std::fs::read(path).unwrap(), original, "inspection must not write the layout to the input"); } +/// `info` inspects an untrusted file: a form's initialize script, which the app runs on opening +/// and which here would change the field's value, does not run. +#[test] +fn info_runs_none_of_the_forms_scripts() { + let script = r#""#; + let path = fixture_with("info-script", script); + let output = Command::new(BIN).arg("info").arg(&path).output().unwrap(); + success(&output); + let info: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(info["xfa_layout"]["pages"], 2, "the form is still laid out: {info}"); + let warnings = info["warnings"].as_array().unwrap(); + assert!(!warnings.iter().any(|w| w.as_str().is_some_and(|w| w.contains("scripts changed it on opening"))), "{warnings:?}"); + assert_eq!(info["repairs"], serde_json::json!([]), "{info}"); +} + #[test] fn text_reads_the_layout_including_a_page_absent_from_the_placeholder() { let path = fixture("text"); @@ -134,3 +155,21 @@ fn a_laid_out_form_keeps_the_original_copy_permissions() { assert_eq!(info["file_size"], original.len()); assert_eq!(std::fs::read(path).unwrap(), original); } + +/// A damaged dynamic XFA form: `info` lists the cross-reference index rebuilt while opening it, +/// as Document Properties does after the form is laid out as one more revision. +#[test] +fn info_lists_the_repairs_of_a_laid_out_form() { + let path = fixture("info-damaged"); + let bytes = std::fs::read(&path).unwrap(); + let at = bytes.windows(10).rposition(|w| w == b"startxref\n").unwrap() + 10; + let mut damaged = bytes[..at].to_vec(); + damaged.extend_from_slice(b"20\n%%EOF\n"); + std::fs::write(&path, &damaged).unwrap(); + let output = Command::new(BIN).arg("info").arg(&path).output().unwrap(); + success(&output); + let info: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(info["xfa_layout"]["pages"], 2, "the form is still laid out: {info}"); + let repairs = info["repairs"].as_array().unwrap(); + assert!(repairs.iter().any(|l| l.as_str().is_some_and(|l| l.contains("rebuilt cross-reference index"))), "{repairs:?}"); +} diff --git a/apps/pdfcraft-ocr-worker/Cargo.toml b/apps/pdfcraft-ocr-worker/Cargo.toml new file mode 100644 index 0000000000..7360c13440 --- /dev/null +++ b/apps/pdfcraft-ocr-worker/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "pdfcraft-ocr-worker" +description = "PdfCraft in the browser: the Web Worker that recognises text (wasm32; trunk builds it with pdfcraft-web)" +version.workspace = true +edition.workspace = true +license.workspace = true +publish = false + +[lints] +workspace = true + +[dependencies] +pdfcraft-ocr = { workspace = true } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = "0.2" +js-sys = "0.3" +web-sys = { version = "0.3", features = ["DedicatedWorkerGlobalScope", "MessageEvent"] } diff --git a/apps/pdfcraft-ocr-worker/src/main.rs b/apps/pdfcraft-ocr-worker/src/main.rs new file mode 100644 index 0000000000..a22099e27c --- /dev/null +++ b/apps/pdfcraft-ocr-worker/src/main.rs @@ -0,0 +1,101 @@ +//! PdfCraft in the browser: the Web Worker that recognises text, so reading a scanned page doesn't +//! stop the page responding and several pages are read at once. Trunk builds it alongside +//! `pdfcraft-web` (see apps/pdfcraft-web/index.html); the page starts and feeds the workers +//! (crates/ui-egui/src/workflows/ocr_ui.rs). +//! +//! Messages are plain objects with a `type`: +//! +//! - `ready` (to the page): the worker is listening; send the models now. +//! - `models` (from the page): `{ detection, recognition }`, each a `Uint8Array` holding a model +//! file. Answered with `loaded`, or `failed` with an `error`. +//! - `page` (from the page): `{ id, width, height, pixels }`, the RGBA pixels of a rendered page +//! as a `Uint8Array`. Answered with `lines` `{ id, lines }`, the recognised lines as +//! `pdfcraft_ocr::lines_to_json` writes them, or `failed` `{ id, error }`. + +#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] + +#[cfg(target_arch = "wasm32")] +fn main() { + use wasm_bindgen::{JsCast, closure::Closure}; + + let Ok(scope) = js_sys::global().dyn_into::() else { + return; + }; + let reply_to = scope.clone(); + let mut ocr = None; + let on_message = Closure::::new(move |e: web_sys::MessageEvent| { + let _ = reply_to.post_message(&worker::handle(&mut ocr, &e.data())); + }); + scope.set_onmessage(Some(on_message.as_ref().unchecked_ref())); + // The handler lives as long as the worker. + on_message.forget(); + let _ = scope.post_message(&worker::message(&[("type", "ready".into())])); +} + +#[cfg(not(target_arch = "wasm32"))] +fn main() { + eprintln!("pdfcraft-ocr-worker runs in a browser: trunk builds it with pdfcraft-web"); +} + +#[cfg(target_arch = "wasm32")] +mod worker { + use pdfcraft_ocr::Ocr; + use wasm_bindgen::{JsCast, JsValue}; + + /// The models are about 12 MB together. + const MAX_MODEL_BYTES: u32 = 64 << 20; + /// The renderer keeps pages far smaller; anything larger isn't a page. + const MAX_PIXEL_BYTES: u64 = 1 << 30; + + pub fn message(fields: &[(&str, JsValue)]) -> JsValue { + let o = js_sys::Object::new(); + for (k, v) in fields { + let _ = js_sys::Reflect::set(&o, &(*k).into(), v); + } + o.into() + } + + /// Answer one message from the page. + pub fn handle(ocr: &mut Option, data: &JsValue) -> JsValue { + let get = |k: &str| js_sys::Reflect::get(data, &k.into()).unwrap_or(JsValue::UNDEFINED); + let id = get("id"); + let reply = match get("type").as_string().as_deref() { + Some("models") => load(&get("detection"), &get("recognition")).map(|o| { + *ocr = Some(o); + message(&[("type", "loaded".into())]) + }), + Some("page") => match ocr.as_ref() { + None => Err("the text recognition models aren't loaded".into()), + Some(o) => recognize(o, &get("width"), &get("height"), &get("pixels")) + .map(|lines| message(&[("type", "lines".into()), ("id", id.clone()), ("lines", lines.into())])), + }, + _ => Err("unknown message".into()), + }; + reply.unwrap_or_else(|error| message(&[("type", "failed".into()), ("id", id), ("error", error.into())])) + } + + fn bytes(v: &JsValue, max: u64) -> Result, String> { + let a = v.dyn_ref::().ok_or("expected a Uint8Array")?; + if u64::from(a.length()) > max { + return Err(format!("{} bytes is too large", a.length())); + } + Ok(a.to_vec()) + } + + fn load(detection: &JsValue, recognition: &JsValue) -> Result { + let max = u64::from(MAX_MODEL_BYTES); + Ocr::from_bytes(bytes(detection, max)?, bytes(recognition, max)?).map_err(|e| e.to_string()) + } + + fn recognize(ocr: &Ocr, width: &JsValue, height: &JsValue, pixels: &JsValue) -> Result { + let side = |v: &JsValue| v.as_f64().filter(|v| v.fract() == 0.0 && (1.0..=f64::from(u32::MAX)).contains(v)).map(|v| v as u32); + let (Some(w), Some(h)) = (side(width), side(height)) else { + return Err("the page size is not a positive whole number".into()); + }; + let px = bytes(pixels, MAX_PIXEL_BYTES)?; + if u64::from(w).checked_mul(u64::from(h)).and_then(|n| n.checked_mul(4)) != Some(px.len() as u64) { + return Err("the pixels don't match the page size".into()); + } + ocr.recognize(&px, w, h).map(|lines| pdfcraft_ocr::lines_to_json(&lines)).map_err(|e| e.to_string()) + } +} diff --git a/apps/pdfcraft-web/Cargo.toml b/apps/pdfcraft-web/Cargo.toml index ad8243d594..ad5b62229c 100644 --- a/apps/pdfcraft-web/Cargo.toml +++ b/apps/pdfcraft-web/Cargo.toml @@ -11,10 +11,15 @@ workspace = true [dependencies] pdfcraft-ui-egui = { workspace = true } +# The host messages (storytold/craft-libs): one contract and one bridge for the web builds. +# The page command API (storytold/craft-libs#413): window.pdfcraft over the UI control channel. +craft-web-api = { git = "https://github.com/storytold/craft-libs", rev = "fd5e2feff6ea25bed371a99cc732b3968d621505", version = "0.1.0" } +craft-web-host = { git = "https://github.com/storytold/craft-libs", rev = "fd5e2feff6ea25bed371a99cc732b3968d621505", version = "0.1.0" } eframe = { workspace = true, features = ["default_fonts", "glow", "persistence", "accesskit"] } egui = { workspace = true, features = ["default_fonts", "monochrome_emoji_fonts"] } +serde_json = { workspace = true } [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen-futures = "0.4" -web-sys = { version = "0.3", features = ["Window", "Document", "Element", "HtmlCanvasElement", "HtmlElement", "Location", "Url", "UrlSearchParams", "Response", "Event", "MessageEvent", "EventTarget"] } +web-sys = { version = "0.3", features = ["Window", "Document", "Element", "HtmlCanvasElement", "HtmlElement", "Location", "Url", "UrlSearchParams", "Response", "Event", "MessageEvent", "EventTarget", "CustomEvent", "CustomEventInit"] } js-sys = "0.3" diff --git a/apps/pdfcraft-web/Trunk.toml b/apps/pdfcraft-web/Trunk.toml index 563c2267c1..f03ccc6ea1 100644 --- a/apps/pdfcraft-web/Trunk.toml +++ b/apps/pdfcraft-web/Trunk.toml @@ -9,3 +9,10 @@ filehash = true [serve] addresses = ["127.0.0.1"] port = 8766 + +# Trunk leaves the OCR worker's files unhashed and unnamed in index.html: hash them and name the +# worker for the page (xtask/src/web.rs). +[[hooks]] +stage = "post_build" +command = "cargo" +command_arguments = ["xtask", "web-worker-link"] diff --git a/apps/pdfcraft-web/index.html b/apps/pdfcraft-web/index.html index 46b3a25e21..f93bb0812d 100644 --- a/apps/pdfcraft-web/index.html +++ b/apps/pdfcraft-web/index.html @@ -5,6 +5,8 @@ PdfCraft + + "# + ) + }; + let tpl = format!( + r#""#, + ready("sheet", "this.rawValue = xfa.layout.sheet(this) + 1"), + ready("sheets", "this.rawValue = xfa.layout.sheetCount()"), + ready("bare", "this.rawValue = xfa.layout.sheet(this)"), + rich("inDraw"), + rich("inCaption"), + rich("inValue"), + ); + let (tpl, _) = parse(&tpl).unwrap(); + assert_eq!(tpl.embedded.iter().map(String::as_str).collect::>(), ["inCaption", "inDraw", "inValue"]); + assert_eq!(tpl.page_roles, vec![("sheet".to_string(), PageRole::Number), ("sheets".to_string(), PageRole::Count)]); +} + +/// An embedded sheet number is the page's own on every page, not a constant 1, and the sheet +/// count is the number of pages. +#[test] +fn an_embedded_sheet_number_counts_the_pages() { + let span = |id: &str| format!(r##""##); + let ready = |id: &str, script: &str| { + format!( + r#""# + ) + }; + let tpl = format!( + r#""#, + ready("sheet", "this.rawValue = xfa.layout.sheet(this)+1"), + ready("sheets", "this.rawValue = xfa.layout.sheetCount()"), + span("sheet"), + span("sheets") + ); + let form = layout_xml(&tpl).unwrap(); + assert_eq!(form.pages.len(), 2); + for (i, page) in form.pages.iter().enumerate() { + let shown = texts(page).join(" "); + assert!(shown.contains(&format!("Sheet {} of 2", i + 1)), "page {}: {shown}", i + 1); + } +} + +/// A FormCalc script names the field `$`, and a field may have a ready script that does +/// something else before the one that gives the sheet number: both still play the role. +#[test] +fn sheet_roles_are_read_from_formcalc_and_from_any_ready_script() { + let tpl = r#""#; + let (tpl, _) = parse(tpl).unwrap(); + // FormCalc names methods case-insensitively, JavaScript does not. + assert_eq!( + tpl.page_roles, + vec![ + ("fc".to_string(), PageRole::Number), + ("fcCase".to_string(), PageRole::Number), + ("fcCount".to_string(), PageRole::Count), + ("second".to_string(), PageRole::Number) + ] + ); +} + +/// The data paths of embedded fields are the ones scripts write: a page area's field under the +/// area, as master pages lay it out; a field in a repeating row by name, whatever its instance. +#[test] +fn embedded_fields_are_known_by_the_data_paths_scripts_write() { + let span = |id: &str| format!(r##""##); + let tpl = format!( + r#""#, + span("stamp"), + span("amount") + ); + let (tpl, _) = parse(&tpl).unwrap(); + let names = |v: &[&str]| v.iter().map(|s| s.to_string()).collect::>(); + assert_eq!(tpl.embedded_paths.iter().cloned().collect::>(), [names(&["form", "row", "amount"]), names(&["front", "stamp"])]); + assert!(crate::script::field_is_embedded(&tpl, "front[0].stamp[0]"), "a page area's field"); + assert!(crate::script::field_is_embedded(&tpl, "form[0].row[3].amount[0]"), "any row"); + assert!(!crate::script::field_is_embedded(&tpl, "form[0].plain[0]")); +} + +/// Text embedding a field shows the value the data gives it: each row of a repeating subform +/// its own row's, a page area's field the value scripts write under the area, a multi-select +/// list its picks, a date in its display picture (XFA 3.3 p. 221) in every row; a row without +/// data shows the template's default, not the first row's value. +#[test] +fn embedded_text_shows_each_instances_data() { + let span = |id: &str| format!(r##""##); + let rich = |label: &str, id: &str| { + format!(r#"

{label} {}

"#, span(id)) + }; + let xml = format!( + r#" + + +
102026-11-04202026-12-25AB +Paid +
"#, + rich("Stamp", "stamp"), + rich("Amount", "amount"), + rich("Due", "due"), + rich("Picks", "picks") + ); + let (tpl, _) = parse(&xml).unwrap(); + let data = parse_datasets(&xml).unwrap(); + let form = crate::layout::layout(&tpl, Some(&data)).unwrap(); + let all: Vec = form.pages.iter().flat_map(texts).collect(); + let joined = all.join(" | "); + for want in ["Stamp Paid", "Amount 10", "Amount 20", "Amount none", "Due 11/04/2026", "Due 12/25/2026", "Picks A"] { + assert!(joined.contains(want), "{want} in {joined}"); + } + assert!(joined.contains('B'), "both picks: {joined}"); +} + #[test] fn the_template_parses_with_fonts_captions_and_rich_text() { let (tpl, warnings) = parse(&template(2)).unwrap(); @@ -188,6 +353,46 @@ fn the_template_parses_with_fonts_captions_and_rich_text() { assert!(last.break_before_page); } +/// A page area that declares far more content areas than any real master keeps only the first +/// 128, with a warning. `split_block` walks every content area of a page before starting a new +/// one, re-scanning the unplaced items each time, so tens of thousands of them cost time +/// quadratic in a splittable subform's paragraphs. Capping the count bounds that walk. +#[test] +fn a_page_area_keeps_at_most_a_hundred_and_twenty_eight_content_areas() { + let areas = (0..200).map(|i| format!("", i)).collect::(); + let xml = format!( + r##" + + + +"## + ); + let (tpl, warnings) = parse(&xml).unwrap(); + let ps = tpl.root.page_set.as_ref().unwrap(); + assert_eq!(ps.areas[0].content.len(), 128, "the content areas are capped"); + assert!(warnings.iter().any(|w| w.contains("content areas")), "a warning records the cap: {warnings:?}"); + // Still said after a hundred other warnings (CodeRabbit review on #5844): images that can't + // be decoded, parsed before the page set, fill the warnings' shared cap first. + let bad = "!!!".repeat(120); + let crowded = xml.replace("", &format!("{bad}")); + let (_, warnings) = parse(&crowded).unwrap(); + assert!(warnings.len() > 100, "the other warnings came first: {}", warnings.len()); + assert!(warnings.iter().any(|w| w.contains("content areas")), "the cap is still recorded"); + // Many capped page areas say it once, found without scanning every warning (it comes first). + let page_area = |n: usize| format!(r#"{areas}"#); + let many: String = (0..50).map(page_area).collect(); + let crowded = crowded.replacen(&page_area(0).replace("p0", "front"), &many, 1); + let (_, warnings) = parse(&crowded).unwrap(); + assert_eq!(warnings.iter().filter(|w| w.contains("content areas")).count(), 1, "said once"); + assert!(warnings[0].contains("content areas"), "kept first: {:?}", warnings.first()); +} + #[test] fn rich_text_max_length_reaches_the_field_and_layout() { for (attribute, expected) in [ @@ -678,6 +883,29 @@ fn empty_repeating_subforms_spend_the_layout_budget() { assert!(layout_xml(&tpl).is_ok()); } +#[test] +fn a_block_split_across_very_many_pages_spends_the_layout_budget() { + // One-point content areas slice a flowed subform's paragraphs one line to a page, so the + // block is split across hundreds of pages; split_block rescans the still-unplaced + // paragraphs once per page, which is quadratic in their number. Fewer paragraphs than this + // lay out (188 pages here), so the page limit does not catch it; the rescans spend the + // work budget instead. + let areas: String = (0..128).map(|i| format!("")).collect(); + let paras: String = (0..2500).map(|i| format!("

Paragraph number {i} with a few words to fill a line.

")).collect(); + let tpl = format!( + "" + ); + match layout_xml(&tpl) { + // It says what ran out, not that the form is nested too deeply (CodeRabbit review on #5844). + Err(XfaError::TooLarge(m)) => assert!(m.contains("split across"), "{m}"), + other => panic!("a block split across very many pages must be refused: {other:?}"), + } +} + #[test] fn a_packet_stream_listed_many_times_is_read_once() { let mut doc = Document::open(Arc::new(shell(""))).unwrap(); diff --git a/crates/xfdf/src/lib.rs b/crates/xfdf/src/lib.rs index 16c554d72f..c42d414918 100644 --- a/crates/xfdf/src/lib.rs +++ b/crates/xfdf/src/lib.rs @@ -29,9 +29,9 @@ pub use fdf::export_fdf; use fdf::{ImportBudget, fdf_body, import_fdf, walk_fdf}; #[cfg(test)] use fdf::{MAX_FDF_BYTES, MAX_FDF_OBJECTS}; -pub use xfdf::export_xfdf; #[cfg(test)] use xfdf::{LOCKED, find_by_name}; +pub use xfdf::{Outline, comment_count, comment_outlines, comments_bounds, export_xfdf, export_xfdf_comments, import_comments, is_xfdf_comments}; use xfdf::{apply_values, import_xfdf}; #[cfg(test)] diff --git a/crates/xfdf/src/tests.rs b/crates/xfdf/src/tests.rs index 0da17c1e67..52ce9e9508 100644 --- a/crates/xfdf/src/tests.rs +++ b/crates/xfdf/src/tests.rs @@ -4,6 +4,7 @@ use pdfcraft_annot::{Markup, Meta, NewAnnotation, Shape, Style, TextReplacement, use pdfcraft_cos::{SaveOptions, write_full, write_incremental}; use pdfcraft_forms::{NewField, add_field, fields, set_value}; +use super::xfdf::PDFCRAFT_NS; use super::*; /// Two blank pages with three fields (text, check box, list). @@ -142,14 +143,14 @@ fn values(doc: &Document) -> Vec<(String, Vec)> { fields(doc).into_iter().map(|f| (f.name, f.value)).collect() } -/// #2666: Replace Text writes a StrikeOut and a Caret that replies to it. XFDF names a reply by -/// the `/NM` of what it replies to, so when both carried one id the caret's `inreplyto` was its -/// own `name`: importing it found a comment replying to itself and the pair was lost. +/// #2666: Replace Text writes a Caret and a StrikeOut grouped under it. XFDF names the link by +/// the `/NM` of the annotation it points at, so when both carried one id the `inreplyto` was the +/// strikeout's own `name`: importing it found a comment replying to itself and the pair was lost. #[test] fn a_replace_text_pair_survives_an_xfdf_round_trip() { let mut src = blank(); let quads = [[100.0, 720.0, 200.0, 720.0, 100.0, 706.0, 200.0, 706.0]]; - let style = Style { color: [0.89, 0.13, 0.13], opacity: 1.0, width: 1.0, fill: None }; + let style = Style { color: [0.89, 0.13, 0.13], opacity: 1.0, width: 1.0, fill: None, ..Style::default() }; let meta = |id: &str| Meta { date: Some("D:20261001120000Z".into()), id: id.into() }; add_text_replacement( &mut src, @@ -169,8 +170,8 @@ fn a_replace_text_pair_survives_an_xfdf_round_trip() { let xfdf = export_xfdf(&src, true, false, "a.pdf"); assert!(xfdf.contains("name=\"strike\"") && xfdf.contains("name=\"caret\""), "each gets its own name: {xfdf}"); - assert!(xfdf.contains("inreplyto=\"strike\""), "the caret replies to the strikeout: {xfdf}"); - assert!(!xfdf.contains("inreplyto=\"caret\""), "never a reply to itself"); + assert!(xfdf.contains("inreplyto=\"caret\""), "the strikeout is grouped under the caret: {xfdf}"); + assert!(!xfdf.contains("inreplyto=\"strike\""), "never a reply to itself"); let mut dst = blank(); let r = import(&mut dst, xfdf.as_bytes()).unwrap(); @@ -179,6 +180,135 @@ fn a_replace_text_pair_survives_an_xfdf_round_trip() { let kinds: Vec<&str> = found.iter().map(|s| s.subtype.as_str()).collect(); assert!(kinds.contains(&"StrikeOut") && kinds.contains(&"Caret"), "{kinds:?}"); assert_eq!(comment_view(&dst), comment_view(&src), "the pair and its thread survive"); + // Review on #5689 (CodeRabbit): the intents travel too, so the imported pair is still + // Replace Text, and deleting its strikeout takes the caret and its replacement with it. + assert!(xfdf.contains("intent=\"Replace\"") && xfdf.contains("intent=\"StrikeOutTextEdit\""), "{xfdf}"); + let strike = summaries(&dst).into_iter().find(|s| s.subtype == "StrikeOut").expect("the strikeout"); + pdfcraft_annot::delete_annotation(&mut dst, 0, strike.index).unwrap(); + assert!(summaries(&dst).iter().all(|s| s.subtype != "Caret"), "the caret goes with the strikeout"); +} + +/// Review on #5689 (CodeRabbit): XFDF links a reply to its target by the target's `name`, so a +/// group whose primary has no `/NM` lost its `inreplyto` on export and came back as two loose +/// comments. Both layouts: the caret primary (this PR) and the strikeout primary (older files). +#[test] +fn an_unnamed_group_primary_gets_an_export_only_name() { + for caret_primary in [true, false] { + let mut src = blank(); + let quads = [[100.0, 720.0, 200.0, 720.0, 100.0, 706.0, 200.0, 706.0]]; + let style = Style { color: [0.89, 0.13, 0.13], opacity: 1.0, width: 1.0, fill: None, ..Style::default() }; + let unnamed = Meta { date: None, id: String::new() }; + add_text_replacement( + &mut src, + TextReplacement { + page: 0, + quads: &quads, + replacement: "new words", + author: "Ada", + strike: &style, + caret: &style, + strike_meta: &unnamed, + caret_meta: &unnamed, + }, + ) + .unwrap(); + let page = pdfcraft_model::pages(&src)[0].obj; + let annots = src.get(page).as_dict().unwrap().get(b"Annots").and_then(|a| src.resolve(a).as_array().cloned()).unwrap(); + let refs: Vec = annots.iter().filter_map(Object::as_ref).collect(); + let is = |doc: &Document, r: ObjRef, s: &[u8]| doc.get(r).as_dict().and_then(|d| d.name(b"Subtype")) == Some(s); + let strike = *refs.iter().find(|r| is(&src, **r, b"StrikeOut")).unwrap(); + let caret = *refs.iter().find(|r| is(&src, **r, b"Caret")).unwrap(); + let (primary, sub) = if caret_primary { (caret, strike) } else { (strike, caret) }; + if !caret_primary { + src.update_dict(strike, |d| { + d.remove(b"IRT"); + d.remove(b"RT"); + }) + .unwrap(); + src.update_dict(caret, |d| { + d.set(b"IRT".to_vec(), Object::Ref(strike)); + d.set(b"RT".to_vec(), Object::name("Group")); + }) + .unwrap(); + } + // A comment already named what the made-up name would be: the two must not collide. + let clash = format!("pdfcraft-export-{}-{}", primary.num, primary.generation); + let note = NewAnnotation { + page: 0, + shape: Shape::Note { at: [300.0, 300.0], icon: Default::default() }, + style: style.clone(), + contents: "note".into(), + author: "Ada".into(), + }; + add_annotation(&mut src, ¬e, &Meta { date: None, id: clash.clone() }).unwrap(); + + let xfdf = export_xfdf(&src, true, false, "a.pdf"); + let doc = roxmltree::Document::parse(&xfdf).unwrap(); + let el = |t: &str| doc.descendants().find(|n| n.has_tag_name(t)).unwrap_or_else(|| panic!("no <{t}>: {xfdf}")); + let (tag, sub_tag) = if caret_primary { ("caret", "strikeout") } else { ("strikeout", "caret") }; + let name = el(tag).attribute("name").unwrap_or_else(|| panic!("the unnamed primary gets a name ({caret_primary}): {xfdf}")); + assert_eq!(el(sub_tag).attribute("inreplyto"), Some(name), "{xfdf}"); + assert_ne!(name, clash, "a made-up name never repeats a real /NM"); + assert_eq!(doc.descendants().filter(|n| n.attribute("name") == Some(name)).count(), 1, "{xfdf}"); + assert!(src.get(primary).as_dict().unwrap().get(b"NM").is_none(), "the name is for the export only"); + assert!(src.get(sub).as_dict().unwrap().get(b"NM").is_none()); + + let mut dst = blank(); + import(&mut dst, xfdf.as_bytes()).unwrap(); + // The imported subordinate is grouped under the imported primary again. + let dpage = pdfcraft_model::pages(&dst)[0].obj; + let dannots = dst.get(dpage).as_dict().unwrap().get(b"Annots").and_then(|a| dst.resolve(a).as_array().cloned()).unwrap(); + let drefs: Vec = dannots.iter().filter_map(Object::as_ref).collect(); + let (p_sub, s_sub): (&[u8], &[u8]) = if caret_primary { (b"Caret", b"StrikeOut") } else { (b"StrikeOut", b"Caret") }; + let dprimary = *drefs.iter().find(|r| is(&dst, **r, p_sub)).unwrap(); + let dsub = *drefs.iter().find(|r| is(&dst, **r, s_sub)).unwrap(); + let irt = dst.get(dsub).as_dict().unwrap().get(b"IRT").and_then(Object::as_ref); + assert_eq!(irt, Some(dprimary), "the group survives the round trip ({caret_primary})"); + } +} + +/// An annotation written inline in `/Annots` (a direct dictionary, not a reference) is exported +/// under its `/NM` too, so a made-up name must not repeat it either. +#[test] +fn an_export_only_name_never_repeats_an_inline_annotations_nm() { + let mut src = blank(); + let quads = [[100.0, 720.0, 200.0, 720.0, 100.0, 706.0, 200.0, 706.0]]; + let style = Style { color: [0.89, 0.13, 0.13], opacity: 1.0, width: 1.0, fill: None, ..Style::default() }; + let unnamed = Meta { date: None, id: String::new() }; + add_text_replacement( + &mut src, + TextReplacement { + page: 0, + quads: &quads, + replacement: "new words", + author: "Ada", + strike: &style, + caret: &style, + strike_meta: &unnamed, + caret_meta: &unnamed, + }, + ) + .unwrap(); + let page = pdfcraft_model::pages(&src)[0].obj; + let mut annots = src.get(page).as_dict().unwrap().get(b"Annots").and_then(|a| src.resolve(a).as_array().cloned()).unwrap(); + let primary = + annots.iter().filter_map(Object::as_ref).find(|r| src.get(*r).as_dict().and_then(|d| d.name(b"Subtype")) == Some(&b"Caret"[..])).unwrap(); + // An inline note already named what the made-up name would be. + let clash = format!("pdfcraft-export-{}-{}", primary.num, primary.generation); + let mut note = Dict::new(); + note.set(b"Type".to_vec(), Object::name("Annot")); + note.set(b"Subtype".to_vec(), Object::name("Text")); + note.set(b"NM".to_vec(), PdfString::text(&clash)); + note.set(b"Rect".to_vec(), Object::Array(vec![300.into(), 300.into(), 320.into(), 320.into()])); + annots.push(Object::Dict(note)); + src.update_dict(page, |p| p.set(b"Annots".to_vec(), Object::Array(annots))).unwrap(); + + let xfdf = export_xfdf(&src, true, false, "a.pdf"); + let doc = roxmltree::Document::parse(&xfdf).unwrap(); + let caret = doc.descendants().find(|n| n.has_tag_name("caret")).unwrap_or_else(|| panic!("no : {xfdf}")); + let name = caret.attribute("name").unwrap_or_else(|| panic!("the unnamed primary gets a name: {xfdf}")); + assert_ne!(name, clash, "a made-up name never repeats an inline annotation's /NM: {xfdf}"); + assert_eq!(doc.descendants().filter(|n| n.attribute("name") == Some(&clash[..])).count(), 1, "{xfdf}"); } #[test] @@ -2099,3 +2229,323 @@ fn an_fdf_comment_popup_without_a_rect_is_dropped() { let popups = page.iter().filter(|a| dst.resolve(a).as_dict().is_some_and(|d| d.name(b"Subtype") == Some(b"Popup".as_slice()))).count(); assert_eq!(popups, 0); } + +/// Copy: the chosen comments travel with their replies and nothing else. +#[test] +fn exporting_selected_comments_takes_them_and_their_replies() { + let src = filled(); + let note = summaries(&src).into_iter().find(|s| s.subtype == "Text").unwrap(); + let xfdf = export_xfdf_comments(&src, &[(note.page, note.index)], "form.pdf"); + assert!(xfdf.contains(""), "no field values"); + assert!(is_xfdf_comments(&xfdf)); + assert!(!is_xfdf_comments("") && !is_xfdf_comments("just text")); + // A position that is no comment exports nothing but a valid, empty file. + let empty = export_xfdf_comments(&src, &[(7, 7)], "form.pdf"); + assert!(empty.contains("\n"), "{empty}"); +} + +/// Paste: the copies land on the asked-for page under names of their own, so nothing is +/// replaced and a second paste gives a second set; the reply keeps its copied parent. +#[test] +fn importing_comments_as_copies_adds_them_on_the_page_with_fresh_names() { + let src = filled(); + let note = summaries(&src).into_iter().find(|s| s.subtype == "Text").unwrap(); + let xfdf = export_xfdf_comments(&src, &[(note.page, note.index)], "form.pdf"); + let mut dst = blank(); + let r = import_comments(&mut dst, &xfdf, Some(1), Some("paste1"), None).unwrap(); + assert_eq!((r.comments, r.fields), (2, 0), "{r:?}"); + let first: Vec<_> = summaries(&dst); + assert!(first.iter().all(|s| s.page == 1), "every copy is on page 2: {first:?}"); + let parent = first.iter().find(|s| s.subtype == "Text").unwrap(); + let reply = first.iter().find(|s| s.in_reply_to.is_some()).expect("the reply came along"); + assert_eq!(reply.in_reply_to.as_deref(), Some("paste1-1"), "and replies to the copied note, by its new name ({parent:?})"); + // Again: two sets, not one replaced set. + import_comments(&mut dst, &xfdf, Some(0), Some("paste2"), None).unwrap(); + let second = summaries(&dst); + assert_eq!(second.len(), 4, "{second:?}"); + assert_eq!(second.iter().filter(|s| s.page == 0).count(), 2); + // The copies' names are their own, not the originals'. + let xfdf2 = export_xfdf(&dst, true, false, "dst.pdf"); + assert!(!xfdf2.contains("name=\"n1\"") && !xfdf2.contains("name=\"r1\""), "{xfdf2}"); + assert!(xfdf2.contains("name=\"paste1-1\"") && xfdf2.contains("inreplyto=\"paste1-1\""), "{xfdf2}"); + // A page the document doesn't have is refused, and nothing changes. + assert!(import_comments(&mut dst, &xfdf, Some(9), Some("paste3"), None).is_err()); + assert_eq!(summaries(&dst).len(), 4); + // Without a target page the copies keep their own pages; without a prefix the import is + // the usual one (same names replace). + let mut other = blank(); + import_comments(&mut other, &xfdf, None, Some("p"), None).unwrap(); + assert!(summaries(&other).iter().all(|s| s.page == 0)); + import_comments(&mut other, &xfdf, None, None, None).unwrap(); + import_comments(&mut other, &xfdf, None, None, None).unwrap(); + assert_eq!(summaries(&other).len(), 4, "the named import replaced its own earlier copy"); +} + +/// Other tools write comments straight into the page's `/Annots` array, without an object of +/// their own: they copy like any other, and two source comments sharing a name give two copies. +#[test] +fn comments_written_directly_into_the_page_array_copy_and_shared_names_give_two_copies() { + let mut doc = blank(); + let page = pdfcraft_model::pages(&doc)[0].obj; + let mut direct = Dict::new(); + direct.set(b"Type".to_vec(), Object::name("Annot")); + direct.set(b"Subtype".to_vec(), Object::name("Square")); + direct.set(b"Rect".to_vec(), arr(&[10.0, 10.0, 60.0, 40.0])); + direct.set(b"Contents".to_vec(), PdfString::text("drawn elsewhere")); + let mut twin = direct.clone(); + twin.set(b"NM".to_vec(), PdfString::text("shared")); + let mut other_twin = twin.clone(); + other_twin.set(b"Rect".to_vec(), arr(&[100.0, 10.0, 150.0, 40.0])); + doc.update_dict(page, |p| p.set(b"Annots".to_vec(), Object::Array(vec![Object::Dict(direct), Object::Dict(twin), Object::Dict(other_twin)]))) + .unwrap(); + assert_eq!(summaries(&doc).len(), 3); + let xfdf = export_xfdf_comments(&doc, &[(0, 0)], "x.pdf"); + assert!(xfdf.contains("\n"), "a position that is no annotation copies nothing"); +} + +/// A selected comment without a name of its own (other tools leave /NM out) gets one for the +/// export, so its replies still find it when the copies are pasted. +#[test] +fn replies_to_a_selected_comment_without_a_name_follow_it_into_the_copy() { + let meta = |id: &str| Meta { date: None, id: id.into() }; + let mut doc = blank(); + let shape = Shape::Rectangle { rect: [10.0, 10.0, 60.0, 40.0] }; + add_annotation( + &mut doc, + &NewAnnotation { page: 0, style: Style::default_for(&shape), shape, contents: "nameless".into(), author: "Ada".into() }, + &meta("gone"), + ) + .unwrap(); + let index = summaries(&doc)[0].index; + add_reply(&mut doc, 0, index, "Agreed", "Bob", &meta("r1")).unwrap(); + // The parent loses its /NM, as another tool may have left it. + let annots = pdfcraft_model::pages(&doc)[0].dict.get(b"Annots").map(|a| doc.resolve(a)).and_then(|a| a.as_array().cloned()).unwrap(); + let parent = annots[index].as_ref().unwrap(); + doc.update_dict(parent, |d| { + d.remove(b"NM"); + }) + .unwrap(); + assert_eq!(threads(&doc)[0].0, None, "{:?}", threads(&doc)); + // A comment written straight into the page array already carries the name the parent would + // get: the parent's export name steers clear of it. + let mut direct = Dict::new(); + direct.set(b"Type".to_vec(), Object::name("Annot")); + direct.set(b"Subtype".to_vec(), Object::name("Square")); + direct.set(b"Rect".to_vec(), arr(&[100.0, 100.0, 150.0, 140.0])); + direct.set(b"NM".to_vec(), PdfString::text(&format!("pdfcraft-0-{index}"))); + let page = pdfcraft_model::pages(&doc)[0].obj; + doc.update_dict(page, |p| { + if let Some(Object::Array(annots)) = p.get_mut(b"Annots") { + annots.push(Object::Dict(direct)); + } + }) + .unwrap(); + let xfdf = export_xfdf_comments(&doc, &[(0, index)], "x.pdf"); + let given = format!("pdfcraft-0-{index}x"); + assert!(xfdf.contains(&format!("name=\"{given}\"")) && xfdf.contains(&format!("inreplyto=\"{given}\"")), "{xfdf}"); + let mut dst = blank(); + import_comments(&mut dst, &xfdf, Some(0), Some("p"), None).unwrap(); + let copies = threads(&dst); + assert_eq!(copies.len(), 2, "{copies:?}"); + let parent_name = copies.iter().find(|(_, irt, _)| irt.is_none()).and_then(|(n, _, _)| n.clone()); + assert!(parent_name.is_some(), "{copies:?}"); + let reply_to = copies.iter().find(|(_, irt, _)| irt.is_some()).and_then(|(_, irt, _)| irt.clone()); + assert_eq!(reply_to, parent_name, "the reply follows its copied parent: {copies:?}"); +} + +/// A paste placed elsewhere: every copy moves by the offset, rect, quads, ink and vertices +/// alike; the bounds and count of a clipboard text say what hangs on the pointer. +#[test] +fn copies_placed_with_an_offset_move_whole_and_the_text_reports_its_bounds() { + let src = filled(); + let all: Vec<(usize, usize)> = summaries(&src).iter().filter(|s| s.in_reply_to.is_none()).map(|s| (s.page, s.index)).collect(); + let xfdf = export_xfdf_comments(&src, &all, "form.pdf"); + assert_eq!(comment_count(&xfdf), 3, "note, highlight, ink (the reply is not counted)"); + let bounds = comments_bounds(&xfdf).unwrap(); + assert!(bounds[0] <= 10.0 && bounds[1] <= 10.0 && bounds[2] >= 100.0 && bounds[3] >= 500.0, "{bounds:?}"); + assert_eq!(comment_count("just text"), 0); + assert_eq!(comments_bounds(""), None); + // A reply with a rect of its own (some tools write one, at 0,0 or away from its parent) + // doesn't widen the box the paste is centred on: it has no place on the page. + let with_reply = r#" + + re + "#; + assert_eq!(comments_bounds(with_reply), Some([100.0, 100.0, 140.0, 120.0])); + let mut dst = blank(); + import_comments(&mut dst, &xfdf, Some(0), Some("p"), Some([100.0, -50.0])).unwrap(); + let moved = summaries(&dst); + let note = moved.iter().find(|s| s.subtype == "Text").unwrap(); + let original = summaries(&src).into_iter().find(|s| s.subtype == "Text").unwrap(); + let (a, b) = (note.rect, original.rect); + assert!((a[0] - b[0] - 100.0).abs() < 0.01 && (a[1] - b[1] + 50.0).abs() < 0.01, "{a:?} from {b:?}"); + let again = export_xfdf(&dst, true, false, "dst.pdf"); + assert!(again.contains("coords=\"110,60,190,60,110,50,190,50\""), "quads moved too: {again}"); + assert!(again.contains("110,-40;120,-20;140,-30"), "ink moved too: {again}"); + // The outlines say what hangs on the pointer: a box for the note, quads for the highlight, + // strokes for the ink, each in its colour; the reply draws nothing of its own. + let outlines = comment_outlines(&xfdf); + assert_eq!(outlines.len(), 3, "{outlines:?}"); + assert!(outlines.iter().any(|o| matches!(o, Outline::Rect { round: false, .. }))); + assert!(outlines.iter().any(|o| matches!(o, Outline::Quads { quads, .. } if quads.len() == 1 && quads[0][0] == 10.0))); + assert!(outlines.iter().any(|o| matches!(o, Outline::Ink { strokes, .. } if strokes == &vec![vec![[10.0, 10.0], [20.0, 30.0], [40.0, 20.0]]]))); + assert!(comment_outlines("no xfdf").is_empty()); + let drawn = "\ + \ + 1,1;5,5;9,1\ + 1,1;5,5;9,1\ + \ + \ + "; + let outlines = comment_outlines(drawn); + assert_eq!(outlines.len(), 6, "{outlines:?}"); + assert_eq!(outlines[0], Outline::Line { points: vec![[1.0, 2.0], [3.0, 4.0]], color: Some([1.0, 0.0, 0.0]) }); + assert!(matches!(&outlines[1], Outline::Poly { closed: false, points, .. } if points.len() == 3)); + assert!(matches!(&outlines[2], Outline::Poly { closed: true, .. })); + assert!(matches!(&outlines[3], Outline::Rect { round: true, .. })); + assert!(matches!(&outlines[5], Outline::Line { points, .. } if points == &vec![[20.0, 20.0], [40.0, 50.0]])); + // A non-finite offset is ignored rather than written into the document. + import_comments(&mut dst, &xfdf, Some(1), Some("q"), Some([f64::NAN, 0.0])).unwrap(); + assert_eq!(summaries(&dst).iter().filter(|s| s.page == 1 && s.subtype == "Text").map(|s| s.rect[0]).next(), Some(b[0])); +} + +/// Fill & Sign marks and text boxes copied: the bundle on the pointer shows the tick, cross, dot +/// and line themselves (not empty boxes) and a text box's text, and the pasted copies are drawn +/// with their mark. +#[test] +fn copied_marks_and_text_boxes_show_what_they_are_while_placed_and_once_pasted() { + use pdfcraft_annot::FillMark; + let mut src = blank(); + let meta = Meta { date: Some("D:20261020120000Z".into()), id: "m".into() }; + let marks = [FillMark::Check, FillMark::Cross, FillMark::Dot, FillMark::Line]; + for (k, mark) in marks.into_iter().enumerate() { + let shape = Shape::Mark { rect: [100.0 + 40.0 * k as f64, 600.0, 120.0 + 40.0 * k as f64, 620.0], mark }; + let new = NewAnnotation { page: 0, style: Style::default_for(&shape), shape, contents: String::new(), author: "Ada".into() }; + add_annotation(&mut src, &new, &meta).unwrap(); + } + let text = Shape::TextBox { rect: [100.0, 400.0, 200.0, 430.0], font_size: 14.0 }; + let new = NewAnnotation { page: 0, style: Style::default_for(&text), shape: text, contents: "Abnahme\nok".into(), author: "Ada".into() }; + add_annotation(&mut src, &new, &meta).unwrap(); + let all: Vec<(usize, usize)> = summaries(&src).iter().filter(|s| s.in_reply_to.is_none()).map(|s| (s.page, s.index)).collect(); + let xfdf = export_xfdf_comments(&src, &all, "protocol.pdf"); + let outlines = comment_outlines(&xfdf); + // The tick is one stroke, the cross two, the dot a circle, the line one stroke: the strokes + // the appearance draws, not the stamps' boxes. + let check = FillMark::Check.strokes([100.0, 600.0, 120.0, 620.0]); + assert!(outlines.iter().any(|o| matches!(o, Outline::Line { points, .. } if *points == check[0])), "{outlines:?}"); + assert!(!outlines.iter().any(|o| matches!(o, Outline::Rect { rect, round: false, .. } if *rect == [100.0, 600.0, 120.0, 620.0])), "{outlines:?}"); + let cross = FillMark::Cross.strokes([140.0, 600.0, 160.0, 620.0]); + assert!(cross.iter().all(|s| outlines.iter().any(|o| matches!(o, Outline::Line { points, .. } if points == s))), "{outlines:?}"); + assert!( + outlines + .iter() + .any(|o| matches!(o, Outline::Rect { rect, round: true, .. } if (rect[0] - 184.0).abs() < 1e-9 && (rect[2] - 196.0).abs() < 1e-9)), + "{outlines:?}" + ); + assert!(outlines.iter().any(|o| matches!(o, Outline::Line { points, .. } if *points == vec![[220.0, 610.0], [240.0, 610.0]])), "{outlines:?}"); + assert!( + outlines.iter().any(|o| matches!(o, Outline::Text { text, size, .. } if text == "Abnahme\nok" && (*size - 14.0).abs() < 1e-9)), + "the text box shows its text at its size: {outlines:?}" + ); + // Pasted into another document, each mark is drawn with its own strokes. + let mut dst = blank(); + import_comments(&mut dst, &xfdf, Some(1), Some("p"), Some([10.0, -20.0])).unwrap(); + let page = pdfcraft_model::pages(&dst)[1].obj; + let annots = + dst.get(page).as_dict().and_then(|p| p.get(b"Annots").cloned()).map(|a| dst.resolve(&a).as_array().cloned().unwrap_or_default()).unwrap(); + let stamps: Vec = + annots.iter().filter_map(|a| dst.resolve(a).as_dict().cloned()).filter(|d| d.name(b"Subtype") == Some(b"Stamp".as_slice())).collect(); + assert_eq!(stamps.len(), 4); + for d in &stamps { + let ap = d.get(b"AP").map(|a| dst.resolve(a)).and_then(|a| a.as_dict().cloned()).and_then(|a| a.get(b"N").cloned()).expect("an appearance"); + let normal = dst.resolve(&ap); + let Object::Stream(s) = normal.as_ref() else { panic!("no appearance stream") }; + let content = String::from_utf8_lossy(&s.decoded().unwrap()).into_owned(); + let drawn = + if d.name(b"Name") == Some(b"PCDot".as_slice()) { content.contains(" c\n") || content.contains(" c ") } else { content.contains(" S\n") }; + assert!(drawn, "{:?}: {content}", d.name(b"Name")); + } +} + +/// A pasted copy is what the original was: typewriter text stays typewriter text (`/IT`) and a +/// Fill & Sign item, a cloud keeps its cloudy border, and PdfCraft's own keys (a turned comment's +/// `/PCBox` and `/PCAngle`) come along. A reference into the source document stays behind, and +/// keys that aren't PdfCraft's, or a malformed attribute, are not taken from the attribute. +#[test] +fn copies_keep_their_intent_cloud_and_pdfcraft_keys() { + let mut src = blank(); + let meta = |id: &str| Meta { date: Some("D:20261023120000Z".into()), id: id.into() }; + let typed = Shape::Typewriter { rect: [50.0, 500.0, 150.0, 520.0], font_size: 12.0 }; + let cloud = Shape::Polygon { vertices: vec![[200.0, 200.0], [300.0, 200.0], [250.0, 280.0]], cloud: true }; + let square = Shape::Rectangle { rect: [100.0, 100.0, 180.0, 160.0] }; + for (shape, id) in [(typed, "t1"), (cloud, "c1"), (square, "s1")] { + let new = NewAnnotation { page: 0, style: Style::default_for(&shape), shape, contents: format!("{id} text"), author: "Ada".into() }; + add_annotation(&mut src, &new, &meta(id)).unwrap(); + } + let annots = |doc: &Document, page: usize| -> Vec { + let pages = pdfcraft_model::pages(doc); + let list = pages[page].dict.get(b"Annots").map(|a| doc.resolve(a)).and_then(|a| a.as_array().cloned()).unwrap_or_default(); + list.iter().filter_map(|a| doc.resolve(a).as_dict().cloned()).filter(|d| !matches!(d.name(b"Subtype"), Some(b"Popup" | b"Widget"))).collect() + }; + let refs: Vec = { + let pages = pdfcraft_model::pages(&src); + let list = pages[0].dict.get(b"Annots").and_then(|a| src.resolve(a).as_array().cloned()).unwrap(); + list.iter().filter_map(Object::as_ref).collect() + }; + let target = src.add(Object::Dict(Dict::new())); + let square_ref = *refs.iter().find(|r| src.get(**r).as_dict().is_some_and(|d| d.name(b"Subtype") == Some(b"Square".as_slice()))).unwrap(); + src.update_dict(square_ref, |d| { + d.set(b"PCBox".to_vec(), arr(&[110.0, 110.0, 170.0, 150.0])); + d.set(b"PCAngle".to_vec(), Object::Real(30.0)); + d.set(b"PCElsewhere".to_vec(), Object::Ref(target)); + }) + .unwrap(); + let which: Vec<(usize, usize)> = summaries(&src).iter().map(|c| (c.page, c.index)).collect(); + let xfdf = export_xfdf_comments(&src, &which, "src.pdf"); + assert!(xfdf.contains("intent=\"FreeTextTypeWriter\"") && xfdf.contains("style=\"cloudy\""), "{xfdf}"); + let mut dst = blank(); + import_comments(&mut dst, &xfdf, Some(1), Some("p"), Some([5.0, -5.0])).unwrap(); + let copies = annots(&dst, 1); + assert_eq!(copies.len(), 3, "{copies:?}"); + let typed = copies.iter().find(|d| d.name(b"Subtype") == Some(b"FreeText".as_slice())).unwrap(); + assert_eq!(typed.name(b"IT"), Some(b"FreeTextTypeWriter".as_slice()), "{typed:?}"); + assert!(pdfcraft_annot::is_fill_sign(&dst, typed), "still a Fill & Sign item: {typed:?}"); + assert!(matches!(typed.get(b"PCFillSign"), Some(Object::Bool(true))), "{typed:?}"); + let cloud = copies.iter().find(|d| d.name(b"Subtype") == Some(b"Polygon".as_slice())).unwrap(); + assert_eq!(cloud.name(b"IT"), Some(b"PolygonCloud".as_slice())); + let be = cloud.get(b"BE").and_then(Object::as_dict).expect("a cloudy border"); + assert_eq!(be.name(b"S"), Some(b"C".as_slice())); + assert_eq!(be.get(b"I").and_then(Object::as_f64), Some(1.0)); + let square = copies.iter().find(|d| d.name(b"Subtype") == Some(b"Square".as_slice())).unwrap(); + assert_eq!(square.get(b"PCAngle").and_then(Object::as_f64), Some(30.0)); + assert!(square.get(b"PCBox").and_then(Object::as_array).is_some_and(|b| b.len() == 4)); + assert!(square.get(b"PCElsewhere").is_none(), "a reference into the source stays behind: {square:?}"); + // File ▸ Export comments writes the standard attributes only. + let exported = export_xfdf(&src, true, false, "src.pdf"); + assert!(exported.contains("intent=\"FreeTextTypeWriter\"") && !exported.contains("pdfcraft:keys"), "{exported}"); + // A hand-made attribute: only PdfCraft keys with values of their own are taken. + let crafted = format!( + "\ + \ + " + ); + let mut third = blank(); + import_comments(&mut third, &crafted, Some(0), Some("q"), None).unwrap(); + let got = annots(&third, 0); + assert_eq!(got.len(), 2, "{got:?}"); + assert_eq!(got[0].get(b"PCAngle").and_then(Object::as_f64), Some(45.0)); + assert_eq!(got[0].name(b"Subtype"), Some(b"Square".as_slice()), "not taken from the attribute"); + assert!(got[0].get(b"PCRef").is_none() && got[1].get(b"PCAngle").is_none(), "{got:?}"); +} diff --git a/crates/xfdf/src/xfdf.rs b/crates/xfdf/src/xfdf.rs index 06ad9f34ec..3368003483 100644 --- a/crates/xfdf/src/xfdf.rs +++ b/crates/xfdf/src/xfdf.rs @@ -6,22 +6,196 @@ use super::*; // ── export ────────────────────────────────────────────────────────────────────────────────── -/// Every comment as an XFDF element (one line per comment, replies included). -fn xfdf_annots(doc: &Document, out: &mut String) { +/// The annotations of `which` (0-based page, position in the page's `/Annots`) as positions, +/// with every reply to them (and to those replies), so a thread travels whole. Positions, not +/// references: other tools write comments straight into the page's array, without an object of +/// their own, and those copy too. +fn annots_with_replies(doc: &Document, which: &[(usize, usize)]) -> HashSet<(usize, usize)> { let pages = pdfcraft_model::pages(doc); - // NM of every annotation, for inreplyto. + let annots = |p: &pdfcraft_model::Page| -> Vec { + p.dict.get(b"Annots").map(|a| doc.resolve(a)).and_then(|a| a.as_array().cloned()).unwrap_or_default() + }; + /// An annotation's position, its reference (when it has one) and its parent's (`/IRT`). + struct Entry { + pos: (usize, usize), + obj: Option, + parent: Option, + } + let all: Vec = pages + .iter() + .enumerate() + .flat_map(|(pi, p)| { + annots(p).into_iter().enumerate().map(move |(ai, a)| { + let parent = doc.resolve(&a).as_dict().and_then(|d| d.get(b"IRT")).and_then(Object::as_ref); + Entry { pos: (pi, ai), obj: a.as_ref(), parent } + }) + }) + .collect(); + let mut set: HashSet<(usize, usize)> = which.iter().copied().filter(|w| all.iter().any(|e| e.pos == *w)).collect(); + // Replies point at their parent through /IRT: a pass per reply depth, bounded by the count. + for _ in 0..all.len() { + let before = set.len(); + let parents: HashSet = all.iter().filter(|e| set.contains(&e.pos)).filter_map(|e| e.obj).collect(); + for e in &all { + if e.parent.is_some_and(|parent| parents.contains(&parent)) { + set.insert(e.pos); + } + } + if set.len() == before { + break; + } + } + set +} + +/// Every comment as an XFDF element (one line per comment, replies included), or only those at +/// the positions in `only`. +/// The namespace of PdfCraft's own attributes in XFDF (`pdfcraft:keys`): other readers skip them. +pub(crate) const PDFCRAFT_NS: &str = "https://github.com/storytold/pdfcraft#xfdf"; +/// At most this much text in a `pdfcraft:keys` attribute is read. +const MAX_OWN_KEYS: usize = 64 * 1024; + +/// A value that means the same in another document: no reference or stream into this one. +fn portable(o: &Object, depth: usize) -> bool { + match o { + Object::Ref(_) | Object::Stream(_) => false, + Object::Array(a) => depth < 4 && a.iter().all(|o| portable(o, depth + 1)), + Object::Dict(d) => depth < 4 && d.iter().all(|(_, o)| portable(o, depth + 1)), + _ => true, + } +} + +/// The annotation's own PdfCraft keys (`/PC…`: a Fill & Sign item, a turned comment's box and +/// angle, …) as one PDF dictionary, for a copy that is what the original was. Values that point +/// into this document stay behind. +fn own_keys(d: &Dict) -> Option { + let mut keys = Dict::new(); + // Each value must also write as text: XML can't hold raw bytes. + let writes_as_text = |v: &Object| { + let mut out = Vec::new(); + pdfcraft_cos::serialize(v, &mut out); + std::str::from_utf8(&out).is_ok() + }; + for (k, v) in d.iter().filter(|(k, v)| k.starts_with(b"PC") && portable(v, 0) && writes_as_text(v)) { + keys.set(k.clone(), v.clone()); + } + if keys.is_empty() { + return None; + } + let mut out = Vec::new(); + pdfcraft_cos::serialize(&Object::Dict(keys), &mut out); + String::from_utf8(out).ok() +} + +/// The `pdfcraft:keys` of an XFDF comment back as dictionary entries: PdfCraft's keys only, with +/// values that mean the same here. Anything malformed is skipped. +fn read_own_keys(node: roxmltree::Node, d: &mut Dict) { + let Some(text) = node.attribute((PDFCRAFT_NS, "keys")).filter(|t| t.len() <= MAX_OWN_KEYS) else { return }; + let Ok(Object::Dict(keys)) = pdfcraft_cos::Lexer::new(text.as_bytes(), 0).object() else { return }; + for (k, v) in keys.iter().filter(|(k, v)| k.starts_with(b"PC") && portable(v, 0)) { + if !d.contains(k) { + d.set(k.clone(), v.clone()); + } + } +} + +/// The name of every annotation, for `inreplyto`: its /NM, or, for one without one (other tools +/// leave /NM out) that is in `only` or that another annotation replies to or is grouped under +/// (`/IRT`, ISO 32000-2 §12.5.6.2; a Replace Text pair is such a group), a name new to the +/// document for this export, so the link survives the round trip. Made-up names are never written +/// into the PDF. +fn annotation_names(doc: &Document, pages: &[pdfcraft_model::Page], only: Option<&HashSet<(usize, usize)>>) -> HashMap { let mut names: HashMap = HashMap::new(); - for p in &pages { - for a in p.dict.get(b"Annots").map(|a| doc.resolve(a)).and_then(|a| a.as_array().cloned()).unwrap_or_default() { - if let Some(r) = a.as_ref() - && let Some(nm) = doc.get(r).as_dict().and_then(|d| text_of(doc, d, b"NM")) - { - names.insert(r, nm); + let mut unnamed: Vec<(usize, usize, ObjRef)> = Vec::new(); + let mut targets: HashSet = HashSet::new(); + // Every name in use, those of annotations written straight into the page array too, so a + // given name is new. + let mut taken: HashSet = HashSet::new(); + for (pi, p) in pages.iter().enumerate() { + for (ai, a) in p.dict.get(b"Annots").map(|a| doc.resolve(a)).and_then(|a| a.as_array().cloned()).unwrap_or_default().into_iter().enumerate() { + let d = doc.resolve(&a).as_dict().cloned(); + if let Some(t) = d.as_ref().and_then(|d| d.get(b"IRT")).and_then(Object::as_ref) { + targets.insert(t); + } + let nm = d.as_ref().and_then(|d| text_of(doc, d, b"NM")).filter(|nm| !nm.is_empty()); + if let Some(nm) = &nm { + taken.insert(nm.clone()); + } + // Only an indirect annotation can be an `/IRT` target, so only it is keyed here. + let Some(r) = a.as_ref() else { continue }; + match nm { + Some(nm) => { + names.insert(r, nm); + } + None => unnamed.push((pi, ai, r)), } } } + unnamed.retain(|(pi, ai, r)| only.is_some_and(|only| only.contains(&(*pi, *ai))) || targets.contains(r)); + for (pi, ai, r) in unnamed { + let mut name = format!("pdfcraft-{pi}-{ai}"); + while taken.contains(&name) { + name.push('x'); + } + taken.insert(name.clone()); + names.insert(r, name); + } + names +} + +/// The elements inside a comment's XFDF element: its text, default appearance, ink, vertices +/// and pop-up (on page `pi`). +fn xfdf_annot_children(doc: &Document, out: &mut String, d: &Dict, pi: usize) { + if let Some(c) = text_of(doc, d, b"Contents") { + let _ = write!(out, "{}", esc(&c)); + } + if let Some(da) = text_of(doc, d, b"DA") { + let _ = write!(out, "{}", esc(&da)); + } + let ink = d.get(b"InkList").map(|o| doc.resolve(o)).and_then(|o| o.as_array().cloned()).unwrap_or_default(); + if !ink.is_empty() { + out.push_str(""); + for g in ink { + let v = nums_of(doc, Some(&g)); + let pts: Vec = v.as_chunks::<2>().0.iter().map(|p| format!("{},{}", n(p[0]), n(p[1]))).collect(); + let _ = write!(out, "{}", pts.join(";")); + } + out.push_str(""); + } + // Polygon and polyline points (XFDF ``: `x,y;x,y;…`). + let vertices = nums_of(doc, d.get(b"Vertices")); + let pts: Vec = vertices.as_chunks::<2>().0.iter().map(|p| format!("{},{}", n(p[0]), n(p[1]))).collect(); + if !pts.is_empty() { + let _ = write!(out, "{}", pts.join(";")); + } + if let Some(pd) = d.get(b"Popup").map(|p| doc.resolve(p)).and_then(|p| p.as_dict().cloned()) { + let r = nums_of(doc, pd.get(b"Rect")); + if r.len() == 4 { + let open = matches!(pd.get(b"Open"), Some(Object::Bool(true))); + let _ = write!( + out, + "", + n(r[0]), + n(r[1]), + n(r[2]), + n(r[3]), + if open { "yes" } else { "no" } + ); + } + } +} + +/// `own`: write each comment's PdfCraft keys too (see [`own_keys`]), for a copy and paste. +fn xfdf_annots(doc: &Document, out: &mut String, only: Option<&HashSet<(usize, usize)>>, own: bool) { + let pages = pdfcraft_model::pages(doc); + // NM of every annotation, for inreplyto. A selected or replied-to annotation without one + // (other tools leave /NM out) gets a name for this export, so its replies still find it. + let names = annotation_names(doc, &pages, only); for (pi, p) in pages.iter().enumerate() { - for a in p.dict.get(b"Annots").map(|a| doc.resolve(a)).and_then(|a| a.as_array().cloned()).unwrap_or_default() { + for (ai, a) in p.dict.get(b"Annots").map(|a| doc.resolve(a)).and_then(|a| a.as_array().cloned()).unwrap_or_default().into_iter().enumerate() { + if only.is_some_and(|only| !only.contains(&(pi, ai))) { + continue; + } let Some(d) = doc.resolve(&a).as_dict().cloned() else { continue }; let Some(sub) = d.name(b"Subtype") else { continue }; let Some((_, tag)) = SUBTYPES.iter().find(|(s, _)| s.as_bytes() == sub) else { continue }; @@ -37,9 +211,12 @@ fn xfdf_annots(doc: &Document, out: &mut String) { n(rect[1].max(rect[3])) ); } + // Its `/NM`, or the name given above for this export. + if let Some(nm) = a.as_ref().and_then(|r| names.get(&r)).cloned().or_else(|| text_of(doc, &d, b"NM")) { + let _ = write!(attrs, " name=\"{}\"", esc(&nm)); + } for (key, attr) in [ - (&b"NM"[..], "name"), - (b"T", "title"), + (&b"T"[..], "title"), (b"Subj", "subject"), (b"M", "date"), (b"CreationDate", "creationdate"), @@ -81,7 +258,7 @@ fn xfdf_annots(doc: &Document, out: &mut String) { let name = |o: &Object| o.as_name().map(|x| String::from_utf8_lossy(x).into_owned()).unwrap_or_else(|| "None".into()); let _ = write!(attrs, " head=\"{}\" tail=\"{}\"", esc(&name(&le[0])), esc(&name(&le[1]))); } - // A callout: its leader path, text-box inset, intent and (single-name) line ending. + // A callout: its leader path, text-box inset and (single-name) line ending. let cl = nums_of(doc, d.get(b"CL")); if cl.len() == 4 || cl.len() == 6 { let _ = write!(attrs, " callout=\"{}\"", cl.iter().map(|v| n(*v)).collect::>().join(",")); @@ -89,54 +266,31 @@ fn xfdf_annots(doc: &Document, out: &mut String) { if rd.len() == 4 { let _ = write!(attrs, " fringe=\"{}\"", rd.iter().map(|v| n(*v)).collect::>().join(",")); } - if let Some(it) = d.get(b"IT").map(|o| doc.resolve(o)).and_then(|o| o.as_name().map(|x| String::from_utf8_lossy(x).into_owned())) { - let _ = write!(attrs, " intent=\"{}\"", esc(&it)); - } if let Some(le) = d.get(b"LE").map(|o| doc.resolve(o)).and_then(|o| o.as_name().map(|x| String::from_utf8_lossy(x).into_owned())) { let _ = write!(attrs, " head=\"{}\"", esc(&le)); } } + // What the comment is meant as (/IT): typewriter text, a callout, a cloud, an arrow. + if let Some(it) = d.get(b"IT").map(|o| doc.resolve(o)).and_then(|o| o.as_name().map(|x| String::from_utf8_lossy(x).into_owned())) { + let _ = write!(attrs, " intent=\"{}\"", esc(&it)); + } + // A cloudy border (/BE /S /C) and how far its arcs bulge. + if let Some(be) = + d.get(b"BE").map(|o| doc.resolve(o)).and_then(|o| o.as_dict().cloned()).filter(|b| b.name(b"S") == Some(b"C".as_slice())) + { + attrs.push_str(" style=\"cloudy\""); + if let Some(i) = be.get(b"I").and_then(Object::as_f64) { + let _ = write!(attrs, " intensity=\"{}\"", n(i)); + } + } if let Some(irt) = d.get(b"IRT").and_then(Object::as_ref).and_then(|r| names.get(&r)) { let _ = write!(attrs, " inreplyto=\"{}\"", esc(irt)); } + if own && let Some(keys) = own_keys(&d) { + let _ = write!(attrs, " pdfcraft:keys=\"{}\"", esc(&keys)); + } let _ = write!(out, "<{tag}{attrs}>"); - if let Some(c) = text_of(doc, &d, b"Contents") { - let _ = write!(out, "{}", esc(&c)); - } - if let Some(da) = text_of(doc, &d, b"DA") { - let _ = write!(out, "{}", esc(&da)); - } - let ink = d.get(b"InkList").map(|o| doc.resolve(o)).and_then(|o| o.as_array().cloned()).unwrap_or_default(); - if !ink.is_empty() { - out.push_str(""); - for g in ink { - let v = nums_of(doc, Some(&g)); - let pts: Vec = v.as_chunks::<2>().0.iter().map(|p| format!("{},{}", n(p[0]), n(p[1]))).collect(); - let _ = write!(out, "{}", pts.join(";")); - } - out.push_str(""); - } - // Polygon and polyline points (XFDF ``: `x,y;x,y;…`). - let vertices = nums_of(doc, d.get(b"Vertices")); - let pts: Vec = vertices.as_chunks::<2>().0.iter().map(|p| format!("{},{}", n(p[0]), n(p[1]))).collect(); - if !pts.is_empty() { - let _ = write!(out, "{}", pts.join(";")); - } - if let Some(pd) = d.get(b"Popup").map(|p| doc.resolve(p)).and_then(|p| p.as_dict().cloned()) { - let r = nums_of(doc, pd.get(b"Rect")); - if r.len() == 4 { - let open = matches!(pd.get(b"Open"), Some(Object::Bool(true))); - let _ = write!( - out, - "", - n(r[0]), - n(r[1]), - n(r[2]), - n(r[3]), - if open { "yes" } else { "no" } - ); - } - } + xfdf_annot_children(doc, out, &d, pi); let _ = writeln!(out, ""); } } @@ -191,12 +345,235 @@ fn xfdf_fields(doc: &Document, out: &mut String) { } } +/// XFDF with these comments alone (0-based page and position in the page's `/Annots`), each +/// with its replies: what Copy puts on the clipboard, and Export selected comments writes. +pub fn export_xfdf_comments(doc: &Document, which: &[(usize, usize)], file: &str) -> String { + let only = annots_with_replies(doc, which); + let mut out = format!( + "\n\n" + ); + out.push_str("\n"); + xfdf_annots(doc, &mut out, Some(&only), true); + out.push_str("\n"); + let _ = writeln!(out, "", esc(file)); + out.push_str("\n"); + out +} + +/// Whether `text` is XFDF with comments in it (what a paste of copied comments carries). +pub fn is_xfdf_comments(text: &str) -> bool { + let head: String = text.chars().take(400).collect(); + head.contains(" }, + /// A line or arrow, and a callout's leader. + Line { points: Vec<[f64; 2]>, color: Option<[f64; 3]> }, + /// A polygon (closed) or connected lines. + Poly { points: Vec<[f64; 2]>, closed: bool, color: Option<[f64; 3]> }, + /// Freehand strokes. + Ink { strokes: Vec>, color: Option<[f64; 3]> }, + /// Text markup: each quad is x1,y1 (upper left), x2,y2 (upper right), x3,y3 (lower left), x4,y4. + Quads { quads: Vec<[f64; 8]>, color: Option<[f64; 3]> }, + /// A text box's text, from the top-left of its box (`rect`, which it wraps in), at its font + /// `size` in points and in its text colour. + Text { rect: [f64; 4], text: String, size: f64, color: Option<[f64; 3]> }, +} + +/// Every comment element of an XFDF text, in order (none when it is no XFDF). +fn with_comment_nodes(text: &str, mut f: impl FnMut(roxmltree::Node<'_, '_>)) { + if check_form_xml_depth(text).is_err() { + return; + } + let Ok(xml) = parse_form_xml(text) else { return }; + let root = xml.root_element(); + if root.tag_name().name() != "xfdf" { + return; + } + let Some(annots) = root.children().find(|c| c.has_tag_name("annots")) else { return }; + for node in annots.children().filter(|c| c.is_element()) { + if SUBTYPES.iter().any(|(_, t)| *t == node.tag_name().name().to_ascii_lowercase()) { + f(node); + } + } +} + +/// The comment elements of an XFDF text: tag, `inreplyto`, rect. +fn comment_nodes(text: &str) -> Vec<(String, Option, Vec)> { + let mut out = Vec::new(); + with_comment_nodes(text, |node| { + out.push(( + node.tag_name().name().to_owned(), + node.attribute("inreplyto").map(str::to_owned), + node.attribute("rect").map(csv).unwrap_or_default(), + )); + }); + out +} + +/// Pairs `x,y;x,y;…` (XFDF vertices and ink gestures). +fn pairs(text: &str) -> Vec<[f64; 2]> { + text.split(';').filter_map(|p| if let [x, y] = csv(p).as_slice() { Some([*x, *y]) } else { None }).collect() +} + +/// The outlines of the comments in an XFDF text (replies, which have no place of their own, +/// left out): what a paste shows hanging on the pointer. +pub fn comment_outlines(text: &str) -> Vec { + let mut out = Vec::new(); + with_comment_nodes(text, |node| { + if node.attribute("inreplyto").is_some() { + return; + } + let tag = node.tag_name().name().to_ascii_lowercase(); + let color = node.attribute("color").and_then(parse_hex); + let rect = node + .attribute("rect") + .map(csv) + .filter(|r| r.len() == 4 && r.iter().all(|v| v.is_finite())) + .map(|r| [r[0].min(r[2]), r[1].min(r[3]), r[0].max(r[2]), r[1].max(r[3])]); + match tag.as_str() { + "line" => { + if let (Some(a), Some(b)) = (node.attribute("start").map(csv), node.attribute("end").map(csv)) + && let ([ax, ay], [bx, by]) = (a.as_slice(), b.as_slice()) + { + out.push(Outline::Line { points: vec![[*ax, *ay], [*bx, *by]], color }); + } + } + "polygon" | "polyline" => { + let points = node.children().find(|c| c.has_tag_name("vertices")).and_then(|v| v.text()).map(pairs).unwrap_or_default(); + if points.len() >= 2 { + out.push(Outline::Poly { points, closed: tag == "polygon", color }); + } + } + "ink" => { + let strokes: Vec> = node + .children() + .find(|c| c.has_tag_name("inklist")) + .map(|list| { + list.children().filter(|g| g.has_tag_name("gesture")).filter_map(|g| g.text()).map(pairs).filter(|s| !s.is_empty()).collect() + }) + .unwrap_or_default(); + if !strokes.is_empty() { + out.push(Outline::Ink { strokes, color }); + } + } + "highlight" | "underline" | "strikeout" | "squiggly" | "redact" => { + let coords = node.attribute("coords").map(csv).unwrap_or_default(); + let quads: Vec<[f64; 8]> = coords.as_chunks::<8>().0.to_vec(); + if quads.is_empty() { + if let Some(rect) = rect { + out.push(Outline::Rect { rect, round: false, color }); + } + } else { + out.push(Outline::Quads { quads, color }); + } + } + // A Fill & Sign mark: its tick, cross, dot or line, as its appearance draws it. + "stamp" + if let Some(rect) = rect + && let Some(mark) = node.attribute("icon").and_then(|i| pdfcraft_annot::FillMark::from_name(i.as_bytes())) => + { + if mark == pdfcraft_annot::FillMark::Dot { + let r = (rect[2] - rect[0]).min(rect[3] - rect[1]) * 0.3; + let (cx, cy) = ((rect[0] + rect[2]) / 2.0, (rect[1] + rect[3]) / 2.0); + out.push(Outline::Rect { rect: [cx - r, cy - r, cx + r, cy + r], round: true, color }); + } + for points in mark.strokes(rect) { + out.push(Outline::Line { points, color }); + } + } + _ => { + if let Some(rect) = rect { + out.push(Outline::Rect { rect, round: tag == "circle", color }); + } + // A callout's leader, from the box to what it points at. + let callout = node.attribute("callout").map(csv).filter(|c| c.len() == 4 || c.len() == 6); + if tag == "freetext" + && let Some(cl) = &callout + { + out.push(Outline::Line { points: cl.as_chunks::<2>().0.to_vec(), color }); + } + // A text box's text, in its own colour and size, inside the box (a callout's box + // is its rect inset by the fringe). + if tag == "freetext" + && let Some(rect) = rect + && let Some(text) = node.children().find(|c| c.has_tag_name("contents")).and_then(|c| c.text()).filter(|t| !t.trim().is_empty()) + { + let mut da = Dict::new(); + if let Some(s) = node.children().find(|c| c.has_tag_name("defaultappearance")).and_then(|c| c.text()) { + da.set(b"DA".to_vec(), PdfString::literal(s.as_bytes().to_vec())); + } + let (text_color, size) = pdfcraft_annot::appearance::parse_da(&da); + let fringe = node.attribute("fringe").map(csv).filter(|f| callout.is_some() && f.len() == 4 && f.iter().all(|v| v.is_finite())); + let rect = match fringe.as_deref() { + Some([l, b, r, t]) => [rect[0] + l, rect[1] + b, rect[2] - r, rect[3] - t], + _ => rect, + }; + out.push(Outline::Text { rect, text: text.replace("\r\n", "\n").replace('\r', "\n"), size, color: Some(text_color) }); + } + } + } + }); + out +} + +/// How many comments (replies not counted) an XFDF text holds. +pub fn comment_count(text: &str) -> usize { + comment_nodes(text).iter().filter(|(_, irt, _)| irt.is_none()).count() +} + +/// The box around every comment in an XFDF text, in the pages' user space: [x0, y0, x1, y1], +/// or `None` without a comment with a rect. What a paste shows hanging on the pointer and centres +/// on the point: replies, which have no place of their own (some tools still give them a rect, at +/// 0,0 or away from their parent), are left out, as [`comment_outlines`] leaves them out. +pub fn comments_bounds(text: &str) -> Option<[f64; 4]> { + let mut bounds: Option<[f64; 4]> = None; + for (_, in_reply_to, rect) in comment_nodes(text) { + if in_reply_to.is_some() { + continue; + } + if let [x0, y0, x1, y1] = rect.as_slice() + && rect.iter().all(|v| v.is_finite()) + { + let r = [x0.min(*x1), y0.min(*y1), x0.max(*x1), y0.max(*y1)]; + bounds = Some(match bounds { + None => r, + Some(b) => [b[0].min(r[0]), b[1].min(r[1]), b[2].max(r[2]), b[3].max(r[3])], + }); + } + } + bounds +} + +/// Move an annotation's geometry by (dx, dy) in user space: its rect, quads, vertices, ink, +/// line ends and callout leader (what a copy placed elsewhere needs). +fn translate(d: &mut Dict, dx: f64, dy: f64) { + let shift = |o: &Object| -> Option { + let v: Vec = o.as_array()?.iter().map(|x| x.as_f64()).collect::>>()?; + Some(arr(&v.iter().enumerate().map(|(i, x)| if i % 2 == 0 { x + dx } else { x + dy }).collect::>())) + }; + for key in [&b"Rect"[..], b"QuadPoints", b"Vertices", b"L", b"CL"] { + if let Some(moved) = d.get(key).and_then(shift) { + d.set(key.to_vec(), moved); + } + } + if let Some(Object::Array(strokes)) = d.get(b"InkList").cloned() { + let moved: Vec = strokes.iter().map(|s| shift(s).unwrap_or_else(|| s.clone())).collect(); + d.set(b"InkList".to_vec(), Object::Array(moved)); + } +} + /// XFDF with the document's comments and/or field values. pub fn export_xfdf(doc: &Document, comments: bool, fields: bool, file: &str) -> String { let mut out = String::from("\n\n"); if comments { out.push_str("\n"); - xfdf_annots(doc, &mut out); + xfdf_annots(doc, &mut out, None, false); out.push_str("\n"); } if fields { @@ -311,6 +688,16 @@ fn annot_from_xml(node: roxmltree::Node, subtype: &str, page_ref: ObjRef) -> Opt Object::Array(vec![Object::name(node.attribute("head").unwrap_or("None")), Object::name(node.attribute("tail").unwrap_or("None"))]), ); } + if let Some(it) = node.attribute("intent").filter(|it| !it.is_empty()) { + d.set(b"IT".to_vec(), Object::name(it)); + } + if node.attribute("style") == Some("cloudy") { + let mut be = Dict::new(); + be.set(b"S".to_vec(), Object::name("C")); + let intensity = node.attribute("intensity").and_then(|i| i.parse::().ok()).filter(|i| i.is_finite()).unwrap_or(1.0); + be.set(b"I".to_vec(), Object::Real(intensity.clamp(0.0, 2.0))); + d.set(b"BE".to_vec(), Object::Dict(be)); + } if let Some(cl) = node.attribute("callout").map(csv).filter(|c| c.len() == 4 || c.len() == 6) { d.set(b"CL".to_vec(), arr(&cl)); if let Some(rd) = node.attribute("fringe").map(csv).filter(|r| r.len() == 4) { @@ -352,6 +739,7 @@ fn annot_from_xml(node: roxmltree::Node, subtype: &str, page_ref: ObjRef) -> Opt _ => {} } } + read_own_keys(node, &mut d); Some(d) } @@ -472,7 +860,32 @@ pub(crate) fn add_popup(doc: &mut Document, page: ObjRef, parent: ObjRef, rect: Ok(()) } +/// Import XFDF comments as copies: every comment lands on `page` (0-based; its own page when +/// `None`) and, with `fresh` (a prefix unique to this import), under a new name, so nothing in +/// the document is replaced and pasting the same comments twice gives two copies. Replies keep +/// their thread among the pasted comments; a reply to a comment that was not copied stands +/// alone. Field values in the file are ignored. +pub fn import_comments( + doc: &mut Document, + text: &str, + page: Option, + fresh: Option<&str>, + offset: Option<[f64; 2]>, +) -> Result { + import_xfdf_with(doc, text, page, fresh, offset.filter(|o| o.iter().all(|v| v.is_finite()))) +} + pub(crate) fn import_xfdf(doc: &mut Document, text: &str) -> Result { + import_xfdf_with(doc, text, None, None, None) +} + +fn import_xfdf_with( + doc: &mut Document, + text: &str, + target_page: Option, + fresh: Option<&str>, + offset: Option<[f64; 2]>, +) -> Result { check_form_xml_depth(text)?; let xml = parse_form_xml(text).map_err(|e| DataError::Malformed(e.to_string()))?; let root = xml.root_element(); @@ -481,28 +894,65 @@ pub(crate) fn import_xfdf(doc: &mut Document, text: &str) -> Result = pdfcraft_model::pages(doc).iter().map(|p| p.obj).collect(); + if let Some(p) = target_page + && p >= pages.len() + { + return Err(DataError::Malformed(format!("page {} is not in the document", p + 1))); + } // Comments: parents first, then replies (whose /IRT needs the parent's reference). let mut by_name: HashMap = HashMap::new(); let mut later: HashMap = HashMap::new(); + // Fresh names: every copy gets one from its own position, and `name` → the first copy's, + // so replies find their copied parent (a name two source comments share, as other tools + // write, still gives two copies). + let mut renamed: HashMap = HashMap::new(); + if let (Some(prefix), Some(annots)) = (fresh, root.children().find(|c| c.has_tag_name("annots"))) { + for (n, node) in annots.children().filter(|c| c.is_element()).enumerate() { + if let Some(nm) = node.attribute("name") { + renamed.entry(nm.to_string()).or_insert_with(|| format!("{prefix}-{}", n + 1)); + } + } + } if let Some(annots) = root.children().find(|c| c.has_tag_name("annots")) { - for node in annots.children().filter(|c| c.is_element()) { + for (n, node) in annots.children().filter(|c| c.is_element()).enumerate() { let tag = node.tag_name().name().to_ascii_lowercase(); let Some((subtype, _)) = SUBTYPES.iter().find(|(_, t)| *t == tag) else { continue }; - let Some(page) = node.attribute("page").and_then(|p| p.parse::().ok()).and_then(|p| pages.get(p).copied()) else { continue }; - let Some(d) = annot_from_xml(node, subtype, page) else { continue }; + let own_page = node.attribute("page").and_then(|p| p.parse::().ok()); + let Some(page) = target_page.or(own_page).and_then(|p| pages.get(p).copied()) else { continue }; + let Some(mut d) = annot_from_xml(node, subtype, page) else { continue }; + let assigned = fresh.map(|prefix| format!("{prefix}-{}", n + 1)); + if let Some(name) = &assigned { + // A copy under a name of its own (one even where the original had none). + d.set(b"NM".to_vec(), PdfString::text(name)); + } + if let Some([dx, dy]) = offset { + translate(&mut d, dx, dy); + } let r = place(doc, page, d)?; // A later same-name record replaces this comment, including its reply relation. later.remove(&r); - if let Some(nm) = node.attribute("name") { - by_name.insert(nm.to_string(), r); + if let Some(name) = assigned.clone().or_else(|| node.attribute("name").map(str::to_owned)) { + by_name.insert(name, r); } if let Some(irt) = node.attribute("inreplyto") { - later.insert(r, irt.to_string()); + match (fresh, renamed.get(irt)) { + // The parent is among the copies: the reply follows it. + (Some(_), Some(parent)) => later.insert(r, parent.clone()), + // A copy's parent was not copied: the reply stands alone rather than + // attaching to a same-named comment the document happens to have. + (Some(_), None) => None, + (None, _) => later.insert(r, irt.to_string()), + }; } // A pop-up window. if let Some(pop) = node.children().find(|c| c.has_tag_name("popup")) - && let Some(rect) = pop.attribute("rect").map(csv).filter(|r| r.len() == 4) + && let Some(mut rect) = pop.attribute("rect").map(csv).filter(|r| r.len() == 4) { + if let Some([dx, dy]) = offset { + for (i, v) in rect.iter_mut().enumerate() { + *v += if i % 2 == 0 { dx } else { dy }; + } + } add_popup(doc, page, r, &rect, pop.attribute("open") == Some("yes"))?; } report.comments += 1; @@ -513,6 +963,10 @@ pub(crate) fn import_xfdf(doc: &mut Document, text: &str) -> Result **Last reviewed:** 2026-10-28 | **Last updated:** 2026-10-28 | **Change:** minor (M10, #4119 partial: Enhance scanned file cleans scanned pages in place: background removal, edge shadow removal, despeckle; estimates unchanged) > **Last reviewed:** 2026-10-25 · **Last updated:** 2026-10-25 · **Change:** minor (gaps 5 and 14 current state: native release Hans face via LXGW WenKai Lite; Arabic logical catalog, explicit RTL direction and mirrored stock chrome, with native/browser/IME workflows unqualified; no readiness re-estimate) > **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-15 · **Change:** minor (gap 3: #2517 a blank line stays a paragraph break; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) @@ -173,7 +174,11 @@ update the numbers. ### 7. OCR reads only unaccented Latin - **Missing:** every other script and accented Latin; editable-text output; deskew, rotation, - despeckle, background and camera clean-up; MRC compression; pluggable engines; OCR in the + despeckle and background clean-up as options of Recognize text (Enhance scanned file removes + the background and edge shadows on grey and RGB scans and despeckles those and 1-bit ones; it + reports CMYK, indexed, JPEG 2000, JBIG2 and CCITT scans and scans drawn inside Form XObjects + and leaves them as they are); MRC compression; + pluggable engines; OCR in the browser; scanner input. - **Evidence:** `ocr.ocr-languages` partial ("the models read only the Latin alphabet without accents"); 19 of 26 OCR features planned; [#744](https://github.com/storytold/pdfcraft/issues/744) diff --git a/docs/hardware-parity.md b/docs/hardware-parity.md index da2b8d92b1..455c7bcd78 100644 --- a/docs/hardware-parity.md +++ b/docs/hardware-parity.md @@ -1,6 +1,6 @@ # Hardware and platform parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (web printing through the browser's print dialog; earlier: shared craft-print spooler with passing Windows CI; Windows printers row and 1:1 touchpad scrolling, #756, #759) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (web OCR shipped: Web Workers with SIMD, checked in a browser; earlier: web printing through the browser's print dialog; earlier: shared craft-print spooler with passing Windows CI; Windows printers row and 1:1 touchpad scrolling, #756, #759) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) Each hardware feature Acrobat Pro uses, per platform, ours against Acrobat's. Acrobat runs on macOS and Windows (plus a limited web viewer/editor in Document Cloud); PdfCraft runs on macOS, Windows, @@ -34,12 +34,14 @@ printing are counted in their feature areas). **Platforms ≈ 70% ready, 15–30 | Windows (x64, x86, ARM64) | yes (x64, ARM64) | yes | signed installers | — | | Linux | no | yes | AppImage, deb, rpm, Flatpak, tarball | Chinese faces in Flatpak (#689) | | FreeBSD | no | yes | package | — | -| Web | Acrobat online (cloud) | yes (WASM, offline in the browser) | static site | OCR, crash recovery, PKCS #12 signing, hosted deployment | +| Web | Acrobat online (cloud) | yes (WASM, offline in the browser) | static site | OCR as fast as native (Web Workers with SIMD read a page about 17× slower), crash recovery, PKCS #12 signing, hosted deployment | ## Revision history | Date | Change | Summary | |---|---|---| +| 2026-10-12 | minor | Web OCR (`ocr.ocr-web`) shipped after a browser check | +| 2026-10-12 | minor | Web platform row: OCR downloads its models on first use and reads in Web Workers with SIMD (`ocr.ocr-web`, partial); what's left is speed | | 2026-10-11 | minor | Web printers row: printing through the browser's print dialog (gap 4 closed); Web platform row no longer lists printing | | 2026-10-10 | major | Created from the code, `vendor/README.md`, `.github/workflows/release.yml`, the print crate's README and user issues | | 2026-10-10 | minor | Windows printing shipped (#756): the Windows row's gap is closed; the web row keeps "printing" | diff --git a/docs/images/pdfcraft-ocr-verify.png b/docs/images/pdfcraft-ocr-verify.png new file mode 100644 index 0000000000..14a5b3dd5c Binary files /dev/null and b/docs/images/pdfcraft-ocr-verify.png differ diff --git a/docs/localization-parity.md b/docs/localization-parity.md index 1944575ce3..4e30fcdf0a 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -1,24 +1,32 @@ # Localization parity -> **Last reviewed:** 2026-11-03 · **Last updated:** 2026-11-03 · **Change:** minor (Turkish: 2,867 entries, 0 missing in `cargo xtask i18n-report` on 2026-11-03; AI-assisted) -> **Last reviewed:** 2026-10-25 · **Last updated:** 2026-10-25 · **Change:** minor (Persian (`fa`, 2,785 entries) added right-to-left like Arabic and Kurdish; Arabic current state corrected: catalogs stay logical, RTL labels take an explicit direction and stock chrome mirrors; native/browser RTL input, IME and complex workflows remain unqualified. Native release Hans face via LXGW WenKai Lite; release-pinned craft-fonts still has no Telugu face. Current union 2,798 keys. Scores and estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) +> **Last reviewed:** 2026-11-03 · **Last updated:** 2026-11-03 · **Change:** minor (every catalog restated from `cargo xtask i18n-report` on `main` at `14ae839a`: 2,897 strings in the report's denominator; Hindi, Indonesian, Korean, Thai, Lao and Central Kurdish listed with their catalogs. Scores and estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) Per-language status of PdfCraft's interface against Acrobat Pro's. How translations work and how to add a language: [localization.md](localization.md). Summary in [ROADMAP.md](../ROADMAP.md). ## How this was measured -- **Strings translated** is measured: the catalogs in `crates/ui-egui/src/i18n/.tsv` (since moved to craft-i18n's `locales//`), counted - as distinct `(context, English)` keys with a non-empty translation. The denominator is the union of - keys across all catalogs, counting language-specific context rows such as `theme`/`Appearance`. - **That union is a moving target**: every change that adds a string widens it, so a count is only - comparable with others measured on the same date, and a catalog that reached 100% falls behind - without losing a single translation. Every figure here therefore carries its date. The current - union is **2,798** keys as of 2026-10-25, against the **2,143-key snapshot** of 2026-10-10; earlier - revisions quote the union of their own date (see the revision history), not this one. The - twelve-language percentage table below still quotes the 2026-10-10 snapshot for every language - except Spanish, Vietnamese, Lithuanian and Brazilian Portuguese, which are measured against the current union. English is +- **Strings translated** is measured with `cargo xtask i18n-report`, which CI also runs: the + English strings in its denominator, less the ones a catalog has no translation for. The + denominator is the UI source's literals, the command, All tools and engine wordings, and every + key of every bundled catalog, which brings in the contextual, plural and dynamically looked-up + strings the scan can't see. On `main` at `14ae839a` (2026-11-03) that is **2,897** strings, and + every count and percentage in the tables below is of that number. A key only some catalogs have + counts as missing in the others; a contextual one (such as `theme`/`Appearance`) is met by a + plain translation of the same English, but a `@plural` one only by its own plural forms. The + denominator still grows whenever the interface gains a string, so a catalog that was complete + falls behind without losing a translation, and every figure here carries its date. English is built in. +- **Catalogs:** the figures above were taken from the in-repo catalogs + (`crates/ui-egui/src/i18n/.tsv`), which have since moved to craft-i18n's `locales//`. + The report reads the catalogs the build embeds, so measuring again needs `CRAFT_I18N_DIR` set to a + craft-i18n checkout at the commit in `craft-i18n.rev`; without it the report says it was + skipped. +- **Before 2026-11-03** this page counted distinct `(context, English)` keys against the union of + keys across all catalogs: 2,143 keys on 2026-10-10, 2,798 on 2026-10-25, and 2,895 on + `14ae839a`. That union also grew whenever one catalog added a key no other had, so it moved every + row at once. Earlier revisions in the history quote it, at the size of their own date. - **Not counted:** error details from the engine and the operating system, which are shown in English inside a translated frame; there is no translated user documentation or help yet (Acrobat ships localized help). @@ -32,64 +40,71 @@ add a language: [localization.md](localization.md). Summary in [ROADMAP.md](../R ## The twelve key languages -| Language | Code | UI strings translated | Dialogs / tooltips / help | Script support | Native review | Status | Acrobat ships it | To `full` (h) | +| Language | Code | UI strings translated (of 2,897, 2026-11-03) | Dialogs / tooltips / help | Script support | Native review | Status | Acrobat ships it | To `full` (h) | |---|---|---|---|---|---|---|---|---| | English | `en` | built in (100%) | all; README and docs | Latin | yes | **full** | yes | 0 | -| Simplified Chinese | `zh-hans` | 2,088 (97.4%) | dialogs and tooltips through `tl!`; no help | CJK text and IME work; native craft-fonts builds embed the **LXGW WenKai Lite** Hans face, while the web build has no Hans face beyond BIZ UDPGothic, and missing-glyph reports (#826, #688, #728, #758; Flatpak #689) predate the native face | no | partial | yes | 6–12 | -| Spanish | `es` | 2,798 (100% of current key union) | dialogs and tooltips; no help | Latin | no | partial | yes | 2–4 | -| Hindi | `hi` | 0 | none | Devanagari not tested (egui shapes Telugu conjuncts, so shaping is likely workable); the pinned craft-fonts manifest has Noto Sans Devanagari Regular; rendering remains unqualified | no | none | no | 10–18 | -| Arabic | `ar` | 2,143 (100%) | dialogs and tooltips; no help | Logical-order catalog; egui 0.37's epaint reorders with UAX #9, Arabic, Kurdish and Persian labels use an explicit right-to-left paragraph direction, and stock chrome is mirrored. Native/browser RTL input and IME, and complex bidirectional workflows, are not yet qualified (`localization.md` §Arabic) | no | partial | yes (Middle East edition) | 25–45 | -| French | `fr` | 2,128 (99.3%) | dialogs and tooltips; no help | Latin | no | partial | yes | 1–2 | -| Portuguese (Brazil) | `pt-br` | 2,798 (100% of current union) | dialogs and tooltips; no help | Latin | community (#288) | partial | yes | 1–2 | -| Indonesian | `id` | 0 | none | Latin | no | none | no | 3–5 | -| Japanese | `ja` | 2,123 (99.1%) | dialogs and tooltips; no help | BIZ UDPGothic from craft-fonts; IME patched on macOS, Windows, X11, Wayland; vertical text renders but can't be edited | no | partial | yes | 4–8 | -| German | `de` | 2,128 (99.3%) | dialogs and tooltips; no help | Latin | community, in progress (#828: terminology, clipping) | partial | yes | 2–4 | -| Korean | `ko` | 0 | none | Hangul IME patched (Microsoft and Apple Korean, `vendor/README.md` winit 3, 6, 9, 10); no Hangul UI face in craft-fonts | no | none | yes | 6–10 | -| Vietnamese | `vi` | 2,798 (100% of current key union) | dialogs and tooltips; no help | Latin with stacked diacritics; bundled Inter covers them (fonts test) | community | partial | no | 2–4 | +| Simplified Chinese | `zh-hans` | 2,560 (88.4%) | dialogs and tooltips through `tl!`; no help | CJK text and IME work; native craft-fonts builds embed the **LXGW WenKai Lite** Hans face, while the web build has no Hans face beyond BIZ UDPGothic, and missing-glyph reports (#826, #688, #728, #758; Flatpak #689) predate the native face | no | partial | yes | 6–12 | +| Spanish | `es` | 2,851 (98.4%) | dialogs and tooltips; no help | Latin | no | partial | yes | 2–4 | +| Hindi | `hi` | 2,825 (97.5%) | dialogs and tooltips; no help | Devanagari not tested (egui shapes Telugu conjuncts, so shaping is likely workable); the pinned craft-fonts manifest has Noto Sans Devanagari Regular; rendering remains unqualified | no | partial | no | 10–18 | +| Arabic | `ar` | 2,836 (97.9%) | dialogs and tooltips; no help | Logical-order catalog; egui 0.37's epaint reorders with UAX #9, Arabic, Kurdish and Persian labels use an explicit right-to-left paragraph direction, and stock chrome is mirrored. Native/browser RTL input and IME, and complex bidirectional workflows, are not yet qualified (`localization.md` §Arabic) | no | partial | yes (Middle East edition) | 25–45 | +| French | `fr` | 2,851 (98.4%) | dialogs and tooltips; no help | Latin | no | partial | yes | 1–2 | +| Portuguese (Brazil) | `pt-br` | 2,851 (98.4%) | dialogs and tooltips; no help | Latin | community (#288) | partial | yes | 1–2 | +| Indonesian | `id` | 2,825 (97.5%) | dialogs and tooltips; no help | Latin | no | partial | no | 3–5 | +| Japanese | `ja` | 2,597 (89.6%) | dialogs and tooltips; no help | BIZ UDPGothic from craft-fonts; IME patched on macOS, Windows, X11, Wayland; vertical text renders but can't be edited | no | partial | yes | 4–8 | +| German | `de` | 2,600 (89.7%) | dialogs and tooltips; no help | Latin | community, in progress (#828: terminology, clipping) | partial | yes | 2–4 | +| Korean | `ko` | 2,825 (97.5%) | dialogs and tooltips; no help | Hangul IME patched (Microsoft and Apple Korean, `vendor/README.md` winit 3, 6, 9, 10); no Hangul UI face in craft-fonts | no | partial | yes | 6–10 | +| Vietnamese | `vi` | 2,851 (98.4%) | dialogs and tooltips; no help | Latin with stacked diacritics; bundled Inter covers them (fonts test) | community | partial | no | 2–4 | -**Localization dimension ≈ 68%**: the mean over the twelve, counting a catalog's percentage but only -60% for Chinese and Arabic until their script problems are fixed, and 0 for Hindi, Indonesian and -Korean (still missing). Vietnamese moved from 0% to 100% in that mean (59% → 68%). **≈ 55–110 h** +**Localization dimension ≈ 68%** (scored 2026-10-13, before the counts above): the mean over the +twelve, counting a catalog's percentage of the 2,143-key snapshot but only 60% for Chinese and +Arabic until their script problems are fixed, and 0 for Hindi, Indonesian and Korean, which had no +catalog yet. Vietnamese moved from 0% to 100% in that mean (59% → 68%). Hindi, Indonesian and +Korean now have catalogs, so the score understates them. It is not re-scored here, because that +decides how their script caveats count (Devanagari rendering unqualified, no Hangul UI face). **≈ 55–110 h** to bring all twelve to `full`, of which ≈ 35–60 h is engineering (remaining RTL qualification, a web Hans face, bidi text) and the rest catalogs and fixes; native-speaker review needs humans. No language is `full` except English: every catalog leaves engine error details in English, and -none has translated help. The **To `full`** hours of Vietnamese and Spanish are therefore residual -help/docs polish (catalog and script work are already done), not missing UI strings. +none has translated help. The **To `full`** hours of Vietnamese and Spanish are therefore mostly +help/docs polish: their script work is done, and each catalog lacks 46 strings on 2026-11-03. ## Other shipped languages -| Language | Code | UI strings translated | Status | Acrobat ships it | +| Language | Code | UI strings translated (of 2,897, 2026-11-03) | Status | Acrobat ships it | |---|---|---|---|---| -| Traditional Chinese | `zh-hant` | 2,016 (94.1%) | partial (same Hans/Hant face caveat) | yes | -| Russian | `ru` | 2,043 (95.3%) | partial | yes | -| Bulgarian | `bg` | 2,094 (97.7%) | partial | no | -| Czech | `cs` | 2,794 entries (all but four keys of the current union; the engine's sentences, weekdays and certificate fields) | partial | yes | -| Telugu | `te` | 2,058 (96.0%) | partial (the release-pinned craft-fonts has no Telugu face yet) | no | -| Hungarian | `hu` | 2,126 (99.2%) | partial | yes | -| Ukrainian | `uk` | 2,142 (100%) | partial | yes | -| Italian | `it` | 2,736 entries (all but 62 keys of the current union) | partial | yes | -| Kazakh | `kk` | 2,539 entries | partial | no | -| Polish | `pl` | 2,611 entries | partial | yes | -| Slovenian | `sl` | 2,536 entries | partial | no | -| Greek | `el` | 2,545 entries | partial | no | -| Turkish | `tr` | 2,867 entries (0 missing in `cargo xtask i18n-report`, 2026-11-03) | partial; AI-assisted, judgement calls reviewed by a native speaker, full proofreading pending | yes | -| Lithuanian | `lt` | 2,798 (100% of current key union) | partial; community native-speaker review ([#1401](https://github.com/storytold/pdfcraft/pull/1401)) | no | -| Finnish | `fi` | 2,798 entries (every current key) | partial; AI-assisted, native proofreading pending | yes | -| Albanian | `sq` | 2,524 entries | partial; not yet reviewed by a native speaker | no | -| Catalan | `ca` | 2,798 entries | partial: no translated help, engine error details in English; native-speaker review by the contributor | no | -| Romanian | `ro` | 2,798 entries (every current key) | partial; AI-assisted, not yet reviewed line by line by a native speaker | no | -| Norwegian Bokmål | `nb` | 2,707 entries | partial; native-speaker terminology review by the contributor ([#1836](https://github.com/storytold/pdfcraft/pull/1836)), not yet line by line | yes | -| Persian | `fa` | 2,785 entries (all but 13 keys of the current 2,798-key union) | partial; AI-assisted, not yet reviewed line by line by a native speaker | no | +| Traditional Chinese | `zh-hant` | 2,559 (88.3%) | partial (same Hans/Hant face caveat) | yes | +| Russian | `ru` | 2,515 (86.8%) | partial | yes | +| Bulgarian | `bg` | 2,564 (88.5%) | partial | no | +| Czech | `cs` | 2,847 (98.3%) | partial | yes | +| Telugu | `te` | 2,513 (86.7%) | partial (the release-pinned craft-fonts has no Telugu face yet) | no | +| Hungarian | `hu` | 2,599 (89.7%) | partial | yes | +| Ukrainian | `uk` | 2,600 (89.7%) | partial | yes | +| Italian | `it` | 2,790 (96.3%) | partial | yes | +| Kazakh | `kk` | 2,609 (90.1%) | partial | no | +| Polish | `pl` | 2,681 (92.5%) | partial | yes | +| Slovenian | `sl` | 2,618 (90.4%) | partial | no | +| Greek | `el` | 2,615 (90.3%) | partial | no | +| Turkish | `tr` | 2,895 (99.9%) | partial; AI-assisted, judgement calls reviewed by a native speaker, full proofreading pending | yes | +| Lithuanian | `lt` | 2,851 (98.4%) | partial; community native-speaker review ([#1401](https://github.com/storytold/pdfcraft/pull/1401)) | no | +| Finnish | `fi` | 2,849 (98.3%) | partial; AI-assisted, native proofreading pending | yes | +| Albanian | `sq` | 2,604 (89.9%) | partial; not yet reviewed by a native speaker | no | +| Catalan | `ca` | 2,849 (98.3%) | partial: no translated help, engine error details in English; native-speaker review by the contributor | no | +| Romanian | `ro` | 2,849 (98.3%) | partial; AI-assisted, not yet reviewed line by line by a native speaker | no | +| Norwegian Bokmål | `nb` | 2,758 (95.2%) | partial; native-speaker terminology review by the contributor ([#1836](https://github.com/storytold/pdfcraft/pull/1836)), not yet line by line | yes | +| Persian | `fa` | 2,801 (96.7%) | partial; AI-assisted, not yet reviewed line by line by a native speaker | no | +| Central Kurdish (Sorani) | `ckb` | 2,754 (95.1%) | partial; right-to-left like Arabic | no | +| Thai | `th` | 2,751 (95.0%) | partial | no | +| Lao | `lo` | 2,504 (86.4%) | partial | no | -**20 other languages** ship (34 catalogs plus English = **35** interface languages in Preferences). -The entry counts without percentages are as of 2026-10-25; they are not percentages of the older 2,143-key snapshot above. Their presence does not imply a detailed linguistic or runtime review. +**23 other languages** ship (34 catalogs plus English = **35** interface languages in Preferences). +Their presence does not imply a detailed linguistic or runtime review. Acrobat languages PdfCraft lacks: Danish, Dutch, Swedish, Hebrew. Vietnamese is shipped here but not by Acrobat. ## Revision history | Date | Change | Summary | |---|---|---| +| 2026-11-03 | minor | Every catalog restated from `cargo xtask i18n-report` on `14ae839a` (of the 2,897 strings in the report's denominator), replacing the catalog-union measure, which had reached 2,895 keys and left rows "100%" that were 44 short (#5805). Hindi, Indonesian and Korean restated from 0 to their catalogs, and Thai, Lao and Central Kurdish added to the table. The ≈ 68% score is not re-scored | | 2026-10-25 | minor | Arabic current state corrected: catalogs stay logical, Arabic/Kurdish labels take an explicit RTL direction, and stock chrome mirrors; native/browser RTL input, IME and complex workflows remain unqualified. Native Simplified Chinese uses LXGW WenKai Lite; the web build still has no Hans face and the release-pinned craft-fonts still has no Telugu face. Scores and estimates unchanged | | 2026-10-25 | minor | Added Persian (`fa`, 2,785 entries, all but 13 of the current 2,798-key union); right-to-left like Arabic and Kurdish, with logical entries reordered by egui 0.37's epaint; AI-assisted, not yet native-reviewed | | 2026-10-21 | minor | Brazilian Portuguese (`pt-br`) now covers all 2,674 distinct keys in the current catalog union; no help or engine/OS error-detail localization; twelve-language mean still rounds to ≈ 68% | diff --git a/docs/localization.md b/docs/localization.md index 2f9e98a676..a7860fa1c0 100644 --- a/docs/localization.md +++ b/docs/localization.md @@ -1,6 +1,6 @@ # Interface language -Choose **Menu > Edit > Preferences…** (Command-comma on macOS, Ctrl-comma elsewhere) **> Interface language** and select **Auto**, **English**, **日本語**, **简体中文**, **繁體中文**, **Čeština**, **Português (Brasil)**, **Deutsch**, **Español**, **Français**, **Русский**, **Қазақша**, **Български**, **తెలుగు**, **Magyar**, **Українська**, **Italiano**, **العربية**, **کوردیی ناوەندی**, **فارسی**, **Polski**, **Slovenščina**, **Ελληνικά**, **Türkçe**, **Lietuvių**, **Tiếng Việt**, **Suomi**, **Shqip**, **Català**, **Română** or **Norsk bokmål**. The change applies immediately and persists between launches. Command ids, document contents and file names are unchanged. +Choose **Menu > Edit > Preferences…** (Command-comma on macOS, Ctrl-comma elsewhere) **> Interface language** and select **Auto**, **English**, **日本語**, **简体中文**, **繁體中文**, **Čeština**, **Português (Brasil)**, **Deutsch**, **Español**, **Français**, **Русский**, **Қазақша**, **Български**, **తెలుగు**, **Magyar**, **Українська**, **Italiano**, **العربية**, **کوردیی ناوەندی**, **فارسی**, **Polski**, **Slovenščina**, **Ελληνικά**, **Türkçe**, **Lietuvių**, **Tiếng Việt**, **Suomi**, **Shqip**, **Català**, **Română** or **Norsk bokmål**. The change applies immediately and persists between launches. Command ids, document contents and file names are unchanged. The status caption PdfCraft draws into a named-layer signature appearance ("Signature valid" and its siblings) follows it too, on screen, in exports and in print; it is never saved. It is drawn in Helvetica, or shaped in embedded Inter, or shaped in a craft-fonts face for Arabic, Japanese, Telugu, Thai and Devanagari when the build has one (drawn as outlines), and otherwise in English, as Chinese always is; saving never writes it. Displayed keyboard shortcuts use the same active interface language. For example, German shows `Strg+K` and `Strg+Umschalt+S` in the toolbar, menus, command palette, tooltips and Keyboard shortcuts dialog; macOS keeps its modifier symbols (`⌘`, `⇧`, `⌃`). Key names have their own `key` catalog context, so the Delete key can read `Entf` while the command remains `Löschen`. A missing key-name entry falls back to the English key name. Actual bindings and machine-facing command ids remain stable. @@ -20,7 +20,7 @@ Whole sentences that the layers below word — what the accessibility checker fo The Ukrainian catalog has 2,530 entries, written from the meaning of the English labels using Ukrainian computing vocabulary. Commands use infinitives; fixed-count messages use count-neutral wording. Plural-aware messages use the three [CLDR Ukrainian integer forms](https://www.unicode.org/cldr/charts/48/supplemental/language_plural_rules.html#uk): `1 сторінка`, `2 сторінки`, `5 сторінок`; 11–14 take the third form, while 21 and 22 return to the first and second. Calendar dates use genitive month names (`9 жовтня 2026`), while headings use standalone names (`Жовтень 2026`). Home recommendation cards grow to fit translated text; the toolbar search hint truncates before the shortcut, with the full label available on hover. -The Lithuanian catalog has 2,798 entries and three integer plural forms. Calendar headings use standalone month names; dates use genitive forms. Calendar, inspector and password-dialog UI regressions check dynamic labels and preserve document values. A native Lithuanian-speaking contributor has reviewed the translation. +The Lithuanian catalog has 2,798 entries and three integer plural forms. Calendar headings use standalone month names; dates use genitive forms. Calendar, inspector and password-dialog UI regressions check dynamic labels and preserve document values. A native Lithuanian-speaking contributor has reviewed the translation. The Scan & OCR review strings added afterwards (M10: the OCR Verify screen, the batch options and the engine choice) were written without native review. The Vietnamese catalog has 2,798 entries and one plural form (`plural_none`), covering the current catalog-key union. Preferences theme `Appearance` uses context `theme` → `Giao diện`; object/comment/signature appearance stays plain `Diện mạo`. Form fields use `Trường`; redaction apply wording states permanent removal. Calendar months are numeric (`Tháng 1`…`12` / `tháng 1`…`12`). A native Vietnamese-speaking contributor has reviewed the translation. Status stays `partial`: no translated help, and engine/OS details stay English inside a translated frame. diff --git a/docs/pdf-spec-parity.md b/docs/pdf-spec-parity.md index aefc959698..e25c30067e 100644 --- a/docs/pdf-spec-parity.md +++ b/docs/pdf-spec-parity.md @@ -2,7 +2,7 @@ > Integration update 2026-10-30: lossless JBIG2 generic and exact-copy symbol coding are included; no lossy substitution. Combined tests passed at bdf5620; the subsequent review fixes require fresh CI. -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (§7.4: JBIG2 decoding in `pdfcraft-filters`, so redaction clears JBIG2 scans; earlier, §7.4: redaction decodes CCITT fax images, Group 3 on bytes aside) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-05 · **Change:** minor (subset standards: the PDF/A rows name 2b and 3b only, and the PDF/X row says what is there — the identification is carried through Combine and can be removed; no verifier, no converter) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) How much of ISO 32000-2 (PDF 2.0) and the PDF subset standards PdfCraft implements, set against what Acrobat Pro implements. This is PdfCraft's equivalent of a "geometry" or "codec" checklist: @@ -121,7 +121,7 @@ are borrowed; PDF 2.0 additions and most subset standards are missing. **150–2 | PDF/A-1a/b (ISO 19005-1) | verify, convert | none | | | PDF/A-2a/b/u, PDF/A-3a/b/u | verify, convert | **partial**: 2b and 3b structural rules and conversion (sRGB intent; not CMYK) | | | PDF/A-4, 4e, 4f | verify, convert | none | | -| PDF/X-1a, X-3, X-4 (ISO 15930) | verify, convert | none | | +| PDF/X-1a, X-3, X-4 (ISO 15930) | verify, convert | **partial**: PDF/X declarations are carried through Combine and can be removed (`standards_remove_id`, which matches the `pdfxid:` prefix only); no verifier, no converter | | | PDF/UA-1, PDF/UA-2 (ISO 14289) | verify (Preflight), checker | Accessibility Checker (32 rules, Acrobat's own), no PDF/UA verification | | | PDF/E, PDF/VT | verify, convert | none | | | Preflight (profiles, fixups, reports) | hundreds of profiles | none | | @@ -132,6 +132,7 @@ are borrowed; PDF 2.0 additions and most subset standards are missing. **150–2 | Date | Change | Summary | |---|---|---| +| 2026-11-05 | minor | Subset standards: the PDF/A rows name 2b and 3b only, and the PDF/X row says what is there — the identification is carried through Combine and can be removed; no verifier, no converter. No feature status changed | | 2026-10-30 | minor | §7.4: JBIG2 decoding (`hayro-jbig2`) in `pdfcraft-filters`, which `Document::decoded_image` uses with the image's `/JBIG2Globals`: export reads JBIG2 behind other filters too, and redaction clears pixels in JBIG2 images instead of removing them whole (one whose page isn't its size still is) | | 2026-10-26 | minor | §7.4: redaction clears pixels in CCITT fax images through the CCITT decoder, instead of removing them whole (Group 4 and Group 3 off bytes; Group 3 on bytes is still removed whole, as the bootstrap renderer may draw it as other rows than the decoder reads) | | 2026-10-26 | minor | §7.4: CCITT decoding (`hayro-ccitt`) in `pdfcraft-filters`, for export and the Optimizer | diff --git a/docs/research/pdf-workflows/README.md b/docs/research/pdf-workflows/README.md new file mode 100644 index 0000000000..32a855a570 --- /dev/null +++ b/docs/research/pdf-workflows/README.md @@ -0,0 +1,73 @@ +# PDF workflow research reference + +> **Last reviewed:** 2026-10-17 · **Last updated:** 2026-10-17 · **Status:** reference documentation. Feature and roadmap snapshot: [`924c4da4`](https://github.com/storytold/pdfcraft/tree/924c4da4ac2879ac9107c6c82e20ac6be5570f0a), dated 2026-10-16. Detailed code inspection remains pinned to `3f044b62`, dated 2026-10-13. + +This reference consolidates archived comparative research, public source locators and +PdfCraft feature evidence. It separates documented workflows, historical observations, +evaluation criteria and current feature records so contributors can assess the collected +material without treating an old research claim as a present product fact. + +The documentation is independent of feature implementation. It introduces no new project +roadmap, implementation assignments, delivery estimates or feature-completion claims. +Planning remains in [ROADMAP.md](../../../ROADMAP.md) and the +[feature checklist](../../../parity/acrobat-features.toml). + +## Contents + +| Reference | Contents | +|---|---| +| [Comparative research](competitive-research.md) | Nineteen product studies and eighteen workflow families, with evidence limitations and archived corrections. | +| [Source register](source-register.csv) | 180 unique public locators, their archive dates, recovered provenance and explicit verification states. | +| [Workflow reference](workflow-reference.md) | Twenty research domains: existing foundations, retained findings and evaluation dimensions. | +| [Feature mapping](feature-mapping.csv) | 124 existing parity IDs with their exact status and milestone at the pinned snapshot, linked to the research domains. | +| [Interaction evidence](interaction-evidence.md) | Inspected document ownership, job outcomes, save, keyboard and review foundations, distinguished from historical evaluation criteria. | + +## Evidence and dates + +The archive includes sixteen product studies, a community-demand sample, an expanded +desktop/platform survey and interface investigations. Research dates span July–October +2026. Three additional desktop products appear in the expanded survey. The source register +is a navigation aid rather than independent proof of any product's current behavior. + +`archived-not-revalidated` means that the locator was recovered from a dated study; its +supporting passage and current edition have not been checked for this handoff. +`locator-reconstructed-not-revalidated` means that the original study named a source +without preserving its complete URL. Neither state means a web-archive capture exists. +Vendor-hosted community posts remain community reports, not vendor policy statements. + +Community snippets describe questions and reported pain points. They do not establish +market share, incidence or representative satisfaction. Historical pricing, licensing, +legal, security and superiority assertions are excluded. No competitor was installed or +benchmarked for this documentation contribution. + +## Reconciliation with PdfCraft + +Earlier research described measurement, a command palette, visual comparison, Tags panels +and reusable actions as missing. The inspected source and newer checklist already contain +those foundations, including synchronized side-by-side comparison and recursive folder +batches. The workflow reference records those corrections and reuses the existing oracle +tools rather than describing a second implementation. + +The dated mapping is not another live checklist. Current status comes from +`parity/acrobat-features.toml`; the ranked priorities and progress remain in +[gaps.md](../../gaps.md), [roadmap.md](../../roadmap.md) and +[ui-parity.md](../../ui-parity.md). An existing inconsistency in knockout-rendering +evidence is identified without changing its recorded status. + +## Publication and verification scope + +These are newly written summaries and public source links. No third-party screenshots, +visual designs, source code, fonts, sample documents, private paths, account data or raw +session logs are included. Historical implementation classes, toolkit choices and old +effort estimates are not adopted as PdfCraft architecture. + +Static checks cover the feature IDs, statuses, milestone assignments, reference IDs and +local links. Evaluation scenarios are research criteria, not tests already executed. +This handoff claims no native acceptance, screen-reader verification, external-reader +compatibility result, vendor benchmark or compliance certification. + +## Revision history + +| Date | Change | +|---|---| +| 2026-10-17 | Consolidated the archived research, source register, workflow evidence and pinned feature mapping. | diff --git a/docs/research/pdf-workflows/competitive-research.md b/docs/research/pdf-workflows/competitive-research.md new file mode 100644 index 0000000000..f5dc1400dc --- /dev/null +++ b/docs/research/pdf-workflows/competitive-research.md @@ -0,0 +1,211 @@ +# Comparative PDF workflow research + +Prepared 2026-10-17 from a September–October 2026 research archive. This reference summarizes product coverage, workflow distinctions and evidence limits. It is not a current competitor parity assessment, implementation specification or additional roadmap. + +## Scope and evidence rules + +The archive contains sixteen product studies dated September 7–8, a community-demand study, a September 8 synthesis with September 20 reconciliation, September 25 desktop/web/platform and interaction studies, and detailed design investigations through October 8. Three additional desktop products were examined on September 25: Power PDF, PDFelement and SumatraPDF. The accompanying [source register](source-register.csv) contains 180 distinct public source locators recovered from this material or reconstructed from its source titles. + +The product sheets combined documented capabilities, approximate edition/version history, secondary reviews, inference and historical implementation observations. These categories have different evidentiary limits: + +| Evidence category | Information retained | Limit | +|---|---|---| +| Archived primary documentation | A dated vendor-described workflow or technical interface | Not independent reliability evidence, a current edition guarantee or proof of superiority | +| Archived release notes | A reported change in a named release | Not proof of availability in every platform or edition | +| Archived source inspection | Behavior recorded at the inspected revision | Not verification of another revision, release or backend | +| Community reports and search snippets | Reported pain points and terminology | Not market share, incidence, representative satisfaction or security conclusions | +| Historical design investigation | Documented problem, constraints and evaluation dimensions | Not PdfCraft implementation status, an accepted architecture or dependency approval | + +Unless explicitly updated in the register, links are **archived-not-revalidated**. An archive date is the date written in the collected study, not a web-archive capture, freshness guarantee or current product test. **Locator-reconstructed-not-revalidated** identifies a URL reconstructed from a named source rather than recovered as a complete citation. Vendor-hosted community discussions remain community evidence; their hosting does not make them vendor capability statements. + +The register is a source-finding aid, not a claim that every linked page supports every observation in this document. Its scope notes distinguish general entry pages from more specific references. No competitor was installed or benchmarked for this handoff. Screenshots, downloaded proprietary documents, user documents, implementation prompts and copied source code are not included. + +No competitor absence counts, rankings, prices, subscription assertions, security certifications, processing-retention promises or legal deadlines are reproduced as current facts. Corrections within the archive are recorded below. PdfCraft implementation status and planning remain in its existing roadmap, parity records and source evidence. + +## Product coverage + +This table is an index to the subjects examined by each archived study. It does not assert that a product uniquely provides a capability or that PdfCraft lacks it. Product editions and platforms differed within the original studies. + +| Product study | Workflow subjects recorded | Comparison dimensions used in the research | +|---|---|---| +| Adobe Acrobat | Tool contexts; staged redaction; form events; validation/fixups; reusable actions; search and structure | Review versus apply, repeated-operation settings, document structure and edition boundaries | +| ABBYY FineReader | Linked scan/text/word zoom; uncertain-item navigation; dictionaries; page verification; cross-format comparison; scheduled processing | Correction effort, source/text synchronization, review completion and recognition output | +| Foxit PDF Editor | Custom actions; search; form events; structured review; layers; region recognition; deployment policies | GUI/automation consistency, search scope, form compatibility and platform/edition differences | +| PDF-XChange Editor | Object/field properties; annotation filters; dynamic stamps; scan correction; tags; identifiers | Selection precision, property inspection, annotation interchange and reusable configuration | +| Nitro PDF Pro | Sensitive-data review; table/form extraction; Office-oriented navigation; repeated operations | Detection versus removal, structured extraction and task-level discoverability | +| Bluebeam Revu | Markup records; reusable symbols; calibrated measurements; revisions; compare and overlay | Review metadata, technical drawing scale, revision correspondence and interchange | +| Master PDF Editor | Object inspector; menus/toolbars; forms; certificate operations | Live-document inspection, field properties and advanced operations beside the page | +| UPDF | Tool access; reading comfort; cross-page operations; summaries; platform variations | Reading/editing state, page-spanning work and platform capability differences | +| PDF Expert | Focused reading/editing; thumbnail operations; customizable access; signature placement | Page-centered work, document manipulation and contextual properties | +| PDFgear | Task-led entry; region extraction; conversion | Goal-oriented entry, region selection and output artifacts | +| Sejda | Verb-first tools; per-operation limits; filename templates; chains; unsupported-input explanations | Prerequisites, output naming, operation chaining and declared limits | +| iLovePDF | Task grid; chained processing; page/conversion utilities; desktop/web differences | Single-task entry, result reuse and processing-location disclosure | +| PDF24 | Local utilities; scanning/recognition; compression readouts; batch options; hybrid invoices | Measured outcomes, repeated processing and embedded business data | +| Smallpdf | Narrow task flows; conversion; signing categories | Entry friction, option disclosure and task-specific outputs | +| LibreOffice Draw | Import/export; headless conversion; command options; accessibility/export settings | Native editing versus conversion, semantic preservation and format tradeoffs | +| Okular | Keyboard annotation; view history; reading transforms; backend capabilities; signatures | Review navigation, reading state and unsupported-format reporting | +| Power PDF | Office-oriented navigation; deployment differences; refreshed entry surfaces | Navigation, product/edition boundaries and installation context | +| PDFelement | Customizable tool access; reusable annotations; table/form/document workflows | Repeated-action access and grouping of related operations | +| SumatraPDF | Minimal chrome; searchable commands/navigation targets; keyboard reading | Discoverability, reading navigation and command access | + +Public entry points include [Acrobat help](https://helpx.adobe.com/acrobat/user-guide.html), [FineReader help](https://help.abbyy.com/en-us/finereader/16/), [Foxit user guides](https://www.foxit.com/resource-hub/user-guide/), the [PDF-XChange feature catalogue](https://www.pdf-xchange.com/product/pdf-xchange-editor), [Bluebeam markup guidance](https://support.bluebeam.com/revu/how-to/track-and-manage-markups-using-markups-list.html), and [SumatraPDF command-palette documentation](https://www.sumatrapdfreader.org/docs/Command-Palette). Their archive or reconstruction states are recorded in the register; listing them here does not assert fresh product verification. + +## Community evidence and its limits + +The September community study used sixteen targeted searches across PDF, administration, legal, archival and related communities. Direct thread retrieval was frequently blocked, so many observations came from indexed snippets. Counts in that study represented distinct surfaced threads with unknown search coverage and overlapping audiences. They were not survey percentages. No thread-frequency ranking or verbatim snippet quotation is reproduced here. + +Five recurring themes were recorded: + +| Theme in the collected material | Distinction expressed by the reports | Evidence limit | +|---|---|---| +| Redaction and saving | Visual covering versus content removal; saved artifact versus on-screen appearance | Reports did not independently establish removal correctness across products | +| Confidential and repeated processing | Processing location, input/output predictability and reuse of options | Search results did not establish representative privacy preferences | +| Layout damage during editing | A small intended change versus font substitution or wider reflow | Individual complaints did not establish defect rates or comparative fidelity | +| Form interactivity | Filling, authoring, flattening, printing and data interchange | Similar terminology did not imply identical field or script compatibility | +| Large and mixed-result jobs | Overall progress versus per-file success, failure and unchanged output | Reported jobs were not a controlled performance benchmark | + +Ownership expectations, interruptions, keyboard review and readable themes also appeared in the collected material. They remain historical observations, rather than current pricing conclusions or statistically measured demand. + +## Workflow families examined + +The archive grouped PDF work into the following eighteen families. The dimensions below describe what the studies compared or distinguished. They do not introduce new acceptance contracts or implementation assignments for PdfCraft. + +### 1. Reading, navigation and search + +The viewing research distinguished page content from temporary reading state: zoom, viewport rotation, reading colors, navigation history and validation indicators. Search and bookmark movement were examined in relation to page labels, physical page numbers and returning to the prior reading context. + +The studies treated current-document search, cross-document search, folder indexing and recognition-backed search as different scopes. Saved sessions and view history were also distinct from saved document edits. Historical comparisons of search depth were documentation-based, not measurements of retrieval accuracy or speed. + +### 2. Text and object editing + +The editing studies separated replacement of existing text, added overlays, image/object changes and whole-page conversion. Font substitution, subset fonts, positioned text, unsupported glyphs, rotation and mixed writing systems were recurring explanations for unexpected editing results. + +The archive evaluated fidelity in terms of both appearance and extractable text. It also examined object bounds, selection properties, reflow disclosure and external-editor round trips. Reports of formatting damage did not establish a product-wide defect rate, and a single successful font/script example did not establish general CJK or right-to-left editing support. + +### 3. Page organization and composition + +Page studies covered selection, insertion, extraction, reordering, reversal, splitting, interleaving and assembly from multiple documents. They distinguished thumbnail position from page identity and distinguished a displayed page label from its physical index. + +Recorded evaluation dimensions included output-name previews, split boundaries, unequal source lengths, page-size differences, retained links/bookmarks/forms and the relationship between undo and selection. The archived cross-document staging layouts were design explorations; they are not adopted interface requirements or evidence of a current PdfCraft gap. + +### 4. OCR and human verification + +The recognition research examined the correction loop around recognition results: source image, recognized text, magnified region, uncertain-item navigation, replacement, skipping and human-reviewed state. The FineReader study emphasized synchronization and repetitive keyboard navigation rather than confidence coloring alone. + +Engine confidence and human review were separate concepts. The archive also distinguished an edited preview from a corrected searchable text layer, region recognition from whole-page recognition, and already-text input from scanned input. Language selection, preprocessing, alternate suggestions, page orientation and failed/cancelled recognition were recorded as separate dimensions. Ensemble architecture and language counts were not independent accuracy evidence; no held-out comparative accuracy result is established by this handoff. + +### 5. Forms and document scripts + +Forms research separated authoring, filling, formatting, validation, calculation, flattening and data interchange. Field lists, tab order, calculation order and event dispatch appeared in the archived comparisons. A populated field and a functioning calculation/event model were not treated as interchangeable evidence. + +The historical script investigations recorded runtime compatibility and security constraints: allowed events and APIs, resource bounds, event recursion, external access and non-executing inspection. Those investigations evaluated another implementation's options. They do not select a runtime, authorize a dependency or establish PdfCraft script behavior. Unsupported fields and unsupported events were distinct from a document that had been verified to execute correctly. + +### 6. Comments, markups and review data + +Annotation studies examined both visible appearance and review records: author, subject, page, status, replies, timestamps and summaries. Bluebeam's study devoted particular attention to structured markup lists, reusable symbol sets and drawing-review metadata. Other studies covered annotation filters, stamps, comment summaries and import/export. + +The archive distinguished filtering from deletion, literal stamp content from values inserted at placement, and standard PDF relationships from application-specific metadata. Interchange and CSV export were considered separately from the appearance of the annotation in its originating application. Specialized construction-review workflows were a distinct research category rather than a general requirement for every PDF editor. + +### 7. Redaction and sanitization + +The collected material separated candidate detection, human review, application of removal, sanitization and verification. Search patterns and entity detection concerned finding possible sensitive material; they did not themselves establish removal. Visual black fill, blur and pixelation were distinguished from removal of the underlying PDF content. + +Recorded assurance dimensions included saved-file extraction, affected images, metadata, annotations, attachments, layers, previous revisions and nested content. Geometry on rotated pages and partial-word cases were separate concerns. The reports also noted that an audit artifact can leak the very content being removed. Claims of uniquely verifiable or universally safe redaction are not reproduced; the archive is not a security certification. + +### 8. Encryption, permissions and hostile-document handling + +Security studies distinguished open-password encryption, permission flags and recipient-certificate encryption. They also examined external links, launch actions, scripts, attachments and processing of malformed files. These are different controls and do not have the same threat model. + +The archive's evaluation dimensions included credential handling, failure/refusal behavior, resource limits and consistency between the displayed operation and its execution policy. PDF permission flags were not equivalent to preventing arbitrary software from extracting accessible content. Historical algorithm and certification references remain source locators, not current conformance findings. + +### 9. Digital signatures and signing requests + +Signing research distinguished a drawn appearance, certificate-based signing, document certification, permitted changes and validation. Integrity of signed bytes, signer trust, timestamps, revocation evidence and later document changes were separate observations. A displayed validation result was tied to the document revision inspected. + +The archived local-request investigation examined prepared documents, manifests, handoff and inspection of returned files. That historical design was not a hosted signing service or identity-verification system. The archive also distinguished the local embedding of validation evidence from retrieval that may require network access. Its PAdES, dependency and legal terminology does not establish PdfCraft's attained signature profiles or the legal status of an output. + +### 10. Document comparison + +Comparison studies separated text, raster/visual, page-structure and annotation differences. They examined page correspondence after insertions, removals, duplication and reordering, together with linked scrolling, change lists, filtering, navigation and report export. + +An overlay and a textual diff supplied different observations; neither alone established comprehensive equivalence. Scanned comparison introduced recognition dependence. The archive also recorded uncertain alignment and the distinction between a filtered view, a complete report and a user-reviewed subset. No comparative false-positive or missed-change rate was measured for this handoff. + +### 11. Batch processing and presets + +The detailed batch investigations recorded ordered steps, versioned preset data, option validation, output naming, conflicts, stop/continue policy, numbering continuity and per-file reports. They distinguished inert configuration data from executable scripts or shell commands. + +Atomicity was an explicit research dimension: a per-file chain could be all-or-nothing while a whole run had mixed outcomes; a whole-run transaction was a different guarantee. Cancellation could leave previously committed work, depending on the documented boundary. The archive also examined watch-folder repetition, result retention and deterministic numbering when files completed in a different order. These historical designs are not a replacement action format or another batch runner for PdfCraft. + +### 12. Conversion, extraction and document fidelity + +Conversion studies distinguished extracted PDF text, reconstructed document structure, page images, editable Office output and tabular data. A file extension, package structure check and visual rendering each supplied different evidence. Table boundaries, cell order, merged cells, page ranges and output settings were recorded separately. + +The archived fidelity questions concerned retained bookmarks, annotations, attachments, form semantics and unsupported content as well as visual layout. The studies also noted that exported text can be interpreted as spreadsheet formulas. Historical claims about conversion breadth or editable output were not independent measurements of arbitrary-document fidelity. + +### 13. Compression, fonts and color + +Optimization studies separated lossless cleanup, image recompression, resolution reduction, font subsetting and color conversion. Estimated savings and measured before/after bytes were different quantities. An output larger than its input was still a possible result of processing. + +The October font investigation documented that a control's name alone did not establish actual subsetting for every font class in the implementation studied. The color investigation examined decoder behavior, embedded profiles, image representations and rendering intent. Their findings are historical case studies, not PdfCraft defect reports or dependency choices. Rendering, selectable text, font handling and retention of associated files were independent evaluation dimensions. + +### 14. Printing, measurements and preflight + +Printing research covered page order, N-up, booklet, duplex, comments and preview. Measurement research distinguished viewport scale, physical page geometry, calibration, units and stored annotations. Rotated pages and nonzero page-box origins affected the interpretation of geometric results. + +Preflight, fixups and production previews were separate categories. A check reported an observation; a fixup changed the document; a production preview concerned the intended output process. The archive examined named profiles and skipped checks without treating a collection of lightweight checks as a complete production or standards validator. + +### 15. Accessibility and archival formats + +The archive distinguished accessible application operation from accessible PDF output. For the application, it examined keyboard paths, focus, labels, status, contrast, custom-painted controls and actual assistive-technology behavior. For the document, it examined tags, reading order, alternative text, language, lists, tables and the relationship between content identifiers and structure. + +The historical tagging investigation bounded the document classes it considered and separated automatic classification from human review. A structure check, a screen-reader session and a standards validator reported different forms of evidence. Automated checks did not by themselves establish document accessibility or legal compliance. The register retains standards references without reproducing the archive's changing legal-deadline assertions. + +### 16. Layers, attachments and structured business documents + +Layer research separated visibility, printing state, membership, ordering and properties. Displaying a layer panel was not evidence that every optional-content transformation was supported. Hidden content also had implications for searching and sanitization. + +The archive treated attachments and embedded XML as potentially essential document data. Hybrid-invoice discussions distinguished detecting and preserving a payload from reading it, validating it, editing it or generating a compliant invoice. Those were separate scopes; the handoff carries no legal mandate, invoice-conformance finding or current product absence assertion. + +### 17. Deployment, automation and integration + +Integration research examined menus, keyboard access, contextual commands, command search, CLI operations, structured automation and watched folders. They were access paths to operations, not equivalent guarantees about authorization, cancellation or execution policy. + +The archive recorded coordinate naming, document identity, input/output paths, confinement, authorization, limits and headless reporting as evaluation dimensions. Silent installation, managed policies, portable builds, platform signing and architecture support were additional deployment subjects. Descriptions of other applications' registries or toolkits do not establish a new architecture for PdfCraft. + +### 18. Assisted recognition and grounded document answers + +The expanded survey covered suggested sensitive entities, image descriptions, extracted structures and answers about document content. It distinguished suggestions from accepted edits, source-grounded answers from unsupported statements, and processing location from the appearance of the interface. + +The studies recorded language coverage, available runtimes, network behavior, source-page references and editable suggestions as evaluation dimensions. They did not supply held-out accuracy measurements, an approved dependency selection or proof that generated descriptions established accessibility. Sensitive-data suggestions and actual redaction remained different stages. + +## Cross-cutting interface observations + +The September 25 interaction studies recorded several recurring dimensions across product families: + +| Dimension | Distinctions recorded in the archive | +|---|---| +| Command access | A command's identity, its entry points, its availability and its explanation when unavailable | +| Task surface | One-shot configuration, repeated review, live-document inspection and long-running monitoring | +| Document context | Active tab versus operation owner; current document versus the revision analyzed | +| Result state | Preview, pending mutation, completed edit, saved output, stale report and partial result | +| Focus and navigation | Entering a tool, moving between related views and returning to the prior reading position | +| State communication | Color, text, shape, control state and assistive-technology semantics | +| Layout and scaling | Narrow windows, large text, display scales, long translations and secondary-panel visibility | +| Visual consistency | Icon meaning, labels, themes and the difference between page colors and application chrome | +| Long operations | Start, cancellation request, partial completion, error, retry and completed output | + +The archive included sketches of persistent OCR/comparison views and inspectors. Those sketches are historical research artifacts, not adopted navigation plans, prescribed workspace layouts or new implementation requirements. No third-party visual design is imported here. + +## Corrections and unresolved evidence in the archive + +Several later reports qualified earlier claims. The following distinctions prevent an archived comparison from becoming an inaccurate current scoreboard: + +1. **Licensing:** later research qualified an early universal subscription assertion. Product licensing varied by edition and date. No current price or subscription conclusion is carried forward. +2. **Shipping evidence:** later inspection distinguished a low-level engine capability, a visible entry point and tested end-to-end behavior. They were not equivalent evidence of a complete product workflow. +3. **Signing terminology:** appearance, certification, long-term validation evidence and qualified-signature services were conflated in parts of the earlier material. This reference treats them separately. +4. **Absence claims:** some negative findings came from documentation searches or snippets rather than exhaustive product tests. They are not reproduced as current facts. +5. **Accuracy and security:** architecture and feature lists were sometimes used to infer recognition accuracy or removal reliability. This handoff records no such superiority or safety conclusion. +6. **Regulatory references:** accessibility and invoicing deadlines were corrected within the archive. No legal deadline or compliance conclusion is asserted here. +7. **Implementation transfer:** toolkit-specific source paths, proposed dependencies and defects from another codebase did not establish PdfCraft behavior. This document retains workflow distinctions and historical observations, not those implementation directions. + +Current PdfCraft planning and feature status remain in the existing roadmap and parity records. The source register and this reference document only describe the collected research and its limits. diff --git a/docs/research/pdf-workflows/feature-mapping.csv b/docs/research/pdf-workflows/feature-mapping.csv new file mode 100644 index 0000000000..d2daefabe7 --- /dev/null +++ b/docs/research/pdf-workflows/feature-mapping.csv @@ -0,0 +1,125 @@ +domain,research_topic,parity_id,status,milestone,snapshot_commit,feature_evidence_source,research_reference,source_register,source_inspection_commit,evidence_limit,evidence_note +B01,Rendering and output fidelity,view.render-pages,partial,M2,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B01,Rendering and output fidelity,view.render-shadings-patterns,partial,M2,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B01,Rendering and output fidelity,view.render-transparency,partial,M2,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B01,Rendering and output fidelity,view.render-knockout,shipped,M11,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Recorded shipped status conflicts with signing-related notes/evidence and transparency notes; maintainer evidence reconciliation required; no status changed here +B01,Rendering and output fidelity,view.render-type3,partial,M2,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B01,Rendering and output fidelity,view.render-cid-fonts,partial,M2,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B01,Rendering and output fidelity,view.render-image-codecs,partial,M2,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B02,Existing-text editing,edit.text-edit,partial,M7,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B02,Existing-text editing,edit.rtl-cjk-editing,partial,M7,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B02,Existing-text editing,edit.rotated-text-editing,planned,M7,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B02,Existing-text editing,edit.keep-tags-on-edit,planned,M7,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B03,Document comparison,misc.compare-text,shipped,M12,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B03,Document comparison,misc.compare-visual,shipped,M12,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B03,Document comparison,misc.compare-report,shipped,M12,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B03,Document comparison,misc.compare-side-by-side,shipped,M12,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B03,Document comparison,misc.compare-filters,planned,M12,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B03,Document comparison,misc.compare-page-ranges,planned,M12,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B03,Document comparison,misc.compare-scanned,planned,M12,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B04,Recognition proofing and correction,ocr.correct-recognized-text,planned,M10,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B04,Recognition proofing and correction,ocr.editable-text-output,partial,M10,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B04,Recognition proofing and correction,ocr.recognize-text,shipped,M10,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B04,Recognition proofing and correction,ocr.recognize-range,shipped,M10,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B04,Recognition proofing and correction,ocr.recognize-multiple,shipped,M10,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B05,Recognition languages and scan preparation,ocr.ocr-languages,partial,M10,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B05,Recognition languages and scan preparation,ocr.rotation-detection,planned,M10,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B05,Recognition languages and scan preparation,ocr.deskew,planned,M10,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B05,Recognition languages and scan preparation,ocr.ocr-engines,planned,M10,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B05,Recognition languages and scan preparation,ocr.despeckle,partial,M10,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B05,Recognition languages and scan preparation,ocr.background-removal,partial,M10,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B05,Recognition languages and scan preparation,ocr.camera-perspective,partial,M10,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B06,Reusable batch processing,misc.action-wizard-run,shipped,M13,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B06,Reusable batch processing,misc.action-wizard-create,shipped,M13,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B06,Reusable batch processing,misc.action-wizard-manage,planned,M13,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B06,Reusable batch processing,misc.action-prompt-steps,planned,M13,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B07,Document structure and accessibility authoring,a11y.autotag,partial,M12,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B07,Document structure and accessibility authoring,a11y.tags-panel,partial,M12,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B07,Document structure and accessibility authoring,a11y.tag-properties,partial,M12,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B07,Document structure and accessibility authoring,a11y.reading-order-tool,partial,M12,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B07,Document structure and accessibility authoring,a11y.order-panel,partial,M12,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B07,Document structure and accessibility authoring,a11y.table-editor,planned,M12,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B07,Document structure and accessibility authoring,a11y.create-tag-from-selection,planned,M12,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B07,Document structure and accessibility authoring,a11y.artifact-marking,partial,M12,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B08,Preflight and archival validation,optimize.pdfa-validate,partial,M11,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B08,Preflight and archival validation,optimize.pdfa-convert,partial,M11,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B08,Preflight and archival validation,optimize.preflight,planned,M11,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B08,Preflight and archival validation,optimize.preflight-results,planned,M11,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B08,Preflight and archival validation,optimize.preflight-reports,planned,M11,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B09,Redaction and assurance evidence,protect.redaction-verification,shipped,M8,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B09,Redaction and assurance evidence,protect.redact-glyph-removal,shipped,M8,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B09,Redaction and assurance evidence,protect.redact-image-pixels,shipped,M8,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B09,Redaction and assurance evidence,protect.redact-vectors,shipped,M8,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B09,Redaction and assurance evidence,protect.redact-annots-fields,shipped,M8,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B09,Redaction and assurance evidence,protect.redact-struct-tree,shipped,M8,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B09,Redaction and assurance evidence,protect.redact-pattern-locales,planned,M8,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B09,Redaction and assurance evidence,protect.redact-partial-words,planned,M8,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B09,Redaction and assurance evidence,protect.redact-folder,planned,M13,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B09,Redaction and assurance evidence,protect.redaction-properties,shipped,M8,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B09,Redaction and assurance evidence,protect.redaction-codes,partial,M8,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B10,Digital signatures and validation evidence,sign.cades-pades-bb,shipped,M9,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B10,Digital signatures and validation evidence,sign.pkcs12,shipped,M9,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B10,Digital signatures and validation evidence,sign.windows-cert-store,shipped,M9,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B10,Digital signatures and validation evidence,sign.macos-keychain,shipped,M9,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B10,Digital signatures and validation evidence,sign.certify,shipped,M9,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B10,Digital signatures and validation evidence,sign.timestamp-servers,planned,M9,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B10,Digital signatures and validation evidence,sign.pkcs11,planned,M9,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B10,Digital signatures and validation evidence,sign.field-mdp,planned,M9,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B10,Digital signatures and validation evidence,sign.pades-bt,partial,M9,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B10,Digital signatures and validation evidence,sign.ltv,partial,M9,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B10,Digital signatures and validation evidence,sign.pades-blta,partial,M9,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B10,Digital signatures and validation evidence,sign.ocsp-crl,partial,M9,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B11,"Forms, events and scripting",form.custom-scripts,shipped,M6,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B11,"Forms, events and scripting",form.event-order,shipped,M6,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B11,"Forms, events and scripting",form.calculation-order,shipped,M6,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B11,"Forms, events and scripting",form.submit-form,planned,M6,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B11,"Forms, events and scripting",edit.action-javascript,planned,M6,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B11,"Forms, events and scripting",form.push-buttons,partial,M6,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B12,Measurement and scale interpretation,comment.measure-distance,partial,M12,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B12,Measurement and scale interpretation,comment.measure-scale,partial,M12,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B12,Measurement and scale interpretation,comment.measure-perimeter,shipped,M12,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B12,Measurement and scale interpretation,comment.measure-area,shipped,M12,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B12,Measurement and scale interpretation,comment.measure-snap,shipped,M12,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B12,Measurement and scale interpretation,comment.measure-info-panel,shipped,M12,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B12,Measurement and scale interpretation,comment.measure-export,shipped,M12,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B13,Page assembly and document composition,organize.combine-files,shipped,M4,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B13,Page assembly and document composition,organize.combine-choose-pages,shipped,M4,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B13,Page assembly and document composition,organize.combine-sort,shipped,M4,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B13,Page assembly and document composition,organize.combine-thumbnails,shipped,M4,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B13,Page assembly and document composition,organize.field-name-conflicts,shipped,M4,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B13,Page assembly and document composition,organize.split-every-n,shipped,M4,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B13,Page assembly and document composition,organize.split-by-bookmarks,shipped,M4,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B13,Page assembly and document composition,organize.struct-tree-merge,planned,M12,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B13,Page assembly and document composition,organize.split-multiple-files,planned,M13,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B13,Page assembly and document composition,organize.split-naming,planned,M4,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B14,Creation and conversion fidelity,create.from-office,planned,M10,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B14,Creation and conversion fidelity,create.export-docx,partial,M10,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B14,Creation and conversion fidelity,create.export-xlsx,planned,M10,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B14,Creation and conversion fidelity,create.export-pptx,partial,M10,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B15,Document performance and resource use,view.large-document-performance,shipped,M3,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B15,Document performance and resource use,misc.performance-budgets,planned,M14,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B15,Document performance and resource use,core.lazy-loading,planned,M1,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B16,Search across document collections,view.advanced-search,partial,M12,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B16,Search across document collections,view.search-folder,planned,M12,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B16,Search across document collections,view.search-boolean,planned,M12,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B16,Search across document collections,view.search-index,planned,M12,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B17,Keyboard and assistive-technology interaction,a11y.app-screen-reader,partial,M3,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B17,Keyboard and assistive-technology interaction,a11y.keyboard-only,planned,M14,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B17,Keyboard and assistive-technology interaction,a11y.replace-page-colors,shipped,M12,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B18,Preferences and capability disclosure,misc.preferences-dialog,partial,M14,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B18,Preferences and capability disclosure,misc.prefs-search,planned,M14,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B18,Preferences and capability disclosure,misc.admin-preferences,planned,M14,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B18,Preferences and capability disclosure,misc.rtl-ui,planned,M14,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B19,Print preparation and color behavior,organize.print-presets,planned,M10,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B19,Print preparation and color behavior,print.output-preview,planned,M11,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B19,Print preparation and color behavior,print.color-warnings,planned,M11,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B19,Print preparation and color behavior,print.convert-colors,planned,M11,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B19,Print preparation and color behavior,print.color-management,planned,M11,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B20,"Document identity, history and failure outcomes",core.undo-redo,shipped,M4,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B20,"Document identity, history and failure outcomes",core.undo-after-save,shipped,M4,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B20,"Document identity, history and failure outcomes",core.atomic-batch-edits,shipped,M4,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B20,"Document identity, history and failure outcomes",core.autosave,shipped,M4,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B20,"Document identity, history and failure outcomes",core.crash-recovery,shipped,M4,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B20,"Document identity, history and failure outcomes",core.unsaved-changes-prompt,shipped,M4,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B20,"Document identity, history and failure outcomes",core.web-recovery,planned,M4,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package +B20,"Document identity, history and failure outcomes",view.multiple-windows,planned,M14,924c4da4ac2879ac9107c6c82e20ac6be5570f0a,parity/acrobat-features.toml,workflow-reference.md,source-register.csv,3f044b6263aa6605c3aa44ca6d065e1ab37d13b6,Checklist snapshot checked; implementation tests not rerun.,Catalogued status/evidence inspected; cited implementation tests were not rerun for this research package diff --git a/docs/research/pdf-workflows/interaction-evidence.md b/docs/research/pdf-workflows/interaction-evidence.md new file mode 100644 index 0000000000..7773505747 --- /dev/null +++ b/docs/research/pdf-workflows/interaction-evidence.md @@ -0,0 +1,143 @@ +# Interaction evidence and evaluation reference + +Detailed source inspection used PdfCraft revision +`3f044b6263aa6605c3aa44ca6d065e1ab37d13b6`. A later documentation review used +`924c4da4ac2879ac9107c6c82e20ac6be5570f0a` on 2026-10-17. Links retain those +inspected revisions. Neither review was a runtime parity assessment. + +This reference summarizes interaction findings from archived comparative research and +relates them to existing PdfCraft mechanisms. It contains no implementation plan, +proposed workspace design, feature commitment or new architecture. The project's +roadmap and machine-readable parity checklist retain their planning and status roles. +Historical design examples and test descriptions are distinguished from current product +evidence below. + +## Existing mechanisms and corrected historical descriptions + +Several early comparative notes described capabilities that later existed in PdfCraft. +The table records the inspected evidence and its limits rather than preserving those +absence claims. + +| Area | Inspected evidence | What that evidence establishes | +|---|---|---| +| Command discovery and dispatch | [`commands.rs`](https://github.com/storytold/pdfcraft/blob/3f044b6263aa6605c3aa44ca6d065e1ab37d13b6/crates/ui-egui/src/commands.rs), [`palette.rs`](https://github.com/storytold/pdfcraft/blob/3f044b6263aa6605c3aa44ca6d065e1ab37d13b6/crates/ui-egui/src/palette.rs) | Menus, shortcuts, palette and other entry points share command metadata/dispatch. The palette is an existing feature, not an absent research concept. | +| Measurement | [`measure_ui.rs`](https://github.com/storytold/pdfcraft/blob/3f044b6263aa6605c3aa44ca6d065e1ab37d13b6/crates/ui-egui/src/measure_ui.rs) | Distance/perimeter/area controls, calibration, snapping and saved-measurement state exist. An older session-only calibration description does not describe every current persisted scale. | +| Comparison | [`compare_ui.rs`](https://github.com/storytold/pdfcraft/blob/3f044b6263aa6605c3aa44ca6d065e1ab37d13b6/crates/ui-egui/src/compare_ui.rs) | Text changes, visual regions, document highlights, report and comment actions exist. This older source link does not establish all later split-view behaviour. | +| OCR | [`ocr_ui.rs`](https://github.com/storytold/pdfcraft/blob/3f044b6263aa6605c3aa44ca6d065e1ab37d13b6/crates/ui-egui/src/ocr_ui.rs) | Background recognition, cancellation request and multiple-file processing exist. Cancellation can retain pages already recognized; it is not synonymous with rollback. | +| Accessibility review | [`accessibility.rs` tests](https://github.com/storytold/pdfcraft/blob/3f044b6263aa6605c3aa44ca6d065e1ab37d13b6/crates/ui-egui/tests/accessibility.rs) | Source includes checks for finding navigation, Tags-panel review, automatic text tagging and per-document skipped-rule state. Tests were inspected, not executed for this handoff. | +| Interface appearance | [`theme.rs`](https://github.com/storytold/pdfcraft/blob/3f044b6263aa6605c3aa44ca6d065e1ab37d13b6/crates/ui-egui/src/theme.rs) | A shared token type and System/Light/Dark preferences exist. Their presence is not a measured contrast result. | +| International text input | [`commands.rs` tests](https://github.com/storytold/pdfcraft/blob/3f044b6263aa6605c3aa44ca6d065e1ab37d13b6/crates/ui-egui/tests/commands.rs) | Palette tests cover idle composition frames, Chinese preedit/commit, cancellation and outside-click behaviour. This does not establish native input-method behaviour in every editable field. | +| Automation conventions | [`automation/README.md`](https://github.com/storytold/pdfcraft/blob/3f044b6263aa6605c3aa44ca6d065e1ab37d13b6/crates/automation/README.md) | The documented contract uses displayed top-left rectangles, 1-based pages/positions, confined paths and a shared tool table. Historical mixed-coordinate criticism is not current evidence of a defect. | + +The later [feature checklist](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/parity/acrobat-features.toml) +marks synchronized side-by-side comparison shipped. Its notes describe two document +views when the older version remains open and the window is at least 1000 pixels wide; +synchronization follows offsets rather than matching content. The record identifies +page-size drift and old-tab closure as limits. It also records Action Wizard +creation/editing/running and recursive CLI/MCP folder batches with bounded recipes, +relative paths, collision-safe output names and per-file reports. + +The later [roadmap](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/ROADMAP.md) +records changed-on-disk Save confirmation and a shared staging-file writer. It describes +new outputs being reopened/checked before publication. These are existing safety +mechanisms; the archived research is not evidence that atomic saves or conflict +notifications are absent. + +Some prose gap tables at the same revision still describe comparison or Tags panels as +missing, while detailed checklist entries and inspected source describe implementations. +This is a documentation inconsistency in the inspected snapshot, not evidence that the +code is absent or that its listed limitations are solved. + +## What the archived interaction research distinguished + +The research compared workflow structures rather than implementation languages. Its +recurring distinctions are useful when reading feature descriptions and test evidence: + +- **Document identity versus current selection.** A tab index, display name and filesystem + path describe different things. Changing the selected tab does not change which source + a previously started operation analyzed. Reopening the same path can create a different + session, and an edit can change the revision within one session. +- **Analysis versus edit versus export.** A finding list describes an input revision; a + preview can remain uncommitted; an export can describe an invocation snapshot. The + existence of a preview does not establish that document bytes changed or a file saved. +- **Cancellation request versus terminal cancellation.** A request can be pending while + a worker reaches a boundary. A completed subset may be intentionally retained. An + atomic commit and the surrounding multi-file run have different interruption scopes. +- **History availability versus restoration outcome.** A named undo entry establishes + that history exists. Successful restoration, rendering refresh and save/reopen are + separate observable outcomes. The research did not reproduce a PdfCraft history failure. +- **Atomic replacement versus destination conflict.** Staging avoids half-written output; + conflict detection deals with another writer changing the intended destination. They + address different failure modes. The roadmap records mechanisms in both categories. +- **Automated finding versus conformance.** A checker pass describes supported checks. + Manual review, unsupported constructs and a report's input identity affect how that + result can be interpreted. A pass is not independent certification. + +These distinctions are an interpretation vocabulary, not a checklist of newly required +features. The underlying product's declared behaviour determines whether retaining a +partial result or refusing an operation is the expected outcome. + +## Evaluation observations by workflow + +The following table describes what the archived evaluations observed or separated. It +neither reports a fresh PdfCraft test run nor assigns implementation tasks. Example +conditions describe evidence scope rather than newly discovered bugs. + +| Workflow | Observable relationship examined | Limits on interpreting the observation | +|---|---|---| +| OCR review | The recognized word, source region, selected row and magnified crop referred to the same token; a saved correction was distinguished from text displayed only in a preview. | Confidence was backend evidence, not a universal accuracy score. Correcting invisible searchable text and visibly editing a scanned phrase were distinct operations. | +| Comparison | Original/revised direction, selected change, displayed pages and exported report referred to the same comparison. Filtering and review disposition were distinguished from the full result set. | Equal text did not establish equal appearance or structure. Offset synchronization and content correspondence were distinct. A missing corresponding page was not page zero or a valid empty match. | +| Batch processing | The input, selected action, current step, saved outputs and per-file failures were separately observable. Completed outputs remained meaningful in a partially failed run. | Per-file atomicity did not imply a whole-run transaction. A retry could repeat a previously committed effect. `command_batch` documents that earlier edits can remain after a later error. | +| Accessibility review | Findings, selected tags and page locations referred to the same reachable objects. Proposed classification and applied structure were distinguished. | An automatic structural result did not establish reading quality. Text-only tagging evidence did not cover arbitrary images, tables or existing structure. | +| Standards checks | The profile, checked revision, findings and saved conversion artifact were separate evidence fields. | Structural checks for a subset of a standard were not a full validation claim. An unavailable external validator was not a passing result. | +| Page organization | Source selection, destination identity, permutation and resulting saved page order were evaluated separately. | Successful page transfer did not by itself establish preservation of every bookmark, form relationship or structure tree. | +| Redaction review | Marked regions, committed removal and checks on the saved output were separate stages. Verification wording was tied to the supported check scope. | A visual rectangle was not removal evidence. Reports and logs could themselves expose removed content. Absence under one extraction strategy did not prove absence under all encodings. | + +Archived state-machine notes used terms such as running, review-ready, saving, complete, +partial, failed and stale. Those terms describe distinct user-visible outcomes; they do +not establish that PdfCraft has, or needs, a single global state-machine type. Likewise, +historical multi-pane layouts described a way to observe several related values at once; +they are not adopted interface designs or a request for a replacement shell. + +## Keyboard, input and appearance evidence + +The research separated semantic labels from complete task access. A headless +accessibility tree can expose a control's name, role and state without establishing that +a person can finish a keyboard-only task or that a native screen reader announces the +result clearly. Focus ownership, return focus after closing a surface, background focus +changes and non-drag alternatives were observed independently from visual arrangement. +No native screen-reader session was performed for this handoff. + +[WCAG 2.2](https://www.w3.org/TR/WCAG22/) supplies technology-neutral criteria for +keyboard operation, focus, contrast and target sizes. It is a reference, not evidence +that this native application conforms to a level. The [WAI-ARIA tabs pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/) +describes web tab focus/activation semantics; it does not determine native widget or +platform accessibility implementations. + +Input-method research distinguished preedit text, committed text, selection, undo and +cancellation. Enter/Escape can have composition meanings in addition to dialog meanings. +Chinese palette composition tests exist in the inspected source. Their presence does +not establish candidate-window behaviour on Windows, macOS or Linux, or equivalent +coverage in other fields. RTL interface direction and PDF coordinate conventions were +also separate concerns. + +Appearance research distinguished interface chrome, page content and reading-colour +transforms. Switching an interface theme did not constitute editing a PDF's colours. +Token tables described intended colours; painted states, alpha composition, focus and +selection established different evidence. Layout evaluation recorded viewport, scaling, +translation length and text direction because a layout observation at one size did not +establish results at other sizes. No archived palette, icon family, font, screenshot or +prototype layout is included or recommended by this reference. + +## Evidence boundaries + +The material includes archived rationale, source inspection, documented conventions and +proposed-test descriptions from historical research. It does not include fresh native +acceptance, manual assistive-technology checks, independently measured OCR accuracy, +rendering benchmarks or external-reader compatibility results for this handoff. + +Names of tests in linked source establish that the source contains those tests. They do +not mean the tests were rerun here, or that an archived implementation's test result +transfers to PdfCraft. The code-inspection and documentation baselines differ and remain +explicit. Current statuses and future work belong to the repository's existing roadmap +and parity records, not this evaluation reference. diff --git a/docs/research/pdf-workflows/source-register.csv b/docs/research/pdf-workflows/source-register.csv new file mode 100644 index 0000000000..558fef67a5 --- /dev/null +++ b/docs/research/pdf-workflows/source-register.csv @@ -0,0 +1,181 @@ +source_id,product,topic,url,archive_date,evidence_state,origin_id,scope_note +SR001,ABBYY FineReader,Start window and task entry (FineReader 14 guide),https://help.abbyy.com/en-us/finereader/14/user_guide/startwindow_firstpage/,2026-09-25,archived-not-revalidated,expanded-01,"Archived source locator; supporting passage, edition and present behavior require review." +SR002,ABBYY FineReader,"User guide index: verification, comparison and automation",https://help.abbyy.com/en-us/finereader/16/,2026-09-07,locator-reconstructed-not-revalidated,source-title-recovery,Core study names guide chapters but not full URLs; locator reconstructed for discovery. +SR003,ABBYY FineReader,Product and feature documentation entry,https://pdf.abbyy.com/,2026-09-07,locator-reconstructed-not-revalidated,source-title-recovery,Core study identifies product documentation and brochure; root locator reconstructed. +SR004,ABBYY FineReader Engine SDK,FineReader Engine 12 SDK release notes,https://support.abbyy.com/hc/en-us/articles/37502278922642-FineReader-Engine-12-for-Windows-Release-8-Update-2,2026-09-25,archived-not-revalidated,expanded-01,"Archived source locator; supporting passage, edition and present behavior require review." +SR007,Adobe Acrobat,Acrobat product help index,https://helpx.adobe.com/acrobat/user-guide.html,2026-09-07,locator-reconstructed-not-revalidated,source-title-recovery,Core study names Help Center but not an exact URL; locator reconstructed for discovery. +SR008,Adobe Express,Adobe Express PDF editing guide,https://helpx.adobe.com/express/web/documents-and-presentations/edit-pdf.html,2026-09-25,archived-not-revalidated,expanded-02,"Archived source locator; supporting passage, edition and present behavior require review." +SR009,Adobe Express,Adobe Express release notes,https://helpx.adobe.com/express/web/whats-new/release-notes.html,2026-09-25,archived-not-revalidated,expanded-02,"Archived source locator; supporting passage, edition and present behavior require review." +SR010,Adobe Acrobat,Acrobat Studio product announcement,https://news.adobe.com/news/2026/01/adobe-acrobat-studio-transforms,2026-09-25,archived-not-revalidated,expanded-01,"Archived source locator; supporting passage, edition and present behavior require review." +SR011,Adobe Acrobat,Acrobat productivity product announcement,https://news.adobe.com/news/2026/05/adobes-new-productivity-agent,2026-09-25,archived-not-revalidated,expanded-01,"Archived source locator; supporting passage, edition and present behavior require review." +SR012,Adobe Acrobat,Acrobat JavaScript API reference,https://opensource.adobe.com/dc-acrobat-sdk-docs/library/jsapiref/index.html,2026-09-07,archived-not-revalidated,design-form-js,"Archived source locator; supporting passage, edition and present behavior require review." +SR013,Adobe Acrobat,Acrobat JavaScript developer guide,https://opensource.adobe.com/dc-acrobat-sdk-docs/library/jsdevguide/index.html,2026-09-07,archived-not-revalidated,design-form-js,"Archived source locator; supporting passage, edition and present behavior require review." +SR014,Adobe Acrobat,ISO 32000-1:2008 PDF specification,https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf,2026-09-07,archived-not-revalidated,design-form-js,"Archived source locator; supporting passage, edition and present behavior require review." +SR015,Bluebeam Revu,Community discussion (secondary workflow evidence),https://community.bluebeam.com/discussion/363,2026-09-07,archived-not-revalidated,core-bluebeam,Vendor-hosted community discussion; authorship and supporting passage need review. Not a vendor capability or policy statement. +SR016,Bluebeam Revu,Community discussion (secondary workflow evidence),https://community.bluebeam.com/discussion/5299/about-studio-prime-end-of-life-eol,2026-09-07,archived-not-revalidated,core-bluebeam,Vendor-hosted community discussion; authorship and supporting passage need review. Not a vendor capability or policy statement. +SR017,Bluebeam Revu,Documentation locator: markups list mtv,https://support.bluebeam.com/online-help/revu20/Content/RevuHelp/Menus/Window/Panels/Markups/Markups-List--MTV.htm,2026-09-07,archived-not-revalidated,core-bluebeam,"Archived source locator; supporting passage, edition and present behavior require review." +SR018,Bluebeam Revu,Documentation locator: compare documents vs overlay pages,https://support.bluebeam.com/revu/features/compare-documents-vs-overlay-pages.html,2026-09-07,archived-not-revalidated,core-bluebeam,"Archived source locator; supporting passage, edition and present behavior require review." +SR019,Bluebeam Revu,Documentation locator: manage and review markups with filter list,https://support.bluebeam.com/revu/how-to/manage-and-review-markups-with-filter-list.html,2026-09-07,archived-not-revalidated,core-bluebeam,"Archived source locator; supporting passage, edition and present behavior require review." +SR020,Bluebeam Revu,Documentation locator: track and manage markups using markups list,https://support.bluebeam.com/revu/how-to/track-and-manage-markups-using-markups-list.html,2026-09-07,archived-not-revalidated,core-bluebeam,"Archived source locator; supporting passage, edition and present behavior require review." +SR021,Bluebeam Revu,Documentation locator: revu 20 eol,https://support.bluebeam.com/revu/resources/revu-20-eol.html,2026-09-07,archived-not-revalidated,core-bluebeam,"Archived source locator; supporting passage, edition and present behavior require review." +SR022,Bluebeam Revu,Release notes and change history,https://support.bluebeam.com/revu/resources/revu-21-release-notes.html,2026-09-07,archived-not-revalidated,core-bluebeam;expanded-01,"Archived source locator; supporting passage, edition and present behavior require review." +SR023,Bluebeam Revu,Documentation locator: revu mcp,https://support.bluebeam.com/revu/resources/revu-mcp.html,2026-09-25,archived-not-revalidated,expanded-01,"Archived source locator; supporting passage, edition and present behavior require review." +SR024,Bluebeam Revu,Documentation locator: bluebeam subscription faq,https://support.bluebeam.com/revu/subscription/onboarding/bluebeam-subscription-faq.html,2026-09-07,archived-not-revalidated,core-bluebeam,"Archived source locator; supporting passage, edition and present behavior require review." +SR025,Bluebeam Revu,Documentation locator: subscription features,https://support.bluebeam.com/revu/subscription/subscription-features.html,2026-09-07,archived-not-revalidated,core-bluebeam,"Archived source locator; supporting passage, edition and present behavior require review." +SR026,Bluebeam Revu,Documentation locator: studio prime eol announcement,https://support.bluebeam.com/studio/resources/studio-prime-eol-announcement.html,2026-09-07,archived-not-revalidated,core-bluebeam,"Archived source locator; supporting passage, edition and present behavior require review." +SR027,Bluebeam Revu,Documentation locator: link,https://support.bluebeam.com/user-manual/menus/batch/link.html,2026-09-07,archived-not-revalidated,core-bluebeam,"Archived source locator; supporting passage, edition and present behavior require review." +SR028,Bluebeam Revu,Documentation locator: sign seal,https://support.bluebeam.com/user-manual/menus/batch/sign-seal.html,2026-09-07,archived-not-revalidated,core-bluebeam,"Archived source locator; supporting passage, edition and present behavior require review." +SR029,Bluebeam Revu,Documentation locator: slip sheet,https://support.bluebeam.com/user-manual/menus/batch/slip-sheet.html,2026-09-07,archived-not-revalidated,core-bluebeam,"Archived source locator; supporting passage, edition and present behavior require review." +SR030,Bluebeam Revu,Documentation locator: compare documents,https://support.bluebeam.com/user-manual/menus/document/compare-documents.html,2026-09-07,archived-not-revalidated,core-bluebeam,"Archived source locator; supporting passage, edition and present behavior require review." +SR031,Bluebeam Revu,Documentation locator: ocr,https://support.bluebeam.com/user-manual/menus/document/ocr.html,2026-09-07,archived-not-revalidated,core-bluebeam,"Archived source locator; supporting passage, edition and present behavior require review." +SR032,Bluebeam Revu,Documentation locator: overlay pages,https://support.bluebeam.com/user-manual/menus/document/overlay-pages.html,2026-09-07,archived-not-revalidated,core-bluebeam,"Archived source locator; supporting passage, edition and present behavior require review." +SR033,Bluebeam Revu,Documentation locator: erase content,https://support.bluebeam.com/user-manual/menus/edit/erase-content.html,2026-09-07,archived-not-revalidated,core-bluebeam,"Archived source locator; supporting passage, edition and present behavior require review." +SR034,Bluebeam Revu,Documentation locator: redaction,https://support.bluebeam.com/user-manual/menus/edit/redaction.html,2026-09-07,archived-not-revalidated,core-bluebeam,"Archived source locator; supporting passage, edition and present behavior require review." +SR035,Bluebeam Revu,Documentation locator: create forms,https://support.bluebeam.com/user-manual/menus/tools/create-forms.html,2026-09-07,archived-not-revalidated,core-bluebeam,"Archived source locator; supporting passage, edition and present behavior require review." +SR036,Bluebeam Revu,Documentation locator: form fields,https://support.bluebeam.com/user-manual/menus/tools/form-fields.html,2026-09-07,archived-not-revalidated,core-bluebeam,"Archived source locator; supporting passage, edition and present behavior require review." +SR037,Bluebeam Revu,Documentation locator: manage self signed ids,https://support.bluebeam.com/user-manual/menus/tools/manage-self-signed-ids.html,2026-09-07,archived-not-revalidated,core-bluebeam,"Archived source locator; supporting passage, edition and present behavior require review." +SR038,Bluebeam Revu,Documentation locator: create edit sets,https://support.bluebeam.com/user-manual/menus/window/create-edit-sets.html,2026-09-07,archived-not-revalidated,core-bluebeam,"Archived source locator; supporting passage, edition and present behavior require review." +SR039,Bluebeam Revu,Documentation locator: punch tool set,https://support.bluebeam.com/user-manual/menus/window/punch-tool-set.html,2026-09-07,archived-not-revalidated,core-bluebeam,"Archived source locator; supporting passage, edition and present behavior require review." +SR040,Bluebeam Revu,Licensing and edition reference (historical; no current price assertion),https://www.bluebeam.com/pricing/,2026-09-07,archived-not-revalidated,core-bluebeam,"Archived source locator; supporting passage, edition and present behavior require review." +SR041,ETSI,Documentation locator: 31914201,https://www.etsi.org/deliver/etsi_en/319100_319199/31914201/,2026-09-09,archived-not-revalidated,design-send-for-signing,"Archived source locator; supporting passage, edition and present behavior require review." +SR042,ETSI,Documentation locator: en 31914201v010201p,https://www.etsi.org/deliver/etsi_en/319100_319199/31914201/01.02.01_60/en_31914201v010201p.pdf,2026-09-09,archived-not-revalidated,design-send-for-signing,"Archived source locator; supporting passage, edition and present behavior require review." +SR043,Firefox / pdf.js,Form scripting API implementation reference,https://github.com/mozilla/pdf.js/blob/master/src/scripting_api/aform.js,2026-09-07,archived-not-revalidated,design-form-js,"Archived source locator; supporting passage, edition and present behavior require review." +SR044,Firefox / pdf.js,Documentation locator: pdf alt text,https://support.mozilla.org/en-US/kb/pdf-alt-text,2026-09-25,archived-not-revalidated,expanded-02,"Archived source locator; supporting passage, edition and present behavior require review." +SR045,Foxit PDF Editor,Documentation locator: pdf products comparison,https://cdn01.foxitsoftware.com/pub/foxit/datasheet/general/en_us/pdf-products-comparison.pdf,2026-09-07,archived-not-revalidated,core-foxit,"Archived source locator; supporting passage, edition and present behavior require review." +SR046,Foxit PDF Editor,Documentation locator: articles,https://kb.foxit.com/s/articles/,2026-09-07,archived-not-revalidated,core-foxit,"Archived source locator; supporting passage, edition and present behavior require review." +SR047,Foxit PDF Editor,Documentation locator: 360040658511,https://kb.foxit.com/s/articles/360040658511,2026-09-07,archived-not-revalidated,core-foxit,"Archived source locator; supporting passage, edition and present behavior require review." +SR048,Foxit PDF Editor,Documentation locator: 4410630911764,https://kb.foxit.com/s/articles/4410630911764,2026-09-07,archived-not-revalidated,core-foxit,"Archived source locator; supporting passage, edition and present behavior require review." +SR049,Foxit PDF Editor,Documentation locator: foxit pdf editor perpetual license support policy,https://kb.foxit.com/s/articles/Foxit-PDF-Editor-Perpetual-License-Support-Policy,2026-09-07,archived-not-revalidated,core-foxit,"Archived source locator; supporting passage, edition and present behavior require review." +SR050,Foxit PDF Editor,Documentation locator: steps to upgrade to foxit pdf editor version 14 on windows,https://kb.foxit.com/s/articles/Steps-to-Upgrade-to-Foxit-PDF-Editor-Version-14-on-Windows,2026-09-07,archived-not-revalidated,core-foxit,"Archived source locator; supporting passage, edition and present behavior require review." +SR051,Foxit PDF Editor,Documentation locator: foxit pdf editor aec drawing sets update,https://www.foxit.com/blog/foxit-pdf-editor-aec-drawing-sets-update/,2026-09-25,archived-not-revalidated,expanded-01,"Archived source locator; supporting passage, edition and present behavior require review." +SR052,Foxit PDF Editor,Documentation locator: foxits perpetual license support policy what you need to know,https://www.foxit.com/blog/foxits-perpetual-license-support-policy-what-you-need-to-know/,2026-09-07,archived-not-revalidated,core-foxit,"Archived source locator; supporting passage, edition and present behavior require review." +SR053,Foxit PDF Editor,Documentation locator: 11200,https://www.foxit.com/company/press/11200.html,2026-09-07,archived-not-revalidated,core-foxit,"Archived source locator; supporting passage, edition and present behavior require review." +SR054,Foxit PDF Editor,Licensing and edition reference (historical; no current price assertion),https://www.foxit.com/pdf-editor/pricing/,2026-09-07,archived-not-revalidated,core-foxit,"Archived source locator; supporting passage, edition and present behavior require review." +SR055,Foxit PDF Editor,Release notes and change history,https://www.foxit.com/pdf-editor/version-history.html,2026-09-07,archived-not-revalidated,core-foxit,"Archived source locator; supporting passage, edition and present behavior require review." +SR056,Foxit PDF Editor,Documentation locator: add custom commands to custom actions,https://www.foxit.com/resource-hub/user-guide/foxit-pdf-editor-for-windows/add-custom-commands-to-custom-actions/,2026-09-07,archived-not-revalidated,core-foxit,"Archived source locator; supporting passage, edition and present behavior require review." +SR057,Foxit PDF Editor,Documentation locator: shared review,https://www.foxit.com/resource-hub/user-guide/foxit-pdf-editor-for-windows/shared-review/,2026-09-07,archived-not-revalidated,core-foxit,"Archived source locator; supporting passage, edition and present behavior require review." +SR058,Foxit PDF Editor,Published policy or security reference (historical),https://www.foxit.com/support/refund-policy/,2026-09-07,archived-not-revalidated,core-foxit,"Archived source locator; supporting passage, edition and present behavior require review." +SR059,Icon system,Icon asset provenance and licensing reference,https://github.com/google/material-design-icons,2026-09-25,archived-not-revalidated,expanded-04,"Archived source locator; supporting passage, edition and present behavior require review." +SR060,Icon system,Icon asset provenance and licensing reference,https://github.com/lucide-icons/lucide,2026-09-25,archived-not-revalidated,expanded-04,"Archived source locator; supporting passage, edition and present behavior require review." +SR061,Icon system,Icon asset provenance and licensing reference,https://github.com/microsoft/fluentui-system-icons,2026-09-25,archived-not-revalidated,expanded-04,"Archived source locator; supporting passage, edition and present behavior require review." +SR062,Icon system,Icon asset provenance and licensing reference,https://github.com/tabler/tabler-icons,2026-09-25,archived-not-revalidated,expanded-04,"Archived source locator; supporting passage, edition and present behavior require review." +SR063,Icon system,License text (historical locator; review exact version before reuse),https://raw.githubusercontent.com/lucide-icons/lucide/main/LICENSE,2026-09-25,archived-not-revalidated,expanded-04,"Archived source locator; supporting passage, edition and present behavior require review." +SR064,IETF,Documentation locator: rfc3161,https://datatracker.ietf.org/doc/html/rfc3161,2026-09-09,archived-not-revalidated,design-send-for-signing,"Archived source locator; supporting passage, edition and present behavior require review." +SR065,iLovePDF,Documentation locator: pdf web or desktop,https://www.ilovepdf.com/blog/pdf-web-or-desktop,2026-09-08,archived-not-revalidated,core-ilovepdf,"Archived source locator; supporting passage, edition and present behavior require review." +SR066,iLovePDF,Documentation locator: desktop,https://www.ilovepdf.com/desktop,2026-09-08,archived-not-revalidated,core-ilovepdf,"Archived source locator; supporting passage, edition and present behavior require review." +SR067,iLovePDF,Licensing and edition reference (historical; no current price assertion),https://www.ilovepdf.com/pricing,2026-09-08,archived-not-revalidated,core-ilovepdf,"Archived source locator; supporting passage, edition and present behavior require review." +SR068,iLovePDF,Documentation locator: sitemap.xml,https://www.ilovepdf.com/sitemap.xml,2026-09-08,archived-not-revalidated,core-ilovepdf,"Archived source locator; supporting passage, edition and present behavior require review." +SR069,KDE,Documentation locator: show bug.cgi,https://bugs.kde.org/show_bug.cgi?id=443403,2026-09-07,archived-not-revalidated,core-okular,"Archived source locator; supporting passage, edition and present behavior require review." +SR070,KDE,Documentation locator: show bug.cgi,https://bugs.kde.org/show_bug.cgi?id=489299,2026-09-07,archived-not-revalidated,core-okular,"Archived source locator; supporting passage, edition and present behavior require review." +SR072,KDE,Release notes and change history,https://kde.org/announcements/changelogs/gear/,2026-09-07,archived-not-revalidated,core-okular,"Archived source locator; supporting passage, edition and present behavior require review." +SR073,KDE,Documentation locator: 22.04.0,https://kde.org/announcements/gear/22.04.0/,2026-09-07,archived-not-revalidated,core-okular,"Archived source locator; supporting passage, edition and present behavior require review." +SR074,KDE,Documentation locator: 23.08.0,https://kde.org/announcements/gear/23.08.0/,2026-09-07,archived-not-revalidated,core-okular,"Archived source locator; supporting passage, edition and present behavior require review." +SR075,KDE,Documentation locator: 26.08.0,https://kde.org/announcements/gear/26.08.0/,2026-09-25,archived-not-revalidated,expanded-01,"Archived source locator; supporting passage, edition and present behavior require review." +SR076,LibreOffice Draw,PDF export and security help index,https://help.libreoffice.org/latest/en-US/text/shared/01/ref_pdf_export.html,2026-09-07,locator-reconstructed-not-revalidated,source-title-recovery,Core study names export help paths without full URLs; locator reconstructed for discovery. +SR077,Master PDF Editor,Documentation locator: downloads,https://code-industry.net/downloads/,2026-09-07,archived-not-revalidated,core-masterpdf,"Archived source locator; supporting passage, edition and present behavior require review." +SR078,Master PDF Editor,Documentation locator: faq,https://code-industry.net/faq/,2026-09-07,archived-not-revalidated,core-masterpdf,"Archived source locator; supporting passage, edition and present behavior require review." +SR079,Master PDF Editor,Documentation locator: masterpdfeditor help,https://code-industry.net/masterpdfeditor-help/,2026-09-07,archived-not-revalidated,core-masterpdf,"Archived source locator; supporting passage, edition and present behavior require review." +SR080,Master PDF Editor,Workspace navigation and object inspection,https://code-industry.net/masterpdfeditor-help/workspace-master-pdf-editor/,2026-09-25,archived-not-revalidated,expanded-01,"Archived source locator; supporting passage, edition and present behavior require review." +SR081,Master PDF Editor,Documentation locator: masterpdfeditor,https://code-industry.net/masterpdfeditor/,2026-09-07,archived-not-revalidated,core-masterpdf,"Archived source locator; supporting passage, edition and present behavior require review." +SR082,Master PDF Editor,Licensing and edition reference (historical; no current price assertion),https://code-industry.net/purchase/,2026-09-07,archived-not-revalidated,core-masterpdf,"Archived source locator; supporting passage, edition and present behavior require review." +SR083,Master PDF Editor,Documentation locator: what is new in master pdf editor 5,https://code-industry.net/what-is-new-in-master-pdf-editor-5/,2026-09-07,archived-not-revalidated,core-masterpdf,"Archived source locator; supporting passage, edition and present behavior require review." +SR084,Microsoft platform documentation,Documentation locator: 9n41msq1wnm8,https://apps.microsoft.com/detail/9n41msq1wnm8,2026-09-07,archived-not-revalidated,core-okular,"Archived source locator; supporting passage, edition and present behavior require review." +SR085,Microsoft platform documentation,Documentation locator: xpfd51h3vqzfm0,https://apps.microsoft.com/detail/xpfd51h3vqzfm0,2026-09-08,archived-not-revalidated,core-pdf24,"Archived source locator; supporting passage, edition and present behavior require review." +SR086,Microsoft platform documentation,Documentation locator: pdf reader,https://explore.microsoft.com/en-us/edge/features/pdf-reader,2026-09-25,archived-not-revalidated,expanded-02,"Archived source locator; supporting passage, edition and present behavior require review." +SR087,Nitro PDF Pro,Community discussion (secondary workflow evidence),https://community.gonitro.com/topic/12609/,2026-09-07,archived-not-revalidated,core-nitro,Vendor-hosted community discussion; authorship and supporting passage need review. Not a vendor capability or policy statement. +SR088,Nitro PDF Pro,Community discussion (secondary workflow evidence),https://community.gonitro.com/topic/21169/,2026-09-07,archived-not-revalidated,core-nitro,Vendor-hosted community discussion; authorship and supporting passage need review. Not a vendor capability or policy statement. +SR089,Nitro PDF Pro,Community discussion (secondary workflow evidence),https://community.gonitro.com/topic/22140/,2026-09-07,archived-not-revalidated,core-nitro,Vendor-hosted community discussion; authorship and supporting passage need review. Not a vendor capability or policy statement. +SR090,Nitro PDF Pro,Documentation locator: automate,https://www.gonitro.com/automate,2026-09-07,archived-not-revalidated,core-nitro,"Archived source locator; supporting passage, edition and present behavior require review." +SR091,Nitro PDF Pro,Documentation locator: bulk send and templates,https://www.gonitro.com/bulk-send-and-templates,2026-09-07,archived-not-revalidated,core-nitro,"Archived source locator; supporting passage, edition and present behavior require review." +SR092,Nitro PDF Pro,Release notes and change history,https://www.gonitro.com/documentation/release-notes,2026-09-07,archived-not-revalidated,core-nitro;expanded-01,"Archived source locator; supporting passage, edition and present behavior require review." +SR093,Nitro PDF Pro,Documentation locator: pdf pro,https://www.gonitro.com/pdf-pro,2026-09-07,archived-not-revalidated,core-nitro,"Archived source locator; supporting passage, edition and present behavior require review." +SR094,Nitro PDF Pro,Licensing and edition reference (historical; no current price assertion),https://www.gonitro.com/pricing,2026-09-07,archived-not-revalidated,core-nitro,"Archived source locator; supporting passage, edition and present behavior require review." +SR095,Nitro PDF Pro,Release notes and change history,https://www.gonitro.com/release-hub/releases,2026-09-25,archived-not-revalidated,expanded-01,"Archived source locator; supporting passage, edition and present behavior require review." +SR096,Nitro PDF Pro,Release notes and change history,https://www.gonitro.com/release-hub/releases/sign-api,2026-09-07,archived-not-revalidated,core-nitro,"Archived source locator; supporting passage, edition and present behavior require review." +SR097,Nitro PDF Pro,Documentation locator: nitros complete guide to secure automated pdf redaction,https://www.gonitro.com/resources/nitros-complete-guide-to-secure-automated-pdf-redaction,2026-09-07,archived-not-revalidated,core-nitro,"Archived source locator; supporting passage, edition and present behavior require review." +SR098,Nitro PDF Pro,Documentation locator: sign,https://www.gonitro.com/sign,2026-09-07,archived-not-revalidated,core-nitro,"Archived source locator; supporting passage, edition and present behavior require review." +SR099,Okular,Project source and technical documentation reference,https://github.com/KDE/okular,2026-09-07,archived-not-revalidated,core-okular,"Archived source locator; supporting passage, edition and present behavior require review." +SR100,Okular,Product or project documentation entry,https://okular.kde.org/,2026-09-07,archived-not-revalidated,core-okular,Landing/index locator only; locate a specific supporting page before citing a feature. +SR101,Okular,Documentation locator: download,https://okular.kde.org/download/,2026-09-07,archived-not-revalidated,core-okular,"Archived source locator; supporting passage, edition and present behavior require review." +SR102,Okular,Documentation locator: faq,https://okular.kde.org/faq/,2026-09-07,archived-not-revalidated,core-okular,"Archived source locator; supporting passage, edition and present behavior require review." +SR103,Okular,Documentation locator: formats,https://okular.kde.org/formats/,2026-09-07,archived-not-revalidated,core-okular,"Archived source locator; supporting passage, edition and present behavior require review." +SR104,Okular,Project source and technical documentation reference,https://raw.githubusercontent.com/KDE/okular/master/doc/index.docbook,2026-09-07,archived-not-revalidated,core-okular,"Archived source locator; supporting passage, edition and present behavior require review." +SR105,PDF Association,Product or project documentation entry,https://pdfa.org,2026-09-07,archived-not-revalidated,core-updf,Landing/index locator only; locate a specific supporting page before citing a feature. +SR106,PDF Association,Release notes and change history,https://pdfa.org/foxit-20252-and-v14-release-highlights-faster-smarter-more-secure/,2026-09-07,archived-not-revalidated,core-foxit,"Archived source locator; supporting passage, edition and present behavior require review." +SR107,PDF Association,Foxit PDF Editor 2026.2 release announcement,https://pdfa.org/foxit-pdf-editor-20262-release-notes/,2026-09-25,archived-not-revalidated,expanded-01,"Archived source locator; supporting passage, edition and present behavior require review." +SR108,PDF Association,Accessible PDF standards and reference material,https://pdfa.org/iso-14289-2-pdf-ua-2-the-gold-standard-for-accessibility-in-pdf-2-0-has-arrived/,2026-09-25,archived-not-revalidated,expanded-02,"Archived source locator; supporting passage, edition and present behavior require review." +SR109,PDF Association,UPDF 2.5 release announcement,https://pdfa.org/updf-25-officially-launches-worldwide-the-first-pdf-editor-powered-by-autonomous-ai-agents/,2026-09-25,archived-not-revalidated,expanded-01,"Archived source locator; supporting passage, edition and present behavior require review." +SR110,PDF Association,Accessible PDF standards and reference material,https://pdfa.org/wtpdf/,2026-09-25,archived-not-revalidated,expanded-02,"Archived source locator; supporting passage, edition and present behavior require review." +SR111,PDF Expert,Product or project documentation entry,https://copilot.pdfexpert.com/,2026-09-07,archived-not-revalidated,core-pdfexpert;expanded-01,Landing/index locator only; locate a specific supporting page before citing a feature. +SR112,PDF Expert,Product or project documentation entry,https://pdfexpert.com/,2026-09-07,archived-not-revalidated,core-pdfexpert,Landing/index locator only; locate a specific supporting page before citing a feature. +SR113,PDF Expert,PDF Expert assisted document-reading announcement,https://pdfexpert.com/blog/introducing-ai-chat,2026-09-25,archived-not-revalidated,expanded-01,"Archived source locator; supporting passage, edition and present behavior require review." +SR114,PDF Expert,Product or project documentation entry,https://readdle.com/,2026-09-07,archived-not-revalidated,core-pdfexpert,Landing/index locator only; locate a specific supporting page before citing a feature. +SR115,PDF Expert,Documentation locator: is pdf expert available on windows,https://support.readdle.com/pdfexpert/en_US/billing-subscription/is-pdf-expert-available-on-windows,2026-09-07,archived-not-revalidated,core-pdfexpert;expanded-01,"Archived source locator; supporting passage, edition and present behavior require review." +SR116,PDF library documentation,Project source and technical documentation reference,https://raw.githubusercontent.com/podofo/podofo/1.1.0/src/podofo/main/PdfSignerCms.cpp,2026-09-09,archived-not-revalidated,design-send-for-signing,"Archived source locator; supporting passage, edition and present behavior require review." +SR117,PDF-XChange,Documentation locator: features added in v9,https://help.pdf-xchange.com/pdfxe11/features-added-in-v9.html,2026-09-25,archived-not-revalidated,expanded-01,"Archived source locator; supporting passage, edition and present behavior require review." +SR118,PDF-XChange,Documentation locator: 352,https://www.pdf-xchange.com/knowledgebase/352-,2026-09-07,archived-not-revalidated,core-pdfxchange,"Archived source locator; supporting passage, edition and present behavior require review." +SR119,PDF-XChange,Documentation locator: 554,https://www.pdf-xchange.com/knowledgebase/554-,2026-09-07,archived-not-revalidated,core-pdfxchange,"Archived source locator; supporting passage, edition and present behavior require review." +SR120,PDF-XChange,Documentation locator: pdf xchange ocr,https://www.pdf-xchange.com/pdf-xchange-ocr,2026-09-07,archived-not-revalidated,core-pdfxchange,"Archived source locator; supporting passage, edition and present behavior require review." +SR121,PDF-XChange,Documentation locator: pdf xchange products comparison chart,https://www.pdf-xchange.com/pdf-xchange-products-comparison-chart,2026-09-07,archived-not-revalidated,core-pdfxchange,"Archived source locator; supporting passage, edition and present behavior require review." +SR122,PDF-XChange,Published policy or security reference (historical),https://www.pdf-xchange.com/privacy-policy,2026-09-07,archived-not-revalidated,core-pdfxchange,"Archived source locator; supporting passage, edition and present behavior require review." +SR123,PDF-XChange,Documentation locator: pdf xchange editor,https://www.pdf-xchange.com/product/pdf-xchange-editor,2026-09-07,archived-not-revalidated,core-pdfxchange;expanded-01,"Archived source locator; supporting passage, edition and present behavior require review." +SR124,PDF-XChange,Release notes and change history,https://www.pdf-xchange.com/product/pdf-xchange-editor/history,2026-09-07,archived-not-revalidated,core-pdfxchange;expanded-01,"Archived source locator; supporting passage, edition and present behavior require review." +SR125,PDF-XChange,Published policy or security reference (historical),https://www.pdf-xchange.com/support/security-bulletins.html,2026-09-07,archived-not-revalidated,core-pdfxchange,"Archived source locator; supporting passage, edition and present behavior require review." +SR126,PDF24,Desktop Creator documentation entry,https://creator.pdf24.org/,2026-09-08,locator-reconstructed-not-revalidated,source-title-recovery,Product documentation index; exact supporting feature pages require review. +SR127,PDF24,Release notes and change history,https://creator.pdf24.org/changelog/en.html,2026-09-08,archived-not-revalidated,core-pdf24,"Archived source locator; supporting passage, edition and present behavior require review." +SR128,PDF24,Documentation locator: 11,https://creator.pdf24.org/manual/11/,2026-09-08,archived-not-revalidated,core-pdf24,"Archived source locator; supporting passage, edition and present behavior require review." +SR129,PDF24,Documentation locator: avoid autostart of pdf24,https://help.pdf24.org/en/forums/topic/avoid-autostart-of-pdf24/,2026-09-08,archived-not-revalidated,core-pdf24,"Archived source locator; supporting passage, edition and present behavior require review." +SR130,PDF24,Documentation locator: all tools,https://tools.pdf24.org/en/all-tools,2026-09-08,archived-not-revalidated,core-pdf24,"Archived source locator; supporting passage, edition and present behavior require review." +SR131,PDF24,Documentation locator: compress pdf,https://tools.pdf24.org/en/compress-pdf,2026-09-08,archived-not-revalidated,core-pdf24,"Archived source locator; supporting passage, edition and present behavior require review." +SR132,PDF24,Documentation locator: creator,https://tools.pdf24.org/en/creator,2026-09-08,archived-not-revalidated,core-pdf24,"Archived source locator; supporting passage, edition and present behavior require review." +SR133,PDF24,Documentation locator: ocr pdf,https://tools.pdf24.org/en/ocr-pdf,2026-09-08,archived-not-revalidated,core-pdf24,"Archived source locator; supporting passage, edition and present behavior require review." +SR134,PDF24,Documentation locator: redact pdf,https://tools.pdf24.org/en/redact-pdf,2026-09-08,archived-not-revalidated,core-pdf24,"Archived source locator; supporting passage, edition and present behavior require review." +SR135,PDF24,Documentation locator: en,https://www.pdf24.org/en/,2026-09-08,archived-not-revalidated,core-pdf24,"Archived source locator; supporting passage, edition and present behavior require review." +SR136,PDF4QT,Project source and technical documentation reference,https://github.com/JakubMelka/PDF4QT,2026-09-25,archived-not-revalidated,expanded-02,"Archived source locator; supporting passage, edition and present behavior require review." +SR138,PDFelement,PDFelement version history,https://pdf.wondershare.com/whats-new.html,2026-09-25,archived-not-revalidated,expanded-01,"Archived source locator; supporting passage, edition and present behavior require review." +SR139,PDFgear,Product or project documentation entry,https://www.pdfgear.com/,2026-09-07,archived-not-revalidated,core-pdfgear,Landing/index locator only; locate a specific supporting page before citing a feature. +SR140,PDFgear,PDFgear version history,https://www.pdfgear.com/whats-new/,2026-09-25,archived-not-revalidated,expanded-01,"Archived source locator; supporting passage, edition and present behavior require review." +SR141,PDFsam,Documentation locator: faq,https://pdfsam.org/faq/,2026-09-25,archived-not-revalidated,expanded-02,"Archived source locator; supporting passage, edition and present behavior require review." +SR142,Power PDF,Power PDF licensing and edition reference (historical),https://www.tungstenautomation.com/Products/power-pdf/pricing,2026-09-25,archived-not-revalidated,expanded-01,"Archived source locator; supporting passage, edition and present behavior require review." +SR143,Power PDF,Power PDF 2025.3 release announcement,https://www.tungstenautomation.com/press-releases/2025/tungsten-automation-launches-power-pdf-2025-3,2026-09-25,archived-not-revalidated,expanded-01,"Archived source locator; supporting passage, edition and present behavior require review." +SR144,Qt documentation,Native UI or image/color API documentation,https://doc.qt.io/qt-6.8/qtquickcontrols-fluentwinui3.html,2026-09-25,archived-not-revalidated,expanded-04,"Archived source locator; supporting passage, edition and present behavior require review." +SR145,Qt documentation,Native UI or image/color API documentation,https://doc.qt.io/qt-6/qcolorspace.html,2026-10-04,archived-not-revalidated,design-cmyk-lcms2-plan-2026-10-04,"Archived source locator; supporting passage, edition and present behavior require review." +SR146,Qt documentation,Native UI or image/color API documentation,https://doc.qt.io/qt-6/qimage.html,2026-10-04,archived-not-revalidated,design-cmyk-lcms2-plan-2026-10-04,"Archived source locator; supporting passage, edition and present behavior require review." +SR147,Qt documentation,Native UI or image/color API documentation,https://doc.qt.io/qt-6/qtquickcontrols-fluentwinui3.html,2026-09-25,archived-not-revalidated,expanded-04,"Archived source locator; supporting passage, edition and present behavior require review." +SR148,Qt documentation,Native UI or image/color API documentation,https://doc.qt.io/qt-6/style-reference.html,2026-09-25,archived-not-revalidated,expanded-04,"Archived source locator; supporting passage, edition and present behavior require review." +SR149,Qt documentation,Native UI or image/color API documentation,https://doc.qt.io/qt-6/whatsnew68.html,2026-10-04,archived-not-revalidated,design-cmyk-lcms2-plan-2026-10-04,"Archived source locator; supporting passage, edition and present behavior require review." +SR150,Qt documentation,Release notes and change history,https://forum.qt.io/topic/156001/style-mistake-in-win11-with-qt-6-7-mingw-11-2-official-online-release,2026-09-25,archived-not-revalidated,expanded-04,"Archived source locator; supporting passage, edition and present behavior require review." +SR151,Qt documentation,Release notes and change history,https://www.qt.io/blog/qt-6.7-released,2026-09-25,archived-not-revalidated,expanded-04,"Archived source locator; supporting passage, edition and present behavior require review." +SR152,Qt documentation,Release notes and change history,https://www.qt.io/blog/qt-6.8-released,2026-09-25,archived-not-revalidated,expanded-04,"Archived source locator; supporting passage, edition and present behavior require review." +SR153,Script runtime documentation,Script runtime technical and licensing reference,https://duktape.org/download,2026-09-07,archived-not-revalidated,design-form-js,"Archived source locator; supporting passage, edition and present behavior require review." +SR154,Script runtime documentation,Script runtime technical and licensing reference,https://duktape.org/guide,2026-09-07,archived-not-revalidated,design-form-js,"Archived source locator; supporting passage, edition and present behavior require review." +SR155,Script runtime documentation,Script runtime technical and licensing reference,https://github.com/ArtifexSoftware/mujs/tags,2026-09-07,archived-not-revalidated,design-form-js,"Archived source locator; supporting passage, edition and present behavior require review." +SR156,Script runtime documentation,Script runtime repository and licensing reference,https://github.com/quickjs-ng/quickjs,2026-09-07,archived-not-revalidated,design-form-js,"Archived source locator; supporting passage, edition and present behavior require review." +SR157,Script runtime documentation,Script runtime technical and licensing reference,https://github.com/svaarala/duktape,2026-09-07,archived-not-revalidated,design-form-js,"Archived source locator; supporting passage, edition and present behavior require review." +SR158,Script runtime documentation,Script runtime technical and licensing reference,https://mujs.com/,2026-09-07,archived-not-revalidated,design-form-js,Landing/index locator only; locate a specific supporting page before citing a feature. +SR159,Script runtime documentation,License text (historical locator; review exact version before reuse),https://mujs.com/license.html,2026-09-07,archived-not-revalidated,design-form-js,"Archived source locator; supporting passage, edition and present behavior require review." +SR160,Script runtime documentation,Script runtime technical and licensing reference,https://quickjs-ng.github.io/quickjs/developer-guide/intro/,2026-09-07,archived-not-revalidated,design-form-js,"Archived source locator; supporting passage, edition and present behavior require review." +SR161,Sejda,Project source and technical documentation reference,https://github.com/torakiki/sejda,2026-09-08,archived-not-revalidated,core-sejda,"Archived source locator; supporting passage, edition and present behavior require review." +SR162,Sejda,Product or project documentation entry,https://www.sejda.com/,2026-09-08,archived-not-revalidated,core-sejda,Landing/index locator only; locate a specific supporting page before citing a feature. +SR163,Sejda,Documentation locator: workflows,https://www.sejda.com/workflows,2026-09-09,archived-not-revalidated,design-batch-presets,"Archived source locator; supporting passage, edition and present behavior require review." +SR164,Smallpdf,Documentation locator: compare,https://smallpdf.com/compare,2026-09-08,archived-not-revalidated,core-smallpdf,"Archived source locator; supporting passage, edition and present behavior require review." +SR165,Smallpdf,Documentation locator: desktop,https://smallpdf.com/desktop,2026-09-08,archived-not-revalidated,core-smallpdf,"Archived source locator; supporting passage, edition and present behavior require review." +SR166,Smallpdf,Documentation locator: windows,https://smallpdf.com/download/windows,2026-09-08,archived-not-revalidated,core-smallpdf,"Archived source locator; supporting passage, edition and present behavior require review." +SR167,Smallpdf,Documentation locator: edit pdf,https://smallpdf.com/edit-pdf,2026-09-08,archived-not-revalidated,core-smallpdf,"Archived source locator; supporting passage, edition and present behavior require review." +SR168,Smallpdf,Licensing and edition reference (historical; no current price assertion),https://smallpdf.com/pricing,2026-09-08,archived-not-revalidated,core-smallpdf,"Archived source locator; supporting passage, edition and present behavior require review." +SR169,Smallpdf,Published policy or security reference (historical),https://smallpdf.com/privacy,2026-09-08,archived-not-revalidated,core-smallpdf,"Archived source locator; supporting passage, edition and present behavior require review." +SR170,Stirling-PDF,Product or project documentation entry,https://docs.stirlingpdf.com/,2026-09-25,archived-not-revalidated,expanded-02,Landing/index locator only; locate a specific supporting page before citing a feature. +SR171,Stirling-PDF,Structured automation interface documentation,https://docs.stirlingpdf.com/Configuration/Automation/MCP%20Server/,2026-09-25,archived-not-revalidated,expanded-02,"Archived source locator; supporting passage, edition and present behavior require review." +SR172,Stirling-PDF,Automation pipeline documentation,https://docs.stirlingpdf.com/Configuration/Automation/Pipeline/,2026-09-25,archived-not-revalidated,expanded-02,"Archived source locator; supporting passage, edition and present behavior require review." +SR173,Stirling-PDF,Licensing and edition reference (historical; no current price assertion),https://docs.stirlingpdf.com/Modes%20and%20Licensing/,2026-09-25,archived-not-revalidated,expanded-02,"Archived source locator; supporting passage, edition and present behavior require review." +SR174,Stirling-PDF,Licensing and edition reference (historical; no current price assertion),https://docs.stirlingpdf.com/Paid-Offerings/,2026-09-25,archived-not-revalidated,expanded-02,"Archived source locator; supporting passage, edition and present behavior require review." +SR175,Stirling-PDF,Project source and technical documentation reference,https://github.com/Stirling-Tools/Stirling-PDF,2026-09-25,archived-not-revalidated,expanded-02,"Archived source locator; supporting passage, edition and present behavior require review." +SR176,SumatraPDF,SumatraPDF change history,https://www.sumatrapdfreader.org/docs/Changelog,2026-09-25,archived-not-revalidated,expanded-01,"Archived source locator; supporting passage, edition and present behavior require review." +SR177,SumatraPDF,Command palette and navigation prefixes,https://www.sumatrapdfreader.org/docs/Command-Palette,2026-09-25,archived-not-revalidated,expanded-01,"Archived source locator; supporting passage, edition and present behavior require review." +SR178,UPDF,Product or project documentation entry,https://ai.updf.com/,2026-09-07,archived-not-revalidated,core-updf,Landing/index locator only; locate a specific supporting page before citing a feature. +SR179,UPDF,Documentation locator: newgoodslist,https://checkout.updf.com/v1/order/newGoodsList?goodsType=1&channel=SEO-EN-AI¤cy=USD,2026-09-07,archived-not-revalidated,core-updf,"Archived source locator; supporting passage, edition and present behavior require review." +SR180,UPDF,Product or project documentation entry,https://sign.updf.com/,2026-09-07,archived-not-revalidated,core-updf,Landing/index locator only; locate a specific supporting page before citing a feature. +SR181,UPDF,Licensing and edition reference (historical; no current price assertion),https://updf.com/pricing/,2026-09-07,archived-not-revalidated,core-updf,"Archived source locator; supporting passage, edition and present behavior require review." +SR182,UPDF,Documentation locator: tech spec,https://updf.com/tech-spec/,2026-09-07,archived-not-revalidated,core-updf,"Archived source locator; supporting passage, edition and present behavior require review." +SR183,UPDF,Release notes and change history,https://updf.com/whats-new/,2026-09-07,archived-not-revalidated,core-updf;expanded-01,"Archived source locator; supporting passage, edition and present behavior require review." +SR184,UPDF,Documentation locator: write a review for updf,https://updf.com/write-a-review-for-updf/,2026-09-07,archived-not-revalidated,core-updf,"Archived source locator; supporting passage, edition and present behavior require review." diff --git a/docs/research/pdf-workflows/workflow-reference.md b/docs/research/pdf-workflows/workflow-reference.md new file mode 100644 index 0000000000..c0a419711a --- /dev/null +++ b/docs/research/pdf-workflows/workflow-reference.md @@ -0,0 +1,430 @@ +# Workflow research and verification reference + +This reference connects archived workflow research with PdfCraft's existing feature catalogue. It is not a roadmap, a set of implementation assignments, or an approval to introduce features, storage formats, dependencies or architecture. Planning remains in the repository's existing `ROADMAP.md` and `parity/` records. +The catalogue and roadmap snapshot is [`924c4da4`](https://github.com/storytold/pdfcraft/tree/924c4da4ac2879ac9107c6c82e20ac6be5570f0a), dated 2026-10-16. Engine and automation names were inspected separately at [`3f044b62`](https://github.com/storytold/pdfcraft/tree/3f044b6263aa6605c3aa44ca6d065e1ab37d13b6), dated 2026-10-13. Catalogue status means the status recorded at that revision; it is not an independent verification verdict. No implementation tests, benchmarks, competitor sessions or conformance checks were executed for this reference. +The [competitive research digest](competitive-research.md) provides the archived context. The [source register](source-register.csv) records source identifiers, URLs, archive dates and verification limits. Links below are locators for the retained research, not fresh confirmations of vendor behavior. The [feature mapping](feature-mapping.csv) contains the corresponding catalogue records. B01–B20 are reference section identifiers, not a second task or priority system. +Evaluation dimensions name what an observation describes; they do not prescribe targets or new acceptance gates. Availability, structural validity, visual fidelity, interoperability and human usability are distinct evidence. Negative or incomplete observations remain distinguishable from successful checks. + +## B01 — Rendering and output fidelity + +**Recorded baseline.** The checklist records six mapped rendering features as partial. Its knockout entry is marked shipped, but cites signing tests and signed-save notes; the transparency entry describes knockout rendering as incomplete. Those are inconsistent catalogue references, not evidence from a fresh rendering test. The current progress log and gap 12 document existing `cargo xtask render-oracle` and `cargo xtask text-oracle` tools, including pixel disagreements and reading-order measurements. These tools report differences; their output is not a fidelity certification. + +| Existing parity ID | Recorded status | Existing milestone | +|---|---|---| +| `view.render-pages` | partial | M2 | +| `view.render-shadings-patterns` | partial | M2 | +| `view.render-transparency` | partial | M2 | +| `view.render-knockout` | shipped | M11 | +| `view.render-type3` | partial | M2 | +| `view.render-cid-fonts` | partial | M2 | +| `view.render-image-codecs` | partial | M2 | + +**Archived research question.** The archived viewing and conversion studies distinguish whether a file opens, whether its text can be extracted, and whether the visible page matches a reference. These observations concern different kinds of fidelity. + +**Evaluation dimensions.** Pixel differences and their locations; glyph identity and reading order; clipping and transparency; independent-reader outcomes; oracle version and tolerance; unsupported or incomplete analysis. + +**Existing implementation evidence.** The [pinned checklist](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/parity/acrobat-features.toml) cites [`crates/render/src/raster.rs::renders_a_blue_rectangle`](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/crates/render/src/raster.rs). These are existing evidence references, not checks rerun for this document. + +**Archived source locators.** [SR014: Adobe Acrobat — ISO 32000-1:2008 PDF specification](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf); [SR007: Adobe Acrobat — Acrobat product help index](https://helpx.adobe.com/acrobat/user-guide.html). Verification state and date are in the source register. + +## B02 — Existing-text editing + +**Recorded baseline.** Text editing and RTL/CJK editing are partial. The mapped rotated-text and tag-preservation entries remain planned. Inspected source exposes text_edit, text_edit_layout, text_fonts and text_font_import; this does not establish general support for every font or script. + +| Existing parity ID | Recorded status | Existing milestone | +|---|---|---| +| `edit.text-edit` | partial | M7 | +| `edit.rtl-cjk-editing` | partial | M7 | +| `edit.rotated-text-editing` | planned | M7 | +| `edit.keep-tags-on-edit` | planned | M7 | + +**Archived research question.** The archived editing studies ask whether correcting text preserves its font, baseline, writing order, nearby content and document structure. A visually acceptable edit and a semantically faithful edit are separate observations. + +**Evaluation dimensions.** Extracted Unicode and logical order; shaping and missing-glyph behavior; retained font/style; neighbouring content; selection/caret behavior; saved/reopened layout; unsupported transformations. + +**Existing implementation evidence.** The [pinned checklist](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/parity/acrobat-features.toml) cites [`crates/edit/src/tests.rs::paragraphs_are_found_and_rewrapped`](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/crates/edit/src/tests.rs); [`crates/edit/src/bidi.rs::hebrew_sentences_numbers_latin_and_brackets_round_trip`](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/crates/edit/src/bidi.rs). These are existing evidence references, not checks rerun for this document. + +**Archived source locators.** [SR014: Adobe Acrobat — ISO 32000-1:2008 PDF specification](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf); [SR007: Adobe Acrobat — Acrobat product help index](https://helpx.adobe.com/acrobat/user-guide.html). Verification state and date are in the source register. + +## B03 — Document comparison + +**Recorded baseline.** Text comparison, visual comparison, PDF reports and side-by-side views are shipped in the current checklist. The side-by-side notes describe an old-tab requirement, a minimum window width of 1000 px and offset-based synchronization. They identify content-aligned scrolling and retaining comparison after the old tab closes as limitations. Filters, selected ranges and scanned comparison remain planned. + +| Existing parity ID | Recorded status | Existing milestone | +|---|---|---| +| `misc.compare-text` | shipped | M12 | +| `misc.compare-visual` | shipped | M12 | +| `misc.compare-report` | shipped | M12 | +| `misc.compare-side-by-side` | shipped | M12 | +| `misc.compare-filters` | planned | M12 | +| `misc.compare-page-ranges` | planned | M12 | +| `misc.compare-scanned` | planned | M12 | + +**Archived research question.** Archived comparison studies distinguish a textual change list, raster differences, overlays and synchronized old/new context. The research question is how a reviewer associates a reported difference with both document locations. + +**Evaluation dimensions.** Difference type and location; page alignment with insertions or unequal sizes; zoom/scroll correspondence; navigation counts; report destinations; identical-input outcomes; input immutability. + +**Existing implementation evidence.** The [pinned checklist](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/parity/acrobat-features.toml) cites [`crates/compare/src/lib.rs::insertions_deletions_and_replacements`](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/crates/compare/src/lib.rs); [`crates/engine/src/tests.rs::comparing_two_versions_of_a_document`](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/crates/engine/src/tests.rs). These are existing evidence references, not checks rerun for this document. + +**Archived source locators.** [SR018: Bluebeam Revu — Documentation locator: compare documents vs overlay pages](https://support.bluebeam.com/revu/features/compare-documents-vs-overlay-pages.html); [SR030: Bluebeam Revu — Documentation locator: compare documents](https://support.bluebeam.com/user-manual/menus/document/compare-documents.html); [SR002: ABBYY FineReader — User guide index: verification, comparison and automation](https://help.abbyy.com/en-us/finereader/16/). Verification state and date are in the source register. + +## B04 — Recognition proofing and correction + +**Recorded baseline.** Recognition of pages, ranges and multiple files is shipped; editable-text output is partial; correction of low-confidence suspects is planned. Inspected source includes ocr_recognize, ocr_recognize_files and ocr_status. These operations alone do not establish a proofing workflow. + +| Existing parity ID | Recorded status | Existing milestone | +|---|---|---| +| `ocr.correct-recognized-text` | planned | M10 | +| `ocr.editable-text-output` | partial | M10 | +| `ocr.recognize-text` | shipped | M10 | +| `ocr.recognize-range` | shipped | M10 | +| `ocr.recognize-multiple` | shipped | M10 | + +**Archived research question.** The archived recognition study separates image/text/zoom synchronization, navigation among uncertain material, correction, skipping and human-reviewed state. Confidence from an engine and a human review decision are distinct evidence. + +**Evaluation dimensions.** Word-to-image correspondence; confidence provenance or absence; correction in extracted text after save/reopen; retained scan appearance; skipped versus reviewed state; stale recognition results; keyboard reachability. + +**Existing implementation evidence.** The [pinned checklist](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/parity/acrobat-features.toml) cites [`crates/engine/tests/ocr_edit.rs::editing_an_ocr_word_covers_the_scan_and_shows_the_new_text`](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/crates/engine/tests/ocr_edit.rs); [`crates/engine/src/tests.rs::recognize_text_makes_a_scanned_page_searchable`](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/crates/engine/src/tests.rs). These are existing evidence references, not checks rerun for this document. + +**Archived source locators.** [SR002: ABBYY FineReader — User guide index: verification, comparison and automation](https://help.abbyy.com/en-us/finereader/16/); [SR031: Bluebeam Revu — Documentation locator: ocr](https://support.bluebeam.com/user-manual/menus/document/ocr.html). Verification state and date are in the source register. + +## B05 — Recognition languages and scan preparation + +**Recorded baseline.** Language support is partial; orientation detection, deskew and pluggable engines are planned. Camera-image cleanup provides partial despeckle, background-removal and perspective capabilities. The notes do not establish equivalent preprocessing of arbitrary scanned pages. + +| Existing parity ID | Recorded status | Existing milestone | +|---|---|---| +| `ocr.ocr-languages` | partial | M10 | +| `ocr.rotation-detection` | planned | M10 | +| `ocr.deskew` | planned | M10 | +| `ocr.ocr-engines` | planned | M10 | +| `ocr.despeckle` | partial | M10 | +| `ocr.background-removal` | partial | M10 | +| `ocr.camera-perspective` | partial | M10 | + +**Archived research question.** The archived scan studies distinguish selected language from the language actually recognized and distinguish camera perspective correction from text orientation, deskew and image cleanup. + +**Evaluation dimensions.** Actual backend/language; model availability and provenance; rotation/skew geometry; text accuracy by script; annotation/search coordinate agreement; pages with existing text; duplicate-layer behavior; explicit unsupported options. + +**Existing implementation evidence.** The [pinned checklist](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/parity/acrobat-features.toml) cites [`crates/ui-egui/tests/ocr.rs::recognize_text_dialog_adds_searchable_text`](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/crates/ui-egui/tests/ocr.rs); [`crates/engine/src/camera_clean.rs::a_shadow_and_a_yellow_cast_become_white_paper`](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/crates/engine/src/camera_clean.rs). These are existing evidence references, not checks rerun for this document. + +**Archived source locators.** [SR002: ABBYY FineReader — User guide index: verification, comparison and automation](https://help.abbyy.com/en-us/finereader/16/); [SR120: PDF-XChange — Documentation locator: pdf xchange ocr](https://www.pdf-xchange.com/pdf-xchange-ocr); [SR133: PDF24 — Documentation locator: ocr pdf](https://tools.pdf24.org/en/ocr-pdf). Verification state and date are in the source register. + +## B06 — Reusable batch processing + +**Recorded baseline.** Action creation and execution are shipped; management/exchange and prompt-user steps remain planned. The inspected 2026-10-13 actions.rs has 12 step kinds, and action_list/action_run are present. This is an older source observation, not a new inspection of the current runner. + +| Existing parity ID | Recorded status | Existing milestone | +|---|---|---| +| `misc.action-wizard-run` | shipped | M13 | +| `misc.action-wizard-create` | shipped | M13 | +| `misc.action-wizard-manage` | planned | M13 | +| `misc.action-prompt-steps` | planned | M13 | + +**Archived research question.** Archived workflow studies describe named sequences, ordered steps, portable definitions and per-file outcomes. They distinguish a saved process definition from the outcomes of a particular execution. + +**Evaluation dimensions.** Definition/version round-trip; step order and arguments; input/output identity; occupied output names; individual failure reporting; partial batch outcomes; cancellation; path confinement; secret handling in reports. + +**Existing implementation evidence.** The [pinned checklist](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/parity/acrobat-features.toml) cites [`crates/ui-egui/tests/actions.rs::desktop_actions_preserve_duplicate_names_and_results_from_earlier_runs`](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/crates/ui-egui/tests/actions.rs); [`crates/automation/tests/automation.rs::actions_through_tools`](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/crates/automation/tests/automation.rs). These are existing evidence references, not checks rerun for this document. + +**Archived source locators.** [SR056: Foxit PDF Editor — Documentation locator: add custom commands to custom actions](https://www.foxit.com/resource-hub/user-guide/foxit-pdf-editor-for-windows/add-custom-commands-to-custom-actions/); [SR172: Stirling-PDF — Automation pipeline documentation](https://docs.stirlingpdf.com/Configuration/Automation/Pipeline/). Verification state and date are in the source register. + +## B07 — Document structure and accessibility authoring + +**Recorded baseline.** Text-only autotag, Tags/property editing, sibling reading-order edits and artifact marking are partial. The current artifact entry covers reachable indirect leaf tags owning direct MCIDs through the panel and accessibility_mark_tag_artifact. Arbitrary painted selection, direct dictionaries and whole tagged subtrees remain outside that entry. accessibility_tags is described as paginated and filterable. + +| Existing parity ID | Recorded status | Existing milestone | +|---|---|---| +| `a11y.autotag` | partial | M12 | +| `a11y.tags-panel` | partial | M12 | +| `a11y.tag-properties` | partial | M12 | +| `a11y.reading-order-tool` | partial | M12 | +| `a11y.order-panel` | partial | M12 | +| `a11y.table-editor` | planned | M12 | +| `a11y.create-tag-from-selection` | planned | M12 | +| `a11y.artifact-marking` | partial | M12 | + +**Archived research question.** Archived accessibility studies distinguish automatic classification, actual structure-tree changes, reading order and human interpretation. A visual panel, a structurally valid parent tree and an accessible reading experience are different observations. + +**Evaluation dimensions.** MCID/parent-tree consistency; content ownership; tag and reading order; alternate text; direct/indirect dictionary coverage; malformed or ambiguous structure; appearance after artifact marking; saved/reopened structure; manual-check findings. + +**Existing implementation evidence.** The [pinned checklist](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/parity/acrobat-features.toml) cites [`crates/automation/tests/automation.rs::autotag_preview_proposes_text_without_changing_the_pdf`](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/crates/automation/tests/automation.rs); [`crates/ui-egui/tests/accessibility.rs::tags_panel_edits_a_tag_property_and_undoes_it`](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/crates/ui-egui/tests/accessibility.rs). These are existing evidence references, not checks rerun for this document. + +**Archived source locators.** [SR108: PDF Association — Accessible PDF standards and reference material](https://pdfa.org/iso-14289-2-pdf-ua-2-the-gold-standard-for-accessibility-in-pdf-2-0-has-arrived/); [SR110: PDF Association — Accessible PDF standards and reference material](https://pdfa.org/wtpdf/); [SR014: Adobe Acrobat — ISO 32000-1:2008 PDF specification](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf). Verification state and date are in the source register. + +## B08 — Preflight and archival validation + +**Recorded baseline.** PDF/A verification and conversion are partial. Preflight analysis, result presentation and reports remain planned. Inspected source exposes pdfa_verify, pdfa_convert and print_readiness; their availability is not proof of full conformance validation. + +| Existing parity ID | Recorded status | Existing milestone | +|---|---|---| +| `optimize.pdfa-validate` | partial | M11 | +| `optimize.pdfa-convert` | partial | M11 | +| `optimize.preflight` | planned | M11 | +| `optimize.preflight-results` | planned | M11 | +| `optimize.preflight-reports` | planned | M11 | + +**Archived research question.** The archived standards studies distinguish declared conformance, implemented structural checks, conversion changes and independent validation. A repairable finding is different from a condition the software cannot evaluate. + +**Evaluation dimensions.** Profile and rule coverage; page/object identification; unsupported checks; fonts and output intents; changes made by conversion; remaining findings after save/reopen; agreement and disagreement with an identified independent validator. + +**Existing implementation evidence.** The [pinned checklist](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/parity/acrobat-features.toml) cites [`crates/preflight/src/tests.rs::verify_reports_what_breaks_pdfa`](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/crates/preflight/src/tests.rs); [`crates/automation/tests/automation.rs::pdfa_through_tools`](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/crates/automation/tests/automation.rs). These are existing evidence references, not checks rerun for this document. + +**Archived source locators.** [SR014: Adobe Acrobat — ISO 32000-1:2008 PDF specification](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf); [SR076: LibreOffice Draw — PDF export and security help index](https://help.libreoffice.org/latest/en-US/text/shared/01/ref_pdf_export.html); [SR105: PDF Association — Product or project documentation entry](https://pdfa.org). Verification state and date are in the source register. + +## B09 — Redaction and assurance evidence + +**Recorded baseline.** The mapped excision and verification features are shipped. Redaction properties are also shipped, including overlay/mark styling, persistent defaults, session-only changes and cancel behavior. Pattern locales, partial words and folder redaction remain planned; code sets are partial. + +| Existing parity ID | Recorded status | Existing milestone | +|---|---|---| +| `protect.redaction-verification` | shipped | M8 | +| `protect.redact-glyph-removal` | shipped | M8 | +| `protect.redact-image-pixels` | shipped | M8 | +| `protect.redact-vectors` | shipped | M8 | +| `protect.redact-annots-fields` | shipped | M8 | +| `protect.redact-struct-tree` | shipped | M8 | +| `protect.redact-pattern-locales` | planned | M8 | +| `protect.redact-partial-words` | planned | M8 | +| `protect.redact-folder` | planned | M13 | +| `protect.redaction-properties` | shipped | M8 | +| `protect.redaction-codes` | partial | M8 | + +**Archived research question.** Archived redaction studies distinguish a visible cover from removal of underlying content and distinguish a scoped verification result from an unlimited security guarantee. A report containing removed text can itself disclose sensitive content. + +**Evaluation dimensions.** Search/extraction after save/reopen; image/vector/annotation/structure paths covered; hidden-information treatment; input/output hashes; unsupported constructs; false success on a cover-only control; refusal/cancellation behavior; sensitive data in exported records. + +**Existing implementation evidence.** The [pinned checklist](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/parity/acrobat-features.toml) cites [`crates/redact/src/tests.rs::unreadable_content_fails_closed`](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/crates/redact/src/tests.rs); [`crates/ui-egui/tests/ui_group/redact.rs::marking_applying_and_clearing`](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/crates/ui-egui/tests/ui_group/redact.rs). These are existing evidence references, not checks rerun for this document. + +**Archived source locators.** [SR034: Bluebeam Revu — Documentation locator: redaction](https://support.bluebeam.com/user-manual/menus/edit/redaction.html); [SR097: Nitro PDF Pro — Documentation locator: nitros complete guide to secure automated pdf redaction](https://www.gonitro.com/resources/nitros-complete-guide-to-secure-automated-pdf-redaction); [SR014: Adobe Acrobat — ISO 32000-1:2008 PDF specification](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf). Verification state and date are in the source register. + +## B10 — Digital signatures and validation evidence + +**Recorded baseline.** The mapped B-B, PKCS #12, native Windows/macOS identity and certification capabilities are shipped. Timestamp preferences, PKCS #11 and FieldMDP are planned; B-T/LTV/B-LTA and online revocation support are partial. Inspected source exposes sign_document, sign_list and sign_trust. + +| Existing parity ID | Recorded status | Existing milestone | +|---|---|---| +| `sign.cades-pades-bb` | shipped | M9 | +| `sign.pkcs12` | shipped | M9 | +| `sign.windows-cert-store` | shipped | M9 | +| `sign.macos-keychain` | shipped | M9 | +| `sign.certify` | shipped | M9 | +| `sign.timestamp-servers` | planned | M9 | +| `sign.pkcs11` | planned | M9 | +| `sign.field-mdp` | planned | M9 | +| `sign.pades-bt` | partial | M9 | +| `sign.ltv` | partial | M9 | +| `sign.pades-blta` | partial | M9 | +| `sign.ocsp-crl` | partial | M9 | + +**Archived research question.** Archived signing studies distinguish document integrity, signer trust, permitted changes, timestamp evidence and long-term validation material. A local signing workflow is different from a hosted signature-request service. + +**Evaluation dimensions.** Covered revision; permitted/disallowed changes; intact versus trusted status; timestamp presence and trust; revocation-evidence freshness; requested versus attained level; network-consent state; private-key custody; reopened independent validation. + +**Existing implementation evidence.** The [pinned checklist](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/parity/acrobat-features.toml) cites [`crates/sign/tests/crypto.rs::signatures_round_trip_for_every_key_type`](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/crates/sign/tests/crypto.rs); [`crates/sign/tests/pdf.rs::signing_then_validating_with_and_without_trust`](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/crates/sign/tests/pdf.rs). These are existing evidence references, not checks rerun for this document. + +**Archived source locators.** [SR028: Bluebeam Revu — Documentation locator: sign seal](https://support.bluebeam.com/user-manual/menus/batch/sign-seal.html); [SR014: Adobe Acrobat — ISO 32000-1:2008 PDF specification](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf); [SR007: Adobe Acrobat — Acrobat product help index](https://helpx.adobe.com/acrobat/user-guide.html). Verification state and date are in the source register. + +## B11 — Forms, events and scripting + +**Recorded baseline.** Custom scripts, event order and calculation order are shipped. Form submission and the mapped general JavaScript action are planned; push-button actions are partial. Inspected source includes js_run, js_document_scripts, form_set_script, form_actions, form_set_actions and js_enabled. + +| Existing parity ID | Recorded status | Existing milestone | +|---|---|---| +| `form.custom-scripts` | shipped | M6 | +| `form.event-order` | shipped | M6 | +| `form.calculation-order` | shipped | M6 | +| `form.submit-form` | planned | M6 | +| `edit.action-javascript` | planned | M6 | +| `form.push-buttons` | partial | M6 | + +**Archived research question.** Archived form studies separate compatibility with installed forms from authoring tools and separate field-event order from document-level actions. Scripting availability does not imply filesystem or network privileges. + +**Evaluation dimensions.** Trigger timing; validation/calculation/format order; committed values after save/reopen; enabled/disabled script state; resource bounds; malformed scripts; unwanted external effects; cancellation and refusal outcomes. + +**Existing implementation evidence.** The [pinned checklist](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/parity/acrobat-features.toml) cites [`crates/engine/src/tests.rs::form_javascript_validates_calculates_and_formats`](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/crates/engine/src/tests.rs); [`crates/forms/src/tests.rs::formats_validation_and_calculations_run_like_acrobat`](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/crates/forms/src/tests.rs). These are existing evidence references, not checks rerun for this document. + +**Archived source locators.** [SR012: Adobe Acrobat — Acrobat JavaScript API reference](https://opensource.adobe.com/dc-acrobat-sdk-docs/library/jsapiref/index.html); [SR013: Adobe Acrobat — Acrobat JavaScript developer guide](https://opensource.adobe.com/dc-acrobat-sdk-docs/library/jsdevguide/index.html); [SR043: Firefox / pdf.js — Form scripting API implementation reference](https://github.com/mozilla/pdf.js/blob/master/src/scripting_api/aform.js). Verification state and date are in the source register. + +## B12 — Measurement and scale interpretation + +**Recorded baseline.** Distance and scale are partial; perimeter, area, snapping, information display and CSV export are shipped. Current mapped measurement entries are assigned to M12. Inspected source includes the measure_* tool family. + +| Existing parity ID | Recorded status | Existing milestone | +|---|---|---| +| `comment.measure-distance` | partial | M12 | +| `comment.measure-scale` | partial | M12 | +| `comment.measure-perimeter` | shipped | M12 | +| `comment.measure-area` | shipped | M12 | +| `comment.measure-snap` | shipped | M12 | +| `comment.measure-info-panel` | shipped | M12 | +| `comment.measure-export` | shipped | M12 | + +**Archived research question.** Archived technical-review studies distinguish measurement geometry from unit formatting, calibration and interchange. Reading a saved measurement is different from assuming every imported scale is supported. + +**Evaluation dimensions.** Known dimensions and numeric tolerance; selected viewport and scale; compound/fractional formats; rotated/cropped pages; imported unsupported formats; self-intersecting boundaries; saved/reopened values; CSV escaping and spreadsheet safety. + +**Existing implementation evidence.** The [pinned checklist](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/parity/acrobat-features.toml) cites [`crates/automation/tests/automation.rs::measurements_calibrate_draw_save_reopen_and_export`](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/crates/automation/tests/automation.rs); [`crates/measure/src/tests.rs::viewport_precedence_first_point_user_unit_and_roundtrip`](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/crates/measure/src/tests.rs). These are existing evidence references, not checks rerun for this document. + +**Archived source locators.** [SR014: Adobe Acrobat — ISO 32000-1:2008 PDF specification](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf); [SR107: PDF Association — Foxit PDF Editor 2026.2 release announcement](https://pdfa.org/foxit-pdf-editor-20262-release-notes/). Verification state and date are in the source register. + +## B13 — Page assembly and document composition + +**Recorded baseline.** Combine, selected page ranges, sorting/grid previews, every-N/bookmark splitting and field-name conflict handling are shipped. Structure-tree merging, multiple-file split and the mapped naming refinements remain planned. Existing combine and page tools were present in the inspected source. + +| Existing parity ID | Recorded status | Existing milestone | +|---|---|---| +| `organize.combine-files` | shipped | M4 | +| `organize.combine-choose-pages` | shipped | M4 | +| `organize.combine-sort` | shipped | M4 | +| `organize.combine-thumbnails` | shipped | M4 | +| `organize.field-name-conflicts` | shipped | M4 | +| `organize.split-every-n` | shipped | M4 | +| `organize.split-by-bookmarks` | shipped | M4 | +| `organize.struct-tree-merge` | planned | M12 | +| `organize.split-multiple-files` | planned | M13 | +| `organize.split-naming` | planned | M4 | + +**Archived research question.** Archived composition studies examine how users review source pages, order and destination before a transfer. Visible selection, the captured source revision and the resulting document are separate pieces of evidence. + +**Evaluation dimensions.** Page order; source/target identity; unsaved-source handling; links, fields and bookmarks; size mismatch; field-name conflicts; structure preservation; source immutability; undo; saved/reopened destinations. + +**Existing implementation evidence.** The [pinned checklist](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/parity/acrobat-features.toml) cites [`crates/organize/src/tests.rs::combine_concatenates_with_a_bookmark_per_file`](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/crates/organize/src/tests.rs); [`crates/ui-egui/tests/editing.rs::combine_files_takes_chosen_pages_in_the_order_listed`](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/crates/ui-egui/tests/editing.rs). These are existing evidence references, not checks rerun for this document. + +**Archived source locators.** [SR014: Adobe Acrobat — ISO 32000-1:2008 PDF specification](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf); [SR007: Adobe Acrobat — Acrobat product help index](https://helpx.adobe.com/acrobat/user-guide.html). Verification state and date are in the source register. + +## B14 — Creation and conversion fidelity + +**Recorded baseline.** Office import and spreadsheet export are planned; DOCX and presentation export are partial. DOCX notes record package validation and DrawingML picture anchors on positioned pages, with that positioned output still unverified in Word. Package validity does not establish visual fidelity. + +| Existing parity ID | Recorded status | Existing milestone | +|---|---|---| +| `create.from-office` | planned | M10 | +| `create.export-docx` | partial | M10 | +| `create.export-xlsx` | planned | M10 | +| `create.export-pptx` | partial | M10 | + +**Archived research question.** Archived conversion studies distinguish reflowed editable output, positioned output, raster output and native PDF edits. Their suitability depends on what information the user needs to retain. + +**Evaluation dimensions.** Text and table semantics; headers/footers; page dimensions; images and vector rules; paint order; exact font matching; package validity; reopened rendering; independent application/version; explicit unsupported input and output. + +**Existing implementation evidence.** The [pinned checklist](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/parity/acrobat-features.toml) cites [`crates/export/src/lib.rs::word_does_not_invent_borders_for_aligned_invoice_fields`](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/crates/export/src/lib.rs); [`crates/export/src/pptx.rs::pptx_is_a_presentationml_package`](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/crates/export/src/pptx.rs). These are existing evidence references, not checks rerun for this document. + +**Archived source locators.** [SR076: LibreOffice Draw — PDF export and security help index](https://help.libreoffice.org/latest/en-US/text/shared/01/ref_pdf_export.html); [SR007: Adobe Acrobat — Acrobat product help index](https://helpx.adobe.com/acrobat/user-guide.html). Verification state and date are in the source register. + +## B15 — Document performance and resource use + +**Recorded baseline.** Smooth 500-page scrolling is shipped. Performance budgets and lazy loading of very large files are planned. Existing notes describe whole-file input and remaining save-memory peaks; they do not establish bounded performance for every document. + +| Existing parity ID | Recorded status | Existing milestone | +|---|---|---| +| `view.large-document-performance` | shipped | M3 | +| `misc.performance-budgets` | planned | M14 | +| `core.lazy-loading` | planned | M1 | + +**Archived research question.** Archived performance studies distinguish open time, first useful page, interaction latency and job completion. Input byte size and page count alone do not describe rendering complexity. + +**Evaluation dimensions.** Release build and hardware; corpus generation; fresh-process runs and variance; peak resident memory; open/edit/save timings; UI-thread stalls; cache state; cancellation latency; bounds on hostile dimensions; output validity after reopen. + +**Existing implementation evidence.** The [pinned checklist](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/parity/acrobat-features.toml) cites [`crates/ui-egui/tests/perf.rs::scrolling_a_500_page_document_stays_within_the_frame_budget`](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/crates/ui-egui/tests/perf.rs). These are existing evidence references, not checks rerun for this document. + +**Archived source locators.** [SR014: Adobe Acrobat — ISO 32000-1:2008 PDF specification](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf); [SR099: Okular — Project source and technical documentation reference](https://github.com/KDE/okular). Verification state and date are in the source register. + +## B16 — Search across document collections + +**Recorded baseline.** Advanced current-document search is partial; folder, Boolean and indexed search are planned. Inspected source includes text_find and extraction. Mapped search entries belong to M12. + +| Existing parity ID | Recorded status | Existing milestone | +|---|---|---| +| `view.advanced-search` | partial | M12 | +| `view.search-folder` | planned | M12 | +| `view.search-boolean` | planned | M12 | +| `view.search-index` | planned | M12 | + +**Archived research question.** Archived search studies distinguish current-file matching, collection traversal, persistent indexing and navigation back to a location. A result count is incomplete without the set of files actually searched. + +**Evaluation dimensions.** Query semantics; searched/excluded/failed files; path aliases and cycles; root confinement; result and cache bounds; stale locations after edits; location after reopen; cancellation; truncation and incomplete-result reporting. + +**Existing implementation evidence.** The [pinned checklist](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/parity/acrobat-features.toml) cites [`crates/ui-egui/tests/viewer.rs::advanced_search_lists_results_with_context`](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/crates/ui-egui/tests/viewer.rs). These are existing evidence references, not checks rerun for this document. + +**Archived source locators.** [SR177: SumatraPDF — Command palette and navigation prefixes](https://www.sumatrapdfreader.org/docs/Command-Palette); [SR007: Adobe Acrobat — Acrobat product help index](https://helpx.adobe.com/acrobat/user-guide.html). Verification state and date are in the source register. + +## B17 — Keyboard and assistive-technology interaction + +**Recorded baseline.** App screen-reader support is partial and keyboard-only operation is planned. Reading colors are shipped; current notes cover annotation appearances and highlight legibility. Existing labels and display-color behavior do not establish completion of every task with assistive technology. + +| Existing parity ID | Recorded status | Existing milestone | +|---|---|---| +| `a11y.app-screen-reader` | partial | M3 | +| `a11y.keyboard-only` | planned | M14 | +| `a11y.replace-page-colors` | shipped | M12 | + +**Archived research question.** Archived interaction studies distinguish the accessibility tree, keyboard focus and what a platform screen reader announces. Successful automated tree inspection and a successful human task are different evidence. + +**Evaluation dimensions.** Names, roles, values and states; complete focus path; focus restoration; error/completion announcements; disabled-state explanations; selection after page changes; non-color information; platform/screen-reader version; manual versus automated findings. + +**Existing implementation evidence.** The [pinned checklist](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/parity/acrobat-features.toml) cites [`crates/ui-egui/tests/control.rs::inspect_and_click_by_label_and_id`](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/crates/ui-egui/tests/control.rs); [`crates/automation/tests/automation.rs::reading_colors_through_tools`](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/crates/automation/tests/automation.rs). These are existing evidence references, not checks rerun for this document. + +**Archived source locators.** [SR099: Okular — Project source and technical documentation reference](https://github.com/KDE/okular); [SR104: Okular — Project source and technical documentation reference](https://raw.githubusercontent.com/KDE/okular/master/doc/index.docbook); [SR108: PDF Association — Accessible PDF standards and reference material](https://pdfa.org/iso-14289-2-pdf-ua-2-the-gold-standard-for-accessibility-in-pdf-2-0-has-arrived/). Verification state and date are in the source register. + +## B18 — Preferences and capability disclosure + +**Recorded baseline.** The preferences dialog is partial; settings search, admin defaults and RTL UI are planned. Existing categories include language, document/view, identity, Fill & Sign and JavaScript. Current notes describe XDG migration that preserves configured preferences; they do not establish a complete signature-preferences category. + +| Existing parity ID | Recorded status | Existing milestone | +|---|---|---| +| `misc.preferences-dialog` | partial | M14 | +| `misc.prefs-search` | planned | M14 | +| `misc.admin-preferences` | planned | M14 | +| `misc.rtl-ui` | planned | M14 | + +**Archived research question.** Archived shell and deployment studies distinguish application preferences, document overrides, session state and managed policy. A visible option is different from an available runtime capability. + +**Evaluation dimensions.** Scope and precedence; default versus override; persistence across restart/reopen; malformed stored values; migration preservation; policy-locked state; cancellation of drafts; consistency among supported entry points. + +**Existing implementation evidence.** The [pinned checklist](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/parity/acrobat-features.toml) cites [`crates/ui-egui/tests/javascript.rs::document_scripts_and_preferences`](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/crates/ui-egui/tests/javascript.rs). These are existing evidence references, not checks rerun for this document. + +**Archived source locators.** [SR080: Master PDF Editor — Workspace navigation and object inspection](https://code-industry.net/masterpdfeditor-help/workspace-master-pdf-editor/); [SR177: SumatraPDF — Command palette and navigation prefixes](https://www.sumatrapdfreader.org/docs/Command-Palette). Verification state and date are in the source register. + +## B19 — Print preparation and color behavior + +**Recorded baseline.** Printing, n-up/booklet and previews exist. Saved print presets and the mapped output-preview/color-production features are planned. Inspected source includes print_readiness; that inspection path is not a full production Preflight. + +| Existing parity ID | Recorded status | Existing milestone | +|---|---|---| +| `organize.print-presets` | planned | M10 | +| `print.output-preview` | planned | M11 | +| `print.color-warnings` | planned | M11 | +| `print.convert-colors` | planned | M11 | +| `print.color-management` | planned | M11 | + +**Archived research question.** Archived print studies distinguish display-only reading colors, print-preview appearance, color conversion and the output intent. An ICC profile or a printer selection has meaning only with an identified processing path. + +**Evaluation dimensions.** Page order and dimensions; persistent print options; preview/output correspondence; fonts and color-space findings; profile provenance and rendering intent; unsupported separations; device/platform differences; reopened print-ready output. + +**Existing implementation evidence.** The [pinned checklist](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/parity/acrobat-features.toml) cites the per-feature evidence recorded in its entries. These are existing evidence references, not checks rerun for this document. + +**Archived source locators.** [SR076: LibreOffice Draw — PDF export and security help index](https://help.libreoffice.org/latest/en-US/text/shared/01/ref_pdf_export.html); [SR014: Adobe Acrobat — ISO 32000-1:2008 PDF specification](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf). Verification state and date are in the source register. + +## B20 — Document identity, history and failure outcomes + +**Recorded baseline.** The mapped undo, atomic batch-edit, autosave, crash-recovery and unsaved-change capabilities are shipped. Current recovery notes cover instance locks that prevent another running instance from offering/discarding live autosaves. Browser recovery and multiple windows remain planned. + +| Existing parity ID | Recorded status | Existing milestone | +|---|---|---| +| `core.undo-redo` | shipped | M4 | +| `core.undo-after-save` | shipped | M4 | +| `core.atomic-batch-edits` | shipped | M4 | +| `core.autosave` | shipped | M4 | +| `core.crash-recovery` | shipped | M4 | +| `core.unsaved-changes-prompt` | shipped | M4 | +| `core.web-recovery` | planned | M4 | +| `view.multiple-windows` | planned | M14 | + +**Archived research question.** Archived document-ownership studies distinguish the active tab from the document captured by a long-running operation and distinguish a staged candidate from an accepted save. Those studies are not evidence of a defect in current PdfCraft. + +**Evaluation dimensions.** Captured document/revision; destination identity; close-during-save outcome; external file replacement; write failure; coherent undo/redo; unrelated-document preservation; recovery binding; saved/reopened accepted state; cancellation and reported non-interruptible stages. + +**Existing implementation evidence.** The [pinned checklist](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/parity/acrobat-features.toml) cites [`crates/engine/src/tests.rs::undo_and_redo_restore_exact_states`](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/crates/engine/src/tests.rs); [`crates/ui-egui/tests/recovery.rs::saving_or_closing_clears_the_entry_and_nothing_is_written_for_clean_documents`](https://github.com/storytold/pdfcraft/blob/924c4da4ac2879ac9107c6c82e20ac6be5570f0a/crates/ui-egui/tests/recovery.rs). These are existing evidence references, not checks rerun for this document. + +**Archived source locators.** [SR014: Adobe Acrobat — ISO 32000-1:2008 PDF specification](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf); [SR099: Okular — Project source and technical documentation reference](https://github.com/KDE/okular). Verification state and date are in the source register. diff --git a/docs/roadmap.md b/docs/roadmap.md index d17893c1aa..ac9bcbc9e1 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,10 +1,15 @@ # PdfCraft roadmap: milestones and what's next +> **Last reviewed:** 2026-11-07 | **Last updated:** 2026-11-07 | **Change:** patch (M7 #2964 / #3185: grouped source text and Thai font/style preservation, with reported deep or unsafe groups and a per-glyph check of the Thai map repair; estimates unchanged) + +> **Last reviewed:** 2026-10-28 | **Last updated:** 2026-10-28 | **Change:** minor (M10, #4119 partial: Enhance scanned file cleans scanned pages in place: background removal, edge shadow removal, despeckle; estimates unchanged) > **Last reviewed:** 2026-11-03 | **Last updated:** 2026-11-03 | **Change:** patch (M5 #5850 review: indirect typewriter intent and composer edits preserve outer whitespace; movement regression commits by outside click; estimates unchanged) > **Last reviewed:** 2026-11-03 | **Last updated:** 2026-11-03 | **Change:** patch (M5 #5850: Enter adds Fill & Sign line breaks; outside clicks commit, Escape cancels, and saved multiline appearances retain their text and height; estimates unchanged) > **2026-11-03, M7 / #5835 review:** active image crops retain secondary clicks over overlapping images; crop controls share measured layout with hit detection. Rotation, handle and page-edge regressions added; focused tests, scoped Clippy, formatting, layering and asset checks passed locally. Estimates unchanged. +> **Last reviewed:** 2026-10-31 | **Last updated:** 2026-10-31 | **Change:** minor (M3/M4, #5330: Crop pages is directly available in the document quick toolbar; estimates unchanged) + > **Last reviewed:** 2026-11-01 | **Last updated:** 2026-11-01 | **Change:** patch (M7 #5657: header/footer, watermark and background operations commit all selected pages together; later-page errors preserve marks, content, shared/inherited resources and pending edits; estimates unchanged) @@ -22,7 +27,7 @@ > **Last reviewed:** 2026-10-26 · **Last updated:** 2026-10-26 · **Change:** patch (M2, PR #4172: document installed-font helpers and regression contracts; behavior and estimates unchanged) > **Last reviewed:** 2026-10-26 · **Last updated:** 2026-10-26 · **Change:** patch (M2, PR #4172: print-preview font policy matches output, with isolated caches and GBK switching regressions; estimates unchanged) > **Last reviewed:** 2026-10-26 · **Last updated:** 2026-10-26 · **Change:** patch (M2, PR #4172: reconcile current main and retain Japanese serif/weight selection alongside display-only CJK fallback; estimates unchanged) -> **Last reviewed:** 2026-10-29 | **Last updated:** 2026-10-29 | **Change:** minor (M7 #2517: a narrowed then widened paragraph keeps the typed spaces the wrap consumed, after save and reopen; estimates unchanged) +> **Last reviewed:** 2026-10-29 | **Last updated:** 2026-10-29 | **Change:** minor (M2: scans keep their strokes at screen size and are smoothed when zoomed; M7 #2517: a narrowed then widened paragraph keeps the typed spaces the wrap consumed, after save and reopen; estimates unchanged) > **Last reviewed:** 2026-10-27 | **Last updated:** 2026-10-27 | **Change:** minor (M10: OCR model downloads use verified ONNX pairs with legacy discovery fallback; estimates unchanged) > **Last reviewed:** 2026-10-25 | **Last updated:** 2026-10-25 | **Change:** minor (M5: one reply in the Comments panel is edited, copied or deleted from its own "…" menu; estimates unchanged) > **Last reviewed:** 2026-10-26 · **Last updated:** 2026-10-26 · **Change:** minor (M11: lossless JBIG2 stores each distinct mark once, as a symbol; estimates unchanged) @@ -63,6 +68,10 @@ > **Last reviewed:** 2026-10-17 · **Last updated:** 2026-10-17 · **Change:** minor (M8, gap 6 #2662: the redaction audit counts a cover over one word of a long line; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (M10: dotted-leader contents entries export as complete paragraphs; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-22 · **Last updated:** 2026-10-22 · **Change:** minor (M6: calculation scripts read their notation only from a comment, and a regular expression after a condition as one; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) + +> **Last reviewed:** 2026-10-20 · **Last updated:** 2026-10-20 · **Change:** patch (M1, #1143: repaired aliases survive preflight, snapshots and normalized saves; retained materialized revisions report generation disagreements; estimates unchanged) + +> **Last reviewed:** 2026-10-18 · **Last updated:** 2026-10-18 · **Change:** minor (M1, #1143: explicit live xref/header generation repair policy and bounded diagnostics; estimates unchanged) > **Last reviewed:** 2026-10-18 · **Last updated:** 2026-10-18 · **Change:** minor (M5, PR #1971: bounded nested FDF export excludes source page/page-tree dictionaries; refreshed against upstream 4db6ac4a; estimates unchanged) > **Last reviewed:** 2026-10-18 · **Last updated:** 2026-10-18 · **Change:** patch (M2 #3086: dotted TOC leaders no longer hide word spaces; estimates unchanged) > @@ -78,6 +87,7 @@ > **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-15 · **Change:** minor (M6: AcroForm XML text; estimates unchanged) > **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-15 · **Change:** minor (M6: preserve XML/XFDF attribute whitespace; refreshed upstream; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-15 · **Change:** minor (M1: raw-deflate fallback preserves decoding-limit errors; refreshed upstream; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) +> **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-15 · **Change:** minor (M1: refresh object-generation reference identity against current upstream; estimates unchanged) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-15 · **Change:** minor (M2: objects in one large object stream are read in linear time; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-16; **Last updated:** 2026-10-16; **Change:** minor (M5 #2669, item 5: reply groups and page root counts per frame; estimates unchanged) > @@ -141,6 +151,8 @@ repaint at the existing 60-second autosave deadline, even with no mouse or keybo Clean and already-snapshotted documents do not schedule recovery wakeups. Snapshot contents, encryption and storage-write retry behavior are unchanged; this does not resolve #1803. +The [PDF workflow research reference](research/pdf-workflows/README.md) collects archived findings and a dated feature mapping. Planning remains in this roadmap and the parity checklist. + ## Alpha gate The alpha gate: Acrobat Pro's everyday core workflows must each work end to end on the @@ -204,11 +216,11 @@ total matches the parity estimate. | M | Milestone | Plan size (h) | Done | Remaining (h, 2026-10-10) | Notes | |---|---|---|---|---|---| | M0 | Skeleton: workspace, xtask gates, CI | 15–30 | 98% | 1–2 | GitHub workflow, `deny.toml` (licence audit of every dependency), parity checklist (826 features, `xtask parity`) done. Missing: remaining crate stubs, testkit/oracle crates | -| M1 | COS: filters, crypt, parser, xref, writer | 120–200 | 80% | 20–40 | Done:
- filters and crypt: every standard-security revision R2–R6 (RC4, AES-128/256), SASLprep, permissions, creating encryption;
- cos: parse and repair (including invalid xref object-number ranges), decoded-stream limits including unfiltered data, decrypt on load, re-encrypt on save, incremental and full writing.
Corpus: open/edit/save passes on 958 files, and all 7 password-protected files open.
Full saves now pack objects into compressed object streams. An unchanged incremental save of a classic table writes a zero-length cross-reference subsection instead of a section with no subsections (#1339). A full rewrite keeps unknown trailer entries, including objects reachable only from them (#1338). Fuzzing runs nightly (`xtask fuzz`). Raw-deflate fallback preserves decoding-limit errors, including through tolerant COS stream decoding. Done since: #2658: macOS atomic saves retain destination extended attributes, including Finder tags, and reject partial resource-fork reads after size changes. Metadata failures retain the original PDF and unsaved edit. Missing: ≥ 250 tests, own image codecs | +| M1 | COS: filters, crypt, parser, xref, writer | 120–200 | 80% | 20–40 | Done:
- filters and crypt: every standard-security revision R2–R6 (RC4, AES-128/256), SASLprep, permissions, creating encryption;
- cos: parse and repair (including invalid xref object-number ranges), decoded-stream limits including unfiltered data, decrypt on load, re-encrypt on save, incremental and full writing.
Corpus: open/edit/save passes on 958 files, and all 7 password-protected files open.
Full saves now pack objects into compressed object streams. An unchanged incremental save of a classic table writes a zero-length cross-reference subsection instead of a section with no subsections (#1339). A full rewrite keeps unknown trailer entries, including objects reachable only from them (#1338). Fuzzing runs nightly (`xtask fuzz`). Raw-deflate fallback preserves decoding-limit errors, including through tolerant COS stream decoding. Refreshed against upstream `99941daa` (2026-10-15). COS reference lookups and full saves distinguish complete object references, so stale generations cannot expose replacement content. Snapshot restoration preserves edited nonzero-generation objects through incremental save/reopen. Synthetic regressions cover caches, overlays, snapshots, stream lengths and classic/packed saves. For damaged live rows, either the xref generation or a same-number header at its indexed offset may identify the object; neither-match and free slots remain null. Header probes read at most 128 bytes per candidate offset, and opening records at most 256 object disagreements plus a summary without expanding compact xref rows. Synthetic dictionary-edit and independent save/reopen controls cover this repair policy. Repair-aware preflight edits and restored snapshots retain both accepted aliases, with full rewrite for repaired edits; classic/packed output coalesces them to one object identity. Materialized diagnostics inspect retained primary/hybrid rows before folding, with the same cap. Done since: #2658: macOS atomic saves retain destination extended attributes, including Finder tags, and reject partial resource-fork reads after size changes. Metadata failures retain the original PDF and unsaved edit. Missing: ≥ 250 tests, own image codecs | | M2 | Model, render, text | 200–350 | 15% | 130–230 | hayro bootstrap renderer (vendored patches); inspector predictor scratch is bounded by available input, and JPX sample rescaling handles 1–32 bits safely. Text extraction reaches word-F1 0.98 against pdftotext. Short same-line redraws split before placement rather than interleaving. Missing: model crate, fonts, DisplayList, renderer independent of hayro. Inspector page-label traversal and output are resource-bounded, with physical-number fallback warnings. Packed TIFF-predictor RGB rows retain later rows in decoding and rendering. Page marks, added content and print output retain small page-unit transform coefficients when serialized. Forms without Resources use page Properties for ActualText and layer visibility; own dictionaries stop fallback. Oversized missing inline-image samples, including header and terminator overhead, no longer pin the content window (#3107 / PR #3438); normal large streamed images retain deterministic sample boundaries. Radial luminosity fades have synthetic coverage for the existing #2132 mask guard, including per-pixel references, antialiased clips, previews, scales and tiles; no additional production fix was needed. Dotted TOC leaders no longer inflate letter tracking or hide word spaces (#3086); phrase search and selection are covered. Short tracked ellipses retain their gap samples and remain searchable. Missing spacing glyphs in non-embedded Japanese CID substitutes render blank without changing PDF advances or Unicode (#3601). The desktop viewer can use installed CJK fonts, including Windows GBK Song/Hei/Kai/FangSong and subset-prefixed Founder names; Combine thumbnails and accessibility figure previews use the same display-only fallback, without changing export or print font policy (#234, continuation of #1567, review #4172). Print previews exclude installed faces and isolate their font cache; GBK regressions cover view/print switching for inline and pooled whole-page/tile rendering. Installed-font helpers and their regression contracts have function documentation. GBK regression rasters match explicit-Unicode references with independently selected installed faces; wrong-glyph and wrong-family negative controls fail. Integration with main fa5f9f8c retains viewer fallback across the split engine session modules, renderer tests and Combine thumbnail workers. | | M2 | Model, render, text | 200–350 | 15% | 130–230 | hayro bootstrap renderer (vendored patches); inspector predictor scratch is bounded by available input, and JPX sample rescaling handles 1–32 bits safely. Text extraction reaches word-F1 0.98 against pdftotext. Short same-line redraws split before placement rather than interleaving. Missing: model crate, fonts, DisplayList, renderer independent of hayro. Inspector page-label traversal and output are resource-bounded, with physical-number fallback warnings. Packed TIFF-predictor RGB rows retain later rows in decoding and rendering. The renderer keeps its one-point crop-size floor in physical points after UserUnit scaling, with regression coverage for crop origins, rotations and f32 reciprocal rounding. Existing unit policy and nearly-zero fallback are retained. | | M2 | Model, render, text | 200–350 | 15% | 130–230 | hayro bootstrap renderer (vendored patches); inspector predictor scratch is bounded by available input, and JPX sample rescaling handles 1–32 bits safely. Text extraction reaches word-F1 0.98 against pdftotext. Short same-line redraws split before placement rather than interleaving. Missing: model crate, fonts, DisplayList, renderer independent of hayro. Inspector page-label traversal and output are resource-bounded, with physical-number fallback warnings | -| M3 | Viewer app (native + web) | 80–150 | 90% | 10–20 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). #3808: the Pages inspector offers a 50–150% thumbnail slider and buttons, also available through `ui.set`. Missing: 60 fps test on a 500-page document, snapshot tests of every panel
Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel; numeric Linux portal events and idle wakeups completing #881, with native appearance inheritance retained (#446); late browser startup downloads preserve the current document and Save target (#171). Selected-text Copy in Edit and the canvas context menu is covered by focused regressions and native clipboard checks (#1225); document copy restrictions apply to the floating action too. #1364 candidate: absolute local GoToR links ask before target access; headless callers explicitly opt in. Native-opening candidate: chosen tools survive preparation and password retries; startup intake waits for pending preparation. Page-label navigation displays logical labels, retains the current page on unchanged submission and refreshes focused same-document replacements; clean file-watcher reload preserves the chosen scrolling mode, preventing a Single Page reader from jumping to page 1 during replacement. Synthetic control regressions cover these behaviors. #3149: page-list changes discard render demand based on old geometry; actual raster dimensions are checked against the GPU limit before upload. Synthetic crash and continued-rendering regressions cover the replacement path. Save-prompt shortcuts yield to dialogs above them; stacked link-consent UI regression covers Escape and Return. Comment hover follows individual marked lines and original annotation paint order (#3541/#3620), independently of spatial inspection sorting; synthetic rotation, differing-upper-edge overlap and unchanged-document regressions. Optional page gaps join page edges without cropping PDF margins; current-document toggle, persisted default and UI control options are covered by viewer tests. Gap changes in Organize Pages preserve document scroll position, and both comparison panes follow menu and control choices. Repeated window resizes by an edge and moves by the tab strip on GNOME/Wayland work without a click in between. Optional --two-paged applies noncontinuous two-page layout to explicitly opened CLI files without changing defaults; explicit --layout wins. | +| M3 | Viewer app (native + web) | 80–150 | 90% | 10–20 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). #3808: the Pages inspector offers a 50–150% thumbnail slider and buttons, also available through `ui.set`. Missing: 60 fps test on a 500-page document, snapshot tests of every panel
Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel; numeric Linux portal events and idle wakeups completing #881, with native appearance inheritance retained (#446); late browser startup downloads preserve the current document and Save target (#171). Selected-text Copy in Edit and the canvas context menu is covered by focused regressions and native clipboard checks (#1225); document copy restrictions apply to the floating action too. #1364 candidate: absolute local GoToR links ask before target access; headless callers explicitly opt in. Native-opening candidate: chosen tools survive preparation and password retries; startup intake waits for pending preparation. Page-label navigation displays logical labels, retains the current page on unchanged submission and refreshes focused same-document replacements; clean file-watcher reload preserves the chosen scrolling mode, preventing a Single Page reader from jumping to page 1 during replacement. Synthetic control regressions cover these behaviors. #3149: page-list changes discard render demand based on old geometry; actual raster dimensions are checked against the GPU limit before upload. Synthetic crash and continued-rendering regressions cover the replacement path. Save-prompt shortcuts yield to dialogs above them; stacked link-consent UI regression covers Escape and Return. Comment hover follows individual marked lines and original annotation paint order (#3541/#3620), independently of spatial inspection sorting; synthetic rotation, differing-upper-edge overlap and unchanged-document regressions. Optional page gaps join page edges without cropping PDF margins; current-document toggle, persisted default and UI control options are covered by viewer tests. Gap changes in Organize Pages preserve document scroll position, and both comparison panes follow menu and control choices. Repeated window resizes by an edge and moves by the tab strip on GNOME/Wayland work without a click in between. Optional --two-paged applies noncontinuous two-page layout to explicitly opened CLI files without changing defaults; explicit --layout wins. Crop pages is directly available in the quick toolbar (#5330). | | M3 | Viewer app (native + web) | 80–150 | 90% | 10–20 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). Missing: 60 fps test on a 500-page document, snapshot tests of every panel
Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel; numeric Linux portal events and idle wakeups completing #881, with native appearance inheritance retained (#446); late browser startup downloads preserve the current document and Save target (#171). Selected-text Copy in Edit and the canvas context menu is covered by focused regressions and native clipboard checks (#1225); document copy restrictions apply to the floating action too. #1364 candidate: absolute local GoToR links ask before target access; headless callers explicitly opt in. Native-opening candidate: chosen tools survive preparation and password retries; startup intake waits for pending preparation. Page-label navigation displays logical labels, retains the current page on unchanged submission and refreshes focused same-document replacements; clean file-watcher reload preserves the chosen scrolling mode, preventing a Single Page reader from jumping to page 1 during replacement. Synthetic control regressions cover these behaviors. Wrapped revision buttons stay above Binding in Document Properties (#2739); a synthetic UI test checks layout and saved revision content. | | M3 | Viewer app (native + web) | 80–150 | 90% | 10–20 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). Missing: 60 fps test on a 500-page document, snapshot tests of every panel
Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel; numeric Linux portal events and idle wakeups completing #881, with native appearance inheritance retained (#446); late browser startup downloads preserve the current document and Save target (#171). #1364 candidate: absolute local GoToR links ask before target access; headless callers explicitly opt in. Native-opening candidate: chosen tools survive preparation and password retries; startup intake waits for pending preparation. Later native-open replies stay queued while a modal or picker owns the foreground; regressions cover recipient ownership, completion/cancellation and one-shot wakes. | | M3 | Viewer app (native + web) | 80–150 | 90% | 10–20 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). Missing: 60 fps test on a 500-page document, snapshot tests of every panel
Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel; numeric Linux portal events and idle wakeups completing #881, with native appearance inheritance retained (#446); late browser startup downloads preserve the current document and Save target (#171). #1364 candidate: absolute local GoToR links ask before target access; headless callers explicitly opt in. Native-opening candidate: chosen tools survive preparation and password retries; startup intake waits for pending preparation. #1980: exact quit modifiers preserve Ctrl+Alt+Q email input; Ctrl+Q/Cmd+Q asks before discarding unsaved documents. | @@ -231,15 +243,15 @@ total matches the parity estimate. | M6 | Forms + JavaScript | 160–320 | 50% | 40–75 | Done: filling all field types with regenerated appearances; Prepare a form (every field type, multi-select/shared properties, move/resize/delete, Field Properties General/Appearance/Position/Options/Format/Validate/Calculate); Acrobat's AF format/keystroke/validate/calculate functions and simplified field notation run natively in Acrobat's event order; tab order; push-button actions; flatten; agent tools. Form data exchange (FDF, XFDF, XML, CSV, text) done; XFDF values retain text across XML comments and processing instructions. Plain AcroForm XML imports retain all direct text around comments and processing instructions, including CDATA and character references.
Done since: JavaScript engine (boa, sandboxed) running custom keystroke/validate/calculate/format and button scripts, document JavaScripts, console. Missing: Actions tab, wider object model (annotations, layers, dialogs), XFA
Done since: Multi-select choice fields write ascending `/I` option indices while preserving `/V` selection order; synthetic forms/engine regressions cover reverse selection and save/reopen. CSV export preserves carriage returns in names and values. | | M6 | Forms + JavaScript | 160–320 | 50% | 40–75 | Done: filling all field types with regenerated appearances; Prepare a form (every field type, multi-select/shared properties, move/resize/delete, Field Properties General/Appearance/Position/Options/Format/Validate/Calculate); Acrobat's AF format/keystroke/validate/calculate functions and simplified field notation run natively in Acrobat's event order; tab order; push-button actions; flatten; agent tools. Form data exchange (FDF, XFDF, XML, CSV, text) done; XFDF values retain text across XML comments and processing instructions. XML/XFDF exports preserve tabs and line breaks in names and comment metadata. Refreshed against upstream `99941daa` (2026-10-15).
Done since: JavaScript engine (boa, sandboxed) running custom keystroke/validate/calculate/format and button scripts, document JavaScripts, console. Missing: Actions tab, wider object model (annotations, layers, dialogs), XFA
Done since: Multi-select choice fields write ascending `/I` option indices while preserving `/V` selection order; synthetic forms/engine regressions cover reverse selection and save/reopen. CSV export preserves carriage returns in names and values. | | M6 | Forms + JavaScript | 160–320 | 50% | 40–75 | Done: filling all field types with regenerated appearances; Prepare a form (every field type, multi-select/shared properties, move/resize/delete, Field Properties General/Appearance/Position/Options/Format/Validate/Calculate); Acrobat's AF format/keystroke/validate/calculate functions and simplified field notation run natively in Acrobat's event order; tab order; push-button actions; flatten; agent tools. Form data exchange (FDF, XFDF, XML, CSV, text) done; XFDF values retain text across XML comments and processing instructions. XML/XFDF exports preserve tabs and line breaks in names and comment metadata. Plain AcroForm XML imports retain all direct text around comments and processing instructions, including CDATA and character references. CSV export and spreadsheet merging quote tabs, and import distinguishes quoted tabs from TSV delimiters without changing field identities.
Done since: JavaScript engine (boa, sandboxed) running custom keystroke/validate/calculate/format and button scripts, document JavaScripts, console. Missing: Actions tab, wider object model (annotations, layers, dialogs), XFA
Done since: Multi-select choice fields write ascending `/I` option indices while preserving `/V` selection order; synthetic forms/engine regressions cover reverse selection and save/reopen. CSV export preserves carriage returns in names and values. | -| M7 | Content editing (text, images, header/footer, watermark) | 250–500 | 20% | 100–180 | Done: header & footer, watermarks, backgrounds, Bates; added text and images that stay editable (move, resize, format, rotate, flip, crop, replace); links (Link tool, Link Properties, create from URLs, remove all). Done since: Edit text preserves Arabic/Hebrew `/ActualText` typing order, with font-independent regressions; single-glyph RTL ligature spans (including lam-alef) join their surrounding word. Done since (#1116): editable text regions grouped across compatible words and subset fonts, one rich-text editor over a background with the source text removed, embedded sfnt faces reused as editor faces, and visible (clipped) bounds for cropped-image selection and resize. Exclusive crops now resize across nested states and content streams, with shared clips fixed.
Done since: #594 ordinary paragraph drags preserve overlapping source text; UI/engine regressions, native save/reopen, independent-reader checks and full CI passed in the Linux test environment. #1200 candidate: local GoToR links open in desktop tabs and through root-confined automation; appearance-only edits preserve existing actions. Named destinations remain unapplied and `/NewWindow` is a tab hint. Done since: #3187: Added-image automation normalizes extreme signed rotation angles before combining them with the current orientation; integer bounds, history, pixels, save/reopen and MCP have regressions. Done since: #2592: Styled fallback fonts batch up to 240 glyphs per actual face and retain source styles. Budget refusal and distinct same-BaseFont descriptor weights have mutation-checked regressions; coverage and complex mixed runs remain limited. Done since: #5657 page-mark operations are atomic across selected pages, including shared/inherited resources and existing unsaved edits. Missing: editing existing text and images in place (the longest pole), image/PDF watermarks PR #2155 candidate: editor-owned crops preserve original clips through group moves; Arrange preserves added-item stream ownership and refuses unsafe scope/state/tag extraction; image handles and flips follow page orientation; general transforms and Unicode script toggles have headless tools. Clipboard, mixed-size formatting, Home pins and reloadable installed-font discovery have review regressions. Latest follow-up uses explicit image-paste events, refreshes the mixed-size display, and documents script normalization with Undo. Further review covers inherited Form text and mask chronology, scoped transform composition, affine resize/rotation pivots, paste/selection ownership, no-op scripts and page-menu permissions. Done since: #5835 a right-click on an image in crop mode keeps the crop and opens no menu, also over overlapping and rotated images. | +| M7 | Content editing (text, images, header/footer, watermark) | 250–500 | 20% | 100–180 | Done: header & footer, watermarks, backgrounds, Bates; added text and images that stay editable (move, resize, format, rotate, flip, crop, replace); links (Link tool, Link Properties, create from URLs, remove all). Done since: Edit text preserves Arabic/Hebrew `/ActualText` typing order, with font-independent regressions; single-glyph RTL ligature spans (including lam-alef) join their surrounding word. Done since (#1116): editable text regions grouped across compatible words and subset fonts, one rich-text editor over a background with the source text removed, embedded sfnt faces reused as editor faces, and visible (clipped) bounds for cropped-image selection and resize. Exclusive crops now resize across nested states and content streams, with shared clips fixed.
Done since: #594 ordinary paragraph drags preserve overlapping source text; UI/engine regressions, native save/reopen, independent-reader checks and full CI passed in the Linux test environment. #1200 candidate: local GoToR links open in desktop tabs and through root-confined automation; appearance-only edits preserve existing actions. Named destinations remain unapplied and `/NewWindow` is a tab hint. Done since: #3187: Added-image automation normalizes extreme signed rotation angles before combining them with the current orientation; integer bounds, history, pixels, save/reopen and MCP have regressions. Done since: #2592: Styled fallback fonts batch up to 240 glyphs per actual face and retain source styles. Budget refusal and distinct same-BaseFont descriptor weights have mutation-checked regressions; coverage and complex mixed runs remain limited. Done since: #5657 page-mark operations are atomic across selected pages, including shared/inherited resources and existing unsaved edits. Missing: editing existing text and images in place (the longest pole), image/PDF watermarks PR #2155 candidate: editor-owned crops preserve original clips through group moves; Arrange preserves added-item stream ownership and refuses unsafe scope/state/tag extraction; image handles and flips follow page orientation; general transforms and Unicode script toggles have headless tools. Clipboard, mixed-size formatting, Home pins and reloadable installed-font discovery have review regressions. Latest follow-up uses explicit image-paste events, refreshes the mixed-size display, and documents script normalization with Undo. Further review covers inherited Form text and mask chronology, scoped transform composition, affine resize/rotation pivots, paste/selection ownership, no-op scripts and page-menu permissions. Done since: #5835 a right-click on an image in crop mode keeps the crop and opens no menu, also over overlapping and rotated images. Thai editing (#2964, #3185): supported Form text is editable; legacy Thai retains source fonts and text settings. Cross-family replacement is explicit; unsafe groups and mixed contextual styles remain partial. | | M7 | Content editing (text, images, header/footer, watermark) | 250–500 | 20% | 100–180 | Done: header & footer, watermarks, backgrounds, Bates; added text and images that stay editable (move, resize, format, rotate, flip, crop, replace); links (Link tool, Link Properties, create from URLs, remove all). Done since: Edit text preserves Arabic/Hebrew `/ActualText` typing order, with font-independent regressions. Done since (#1116): editable text regions grouped across compatible words and subset fonts, one rich-text editor over a background with the source text removed, embedded sfnt faces reused as editor faces, and visible (clipped) bounds for cropped-image selection and resize. Exclusive crops now resize across nested states and content streams, with shared clips fixed.
Done since: #594 ordinary paragraph drags preserve overlapping source text; UI/engine regressions, native save/reopen, independent-reader checks and full CI passed in the Linux test environment. #1200 candidate: local GoToR links open in desktop tabs and through root-confined automation; appearance-only edits preserve existing actions. Named destinations remain unapplied and `/NewWindow` is a tab hint. #1316: replacing a line or paragraph keeps a neighbouring line whose glyph box overlaps when the baselines differ; a second copy on the same baseline is still replaced. Missing: editing existing text and images in place (the longest pole), image/PDF watermarks | | M7 | Content editing (text, images, header/footer, watermark) | 250–500 | 20% | 100–180 | Done: header & footer, watermarks, backgrounds, Bates; added text and images that stay editable (move, resize, format, rotate, flip, crop, replace); links (Link tool, Link Properties, create from URLs, remove all). #3212: Headers, Bates numbers, watermarks and added text stay visible after unfinished marked content, without revealing hidden content. Tracked editable text distinguishes repeated glyph spacing from word gaps, with synthetic paragraph/object regressions. Missing: editing existing text and images in place (the longest pole), watermark print/screen visibility and repeat | | M6 | Forms + JavaScript | 160–320 | 50% | 40–75 | Done: filling all field types with regenerated appearances; Prepare a form (every field type, multi-select/shared properties, move/resize/delete, Field Properties General/Appearance/Position/Options/Format/Validate/Calculate); Acrobat's AF format/keystroke/validate/calculate functions and simplified field notation run natively in Acrobat's event order; tab order; push-button actions; flatten; agent tools. Form data exchange (FDF, XFDF, XML, CSV, text) done; XFDF values retain text across XML comments and processing instructions.
Done since: JavaScript engine (boa, sandboxed) running custom keystroke/validate/calculate/format and button scripts, document JavaScripts, console. Missing: Actions tab, wider object model (annotations, layers, dialogs), XFA
Done since: Existing simple field-font encodings and Differences are respected when generating appearances; an unencodable value uses a separate Helvetica appearance font without changing the source font. Synthetic reopen regressions cover punctuation, subset fallback and multiline text. Exact font metrics/layout and broader Unicode fallback remain incomplete. | | M6 | Forms + JavaScript | 160–320 | 50% | 40–75 | Done: filling all field types with regenerated appearances; Prepare a form (every field type, multi-select/shared properties, move/resize/delete, Field Properties General/Appearance/Position/Options/Format/Validate/Calculate); Acrobat's AF format/keystroke/validate/calculate functions and simplified field notation run natively in Acrobat's event order; tab order; push-button actions; flatten; agent tools. Form data exchange (FDF, XFDF, XML, CSV, text) done; XFDF values retain text across XML comments and processing instructions.
Done since: JavaScript engine (boa, sandboxed) running custom keystroke/validate/calculate/format and button scripts, document JavaScripts, console. Missing: Actions tab, wider object model (annotations, layers, dialogs), XFA
Done since: Tab and Shift-Tab wrapping to the same field or another widget of that field refresh the newly opened editor from the accepted value before Save. Synthetic UI regressions cover both directions, same-frame Save, validation refusal and undo/redo. Renaming a field renames it in the calculations that read it (field lists, notation, literal `getField` names), in one undo step, and says how many changed. Prepare a form ▸ Calculation order, a page of its own: each calculated field's formula and value, what is wrong (fields that calculate too early, loops, fields missing from `/CO`) and Fix order, Up/Down, Alt+↑/↓ and drag, and on the page each field's step and arrows to and from the field in focus (`form_calculation_order` headlessly). | | M7 | Content editing (text, images, header/footer, watermark) | 250–500 | 20% | 100–180 | Done: header & footer, watermarks, backgrounds, Bates; added text and images that stay editable (move, resize, format, rotate, flip, crop, replace); links (Link tool, Link Properties, create from URLs, remove all). Missing: editing existing text and images in place (the longest pole), image/PDF watermarks | -| M8 | Security + redaction | 100–180 | 68% | 15–30 | Done: opening protected documents, permissions, Protect Using Password, Remove security; redaction (mark text/areas/pages, Search & Redact with patterns, apply removing glyphs, image pixels, vectors, XObject content, annotations and fields, verification, full rewrite on save); Remove hidden information and Sanitize.
Done since: redaction codes (U.S. FOIA and Privacy Act sets as overlay text, in Redaction properties and `redact_mark`). Redaction cleans indirect and form structure references, preserving surviving form tags. Redaction retires inherited resource dictionaries, so a parent `/Pages` can't keep a removed XObject reachable; unredacted pages are left untouched. Image redaction removes alternate images and unsupported masks only on affected images. Changed marked-content spans lose replacement descriptions; untouched spans keep theirs. Hidden-text cleanup retires unused named descriptions across inherited/shared page and form resources (64 focused tests and scoped Rust 1.96 Clippy/wasm checks pass; publication CI pending). The redaction audit counts a filled cover that hides half of any one word of a long line (#2662), applied or not. Phone-pattern redaction also matches labelled Italian geographic numbers with `+39` and a slash after the area code (#3205 follow-up); unlabelled international slash numbers remain unsupported. Missing: certificate security, custom redaction code sets and pattern locales, DCT re-encoding | -| M9 | Signatures (PAdES, validation) | 160–280 | 60% | 40–70 | Done: new `sign` crate (DER, X.509, CMS, PKCS #12 on RustCrypto; aws-lc-rs for RSA private keys); PAdES B-B signing (visible, invisible, existing fields, certification with DocMDP); validation with trust store and changes-after-signing classification; self-signed digital IDs; Signatures panel, message bar, sign dialogs; agent tools. Checked with pdfsig and OpenSSL.
Done since: macOS Keychain and Windows Current User Personal store signing (software-backed CNG keys), certificate viewer, signed documents protected from rewrites, page validation overlays separate from original signature artwork (baked Not Verified captions remain; #79 open), snapshot tabs that reuse instead of multiplying. Missing: timestamps (B-T), LTV (DSS, OCSP, CRL), FieldMDP, smart cards, PKCS #11 | -| M10 | OCR, create, export, print | 200–350 | 33% | 120–215 | Done: create from blank/text/PNG/JPEG/TIFF (multi-page)/GIF/BMP; export PNG/JPEG/TIFF and text; Print (Acrobat's sizing, n-up, booklet, poster, comments & forms, preview, the CUPS and Windows spoolers, print-ready PDF).
Done since: embedded/72/custom DPI choices for image imports; export all images; OCR (searchable image for pages, ranges and multiple files); single-sided cut-and-stack imposition with cut marks, through Print and doc_print; architectural/ANSI/large ISO sheet presets, Match page at 100% and PDF-scale preview (#599 follow-up); Create PDF from multiple files (PDFs, images and text, edited as pages in the grid); OCR can also read pictures on pages that have text, without adding that text twice (#1028). Model staging fetches SHA-256-verified ONNX detection/recognition files after the upstream RTEN URL brownout; runtime discovery prefers complete ONNX pairs per directory, then complete legacy RTEN pairs, without mixing formats or changing directory precedence. Done since: Word invoice export uses borderless inferred tables, floating positioned text/artwork only where text overlaps an image or is rotated, and per-page sizes, covered by synthetic invoice automation regressions (#773); printing on the web through the browser's print dialog, sheets drawn by PdfCraft (gap 4). Lossless image export applies per-component Decode, including Indexed mapping (#1201 partial). Existing 16-bit precision and color-management limits remain. #3532 partial: vector printing retains existing appearances with absent Subtype through a print-owned Form copy; source artwork stays unchanged. #3120: Office export reads form XObjects 50 levels deep, as the renderer does, and up to 50 000 a page; pages past that are listed in `truncated_pages`. Word table cells use the grid columns they span, including cells after a merged cell. Done since: text in a table slot covered by another cell's span remains as paragraph text in Word, HTML, RTF and PowerPoint. Missing: OCR languages beyond Latin, editable-text OCR output, full Office layout fidelity/import. Done since: dotted-leader contents entries stay in separate complete paragraphs (#1757) #2156 candidate: last selected print paper persists across sessions; other print preferences unchanged. Image export applies soft-mask Decode opacity intervals before resizing, including indirect entries and inverted ranges. Vector printing gives annotation appearances unoccupied XObject names so existing page artwork survives. | +| M8 | Security + redaction | 100–180 | 68% | 15–30 | Done: opening protected documents, permissions, Protect Using Password, Remove security; redaction (mark text/areas/pages, Search & Redact with patterns, apply removing glyphs, image pixels, vectors, XObject content, annotations and fields, verification, full rewrite on save); Remove hidden information and Sanitize.
Done since: redaction codes (U.S. FOIA and Privacy Act sets as overlay text, in Redaction properties and `redact_mark`). Redaction cleans indirect and form structure references, preserving surviving form tags. Redaction retires inherited resource dictionaries, so a parent `/Pages` can't keep a removed XObject reachable; unredacted pages are left untouched. Image redaction removes alternate images and unsupported masks only on affected images. Changed marked-content spans lose replacement descriptions; untouched spans keep theirs. Hidden-text cleanup retires unused named descriptions across inherited/shared page and form resources (64 focused tests and scoped Rust 1.96 Clippy/wasm checks pass; publication CI pending). Retained text/image-bearing or unreadable tiling-pattern cells now make Apply refuse atomically after a bounded check of rewritten resources; fully removed cells still apply, and cross-page retention is checked. The redaction audit counts a filled cover that hides half of any one word of a long line (#2662), applied or not. Phone-pattern redaction also matches labelled Italian geographic numbers with `+39` and a slash after the area code (#3205 follow-up); unlabelled international slash numbers remain unsupported. Missing: certificate security, custom redaction code sets and pattern locales, DCT re-encoding | +| M9 | Signatures (PAdES, validation) | 160–280 | 60% | 40–70 | Done: new `sign` crate (DER, X.509, CMS, PKCS #12 on RustCrypto; aws-lc-rs for RSA private keys); PAdES B-B signing (visible, invisible, existing fields, certification with DocMDP); validation with trust store and changes-after-signing classification; self-signed digital IDs; Signatures panel, message bar, sign dialogs; agent tools. Checked with pdfsig and OpenSSL.
Done since: macOS Keychain and Windows Current User Personal store signing (software-backed CNG keys), certificate viewer, signed documents protected from rewrites, page validation overlays separate from original signature artwork, placed in the widget's bottom-right corner as it lies on the page, at a quarter of its shorter side and 48 screen points at most, so they annotate the artwork from a corner instead of covering the middle of it and turn with the page and the view (a field falling under 32 screen points, as a page does when zoomed out to fit, keeps its outline), snapshot tabs that reuse instead of multiplying, named-layer signature appearances redrawn for their real state (#79; display, the CLI's render, exports and printing, never saves; captions in the interface language, Chinese in English; the badge stays). Missing: timestamps (B-T), LTV (DSS, OCSP, CRL), FieldMDP, smart cards, PKCS #11 | +| M10 | OCR, create, export, print | 200–350 | 33% | 120–215 | Done: create from blank/text/PNG/JPEG/TIFF (multi-page)/GIF/BMP; export PNG/JPEG/TIFF and text; Print (Acrobat's sizing, n-up, booklet, poster, comments & forms, preview, the CUPS and Windows spoolers, print-ready PDF).
Done since: embedded/72/custom DPI choices for image imports; export all images; OCR (searchable image for pages, ranges and multiple files); single-sided cut-and-stack imposition with cut marks, through Print and doc_print; architectural/ANSI/large ISO sheet presets, Match page at 100% and PDF-scale preview (#599 follow-up); Create PDF from multiple files (PDFs, images and text, edited as pages in the grid); OCR can also read pictures on pages that have text, without adding that text twice (#1028). Model staging fetches SHA-256-verified ONNX detection/recognition files after the upstream RTEN URL brownout; runtime discovery prefers complete ONNX pairs per directory, then complete legacy RTEN pairs, without mixing formats or changing directory precedence. Done since: Word invoice export uses borderless inferred tables, floating positioned text/artwork only where text overlaps an image or is rotated, and per-page sizes, covered by synthetic invoice automation regressions (#773); printing on the web through the browser's print dialog, sheets drawn by PdfCraft (gap 4). Lossless image export applies per-component Decode, including Indexed mapping (#1201 partial). Existing 16-bit precision and color-management limits remain. #3532 partial: vector printing retains existing appearances with absent Subtype through a print-owned Form copy; source artwork stays unchanged. #3120: Office export reads form XObjects 50 levels deep, as the renderer does, and up to 50 000 a page; pages past that are listed in `truncated_pages`. Word table cells use the grid columns they span, including cells after a merged cell. Done since: text in a table slot covered by another cell's span remains as paragraph text in Word, HTML, RTF and PowerPoint. Web OCR downloads its models on first use and reads in Web Workers with SIMD (`ocr.ocr-web`, shipped). Missing: OCR languages beyond Latin, web OCR near native speed, editable-text OCR output, full Office layout fidelity/import. Done since: dotted-leader contents entries stay in separate complete paragraphs (#1757) #2156 candidate: last selected print paper persists across sessions; other print preferences unchanged. Image export applies soft-mask Decode opacity intervals before resizing, including indirect entries and inverted ranges. Vector printing gives annotation appearances unoccupied XObject names so existing page artwork survives. Done since: Enhance scanned file cleans scanned pages in place (background removal, edge shadow removal, despeckle; the OCR text layer and comments stay; #4119 partial). | | M11 | Optimize, preflight, PDF/A/X/UA, print production | 200–350 | 18% | 80–140 | Done: new `optimize` crate: Reduce File Size and the PDF Optimizer (images measured where drawn, bicubic downsampling, JPEG/ZIP recompression only when smaller, discard objects and user data, Flate clean-up, resource merging, object streams). Target-version compatibility is partial: newer content is not detected or converted. Content stream boundaries preserve comments, split operands and graphics state when image fragments merge. Image downsampling includes page-local `/UserUnit` scaling when measuring physical resolution. Done since: reject invalid image and soft-mask dimensions with checked sizing (#2178); black-and-white images stored losslessly as CCITT Group 4 when smaller, averaged down to 300 ppi above 450 (halftones aside), Group 3 stored again as Group 4, and black-and-white RGB and indexed images that use only palette entries 0 and 1 packed to one bit. Missing: fonts and transparency panels, space audit, preflight, PDF/A/X/UA | | M12 | Accessibility, compare, measure, search, XFA | 200–380 | 30% | 90–160 | Done: new `a11y` crate with the Accessibility Checker (all 32 rules, report, Fix/Skip/Explain, options dialog and results panel, agent tools). Locatable figure, annotation and form findings highlight their page bounds. Character encoding accepts Identity-H/V with Adobe CJK collections and predefined CJK CMaps independently (#3673); per-glyph validation and embedded-font extraction remain separate.
Done since: 2D distance, perimeter and area measurements with persistent viewport calibration, snapping, live information and CSV export; a bounded, text-only auto-tag proposal tool for one page. Marking a figure decorative rewrites only that stream (#1324). An XFA radio item whose value contains a space is written back unchanged (#1319). Done since: #2569: Growable multiline XFA text reflows after edits, with undo/redo, save/reopen and print coverage; oversized fields still become read-only page text. Structured-data synchronization warnings retain accepted whitespace without relayout from stale data. Done since: #2572: Every repeated page-master button resolves its own field to the shared XFA click script, including print buttons. Done since: #2570: Dynamic XFA rich-text fields carry exData/maxLength into AcroForm MaxLen for reported and enforced Unicode character limits, retained after save/reopen. Done since: #2589: Active XFA skips PDF-container fallback scripts while retaining requested messages and saved scripts; ordinary/static/unlaid-out documents and explicit console/field calls keep their existing behavior. Missing: writing proposed tags and parent tree, field autotag, visual Tags/Order/Content panels, Reading Order tool, compare, geospatial/3D measurement, search index, XFA
Reading colours: pending page backgrounds follow the configured colour (#2059). | | M13 | Automation (MCP, Action Wizard, CLI) + AI providers | 60–120 | 45% | 25–45 | Done: MCP resources (document info, text, page images); headless tool table (123 tools incl. signing, optimizing, initial view, links, stamps, data exchange, comment review, forms authoring and scripts, redaction, sanitize, print, add content), opt-in MCP server over stdio, CLI `run`/`tools` (closed stdout pipes exit cleanly), UI control channel with drag. Missing: Action Wizard, AI providers
Done since: CLI run and MCP reject missing, empty, repeated and option-valued --root arguments before any tool/script execution or MCP initialization. A single-hyphen directory name remains valid; synthetic CLI regressions preserve a would-be overwritten file. `text_find` also caps the per-page match ranges it materializes at the remaining result limit plus one continuation match. CLI robustness scans visit each directory once through overlapping roots and symlink aliases. | @@ -288,6 +300,8 @@ M2 (renderer), M7 (content editing), M10 (OCR and Office), M11 (Preflight and st ## Revision history +**2026-10-31 · minor:** M3/M4 #5330: direct quick-toolbar Crop pages action reuses command permissions and the existing crop tool. Estimates unchanged. + **2026-10-26 · patch:** M2/PR #4172: reconcile current main and preserve upstream Japanese serif/weight selection alongside the installed CJK viewer and preview fallback. Estimates unchanged. **2026-10-25 · minor:** M14 localization current state: Arabic catalogs stay logical, Arabic/Kurdish labels take an explicit RTL direction, and stock chrome mirrors; native/browser RTL input, IME and complex workflows remain unqualified. Native Simplified Chinese uses LXGW WenKai Lite; the web build has no Hans face. Estimates unchanged. @@ -304,10 +318,14 @@ Estimates unchanged. | Date | Change | Summary | |---|---|---| +| 2026-11-07 | patch | M10 `ocr.ocr-web` ported onto #977's engines and preprocessing: the page prepares each page, a Web Worker runs ocrs, the downloaded models join the engine cache. Estimates unchanged. | +| 2026-11-05 | patch | M7 #2964 / #3185: grouped text and Thai source-font preservation; limitations and estimates unchanged. | | 2026-11-03 | minor | M11: Optimize's JBIG2 images share a symbol dictionary (`/JBIG2Globals`), up to 16 at a time and when smaller (`pdfcraft_filters::encode_jbig2_shared`): 41–46% smaller on rendered text in proportional fonts, 10% in Courier | | 2026-11-03 | patch | M5 #5850 review: indirect typewriter intent and composer edits preserve outer whitespace; movement regression commits by outside click; estimates unchanged. | | 2026-11-03 | patch | M5 #5850: Enter adds Fill & Sign line breaks; outside clicks commit, Escape cancels, and saved multiline appearances retain their text and height; estimates unchanged. | | 2026-11-02 | patch | M10 #5743: macOS print jobs carry the job name and owner some vendors' filters need; not yet checked on a physical printer | +| 2026-11-02 | patch | M3 #5708: pages shorter than the window sit in its middle, on a whole physical pixel | +| 2026-11-02 | patch | M14 #5737, #5460: Preferences turns off the update check at start, on by default | | 2026-11-01 | patch | M7 #5657: header/footer, watermark and background operations commit all selected pages together; later-page errors preserve marks, content, shared/inherited resources and pending edits; estimates unchanged. | | 2026-10-31 | patch | #1577: reconcile optional page shadows with current viewer/organizer and settings modules; CI validation pending. | | 2026-10-31 | patch | M3 #1354: reconcile the launch override with split app/file modules and the structured open result; preserve headless help/version exits. CI validation pending. | @@ -322,7 +340,9 @@ Estimates unchanged. | 2026-10-23 | minor | M11: black-and-white images (1-bit, and image masks) are stored losslessly as CCITT Group 4 when that is smaller: PDF Optimizer ▸ Images ▸ Monochrome Images (on by default, so Reduce File Size too), `doc_optimize` `mono`; `pdfcraft_filters::encode_g4`. JBIG2 and monochrome downsampling not yet. Estimates unchanged. | | 2026-10-23 | minor | M6: calculation checks: a formula reading a field the form doesn't have, and a script building the names it reads, are flagged by `form_calculation_order` and on the Calculation order page (a red mark, a card naming the broken formulas, a note for built names); `form_calculation_order` gives each formula and `form_fields` a custom Calculate script. Estimates unchanged. | | 2026-10-29 | minor | M7 #2517: widening a paragraph rejoins a wrap with the spaces that wrap consumed, including a typed run and none for a Chinese or Japanese word split, after an incremental save and a full rewrite. Estimates unchanged. | +| 2026-10-29 | minor | M2: non-interpolated images drawn below their resolution are area-averaged to whole device pixels instead of nearest-neighbour (scanned text kept one sample in N per axis); page-scan-size images are smoothed when enlarged. Estimates unchanged. | | 2026-10-27 | minor | M10: OCR stages SHA-256-verified ONNX models after the upstream RTEN URL brownout; discovery prefers a complete ONNX pair per directory, falls back to a complete RTEN pair, and never mixes formats or directories; language/web limits and estimates unchanged. | +| 2026-10-20 | minor | M9 #79 review, M2, M8: /ActualText that marks no glyph extracts where it paints, as far as it is visible; redaction strips /ActualText around paint it clips; named-layer recognition needs the layer form to draw n1 and n3. Estimates unchanged. | | 2026-10-24 | minor | M3, #3970 follow-up: the button release given back after a window move or resize goes before the pointer leaving, so hover doesn't stick. Estimates unchanged. | | 2026-10-23 | minor | M8, #3205 follow-up: phone-pattern redaction includes the +39 country code on labelled Italian geographic numbers with a slash; dates, protocol identifiers and whole-run guards remain. Estimates unchanged. | | 2026-10-23 | minor | M10: Word table cells use the grid columns they span, including cells after a merged cell; synthetic export regression and CLI reproduction. Estimates unchanged. | @@ -350,8 +370,12 @@ Estimates unchanged. | 2026-10-20 | minor | M10, #3532 partial: vector printing retains existing appearances with absent Subtype; estimates unchanged. | | 2026-10-19 | minor | M4 #3309: named-destination pruning keeps large documents usable; 60,000-name and deep-tree regressions verify targets through undo/redo and both save modes. Estimates unchanged. | | 2026-10-19 | minor | M6: Text field table (rows and columns of text fields as one edit, `form_add_field_table`); estimates unchanged | +| 2026-10-20 | patch | M1, #1143 review: preserve accepted aliases through preflight/snapshots, normalize them to one output identity with full-save fallback for repaired edits, and report retained materialized/hybrid generation disagreements before folding. Synthetic red/green and four-save controls; estimates unchanged. | +| 2026-10-18 | minor | M1, #1143: a live reference may match the xref generation or a same-number header at its indexed offset; neither-match and free slots remain null. Bounded repair diagnostics and synthetic cache, free-slot, edit and classic/packed save regressions; estimates unchanged. | | 2026-10-18 | minor | M5, PR #1971: nested source `/Page` and `/Pages` dictionaries export as null without traversing page content or the page tree. Synthetic saved/reopened three-page regression retains unrelated metadata and aliases. FDF import semantics, copied-graph limits and estimates unchanged. | | 2026-10-15 | minor | M5: budget retained FDF entries before cloning; discovery keeps lazy array owners and annotation positions, with alias/cache-eviction stress fixtures. Limits cover the copied graph; source parsing/page discovery retain existing COS/parser limits, with no total source-memory bound. FDF import semantics and estimates unchanged. | +| 2026-10-16 | minor | M9 #79: named-layer signature appearances show the signature's real state in the display copy, exports and both print modes, never in saves; captions follow the interface language. Estimates unchanged. | +| 2026-10-16 | minor | M9: the page validation overlay moves to the widget's bottom-right corner as it lies on the page and stops growing past 48 screen points, so it annotates the signer's artwork from a corner instead of covering the middle of it; a field under 32 screen points keeps its outline. Estimates unchanged. | | 2026-10-15 | minor | M7: Edit text caret height follows its row, with a kittest regression; estimates unchanged | | 2026-10-19 | minor | M3 #3360: retain each document’s scroll position when switching Read and the docking workspace at fixed zoom. Estimates unchanged. | | 2026-10-18 | minor | M4, PR #1919 review: repeated page indices apply each rotation to the latest page-local value, preserving indirect/inherited angles. Synthetic adjacent/interleaved duplicates retain siblings and page contents through incremental and full saves. Estimates unchanged. | @@ -362,10 +386,13 @@ Estimates unchanged. | 2026-10-14 | minor | M1: raw-deflate fallback preserves decoding-limit errors in strict and tolerant decoding; synthetic filter and COS stream regressions. Estimates unchanged. | | 2026-10-16 | minor | M5 #2669, item 5: the Comments panel groups replies and counts all page roots once per frame. Estimates unchanged. | | 2026-10-15 | minor | M9 #1895: generic appearance rewriting deferred; separate validation overlays preserve original artwork, exports and printing. Baked captions remain unchanged (#79 open). Estimates unchanged. | +| 2026-10-15 | minor | M1: refresh object-generation reference identity, retaining current upstream progress and regression tests. Estimates unchanged. | | 2026-10-16 | minor | M10 #1201 partial: lossless image export applies component Decode pairs and Indexed mapping. Estimates unchanged. | | 2026-10-15 | minor | M6: CSV export and spreadsheet merging quote tabs, and import distinguishes quoted tabs from TSV delimiters without changing field identities. Estimates unchanged. | | 2026-10-13 | minor | M4: rotation queries and edits resolve indirect page-tree values; inherited, negative and large rotations verified through both saves. Estimates unchanged. | | 2026-10-14 | minor | M12: pending page backgrounds respect reading colours (#2059); estimates unchanged. | +| 2026-10-12 | minor | M10 `ocr.ocr-web` shipped: Web Worker OCR checked by hand in a browser. Estimates unchanged. | +| 2026-10-12 | minor | M10 `ocr.ocr-web` (partial): the web build downloads the OCR models on first use (the web zip ships them) and reads pages in Web Workers built with SIMD. Estimates unchanged. | | 2026-10-12 | minor | M6: valid AcroForm XML element names with original PDF field names preserved; saved-PDF collision and Unicode regressions. Estimates unchanged. | | 2026-10-12 | minor | M6: preserve carriage returns in exported CSV field names and values; saved-PDF round-trip coverage. Estimates unchanged. | | 2026-10-12 | minor | M7 local candidate: text drafts grow and save their width; single visual rows grow across style boundaries; both text tools retype added items and Add Text edits source text. Added-item textless previews are read-only and covered by UI/tool regressions. Estimates unchanged. | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index d03c27c95c..2ed8365a73 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,5 +1,6 @@ # PdfCraft parity with Adobe Acrobat Pro +> **Last reviewed:** 2026-10-28 | **Last updated:** 2026-10-28 | **Change:** minor (M10, #4119 partial: Enhance scanned file cleans scanned pages in place: background removal, edge shadow removal, despeckle; estimates unchanged) > Integration update 2026-10-30: lossless JBIG2 generic and exact-copy symbol coding are included; no lossy substitution. Combined tests passed at bdf5620; the subsequent review fixes require fresh CI. > **Last reviewed:** 2026-10-25 · **Last updated:** 2026-10-26 · **Change:** minor (K Optimize and standards no longer lists monochrome downsampling as missing; scores and estimates unchanged) @@ -190,7 +191,7 @@ features total and are shown for orientation; the others add to it. | Localization | ≈ 68% over the 12 key languages | 5% | 35–60 | additional | 22 catalogs + English (23 interface languages); native craft-fonts builds embed a Simplified Chinese face (LXGW WenKai Lite) but the web build has none; Arabic labels stay logical with explicit RTL direction and mirrored stock chrome, while native/browser/IME workflows are unqualified; no Hindi, Indonesian, Korean | [localization-parity.md](localization-parity.md) | | Performance | ≈ 40% | 6% | 25–45 | additional | 500-page smooth scrolling and large-file memory work (#307) done; no performance budgets (`misc.performance-budgets`), no lazy loading of GB files; blurry text at 1080p/1440p (#730) | [gaps.md](gaps.md) | | Stability | ≈ 50% | 8% | 45–80 | additional | Never-crash rules and nightly fuzzing; 245 open issues on 2026-10-10, many of them `fix(...)` reports of wrong results in shipped features (the #799–#821 batch is fixed) | [gaps.md](gaps.md) | -| Platforms | ≈ 70% | 5% | 15–30 | additional | macOS (signed, notarized), Windows x64/x86/ARM64 (signed), Linux (AppImage, deb, rpm, Flatpak), FreeBSD, web (WASM). Acrobat has no Linux, FreeBSD or full web editor; we lack web OCR and web recovery | [hardware-parity.md](hardware-parity.md) | +| Platforms | ≈ 70% | 5% | 15–30 | additional | macOS (signed, notarized), Windows x64/x86/ARM64 (signed), Linux (AppImage, deb, rpm, Flatpak), FreeBSD, web (WASM). Acrobat has no Linux, FreeBSD or full web editor; web OCR reads in Web Workers but is far slower than native, and we lack web recovery | [hardware-parity.md](hardware-parity.md) | | Ecosystem and plug-ins | ≈ 30% | 1% | 10–20 | within features | No plug-in SDK or folder-level JavaScript; Action Wizard partial. Agent automation (CLI, MCP, UI control channel) has no Acrobat equivalent | [gaps.md](gaps.md) | | AI features | ≈ 0% (in scope) | 1% | 20–40 | within features | Acrobat's AI Assistant is cloud-only (out of scope). Local, opt-in provider interface (`misc.ai-provider`) not started | [gaps.md](gaps.md) | | **Total (weighted sum)** | **≈ 50%** | 100% | **≈ 780–1,400** | | | | @@ -210,7 +211,7 @@ full parity in that area. | F Forms and JavaScript | 79 | 58 | 8 | 78.5% | 65% | 40–75 | Fill and author every AcroForm field type, Acrobat's AF functions in Acrobat's event order, sandboxed JavaScript (boa), data exchange, Fill & Sign; XFA static/dynamic layout, FormCalc and XFA data (partial). CLI info, text and render use the laid-out dynamic pages (#1645). Missing: wider JavaScript object model, document actions, submit, debugger, barcode fields, XFA flattening and full dynamic fidelity | | G Protect, redact, sanitize | 41 | 23 | 2 | 58.5% | 55% | 15–30 | Passwords and permissions, Remove security, redaction that removes glyphs, pixels, vectors, annotations and tags, with verification; word lists, patterns, FOIA/Privacy Act codes; sanitize. Missing: certificate security, pattern locales, custom code sets, partial-word redaction, folder redaction, protected view | | H Digital signatures | 45 | 23 | 10 | 62.2% | 45% | 40–70 | PAdES B-B signing and validation (PKCS #12, macOS Keychain, Windows store with PIN prompts), certification, encrypted documents, certificate usage checks; B-T, LTV, OCSP/CRL with caller-supplied evidence. Missing: PKCS #11 and smart cards outside Windows, timestamp-server preferences, online revocation fetching, FieldMDP and field locking, OS trust store, EU Trusted Lists, compare signed version | -| I Scan and OCR | 26 | 5 | 2 | 23.1% | 15% | 40–75 | Searchable image (ocrs, Latin script without accents) on pages, ranges and many files. Missing: every other language (#744 Dutch; accents, CJK, Cyrillic, Arabic), editable-text output, deskew, rotation, despeckle, camera images beyond straightening (found or placed corners): orientation, clean-up, MRC compression, scanners | +| I Scan and OCR | 26 | 5 | 2 | 23.1% | 15% | 40–75 | Searchable image (ocrs, Latin script without accents) on pages, ranges and many files. Missing: every other language (#744 Dutch; accents, CJK, Cyrillic, Arabic), editable-text output, deskew, rotation, camera images beyond straightening (found or placed corners): orientation, clean-up, MRC compression, scanners. Background removal, edge shadow removal and despeckle work on scanned pages (Enhance scanned file) but not yet as options of Recognize text, nor on CMYK, indexed, JBIG2 or CCITT scans | | J Create and export | 34 | 12 | 3 | 39.7% | 25% | 55–100 | Create from images, multipage TIFF, text, clipboard, blank, many files at once; export PNG/JPEG/TIFF, text, RTF; Word and HTML partial. Missing: Office/HTML/web/PostScript to PDF, Excel and PowerPoint export, Word layout fidelity (#773), SVG/XML/EPS/PS/JPEG 2000 export, accessible text | | K Optimize and standards | 58 | 15 | 11 | 35.3% | 20% | 65–115 | Reduce File Size and the Optimizer (images, CCITT and lossless JBIG2 monochrome compression, discards, clean-up, space audit); PDF/A-2b/3b verify and convert (partial). Missing: Preflight (profiles, fixups, reports), PDF/A-1/4 and a/u levels, PDF/X, PDF/UA, PDF/E, PDF/VT, transparency flattening, font unembedding, lossy/refinement JBIG2 | | L Print | 39 | 15 | 1 | 39.7% | 35% | 30–55 | Acrobat's sizing, n-up, booklet, poster, comments & forms, preview, print-ready PDF, CUPS on macOS/Linux, the Windows spooler (#756) and the browser's print dialog on the web. Missing: print as image, PostScript, all print-production tools (output preview, separations, ink manager, printer marks, colour conversion) | @@ -302,6 +303,8 @@ licensed OCR or AI model. | 2026-10-26 | minor | `optimize.mono-compression` became partial (CCITT Group 4), counted on the table's own measurement: K Optimize and standards 15 shipped and 11 partial of 58 (35.3%), no longer listing CCITT as missing; breadth 57.0% unweighted (423 + 75 of 808), mainstream 63.3% (292 + 45 of 497). Estimates unchanged. | | 2026-10-25 | minor | Localization current state: Arabic catalogs stay logical, Arabic/Kurdish labels take an explicit RTL direction, and stock chrome mirrors; native/browser RTL input, IME and complex workflows remain unqualified. Native Simplified Chinese uses LXGW WenKai Lite; the web build has no Hans face. Scores and estimates unchanged. | | 2026-10-20 | minor | M12 #3673: the Character encoding rule accepts Identity-H/V fonts with Adobe CJK collections, or a predefined non-Identity CMap, while rejecting unknown names and foreign registries. Synthetic checker and read-only automation regressions passed after failing before the fix; per-glyph mapping validation and embedded-font extraction remain separate. Breadth, readiness and hours unchanged. | +| 2026-10-12 | minor | Web OCR (`ocr.ocr-web`) shipped after a browser check; scores not re-judged | +| 2026-10-12 | minor | Web OCR (`ocr.ocr-web`, partial): the web zip serves the models, Recognize Text downloads them on first use, and pages are read in Web Workers built with SIMD. Platforms text updated; scores not re-judged | | 2026-10-12 | minor | Exclusive image crops resize with their pixels across nested states, intervening operators and content streams; shared clips remain fixed. Synthetic engine/tool regressions and private image save/reopen checks; estimates unchanged. | | 2026-10-11 | minor | Web printing shipped (gap 4): Hardware, Platforms and L Print text updated. Text only: scores, readiness and hours not re-judged | | 2026-10-11 | minor | Contributor fork: document-font selection and subset completion, CFF, style ranges, contextual/bidi save/reopen and all-page Chinese keyboard acceptance; curve/hole hit testing (shared clips stay fixed, as on main). Scope remains bounded; percentages unchanged | diff --git a/docs/ui-parity.md b/docs/ui-parity.md index f950e36a94..a0ea857f1a 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI and interaction parity -> **Last reviewed:** 2026-10-21 · **Last updated:** 2026-10-21 · **Change:** minor (Pages inspector thumbnail size slider and buttons #3808; full screen presents one page at a time #1459; the selected field nudges with the arrow keys in Prepare a form #2608) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) +> **Last reviewed:** 2026-10-21 · **Last updated:** 2026-10-24 · **Change:** minor (→ / ← scroll a page wider than the window before turning it; a comment's text, border or line, and fill each at an opacity of its own) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) How PdfCraft's tools, panels, shortcuts and feel compare with Acrobat Pro's current interface (the 2023+ "new Acrobat": All tools pane on the left, quick-action toolbar, Home view). Behaviour is @@ -9,6 +9,8 @@ interface is copied. Gaps ranked in [gaps.md](gaps.md). **UI/UX fidelity ≈ 55% ready** (estimated), **30–60 h** additional to the features total. +The [PDF workflow research reference](research/pdf-workflows/README.md) collects comparative source locators, interaction evidence and evaluation criteria. It does not change this checklist. + ## Measured - **184 registered commands** (`crates/engine/src/commands.rs`), every one in the menus, the @@ -44,11 +46,12 @@ No dump of Acrobat's menu tree has been diffed against the registry yet (gap 24) | Zoom: wheel, pinch, keys, marquee, fit page/width/height/visible | yes | yes | shipped | | Fixed zoom percentages match Acrobat's physical size | yes | differ (#739, documented in #736) | gap | | Dynamic zoom, loupe, pan-and-zoom window | yes | no | planned | -| Hand tool, middle-button pan, autoscroll | yes | hand tool; Linux autoscroll | partial | +| Hand tool, middle-button pan, autoscroll | yes | hand tool; Linux autoscroll; → / ← scroll a page wider than the window before turning it | partial | | Text selection, column select | yes, Alt-drag columns | reading-order selection; Alt/Option-drag or Edit ▸ Column select (#740) | shipped | | Select and move several objects at once | yes | on one page: Shift/Ctrl/⌘-click and marquee, one undo step (#844); not across pages, no group resize | partial | | Moved comments drawn where they go while dragged | yes | yes: the comment's own pixels follow the pointer, the page shows it where it was until release; stamps and Fill & Sign marks also hide the original | shipped | | Object handles: corners keep proportion, edges free | yes | yes for added images and signatures; Shift on a corner keeps the proportions of comments, stamps and form fields (#2199); no Alt resize from the centre | shipped | +| Shift while drawing a line | holds it to 45° steps | holds it level or plumb (a CAD drawing's ortho): new lines and arrows, each side of connected lines, polygons and clouds, and a dragged end or corner | shipped | | Arrange, align and distribute page objects | yes | fields only | partial | | Rulers, grids, snap to grid, guides | yes | no | planned | | Cursor coordinates, line weights | yes | no | planned | @@ -75,13 +78,16 @@ No dump of Acrobat's menu tree has been diffed against the registry yet (gap 24) | Print dialog with preview | yes | yes | shipped | | Document Properties (description, security, fonts, initial view, custom, advanced) | yes | all but Custom and XMP | partial | | Field Properties (General, Appearance, Position, Options, Actions, Format, Validate, Calculate, Signed) | yes | all but Signed | partial | -| Comment properties and properties bar | yes | yes | shipped | +| Comment properties and properties bar | yes | yes; a text box's text, border and fill and a shape's line and fill each take an opacity of their own (Acrobat: one per comment); text boxes and typed text take an oval or a circle for their border (Acrobat: a rectangle) | shipped | +| Select several comments; copy and paste comments between documents | yes | yes: ⌘/Ctrl-click adds, Delete, a drag, Properties and Colour act on all, ⌘C/⌘V as XFDF (also across PdfCraft processes) | shipped | | Preferences | ≈ 20 pages | Interface language, Documents and view, Identity, Fill & Sign, JavaScript | partial (gap 19) | ## Revision history | Date | Change | Summary | |---|---|---| +| 2026-10-24 | minor | → / ← scroll a page wider than the window sideways before turning it, by the current page's or spread's edges in continuous view; ⌘→ / ⌘← turn at once | +| 2026-10-24 | minor | Comment Properties sets a text box's text, border and fill opacity and a shape's line and fill opacity each on its own (Acrobat: one per comment); Make Properties Default carries them into the tool | | 2026-10-21 | minor | Pages inspector thumbnail sizing (#3808), separate from page and Organize zoom; view setting retained on reload and exposed through UI control. Not persisted between sessions. | | 2026-10-15 | minor | A selected field nudges with the arrow keys in Prepare a form: a point, Shift ten points, one undo step per press (#2608) | | 2026-10-14 | minor | Full screen shows one fitted page (or spread) at a time on black with click and key paging, as Acrobat's Full Screen Mode (#379, #1459, #1353) | diff --git a/docs/windows-printing.md b/docs/windows-printing.md new file mode 100644 index 0000000000..e96566fc3d --- /dev/null +++ b/docs/windows-printing.md @@ -0,0 +1,25 @@ +# Windows print jobs rejected before submission + +A report on PdfCraft 0.19.0 described the preview closing without output while Edge printed the same PDF. On the investigated Windows machine, the HUAWEI CV81Z driver accepted the PrintTicket (paper, grayscale, duplex and collation), and its capabilities XML parsed successfully. + +The native submission test failed before queueing: `StorageFile::GetFileFromPathAsync` returned `0x80070005` for the temporary PDF. Ordinary desktop file access could read it. This isolates one reproducible failure; it does not establish the cause of every report of an empty print queue. + +The winprint loader uses `CreateRandomAccessStreamOnFile` with read-only access, then calls `PdfDocument::LoadFromStreamAsync`. The stream is backed by the job file and reads on demand. There is no whole-job `InMemoryRandomAccessStream` copy, `DataWriter`, transfer loop or per-chunk `StoreAsync().join()`. The path is made absolute and embedded NUL characters are rejected before calling the native API. The temporary file remains alive throughout rendering and submission through craft-print's existing lifetime management. PDF decoding and rendering can still allocate memory; the change removes the loader's full-file copy, not all printing memory use. + +Upstream loader: https://github.com/storytold/winprint.rs/pull/15 (`51934db3`). Dependency follow-up: https://github.com/storytold/craft-libs/pull/636 (`7ee35a9f`, includes merged #605). PdfCraft updates only its normal `craft-print` dependency. There is no global patch or personal-fork source. The loader revision descends from `64e7afd4`, preserving the multi-threaded Direct2D factory fix for #5006. Upstream reviews are still required before release. + +The public `craft_print::rgb` GDI transport remains pinned to main's `64e7afd4` revision. It is independent of the internal PDF-loader pin `51934db3`: retaining the public source identity avoids incompatible Rust types when a downstream application also uses that GDI revision directly. PdfCraft resolves one GDI package at that stable revision. + +## Evidence + +- Original Windows loader: synthetic A4 submission failed at PDF loading with `0x80070005`. The earlier memory-stream prototype submitted the same HUAWEI job; physical output was not independently confirmed. +- Final file-backed loader: five focused tests pass, covering a hidden PDF, read-only stream behavior, reading the final byte of a 256 MiB file, a missing file, and a path containing NUL. +- The hidden-file test was observed failing with the original StorageFile loader. The read-only test was observed failing with the earlier in-memory implementation and passing with the final loader. +- Isolated craft-print suite with the final loader and multi-threaded Direct2D source: 63 passed, 3 hardware tests ignored. These diagnostic tests are not a full build of the final dependency graph; the disabled PDFium downloader build dependencies were omitted locally. +- The file-backed loader at `b334ebbb` was subsequently submitted natively to the HUAWEI CV81Z; the reporter confirmed physical output of the synthetic A4 page. This used an isolated diagnostic workspace and writable TEMP directory, not the packaged UI. See https://github.com/storytold/winprint.rs/pull/15#issuecomment-6704470691. +- The current loader pin `51934db3` also includes the review repairs: standard-library NUL validation, GetPage(0) before document/file cleanup, RAII temporary-file cleanup, and the explicit WinRT feature. In a complete Windows checkout with normal build dependencies, five loader tests and 14 Print Schema tests passed; injected failures left no temporary files behind. +- Full final-graph build/quality gates must be rerun for the updated pins. Packaged-UI and wider printer/duplex qualification remain unverified; single-page output does not establish those. + +When reporting a different failure, include the PdfCraft version, printer/driver name, displayed error and whether a job appears in the Windows queue. Saving the print-ready PDF and printing it in another viewer remains a workaround until the fix ships. + +The loader pin also includes a bounded 30-second wait in the large-file native stream regression test. Production `PdfDocument::LoadFromStreamAsync` still has no dedicated timeout; the terminal print-completion timeout does not cover document loading. Current dependency-graph CI must pass before merging; earlier-head results do not qualify this update. diff --git a/docs/word-count.md b/docs/word-count.md index 4b3d02cba4..a211236075 100644 --- a/docs/word-count.md +++ b/docs/word-count.md @@ -25,8 +25,9 @@ Explicit Table/Code/Figure marked content still works there. Cyclic and oversize graphs are bounded and reported as incomplete. PDF `/ActualText` replacements are respected. If one replacement spans both included and -excluded glyphs, there is no reliable mapping of its words to the selected content: counting -fails with an explanation instead of silently returning the wrong total. Disable exclusions to +excluded glyphs (or, with no glyphs, included and excluded outlines or pictures), there is no +reliable mapping of its words to the selected content: counting fails with an explanation +instead of silently returning the wrong total. Disable exclusions to count that page. Unreadable pages likewise fail the operation, without a misleading partial total. Images alone contain no extractable words. Run Scan & OCR first when counting scanned pages. diff --git a/licenses/ICU4X-LICENSE.txt b/licenses/ICU4X-LICENSE.txt new file mode 100644 index 0000000000..c9be6012c5 --- /dev/null +++ b/licenses/ICU4X-LICENSE.txt @@ -0,0 +1,46 @@ +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2020-2024 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +— + +Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. +ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. diff --git a/packaging/web/README.md b/packaging/web/README.md index 7aaa49d6a5..a066cab94a 100644 --- a/packaging/web/README.md +++ b/packaging/web/README.md @@ -8,7 +8,9 @@ static site in `pdfcraft-web-/`: | `index.html` | The page. It loads everything through relative URLs. | | `pdfcraft-web-.js` | wasm-bindgen glue (generated, ES module) | | `pdfcraft-web-_bg.wasm` | The app, about 13 MB, or 5 MB with compression | +| `pdfcraft-ocr-worker-.js`, `_bg.wasm`, `_loader.js` | The text recognition worker (about 3 MB), started only when OCR runs | | `_headers`, `.htaccess` | Sample header rules for Netlify/Cloudflare Pages and Apache | +| `models/` | The text recognition (OCR) models, about 12 MB, with their licences (CC-BY-SA-4.0) | There is no server-side code. Upload the folder's contents anywhere that serves static files. @@ -55,6 +57,26 @@ location /pdfcraft/ { Local test: `python3 -m http.server 8765` inside the folder, then open http://localhost:8765/. +## Text recognition (OCR) + +Scan & OCR ▸ Recognize Text downloads `models/text-detection.onnx` and +`models/text-recognition.onnx`, relative to the page, the first time it is opened in a visit. +Keep `models/` beside `index.html`. The app loads only the files whose SHA-256 matches +`ATTRIBUTION.toml`, so a wrong file fails like a missing one. Their file names carry no content +hash, so the app asks the server before using a cached copy (`no-cache`): the server must answer +that check correctly (an ETag, or a `Last-Modified` that changes whenever the file does). Without +the models, everything else works and the Recognize Text dialog says the download failed. + +Pages are read in Web Workers (`pdfcraft-ocr-worker-_loader.js`, a module worker), so the +app keeps responding, and two pages are read at once on a machine with three or more cores. Each +worker needs about 600 MB while it reads; idle workers close after a minute. The workers need no +special headers, but a `Content-Security-Policy` must allow them (`worker-src 'self'`, or +`script-src 'self'`). Where they can't start, the app reads the pages itself, a page at a time, +and doesn't respond while each page is read. + +From a source build, `trunk build --release` doesn't fetch the models. Add them with +`cargo xtask models dist/web/models` (after the build, which replaces `dist/web`). + ## Embedding in a page (iframe) ```html @@ -78,6 +100,76 @@ Local test: `python3 -m http.server 8765` inside the folder, then open http://lo downloads) are blocked. - Don't send `X-Frame-Options: DENY` or a `frame-ancestors` CSP that excludes the embedding page. +## Same-document host mode (no iframe) + +A host application can also run PdfCraft from its own document: it loads the wasm-bindgen module +itself and gives PdfCraft one of its own canvases, so there is no iframe and no `?host=parent`. +It opts in with `data-host="page"` on that canvas — the exact value `page` — and the canvas +keeps the id the app looks up (`id="pdfcraft"`). + +The page and the app then talk with DOM `CustomEvent`s on the canvas instead of `postMessage`. +The app dispatches the same messages it would post to a parent — `pdfcraft:ready`, +`pdfcraft:save`, `pdfcraft:dirty`, `pdfcraft:failed` — with the message fields (without the +`type`) as the event's `detail`, and it opens a file when the page dispatches a `pdfcraft:open` +event with `detail: { name, bytes }`. Events do not bubble, so listen on the canvas itself. + +Add the listeners before `init`: the app announces itself with `pdfcraft:ready` while `init` is +still running, and a file sent in reply would otherwise be missed. + +```js +import init from "./pdfcraft-web-.js"; // the wasm-bindgen glue shipped with the app + +const canvas = document.getElementById("pdfcraft"); +canvas.dataset.host = "page"; // or + +canvas.addEventListener("pdfcraft:ready", () => { + // The app is listening; hand it a document now, if you have one. + canvas.dispatchEvent(new CustomEvent("pdfcraft:open", { + detail: { name: "handout.pdf", bytes: pdfBytes }, // ArrayBuffer or Uint8Array + })); +}); +canvas.addEventListener("pdfcraft:save", (event) => { + const { name, saveAs, bytes } = event.detail; // bytes is an ArrayBuffer + // Store the file under `name`; `saveAs` is true for Save As. Report your own errors. +}); +canvas.addEventListener("pdfcraft:dirty", (event) => { + unsaved = event.detail.dirty; // warn before leaving, as you like +}); +canvas.addEventListener("pdfcraft:failed", (event) => { + showError(event.detail.error); // the app couldn't start +}); + +await init({ module_or_path: "./pdfcraft-web-_bg.wasm" }); +``` + +- Saves are not downloads in this mode: the `bytes` in `pdfcraft:save` hold the saved PDF, and + the host decides where the file goes. +- `?author=` still signs the user's comments, as with `?host=parent`. +- An unframed page that uses `?host=parent` keeps the normal app with downloads. + `data-host="page"` turns this mode on only when `?host=parent` is absent: if `?host=parent` is + present it takes precedence, so a same-origin framed page uses the parent bridge, while an + unframed or cross-origin page gets no bridge and keeps normal downloads. + +The messages and both transports are the shared contract of [`craft-web-host`](https://github.com/storytold/craft-libs/tree/main/crates/web-host) (storytold/craft-libs#420), the same for every Craft app: its README lists every message, including `saved`, with the compatibility notes. In `pdfcraft:ready`, `version` is the contract's (`"1"`) and `appVersion` is this app's release. PdfCraft reads bytes, so a host sends `bytes`, not a `File`. + +### Trust and limits + +- **Who is trusted.** In page host mode the page and PdfCraft share one document, so any script on the page can drive the app: open files in it and receive what it saves. Use this mode only for a page you control. The iframe mode (`?host=parent`) answers only the parent window, on the page's own origin. +- **Files from the host are untrusted input.** An opened file goes through the same reading as one picked with File ▸ Open or dropped on the window, with the same format limits; before it is copied into the app's memory, its size is checked against the host open limit (512 MiB) and a larger file is refused, and a name is cut to 255 characters. +- **The event and message names are the interface.** `pdfcraft:ready`, `pdfcraft:open`, `pdfcraft:save`, `pdfcraft:dirty` and `pdfcraft:failed`, and the fields they carry (the same as the `?host=parent` messages), stay stable; changing one is a breaking change. + +### Driving it from the page (`window.pdfcraft`) + +`?control` on the page's URL puts `window.pdfcraft` on the page, from [`craft-web-api`](https://github.com/storytold/craft-libs/tree/main/crates/web-api) (storytold/craft-libs#413), the same shape in every Craft app (`index.html?control`, which can be combined with other flags). It is opt-in because the control channel keeps the accessibility tree of every frame. The browser already exposes PdfCraft's canvas (`id="pdfcraft"`) as `window.pdfcraft`, so the functions are added to that element. `pdfcraft.request(method, params)` calls any method of the UI control channel the desktop app serves with `--control` (`ui.state`, `ui.inspect`, `ui.click`, `ui.key`, `ui.set`…; `ui.open` needs a file system, so on the web it answers with an error and a file is dropped on the page instead), and `execute(command, params)` (`ui.command`: a registry command id), `commands()` (`ui.commands`), `docInspect(params)` (`ui.state`), `uiInspect(params)` (`ui.inspect`) and `screenshot(params)` are shortcuts for the common ones. Each returns a Promise that resolves with the method's result and rejects with an `Error`; a call the app does not answer within 30 seconds rejects, and the app may still reach it later and run it (or reject it), as it does for a desktop client that timed out, so a timed-out call is not guaranteed to have been skipped. + +```js +const state = await pdfcraft.docInspect(); // open documents, page, zoom, panels, dialog +await pdfcraft.request("ui.set", { key: "show_page_edge_shadows", value: "false" }); +const { data } = await pdfcraft.screenshot(); // the window as PNG (base64) +``` + +Opening a PDF, handing a saved or exported file to the page, the file name and the unsaved flag stay with the host messages above. + ## Renderer selection and fallback flags PdfCraft renders with wgpu. It uses **WebGPU** when the browser has it and falls back to @@ -92,3 +184,7 @@ PdfCraft renders with wgpu. It uses **WebGPU** when the browser has it and falls For example: `