Merge pull request #4599: native Windows printer settings and RGB printing

Share native Windows driver settings and RGB printing through craft-print
This commit is contained in:
skylar taylor-barrick authored and GitHub committed 2026-10-13 22:38:25 -06:00
commit 369704ac55
29 files changed
+1742 -48

No files matched your search

Generated
+1225 -17
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -38,7 +38,7 @@ photocraft-testkit = { path = "crates/testkit" }
photocraft-tablet = { path = "crates/tablet" }
# external
craft-print = { git = "https://github.com/storytold/craft-libs", rev = "d991bf6792f8ade99c51358bf3c1d902c8cd96b5" }
craft-print = { git = "https://github.com/storytold/craft-libs", rev = "3d37da389a3618ab9b76109da11416e9ca4ea719" }
thiserror = "2"
serde = { version = "1", features = ["derive", "rc"] }
serde_json = "1"
+154 -10
View File
@@ -14,6 +14,8 @@
use std::io::Write as _;
mod windows_marks;
use photocraft_color::ColorMode;
use photocraft_doc::{Document, LayerContent, SmartSource};
use serde_json::{Value, json};
@@ -388,6 +390,21 @@ fn render_and_spool(doc: &Document, p: &Value, cmd: &str) -> Result<Value> {
let send = p.get("send").and_then(Value::as_bool).unwrap_or(output.is_none());
let copies = crate::commands::int(p, "copies").unwrap_or(1).clamp(1, 999) as u32;
let dry = p.get("dryRun").and_then(Value::as_bool).unwrap_or(false);
// Shared RGB transport handles compatible Windows pages; PDF-only content retains its route.
// Sending follows the shared command contract; fuzz tests explicitly request dryRun.
// The worker returns its result before repeat settings and hooks are applied once.
let handling = p.get("colorHandling").and_then(Value::as_str).unwrap_or("printerManages");
if cfg!(windows) && send && !dry && output.is_none() && native_rgb_compatible(&page, handling) {
require_printing_enabled()?;
let printed = print_windows(&page, p, &doc.name, copies)?;
return Ok(json!({
"pdf": Value::Null, "paper": [pw, ph], "imageRect": [x, y, w, h], "emulsionDown": emulsion_down, "scale": scale * 100.0,
"copies": copies, "command": Value::Null, "sent": true, "spooler": printed, "color": color,
}));
}
if cfg!(windows) && send && !dry && output.is_none() && printer_settings(p)?.is_some() {
return Err(other("the selected color profile or page text requires PDF printing. Save as PDF, or clear Print Settings to use PDF printing."));
}
let pdf = print_pdf(&page);
let job = craft_print::Job {
printer: p.get("printer").and_then(Value::as_str).filter(|v| !v.is_empty()).map(str::to_string),
@@ -409,13 +426,7 @@ fn render_and_spool(doc: &Document, p: &Value, cmd: &str) -> Result<Value> {
let mut sent = false;
let mut spool = Value::Null;
if send && !dry {
// The last line of defence: a test or fuzz run must never print a page (`dryRun` still
// renders and reports the command).
if printing_disabled() {
return Err(other(
"printing is disabled in this process (photocraft_engine::print_cmds::disable_printing or PHOTOCRAFT_NO_PRINT); use dryRun: true",
));
}
require_printing_enabled()?;
spool = json!(craft_print::submit(&pdf, &job).map_err(other)?);
sent = true;
}
@@ -424,6 +435,15 @@ fn render_and_spool(doc: &Document, p: &Value, cmd: &str) -> Result<Value> {
)
}
/// Keep CMYK output profiles and page text on the existing PDF composition path. GDI currently
/// draws RGB/gray and vector marks only; it must never silently omit requested page content.
fn native_rgb_compatible(page: &PrintPage, handling: &str) -> bool {
matches!(page.image.channels, 1 | 3)
&& (page.image.icc.is_none() || matches!(handling, "photocraftManages" | "photoshopManages" | "noColorManagement"))
&& page.description.as_deref().is_none_or(str::is_empty)
&& page.label.as_deref().is_none_or(str::is_empty)
}
/// Print One Copy repeats these.
fn remember_print(s: &mut Session, p: &Value) {
let mut remembered = p.clone();
@@ -436,6 +456,101 @@ fn remember_print(s: &mut Session, p: &Value) {
s.file_menu.last_print = Some(remembered);
}
/// The `printer` parameter, or the system's default printer.
fn printer_name(p: &Value) -> Result<String> {
if let Some(name) = p.get("printer").and_then(Value::as_str).filter(|v| !v.trim().is_empty()) {
return Ok(name.to_string());
}
craft_print::rgb::default_printer().map_err(other)?.ok_or_else(|| other("no printer chosen and no default printer is set"))
}
/// The driver settings chosen with Print Settings… (`printerSettings`, hex), when any.
fn printer_settings(p: &Value) -> Result<Option<craft_print::rgb::DevMode>> {
match p.get("printerSettings").and_then(Value::as_str).filter(|v| !v.is_empty()) {
Some(hex) => craft_print::rgb::DevMode::from_hex(hex).map(Some).map_err(|e| bad("file.print", e.to_string())),
None => Ok(None),
}
}
/// The page's image as 8-bit RGB rows (gray expanded), mirrored for Emulsion Down.
pub fn page_rgb(page: &PrintPage) -> Result<Vec<u8>> {
let img = &page.image;
let (w, h) = (img.width as usize, img.height as usize);
let mut rgb = match img.channels {
3 => img.data.clone(),
1 => img.data.iter().flat_map(|g| [*g, *g, *g]).collect(),
_ => return Err(other("this printer profile is CMYK; Windows printers take RGB, so choose the printer's own (RGB) profile")),
};
let row = w.checked_mul(3).ok_or_else(|| other("print image is too large"))?;
if row == 0 || row.checked_mul(h) != Some(rgb.len()) {
return Err(other("nothing to print"));
}
if page.emulsion_down {
for row in rgb.chunks_exact_mut(row) {
row.as_chunks_mut::<3>().0.reverse();
}
}
Ok(rgb)
}
/// The image box with its origin at the sheet's top-left (GDI's), mirrored for Emulsion Down.
pub fn page_rect_top_left(page: &PrintPage) -> (f64, f64, f64, f64) {
let (pw, ph) = page.paper;
let (x, y, w, h) = page.rect;
let x = if page.emulsion_down { pw - x - w } else { x };
(x, ph - y - h, w, h)
}
/// Prints `page` on a Windows printer. Corner/centre crop and registration marks are drawn with
/// GDI vector calls, matching the PDF path's geometry; description and label text are still
/// PDF-only (no font/text layout in the GDI path yet).
fn print_windows(page: &PrintPage, p: &Value, title: &str, copies: u32) -> Result<Value> {
let printer = printer_name(p)?;
let settings = printer_settings(p)?;
let device_page = craft_print::rgb::page(&printer, settings.as_ref()).map_err(other)?;
if (device_page.paper.0 - page.paper.0).abs() > 2.0 || (device_page.paper.1 - page.paper.1).abs() > 2.0 {
return Err(other("the driver paper or orientation differs from the print layout; choose the matching paper in Print Settings before printing"));
}
let rgb = page_rgb(page)?;
let image = craft_print::rgb::RgbImage { width: page.image.width, height: page.image.height, data: &rgb };
let rect = page_rect_top_left(page);
let primitives = windows_marks::primitives(page.marks, rect);
let color_policy = match p.get("colorHandling").and_then(Value::as_str) {
Some("photocraftManages" | "photoshopManages") => craft_print::rgb::ColorPolicy::ApplicationManaged,
Some("noColorManagement") => craft_print::rgb::ColorPolicy::Unmanaged,
_ => craft_print::rgb::ColorPolicy::DriverManaged,
};
let job = craft_print::rgb::Job {
printer: &printer,
settings: settings.as_ref(),
title,
output_file: None,
copies: copies.clamp(1, 999),
color_policy,
primitives: &primitives,
};
let done = craft_print::rgb::print(&job, &image, rect).map_err(other)?;
Ok(
json!({"printer": printer, "dpi": [done.dpi.0, done.dpi.1], "deviceRect": [done.device_rect.0, done.device_rect.1, done.device_rect.2, done.device_rect.3]}),
)
}
fn printers(_: &mut Session, _: &Value) -> Result<Value> {
match craft_print::rgb::printers() {
Ok(list) => Ok(json!({"supported": true, "printers": list.iter().map(|p| json!({"name": p.name, "default": p.default})).collect::<Vec<_>>()})),
Err(craft_print::rgb::PrintError::Unsupported) => Ok(json!({"supported": false, "printers": []})),
Err(e) => Err(other(e)),
}
}
fn printer_page(_: &mut Session, p: &Value) -> Result<Value> {
let printer = printer_name(p)?;
let pg = craft_print::rgb::page(&printer, printer_settings(p)?.as_ref()).map_err(other)?;
Ok(
json!({"printer": printer, "paper": [pg.paper.0, pg.paper.1], "printable": [pg.printable.0, pg.printable.1, pg.printable.2, pg.printable.3], "dpi": [pg.dpi.0, pg.dpi.1]}),
)
}
/// Set once by test harnesses (the panic hunt) so no command in the process can reach a printer.
static PRINTING_DISABLED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
@@ -450,9 +565,17 @@ pub fn printing_disabled() -> bool {
PRINTING_DISABLED.load(std::sync::atomic::Ordering::Relaxed) || std::env::var_os("PHOTOCRAFT_NO_PRINT").is_some_and(|v| !v.is_empty() && v != "0")
}
fn require_printing_enabled() -> Result<()> {
if printing_disabled() {
Err(other("printing is disabled in this process (photocraft_engine::print_cmds::disable_printing or PHOTOCRAFT_NO_PRINT); use dryRun: true"))
} else {
Ok(())
}
}
/// The printers the system spooler knows (Print's Printer menu): CUPS queues and driverless
/// destinations on macOS and Linux, `Win32_Printer` on Windows, none in the browser.
fn printers(_: &mut Session, _: &Value) -> Result<Value> {
fn system_printers(_: &mut Session, _: &Value) -> Result<Value> {
let list = craft_print::printers();
let default = list.iter().find(|p| p.default).map(|p| p.name.clone());
let printers: Vec<Value> = list.into_iter().map(|p| json!({"name": p.name, "default": p.default})).collect();
@@ -627,9 +750,30 @@ pub fn specs() -> Vec<CommandSpec> {
CommandSpec { id: $id, label: $label, menu: $menu, shortcut: $sc, params: $params, enabled: $enabled, journal: true, run: $run }
};
}
const PRINT_PARAMS: &str = r##"{"printer":name? (default printer),"copies":1..999=1,"paper":"letter|legal|tabloid|a3|a4|a5|4x6|5x7"|[w,h] pt="letter","orientation":"portrait|landscape"="portrait","center":bool=true,"top":in?,"left":in?,"scale":%=100,"scaleToFit":bool=false,"colorHandling":"printerManages|photocraftManages|noColorManagement"="printerManages","printerProfile":profile? (photocraftManages),"intent":"perceptual|relative|saturation|absolute"="relative","bpc":bool=true,"cornerCropMarks":bool,"centerCropMarks":bool,"registrationMarks":bool,"description":bool,"labels":bool,"emulsionDown":bool=false (mirror the page left to right, for sublimation and other transfer printing),"output":pdf path? (print to PDF; then "send" defaults to false),"send":bool?,"dryRun":bool=false (render the PDF, report the backend command, don't spool; write a PDF only at an explicit output path)} → {pdf:path|null (null without explicit output), imageRect, command:diagnostic array|null, sent}"##;
const PRINT_PARAMS: &str = r##"{"printerSettings":hex? (Windows driver settings),"printer":name? (default printer),"copies":1..999=1,"paper":"letter|legal|tabloid|a3|a4|a5|4x6|5x7"|[w,h] pt="letter","orientation":"portrait|landscape"="portrait","center":bool=true,"top":in?,"left":in?,"scale":%=100,"scaleToFit":bool=false,"colorHandling":"printerManages|photocraftManages|noColorManagement"="printerManages","printerProfile":profile? (photocraftManages),"intent":"perceptual|relative|saturation|absolute"="relative","bpc":bool=true,"cornerCropMarks":bool,"centerCropMarks":bool,"registrationMarks":bool,"description":bool,"labels":bool,"emulsionDown":bool=false (mirror the page left to right, for sublimation and other transfer printing),"output":pdf path? (print to PDF; then "send" defaults to false),"send":bool?,"dryRun":bool=false (render the PDF, report the backend command, don't spool; write a PDF only at an explicit output path)} → {pdf:path|null (null without explicit output), imageRect, command:diagnostic array|null, sent}"##;
vec![
spec!("file.print", "Print…", &["File"], Some("Cmd+P"), PRINT_PARAMS, native_doc, print),
CommandSpec {
id: "file.print.printers",
label: "Printers",
menu: &[],
shortcut: None,
params: r##"{} → {supported, printers: [{name, default}]} (supported: Windows; elsewhere Print spools with lp)"##,
enabled: |_| Ok(()),
journal: false,
run: printers,
},
CommandSpec {
id: "file.print.page",
label: "Printer Page",
menu: &[],
shortcut: None,
params: r##"{"printer":name? (default printer),"printerSettings":hex?} → {printer, paper: [w,h] pt, printable: [x,y,w,h] pt from the top-left, dpi: [x,y]} (Windows)"##,
enabled: |_| Ok(()),
journal: false,
run: printer_page,
},
CommandSpec {
id: "file.printers",
label: "Printers",
@@ -638,7 +782,7 @@ pub fn specs() -> Vec<CommandSpec> {
params: r##"{} → {printers:[{name, default}], default:name|null} (the system spooler's printers; a name is Print's "printer")"##,
enabled: crate::commands::always,
journal: false,
run: printers,
run: system_printers,
},
spec!(
"file.printOneCopy",
+56 -2
View File
@@ -52,7 +52,7 @@ fn print_dry_run_renders_a_pdf_and_reports_the_platform_backend() {
assert!(!cmd.contains(&out), "PDF bytes are streamed instead of passing a path");
}
#[cfg(windows)]
assert!(cmd.last().unwrap().contains("craft-print PDF spooler"));
assert_eq!(cmd.first().map(String::as_str), Some("xps-print"));
assert_eq!(r["sent"], false);
assert_eq!(r["pdf"], out);
let pdf = std::fs::read(&out).unwrap();
@@ -151,6 +151,60 @@ fn emulsion_down_mirrors_the_whole_page() {
let _ = std::fs::remove_dir_all(&dir);
}
fn page_of(channels: usize, data: Vec<u8>, emulsion_down: bool) -> PrintPage {
PrintPage {
paper: (600.0, 800.0),
image: PrintImage { width: 2, height: 1, channels, data, icc: None },
rect: (100.0, 200.0, 144.0, 72.0),
marks: Marks::default(),
description: None,
label: None,
emulsion_down,
}
}
#[test]
fn windows_print_image_is_rgb_mirrored_for_emulsion_down() {
assert_eq!(page_rgb(&page_of(3, vec![1, 2, 3, 4, 5, 6], false)).unwrap(), [1, 2, 3, 4, 5, 6]);
assert_eq!(page_rgb(&page_of(3, vec![1, 2, 3, 4, 5, 6], true)).unwrap(), [4, 5, 6, 1, 2, 3]);
assert_eq!(page_rgb(&page_of(1, vec![7, 9], true)).unwrap(), [9, 9, 9, 7, 7, 7]);
assert!(page_rgb(&page_of(4, vec![0; 8], false)).is_err(), "CMYK can't go to a GDI printer");
assert!(page_rgb(&page_of(3, vec![0; 5], false)).is_err(), "short data");
// GDI's origin is the sheet's top-left; Emulsion Down moves the box to the mirrored side.
assert_eq!(page_rect_top_left(&page_of(3, vec![0; 6], false)), (100.0, 528.0, 144.0, 72.0));
assert_eq!(page_rect_top_left(&page_of(3, vec![0; 6], true)), (356.0, 528.0, 144.0, 72.0));
}
#[test]
fn native_rgb_route_preserves_pdf_only_color_and_page_content() {
let mut page = page_of(3, vec![0; 6], false);
assert!(native_rgb_compatible(&page, "printerManages"));
page.label = Some("Print Me".into());
assert!(!native_rgb_compatible(&page, "printerManages"));
page.label = None;
page.description = Some("Proof description".into());
assert!(!native_rgb_compatible(&page, "printerManages"));
page.description = Some(String::new());
assert!(native_rgb_compatible(&page, "printerManages"));
page.image.icc = Some(vec![1, 2, 3]);
assert!(!native_rgb_compatible(&page, "printerManages"), "preserve embedded source ICC in the PDF route");
assert!(native_rgb_compatible(&page, "photocraftManages"));
assert!(native_rgb_compatible(&page, "photoshopManages"));
assert!(!native_rgb_compatible(&page_of(4, vec![0; 8], false), "photocraftManages"));
assert!(native_rgb_compatible(&page_of(1, vec![0; 2], false), "noColorManagement"));
}
#[test]
fn printer_commands_answer_without_a_document_and_reject_bad_settings() {
let mut s = Session::new();
let r = s.execute("file.print.printers", json!({})).unwrap();
assert_eq!(r["supported"], cfg!(windows));
assert!(r["printers"].is_array());
assert!(s.execute("file.print.page", json!({"printer": "Anything", "printerSettings": "zz"})).is_err());
let mut s = session("rgb", 8);
assert!(s.execute("file.print", json!({"printer": "Anything", "printerSettings": "0102", "send": true})).is_err());
}
#[test]
fn printer_profile_can_be_an_installed_icc_file() {
let dir = tmp("icc");
@@ -183,7 +237,7 @@ fn print_one_copy_repeats_the_last_settings() {
#[cfg(unix)]
assert_eq!(&cmd[..3], &["lp", "-n", "1"]);
#[cfg(windows)]
assert!(cmd.last().unwrap().contains("craft-print PDF spooler"));
assert_eq!(cmd.first().map(String::as_str), Some("xps-print"));
assert!(r["pdf"].is_null());
}
@@ -0,0 +1,47 @@
//! App-owned print-mark composition in points from the sheet's top-left.
use super::Marks;
use craft_print::rgb::{Primitive, Shape};
pub(super) fn primitives(marks: Marks, (x, y, w, h): (f64, f64, f64, f64)) -> Vec<Primitive> {
let mut out = Vec::with_capacity(24);
let line = |out: &mut Vec<Primitive>, x0, y0, x1, y1| {
out.push(Primitive { shape: Shape::Line { from: (x0, y0), to: (x1, y1) }, width: 0.3, color: [0; 3] });
};
let (gap, len) = (6.0, 18.0);
if marks.corner_crop {
for (cx, cy, dx, dy) in [(x, y, -1.0, -1.0), (x + w, y, 1.0, -1.0), (x, y + h, -1.0, 1.0), (x + w, y + h, 1.0, 1.0)] {
line(&mut out, cx + dx * gap, cy, cx + dx * (gap + len), cy);
line(&mut out, cx, cy + dy * gap, cx, cy + dy * (gap + len));
}
}
if marks.center_crop {
let (mx, my) = (x + w / 2.0, y + h / 2.0);
line(&mut out, mx, y - gap, mx, y - gap - len);
line(&mut out, mx, y + h + gap, mx, y + h + gap + len);
line(&mut out, x - gap, my, x - gap - len, my);
line(&mut out, x + w + gap, my, x + w + gap + len, my);
}
if marks.registration {
let (r, off) = (5.0, gap + len / 2.0);
for (cx, cy) in [(x + w / 2.0, y - off), (x + w / 2.0, y + h + off), (x - off, y + h / 2.0), (x + w + off, y + h / 2.0)] {
out.push(Primitive { shape: Shape::Ellipse { bounds: (cx - r, cy - r, 2.0 * r, 2.0 * r) }, width: 0.3, color: [0; 3] });
line(&mut out, cx - r * 1.6, cy, cx + r * 1.6, cy);
line(&mut out, cx, cy - r * 1.6, cx, cy + r * 1.6);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn marks_surround_the_final_image_box_in_physical_points() {
let out = primitives(Marks { corner_crop: true, center_crop: true, registration: true }, (100.0, 200.0, 144.0, 72.0));
assert_eq!(out.len(), 24);
assert_eq!(out[0].shape, Shape::Line { from: (94.0, 200.0), to: (76.0, 200.0) });
assert_eq!(out[12].shape, Shape::Ellipse { bounds: (167.0, 180.0, 10.0, 10.0) });
assert!(out.iter().all(|p| p.width == 0.3 && p.color == [0; 3]));
assert!(primitives(Marks::default(), (0.0, 0.0, 10.0, 10.0)).is_empty());
}
}
+10 -5
View File
@@ -60,13 +60,18 @@ fn no_command_panics_or_hangs_on_adversarial_params() {
json!({"x": 1e5, "y": -1e5, "radius": 300, "amount": 300, "angle": 1e5, "opacity": -100, "scale": -5, "gamma": -1}),
json!({"points": [], "from": [0, 0], "to": [0, 0], "stops": [], "matrix": [0, 0, 0, 0, 0, 0], "colors": [], "name": "", "mode": "", "style": ""}),
];
// Commands whose success reaches outside the process: `file.print` and `file.printOneCopy`
// spool to CUPS, so a fuzz run with `{}` printed a blank page on the default printer. Their
// params are validated like everyone else's; the spooling itself is not fuzzed.
const EXTERNAL: &[&str] = &["file.print", "file.printOneCopy"];
let mut bad: Vec<String> = Vec::new();
for spec in command_specs().iter().filter(|spec| !EXTERNAL.contains(&spec.id)) {
for spec in command_specs() {
for p in &params {
// Print and Print One Copy reach real printers (lp, or the Windows driver): fuzz them
// as dry runs so the hunt never prints a page.
let mut p = p.clone();
if spec.id.starts_with("file.print")
&& let Some(o) = p.as_object_mut()
{
o.insert("dryRun".into(), json!(true));
}
let p = &p;
if let Err(why) = run_guarded(spec.id, p) {
bad.push(format!("{} [{why}] <- {p}", spec.id));
break;
+1
View File
@@ -8,6 +8,7 @@ license.workspace = true
workspace = true
[dependencies]
craft-print = { workspace = true }
craft-ui = { workspace = true }
photocraft-geom = { workspace = true }
photocraft-color = { workspace = true }
+106 -13
View File
@@ -726,7 +726,7 @@ pub fn open_print(app: &mut PhotocraftApp) -> u64 {
f.insert("__label".into(), json!("PhotoCraft Print Settings"));
// Asked once per opening: the Printer menu lists what the spooler knows now.
let printers = app.session.execute("file.printers", json!({})).unwrap_or_default();
f.insert("__printers".into(), printers["printers"].clone());
f.insert("__systemPrinters".into(), printers["printers"].clone());
if let Some(Value::Object(m)) = app.session.file_menu.last_print.clone() {
f.extend(m);
}
@@ -755,9 +755,35 @@ pub fn open_print(app: &mut PhotocraftApp) -> u64 {
] {
f.entry(k.to_string()).or_insert(v);
}
// Windows: the system's printers, and the paper from the chosen printer's driver settings.
if let Ok(list) = craft_print::rgb::printers() {
f.insert("__printers".into(), json!(list.iter().map(|p| p.name.clone()).collect::<Vec<_>>()));
let known = |n: &str| list.iter().any(|p| p.name == n);
if !known(&s(&f, "printer", "")) {
let fallback = list.iter().find(|p| p.default).or(list.first()).map(|p| p.name.clone()).unwrap_or_default();
f.insert("printer".into(), json!(fallback));
f.remove("printerSettings");
}
}
app.ui.open_dialog(DialogKind::Command, f)
}
/// Takes the paper from the printer's driver settings (Windows), as Photoshop does.
fn driver_page(f: &mut Map<String, Value>) {
let printer = s(f, "printer", "");
let settings = f.get("printerSettings").and_then(Value::as_str).and_then(|h| craft_print::rgb::DevMode::from_hex(h).ok());
match craft_print::rgb::page(&printer, settings.as_ref()) {
Ok(pg) => {
f.insert("paper".into(), json!([pg.paper.0, pg.paper.1]));
f.insert("orientation".into(), json!("portrait"));
f.remove("__pageError");
}
Err(e) => {
f.insert("__pageError".into(), json!(e.to_string()));
}
}
}
/// Points per unit for the Position and Size fields.
fn unit_pt(units: &str) -> f64 {
match units {
@@ -839,6 +865,22 @@ fn print_paper_controls(ui: &mut egui::Ui, f: &mut Map<String, Value>) {
fn print_body(app: &mut PhotocraftApp, ui: &mut egui::Ui, f: &mut Map<String, Value>) {
let t = Tokens::get(ui.ctx());
let Some(doc) = app.session.active().map(|d| d.doc.clone()) else { return };
// PDF exports and previews retain editable paper on Windows too. Only a real native
// submission takes its media from the printer; entering that mode refreshes the driver
// geometry after any export-only paper edits.
let printers: Option<Vec<String>> = if !b(f, "dryRun", false) && s(f, "output", "").is_empty() {
f.get("__printers").and_then(Value::as_array).map(|a| a.iter().filter_map(|v| v.as_str().map(str::to_string)).collect())
} else {
None
};
if printers.is_some() {
if !b(f, "__driverPageInitialized", false) {
driver_page(f);
f.insert("__driverPageInitialized".into(), json!(true));
}
} else {
f.remove("__driverPageInitialized");
}
let p = params(f);
ui.horizontal_top(|ui| {
// Page preview.
@@ -915,17 +957,57 @@ fn print_body(app: &mut PhotocraftApp, ui: &mut egui::Ui, f: &mut Map<String, Va
ui.label(egui::RichText::new(s).font(crate::theme::semibold(12.0)).color(t.text));
};
head(ui, tl!("Printer Setup"));
ui.horizontal(|ui| {
ui.label(egui::RichText::new(tl!("Printer")).color(t.text_dim));
let mut pr = s(f, "printer", "");
let opts = printer_options(f.get("__printers"), &pr);
let o: Vec<(String, &str)> = opts.iter().map(|(k, l)| (k.clone(), l.as_str())).collect();
if crate::widgets::dropdown(ui, "print-printer", &mut pr, &o, 240.0) {
f.insert("printer".into(), json!(pr));
if let Some(printers) = &printers {
// Windows: Photoshop's Printer menu and Print Settings… (the driver's own dialog).
ui.horizontal(|ui| {
ui.label(egui::RichText::new(tl!("Printer")).color(t.text_dim));
let before = s(f, "printer", "");
let opts: Vec<(&str, &str)> = printers.iter().map(|n| (n.as_str(), n.as_str())).collect();
dropdown_str(ui, "print-printer", f, "printer", &opts, 220.0);
if s(f, "printer", "") != before {
f.remove("printerSettings");
driver_page(f);
}
});
ui.horizontal(|ui| {
number(ui, f, "copies", tl!("Copies"), 1.0..=999.0, "", 1.0);
if ui.button(tl!("Print Settings…")).clicked() {
let printer = s(f, "printer", "");
let current = f.get("printerSettings").and_then(Value::as_str).and_then(|h| craft_print::rgb::DevMode::from_hex(h).ok());
match craft_print::rgb::settings_dialog(&printer, current.as_ref(), None) {
Ok(Some(dm)) => {
f.insert("printerSettings".into(), json!(dm.to_hex()));
driver_page(f);
}
Ok(None) => {}
Err(e) => {
f.insert("__pageError".into(), json!(e.to_string()));
}
}
}
});
let paper = p.get("paper").and_then(Value::as_array).map(|a| (a.first().and_then(Value::as_f64), a.get(1).and_then(Value::as_f64)));
let text = match paper {
Some((Some(w), Some(h))) => format!("{:.0} × {:.0} mm · {}", w / 72.0 * 25.4, h / 72.0 * 25.4, tl!("set in Print Settings…")),
_ => String::new(),
};
ui.label(egui::RichText::new(text).color(t.text_dim).size(11.0));
if let Some(e) = f.get("__pageError").and_then(Value::as_str) {
ui.label(egui::RichText::new(e).color(t.warning).size(11.0));
}
});
number(ui, f, "copies", tl!("Copies"), 1.0..=999.0, "", 1.0);
print_paper_controls(ui, f);
} else {
ui.horizontal(|ui| {
ui.label(egui::RichText::new(tl!("Printer")).color(t.text_dim));
let mut pr = s(f, "printer", "");
let opts = printer_options(f.get("__systemPrinters"), &pr);
let o: Vec<(String, &str)> = opts.iter().map(|(k, l)| (k.clone(), l.as_str())).collect();
if crate::widgets::dropdown(ui, "print-printer", &mut pr, &o, 240.0) {
f.insert("printer".into(), json!(pr));
}
});
number(ui, f, "copies", tl!("Copies"), 1.0..=999.0, "", 1.0);
print_paper_controls(ui, f);
}
head(ui, tl!("Color Management"));
dropdown_str(
ui,
@@ -977,6 +1059,11 @@ fn print_body(app: &mut PhotocraftApp, ui: &mut egui::Ui, f: &mut Map<String, Va
);
check(ui, f, "bpc", tl!("Black Point Compensation"), true);
});
if printers.is_some() {
ui.label(
egui::RichText::new(tl!("Remember to turn off the printer's own colour management in Print Settings…")).color(t.warning).size(11.0),
);
}
}
head(ui, tl!("Position and Size"));
dropdown_str(ui, "print-units", f, "units", &[("cm", tl!("Centimeters")), ("mm", tl!("Millimeters")), ("in", tl!("Inches"))], 130.0);
@@ -1040,6 +1127,9 @@ fn print_body(app: &mut PhotocraftApp, ui: &mut egui::Ui, f: &mut Map<String, Va
});
head(ui, tl!("Functions"));
check(ui, f, "emulsionDown", tl!("Emulsion Down"), false);
if b(f, "emulsionDown", false) && printers.is_some() {
ui.label(egui::RichText::new(tl!("Don't also tick Mirror Image in Print Settings…: the two flips cancel out.")).color(t.text_dim).size(11.0));
}
head(ui, tl!("Save as PDF"));
let mut out = s(f, "output", "");
if ui.add(egui::TextEdit::singleline(&mut out).hint_text(tl!("(print to the printer)")).desired_width(300.0)).changed() {
@@ -1058,7 +1148,9 @@ fn print_confirm(app: &mut PhotocraftApp, f: &Map<String, Value>) -> Result<Valu
}
pub(crate) fn print_status(app: &mut PhotocraftApp, r: &Value) {
app.ui.status = if r["sent"] == json!(true) {
app.ui.status = if let Some(printer) = r["spooler"]["printer"].as_str() {
crate::i18n::fmt(tl!("Sent to {printer}"), &[("printer", printer)])
} else if r["sent"] == json!(true) {
let template = "Sent to the printer ({spooler})";
crate::i18n::fmt(tl!(template), &[("spooler", r["spooler"].as_str().unwrap_or(""))])
} else {
@@ -1353,7 +1445,8 @@ mod tests {
let (mut app, ctx) = app();
let id = crate::menus::invoke(&mut app, &ctx, "file.print", json!({})).unwrap()["dialog"].as_u64().unwrap();
let f = &app.ui.dialog_mut(id).unwrap().fields;
assert!(f.get("__printers").is_some_and(Value::is_array));
assert!(f.get("__systemPrinters").is_some_and(Value::is_array));
assert!(params(f).get("__systemPrinters").is_none());
assert!(params(f).get("__printers").is_none());
}
+7
View File
@@ -3744,4 +3744,11 @@ navigator Zoom Zvětšení
Step {step} is waiting for its dialog Krok {step} čeká na svůj dialog
@plural {n} action|{n} actions {n} akce|{n} akce|{n} akcí
Auto Add/Delete Automaticky přidat/odstranit
# Native Windows printing qualification
Don't also tick Mirror Image in Print Settings…: the two flips cancel out. Nezapínejte současně zrcadlení v nastavení tisku: obě převrácení se navzájem zruší.
Print Settings… Nastavení tisku…
Remember to turn off the printer's own colour management in Print Settings… Nezapomeňte v nastavení tisku vypnout vlastní správu barev tiskárny…
Sent to {printer} Odesláno do {printer}
set in Print Settings… nastavuje se v nastavení tisku…
ICO is limited to 256 × 256 pixels ICO je omezen na 256 × 256 pixelů
Can't render this file because it contains an unexpected character in line 6 and column 47.
+7
View File
@@ -3134,4 +3134,11 @@ curves Smooth Glätten
Step {step} is waiting for its dialog Schritt {step} wartet auf seinen Dialog
@plural {n} action|{n} actions {n} Aktion|{n} Aktionen
Auto Add/Delete Autom. hinzuf./löschen
# Native Windows printing qualification
Don't also tick Mirror Image in Print Settings…: the two flips cancel out. Aktiviere nicht zusätzlich das Spiegelbild in den Druckeinstellungen: Die beiden Spiegelungen heben sich auf.
Print Settings… Druckeinstellungen…
Remember to turn off the printer's own colour management in Print Settings… Denke daran, die eigene Farbverwaltung des Druckers in den Druckeinstellungen auszuschalten…
Sent to {printer} An {printer} gesendet
set in Print Settings… in den Druckeinstellungen festlegen…
ICO is limited to 256 × 256 pixels ICO ist auf 256 × 256 Pixel begrenzt
Can't render this file because it contains an unexpected character in line 6 and column 47.
+7
View File
@@ -3834,4 +3834,11 @@ curves Smooth Εξομάλυνση
Step {step} is waiting for its dialog Το βήμα {step} περιμένει το παράθυρο διαλόγου του
@plural {n} action|{n} actions {n} ενέργεια|{n} ενέργειες
Auto Add/Delete Αυτόματη προσθήκη/διαγραφή
# Native Windows printing qualification
Don't also tick Mirror Image in Print Settings…: the two flips cancel out. Μην ενεργοποιείτε επίσης τον κατοπτρισμό στις ρυθμίσεις εκτύπωσης: οι δύο αναστροφές αλληλοαναιρούνται.
Print Settings… Ρυθμίσεις εκτύπωσης…
Remember to turn off the printer's own colour management in Print Settings… Θυμηθείτε να απενεργοποιήσετε τη διαχείριση χρωμάτων του εκτυπωτή στις ρυθμίσεις εκτύπωσης…
Sent to {printer} Στάλθηκε στον {printer}
set in Print Settings… ορίζεται στις ρυθμίσεις εκτύπωσης…
ICO is limited to 256 × 256 pixels Το ICO περιορίζεται σε 256 × 256 εικονοστοιχεία
Can't render this file because it contains an unexpected character in line 1029 and column 18.
+7
View File
@@ -2821,4 +2821,11 @@ curves Smooth Suavizar
Step {step} is waiting for its dialog El paso {step} está esperando su cuadro de diálogo
@plural {n} action|{n} actions {n} acción|{n} acciones
Auto Add/Delete Añadir/eliminar automáticamente
# Native Windows printing qualification
Don't also tick Mirror Image in Print Settings…: the two flips cancel out. No actives también Imagen especular en Ajustes de impresión: las dos inversiones se anulan.
Print Settings… Ajustes de impresión…
Remember to turn off the printer's own colour management in Print Settings… Recuerda desactivar la gestión de color de la impresora en Ajustes de impresión…
Sent to {printer} Enviado a {printer}
set in Print Settings… se establece en Ajustes de impresión…
ICO is limited to 256 × 256 pixels ICO está limitado a 256 × 256 píxeles
Can't render this file because it contains an unexpected character in line 4 and column 47.
+7
View File
@@ -3138,4 +3138,11 @@ curves Smooth Lisser
Step {step} is waiting for its dialog L’étape {step} attend sa boîte de dialogue
@plural {n} action|{n} actions {n} action|{n} actions
Auto Add/Delete Ajout/suppr. auto
# Native Windows printing qualification
Don't also tick Mirror Image in Print Settings…: the two flips cancel out. Ne cochez pas aussi Image miroir dans les paramètres d’impression : les deux inversions s’annulent.
Print Settings… Paramètres d’impression…
Remember to turn off the printer's own colour management in Print Settings… Pensez à désactiver la gestion des couleurs de l’imprimante dans les paramètres d’impression…
Sent to {printer} Envoyé à {printer}
set in Print Settings… défini dans les paramètres d’impression…
ICO is limited to 256 × 256 pixels Le format ICO est limité à 256 × 256 pixels
Can't render this file because it contains an unexpected character in line 6 and column 47.
+7
View File
@@ -4050,6 +4050,13 @@ cameraRaw Yellow Sárga
Loaded Camera Raw settings from {file} Camera Raw-beállítások betöltve innen: {file}
Loaded Camera Raw settings from {file} ({n} not supported) Camera Raw-beállítások betöltve innen: {file} ({n} nem támogatott)
Auto Add/Delete Automatikus hozzáadás/törlés
# Native Windows printing qualification
Don't also tick Mirror Image in Print Settings…: the two flips cancel out. A nyomtatási beállításokban ne jelöld be a tükrözést is: a két tükrözés kioltja egymást.
Print Settings… Nyomtatási beállítások…
Remember to turn off the printer's own colour management in Print Settings… Ne felejtsd el kikapcsolni a nyomtató saját színkezelését a nyomtatási beállításokban…
Sent to {printer} Elküldve ide: {printer}
set in Print Settings… a nyomtatási beállításokban adható meg…
ICO is limited to 256 × 256 pixels Az ICO legfeljebb 256 × 256 képpont lehet
# Channel Options and New Spot Channel (#3761)
Can't render this file because it contains an unexpected character in line 6 and column 47.
+7
View File
@@ -3125,4 +3125,11 @@ curves Smooth Haluskan
Step {step} is waiting for its dialog Langkah {step} menunggu dialognya
@plural {n} action|{n} actions {n} tindakan
Auto Add/Delete Tambah/Hapus Otomatis
# Native Windows printing qualification
Don't also tick Mirror Image in Print Settings…: the two flips cancel out. Jangan aktifkan juga Gambar Cermin di Pengaturan Cetak: kedua pembalikan saling meniadakan.
Print Settings… Pengaturan Cetak…
Remember to turn off the printer's own colour management in Print Settings… Ingat untuk menonaktifkan pengelolaan warna milik printer di Pengaturan Cetak…
Sent to {printer} Dikirim ke {printer}
set in Print Settings… diatur di Pengaturan Cetak…
ICO is limited to 256 × 256 pixels ICO dibatasi hingga 256 × 256 piksel
Can't render this file because it contains an unexpected character in line 4 and column 47.
+7
View File
@@ -4099,4 +4099,11 @@ navigator Zoom Zoom
Step {step} is waiting for its dialog Il passaggio {step} attende la sua finestra di dialogo
@plural {n} action|{n} actions {n} azione|{n} azioni
Auto Add/Delete Aggiungi/elimina automaticamente
# Native Windows printing qualification
Don't also tick Mirror Image in Print Settings…: the two flips cancel out. Non attivare anche Immagine speculare nelle impostazioni di stampa: le due inversioni si annullano.
Print Settings… Impostazioni di stampa…
Remember to turn off the printer's own colour management in Print Settings… Ricorda di disattivare la gestione del colore della stampante nelle impostazioni di stampa…
Sent to {printer} Inviato a {printer}
set in Print Settings… impostato nelle impostazioni di stampa…
ICO is limited to 256 × 256 pixels ICO è limitato a 256 × 256 pixel
Can't render this file because it contains an unexpected character in line 2647 and column 53.
+6
View File
@@ -3809,6 +3809,12 @@ curves Smooth スムーズ
Vector Mask Hides Effects ベクトルマスクで効果を隠す
Auto Add/Delete 自動追加・削除
# Native Windows printing qualification
Don't also tick Mirror Image in Print Settings…: the two flips cancel out. 印刷設定の「鏡像」も有効にしないでください。2回反転すると元に戻ります。
Print Settings… 印刷設定…
Remember to turn off the printer's own colour management in Print Settings… 印刷設定でプリンター独自のカラーマネジメントを無効にしてください…
Sent to {printer} {printer} に送信しました
set in Print Settings… 印刷設定で指定…
# Restored rows (custom print paper #3820, brush presets, Vanishing Point help, brush panel menu)
Drag to pan · Scroll to zoom · Double-click to fit ドラッグで移動 · スクロールでズーム · ダブルクリックで全体表示
Clear Brush Controls ブラシのコントロールを消去
Can't render this file because it contains an unexpected character in line 6 and column 47.
+7
View File
@@ -3311,4 +3311,11 @@ curves Smooth 매끄럽게
Step {step} is waiting for its dialog {step}단계가 대화 상자를 기다리고 있습니다
@plural {n} action|{n} actions 액션 {n}개
Auto Add/Delete 자동 추가/삭제
# Native Windows printing qualification
Don't also tick Mirror Image in Print Settings…: the two flips cancel out. 인쇄 설정에서도 이미지 반전을 켜지 마세요. 두 번 반전하면 원래대로 돌아갑니다.
Print Settings… 인쇄 설정…
Remember to turn off the printer's own colour management in Print Settings… 인쇄 설정에서 프린터 자체의 색상 관리를 끄세요…
Sent to {printer} {printer}(으)로 전송됨
set in Print Settings… 인쇄 설정에서 지정…
ICO is limited to 256 × 256 pixels ICO는 256 × 256픽셀로 제한됩니다
Can't render this file because it contains an unexpected character in line 3160 and column 53.
+7
View File
@@ -3896,4 +3896,11 @@ curves Smooth Vloeiend maken
Step {step} is waiting for its dialog Stap {step} wacht op zijn dialoogvenster
@plural {n} action|{n} actions {n} handeling|{n} handelingen
Auto Add/Delete Automatisch toevoegen/verwijderen
# Native Windows printing qualification
Don't also tick Mirror Image in Print Settings…: the two flips cancel out. Schakel niet ook Spiegelbeeld in bij de afdrukinstellingen: de twee spiegelingen heffen elkaar op.
Print Settings… Afdrukinstellingen…
Remember to turn off the printer's own colour management in Print Settings… Vergeet niet het eigen kleurbeheer van de printer uit te schakelen in de afdrukinstellingen…
Sent to {printer} Verzonden naar {printer}
set in Print Settings… instellen in de afdrukinstellingen…
ICO is limited to 256 × 256 pixels ICO is beperkt tot 256 × 256 pixels
Can't render this file because it contains an unexpected character in line 8 and column 34.
+7
View File
@@ -3734,4 +3734,11 @@ curves Smooth Wygładź
Step {step} is waiting for its dialog Krok {step} czeka na swoje okno dialogowe
@plural {n} action|{n} actions {n} operacja|{n} operacje|{n} operacji
Auto Add/Delete Automatyczne dodawanie/usuwanie
# Native Windows printing qualification
Don't also tick Mirror Image in Print Settings…: the two flips cancel out. Nie włączaj też odbicia lustrzanego w ustawieniach druku: oba odbicia wzajemnie się znoszą.
Print Settings… Ustawienia druku…
Remember to turn off the printer's own colour management in Print Settings… Pamiętaj o wyłączeniu zarządzania kolorami drukarki w ustawieniach druku…
Sent to {printer} Wysłano do {printer}
set in Print Settings… ustawiane w ustawieniach druku…
ICO is limited to 256 × 256 pixels ICO jest ograniczony do 256 × 256 pikseli
Can't render this file because it contains an unexpected character in line 6 and column 47.
+7
View File
@@ -3926,4 +3926,11 @@ layerLabel No Color Sem cor
Step {step} is waiting for its dialog A etapa {step} está aguardando sua caixa de diálogo
@plural {n} action|{n} actions {n} ação|{n} ações
Auto Add/Delete Adicionar/excluir automaticamente
# Native Windows printing qualification
Don't also tick Mirror Image in Print Settings…: the two flips cancel out. Não ative também a imagem espelhada nas configurações de impressão: as duas inversões se anulam.
Print Settings… Configurações de impressão…
Remember to turn off the printer's own colour management in Print Settings… Lembre-se de desativar o gerenciamento de cores da própria impressora nas configurações de impressão…
Sent to {printer} Enviado para {printer}
set in Print Settings… definido nas configurações de impressão…
ICO is limited to 256 × 256 pixels ICO é limitado a 256 × 256 pixels
Can't render this file because it contains an unexpected character in line 6 and column 47.
+7
View File
@@ -3082,6 +3082,13 @@ curves Smooth Сгладить
Step {step} is waiting for its dialog Шаг {step} ожидает своего диалога
@plural {n} action|{n} actions {n} операция|{n} операции|{n} операций
Auto Add/Delete Автодобавление/удаление
# Native Windows printing qualification
Don't also tick Mirror Image in Print Settings…: the two flips cancel out. Не включайте также зеркальное отражение в параметрах печати: два отражения отменяют друг друга.
Print Settings… Параметры печати…
Remember to turn off the printer's own colour management in Print Settings… Не забудьте отключить управление цветом самого принтера в параметрах печати…
Sent to {printer} Отправлено на {printer}
set in Print Settings… задаётся в параметрах печати…
ICO is limited to 256 × 256 pixels ICO ограничен размером 256 × 256 пикселей
# Dynamic selection context menu and appearance favorites
Can't render this file because it contains an unexpected character in line 6 and column 47.
+8
View File
@@ -4200,9 +4200,17 @@ shape Appearance Izgled
Choose a gradient file (.grd). Izaberite datoteku gradijenta (.grd).
Auto Add/Delete Automatsko dodavanje/brisanje
# Native Windows printing qualification
Don't also tick Mirror Image in Print Settings…: the two flips cancel out. Nemojte uključiti i ogledalsku sliku u podešavanjima štampanja: dva preslikavanja se poništavaju.
Print Settings… Podešavanja štampanja…
Remember to turn off the printer's own colour management in Print Settings… Ne zaboravite da isključite upravljanje bojama samog štampača u podešavanjima štampanja…
Sent to {printer} Poslato na {printer}
set in Print Settings… podešava se u podešavanjima štampanja…
# Custom print paper (#3820)
Paper Width Širina papira
Paper Height Visina papira
# ICO dimensions
ICO is limited to 256 × 256 pixels ICO je ograničen na 256 × 256 piksela
# Channel Options and New Spot Channel (#3761)
Can't render this file because it contains an unexpected character in line 6 and column 47.
+7
View File
@@ -3861,6 +3861,13 @@ cameraData Lens Lens
Loaded Camera Raw settings from {file} Camera Raw ayarları {file} dosyasından yüklendi
Loaded Camera Raw settings from {file} ({n} not supported) Camera Raw ayarları {file} dosyasından yüklendi ({n} desteklenmiyor)
Auto Add/Delete Otomatik Ekle/Sil
# Native Windows printing qualification
Don't also tick Mirror Image in Print Settings…: the two flips cancel out. Yazdırma Ayarlarında ayrıca Ayna Görüntüsünü açmayın: iki yansıtma birbirini iptal eder.
Print Settings… Yazdırma Ayarları…
Remember to turn off the printer's own colour management in Print Settings… Yazdırma Ayarlarında yazıcının kendi renk yönetimini kapatmayı unutmayın…
Sent to {printer} {printer} yazıcısına gönderildi
set in Print Settings… Yazdırma Ayarlarında belirlenir…
ICO is limited to 256 × 256 pixels ICO 256 × 256 piksel ile sınırlıdır
# Channel Options and New Spot Channel (#3761)
Can't render this file because it contains an unexpected character in line 8 and column 34.
+7
View File
@@ -3835,4 +3835,11 @@ curves Smooth Згладити
Step {step} is waiting for its dialog Крок {step} очікує на свій діалог
@plural {n} action|{n} actions {n} операція|{n} операції|{n} операцій
Auto Add/Delete Автододавання/видалення
# Native Windows printing qualification
Don't also tick Mirror Image in Print Settings…: the two flips cancel out. Не вмикайте також дзеркальне відображення в параметрах друку: два відображення скасовують одне одного.
Print Settings… Параметри друку…
Remember to turn off the printer's own colour management in Print Settings… Не забудьте вимкнути керування кольором самого принтера в параметрах друку…
Sent to {printer} Надіслано на {printer}
set in Print Settings… задається в параметрах друку…
ICO is limited to 256 × 256 pixels ICO обмежено розміром 256 × 256 пікселів
Can't render this file because it contains an unexpected character in line 1387 and column 18.
+7
View File
@@ -3623,4 +3623,11 @@ curves Smooth Làm mượt
Import .GRD Nhập .GRD
Choose a gradient file (.grd). Chọn tệp chuyển sắc (.grd).
Auto Add/Delete Tự động thêm/xóa
# Native Windows printing qualification
Don't also tick Mirror Image in Print Settings…: the two flips cancel out. Đừng bật thêm Ảnh phản chiếu trong thiết lập in: hai lần lật sẽ triệt tiêu nhau.
Print Settings… Thiết lập in…
Remember to turn off the printer's own colour management in Print Settings… Nhớ tắt quản lý màu riêng của máy in trong thiết lập in…
Sent to {printer} Đã gửi tới {printer}
set in Print Settings… đặt trong thiết lập in…
ICO is limited to 256 × 256 pixels ICO giới hạn ở 256 × 256 pixel
Can't render this file because it contains an unexpected character in line 6 and column 47.
+7
View File
@@ -2746,4 +2746,11 @@ curves Smooth 平滑
Step {step} is waiting for its dialog 步骤 {step} 正在等待其对话框
@plural {n} action|{n} actions {n} 个动作
Auto Add/Delete 自动添加/删除
# Native Windows printing qualification
Don't also tick Mirror Image in Print Settings…: the two flips cancel out. 不要同时勾选打印设置中的镜像:两次翻转会相互抵消。
Print Settings… 打印设置…
Remember to turn off the printer's own colour management in Print Settings… 请记得在打印设置中关闭打印机自身的色彩管理…
Sent to {printer} 已发送到 {printer}
set in Print Settings… 在打印设置中设置…
ICO is limited to 256 × 256 pixels ICO 最大为 256 × 256 像素
Can't render this file because it contains an unexpected character in line 2593 and column 53.
+7
View File
@@ -2731,4 +2731,11 @@ curves Smooth 平滑
Step {step} is waiting for its dialog 步驟 {step} 正在等待其對話框
@plural {n} action|{n} actions {n} 個動作
Auto Add/Delete 自動增加/刪除
# Native Windows printing qualification
Don't also tick Mirror Image in Print Settings…: the two flips cancel out. 請勿同時勾選列印設定中的鏡像:兩次翻轉會互相抵消。
Print Settings… 列印設定…
Remember to turn off the printer's own colour management in Print Settings… 請記得在列印設定中關閉印表機本身的色彩管理…
Sent to {printer} 已傳送至 {printer}
set in Print Settings… 在列印設定中設定…
ICO is limited to 256 × 256 pixels ICO 最大為 256 × 256 像素
Can't render this file because it contains an unexpected character in line 6 and column 47.
@@ -10,6 +10,8 @@ fn app() -> (PhotocraftApp, u64) {
let id = open_print(&mut app);
// Every UI path in these fixtures is safe even if an unexpected Enter confirms the dialog.
app.ui.dialog_mut(id).unwrap().fields.insert("dryRun".into(), json!(true));
// Exercise PDF paper controls even when native printers are available, on every OS.
app.ui.dialog_mut(id).unwrap().fields.insert("__printers".into(), json!(["Test native printer"]));
(app, id)
}