use anyhow::Result; use eframe::egui; use serde_json::Value; use std::collections::HashMap; use crate::api::ApiClient; use crate::core::print::parsing::{parse_layout_json, parse_printer_settings, PrinterSettings}; use crate::core::print::plugins::pdf::PdfPlugin; use crate::core::print::printer_manager::{PrinterInfo, SharedPrinterManager}; use crate::core::print::renderer::LabelRenderer; use poll_promise::Promise; use std::path::PathBuf; #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum PaperSizeOverride { UseSaved, A4, Letter, Custom, } /// Print options selected by user #[derive(Debug, Clone)] pub struct PrintOptions { pub printer_id: Option, pub printer_name: String, pub label_template_id: Option, pub label_template_name: String, pub copies: i32, } impl Default for PrintOptions { fn default() -> Self { Self { printer_id: None, printer_name: String::new(), label_template_id: None, label_template_name: String::new(), copies: 1, } } } /// Print dialog for selecting printer, template, and preview pub struct PrintDialog { options: PrintOptions, pub asset_data: HashMap, printers: Vec, templates: Vec, renderer: Option, preview_scale: f32, error_message: Option, loading: bool, // Promise for handling async PDF export pdf_export_promise: Option>>, // OS printer fallback popup os_popup_visible: bool, os_printers: Vec, os_selected_index: usize, os_print_path: Option, os_error_message: Option, os_base_settings: Option, os_renderer: Option, os_size_override: PaperSizeOverride, os_custom_width_mm: f32, os_custom_height_mm: f32, } impl PrintDialog { /// Create new print dialog with asset data pub fn new(asset_data: HashMap) -> Self { Self { options: PrintOptions::default(), asset_data, printers: Vec::new(), templates: Vec::new(), renderer: None, preview_scale: 3.78, // Default scale: 1mm = 3.78px at 96 DPI error_message: None, loading: false, pdf_export_promise: None, os_popup_visible: false, os_printers: Vec::new(), os_selected_index: 0, os_print_path: None, os_error_message: None, os_base_settings: None, os_renderer: None, os_size_override: PaperSizeOverride::UseSaved, os_custom_width_mm: 0.0, os_custom_height_mm: 0.0, } } /// Initialize with default printer and template if available pub fn with_defaults( mut self, default_printer_id: Option, label_template_id: Option, last_printer_id: Option, ) -> Self { // Prefer last-used printer if available, otherwise fall back to default self.options.printer_id = last_printer_id.or(default_printer_id); // Label template is *not* persisted across sessions; if none is set on the asset, // the dialog will require the user to choose one. self.options.label_template_id = label_template_id; self } /// Load printers and templates from API pub fn load_data(&mut self, api_client: &ApiClient) -> Result<()> { self.loading = true; self.error_message = None; // Load printers match crate::core::tables::get_printers(api_client) { Ok(printers) => self.printers = printers, Err(e) => { self.error_message = Some(format!("Failed to load printers: {}", e)); return Err(e); } } // Load templates match crate::core::tables::get_label_templates(api_client) { Ok(templates) => self.templates = templates, Err(e) => { self.error_message = Some(format!("Failed to load templates: {}", e)); return Err(e); } } // Set default selections if IDs provided if let Some(printer_id) = self.options.printer_id { if let Some(printer) = self .printers .iter() .find(|p| p.get("id").and_then(|v| v.as_i64()) == Some(printer_id)) { self.options.printer_name = printer .get("printer_name") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(); // Fetch printer_settings for preview sizing/orientation let resp = api_client.select( "printer_settings", Some(vec!["printer_settings".into()]), Some(serde_json::json!({"id": printer_id})), None, Some(1), )?; if let Some(first) = resp.data.as_ref().and_then(|d| d.get(0)) { if let Some(ps_val) = first.get("printer_settings") { if let Ok(ps) = parse_printer_settings(ps_val) { self.os_base_settings = Some(ps); } } } } } if let Some(template_id) = self.options.label_template_id { if let Some(template) = self .templates .iter() .find(|t| t.get("id").and_then(|v| v.as_i64()) == Some(template_id)) { let template_name = template .get("template_name") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(); self.options.label_template_name = template_name.clone(); // Load renderer for preview if let Some(layout_json) = template.get("layout_json").and_then(|v| v.as_str()) { if layout_json.trim().is_empty() { log::warn!("Label template '{}' has empty layout_json", template_name); self.error_message = Some("This label template has no layout defined. Please edit the template in Label Templates view.".to_string()); } else { match LabelRenderer::from_json(layout_json) { Ok(renderer) => self.renderer = Some(renderer), Err(e) => { log::warn!( "Failed to parse label layout for '{}': {}", template_name, e ); self.error_message = Some(format!("Invalid template layout JSON. Please fix in Label Templates view.\n\nError: {}", e)); } } } } else { log::warn!( "Label template '{}' missing layout_json field", template_name ); self.error_message = Some( "This label template is missing layout data. Please edit the template." .to_string(), ); } } } self.loading = false; Ok(()) } /// Show the dialog and return true if user clicked Print and the action is complete pub fn show( &mut self, ctx: &egui::Context, open: &mut bool, api_client: Option<&ApiClient>, ) -> bool { let mut print_action_complete = false; let mut close_dialog = false; if let Some(_response) = egui::Window::new("Print Label") .open(open) .resizable(true) .default_width(600.0) .default_height(500.0) .show(ctx, |ui| { // Load data if not loaded yet if self.printers.is_empty() && !self.loading && api_client.is_some() { if let Err(e) = self.load_data(api_client.unwrap()) { log::error!("Failed to load print data: {}", e); } } // Show error if any if let Some(error) = &self.error_message { ui.colored_label(egui::Color32::RED, error); ui.add_space(8.0); } // Show loading spinner if self.loading { ui.horizontal(|ui| { ui.spinner(); ui.label("Loading printers and templates..."); }); return; } // Options panel egui::ScrollArea::vertical() .id_salt("print_options_scroll") .show(ui, |ui| { self.show_options(ui); ui.add_space(12.0); self.show_preview(ui); }); // Handle PDF export promise if let Some(promise) = &self.pdf_export_promise { if let Some(result) = promise.ready() { match result { Some(path) => { log::info!("PDF export promise ready, path: {:?}", path); // The file dialog is done, now we can save the file. // We need the ApiClient and other details again. if let Some(client) = api_client { if let Err(e) = self.execute_pdf_export(path, client) { self.error_message = Some(format!("Failed to export PDF: {}", e)); } else { // Successfully exported, close dialog print_action_complete = true; close_dialog = true; } } else { self.error_message = Some( "API client not available for PDF export.".to_string(), ); } } None => { // User cancelled the dialog log::info!("PDF export cancelled by user."); } } self.pdf_export_promise = None; // Consume the promise } else { ui.spinner(); ui.label("Waiting for file path..."); } } // Bottom buttons ui.add_space(8.0); ui.separator(); ui.horizontal(|ui| { if ui.button("Cancel").clicked() { close_dialog = true; } ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { let can_print = self.options.printer_id.is_some() && self.options.label_template_id.is_some() && self.options.copies > 0 && self.pdf_export_promise.is_none(); // Disable while waiting for path ui.add_enabled_ui(can_print, |ui| { if ui.button("Print").clicked() { if let Some(client) = api_client { match self.execute_print(client) { Ok(completed) => { if completed { print_action_complete = true; close_dialog = true; } // if not completed, dialog stays open for promise } Err(e) => { self.error_message = Some(format!("Print error: {}", e)); } } } else { self.error_message = Some("API Client not available.".to_string()); } } }); }); }); }) { // Window was shown } // Render OS printer fallback popup if requested if self.os_popup_visible { let mut close_os_popup = false; let mut keep_open_flag = true; egui::Window::new("Select System Printer") .collapsible(false) .resizable(true) .default_width(420.0) .open(&mut keep_open_flag) .show(ctx, |ui| { if let Some(err) = &self.os_error_message { ui.colored_label(egui::Color32::RED, err); ui.separator(); } if self.os_printers.is_empty() { let mgr = SharedPrinterManager::new(); self.os_printers = mgr.get_printers(); if let Some(base) = &self.os_base_settings { if let Some(target_name) = &base.printer_name { if let Some((idx, _)) = self .os_printers .iter() .enumerate() .find(|(_, p)| &p.name == target_name) { self.os_selected_index = idx; } } } } if self.os_printers.is_empty() { ui.label("No system printers found."); } else { if self.os_selected_index >= self.os_printers.len() { self.os_selected_index = 0; } let current = self .os_printers .get(self.os_selected_index) .map(|p| p.name.clone()) .unwrap_or_default(); egui::ComboBox::from_id_salt("os_printers_combo") .selected_text(if current.is_empty() { "Select printer" } else { ¤t }) .show_ui(ui, |ui| { for (i, p) in self.os_printers.iter().enumerate() { if ui .selectable_label(i == self.os_selected_index, &p.name) .clicked() { self.os_selected_index = i; } } }); } ui.separator(); if let Some(base) = &self.os_base_settings { let saved_label = format!( "Use saved ({})", base.paper_size.as_str() ); egui::ComboBox::from_id_salt("os_size_override") .selected_text(match self.os_size_override { PaperSizeOverride::UseSaved => saved_label.clone(), PaperSizeOverride::A4 => "A4 (210×297 mm)".into(), PaperSizeOverride::Letter => "Letter (215.9×279.4 mm)".into(), PaperSizeOverride::Custom => "Custom size".into(), }) .show_ui(ui, |ui| { if ui .selectable_value( &mut self.os_size_override, PaperSizeOverride::UseSaved, saved_label, ) .clicked() { self.os_error_message = None; } if ui .selectable_value( &mut self.os_size_override, PaperSizeOverride::A4, "A4 (210×297 mm)", ) .clicked() { self.os_error_message = None; } if ui .selectable_value( &mut self.os_size_override, PaperSizeOverride::Letter, "Letter (215.9×279.4 mm)", ) .clicked() { self.os_error_message = None; } if ui .selectable_value( &mut self.os_size_override, PaperSizeOverride::Custom, "Custom size", ) .clicked() { if base.custom_width_mm.is_some() && base.custom_height_mm.is_some() { let (w, h) = base.get_dimensions_mm(); self.os_custom_width_mm = w; self.os_custom_height_mm = h; } else { self.os_custom_width_mm = 0.0; self.os_custom_height_mm = 0.0; } self.os_error_message = None; } }); if matches!(self.os_size_override, PaperSizeOverride::Custom) { ui.vertical(|ui| { ui.label("Custom page size (mm)"); ui.horizontal(|ui| { ui.label("Width:"); ui.add( egui::DragValue::new(&mut self.os_custom_width_mm) .range(10.0..=600.0) .suffix(" mm"), ); ui.label("Height:"); ui.add( egui::DragValue::new(&mut self.os_custom_height_mm) .range(10.0..=600.0) .suffix(" mm"), ); }); }); } } ui.separator(); ui.horizontal(|ui| { if ui.button("Cancel").clicked() { self.os_print_path = None; self.os_base_settings = None; close_os_popup = true; } ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { let can_print = !self.os_printers.is_empty() && self .os_printers .get(self.os_selected_index) .is_some(); ui.add_enabled_ui(can_print, |ui| { if ui.button("Print").clicked() { let selected_name = self .os_printers .get(self.os_selected_index) .map(|p| p.name.clone()); if let Some(name) = selected_name { match self.print_via_os_popup(&name) { Ok(true) => { self.os_print_path = None; self.os_base_settings = None; close_os_popup = true; print_action_complete = true; close_dialog = true; } Ok(false) => { /* not used: function only returns true on success */ } Err(e) => { self.os_error_message = Some(e); } } } } }); }); }); }); // Apply window close state after rendering if !keep_open_flag { close_os_popup = true; } if close_os_popup { self.os_popup_visible = false; self.os_base_settings = None; } } if close_dialog { *open = false; } print_action_complete } /// Show options section fn show_options(&mut self, ui: &mut egui::Ui) { ui.heading("Print Options"); ui.add_space(8.0); egui::Grid::new("print_options_grid") .num_columns(2) .spacing([8.0, 8.0]) .show(ui, |ui| { // Printer selection ui.label("Printer:"); egui::ComboBox::from_id_salt("printer_select") .selected_text(&self.options.printer_name) .width(300.0) .show_ui(ui, |ui| { for printer in &self.printers { let printer_id = printer.get("id").and_then(|v| v.as_i64()); let printer_name = printer .get("printer_name") .and_then(|v| v.as_str()) .unwrap_or("Unknown"); if ui .selectable_label( self.options.printer_id == printer_id, printer_name, ) .clicked() { self.options.printer_id = printer_id; self.options.printer_name = printer_name.to_string(); // Try to parse printer settings for preview (if provided by the DB row) if let Some(ps_val) = printer.get("printer_settings") { match parse_printer_settings(ps_val) { Ok(ps) => { self.os_base_settings = Some(ps); } Err(e) => { log::warn!( "Failed to parse printer_settings for preview: {}", e ); self.os_base_settings = None; } } } else { self.os_base_settings = None; } } } }); ui.end_row(); // Template selection ui.label("Label Template:"); egui::ComboBox::from_id_salt("template_select") .selected_text(&self.options.label_template_name) .width(300.0) .show_ui(ui, |ui| { for template in &self.templates { let template_id = template.get("id").and_then(|v| v.as_i64()); let template_name = template .get("template_name") .and_then(|v| v.as_str()) .unwrap_or("Unknown"); if ui .selectable_label( self.options.label_template_id == template_id, template_name, ) .clicked() { self.options.label_template_id = template_id; self.options.label_template_name = template_name.to_string(); // Update renderer if let Some(layout_json) = template.get("layout_json").and_then(|v| v.as_str()) { match LabelRenderer::from_json(layout_json) { Ok(renderer) => { self.renderer = Some(renderer); self.error_message = None; } Err(e) => { log::warn!("Failed to parse label layout: {}", e); self.error_message = Some(format!("Invalid template: {}", e)); self.renderer = None; } } } } } }); ui.end_row(); // Number of copies ui.label("Copies:"); ui.add(egui::DragValue::new(&mut self.options.copies).range(1..=99)); ui.end_row(); }); } /// Show preview section fn show_preview(&mut self, ui: &mut egui::Ui) { ui.add_space(8.0); ui.heading("Preview"); ui.add_space(8.0); // Preview scale control ui.horizontal(|ui| { ui.label("Scale:"); ui.add(egui::Slider::new(&mut self.preview_scale, 2.0..=8.0).suffix("x")); }); ui.add_space(8.0); // Render preview if let Some(renderer) = &self.renderer { egui::ScrollArea::both() // Enable both horizontal and vertical scrolling .max_height(300.0) .auto_shrink([false, false]) // Don't shrink in either direction .show(ui, |ui| { egui::Frame::new() .fill(egui::Color32::from_gray(240)) .stroke(egui::Stroke::new(1.0, egui::Color32::from_gray(200))) .inner_margin(16.0) .show(ui, |ui| { if let Err(e) = renderer.render_preview( ui, &self.asset_data, self.preview_scale, self.os_base_settings.as_ref(), ) { ui.colored_label( egui::Color32::RED, format!("Preview error: {}", e), ); } }); }); } else { ui.colored_label( egui::Color32::from_gray(150), "Select a label template to see preview", ); } } /// Get asset data reference pub fn asset_data(&self) -> &HashMap { &self.asset_data } /// Get current print options pub fn options(&self) -> &PrintOptions { &self.options } /// Executes the actual PDF file saving. This is called after the promise resolves. fn execute_pdf_export(&self, path: &PathBuf, api_client: &ApiClient) -> Result<()> { let template_id = self .options .label_template_id .ok_or_else(|| anyhow::anyhow!("No template selected"))?; let printer_id = self .options .printer_id .ok_or_else(|| anyhow::anyhow!("No printer selected"))?; // Fetch template let template_resp = api_client.select( "label_templates", Some(vec!["layout_json".into()]), Some(serde_json::json!({"id": template_id})), None, Some(1), )?; let template_data = template_resp .data .as_ref() .and_then(|d| d.get(0)) .ok_or_else(|| anyhow::anyhow!("Template not found"))?; let layout_json = template_data .get("layout_json") .ok_or_else(|| anyhow::anyhow!("No layout JSON"))?; let layout = parse_layout_json(layout_json)?; // Fetch printer settings let printer_resp = api_client.select( "printer_settings", Some(vec!["printer_settings".into()]), Some(serde_json::json!({"id": printer_id})), None, Some(1), )?; let printer_data = printer_resp .data .as_ref() .and_then(|d| d.get(0)) .ok_or_else(|| anyhow::anyhow!("Printer settings not found"))?; let printer_settings_value = printer_data .get("printer_settings") .ok_or_else(|| anyhow::anyhow!("No printer settings JSON"))?; let printer_settings = parse_printer_settings(printer_settings_value)?; // Generate and save PDF let renderer = LabelRenderer::new(layout); let (doc, _, _) = renderer.generate_pdf_with_settings(&self.asset_data, &printer_settings)?; let pdf_plugin = PdfPlugin::new(); pdf_plugin.export_pdf(doc, path) } /// Execute print job - handles all the loading, parsing, and printing. /// Returns Ok(true) if the job is complete, Ok(false) if it's pending (e.g., PDF export). pub fn execute_print(&mut self, api_client: &ApiClient) -> Result { let printer_id = self .options .printer_id .ok_or_else(|| anyhow::anyhow!("No printer selected"))?; let template_id = self .options .label_template_id .ok_or_else(|| anyhow::anyhow!("No template selected"))?; log::info!( "Executing print: printer_id={}, template_id={}, copies={}", printer_id, template_id, self.options.copies ); // 1. Load printer settings and plugin info let printer_resp = api_client.select( "printer_settings", Some(vec![ "printer_name".into(), "printer_settings".into(), "printer_plugin".into(), ]), Some(serde_json::json!({ "id": printer_id })), None, Some(1), )?; let printer_data = printer_resp .data .as_ref() .and_then(|d| if !d.is_empty() { Some(d) } else { None }) .ok_or_else(|| anyhow::anyhow!("Printer {} not found", printer_id))?; let printer_plugin = printer_data[0] .get("printer_plugin") .and_then(|v| v.as_str()) .unwrap_or("System"); let printer_settings_value = printer_data[0] .get("printer_settings") .ok_or_else(|| anyhow::anyhow!("printer_settings field not found"))?; let printer_settings = parse_printer_settings(printer_settings_value)?; // 2. Load label template layout let template_resp = api_client.select( "label_templates", Some(vec!["layout_json".into()]), Some(serde_json::json!({"id": template_id})), None, Some(1), )?; let template_data = template_resp .data .as_ref() .and_then(|d| if !d.is_empty() { Some(d) } else { None }) .ok_or_else(|| anyhow::anyhow!("Label template {} not found", template_id))?; let layout_json_value = template_data[0] .get("layout_json") .ok_or_else(|| anyhow::anyhow!("layout_json field not found in template"))?; let layout = parse_layout_json(layout_json_value)?; // 3. Dispatch to appropriate plugin based on the printer_plugin field match printer_plugin { "PDF" => { // Use a promise to handle the blocking file dialog in a background thread let promise = Promise::spawn_thread("pdf_export_dialog", || { rfd::FileDialog::new() .add_filter("PDF Document", &["pdf"]) .set_file_name("label.pdf") .save_file() }); self.pdf_export_promise = Some(promise); } "System" | _ => { // Use SystemPrintPlugin for system printing use crate::core::print::plugins::system::SystemPrintPlugin; let renderer = LabelRenderer::new(layout); let (doc, _, _) = renderer.generate_pdf_with_settings(&self.asset_data, &printer_settings)?; let system_plugin = SystemPrintPlugin::new() .map_err(|e| anyhow::anyhow!("Failed to initialize system print: {}", e))?; // Save PDF first since doc can't be cloned let pdf_path = system_plugin.save_pdf_to_temp(doc)?; // Try direct print to named system printer if provided if let Some(name) = printer_settings.printer_name.clone() { let mgr = SharedPrinterManager::new(); match mgr.print_pdf_to(&name, pdf_path.as_path(), Some(&printer_settings)) { Ok(()) => { return Ok(true); } Err(e) => { log::warn!("Direct system print failed: {}", e); let fallback = printer_settings.show_dialog_if_unfound.unwrap_or(true); if fallback { // Show OS printer chooser popup self.os_print_path = Some(pdf_path); self.os_popup_visible = true; self.os_error_message = Some(format!( "Named printer '{}' not found. Please select a system printer.", name )); // Provide base settings and renderer so overrides can regenerate PDF self.os_base_settings = Some(printer_settings.clone()); self.os_renderer = Some(renderer.clone()); self.os_size_override = PaperSizeOverride::UseSaved; return Ok(false); } else { // Fallback to opening in viewer using SystemPrintPlugin system_plugin.open_print_dialog(&pdf_path)?; return Ok(true); } } } } else { // No printer_name provided: either show chooser or open viewer if printer_settings.show_dialog_if_unfound.unwrap_or(true) { self.os_print_path = Some(pdf_path); self.os_popup_visible = true; self.os_error_message = None; // Provide base settings and renderer so overrides can regenerate PDF self.os_base_settings = Some(printer_settings.clone()); self.os_renderer = Some(renderer.clone()); self.os_size_override = PaperSizeOverride::UseSaved; return Ok(false); } else { system_plugin.open_print_dialog(&pdf_path)?; return Ok(true); } } } } log::info!("Print job for plugin '{}' dispatched.", printer_plugin); Ok(false) // Dialog should remain open for PDF export } /// Print via the OS popup selection with optional paper size overrides. /// Returns Ok(true) if a job was sent, Err(message) on failure. fn print_via_os_popup(&mut self, target_printer_name: &str) -> Result { // Determine the PDF to print: reuse existing if no override, or regenerate if overridden let (path_to_print, job_settings) = match self.os_size_override { PaperSizeOverride::UseSaved => { let mut settings = self .os_base_settings .clone() .unwrap_or_else(|| PrinterSettings::default()); settings.canonicalize_dimensions(); let path = self .os_print_path .clone() .ok_or_else(|| "No PDF available to print".to_string())?; (path, settings) } PaperSizeOverride::A4 | PaperSizeOverride::Letter | PaperSizeOverride::Custom => { let base = self .os_base_settings .clone() .ok_or_else(|| "Missing base printer settings for override".to_string())?; let renderer = self .os_renderer .clone() .ok_or_else(|| "Missing renderer for override".to_string())?; let mut settings = base.clone(); match self.os_size_override { PaperSizeOverride::A4 => { settings.paper_size = "A4".to_string(); settings.custom_width_mm = None; settings.custom_height_mm = None; } PaperSizeOverride::Letter => { settings.paper_size = "Letter".to_string(); settings.custom_width_mm = None; settings.custom_height_mm = None; } PaperSizeOverride::Custom => { let w = self.os_custom_width_mm.max(0.0); let h = self.os_custom_height_mm.max(0.0); if w <= 0.0 || h <= 0.0 { return Err("Please enter a valid custom size in mm".into()); } settings.custom_width_mm = Some(w); settings.custom_height_mm = Some(h); } PaperSizeOverride::UseSaved => unreachable!(), } settings.canonicalize_dimensions(); // Regenerate the PDF with overridden settings let (doc, _, _) = renderer .generate_pdf_with_settings(&self.asset_data, &settings) .map_err(|e| format!("Failed to generate PDF: {}", e))?; let new_path = Self::save_pdf_to_temp(doc) .map_err(|e| format!("Failed to save PDF: {}", e))?; // Update stored state for potential re-prints self.os_print_path = Some(new_path.clone()); self.os_base_settings = Some(settings.clone()); (new_path, settings) } }; // Send to the selected OS printer let mgr = SharedPrinterManager::new(); let job_settings_owned = job_settings; let result = mgr.print_pdf_to( target_printer_name, path_to_print.as_path(), Some(&job_settings_owned), ); if result.is_ok() { // Ensure latest settings persist for future retries when using saved path self.os_base_settings = Some(job_settings_owned.clone()); self.os_print_path = Some(path_to_print.clone()); } result.map(|_| true) } fn save_pdf_to_temp(doc: printpdf::PdfDocumentReference) -> Result { use anyhow::Context; use std::fs::File; use std::io::BufWriter; let temp_dir = std::env::temp_dir().join("beepzone_labels"); std::fs::create_dir_all(&temp_dir).context("Failed to create temp directory for labels")?; let timestamp = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs(); let pdf_path = temp_dir.join(format!("label_{}.pdf", timestamp)); let file = File::create(&pdf_path).context("Failed to create temp PDF file")?; let mut writer = BufWriter::new(file); doc.save(&mut writer).context("Failed to save temp PDF")?; Ok(pdf_path) } }