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, #[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, #[serde(default)] pub custom_height_mm: Option, // New optional direct-print fields #[serde(default)] pub printer_name: Option, #[serde(default)] pub show_dialog_if_unfound: Option, #[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(value: &Value) -> Result 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 { parse_flexible_json(layout_json_value) } pub fn parse_printer_settings(settings_value: &Value) -> Result { parse_flexible_json(settings_value) }