Skip to main content

winprint_kit/
print.rs

1//! Print options, capability probing, and print ticket building.
2
3use winprint_ext::printer::PrinterDevice;
4use winprint_ext::ticket::{
5    Copies, FeatureOptionPack, PrintCapabilities, PrintTicket, PrintTicketBuilder,
6};
7
8#[derive(Debug, Clone, serde::Deserialize)]
9#[serde(default, rename_all = "PascalCase")]
10pub struct PrintOptions {
11    #[serde(default = "default_copies")]
12    pub copies: u16,
13    pub page_size: Option<String>,
14    pub duplex: Option<String>,
15    pub color: Option<String>,
16    pub orientation: Option<String>,
17    pub resolution: Option<String>,
18}
19
20impl Default for PrintOptions {
21    fn default() -> Self {
22        Self {
23            copies: 1,
24            page_size: None,
25            duplex: None,
26            color: None,
27            orientation: None,
28            resolution: None,
29        }
30    }
31}
32
33fn default_copies() -> u16 {
34    1
35}
36
37#[derive(Debug, Clone, serde::Serialize)]
38pub struct PageSizeDetail {
39    pub display_name: String,
40    pub width_inches: f64,
41    pub height_inches: f64,
42}
43
44#[derive(Debug, Clone, serde::Serialize)]
45#[serde(rename_all = "camelCase")]
46pub struct PrinterCapabilities {
47    pub page_size: Vec<String>,
48    pub duplex: Vec<String>,
49    pub color: Vec<String>,
50    pub orientation: Vec<String>,
51    pub resolution: Vec<String>,
52    pub page_sizes: Vec<PageSizeDetail>,
53}
54
55pub(crate) fn extract_capabilities(caps: &PrintCapabilities) -> PrinterCapabilities {
56    PrinterCapabilities {
57        page_size: caps
58            .page_media_sizes()
59            .filter_map(|o| o.display_name().map(|s| s.to_string()))
60            .collect(),
61        duplex: caps
62            .duplexes()
63            .filter_map(|o| o.display_name().map(|s| s.to_string()))
64            .collect(),
65        color: caps
66            .page_output_colors()
67            .filter_map(|o| o.display_name().map(|s| s.to_string()))
68            .collect(),
69        orientation: caps
70            .page_orientations()
71            .filter_map(|o| o.display_name().map(|s| s.to_string()))
72            .collect(),
73        resolution: caps
74            .page_resolutions()
75            .filter_map(|o| o.display_name().map(|s| s.to_string()))
76            .collect(),
77        page_sizes: caps
78            .page_media_sizes()
79            .filter_map(|o| {
80                let name = o.display_name()?;
81                let size = o.size();
82                Some(PageSizeDetail {
83                    display_name: name.to_string(),
84                    width_inches: size.width_in_micron() as f64 / 25400.0,
85                    height_inches: size.height_in_micron() as f64 / 25400.0,
86                })
87            })
88            .collect(),
89    }
90}
91
92pub(crate) fn build_print_ticket(
93    device: &PrinterDevice,
94    capabilities: &PrintCapabilities,
95    options: &PrintOptions,
96) -> Result<PrintTicket, String> {
97    if options.copies == 0 {
98        return Err("copies must be greater than 0".to_string());
99    }
100    let mut builder = PrintTicketBuilder::new(device)
101        .map_err(|e| format!("Failed to create print ticket builder: {}", e))?;
102
103    builder
104        .merge(Copies(options.copies))
105        .map_err(|e| format!("Failed to set copies: {}", e))?;
106
107    if let Some(ref input) = options.page_size {
108        let items: Vec<_> = capabilities.page_media_sizes().collect();
109        let option = find_option(&items, input).ok_or_else(|| {
110            format!(
111                "Printer does not support paper size '{}'. Supported: {}",
112                input,
113                list_display_names(&items)
114            )
115        })?;
116        builder
117            .merge(option)
118            .map_err(|e| format!("Failed to set page size: {}", e))?;
119    }
120
121    if let Some(ref input) = options.duplex {
122        let items: Vec<_> = capabilities.duplexes().collect();
123        let option = find_option(&items, input).ok_or_else(|| {
124            format!(
125                "Printer does not support duplex '{}'. Supported: {}",
126                input,
127                list_display_names(&items)
128            )
129        })?;
130        builder
131            .merge(option)
132            .map_err(|e| format!("Failed to set duplex: {}", e))?;
133    }
134
135    if let Some(ref input) = options.color {
136        let items: Vec<_> = capabilities.page_output_colors().collect();
137        let option = find_option(&items, input).ok_or_else(|| {
138            format!(
139                "Printer does not support color mode '{}'. Supported: {}",
140                input,
141                list_display_names(&items)
142            )
143        })?;
144        builder
145            .merge(option)
146            .map_err(|e| format!("Failed to set color: {}", e))?;
147    }
148
149    if let Some(ref input) = options.orientation {
150        let items: Vec<_> = capabilities.page_orientations().collect();
151        let option = find_option(&items, input).ok_or_else(|| {
152            format!(
153                "Printer does not support orientation '{}'. Supported: {}",
154                input,
155                list_display_names(&items)
156            )
157        })?;
158        builder
159            .merge(option)
160            .map_err(|e| format!("Failed to set orientation: {}", e))?;
161    }
162
163    if let Some(ref input) = options.resolution {
164        let items: Vec<_> = capabilities.page_resolutions().collect();
165        let option = find_option(&items, input).ok_or_else(|| {
166            format!(
167                "Printer does not support resolution '{}'. Supported: {}",
168                input,
169                list_display_names(&items)
170            )
171        })?;
172        builder
173            .merge(option)
174            .map_err(|e| format!("Failed to set resolution: {}", e))?;
175    }
176
177    builder
178        .build()
179        .map_err(|e| format!("Failed to build print ticket: {}", e))
180}
181
182fn find_option<T: FeatureOptionPack + Clone>(options: &[T], input: &str) -> Option<T> {
183    let normalized = input.trim().to_lowercase();
184    options
185        .iter()
186        .find(|o| {
187            o.display_name()
188                .map(|d| d.trim().to_lowercase() == normalized)
189                .unwrap_or(false)
190        })
191        .cloned()
192}
193
194fn list_display_names<T: FeatureOptionPack>(options: &[T]) -> String {
195    options
196        .iter()
197        .filter_map(|o| o.display_name())
198        .collect::<Vec<_>>()
199        .join(", ")
200}
201