aboutsummaryrefslogtreecommitdiff
path: root/src/core/print/printer_manager.rs
diff options
context:
space:
mode:
authorUMTS at Teleco <crt@teleco.ch>2025-12-13 02:51:15 +0100
committerUMTS at Teleco <crt@teleco.ch>2025-12-13 02:51:15 +0100
commit8323fdd73272a2882781aba3c499ba0be3dff2a6 (patch)
treeffbf86473933e69cfaeef30d5c6ea7e5b494856c /src/core/print/printer_manager.rs
committing to insanityHEADmaster
Diffstat (limited to 'src/core/print/printer_manager.rs')
-rw-r--r--src/core/print/printer_manager.rs228
1 files changed, 228 insertions, 0 deletions
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<PrinterInfo>,
+ 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<String> {
+ 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<String> {
+ 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<Mutex<PrinterManager>>);
+
+impl SharedPrinterManager {
+ pub fn new() -> Self {
+ Self(Arc::new(Mutex::new(PrinterManager::new())))
+ }
+
+ pub fn get_printers(&self) -> Vec<PrinterInfo> {
+ 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()
+ }
+}