M10: Print ▸ Properties… opens the printer driver's own settings (#482)

* M5.7: date format and date language for Fill & Sign in the engine

* M5: flatten Fill & Sign marks when saving

Saving can now bake typed text, marks and signatures into the page, from Preferences or doc_save. The option defaults off, so a normal save still leaves them editable, and other comments and form fields stay.

* M4: zoom the Organize page grid

The page grid draws its pages at 50% to 300%: toolbar buttons with a
percentage that resets to 100%, trackpad pinch or Ctrl/⌘ + wheel about
the page under the pointer, and ⌘+ / ⌘− / ⌘0 (which no longer change the
hidden page view's zoom while the grid shows).

Thumbnails are re-rendered sharper in a few steps as the grid grows,
capped so one document's thumbnails stay within 256 MB. The page under
the pointer (or the first one in view) stays where it is across a zoom.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* M4: zoomed grid pages are rendered at the size drawn

A zoomed page grid stretched 132 pt thumbnails, and the stepped detail
that was meant to help was capped by a memory budget, so a long document
stayed blurry at any zoom.

Pages drawn larger than their thumbnails now get a render at exactly the
size drawn while they are in view; the thumbnail stands in until it
arrives. Only pages in view keep one, so memory no longer depends on the
page count, and thumbnails go back to their one fixed size.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* M5.7: fill_sign_date_format tool; format and language on fill_sign_add

* M5.7: Preferences ▸ Date format for Fill & Sign dates (refs #158)

* M5.7: refuse Fill & Sign dates the PDF text font can't hold

* M5.7: Spanish labels for Preferences ▸ Date format

* M10: Print ▸ Properties… opens the printer driver's own settings

CUPS: the queue's PPD job options (with lpoptions defaults), grouped in the
Print dialog and sent with this job as lp -o key=value; installable options
and what the dialog sets itself are left out. Windows: the driver's Printing
Preferences window through printui (no unsafe FFI), saved as the printer's
per-user defaults. Headless: printer_options tool and doc_print options.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* View: the middle button pans instead of drawing with markup tools

egui's drag responses accept every pointer button, so on Windows and macOS a
middle-button drag reached the page tools: with Draw (ink) selected it drew a
stroke, and other tools cropped, selected or moved. Linux was unaffected
because its middle-click auto-scroll already blocks page input.

Outside Linux a middle-button drag that starts in the viewport (document or
Organize Pages) now pans 1:1 while the button is held, with a Grabbing
cursor, and page input is blocked from the press through the release. Linux
keeps its latched auto-scroll.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* M3: a short page gone to from the Pages panel stays current

The current page was whichever page showed most of itself. Going to a short
page (a cheque) above a tall one scrolls it to the top, where the tall page
below shows more, so the current page and the Pages panel's highlight moved
to the tall page instead of the one clicked. The page navigated to now stays
current while it is wholly on screen.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* M4: page operations from the Pages panel: context menu and drag to reorder

Right-clicking a thumbnail offers Extract pages, Cut, Copy and Paste after, on
the selection or on the page clicked (which becomes the selection), as the
organize grid does. Dragging thumbnails moves the selection, or the page
grabbed, to the gap it is dropped on: a bar marks the gap, the page count
follows the pointer, and the panel scrolls near its edges. The move is one
undoable step; the moved pages stay selected and the current page follows
its page. Documents that forbid assembly keep click-only thumbnails.

Parity: view.thumbnail-context-ops is shipped, with what it still lacks.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Pages panel: only the primary button drags thumbnails; a drag left over after the panel closed or the document changed is dropped

* Fill & Sign dates: German, Bulgarian, Hungarian and Ukrainian month and weekday names, matching the interface languages

* uk: keep the author's wording for the flatten setting

---------

Co-authored-by: KissT <[email protected]>
Co-authored-by: Mark Bartalos <[email protected]>
Co-authored-by: samehelhawary <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Cody Vogt <[email protected]>
Co-authored-by: William Max & Co. <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
This commit is contained in:
22 files changed
+550 -24

No files matched your search

+11 -11
View File
@@ -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 = "25a6a7ccbc113f8a64092b8962c9c0bfe64bfb6dc2000e2a6164881b492e8684"
sha256 = "d1f4c7c71d2359e816878e8de837119649513e20b8cf1036276f4d18639398f5"
[[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 = "957b2ca722eb3ebb6c6303ff5e25245b850b3a3e6ddba094516174e804aa96c1"
sha256 = "1cac4789161502b2fc3c52ac0bc9a89fd714495a87c594ba7a4f04e49e5a6456"
[[asset]]
path = "crates/ui-egui/src/i18n/cs.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 = "4d139791454c1c3a00e2726e495b1170eb1f5be95f6cd5e12e7311c85aca0259"
sha256 = "85a57e66f819e93c8c541548cd932ec98cd3dca888c25b62d5c973fb795eb78a"
[[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 = "716fb1f47bc69c355a688f116715b819a03c59d99ba9e03d65db2dd79bbe11b9"
sha256 = "f005a60c77110251be379572e3eb6bf9a7907946788f5ce0f8689800f5cc7b76"
[[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 = "f78e4175753553fcb27d61697e64ab092cede8433a8ae68ca1f553879b4217dc"
sha256 = "a53fb2b7d91a6f9cb37c5d7fcc77e0884b9b23a0f406569ba203062cff731be1"
[[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 = "329b95966eca05438ea23deb2a48f61d9af9e984017d14942af06b3bb0799e4b"
sha256 = "affd4f7f2e7285abfb987625bbb726cf3d761f0a67ccaf00c18d986be3fa5f9d"
[[asset]]
path = "crates/ui-egui/src/i18n/ru.tsv"
@@ -107,7 +107,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 = "50390d434fbebc5d6ed1a259db96d73d76be5678f4c87a8b04ddd1c38a4901f7"
sha256 = "88e402a8a16ba8ccd6f73e91326691ce4c4f86bf113cc484fb4c2af953ab3122"
[[asset]]
path = "crates/ui-egui/src/i18n/hu.tsv"
@@ -118,7 +118,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 = "4b7618b83b5b4479e5b8e4022a302689d8acf194a10368d7ef34302f8e19e299"
sha256 = "6c2860945882f12af506be1830182ddb6c783a524f718ee0c0540bf781400641"
[[asset]]
path = "crates/ui-egui/src/i18n/bg.tsv"
@@ -129,7 +129,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 = "78053df6ec3de2d0b6a10338b57618302deb4ec8cfb794be5a4a4d2bdc2f950b"
sha256 = "af9bd7f0a2c9f64faaea928e219be50a9ce8a71d40ae4c13e84cfe1aed751f40"
[[asset]]
path = "crates/ui-egui/src/i18n/ja.tsv"
@@ -140,7 +140,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 = "285d3d9c8fff269ceb032e6be67a1c33d596f2ed80c9d8677f5655fb014f43b5"
sha256 = "b93836c384d11e5027b902f6f5a9fa389a5a793213bb77f6754e4cf71fdc28c8"
[[asset]]
path = "crates/ui-egui/src/i18n/uk.tsv"
@@ -151,7 +151,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 = "06e8584e6f16528598e311e1b0dbd167701967c6786e14701886285babb38c9f"
sha256 = "750c7229170e1ffd96b140ed13f0ce85a7c04a5223ed2648a75c96e2c0d65140"
[[asset]]
path = "assets/icons/accessibility.svg"
+3
View File
@@ -135,6 +135,7 @@ M0 → M1 → M2 → M3 → M4 must happen in order. After M4, M5–M12 can run
- **2026-10-09 (M14, #577):** The desktop GPU setup enables the selected adapter's supported 2D texture extent instead of egui-wgpu's fixed 8192-pixel extent. This removes the artificial surface ceiling behind a mixed-DPI Windows startup crash when opening Outlook PDF attachments. Synthetic regressions cover the observed 8600 × 3423 surface, downlevel adapters and preservation of other device limits. Hardware resolution limits still apply; milestone and overall effort estimates are unchanged.
- **2026-10-09 (M5, Fill & Sign flatten on save):** Preferences and `doc_save` can bake Fill & Sign text, marks and signatures into the page when saving. The option defaults off. Other comments and form fields stay, a second flatten does not reuse an XObject name already on the page, and a document that cannot be changed is left unwritten. Overall estimate unchanged (about 30–35%).
- **2026-10-09 (M5, Fill & Sign flatten on save):** Preferences and `doc_save` can bake Fill & Sign text, marks and signatures into the page when saving. The option defaults off. Other comments and form fields stay, a second flatten does not reuse an XObject name already on the page, and a document that cannot be changed is left unwritten. Overall estimate unchanged (about 30–35%).
- **2026-10-09 (M5, Fill & Sign flatten on save):** Preferences and `doc_save` can bake Fill & Sign text, marks and signatures into the page when saving. The option defaults off. Other comments and form fields stay, a second flatten does not reuse an XObject name already on the page, and a document that cannot be changed is left unwritten. Overall estimate unchanged (about 30–35%).
- **2026-10-09 (M5.7, #434 partial):** Natural-size image stamps placed at a click or through `stamp_custom` remain upright on rotated pages, with a displayed-space-sized rectangle and a counterrotated standard appearance matrix. Move, resize, restyle and undo/redo preserve that orientation. Native and external Poppler save/reopen rendering cover all four quarter-turns; UI tests verify the displayed bounds. Explicit rectangle placement, image signatures, PDF-page stamps and image-file orientation metadata remain outside this repair. #434 stays open; milestone table and effort estimates remain unchanged.
@@ -148,6 +149,8 @@ M0 → M1 → M2 → M3 → M4 must happen in order. After M4, M5–M12 can run
- **2026-10-09 (M4, page grid zoom):** The Organize page grid zooms from 50% to 300%: toolbar buttons with a percentage that resets, trackpad pinch or Ctrl/⌘ + wheel about the page under the pointer, and ⌘+ / ⌘− / ⌘0. Pages drawn larger than their thumbnails are rendered at the size drawn while in view, so text stays sharp in documents of any length. Not yet: a slider, remembering the size, a size control for the Pages panel. Overall estimate unchanged (about 30–35%).
- **2026-10-09 (M4, page grid zoom):** The Organize page grid zooms from 50% to 300%: toolbar buttons with a percentage that resets, trackpad pinch or Ctrl/⌘ + wheel about the page under the pointer, and ⌘+ / ⌘− / ⌘0. Pages drawn larger than their thumbnails are rendered at the size drawn while in view, so text stays sharp in documents of any length. Not yet: a slider, remembering the size, a size control for the Pages panel. Overall estimate unchanged (about 30–35%).
- **2026-10-08 (M5.7, signature resize proportions):** Image signature corner handles preserve the embedded image's original aspect ratio and anchor the opposite corner; edge midpoint handles stretch width or height independently. The live preview and committed edit share the constrained geometry, including page/view rotation and reopened PDFs whose rectangles were stretched previously. Synthetic UI tests cover all corners, edge resizing, undo and redo. Milestone and effort estimates unchanged.
- **2026-10-08 (M5.7, live signature adjustment):** Embedded image signatures resize and move with every pointer frame. Movement hides the frame and handles, which return on release; Escape cancels without committing. A cached background preserves the page beneath the image, and only the completed gesture enters undo history. The read-only engine preview and `comment_image_preview` tool share bounded image decoding and render layers. Synthetic pixel tests cover live geometry, alpha, coloured page content, rotation, cancellation and encrypted save/reopen. Milestone and effort estimates unchanged.
- **2026-10-08 (M10/M4, create from multiple files):** Create a PDF ▸ Multiple files is live. The picked PDFs, images and `.txt` files open as one unsaved document in the Organize page grid, where pages are removed and reordered and Save pages writes what is shown. The grid gained a "+" at every gap that inserts files right there, and on desktop files dropped on the grid go to the gap under the pointer; Insert from file now takes several files in order, converts images and text, and selects what it inserted. File-type detection moved from the shell into `pdfcraft-create` (`source_kind`), with one engine entry point (`Session::convert_to_pdf`) shared by Open, Insert, Create and the new `doc_create_multiple` tool (which can also write one PDF per file). Not yet: Office/HTML/Markdown inputs, PDF Portfolios, progress for big batches, dropping onto the grid in the web build. Overall estimate unchanged (about 30–35%).
+1
View File
@@ -516,6 +516,7 @@ impl Automation {
"redact_clear" => self.redact_clear(&a)?,
"doc_hidden_info" => self.doc_hidden_info(&a)?,
"printers" => self.printers()?,
"printer_options" => self.printer_options(&a)?,
"link_list" => self.link_list(&a)?,
"link_add" => self.link_add(&a)?,
"link_edit" => self.link_edit(&a)?,
+23
View File
@@ -16,6 +16,18 @@ impl Automation {
Ok(json!({ "count": list.len(), "printers": list }))
}
pub(crate) fn printer_options(&self, a: &Args) -> Result<Value> {
let printer = a.str("printer")?;
let list: Vec<Value> = spool::printer_options(printer)
.into_iter()
.map(|o| {
let choices: Vec<Value> = o.choices.iter().map(|(value, label)| json!({ "value": value, "label": label })).collect();
json!({ "key": o.key, "label": o.label, "group": o.group, "default": o.default, "choices": choices })
})
.collect();
Ok(json!({ "printer": printer, "count": list.len(), "options": list }))
}
pub(crate) fn doc_print(&mut self, a: &Args) -> Result<Value> {
let doc = self.doc(a)?;
let (id, count) = (doc.id, doc.info.pages.len());
@@ -122,6 +134,17 @@ impl Automation {
},
grayscale: a.opt_bool("grayscale")?.unwrap_or(false),
title: name,
options: match a.get("options") {
None => Vec::new(),
Some(Value::Object(m)) => m
.iter()
.map(|(k, v)| match v.as_str() {
Some(v) => Ok((k.clone(), v.to_string())),
None => Err(bad(format!("options.{k} must be a string (a choice from printer_options)"))),
})
.collect::<Result<Vec<_>>>()?,
Some(_) => return Err(bad("options must be an object of option keys and choices from printer_options")),
},
};
out["job"] = json!(spool::submit(&bytes, &job).map_err(|e| failed(e.to_string()))?);
}
+5 -1
View File
@@ -395,10 +395,13 @@ pub fn tools() -> Vec<ToolDef> {
&["doc"],
)),
t("printers", "List printers", "The printers the system's print spooler knows (CUPS on macOS and Linux), with the default marked.").ro().with(schema(json!({}), &[])),
t("printer_options", "List printer options", "A printer driver's own job options (CUPS: from its PPD), such as the paper tray, paper type or finishing: key, label, group, default and choices. Pass chosen values to doc_print as options. Empty on Windows, where the driver's preferences window holds them.")
.ro()
.with(schema(json!({ "printer": { "type": "string" } }), &["printer"])),
t(
"doc_print",
"Print",
"Print with Acrobat's Print dialog options, or save the print-ready PDF. pages: a range such as \"1-3, 6, 9-\" (page labels allowed; default all); subset odd/even; reverse. layout: fit (default), actual, shrink, custom (scale %), multiple (per_sheet 2/4/6/9/16, order, border, auto_rotate; cut-stack order arranges single-sided sheets for cutting into piles and stacking left to right, top to bottom, keeping sheet order within each pile; duplex must be off), booklet (booklet_subset both/front/back, binding left/right), poster (scale %, overlap pt, cut_marks). orientation auto/portrait/landscape; comments_forms document / document-and-markups (default) / document-and-stamps / form-fields-only; paper Letter/Legal/Tabloid/A3/A4/A5. Then path (save) or printer (a name or \"default\") with copies, collate, duplex off/long-edge/short-edge, grayscale.",
"Print with Acrobat's Print dialog options, or save the print-ready PDF. pages: a range such as \"1-3, 6, 9-\" (page labels allowed; default all); subset odd/even; reverse. layout: fit (default), actual, shrink, custom (scale %), multiple (per_sheet 2/4/6/9/16, order, border, auto_rotate; cut-stack order arranges single-sided sheets for cutting into piles and stacking left to right, top to bottom, keeping sheet order within each pile; duplex must be off), booklet (booklet_subset both/front/back, binding left/right), poster (scale %, overlap pt, cut_marks). orientation auto/portrait/landscape; comments_forms document / document-and-markups (default) / document-and-stamps / form-fields-only; paper Letter/Legal/Tabloid/A3/A4/A5. Then path (save) or printer (a name or \"default\") with copies, collate, duplex off/long-edge/short-edge, grayscale, and options (driver options from printer_options, CUPS).",
)
.with(schema(
json!({
@@ -425,6 +428,7 @@ pub fn tools() -> Vec<ToolDef> {
"collate": { "type": "boolean" },
"duplex": { "type": "string", "enum": ["off", "long-edge", "short-edge"] },
"grayscale": { "type": "boolean" },
"options": { "type": "object", "additionalProperties": { "type": "string" }, "description": "Printer driver options from printer_options: key → choice value." },
}),
&["doc"],
)),
+11
View File
@@ -1836,6 +1836,17 @@ fn printing_through_tools() {
let mut a = auto(&dir);
let doc = ok(&mut a, "doc_open", json!({ "path": "a.pdf" }))["doc"].as_u64().unwrap();
assert!(ok(&mut a, "printers", json!({}))["printers"].is_array());
let none = ok(&mut a, "printer_options", json!({ "printer": "No_Such_Queue_PdfCraft" }));
assert_eq!((none["count"].as_u64(), none["options"].as_array().map(Vec::len)), (Some(0), Some(0)));
assert!(matches!(a.call("printer_options", &json!({})), Err(ToolError::InvalidArgs(_))));
assert!(matches!(
a.call("doc_print", &json!({ "doc": doc, "printer": "default", "options": ["InputSlot"] })),
Err(ToolError::InvalidArgs(m)) if m.contains("printer_options")
));
assert!(matches!(
a.call("doc_print", &json!({ "doc": doc, "printer": "default", "options": { "InputSlot": 2 } })),
Err(ToolError::InvalidArgs(m)) if m.contains("options.InputSlot")
));
let n = ok(&mut a, "doc_info", json!({ "doc": doc }))["document"]["pages"].as_u64().unwrap();
let r = ok(&mut a, "doc_print", json!({ "doc": doc, "layout": "multiple", "per_sheet": 4, "path": "sheets.pdf" }));
assert_eq!(r["sheets"].as_u64(), Some(n.div_ceil(4)));
+192 -1
View File
@@ -27,11 +27,14 @@ pub struct Job {
pub duplex: Duplex,
pub grayscale: bool,
pub title: String,
/// The printer driver's own job options (a PPD keyword and the chosen value), such as the
/// paper tray or paper type; see [`printer_options`]. CUPS only.
pub options: Vec<(String, String)>,
}
impl Default for Job {
fn default() -> Self {
Job { printer: None, copies: 1, collate: true, duplex: Duplex::Off, grayscale: false, title: "PdfCraft".into() }
Job { printer: None, copies: 1, collate: true, duplex: Duplex::Off, grayscale: false, title: "PdfCraft".into(), options: Vec::new() }
}
}
@@ -65,9 +68,197 @@ pub fn lp_args(job: &Job) -> Vec<String> {
}
// The sheets are already laid out at their final size.
opt("fit-to-page=false");
for (key, value) in &job.options {
// Driver options are PPD keywords; anything else is not something lp can be given.
if is_ppd_keyword(key) && is_ppd_keyword(value) && !HANDLED_OPTIONS.contains(&key.as_str()) {
a.extend(["-o".to_string(), format!("{key}={value}")]);
}
}
a
}
/// One of the printer driver's job options (a PPD `OpenUI` entry), such as the paper tray.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PrinterOption {
/// The PPD keyword (`InputSlot`).
pub key: String,
/// What the driver calls it (`Paper tray`); the keyword when it gives no name.
pub label: String,
/// The driver's group (`Paper Handling`); empty outside a group.
pub group: String,
/// The choices: keyword and label (`Tray2`, `Tray 2`).
pub choices: Vec<(String, String)>,
/// The choice the printer uses when a job doesn't say.
pub default: String,
}
/// Options PdfCraft's Print dialog sets itself (paper, two-sided, collation), so the driver's
/// copies of them are neither shown nor sent.
pub const HANDLED_OPTIONS: &[&str] = &["PageSize", "PageRegion", "Duplex", "Collate", "ImageableArea", "PaperDimension"];
/// Caps on what a PPD may make the dialog hold (PPDs are text files from the printer vendor).
const MAX_OPTIONS: usize = 400;
const MAX_CHOICES: usize = 400;
/// A PPD keyword: printable ASCII without spaces, `/`, `:` or `=`.
fn is_ppd_keyword(s: &str) -> bool {
!s.is_empty() && s.len() <= 64 && s.bytes().all(|b| b.is_ascii_graphic() && !matches!(b, b'/' | b':' | b'=' | b'"'))
}
/// `Name/Label` → (`Name`, `Label`), the label falling back to the name.
fn name_label(s: &str) -> (&str, &str) {
match s.split_once('/') {
Some((n, l)) if !l.trim().is_empty() => (n.trim(), l.trim()),
Some((n, _)) => (n.trim(), n.trim()),
None => (s.trim(), s.trim()),
}
}
/// The job options a PPD offers, in file order, without the installable-hardware group and the
/// options PdfCraft sets itself ([`HANDLED_OPTIONS`]). Lenient: malformed lines are skipped.
pub fn parse_ppd(ppd: &str) -> Vec<PrinterOption> {
let mut out: Vec<PrinterOption> = Vec::new();
let (mut group, mut group_key) = (String::new(), String::new());
let mut open: Option<PrinterOption> = None;
for line in ppd.lines() {
let line = line.trim_end_matches('\r');
if let Some(rest) = line.strip_prefix("*OpenGroup:") {
let (k, l) = name_label(rest);
(group_key, group) = (k.to_string(), l.to_string());
} else if line.starts_with("*CloseGroup") {
(group_key, group) = (String::new(), String::new());
} else if let Some(rest) = line.strip_prefix("*OpenUI") {
let Some((head, _kind)) = rest.trim().rsplit_once(':') else { continue };
let (key, label) = name_label(head.trim().trim_start_matches('*'));
open = is_ppd_keyword(key).then(|| PrinterOption {
key: key.to_string(),
label: label.to_string(),
group: group.clone(),
choices: Vec::new(),
default: String::new(),
});
} else if line.starts_with("*CloseUI") {
if let Some(o) = open.take()
&& !o.choices.is_empty()
&& group_key != "InstallableOptions"
&& !HANDLED_OPTIONS.contains(&o.key.as_str())
&& out.len() < MAX_OPTIONS
{
out.push(o);
}
} else if let Some(o) = open.as_mut() {
let Some(rest) = line.strip_prefix('*') else { continue };
if let Some(value) = rest.strip_prefix("Default").and_then(|r| r.strip_prefix(o.key.as_str())).and_then(|r| r.strip_prefix(':')) {
o.default = value.trim().to_string();
} else if let Some(rest) = rest.strip_prefix(o.key.as_str()).and_then(|r| r.strip_prefix(' ')) {
// `*InputSlot Tray2/Tray 2: "<< ... >>"`; the code after the colon is the driver's.
let head = rest.split_once(':').map_or(rest, |(h, _)| h);
let (choice, label) = name_label(head);
if is_ppd_keyword(choice) && o.choices.len() < MAX_CHOICES && o.choices.iter().all(|(c, _)| c != choice) {
o.choices.push((choice.to_string(), label.to_string()));
}
}
}
}
for o in &mut out {
if !o.choices.iter().any(|(c, _)| *c == o.default) {
o.default = o.choices.first().map(|(c, _)| c.clone()).unwrap_or_default();
}
}
out
}
/// The current choice of each option in `lpoptions -p NAME -l` output (`Key/Label: a *b c`): the
/// queue's defaults, including the user's own (`~/.cups/lpoptions`).
pub fn parse_lpoptions(out: &str) -> Vec<(String, String)> {
out.lines()
.filter_map(|l| {
let (head, choices) = l.split_once(':')?;
let (key, _) = name_label(head);
let current = choices.split_whitespace().find_map(|c| c.strip_prefix('*'))?;
Some((key.to_string(), current.to_string()))
})
.collect()
}
/// A PPD's text: UTF-8, or ISO Latin-1 (the PPD default) when it isn't valid UTF-8.
pub fn ppd_text(bytes: &[u8]) -> String {
match std::str::from_utf8(bytes) {
Ok(s) => s.to_string(),
Err(_) => bytes.iter().map(|&b| char::from(b)).collect(),
}
}
/// The driver's job options for a printer (CUPS: its PPD, with the queue's current defaults);
/// empty when it has none or on other platforms. Reads files and runs `lpoptions`, so call it
/// when the user asks, not every frame.
pub fn printer_options(printer: &str) -> Vec<PrinterOption> {
#[cfg(all(unix, not(target_arch = "wasm32")))]
{
// Queue names never contain `/`; anything else is not a queue and not a path to follow.
if !is_ppd_keyword(printer) {
return Vec::new();
}
const PPD_CAP: u64 = 16 << 20;
let ppd = std::fs::File::open(format!("/etc/cups/ppd/{printer}.ppd")).ok().and_then(|f| {
use std::io::Read as _;
let mut bytes = Vec::new();
f.take(PPD_CAP).read_to_end(&mut bytes).ok().map(|_| bytes)
});
let mut options = ppd.map(|b| parse_ppd(&ppd_text(&b))).unwrap_or_default();
let current = std::process::Command::new("lpoptions")
.args(["-p", printer, "-l"])
.output()
.map(|o| parse_lpoptions(&String::from_utf8_lossy(&o.stdout)))
.unwrap_or_default();
for (key, value) in current {
if let Some(o) = options.iter_mut().find(|o| o.key == key)
&& o.choices.iter().any(|(c, _)| *c == value)
{
o.default = value;
}
}
options
}
#[cfg(not(all(unix, not(target_arch = "wasm32"))))]
{
let _ = printer;
Vec::new()
}
}
/// Whether [`open_printer_preferences`] can show the driver's own settings window (Windows).
pub const HAS_PRINTER_PREFERENCES: bool = cfg!(windows);
/// Open the printer driver's Printing Preferences window (Windows: `printui.dll`). What the user
/// sets there is saved as that printer's defaults for this Windows user, which every job starts
/// from; the Print dialog's copies, two-sided, grayscale and paper still apply on top.
pub fn open_printer_preferences(printer: &str) -> Result<(), PrintError> {
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
// printui takes the name in quotes; Windows printer names can't contain one anyway.
if printer.is_empty() || printer.contains('"') {
return Err(PrintError::Spool("not a printer name".into()));
}
let root = std::env::var_os("SystemRoot").unwrap_or_else(|| "C:\\Windows".into());
let rundll = std::path::Path::new(&root).join("System32").join("rundll32.exe");
std::process::Command::new(rundll)
.raw_arg("printui.dll,PrintUIEntry")
.raw_arg("/e")
.raw_arg("/n")
.raw_arg(format!("\"{printer}\""))
.spawn()
.map(|_| ())
.map_err(|e| PrintError::Spool(format!("the printer's preferences could not be opened: {e}")))
}
#[cfg(not(windows))]
{
let _ = printer;
Err(PrintError::Spool("printer preferences open only on Windows; use the printer's options instead".into()))
}
}
/// `lpstat -p -d`, forced to print untranslated messages so [`parse_lpstat`] can read them.
///
/// `LC_ALL`/`LANG=C` is enough on Linux. macOS CUPS ignores them and follows the user's
+88 -2
View File
@@ -190,8 +190,15 @@ fn spooler_arguments_and_printer_list() {
[spool::Printer { name: "Office_Laser".into(), default: true }, spool::Printer { name: "Label_Writer".into(), default: false }]
);
assert!(parse_lpstat("lpstat: No destinations added.\nno system default destination\n").is_empty());
let job =
Job { printer: Some("Office_Laser".into()), copies: 3, collate: false, duplex: Duplex::LongEdge, grayscale: true, title: "memo.pdf".into() };
let job = Job {
printer: Some("Office_Laser".into()),
copies: 3,
collate: false,
duplex: Duplex::LongEdge,
grayscale: true,
title: "memo.pdf".into(),
options: Vec::new(),
};
assert_eq!(
lp_args(&job).join(" "),
"-d Office_Laser -n 3 -t memo.pdf -o collate=false -o sides=two-sided-long-edge -o print-color-mode=monochrome -o fit-to-page=false"
@@ -359,3 +366,82 @@ fn the_spoolers_reply_drops_the_file_count_of_a_job_sent_on_stdin() {
);
assert_eq!(spool::job_message(b""), "");
}
/// A PPD shaped like a Fiery's: installable options, multi-line PostScript in the choices, Latin-1
/// labels, options the Print dialog sets itself.
const FIERY_PPD: &[u8] = b"*PPD-Adobe: \"4.3\"\n\
*LanguageEncoding: ISOLatin1\n\
*OpenGroup: InstallableOptions/Installable Options\n\
*OpenUI *EFFinisher/Finisher option: PickOne\n\
*DefaultEFFinisher: False\n\
*EFFinisher False/Not installed: \"\"\n\
*EFFinisher SingleStapler/Single stapler: \"\"\n\
*CloseUI: *EFFinisher\n\
*CloseGroup: InstallableOptions\n\
*OpenGroup: FPPaperSource/Media\n\
*OpenUI *InputSlot/Paper tray: PickOne\n\
*OrderDependency: 20.0 AnySetup *InputSlot\n\
*DefaultInputSlot: AutoSelect\n\
*InputSlot AutoSelect/Auto tray select: \"\n\
userdict /XJXEFIsetpageproperties known\n\
{ << /XJXsettrayselV2 [ 7 ] >> XJXEFIsetpageproperties } if\"\n\
*End\n\
*InputSlot ManualFeed/Bypass tray: \"\n\
{ pop 2 XJXsettrayselV2 } if\"\n\
*End\n\
*InputSlot Tray2/Tray 2: \"\"\n\
*fr.InputSlot Tray2/Bac 2: \"\"\n\
*CloseUI: *InputSlot\n\
*OpenUI *EFMediaType/Paper type: PickOne\n\
*DefaultEFMediaType: Plain\n\
*EFMediaType Plain/Plain: \"\"\n\
*EFMediaType Heavy1/Thick 1 (106\xad163 g/m\xb2): \"\"\n\
*CloseUI: *EFMediaType\n\
*CloseGroup: FPPaperSource\n\
*OpenUI *PageSize/Page size: PickOne\n\
*DefaultPageSize: A4\n\
*PageSize A4/A4: \"\"\n\
*CloseUI: *PageSize\n\
*OpenUI *EFRaster/Print queue action: PickOne\n\
*DefaultEFRaster: Bogus\n\
*EFRaster False/Print: \"\"\n\
*EFRaster Hold: \"\"\n\
*CloseUI: *EFRaster\n";
#[test]
fn printer_options_come_from_the_ppd() {
let options = spool::parse_ppd(&spool::ppd_text(FIERY_PPD));
let keys: Vec<&str> = options.iter().map(|o| o.key.as_str()).collect();
assert_eq!(keys, ["InputSlot", "EFMediaType", "EFRaster"], "no installable options, no PageSize");
let tray = &options[0];
assert_eq!((tray.label.as_str(), tray.group.as_str(), tray.default.as_str()), ("Paper tray", "Media", "AutoSelect"));
assert_eq!(
tray.choices,
[("AutoSelect".into(), "Auto tray select".into()), ("ManualFeed".into(), "Bypass tray".into()), ("Tray2".into(), "Tray 2".into())],
"PostScript lines and translations (*fr.InputSlot) are not choices"
);
assert_eq!(options[1].choices[1].1, "Thick 1 (106\u{ad}163 g/m²)", "Latin-1 labels");
// A default that isn't one of the choices falls back to the first; a choice without a label shows its keyword.
assert_eq!((options[2].default.as_str(), options[2].choices[1].1.as_str()), ("False", "Hold"));
assert!(spool::parse_ppd("*OpenUI *Broken\n*Broken A/B\n").is_empty(), "unclosed or malformed blocks are skipped");
}
#[test]
fn printer_option_defaults_and_job_arguments() {
let current =
spool::parse_lpoptions("InputSlot/Paper tray: AutoSelect *Tray2 ManualFeed\nEFRaster/Print queue action: *False Hold\nbroken line\n");
assert_eq!(current, [("InputSlot".to_string(), "Tray2".to_string()), ("EFRaster".to_string(), "False".to_string())]);
let job = Job {
options: vec![
("InputSlot".into(), "Tray2".into()),
("EFMediaType".into(), "Heavy1".into()),
("Duplex".into(), "DuplexTumble".into()),
("Bad Key".into(), "x".into()),
("EFRaster".into(), "a=b".into()),
],
..Job::default()
};
let args = lp_args(&job).join(" ");
assert!(args.ends_with("-o fit-to-page=false -o InputSlot=Tray2 -o EFMediaType=Heavy1"), "{args}");
assert!(spool::printer_options("../../etc/passwd").is_empty(), "not a queue name");
}
+6
View File
@@ -2061,3 +2061,9 @@ action wizard Start Старт
yyyy or yy year · m or mm month · mmm or mmmm month name · d or dd day · ddd or dddd weekday. H, h, M, s and t are time letters and can't be used on their own: put {escape} before a letter to show it as is. yyyy или yy година · m или mm месец · mmm или mmmm име на месеца · d или dd ден · ddd или dddd ден от седмицата. H, h, M, s и t са букви за час и не могат да се използват самостоятелно: поставете {escape} пред буква, за да се покаже както е.
Same as interface ({language}) Като интерфейса ({language})
{chars} can't be written into the PDF yet: Fill & Sign text is Western European only. Pick a numeric format such as dd.mm.yyyy. {chars} все още не може да се запише в PDF: текстът при попълване и подписване поддържа само западноевропейски знаци. Изберете числов формат, например dd.mm.yyyy.
Opens the printer driver's preferences. They are saved for every print from this computer; the copies, two-sided, grayscale and paper chosen here still apply. Отваря предпочитанията на драйвера на принтера. Те се запазват за всеки печат от този компютър; избраните тук копия, двустранен печат, сива скала и хартия продължават да важат.
The printer driver's own settings for this print: paper tray, paper type, finishing and more. Собствените настройки на драйвера на принтера за този печат: тава за хартия, вид хартия, довършителна обработка и други.
Couldn't open the printer's preferences: {e} Предпочитанията на принтера не можаха да се отворят: {e}
Printer properties Свойства на принтера
Reset to printer defaults Възстанови настройките по подразбиране на принтера
This printer offers no settings of its own. Този принтер няма собствени настройки.
Can't render this file because it contains an unexpected character in line 8 and column 16.
+6
View File
@@ -2099,3 +2099,9 @@ action wizard Start Starten
yyyy or yy year · m or mm month · mmm or mmmm month name · d or dd day · ddd or dddd weekday. H, h, M, s and t are time letters and can't be used on their own: put {escape} before a letter to show it as is. yyyy oder yy Jahr · m oder mm Monat · mmm oder mmmm Monatsname · d oder dd Tag · ddd oder dddd Wochentag. H, h, M, s und t sind Zeitbuchstaben und können nicht allein verwendet werden: Setze {escape} vor einen Buchstaben, um ihn unverändert anzuzeigen.
Same as interface ({language}) Wie die Oberfläche ({language})
{chars} can't be written into the PDF yet: Fill & Sign text is Western European only. Pick a numeric format such as dd.mm.yyyy. {chars} kann noch nicht in das PDF geschrieben werden: Text beim Ausfüllen und Unterschreiben ist auf westeuropäische Zeichen beschränkt. Wähle ein numerisches Format wie dd.mm.yyyy.
Opens the printer driver's preferences. They are saved for every print from this computer; the copies, two-sided, grayscale and paper chosen here still apply. Öffnet die Einstellungen des Druckertreibers. Sie gelten für jeden Druck von diesem Computer; die hier gewählten Kopien, Duplex, Graustufen und das Papier gelten weiterhin.
The printer driver's own settings for this print: paper tray, paper type, finishing and more. Die eigenen Einstellungen des Druckertreibers für diesen Druck: Papierfach, Papiersorte, Endverarbeitung und mehr.
Couldn't open the printer's preferences: {e} Die Druckereinstellungen konnten nicht geöffnet werden: {e}
Printer properties Druckereigenschaften
Reset to printer defaults Auf Druckerstandard zurücksetzen
This printer offers no settings of its own. Dieser Drucker bietet keine eigenen Einstellungen.
Can't render this file because it contains an unexpected character in line 6 and column 51.
+6
View File
@@ -2015,3 +2015,9 @@
yyyy or yy year · m or mm month · mmm or mmmm month name · d or dd day · ddd or dddd weekday. H, h, M, s and t are time letters and can't be used on their own: put {escape} before a letter to show it as is. yyyy o yy año · m o mm mes · mmm o mmmm nombre del mes · d o dd día · ddd o dddd día de la semana. H, h, M, s y t son letras de hora y no pueden usarse solas: pon {escape} delante de una letra para mostrarla tal cual.
Same as interface ({language}) Igual que la interfaz ({language})
{chars} can't be written into the PDF yet: Fill & Sign text is Western European only. Pick a numeric format such as dd.mm.yyyy. {chars} todavía no se puede escribir en el PDF: el texto de Rellenar y firmar solo admite caracteres de Europa occidental. Elige un formato numérico como dd.mm.yyyy.
Opens the printer driver's preferences. They are saved for every print from this computer; the copies, two-sided, grayscale and paper chosen here still apply. Abre las preferencias del controlador de la impresora. Se guardan para todas las impresiones de este equipo; las copias, la impresión a doble cara, la escala de grises y el papel elegidos aquí se siguen aplicando.
The printer driver's own settings for this print: paper tray, paper type, finishing and more. Los ajustes propios del controlador de la impresora para esta impresión: bandeja, tipo de papel, acabado y más.
Couldn't open the printer's preferences: {e} No se pudieron abrir las preferencias de la impresora: {e}
Printer properties Propiedades de la impresora
Reset to printer defaults Restablecer los valores predeterminados de la impresora
This printer offers no settings of its own. Esta impresora no ofrece ajustes propios.
Can't render this file because it contains an unexpected character in line 6 and column 60.
+6
View File
@@ -2052,3 +2052,9 @@
yyyy or yy year · m or mm month · mmm or mmmm month name · d or dd day · ddd or dddd weekday. H, h, M, s and t are time letters and can't be used on their own: put {escape} before a letter to show it as is. yyyy ou yy année · m ou mm mois · mmm ou mmmm nom du mois · d ou dd jour · ddd ou dddd jour de la semaine. H, h, M, s et t sont des lettres d'heure et ne peuvent pas être utilisées seules : placez {escape} devant une lettre pour l'afficher telle quelle.
Same as interface ({language}) Comme l'interface ({language})
{chars} can't be written into the PDF yet: Fill & Sign text is Western European only. Pick a numeric format such as dd.mm.yyyy. {chars} ne peut pas encore être écrit dans le PDF : le texte de Remplir et signer est limité aux caractères d'Europe occidentale. Choisissez un format numérique comme dd.mm.yyyy.
Opens the printer driver's preferences. They are saved for every print from this computer; the copies, two-sided, grayscale and paper chosen here still apply. Ouvre les préférences du pilote d'imprimante. Elles sont enregistrées pour toutes les impressions de cet ordinateur ; les copies, le recto verso, les niveaux de gris et le papier choisis ici s'appliquent toujours.
The printer driver's own settings for this print: paper tray, paper type, finishing and more. Les réglages propres au pilote d'imprimante pour cette impression : bac, type de papier, finition, etc.
Couldn't open the printer's preferences: {e} Impossible d'ouvrir les préférences de l'imprimante : {e}
Printer properties Propriétés de l'imprimante
Reset to printer defaults Rétablir les réglages par défaut de l'imprimante
This printer offers no settings of its own. Cette imprimante ne propose aucun réglage propre.
Can't render this file because it contains an unexpected character in line 6 and column 39.
+6
View File
@@ -2097,3 +2097,9 @@ action wizard Start Indítás
yyyy or yy year · m or mm month · mmm or mmmm month name · d or dd day · ddd or dddd weekday. H, h, M, s and t are time letters and can't be used on their own: put {escape} before a letter to show it as is. yyyy vagy yy év · m vagy mm hónap · mmm vagy mmmm a hónap neve · d vagy dd nap · ddd vagy dddd a hét napja. A H, h, M, s és t időbetűk, önmagukban nem használhatók: tegyél {escape} jelet a betű elé, hogy változatlanul jelenjen meg.
Same as interface ({language}) Mint a felület ({language})
{chars} can't be written into the PDF yet: Fill & Sign text is Western European only. Pick a numeric format such as dd.mm.yyyy. {chars} még nem írható a PDF-be: a kitöltés és aláírás szövege csak nyugat-európai karaktereket támogat. Válassz számokból álló formátumot, például dd.mm.yyyy.
Opens the printer driver's preferences. They are saved for every print from this computer; the copies, two-sided, grayscale and paper chosen here still apply. Megnyitja a nyomtató-illesztőprogram beállításait. Ezek a számítógépről indított minden nyomtatásra érvényesek; az itt választott példányszám, kétoldalas nyomtatás, szürkeárnyalat és papír továbbra is érvényes.
The printer driver's own settings for this print: paper tray, paper type, finishing and more. A nyomtató-illesztőprogram saját beállításai ehhez a nyomtatáshoz: papírtálca, papírtípus, utófeldolgozás és egyebek.
Couldn't open the printer's preferences: {e} Nem sikerült megnyitni a nyomtató beállításait: {e}
Printer properties Nyomtató tulajdonságai
Reset to printer defaults A nyomtató alapértékeinek visszaállítása
This printer offers no settings of its own. Ennek a nyomtatónak nincsenek saját beállításai.
Can't render this file because it contains an unexpected character in line 7 and column 44.
+6
View File
@@ -1991,3 +1991,9 @@
yyyy or yy year · m or mm month · mmm or mmmm month name · d or dd day · ddd or dddd weekday. H, h, M, s and t are time letters and can't be used on their own: put {escape} before a letter to show it as is. yyyy または yy 年 · m または mm 月 · mmm または mmmm 月名 · d または dd 日 · ddd または dddd 曜日。H、h、M、s、t は時刻の文字なので単独では使えません。文字をそのまま表示するには前に {escape} を付けます。
Same as interface ({language}) 表示言語と同じ({language})
{chars} can't be written into the PDF yet: Fill & Sign text is Western European only. Pick a numeric format such as dd.mm.yyyy. {chars} はまだ PDF に書き込めません。入力と署名のテキストは西ヨーロッパの文字のみに対応しています。dd.mm.yyyy などの数字の形式を選んでください。
Opens the printer driver's preferences. They are saved for every print from this computer; the copies, two-sided, grayscale and paper chosen here still apply. プリンタードライバーの基本設定を開きます。設定はこのコンピューターからのすべての印刷に保存されます。ここで選んだ部数、両面、グレースケール、用紙は引き続き適用されます。
The printer driver's own settings for this print: paper tray, paper type, finishing and more. この印刷に使うプリンタードライバー独自の設定:給紙トレイ、用紙の種類、後処理など。
Couldn't open the printer's preferences: {e} プリンターの基本設定を開けませんでした: {e}
Printer properties プリンターのプロパティ
Reset to printer defaults プリンターの初期設定に戻す
This printer offers no settings of its own. このプリンターには独自の設定がありません。
Can't render this file because it contains an unexpected character in line 6 and column 47.
+6
View File
@@ -2013,3 +2013,9 @@
yyyy or yy year · m or mm month · mmm or mmmm month name · d or dd day · ddd or dddd weekday. H, h, M, s and t are time letters and can't be used on their own: put {escape} before a letter to show it as is. yyyy или yy — год · m или mm — месяц · mmm или mmmm — название месяца · d или dd — день · ddd или dddd — день недели. H, h, M, s и t — буквы времени, их нельзя использовать отдельно: поставьте {escape} перед буквой, чтобы показать её как есть.
Same as interface ({language}) Как в интерфейсе ({language})
{chars} can't be written into the PDF yet: Fill & Sign text is Western European only. Pick a numeric format such as dd.mm.yyyy. {chars} пока нельзя записать в PDF: текст «Заполнить и подписать» поддерживает только западноевропейские символы. Выберите числовой формат, например dd.mm.yyyy.
Opens the printer driver's preferences. They are saved for every print from this computer; the copies, two-sided, grayscale and paper chosen here still apply. Открывает настройки драйвера принтера. Они сохраняются для всей печати с этого компьютера; выбранные здесь копии, двусторонняя печать, оттенки серого и бумага по-прежнему применяются.
The printer driver's own settings for this print: paper tray, paper type, finishing and more. Собственные параметры драйвера принтера для этой печати: лоток, тип бумаги, финишная обработка и другое.
Couldn't open the printer's preferences: {e} Не удалось открыть настройки принтера: {e}
Printer properties Свойства принтера
Reset to printer defaults Вернуть параметры принтера по умолчанию
This printer offers no settings of its own. У этого принтера нет собственных параметров.
Can't render this file because it contains an unexpected character in line 9 and column 16.
+6
View File
@@ -2031,3 +2031,9 @@ action wizard Start ప్రారంభించు
yyyy or yy year · m or mm month · mmm or mmmm month name · d or dd day · ddd or dddd weekday. H, h, M, s and t are time letters and can't be used on their own: put {escape} before a letter to show it as is. yyyy లేదా yy సంవత్సరం · m లేదా mm నెల · mmm లేదా mmmm నెల పేరు · d లేదా dd రోజు · ddd లేదా dddd వారం రోజు. H, h, M, s, t సమయ అక్షరాలు, వాటిని ఒంటరిగా ఉపయోగించలేరు: అక్షరాన్ని యథాతథంగా చూపడానికి దాని ముందు {escape} పెట్టండి.
Same as interface ({language}) ఇంటర్‌ఫేస్ లాగానే ({language})
{chars} can't be written into the PDF yet: Fill & Sign text is Western European only. Pick a numeric format such as dd.mm.yyyy. {chars} ను ఇంకా PDF లో రాయలేము: నింపడం & సంతకం టెక్స్ట్ పశ్చిమ యూరోపియన్ అక్షరాలకు మాత్రమే పరిమితం. dd.mm.yyyy వంటి సంఖ్యా ఫార్మాట్‌ను ఎంచుకోండి.
Opens the printer driver's preferences. They are saved for every print from this computer; the copies, two-sided, grayscale and paper chosen here still apply. ప్రింటర్ డ్రైవర్ ప్రాధాన్యతలను తెరుస్తుంది. అవి ఈ కంప్యూటర్ నుండి ప్రతి ముద్రణ కోసం సేవ్ అవుతాయి; ఇక్కడ ఎంచుకున్న కాపీలు, రెండు వైపులా, గ్రేస్కేల్ మరియు కాగితం ఇప్పటికీ వర్తిస్తాయి.
The printer driver's own settings for this print: paper tray, paper type, finishing and more. ఈ ముద్రణ కోసం ప్రింటర్ డ్రైవర్ స్వంత సెట్టింగ్‌లు: కాగితపు ట్రే, కాగితం రకం, ఫినిషింగ్ మరియు మరిన్ని.
Couldn't open the printer's preferences: {e} ప్రింటర్ ప్రాధాన్యతలను తెరవడం సాధ్యం కాలేదు: {e}
Printer properties ప్రింటర్ లక్షణాలు
Reset to printer defaults ప్రింటర్ డిఫాల్ట్‌లకు రీసెట్ చేయి
This printer offers no settings of its own. ఈ ప్రింటర్‌కు దాని స్వంత సెట్టింగ్‌లు లేవు.
Can't render this file because it contains an unexpected character in line 13 and column 16.
+6
View File
@@ -2119,3 +2119,9 @@ calendar date December грудня
yyyy or yy year · m or mm month · mmm or mmmm month name · d or dd day · ddd or dddd weekday. H, h, M, s and t are time letters and can't be used on their own: put {escape} before a letter to show it as is. yyyy або yy рік · m або mm місяць · mmm або mmmm назва місяця · d або dd день · ddd або dddd день тижня. H, h, M, s і t — літери часу, їх не можна використовувати окремо: поставте {escape} перед літерою, щоб показати її як є.
Same as interface ({language}) Як в інтерфейсі ({language})
{chars} can't be written into the PDF yet: Fill & Sign text is Western European only. Pick a numeric format such as dd.mm.yyyy. {chars} поки не можна записати в PDF: текст заповнення й підпису підтримує лише західноєвропейські символи. Виберіть числовий формат, наприклад dd.mm.yyyy.
Opens the printer driver's preferences. They are saved for every print from this computer; the copies, two-sided, grayscale and paper chosen here still apply. Відкриває налаштування драйвера принтера. Вони зберігаються для кожного друку з цього комп’ютера; вибрані тут кількість копій, двосторонній друк, відтінки сірого й папір і далі діють.
The printer driver's own settings for this print: paper tray, paper type, finishing and more. Власні налаштування драйвера принтера для цього друку: лоток, тип паперу, післядрукова обробка тощо.
Couldn't open the printer's preferences: {e} Не вдалося відкрити налаштування принтера: {e}
Printer properties Властивості принтера
Reset to printer defaults Скинути до налаштувань принтера за замовчуванням
This printer offers no settings of its own. Цей принтер не має власних налаштувань.
Can't render this file because it contains an unexpected character in line 6 and column 47.
+6
View File
@@ -2063,3 +2063,9 @@
yyyy or yy year · m or mm month · mmm or mmmm month name · d or dd day · ddd or dddd weekday. H, h, M, s and t are time letters and can't be used on their own: put {escape} before a letter to show it as is. yyyy 或 yy 年 · m 或 mm 月 · mmm 或 mmmm 月份名称 · d 或 dd 日 · ddd 或 dddd 星期。H、h、M、s 和 t 是时间字母,不能单独使用:在字母前加 {escape} 可按原样显示。
Same as interface ({language}) 与界面相同({language})
{chars} can't be written into the PDF yet: Fill & Sign text is Western European only. Pick a numeric format such as dd.mm.yyyy. 暂时无法将 {chars} 写入 PDF:填写与签名的文字仅支持西欧字符。请选择数字格式,例如 dd.mm.yyyy。
Opens the printer driver's preferences. They are saved for every print from this computer; the copies, two-sided, grayscale and paper chosen here still apply. 打开打印机驱动程序的首选项。这些设置会保存并用于此电脑上的所有打印;此处选择的份数、双面、灰度和纸张仍然适用。
The printer driver's own settings for this print: paper tray, paper type, finishing and more. 此次打印的打印机驱动程序专属设置:纸盒、纸张类型、装订整理等。
Couldn't open the printer's preferences: {e} 无法打开打印机首选项:{e}
Printer properties 打印机属性
Reset to printer defaults 恢复打印机默认设置
This printer offers no settings of its own. 此打印机没有专属设置。
Can't render this file because it contains an unexpected character in line 9 and column 16.
+6
View File
@@ -1984,3 +1984,9 @@
yyyy or yy year · m or mm month · mmm or mmmm month name · d or dd day · ddd or dddd weekday. H, h, M, s and t are time letters and can't be used on their own: put {escape} before a letter to show it as is. yyyy 或 yy 年 · m 或 mm 月 · mmm 或 mmmm 月份名稱 · d 或 dd 日 · ddd 或 dddd 星期。H、h、M、s 和 t 是時間字母,不能單獨使用:在字母前加上 {escape} 即可照原樣顯示。
Same as interface ({language}) 與介面相同({language})
{chars} can't be written into the PDF yet: Fill & Sign text is Western European only. Pick a numeric format such as dd.mm.yyyy. 暫時無法將 {chars} 寫入 PDF:填寫與簽署的文字僅支援西歐字元。請選擇數字格式,例如 dd.mm.yyyy。
Opens the printer driver's preferences. They are saved for every print from this computer; the copies, two-sided, grayscale and paper chosen here still apply. 開啟印表機驅動程式的喜好設定。這些設定會儲存並用於此電腦的所有列印;此處選擇的份數、雙面、灰階和紙張仍然適用。
The printer driver's own settings for this print: paper tray, paper type, finishing and more. 此次列印的印表機驅動程式專屬設定:紙匣、紙張類型、裝訂整理等。
Couldn't open the printer's preferences: {e} 無法開啟印表機喜好設定:{e}
Printer properties 印表機內容
Reset to printer defaults 還原印表機預設值
This printer offers no settings of its own. 此印表機沒有專屬設定。
Can't render this file because it contains an unexpected character in line 9 and column 16.
+109 -6
View File
@@ -59,6 +59,13 @@ pub struct PrintDraft {
/// The sheet shown in the preview (0-based).
pub sheet: usize,
pub current_page: usize,
/// The printer driver's own options and the printer they were read for (Properties…, CUPS).
pub driver_options: Option<(String, Vec<spool::PrinterOption>)>,
/// Driver options set away from the printer's defaults, for `driver_options`' printer.
pub driver_choices: std::collections::BTreeMap<String, String>,
pub show_driver_options: bool,
/// Why the printer's preferences window didn't open (Windows).
pub driver_error: Option<String>,
}
impl Default for PrintDraft {
@@ -92,6 +99,10 @@ impl Default for PrintDraft {
paper: 0,
sheet: 0,
current_page: 0,
driver_options: None,
driver_choices: Default::default(),
show_driver_options: false,
driver_error: None,
}
}
}
@@ -139,6 +150,11 @@ impl PrintDraft {
duplex: self.duplex,
grayscale: self.grayscale,
title: title.to_string(),
// Only for the printer the options were read from.
options: match (&self.printer, &self.driver_options) {
(Some(p), Some((read_for, _))) if p == read_for => self.driver_choices.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
_ => Vec::new(),
},
}
}
}
@@ -253,6 +269,61 @@ impl PdfCraftApp {
}
}
/// Properties… on CUPS: the driver's options for this print, grouped as the driver groups them.
/// Only choices away from the printer's defaults are kept, and sent with the job.
fn driver_options_panel(ui: &mut egui::Ui, d: &mut PrintDraft, t: &Tokens) {
let PrintDraft { driver_options: Some((_, options)), driver_choices: choices, show_driver_options: show, .. } = d else { return };
egui::Frame::new().fill(t.hover).corner_radius(egui::CornerRadius::same(6)).inner_margin(egui::Margin::same(10)).show(ui, |ui| {
ui.set_width(ui.available_width());
ui.horizontal(|ui| {
ui.label(egui::RichText::new(tl!("Printer properties")).font(theme::semibold(13.0)));
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button(tl!("Done")).clicked() {
*show = false;
}
if ui.add_enabled(!choices.is_empty(), egui::Button::new(tl!("Reset to printer defaults"))).clicked() {
choices.clear();
}
});
});
if options.is_empty() {
ui.label(egui::RichText::new(tl!("This printer offers no settings of its own.")).small().color(t.text_muted));
return;
}
egui::ScrollArea::vertical().id_salt("driver-options").max_height(240.0).show(ui, |ui| {
for (n, chunk) in options.chunk_by(|a, b| a.group == b.group).enumerate() {
let group = chunk.first().map_or("", |o| o.group.as_str());
if !group.is_empty() {
ui.add_space(4.0);
// Option and group names are the driver's own text.
ui.label(egui::RichText::new(group).small().color(t.text_muted));
}
egui::Grid::new(("driver-group", n)).num_columns(2).spacing([10.0, 4.0]).show(ui, |ui| {
for o in chunk {
ui.label(&o.label);
let current = choices.get(&o.key).cloned().unwrap_or_else(|| o.default.clone());
let label_of = |c: &str| o.choices.iter().find(|(k, _)| k == c).map_or_else(|| c.to_string(), |(_, l)| l.clone());
let mut pick = current.clone();
egui::ComboBox::from_id_salt(("driver-option", &o.key)).selected_text(label_of(&current)).width(200.0).show_ui(ui, |ui| {
for (k, l) in &o.choices {
ui.selectable_value(&mut pick, k.clone(), l.as_str());
}
});
if pick != current {
if pick == o.default {
choices.remove(&o.key);
} else {
choices.insert(o.key.clone(), pick);
}
}
ui.end_row();
}
});
}
});
});
}
fn combo<T: PartialEq + Copy>(ui: &mut egui::Ui, id: &str, value: &mut T, choices: &[(T, &str)], width: f32) {
let shown = choices.iter().find(|c| c.0 == *value).map_or("", |c| c.1);
egui::ComboBox::from_id_salt(id).selected_text(shown).width(width).show_ui(ui, |ui| {
@@ -284,14 +355,40 @@ pub(crate) fn body(
ui.set_width(470.0);
egui::Grid::new("print-top").num_columns(2).spacing([10.0, 8.0]).show(ui, |ui| {
ui.label(tl!("Printer:"));
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| {
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);
let before = d.printer.clone();
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| {
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);
}
ui.selectable_value(&mut d.printer, None, tl!("Save as PDF"));
});
let tip = if spool::HAS_PRINTER_PREFERENCES {
tl!("Opens the printer driver's preferences. They are saved for every print from this computer; the copies, two-sided, grayscale and paper chosen here still apply.")
} 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);
if properties.clicked()
&& let Some(p) = d.printer.clone()
{
if spool::HAS_PRINTER_PREFERENCES {
d.driver_error = spool::open_printer_preferences(&p).err().map(|e| e.to_string());
} else {
if d.driver_options.as_ref().is_none_or(|(read_for, _)| *read_for != p) {
d.driver_options = Some((p.clone(), spool::printer_options(&p)));
d.driver_choices.clear();
}
d.show_driver_options = !d.show_driver_options;
}
}
ui.selectable_value(&mut d.printer, None, tl!("Save as PDF"));
});
if d.printer != before {
d.show_driver_options = false;
d.driver_error = None;
}
ui.end_row();
ui.label(tl!("Copies:"));
ui.horizontal(|ui| {
@@ -321,6 +418,12 @@ pub(crate) fn body(
});
ui.end_row();
});
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));
}
if d.show_driver_options {
driver_options_panel(ui, d, t);
}
ui.add_space(6.0);
widgets::section_title(ui, tl!("Pages to Print"));
ui.horizontal(|ui| {
+38
View File
@@ -152,3 +152,41 @@ fn a_failed_save_as_pdf_keeps_the_dialog_open() {
let toast = h.state().toast.as_ref().map(|(m, _)| m.clone()).unwrap_or_default();
assert!(toast.contains("Could not save"), "the user is told why: {toast:?}");
}
/// Properties… shows the printer driver's own options (CUPS) and sends only those changed.
#[test]
fn print_dialog_shows_the_printer_driver_options() {
use pdfcraft_engine::print::spool;
let mut h = harness();
assert!(h.state_mut().execute("print.dialog"));
h.run_steps(3);
h.state_mut().print_draft.printer = None;
h.run_steps(1);
assert!(h.query_by(|n| n.label().as_deref() == Some("Properties…") && n.is_disabled()).is_some(), "Save as PDF has no driver");
let tray = spool::PrinterOption {
key: "InputSlot".into(),
label: "Paper tray".into(),
group: "Media".into(),
choices: vec![("AutoSelect".into(), "Auto tray select".into()), ("Tray2".into(), "Tray 2".into())],
default: "AutoSelect".into(),
};
{
let d = &mut h.state_mut().print_draft;
d.printer = Some("Test_Queue".into());
d.driver_options = Some(("Test_Queue".into(), vec![tray]));
d.driver_choices.insert("InputSlot".into(), "Tray2".into());
d.show_driver_options = true;
}
h.run_steps(2);
h.get_by_label("Printer properties");
h.get_by_label("Media");
h.get_by_label("Paper tray");
assert_eq!(h.state().print_draft.job("form.pdf").options, [("InputSlot".to_string(), "Tray2".to_string())]);
h.get_by_label("Reset to printer defaults").click();
h.run_steps(2);
assert!(h.state().print_draft.job("form.pdf").options.is_empty(), "defaults are not sent");
// Options read for another printer never go with this one's job.
h.state_mut().print_draft.driver_choices.insert("InputSlot".into(), "Tray2".into());
h.state_mut().print_draft.printer = Some("Other_Queue".into());
assert!(h.state().print_draft.job("form.pdf").options.is_empty());
}
+3 -3
View File
@@ -7829,9 +7829,9 @@ tier = "P0"
milestone = "M10"
status = "shipped"
commands = ["print.dialog"]
tools = ["printers", "doc_print"]
evidence = ["crates/print/src/tests.rs::spooler_arguments_and_printer_list"]
notes = "CUPS printers via lpstat; Save as PDF; Windows spooler pending"
tools = ["printers", "printer_options", "doc_print"]
evidence = ["crates/print/src/tests.rs::spooler_arguments_and_printer_list", "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 printers via lpstat; Save as PDF; Windows spooler pending. 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"