aboutsummaryrefslogtreecommitdiff
path: root/src/core/print/printer_manager.rs
blob: e8dd7fd2b0d5c8e3bd0e2e5d09b0f195b9d761a4 (plain)
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
220
221
222
223
224
225
226
227
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()
    }
}