use printers::common::base::job::PrinterJobOptions; use printers::{get_default_printer, get_printer_by_name, get_printers}; use std::path::Path; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use crate::core::print::parsing::PrinterSettings; #[derive(Clone)] pub struct PrinterInfo { pub name: String, #[allow(dead_code)] pub is_default: bool, } pub struct PrinterManager { available_printers: Vec, last_refresh: Instant, } impl PrinterManager { pub fn new() -> Self { let mut manager = Self { available_printers: Vec::new(), last_refresh: Instant::now() - Duration::from_secs(3600), // Force refresh on first call }; manager.refresh_printers(); manager } /// Refresh the list of available printers from the system. pub fn refresh_printers(&mut self) { log::info!("Refreshing printer list..."); let default_printer = get_default_printer(); let default_name = default_printer.as_ref().map(|p| p.name.clone()); self.available_printers = get_printers() .into_iter() .map(|p| { let name = p.name.clone(); let is_default = default_name.as_ref() == Some(&name); PrinterInfo { name, is_default } }) .collect(); self.last_refresh = Instant::now(); log::info!("Found {} printers.", self.available_printers.len()); } /// Get a list of all available printers, refreshing if cache is stale. pub fn get_printers(&mut self) -> &[PrinterInfo] { if self.last_refresh.elapsed() > Duration::from_secs(60) { self.refresh_printers(); } &self.available_printers } /// Print a PDF file to the specified printer. pub fn print_pdf_to( &self, printer_name: &str, pdf_path: &Path, printer_settings: Option<&PrinterSettings>, ) -> Result<(), String> { let normalized_settings = printer_settings.map(|ps| { let mut copy = ps.clone(); copy.canonicalize_dimensions(); copy }); let effective_settings = normalized_settings.as_ref(); if let Some(ps) = effective_settings { let (page_w, page_h) = ps.get_dimensions_mm(); log::info!( "Attempting to print '{}' to printer '{}' (paper_size={}, orientation={}, page={}×{} mm)", pdf_path.display(), printer_name, ps.paper_size, ps.orientation, page_w, page_h ); } else { log::info!( "Attempting to print '{}' to printer '{}' without explicit printer settings", pdf_path.display(), printer_name ); } let printer = get_printer_by_name(printer_name) .ok_or_else(|| format!("Printer '{}' not found on the system.", printer_name))?; let pdf_path_str = pdf_path .to_str() .ok_or_else(|| format!("PDF path '{}' contains invalid UTF-8", pdf_path.display()))?; let owned_options = Self::build_job_options(effective_settings); let borrowed_options: Vec<(&str, &str)> = owned_options .iter() .map(|(key, value)| (key.as_str(), value.as_str())) .collect(); let result = if borrowed_options.is_empty() { printer.print_file(pdf_path_str, PrinterJobOptions::none()) } else { log::info!( "Applying {} print option(s) via CUPS", borrowed_options.len() ); for (key, value) in borrowed_options.iter() { log::debug!(" job option: {}={}", key, value); } let job_options = PrinterJobOptions { name: Some("BeepZone Label"), raw_properties: borrowed_options.as_slice(), }; printer.print_file(pdf_path_str, job_options) }; result .map(|_| ()) .map_err(|e| format!("Failed to send print job: {}", e)) } fn build_job_options(printer_settings: Option<&PrinterSettings>) -> Vec<(String, String)> { let mut owned: Vec<(String, String)> = Vec::new(); if let Some(ps) = printer_settings { let compat_mode = ps.compatibility_mode; // In strict compatibility mode, send NO job options at all // This avoids triggering buggy printer filters if compat_mode { log::info!("Compatibility mode enabled - sending no CUPS job options"); return owned; } // Determine media first (always in portrait orientation) if let Some(media_value) = Self::media_to_cups(ps) { owned.push(("media".to_string(), media_value.clone())); owned.push(("PageSize".to_string(), media_value)); } // Send orientation-requested to tell CUPS to rotate the media if let Some(orientation_code) = Self::orientation_to_cups(ps) { owned.push(("orientation-requested".to_string(), orientation_code)); } if ps.copies > 1 { owned.push(("copies".to_string(), ps.copies.to_string())); } } owned } fn orientation_to_cups(ps: &PrinterSettings) -> Option { let orientation_raw = ps.orientation.trim(); if orientation_raw.is_empty() { return None; } match orientation_raw.to_ascii_lowercase().as_str() { "portrait" => Some("3".to_string()), "landscape" => Some("4".to_string()), "reverse_landscape" | "reverse-landscape" => Some("5".to_string()), "reverse_portrait" | "reverse-portrait" => Some("6".to_string()), _ => None, } } fn media_to_cups(ps: &PrinterSettings) -> Option { if let (Some(w), Some(h)) = (ps.custom_width_mm, ps.custom_height_mm) { // For custom sizes, use dimensions exactly as specified // The user knows their media dimensions and orientation needs let width_str = Self::format_mm(w); let height_str = Self::format_mm(h); return Some(format!("Custom.{width_str}x{height_str}mm")); } let paper = ps.paper_size.trim(); if paper.is_empty() { return None; } Some(paper.to_string()) } fn format_mm(value: f32) -> String { let rounded = (value * 100.0).round() / 100.0; if (rounded - rounded.round()).abs() < 0.005 { format!("{:.0}", rounded.round()) } else { format!("{:.2}", rounded) } } } // A thread-safe, shared wrapper for the PrinterManager #[derive(Clone)] pub struct SharedPrinterManager(Arc>); impl SharedPrinterManager { pub fn new() -> Self { Self(Arc::new(Mutex::new(PrinterManager::new()))) } pub fn get_printers(&self) -> Vec { self.0.lock().unwrap().get_printers().to_vec() } pub fn print_pdf_to( &self, printer_name: &str, pdf_path: &Path, printer_settings: Option<&PrinterSettings>, ) -> Result<(), String> { self.0 .lock() .unwrap() .print_pdf_to(printer_name, pdf_path, printer_settings) } } impl Default for SharedPrinterManager { fn default() -> Self { Self::new() } }