diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 4d0549981..28bd17c7c 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -4,7 +4,7 @@ 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 SHA-256 hashes, is [ATTRIBUTION.toml](ATTRIBUTION.toml). Licence texts are kept beside the assets and summarised in [NOTICE](NOTICE). -## In this repository (260) +## In this repository (264) | Asset | Title | Author | Licence | Source | Used for | |---|---|---|---|---|---| @@ -17,6 +17,7 @@ Every asset PdfCraft includes, bundles or uses to build its published material, | `crates/ui-egui/src/i18n/de.tsv` | German interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | | `crates/ui-egui/src/i18n/te.tsv` | Telugu interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | | `crates/ui-egui/src/i18n/it.tsv` | Italian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Italian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | +| `crates/ui-egui/src/i18n/pl.tsv` | Polish interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Polish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | | `crates/ui-egui/src/i18n/ru.tsv` | Russian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | | `crates/ui-egui/src/i18n/hu.tsv` | Hungarian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | | `crates/ui-egui/src/i18n/bg.tsv` | Bulgarian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | @@ -134,6 +135,7 @@ Every asset PdfCraft includes, bundles or uses to build its published material, | `assets/icons/messages-square.svg` | Lucide icon "messages-square" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/messages-square.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/icons/mic.svg` | Lucide icon "mic" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/mic.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/icons/minimize.svg` | Lucide icon "minimize" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/minimize.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | +| `assets/icons/minimize-2.svg` | Lucide icon "minimize-2" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/minimize-2.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/icons/minus.svg` | Lucide icon "minus" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/minus.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/icons/moon.svg` | Lucide icon "moon" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/moon.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/icons/mouse-pointer-2.svg` | Lucide icon "mouse-pointer-2" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/mouse-pointer-2.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | @@ -230,6 +232,8 @@ Every asset PdfCraft includes, bundles or uses to build its published material, | `assets/app-icon/pdfcraft-1024.png` | PdfCraft app icon, 1024 px on Apple's icon grid | PdfCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py) | MIT OR Apache-2.0 | Contributor-original (see assets/app-icon/README.md) | Runtime window/Dock icon on macOS (apps/pdfcraft/src/main.rs) | | `assets/app-icon/pdfcraft.icns` | PdfCraft app icon (macOS .icns) | PdfCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py) | MIT OR Apache-2.0 | Contributor-original (see assets/app-icon/README.md) | macOS app bundle icon | | `assets/app-icon/pdfcraft.ico` | PdfCraft app icon (Windows .ico, 16-256 px) | PdfCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py) | MIT OR Apache-2.0 | Contributor-original (see assets/app-icon/README.md) | Embedded in pdfcraft.exe by apps/pdfcraft/build.rs (Explorer, taskbar, Start menu, Alt-Tab) | +| `assets/app-icon/pdfcraft-document.svg` | PdfCraft PDF document icon (master vector) | Lucide Icons and Contributors (document outline); PdfCraft contributors (PDF badge, lettering and composition) | ISC AND (MIT OR Apache-2.0) | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-text.svg); contributor-original composition (assets/app-icon/README.md) | Master artwork for the Windows PDF file type icon; rendered by packaging/icons.sh, without fonts or external images | +| `assets/app-icon/pdfcraft-document.ico` | PdfCraft PDF document icon (Windows .ico, 16-256 px) | Lucide Icons and Contributors (document outline); PdfCraft contributors (PDF badge, lettering and composition) | ISC AND (MIT OR Apache-2.0) | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-text.svg); contributor-original composition (assets/app-icon/README.md) | Embedded in pdfcraft.exe as resource 2; the Windows installer uses it for PdfCraft.Document and Applications/pdfcraft.exe PDF file associations | | `assets/app-icon/hicolor/16x16/apps/ai.storyteller.pdfcraft.png` | PdfCraft app icon, 16 px | PdfCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py) | MIT OR Apache-2.0 | Contributor-original (see assets/app-icon/README.md) | Linux hicolor theme icon (packaging/linux/ai.storyteller.pdfcraft.desktop) | | `assets/app-icon/hicolor/24x24/apps/ai.storyteller.pdfcraft.png` | PdfCraft app icon, 24 px | PdfCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py) | MIT OR Apache-2.0 | Contributor-original (see assets/app-icon/README.md) | Linux hicolor theme icon (packaging/linux/ai.storyteller.pdfcraft.desktop) | | `assets/app-icon/hicolor/32x32/apps/ai.storyteller.pdfcraft.png` | PdfCraft app icon, 32 px | PdfCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py) | MIT OR Apache-2.0 | Contributor-original (see assets/app-icon/README.md) | Linux hicolor theme icon (packaging/linux/ai.storyteller.pdfcraft.desktop) | diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index b9f908c90..17a90044d 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -19,7 +19,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "5aedd14b5c652b54db8bfb00d32657d690b4601618de1d574919ff8fec161694" +sha256 = "500ba36d4a37162d935a1b5e2b16eff5b2011c6aa9f9285a87d46807acdb97e6" [[asset]] path = "crates/ui-egui/src/i18n/zh-hant.tsv" @@ -30,7 +30,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "1cf01b46973e74f2df8d3269e6569cf1264ebcca5351a569bd494e9cfcf86b91" +sha256 = "57c35909b2f19f16ff0497d110abbf1c79a5627d2a98f95706e726c59348d09e" [[asset]] path = "crates/ui-egui/src/i18n/cs.tsv" @@ -41,7 +41,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "b243b1b0152d1236b99fc375fb42f5810d55786292d3b8d5d2393593a5789a5c" +sha256 = "abd8d0c409be846fcf48cbc1338def7f77f2d301bd84d863e4ad66434e5f821f" [[asset]] path = "crates/ui-egui/src/i18n/pt-br.tsv" @@ -52,7 +52,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "e0b6b7d1441501b929f9df6409edda88283d00a4343a0627acdaa126e549286a" +sha256 = "130059ef7014f1dfb59ec8dc2435657d5158abca2d1825959c6366288d28ea92" [[asset]] path = "crates/ui-egui/src/i18n/es.tsv" @@ -63,7 +63,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "3528e2b18c73c038b0ffc4b364155827ddede69a8cecd5aa0b20bbe409598738" +sha256 = "c67b49f2caa527fe5a241b1046f582b83f324ac86945319fd489971c038acd9b" [[asset]] path = "crates/ui-egui/src/i18n/fr.tsv" @@ -74,7 +74,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "36b08deba87a7cb9c52a678d37f4f289bb291a0a097615bc1a9a77f2d8b44b2a" +sha256 = "4e8e7f71dd896c58ad2bcad8d8c57ae8c9933611cff412985eb32845821f8899" [[asset]] path = "crates/ui-egui/src/i18n/de.tsv" @@ -85,7 +85,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "832a8f15f8402d0bc09a40937fd47feffe7afb002d41c288878c3920996fbd4f" +sha256 = "09db52c316a7e9312a30d80ce4e385f2c957096563c509b7b6ed2bcc95bf39d8" [[asset]] path = "crates/ui-egui/src/i18n/te.tsv" @@ -96,7 +96,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "c22c0153196e3e8f6a846e5a2f4210ffe393cc6997a32a4a8214bea222f98d56" +sha256 = "21767432503afac87196a87557637692dcec76c0eba02a1dcf6bd1294926107a" [[asset]] path = "crates/ui-egui/src/i18n/it.tsv" @@ -107,7 +107,18 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Italian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "2edbaa97d218efa890043f780268f74ee6d4b4b56512523bbd050f447ced37df" +sha256 = "58f9514a06c74a7cdf90c6f7fb86ebb092af39857633f0779b44f0867a1c96e0" + +[[asset]] +path = "crates/ui-egui/src/i18n/pl.tsv" +title = "Polish interface translation catalog" +author = "PdfCraft contributors" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +licence = "MIT OR Apache-2.0" +licence_file = "LICENSE-MIT" +kind = "translation" +usage = "Polish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" +sha256 = "7052f5ee37d26e0173bc5fe48411c94e1b48e31e0fc8a9188fca2bec54f29e32" [[asset]] path = "crates/ui-egui/src/i18n/ru.tsv" @@ -118,7 +129,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "64b352359b9b3919e5f04fa0fafa44f9cf1d3759f1829522a754bf391a20bd60" +sha256 = "e780ffd93f9606c1a762862a961c3a82bde948a8a5f116b46e6f78ba84ac6ff4" [[asset]] path = "crates/ui-egui/src/i18n/hu.tsv" @@ -129,7 +140,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "3afb05c1bfedaca890e3d79ff27640be200b2174d550ce0474103eb4c33b31fa" +sha256 = "ebbd6cd7bc586ca253047f7aa3b5c34cd9ecb29645afc3d9d2097c35277ffddc" [[asset]] path = "crates/ui-egui/src/i18n/bg.tsv" @@ -140,7 +151,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "a91e7153bc3208aafc7705601848666499ae2ac95dc4bd3b9a78ce749693c092" +sha256 = "b760879b720c1d6de0dc9286ab044bebe4360962dece93060d7041b8955be7b2" [[asset]] path = "crates/ui-egui/src/i18n/ja.tsv" @@ -151,7 +162,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "65927f653a4b33944f981ecef78c2bd81a7840924804063581bd09af94beee22" +sha256 = "b07366d224bd5a394f705c17fa5d26e4f45d8903927e5dc18ed12e7713751d0a" [[asset]] path = "crates/ui-egui/src/i18n/uk.tsv" @@ -162,7 +173,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "d96bb503b25afc2f584f00253b371e8bd0266fa277a0b631d537377742c272cb" +sha256 = "97c79be066c982347b48556a19b71e0db67a7f46932d1ce342b2ba5d29bf4582" [[asset]] path = "crates/ui-egui/src/i18n/ar.tsv" @@ -173,7 +184,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Arabic UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "f33971aa4e215933494fe181b83ee102f6e30178a83bfc944d35f7e7824d7692" +sha256 = "f5e369934bf2ceca49224bf57f72289f0c82fc4c81b7c6363676d39362750d17" [[asset]] path = "assets/icons/accessibility.svg" @@ -1396,6 +1407,17 @@ kind = "icon" usage = "UI icon, embedded via crates/ui-egui/src/icon_data.rs" sha256 = "fe917d6144a9aed9a5297cb28addb9e582ac591a8f1c498a55aecea9b5c1783e" +[[asset]] +path = "assets/icons/minimize-2.svg" +title = "Lucide icon \"minimize-2\"" +author = "Lucide Contributors (portions Cole Bemis, Feather)" +source = "https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/minimize-2.svg)" +licence = "ISC" +licence_file = "assets/icons/LICENSE-lucide.txt" +kind = "icon" +usage = "UI icon, embedded via crates/ui-egui/src/icon_data.rs" +sha256 = "1150437d5a870321c59b7ebca2e1bc3f6227f0fa03dd10484ab384dfe5110739" + [[asset]] path = "assets/icons/minus.svg" title = "Lucide icon \"minus\"" @@ -2786,6 +2808,28 @@ kind = "icon" usage = "Embedded in pdfcraft.exe by apps/pdfcraft/build.rs (Explorer, taskbar, Start menu, Alt-Tab)" sha256 = "56daaaae9ae5389794909570885809adf5e1d9183bef2d9669ed2f9bba3ed6f5" +[[asset]] +path = "assets/app-icon/pdfcraft-document.svg" +title = "PdfCraft PDF document icon (master vector)" +author = "Lucide Icons and Contributors (document outline); PdfCraft contributors (PDF badge, lettering and composition)" +source = "https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-text.svg); contributor-original composition (assets/app-icon/README.md)" +licence = "ISC AND (MIT OR Apache-2.0)" +licence_file = "assets/app-icon/LICENSE-document.txt" +kind = "icon" +usage = "Master artwork for the Windows PDF file type icon; rendered by packaging/icons.sh, without fonts or external images" +sha256 = "8da8278c2b89362be0a960879d453ff879d7544d6d58e47d5389574e8dfff77b" + +[[asset]] +path = "assets/app-icon/pdfcraft-document.ico" +title = "PdfCraft PDF document icon (Windows .ico, 16-256 px)" +author = "Lucide Icons and Contributors (document outline); PdfCraft contributors (PDF badge, lettering and composition)" +source = "https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-text.svg); contributor-original composition (assets/app-icon/README.md)" +licence = "ISC AND (MIT OR Apache-2.0)" +licence_file = "assets/app-icon/LICENSE-document.txt" +kind = "icon" +usage = "Embedded in pdfcraft.exe as resource 2; the Windows installer uses it for PdfCraft.Document and Applications/pdfcraft.exe PDF file associations" +sha256 = "ba3f9128b368fc6d17d376cf62dbdbc22455451c5c8c4779210d7a2a7409b9e6" + [[asset]] path = "assets/app-icon/hicolor/16x16/apps/ai.storyteller.pdfcraft.png" title = "PdfCraft app icon, 16 px" diff --git a/CLAUDE.md b/CLAUDE.md index ccad9e7bd..77d6445b4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,8 +3,8 @@ 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` holds the milestone estimates and progress; update its table and log at the end of every session. - - Read `ROADMAP.md` §Honest assessment and §Where we're lacking and where we're going before choosing work. They rank the gaps (own renderer, hardening, fidelity against Acrobat, editing existing content, Pro workflows, 1.0 polish); prefer them over new P2/P3 features, and keep both sections true when things change. +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 follow craftrules `standards/progress-docs.md`: status line, 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. diff --git a/Cargo.lock b/Cargo.lock index 1ca3786b2..40fc240e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3648,6 +3648,7 @@ dependencies = [ name = "pdfcraft-create" version = "0.5.0" dependencies = [ + "encoding_rs", "hayro-syntax", "image", "pdfcraft-cos", @@ -3823,6 +3824,9 @@ dependencies = [ [[package]] name = "pdfcraft-platform" version = "0.5.0" +dependencies = [ + "zbus", +] [[package]] name = "pdfcraft-preflight" diff --git a/NOTICE b/NOTICE index 62e047ec0..721847793 100644 --- a/NOTICE +++ b/NOTICE @@ -13,6 +13,10 @@ This product bundles third-party material under its own licenses, listed below. list (author, source, license) is in ATTRIBUTION.md. - Lucide icons (assets/icons/*.svg) — ISC License. See assets/icons/LICENSE-lucide.txt. + The Windows PDF file icon (assets/app-icon/pdfcraft-document.svg and .ico) adapts Lucide's + file-text outline and combines it with contributor-original artwork. See + assets/app-icon/LICENSE-document.txt (ISC AND (MIT OR Apache-2.0)), packaged as + LICENSE-document.txt beside the Windows executables. - Inter typeface (assets/fonts/Inter-*.ttf) — SIL Open Font License 1.1. See assets/fonts/OFL-Inter.txt. - JetBrains Mono typeface (assets/fonts/JetBrainsMono-*.ttf) — SIL Open Font License 1.1. See assets/fonts/OFL-JetBrainsMono.txt. - Dancing Script typeface (assets/fonts/DancingScript.ttf), Copyright 2016 The Dancing Script Project Authors — SIL Open Font License 1.1 (Reserved Font Name "Dancing Script"). See assets/fonts/OFL-DancingScript.txt. diff --git a/README.md b/README.md index 29f6636d6..2673229be 100644 --- a/README.md +++ b/README.md @@ -412,16 +412,16 @@ Logs, environment variables and other development notes are in [docs/development PdfCraft is young and moving fast. The aim is a workbench where you can view, organize, annotate, fill, sign and edit PDFs, at parity with Acrobat Pro. -**Where it stands (October 2026), honestly:** about half of Acrobat Pro's offline features are in (88% of the must-haves), but that is roughly a third of the work, because the hardest parts are still ahead. +**Where it stands (October 2026), honestly:** PdfCraft is **alpha**. Most of Acrobat Pro's offline features exist in some form (57% of all tracked features, 94% of the must-haves). For everyday work (viewing, commenting, filling and signing forms, organizing pages) it is about 55% of the way to replacing Acrobat; against all of Acrobat Pro, about 49%; for someone who only needs the essentials, about 66%. Every everyday workflow can be completed, but converting and sharing is weak: password-protected files Acrobat can't open, no Office-to-PDF, and Word export that loses layout. - **Good today:** viewing and search; organizing, combining and splitting; most kinds of comment; filling and authoring forms (with sandboxed JavaScript); passwords, redaction and sanitizing; basic digital signatures; printing; the Accessibility Checker; agent control through the CLI and MCP. - **Still borrowed:** pages are drawn by the `hayro` crate while our own renderer is built. -- **Thin or missing:** reliable editing of existing text (especially CJK), OCR beyond Latin script, Office import/export, signature timestamps and long-term validation, PDF/A/X/UA preflight, XFA forms and localization. +- **Thin or missing:** reliable editing of existing text (especially CJK and right-to-left), OCR beyond Latin script, Office import/export, signature tokens and online long-term validation, Preflight and PDF/X/UA, printing on Windows, and full XFA forms. - **Hardening:** fuzzing still turns up crashes and hangs on hostile files; each one is fixed with a regression test. Quality has not yet been compared with Acrobat side by side. -**Next, in order:** our own renderer, hardening and a fidelity harness against Acrobat, editing existing content, then the Pro workflows (signatures, OCR, Office, preflight, XFA) and 1.0 polish. +**Next, in order:** encrypted files that Acrobat opens, the reported bugs and Windows printing, a fidelity harness against Acrobat and the renderer, editing existing content, then the Pro workflows (OCR, Office, Preflight, signatures, accessibility tagging) and 1.0 polish. -The honest assessment by area, what's lacking and where we're going are in **[ROADMAP.md](ROADMAP.md#honest-assessment-2026-10-05)**, with the full plan, progress and estimates. +The summary is in **[ROADMAP.md](ROADMAP.md)**; the full assessment by dimension and area is in [docs/target-app-parity.md](docs/target-app-parity.md), the ranked gaps in [docs/gaps.md](docs/gaps.md), and the milestones in [docs/roadmap.md](docs/roadmap.md). --- diff --git a/ROADMAP.md b/ROADMAP.md index 62b107043..452c410b0 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,160 +1,175 @@ # PdfCraft roadmap -The milestones, current progress and time estimates to Acrobat Pro feature parity. Keep this up to date: -- **Every session:** update the progress column and add a line to the log. -- **Every milestone:** re-estimate. +**Stage: alpha** · next: beta, ~26% (ready for real work 49% → 75%) and ~400–700 h away -Detailed task lists and acceptance tests are in `plan/execution-plan.md` (local-only). This file is the public summary. +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (localized shortcut labels, #496; readiness table with hours per audience; full number as weighted sum 49%; earlier: second look: ready 40% → 47%, mainstream 55% added; core-workflow gate applied → alpha) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) -**What "parity" means here:** the offline feature set of Acrobat Pro, milestones M0–M14. It excludes Adobe's cloud services (Document Cloud storage, Adobe Sign, the Adobe AI Assistant). Those have no clean-room equivalent; PdfCraft's alternatives are local-first, plus opt-in providers (M13). +PdfCraft's one-page summary: how close it is to Adobe Acrobat Pro's offline feature set, and what +comes next. Adobe's cloud services (Document Cloud, Acrobat Sign requests, shared review, the AI +Assistant) are out of scope; PdfCraft's alternatives are local-first plus opt-in providers. -## Estimate summary +| Document | Holds | +|---|---| +| [docs/target-app-parity.md](docs/target-app-parity.md) | The authoritative assessment: method, weights, every dimension and area, stage, calibration | +| [docs/gaps.md](docs/gaps.md) | Every known shortfall, ranked: the work list | +| [docs/roadmap.md](docs/roadmap.md) | Milestones M0–M14, current focus, upcoming milestones, risks | +| [docs/pdf-spec-parity.md](docs/pdf-spec-parity.md) | ISO 32000-2 coverage, annotations, actions, signatures, PDF/A/X/UA | +| [docs/file-format-parity.md](docs/file-format-parity.md) | Every format Acrobat reads or writes, create/export conversions, OCR | +| [docs/ui-parity.md](docs/ui-parity.md) | Tools, handles, shortcuts, panels, preferences | +| [docs/hardware-parity.md](docs/hardware-parity.md) | GPU, printers, scanners, smart cards, pen, platforms | +| [docs/localization-parity.md](docs/localization-parity.md) | Per-language status | +| [docs/architecture.md](docs/architecture.md) | How PdfCraft is built | +| `parity/acrobat-features.toml` | The machine-checked checklist (`cargo xtask parity`) | -The unit is **wall-clock hours of agent work** (Claude Opus 5.5 coding continuously; human review time not included). +**Keep it current:** every session, update the milestone row in [docs/roadmap.md](docs/roadmap.md) +and add a line to the [progress log](#progress-log); when a gap closes, remove it from +[docs/gaps.md](docs/gaps.md); re-measure fully when a major arc lands, at least monthly. -**Where we are (2026-10-07, measured by `cargo xtask parity` over 806 tracked Acrobat Pro features; 23 more are Adobe-cloud-only and out of scope):** +## Headline numbers -| Tier | Features | Shipped | Partial | Shipped % | Weighted % (partial = ½) | -|---|---|---|---|---|---| -| P0 (must-have for 1.0) | 251 | 222 | 25 | 88.4% | 93.4% | -| P1 | 326 | 174 | 38 | 53.4% | 59.2% | -| P2 | 186 | 14 | 7 | 7.5% | 9.4% | -| P3 | 43 | 1 | 0 | 2.3% | 2.3% | -| **All** | **806** | **411** | **70** | **51.0%** | **55.3%** | - -**Effort-weighted parity: ≈ 30–35%.** Feature counts overstate progress: the remaining features include the hardest ones (our own renderer and font engine, editing existing text and reflow, OCR beyond Latin, XFA, PDF/A/X/UA preflight, Office export, long-term signature validation). Weighting each milestone by its estimated size gives about a third of the total work done. See **[Honest assessment](#honest-assessment-2026-10-05)** for what the numbers don't show. - -**Observed throughput:** about 72k lines of kept, tested Rust (plus 97 agent tools and ≈ 600 tests) in roughly 65–75 agent-hours since 2026-09-30, about 1.0k lines per agent-hour, with corpus, oracle and visual checks. That is faster than the original plan assumed, so the hour estimates below are revised down from the first plan (2,000–4,000 h). - -| Scenario | Remaining hours to full parity (M0–M14) | Calendar time | +| | Value | Kind | |---|---|---| -| One Opus 5.5 agent, continuous | **≈ 600–1,100 h** | ≈ 4–7 weeks | -| 3–5 agents in parallel on separate crates | **≈ 250–450 h wall clock** | ≈ 2–3 weeks | -| With human review, integration and pauses | — | ≈ 2–4 months | +| **Feature breadth** | **≈ 67%** tier-weighted (56.9% unweighted: 423 shipped + 74 partial of 808; P0 94.2%) | measured, `parity/acrobat-features.toml` | +| Feature breadth, mainstream practitioner | ≈ 70% tier-weighted (63.2% unweighted; P0 95.3%) | measured ([split](docs/target-app-parity.md#mainstream-practitioner-vs-full-acrobat-pro)) | +| **Ready for real work**, full Acrobat Pro | **≈ 49%** (45–53%) | estimated, weighted sum over [dimensions](#by-dimension) | +| **Ready for real work**, mainstream practitioner | **≈ 55%** (50–60%) | estimated ([split](docs/target-app-parity.md#mainstream-practitioner-vs-full-acrobat-pro)) | +| **Ready for real work**, essentials user | **≈ 66%** (60–70%) | estimated ([method](docs/target-app-parity.md#essentials-user)) | +| Remaining to **beta** | ≈ 400–700 Opus 5.5 agent-hours | estimated | +| Remaining to **full parity** | ≈ 780–1,400 Opus 5.5 agent-hours (≈ 300–550 h wall clock with 3–5 agents) | estimated, calibrated on this repo's history | -**How the estimate is built:** each milestone's remaining fraction (table below) times its size, re-based on the measured rate. The long poles are M2 (own renderer, ≈ 150–250 h), M7 (editing existing text and images, ≈ 150–250 h), M10 OCR and Office export (≈ 100–150 h), M6 JavaScript engine (≈ 60–100 h), M11 PDF/A/X/UA preflight (≈ 80–140 h) and M12 XFA/compare (≈ 80–140 h). The last 5–10% (odd real-world files, pixel-level polish against Acrobat) costs about as much as a mid-sized milestone. +**Why alpha:** all six of Acrobat Pro's everyday workflows can be completed end to end on macOS +(the [alpha gate](docs/roadmap.md#alpha-gate)), and users praise them (7.6k stars, ≈ 217k downloads +of 0.4–0.5, switching reports such as #179). Two are only partial: editing (no CJK/RTL) and convert +and share (password-protected files we write don't open in Acrobat or Reader, #774; no in-app +Office→PDF; Word export loses layout, #773). Those, unmeasured quality against Acrobat, the borrowed +`hayro` renderer, no printing on Windows (#756) and 245 open issues keep it far from beta. -## Honest assessment (2026-10-05) +### Readiness by audience -Read this before choosing work. The feature table above counts what exists; this section says how solid it is. +| Audience | Ready | Opus 5.5 agent-hours to ≈ 95% | Work that dominates | +|---|---|---|---| +| Full Acrobat Pro (ready for real work) | ≈ 49% | ≈ 780–1,400 h (≈ 300–550 h wall clock with 3–5 agents; 60–70% parallelizes) | Own renderer and fidelity harness, editing existing content, OCR and Office conversion, Preflight and PDF/A/X/UA, accessibility tagging, signatures for institutions | +| Mainstream practitioner | ≈ 55% | ≈ 500–900 h (≈ 200–400 h wall clock; ≈ 60% parallelizes) | Rendering fidelity, editing existing text (CJK, RTL, subsets), conversion (Word layout, Excel, PowerPoint, Office→PDF), OCR languages, Windows printing, the bug backlog | +| Essentials user | ≈ 66% | ≈ 180–330 h (≈ 90–180 h wall clock; ≈ 50% parallelizes) | Rendering fidelity on everyday files, Windows printing, launch and GPU stability, encrypted output Acrobat opens (#774), simple text fixes, Word export layout, Chinese UI glyphs | -| Dimension | State | In one line | +Hours are calibrated as in [Effort and calibration](docs/target-app-parity.md#effort-and-calibration) (≈ 1k kept lines per agent-hour for new features, 3–10× slower for hardening and fidelity). The audiences are nested subsets, so essentials ≤ mainstream ≤ full. + +## By dimension + +| Dimension | Ready | Weight | Remaining (h) | Doc | +|---|---|---|---|---| +| Features (14 areas) | ≈ 49% depth · 67% breadth | 40% | 610–1,100 | [target-app-parity.md](docs/target-app-parity.md#by-feature-area) | +| UI/UX fidelity | ≈ 55% | 10% | 30–60 | [ui-parity.md](docs/ui-parity.md) | +| File formats and conversion | ≈ 40% | 12% | 120–220 (within features) | [file-format-parity.md](docs/file-format-parity.md) | +| PDF specification and standards | ≈ 60% | 8% | 150–260 (within features) | [pdf-spec-parity.md](docs/pdf-spec-parity.md) | +| Hardware | ≈ 35% | 4% | 25–50 | [hardware-parity.md](docs/hardware-parity.md) | +| Localization | ≈ 59% | 5% | 35–60 | [localization-parity.md](docs/localization-parity.md) | +| Performance | ≈ 40% | 6% | 25–45 | [gaps.md](docs/gaps.md) gap 13 | +| Stability | ≈ 50% | 8% | 45–80 | [gaps.md](docs/gaps.md) gap 6 | +| Platforms | ≈ 70% | 5% | 15–30 | [hardware-parity.md](docs/hardware-parity.md#platforms) | +| Ecosystem and plug-ins | ≈ 30% | 1% | 10–20 (within features) | [gaps.md](docs/gaps.md) | +| AI features | ≈ 0% (in scope: local providers) | 1% | 20–40 (within features) | [gaps.md](docs/gaps.md) gap 23 | +| **Total (weighted sum)** | **≈ 49%** | 100% | **≈ 780–1,400** | | + +## Features + +| Area | Breadth (measured) | Ready (estimated) | Remaining (h) | +|---|---|---|---| +| A Core model and fidelity | 76.8% | 70% | 25–45 | +| B View and navigation | 62.5% | 55% | 110–190 | +| C Content editing | 53.3% | 30% | 100–180 | +| D Organize pages | 61.2% | 70% | 12–25 | +| E Comments and review | 77.7% | 70% | 12–25 | +| F Forms and JavaScript | 78.5% | 65% | 40–75 | +| G Protect, redact, sanitize | 58.5% | 55% | 15–30 | +| H Digital signatures | 62.2% | 45% | 40–70 | +| I Scan and OCR | 23.1% | 15% | 40–75 | +| J Create and export | 39.7% | 25% | 55–100 | +| K Optimize and standards | 34.5% | 20% | 65–115 | +| L Print | 39.7% | 35% | 30–55 | +| M Accessibility | 63.1% | 30% | 40–70 | +| N Automation, preferences, misc | 30.0% | 35% | 25–45 | + +What's strong and missing in each: [target-app-parity.md](docs/target-app-parity.md#by-feature-area). + +## Languages + +Measured from `crates/ui-egui/src/i18n/*.tsv` (2,143 keys). 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 (no Hans UI face in releases, #826) | +| Spanish | `es` | 95.5% | partial | +| Hindi | `hi` | 0% | none | +| Arabic | `ar` | 100% | partial (layout not mirrored) | +| French | `fr` | 99.3% | partial | +| Portuguese (Brazil) | `pt-br` | 99.4% | partial | +| Indonesian | `id` | 0% | none | +| Japanese | `ja` | 99.1% | partial | +| German | `de` | 99.3% | partial | +| Korean | `ko` | 0% | none | +| Vietnamese | `vi` | 0% | none | + +Also shipped: Traditional Chinese, Russian, Bulgarian, Telugu, Hungarian, Ukrainian, Italian +(94–100%) and Czech (menus only): **8 more languages**. + +## Upcoming + +Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md). + +| Rank | Milestone | Estimate (h) | |---|---|---| -| Feature count | 51.0% shipped (P0 88%, P1 53%, P2 8%, P3 2%) | A typical viewer, annotator, form-filler or page organizer is mostly covered | -| Effort | ≈ 30–35% | The remaining work is the hardest: M2 15%, M7 17%, M11 18%, M12 23%, M14 0% | -| Foundations | Weakest | Rendering is still the bootstrap `hayro`; the local #307 candidate reads inspector data lazily through COS with `lopdf` compatibility logic; vendored patches are still carried | -| Robustness | Early beta | Each 15-minute fuzz run found new out-of-memory crashes or hangs until 2026-10-05 | -| Acrobat fidelity | Unmeasured | No systematic side-by-side comparison with Acrobat, visual or behavioural | -| Product readiness | 0.2.1 | No code signing, performance budgets or keyboard-only operation yet; localization has its catalog system (English, partial Japanese) but few strings go through it | +| 0 | Convert and share: encrypted output opens in Acrobat (#774), Create PDF from Office, Word export that keeps layout (#773) | 40–70 | +| 1 | Backlog and platform: crash and wrong-result reports, Windows printing (#756), Chinese UI face (#826) | 65–120 | +| 2 | Fidelity harness against Acrobat and the renderer decision (M2; owner decision on #841) | 110–190 | +| 3 | Editing existing content (M7) | 100–180 | +| 4 | OCR languages and Office export/import (M10) | 95–175 | +| 5 | Preflight and PDF/A/X/UA (M11) | 65–115 | +| 6 | Signatures for institutions: PKCS #11, timestamp servers, online LTV (M9) | 40–70 | +| 7 | Accessibility tagging (M12) | 40–70 | +| 8 | 1.0 polish: performance, right-to-left, missing languages, keyboard-only (M14) | 55–95 | -**By area** (shipped share of each area's features): +## Progress log -| Area | Shipped | What's strong / what's missing | -|---|---|---| -| A Core | 75% | Parser, repair, encryption, incremental saves. Missing: lazy loading (`ByteSource`), own image codecs, PDF 2.0 extras, Arlington validation | -| F Forms | 71% | Filling, authoring, AF scripts, sandboxed JavaScript, data exchange. Missing: XFA, wider JavaScript object model | -| E Comments | 71% | Markup types with appearances, XFDF/FDF, summaries, calibrated 2D measuring (single-unit decimal scales). Missing: replace-text proposals, summary layouts, compound/fractional measurement formats | -| M Accessibility | 62% | Checker (all 32 rules). Missing: autotag, Tags/Order/Content panels, Reading Order tool, keyboard-only operation | -| B View | 55% | Shell, find, panels, tiles, web build. **Rendering is borrowed (`hayro`)**, so the 7 rendering P0s are only partial | -| D Organize | 55% | Pages, combine, split, bookmarks, labels. Missing: replace pages, transitions | -| G Protect | 54% | Passwords, permissions, redaction (including word lists), sanitize. Redaction codes (built-in FOIA and Privacy Act sets). Missing: certificate security, custom redaction code sets | -| H Sign | 48% | PAdES B-B signing and validation; macOS Keychain and Windows CNG certificate store signing. Missing: timestamps (B-T), LTV (DSS/OCSP/CRL), FieldMDP, smart cards, PKCS #11 | -| C Edit | 46% | Added text and images stay editable; header/footer/watermark. Missing: robust editing of existing text and images (fonts, subsets, reflow) | -| L Print | 36% | Acrobat-style sizing, n-up, booklet, CUPS. Missing: Windows and web printing, production options | -| J Create | 32% | From images, text, clipboard; Word/HTML/RTF export. Missing: Office import, Excel/PowerPoint export | -| N Misc | 27% | CLI, MCP, UI control channel, Action Wizard. Missing: AI providers, performance budgets | -| K Optimize | 26% | Reduce File Size, Optimizer. Missing: preflight, PDF/X/UA, transparency/fonts panels | -| I OCR | 19% | Searchable image for Latin script. Missing: other scripts and accents, editable-text output, deskew | +Newest first: the date, what moved, and the new overall estimate. -**What "shipped" means, and doesn't.** A feature is shipped when it exists and at least one specific test covers it. 159 of 398 shipped features rest on exactly one test, and only about 5 cite an external oracle (`pdftotext`, `pdfsig`, OpenSSL, a corpus). Shipped does not mean "as good as Acrobat". +- **2026-10-10 (M14, interface size):** Preferences ▸ Interface size scales the whole interface (50–250 %, setting `ui_scale`, control option `ui-scale`); 50 % brings an interface the system draws at 200 % back to 100 % (#456). Auto, the default, follows the desktop's text scaling: on Linux and the BSDs the desktop app reads GNOME's `text-scaling-factor` once at launch, so with Large Text (1.25) on a 1× display the interface no longer draws 13 px labels that show their pixels next to the system's text. The size is egui's zoom factor, so `pixels_per_point` grows with it and page rasters stay one texel per screen pixel; egui's own ⌘+/⌘− interface zoom is turned off so those keys stay page zoom. Still missing: KDE's font DPI and Windows' "Make text bigger" aren't read (both scale only text there), and egui's text is still greyscale-antialiased without the system's gamma, so small text looks rougher than GTK's at 100 %. +- **2026-10-10 (M14, #496):** Shortcut displays share a UI formatter across toolbar search, registry menus, command palette, Help and tooltips. Modifier and key names use a dedicated catalog context in every bundled translation; missing entries keep the English key name rather than a command translation. German shows Strg/Umschalt/Entf, macOS retains its symbols, and runtime language changes refresh the display. Regression coverage checks localized layout widths, English/German switching, platform conventions and unchanged command execution. Milestone and effort estimates unchanged. -**Robustness, measured.** `main`'s CI was red for 30+ runs until 2026-10-04 (a clippy lint, Windows line endings, flaky perf tests). Once the nightly fuzz job could finish, three 15-minute runs found 7 out-of-memory crashes (one aborted on every platform) and 11 hangs, 7 of them in our own `cos` parser rather than `hayro`. All are fixed or in review, but expect more: keep the nightly fuzz job green and turn every finding into a synthetic test. +- **2026-10-10 (M5, shape fill colour, #686):** Rectangles, ovals, polygons and clouds can be filled. The comment quick bar has a Fill colour control for those tools ("No fill" or a swatch; it persists with the other per-tool styles), Comment Properties ▸ Appearance has a Fill colour row (one undo step with the other changes, carried over by Make Current Properties Default), and `comment_edit` takes `fill` (`"none"` removes it). `pdfcraft-annot::set_fill` writes or removes `/IC` and redraws the appearance; lines, ink and locked comments refuse it. Still missing: a custom colour picker (only the comment palette), and a separate fill for line endings. +- **2026-10-10 (M10, #645):** Export all images and Save image as keep transparency. A JPEG with a soft mask (the usual cut-out photo) was written as the bare JPEG, and soft masks of another size than the image, JPEG-compressed soft masks, stencil `/Mask` images and colour-key `/Mask` arrays were dropped, so cut-outs came out on solid white or black. These images are now PNG with the mask as alpha (soft masks scaled to the image, `/Matte` un-blended); JPEGs without transparency are still written byte for byte. A negative image size no longer overflows in Save image as. JPX/JBIG2/CCITT masks are still left out. Overall estimate unchanged (about 30–35%). +- **2026-10-10 (M12, XFA rows and areas):** Dynamic XFA forms lay out a repeating subform inside a top-to-bottom container in positioned content (a table in an area) with all its instances, drawn and measured, instead of one; and areas no longer appear in data paths (`pdfcraft_xfa::data_path`), since they are not data scopes. Before, a form whose fields sit in an area read none of the values Adobe's viewers saved, wrote what was filled here under a new element named after the area, which Adobe's viewers would most likely not read, and a FormCalc total looping over the table's rows failed on the rows that were missing. Layout, the script tree, reading and writing the data and the engine's value and row operations share the one path rule. Synthetic tests cover the layout, the path rule and the whole round trip (rows, Adobe-written values, a FormCalc total over all rows, the written data, reopening). Still open for such forms: XFA script objects (` .to_string() } +/// Shaped like a real Designer leave-request form: a positioned page whose unnamed subform holds +/// an area (`box`) with a top-to-bottom table of positioned rows (``) and a +/// read-only total that a FormCalc calculate works out over a fixed range of rows, as such forms +/// do. `data` is `xfa:data` content as Adobe's viewers write it: areas are not data scopes, so +/// there is no `box` element. +pub fn leave_template(data: &str) -> String { + let datasets = if data.is_empty() { + String::new() + } else { + format!("{data}") + }; + format!( + r##" + + +{datasets} + +"## + ) +} + /// A 2 × 2 RGBA PNG: red, green / blue, transparent. pub fn tiny_png() -> Vec { TINY_PNG.to_vec() diff --git a/crates/xfa/src/layout.rs b/crates/xfa/src/layout.rs index fa5011361..615afbb0e 100644 --- a/crates/xfa/src/layout.rs +++ b/crates/xfa/src/layout.rs @@ -13,7 +13,7 @@ use std::sync::Arc; use base64::Engine; use crate::XfaError; -use crate::data::{DataNode, is_on, iso_to_pattern, som_to_path}; +use crate::data::{DataNode, data_path, is_on, iso_to_pattern}; use crate::model::*; use crate::text::{self, Block, Face, Span}; @@ -508,11 +508,7 @@ impl Layouter<'_> { self.new_page()?; } // Data adds instances of a repeating subform (rows added in another viewer). - let from_data = match (&sf.common.name, self.data) { - (Some(name), Some(d)) if sf.occur.max != Some(1) => d.count(&som_to_path(&ctx.som), name), - _ => 0, - }; - let instances = instance_count(&sf.occur, from_data); + let instances = self.instances(node, &ctx.som); let ctx_parent = ctx; for i in 0..instances { let ctx = ctx_parent.with_som(&sf.common, sib + i); @@ -583,11 +579,7 @@ impl Layouter<'_> { && row.layout == Layout::Row { // Rows added as data (in another viewer) repeat the row. - let from_data = match (&row.common.name, self.data) { - (Some(name), Some(d)) if row.occur.max != Some(1) => d.count(&som_to_path(&ctx.som), name), - _ => 0, - }; - let instances = instance_count(&row.occur, from_data); + let instances = self.instances(child, &ctx.som); for inst in 0..instances { let h = self.row_height(row, &cols, inner_w, ctx, depth + 1); if self.make_room(h)? @@ -626,6 +618,17 @@ impl Layouter<'_> { /// The size a node takes when given `avail_w` of width. Containers are measured once per /// (node, width, depth): measuring a width-less container measures its content twice (for /// its width, then its height), which would otherwise be exponential in the nesting. + /// How many instances of `node` to lay out under the container at `parent_som`: a + /// repeating subform's `occur` and data instances ([`instance_count`]); one for anything else. + fn instances(&self, node: &Node, parent_som: &str) -> usize { + let Node::Subform(sf) = node else { return 1 }; + let from_data = match (&sf.common.name, self.data) { + (Some(name), Some(d)) if sf.occur.max != Some(1) => d.count(&data_path(self.tpl, parent_som), name), + _ => 0, + }; + instance_count(&sf.occur, from_data) + } + fn size_of(&self, node: &Node, avail_w: f64, ctx: &Ctx, depth: usize) -> (f64, f64) { let c = node.common(); let spent = self.measures.get().saturating_add(1); @@ -659,6 +662,9 @@ impl Layouter<'_> { fn measure(&self, node: &Node, avail_w: f64, ctx: &Ctx, depth: usize) -> (f64, f64) { let c = node.common(); + // The container's own SOM, under which its repeating children count their instances. + // Measurements are cached per template node, so instance 0 stands for all of them. + let som = ctx.with_som(c, 0).som; let ctx = ctx.child(c); // A container without a width is as wide as its content (a radio group beside its // question, say); a leaf without one takes what is available. @@ -684,9 +690,13 @@ impl Layouter<'_> { Node::Area(a) => self.positioned_height(&a.children, inner_w, &ctx, depth), Node::Subform(s) => match s.layout { Layout::Positioned => self.positioned_height(&s.children, inner_w, &ctx, depth), - Layout::Tb => { - s.children.iter().filter(|k| k.common().presence.occupies()).map(|k| self.size_of(k, inner_w, &ctx, depth + 1).1).sum() - } + // Each instance of a repeating child takes its height again. + Layout::Tb => s + .children + .iter() + .filter(|k| k.common().presence.occupies()) + .map(|k| self.size_of(k, inner_w, &ctx, depth + 1).1 * self.instances(k, &som) as f64) + .sum(), Layout::LrTb | Layout::RlTb => self.packed_height(&s.children, inner_w, &ctx, depth), Layout::Table => { let cols = self.columns(s, inner_w); @@ -943,8 +953,12 @@ impl Layouter<'_> { continue; } let (w, h) = self.size_of(k, inner.w, &ctx, depth + 1); - self.draw_node(k, Rect::new(inner.x, y, w, h), &ctx, depth + 1, sib_index(&s.children, i))?; - y += h; + // A repeating subform here (a table in an area of a positioned page, + // say) gets its instances, as in a flowed one, one under another. + for instance in 0..self.instances(k, &ctx.som) { + self.draw_node(k, Rect::new(inner.x, y, w, h), &ctx, depth + 1, sib_index(&s.children, i) + instance)?; + y += h; + } } } Layout::LrTb | Layout::RlTb => { @@ -1061,7 +1075,7 @@ impl Layouter<'_> { let ctx = ctx.with_som(&g.common, sib); // The group's data node holds the selected button's on value. let selected: Option = - self.data.and_then(|d| d.text_at(&som_to_path(&ctx.som))).map(|t| t.trim().to_string()).filter(|t| !t.is_empty()); + self.data.and_then(|d| d.text_at(&data_path(self.tpl, &ctx.som))).map(|t| t.trim().to_string()).filter(|t| !t.is_empty()); let inner = rect.inset(&g.common.margin); let group = self.unique_name(g.common.name.as_deref().unwrap_or("group")); let nodes: Vec = g.fields.iter().map(|f| Node::Field(Box::new(f.clone()))).collect(); @@ -1182,7 +1196,7 @@ impl Layouter<'_> { let field_name = f.common.name.clone().unwrap_or_else(|| "an image field".into()); let text: Vec = self .data - .and_then(|d| d.text_at(&som_to_path(&som_path()))) + .and_then(|d| d.text_at(&data_path(self.tpl, &som_path()))) .map(|t| t.bytes().filter(|b| !b.is_ascii_whitespace()).collect()) .unwrap_or_default(); let from_data = if text.is_empty() { @@ -1215,8 +1229,12 @@ impl Layouter<'_> { Ui::Barcode => { // The value as text, where the bars would be. self.warn_once("barcode fields show their value as text, not as bars"); - let value = - self.data.and_then(|d| d.text_at(&som_to_path(&som_path()))).map(str::to_string).or_else(|| f.value.plain()).unwrap_or_default(); + let value = self + .data + .and_then(|d| d.text_at(&data_path(self.tpl, &som_path()))) + .map(str::to_string) + .or_else(|| f.value.plain()) + .unwrap_or_default(); if !value.trim().is_empty() { let mut para = fctx.para.clone(); para.h_align = HAlign::Center; @@ -1244,7 +1262,7 @@ impl Layouter<'_> { let read_only = matches!(f.access, Access::ReadOnly | Access::Protected); // The data's value wins over the template's default. let data_value: Option = - self.data.and_then(|d| d.value_at(&som_to_path(&som))).map(|t| t.trim().to_string()).filter(|t| !t.is_empty()); + self.data.and_then(|d| d.value_at(&data_path(self.tpl, &som))).map(|t| t.trim().to_string()).filter(|t| !t.is_empty()); // A choice list's default is a saved value too. let template_default = f.value.plain().map(|v| f.saved_value(v.trim())).filter(|v| !v.is_empty()); let plain_value = data_value.clone().or_else(|| template_default.clone()); diff --git a/crates/xfa/src/lib.rs b/crates/xfa/src/lib.rs index 6b4e31d4e..a1758ab47 100644 --- a/crates/xfa/src/lib.rs +++ b/crates/xfa/src/lib.rs @@ -21,8 +21,9 @@ pub mod text; use pdfcraft_cos::Document; pub use data::{ - DataNode, DataOp, DataPath, DatasetsWrite, FieldData, FieldDatum, add_data_instances, build_data, iso_to_pattern, parse_datasets, pattern_to_iso, - read_values, remove_data_instance, som_to_path, write_data_ops, write_data_value, write_datasets, write_datasets_reusing, + DataNode, DataOp, DataPath, DatasetsWrite, FieldData, FieldDatum, add_data_instances, build_data, data_path, iso_to_pattern, parse_datasets, + pattern_to_iso, read_values, read_values_with, remove_data_instance, som_to_path, write_data_ops, write_data_value, write_datasets, + write_datasets_reusing, write_datasets_with, }; pub use layout::{Action, BorderShape, Form, Item, MAX_PAGES, Page, Widget, WidgetKind, layout}; pub use packets::{Encoding, Packets, decode as decode_packet, encode as encode_packet, read_packets}; diff --git a/crates/xfa/src/script.rs b/crates/xfa/src/script.rs index c2088deee..255c7f45a 100644 --- a/crates/xfa/src/script.rs +++ b/crates/xfa/src/script.rs @@ -6,7 +6,7 @@ use std::collections::{BTreeMap, HashMap}; use pdfcraft_cos::{Dict, Document, Object, PdfString}; -use crate::data::{DataNode, parse_datasets, som_to_path}; +use crate::data::{DataNode, data_path, parse_datasets}; use crate::model::*; use crate::{Report, XfaError}; @@ -171,6 +171,8 @@ fn child_som(parent: &str, name: Option<&str>, index: usize) -> String { } struct Walker<'a> { + /// For data paths (areas are not data scopes). + tpl: &'a Template, data: Option<&'a DataNode>, ov: &'a Overrides, events: Vec, @@ -241,7 +243,7 @@ impl Walker<'_> { fn field(&mut self, f: &Field, parent_som: &str, index: usize, radio: Option<&str>) -> FormNode { let som = child_som(parent_som, f.common.name.as_deref().or(Some("field")), index); - let data_value = self.data.and_then(|d| d.value_at(&som_to_path(&som))).map(|t| t.trim().to_string()).filter(|t| !t.is_empty()); + let data_value = self.data.and_then(|d| d.value_at(&data_path(self.tpl, &som))).map(|t| t.trim().to_string()).filter(|t| !t.is_empty()); let value = match (radio, data_value) { // A radio button's value is its on value when the group picked it. (Some(selected), _) => { @@ -281,7 +283,7 @@ impl Walker<'_> { match n { Node::Subform(sf) => { let from_data = match (&sf.common.name, self.data) { - (Some(name), Some(d)) if sf.occur.max != Some(1) => d.count(&som_to_path(parent_som), name), + (Some(name), Some(d)) if sf.occur.max != Some(1) => d.count(&data_path(self.tpl, parent_som), name), _ => 0, }; let instances = crate::layout::instance_count(&sf.occur, from_data); @@ -344,7 +346,8 @@ impl Walker<'_> { } Node::ExclGroup(g) => { let som = child_som(parent_som, g.common.name.as_deref().or(Some("group")), sib); - let selected = self.data.and_then(|d| d.text_at(&som_to_path(&som))).map(|t| t.trim().to_string()).filter(|t| !t.is_empty()); + let selected = + self.data.and_then(|d| d.text_at(&data_path(self.tpl, &som))).map(|t| t.trim().to_string()).filter(|t| !t.is_empty()); self.push_events(&som, &g.scripts, None, None, None); let fields: Vec = g.fields.iter().map(|f| Node::Field(Box::new(f.clone()))).collect(); let mut children = Vec::new(); @@ -378,7 +381,7 @@ impl Walker<'_> { /// from the template and `ov`) and every scripted event, in document order, within /// [`MAX_FORM_NODES`] objects. pub fn form_tree(tpl: &Template, data: Option<&DataNode>, ov: &Overrides) -> LiveForm { - let mut w = Walker { data, ov, events: Vec::new(), nodes: 0, truncated: false }; + let mut w = Walker { tpl, data, ov, events: Vec::new(), nodes: 0, truncated: false }; let root = Node::Subform(Box::new(tpl.root.clone())); let mut nodes = w.children(std::slice::from_ref(&root), "", 0); let mut root_node = nodes.pop().unwrap_or_default(); diff --git a/crates/xfa/src/tests.rs b/crates/xfa/src/tests.rs index 4a4d8ad5d..a690e7f3c 100644 --- a/crates/xfa/src/tests.rs +++ b/crates/xfa/src/tests.rs @@ -1041,3 +1041,71 @@ fn pictures_are_embedded_once_across_relayouts_and_bad_data_pictures_are_reporte } assert!(find_image(&t.root.children).is_some_and(|n| n > 1_200_000)); } + +/// A repeating subform drawn inside positioned content (a top-to-bottom table in an area of a +/// positioned subform) gets its `occur` instances, as in Adobe's viewers: real Designer forms +/// have tables like this whose calculations read every row. Before, only one row was laid +/// out, and the field after the table was placed as if there were one. +#[test] +fn repeating_subforms_inside_positioned_content_get_their_instances() { + let xdp = r##" + + + +"##; + let form = layout_xml(xdp).unwrap(); + let all: Vec<&Widget> = form.pages.iter().flat_map(widgets).collect(); + let rows: Vec<&Widget> = all.iter().copied().filter(|w| w.som.ends_with(".reason[0]")).collect(); + let soms: Vec<&str> = rows.iter().map(|w| w.som.as_str()).collect(); + assert_eq!( + soms, + [ + "form[0].page1[0].box[0].wrap[0].table[0].row[0].reason[0]", + "form[0].page1[0].box[0].wrap[0].table[0].row[1].reason[0]", + "form[0].page1[0].box[0].wrap[0].table[0].row[2].reason[0]", + ], + "one widget per instance, each with its own SOM index" + ); + // The rows are stacked, not drawn on top of each other… + let step = rows[1].rect.y - rows[0].rect.y; + assert!(step.abs() > 1.0, "rows overlap: {:?}", rows.iter().map(|w| w.rect).collect::>()); + assert!(((rows[2].rect.y - rows[1].rect.y) - step).abs() < 0.01, "rows are evenly spaced"); + // …and what follows the table is placed after all of them (the table was measured with + // its instances, not with one row). + let after = all.iter().find(|w| w.som.ends_with(".after[0]")).expect("the field after the table"); + assert!(((after.rect.y - rows[2].rect.y) - step).abs() < 0.01, "after: {:?}, last row: {:?}", after.rect, rows[2].rect); +} + +/// Areas are named in SOM expressions but are not data scopes; unnamed subforms and areas have +/// no segment at all. What the template doesn't have is kept as it is. +#[test] +fn data_paths_leave_areas_out() { + let (tpl, _) = parse(&crate::fixtures::leave_template("")).unwrap(); + let path = |som: &str| data_path(&tpl, som).into_iter().map(|(n, i)| format!("{n}[{i}]")).collect::>().join("."); + // The area `box` (inside an unnamed subform) is dropped; indexes are kept. + assert_eq!(path("form[0].page[0].box[0].table[0].row[2].days[0]"), "form[0].page[0].table[0].row[2].days[0]"); + assert_eq!(path("form[0].page[0].box[0].rest[0]"), "form[0].page[0].rest[0]"); + // No area on the way: as som_to_path. + assert_eq!(path("form[0].page[0]"), "form[0].page[0]"); + // Unknown segments, past a leaf, or a SOM from another template: kept. + assert_eq!(path("form[0].page[0].box[0].nothing[0].x[1]"), "form[0].page[0].nothing[0].x[1]"); + assert_eq!(path("form[0].page[0].box[0].rest[0].box[0]"), "form[0].page[0].rest[0].box[0]"); + assert_eq!(path("other[0].box[0].rest[0]"), "other[0].box[0].rest[0]"); + assert_eq!(data_path(&tpl, ""), Vec::new()); +} diff --git a/crates/xfdf/README.md b/crates/xfdf/README.md index f30155b63..fd2852584 100644 --- a/crates/xfdf/README.md +++ b/crates/xfdf/README.md @@ -12,11 +12,13 @@ let report = import(&mut doc, &bytes)?; // XFDF, FDF, XML, CSV o - **XFDF** (ISO 19444-1): every comment type PdfCraft knows (text, free text, line, square, circle, polygon, polyline, markup, stamp, caret, ink, attachments, sound, redact) with rect, name, author, subject, dates, flags, colours, opacity, width, icon, quads, line ends, ink, - default appearance, pop-ups and reply threads (`inreplyto`); field values nested by name. + default appearance, callouts (`callout`, `fringe`, `intent`, `head`), pop-ups and reply threads + (`inreplyto`); field values nested by name. - **FDF**: fields (with `/Kids` hierarchies) and comments as direct objects. - **Form data**: Acrobat's XML (`xfdf:original` keeps names that aren't XML names), CSV and tab-delimited text (a row of names, a row of values). -Import replaces comments with the same `/NM`, draws appearances PdfCraft can draw, and sets -values through the form's own checks (formats, validation, recalculation); read-only fields take -imported values, as in Acrobat. XML is read with `roxmltree`. +Import replaces comments with the same `/NM` (and drops the replaced comment's pop-up), draws +appearances PdfCraft can draw, and sets values through the form's own checks (formats, +validation, recalculation); read-only fields take imported values, as in Acrobat. XML is read +with `roxmltree`. diff --git a/crates/xfdf/src/lib.rs b/crates/xfdf/src/lib.rs index 190080611..91c3d2454 100644 --- a/crates/xfdf/src/lib.rs +++ b/crates/xfdf/src/lib.rs @@ -2,7 +2,7 @@ //! //! - **XFDF** (ISO 19444-1, Acrobat's "Export all to data file" / "Import comments", and form //! data): comments with their geometry, colours, flags, authors, dates, replies, pop-ups, -//! ink, quadrilaterals and line ends; field values, nested by name. +//! ink, quadrilaterals, line ends and callouts; field values, nested by name. //! - **FDF** (ISO 32000-2 §12.7.8): fields and comments in PDF syntax. //! - Form data as **XML**, **CSV** and **tab-delimited text** (Acrobat's Export data formats). //! @@ -244,6 +244,21 @@ 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=\"{}\"", name(&le[0]), name(&le[1])); } + // A callout: its leader path, text-box inset, intent 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(",")); + let rd = nums_of(doc, d.get(b"RD")); + 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)); + } + } 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)); } @@ -264,6 +279,12 @@ fn xfdf_annots(doc: &Document, out: &mut String) { } 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 { @@ -601,6 +622,20 @@ 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(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) { + d.set(b"RD".to_vec(), arr(&rd)); + } + d.set(b"IT".to_vec(), Object::name(node.attribute("intent").unwrap_or("FreeTextCallout"))); + // A callout's /LE is one name, not the head/tail pair of a line. + match node.attribute("head") { + Some(h) => d.set(b"LE".to_vec(), Object::name(h)), + None => { + d.remove(b"LE"); + } + } + } for child in node.children().filter(|c| c.is_element()) { match child.tag_name().name() { "contents" => { @@ -617,6 +652,14 @@ fn annot_from_xml(node: roxmltree::Node, subtype: &str, page_ref: ObjRef) -> Opt .collect(); d.set(b"InkList".to_vec(), Object::Array(list)); } + "vertices" => { + // Whole x,y pairs only; a stray trailing number is dropped. + let mut v = csv(child.text().unwrap_or("")); + v.truncate(v.len() - v.len() % 2); + if !v.is_empty() { + d.set(b"Vertices".to_vec(), arr(&v)); + } + } _ => {} } } @@ -635,13 +678,21 @@ fn place(doc: &mut Document, page_ref: ObjRef, d: Dict) -> Result = Vec::new(); for a in annots { let same = nm.is_some() && a.as_ref().and_then(|r| doc.get(r).as_dict().and_then(|x| x.get(b"NM").and_then(|n| n.as_string().map(|s| s.to_text())))) == nm; - if !same { + if same { + removed.extend(a.as_ref()); + } else { kept.push(a); } } + // The replaced note's pop-up goes with it; otherwise it stays on the page pointing at a note that is no longer there. + kept.retain(|a| { + let Some(x) = a.as_ref().map(|r| doc.get(r)).and_then(|o| o.as_dict().cloned()) else { return true }; + x.name(b"Subtype") != Some(b"Popup".as_slice()) || x.reference(b"Parent").is_none_or(|p| !removed.contains(&p)) + }); let r = doc.add(Object::Dict(d)); // Our own appearance where we can draw one; others keep none (viewers draw a default). let _ = pdfcraft_annot::set_appearance(doc, r); diff --git a/crates/xfdf/src/tests.rs b/crates/xfdf/src/tests.rs index ca1debe49..33b3a5a88 100644 --- a/crates/xfdf/src/tests.rs +++ b/crates/xfdf/src/tests.rs @@ -331,3 +331,117 @@ fn unicode_annotation_colours_are_ignored_without_panicking() { assert_eq!(report.comments, 1); } } + +/// Importing the same note again must not leave the earlier pop-up on the page. +#[test] +fn reimporting_a_note_replaces_its_popup() { + let xfdf = concat!( + r#"body"#, + r#""#, + ); + let mut doc = blank(); + for _ in 0..3 { + import(&mut doc, xfdf.as_bytes()).unwrap(); + } + let page = pdfcraft_model::pages(&doc)[0].obj; + let annots = doc.get(page).as_dict().and_then(|p| p.get(b"Annots").cloned()).and_then(|a| doc.resolve(&a).as_array().cloned()).unwrap(); + let refs: Vec = annots.iter().filter_map(|a| a.as_ref()).collect(); + let is_popup = |r: &ObjRef| doc.get(*r).as_dict().and_then(|d| d.name(b"Subtype")) == Some(b"Popup".as_slice()); + let popups: Vec = refs.iter().copied().filter(is_popup).collect(); + assert_eq!(popups.len(), 1, "one current pop-up, not one per import"); + let parent = doc.get(popups[0]).as_dict().and_then(|d| d.reference(b"Parent")).unwrap(); + assert!(refs.contains(&parent), "the pop-up belongs to a note that is still on the page"); +} + +/// The `/Vertices` of every annotation on the document's pages, by subtype. +fn vertices(doc: &Document) -> Vec<(String, Vec)> { + let mut out = Vec::new(); + for p in pdfcraft_model::pages(doc) { + for a in p.dict.get(b"Annots").map(|a| doc.resolve(a)).and_then(|a| a.as_array().cloned()).unwrap_or_default() { + let Some(d) = doc.resolve(&a).as_dict().cloned() else { continue }; + let sub = String::from_utf8_lossy(d.name(b"Subtype").unwrap_or_default()).into_owned(); + let v = nums_of(doc, d.get(b"Vertices")); + if !v.is_empty() { + out.push((sub, v)); + } + } + } + out +} + +/// storytold/pdfcraft#809: XFDF exchange dropped polygon and polyline vertices, so the +/// imported comments had no shape (and no appearance). +#[test] +fn xfdf_keeps_polygon_and_polyline_vertices() { + let mut src = blank(); + let meta = |id: &str| Meta { date: Some("D:20261002120000Z".into()), id: id.into() }; + let polygon = Shape::Polygon { vertices: vec![[100.0, 100.0], [200.0, 120.0], [150.0, 220.5]], cloud: false }; + let polyline = Shape::PolyLine { + vertices: vec![[50.0, 300.0], [120.0, 380.0], [200.0, 310.0]], + start: pdfcraft_annot::LineEnding::None, + end: pdfcraft_annot::LineEnding::ClosedArrow, + }; + for (page, shape, id) in [(0, polygon, "pg"), (1, polyline, "pl")] { + let new = NewAnnotation { page, style: Style::default_for(&shape), shape, contents: id.into(), author: "Ada".into() }; + add_annotation(&mut src, &new, &meta(id)).unwrap(); + } + let xfdf = export_xfdf(&src, true, false, "shapes.pdf"); + assert!(xfdf.contains("100,100;200,120;150,220.5"), "{xfdf}"); + assert!(xfdf.contains("50,300;120,380;200,310"), "{xfdf}"); + let mut dst = blank(); + let r = import(&mut dst, xfdf.as_bytes()).unwrap(); + assert_eq!(r.comments, 2, "{r:?}"); + assert_eq!(vertices(&dst), vertices(&src)); + // The imported shapes are drawn again. + for p in pdfcraft_model::pages(&dst) { + for a in p.dict.get(b"Annots").map(|a| dst.resolve(a)).and_then(|a| a.as_array().cloned()).unwrap_or_default() { + let d = dst.resolve(&a).as_dict().cloned().unwrap(); + assert!(d.get(b"AP").is_some(), "{d:?}"); + } + } + // Acrobat-style input: a stray trailing number is dropped and only whole points are kept. + let odd = r#" + 1,2;3,4;5,6;7 + junk + "#; + let mut dst = blank(); + import(&mut dst, odd.as_bytes()).unwrap(); + assert_eq!(vertices(&dst), vec![("Polygon".to_string(), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0])]); +} + +#[test] +fn xfdf_keeps_callout_leader_inset_and_ending() { + let mut src = blank(); + let shape = Shape::Callout { + rect: [40.0, 300.0, 180.0, 360.0], + knee: [260.0, 330.0], + point: [320.0, 200.0], + font_size: 12.0, + ending: pdfcraft_annot::LineEnding::ClosedArrow, + }; + let meta = Meta { date: Some("D:20261002120000Z".into()), id: "c1".into() }; + add_annotation( + &mut src, + &NewAnnotation { page: 0, style: Style::default_for(&shape), shape, contents: "leader".into(), author: "Ada".into() }, + &meta, + ) + .unwrap(); + let xfdf = export_xfdf(&src, true, false, "form.pdf"); + let mut dst = blank(); + import(&mut dst, xfdf.as_bytes()).unwrap(); + let callout_keys = |doc: &Document| { + let page = pdfcraft_model::pages(doc).remove(0); + let annots = page.dict.get(b"Annots").map(|a| doc.resolve(a)).and_then(|a| a.as_array().cloned()).unwrap_or_default(); + let d = annots.iter().filter_map(|a| doc.resolve(a).as_dict().cloned()).find(|d| d.name(b"Subtype") == Some(b"FreeText")).unwrap(); + ( + nums_of(doc, d.get(b"Rect")), + nums_of(doc, d.get(b"CL")), + nums_of(doc, d.get(b"RD")), + d.name(b"IT").map(<[u8]>::to_vec), + d.get(b"LE").and_then(|o| o.as_name()).map(<[u8]>::to_vec), + ) + }; + let want = callout_keys(&src); + assert_eq!(want.1.len(), 6, "{want:?}"); + assert_eq!(callout_keys(&dst), want, "{xfdf}"); +} diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 000000000..eb56cdb3e --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,112 @@ +# PdfCraft architecture + +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (first public architecture document, written from the code on main) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) + +How PdfCraft is built today. The design rationale and the planned crates live in +`plan/architecture.md` and `plan/adr/` (local-only); this file describes what is on `main`. Each +crate's `README.md` has the detail. + +## Shape + +One Cargo workspace (`Cargo.toml`, edition 2024, Rust ≥ 1.90, `MIT OR Apache-2.0`), 31 library +crates in `crates/`, three binaries in `apps/`, build tooling in `xtask/`, and patched third-party +crates in `vendor/`. About 152k lines of Rust and 1,765 tests (2026-10-10). Rust only: no +handwritten JS/TS, in the product or the build. + +| Binary | What it is | +|---|---| +| `apps/pdfcraft` | The desktop app (macOS, Windows, Linux, FreeBSD): eframe/egui on wgpu, single instance, Apple events, update check on request | +| `apps/pdfcraft-cli` | `inspect`, `edit`, `combine`, `extract`, `split`, `run` (any automation tool or a JSON script), `tools`, `ui` (drive the running app), and the opt-in `mcp` server | +| `apps/pdfcraft-web` | The same egui shell compiled to WebAssembly | + +## Layers + +`cargo xtask layers` enforces the layering (`xtask/src/layers.rs`). A crate may depend only on +crates in lower layers; nothing below L7 may depend on egui, winit, eframe or rfd; L0–L1 crates are +standalone and publishable. + +| Layer | Crates today | Role | +|---|---|---| +| L0 foundation | `geom`, `filters`, `crypt` | Geometry; stream filters (Flate, LZW, predictors, ASCII85, ASCIIHex, RunLength); standard security handler R2–R6 (RC4, AES-128/256, SASLprep) | +| L1 objects | `cos` | Lazy, tolerant parser with xref repair; copy-on-write object graph; incremental and full writers with object streams; decryption on load and re-encryption on save | +| L2 core model | `content`, `model`, `fonts` | Content-stream operators with source spans and a serializer; typed page tree with inherited attributes and display geometry (`/UserUnit`, `/Rotate`); font metrics and encodings for generated appearances | +| L3 core services | `render`, `annot`, `forms`, `js`, `xfa` | Rendering and inspection (bootstrap: vendored `hayro`, `lopdf`); annotation builders and appearance streams; AcroForm model, filling, appearances; sandboxed JavaScript (boa) and FormCalc; XFA template layout and data | +| L4 features | `edit`, `organize`, `redact`, `xfdf`, `sign`, `ocr`, `create`, `export`, `optimize`, `preflight`, `a11y`, `compare`, `measure`, `print` | One crate per Acrobat tool family | +| L6 façade | `engine` | Session, open documents, edit history, the command registry and tool catalogue. Frontends never reach past it | +| L7 platform and frontends | `platform`, `ui-egui`, `automation` | OS services; the egui shell (desktop and web); headless JSON-Schema tools, CLI and MCP | + +Planned crates that the layer table already reserves (`arlington`, `color`, `ops`, `text`, `ml`, +`security`, `prepress`, `search`, `media`, `ai`, `viewport`, `tools`, `ui-common`) don't exist yet. + +## Data model and editing + +The PDF object graph *is* the model (`crates/engine/README.md` §The editing model): + +1. Each open document holds a `pdfcraft_cos::Document`: the parsed objects plus a copy-on-write + overlay. Unknown data is preserved because nothing is rebuilt that wasn't understood. +2. An edit runs on a clone; on success the previous state goes onto the undo stack. Clones share + unchanged data, so snapshots are cheap. +3. After every edit an incremental write produces the working file (original bytes plus one + revision); the view is refreshed from those bytes, so what you see is what Save writes. +4. Saves are incremental and atomic. A full rewrite happens only where it must: redaction, PDF/A + conversion, print imposition, a changed password, Save As with garbage collection. +5. Autosave and crash recovery keep encrypted documents encrypted. Signed documents are protected + from rewrites. + +## Rendering and text + +- Pages are rasterized on the CPU by the vendored `hayro` family (`hayro`, `hayro-interpret`, + `hayro-syntax`, `hayro-jbig2`), with local patches for appearance states, layers, resource + limits, cancellation, tiles and `/UserUnit` (`vendor/README.md`). A render pool renders visible + pages first, then neighbours and thumbnails; tiles keep deep zoom sharp; a watchdog skips + pathological pages. +- The raster is uploaded to the GPU as a texture; egui draws the interface through wgpu (OpenGL + fallback when a device can't be configured). +- Text extraction (reading order, columns, vertical CJK, rotated text) feeds find, select, copy, + redaction, export and compare; word-F1 0.98 against `pdftotext` on the corpus. +- The replacement (M2, ADR-0004) is the `model` crate, our own font engine and DisplayList devices; + only the first pieces of `model` exist. See [gaps.md](gaps.md) gap 2. + +## File I/O + +- PDF in: any revision chain, xref tables or streams, hybrid files, damaged files (repaired and + logged), all standard-security revisions. Image codecs (DCT, JPX, JBIG2, CCITT) decode through + the bootstrap renderer. +- PDF out: incremental or full saves, object streams, re-encryption, resource de-duplication when + combining. +- Other formats: images and text in (`create`); PNG/JPEG/TIFF, text, RTF, Word and HTML out + (`export`, `render`); FDF/XFDF/XML/CSV/TXT form and comment data (`xfdf`); PKCS #12, X.509, + PEM/DER certificates (`sign`). Full matrix: [file-format-parity.md](file-format-parity.md). + +## Agent control + +Every user-facing feature is reachable headlessly (`AGENTS.md` §3): + +- the **command registry** (`crates/engine/src/commands.rs`, 184 commands) generates menus, + shortcuts and the command palette; +- the **automation tool table** (`crates/automation`, 141 tools) is shared by `pdfcraft-cli run` + and the opt-in **MCP server** (stdio only; `pdfcraft-cli mcp`), confined to `--root`; +- the opt-in **UI control channel** (`pdfcraft --control FILE`; loopback, per-launch token) lets an + agent inspect, click, type, run commands and take screenshots of the real interface. + +## Safety + +- `unsafe_code = "forbid"` workspace-wide; crates deny `unwrap`/`expect`/`panic!`; engine entry + points keep a last-resort `catch_unwind` (`AGENTS.md` §4). +- Every input-derived size is bounded (decompression, images, recursion, content per page). +- `cargo xtask fuzz` runs nightly; every finding becomes a synthetic regression test. + +## Build and quality gates + +`cargo xtask ci` runs fmt, clippy (`-D warnings`), tests, the layering check, the WASM check, +`xtask assets` (asset licences and hashes), `xtask parity` (checklist claims must cite real commands, +tools and tests) and `xtask deny` (dependency licences). Other tasks: `xtask fuzz`, `xtask corpus` and `xtask check` (robustness +sweep over the pinned pdf.js corpus), `xtask text-oracle` (word F1 against `pdftotext`), +`xtask demo-pdf`, `xtask screenshots`, `xtask models` (OCR models), `xtask version`. Releases are +built by `.github/workflows/release.yml` ([releasing.md](releasing.md)). + +## Revision history + +| Date | Change | Summary | +|---|---|---| +| 2026-10-10 | major | Created from the code on main (crate list, `xtask/src/layers.rs`, crate READMEs, `vendor/README.md`) | diff --git a/docs/editing-multiple-objects.md b/docs/editing-multiple-objects.md new file mode 100644 index 000000000..2eb7d84a5 --- /dev/null +++ b/docs/editing-multiple-objects.md @@ -0,0 +1,87 @@ +# Move several objects together + +In **Edit a PDF → Edit text & images**, select several items on the same page: + +- **Shift-click**, **Ctrl-click**, or **Command-click** toggles an item. An ordinary selected + paragraph, image, or added item becomes the first member when you extend its selection. +- Drag through empty page space to draw a selection rectangle. Items intersecting the rectangle + are selected; hold one of those modifiers to add to the selection. +- Drag the selection to move all its items by the same distance. Blue outlines preview the move; + releasing commits it as **one undo step**. Escape cancels the gesture or clears the selection. +- Switching tabs, opening Home or a modal dialog, or losing window focus cancels a drag preview. + Returning to the document keeps the selection without committing a release from elsewhere. +- Modifier-clicks prefer added items and text over background artwork, so labels remain selectable. +- Clear the selection or double-click an item to return to ordinary text editing and image resizing. + Finish or discard a text draft before starting a multi-selection. + +The supported items are the existing paragraph boxes, raster Image XObjects, whole Form artwork, +and text/images added by PdfCraft. A Form remains one item, including its nested artwork and +resources. Added items appear once in the selection inventory. Selections stay on one page and +contain at most 1,000 items; pages with more than 10,000 recognised objects are refused for group +selection. Content streams must decode completely within 64 MiB each and 128 MiB per page, with +a 1,024-level graphics-state limit and 100,000 content operations. These limits apply to both the +original page and the moved result; unreadable or missing streams are refused. Moving between pages, +arbitrary paths, inline images, group resizing, align/distribute, +and vertical or clipping text are outside this operation's scope. A refusal changes nothing. + +Added streams move by changing their outer placement, preserving drawing bytes, fonts, crop paths, +and image transforms even if the page was rotated after the item was added. Their original local +coordinate basis is retained in `/PCAdded/MoveOrigin` so metadata and source placement agree after +repeated moves. Ordinary content updates replace that stream and reset the basis. + +Existing text moves without retyping, font substitution, or reflow: its original encoded glyph +strings and kerning are replayed with temporary numeric `TJ` adjustments and text rise. The reader +performs the original glyph advances, so following unselected text keeps its position. Paragraph +indices can change when detection groups lines differently after a move. The UI remaps its own +selection only when the new geometry matches uniquely; other document edits clear stale selection. + +## Engine + +`Document::editable_objects(page)` returns a mixed inventory with zero-based `ObjectTarget` +references and user-space rectangles. Apply `Edit::MoveObjects { page, objects, offset }` through +`Session`. Targets resolve together before mutation. Permission checks, dirty state, history, +incremental saving, and refreshing use the same transaction as other edits. + +`Document::object_move_offset(page, [dx, dy])` converts a displayed-page vector (points, right/down) +to the user-space vector required by the edit, accounting for the crop box, `/Rotate`, and `/UserUnit`. +A frontend with an additional view rotation converts its pointer positions to displayed page space +first. Duplicate, missing, non-finite, oversized, or unsupported targets fail atomically. Engine callers +re-list after changes; the automation generation guard detects stale references before the edit. + +## CLI and MCP + +Both frontends use the same tools. `object_list` returns a document `generation` and items with +`kind` (`added`, `text`, or `image`), 1-based `index`, `type`, displayed `rect`, and optional `text`. +Treat those references as belonging to that generation and pass it back when moving: + +```json +[ + {"tool":"doc_open","args":{"path":"input.pdf"}}, + {"tool":"object_list","args":{"doc":1,"page":1}} +] +``` + +After choosing references from the returned list, use a move call like this in the same MCP +session. For a CLI script, include `doc_open` in that script before the move: + +```json +[ + {"tool":"object_move","args":{ + "doc":1,"page":1,"generation":0, + "objects":[{"kind":"text","index":1},{"kind":"image","index":1}], + "offset":[24,18] + }}, + {"tool":"doc_save","args":{"doc":1,"path":"moved.pdf"}} +] +``` + +Use the actual generation and references from your document, rather than assuming these example +values. `offset` is in displayed points: positive x moves right, positive y moves down. The optional +generation guard rejects a changed document before interpreting references. Re-list after any edit, +undo, redo, or save; there is no claim that an index persists across generations. Run a script with +`pdfcraft-cli run --script steps.json --root DIR` or call these tools through the opt-in MCP server. + +Regression coverage: `crates/edit/src/tests.rs` (glyph bytes, neighbours, split streams, shared Forms, +rotated/cropped/UserUnit pages and refusal), `crates/engine/src/tests.rs` (permissions, history, +rendering and save/reopen), `crates/automation/tests/automation.rs` (generation guards and hostile +references), and `crates/ui-egui/tests/object_selection.rs` (real-shell selection and gestures). diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md new file mode 100644 index 000000000..4fc21f59d --- /dev/null +++ b/docs/file-format-parity.md @@ -0,0 +1,112 @@ +# File format parity + +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (first format matrix; Acrobat's formats from its bundle) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) + +Every format Acrobat Pro reads or writes, and PdfCraft's support for it. Acrobat's list comes from +its bundle's `Info.plist` `CFBundleDocumentTypes` (what it opens) and its public documentation +(Create PDF and Export PDF). PdfCraft's support comes from the code and +`parity/acrobat-features.toml`. Summary in [target-app-parity.md](target-app-parity.md); gaps +in [gaps.md](gaps.md). + +Legend: **R** read/import, **W** write/export. Fidelity: *high* (tested against an external oracle +or round-trips exactly), *good* (tested, known limits), *basic* (works, visibly lossy), *none*. + +**File formats and conversion ≈ 40% ready** (estimated): PDF itself is strong except encrypted +output interoperability; the conversion formats users reach for most (Word, Excel, PowerPoint, +Office to PDF, OCR beyond English) are weak or missing. **120–220 h** to parity (within the +features total). + +## PDF + +| Format | Acrobat | PdfCraft | Fidelity | Tests and evidence | +|---|---|---|---|---| +| PDF 1.0–1.7, ISO 32000-1 | R W | R W | high for reading and unencrypted writing | 958-file pdf.js corpus open/edit/save; `qpdf --check` on saved outputs; `xtask check` baselines | +| PDF 2.0 (ISO 32000-2) | R W | R W (2.0-only features ignored but preserved) | good | No UTF-8 strings, `/AF`, DPart, namespaces ([pdf-spec-parity.md](pdf-spec-parity.md)) | +| Encrypted PDF (standard security R2–R6) | R W | R W | **reading high; writing fails in Acrobat and Reader** | `crates/crypt`, `crates/cos` tests; all 7 corpus encrypted files open; **#774** | +| Certificate-encrypted PDF (public-key handler) | R W | none (clear message) | none | `core.unsupported-handlers` | +| Linearized PDF (Fast Web View) | R W | R (as normal PDF); no W | none for W | `core.linearization` planned | +| Damaged PDF | R (repair) | R (repair, logged) | high | `core.repair-broken-files`, nightly fuzzing | +| PDF/A-1, 2, 3, 4 | R W, verify | R; verify/convert 2b and 3b (partial) | basic | `optimize.pdfa-*`; veraPDF not yet used as oracle | +| PDF/X, PDF/UA, PDF/E, PDF/VT | R W, verify | R only | none | [pdf-spec-parity.md](pdf-spec-parity.md) | +| PDF Portfolio (collection) | R W | R as a PDF with attachments; no portfolio view | none | `organize.portfolio-*` planned | +| XFA forms (in PDF) | R W (fill) | R, lay out dynamic forms, fill, save datasets (partial) | basic | `form.xfa-*` partial; `crates/xfa` tests | + +## Data and interchange formats + +| Format | Acrobat | PdfCraft | Fidelity | Tests and evidence | +|---|---|---|---|---| +| FDF (`.fdf`) comments and form data | R W | R W | good | `crates/xfdf`; bugs #807, #809, #819 in comment exchange | +| XFDF (`.xfdf`, ISO 19444-1) | R W | R W | good | as above | +| XML form data | R W | R W | good | `form.data-xml-csv-txt` | +| XDP (`.xdp`, XFA data package) | R | none | none | Acrobat opens `.xdp` as a document type | +| CSV / tab-delimited form data | W (merge to spreadsheet) | R W; merge to spreadsheet | good | `form.merge-to-spreadsheet` | +| Comment summary (PDF) | W | W (comments only layout) | basic | `comment.summarize` partial | +| Measurement CSV | W | W | good | `comment.measure-export` | +| Compare report (PDF) | W | W | good | `misc.compare-report` | +| Action Wizard sequences (`.sequ`) | R W | own JSON actions; no `.sequ` | none for `.sequ` | `misc.action-wizard-*` | +| Security settings (`.acrobatsecuritysettings`), `.apf` profiles, `.secstore` | R W | none | none | out of scope unless asked | +| Search index (`.pdx`) | R W | none | none | `view.search-index` planned | +| JDF / MJD job definitions | R | none | none | `print.jdf` planned (P3) | +| Preflight profiles (`.kfp`) and reports | R W | none | none | gap 9 | +| PDF Optimizer settings (`.optimize`) | R W | none (dialog settings not saved as presets) | none | `optimize.optimizer-presets` planned | + +## Certificates and keys + +| Format | Acrobat | PdfCraft | Fidelity | Tests and evidence | +|---|---|---|---|---| +| PKCS #12 digital IDs (`.p12`, `.pfx`) | R W | R W (self-signed IDs) | high | OpenSSL-made fixtures (`crates/sign/tests/data/README.md`) | +| Certificates (`.cer`, `.der`, `.pem`, `.p7c`, `.p7b`) | R W | R (`.cer/.pem/.der/.p12` trust import); W public certificate | good; `.p7c/.p7b` bundles not read | `sign.trust-import` partial, `sign.export-certificate` | +| CMS / PKCS #7 signatures, RFC 3161 tokens, OCSP, CRL | R W | R W | high | `pdfsig` and OpenSSL oracles | + +## Create PDF from + +| Source | Acrobat | PdfCraft | Fidelity | Tests and evidence | +|---|---|---|---|---| +| JPEG, PNG, TIFF (multipage), GIF, BMP, JPEG 2000 | yes | yes (ICC profiles kept; DPI choices) | good | `create.from-images`, `create.from-multipage-tiff` | +| PCX | yes | no | none | in Acrobat's document types | +| Plain text | yes | yes | good | `create.from-text` | +| Clipboard (image or text) | yes | yes | good | `create.from-clipboard` | +| Screenshot | yes | no | none | `create.from-screenshot` planned | +| Word, Excel, PowerPoint (and Visio, Publisher on Windows) | yes | no | none | `create.from-office` planned (LibreOffice sidecar) | +| HTML file, web page URL | yes | no | none | `create.from-html`, `create.from-web-page` planned | +| RTF | yes | no | none | | +| PostScript / EPS (Distiller) | yes | no | none | `create.distiller` planned (P3) | +| Markdown | no | no | none | `create.from-markdown` planned (P2; not an Acrobat feature) | +| Scanner | yes | no | none | `ocr.scanner-acquire` planned | +| Several files at once | yes | yes (PDFs, images and text) | good | `create.create-multiple` | + +## Export PDF to + +| Target | Acrobat | PdfCraft | Fidelity | Tests and evidence | +|---|---|---|---|---| +| Word (`.docx`) | yes, layout-preserving | flowing document: paragraphs, headings, bold/italic, colour, images, tables | **basic** (#773: layout lost, fake borders, duplicated pages) | `create.export-docx` partial | +| Word 97–2003 (`.doc`) | yes | no | none | | +| Excel (`.xlsx`), XML Spreadsheet 2003 | yes | no | none | `create.export-xlsx` planned | +| PowerPoint (`.pptx`) | yes | no | none | `create.export-pptx` planned | +| RTF | yes | yes | basic | `create.export-rtf` | +| HTML (single page, multiple pages) | yes | single file with headings, paragraphs, images, tables | basic | `create.export-html` partial | +| Plain text | yes | yes (reading order) | good (word-F1 0.98 vs `pdftotext`) | `create.export-text`, `xtask text-oracle` | +| Accessible text (from tags) | yes | no | none | `create.export-accessible-text` planned | +| PNG, JPEG, TIFF | yes | yes | good | `create.export-png`, `create.export-jpeg-tiff` | +| JPEG 2000 | yes | no | none | `create.export-jpeg2000` planned | +| All embedded images | yes | yes | good | `create.export-all-images` | +| XML 1.0 | yes | no | none | `create.export-xml` planned | +| SVG | no (dropped by Adobe) | no | none | `create.export-svg` planned | +| PostScript, EPS | yes | no | none | `create.export-ps`, `create.export-eps` planned | + +## OCR (scans to searchable PDF) + +| Capability | Acrobat | PdfCraft | Fidelity | +|---|---|---|---| +| Searchable image (invisible text layer) | yes | yes (Tr 3 text fitted to word boxes) | good for English | +| Editable text and images output | yes | no | none | +| Languages | dozens (Latin with accents, Cyrillic, Greek, CJK, Arabic, Hebrew, Indic) | Latin without accents only (ocrs models) | **basic** (#744) | +| Deskew, rotation, despeckle, background and halo removal, descreen | yes | no | none | +| Camera images (edges, perspective, whiteboard, card) | yes | no | none | +| MRC (adaptive) compression, PDF/A output of scans | yes | no | none | + +## Revision history + +| Date | Change | Summary | +|---|---|---| +| 2026-10-10 | major | Created from Acrobat 26.002.21931's `CFBundleDocumentTypes`, Acrobat's public Create/Export documentation, and PdfCraft's code and checklist | diff --git a/docs/gaps.md b/docs/gaps.md new file mode 100644 index 000000000..d1e90e348 --- /dev/null +++ b/docs/gaps.md @@ -0,0 +1,305 @@ +# Where PdfCraft falls short of Acrobat Pro + +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (alpha markers removed after the gate was re-judged: no gap blocks alpha) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) + +Every known shortfall, one entry each, ranked by what it costs users. This is the work list: agents +choose from the top unless the owner says otherwise, and prefer these over new P2/P3 checklist +features. The numbers behind it are in [target-app-parity.md](target-app-parity.md); the per-feature +detail is in `parity/acrobat-features.toml` (`cargo xtask parity --partial` lists what each partial +feature lacks). + +No gap blocks the [alpha gate](roadmap.md#alpha-gate); gaps 1 and 8 hold the convert-and-share +workflow's partial sub-cases and are the first beta items. + +Hours are Opus 5.5 agent wall-clock hours. "Doc" names the parity document the gap belongs to. +When a gap closes, delete its entry, note it in the [ROADMAP.md](../ROADMAP.md) progress log and +update the numbers. + +## Summary + +| # | Gap | Kind | Hours | Doc | +|---|---|---|---|---| +| 1 | Encrypted files we write don't open in Acrobat | file format | 8–15 | [file-format-parity.md](file-format-parity.md) | +| 2 | Rendering is borrowed and its fidelity unmeasured | feature, spec | 110–190 | [pdf-spec-parity.md](pdf-spec-parity.md) | +| 3 | Editing existing text is fragile on real files | feature | 100–180 | [target-app-parity.md](target-app-parity.md) | +| 4 | No printing on Windows or the web | feature, platform | 15–30 | [hardware-parity.md](hardware-parity.md) | +| 5 | Chinese UI shows missing-glyph boxes in releases | localization | 6–12 | [localization-parity.md](localization-parity.md) | +| 6 | Open-issue backlog of wrong results in shipped features | stability | 45–80 | this file | +| 7 | OCR reads only unaccented Latin | feature | 40–75 | [file-format-parity.md](file-format-parity.md) | +| 8 | Office export weak, Office import absent | file format | 55–100 | [file-format-parity.md](file-format-parity.md) | +| 9 | No Preflight; PDF/A partial; PDF/X and PDF/UA absent | standards | 65–115 | [pdf-spec-parity.md](pdf-spec-parity.md) | +| 10 | Signatures: no PKCS #11, timestamp servers or online revocation | feature, hardware | 40–70 | [pdf-spec-parity.md](pdf-spec-parity.md) | +| 11 | Accessibility: checker only, no tagging tools | feature | 40–70 | [target-app-parity.md](target-app-parity.md) | +| 12 | No measured fidelity harness against Acrobat | quality | (in 2) | [target-app-parity.md](target-app-parity.md) | +| 13 | Performance budgets and lazy loading of very large files | performance | 25–45 | this file | +| 14 | Arabic not mirrored; no Hindi, Indonesian, Korean, Vietnamese | localization | 30–50 | [localization-parity.md](localization-parity.md) | +| 15 | Forms: JavaScript object model, actions, submit, XFA depth | feature | 40–75 | [pdf-spec-parity.md](pdf-spec-parity.md) | +| 16 | UI precision gaps (multi-select move, rulers, accelerators, windows) | UI/UX | 30–60 | [ui-parity.md](ui-parity.md) | +| 17 | Certificate security and other protect gaps | feature, spec | 15–30 | [pdf-spec-parity.md](pdf-spec-parity.md) | +| 18 | Print production tools absent | feature | 25–45 | [target-app-parity.md](target-app-parity.md) | +| 19 | Preferences mostly missing | UI/UX | 15–25 | [ui-parity.md](ui-parity.md) | +| 20 | Scanners, GPU page rendering, pen pressure, Read Out Loud | hardware | 25–50 | [hardware-parity.md](hardware-parity.md) | +| 21 | Compare: no side-by-side view or filters | feature | 10–20 | [target-app-parity.md](target-app-parity.md) | +| 22 | PDF 2.0 extras, linearization, Arlington validation | spec | 20–35 | [pdf-spec-parity.md](pdf-spec-parity.md) | +| 23 | Local AI provider (summarize, ask, alt text, redaction suggestions) | AI | 20–40 | [target-app-parity.md](target-app-parity.md) | +| 24 | Checklist not diffed against Acrobat's own menus | measurement | 4–8 | [target-app-parity.md](target-app-parity.md) | + +## The gaps + +### 1. Encrypted files we write don't open in Acrobat + +- **Missing:** interoperable encryption output. A document protected with a password in PdfCraft + opens in PdfCraft, SumatraPDF, PDF24 and LibreOffice, but Adobe Reader, Acrobat and PDF-XChange + report it as damaged. +- **Evidence:** [#774](https://github.com/storytold/pdfcraft/issues/774) (2026-10-10, confirmed by a + second user). Our encryption tests round-trip through our own reader and `qpdf --check`, which + is more lenient than Acrobat. +- **Impact:** blocking. Passwords are the most common reason to touch security; a file a recipient + can't open is data loss in practice. Partial sub-case of the convert-and-share gate row (unencrypted sharing works), so not an alpha blocker; the first beta item. +- **Fix:** find the divergence (likely `/Encrypt` dictionary or R6 `/Perms`/`/OE`/`/UE` details, or + a string/stream encrypted that must not be), then add an interop suite that opens every kind of + output we write in other readers (pdf.js, MuPDF and Poppler as oracle processes; Acrobat by hand + on the owner's Mac). +- **Estimate:** 8–15 h. **Doc:** [file-format-parity.md](file-format-parity.md). + +### 2. Rendering is borrowed and its fidelity unmeasured + +- **Missing:** our own renderer (M2: `model` crate, font engine, DisplayList devices, ADR-0004), or + an owner decision to adopt another engine; and any measurement of rendering against Acrobat. +- **Evidence:** pages are drawn by vendored `hayro` 0.7 with 22 local patches to `hayro-interpret`, + 9 to `hayro`, 3 to `hayro-jbig2` and 13 to `hayro-syntax` (`vendor/README.md`); `lopdf` still + backs document inspection. All 7 rendering P0 features are `partial`. User reports: TikZ pattern + fills drawn wrong ([#794](https://github.com/storytold/pdfcraft/issues/794)), blurry text at + 1080p/1440p ([#730](https://github.com/storytold/pdfcraft/issues/730)), fixed zoom sizes differ + from Acrobat ([#739](https://github.com/storytold/pdfcraft/issues/739)). An outside author + proposes his MIT/Apache renderer as the bridge + ([#841](https://github.com/storytold/pdfcraft/issues/841)). +- **Impact:** every page a user sees. Wrong glyphs, shadings or blend modes erode trust faster than + any missing feature. +- **Fix:** first a fidelity harness (gap 12), then the renderer: own devices (150–250 h in the old + plan) or an adopted engine plus our own font and text layers. +- **Estimate:** 110–190 h (includes the harness). **Owner decision needed** on #841 vs M2. + **Doc:** [pdf-spec-parity.md](pdf-spec-parity.md). + +### 3. Editing existing text is fragile on real files + +- **Missing:** robust in-place editing of text PdfCraft didn't write: subset fonts without the + needed glyphs, CJK and right-to-left text, rotated text, lists, superscript/subscript, keeping + structure tags, find and replace in edit mode; vector and object editing; arrange and align. +- **Evidence:** `edit.text-edit` is partial (font reused or Helvetica substituted); + `edit.rtl-cjk-editing`, `edit.rotated-text-editing`, `edit.keep-tags-on-edit`, + `edit.vector-edit`, `edit.edit-object-tool` are planned. Users: + [#766](https://github.com/storytold/pdfcraft/issues/766) (editing Arabic text), + [#844](https://github.com/storytold/pdfcraft/issues/844) (can't move several elements), + [#787](https://github.com/storytold/pdfcraft/issues/787) (move images between pages), + [#791](https://github.com/storytold/pdfcraft/issues/791). +- **Impact:** "Edit PDF" is one of Acrobat Pro's top reasons to buy. Substituting Helvetica changes + how a document looks. +- **Estimate:** 100–180 h (the longest feature pole, M7). **Doc:** [target-app-parity.md](target-app-parity.md). + +### 4. No printing on Windows or the web + +- **Missing:** a Windows spooler (and printer properties), browser printing. +- **Evidence:** [#756](https://github.com/storytold/pdfcraft/issues/756): on Windows 11 the Print + dialog only saves a PDF. `crates/print/README.md`: "Not yet: Windows and web spoolers". +- **Impact:** Windows is most of Acrobat's market. Printing is a P0 workflow. +- **Estimate:** 15–30 h. **Doc:** [hardware-parity.md](hardware-parity.md). + +### 5. Chinese UI shows missing-glyph boxes in releases + +- **Missing:** a Simplified Chinese (`Hans`) UI face in release builds and a working fallback. +- **Evidence:** [#826](https://github.com/storytold/pdfcraft/issues/826), + [#688](https://github.com/storytold/pdfcraft/issues/688), + [#728](https://github.com/storytold/pdfcraft/issues/728), + [#758](https://github.com/storytold/pdfcraft/issues/758), + [#689](https://github.com/storytold/pdfcraft/issues/689) (Flatpak). `AGENTS.md` §1.1 bars Noto + CJK (Source Han rebranded), so craft-fonts needs another openly licensed Hans face. +- **Impact:** the second-largest language group sees a broken interface although the catalog is 97% + translated. +- **Estimate:** 6–12 h (font sourcing in craft-fonts plus the fallback chain). **Doc:** + [localization-parity.md](localization-parity.md). + +### 6. Open-issue backlog of wrong results in shipped features + +- **Missing:** fixes for ≈ 25 filed reports of features marked `shipped` that give wrong results, + plus triage of ≈ 220 other open issues. +- **Evidence:** 245 open issues on 2026-10-10, e.g. XFDF exchange loses polygon vertices and turns + callouts into text boxes (#809, #819), page deletion leaves null destinations (#817, #821), + automatic URL linking panics on Unicode text (#816), autosave skips new unsaved PDFs (#815), + saving ignores typing in an open paragraph (#811), automation reports success after replacing an + earlier result (#799, #800), Shift-Cmd-G goes forward (#810). +- **Impact:** every one is a user who lost trust. #816 is a crash, which outranks feature work + (`AGENTS.md` §4). +- **Estimate:** 45–80 h (≈ 0.2–0.4 h per issue). **Doc:** this file. + +### 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 + 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) + (Dutch). Acrobat ships on-device recognition for dozens of languages. +- **Impact:** scanned documents are a core Pro workflow outside English. +- **Estimate:** 40–75 h, partly blocked on openly licensed models. **Doc:** + [file-format-parity.md](file-format-parity.md). + +### 8. Office export weak, Office import absent + +- **Missing:** Word export that keeps layout; Excel and PowerPoint export; creating PDFs from Office, + HTML, web pages and PostScript; SVG/XML/EPS/PS/JPEG 2000 export. +- **Evidence:** `create.export-docx` partial; [#773](https://github.com/storytold/pdfcraft/issues/773) + (an invoice converted to Word loses its layout, gains table borders and duplicates pages); + `create.export-xlsx`, `create.export-pptx`, `create.from-office`, `create.from-html` planned. +- **Impact:** "Export PDF" and "Create PDF" are two of Acrobat's most used tools. +- **Gate:** lossy or missing sub-cases of the convert-and-share workflow (partial, not blocking). + First beta items: Create PDF from Office (LibreOffice sidecar, 10–20 h) and a Word export that + keeps layout (20–35 h). +- **Estimate:** 55–100 h. **Doc:** [file-format-parity.md](file-format-parity.md). + +### 9. No Preflight; PDF/A partial; PDF/X and PDF/UA absent + +- **Missing:** Preflight (profiles, single checks and fixups, results tree, reports, droplets); + PDF/A-1, PDF/A-4 and conformance levels a/u; PDF/X verify and convert; PDF/UA-1/2 verify; PDF/E, + PDF/VT; output-intent choice; transparency flattening. +- **Evidence:** 12 Preflight features and 9 standards features planned; `optimize.pdfa-validate`, + `optimize.pdfa-convert` partial (2b/3b structural rules; CMYK documents get no output intent). + Acrobat ships hundreds of built-in Preflight profiles. +- **Impact:** blocks print shops, archives and accessibility compliance work. +- **Estimate:** 65–115 h. **Doc:** [pdf-spec-parity.md](pdf-spec-parity.md). + +### 10. Signatures: no PKCS #11, timestamp servers or online revocation + +- **Missing:** PKCS #11 tokens and smart cards on macOS and Linux; timestamp-server preferences; + fetching OCSP/CRL online for LTV; FieldMDP and locking fields on signing; the OS trust store; EU + Trusted Lists; signature graphics and saved appearances; compare signed version; signing in the + browser. +- **Evidence:** `sign.pkcs11`, `sign.timestamp-servers`, `sign.field-mdp`, `sign.os-trust`, + `sign.eutl` planned; `sign.pades-bt`, `sign.ltv`, `sign.ocsp-crl` partial (caller-supplied + evidence). Users ask for Acrobat-like validation + ([#824](https://github.com/storytold/pdfcraft/issues/824), + [#775](https://github.com/storytold/pdfcraft/issues/775), + [#718](https://github.com/storytold/pdfcraft/issues/718)). +- **Impact:** government and legal users sign with tokens and need B-LT/B-LTA. +- **Estimate:** 40–70 h; tokens need hardware to test. **Doc:** [pdf-spec-parity.md](pdf-spec-parity.md). + +### 11. Accessibility: checker only, no tagging tools + +- **Missing:** autotag (document and fields), Tags, Order and Content panels, tag properties, + Reading Order tool, artifact marking, table editor, role map, Make Accessible action, Read Out + Loud; full keyboard operation of PdfCraft itself and a screen-reader audit. +- **Evidence:** all 32 checker rules shipped, but 21 tagging and reading features planned; + `a11y.keyboard-only` (P0) planned, `a11y.app-screen-reader` partial. +- **Impact:** remediation work is impossible; PdfCraft itself is not yet usable without a mouse. +- **Estimate:** 40–70 h. **Doc:** [target-app-parity.md](target-app-parity.md). + +### 12. No measured fidelity harness against Acrobat + +- **Missing:** side-by-side comparison of rendering, text extraction, form calculation and + signature validation against Acrobat on synthetic fixtures. +- **Evidence:** none exists; only text extraction has an oracle (word-F1 0.98 against `pdftotext`). + Acrobat captures must stay local in `plan/acrobat/` (clean-room, `AGENTS.md` §1.1). +- **Impact:** until it exists, every quality number here is a judgement. +- **Estimate:** counted in gap 2 (≈ 20–30 h of it). **Doc:** [target-app-parity.md](target-app-parity.md). + +### 13. Performance budgets and lazy loading of very large files + +- **Missing:** `misc.performance-budgets` (P0) and `core.lazy-loading` (P0): open GB files within a + memory budget, with budgets checked on Tier-1 platforms. +- **Evidence:** #307 brought a 9,156-page file from > 10 GiB to ≈ 500 MiB inspection, but + file-backed input and global memory budgets remain open; blurry fonts at common resolutions + (#730). +- **Estimate:** 25–45 h. **Doc:** this file. + +### 14. Arabic not mirrored; no Hindi, Indonesian, Korean, Vietnamese + +- **Missing:** a right-to-left layout (panels, menus, alignment), bidirectional text in the text + layout itself (wrapping, typed text); catalogs for Hindi, Indonesian, Korean and Vietnamese + (Acrobat ships Korean; it ships Arabic in its Middle East edition; it ships none of the other + three). +- **Evidence:** [localization.md](localization.md) §Arabic; `misc.rtl-ui` planned. +- **Estimate:** 30–50 h (Arabic RTL 20–35, four catalogs 10–15 plus review). **Doc:** + [localization-parity.md](localization-parity.md). + +### 15. Forms: JavaScript object model, actions, submit, XFA depth + +- **Missing:** the wider Acrobat JavaScript object model (annotations, layers, dialogs), document + actions, the Actions tab triggers that run JavaScript, form submission, the debugger, barcode + fields, rich-text values; XFA flattening and complete dynamic-form fidelity. +- **Evidence:** `form.js-host` partial, `form.document-actions`, `form.submit-form`, + `edit.action-javascript`, `form.js-debugger`, `form.xfa-flatten` planned; XFA layout, FormCalc + and data partial. +- **Estimate:** 40–75 h. **Doc:** [pdf-spec-parity.md](pdf-spec-parity.md). + +### 16. UI precision gaps + +- **Missing:** moving several selected objects together (#844), nudging with arrow keys, single-key tool accelerators, + rulers, grids and guides, loupe and dynamic zoom, multiple windows and split view, comment + pop-ups that can be moved, connector lines, customisable quick tools, column text selection + (#740); shortcut display and touchpad scrolling bugs (#746, #759, #721), macOS window controls + (#789). +- **Estimate:** 30–60 h. **Doc:** [ui-parity.md](ui-parity.md). + +### 17. Certificate security and other protect gaps + +- **Missing:** encrypting for certificate recipients (and opening such files), security policies, + attachment-only encryption, redaction pattern locales and partial-word redaction, folder + redaction, protected view, URL and attachment trust. +- **Estimate:** 15–30 h. **Doc:** [pdf-spec-parity.md](pdf-spec-parity.md). + +### 18. Print production tools absent + +- **Missing:** Output Preview, separations, total area coverage, overprint preview, ink manager, + colour conversion, printer marks, fix hairlines, flattener preview, colour management settings; + print as image; PostScript output. +- **Evidence:** 18 print-production features planned (L area). +- **Estimate:** 25–45 h (part of the L area's 30–55). **Doc:** [target-app-parity.md](target-app-parity.md). + +### 19. Preferences mostly missing + +- **Missing:** most of Acrobat's twenty or so preference pages (General, Documents, Commenting, Forms, Identity, + Signatures, Security, Measuring, Units and guides, Convert, …). Today: Interface language, + Documents and view, Identity, Fill & Sign, JavaScript. +- **Estimate:** 15–25 h. **Doc:** [ui-parity.md](ui-parity.md). + +### 20. Scanners, GPU page rendering, pen pressure, Read Out Loud + +- **Missing:** TWAIN/WIA/ICA/SANE scanning; GPU-accelerated page rendering (pages are rasterized on + the CPU and uploaded); pressure-sensitive ink; platform text-to-speech. +- **Estimate:** 25–50 h; scanners need hardware. **Doc:** [hardware-parity.md](hardware-parity.md). + +### 21. Compare: no side-by-side view or filters + +- **Missing:** synchronised side-by-side view, filters by change type, page ranges, scanned + documents. +- **Estimate:** 10–20 h. **Doc:** [target-app-parity.md](target-app-parity.md). + +### 22. PDF 2.0 extras, linearization, Arlington validation + +- **Missing:** UTF-8 text strings, associated files (`/AF`), document parts, structure namespaces; + linearized save (Fast Web View); deterministic save; structural validation against the Arlington + model. +- **Estimate:** 20–35 h. **Doc:** [pdf-spec-parity.md](pdf-spec-parity.md). + +### 23. Local AI provider + +- **Missing:** the opt-in provider interface (local model or the user's own endpoint) and the + features on it: summarize, ask with page citations, alternate-text and redaction suggestions. + Acrobat's AI Assistant is a cloud service and out of scope. +- **Estimate:** 20–40 h. **Doc:** [target-app-parity.md](target-app-parity.md). + +### 24. Checklist not diffed against Acrobat's own menus + +- **Missing:** a menu and tool listing captured from Acrobat (names only, `plan/acrobat/`, never + committed) diffed against the checklist, so that features the checklist never listed show up. + Also correct `misc.localization` and `misc.installers`, which are still `planned` though both + ship. +- **Estimate:** 4–8 h. **Doc:** [target-app-parity.md](target-app-parity.md). + +## Revision history + +| Date | Change | Summary | +|---|---|---| +| 2026-10-10 | minor | Removed the alpha markers: with the cross-app gate rule, gaps 1 and 8 are partial sub-cases, not blockers; stage alpha | +| 2026-10-10 | minor | Marked the alpha blockers (gap 1 and the Office part of gap 8) after the core-workflow gate put the stage at pre-alpha | +| 2026-10-10 | major | Created. Ranked 24 gaps from the 2026-10-10 re-measure, the open GitHub issues and the former ROADMAP.md §Where we're lacking and where we're going (renderer, hardening, fidelity, editing, Pro workflows, 1.0 polish), which this file replaces | diff --git a/docs/hardware-parity.md b/docs/hardware-parity.md new file mode 100644 index 000000000..1e68f0bf8 --- /dev/null +++ b/docs/hardware-parity.md @@ -0,0 +1,43 @@ +# Hardware and platform parity + +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (first hardware and platform matrix) · **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, +Linux, FreeBSD and the web. Gaps ranked in [gaps.md](gaps.md). + +**Hardware ≈ 35% ready, 25–50 h** (additional to the features total; scanners, smart cards and +printing are counted in their feature areas). **Platforms ≈ 70% ready, 15–30 h.** + +## Hardware + +| Feature | Acrobat (macOS / Windows) | PdfCraft macOS | PdfCraft Windows | PdfCraft Linux / FreeBSD | PdfCraft web | Evidence | +|---|---|---|---|---|---|---| +| GPU page rendering | yes (Page Display ▸ GPU-accelerated rendering) | no: CPU raster, GPU for UI compositing (wgpu, Metal) | no (wgpu DX12, GL fallback) | no (wgpu Vulkan/GL) | no (WebGL/WebGPU UI) | `crates/render`; gap 20 | +| Multi-threaded rendering | yes | yes (render pool, tiles, watchdog) | yes | yes | single thread | `view.render-priority` | +| HiDPI / Retina | yes | yes | yes, per-monitor DPI v2 (#324, #725) | yes | yes | text blurry at 1080p/1440p (#730) | +| Multiple monitors | yes, multiple windows | one window | one window; surfaces fit the GPU limit (#577) | one window | n/a | `view.multiple-windows` planned | +| Printers | all, PostScript, printer properties | CUPS spooler | **none** (#756) | CUPS spooler | **none** | `crates/print/README.md` | +| Scanners (TWAIN, WIA, ICA) | yes | none | none | none (SANE) | n/a | `ocr.scanner-acquire` planned | +| Smart cards and USB tokens | yes (PKCS #11, CryptoTokenKit, CNG) | Keychain identities (software keys) | CNG store keys, with PIN prompts for cards (#742) | none (PKCS #11 planned) | none | `sign.pkcs11` planned | +| Pen and touch | Touch mode, pen ink, Windows Ink | trackpad pinch zoom; ink from pointer, no pressure | ink from pointer, no pressure or touch mode | as macOS | pointer | touchpad scrolling not 1:1 (#759) | +| Keyboard input methods (IME) | yes | yes; Japanese and Korean IME fixes patched into winit | yes (Microsoft Korean cursor fix) | X11 and Wayland IME fixes | browser | `vendor/README.md` winit patches 2–10 | +| Screen readers | VoiceOver, NVDA, JAWS | AccessKit labels (partial) | AccessKit (partial) | AccessKit (partial) | partial | `a11y.app-screen-reader` partial | +| Text to speech (Read Out Loud) | yes | none | none | none | none | `a11y.read-aloud` planned | +| Apple Silicon / ARM64 | native | native | ARM64 build (#61) | ARM64 builds | n/a | `release.yml` | + +## Platforms + +| Platform | Acrobat | PdfCraft | Packaging | Gaps | +|---|---|---|---|---| +| macOS | yes | yes | signed and notarized DMG | — | +| Windows (x64, x86, ARM64) | yes (x64, ARM64) | yes | signed installers | printing (#756) | +| 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 | printing, OCR, crash recovery, PKCS #12 signing, hosted deployment | + +## Revision history + +| Date | Change | Summary | +|---|---|---| +| 2026-10-10 | major | Created from the code, `vendor/README.md`, `.github/workflows/release.yml`, the print crate's README and user issues | diff --git a/docs/localization-parity.md b/docs/localization-parity.md new file mode 100644 index 000000000..0ea3daa7e --- /dev/null +++ b/docs/localization-parity.md @@ -0,0 +1,72 @@ +# Localization parity + +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (first per-language status, measured from the catalogs) · **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`, counted + as distinct `(context, English)` keys with a non-empty translation. The denominator is the union + of keys across all catalogs, **2,143**. Cross-check: of the 1,151 distinct `tl!("…")` literals in + `crates/ui-egui/src`, every full catalog covers 96.5–99.4% (7 literals are in no catalog yet). + English is built in. +- **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). +- **Script support** comes from [localization.md](localization.md), the vendored winit IME patches + (`vendor/README.md`) and user reports. +- **Native review**: "community" means a native speaker contributed or corrected the catalog in a + pull request or issue; "no" means no record of review. No catalog has had a formal review. +- **Acrobat** ships 24 interface localizations on macOS (`.lproj`: cs, da, de, en, es, fi, fr, hu, + it, ja, ko, nl, no, pl, pt, ru, sv, tr, uk, zh_CN, zh_TW, plus en_AE, en_IL, fr_MA) and Middle + East / North Africa editions (MEA Arabic, MEH Hebrew, NAF French; `Resources/Sequences/`). + +## The twelve key languages + +| Language | Code | UI strings translated | 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, but **release builds have no Hans face**: many characters show as boxes (#826, #688, #728, #758; Flatpak #689) | no | partial | yes | 6–12 | +| Spanish | `es` | 2,046 (95.5%) | 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); no Devanagari face in craft-fonts | no | none | no | 10–18 | +| Arabic | `ar` | 2,143 (100%) | dialogs and tooltips; no help | Letters joined and lines reordered at load, but **layout not mirrored**, wrapped lines break at the wrong end, typed text runs left to right (`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,130 (99.4%) | 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` | 0 | none | Latin with stacked diacritics; bundled Inter covers them (not tested) | no | none | no | 3–5 | + +**Localization dimension ≈ 59%**: 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 the four missing +languages. **≈ 60–115 h** to bring all twelve to `full`, of which ≈ 35–60 h is engineering (Hans +face, RTL layout, 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. + +## Other shipped languages + +| Language | Code | UI strings translated | 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` | 135 (6.3%) | menus only | 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,130 (99.4%) | partial | yes | + +**8 other languages** ship (15 catalogs plus English, 16 interface languages in Preferences). +Acrobat languages PdfCraft lacks: Danish, Dutch, Finnish, Norwegian, Polish, Swedish, Turkish, +Hebrew. + +## Revision history + +| Date | Change | Summary | +|---|---|---| +| 2026-10-10 | major | Created. Catalog counts measured from `crates/ui-egui/src/i18n/*.tsv` against the 2,143-key union; Acrobat's localizations from its bundle's `.lproj` and Sequences folder names | diff --git a/docs/localization.md b/docs/localization.md index de74c4e8a..31fec64c1 100644 --- a/docs/localization.md +++ b/docs/localization.md @@ -2,6 +2,8 @@ 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** or **العربية**. The change applies immediately and persists between launches. Command ids, document contents and file names are unchanged. +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. + **Auto** (the default) follows the system language: `LC_ALL`, `LC_MESSAGES` or `LANG`, then the preferred-languages list on macOS. Any Portuguese locale (`pt_BR`, `pt_PT`) uses the Brazilian catalog, any French locale (`fr`, `fr_FR`, `fr_CA`, `fr_BE`) the French one, any German locale (`de`, `de_DE`, `de_AT`, `de_CH`) the German one, and Chinese locales for Taiwan, Hong Kong and Macau (`zh_TW`, `zh_HK`, `zh_MO`, `zh-Hant`) the Traditional Chinese one; other Chinese locales (`zh`, `zh_CN`, `zh_SG`, `zh-Hans`) the Simplified Chinese one. Telugu locales (`te`, `te_IN`) use the Telugu catalog, Bulgarian locales (`bg`, `bg_BG`) the Bulgarian one, and Hungarian locales (`hu`, `hu_HU`) the Hungarian one, and Ukrainian locales (`uk`, `uk_UA`) the Ukrainian one, and Italian locales (`it`, `it_IT`, `it_CH`) the Italian one, and Arabic locales (`ar`, `ar_MA`, `ar_EG`, `ar_SA`, …) the Arabic one. A system language without a catalog shows English. On Windows, **Auto** uses the Windows display languages, in preference order, when no supported locale is selected by those environment variables. Neutral `C`/`POSIX` locale values (including `C.UTF-8`) inherited from a launcher do not override the Windows display language; explicitly named supported locales such as `fr_FR.UTF-8` still do. The query asks Windows directly (`GetUserPreferredUILanguages`, through the `sys-locale` crate) without starting a process, and is cached for the process. If detection fails or the display language has no catalog, PdfCraft shows English. The control channel exposes the setting through `ui.set`: @@ -12,15 +14,13 @@ The control channel exposes the setting through `ui.set`: The value is `auto` or a language code (`en`, `ja`, `zh-hans`, `zh-hant`, `ru`, `bg`, `cs`, `pt-br`, `de`, `es`, `fr`, `te`, `hu`, `uk`, `it`, `ar`), in any case. `ui.state` reports `language` as written in the preferences (`auto`, `en`, `ja`, `zh-hans`, `zh-hant`, `ru`, `bg`, `cs`, `pt-br`, `de`, `es`, `fr`, `te`, `hu`, `uk`, `it` or `ar`). Unknown values return an error without changing the current setting. Preferences saved before this setting existed follow the system language. -Dialogs, panels, menus, notices and history labels go through the catalog (`tl!`). Japanese, Traditional Chinese, Simplified Chinese, Russian, Bulgarian, German, Spanish, French, Telugu, Hungarian, Ukrainian, Italian and Arabic translate them (about 1,800 entries each); Czech and Brazilian Portuguese cover the menus so far. Untranslated labels use English. Error details that come from the engine or the operating system are shown as they are, inside a translated frame ("操作「…」に失敗しました: …"). The command palette matches the translated label, the English label and the command id. Vertical Japanese PDF rendering is an existing viewer feature; this change does not add vertical text editing. -Dialogs, panels, menus, notices and history labels go through the catalog (`tl!`). Japanese, Traditional Chinese, Simplified Chinese, Russian, Bulgarian, German, Spanish, French, Telugu, Hungarian, Ukrainian and Brazilian Portuguese translate them (about 1,800 entries each); Czech covers the menus so far. Untranslated labels use English. Error details that come from the engine or the operating system are shown as they are, inside a translated frame ("操作「…」に失敗しました: …"). The command palette matches the translated label, the English label and the command id. Vertical Japanese PDF rendering is an existing viewer feature; this change does not add vertical text editing. +Dialogs, panels, menus, notices and history labels go through the catalog (`tl!`). Japanese, Traditional Chinese, Simplified Chinese, Russian, Bulgarian, German, Spanish, French, Telugu, Hungarian, Ukrainian, Italian, Brazilian Portuguese and Arabic translate them (about 2,000–2,140 entries each); Czech covers the menus so far. Untranslated labels use English. Error details that come from the engine or the operating system are shown as they are, inside a translated frame ("操作「…」に失敗しました: …"). The command palette matches the translated label, the English label and the command id. Vertical Japanese PDF rendering is an existing viewer feature; this change does not add vertical text editing. Per-language coverage, script support and what each language still needs are in [localization-parity.md](localization-parity.md). The Ukrainian catalog has 2,047 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. Japanese interface text uses BIZ UDPGothic from [craft-fonts](https://github.com/storytold/craft-fonts), an optional build input that every release includes (`CRAFT_FONTS_DIR`; see the README). A build made without it has no Japanese face, so Japanese labels show replacement boxes. Chinese is drawn with the same craft-fonts CJK faces (in Simplified Chinese the `Hans` faces come first, so a line never mixes faces with different baselines), so a character none of them has shows a replacement box (and every Chinese character does in a build without craft-fonts). Russian, Bulgarian, Czech, Brazilian Portuguese, German, Spanish, French, Hungarian and Italian need only the bundled Latin faces (tested in `tests/fonts.rs`). Hungarian uses the same one/other plural rule as English: 1 is singular, and every other count — including 0 — is the other form. After a numeral the noun stays singular (`1 oldal`, `2 oldal`). Telugu is drawn with a craft-fonts `Telu` face (Noto Sans Telugu, kept in the web build too), whose conjuncts egui shapes; a build without one shows Telugu labels as replacement boxes, and the release-pinned craft-fonts input has no Telugu face yet. -The Brazilian Portuguese catalog has 2,000 entries: the ~1,826-entry gist André Cardoso posted in [#288](https://github.com/storytold/pdfcraft/issues/288), the labels added since, and a review of the existing wording. Portuguese, like French, treats 0 and 1 as singular (`1 página`, `2 páginas`). -Japanese interface text uses BIZ UDPGothic from [craft-fonts](https://github.com/storytold/craft-fonts), an optional build input that every release includes (`CRAFT_FONTS_DIR`; see the README). A build made without it has no Japanese face, so Japanese labels show replacement boxes. Chinese is drawn with the same craft-fonts CJK faces (in Simplified Chinese the `Hans` faces come first, so a line never mixes faces with different baselines), so a character none of them has shows a replacement box (and every Chinese character does in a build without craft-fonts). Russian, Bulgarian, Czech, Brazilian Portuguese, German, Spanish, French and Hungarian need only the bundled Latin faces (tested in `tests/fonts.rs`). Hungarian uses the same one/other plural rule as English: 1 is singular, and every other count — including 0 — is the other form. After a numeral the noun stays singular (`1 oldal`, `2 oldal`). Telugu is drawn with a craft-fonts `Telu` face (Noto Sans Telugu, kept in the web build too), whose conjuncts egui shapes; a build without one shows Telugu labels as replacement boxes, and the release-pinned craft-fonts input has no Telugu face yet. +The Brazilian Portuguese catalog has 2,000 entries: the ~1,826-entry gist André Cardoso posted in [#288](https://github.com/storytold/pdfcraft/issues/288), the labels added since, and a review of the existing wording. Portuguese, like French, treats 0 and 1 as singular (`1 página`, `2 páginas`). ## Arabic and right-to-left text diff --git a/docs/pdf-spec-parity.md b/docs/pdf-spec-parity.md new file mode 100644 index 000000000..6c74039f5 --- /dev/null +++ b/docs/pdf-spec-parity.md @@ -0,0 +1,132 @@ +# PDF specification and standards parity + +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (first spec-coverage checklist) · **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: +the parts of the format users never see named but always feel. Feature-level detail is in +`parity/acrobat-features.toml`; the gaps are ranked in [gaps.md](gaps.md). + +Legend: **own** (our code), **borrowed** (the vendored `hayro` bootstrap or `lopdf`), **partial**, +**none**. "Evidence" names tests or checklist ids. + +**PDF specification and standards ≈ 60% ready** (estimated): syntax, encryption, annotations, +AcroForm, redaction and signatures are our own and well tested; graphics, fonts and image codecs +are borrowed; PDF 2.0 additions and most subset standards are missing. **150–260 h** to parity +(within the features total; most of it is the renderer and font engine, and Preflight). + +## ISO 32000-2 by clause + +| Clause | Topic | Acrobat | PdfCraft | Evidence / what's missing | +|---|---|---|---|---| +| §7.2–7.5 | Syntax, objects, file structure, xref tables and streams, object streams, hybrid files, incremental updates | full | **own**, tolerant, repairs damaged files | `crates/cos`; corpus 958 files; fuzzed nightly | +| §7.4 | Stream filters: Flate, LZW, ASCII85, ASCIIHex, RunLength, predictors | full | **own** | `crates/filters` | +| §7.4 | Image filters: DCT, JPX, JBIG2, CCITTFax | full | **borrowed** (decode only) | `core.image-filters` partial; own codecs M1.1/M1.2; no JBIG2/CCITT encoding (Optimizer) | +| §7.5.8 | Linearization | R W | R only | `core.linearization` planned | +| §7.6.4 | Standard security handler R2–R6 (RC4 40/128, AES-128, AES-256), crypt filters, `/EncryptMetadata`, SASLprep | full | **own**, read and write | `crates/crypt`; **written files rejected by Acrobat (#774)** | +| §7.6.5 | Public-key security handler | full | none | `core.open-public-key`, `protect.certificate-encryption` planned | +| §7.6.2 | Unencrypted signature `/Contents` | yes | yes | signing encrypted documents (#753) | +| §7.9.2 | Text strings incl. PDF 2.0 UTF-8 | full | PDFDocEncoding and UTF-16; UTF-8 none | `core.pdf20-utf8-strings` planned | +| §7.11 | File specifications, embedded files | full | read and save attachments; no add/delete/describe | `organize.attachments-*` planned | +| §7.12 | Extensions dictionary | yes | preserved | | +| §8 | Graphics: paths, colour spaces, patterns, shadings, images, XObjects | full | **borrowed** renderer; own content parser and serializer (`crates/content`) for editing and redaction | rendering P0s partial | +| §8.6 | ICC-based colour, output intents, colour management | full | ICC preserved; sRGB output intent for PDF/A; no colour management settings or soft proofing | `print.color-management` planned | +| §9 | Text and fonts: Type 1, TrueType, OpenType/CFF, Type 3, CID-keyed, CMaps, ToUnicode | full | **borrowed** rendering; own text extraction (word-F1 0.98); standard-14 metrics for generated appearances; embedded fonts reused for editing, else Helvetica | `view.render-type3`, `view.render-cid-fonts` partial; no own font engine; no font embedding/subsetting for arbitrary new text beyond craft-fonts Japanese | +| §10 | Rendering: transfer functions, halftones, overprint, smoothness | full | borrowed; no overprint preview | `print.overprint-preview` planned | +| §11 | Transparency: blend modes, soft masks, groups, knockout | full | borrowed (knockout shipped) | `view.render-transparency` partial | +| §12.1–12.3 | Viewer preferences, page layout, page labels, outlines, destinations | full | **own**: initial view, page labels, bookmarks edit and from structure, named destinations rewired on copy | named-destination editing panel planned | +| §12.4 | Page transitions, thumbnails, articles, presentations | full | none (thumbnails discarded or kept) | `organize.page-transitions`, `view.articles-panel` planned | +| §12.5 | Annotations | 28 types | see table below | | +| §12.6 | Actions | 19 types | see table below | | +| §12.7 | Interactive forms (AcroForm): fields, appearances, calculation order, JavaScript actions | full | **own**: all field types, regenerated appearances, Acrobat's AF functions and event order, sandboxed JavaScript (boa) | `form.js-host` partial (object-model subset); submit planned | +| §12.7.8 | XFA (deprecated in PDF 2.0; Acrobat still fills XFA) | static and dynamic | **own**, partial: template layout, FormCalc, XFA JavaScript model, datasets | `form.xfa-*` partial; no flattening | +| §12.8 | Digital signatures | see table below | | | +| §12.9 | Measurement and geospatial | full | 2D measuring with viewports and calibration; no geospatial | `comment.geospatial` planned | +| §12.10 | Document requirements | yes | none | | +| §13 | Multimedia: sound, movies, screen, rich media, 3D (U3D, PRC) | yes | preserved untouched; not played or shown | `misc.rich-media-*`, `misc.3d-view` planned (P3) | +| §14.3 | Metadata: info dictionary, XMP | full, XMP editor | info dictionary edited; XMP kept in step for PDF/A; no XMP editor or custom properties | `organize.xmp-editor` planned | +| §14.6–14.8 | Marked content, logical structure, tagged PDF | full, with editing tools | read (checker, bookmarks from structure, redaction of tags); no tag editing or autotag | gap 11 | +| §14.9 | Accessibility support: alt text, ActualText, language | full | alt text figure by figure, language, title | `a11y.alt-text-workflow` | +| §14.10 | Web capture | yes | none | | +| §14.11 | Prepress: page boxes, output intents, trapping, OPI | full | page boxes and crop; sRGB output intent | `print.*` production planned | +| §14.12 | Document parts (DPart) | yes | none | `core.pdf20-dpart` planned | +| §14.13 | Associated files (`/AF`) | yes | none | `core.pdf20-associated-files` planned | +| — | Structural validation (Arlington PDF model) | yes (Preflight syntax checks) | none | `core.arlington-validation` planned | +| — | Optional content (layers) | full | **own** toggle and default state; set-OCG-state actions; no layer editing | `view.layers-panel` | +| — | Redaction (`/Redact` annotations, applying) | full | **own**: removes glyphs, image pixels, vectors, XObject content, annotations, fields and tags; verifies no residue | `protect.redact-*`, `protect.redaction-verification` | + +## Annotations (§12.5.6) + +| Type | Acrobat | PdfCraft | Notes | +|---|---|---|---| +| Text (note), Popup | create, edit | create, edit; pop-ups not movable windows | `comment.popups` planned | +| Link | create, edit | create, edit, from URLs | `edit.link-*` | +| FreeText (text box, typewriter, callout) | create, rich text | create; rich text written but no per-run formatting | `comment.text-box` partial | +| Line, Square, Circle, Polygon, PolyLine | create | create, all ten line endings | | +| Highlight, Underline, Squiggly, StrikeOut | create | create | | +| Caret (insert/replace text) | create | create; not snapped to text | `comment.insert-text` partial | +| Stamp | create, custom, dynamic | create, custom, dynamic, Sign Here | stamp management planned | +| Ink | create, erase | create, erase | no pressure | +| FileAttachment | create | create | | +| Sound | create, play | preserved | P3 | +| Redact | create, apply | create, apply | own | +| Widget | create, edit | create, edit | AcroForm | +| Watermark | create | page-content watermarks (as Acrobat writes them); `/Watermark` annotations preserved | | +| Screen, Movie, RichMedia, 3D | create, play | preserved | P3 | +| PrinterMark, TrapNet | create (print production) | preserved | | +| Projection | create (PDF 2.0) | preserved | | + +## Actions (§12.6.4) + +| Action | Acrobat | PdfCraft | +|---|---|---| +| GoTo | yes | yes (page with Fit; named destinations and zoom partial) | +| GoToR, GoToE | yes | none (preserved; the Optimizer can remove invalid ones) | +| Launch | yes (restricted) | none | +| Thread | yes | none | +| URI | yes | yes | +| Sound, Movie, Rendition, GoTo3DView, RichMediaExecute | yes | none (P3) | +| Hide | yes | yes | +| Named | yes | safe list (print, page navigation) | +| SubmitForm | yes | explained, not sent | +| ResetForm | yes | yes | +| ImportData | yes | via Import form data, not as an action | +| JavaScript | yes | yes in fields and buttons (sandboxed); document actions none | +| SetOCGState | yes | yes | +| Trans | yes | none | + +## Digital signatures (§12.8, ETSI EN 319 142 PAdES) + +| Capability | Acrobat | PdfCraft | +|---|---|---| +| Sub-filters `adbe.pkcs7.detached`, `ETSI.CAdES.detached`; read `adbe.pkcs7.sha1`, `adbe.x509.rsa_sha1` | yes | yes | +| PAdES B-B | yes | yes (PKCS #12, macOS Keychain, Windows store with PIN) | +| PAdES B-T (signature timestamp), document timestamps | yes, with configured TSA | partial: works when a TSA is given; no timestamp-server preferences | +| PAdES B-LT, B-LTA (DSS/VRI, OCSP/CRL) | yes, fetched online | partial: evidence supplied by the caller; no online fetching | +| DocMDP certification | yes | yes | +| FieldMDP (lock fields) | yes | none | +| UR3 usage rights | preserved, Reader-extending is Adobe-only | none (P3) | +| Seed values | yes | none | +| Validation: chain, key usage, EKU, critical extensions, changes after signing, signed version | yes | yes; name constraints and issuer critical extensions missing | +| Trust: own store, OS store, AATL, EUTL | yes | own store only; AATL out of scope; OS and EUTL planned | +| Smart cards and tokens (PKCS #11, CryptoTokenKit) | yes | Windows CNG store (with PIN prompt) only | + +## Subset standards + +| Standard | Acrobat | PdfCraft | Hours | +|---|---|---|---| +| 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/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 | | +| Factur-X / ZUGFeRD (e-invoice in PDF/A-3) | Preflight checks | none | | +| **All subset standards** | | | **65–115** (gap 9) | + +## Revision history + +| Date | Change | Summary | +|---|---|---| +| 2026-10-10 | major | Created from the code, `vendor/README.md` and `parity/acrobat-features.toml`, against ISO 32000-2 and Acrobat Pro 26.002.21931's documented capabilities | diff --git a/docs/releasing.md b/docs/releasing.md index 06b1c2ce6..8f88a4684 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -204,3 +204,44 @@ unsigned and the run shows a warning. a PowerShell parse, xmllint (WiX, plist, MIME), `desktop-file-validate`, `appstreamcli validate`, and the MSI icon-id check. The FreeBSD and Windows ARM64 workflows also exercise their packaging before a release does. + +## Scanner detections + +Antivirus and reputation scanners sometimes flag a released file. Check the facts before you change +code or answer publicly. The first report was [issue #747](https://github.com/storytold/pdfcraft/issues/747), +a match of the `CobaltStrikeBeacon` family rule. + +1. **Confirm the file is the release.** Compare its SHA-256 with `SHA256SUMS.txt` on the GitHub + Release. Only a file whose hash matches the release says anything about that release. +2. **Record the signature.** `packaging/windows/security-audit.ps1` prints the Authenticode status: + - `Valid`: the signature verifies. + - `NotSigned`: no signature, as in developer and test builds. Not evidence of compromise by itself. + - `HashMismatch`: the file changed after it was signed. Investigate. + - `NotTrusted` or `UnknownError`: a signature exists but did not validate on this machine. Compare + the signer and the hash with the release. +3. **Extract the payloads without running them.** Scan the executables inside the MSI and the + portable zip, not only the outer installer. `7z x -oDIR pdfcraft--windows-x64.msi` + extracts them. The payload names are WiX-internal, so tell the executables apart by their file + type. Don't install or run the MSI or the extracted files. +4. **Scan with YARA and keep every match.** Use your own copies of YARA and the rule. PdfCraft + bundles neither, and the helper never runs in the app, the installer or CI: + + ```powershell + pwsh packaging/windows/security-audit.ps1 -Path .\pdfcraft.exe, .\pdfcraft-cli.exe -YaraExe C:\tools\yara64.exe -YaraRule C:\rules\CobaltStrikeBeacon.yar + ``` + + The output lists each matched identifier with its offset, and the SHA-256 of the rule file. + Record where the rule came from and its revision. For CAPEv2's `CobaltStrikeBeacon` rule, that is + the commit that last changed `data/yara/CAPE/CobaltStrikeBeacon.yar`. +5. **Find the condition branch.** Family rules usually have alternative branches, such as + `all of ($x*) or 2 of ($a*) or ...`. Copy the rule, make one rule per branch with only the strings + it uses, and scan again to see which branch fired. +6. **Interpret with care.** A match on generic repeated-byte patterns says much less than a match on + distinctive code or strings, and it does not show that the software belongs to the family. Rules + change too: CAPEv2 commit `9b599185` (2024-12-05) lengthened this rule's two repeated-byte patterns + from 4 to 16 bytes to reduce false positives, so an older rule revision can match files that the + current one does not. Don't alter PdfCraft's binaries to change a scanner's result. That changes + nothing about the software. +7. **Report.** To the scanner, send the file's SHA-256, the release URL, the rule name and revision, + and the matched identifiers and offsets. Report PdfCraft problems at + with the same facts. diff --git a/docs/roadmap.md b/docs/roadmap.md new file mode 100644 index 000000000..216270a7a --- /dev/null +++ b/docs/roadmap.md @@ -0,0 +1,127 @@ +# PdfCraft roadmap: milestones and what's next + +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (localized shortcut labels, #496; alpha gate added; convert-and-share re-judged with the cross-app rule → alpha) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) + +Forward-looking: the milestones, the current focus and what comes next. The parity numbers behind +it are in [target-app-parity.md](target-app-parity.md), the ranked work list in [gaps.md](gaps.md), +and the one-page summary in [ROADMAP.md](../ROADMAP.md). Detailed task lists and acceptance tests +live in `plan/execution-plan.md` (local-only, gitignored). + +Keep it current: update the milestone row and the [ROADMAP.md](../ROADMAP.md) progress log at the +end of every session; re-estimate when a milestone lands. + +## Alpha gate + +The stage standard's gate: Acrobat Pro's everyday core workflows must each work end to end on the +main platform (macOS), with the work saved and reopened. Any `no`, or a `partial` that blocks the +workflow, keeps PdfCraft pre-alpha; those rows are the alpha checklist. Assessed 2026-10-10 with the +rule applied across the Crafting Apps: a row blocks only when a typical user cannot complete the +workflow at all with the app on its main platform for typical inputs; degraded fidelity, lossy +exchange with Acrobat users or a missing sub-case is `partial, not blocking` (a beta item). + +| Core workflow | Works end to end? | Evidence | Hours to pass | +|---|---|---|---| +| View, search and print | yes | Render, find, select, panels, tiles; 500-page scrolling test; Print with CUPS on macOS and Linux (Windows printing missing, #756, a beta item). Rendering borrowed from `hayro` | 0 | +| Organize, combine and split pages | yes | `organize.*` P0s shipped with tests; links, fields, layers and bookmarks carried over; open fixes #817, #821 are edge cases | 0 | +| Comment and review | yes | Every markup type with appearances, replies, status, XFDF/FDF; comments saved in the PDF reopen in other readers. XFDF fidelity bugs #809, #819 | 0 | +| Fill and sign forms | yes | All AcroForm field types, Acrobat's AF functions, Fill & Sign, PAdES signatures validated with `pdfsig`/OpenSSL | 0 | +| Edit text and images | partial, not blocking | Editing a paragraph of Latin text works (embedded font reused, else Helvetica substituted, rewrapped); added text and images stay editable. Fails for CJK/RTL (#766), can't move several objects (#844); a beta item (gap 3) | 0 (beta: 100–180) | +| Convert and share (Office ↔ PDF, password protection) | partial, not blocking | Unencrypted PDFs we save open in Acrobat and other readers (958-file corpus round trip, `qpdf --check`); export to Word, RTF, HTML, text and images works, lossily (#773: an invoice's layout lost in Word); Create PDF works from images, text, the clipboard and many files at once. Missing sub-cases: password-protected output that Acrobat and Reader accept (#774; the file opens in PdfCraft, SumatraPDF, PDF24 and LibreOffice), an in-app Create PDF from Office (Office on macOS saves PDF itself), Excel/PowerPoint export. All beta items (gaps 1, 8) | 0 (beta: 40–70 for #774, Office→PDF and a layout-keeping Word export) | + +**Result: alpha.** Every core workflow can be completed on macOS; two are lossy or missing sub-cases +(editing CJK/RTL text; encrypted output for Acrobat users, Office→PDF, Word layout), which are the +first beta items. Ready for real work, ≈ 49%, is inside the alpha band. (A first application of the +gate the same day judged convert and share as blocking and put PdfCraft at pre-alpha; re-judged with +the shared rule, encryption and Office→PDF are sub-cases and Word export is lossy, not absent.) + +## Current focus + +In order. Each item names its entry in [gaps.md](gaps.md). + +1. **Convert and share:** encrypted output that Acrobat opens (gap 1, #774, small and the most + damaging), then Create PDF from Office and a Word export that keeps layout (gap 8, #773). +2. **Fix the crash and wrong-result reports** filed against shipped features (gap 6), starting with + the panic in #816. +3. **Printing on Windows** (gap 4, #756) and **a Chinese UI face in releases** (gap 5, #826). +4. **A fidelity harness against Acrobat, then the renderer decision** (gaps 12 and 2). Owner + decision needed: our own M2 devices, or #841's proposal as the bridge. +5. **Editing existing content** (gap 3, M7): fonts, subsets, reflow, CJK and RTL. +6. **Pro workflows:** OCR languages (gap 7), Office export/import (gap 8), Preflight and PDF/A/X/UA + (gap 9), signatures with tokens, timestamp servers and online LTV (gap 10), accessibility + tagging (gap 11). +7. **1.0 polish (M14):** performance budgets and lazy loading (gap 13), right-to-left layout and the + missing languages (gap 14), keyboard-only operation, a screen-reader audit. + +Decided against for now (owner, 2026-10-05): self-installing updates and an update check at start. +Help ▸ Check for updates is manual only. + +## Milestones + +"Plan size" is the original single-agent sizing. "Done" is the estimated share of that milestone's +*acceptance criteria* that are met, not lines of code. "Remaining" is re-measured on 2026-10-10 at +the measured rate (see [target-app-parity.md](target-app-parity.md#effort-and-calibration)); the +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. Fuzzing runs nightly (`xtask fuzz`). 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. 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). 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; late browser startup downloads preserve the current document and Save target (#171). | +| M4 | Engine, history, save, organize | 100–180 | 95% | 8–15 | Done:
- command registry (menus, shortcuts and palette all use it);
- undo/redo; incremental, atomic and encrypted saves;
- autosave and crash recovery;
- organize, combine, extract, split and insert-from-file, with identical fonts and images stored once;
- CLI `edit/combine/extract/split`.
Done since: Combine files in its own tab (sortable, resizable columns; folders; early warnings; unlocking protected files), bookmark editing, page labels (Number pages), CLI `run`, Set Page Boxes, Crop tool, Duplicate pages, Ctrl+A / Cmd+A to select all pages in Organize, Replace pages, bookmarks from tagged headings. Missing: page transitions, recovery on the web | +| M5 | Comments (all annotation types, XFDF) | 120–200 | 88% | 12–25 | Done: notes, highlight/underline/strikeout/squiggly, text boxes, ink, lines, arrows, rectangles, ovals, stamps (dynamic, Sign Here, Standard Business), with appearance streams; replies, status, checkmarks, locking, move/resize/restyle/delete; properties dialog; panel filter and sort; hide all; summaries (comments only); XFDF/FDF import and export; flatten; Fill & Sign; agent tools.
Done since: polygons, connected lines, clouds, callouts, inserted text, colour and checkmark filters; image signature pointer previews, selection handles and live image movement/resizing with proportional corners and independent edge handles. Missing: custom stamps, replace-text proposals, summary layouts with the page | +| 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.
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 | +| 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`). 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. 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, CUPS spooler, 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; Create PDF from multiple files (PDFs, images and text, edited as pages in the grid). Missing: OCR languages beyond Latin, editable-text OCR output, Office export/import, Windows/web printing | +| 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). 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).
Done since: 2D distance, perimeter and area measurements with persistent viewport calibration, snapping, live information and CSV export. Missing: autotag, Tags/Order/Content panels, Reading Order tool, alt-text workflow, compare, geospatial/3D measurement, search index, XFA | +| 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 | +| M14 | 1.0 polish: performance, localization, installers | 120–250 | 15% | 80–150 | Done: PhotoCraft's translation system (`i18n/`: TSV catalogs, `tl!`, command-id and plural entries, system-language detection, strict catalog tests); every dialog, panel and notice goes through `tl!`. Catalogs of ≈ 2,000–2,140 entries for Japanese, Simplified and Traditional Chinese, Russian, Bulgarian, German, Spanish, French, Telugu, Hungarian, Ukrainian, Italian, Brazilian Portuguese and Arabic (logical-order catalog put into display order; layout not yet mirrored); Czech covers the menus. Signed and notarized macOS builds, signed Windows installers (x64, x86, ARM64), AppImage, deb, rpm, Flatpak and FreeBSD packages. #307 (bounded large-PDF memory): shared resource indexes and parser metadata, lazy inspection, bounded caches; a 9,156-page file inspects in ≈ 1.8 s at ≈ 500 MiB instead of exceeding 10 GiB. Shortcut display uses localized modifier/key names across menus, toolbar, palette, Help and tooltips (#496). Missing: a right-to-left layout, a Chinese UI face in releases, Hindi/Indonesian/Korean/Vietnamese, file-backed input and global memory budgets, performance budgets, keyboard-only operation | +| | **Total** | **2,085–3,840** | **≈ 49% ready** | **≈ 770–1,400** | Area view of the same work: [target-app-parity.md](target-app-parity.md#by-feature-area) | + +## Upcoming milestones, ranked + +| Rank | Next milestone | Closes | Estimate (h) | +|---|---|---|---| +| 0 | Convert and share: encrypted output opens in Acrobat, Create PDF from Office, Word export keeps layout | gap 1, part of gap 8 | 40–70 | +| 1 | Backlog and platform: crash and wrong-result reports fixed, Windows printing, Chinese UI face | gaps 4, 5, 6 | 65–120 | +| 2 | Fidelity harness and renderer decision (M2) | gaps 2, 12 | 110–190 | +| 3 | Editing existing content (M7) | gap 3 | 100–180 | +| 4 | Pro conversion: OCR languages and clean-up, Office export/import (M10) | gaps 7, 8 | 95–175 | +| 5 | Standards: Preflight, PDF/A all parts, PDF/UA, PDF/X (M11) | gap 9 | 65–115 | +| 6 | Signatures for institutions: PKCS #11, timestamp servers, online LTV (M9) | gap 10 | 40–70 | +| 7 | Accessibility tagging (M12) | gap 11 | 40–70 | +| 8 | 1.0 polish: performance, RTL, languages, keyboard-only (M14) | gaps 13, 14 | 55–95 | + +Beta needs ranks 1–7 in substance (≈ 400–700 h; see [target-app-parity.md](target-app-parity.md#stage)). + +## Critical path + +M0 → M1 → M2 → M3 → M4 had to happen in order, and did, except that M2's own renderer was deferred +behind the `hayro` bootstrap. After M4, M5–M13 run in parallel across crates. The long poles are +M2 (renderer), M7 (content editing), M10 (OCR and Office), M11 (Preflight and standards) and M12 +(tagging and XFA). + +## Risks most likely to push estimates up + +- Fidelity of text editing: fonts, subsets, reflow, CJK and RTL. +- XFA dynamic layout on real government and bank forms. +- Real-world signature chains, revocation and tokens. +- The rendering long tail: Type 3 fonts, broken fonts, shadings, blend modes. +- Correctness of PDF/A, PDF/X and PDF/UA conversion. +- Interoperability: files other readers (above all Acrobat) reject, as in #774. +- OCR and AI quality limited by what openly licensed models exist. +- CPU rendering performance on the web. +- Hostile input: every fuzz run so far has found new crashes or hangs. +- Unmeasured fidelity: without an Acrobat comparison harness, quality gaps surface as user reports. + +## Revision history + +| Date | Change | Summary | +|---|---|---| +| 2026-10-10 | minor | M14: localized shortcut key names (#496), including runtime switching and native layout checks. Estimates unchanged. | +| 2026-10-10 | minor | Re-judged convert and share with the cross-app gate rule (only a workflow that can't be completed at all blocks): partial, not blocking → stage alpha | +| 2026-10-10 | minor | Added the alpha gate (six core workflows; convert and share fails → pre-alpha, 40–70 h to alpha) and rank 0 in the upcoming milestones | +| 2026-10-10 | major | Created from ROADMAP.md §Milestones, §Critical path, §Risks and §Where we're lacking. Merged the duplicated M1, M2, M3 and M14 rows, re-measured Done and Remaining, added Current focus and the ranked upcoming milestones | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md new file mode 100644 index 000000000..dc7443adf --- /dev/null +++ b/docs/target-app-parity.md @@ -0,0 +1,301 @@ +# PdfCraft parity with Adobe Acrobat Pro + +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (readiness table with hours per audience; full number restated as the standard's additive weighted sum, 47% → 49%; earlier: second look: cross-cutting double count removed, mainstream-practitioner number added, core-workflow gate applied → alpha) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) + +The authoritative assessment of how close PdfCraft is to Acrobat Pro. [ROADMAP.md](../ROADMAP.md) +summarizes it; [gaps.md](gaps.md) lists every shortfall one by one; the deep-dive checklists are +[pdf-spec-parity.md](pdf-spec-parity.md), [file-format-parity.md](file-format-parity.md), +[ui-parity.md](ui-parity.md), [hardware-parity.md](hardware-parity.md) and +[localization-parity.md](localization-parity.md). + +**Scope.** Parity means the *offline* feature set of Acrobat Pro. Adobe's cloud services (Document +Cloud storage, Acrobat Sign requests, shared review, the AI Assistant, Adobe Express, the Adobe +Approved Trust List) are out of scope: 23 such features are tracked as `na` in +`parity/acrobat-features.toml` and excluded from every percentage. + +## Headline + +| | Value | Kind | +|---|---|---| +| **Feature breadth**, tier-weighted | **≈ 67%** | measured (checklist; weights below) | +| Feature breadth, unweighted | 56.9% (423 shipped + 74 partial of 808) | measured (`cargo xtask parity` arithmetic) | +| Must-have (P0) features | 94.2% (226 + 23 partial of 252) | measured | +| Feature breadth, mainstream practitioner | ≈ 70% tier-weighted (63.2% unweighted: 292 + 44 partial of 497; P0 95.3%) | measured ([split below](#mainstream-practitioner-vs-full-acrobat-pro)) | +| **Ready for real work**, full Acrobat Pro | **≈ 49%** (range 45–53%) | estimated (weighted sum over dimensions, [below](#by-dimension)) | +| **Ready for real work**, mainstream practitioner | **≈ 55%** (range 50–60%) | estimated ([split below](#mainstream-practitioner-vs-full-acrobat-pro)) | +| **Ready for real work**, essentials user | **≈ 66%** (range 60–70%) | estimated ([below](#essentials-user)) | +| **Stage** | **alpha** (passes the core-workflow gate) | see [Stage](#stage) | +| Remaining to beta | ≈ 400–700 Opus 5.5 agent-hours | estimated | +| Remaining to full parity | ≈ 780–1,400 Opus 5.5 agent-hours | estimated | + +### Readiness by audience + +| Audience | Ready | Opus 5.5 agent-hours to ≈ 95% | Work that dominates | +|---|---|---|---| +| Full Acrobat Pro (ready for real work) | ≈ 49% | ≈ 780–1,400 h (≈ 300–550 h wall clock with 3–5 agents; 60–70% parallelizes) | Own renderer and fidelity harness, editing existing content, OCR and Office conversion, Preflight and PDF/A/X/UA, accessibility tagging, signatures for institutions | +| Mainstream practitioner | ≈ 55% | ≈ 500–900 h (≈ 200–400 h wall clock; ≈ 60% parallelizes) | Rendering fidelity, editing existing text (CJK, RTL, subsets), conversion (Word layout, Excel, PowerPoint, Office→PDF), OCR languages, Windows printing, the bug backlog | +| Essentials user | ≈ 66% | ≈ 180–330 h (≈ 90–180 h wall clock; ≈ 50% parallelizes) | Rendering fidelity on everyday files, Windows printing, launch and GPU stability, encrypted output Acrobat opens (#774), simple text fixes, Word export layout, Chinese UI glyphs | + +Hours are calibrated as in [Effort and calibration](#effort-and-calibration) (≈ 1k kept lines per agent-hour for new features, 3–10× slower for hardening and fidelity). The audiences are nested subsets, so essentials ≤ mainstream ≤ full. + +The gap between the numbers is the story of this release: most of what an Acrobat user reaches +for *exists*, but much of it is shallower than Acrobat's, rendering is still borrowed, quality has +never been measured side by side with Acrobat, and the first public releases (0.4.0, 0.5.0) drew +245 open issues in two days, including one that makes our encrypted files unreadable in Acrobat +([#774](https://github.com/storytold/pdfcraft/issues/774)). + +## How this was measured + +- **Target identified from the installed app.** `/Applications/Adobe Acrobat DC/Adobe Acrobat.app`, + `CFBundleShortVersionString` 26.002.21931. Inspected without launching it and without opening + anything inside `Contents/Resources/JavaScripts/` (clean-room rules, `AGENTS.md` §2): the + `Info.plist` `CFBundleDocumentTypes` (the formats it opens: see + [file-format-parity.md](file-format-parity.md)), the `.lproj` folder names (24 localizations, + plus the MEA/MEH/NAF Middle East and North Africa editions named in `Resources/Sequences/`), the + names in `Contents/Plugins/` (Accessibility, AcroForm, Checkers, Comments, DigSig, EScript, + MakeAccessible, PPKLite, ReadOutLoud, Reflow, SaveAsRTF, Search, Spelling, WebLink, …), + `Contents/Frameworks/` (AdobeJP2K, Adobe3D, AdobeCoolType, AdobeLinguistic, the OCR-capable + `OnDeviceIntelligence_SDK`, …) and the `PDF Optimizer` and `Sequences` (Action Wizard) folders. + Names and listings only. +- **Feature breadth from the checklist.** `parity/acrobat-features.toml` tracks 831 Acrobat Pro + features (808 in scope). `cargo xtask parity` (in `xtask ci`) rejects any `shipped` claim whose + command, automation tool or cited test does not exist. The numbers here were computed from the + file on `origin/main` at 5d590604 (2026-10-10) with the same arithmetic as the tool (partial + counts half). The checklist itself was written from Acrobat's public documentation, not diffed + against a menu dump, so it can miss features; that risk is one of the gaps. +- **Ready for real work by judgement, from evidence:** the code (152k lines of Rust in `crates/`, + `apps/` and `xtask`, 1,765 tests, 184 registered commands, 141 automation tools); the vendored + renderer's patch list (`vendor/README.md`); user reports on GitHub (245 open, 107 closed issues on + 2026-10-10); release packaging (`.github/workflows/release.yml`); and knowledge of Acrobat Pro's + feature set from Adobe's public documentation. +- **Not measured, and the largest uncertainty:** no side-by-side comparison of rendering, text + extraction or form behaviour against Acrobat exists yet. Every "ready" figure below is an + estimate. + +### Weights + +Feature breadth weights the checklist's tiers by how much Acrobat users rely on them: **P0 50%, +P1 30%, P2 15%, P3 5%** → 0.50 × 94.2 + 0.30 × 61.0 + 0.15 × 12.0 + 0.05 × 2.3 ≈ **67%**. + +Ready for real work weights the areas by how often an Acrobat Pro user reaches for them (judgement: +Adobe publishes no usage data): view 16%, comment 12%, edit 12%, organize 10%, forms 10%, +create/export 9%, sign 7%, protect 5%, print 5%, OCR 4%, core 3%, optimize/standards 3%, +accessibility 2%, automation/preferences 2%. That gives the Features dimension, ≈ 49%. + +The **full ready-for-real-work number is an additive weighted sum over the dimensions** (standard +method; weights by what decides whether a professional can switch): features 40%, file formats and +conversion 12%, UI/UX 10%, PDF specification 8%, stability 8%, performance 6%, localization 5%, +platforms 5%, hardware 4%, ecosystem 1%, AI 1%: +0.40 × 49 + 0.12 × 40 + 0.10 × 55 + 0.08 × 60 + 0.08 × 50 + 0.06 × 40 + 0.05 × 59 + 0.05 × 70 + +0.04 × 35 + 0.01 × 30 + 0.01 × 0 = 49.25 → **≈ 49%**. + +History: the first version of this file subtracted 9 points from the area figure for cross-cutting +shortfalls (40%); the second look found that a double count (Windows printing, encrypted-output +interop and Chinese glyphs were already scored in their areas) and used a 2-point deduction (47%); +this version replaces the deduction with the standard's weighted sum over dimensions (49%). Method +aligned with the standard, no new evidence. Multiplicative discounts are used only for the +mainstream and essentials numbers. + +## Mainstream practitioner vs full Acrobat Pro + +The checklist covers all of Acrobat Pro, including surfaces a typical practitioner rarely opens. +Splitting it (measured from `parity/acrobat-features.toml`): + +| Slice | Features | Shipped | Partial | Unweighted | Tier-weighted | P0 | +|---|---|---|---|---|---|---| +| Mainstream: view, comment (without measuring), edit, organize, forms (without XFA and JavaScript tooling), protect and redact, OCR, create and export, compress (Reduce File Size, Optimizer), print (without print production) | 497 | 292 | 44 | 63.2% | ≈ 70% | 95.3% (193) | +| Specialist: core internals, digital-signature PKI, accessibility remediation, automation and preferences, Preflight and PDF/A/X/UA, print production, measuring, XFA and JavaScript tooling | 311 | 131 | 30 | 46.9% | ≈ 63% | 90.7% (59) | +| All | 808 | 423 | 74 | 56.9% | ≈ 67% | 94.2% (252) | + +So the checklist is **not** dominated by niche surface (38% of rows), and the P0 tier, which is +mostly mainstream, is at 94–95% in both slices. What holds the mainstream number down is P1 depth +(69%) and the P2 long tail (13%: rulers, loupe, portfolios, spell check, Bates across files). + +**Ready for real work, mainstream practitioner ≈ 55%**, weighting the mainstream areas by use and +scoring only their mainstream features: view 20% × 60, comment 15% × 70, edit 15% × 35, +organize 12% × 75, fill and sign 12% × 70, convert 10% × 30, protect and redact 6% × 55, +OCR 4% × 20, compress 3% × 60, print 3% × 50 = 55.6%. Read-only, review, form-filling and page +workflows are close to Acrobat; editing existing content, conversion and OCR are not. + +### Essentials user + +Someone who uses Acrobat for the everyday basics and never opens the rest. Scored on those features +only, by depth, then discounted for what such a user actually runs into. Advanced options, Pro +workflows, exchange edge cases and everything outside the mainstream slice are excluded. + +| Core feature | Weight | Depth | Evidence | +|---|---|---|---| +| Open the PDFs people send and read them (render, zoom, scroll, page through) | 20% | 80% | Repair of damaged files, 958-file corpus; rendering borrowed but broadly right (#794 is a TikZ pattern) | +| Find, select and copy text | 8% | 85% | Find, options, results list; word-F1 0.98 | +| Print | 8% | 55% | Good on macOS and Linux; nothing on Windows, where most such users are (#756) | +| Highlight, sticky notes, text comments | 12% | 85% | All markup types, replies, properties | +| Fill in forms | 10% | 85% | Every AcroForm field type, Acrobat's formatting and calculations | +| Fill & Sign: typed, drawn or image signature | 10% | 85% | Fill & Sign tools; flatten on save | +| Rotate, delete, reorder and extract pages | 8% | 85% | Organize grid, drag reorder, thumbnails | +| Combine files | 6% | 85% | Combine tab with bookmarks per file | +| Fix a typo in existing text | 5% | 40% | Latin paragraphs only; font may be substituted | +| Export to Word or images | 5% | 45% | Images good; Word loses layout (#773) | +| Reduce file size | 3% | 75% | Reduce File Size, Optimizer | +| Protect with a password | 3% | 30% | Works in PdfCraft; Acrobat and Reader reject the file (#774) | +| Undo, save, autosave | 2% | 90% | Undo after save, atomic saves, crash recovery | + +Weighted depth: **≈ 75%**. Discounts an essentials user hits: launch and stability (54 issues about +installing, launching, GPU and windows; OpenGL fallback) −5 points; discoverability and clarity +(an Acrobat-like shell with Home and All tools, but tool-mode friction such as #825 and #721) −2; +opening what people send (Chinese UI glyph boxes #826, CJK documents #758) −2. +**Essentials user ≈ 66%** (60–70%). + +### User sentiment (second look, 2026-10-10) + +Practitioners' praise is real and consistent with these numbers: + +- **Adoption:** 7,595 GitHub stars, 2,661 forks; release downloads 177,301 (v0.4.0) and 39,447 + (v0.5.0, its first day). +- **Praise:** 23 of 359 issues written by users carry explicit praise ("amazing bit of software" + #693, "ridiculously fast and awesome PDF editor" #320, "brilliant" #542, "fantastic and very useful" + #369, "Gigantic stuff… I love it" #449). Switching reports: #179 switched from Adobe Reader on + Windows ("covers nearly everything I used Reader for"); #232 replacing macOS Preview; #273 + "open source alternative to Acrobat"; #195 would "replace Acrobat completely" once hardware-token + signing exists. +- **What users hit** (issue titles, excluding 35 agent-filed `fix(...)` reports): installation, + launch and GPU/window problems 54, rendering and display 46, editing text and images 41, CJK and + other fonts and glyphs 41, signing and certificates 35, printing 23, forms 18, conversion and + export 16, OCR 5. Roughly two thirds are bugs (≈ 213 vs ≈ 111 feature requests), concentrated in + core paths rather than niche features. + +The praise comes overwhelmingly from Reader-class work (view, annotate, fill, sign, organize), where +the mainstream number is highest; the requests and bugs cluster on editing, CJK, printing, +conversion and signing tokens, which is where the gaps are. That supports raising the full number +from 40% to 47% (the double count; restated as 49% by the weighted-sum method) and reporting the mainstream 55% beside it; it does not support +calling PdfCraft a replacement for Acrobat Pro yet. + +## By dimension + +Hours are Opus 5.5 agent wall-clock hours, low–high. Rows marked *within features* are part of the +features total and are shown for orientation; the others add to it. + +| Dimension | Ready | Weight | Remaining (h) | Counted | Evidence | Doc | +|---|---|---|---|---|---|---| +| Features (14 areas) | ≈ 49% depth; 67% breadth | 40% | 610–1,100 | base | Area table below | this file | +| UI/UX fidelity | ≈ 55% | 10% | 30–60 | additional | Acrobat-style shell, All tools, Home, command palette; missing single-key accelerators, rulers/guides, multiple windows, popups; user reports #739, #744, #746, #759, #789, #844 | [ui-parity.md](ui-parity.md) | +| File formats and conversion | ≈ 40% | 12% | 120–220 | within features | PDF read/write strong but encrypted output fails in Acrobat (#774); export Word is layout-poor (#773); no Excel/PowerPoint export, no Office/HTML/PostScript import | [file-format-parity.md](file-format-parity.md) | +| PDF specification and standards | ≈ 60% | 8% | 150–260 | within features | ISO 32000 syntax, encryption, annotations and AcroForm strong; rendering and fonts borrowed from `hayro`; PDF 2.0 extras, linearization, public-key security absent; PDF/A partial, PDF/X/UA none | [pdf-spec-parity.md](pdf-spec-parity.md) | +| Hardware | ≈ 35% | 4% | 25–50 | additional | Page raster is CPU only (Acrobat has GPU rendering); Windows CNG and macOS Keychain keys sign, no PKCS #11; no scanners; Windows has no printing (#756) | [hardware-parity.md](hardware-parity.md) | +| Localization | ≈ 59% over the 12 key languages | 5% | 35–60 | additional | 15 catalogs, 13 at 94–100%; Chinese UI shows tofu in releases (#826, #688); Arabic not mirrored; no Hindi, Indonesian, Korean, Vietnamese | [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, many of them `fix(...)` reports of wrong results in shipped features (#799–#821) | [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 Windows and web printing, web OCR and 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)** | **≈ 49%** | 100% | **≈ 780–1,400** | | | | + +## By feature area + +Breadth is measured from the checklist (partial = ½). Ready is estimated. Hours are remaining to +full parity in that area. + +| Area | Features | Shipped | Partial | Breadth | Ready | Remaining (h) | What's strong / what's missing | +|---|---|---|---|---|---|---|---| +| A Core model and fidelity | 56 | 42 | 2 | 76.8% | 70% | 25–45 | Parser, repair, R2–R6 encryption, incremental and atomic saves, undo, autosave, crash recovery, revisions. Missing: encrypted output that Acrobat opens (#774), lazy loading of GB files, linearization, PDF 2.0 extras, Arlington validation, public-key security, own image codecs | +| B View and navigation | 92 | 52 | 11 | 62.5% | 55% | 110–190 | Shell, layouts, zoom, find, panels, tiles, web build, 500-page scrolling. **Pages are drawn by the vendored `hayro`** (22 + 9 local patches): all 7 rendering P0s are partial. Missing: own renderer or a decided replacement (#841), multiple windows, split view, rulers/guides, loupe, reflow, search across files | +| C Content editing | 61 | 29 | 7 | 53.3% | 30% | 100–180 | Edit existing paragraphs (font reused or Helvetica substituted, rewrapped), added text and images stay editable, header/footer, watermarks, backgrounds, Bates, links. Missing: robust editing of real-world text (subset fonts, CJK, RTL #766, rotated), lists, vector/object editing, arrange/align, spell check, multi-select move (#844) | +| D Organize pages | 76 | 45 | 3 | 61.2% | 70% | 12–25 | Organize grid, insert/extract/replace/split/combine with links, fields, layers and bookmarks carried over; page labels, boxes, bookmarks from structure. Missing: transitions, attachments editing, portfolios, struct-tree merge, field-name conflicts; open fixes #817, #818, #821 | +| E Comments and review | 65 | 46 | 9 | 77.7% | 70% | 12–25 | Every markup type with appearances, replies, status, filters, stamps (custom, dynamic), XFDF/FDF, summaries, flatten, 2D measuring. Missing: rich-text runs, popup windows, summary layouts with pages, per-type hiding; XFDF fidelity bugs #807, #809, #819 | +| 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). 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, MRC compression, scanners | +| 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 | 10 | 34.5% | 20% | 65–115 | Reduce File Size and the Optimizer (images, 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, monochrome JBIG2/CCITT | +| 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. Missing: **printing on Windows (#756) and the web**, print as image, PostScript, all print-production tools (output preview, separations, ink manager, printer marks, colour conversion) | +| M Accessibility | 61 | 38 | 1 | 63.1% | 30% | 40–70 | Accessibility Checker (all 32 rules, report, fixes), alternate text, title and language. Missing: autotag, Tags/Order/Content panels, Reading Order tool, artifact marking, table editor, Read Out Loud, full keyboard operation of the app, screen-reader audit | +| N Automation, preferences, misc | 75 | 20 | 5 | 30.0% | 35% | 25–45 | CLI, MCP, UI control channel, 141 automation tools, Action Wizard (run, create), compare (text, visual, report). Missing: most Preferences pages, side-by-side compare, Action Wizard management, custom commands, AI providers, rich media/3D, hosted web app | +| **All** | **808** | **423** | **74** | **56.9%** | **≈ 49%** | **610–1,100** | Plus 175–325 h of cross-cutting work (dimension table) | + +Shipped features rest on thin evidence: 141 of 423 cite exactly one test, and only a handful cite +an external oracle (`pdftotext`, `pdfsig`, OpenSSL, `qpdf --check`, a corpus). + +The checklist lags reality in two places: `misc.localization` is still `planned` although 15 +catalogs ship, and `misc.installers` is `planned` although signed macOS, Windows and Linux packages +ship (`.github/workflows/release.yml`). Neither moves the numbers by more than 0.2 points; fix them +with evidence when next touched. + +## Stage + +**alpha.** Ready for real work, ≈ 49%, is inside the 40–75% band, and every one of Acrobat Pro's +six everyday workflows can be completed end to end on macOS. Under the gate rule shared by all the +Crafting Apps, a workflow blocks only when a typical user can't complete it at all; lossy exchange +or a missing sub-case is a beta item. **Converting and sharing** is therefore partial, not +blocking: unencrypted PDFs we save open everywhere and export to Word, RTF, HTML, text and images +works, but password-protected output is rejected by Acrobat and Reader (#774), there is no in-app +Create PDF from Office, and Word export loses layout (#773). The gate table, with evidence, is in +[roadmap.md](roadmap.md#alpha-gate). (Earlier the same day this row was judged blocking and the +stage set to pre-alpha; see the revision history.) + +| Core workflow | Works end to end? | +|---|---| +| View, search and print | yes | +| Organize, combine and split pages | yes | +| Comment and review | yes | +| Fill and sign forms | yes | +| Edit text and images | partial, not blocking (Latin text with embedded or standard fonts) | +| Convert and share (Office ↔ PDF, password protection) | partial, not blocking (encryption interop #774, Office→PDF and Word layout #773 are beta items) | + +**To reach beta** (≈ 75% ready, no blocking gap in PDF interchange), ≈ 26 points and +**≈ 400–700 agent-hours**: + +| Beta requirement | Hours | +|---|---| +| Encrypted output opens in Acrobat and Reader (#774), with an interop suite that opens our outputs in other readers | 8–15 | +| Rendering: a fidelity harness against Acrobat captures (local, clean-room) and fixing what it finds; settle the renderer (own M2 devices, or #841's proposal); Chinese UI faces in releases | 80–140 | +| Editing existing text robust on real files: subset fonts, CJK, RTL, rotated text | 70–120 | +| Printing on Windows and the web | 15–30 | +| OCR in the major languages (accents, Cyrillic, CJK), deskew and rotation | 35–60 | +| Export: Word that keeps layout; Excel and PowerPoint | 45–80 | +| Signatures: timestamp servers, online revocation and LTV, PKCS #11, OS trust | 30–55 | +| PDF/A (all parts and levels), PDF/UA validation, a first Preflight | 45–80 | +| Accessibility tagging: autotag, Tags panel, Reading Order | 35–60 | +| Open-issue backlog, stability and performance budgets | 40–70 | +| **Total** | **≈ 400–700** | + +## Effort and calibration + +**Calibrated against this repository's own history.** + +- The repo is 11 days old (first commit 2026-09-30 05:17), with 591 commits and 371 merged pull + requests (all since 2026-10-04). Rust in `crates/`, `apps/` and `xtask/` grew from 37.7k lines + (2026-10-02) to 82.6k (10-04), 99.2k (10-08) and 152.7k (10-10). +- The previous ROADMAP measured about **1.0k kept, tested lines per agent-hour** (72k lines in + roughly 65–75 agent-hours). New features land at about that rate: medium P1 features such as + redaction word lists (#757, 314 lines), bookmarks from structure (#754, 588 lines) and signing + encrypted documents (#753, 308 lines) each took about 1–3 agent-hours including tests, with 2–4 h + from PR to merge, mostly CI. Large arcs take longer: XFA dynamic layout, its JavaScript and + FormCalc (#121, #230, #301; ≈ 13k lines) about 15–25 agent-hours; the Arabic interface + (#692, 2.7k lines) about 3–5. +- Hardening and fidelity work is 3–10× slower per line than new features: the 2026-10-07 → 10-10 + block landed ≈ 330 commits but moved the checklist by only 12 shipped features (411 → 423), because most of + it was never-crash work, Windows fixes, localization and XFA. + +The previous estimate (2026-10-07) was **600–1,100 h** to full parity. Since then XFA, signature +timestamps/LTV and signing encrypted documents progressed (≈ −60 h), but the first public releases +surfaced work the estimate did not hold: 245 open issues, missing Windows printing, Acrobat +interoperability of encrypted output, Chinese UI glyphs and Word-export layout (≈ +200–300 h). +Hence **≈ 780–1,400 h** now. Roughly 60–70% parallelizes across crates (3–5 agents: ≈ 300–550 h +wall clock). + +**Needs a human:** the renderer decision (own M2 devices vs an outside renderer, #841); Acrobat +oracle captures on the owner's Mac (synthetic fixtures only); native-speaker review of every +catalog; smart cards and PKCS #11 tokens, scanners and Windows printers to test against; any +licensed OCR or AI model. + +## Revision history + +| Date | Change | Summary | +|---|---|---| +| 2026-10-10 | minor | Readiness table: hours to ≈ 95% per audience (full 780–1,400, mainstream 500–900, essentials 180–330). Full number restated as the standard's additive weighted sum over dimensions with written weights: 47% → 49%. Method aligned with the standard, no new evidence | +| 2026-10-10 | minor | Added the essentials-user score (≈ 66%): weighted depth of 13 everyday features, discounted for launch, discoverability and opening others' files | +| 2026-10-10 | minor | Consistency check across apps: re-judged the convert-and-share gate row with the shared rule (blocking only when the workflow can't be completed at all; lossy exchange and missing sub-cases are beta items). Encrypted output (#774) and Office→PDF are sub-cases, Word export is lossy not absent → partial, not blocking; stage pre-alpha → **alpha**. Numbers unchanged | +| 2026-10-10 | minor | Second look requested by the owner after practitioners praised PdfCraft: user sentiment gathered (stars, downloads, 23 praising issues, switching reports, issue themes); checklist split into mainstream (497) and specialist (311) features; the 9-point cross-cutting deduction removed as a double count (Windows printing, interop and glyphs were already scored in their areas). Ready for real work 40% → 47%; mainstream practitioner ≈ 55% added. Core-workflow alpha gate applied: convert and share fails → stage alpha → **pre-alpha**, ≈ 40–70 h from alpha | +| 2026-10-10 | major | Created from ROADMAP.md §Estimate summary and §Honest assessment (2026-10-05/07). Full re-measure against Acrobat Pro 26.002.21931: checklist recomputed (56.9% unweighted, ≈ 67% tier-weighted), ready for real work ≈ 40%, stage alpha, hours re-calibrated (780–1,400 h to parity, 400–700 h to beta), dimension and area tables | +| 2026-10-07 | minor | (in ROADMAP.md) 806 tracked features, 51.0% shipped, effort-weighted ≈ 30–35%, 600–1,100 h | +| 2026-10-05 | major | (in ROADMAP.md) First honest assessment by dimension and area | diff --git a/docs/ui-parity.md b/docs/ui-parity.md new file mode 100644 index 000000000..403f490d9 --- /dev/null +++ b/docs/ui-parity.md @@ -0,0 +1,84 @@ +# UI and interaction parity + +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (first UI/UX checklist) · **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 +observed clean-room (`plan/acrobat/02-ui-ux.md`, local-only; `AGENTS.md` §2); nothing from Adobe's +interface is copied. Gaps ranked in [gaps.md](gaps.md). + +**UI/UX fidelity ≈ 55% ready** (estimated), **30–60 h** additional to the features total. + +## Measured + +- **184 registered commands** (`crates/engine/src/commands.rs`), every one in the menus, the + command palette and the automation `command_run` tool; menus are generated from the registry + (`view.menus-from-registry`). +- **141 automation tools** (`crates/automation/src/tools.rs`), so every UI action can be checked + headlessly. +- **View area of the checklist: 62.5% breadth** (52 shipped, 11 partial of 92). + +No dump of Acrobat's menu tree has been diffed against the registry yet (gap 24). + +## Shell and navigation + +| Element | Acrobat | PdfCraft | Status | +|---|---|---|---| +| Home view (recent files, recommended tools) | yes | yes | shipped | +| All tools pane, tool sets | yes | yes, with milestone badges | shipped | +| Quick-action toolbar, customisable | yes | fixed | partial (`view.customize-quick-tools` planned) | +| Command search | "Search tools" | command palette (⌘K), matches translated and English labels | shipped | +| Document tabs | yes | yes, with unsaved marker | shipped | +| Multiple windows, drag a tab out | yes | no | planned | +| Split window, spreadsheet split | yes | no | planned | +| Read mode, full screen | yes | yes | shipped; presentation options planned | +| Light, dark, system theme | yes | yes | shipped | +| Side panels: thumbnails, bookmarks, attachments, layers, comments, fields, signatures | yes | yes; panels keep their width (#704) | shipped | +| Destinations, articles, model tree, Tags, Order, Content panels | yes | no | planned | +| Message bars (restricted, signed, PDF/A, forms) | yes | restricted and signed; no PDF/A view mode | partial | + +## Tools, handles and precision + +| Behaviour | Acrobat | PdfCraft | Status / evidence | +|---|---|---|---| +| 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 | +| Text selection, column select | yes, Alt-drag columns | reading-order selection; no column mode (#740) | partial | +| Select and move several objects at once | yes | no (#844) | gap | +| Object handles: corners keep proportion, edges free | yes | yes for added images and signatures | 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 | +| Measuring snap to paths, endpoints, midpoints, intersections | yes | yes | shipped | +| Nudge selected objects with arrow keys (Shift for larger steps) | yes | no: arrow keys scroll and turn pages | gap | +| Single-key tool accelerators (H, V, Z, …) | yes, opt-in | no | planned | +| Context menus on pages, thumbnails, comments, fields | yes | thumbnails, comments, fields | partial | +| Comment pop-ups as movable windows, connector lines | yes | inline in the panel | planned | +| Disabled commands explain why | no | yes | ahead | + +## Shortcuts and modifiers + +| Item | Acrobat | PdfCraft | Status | +|---|---|---|---| +| Platform shortcuts for registered commands | yes | yes; ⌘ on macOS, Ctrl elsewhere (#695) | shipped; find bar still shows ⌘ on Windows (#746) | +| Keyboard shortcuts reference | yes | yes | shipped | +| Full keyboard operation of the app | yes | no | planned (`a11y.keyboard-only`, P0) | +| Shift-⌘-G finds previous | yes | goes forward (#810) | bug | + +## Dialogs and preferences + +| Item | Acrobat | PdfCraft | Status | +|---|---|---|---| +| 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 | +| Preferences | ≈ 20 pages | Interface language, Documents and view, Identity, Fill & Sign, JavaScript | partial (gap 19) | + +## Revision history + +| Date | Change | Summary | +|---|---|---| +| 2026-10-10 | major | Created from the command registry, automation tools, the View area of the checklist and open UI issues | diff --git a/packaging/icons.sh b/packaging/icons.sh index e8b2c9960..da8228ff5 100755 --- a/packaging/icons.sh +++ b/packaging/icons.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash -# Regenerate every app icon from assets/app-icon/pdfcraft.svg (the master vector). +# Regenerate the app icons from assets/app-icon/pdfcraft.svg and the Windows PDF file icon +# from assets/app-icon/pdfcraft-document.svg. Both are master vectors. # # Needs: resvg (brew install resvg / cargo install resvg) and python3 (stdlib only, for the .ico). # On macOS, iconutil also writes the .icns. The outputs are committed, so building and packaging @@ -39,13 +40,15 @@ done mkdir -p "$DIR/hicolor/scalable/apps" cp "$SVG" "$DIR/hicolor/scalable/apps/$ID.svg" -# Windows .ico: PNG-compressed entries, 16-256 px. -ICO_PNGS=() -for s in 16 20 24 32 40 48 64 128 256; do - render "$SVG" "$s" "$TMP/ico-$s.png" - ICO_PNGS+=("$TMP/ico-$s.png") -done -python3 - "$DIR/pdfcraft.ico" "${ICO_PNGS[@]}" <<'PY' +# Windows .ico: PNG-compressed entries, 16-256 px. The document icon has its own resource +# in pdfcraft.exe; the app icon remains the first/default icon. +for name in pdfcraft pdfcraft-document; do + ICO_PNGS=() + for s in 16 20 24 32 40 48 64 128 256; do + render "$DIR/$name.svg" "$s" "$TMP/$name-$s.png" + ICO_PNGS+=("$TMP/$name-$s.png") + done + python3 - "$DIR/$name.ico" "${ICO_PNGS[@]}" <<'PY' import struct, sys out, pngs = sys.argv[1], sys.argv[2:] blobs = [open(p, "rb").read() for p in pngs] @@ -58,6 +61,7 @@ for b in blobs: offset += len(b) open(out, "wb").write(head + entries + data) PY +done # macOS .icns. if command -v iconutil >/dev/null; then diff --git a/packaging/macos/dmg/README.md b/packaging/macos/dmg/README.md index ec902e30f..460f7bdeb 100644 --- a/packaging/macos/dmg/README.md +++ b/packaging/macos/dmg/README.md @@ -2,8 +2,8 @@ What Finder shows when the DMG opens: a 660 × 400 pt background with the app icon and the `Applications` link side by side. `package.sh` copies these files into the image; nothing here is -generated at build time, so the DMG still builds with `hdiutil makehybrid` (no mounted device, no -Finder scripting on CI, nothing extra installed in the signing job). +generated at build time, so the DMG builds with a plain `hdiutil create -srcfolder` (no Finder +scripting on CI, nothing extra installed in the signing job). | File | What | |---|---| diff --git a/packaging/macos/package.sh b/packaging/macos/package.sh index c73581a24..e04be13c8 100755 --- a/packaging/macos/package.sh +++ b/packaging/macos/package.sh @@ -137,15 +137,40 @@ ln -s /Applications "$STAGE/Applications" mkdir -p "$STAGE/.background" cp "$HERE/dmg/background.tiff" "$STAGE/.background/background.tiff" cp "$HERE/dmg/dmg-layout.DS_Store" "$STAGE/.DS_Store" -rm -f "$DMG" "$WORK/raw.dmg" -# makehybrid + convert builds the image without attaching a device, unlike `create -srcfolder`, -# which is flaky on CI runners ("Resource busy") and hangs in sandboxed sessions. -# The volume name has no version: .DS_Store finds the background through an alias that includes it. -hdiutil makehybrid -hfs -hfs-volume-name "PdfCraft" -hfs-openfolder "$STAGE" -o "$WORK/raw.dmg" "$STAGE" -hdiutil convert "$WORK/raw.dmg" -format UDZO -imagekey zlib-level=9 -o "$DMG" -rm -f "$WORK/raw.dmg" +rm -f "$DMG" +# `hdiutil create -srcfolder` writes the files as they are. `makehybrid -hfs` gave every file a +# com.apple.FinderInfo (a Finder location of -1,-1), which `codesign --verify --strict` rejects as +# "Finder information, or similar detritus" on the app in the image and on a copy installed from it, +# so MDM and compliance checks reported a correctly notarized app as damaged (#390). `create` +# attaches a device while it copies, which fails now and then on CI runners ("Resource busy"), so +# it gets a few tries. The volume name has no version: .DS_Store finds the background through an +# alias that includes it. +# Usage: hdiutil_retry ; tries five times with growing pauses. +hdiutil_retry() { + local what="$1" attempt + shift + for attempt in 1 2 3 4 5; do + if hdiutil "$@"; then + return 0 + fi + [ "$attempt" = 5 ] && { echo "hdiutil $what failed 5 times" >&2; return 1; } + warn "hdiutil $what failed (attempt $attempt of 5); retrying in $((attempt * 10))s" + sleep $((attempt * 10)) + done +} +hdiutil_retry create create -srcfolder "$STAGE" -volname "PdfCraft" -fs HFS+ -format UDZO -imagekey zlib-level=9 -ov "$DMG" sign "$DMG" codesign --verify --strict --verbose=2 "$DMG" +# The app inside the image must still pass --strict, so a makehybrid-style regression can't ship. +MOUNT="$WORK/dmg-check" +rm -rf "$MOUNT" +mkdir -p "$MOUNT" +hdiutil_retry attach attach -nobrowse -readonly -mountpoint "$MOUNT" "$DMG" >/dev/null +if ! codesign --verify --strict --deep --verbose=2 "$MOUNT/PdfCraft.app"; then + hdiutil_retry detach detach "$MOUNT" >/dev/null || true + exit 1 +fi +hdiutil_retry detach detach "$MOUNT" >/dev/null if [ "$NOTARIZE" = 1 ]; then notarize "$DMG" xcrun stapler staple "$DMG" diff --git a/packaging/windows/package.ps1 b/packaging/windows/package.ps1 index c556652d9..524280b97 100644 --- a/packaging/windows/package.ps1 +++ b/packaging/windows/package.ps1 @@ -87,6 +87,12 @@ $Stage = Join-Path $TargetDir "windows-package\$Arch" Remove-Item -Recurse -Force $Stage -ErrorAction SilentlyContinue New-Item -ItemType Directory -Force -Path $Stage | Out-Null Copy-Item (Join-Path $Bin 'pdfcraft.exe'), (Join-Path $Bin 'pdfcraft-cli.exe') $Stage +# The document icon embeds Lucide artwork: carry its ISC notice and the project licences +# beside the binaries in both distributions. +foreach ($f in 'LICENSE-MIT', 'LICENSE-APACHE', 'NOTICE') { + Copy-Item (Join-Path $Root $f) $Stage +} +Copy-Item (Join-Path $Root 'assets\app-icon\LICENSE-document.txt') $Stage & (Join-Path $PSScriptRoot 'sign.ps1') (Join-Path $Stage 'pdfcraft.exe') (Join-Path $Stage 'pdfcraft-cli.exe') @@ -122,6 +128,9 @@ $Portable = Join-Path $TargetDir "windows-package\pdfcraft-$Version-windows-$Arc Remove-Item -Recurse -Force $Portable -ErrorAction SilentlyContinue New-Item -ItemType Directory -Force -Path $Portable | Out-Null Copy-Item (Join-Path $Stage '*.exe') $Portable +foreach ($f in 'NOTICE', 'LICENSE-document.txt') { + Copy-Item (Join-Path $Stage $f) $Portable +} Copy-Item -Recurse $Models (Join-Path $Portable 'models') foreach ($f in 'README.md', 'LICENSE', 'LICENSE-MIT', 'LICENSE-APACHE') { $p = Join-Path $Root $f diff --git a/packaging/windows/pdfcraft.wxs b/packaging/windows/pdfcraft.wxs index 200800af6..11d2e39ab 100644 --- a/packaging/windows/pdfcraft.wxs +++ b/packaging/windows/pdfcraft.wxs @@ -70,10 +70,13 @@ PdfCraft entry to pick was the advertised Start Menu shortcut, which Windows can't run as a file handler ("This app can't run on your PC"). --> - + + + @@ -136,6 +139,19 @@ + + + + + + + + + + + + +