Use shared native print transport and keep page composition in PhotoCraft

This commit is contained in:
skylartaylor committed 2026-10-13 02:56:11 +02:00
1 parent 3702555aa9
commit d9bc5a8cd5
12 files changed
+1288 -927

No files matched your search

Generated
+1214 -15
View File
File diff suppressed because it is too large. Load diff
+1 -2
View File
@@ -36,10 +36,9 @@ photocraft-engine = { path = "crates/engine" }
photocraft-ui-egui = { path = "crates/ui-egui" }
photocraft-testkit = { path = "crates/testkit" }
photocraft-tablet = { path = "crates/tablet" }
photocraft-winprint = { path = "crates/winprint" }
# external
craft-print = { git = "https://github.com/storytold/craft-libs", rev = "d991bf6792f8ade99c51358bf3c1d902c8cd96b5" }
craft-print = { git = "https://github.com/storytold/craft-libs", rev = "ee70afc29a41bc50f2a55affa9aa2c62e0d28e3a" }
thiserror = "2"
serde = { version = "1", features = ["derive", "rc"] }
serde_json = "1"
-1
View File
@@ -19,7 +19,6 @@ photocraft-raster = { workspace = true }
photocraft-doc = { workspace = true }
photocraft-ops = { workspace = true }
photocraft-compose = { workspace = true }
photocraft-winprint = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
+18 -15
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};
@@ -457,13 +459,13 @@ 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());
}
photocraft_winprint::default_printer().map_err(other)?.ok_or_else(|| other("no printer chosen and no default printer is set"))
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<photocraft_winprint::DevMode>> {
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) => photocraft_winprint::DevMode::from_hex(hex).map(Some).map_err(|e| bad("file.print", e.to_string())),
Some(hex) => craft_print::rgb::DevMode::from_hex(hex).map(Some).map_err(|e| bad("file.print", e.to_string())),
None => Ok(None),
}
}
@@ -503,44 +505,45 @@ pub fn page_rect_top_left(page: &PrintPage) -> (f64, f64, f64, f64) {
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 = photocraft_winprint::page(&printer, settings.as_ref()).map_err(other)?;
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 = photocraft_winprint::RgbImage { width: page.image.width, height: page.image.height, data: &rgb };
let marks = photocraft_winprint::Marks { corner_crop: page.marks.corner_crop, center_crop: page.marks.center_crop, registration: page.marks.registration };
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") => photocraft_winprint::ColorPolicy::ApplicationManaged,
Some("noColorManagement") => photocraft_winprint::ColorPolicy::Unmanaged,
_ => photocraft_winprint::ColorPolicy::DriverManaged,
Some("photocraftManages" | "photoshopManages") => craft_print::rgb::ColorPolicy::ApplicationManaged,
Some("noColorManagement") => craft_print::rgb::ColorPolicy::Unmanaged,
_ => craft_print::rgb::ColorPolicy::DriverManaged,
};
let job = photocraft_winprint::Job {
let job = craft_print::rgb::Job {
printer: &printer,
settings: settings.as_ref(),
title,
output_file: None,
copies: copies.clamp(1, 999),
color_policy,
marks,
primitives: &primitives,
};
let done = photocraft_winprint::print(&job, &image, page_rect_top_left(page)).map_err(other)?;
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 photocraft_winprint::printers() {
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(photocraft_winprint::PrintError::Unsupported) => Ok(json!({"supported": false, "printers": []})),
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 = photocraft_winprint::page(&printer, printer_settings(p)?.as_ref()).map_err(other)?;
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]}),
)
+2 -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();
@@ -237,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());
}
}
+1 -1
View File
@@ -8,11 +8,11 @@ license.workspace = true
workspace = true
[dependencies]
craft-print = { workspace = true }
craft-ui = { workspace = true }
photocraft-geom = { workspace = true }
photocraft-color = { workspace = true }
photocraft-cms = { workspace = true }
photocraft-winprint = { workspace = true }
photocraft-raster = { workspace = true }
photocraft-doc = { workspace = true }
photocraft-compose = { workspace = true }
+5 -5
View File
@@ -594,7 +594,7 @@ 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) = photocraft_winprint::printers() {
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", "")) {
@@ -610,8 +610,8 @@ pub fn open_print(app: &mut PhotocraftApp) -> u64 {
/// 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| photocraft_winprint::DevMode::from_hex(h).ok());
match photocraft_winprint::page(&printer, settings.as_ref()) {
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"));
@@ -747,8 +747,8 @@ fn print_body(app: &mut PhotocraftApp, ui: &mut egui::Ui, f: &mut Map<String, Va
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| photocraft_winprint::DevMode::from_hex(h).ok());
match photocraft_winprint::settings_dialog(&printer, current.as_ref(), None) {
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);
-26
View File
@@ -1,26 +0,0 @@
[package]
name = "photocraft-winprint"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "Windows printing: printer list, the driver's Print Settings dialog, page geometry and GDI printing with ICM off"
# An isolated `unsafe` helper crate like photocraft-tablet (craftrules never-crash, AGENTS.md):
# Windows printing needs Win32 calls. Only `src/windows.rs` allows `unsafe`; every block has a
# `SAFETY:` comment. Elsewhere every function returns `Unsupported`.
[lints.rust]
unsafe_code = "deny"
[lints.clippy]
dbg_macro = "warn"
todo = "warn"
# Already in Cargo.lock (winit/wgpu pull it in); pinned exactly: review the diff before bumping.
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "=0.61.2", features = [
"Win32_Foundation",
"Win32_Graphics_Gdi",
"Win32_Graphics_Printing",
"Win32_Storage_Xps",
"Win32_UI_ColorSystem",
] }
-339
View File
@@ -1,339 +0,0 @@
//! Windows printing for File › Print: the system's printers, the driver's own Print Settings
//! dialog (paper, paper type, quality, the driver's colour correction and mirror), the page
//! geometry it implies, and printing an 8-bit RGB image through GDI with ICM off, so colours
//! converted with a printer profile ("PhotoCraft Manages Colors") reach the driver unchanged.
//! Corner/centre crop marks and registration marks are drawn around the image with GDI vector
//! calls (`MoveToEx`/`LineTo`/`Ellipse`), the same geometry as the PDF path's content stream.
//! Description and label text stay PDF-only (no font/text layout here yet).
//!
//! The driver settings travel as an opaque DEVMODE blob ([`DevMode`]), validated before use.
//! Everything here is a safe API; the Win32 calls live in `windows.rs`. Other platforms get
//! [`PrintError::Unsupported`].
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
#[cfg(windows)]
#[allow(unsafe_code)]
mod windows;
use std::fmt;
/// Why a printing call failed.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PrintError {
/// Not on Windows.
Unsupported,
/// Bad input (an unknown printer, a malformed DEVMODE, an empty image).
Invalid(String),
/// A Win32 call failed.
System(String),
}
impl fmt::Display for PrintError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PrintError::Unsupported => f.write_str("printing to a printer needs Windows here"),
PrintError::Invalid(m) | PrintError::System(m) => f.write_str(m),
}
}
}
impl std::error::Error for PrintError {}
pub type Result<T> = std::result::Result<T, PrintError>;
/// One installed printer.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Printer {
pub name: String,
pub default: bool,
}
/// A printer driver's settings (a Win32 DEVMODEW with the driver's private part), as bytes.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DevMode(Vec<u8>);
/// Offsets inside DEVMODEW: `dmSize` and `dmDriverExtra` follow the 32-wchar device name and
/// two version words.
const DM_SIZE_AT: usize = 68;
const DM_EXTRA_AT: usize = 70;
/// The smallest DEVMODEW a driver writes (Windows 95's, up to `dmICMIntent`).
const DM_MIN: usize = 156;
/// Larger blobs are not driver settings.
const DM_MAX: usize = 1 << 20;
impl DevMode {
/// Validates driver settings bytes: the header's own sizes must add up to the length.
pub fn from_bytes(b: Vec<u8>) -> Result<Self> {
let word = |at: usize| b.get(at..at + 2).map(|w| usize::from(u16::from_le_bytes([w[0], w[1]])));
let (Some(size), Some(extra)) = (word(DM_SIZE_AT), word(DM_EXTRA_AT)) else {
return Err(PrintError::Invalid("printer settings are too short".into()));
};
if b.len() > DM_MAX || size < DM_MIN || size.checked_add(extra) != Some(b.len()) {
return Err(PrintError::Invalid("printer settings are damaged; choose Print Settings again".into()));
}
Ok(DevMode(b))
}
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
/// The application emits every copy as a page. Disable driver copies/collation too,
/// preserving the driver's private settings and all other public fields.
#[cfg(any(windows, test))]
pub(crate) fn for_manual_copies(&self) -> Result<Self> {
let mut bytes = self.0.clone();
let invalid = || PrintError::Invalid("printer settings are too short".into());
let fields: [u8; 4] = bytes.get(72..76).and_then(|b| b.try_into().ok()).ok_or_else(invalid)?;
let fields = u32::from_le_bytes(fields) | 0x100 | 0x8000; // DM_COPIES | DM_COLLATE
bytes.get_mut(72..76).ok_or_else(invalid)?.copy_from_slice(&fields.to_le_bytes());
bytes.get_mut(86..88).ok_or_else(invalid)?.copy_from_slice(&1i16.to_le_bytes());
bytes.get_mut(100..102).ok_or_else(invalid)?.copy_from_slice(&0i16.to_le_bytes());
Ok(Self(bytes))
}
#[cfg(any(windows, test))]
pub(crate) fn has_manual_copies(&self) -> bool {
let fields = self.0.get(72..76).and_then(|b| <[u8; 4]>::try_from(b).ok()).map(u32::from_le_bytes);
fields.is_some_and(|f| f & 0x100 != 0)
&& self.0.get(86..88) == Some(1i16.to_le_bytes().as_slice())
&& self.0.get(100..102) == Some(0i16.to_le_bytes().as_slice())
}
/// Hex, for JSON parameters and remembered print settings.
pub fn to_hex(&self) -> String {
self.0.iter().map(|b| format!("{b:02x}")).collect()
}
pub fn from_hex(s: &str) -> Result<Self> {
if !s.len().is_multiple_of(2) || s.len() > DM_MAX * 2 {
return Err(PrintError::Invalid("printer settings are damaged; choose Print Settings again".into()));
}
let bytes: Option<Vec<u8>> = (0..s.len()).step_by(2).map(|i| s.get(i..i + 2).and_then(|h| u8::from_str_radix(h, 16).ok())).collect();
Self::from_bytes(bytes.ok_or_else(|| PrintError::Invalid("printer settings are damaged; choose Print Settings again".into()))?)
}
}
/// What a printer will print on with given settings. Points (1/72 in), origin top-left.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Page {
/// The whole sheet.
pub paper: (f64, f64),
/// The area the printer can reach: (x, y, width, height).
pub printable: (f64, f64, f64, f64),
/// Device resolution (dots per inch).
pub dpi: (u32, u32),
}
/// An 8-bit RGB image, rows top to bottom, `width * 3` bytes each.
pub struct RgbImage<'a> {
pub width: u32,
pub height: u32,
pub data: &'a [u8],
}
/// Printer marks drawn around the image box: corner and centre crop marks, and registration
/// marks. Matches [`photocraft_engine`]'s PDF `Marks`, mirrored here so this crate stays
/// independent of the engine; GDI draws the same geometry with vector line/ellipse calls instead
/// of PDF content stream operators.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Marks {
pub corner_crop: bool,
pub center_crop: bool,
pub registration: bool,
}
impl Marks {
#[cfg(windows)]
pub(crate) fn any(self) -> bool {
self.corner_crop || self.center_crop || self.registration
}
}
/// One print job.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ColorPolicy {
/// The driver applies its color settings to the RGB image.
DriverManaged,
/// The caller has converted the pixels into the printer's RGB space.
ApplicationManaged,
/// The caller deliberately requests no DC color correction.
Unmanaged,
}
/// One print job.
pub struct Job<'a> {
pub printer: &'a str,
pub settings: Option<&'a DevMode>,
/// The name in the print queue.
pub title: &'a str,
/// Print to this file instead of the device (drivers that write files, and tests).
pub output_file: Option<&'a str>,
pub copies: u32,
pub color_policy: ColorPolicy,
pub marks: Marks,
}
/// Where a job went: the image box in device pixels and the device resolution.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Printed {
pub device_rect: (i32, i32, i32, i32),
pub dpi: (u32, u32),
}
/// Maps an image box in points (origin top-left of the sheet) to device pixels (origin at the
/// printable area's corner, as GDI draws).
pub fn device_rect(page: &Page, rect: (f64, f64, f64, f64)) -> (i32, i32, i32, i32) {
let (dx, dy) = (f64::from(page.dpi.0) / 72.0, f64::from(page.dpi.1) / 72.0);
let px = |v: f64| if v.is_finite() { v.round().clamp(-1e8, 1e8) as i32 } else { 0 };
let (x, y, w, h) = rect;
(px((x - page.printable.0) * dx), px((y - page.printable.1) * dy), px(w * dx), px(h * dy))
}
/// The printers installed for this user, the default first.
pub fn printers() -> Result<Vec<Printer>> {
#[cfg(windows)]
{
let mut v = windows::printers()?;
v.sort_by(|a, b| b.default.cmp(&a.default).then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase())));
Ok(v)
}
#[cfg(not(windows))]
Err(PrintError::Unsupported)
}
/// The default printer's name.
pub fn default_printer() -> Result<Option<String>> {
#[cfg(windows)]
{
windows::default_printer()
}
#[cfg(not(windows))]
Err(PrintError::Unsupported)
}
/// A printer's settings as its driver starts them.
pub fn default_settings(printer: &str) -> Result<DevMode> {
#[cfg(windows)]
{
windows::properties(printer, None, None, false)?.ok_or_else(|| PrintError::System("the printer driver gave no settings".into()))
}
#[cfg(not(windows))]
{
let _ = printer;
Err(PrintError::Unsupported)
}
}
/// Shows the driver's Print Settings dialog (modal; `parent` is a window handle). `None` when
/// the user cancels.
pub fn settings_dialog(printer: &str, current: Option<&DevMode>, parent: Option<isize>) -> Result<Option<DevMode>> {
#[cfg(windows)]
{
windows::properties(printer, current, parent, true)
}
#[cfg(not(windows))]
{
let _ = (printer, current, parent);
Err(PrintError::Unsupported)
}
}
/// The sheet and printable area for `printer` with `settings`.
pub fn page(printer: &str, settings: Option<&DevMode>) -> Result<Page> {
#[cfg(windows)]
{
windows::page(printer, settings)
}
#[cfg(not(windows))]
{
let _ = (printer, settings);
Err(PrintError::Unsupported)
}
}
/// Prints `image` into `rect` (points, origin top-left of the sheet), `job.copies` pages.
pub fn print(job: &Job<'_>, image: &RgbImage<'_>, rect: (f64, f64, f64, f64)) -> Result<Printed> {
let row = (image.width as usize).checked_mul(3).ok_or_else(|| PrintError::Invalid("image too large".into()))?;
if image.width == 0 || image.height == 0 || row.checked_mul(image.height as usize) != Some(image.data.len()) {
return Err(PrintError::Invalid("nothing to print".into()));
}
if ![rect.0, rect.1, rect.2, rect.3].iter().all(|v| v.is_finite()) || !(rect.2 > 0.0 && rect.3 > 0.0) {
return Err(PrintError::Invalid("the print size is empty".into()));
}
#[cfg(windows)]
{
windows::print(job, image, rect)
}
#[cfg(not(windows))]
{
let _ = job;
Err(PrintError::Unsupported)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn devmode(size: u16, extra: u16) -> Vec<u8> {
let mut b = vec![0u8; usize::from(size) + usize::from(extra)];
b[DM_SIZE_AT..DM_SIZE_AT + 2].copy_from_slice(&size.to_le_bytes());
b[DM_EXTRA_AT..DM_EXTRA_AT + 2].copy_from_slice(&extra.to_le_bytes());
b
}
#[test]
fn devmode_checks_its_own_sizes_and_round_trips_hex() {
let d = DevMode::from_bytes(devmode(220, 1200)).unwrap();
assert_eq!(DevMode::from_hex(&d.to_hex()).unwrap(), d);
for bad in [vec![], vec![0; 40], devmode(100, 0), {
let mut b = devmode(220, 10);
b.pop();
b
}] {
assert!(DevMode::from_bytes(bad).is_err());
}
for bad in ["", "abc", "zz", &"00".repeat(300)] {
assert!(DevMode::from_hex(bad).is_err(), "{bad}");
}
}
#[test]
fn manual_copies_disable_driver_multiplication_without_losing_private_settings() {
let mut bytes = devmode(220, 32);
bytes[86..88].copy_from_slice(&7i16.to_le_bytes());
bytes[100..102].copy_from_slice(&1i16.to_le_bytes());
bytes[220..].fill(0x5a);
let original = DevMode::from_bytes(bytes).unwrap();
let normalized = original.for_manual_copies().unwrap();
assert!(normalized.has_manual_copies());
assert!(!original.has_manual_copies());
assert_eq!(&normalized.as_bytes()[220..], &original.as_bytes()[220..]);
assert_eq!(normalized.for_manual_copies().unwrap(), normalized);
let mut ignored = normalized.as_bytes().to_vec();
ignored[72..76].copy_from_slice(&0u32.to_le_bytes());
assert!(!DevMode::from_bytes(ignored).unwrap().has_manual_copies(), "a driver must advertise that it accepts dmCopies");
}
#[test]
fn device_rect_is_relative_to_the_printable_corner() {
// A3 at 360 dpi with a 3 mm (8.5 pt) unprintable margin.
let page = Page { paper: (841.89, 1190.55), printable: (8.5, 8.5, 824.89, 1173.55), dpi: (360, 360) };
assert_eq!(device_rect(&page, (8.5, 8.5, 72.0, 36.0)), (0, 0, 360, 180));
assert_eq!(device_rect(&page, (72.0, 144.0, 144.0, 72.0)), (318, 678, 720, 360));
assert_eq!(device_rect(&page, (f64::NAN, f64::INFINITY, 1.0, 1.0)).0, 0);
}
#[test]
fn print_rejects_empty_or_short_images_before_any_system_call() {
let job = Job { printer: "x", settings: None, title: "t", output_file: None, copies: 1, color_policy: ColorPolicy::Unmanaged, marks: Marks::default() };
let short = RgbImage { width: 2, height: 2, data: &[0; 11] };
assert!(matches!(print(&job, &short, (0.0, 0.0, 10.0, 10.0)), Err(PrintError::Invalid(_))));
let ok = RgbImage { width: 2, height: 2, data: &[0; 12] };
assert!(matches!(print(&job, &ok, (0.0, 0.0, 0.0, 10.0)), Err(PrintError::Invalid(_))));
for rect in [(f64::NAN, 0.0, 10.0, 10.0), (0.0, 0.0, f64::INFINITY, 10.0)] {
assert!(matches!(print(&job, &ok, rect), Err(PrintError::Invalid(_))));
}
}
}
-520
View File
@@ -1,520 +0,0 @@
//! The Win32 side: winspool (printers, driver settings) and GDI (page geometry, printing).
//! Every `unsafe` block says why it is sound. Buffers the system writes into are `u64`-backed so
//! the structures read from them are aligned.
use std::ptr::{null, null_mut};
use windows_sys::Win32::Foundation::HWND;
use windows_sys::Win32::Graphics::Gdi::{
BI_RGB, BITMAPINFO, BITMAPINFOHEADER, CreateDCW, CreatePen, DEVMODEW, DIB_RGB_COLORS, DM_IN_BUFFER, DM_IN_PROMPT, DM_OUT_BUFFER, DeleteDC, DeleteObject,
Ellipse, GetDeviceCaps, GetStockObject, HALFTONE, HDC, HOLLOW_BRUSH, HORZRES, LOGPIXELSX, LOGPIXELSY, LineTo, MoveToEx, PHYSICALHEIGHT, PHYSICALOFFSETX,
PHYSICALOFFSETY, PHYSICALWIDTH, PS_SOLID, SRCCOPY, SelectObject, SetBrushOrgEx, SetStretchBltMode, StretchDIBits, VERTRES,
};
use windows_sys::Win32::Graphics::Printing::{
ClosePrinter, DocumentPropertiesW, EnumPrintersW, GetDefaultPrinterW, OpenPrinterW, PRINTER_ENUM_CONNECTIONS, PRINTER_ENUM_LOCAL, PRINTER_HANDLE,
PRINTER_INFO_4W,
};
use windows_sys::Win32::Storage::Xps::{AbortDoc, DOCINFOW, EndDoc, EndPage, StartDocW, StartPage};
use windows_sys::Win32::UI::ColorSystem::{ICM_DONE_OUTSIDEDC, ICM_OFF, SetICMMode};
use crate::{ColorPolicy, DevMode, Job, Marks, Page, PrintError, Printed, Printer, Result, RgbImage};
const IDOK: i32 = 1;
const IDCANCEL: i32 = 2;
/// Wide strings read from the system stop here at the latest.
const MAX_WCHARS: usize = 32 * 1024;
fn last_error(what: &str) -> PrintError {
PrintError::System(format!("{what} failed: {}", std::io::Error::last_os_error()))
}
fn wide(s: &str) -> Result<Vec<u16>> {
if s.contains('\0') {
return Err(PrintError::Invalid("a name contains a NUL character".into()));
}
Ok(s.encode_utf16().chain(Some(0)).collect())
}
/// A zeroed, 8-byte aligned buffer of at least `bytes` bytes.
fn aligned(bytes: usize) -> Vec<u64> {
vec![0u64; bytes.div_ceil(8).max(1)]
}
fn bytes_of(buf: &[u64], len: usize) -> Vec<u8> {
buf.iter().flat_map(|w| w.to_le_bytes()).take(len).collect()
}
/// Reads a NUL-terminated wide string.
///
/// # Safety
/// `p` is null or points to a NUL-terminated UTF-16 string that stays alive for the call.
unsafe fn read_wide(p: *const u16) -> String {
if p.is_null() {
return String::new();
}
let mut n = 0;
// SAFETY: the caller guarantees a NUL-terminated string; we stop at the NUL (or the cap).
while n < MAX_WCHARS && unsafe { *p.add(n) } != 0 {
n += 1;
}
// SAFETY: the `n` units before the NUL were just read and are valid.
String::from_utf16_lossy(unsafe { std::slice::from_raw_parts(p, n) })
}
pub fn printers() -> Result<Vec<Printer>> {
let flags = PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS;
let (mut needed, mut count) = (0u32, 0u32);
// SAFETY: a size query: no buffer (null, 0 bytes); the out-params are valid locals.
unsafe { EnumPrintersW(flags, null(), 4, null_mut(), 0, &mut needed, &mut count) };
if needed == 0 {
return Ok(Vec::new());
}
let mut buf = aligned(needed as usize);
// SAFETY: `buf` holds at least `needed` writable bytes, aligned for PRINTER_INFO_4W.
let ok = unsafe { EnumPrintersW(flags, null(), 4, buf.as_mut_ptr().cast(), needed, &mut needed, &mut count) };
if ok == 0 {
return Err(last_error("listing printers"));
}
let default = default_printer().ok().flatten();
let infos = buf.as_ptr().cast::<PRINTER_INFO_4W>();
let fits = (needed as usize) / std::mem::size_of::<PRINTER_INFO_4W>();
let mut out = Vec::new();
for i in 0..(count as usize).min(fits) {
// SAFETY: the system wrote `count` PRINTER_INFO_4W at the start of `buf` (bounded by
// the buffer size above); their name pointers point into `buf`, which is alive.
let name = unsafe { read_wide((*infos.add(i)).pPrinterName) };
if !name.is_empty() {
out.push(Printer { default: default.as_deref() == Some(name.as_str()), name });
}
}
Ok(out)
}
pub fn default_printer() -> Result<Option<String>> {
let mut len = 0u32;
// SAFETY: a size query with a null buffer; `len` is a valid local.
unsafe { GetDefaultPrinterW(null_mut(), &mut len) };
if len == 0 || len as usize > MAX_WCHARS {
return Ok(None);
}
let mut buf = vec![0u16; len as usize];
// SAFETY: `buf` holds `len` writable wide chars.
if unsafe { GetDefaultPrinterW(buf.as_mut_ptr(), &mut len) } == 0 {
return Ok(None);
}
// SAFETY: on success the buffer holds a NUL-terminated name.
Ok(Some(unsafe { read_wide(buf.as_ptr()) }))
}
/// An open printer handle, closed on drop.
struct OpenPrinter(PRINTER_HANDLE);
impl OpenPrinter {
fn open(name: &[u16]) -> Result<Self> {
let mut h = PRINTER_HANDLE { Value: null_mut() };
// SAFETY: `name` is NUL-terminated; `h` is a valid out-param; no defaults.
if unsafe { OpenPrinterW(name.as_ptr(), &mut h, null()) } == 0 || h.Value.is_null() {
return Err(last_error("opening the printer"));
}
Ok(OpenPrinter(h))
}
}
impl Drop for OpenPrinter {
fn drop(&mut self) {
// SAFETY: the handle came from OpenPrinterW and is closed once.
unsafe { ClosePrinter(self.0) };
}
}
/// `settings` copied into an aligned buffer the driver can read.
///
/// There is deliberately no check of the printer name stored in the settings: Windows keeps only
/// 31 characters of it, and drivers fill it differently, so a long queue name ("Sublimation -
/// EPSON ET-18100 Series") failed such a check and the settings were silently dropped. The driver
/// itself validates them instead (see [`Dc::create`]).
fn aligned_copy(settings: &DevMode) -> Vec<u64> {
let b = settings.as_bytes();
let mut buf = aligned(b.len());
for (slot, chunk) in buf.iter_mut().zip(b.chunks(8)) {
let mut w = [0u8; 8];
w[..chunk.len()].copy_from_slice(chunk);
*slot = u64::from_le_bytes(w);
}
buf
}
pub fn properties(printer: &str, current: Option<&DevMode>, parent: Option<isize>, prompt: bool) -> Result<Option<DevMode>> {
let name = wide(printer)?;
let p = OpenPrinter::open(&name)?;
let hwnd: HWND = parent.map_or(null_mut(), |h| h as HWND);
// SAFETY: with fmode 0 DocumentPropertiesW only returns the size the settings need.
let needed = unsafe { DocumentPropertiesW(hwnd, p.0, name.as_ptr(), null_mut(), null(), 0) };
if needed <= 0 || needed as usize > (1 << 20) {
return Err(last_error("reading the printer's settings"));
}
let mut out = aligned(needed as usize);
let input = current.map(aligned_copy);
let mut mode = DM_OUT_BUFFER;
if input.is_some() {
mode |= DM_IN_BUFFER;
}
if prompt {
mode |= DM_IN_PROMPT;
}
let inp: *const DEVMODEW = input.as_ref().map_or(null(), |b| b.as_ptr().cast());
// SAFETY: `out` holds `needed` aligned writable bytes as the size query asked for; `inp` is
// null or an aligned copy of settings written by this printer's driver.
let r = unsafe { DocumentPropertiesW(hwnd, p.0, name.as_ptr(), out.as_mut_ptr().cast(), inp, mode) };
match r {
IDOK => {
let all = bytes_of(&out, needed as usize);
let word = |at: usize| all.get(at..at + 2).map_or(0, |w| usize::from(u16::from_le_bytes([w[0], w[1]])));
let len = (word(68) + word(70)).min(all.len());
DevMode::from_bytes(all.into_iter().take(len).collect()).map(Some)
}
IDCANCEL => Ok(None),
_ => Err(last_error("the printer's settings dialog")),
}
}
/// A printer device context, deleted on drop.
struct Dc(HDC);
impl Dc {
fn create(printer: &str, settings: Option<&DevMode>, manual_copies: bool) -> Result<Self> {
let name = wide(printer)?;
let normalized = if manual_copies {
let defaults = match settings {
Some(s) => s.clone(),
None => properties(printer, None, None, false)?.ok_or_else(|| PrintError::Invalid("the printer refused its default settings".into()))?,
};
Some(defaults.for_manual_copies()?)
} else {
None
};
let settings = normalized.as_ref().or(settings);
// The driver validates and completes the settings first (DM_IN_BUFFER | DM_OUT_BUFFER, no
// dialog). If it refuses them, that is an error: never print on the defaults instead.
let merged = match settings {
Some(s) => Some(
properties(printer, Some(s), None, false)?
.ok_or_else(|| PrintError::Invalid(format!("printer “{printer}” refused these settings; choose Print Settings… again")))?,
),
None => None,
};
if manual_copies && !merged.as_ref().is_some_and(DevMode::has_manual_copies) {
return Err(PrintError::Invalid("the driver refused single-copy settings; no print job was sent".into()));
}
let dm = merged.as_ref().map(aligned_copy);
let dmp: *const DEVMODEW = dm.as_ref().map_or(null(), |b| b.as_ptr().cast());
// SAFETY: `name` is NUL-terminated; `dmp` is null or aligned settings from this
// printer's driver, alive for the call.
let hdc = unsafe { CreateDCW(null(), name.as_ptr(), null(), dmp) };
if hdc.is_null() {
return Err(last_error(&format!("opening printer “{printer}”")));
}
Ok(Dc(hdc))
}
fn cap(&self, index: u32) -> i32 {
// SAFETY: a valid printer DC and a documented capability index.
unsafe { GetDeviceCaps(self.0, index as i32) }
}
fn page(&self) -> Result<Page> {
let (dx, dy) = (self.cap(LOGPIXELSX), self.cap(LOGPIXELSY));
if dx <= 0 || dy <= 0 {
return Err(PrintError::System("the printer reported no resolution".into()));
}
let (fx, fy) = (72.0 / f64::from(dx), 72.0 / f64::from(dy));
let (pw, ph) = (self.cap(PHYSICALWIDTH), self.cap(PHYSICALHEIGHT));
let (ox, oy) = (self.cap(PHYSICALOFFSETX), self.cap(PHYSICALOFFSETY));
let (hw, vh) = (self.cap(HORZRES), self.cap(VERTRES));
let (pw, ph) = if pw > 0 && ph > 0 { (pw, ph) } else { (hw, vh) };
Ok(Page {
paper: (f64::from(pw) * fx, f64::from(ph) * fy),
printable: (f64::from(ox) * fx, f64::from(oy) * fy, f64::from(hw) * fx, f64::from(vh) * fy),
dpi: (dx as u32, dy as u32),
})
}
}
impl Drop for Dc {
fn drop(&mut self) {
// SAFETY: the DC came from CreateDCW and is deleted once.
unsafe { DeleteDC(self.0) };
}
}
pub fn page(printer: &str, settings: Option<&DevMode>) -> Result<Page> {
Dc::create(printer, settings, false)?.page()
}
/// RGB rows top-down → a bottom-up 24-bit DIB (BGR, rows padded to 4 bytes).
fn dib(image: &RgbImage<'_>) -> Result<(BITMAPINFO, Vec<u8>)> {
let (w, h) = (image.width as usize, image.height as usize);
let stride = (w.checked_mul(3).ok_or_else(|| PrintError::Invalid("image too large".into()))? + 3) & !3;
let total = stride.checked_mul(h).filter(|t| *t <= i32::MAX as usize).ok_or_else(|| PrintError::Invalid("image too large to print in one piece".into()))?;
let mut bits = vec![0u8; total];
for (y, src) in image.data.chunks_exact(w * 3).enumerate() {
let start = (h - 1 - y) * stride;
let Some(dst) = bits.get_mut(start..start + w * 3) else { continue };
for (d, s) in dst.as_chunks_mut::<3>().0.iter_mut().zip(src.as_chunks::<3>().0) {
*d = [s[2], s[1], s[0]];
}
}
let header = BITMAPINFOHEADER {
biSize: std::mem::size_of::<BITMAPINFOHEADER>() as u32,
biWidth: image.width as i32,
biHeight: image.height as i32,
biPlanes: 1,
biBitCount: 24,
biCompression: BI_RGB,
biSizeImage: total as u32,
..Default::default()
};
Ok((BITMAPINFO { bmiHeader: header, bmiColors: [Default::default()] }, bits))
}
/// Vector marks around the image box `(x, y, w, h)` (device pixels, origin top-left, as GDI
/// draws). Same geometry as `photocraft_engine::print_cmds::marks_ops`'s PDF content stream, with
/// the vertical sense flipped (PDF's origin is bottom-left; GDI's is top-left), scaled from points
/// to device pixels with `dpi` instead of PDF's user-space points.
///
/// # Safety
/// `dc` is a valid, selected device context for the current page.
unsafe fn draw_marks(dc: HDC, marks: Marks, (x, y, w, h): (i32, i32, i32, i32), dpi: (u32, u32)) -> Result<()> {
let (sx, sy) = (f64::from(dpi.0) / 72.0, f64::from(dpi.1) / 72.0);
let px = |v: f64| v.round() as i32;
let (gap_x, gap_y) = (px(6.0 * sx), px(6.0 * sy));
let (len_x, len_y) = (px(18.0 * sx), px(18.0 * sy));
let pen_width = px(0.3 * sx).max(1);
// SAFETY: `dc` is valid; the pen and brush are deleted/restored before returning.
let pen = unsafe { CreatePen(PS_SOLID, pen_width, 0x0000_0000) };
let hollow = unsafe { GetStockObject(HOLLOW_BRUSH) };
let old_pen = unsafe { SelectObject(dc, pen) };
let old_brush = unsafe { SelectObject(dc, hollow) };
let ok = std::cell::Cell::new(true);
let line = |x0: i32, y0: i32, x1: i32, y1: i32| {
// SAFETY: `dc` is valid for the duration of this call.
ok.set(ok.get() && unsafe { MoveToEx(dc, x0, y0, null_mut()) != 0 && LineTo(dc, x1, y1) != 0 });
};
if marks.corner_crop {
for (cx, cy, dx, dy) in [(x, y, -1, -1), (x + w, y, 1, -1), (x, y + h, -1, 1), (x + w, y + h, 1, 1)] {
line(cx + dx * gap_x, cy, cx + dx * (gap_x + len_x), cy);
line(cx, cy + dy * gap_y, cx, cy + dy * (gap_y + len_y));
}
}
if marks.center_crop {
let (mx, my) = (x + w / 2, y + h / 2);
line(mx, y - gap_y, mx, y - gap_y - len_y);
line(mx, y + h + gap_y, mx, y + h + gap_y + len_y);
line(x - gap_x, my, x - gap_x - len_x, my);
line(x + w + gap_x, my, x + w + gap_x + len_x, my);
}
if marks.registration {
let (rx, ry) = (px(5.0 * sx), px(5.0 * sy));
let (off_x, off_y) = (gap_x + len_x / 2, gap_y + len_y / 2);
for (cx, cy) in [(x + w / 2, y - off_y), (x + w / 2, y + h + off_y), (x - off_x, y + h / 2), (x + w + off_x, y + h / 2)] {
// SAFETY: `dc` is valid for the duration of this call.
ok.set(ok.get() && unsafe { Ellipse(dc, cx - rx, cy - ry, cx + rx, cy + ry) != 0 });
line(cx - (rx as f64 * 1.6) as i32, cy, cx + (rx as f64 * 1.6) as i32, cy);
line(cx, cy - (ry as f64 * 1.6) as i32, cx, cy + (ry as f64 * 1.6) as i32);
}
}
// SAFETY: restores the DC's previous pen/brush, then frees the ones created above.
unsafe {
SelectObject(dc, old_pen);
SelectObject(dc, old_brush);
DeleteObject(pen);
}
if ok.get() { Ok(()) } else { Err(last_error("drawing printer marks")) }
}
pub fn print(job: &Job<'_>, image: &RgbImage<'_>, rect: (f64, f64, f64, f64)) -> Result<Printed> {
if image.width > i32::MAX as u32 || image.height > i32::MAX as u32 {
return Err(PrintError::Invalid("image too large".into()));
}
let dc = Dc::create(job.printer, job.settings, true)?;
let page = dc.page()?;
let (x, y, w, h) = crate::device_rect(&page, rect);
let (bmi, bits) = dib(image)?;
let title = wide(job.title)?;
let output = job.output_file.map(wide).transpose()?;
let info = DOCINFOW {
cbSize: std::mem::size_of::<DOCINFOW>() as i32,
lpszDocName: title.as_ptr(),
lpszOutput: output.as_ref().map_or(null(), |o| o.as_ptr()),
lpszDatatype: null(),
fwType: 0,
};
let color_mode = match job.color_policy {
ColorPolicy::DriverManaged => ICM_OFF,
// ICM_OFF enables old-style halftone correction. Pixels converted by the caller
// need both DC conversion and that correction off (Microsoft SetICMMode contract).
ColorPolicy::ApplicationManaged | ColorPolicy::Unmanaged => ICM_DONE_OUTSIDEDC,
};
let set_color_mode = || {
// SAFETY: a valid printer DC and a documented SetICMMode value.
if unsafe { SetICMMode(dc.0, color_mode) } == 0 { Err(last_error("setting GDI color policy")) } else { Ok(()) }
};
set_color_mode()?;
// SAFETY: a valid DC; HALFTONE resampling needs the brush origin reset afterwards.
unsafe {
SetStretchBltMode(dc.0, HALFTONE);
SetBrushOrgEx(dc.0, 0, 0, null_mut());
}
// SAFETY: `info` and the strings it points to live until EndDoc/AbortDoc below.
if unsafe { StartDocW(dc.0, &info) } <= 0 {
return Err(last_error("starting the print job"));
}
let fail = |what: &str| {
let e = last_error(what);
// SAFETY: the job was started on this DC.
unsafe { AbortDoc(dc.0) };
e
};
for _ in 0..job.copies.clamp(1, 999) {
// SAFETY: inside a started document on a valid DC.
if unsafe { StartPage(dc.0) } <= 0 {
return Err(fail("starting a page"));
}
// StartPage saves/restores DC state. Reapply the policy for each page, before pixels.
if let Err(error) = set_color_mode() {
// SAFETY: this DC owns the document started above.
unsafe { AbortDoc(dc.0) };
return Err(error);
}
// SAFETY: `bits` is a bottom-up DIB exactly as `bmi` describes (24-bit, padded rows,
// `biSizeImage` bytes); the source rectangle is the whole image.
let lines =
unsafe { StretchDIBits(dc.0, x, y, w, h, 0, 0, image.width as i32, image.height as i32, bits.as_ptr().cast(), &bmi, DIB_RGB_COLORS, SRCCOPY) };
if lines <= 0 {
return Err(fail("sending the image to the printer"));
}
if job.marks.any() {
// SAFETY: `dc.0` is the valid, started page's device context.
if let Err(e) = unsafe { draw_marks(dc.0, job.marks, (x, y, w, h), page.dpi) } {
return Err(fail(&e.to_string()));
}
}
// SAFETY: closes the page started above.
if unsafe { EndPage(dc.0) } <= 0 {
return Err(fail("finishing a page"));
}
}
// SAFETY: closes the document started above.
if unsafe { EndDoc(dc.0) } <= 0 {
return Err(last_error("finishing the print job"));
}
Ok(Printed { device_rect: (x, y, w, h), dpi: page.dpi })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dib_is_bottom_up_bgr_with_padded_rows() {
// 2×2: red, green / blue, white.
let data = [255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255];
let (bmi, bits) = dib(&RgbImage { width: 2, height: 2, data: &data }).unwrap();
assert_eq!(bmi.bmiHeader.biHeight, 2);
assert_eq!(bits.len(), 16, "6 bytes per row padded to 8");
// First stored row is the bottom one: blue, white.
assert_eq!(&bits[0..6], &[255, 0, 0, 255, 255, 255]);
assert_eq!(&bits[8..14], &[0, 0, 255, 0, 255, 0]);
}
#[test]
fn settings_are_copied_aligned_and_intact() {
let mut b: Vec<u8> = (0..230u32).map(|i| (i * 7) as u8).collect();
b[68..70].copy_from_slice(&220u16.to_le_bytes());
b[70..72].copy_from_slice(&10u16.to_le_bytes());
let d = DevMode::from_bytes(b.clone()).unwrap();
let buf = aligned_copy(&d);
assert_eq!(buf.as_ptr().align_offset(std::mem::align_of::<DEVMODEW>()), 0);
assert_eq!(bytes_of(&buf, b.len()), b, "no name check, no truncation: the bytes go through as chosen");
}
/// Settings chosen for a printer are what the driver prints with: paper and resolution survive
/// the driver's own validation (needs "Microsoft Print to PDF"; run by hand).
#[test]
#[ignore]
fn chosen_settings_reach_the_device_context() {
let printer = "Microsoft Print to PDF";
let d = crate::default_settings(printer).unwrap();
let mut b = d.as_bytes().to_vec();
// dmPaperSize = DMPAPER_A3 (8); dmFields |= DM_PAPERSIZE (2).
b[78..80].copy_from_slice(&8i16.to_le_bytes());
let fields = u32::from_le_bytes([b[72], b[73], b[74], b[75]]) | 2;
b[72..76].copy_from_slice(&fields.to_le_bytes());
let a3 = DevMode::from_bytes(b).unwrap();
let pg = crate::page(printer, Some(&a3)).unwrap();
assert!((pg.paper.0 - 841.9).abs() < 2.0 && (pg.paper.1 - 1190.6).abs() < 2.0, "A3, not the default: {:?}", pg.paper);
// Landscape (dmOrientation = 2, DM_ORIENTATION = 1): the page comes back wide.
let mut b = a3.as_bytes().to_vec();
b[76..78].copy_from_slice(&2i16.to_le_bytes());
let fields = u32::from_le_bytes([b[72], b[73], b[74], b[75]]) | 1;
b[72..76].copy_from_slice(&fields.to_le_bytes());
let pg = crate::page(printer, Some(&DevMode::from_bytes(b).unwrap())).unwrap();
assert!((pg.paper.0 - 1190.6).abs() < 2.0 && (pg.paper.1 - 841.9).abs() < 2.0, "landscape A3: {:?}", pg.paper);
}
/// A real job through Windows' "Microsoft Print to PDF" driver, written to a file (needs that
/// printer installed; run by hand).
#[test]
#[ignore]
fn prints_through_a_real_driver_to_a_file() {
let out = std::env::temp_dir().join(format!("pc-winprint-{}.pdf", std::process::id()));
let _ = std::fs::remove_file(&out);
let settings = crate::default_settings("Microsoft Print to PDF").unwrap();
let mut bytes = settings.as_bytes().to_vec();
bytes[86..88].copy_from_slice(&5i16.to_le_bytes());
let fields = u32::from_le_bytes(bytes[72..76].try_into().unwrap()) | 0x100;
bytes[72..76].copy_from_slice(&fields.to_le_bytes());
let settings = DevMode::from_bytes(bytes).unwrap();
let (w, h) = (300u32, 200u32);
let data: Vec<u8> = (0..w * h).flat_map(|i| if (i % w) < w / 2 { [220, 30, 30] } else { [30, 30, 220] }).collect();
let out_s = out.to_string_lossy().into_owned();
let marks = crate::Marks { corner_crop: true, center_crop: true, registration: true };
let job = Job {
printer: "Microsoft Print to PDF",
settings: Some(&settings),
title: "PhotoCraft test",
output_file: Some(&out_s),
copies: 2,
color_policy: ColorPolicy::ApplicationManaged,
marks,
};
let done = crate::print(&job, &RgbImage { width: w, height: h, data: &data }, (72.0, 72.0, 216.0, 144.0)).unwrap();
assert_eq!(done.device_rect.2, done.dpi.0 as i32 * 3, "3 inches wide");
let pdf = std::fs::read(&out).unwrap();
assert!(pdf.starts_with(b"%PDF"), "the driver wrote a PDF");
assert!(pdf.len() > 1000, "the page has content");
// The driver vectorises our GDI line/ellipse calls as real PDF content: a job with every
// mark on is meaningfully bigger than the same image with marks off.
let no_marks_job = Job { marks: crate::Marks::default(), ..job };
let done2 = crate::print(&no_marks_job, &RgbImage { width: w, height: h, data: &data }, (72.0, 72.0, 216.0, 144.0)).unwrap();
let _ = done2;
let pdf_no_marks = std::fs::read(&out).unwrap();
assert!(pdf.len() > pdf_no_marks.len() + 100, "marks add vector content: {} vs {}", pdf.len(), pdf_no_marks.len());
if let Ok(evidence) = std::env::var("PHOTOCRAFT_WINPRINT_TEST_EVIDENCE") {
let evidence = std::path::Path::new(&evidence);
std::fs::create_dir_all(evidence).unwrap();
std::fs::write(evidence.join("copies2-driver5-marks.pdf"), &pdf).unwrap();
std::fs::write(evidence.join("copies2-driver5-no-marks.pdf"), &pdf_no_marks).unwrap();
}
let _ = std::fs::remove_file(&out);
}
/// Lists this machine's printers and their pages (needs real printers; run by hand).
#[test]
#[ignore]
fn lists_real_printers() {
for p in printers().unwrap() {
println!("{}{} {:?}", p.name, if p.default { " (default)" } else { "" }, page(&p.name, None));
}
}
}
-1
View File
@@ -47,7 +47,6 @@ pub const TABLE: &[(&str, Class)] = &[
// Pen tablet input (the one isolated `unsafe` helper: AppKit interop on macOS).
("tablet", Class::Standalone),
// Windows printing (an isolated `unsafe` helper: winspool and GDI).
("winprint", Class::Standalone),
("doc", Class::Layer(1)),
("ops", Class::Layer(2)),
("paint", Class::Layer(2)),