1use 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