From 8323fdd73272a2882781aba3c499ba0be3dff2a6 Mon Sep 17 00:00:00 2001 From: UMTS at Teleco Date: Sat, 13 Dec 2025 02:51:15 +0100 Subject: committing to insanity --- src/core/print/printer_manager.rs | 228 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 src/core/print/printer_manager.rs (limited to 'src/core/print/printer_manager.rs') diff --git a/src/core/print/printer_manager.rs b/src/core/print/printer_manager.rs new file mode 100644 index 0000000..e8dd7fd --- /dev/null +++ b/src/core/print/printer_manager.rs @@ -0,0 +1,228 @@ +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() + } +} -- cgit v1.2.3-70-g09d2