mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-14 19:27:04 -04:00
* M10: Windows printing prints at actual size, off the UI thread (follow-up to #847) - Sheets print at 100% on the driver's paper closest to their size, landscape when wide, instead of being shrunk into .NET's default 1-inch margins (about 76% on Letter) and never rotated. Each sheet is capped at ~64 MP so a poster-sized sheet can't exhaust memory. - PowerShell runs with CREATE_NO_WINDOW: the release app has no console, so Windows opened one over the app for the whole job. - The Print dialog lists printers in the background (the desktop app's printer_source) and spools jobs on a worker thread, so Ctrl+P and long jobs no longer freeze the UI. A list arriving late preselects the default only if the user hasn't picked a printer. - Parity evidence named a test that #847 renamed; xtask parity passes again. The raster-only limit is now stated in the parity notes and README. Co-Authored-By: Claude Opus 5.5 <[email protected]> * M10: refresh catalog hashes in ATTRIBUTION.toml for the new print strings Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
33c14e07cf
commit
bbbe60015c
25 files changed
+318
-49
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"
|
||||
|
||||
@@ -142,6 +142,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")]
|
||||
{
|
||||
|
||||
@@ -39,7 +39,9 @@ piped to `lp` on stdin and never written to a temp file, where another local use
|
||||
swap it. On Windows the printers come from `Win32_Printer` and jobs are rendered by the in-box
|
||||
`Windows.Data.Pdf` and spooled by `System.Drawing.Printing` with the driver's own settings (both
|
||||
through `powershell`, so no Win32 API is called here); that job file exists for the seconds the
|
||||
spooler reads it, under a fresh unpredictable name. Other platforms report that printing to a
|
||||
spooler reads it, under a fresh unpredictable name. Each sheet prints at 100% on the driver's
|
||||
paper closest to its size, turned landscape when it is wide, and is rasterised at the printer's
|
||||
resolution (up to 600 dpi), not sent as vectors. Other platforms report that printing to a
|
||||
printer isn't available yet; the print-ready PDF can always be saved.
|
||||
|
||||
Not yet: the web spooler, print as image, poster labels, PostScript output, colour conversion for
|
||||
|
||||
@@ -349,10 +349,15 @@ pub fn printers_parsed(queues: &str, available: Option<&str>) -> Vec<Printer> {
|
||||
|
||||
/// The `powershell` command that runs a script. `-NoProfile` keeps a user's profile from
|
||||
/// writing banners into what is parsed, `-NonInteractive` from waiting for input.
|
||||
/// `CREATE_NO_WINDOW`: the release app has no console of its own, so without it Windows opens
|
||||
/// a console window for PowerShell over the app for as long as the script runs.
|
||||
#[cfg(windows)]
|
||||
fn powershell_command(script: &str) -> std::process::Command {
|
||||
use std::os::windows::process::CommandExt as _;
|
||||
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
||||
let mut c = std::process::Command::new("powershell");
|
||||
c.args(["-NoProfile", "-NonInteractive", "-Command", script]);
|
||||
c.creation_flags(CREATE_NO_WINDOW);
|
||||
c
|
||||
}
|
||||
|
||||
|
||||
@@ -104,26 +104,62 @@ $settings.Duplex = switch ($env:PDFCRAFT_PRINT_DUPLEX) {
|
||||
}
|
||||
if ($env:PDFCRAFT_PRINT_GRAYSCALE -eq '1') { $printer.DefaultPageSettings.Color = $false }
|
||||
|
||||
# The sheets are already laid out at their final size (CUPS gets `fit-to-page=false`), so each
|
||||
# one is printed at 100% on paper of its own size and orientation. Before every sheet the driver
|
||||
# is asked for that paper — the size closest to the sheet's, within an eighth of an inch, turned
|
||||
# either way — and turned landscape for a wide sheet; a driver without that paper keeps its own.
|
||||
$paperSizes = @($settings.PaperSizes)
|
||||
$script:sheets = 0
|
||||
$printer.add_QueryPageSettings({
|
||||
param($sender, $e)
|
||||
if ($script:sheets -ge $document.PageCount) { return }
|
||||
$page = $document.GetPage($script:sheets)
|
||||
try {
|
||||
# Windows.Data.Pdf measures in 1/96 inch, PageSettings in 1/100 inch.
|
||||
$w = $page.Size.Width / 96.0 * 100.0
|
||||
$h = $page.Size.Height / 96.0 * 100.0
|
||||
} finally {
|
||||
$page.Dispose()
|
||||
}
|
||||
$long = [Math]::Max($w, $h)
|
||||
$short = [Math]::Min($w, $h)
|
||||
$best = $null
|
||||
$bestOff = 12.5
|
||||
foreach ($paper in $paperSizes) {
|
||||
$off = [Math]::Abs([Math]::Max($paper.Width, $paper.Height) - $long) + [Math]::Abs([Math]::Min($paper.Width, $paper.Height) - $short)
|
||||
if ($off -lt $bestOff) { $best = $paper; $bestOff = $off }
|
||||
}
|
||||
if ($best) { $e.PageSettings.PaperSize = $best }
|
||||
$e.PageSettings.Landscape = ($w -gt $h)
|
||||
})
|
||||
|
||||
# One sheet at a time: its page is rendered when the driver asks for it, so a long document
|
||||
# holds only one page's bitmap in memory.
|
||||
$script:sheets = 0
|
||||
$printer.add_PrintPage({
|
||||
param($sender, $e)
|
||||
if ($script:sheets -ge $document.PageCount) { $e.HasMorePages = $false; return }
|
||||
$page = $document.GetPage($script:sheets)
|
||||
$bitmap = $null
|
||||
try {
|
||||
# The driver's page area is in hundredths of an inch; Render-Sheet fits the page into
|
||||
# it, so this is the same scale, turned back into hundredths for the draw rectangle.
|
||||
$area = $e.MarginBounds
|
||||
$w = $page.Size.Width
|
||||
$h = $page.Size.Height
|
||||
$scale = [Math]::Min($area.Width / 100.0 * 96.0 / $w, $area.Height / 100.0 * 96.0 / $h)
|
||||
# Hundredths of an inch throughout. The sheet is drawn at 100%, centred on the whole
|
||||
# page — not its 1-inch default margins — and shrunk only when the paper is smaller than
|
||||
# the sheet. The drawing origin is the printable area's corner, so the page's own corner
|
||||
# is the hard margin back from it; what falls outside the printable area is clipped, as
|
||||
# on CUPS.
|
||||
$paper = $e.PageBounds
|
||||
$w = $page.Size.Width / 96.0 * 100.0
|
||||
$h = $page.Size.Height / 96.0 * 100.0
|
||||
$scale = [Math]::Min(1.0, [Math]::Min($paper.Width / $w, $paper.Height / $h))
|
||||
$drawW = $w * $scale
|
||||
$drawH = $h * $scale
|
||||
# The printer's resolution, at most 600 dpi and at most ~64 megapixels a sheet, so a
|
||||
# poster-sized sheet can't run the machine out of memory.
|
||||
$dpi = [Math]::Min(600, [Math]::Max(72, $e.Graphics.DpiX))
|
||||
$bitmap = Render-Sheet $page $area.Width $area.Height $dpi
|
||||
$drawW = $w * $scale / 96.0 * 100.0
|
||||
$drawH = $h * $scale / 96.0 * 100.0
|
||||
$box = [System.Drawing.RectangleF]::new($area.Left + ($area.Width - $drawW) / 2.0, $area.Top + ($area.Height - $drawH) / 2.0, $drawW, $drawH)
|
||||
$dpi = [Math]::Max(36, [Math]::Min($dpi, [Math]::Sqrt(64e6 / ($drawW / 100.0 * $drawH / 100.0))))
|
||||
$bitmap = Render-Sheet $page $drawW $drawH $dpi
|
||||
$left = ($paper.Width - $drawW) / 2.0 - $e.PageSettings.HardMarginX
|
||||
$top = ($paper.Height - $drawH) / 2.0 - $e.PageSettings.HardMarginY
|
||||
$box = [System.Drawing.RectangleF]::new($left, $top, $drawW, $drawH)
|
||||
$e.Graphics.DrawImage($bitmap, $box)
|
||||
} finally {
|
||||
if ($bitmap) { $bitmap.Dispose() }
|
||||
|
||||
@@ -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::windows_lists_the_printers_the_system_knows", "crates/print/src/tests.rs::a_print_job_reaches_the_windows_spooler", "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::windows_lists_the_printers_the_system_knows", "crates/print/src/tests.rs::the_print_script_renders_the_job_and_reaches_the_spooler", "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, each sheet at 100% on the driver's paper closest to its size (landscape for a wide sheet; shrunk only when no paper is large enough), rasterised at the printer's resolution up to 600 dpi and about 64 megapixels a sheet, not sent as vectors; the printer list and the job run in the background, without a console window; 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"
|
||||
@@ -7878,7 +7878,7 @@ milestone = "M10"
|
||||
status = "shipped"
|
||||
commands = ["print.dialog"]
|
||||
tools = ["doc_print"]
|
||||
evidence = ["crates/print/src/tests.rs::spooler_arguments_and_printer_list", "crates/print/src/tests.rs::the_windows_job_is_the_environment_of_the_print_script", "crates/print/src/tests.rs::a_print_job_reaches_the_windows_spooler"]
|
||||
evidence = ["crates/print/src/tests.rs::spooler_arguments_and_printer_list", "crates/print/src/tests.rs::the_windows_job_is_the_environment_of_the_print_script", "crates/print/src/tests.rs::the_print_script_renders_the_job_and_reaches_the_spooler"]
|
||||
notes = "lp -n, collate option; on Windows the driver's Copies and Collate settings, as the job's"
|
||||
|
||||
[[feature]]
|
||||
@@ -7938,7 +7938,7 @@ milestone = "M10"
|
||||
status = "shipped"
|
||||
commands = ["print.dialog"]
|
||||
tools = ["doc_print"]
|
||||
evidence = ["crates/print/src/tests.rs::spooler_arguments_and_printer_list", "crates/print/src/tests.rs::the_windows_job_is_the_environment_of_the_print_script", "crates/print/src/tests.rs::a_print_job_reaches_the_windows_spooler"]
|
||||
evidence = ["crates/print/src/tests.rs::spooler_arguments_and_printer_list", "crates/print/src/tests.rs::the_windows_job_is_the_environment_of_the_print_script", "crates/print/src/tests.rs::the_print_script_renders_the_job_and_reaches_the_spooler"]
|
||||
notes = "long/short edge via the spooler: lp -o sides=…, and the Windows driver's Duplex setting"
|
||||
|
||||
[[feature]]
|
||||
|
||||
Reference in new issue
Block a user