mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-14 11:16:53 -04:00
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:
29 files changed
+1742
-48
No files matched your search
Generated
+1225
-17
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -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
@@ -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",
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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 ¶ms {
|
||||
// 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;
|
||||
|
||||
@@ -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
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user