M10: print from the web build through the browser's print dialog (gap 4) (#1243)

* Gates: fix two clippy failures on main

- ocr_ui: "Also recognize pages that already have text" used egui's own
  checkbox, which clippy.toml disallows (#1184 landed beside #1173);
  use widgets::checkbox like every other box.
- camera_edges: Rust 1.97's manual_range_contains; the rewrite keeps the
  old answer for a NaN angle (false) with an is_finite check.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019ojKsSCHWk2AuyDTFMHCd7

* M10: print from the web build through the browser's print dialog (gap 4)

The web build had no printer: spool::submit refuses on wasm, so Print
could only download the print-ready PDF. Now its Print dialog offers
"Browser's print dialog" (the default there), which reaches every
printer the operating system knows.

- web_print: PdfCraft draws the print-ready PDF's sheets itself
  (engine Exporter), one sheet a frame since wasm has no threads:
  300 dpi up to 100 sheets, 200 up to 300, 150 up to 1,000, at most
  64 MP a sheet and 1 GiB of PNGs a job. The PNGs become blob URLs
  and print from a hidden srcdoc iframe, one sheet per page, margin 0.
- Chromium prints every page at the default @page size, so a sheet of
  the paper's size turned sideways is rotated 90 degrees CCW onto it
  (as drivers do); other sizes get named @page rules.
- Copies, two-sided and colour are greyed out (the browser's dialog
  sets them). If print() is refused (e.g. a sandboxed frame), the
  print-ready PDF downloads instead, with the reason.
- pdfcraft-print stays free of web APIs; six new strings in all 20
  catalogs (ATTRIBUTION hashes refreshed); docs and parity updated,
  gap 4 closed.

Verified in headless Chromium against a trunk build: three sheets at
300 dpi, the landscape sheet turned, no blank trailing page, and the
download fallback. Firefox and Safari not yet tried.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019ojKsSCHWk2AuyDTFMHCd7

---------

Co-authored-by: Claude <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-10-12 02:31:36 -04:00
1 parent b1784fc2d9
commit 2aea7dcfeb
33 files changed
+769 -72

No files matched your search

+20 -20
View File
@@ -7,7 +7,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Greek UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "2200c7b374baffb569253eadf597e9ca0bff39324830cde30011617a5f1ee192"
sha256 = "0de16c8768d6407bd49d29991d7a079466b20d6034b4e5dd9e599f616888ec41"
# ATTRIBUTION.toml: one entry per asset (AGENTS.md §1). The source of truth for provenance and licences.
# ATTRIBUTION.md is generated from this file: `cargo xtask assets --write`. Checked by `cargo xtask assets` (part of `xtask ci`).
@@ -30,7 +30,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 = "1218460f576264b584b0e8d58b861ccf532a1029b5ec32a1e60a941cdffa3505"
sha256 = "428b841cb1d15d6a2735f7509d2170b1b0a11a87fa0fec04760c1cf895bbb684"
[[asset]]
path = "crates/ui-egui/src/i18n/zh-hant.tsv"
@@ -41,7 +41,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 = "46a79f58780806f0f581ef8b6100269a9022214f59f288e59a9a2822997d7aeb"
sha256 = "29b50165e96db49599478d46ae9a07af496189e715e4ec063e3768aa919d8aab"
[[asset]]
path = "crates/ui-egui/src/i18n/cs.tsv"
@@ -52,7 +52,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 = "3aee40e8fc1f3961cfdb97c01d6fb60aa567136d8d08b546ffb7c78cf9433811"
sha256 = "941a40cc109e57d33040d9a81eceed8fe50afbfe1be2594c214684b1a89c463a"
[[asset]]
path = "crates/ui-egui/src/i18n/pt-br.tsv"
@@ -63,7 +63,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 = "b4d4b329ca741bc097bb52185cc88a2f381a94026fd968123b47c54d943dedb3"
sha256 = "ef66c8a4e9e992d31ca69d95ee8ff11e1298a4a1045d08ab7b24f250ff7af79e"
[[asset]]
path = "crates/ui-egui/src/i18n/es.tsv"
@@ -74,7 +74,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 = "64ff3e12c9065505bc47c5d847ed8aaa436de3e1f8442a9d43ccc14b516a7b19"
sha256 = "c7ea41b09d24f87f1c8ceb5bcfadcd8b2cae348dc3927770c1227180b59fdb13"
[[asset]]
path = "crates/ui-egui/src/i18n/fr.tsv"
@@ -85,7 +85,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 = "954cb27d741c49741e372cf6a18c22b28214c7fffd1088119b8f6388b27a869b"
sha256 = "56d736cefc2189e8dadf11418a7701a746bbf237c58a628e3c1f0ecb64cf5150"
[[asset]]
path = "crates/ui-egui/src/i18n/de.tsv"
@@ -96,7 +96,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 = "dc971a2990664c916e65686dfcd0c1e226855573556769bac6cf4ade8845e349"
sha256 = "9777ed471a3b641a278cf3d1c41b6011bcd1c353fe3ef8e84d1eab350e0b5531"
[[asset]]
path = "crates/ui-egui/src/i18n/te.tsv"
@@ -107,7 +107,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 = "de419eeb6f631ad7ed4b0c409c31ef7a13a6c7bf1c009dbc61eb9a9a1d2663da"
sha256 = "629b769efd3535f31e73bb3a18e9cef7efc5880e4fabb928cad789474dcbf283"
[[asset]]
path = "crates/ui-egui/src/i18n/it.tsv"
@@ -118,7 +118,7 @@ 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 = "963c2f7ed3a84f4c6a66897d21cded494026a6088304a1cb39c8cef032b02ca9"
sha256 = "bc0399c21f0420dd50aa38a596b8d165089874ffabee56de587803ff45e4b79d"
[[asset]]
path = "crates/ui-egui/src/i18n/pl.tsv"
@@ -129,7 +129,7 @@ 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 = "1d3a3cc44812643f8612b3ae921025daad0f52f8c237f2456f4162db955e196b"
sha256 = "f0e696bd3e046f82f5c2499f9c41e1c023293de3c65d6d71a3324bcd2ede4a78"
[[asset]]
path = "crates/ui-egui/src/i18n/ru.tsv"
@@ -140,7 +140,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 = "c619710b677655a8604c646d2028c2ccc95e756bbb03849727f67205f9fe6992"
sha256 = "a2884a44cb42ec85d363417e999b17e2a6f136cacb2112c5ae7caa5a79db3a13"
[[asset]]
path = "crates/ui-egui/src/i18n/hu.tsv"
@@ -151,7 +151,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 = "1fe0610a52cb2c9afdeef8b81e3d28aea9b74764b3c147dda6088ed5652de8b7"
sha256 = "8300d27b697cf724a61425c470675cb47f3aa6f57be0b2e072e1516a52ee9088"
[[asset]]
path = "crates/ui-egui/src/i18n/bg.tsv"
@@ -162,7 +162,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 = "61600a2c313484fe773515169eaa114e5057eef8194aa44f862372f727c5d20b"
sha256 = "d4385634667253f26ccc71924786b826bced04fc04448748e3d9d96254c67a08"
[[asset]]
path = "crates/ui-egui/src/i18n/kk.tsv"
@@ -173,7 +173,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Kazakh UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "99d683a50491edcbb8cc941291d8cb9c223cd9f9e4bbc8ae756bbba2ab51fccf"
sha256 = "571bc6cb212380db31b01f6c7efe9355cba085e4b3d834d7d835565f1eb62550"
[[asset]]
path = "crates/ui-egui/src/i18n/ja.tsv"
@@ -184,7 +184,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 = "72555257a12e4807df23c6719d22b1a2395c90553ed7fbf17c2c24fbbe108dfc"
sha256 = "914d25f465fb951025f2ef8351626992993cbf70f4e76b9cf55ac9b164b882ae"
[[asset]]
path = "crates/ui-egui/src/i18n/uk.tsv"
@@ -195,7 +195,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 = "4b10d18ba3730cf651b223bd1bd106418729ab9e058aafc92a4ac4808453f050"
sha256 = "ae5ec9ce178c91f843773657a609a32be8ed30800addc17e383415c182417215"
[[asset]]
path = "crates/ui-egui/src/i18n/ar.tsv"
@@ -206,7 +206,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 = "64cf360b242de2043f69b09ff53c61672515dd23f18330323a72a9a57fefbb9e"
sha256 = "8974a9566ffa987a76ed3be123ea9e7899cea83ee79ad42c2d289982329eadff"
[[asset]]
path = "crates/ui-egui/src/i18n/tr.tsv"
@@ -217,7 +217,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Turkish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "ed3e33cee1b8a2501b8c3fc89465462049495ff165a49d74a7c6a7b4b90d6d96"
sha256 = "20b7d9813a3be3da0b111f0b7ff93a91f0ded704ccacd431a7e6de3a1a0bc9ed"
[[asset]]
path = "assets/icons/accessibility.svg"
@@ -3328,4 +3328,4 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Slovene UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "c993902f3fd77e1d2db78da1ed4c66a611199127c482119d80e5e6afad50a734"
sha256 = "3e9c1bdfd0b2c47da16922669a195e6a06018d5b6701b2cc19c18850d03cfd87"
+1 -1
View File
@@ -422,7 +422,7 @@ PdfCraft is young and moving fast. The aim is a workbench where you can view, or
- **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 from the web build, physical printer fidelity, 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:** Office-to-PDF and a Word export that keeps layout, the reported bugs, web printing and the Chinese interface font, 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.
**Next, in order:** Office-to-PDF and a Word export that keeps layout, the reported bugs and the Chinese interface font, 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 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).
+3 -2
View File
@@ -46,7 +46,7 @@ and add a line to the [progress log](#progress-log); when a gap closes, remove i
of 0.4–0.5, switching reports such as #179). Two are only partial: editing (no CJK/RTL) and convert
and share (no in-app Office→PDF; Word export loses layout, #773; password-protected files open in
Acrobat since #836, #774). Those, unmeasured quality against Acrobat, the borrowed `hayro` renderer,
no printing on the web and 245 open issues keep it far from beta.
and 245 open issues keep it far from beta.
### Readiness by audience
@@ -126,7 +126,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md
| Rank | Milestone | Estimate (h) |
|---|---|---|
| 0 | Convert and share: Create PDF from Office, Word export that keeps layout (#773) | 32–55 |
| 1 | Backlog and platform: crash and wrong-result reports, web printing, Chinese UI face (#826) | 65–120 |
| 1 | Backlog and platform: crash and wrong-result reports, Chinese UI face (#826) | 50–90 |
| 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 |
@@ -164,6 +164,7 @@ Newest first: the date, what moved, and the new overall estimate.
- **2026-10-11 (M5.7, scanned signatures, #1110):** Fill & Sign ▸ Add signature has a Scan tab: a phone photo of a signature on paper is framed automatically (all words; table, sheet edge and dust ignored; a sideways signature turned upright), the frame can be moved, resized or redrawn, the photo turned, and an ink-sensitivity slider picks up faint ink. A morphological paper estimate removes shadows and uneven light, and the trimmed ink is saved and reused like an image signature. Photos and images can also be dropped on the Image and Scan tabs. Photos keep the image-signature limits (#1124: a 4 MiB file, up to 32 megapixels decoded, scaled down to 4 megapixels), so a scan never processes more than 4 megapixels. `fill_sign_add` takes `scan` and `sensitivity`. Synthetic engine, UI and automation tests; checked by hand on real phone photos. Milestone and effort estimates unchanged.
- **2026-10-12 (M10.5, hosted CI):** Integrated current main, including its date-table formatting and static-storage fixes. The Arabic synthetic drawing regression now checks the original serialized-output golden with and without the optional font face: the face-dependent FontName and FontFamily are checked separately and normalized only in the fixture. The hosted FreeBSD fingerprint is reproduced by those two metadata fields alone. Drawing bytes and all other fields retain the original golden; no printing behavior or readiness estimate changes.
- **2026-10-11 (M10, gap 4):** The web build prints. Its Print dialog offers "Browser's print dialog" (the default there), which reaches every printer the operating system knows. PdfCraft draws the print-ready PDF's sheets itself with its own renderer, a sheet a frame because the web build has no threads: 300 dpi up to 100 sheets, 200 up to 300, 150 up to 1,000, at most 64 megapixels a sheet and 1 GiB of pictures a job. It then prints them from a hidden iframe, one sheet per printed page with no margin. A landscape sheet among portrait ones is turned onto the paper, as a driver does, because Chromium prints every page at the default `@page` size. Copies, two-sided and colour are set in the browser's dialog, so the dialog greys them out. If the browser refuses to print (a sandboxed frame, for example), the print-ready PDF downloads instead. Verified in headless Chromium: the right sheets at 300 dpi, the landscape sheet turned, no blank trailing page, and the download fallback. Not yet tested in Firefox or Safari. Gap 4 closed; rank 1 is 65–120 → 50–90 h; other estimates unchanged.
- **2026-10-11 (M10.5):** Extracted printer discovery, options and PDF submission into craft-libs/craft-print, shared with DesignCraft. Windows preserves composed sheet size and orientation without fitting into default margins; duplex binding edges are corrected. PdfCraft retains permission checks and imposition. Shared transport and local composition, engine, automation and UI coverage validate the boundary; native Windows CI passes, including paused-queue submission and cleanup; physical printer fidelity remains a hardware check. Rust 1.95 is required by the shared crate. No readiness percentage change.
- **2026-10-11 (M10, #1028):** Recognize Text can read pages that already have text: "Also recognize pages that already have text" in the dialog (remembered with the other choices, and used by Recognize text in multiple files) and `skip_text_pages: false` on `ocr_recognize` and, newly, `ocr_recognize_files`. A picture on such a page becomes searchable, and a recognised word at least half covered by the page's own text (an earlier OCR layer included) is left out, so nothing is found twice; `ocr_recognize` reports how many were left out as `already_on_page`. Before, the option existed only for agents and added the page's text a second time. The default still skips pages with text. Synthetic tests (a picture of a sentence plus real text) cover the engine, the tool and the dialog, and fail with the filter turned off. Still missing: reading only the picture areas (the whole page is read, as for a scan). Separately, `recognize_text_makes_a_scanned_page_searchable` fails on main wherever the models are installed (the model reads "quick" as "auick"). Estimates unchanged.
- **2026-10-11 (M2, page-label prefixes):** The page-label readers bound the total size of decoded prefixes. Each range decodes its own prefix, and ranges may share one prefix object, so a small file could ask for a copy per range: within the 200,000-entry tree limit, about 200 MiB of copies of one 1,024-byte prefix. Inspection and Organize now refuse a tree whose decoded prefixes pass 4 MiB in total (`MAX_PREFIX_TOTAL_BYTES`), before any label is made, as the other tree limits do; relabelling such a tree changes nothing. Synthetic tests share one 1,000-byte prefix across 5,000 ranges and keep exactly 4 MiB of prefixes readable. Overall estimate unchanged.
+5 -1
View File
@@ -47,7 +47,11 @@ The desktop UI discovers printers and submits jobs on workers, so neither operat
the dialog; the Windows backend starts without a console window. Physical printer output
still needs hardware validation.
Not yet: the web spooler, print as image, poster labels, PostScript output, colour conversion for
In a browser there is no spooler: the app (`pdfcraft-ui-egui`'s `web_print`) draws the print-ready
PDF's sheets with PdfCraft's renderer and prints them through the browser's print dialog, which
sets copies, two-sided printing and colour. This crate stays free of web APIs.
Not yet: print as image, poster labels, PostScript output, colour conversion for
grayscale.
## Architectural sheets and scale
+1 -1
View File
@@ -60,7 +60,7 @@ rfd = { workspace = true, features = ["file-handle-inner"] }
wasm-bindgen-futures = "0.4"
wasm-bindgen = "0.2"
js-sys = "0.3"
web-sys = { version = "0.3", features = ["Window", "Document", "Element", "HtmlElement", "HtmlAnchorElement", "Blob", "BlobPropertyBag", "Url"] }
web-sys = { version = "0.3", features = ["Window", "Document", "Element", "HtmlElement", "HtmlAnchorElement", "Blob", "BlobPropertyBag", "Url", "HtmlIFrameElement", "HtmlImageElement", "HtmlCollection", "Node", "EventTarget"] }
[dev-dependencies]
pdfcraft-xfa = { workspace = true }
+6
View File
@@ -2288,6 +2288,12 @@ scroll speed Fast سريعة
Greyscale تدرج رمادي
Black and white أبيض وأسود
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… لا يقبل برنامج تشغيل هذه الطابعة إعدادات الطباعة على الوجهين من البرامج الأخرى، لذا يُطبَّق إعداده الخاص. غيّره من خصائص…
Browser's print dialog مربع حوار الطباعة في المتصفح
Copies, two-sided printing and colour are chosen in the browser's print dialog. يُختار عدد النسخ والطباعة على الوجهين والألوان في مربع حوار الطباعة في المتصفح.
Preparing {n} sheets for printing… جارٍ تجهيز {n} من الأوراق للطباعة…
Preparing sheet {done} of {total} for printing… جارٍ تجهيز الورقة {done} من {total} للطباعة…
Opening the browser's print dialog… جارٍ فتح مربع حوار الطباعة في المتصفح…
Couldn't print from the browser ({e}); downloaded {name} to print instead تعذّرت الطباعة من المتصفح ({e})؛ نُزّل {name} لطباعته بدلًا من ذلك
Tools panel side جانب لوحة الأدوات
Add comment إضافة تعليق
Change tag properties تغيير خصائص الوسم
Can't render this file because it contains an unexpected character in line 6 and column 47.
+6
View File
@@ -2230,6 +2230,12 @@ scroll speed Fast Висока
Greyscale Нива на сивото
Black and white Черно-бяло
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… Драйверът на този принтер не приема настройки за двустранен печат от други програми и прилага собствената си настройка. Променете я от Свойства…
Browser's print dialog Диалог за печат на браузъра
Copies, two-sided printing and colour are chosen in the browser's print dialog. Броят копия, двустранният печат и цветът се избират в диалога за печат на браузъра.
Preparing {n} sheets for printing… Подготовка на листове за печат: {n}…
Preparing sheet {done} of {total} for printing… Подготовка за печат на лист {done} от {total}…
Opening the browser's print dialog… Отваряне на диалога за печат на браузъра…
Couldn't print from the browser ({e}); downloaded {name} to print instead Не може да се печата от браузъра ({e}); вместо това за печат е изтеглен {name}
Tools panel side Страна на панела с инструменти
Add comment Добавяне на коментар
Change tag properties Промяна на свойствата на етикета
Can't render this file because it contains an unexpected character in line 8 and column 16.
+6
View File
@@ -2341,6 +2341,12 @@ scroll speed Fast Rychlá
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… Ovladač této tiskárny nepřijímá nastavení oboustranného tisku z jiných programů, proto platí jeho vlastní nastavení. Změňte ho ve Vlastnosti…
This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. Písmo tohoto pole neumí zobrazit „{c}“ ({code}), proto PdfCraft místo něj ukazuje ?. Hodnota se uloží a jiné prohlížeče PDF pole podle ní překreslí.
The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. Písma, kterými PdfCraft píše, neumí zobrazit „{c}“ ({code}), proto se ve všech prohlížečích PDF zobrazí jako ?. Text se uloží.
Browser's print dialog Dialog tisku prohlížeče
Copies, two-sided printing and colour are chosen in the browser's print dialog. Počet kopií, oboustranný tisk a barva se volí v dialogu tisku prohlížeče.
Preparing {n} sheets for printing… Příprava {n} listů k tisku…
Preparing sheet {done} of {total} for printing… Příprava listu {done} z {total} k tisku…
Opening the browser's print dialog… Otevírá se dialog tisku prohlížeče…
Couldn't print from the browser ({e}); downloaded {name} to print instead Z prohlížeče nelze tisknout ({e}); místo toho byl k tisku stažen soubor {name}
Tools panel side Strana panelu nástrojů
Add comment Přidat komentář
Change tag properties Změnit vlastnosti značky
Can't render this file because it contains an unexpected character in line 6 and column 47.
+6
View File
@@ -2268,6 +2268,12 @@ scroll speed Fast Schnell
Greyscale Graustufen
Black and white Schwarzweiß
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… Der Treiber dieses Druckers übernimmt keine Einstellungen für beidseitigen Druck von anderen Programmen, daher gilt seine eigene Einstellung. Ändern unter Eigenschaften…
Browser's print dialog Druckdialog des Browsers
Copies, two-sided printing and colour are chosen in the browser's print dialog. Exemplare, beidseitiger Druck und Farbe werden im Druckdialog des Browsers gewählt.
Preparing {n} sheets for printing… {n} Blätter werden für den Druck vorbereitet…
Preparing sheet {done} of {total} for printing… Blatt {done} von {total} wird für den Druck vorbereitet…
Opening the browser's print dialog… Der Druckdialog des Browsers wird geöffnet…
Couldn't print from the browser ({e}); downloaded {name} to print instead Drucken aus dem Browser nicht möglich ({e}); stattdessen wurde {name} zum Drucken heruntergeladen
Tools panel side Seite des Werkzeugbereichs
Add comment Kommentar hinzufügen
Change tag properties Tag-Eigenschaften ändern
Can't render this file because it contains an unexpected character in line 6 and column 51.
+6
View File
@@ -2282,6 +2282,12 @@ scroll speed Fast Γρήγορη
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… Το πρόγραμμα οδήγησης αυτού του εκτυπωτή δεν δέχεται ρυθμίσεις εκτύπωσης διπλής όψης από άλλα προγράμματα, οπότε ισχύει η δική του ρύθμιση. Αλλάξτε τη στις Ιδιότητες…
This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. Η γραμματοσειρά αυτού του πεδίου δεν μπορεί να σχεδιάσει το «{c}» ({code}), γι' αυτό το PdfCraft δείχνει ? στη θέση του. Η τιμή αποθηκεύεται και άλλα προγράμματα προβολής PDF επανασχεδιάζουν το πεδίο από αυτήν.
The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. Οι γραμματοσειρές με τις οποίες γράφει το PdfCraft δεν μπορούν να σχεδιάσουν το «{c}» ({code}), γι' αυτό εμφανίζεται ως ? σε κάθε πρόγραμμα προβολής PDF. Το κείμενο αποθηκεύεται.
Browser's print dialog Παράθυρο εκτύπωσης του προγράμματος περιήγησης
Copies, two-sided printing and colour are chosen in the browser's print dialog. Τα αντίγραφα, η εκτύπωση διπλής όψης και το χρώμα επιλέγονται στο παράθυρο εκτύπωσης του προγράμματος περιήγησης.
Preparing {n} sheets for printing… Προετοιμασία {n} φύλλων για εκτύπωση…
Preparing sheet {done} of {total} for printing… Προετοιμασία φύλλου {done} από {total} για εκτύπωση…
Opening the browser's print dialog… Άνοιγμα του παραθύρου εκτύπωσης του προγράμματος περιήγησης…
Couldn't print from the browser ({e}); downloaded {name} to print instead Δεν ήταν δυνατή η εκτύπωση από το πρόγραμμα περιήγησης ({e}); έγινε λήψη του {name} για εκτύπωση
Tools panel side Πλευρά πλαισίου εργαλείων
Add comment Προσθήκη σχολίου
Change tag properties Αλλαγή ιδιοτήτων ετικέτας
Can't render this file because it contains an unexpected character in line 479 and column 12.
+6
View File
@@ -2397,6 +2397,12 @@ scroll speed Fast Rápida
Greyscale Escala de grises
Black and white Blanco y negro
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… El controlador de esta impresora no acepta los ajustes de doble cara de otros programas, así que se aplica su propio ajuste. Cámbielo en Propiedades…
Browser's print dialog Diálogo de impresión del navegador
Copies, two-sided printing and colour are chosen in the browser's print dialog. Las copias, la impresión a doble cara y el color se eligen en el diálogo de impresión del navegador.
Preparing {n} sheets for printing… Preparando {n} hojas para imprimir…
Preparing sheet {done} of {total} for printing… Preparando la hoja {done} de {total} para imprimir…
Opening the browser's print dialog… Abriendo el diálogo de impresión del navegador…
Couldn't print from the browser ({e}); downloaded {name} to print instead No se pudo imprimir desde el navegador ({e}); se descargó {name} para imprimirlo
Tools panel side Lado del panel de herramientas
Add comment Añadir comentario
Change tag properties Cambiar propiedades de la etiqueta
Can't render this file because it contains an unexpected character in line 6 and column 60.
+6
View File
@@ -2267,6 +2267,12 @@ scroll speed Fast Rapide
Greyscale Niveaux de gris
Black and white Noir et blanc
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… Le pilote de cette imprimante n'accepte pas les réglages recto verso des autres programmes ; son propre réglage s'applique. Modifiez-le dans Propriétés…
Browser's print dialog Boîte de dialogue d'impression du navigateur
Copies, two-sided printing and colour are chosen in the browser's print dialog. Le nombre de copies, l'impression recto verso et la couleur se choisissent dans la boîte de dialogue d'impression du navigateur.
Preparing {n} sheets for printing… Préparation de {n} feuilles pour l'impression…
Preparing sheet {done} of {total} for printing… Préparation de la feuille {done} sur {total} pour l'impression…
Opening the browser's print dialog… Ouverture de la boîte de dialogue d'impression du navigateur…
Couldn't print from the browser ({e}); downloaded {name} to print instead Impossible d'imprimer depuis le navigateur ({e}) ; {name} a été téléchargé pour être imprimé
Tools panel side Côté du panneau Outils
Add comment Ajouter un commentaire
Change tag properties Modifier les propriétés de la balise
Can't render this file because it contains an unexpected character in line 6 and column 39.
+6
View File
@@ -2266,6 +2266,12 @@ scroll speed Fast Gyors
Greyscale Szürkeárnyalatos
Black and white Fekete-fehér
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… A nyomtató illesztőprogramja nem fogadja el más programoktól a kétoldalas nyomtatás beállítását, ezért a saját beállítása érvényes. Módosítás: Tulajdonságok…
Browser's print dialog A böngésző nyomtatási párbeszédablaka
Copies, two-sided printing and colour are chosen in the browser's print dialog. A példányszámot, a kétoldalas nyomtatást és a színt a böngésző nyomtatási párbeszédablakában lehet megadni.
Preparing {n} sheets for printing… {n} lap előkészítése nyomtatásra…
Preparing sheet {done} of {total} for printing… {done}/{total}. lap előkészítése nyomtatásra…
Opening the browser's print dialog… A böngésző nyomtatási párbeszédablakának megnyitása…
Couldn't print from the browser ({e}); downloaded {name} to print instead Nem sikerült nyomtatni a böngészőből ({e}); helyette a(z) {name} letöltődött nyomtatáshoz
Tools panel side Az Eszközök panel oldala
Add comment Megjegyzés hozzáadása
Change tag properties Címke tulajdonságainak módosítása
Can't render this file because it contains an unexpected character in line 7 and column 44.
+6
View File
@@ -2276,6 +2276,12 @@ scroll speed Fast Veloce
Greyscale Scala di grigi
Black and white Bianco e nero
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… Il driver di questa stampante non accetta le impostazioni fronte-retro da altri programmi, quindi vale la sua impostazione. Modificala in Proprietà…
Browser's print dialog Finestra di stampa del browser
Copies, two-sided printing and colour are chosen in the browser's print dialog. Copie, stampa fronte/retro e colore si scelgono nella finestra di stampa del browser.
Preparing {n} sheets for printing… Preparazione di {n} fogli per la stampa…
Preparing sheet {done} of {total} for printing… Preparazione del foglio {done} di {total} per la stampa…
Opening the browser's print dialog… Apertura della finestra di stampa del browser…
Couldn't print from the browser ({e}); downloaded {name} to print instead Impossibile stampare dal browser ({e}); è stato scaricato {name} da stampare
Tools panel side Lato del pannello Strumenti
Add comment Aggiungi commento
Change tag properties Modifica proprietà del tag
Can't render this file because it contains an unexpected character in line 6 and column 47.
+6
View File
@@ -2267,6 +2267,12 @@ scroll speed Fast 速い
Greyscale グレースケール
Black and white 白黒
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… このプリンターのドライバーは他のプログラムからの両面印刷の設定を受け付けないため、ドライバー自身の設定が使われます。変更先: プロパティ…
Browser's print dialog ブラウザーの印刷ダイアログ
Copies, two-sided printing and colour are chosen in the browser's print dialog. 部数、両面印刷、カラーはブラウザーの印刷ダイアログで選びます。
Preparing {n} sheets for printing… {n} 枚のシートを印刷用に準備しています…
Preparing sheet {done} of {total} for printing… 印刷用にシート {done}/{total} を準備しています…
Opening the browser's print dialog… ブラウザーの印刷ダイアログを開いています…
Couldn't print from the browser ({e}); downloaded {name} to print instead ブラウザーから印刷できませんでした ({e})。代わりに印刷用の {name} をダウンロードしました
Tools panel side ツールパネルの位置
Add comment コメントを追加
Change tag properties タグのプロパティを変更
Can't render this file because it contains an unexpected character in line 6 and column 47.
+6
View File
@@ -2264,6 +2264,12 @@ scroll speed Fast Жылдам
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… Бұл принтердің драйвері басқа бағдарламалардан екі жақты басып шығару баптауларын қабылдамайды, сондықтан өз баптауы қолданылады. Оны мына жерден өзгертіңіз: Сипаттар…
This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. Бұл өрістің қаріпі «{c}» ({code}) таңбасын сала алмайды, сондықтан PdfCraft оның орнына ? көрсетеді. Мән сақталады, ал басқа PDF қарау құралдары өрісті соған қарап қайта салады.
The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. PdfCraft жазатын қаріптер «{c}» ({code}) таңбасын сала алмайды, сондықтан ол барлық PDF қарау құралдарында ? болып көрінеді. Мәтін сақталады.
Browser's print dialog Браузердің басып шығару терезесі
Copies, two-sided printing and colour are chosen in the browser's print dialog. Көшірмелер саны, екі жақты басып шығару және түс браузердің басып шығару терезесінде таңдалады.
Preparing {n} sheets for printing… Басып шығаруға парақтар дайындалуда: {n}…
Preparing sheet {done} of {total} for printing… Басып шығаруға {total} парақтың {done}-сі дайындалуда…
Opening the browser's print dialog… Браузердің басып шығару терезесі ашылуда…
Couldn't print from the browser ({e}); downloaded {name} to print instead Браузерден басып шығару мүмкін болмады ({e}); оның орнына басып шығару үшін {name} жүктелді
Tools panel side Құралдар панелінің жағы
Add comment Түсініктеме қосу
Change tag properties Тег қасиеттерін өзгерту
Can't render this file because it contains an unexpected character in line 820 and column 12.
+6
View File
@@ -2247,6 +2247,12 @@ scroll speed Fast Szybka
Greyscale Skala szarości
Black and white Czarno-biały
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… Sterownik tej drukarki nie przyjmuje ustawień druku dwustronnego z innych programów, więc obowiązuje jego własne ustawienie. Zmień je w oknie Właściwości…
Browser's print dialog Okno drukowania przeglądarki
Copies, two-sided printing and colour are chosen in the browser's print dialog. Liczbę kopii, druk dwustronny i kolor wybiera się w oknie drukowania przeglądarki.
Preparing {n} sheets for printing… Przygotowywanie arkuszy do druku: {n}…
Preparing sheet {done} of {total} for printing… Przygotowywanie arkusza {done} z {total} do druku…
Opening the browser's print dialog… Otwieranie okna drukowania przeglądarki…
Couldn't print from the browser ({e}); downloaded {name} to print instead Nie można drukować z przeglądarki ({e}); zamiast tego pobrano plik {name} do wydruku
Tools panel side Strona panelu narzędzi
Add comment Dodaj komentarz
Change tag properties Zmień właściwości znacznika
Can't render this file because it contains an unexpected character in line 6 and column 47.
+6
View File
@@ -2281,6 +2281,12 @@ scroll speed Fast Rápida
Greyscale Escala de cinza
Black and white Preto e branco
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… O driver desta impressora não aceita configurações de frente e verso de outros programas, então vale a configuração dele. Altere em Propriedades…
Browser's print dialog Caixa de diálogo de impressão do navegador
Copies, two-sided printing and colour are chosen in the browser's print dialog. Cópias, impressão frente e verso e cor são escolhidas na caixa de diálogo de impressão do navegador.
Preparing {n} sheets for printing… Preparando {n} folhas para impressão…
Preparing sheet {done} of {total} for printing… Preparando a folha {done} de {total} para impressão…
Opening the browser's print dialog… Abrindo a caixa de diálogo de impressão do navegador…
Couldn't print from the browser ({e}); downloaded {name} to print instead Não foi possível imprimir pelo navegador ({e}); {name} foi baixado para impressão
Tools panel side Lado do painel Ferramentas
Add comment Adicionar comentário
Change tag properties Alterar propriedades da tag
Can't render this file because it contains an unexpected character in line 9 and column 16.
+6
View File
@@ -2180,6 +2180,12 @@ scroll speed Fast Высокая
Greyscale Оттенки серого
Black and white Чёрно-белый
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… Драйвер этого принтера не принимает настройки двусторонней печати от других программ, поэтому действует его собственная настройка. Измените её в окне Свойства…
Browser's print dialog Диалог печати браузера
Copies, two-sided printing and colour are chosen in the browser's print dialog. Число копий, двустороннюю печать и цвет выбирают в диалоге печати браузера.
Preparing {n} sheets for printing… Подготовка листов к печати: {n}…
Preparing sheet {done} of {total} for printing… Подготовка к печати листа {done} из {total}…
Opening the browser's print dialog… Открывается диалог печати браузера…
Couldn't print from the browser ({e}); downloaded {name} to print instead Не удалось напечатать из браузера ({e}); вместо этого для печати скачан файл {name}
Tools panel side Сторона панели инструментов
Add comment Добавить комментарий
Change tag properties Изменить свойства тега
Can't render this file because it contains an unexpected character in line 9 and column 16.
+6
View File
@@ -2271,6 +2271,12 @@ scroll speed Fast Hitro
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… Gonilnik tega tiskalnika ne sprejema nastavitev obojestranskega tiskanja iz drugih programov, zato velja njegova lastna nastavitev. Spremenite jo v Lastnosti…
This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. Pisava tega polja ne more prikazati »{c}« ({code}), zato PdfCraft namesto njega prikaže ?. Vrednost se shrani, drugi pregledovalniki PDF pa polje znova narišejo iz nje.
The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. Pisave, s katerimi piše PdfCraft, ne morejo prikazati »{c}« ({code}), zato se v vseh pregledovalnikih PDF prikaže kot ?. Besedilo se shrani.
Browser's print dialog Pogovorno okno za tiskanje v brskalniku
Copies, two-sided printing and colour are chosen in the browser's print dialog. Število kopij, obojestransko tiskanje in barvo izberete v pogovornem oknu za tiskanje v brskalniku.
Preparing {n} sheets for printing… Priprava listov za tiskanje: {n}…
Preparing sheet {done} of {total} for printing… Priprava lista {done} od {total} za tiskanje…
Opening the browser's print dialog… Odpiranje pogovornega okna za tiskanje v brskalniku…
Couldn't print from the browser ({e}); downloaded {name} to print instead Tiskanje iz brskalnika ni mogoče ({e}); namesto tega je bila za tiskanje prenesena datoteka {name}
Tools panel side Stran podokna z orodji
Add comment Dodaj komentar
Change tag properties Spremeni lastnosti oznake
Can't render this file because it contains an unexpected character in line 9 and column 16.
+6
View File
@@ -2180,6 +2180,12 @@ scroll speed Fast వేగంగా
Original అసలు
Greyscale గ్రేస్కేల్
Black and white నలుపు తెలుపు
Browser's print dialog బ్రౌజర్ ముద్రణ డైలాగ్
Copies, two-sided printing and colour are chosen in the browser's print dialog. కాపీలు, రెండు వైపుల ముద్రణ మరియు రంగును బ్రౌజర్ ముద్రణ డైలాగ్‌లో ఎంచుకుంటారు.
Preparing {n} sheets for printing… ముద్రణ కోసం {n} షీట్‌లను సిద్ధం చేస్తోంది…
Preparing sheet {done} of {total} for printing… ముద్రణ కోసం {total}లో షీట్ {done}ను సిద్ధం చేస్తోంది…
Opening the browser's print dialog… బ్రౌజర్ ముద్రణ డైలాగ్‌ను తెరుస్తోంది…
Couldn't print from the browser ({e}); downloaded {name} to print instead బ్రౌజర్ నుండి ముద్రించలేకపోయింది ({e}); బదులుగా ముద్రించడానికి {name}ను డౌన్‌లోడ్ చేసింది
Tools panel side టూల్స్ ప్యానెల్ వైపు
Add comment వ్యాఖ్యను జోడించు
Change tag properties ట్యాగ్ లక్షణాలను మార్చు
Can't render this file because it contains an unexpected character in line 13 and column 16.
+6
View File
@@ -2262,6 +2262,12 @@ scroll speed Fast Hızlı
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… Bu yazıcının sürücüsü diğer programlardan gelen çift taraflı yazdırma ayarlarını kabul etmez, bu yüzden kendi ayarı geçerlidir. Değiştirmek için: Özellikler…
This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. Bu alanın yazı tipi “{c}” ({code}) karakterini çizemiyor, bu yüzden PdfCraft yerine ? gösteriyor. Değer kaydedilir ve diğer PDF görüntüleyiciler alanı ondan yeniden çizer.
The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. PdfCraft'ın yazdığı yazı tipleri “{c}” ({code}) karakterini çizemiyor, bu yüzden her PDF görüntüleyicide ? olarak görünür. Metin kaydedilir.
Browser's print dialog Tarayıcının yazdırma penceresi
Copies, two-sided printing and colour are chosen in the browser's print dialog. Kopya sayısı, çift taraflı yazdırma ve renk tarayıcının yazdırma penceresinde seçilir.
Preparing {n} sheets for printing… Yazdırma için {n} sayfa hazırlanıyor…
Preparing sheet {done} of {total} for printing… Yazdırma için sayfa {done}/{total} hazırlanıyor…
Opening the browser's print dialog… Tarayıcının yazdırma penceresi açılıyor…
Couldn't print from the browser ({e}); downloaded {name} to print instead Tarayıcıdan yazdırılamadı ({e}); bunun yerine yazdırmak için {name} indirildi
Tools panel side Araçlar panelinin yeri
Add comment Yorum ekle
Change tag properties Etiket özelliklerini değiştir
Can't render this file because it contains an unexpected character in line 819 and column 12.
+6
View File
@@ -2289,6 +2289,12 @@ scroll speed Fast Швидка
Greyscale Відтінки сірого
Black and white Чорно-білий
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… Драйвер цього принтера не приймає налаштування двобічного друку від інших програм, тому діє його власне налаштування. Змініть його у вікні Властивості…
Browser's print dialog Діалог друку браузера
Copies, two-sided printing and colour are chosen in the browser's print dialog. Кількість копій, двосторонній друк і колір вибирають у діалозі друку браузера.
Preparing {n} sheets for printing… Підготовка аркушів до друку: {n}…
Preparing sheet {done} of {total} for printing… Підготовка до друку аркуша {done} з {total}…
Opening the browser's print dialog… Відкривається діалог друку браузера…
Couldn't print from the browser ({e}); downloaded {name} to print instead Не вдалося надрукувати з браузера ({e}); натомість для друку завантажено файл {name}
Tools panel side Бік панелі інструментів
Add comment Додати коментар
Change tag properties Змінити властивості тегу
Can't render this file because it contains an unexpected character in line 6 and column 47.
+6
View File
@@ -2231,6 +2231,12 @@ scroll speed Fast 快
Greyscale 灰度
Black and white 黑白
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… 此打印机的驱动程序不接受其他程序的双面打印设置,因此使用其自身设置。更改位置:属性…
Browser's print dialog 浏览器的打印对话框
Copies, two-sided printing and colour are chosen in the browser's print dialog. 份数、双面打印和颜色在浏览器的打印对话框中选择。
Preparing {n} sheets for printing… 正在准备 {n} 张纸用于打印…
Preparing sheet {done} of {total} for printing… 正在准备打印第 {done} 张,共 {total} 张…
Opening the browser's print dialog… 正在打开浏览器的打印对话框…
Couldn't print from the browser ({e}); downloaded {name} to print instead 无法从浏览器打印({e});已改为下载 {name} 以便打印
Tools panel side 工具面板位置
Add comment 添加注释
Change tag properties 更改标签属性
Can't render this file because it contains an unexpected character in line 9 and column 16.
+6
View File
@@ -2141,6 +2141,12 @@ scroll speed Fast 快
Greyscale 灰階
Black and white 黑白
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… 此印表機的驅動程式不接受其他程式的雙面列印設定,因此使用其自身設定。變更位置:屬性…
Browser's print dialog 瀏覽器的列印對話框
Copies, two-sided printing and colour are chosen in the browser's print dialog. 份數、雙面列印和色彩在瀏覽器的列印對話框中選擇。
Preparing {n} sheets for printing… 正在準備 {n} 張紙以供列印…
Preparing sheet {done} of {total} for printing… 正在準備列印第 {done} 張,共 {total} 張…
Opening the browser's print dialog… 正在開啟瀏覽器的列印對話框…
Couldn't print from the browser ({e}); downloaded {name} to print instead 無法從瀏覽器列印({e});已改為下載 {name} 以供列印
Tools panel side 工具面板位置
Add comment 新增註解
Change tag properties 變更標籤屬性
Can't render this file because it contains an unexpected character in line 9 and column 16.
+1
View File
@@ -93,6 +93,7 @@ pub mod prepare;
mod print_ui;
mod signature_drag;
pub mod units;
mod web_print;
pub use print_ui::{Handling as PrintHandling, PrintDraft, PrinterSource, Which as PrintWhich, system_printers};
mod redact_ui;
pub use redact_ui::{HiddenDraft, PagesDraft as RedactPagesDraft, RedactPrefs, SearchDraft as RedactSearchDraft, SearchMode as RedactSearchMode};
+143 -19
View File
@@ -195,6 +195,34 @@ pub(crate) struct PrintTasks {
printers: Option<std::sync::mpsc::Receiver<Vec<spool::Printer>>>,
#[cfg(not(target_arch = "wasm32"))]
jobs: Vec<(String, std::sync::mpsc::Receiver<Result<String, String>>)>,
/// The browser print being drawn, a sheet a frame (`web_print`).
#[cfg(target_arch = "wasm32")]
web_job: Option<crate::web_print::WebPrintJob>,
/// What went wrong after the browser print page was handed over, with the print-ready PDF
/// to download instead.
#[cfg(target_arch = "wasm32")]
web_failed: std::sync::Arc<std::sync::Mutex<Vec<String>>>,
/// The last browser print's PDF and name, for that fallback.
#[cfg(target_arch = "wasm32")]
web_last: Option<(String, std::sync::Arc<Vec<u8>>)>,
}
/// The printers to offer when no [`PrinterSource`] is set: the spooler's, or in a browser its
/// print dialog.
fn listed_printers() -> Vec<spool::Printer> {
#[cfg(target_arch = "wasm32")]
{
vec![spool::Printer { name: crate::web_print::BROWSER_PRINTER.to_string(), default: true, two_sided: None }]
}
#[cfg(not(target_arch = "wasm32"))]
{
spool::printers()
}
}
/// A printer's name as the dialog shows it.
fn printer_label(name: &str) -> String {
if name == crate::web_print::BROWSER_PRINTER { crate::web_print::browser_printer_label().to_string() } else { name.to_string() }
}
/// The printer the dialog preselects: the system default, else the first.
@@ -212,7 +240,7 @@ impl PdfCraftApp {
self.list_printers_in_background(source);
(self.print_draft.printers.clone(), true)
}
_ => (spool::printers(), false),
_ => (listed_printers(), false),
};
let default = default_printer(&printers);
let current = self.views[i].current;
@@ -260,6 +288,8 @@ impl PdfCraftApp {
/// Pick up a printer list or a spooled job that finished (each frame).
pub(crate) fn poll_print(&mut self) {
#[cfg(target_arch = "wasm32")]
self.poll_web_print();
#[cfg(not(target_arch = "wasm32"))]
{
if let Some(rx) = &self.print_tasks.printers {
@@ -332,6 +362,17 @@ impl PdfCraftApp {
}
};
match self.print_draft.printer.clone() {
#[cfg(target_arch = "wasm32")]
Some(printer) if printer == crate::web_print::BROWSER_PRINTER => {
let sizes: Vec<(f64, f64)> = doc.info.pages.iter().map(|p| (f64::from(p.width), f64::from(p.height))).collect();
match print::layout(&sizes, &settings) {
Ok(sheets) => self.print_in_browser(name, bytes, sheets.iter().map(|s| s.size).collect()),
Err(e) => {
self.notify_error(e);
false
}
}
}
Some(printer) => self.send_to_printer(printer, bytes, self.print_draft.job(&name)),
None => self.save_print_pdf(&name, bytes),
}
@@ -364,6 +405,75 @@ impl PdfCraftApp {
ok
}
/// Print through the browser's print dialog: start drawing `sheets` (the pages of the
/// print-ready `bytes`), a sheet a frame in [`PdfCraftApp::poll_print`]. A print still being
/// prepared is replaced. The dialog closes at once.
#[cfg(target_arch = "wasm32")]
fn print_in_browser(&mut self, name: String, bytes: Vec<u8>, sheets: Vec<(f64, f64)>) -> bool {
match crate::web_print::WebPrintJob::new(bytes, sheets, name) {
Ok(job) => {
let n = job.total().to_string();
self.print_tasks.web_job = Some(job);
self.notify_fmt("Preparing {n} sheets for printing…", &[("n", &n)]);
if let Some(ctx) = &self.ctx {
ctx.request_repaint();
}
true
}
Err(e) => {
self.notify_error(e);
false
}
}
}
/// Draw the next sheet of the browser print, and hand the finished page to the browser.
#[cfg(target_arch = "wasm32")]
fn poll_web_print(&mut self) {
let failed: Vec<String> = self.print_tasks.web_failed.lock().map(|mut q| std::mem::take(&mut *q)).unwrap_or_default();
if let Some(e) = failed.into_iter().next() {
self.web_print_failed(&e);
}
let Some(job) = self.print_tasks.web_job.as_mut() else { return };
if !job.finished() {
if let Err(e) = job.step(&mut crate::web_print::blob_url) {
self.print_tasks.web_job = None;
self.notify_error(e);
return;
}
let (done, total) = (job.done().to_string(), job.total().to_string());
self.notify_fmt("Preparing sheet {done} of {total} for printing…", &[("done", &done), ("total", &total)]);
if let Some(ctx) = &self.ctx {
ctx.request_repaint();
}
return;
}
let Some(mut job) = self.print_tasks.web_job.take() else { return };
let html = job.html();
let urls = job.take_urls();
self.print_tasks.web_last = Some((job.name.clone(), job.pdf.clone()));
match crate::web_print::print_in_iframe(&html, urls, self.print_tasks.web_failed.clone(), self.ctx.clone()) {
Ok(()) => self.notify_tr("Opening the browser's print dialog…"),
Err(e) => self.web_print_failed(&e),
}
}
/// The browser wouldn't print (a sandboxed frame, a picture it couldn't decode): offer the
/// print-ready PDF as a download instead, so the work isn't lost.
#[cfg(target_arch = "wasm32")]
fn web_print_failed(&mut self, error: &str) {
let Some((name, pdf)) = self.print_tasks.web_last.take() else {
self.notify_error(error);
return;
};
let stem = name.trim_end_matches(".pdf").trim_end_matches(".PDF");
let file = format!("{stem} (print).pdf");
match crate::editing::download(&file, &pdf) {
Ok(()) => self.notify_fmt("Couldn't print from the browser ({e}); downloaded {name} to print instead", &[("e", error), ("name", &file)]),
Err(_) => self.notify_error(error),
}
}
/// Print ▸ Save as PDF on the desktop: write to `save_override` (tests and automation), or
/// ask where and write on a later frame. Returns `true` once written, or once the save picker
/// is showing.
@@ -551,14 +661,21 @@ pub(crate) fn body(
egui::Grid::new("print-top").num_columns(2).spacing([10.0, 8.0]).show(ui, |ui| {
ui.label(tl!("Printer:"));
let before = d.printer.clone();
// The browser's print dialog sets copies, two-sided printing and colour itself.
let browser = d.printer.as_deref() == Some(crate::web_print::BROWSER_PRINTER);
ui.horizontal(|ui| {
let shown = d.printer.clone().unwrap_or_else(|| tl!("Save as PDF").to_string());
let shown = d.printer.as_deref().map_or_else(|| tl!("Save as PDF").to_string(), printer_label);
egui::ComboBox::from_id_salt("printer").selected_text(shown).width(260.0).show_ui(ui, |ui| {
if d.printers_loading {
ui.add_enabled(false, egui::Label::new(tl!("Looking for printers…")));
}
for p in &d.printers {
let label = if p.default { crate::i18n::fmt(tl!("{name} (default)"), &[("name", &p.name)]) } else { p.name.clone() };
let name = printer_label(&p.name);
let label = if p.default && p.name != crate::web_print::BROWSER_PRINTER {
crate::i18n::fmt(tl!("{name} (default)"), &[("name", &name)])
} else {
name
};
ui.selectable_value(&mut d.printer, Some(p.name.clone()), label);
}
ui.selectable_value(&mut d.printer, None, tl!("Save as PDF"));
@@ -568,7 +685,7 @@ pub(crate) fn body(
} else {
tl!("The printer driver's own settings for this print: paper tray, paper type, finishing and more.")
};
let properties = ui.add_enabled(d.printer.is_some(), egui::Button::new(tl!("Properties…"))).on_hover_text(tip);
let properties = ui.add_enabled(d.printer.is_some() && !browser, egui::Button::new(tl!("Properties…"))).on_hover_text(tip);
if properties.clicked()
&& let Some(p) = d.printer.clone()
{
@@ -590,24 +707,28 @@ pub(crate) fn body(
}
ui.end_row();
ui.label(tl!("Copies:"));
ui.horizontal(|ui| {
ui.add(egui::DragValue::new(&mut d.copies).range(1..=999));
ui.add(widgets::checkbox(&mut d.collate, tl!("Collate")));
ui.add(widgets::checkbox(&mut d.grayscale, tl!("Print in grayscale")));
ui.add_enabled_ui(!browser, |ui| {
ui.horizontal(|ui| {
ui.add(egui::DragValue::new(&mut d.copies).range(1..=999));
ui.add(widgets::checkbox(&mut d.collate, tl!("Collate")));
ui.add(widgets::checkbox(&mut d.grayscale, tl!("Print in grayscale")));
});
});
ui.end_row();
ui.label(tl!("Two-sided:"));
combo(
ui,
"duplex",
&mut d.duplex,
&[
(spool::Duplex::Off, tl!("Off")),
(spool::Duplex::LongEdge, tl!("Flip on long edge")),
(spool::Duplex::ShortEdge, tl!("Flip on short edge")),
],
160.0,
);
ui.add_enabled_ui(!browser, |ui| {
combo(
ui,
"duplex",
&mut d.duplex,
&[
(spool::Duplex::Off, tl!("Off")),
(spool::Duplex::LongEdge, tl!("Flip on long edge")),
(spool::Duplex::ShortEdge, tl!("Flip on short edge")),
],
160.0,
);
});
ui.end_row();
ui.label(tl!("Paper:"));
let paper_name = PAPERS.get(d.paper).map_or("US Letter", |p| p.0);
@@ -625,6 +746,9 @@ pub(crate) fn body(
{
ui.label(egui::RichText::new(tl!("This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties…")).small().color(t.text_muted));
}
if d.printer.as_deref() == Some(crate::web_print::BROWSER_PRINTER) {
ui.label(egui::RichText::new(tl!("Copies, two-sided printing and colour are chosen in the browser's print dialog.")).small().color(t.text_muted));
}
if let Some(e) = &d.driver_error {
ui.label(egui::RichText::new(crate::i18n::fmt(tl!("Couldn't open the printer's preferences: {e}"), &[("e", e)])).small().color(t.text_muted));
}
+453
View File
@@ -0,0 +1,453 @@
//! Printing from the browser (docs/gaps.md gap 4). A web page can't reach a spooler, so the
//! print-ready PDF is drawn sheet by sheet with PdfCraft's own renderer, one sheet a frame (the
//! web build has no threads), and the pictures go to the browser's print dialog in a hidden
//! iframe, one sheet per printed page. That prints the same in every browser, with or without a
//! PDF viewer of its own, and matches the preview. Copies, two-sided printing and colour are
//! chosen in the browser's dialog.
//!
//! The planning, the print page and the job are plain Rust and tested on every target; only
//! handing the pictures to the browser (`blob_url`, `print_in_iframe`) is wasm-only.
// Outside the browser only the tests drive the job; the app itself prints through the spooler.
#![cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
use pdfcraft_engine::export::{ExportSource, Exporter};
/// The printer the web build lists: the browser's print dialog, which reaches the printers the
/// operating system knows. An id, never shown; the dialog shows [`browser_printer_label`].
pub(crate) const BROWSER_PRINTER: &str = "\u{1}browser";
/// The browser printer's name in the UI language.
pub(crate) fn browser_printer_label() -> &'static str {
tl!("Browser's print dialog")
}
/// The most sheets a browser print takes: each is a picture held by the page until the dialog
/// closes. Longer jobs are saved as the print-ready PDF and printed from a PDF viewer.
pub(crate) const MAX_SHEETS: usize = 1000;
/// The most pixels one sheet is drawn with (the Windows spooler's limit too): about A0 at 200 dpi.
pub(crate) const MAX_SHEET_PIXELS: f64 = 64.0e6;
/// The most PNG bytes one job may hold before it stops, so a long job of busy pages can't exhaust
/// the tab's memory.
pub(crate) const MAX_JOB_BYTES: u64 = 1 << 30;
/// The lowest resolution a sheet is drawn at, whatever its size.
const MIN_DPI: f64 = 72.0;
/// The resolution each sheet (`(width, height)` in points) is drawn at: 300 dpi for up to 100
/// sheets, 200 up to 300 and 150 up to [`MAX_SHEETS`], lowered for a sheet that would exceed
/// [`MAX_SHEET_PIXELS`]. Refused for no sheets, too many, or a sheet without a usable size.
pub(crate) fn plan_dpi(sheets: &[(f64, f64)]) -> Result<Vec<f64>, String> {
let base: f64 = match sheets.len() {
0 => return Err("there are no sheets to print".into()),
1..=100 => 300.0,
101..=300 => 200.0,
n if n <= MAX_SHEETS => 150.0,
n => {
return Err(format!(
"{n} sheets are too many to print from the browser (at most {MAX_SHEETS}); save the print-ready PDF and print it from a PDF viewer"
));
}
};
sheets
.iter()
.enumerate()
.map(|(i, &(w, h))| {
if !(w.is_finite() && h.is_finite()) || w < 1.0 || h < 1.0 {
return Err(format!("sheet {} has no usable size", i + 1));
}
let square_inches = (w / 72.0) * (h / 72.0);
let cap = (MAX_SHEET_PIXELS / square_inches).sqrt();
let dpi = if cap.is_finite() { base.min(cap) } else { base };
Ok(dpi.max(MIN_DPI))
})
.collect()
}
/// A length in points for CSS, rounded to 1/1000 pt.
fn pt(v: f64) -> String {
let r = (v * 1000.0).round() / 1000.0;
format!("{r}pt")
}
/// `"` `&` `<` `>` escaped for an HTML attribute value.
fn attr(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => out.push_str("&amp;"),
'"' => out.push_str("&quot;"),
'<' => out.push_str("&lt;"),
'>' => out.push_str("&gt;"),
c => out.push(c),
}
}
out
}
/// Whether two sizes in points are the same within half a point.
fn same_size(a: (f64, f64), b: (f64, f64)) -> bool {
(a.0 - b.0).abs() <= 0.5 && (a.1 - b.1).abs() <= 0.5
}
/// The page the browser prints: each sheet's picture (`urls`, in order) filling a printed page
/// with no margin. The paper is the first sheet's size (the default `@page`). A sheet of that
/// size turned sideways (a landscape sheet among portrait ones) is turned 90° counter-clockwise
/// onto it, as a printer driver does, because browsers print every page at the default size
/// whatever named pages ask for (Chromium does). Any other size gets a named `@page` of its own,
/// which browsers that support it honour. Only the last sheet has no page break after it, so no
/// blank page follows.
pub(crate) fn print_document_html(sheets: &[(f64, f64)], urls: &[String]) -> String {
let paper = sheets.first().copied().unwrap_or((612.0, 792.0));
let (pw, ph) = (pt(paper.0), pt(paper.1));
let mut css = format!(
"html,body{{margin:0;padding:0;background:#fff}}@page{{size:{pw} {ph};margin:0}}\
div{{position:relative;overflow:hidden;margin:0;padding:0;width:{pw};height:{ph};break-after:page;page-break-after:always}}\
div:last-child{{break-after:auto;page-break-after:auto}}img{{display:block;position:absolute;left:0;top:0;margin:0;padding:0;border:0}}\
.turned img{{width:{ph};height:{pw};transform-origin:0 0;transform:translate(0,{ph}) rotate(-90deg)}}"
);
// Sizes other than the paper's, each a named page.
let mut others: Vec<(f64, f64)> = Vec::new();
let mut html_sheets = String::new();
for (&size, url) in sheets.iter().zip(urls) {
let src = attr(url);
if same_size(size, paper) {
html_sheets.push_str(&format!("<div><img alt=\"\" style=\"width:{pw};height:{ph}\" src=\"{src}\"></div>"));
} else if same_size((size.1, size.0), paper) {
html_sheets.push_str(&format!("<div class=\"turned\"><img alt=\"\" src=\"{src}\"></div>"));
} else {
let k = match others.iter().position(|o| same_size(*o, size)) {
Some(k) => k,
None => {
let (w, h) = (pt(size.0), pt(size.1));
css.push_str(&format!("@page s{k}{{size:{w} {h};margin:0}}div.s{k}{{page:s{k};width:{w};height:{h}}}", k = others.len()));
others.push(size);
others.len() - 1
}
};
let (w, h) = (pt(size.0), pt(size.1));
html_sheets.push_str(&format!("<div class=\"s{k}\"><img alt=\"\" style=\"width:{w};height:{h}\" src=\"{src}\"></div>"));
}
}
format!("<!doctype html><html><head><meta charset=\"utf-8\"><title>PdfCraft</title><style>{css}</style></head><body>{html_sheets}</body></html>")
}
/// A browser print being prepared: the print-ready PDF's sheets drawn one [`WebPrintJob::step`]
/// at a time and handed to a sink (on the web, [`blob_url`]), which answers with the picture's URL.
pub(crate) struct WebPrintJob {
exporter: Exporter,
/// The print-ready PDF, kept for the download fallback when the browser won't print.
pub pdf: std::sync::Arc<Vec<u8>>,
pub name: String,
sheets: Vec<(f64, f64)>,
dpi: Vec<f64>,
urls: Vec<String>,
bytes: u64,
/// [`MAX_JOB_BYTES`]; lower in tests.
max_bytes: u64,
}
impl WebPrintJob {
/// A job for `pdf`, whose pages are `sheets` (in points, from `print::layout`).
pub fn new(pdf: Vec<u8>, sheets: Vec<(f64, f64)>, name: String) -> Result<Self, String> {
let dpi = plan_dpi(&sheets)?;
let pdf = std::sync::Arc::new(pdf);
let source = ExportSource {
bytes: pdf.clone(),
config: Default::default(),
pages: sheets.len(),
sizes: sheets.iter().map(|&(w, h)| (w as f32, h as f32)).collect(),
};
Ok(Self { exporter: Exporter::from_source(source), pdf, name, sheets, dpi, urls: Vec::new(), bytes: 0, max_bytes: MAX_JOB_BYTES })
}
pub fn total(&self) -> usize {
self.sheets.len()
}
pub fn done(&self) -> usize {
self.urls.len()
}
pub fn finished(&self) -> bool {
self.urls.len() >= self.sheets.len()
}
/// Draw the next sheet and give its PNG to `sink`. Errors end the job.
pub fn step(&mut self, sink: &mut dyn FnMut(Vec<u8>) -> Result<String, String>) -> Result<(), String> {
let i = self.urls.len();
let Some(&dpi) = self.dpi.get(i) else { return Ok(()) };
let png = self.exporter.png(i, dpi)?;
self.bytes = self.bytes.saturating_add(png.len() as u64);
if self.bytes > self.max_bytes {
return Err(format!(
"the sheets need more than {} MB as pictures, too much to print from the browser; save the print-ready PDF and print it from a PDF viewer",
self.max_bytes >> 20
));
}
let url = sink(png)?;
self.urls.push(url);
Ok(())
}
/// The page to print, once [`WebPrintJob::finished`].
pub fn html(&self) -> String {
print_document_html(&self.sheets, &self.urls)
}
/// The pictures' URLs, now owned by the caller (who revokes them).
pub fn take_urls(&mut self) -> Vec<String> {
std::mem::take(&mut self.urls)
}
}
impl Drop for WebPrintJob {
/// A job dropped before printing (cancelled, failed, replaced) lets go of its pictures.
fn drop(&mut self) {
revoke(&std::mem::take(&mut self.urls));
}
}
/// Let go of pictures handed out by [`blob_url`].
fn revoke(urls: &[String]) {
#[cfg(target_arch = "wasm32")]
for url in urls {
let _ = web_sys::Url::revoke_object_url(url);
}
#[cfg(not(target_arch = "wasm32"))]
let _ = urls;
}
/// A JavaScript exception as text: an error's `message` (read as a property, since an error from
/// the print frame isn't an `instanceof Error` here), a thrown string, else its debug form.
#[cfg(target_arch = "wasm32")]
fn js_error(e: wasm_bindgen::JsValue) -> String {
if let Some(s) = e.as_string() {
return s;
}
js_sys::Reflect::get(&e, &wasm_bindgen::JsValue::from_str("message"))
.ok()
.and_then(|m| m.as_string())
.filter(|m| !m.is_empty())
.unwrap_or_else(|| format!("{e:?}"))
}
/// A PNG as a `blob:` URL. The bytes move out of wasm memory into the browser's.
#[cfg(target_arch = "wasm32")]
pub(crate) fn blob_url(png: Vec<u8>) -> Result<String, String> {
let err = js_error;
let array = js_sys::Uint8Array::from(png.as_slice());
drop(png);
let opts = web_sys::BlobPropertyBag::new();
opts.set_type("image/png");
let blob = web_sys::Blob::new_with_u8_array_sequence_and_options(&js_sys::Array::of1(&array), &opts).map_err(err)?;
web_sys::Url::create_object_url_with_blob(&blob).map_err(err)
}
#[cfg(target_arch = "wasm32")]
thread_local! {
/// The iframe of the last browser print and its pictures, removed when the browser says the
/// dialog closed, or at the latest when the next print starts.
static LAST_FRAME: std::cell::RefCell<Option<(web_sys::HtmlIFrameElement, Vec<String>)>> = const { std::cell::RefCell::new(None) };
}
/// Remove the last print's iframe and let go of its pictures.
#[cfg(target_arch = "wasm32")]
fn clear_last_frame() {
if let Some((frame, urls)) = LAST_FRAME.with(|f| f.borrow_mut().take()) {
frame.remove();
revoke(&urls);
}
}
/// Open the browser's print dialog on `html` in a hidden iframe once its pictures (`urls`) are
/// decoded. What goes wrong after this returns (a picture that won't decode, a browser that
/// refuses to print from a frame) is pushed to `failed`, for the app to report.
#[cfg(target_arch = "wasm32")]
pub(crate) fn print_in_iframe(
html: &str,
urls: Vec<String>,
failed: std::sync::Arc<std::sync::Mutex<Vec<String>>>,
ctx: Option<egui::Context>,
) -> Result<(), String> {
use wasm_bindgen::JsCast;
let err = js_error;
clear_last_frame();
let document = web_sys::window().and_then(|w| w.document()).ok_or("no document")?;
let body = document.body().ok_or("no document body")?;
let frame: web_sys::HtmlIFrameElement = document.create_element("iframe").map_err(err)?.dyn_into().map_err(|_| "not an iframe")?;
// Not `display: none` or hidden: some browsers print nothing from a frame that isn't laid out.
frame.set_attribute("style", "position:fixed;right:0;bottom:0;width:0;height:0;border:0;opacity:0;pointer-events:none").map_err(err)?;
frame.set_attribute("aria-hidden", "true").map_err(err)?;
frame.set_attribute("tabindex", "-1").map_err(err)?;
let on_load = {
let frame = frame.clone();
wasm_bindgen::closure::Closure::once_into_js(move || {
wasm_bindgen_futures::spawn_local(async move {
let report = |e: String| {
if let Ok(mut q) = failed.lock() {
q.push(e);
}
if let Some(ctx) = &ctx {
ctx.request_repaint();
}
};
let (Some(window), Some(doc)) = (frame.content_window(), frame.content_document()) else {
report("the print page didn't load".into());
return;
};
let images = doc.images();
for i in 0..images.length() {
let Some(img) = images.item(i).and_then(|e| e.dyn_into::<web_sys::HtmlImageElement>().ok()) else { continue };
if let Err(e) = wasm_bindgen_futures::JsFuture::from(img.decode()).await {
report(format!("sheet {} couldn't be prepared for printing: {}", i + 1, err(e)));
return;
}
}
// Clean up when the dialog closes; browsers that don't say so are cleaned up by
// the next print.
let after = wasm_bindgen::closure::Closure::once_into_js(clear_last_frame);
let _ = window.add_event_listener_with_callback("afterprint", after.unchecked_ref());
if let Err(e) = window.print() {
report(format!("the browser wouldn't open its print dialog: {}", err(e)));
}
});
})
};
frame.add_event_listener_with_callback("load", on_load.unchecked_ref()).map_err(err)?;
frame.set_srcdoc(html);
body.append_child(&frame).map_err(err)?;
LAST_FRAME.with(|f| *f.borrow_mut() = Some((frame, urls)));
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
const LETTER: (f64, f64) = (612.0, 792.0);
#[test]
fn resolution_falls_with_the_sheet_count_and_long_jobs_are_refused() {
assert_eq!(plan_dpi(&[LETTER]).unwrap(), vec![300.0]);
assert_eq!(plan_dpi(&vec![LETTER; 100]).unwrap()[99], 300.0);
assert_eq!(plan_dpi(&vec![LETTER; 101]).unwrap()[0], 200.0);
assert_eq!(plan_dpi(&vec![LETTER; 300]).unwrap()[0], 200.0);
assert_eq!(plan_dpi(&vec![LETTER; 301]).unwrap()[0], 150.0);
assert_eq!(plan_dpi(&vec![LETTER; MAX_SHEETS]).unwrap().len(), MAX_SHEETS);
let e = plan_dpi(&vec![LETTER; MAX_SHEETS + 1]).unwrap_err();
assert!(e.contains("save the print-ready PDF"), "{e}");
assert!(plan_dpi(&[]).is_err());
}
#[test]
fn large_sheets_are_drawn_within_the_pixel_cap_and_bad_sizes_are_refused() {
// ARCH E, 36 × 48 inches: 300 dpi would be 155 megapixels.
let dpi = plan_dpi(&[(36.0 * 72.0, 48.0 * 72.0)]).unwrap()[0];
assert!(dpi < 300.0 && dpi > 150.0, "{dpi}");
let pixels = (36.0 * dpi) * (48.0 * dpi);
assert!(pixels <= MAX_SHEET_PIXELS * 1.0001, "{pixels}");
// A huge sheet still gets a usable floor rather than nothing.
assert_eq!(plan_dpi(&[(14_400.0, 14_400.0)]).unwrap()[0], MIN_DPI);
for bad in [(f64::NAN, 792.0), (612.0, f64::INFINITY), (0.0, 792.0), (-612.0, 792.0)] {
assert!(plan_dpi(&[LETTER, bad]).unwrap_err().contains("sheet 2"));
}
}
#[test]
fn the_print_page_turns_sideways_sheets_names_other_sizes_and_ends_without_a_blank_page() {
const A4: (f64, f64) = (595.28, 841.89);
let sheets = [LETTER, (792.0, 612.0), A4, (612.2, 791.8), A4];
let urls: Vec<String> = (0..5).map(|i| format!("blob:https://example.test/{i}")).collect();
let html = print_document_html(&sheets, &urls);
// The paper is the first sheet's.
assert!(html.contains("@page{size:612pt 792pt;margin:0}"), "{html}");
assert_eq!(html.matches("<div").count(), 5);
// Letter landscape is turned onto Letter portrait, not given a page of its own.
assert!(html.contains("<div class=\"turned\"><img alt=\"\" src=\"blob:https://example.test/1\"></div>"), "{html}");
assert!(html.contains(".turned img{width:792pt;height:612pt;transform-origin:0 0;transform:translate(0,792pt) rotate(-90deg)}"), "{html}");
// A4 gets one named page, shared by both A4 sheets; a size within half a point is the paper.
assert_eq!(html.matches("@page s0{size:595.28pt 841.89pt;margin:0}").count(), 1);
assert!(!html.contains("@page s1"), "{html}");
assert_eq!(html.matches("<div class=\"s0\">").count(), 2);
assert!(html.contains("<div><img alt=\"\" style=\"width:612pt;height:792pt\" src=\"blob:https://example.test/3\"></div>"));
assert!(html.contains("div:last-child{break-after:auto"));
let quoted = print_document_html(&[LETTER], &["blob:x\"><script>".to_string()]);
assert!(!quoted.contains("<script>"), "{quoted}");
}
/// A PDF of blank pages, one per size in points.
fn blank_pdf(sizes: &[(f64, f64)]) -> Vec<u8> {
let mut out = String::from("%PDF-1.7\n");
let mut offsets = Vec::new();
let n = sizes.len();
let kids: Vec<String> = (0..n).map(|i| format!("{} 0 R", 3 + i)).collect();
let mut objects = vec!["<< /Type /Catalog /Pages 2 0 R >>".to_string(), format!("<< /Type /Pages /Kids [{}] /Count {n} >>", kids.join(" "))];
for (w, h) in sizes {
objects.push(format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {w} {h}] >>"));
}
for (i, o) in objects.iter().enumerate() {
offsets.push(out.len());
out.push_str(&format!("{} 0 obj\n{o}\nendobj\n", i + 1));
}
let xref = out.len();
out.push_str(&format!("xref\n0 {}\n0000000000 65535 f \n", objects.len() + 1));
for o in offsets {
out.push_str(&format!("{o:010} 00000 n \n"));
}
out.push_str(&format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objects.len() + 1));
out.into_bytes()
}
/// A PNG's width and height, from its header.
fn png_size(png: &[u8]) -> (u32, u32) {
let w = u32::from_be_bytes(png[16..20].try_into().unwrap());
let h = u32::from_be_bytes(png[20..24].try_into().unwrap());
(w, h)
}
#[test]
fn a_job_draws_every_sheet_at_its_planned_resolution_then_builds_the_page() {
let sheets = vec![LETTER, (792.0, 612.0), (297.64, 420.94)];
let mut job = WebPrintJob::new(blank_pdf(&sheets), sheets.clone(), "doc.pdf".into()).unwrap();
assert_eq!((job.done(), job.total(), job.finished()), (0, 3, false));
let mut pngs = Vec::new();
while !job.finished() {
job.step(&mut |png| {
pngs.push(png);
Ok(format!("blob:test/{}", pngs.len()))
})
.unwrap();
}
assert_eq!(pngs.len(), 3);
assert_eq!(png_size(&pngs[0]), (2550, 3300), "Letter at 300 dpi");
assert_eq!(png_size(&pngs[1]), (3300, 2550), "landscape Letter at 300 dpi");
let (w, h) = png_size(&pngs[2]);
assert!((w as i64 - 1240).abs() <= 1 && (h as i64 - 1754).abs() <= 1, "A6 at 300 dpi: {w}×{h}");
// A finished job does nothing more.
job.step(&mut |_| panic!("no sheet left")).unwrap();
let html = job.html();
assert_eq!(html.matches("<img ").count(), 3);
assert!(html.contains("blob:test/3"));
assert_eq!(job.take_urls().len(), 3);
}
#[test]
fn a_sink_error_or_a_page_that_isnt_there_stops_the_job() {
let mut job = WebPrintJob::new(blank_pdf(&[LETTER]), vec![LETTER], "doc.pdf".into()).unwrap();
assert_eq!(job.step(&mut |_| Err("out of memory".into())).unwrap_err(), "out of memory");
assert_eq!(job.done(), 0);
// The layout promised two sheets but the PDF has one: an error, not a panic.
let mut short = WebPrintJob::new(blank_pdf(&[LETTER]), vec![LETTER, LETTER], "doc.pdf".into()).unwrap();
short.step(&mut |_| Ok("blob:1".into())).unwrap();
assert!(short.step(&mut |_| Ok("blob:2".into())).is_err());
// A job whose pictures outgrow the byte cap stops instead of filling the tab's memory.
let mut big = WebPrintJob::new(blank_pdf(&[LETTER, LETTER]), vec![LETTER, LETTER], "doc.pdf".into()).unwrap();
big.max_bytes = 1;
let e = big.step(&mut |_| Ok("blob:1".into())).unwrap_err();
assert!(e.contains("save the print-ready PDF"), "{e}");
assert_eq!(big.done(), 0);
// Garbage bytes fail at the first sheet.
let mut garbage = WebPrintJob::new(b"not a pdf".to_vec(), vec![LETTER], "x.pdf".into()).unwrap();
assert!(garbage.step(&mut |_| Ok("blob:1".into())).is_err());
}
}
+4 -13
View File
@@ -1,6 +1,6 @@
# Where PdfCraft falls short of Acrobat Pro
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** exclusive image-crop resize follow-up; minor (Word invoice regression coverage, #773; Office fidelity still partial; gap 2: hayro patch counts after the native-gradient backport) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** gap 4 closed: the web build prints through the browser's print dialog; exclusive image-crop resize follow-up; minor (Word invoice regression coverage, #773; Office fidelity still partial; gap 2: hayro patch counts after the native-gradient backport) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (gap 13: large-file memory measured, edits keep two copies instead of three, file-backed input blocked on gap 2) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (gap 1 closed by #836; fixed issues removed from gaps 3, 6 and 16) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (document-font subset completion and curved/shared image clips in the contributor fork; gap 2: hayro patch counts after the native-gradient backport) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
@@ -13,7 +13,8 @@ feature lacks).
No gap blocks the [alpha gate](roadmap.md#alpha-gate); gap 8 holds the convert-and-share
workflow's partial sub-cases and is the first beta item. Gap numbers are kept when a gap closes, so
references elsewhere stay valid (gap 1, encrypted output Acrobat rejected, closed with #836).
references elsewhere stay valid (gap 1, encrypted output Acrobat rejected, closed with #836; gap 4,
no printing on the web, closed by printing through the browser's print dialog).
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
@@ -25,7 +26,6 @@ update the numbers.
|---|---|---|---|---|
| 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 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) |
@@ -104,16 +104,6 @@ update the numbers.
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 the web
- **Missing:** browser printing.
- **Evidence:** the web build has no spooler: off Windows and unix, `spool::submit` reports that
printing to a printer isn't available and only the print-ready PDF can be saved.
`crates/print/README.md`: "Not yet: the web spooler".
- **Impact:** the web build cannot send a job to a printer; printing is a P0 workflow. Printing on
Windows shipped with [#756](https://github.com/storytold/pdfcraft/issues/756).
- **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.
@@ -322,6 +312,7 @@ update the numbers.
|---|---|---|
| 2026-10-12 | minor | Exclusive image crops resize with their pixels across nested states, intervening operators and content streams; shared clips remain fixed. Synthetic engine/tool regressions and private image save/reopen checks; estimates unchanged. |
| 2026-10-12 | minor | Gap 13: measured large-file memory; edits no longer keep the previous working file while writing the next (peak 3 → 2 copies); file-backed input recorded as blocked on gap 2 |
| 2026-10-11 | minor | Closed gap 4: the web build prints through the browser's print dialog (sheets drawn by PdfCraft at 150–300 dpi, one per printed page); number kept |
| 2026-10-11 | minor | Word invoice failure #773 has synthetic regression coverage; broader Office conversion remains gap 8. |
| 2026-10-11 | minor | Contributor fork: mixed-style visual regions, source-font space reuse and a read-only textless background. Embedded sfnt source faces and Mac subset Unicode mappings keep edit baselines stable; live frames follow Apply/Save geometry as text grows or shrinks. Engine/tool/UI regressions and private all-page checks cover the changes; full matching font import, CFF, range styles, shaping and private Chinese typing checks added; legacy programs and general fidelity remain open; estimates unchanged |
| 2026-10-11 | patch | Gap 2: 23 hayro-interpret and 10 hayro patches after axial and radial shadings became native gradients (backport of hayro 0.8) |
+4 -3
View File
@@ -1,6 +1,6 @@
# Hardware and platform parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (shared craft-print spooler with passing Windows CI; Windows printers row and 1:1 touchpad scrolling, #756, #759) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (web printing through the browser's print dialog; earlier: shared craft-print spooler with passing Windows CI; Windows printers row and 1:1 touchpad scrolling, #756, #759) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
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,
@@ -17,7 +17,7 @@ printing are counted in their feature areas). **Platforms ≈ 70% ready, 15–30
| 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 via craft-print | Windows PDF raster spooler via craft-print (#756, PR #847) | CUPS via craft-print | **none** | `crates/print/README.md` |
| Printers | all, PostScript, printer properties | CUPS via craft-print | Windows PDF raster spooler via craft-print (#756, PR #847) | CUPS via craft-print | the browser's print dialog (every printer the system knows): sheets drawn by PdfCraft at 150–300 dpi; copies, two-sided and colour set there | `crates/print/README.md`, `crates/ui-egui/src/web_print.rs` |
| 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 1:1 (#759, PR #868) |
@@ -34,12 +34,13 @@ printing are counted in their feature areas). **Platforms ≈ 70% ready, 15–30
| Windows (x64, x86, ARM64) | yes (x64, ARM64) | yes | signed installers | — |
| Linux | no | yes | AppImage, deb, rpm, Flatpak, tarball | Chinese faces in Flatpak (#689) |
| FreeBSD | no | yes | package | — |
| Web | Acrobat online (cloud) | yes (WASM, offline in the browser) | static site | printing, OCR, crash recovery, PKCS #12 signing, hosted deployment |
| Web | Acrobat online (cloud) | yes (WASM, offline in the browser) | static site | OCR, crash recovery, PKCS #12 signing, hosted deployment |
## Revision history
| Date | Change | Summary |
|---|---|---|
| 2026-10-11 | minor | Web printers row: printing through the browser's print dialog (gap 4 closed); Web platform row no longer lists printing |
| 2026-10-10 | major | Created from the code, `vendor/README.md`, `.github/workflows/release.yml`, the print crate's README and user issues |
| 2026-10-10 | minor | Windows printing shipped (#756): the Windows row's gap is closed; the web row keeps "printing" |
| 2026-10-11 | minor | Shared craft-print spooler; Windows preserves PDF sheet size/orientation and uses corrected duplex binding edges. Physical output remains a hardware check. |
+5 -4
View File
@@ -1,6 +1,6 @@
# PdfCraft roadmap: milestones and what's next
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** exclusive image-crop resize follow-up; minor (M2: short same-line redraws split before placement; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** exclusive image-crop resize follow-up; minor (M2: short same-line redraws split before placement; earlier: M10, printing on the web through the browser's print dialog, gap 4, rank 1 65–120 → 50–90 h) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (M10, #1028: OCR reads pictures on pages that have text, on request; earlier: direct Organize overview and page actions; 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)
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-11 · **Change:** minor (M13/#971: bound per-page search match allocation; text cache byte budget and extraction warnings remain open) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (#594 paragraph drags validated in the Linux test environment; 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)
@@ -31,7 +31,7 @@ exchange with Acrobat users or a missing sub-case is `partial, not blocking` (a
| 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/Linux and the Windows spooler (web printing missing, a beta item). Rendering borrowed from `hayro` | 0 |
| View, search and print | yes | Render, find, select, panels, tiles; 500-page scrolling test; Print with CUPS on macOS/Linux, the Windows spooler and, on the web, the browser's print dialog. Rendering borrowed from `hayro` | 0 |
| Organize, combine and split pages | yes | `organize.*` P0s shipped with tests; links, fields, layers and bookmarks carried over; page deletion drops dangling destinations and the opening action (#817, #821) | 0 |
| Comment and review | yes | Every markup type with appearances, replies, status, XFDF/FDF; comments saved in the PDF reopen in other readers; XFDF keeps polygon vertices and callouts (#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 |
@@ -90,7 +90,7 @@ total matches the parity estimate.
| 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.<br>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.<br>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, the CUPS and Windows spoolers, print-ready PDF).<br>Done since: embedded/72/custom DPI choices for image imports; export all images; OCR (searchable image for pages, ranges and multiple files); single-sided cut-and-stack imposition with cut marks, through Print and doc_print; architectural/ANSI/large ISO sheet presets, Match page at 100% and PDF-scale preview (#599 follow-up); Create PDF from multiple files (PDFs, images and text, edited as pages in the grid); OCR can also read pictures on pages that have text, without adding that text twice (#1028). Done since: Word invoice export uses borderless inferred tables, floating positioned text/artwork only where text overlaps an image or is rotated, and per-page sizes, covered by synthetic invoice automation regressions (#773). Missing: OCR languages beyond Latin, editable-text OCR output, full Office layout fidelity/import, web printing |
| M10 | OCR, create, export, print | 200–350 | 33% | 120–215 | Done: create from blank/text/PNG/JPEG/TIFF (multi-page)/GIF/BMP; export PNG/JPEG/TIFF and text; Print (Acrobat's sizing, n-up, booklet, poster, comments & forms, preview, the CUPS and Windows spoolers, print-ready PDF).<br>Done since: embedded/72/custom DPI choices for image imports; export all images; OCR (searchable image for pages, ranges and multiple files); single-sided cut-and-stack imposition with cut marks, through Print and doc_print; architectural/ANSI/large ISO sheet presets, Match page at 100% and PDF-scale preview (#599 follow-up); Create PDF from multiple files (PDFs, images and text, edited as pages in the grid); OCR can also read pictures on pages that have text, without adding that text twice (#1028). Done since: Word invoice export uses borderless inferred tables, floating positioned text/artwork only where text overlaps an image or is rotated, and per-page sizes, covered by synthetic invoice automation regressions (#773); printing on the web through the browser's print dialog, sheets drawn by PdfCraft (gap 4). Missing: OCR languages beyond Latin, editable-text OCR output, full Office layout fidelity/import |
| 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). Locatable figure, annotation and form findings highlight their page bounds.<br>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<br>Done since: CLI run and MCP reject missing, empty, repeated and option-valued --root arguments before any tool/script execution or MCP initialization. A single-hyphen directory name remains valid; synthetic CLI regressions preserve a would-be overwritten file. `text_find` also caps the per-page match ranges it materializes at the remaining result limit plus one continuation match. |
@@ -104,7 +104,7 @@ total matches the parity estimate.
| Rank | Next milestone | Closes | Estimate (h) |
|---|---|---|---|
| 0 | Convert and share: Create PDF from Office, Word export keeps layout | part of gap 8 | 32–55 |
| 1 | Backlog and platform: crash and wrong-result reports fixed, web printing, Chinese UI face | gaps 4, 5, 6 | 65–120 |
| 1 | Backlog and platform: crash and wrong-result reports fixed, Chinese UI face (web printing done) | gaps 5, 6 | 50–90 |
| 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 |
@@ -144,6 +144,7 @@ M2 (renderer), M7 (content editing), M10 (OCR and Office), M11 (Preflight and st
| 2026-10-11 | minor | M4: editable Organize page-range selection (#1092), synchronized with thumbnails and available through UI control. Estimates unchanged. |
| 2026-10-11 | minor | M10: Word invoice export regressions (#773); estimates unchanged. |
| 2026-10-12 | minor | M2: short same-line redraws split before placement; estimates unchanged. |
| 2026-10-11 | minor | M10: printing on the web through the browser's print dialog (gap 4 closed); rank 1 drops web printing, 65–120 → 50–90 h |
| 2026-10-11 | minor | M13/#971: cap materialized text-search matches to the requested limit plus one continuation match. |
| 2026-10-11 | minor | M7: #594 ordinary paragraph drags validated in the Linux test environment with source-loss baseline failures, UI/engine regressions, native save/reopen and all eight CI gates. Estimates unchanged. |
| 2026-10-11 | minor | M4: direct Organize overview, checkbox selection and individual page actions. Estimates unchanged. |
+5 -4
View File
@@ -1,6 +1,6 @@
# PdfCraft parity with Adobe Acrobat Pro
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** exclusive image-crop resize follow-up; minor (document-font subset completion, source styles and curved/shared image clips in the contributor fork; closed issues synced, scores not re-judged: #774, #756, #740, #759, #799–#821, #825, #844; earlier: 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)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** web printing shipped, gap 4 closed, scores not re-judged; exclusive image-crop resize follow-up; minor (document-font subset completion, source styles and curved/shared image clips in the contributor fork; closed issues synced, scores not re-judged: #774, #756, #740, #759, #799–#821, #825, #844; earlier: 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
@@ -180,11 +180,11 @@ features total and are shown for orientation; the others add to it.
| 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, #789 (#759 and #844 fixed) | [ui-parity.md](ui-parity.md) |
| File formats and conversion | ≈ 40% | 12% | 120–220 | within features | PDF read/write strong, encrypted output included (#774 fixed); 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; no printing on the web (Windows shipped, #756) | [hardware-parity.md](hardware-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; web printing goes through the browser's print dialog as pictures, not vectors | [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 on 2026-10-10, many of them `fix(...)` reports of wrong results in shipped features (the #799–#821 batch is fixed) | [gaps.md](gaps.md) |
| Platforms | ≈ 70% | 5% | 15–30 | additional | macOS (signed, notarized), Windows x64/x86/ARM64 (signed), Linux (AppImage, deb, rpm, Flatpak), FreeBSD, web (WASM). Acrobat has no Linux, FreeBSD or full web editor; we lack Windows and web printing, web OCR and web recovery | [hardware-parity.md](hardware-parity.md) |
| Platforms | ≈ 70% | 5% | 15–30 | additional | macOS (signed, notarized), Windows x64/x86/ARM64 (signed), Linux (AppImage, deb, rpm, Flatpak), FreeBSD, web (WASM). Acrobat has no Linux, FreeBSD or full web editor; we lack 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** | | | |
@@ -207,7 +207,7 @@ full parity in that area.
| I Scan and OCR | 26 | 5 | 2 | 23.1% | 15% | 40–75 | Searchable image (ocrs, Latin script without accents) on pages, ranges and many files. Missing: every other language (#744 Dutch; accents, CJK, Cyrillic, Arabic), editable-text output, deskew, rotation, despeckle, camera images beyond straightening (found or placed corners): orientation, clean-up, MRC compression, scanners |
| 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 and the Windows spooler (#756). Missing: **printing on the web**, print as image, PostScript, all print-production tools (output preview, separations, ink manager, printer marks, colour conversion) |
| L Print | 39 | 15 | 1 | 39.7% | 35% | 30–55 | Acrobat's sizing, n-up, booklet, poster, comments & forms, preview, print-ready PDF, CUPS on macOS/Linux, the Windows spooler (#756) and the browser's print dialog on the web. Missing: print as image, PostScript, all print-production tools (output preview, separations, ink manager, printer marks, colour conversion) |
| 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) |
@@ -293,6 +293,7 @@ licensed OCR or AI model.
| Date | Change | Summary |
|---|---|---|
| 2026-10-12 | minor | Exclusive image crops resize with their pixels across nested states, intervening operators and content streams; shared clips remain fixed. Synthetic engine/tool regressions and private image save/reopen checks; estimates unchanged. |
| 2026-10-11 | minor | Web printing shipped (gap 4): Hardware, Platforms and L Print text updated. Text only: scores, readiness and hours not re-judged |
| 2026-10-11 | minor | Contributor fork: document-font selection and subset completion, CFF, style ranges, contextual/bidi save/reopen and all-page Chinese keyboard acceptance; curve/hole hit testing (shared clips stay fixed, as on main). Scope remains bounded; percentages unchanged |
| 2026-10-11 | minor | Synced with closed issues: encrypted output opens in Acrobat (#774, #836), Windows prints (#756, #847), column select (#740), 1:1 touchpad scrolling (#759), same-page multi-object move (#844), #825 and the #799–#821 fix batch. Text only: scores, readiness and hours not re-judged |
| 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 |
+4 -4
View File
@@ -7894,8 +7894,8 @@ milestone = "M10"
status = "shipped"
commands = ["print.dialog"]
tools = ["doc_print", "printers"]
evidence = ["crates/ui-egui/tests/print.rs::print_dialog_lays_out_sheets_and_saves_a_pdf", "crates/engine/src/tests.rs::printing_lays_out_sheets_that_render"]
notes = "⌘P; settings and live sheet preview"
evidence = ["crates/ui-egui/tests/print.rs::print_dialog_lays_out_sheets_and_saves_a_pdf", "crates/engine/src/tests.rs::printing_lays_out_sheets_that_render", "crates/ui-egui/src/web_print.rs::a_job_draws_every_sheet_at_its_planned_resolution_then_builds_the_page", "crates/ui-egui/src/web_print.rs::the_print_page_turns_sideways_sheets_names_other_sizes_and_ends_without_a_blank_page"]
notes = "⌘P; settings and live sheet preview; on the web the same dialog prints through the browser's print dialog"
[[feature]]
id = "print.printer-selection"
@@ -7906,8 +7906,8 @@ milestone = "M10"
status = "shipped"
commands = ["print.dialog"]
tools = ["printers", "printer_options", "doc_print"]
evidence = ["crates/print/src/tests.rs::spooler_arguments_and_printer_list", "crates/print/src/tests.rs::driverless_printers_join_the_list_without_a_queue", "crates/print/src/tests.rs::windows_printers_carry_one_default_and_keep_their_names", "crates/print/src/tests.rs::printer_options_come_from_the_ppd", "crates/print/src/tests.rs::printer_option_defaults_and_job_arguments", "crates/ui-egui/tests/print.rs::print_dialog_shows_the_printer_driver_options", "crates/ui-egui/tests/print.rs::printers_listed_in_the_background_preselect_the_default_unless_one_was_picked"]
notes = "CUPS queues via lpstat -p -d and driverless destinations without a queue via lpstat -e; on Windows the queues and the default come from Win32_Printer (PowerShell CIM) and jobs are rendered with Windows.Data.Pdf and spooled through shared craft-print at each sheet's physical size and orientation, with bounded raster resolution up to 600 dpi (not vector passthrough); the printer list and submission run on workers without a console window. Native Windows regression execution is pending; Save as PDF on every platform. Properties…: on CUPS the driver's own PPD job options (tray, paper type, finishing …, not installable options or what the dialog sets) for this job, sent as lp -o; on Windows the driver's Printing Preferences window (printui), saved as that printer's per-user defaults rather than per job"
evidence = ["crates/print/src/tests.rs::spooler_arguments_and_printer_list", "crates/print/src/tests.rs::driverless_printers_join_the_list_without_a_queue", "crates/print/src/tests.rs::windows_printers_carry_one_default_and_keep_their_names", "crates/print/src/tests.rs::printer_options_come_from_the_ppd", "crates/print/src/tests.rs::printer_option_defaults_and_job_arguments", "crates/ui-egui/tests/print.rs::print_dialog_shows_the_printer_driver_options", "crates/ui-egui/tests/print.rs::printers_listed_in_the_background_preselect_the_default_unless_one_was_picked", "crates/ui-egui/src/web_print.rs::resolution_falls_with_the_sheet_count_and_long_jobs_are_refused", "crates/ui-egui/src/web_print.rs::large_sheets_are_drawn_within_the_pixel_cap_and_bad_sizes_are_refused", "crates/ui-egui/src/web_print.rs::a_sink_error_or_a_page_that_isnt_there_stops_the_job"]
notes = "CUPS queues via lpstat -p -d and driverless destinations without a queue via lpstat -e; on Windows the queues and the default come from Win32_Printer (PowerShell CIM) and jobs are rendered with Windows.Data.Pdf and spooled through shared craft-print at each sheet's physical size and orientation, with bounded raster resolution up to 600 dpi (not vector passthrough); the printer list and submission run on workers without a console window. Native Windows regression execution is pending; Save as PDF on every platform. Properties…: on CUPS the driver's own PPD job options (tray, paper type, finishing …, not installable options or what the dialog sets) for this job, sent as lp -o; on Windows the driver's Printing Preferences window (printui), saved as that printer's per-user defaults rather than per job. On the web: \"Browser's print dialog\" (the default there) reaches the system's printers through the browser; PdfCraft draws the sheets itself (300 dpi up to 100 sheets, 200 up to 300, 150 up to 1,000; at most 64 megapixels a sheet and 1 GiB a job) and prints them from a hidden iframe, one per printed page, a landscape sheet among portrait ones turned onto the paper; copies, two-sided and colour are chosen in the browser's dialog; a browser that refuses to print gets the print-ready PDF as a download. Checked in headless Chromium, not yet in Firefox or Safari; pictures, not vectors; sheets of another paper size print at their own size only where the browser honours named @page sizes (Chromium doesn't)"
[[feature]]
id = "print.copies-collate"