mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-14 19:27:04 -04:00
M10.5: Preserve current main printing workers during spooler extraction
This commit is contained in:
commit
6614b370b1
23 files changed
+264
-36
No files matched your search
+15
-15
@@ -19,7 +19,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "500ba36d4a37162d935a1b5e2b16eff5b2011c6aa9f9285a87d46807acdb97e6"
|
||||
sha256 = "2c0f7bc1c225a95d2322e5f92e42ca3b07652f54976d754e9901abeddc4d0dc6"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/zh-hant.tsv"
|
||||
@@ -30,7 +30,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "57c35909b2f19f16ff0497d110abbf1c79a5627d2a98f95706e726c59348d09e"
|
||||
sha256 = "b55ec8f2794966e879334121d48b15923cebf38192bd8e1667d4c4e6bf168095"
|
||||
|
||||
[[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 = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "130059ef7014f1dfb59ec8dc2435657d5158abca2d1825959c6366288d28ea92"
|
||||
sha256 = "fe8fc8af29190950048c3e3d5108ede6cf2da2f85ed7003fb2035ddb517a600c"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/es.tsv"
|
||||
@@ -63,7 +63,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "c67b49f2caa527fe5a241b1046f582b83f324ac86945319fd489971c038acd9b"
|
||||
sha256 = "81ce4a9a18d10149129ecdf7eb0e0d3179cb4a66cf3f344a32de9a984612dafa"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/fr.tsv"
|
||||
@@ -74,7 +74,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "4e8e7f71dd896c58ad2bcad8d8c57ae8c9933611cff412985eb32845821f8899"
|
||||
sha256 = "6d99c6a85e69b5448609ae54847700c908919be27d904bd0c73bf2c220a2da4e"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/de.tsv"
|
||||
@@ -85,7 +85,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "09db52c316a7e9312a30d80ce4e385f2c957096563c509b7b6ed2bcc95bf39d8"
|
||||
sha256 = "09f654d07a87878802e87fc0436f9a5426bc27bfb2156aee912463c5dc8a04ea"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/te.tsv"
|
||||
@@ -96,7 +96,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "21767432503afac87196a87557637692dcec76c0eba02a1dcf6bd1294926107a"
|
||||
sha256 = "fd2b1b33d940ff9eef4bc067452252c39bf6cf02f3fe22e84ecd782943d2cc26"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/it.tsv"
|
||||
@@ -107,7 +107,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 = "58f9514a06c74a7cdf90c6f7fb86ebb092af39857633f0779b44f0867a1c96e0"
|
||||
sha256 = "8a7a2bfba1a2f9858ad62a74bd8229a1615a5bc6e51023b11b9768e7ea21d79c"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/pl.tsv"
|
||||
@@ -118,7 +118,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 = "7052f5ee37d26e0173bc5fe48411c94e1b48e31e0fc8a9188fca2bec54f29e32"
|
||||
sha256 = "95e35de200c66936333a95d89561f3c4d32b1d38f9c0a8357bec5c6b0e75fe1e"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/ru.tsv"
|
||||
@@ -129,7 +129,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "e780ffd93f9606c1a762862a961c3a82bde948a8a5f116b46e6f78ba84ac6ff4"
|
||||
sha256 = "dc0af15b819b9059bc23a73a1e794d1ae5291594b2ff0fae2595274fccc9c281"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/hu.tsv"
|
||||
@@ -140,7 +140,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "ebbd6cd7bc586ca253047f7aa3b5c34cd9ecb29645afc3d9d2097c35277ffddc"
|
||||
sha256 = "16c6e6cf857b70f7ade009bc9c87fceb3831f5f3281ac7fabc0fd7bfd2c5d28e"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/bg.tsv"
|
||||
@@ -151,7 +151,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "b760879b720c1d6de0dc9286ab044bebe4360962dece93060d7041b8955be7b2"
|
||||
sha256 = "9e8c0929c5839b2a5f5c93a2d3457bcce24b118a11fd6b37476aa3584fa5ce76"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/ja.tsv"
|
||||
@@ -162,7 +162,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "b07366d224bd5a394f705c17fa5d26e4f45d8903927e5dc18ed12e7713751d0a"
|
||||
sha256 = "47720091895fc1a4c1ff332a660192fe4a9666f32002e5899030b3ca3421f337"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/uk.tsv"
|
||||
@@ -173,7 +173,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "97c79be066c982347b48556a19b71e0db67a7f46932d1ce342b2ba5d29bf4582"
|
||||
sha256 = "fa2f00414a8f064f4d6e47d57702a100743c8b64b05dcbe93585330427c83f16"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/ar.tsv"
|
||||
@@ -184,7 +184,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Arabic UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "f5e369934bf2ceca49224bf57f72289f0c82fc4c81b7c6363676d39362750d17"
|
||||
sha256 = "271a9b34b0d13d1734de53d88a09b49d589eb556c75fe04dd1fa5172151c5b1a"
|
||||
|
||||
[[asset]]
|
||||
path = "assets/icons/accessibility.svg"
|
||||
|
||||
@@ -144,6 +144,7 @@ Newest first: the date, what moved, and the new overall estimate.
|
||||
- **2026-10-10 (M10, #645):** Export all images and Save image as keep transparency. A JPEG with a soft mask (the usual cut-out photo) was written as the bare JPEG, and soft masks of another size than the image, JPEG-compressed soft masks, stencil `/Mask` images and colour-key `/Mask` arrays were dropped, so cut-outs came out on solid white or black. These images are now PNG with the mask as alpha (soft masks scaled to the image, `/Matte` un-blended); JPEGs without transparency are still written byte for byte. A negative image size no longer overflows in Save image as. JPX/JBIG2/CCITT masks are still left out. Overall estimate unchanged (about 30–35%).
|
||||
- **2026-10-10 (M12, XFA rows and areas):** Dynamic XFA forms lay out a repeating subform inside a top-to-bottom container in positioned content (a table in an area) with all its instances, drawn and measured, instead of one; and areas no longer appear in data paths (`pdfcraft_xfa::data_path`), since they are not data scopes. Before, a form whose fields sit in an area read none of the values Adobe's viewers saved, wrote what was filled here under a new element named after the area, which Adobe's viewers would most likely not read, and a FormCalc total looping over the table's rows failed on the rows that were missing. Layout, the script tree, reading and writing the data and the engine's value and row operations share the one path rule. Synthetic tests cover the layout, the path rule and the whole round trip (rows, Adobe-written values, a FormCalc total over all rows, the written data, reopening). Still open for such forms: XFA script objects (`<variables><script>`), which aren't supported. Overall estimate unchanged (about 30–35%).
|
||||
|
||||
- **2026-10-11 (M10, Windows printing follow-up to #847):** Windows print jobs print each sheet at 100% on the driver's paper closest to the sheet's size, landscape for wide sheets; they had been shrunk into .NET's default 1-inch margins (about 76% on Letter) and not rotated. Sheets are capped at about 64 megapixels so a poster-sized sheet can't exhaust memory. PowerShell runs without a console window, and the Print dialog lists printers and spools jobs off the UI thread (the desktop app's `printer_source`), so neither Ctrl+P nor a long job freezes the app. Kittest regression for the background printer list; the Windows rendering path is still checked only by the Windows CI job. Estimates unchanged.
|
||||
- **2026-10-10 (M9, #179 follow-up):** A Windows store certificate that can't sign is no longer left out of the Sign dialog silently: `windows::list` names it with the reason and Windows error code (no private key, a P-521/Brainpool/Ed25519 key, a key CNG can't open), the picker shows it greyed and unselectable, and `sign_windows_ids` returns it under `unusable`. A key that needs a permission prompt to open is listed instead of skipped. Reported by a user whose CA-issued identity showed in Acrobat but not here; a plain .p12 import into the CNG and legacy Microsoft providers was verified to list, so the next report will carry the reason. Store test adds a key-less certificate; overall estimate unchanged (about 30–35%).
|
||||
- **2026-10-10 (M10.2, JPEG scanning follow-up to #729):** Skip marker fill bytes and keep reading JFIF, APP14 and ICC metadata after the frame header, stopping before scan data or at EOI. Invalid segment lengths return errors; damaged or mismatched ICC profiles retain #729's device-space fallback. Synthetic regression cases added but not run at the contributor's request. Estimates unchanged.
|
||||
- **2026-10-10 (M13, desktop action outputs):** Desktop Action Wizard keeps every result when input basenames repeat, or when the destination already has a result from an earlier run. It preserves the original filename and extension for a free name and adds numbered suffixes for occupied names; the bounded search returns an error rather than reusing an occupied path. A real-shell regression drives Start twice and reopens the three-page and one-page results, checking that the original inputs and earlier output remain intact. This covers the desktop writer separately from the CLI/MCP action_run repair in #877. Milestone and effort estimates unchanged.
|
||||
|
||||
@@ -305,6 +305,7 @@ fn app_creator<'a>(
|
||||
app.integrated_titlebar = integrated;
|
||||
app.system_text_scale = text_scale::system();
|
||||
app.update_source = Some(std::sync::Arc::new(updates::latest_release));
|
||||
app.printer_source = Some(pdfcraft_ui_egui::system_printers());
|
||||
app.os_key_store_ids = cfg!(any(target_os = "macos", target_os = "windows"));
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
|
||||
@@ -43,7 +43,9 @@ are removed on success and failure. Other targets offer the print-ready PDF inst
|
||||
|
||||
The shared dependency requires Rust 1.95. Transport and native Windows regression tests
|
||||
live in craft-libs; local tests retain composition, facade, engine, automation and UI coverage.
|
||||
Physical printer output still needs hardware validation.
|
||||
The desktop UI discovers printers and submits jobs on workers, so neither operation blocks
|
||||
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
|
||||
grayscale.
|
||||
@@ -1566,6 +1566,8 @@
|
||||
{file} is too large for a stamp (at most {n} MB) {file} أكبر من أن يكون ختمًا ({n} ميغابايت على الأكثر)
|
||||
Sent to {printer} أُرسل إلى {printer}
|
||||
Sent to {printer}: {msg} أُرسل إلى {printer}: {msg}
|
||||
Looking for printers… جارٍ البحث عن الطابعات…
|
||||
Sending to {printer}… جارٍ الإرسال إلى {printer}…
|
||||
Choose a folder for the searchable files اختر مجلدًا للملفات القابلة للبحث
|
||||
Files are combined from top to bottom. Leave Pages empty to take every page. تُدمج الملفات من الأعلى إلى الأسفل. اترك «الصفحات» فارغًا لأخذ كل الصفحات.
|
||||
Unlock… إلغاء القفل…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -1527,6 +1527,8 @@
|
||||
{file} is too large for a stamp (at most {n} MB) {file} е твърде голям за печат (най-много {n} MB)
|
||||
Sent to {printer} Изпратено до {printer}
|
||||
Sent to {printer}: {msg} Изпратено до {printer}: {msg}
|
||||
Looking for printers… Търсене на принтери…
|
||||
Sending to {printer}… Изпращане до {printer}…
|
||||
Choose a folder for the searchable files Изберете папка за файловете с възможност за търсене
|
||||
Files are combined from top to bottom. Leave Pages empty to take every page. Файловете се обединяват отгоре надолу. Оставете „Страници“ празно, за да се вземат всички страници.
|
||||
Unlock… Отключи…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 8 and column 16.
|
@@ -1555,6 +1555,8 @@
|
||||
{file} is too large for a stamp (at most {n} MB) {file} ist zu groß für einen Stempel (höchstens {n} MB)
|
||||
Sent to {printer} An {printer} gesendet
|
||||
Sent to {printer}: {msg} An {printer} gesendet: {msg}
|
||||
Looking for printers… Drucker werden gesucht…
|
||||
Sending to {printer}… Wird an {printer} gesendet…
|
||||
Choose a folder for the searchable files Wähle einen Ordner für die durchsuchbaren Dateien
|
||||
Files are combined in this order. Leave Pages empty to take every page. Die Dateien werden in dieser Reihenfolge kombiniert. Lass „Seiten“ leer, um alle Seiten zu übernehmen.
|
||||
Pages of {name} to combine, e.g. 1-3, 6 Zu kombinierende Seiten von {name}, z. B. 1-3, 6
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 51.
|
@@ -1569,6 +1569,8 @@
|
||||
{file} is too large for a stamp (at most {n} MB) {file} es demasiado grande para un sello (como máximo {n} MB)
|
||||
Sent to {printer} Enviado a {printer}
|
||||
Sent to {printer}: {msg} Enviado a {printer}: {msg}
|
||||
Looking for printers… Buscando impresoras…
|
||||
Sending to {printer}… Enviando a {printer}…
|
||||
Choose a folder for the searchable files Elige una carpeta para los archivos con búsqueda
|
||||
Files are combined from top to bottom. Leave Pages empty to take every page. Los archivos se combinan de arriba abajo. Deja Páginas vacío para incluir todas las páginas.
|
||||
Unlock… Desbloquear…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 60.
|
@@ -1554,6 +1554,8 @@
|
||||
{file} is too large for a stamp (at most {n} MB) {file} est trop volumineux pour un tampon ({n} Mo au plus)
|
||||
Sent to {printer} Envoyé à {printer}
|
||||
Sent to {printer}: {msg} Envoyé à {printer} : {msg}
|
||||
Looking for printers… Recherche des imprimantes…
|
||||
Sending to {printer}… Envoi à {printer}…
|
||||
Choose a folder for the searchable files Choisissez un dossier pour les fichiers recherchables
|
||||
Files are combined in this order. Leave Pages empty to take every page. Les fichiers sont combinés dans cet ordre. Laissez Pages vide pour prendre toutes les pages.
|
||||
Pages of {name} to combine, e.g. 1-3, 6 Pages de {name} à combiner, p. ex. 1-3, 6
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 39.
|
@@ -1544,6 +1544,8 @@
|
||||
{file} is too large for a stamp (at most {n} MB) {file} túl nagy bélyegzőnek (legfeljebb {n} MB)
|
||||
Sent to {printer} Elküldve ide: {printer}
|
||||
Sent to {printer}: {msg} Elküldve ide: {printer}: {msg}
|
||||
Looking for printers… Nyomtatók keresése…
|
||||
Sending to {printer}… Küldés ide: {printer}…
|
||||
Choose a folder for the searchable files Válassz mappát a kereshető fájloknak
|
||||
Files are combined in this order. Leave Pages empty to take every page. A fájlok ebben a sorrendben egyesülnek. Hagyd üresen az Oldalak mezőt, ha minden oldalt kéred.
|
||||
Pages of {name} to combine, e.g. 1-3, 6 {name} egyesítendő oldalai, pl. 1-3, 6
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 7 and column 44.
|
@@ -1570,6 +1570,8 @@
|
||||
{file} is too large for a stamp (at most {n} MB) {file} è troppo grande per un timbro (massimo {n} MB)
|
||||
Sent to {printer} Inviato a {printer}
|
||||
Sent to {printer}: {msg} Inviato a {printer}: {msg}
|
||||
Looking for printers… Ricerca delle stampanti…
|
||||
Sending to {printer}… Invio a {printer}…
|
||||
Choose a folder for the searchable files Scegli una cartella per i file ricercabili
|
||||
Files are combined from top to bottom. Leave Pages empty to take every page. I file vengono combinati dall'alto verso il basso. Lascia vuoto Pagine per prendere tutte le pagine.
|
||||
Unlock… Sblocca…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -1575,6 +1575,8 @@
|
||||
{file} is too large for a stamp (at most {n} MB) {file} はスタンプの上限({n} MB)を超えています
|
||||
Sent to {printer} {printer} に送信しました
|
||||
Sent to {printer}: {msg} {printer} に送信しました: {msg}
|
||||
Looking for printers… プリンターを検索しています…
|
||||
Sending to {printer}… {printer} に送信しています…
|
||||
Choose a folder for the searchable files 検索可能なファイルの保存先フォルダーを選択
|
||||
Files are combined from top to bottom. Leave Pages empty to take every page. ファイルは上から順に結合されます。すべてのページを使用する場合は「ページ」を空欄にしてください。
|
||||
Unlock… ロックを解除…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -1561,6 +1561,8 @@
|
||||
{file} is too large for a stamp (at most {n} MB) Plik {file} jest zbyt duży na stempel (maksymalnie {n} MB)
|
||||
Sent to {printer} Wysłano do {printer}
|
||||
Sent to {printer}: {msg} Wysłano do {printer}: {msg}
|
||||
Looking for printers… Wyszukiwanie drukarek…
|
||||
Sending to {printer}… Wysyłanie do {printer}…
|
||||
Choose a folder for the searchable files Wybierz folder dla plików z możliwością wyszukiwania
|
||||
Files are combined from top to bottom. Leave Pages empty to take every page. Pliki są łączone od góry do dołu. Pozostaw pole Strony puste, aby uwzględnić wszystkie strony.
|
||||
Unlock… Odblokuj…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -1459,6 +1459,8 @@
|
||||
{file} is too large for a stamp (at most {n} MB) {file} é muito grande para um carimbo (no máximo {n} MB)
|
||||
Sent to {printer} Enviado para {printer}
|
||||
Sent to {printer}: {msg} Enviado para {printer}: {msg}
|
||||
Looking for printers… Procurando impressoras…
|
||||
Sending to {printer}… Enviando para {printer}…
|
||||
Choose a folder for the searchable files Escolha uma pasta para os arquivos pesquisáveis
|
||||
Files are combined in this order. Leave Pages empty to take every page. Os arquivos são combinados nesta ordem. Deixe Páginas em branco para incluir todas as páginas.
|
||||
Pages of {name} to combine, e.g. 1-3, 6 Páginas de {name} a combinar, ex.: 1-3, 6
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
@@ -1567,6 +1567,8 @@
|
||||
{file} is too large for a stamp (at most {n} MB) {file} слишком велик для штампа (не более {n} МБ)
|
||||
Sent to {printer} Отправлено на {printer}
|
||||
Sent to {printer}: {msg} Отправлено на {printer}: {msg}
|
||||
Looking for printers… Поиск принтеров…
|
||||
Sending to {printer}… Отправка на {printer}…
|
||||
Choose a folder for the searchable files Выберите папку для доступных для поиска файлов
|
||||
Files are combined from top to bottom. Leave Pages empty to take every page. Файлы объединяются сверху вниз. Оставьте «Страницы» пустыми, чтобы взять все страницы.
|
||||
Unlock… Разблокировать…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
@@ -1550,6 +1550,8 @@ action wizard Start ప్రారంభించు
|
||||
{file} is too large for a stamp (at most {n} MB) స్టాంప్కు {file} చాలా పెద్దది (గరిష్ఠంగా {n} MB)
|
||||
Sent to {printer} {printer}కు పంపబడింది
|
||||
Sent to {printer}: {msg} {printer}కు పంపబడింది: {msg}
|
||||
Looking for printers… ప్రింటర్ల కోసం వెతుకుతోంది…
|
||||
Sending to {printer}… {printer}కు పంపుతోంది…
|
||||
Choose a folder for the searchable files వెతకగలిగే ఫైల్ల కోసం ఒక ఫోల్డర్ను ఎంచుకోండి
|
||||
Files are combined in this order. Leave Pages empty to take every page. ఫైల్లు ఈ క్రమంలో కలపబడతాయి. అన్ని పేజీలను తీసుకోవడానికి పేజీలు ఫీల్డ్ను ఖాళీగా వదలండి.
|
||||
Pages of {name} to combine, e.g. 1-3, 6 కలపాల్సిన {name} పేజీలు, ఉదా. 1-3, 6
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 13 and column 16.
|
@@ -1564,6 +1564,8 @@
|
||||
{file} is too large for a stamp (at most {n} MB) {file} завеликий для штампа (не більше {n} МБ)
|
||||
Sent to {printer} Надіслано на {printer}
|
||||
Sent to {printer}: {msg} Надіслано на {printer}: {msg}
|
||||
Looking for printers… Пошук принтерів…
|
||||
Sending to {printer}… Надсилання на {printer}…
|
||||
Choose a folder for the searchable files Виберіть теку для файлів із пошуком
|
||||
Files are combined from top to bottom. Leave Pages empty to take every page. Файли об’єднуються згори донизу. Залиште «Сторінки» порожнім, щоб узяти всі сторінки.
|
||||
Unlock… Розблокувати…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -1534,6 +1534,8 @@
|
||||
{file} is too large for a stamp (at most {n} MB) {file} 对于图章来说太大了(最大 {n} MB)
|
||||
Sent to {printer} 已发送到{printer}
|
||||
Sent to {printer}: {msg} 已发送到{printer}:{msg}
|
||||
Looking for printers… 正在查找打印机…
|
||||
Sending to {printer}… 正在发送到{printer}…
|
||||
Choose a folder for the searchable files 选择可搜索文件的文件夹
|
||||
Files are combined from top to bottom. Leave Pages empty to take every page. 文件按从上到下的顺序合并。页码留空表示取全部页面。
|
||||
Unlock… 解锁…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
@@ -1483,6 +1483,8 @@
|
||||
{file} is too large for a stamp (at most {n} MB) {file} 對於戳記來說太大了(最大 {n} MB)
|
||||
Sent to {printer} 已傳送到{printer}
|
||||
Sent to {printer}: {msg} 已傳送到{printer}:{msg}
|
||||
Looking for printers… 正在尋找印表機…
|
||||
Sending to {printer}… 正在傳送到{printer}…
|
||||
Choose a folder for the searchable files 選擇可搜尋檔案的資料夾
|
||||
Files are combined from top to bottom. Leave Pages empty to take every page. 檔案將按從上到下的順序合併。頁碼留空表示取全部頁面。
|
||||
Unlock… 解除鎖定…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
@@ -86,7 +86,7 @@ mod pickers;
|
||||
pub mod prepare;
|
||||
mod print_ui;
|
||||
mod signature_drag;
|
||||
pub use print_ui::{Handling as PrintHandling, PrintDraft, Which as PrintWhich};
|
||||
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};
|
||||
pub mod i18n;
|
||||
@@ -430,6 +430,12 @@ pub struct PdfCraftApp {
|
||||
/// How to ask for the latest release (the desktop app sets it; see `updates`).
|
||||
pub update_source: Option<updates::UpdateSource>,
|
||||
pub(crate) updates: updates::Updates,
|
||||
/// Lists the Print dialog's printers in the background (the desktop app sets it; see
|
||||
/// [`PrinterSource`]).
|
||||
pub printer_source: Option<PrinterSource>,
|
||||
// A browser has no background print work: the type is empty there.
|
||||
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
||||
pub(crate) print_tasks: print_ui::PrintTasks,
|
||||
pub palette_open: bool,
|
||||
pub palette_query: String,
|
||||
pub all_tools_expanded: bool,
|
||||
@@ -730,6 +736,8 @@ impl PdfCraftApp {
|
||||
dialog: None,
|
||||
update_source: None,
|
||||
updates: updates::Updates::default(),
|
||||
printer_source: None,
|
||||
print_tasks: print_ui::PrintTasks::default(),
|
||||
palette_open: false,
|
||||
palette_query: String::new(),
|
||||
all_tools_expanded: false,
|
||||
@@ -2013,6 +2021,7 @@ impl eframe::App for PdfCraftApp {
|
||||
}
|
||||
}
|
||||
self.poll_updates();
|
||||
self.poll_print();
|
||||
// Shortcuts deferred last frame: the text field has taken that frame's typing since.
|
||||
let deferred = std::mem::take(&mut self.deferred_commands);
|
||||
self.shortcuts(ctx);
|
||||
|
||||
+149
-17
@@ -35,6 +35,10 @@ pub enum Handling {
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PrintDraft {
|
||||
pub printers: Vec<spool::Printer>,
|
||||
/// The printers are still being listed in the background (see [`PrinterSource`]).
|
||||
pub printers_loading: bool,
|
||||
/// The user chose the printer, so a list arriving late doesn't move it to the default.
|
||||
pub printer_picked: bool,
|
||||
/// `None` = Save as PDF.
|
||||
pub printer: Option<String>,
|
||||
pub copies: u32,
|
||||
@@ -78,6 +82,8 @@ impl Default for PrintDraft {
|
||||
fn default() -> Self {
|
||||
PrintDraft {
|
||||
printers: Vec::new(),
|
||||
printers_loading: false,
|
||||
printer_picked: false,
|
||||
printer: None,
|
||||
copies: 1,
|
||||
collate: true,
|
||||
@@ -165,11 +171,44 @@ impl PrintDraft {
|
||||
}
|
||||
}
|
||||
|
||||
/// Lists the printers off the UI thread (the desktop app sets [`PdfCraftApp::printer_source`]):
|
||||
/// on Windows that takes PowerShell a second or more, and CUPS' driverless discovery can wait on
|
||||
/// the network. Without a source the dialog reads them as it opens, so tests stay deterministic.
|
||||
pub type PrinterSource = std::sync::Arc<dyn Fn() -> Vec<spool::Printer> + Send + Sync>;
|
||||
|
||||
/// The system spooler's printers, as the desktop app's [`PrinterSource`].
|
||||
pub fn system_printers() -> PrinterSource {
|
||||
std::sync::Arc::new(spool::printers)
|
||||
}
|
||||
|
||||
/// The Print dialog's work in the background: the printer list being read, and the jobs being
|
||||
/// spooled (the printer each was sent to, and the spooler's answer).
|
||||
#[derive(Default)]
|
||||
pub(crate) struct PrintTasks {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
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 printer the dialog preselects: the system default, else the first.
|
||||
fn default_printer(printers: &[spool::Printer]) -> Option<String> {
|
||||
printers.iter().find(|p| p.default).or(printers.first()).map(|p| p.name.clone())
|
||||
}
|
||||
|
||||
impl PdfCraftApp {
|
||||
pub fn open_print(&mut self) {
|
||||
let Some((i, _)) = self.active_ids() else { return };
|
||||
let printers = spool::printers();
|
||||
let default = printers.iter().find(|p| p.default).or(printers.first()).map(|p| p.name.clone());
|
||||
let (printers, printers_loading) = match self.printer_source.clone() {
|
||||
// The list from the last time the dialog opened, until the fresh one arrives.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
Some(source) => {
|
||||
self.list_printers_in_background(source);
|
||||
(self.print_draft.printers.clone(), true)
|
||||
}
|
||||
_ => (spool::printers(), false),
|
||||
};
|
||||
let default = default_printer(&printers);
|
||||
let current = self.views[i].current;
|
||||
let selected: Vec<usize> = self.views[i].selected.iter().copied().collect();
|
||||
let keep = std::mem::take(&mut self.print_draft);
|
||||
@@ -180,10 +219,85 @@ impl PdfCraftApp {
|
||||
(true, Which::Selected) => Which::All,
|
||||
(true, other) => other,
|
||||
};
|
||||
self.print_draft = PrintDraft { printers, printer: default, current_page: current, sheet: 0, selected, which, ..keep };
|
||||
self.print_draft = PrintDraft {
|
||||
printers,
|
||||
printers_loading,
|
||||
printer_picked: false,
|
||||
printer: default,
|
||||
current_page: current,
|
||||
sheet: 0,
|
||||
selected,
|
||||
which,
|
||||
..keep
|
||||
};
|
||||
self.dialog = Some(crate::Dialog::Print);
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn list_printers_in_background(&mut self, source: PrinterSource) {
|
||||
// A list already being read is as fresh as a new one would be.
|
||||
if self.print_tasks.printers.is_some() {
|
||||
return;
|
||||
}
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let ctx = self.ctx.clone();
|
||||
std::thread::spawn(move || {
|
||||
// The receiver may be gone (the app quit): nothing to report to then.
|
||||
let _ = tx.send(source());
|
||||
if let Some(ctx) = ctx {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
});
|
||||
self.print_tasks.printers = Some(rx);
|
||||
}
|
||||
|
||||
/// Pick up a printer list or a spooled job that finished (each frame).
|
||||
pub(crate) fn poll_print(&mut self) {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
if let Some(rx) = &self.print_tasks.printers {
|
||||
let printers = match rx.try_recv() {
|
||||
Ok(p) => Some(p),
|
||||
Err(std::sync::mpsc::TryRecvError::Empty) => None,
|
||||
// A source that died lists nothing new; the dialog keeps what it has.
|
||||
Err(std::sync::mpsc::TryRecvError::Disconnected) => Some(self.print_draft.printers.clone()),
|
||||
};
|
||||
if let Some(printers) = printers {
|
||||
self.print_tasks.printers = None;
|
||||
let d = &mut self.print_draft;
|
||||
if !d.printer_picked {
|
||||
d.printer = default_printer(&printers);
|
||||
}
|
||||
d.printers = printers;
|
||||
d.printers_loading = false;
|
||||
}
|
||||
}
|
||||
let mut finished = Vec::new();
|
||||
self.print_tasks.jobs.retain(|(printer, rx)| match rx.try_recv() {
|
||||
Ok(r) => {
|
||||
finished.push((printer.clone(), r));
|
||||
false
|
||||
}
|
||||
Err(std::sync::mpsc::TryRecvError::Empty) => true,
|
||||
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
|
||||
finished.push((printer.clone(), Err("the print job stopped unexpectedly".into())));
|
||||
false
|
||||
}
|
||||
});
|
||||
for (printer, result) in finished {
|
||||
self.report_print_job(&printer, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn report_print_job(&mut self, printer: &str, result: Result<String, String>) {
|
||||
match result {
|
||||
Ok(msg) if msg.is_empty() => self.notify(crate::i18n::fmt(tl!("Sent to {printer}"), &[("printer", printer)])),
|
||||
Ok(msg) => self.notify(crate::i18n::fmt(tl!("Sent to {printer}: {msg}"), &[("printer", printer), ("msg", &msg)])),
|
||||
Err(e) => self.notify_error(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Print (or save) with the dialog's settings. Returns `true` on success. Save as PDF without
|
||||
/// a preset path returns `true` once the save picker is showing; the file is written on a
|
||||
/// later frame, when the user has chosen where.
|
||||
@@ -211,24 +325,38 @@ impl PdfCraftApp {
|
||||
}
|
||||
};
|
||||
match self.print_draft.printer.clone() {
|
||||
Some(printer) => match spool::submit(&bytes, &self.print_draft.job(&name)) {
|
||||
Ok(msg) => {
|
||||
self.notify(if msg.is_empty() {
|
||||
crate::i18n::fmt(tl!("Sent to {printer}"), &[("printer", &printer)])
|
||||
} else {
|
||||
crate::i18n::fmt(tl!("Sent to {printer}: {msg}"), &[("printer", &printer), ("msg", &msg)])
|
||||
});
|
||||
true
|
||||
}
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
/// Spool a job off the UI thread: on Windows every sheet is rendered at the printer's
|
||||
/// resolution first, which can take minutes for a long document. The dialog closes at once
|
||||
/// and the spooler's answer arrives as a notice.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn send_to_printer(&mut self, printer: String, bytes: Vec<u8>, job: spool::Job) -> bool {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let ctx = self.ctx.clone();
|
||||
std::thread::spawn(move || {
|
||||
let _ = tx.send(spool::submit(&bytes, &job).map_err(|e| e.to_string()));
|
||||
if let Some(ctx) = ctx {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
});
|
||||
self.notify(crate::i18n::fmt(tl!("Sending to {printer}…"), &[("printer", &printer)]));
|
||||
self.print_tasks.jobs.push((printer, rx));
|
||||
true
|
||||
}
|
||||
|
||||
/// A browser has no spooler: `submit` answers at once.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn send_to_printer(&mut self, printer: String, bytes: Vec<u8>, job: spool::Job) -> bool {
|
||||
let result = spool::submit(&bytes, &job).map_err(|e| e.to_string());
|
||||
let ok = result.is_ok();
|
||||
self.report_print_job(&printer, result);
|
||||
ok
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@@ -419,6 +547,9 @@ pub(crate) fn body(
|
||||
ui.horizontal(|ui| {
|
||||
let shown = d.printer.clone().unwrap_or_else(|| tl!("Save as PDF").to_string());
|
||||
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() };
|
||||
ui.selectable_value(&mut d.printer, Some(p.name.clone()), label);
|
||||
@@ -446,6 +577,7 @@ pub(crate) fn body(
|
||||
}
|
||||
});
|
||||
if d.printer != before {
|
||||
d.printer_picked = true;
|
||||
d.show_driver_options = false;
|
||||
d.driver_error = None;
|
||||
}
|
||||
|
||||
@@ -190,3 +190,56 @@ fn print_dialog_shows_the_printer_driver_options() {
|
||||
h.state_mut().print_draft.printer = Some("Other_Queue".into());
|
||||
assert!(h.state().print_draft.job("form.pdf").options.is_empty());
|
||||
}
|
||||
|
||||
/// The desktop app lists printers in the background (PowerShell on Windows takes a second or
|
||||
/// more): the dialog opens at once, then preselects the default when the list arrives — unless
|
||||
/// the user has already picked a printer.
|
||||
#[test]
|
||||
fn printers_listed_in_the_background_preselect_the_default_unless_one_was_picked() {
|
||||
use pdfcraft_engine::print::spool::Printer;
|
||||
use std::sync::{Arc, Mutex};
|
||||
let mut h = harness();
|
||||
// The source waits until the test lets it answer, so the dialog is seen while it looks.
|
||||
let gate = Arc::new(Mutex::new(()));
|
||||
let hold = gate.lock().unwrap();
|
||||
let wait = gate.clone();
|
||||
h.state_mut().printer_source = Some(Arc::new(move || {
|
||||
let _open = wait.lock();
|
||||
vec![Printer { name: "Office".into(), default: false }, Printer { name: "Front Desk".into(), default: true }]
|
||||
}));
|
||||
assert!(h.state_mut().execute("print.dialog"));
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.state().dialog, Some(Dialog::Print), "the dialog doesn't wait for the printers");
|
||||
assert!(h.state().print_draft.printers_loading);
|
||||
assert_eq!(h.state().print_draft.printer, None);
|
||||
drop(hold);
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
|
||||
while h.state().print_draft.printers_loading && std::time::Instant::now() < deadline {
|
||||
h.run_steps(1);
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
assert!(!h.state().print_draft.printers_loading);
|
||||
assert_eq!(h.state().print_draft.printers.len(), 2);
|
||||
assert_eq!(h.state().print_draft.printer.as_deref(), Some("Front Desk"));
|
||||
|
||||
// Opened again: last time's list shows at once; a choice made before the fresh list
|
||||
// arrives is kept.
|
||||
h.state_mut().dialog = None;
|
||||
let hold = gate.lock().unwrap();
|
||||
assert!(h.state_mut().execute("print.dialog"));
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.state().print_draft.printers.len(), 2, "the last list shows while looking");
|
||||
{
|
||||
let d = &mut h.state_mut().print_draft;
|
||||
d.printer = None;
|
||||
d.printer_picked = true;
|
||||
}
|
||||
drop(hold);
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
|
||||
while h.state().print_draft.printers_loading && std::time::Instant::now() < deadline {
|
||||
h.run_steps(1);
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
assert!(!h.state().print_draft.printers_loading);
|
||||
assert_eq!(h.state().print_draft.printer, None, "Save as PDF, as picked");
|
||||
}
|
||||
@@ -7866,8 +7866,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"]
|
||||
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 with the driver's own settings; 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"]
|
||||
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"
|
||||
|
||||
[[feature]]
|
||||
id = "print.copies-collate"
|
||||
|
||||
Reference in new issue
Block a user