1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
|
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use serde_json::Value;
// This file now centralizes parsing logic that was previously in system_print.rs
// It helps decouple the UI and plugins from the direct implementation of parsing.
/// Represents the layout of a label, deserialized from JSON.
// NOTE: This assumes LabelLayout is defined in your renderer module.
// If not, you might need to move or publicly export it.
use super::renderer::LabelLayout;
/// Represents printer-specific settings.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PrinterSettings {
#[serde(default = "default_paper_size")]
pub paper_size: String,
#[serde(default = "default_orientation")]
pub orientation: String,
#[serde(default)]
pub margins: PrinterMargins,
#[serde(default = "default_color")]
pub color: bool,
#[serde(default = "default_quality")]
pub quality: String,
#[serde(default = "default_copies")]
pub copies: u32,
#[serde(default)]
pub duplex: bool,
#[serde(default)]
pub center: Option<CenterMode>,
#[serde(default)]
pub center_disabled: bool,
#[serde(default = "default_scale_mode")]
pub scale_mode: ScaleMode,
#[serde(default = "default_scale_factor")]
pub scale_factor: f32,
#[serde(default)]
pub custom_width_mm: Option<f32>,
#[serde(default)]
pub custom_height_mm: Option<f32>,
// New optional direct-print fields
#[serde(default)]
pub printer_name: Option<String>,
#[serde(default)]
pub show_dialog_if_unfound: Option<bool>,
#[serde(default)]
pub compatibility_mode: bool,
}
impl Default for PrinterSettings {
fn default() -> Self {
Self {
paper_size: default_paper_size(),
orientation: default_orientation(),
margins: PrinterMargins::default(),
color: default_color(),
quality: default_quality(),
copies: default_copies(),
duplex: false,
center: None,
center_disabled: false,
scale_mode: default_scale_mode(),
scale_factor: default_scale_factor(),
custom_width_mm: None,
custom_height_mm: None,
printer_name: None,
show_dialog_if_unfound: None,
compatibility_mode: false,
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CenterMode {
None,
Horizontal,
Vertical,
Both,
}
impl CenterMode {
pub fn includes_horizontal(self) -> bool {
matches!(self, CenterMode::Horizontal | CenterMode::Both)
}
pub fn includes_vertical(self) -> bool {
matches!(self, CenterMode::Vertical | CenterMode::Both)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PrinterMargins {
pub top: f32,
pub right: f32,
pub bottom: f32,
pub left: f32,
}
// Default value functions for PrinterSettings
fn default_paper_size() -> String {
"A4".to_string()
}
fn default_orientation() -> String {
"portrait".to_string()
}
#[allow(dead_code)]
fn default_scale() -> f32 {
1.0
}
fn default_color() -> bool {
false
}
fn default_quality() -> String {
"high".to_string()
}
fn default_copies() -> u32 {
1
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ScaleMode {
Fit,
FitX,
FitY,
MaxBoth,
MaxX,
MaxY,
Manual,
}
fn default_scale_mode() -> ScaleMode {
ScaleMode::Fit
}
fn default_scale_factor() -> f32 {
1.0
}
impl PrinterSettings {
pub fn canonicalize_dimensions(&mut self) {
// No-op: dimensions are used as specified
}
pub fn get_dimensions_mm(&self) -> (f32, f32) {
if let (Some(w), Some(h)) = (self.custom_width_mm, self.custom_height_mm) {
// For custom dimensions, swap if landscape to create rotated PDF
let orientation = self.orientation.to_ascii_lowercase();
let result = if orientation == "landscape" {
// Landscape: swap dimensions for PDF (rotate 90°)
(h, w)
} else {
// Portrait: use as-is
(w, h)
};
log::info!(
"get_dimensions_mm: custom {}×{} mm, orientation='{}' → PDF {}×{} mm",
w,
h,
self.orientation,
result.0,
result.1
);
result
} else {
// Standard paper sizes
let (width, height) = match self.paper_size.as_str() {
"A4" => (210.0, 297.0),
"A5" => (148.0, 210.0),
"Letter" => (215.9, 279.4),
_ => (100.0, 150.0), // Default
};
if self.orientation == "landscape" {
(height, width)
} else {
(width, height)
}
}
}
}
/// Utility function to parse a JSON value that might be a raw string,
/// a base64-encoded string, or a direct JSON object.
fn parse_flexible_json<T>(value: &Value) -> Result<T>
where
T: for<'de> Deserialize<'de>,
{
match value {
Value::String(s) => {
if let Ok(parsed) = serde_json::from_str(s) {
return Ok(parsed);
}
match base64::Engine::decode(&base64::engine::general_purpose::STANDARD, s) {
Ok(decoded_bytes) => {
let decoded_str = String::from_utf8(decoded_bytes)
.context("Decoded base64 is not valid UTF-8")?;
serde_json::from_str(&decoded_str)
.context("Failed to parse base64-decoded JSON")
}
Err(_) => anyhow::bail!("Value is not valid JSON or base64-encoded JSON"),
}
}
json_obj => serde_json::from_value(json_obj.clone())
.context("Failed to parse value as a direct JSON object"),
}
}
pub fn parse_layout_json(layout_json_value: &Value) -> Result<LabelLayout> {
parse_flexible_json(layout_json_value)
}
pub fn parse_printer_settings(settings_value: &Value) -> Result<PrinterSettings> {
parse_flexible_json(settings_value)
}
|