feat: add print support via CUPS (lp)
- Add Print command to DocumentCommand enum - Add File menu bar with Print and About items - Implement print_document_sync() using CUPS lp command - Raster images converted to PDF via img2pdf (fallback: ImageMagick) - SVG converted to PDF via pdftocairo → img2pdf - PDF sent directly to printer via lp - Ctrl+P keyboard shortcut (via ModifiersChanged subscription) - Print status tracking with printing flag to prevent duplicates
This commit is contained in:
parent
3fd859dbdd
commit
03fd8306a6
3 changed files with 436 additions and 3 deletions
|
|
@ -3,11 +3,13 @@
|
||||||
//
|
//
|
||||||
// Commands that operate on the active document itself.
|
// Commands that operate on the active document itself.
|
||||||
|
|
||||||
/// Commands operating on the active document (zoom, export, annotate, …).
|
/// Commands operating on the active document (zoom, export, print, …).
|
||||||
///
|
///
|
||||||
/// The `DocumentManager` dispatches these after resolving which document is
|
/// The `DocumentManager` dispatches these after resolving which document is
|
||||||
/// active – the UI never needs to know the underlying format.
|
/// active – the UI never needs to know the underlying format.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum DocumentCommand {
|
pub enum DocumentCommand {
|
||||||
|
/// Print the active document to the system printer.
|
||||||
|
Print,
|
||||||
// Reserved for future use: Rotate, Flip, Zoom, Export, Annotate, …
|
// Reserved for future use: Rotate, Flip, Zoom, Export, Annotate, …
|
||||||
}
|
}
|
||||||
|
|
|
||||||
227
src/main.rs
227
src/main.rs
|
|
@ -10,6 +10,8 @@ use cosmic::iced::{
|
||||||
keyboard::{self, key::Named, Key},
|
keyboard::{self, key::Named, Key},
|
||||||
mouse, Alignment, ContentFit, Event, Length, Limits, Subscription,
|
mouse, Alignment, ContentFit, Event, Length, Limits, Subscription,
|
||||||
};
|
};
|
||||||
|
use cosmic::widget::menu::action::MenuAction;
|
||||||
|
use cosmic::widget::menu::Item as MenuItem;
|
||||||
use cosmic::{executor, theme, widget, Application, Element};
|
use cosmic::{executor, theme, widget, Application, Element};
|
||||||
use std::collections::hash_map::DefaultHasher;
|
use std::collections::hash_map::DefaultHasher;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
|
|
@ -28,7 +30,7 @@ struct Args {
|
||||||
#[arg(value_name = "FILE")]
|
#[arg(value_name = "FILE")]
|
||||||
files: Vec<PathBuf>,
|
files: Vec<PathBuf>,
|
||||||
|
|
||||||
/// Compatibility with the previous CLI harness.
|
/// Compatibility with the previous harness.
|
||||||
#[arg(long = "open-document", value_name = "PATH")]
|
#[arg(long = "open-document", value_name = "PATH")]
|
||||||
document: Option<PathBuf>,
|
document: Option<PathBuf>,
|
||||||
|
|
||||||
|
|
@ -37,7 +39,7 @@ struct Args {
|
||||||
directory: Option<PathBuf>,
|
directory: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Copy, Eq, PartialEq)]
|
||||||
enum DocumentKind {
|
enum DocumentKind {
|
||||||
Raster,
|
Raster,
|
||||||
Svg,
|
Svg,
|
||||||
|
|
@ -110,6 +112,30 @@ enum Message {
|
||||||
page: usize,
|
page: usize,
|
||||||
result: Result<PathBuf, String>,
|
result: Result<PathBuf, String>,
|
||||||
},
|
},
|
||||||
|
// Print messages
|
||||||
|
Print,
|
||||||
|
PrintTask {
|
||||||
|
result: Result<(), String>,
|
||||||
|
},
|
||||||
|
About,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Menu action type – only the variants that can appear in the menu.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum MenuActionType {
|
||||||
|
Print,
|
||||||
|
About,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MenuAction for MenuActionType {
|
||||||
|
type Message = Message;
|
||||||
|
|
||||||
|
fn message(&self) -> Self::Message {
|
||||||
|
match self {
|
||||||
|
MenuActionType::Print => Message::Print,
|
||||||
|
MenuActionType::About => Message::About,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct NoctuaApp {
|
struct NoctuaApp {
|
||||||
|
|
@ -118,6 +144,7 @@ struct NoctuaApp {
|
||||||
current: usize,
|
current: usize,
|
||||||
zoom: f32,
|
zoom: f32,
|
||||||
modifiers: keyboard::Modifiers,
|
modifiers: keyboard::Modifiers,
|
||||||
|
printing: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> cosmic::iced::Result {
|
fn main() -> cosmic::iced::Result {
|
||||||
|
|
@ -180,6 +207,7 @@ impl Application for NoctuaApp {
|
||||||
current,
|
current,
|
||||||
zoom: 1.0,
|
zoom: 1.0,
|
||||||
modifiers: keyboard::Modifiers::default(),
|
modifiers: keyboard::Modifiers::default(),
|
||||||
|
printing: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
app.refresh_title();
|
app.refresh_title();
|
||||||
|
|
@ -275,6 +303,51 @@ impl Application for NoctuaApp {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Print handling
|
||||||
|
Message::Print => {
|
||||||
|
if self.printing {
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
|
||||||
|
let document = match self.current_document() {
|
||||||
|
Some(doc) => doc,
|
||||||
|
None => return Task::none(),
|
||||||
|
};
|
||||||
|
let path = document.path.clone();
|
||||||
|
let kind = document.kind;
|
||||||
|
|
||||||
|
self.printing = true;
|
||||||
|
|
||||||
|
return Task::perform(
|
||||||
|
tokio::task::spawn_blocking(move || print_document_sync(&path, kind)),
|
||||||
|
|result| {
|
||||||
|
let inner = result
|
||||||
|
.map_err(|e| format!("Spawn error: {e}"))
|
||||||
|
.and_then(|r| r);
|
||||||
|
cosmic::Action::App(Message::PrintTask { result: inner })
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Message::PrintTask { result } => {
|
||||||
|
self.printing = false;
|
||||||
|
match result {
|
||||||
|
Ok(()) => {
|
||||||
|
// Print succeeded – no visual feedback needed,
|
||||||
|
// the document was sent to the printer.
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
if let Some(document) = self.current_document_mut() {
|
||||||
|
document.error = Some(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::About => {
|
||||||
|
// Simple about: show info in the document area
|
||||||
|
if let Some(document) = self.current_document_mut() {
|
||||||
|
document.error = Some("Noctua – A document and image viewer for the COSMIC desktop\n\nVersion 0.1.0\n© 2025 Wolfgang Morawetz\nLicensed under GPL-3.0-or-later".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.refresh_title();
|
self.refresh_title();
|
||||||
|
|
@ -304,6 +377,18 @@ impl Application for NoctuaApp {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn header_start(&self) -> Vec<Element<'_, Self::Message>> {
|
||||||
|
let doc = self.current_document();
|
||||||
|
let has_document = doc.is_some();
|
||||||
|
|
||||||
|
let menu_bar = widget::menu::bar(vec![widget::menu::Tree::with_children(
|
||||||
|
cosmic::widget::RcElementWrapper::new(Element::from(widget::menu::root("File"))),
|
||||||
|
widget::menu::items(&std::collections::HashMap::new(), menu_items(has_document)),
|
||||||
|
)]);
|
||||||
|
|
||||||
|
vec![Element::from(menu_bar)]
|
||||||
|
}
|
||||||
|
|
||||||
fn view(&self) -> Element<'_, Self::Message> {
|
fn view(&self) -> Element<'_, Self::Message> {
|
||||||
let content = match self.current_document() {
|
let content = match self.current_document() {
|
||||||
Some(document) => self.document_view(document),
|
Some(document) => self.document_view(document),
|
||||||
|
|
@ -320,6 +405,7 @@ impl Application for NoctuaApp {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn footer(&self) -> Option<Element<'_, Self::Message>> {
|
fn footer(&self) -> Option<Element<'_, Self::Message>> {
|
||||||
|
#[allow(clippy::question_mark)]
|
||||||
let Some(document) = self.current_document() else {
|
let Some(document) = self.current_document() else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
@ -380,6 +466,31 @@ impl Application for NoctuaApp {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn menu_items(has_document: bool) -> Vec<MenuItem<MenuActionType, &'static str>> {
|
||||||
|
let mut items = Vec::new();
|
||||||
|
|
||||||
|
// Print button (only when a document is open)
|
||||||
|
if has_document {
|
||||||
|
items.push(MenuItem::Button(
|
||||||
|
"Print",
|
||||||
|
Some(cosmic::widget::icon::from_name("document-print-symbolic").into()),
|
||||||
|
MenuActionType::Print,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Separator
|
||||||
|
items.push(MenuItem::Divider);
|
||||||
|
|
||||||
|
// About
|
||||||
|
items.push(MenuItem::Button(
|
||||||
|
"About Noctua",
|
||||||
|
Some(cosmic::widget::icon::from_name("help-about").into()),
|
||||||
|
MenuActionType::About,
|
||||||
|
));
|
||||||
|
|
||||||
|
items
|
||||||
|
}
|
||||||
|
|
||||||
impl NoctuaApp {
|
impl NoctuaApp {
|
||||||
fn zoom_in(&mut self) {
|
fn zoom_in(&mut self) {
|
||||||
self.zoom = (self.zoom * 1.2).min(5.0);
|
self.zoom = (self.zoom * 1.2).min(5.0);
|
||||||
|
|
@ -732,3 +843,115 @@ fn path_hash(path: &Path) -> u64 {
|
||||||
|
|
||||||
hasher.finish()
|
hasher.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Print service ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Print the currently active document to the system printer via CUPS (lp).
|
||||||
|
///
|
||||||
|
/// - **PDF** → sent directly to CUPS via `lp`
|
||||||
|
/// - **Raster images** → converted to PDF via `img2pdf` (or `convert` as fallback)
|
||||||
|
/// - **SVG** → converted to PDF via `pdftocairo` → `img2pdf`
|
||||||
|
fn print_document_sync(path: &Path, kind: DocumentKind) -> Result<(), String> {
|
||||||
|
let pdf_path = match kind {
|
||||||
|
DocumentKind::Pdf => path.to_path_buf(),
|
||||||
|
DocumentKind::Raster => convert_raster_to_pdf(path)?,
|
||||||
|
DocumentKind::Svg => convert_svg_to_pdf(path)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
send_to_printer(&pdf_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_raster_to_pdf(path: &Path) -> Result<PathBuf, String> {
|
||||||
|
// Try img2pdf first (cleaner, no quality loss)
|
||||||
|
if let Ok(result) = convert_with_cmd("img2pdf", vec![path.to_string_lossy().into_owned()], path)
|
||||||
|
{
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: ImageMagick convert
|
||||||
|
convert_with_cmd("convert", vec![path.to_string_lossy().into_owned()], path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_svg_to_pdf(path: &Path) -> Result<PathBuf, String> {
|
||||||
|
// Render SVG to PNG with pdftocairo, then convert PNG to PDF
|
||||||
|
let prefix = temp_prefix(path, "svg");
|
||||||
|
let status = Command::new("pdftocairo")
|
||||||
|
.arg("-png")
|
||||||
|
.arg("-singlefile")
|
||||||
|
.arg("-f")
|
||||||
|
.arg("1")
|
||||||
|
.arg("-l")
|
||||||
|
.arg("1")
|
||||||
|
.arg("-scale-to")
|
||||||
|
.arg("2400")
|
||||||
|
.arg(path)
|
||||||
|
.arg(&prefix)
|
||||||
|
.status()
|
||||||
|
.map_err(|e| format!("Failed to run pdftocairo: {e}"))?;
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
return Err("Failed to render SVG with pdftocairo".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let png_path = PathBuf::from(format!("{}.png", prefix.display()));
|
||||||
|
if !png_path.exists() {
|
||||||
|
return Err("pdftocairo did not produce output".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
convert_raster_to_pdf(&png_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_with_cmd(
|
||||||
|
cmd_name: &str,
|
||||||
|
args: Vec<String>,
|
||||||
|
source_path: &Path,
|
||||||
|
) -> Result<PathBuf, String> {
|
||||||
|
let output_path = temp_prefix(source_path, "print");
|
||||||
|
|
||||||
|
let mut cmd = Command::new(cmd_name);
|
||||||
|
if cmd_name == "img2pdf" {
|
||||||
|
cmd.args(&args).arg("-o").arg(&output_path);
|
||||||
|
} else {
|
||||||
|
// For convert, the last arg is the output
|
||||||
|
if !args.is_empty() {
|
||||||
|
cmd.args(&args[..args.len().saturating_sub(1)])
|
||||||
|
.arg(&output_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = cmd
|
||||||
|
.status()
|
||||||
|
.map_err(|e| format!("Failed to run {}: {e}", cmd_name))?;
|
||||||
|
if !status.success() {
|
||||||
|
return Err(format!("{} failed", cmd_name));
|
||||||
|
}
|
||||||
|
|
||||||
|
if output_path.exists() {
|
||||||
|
Ok(output_path)
|
||||||
|
} else {
|
||||||
|
Err(format!("{} did not produce output", cmd_name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_to_printer(pdf_path: &Path) -> Result<(), String> {
|
||||||
|
let status = Command::new("lp")
|
||||||
|
.arg(pdf_path)
|
||||||
|
.status()
|
||||||
|
.map_err(|e| format!("Failed to run lp: {e}"))?;
|
||||||
|
|
||||||
|
if status.success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err("Failed to send document to printer".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn temp_prefix(source_path: &Path, prefix: &str) -> PathBuf {
|
||||||
|
std::env::temp_dir()
|
||||||
|
.join(format!(
|
||||||
|
"noctua-print-{}-{}",
|
||||||
|
path_hash(source_path),
|
||||||
|
prefix
|
||||||
|
))
|
||||||
|
.with_extension("pdf")
|
||||||
|
}
|
||||||
|
|
|
||||||
208
src/print_service.rs
Normal file
208
src/print_service.rs
Normal file
|
|
@ -0,0 +1,208 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// src/print_service.rs
|
||||||
|
//
|
||||||
|
// Print service – sends documents to the system printer via CUPS (lp).
|
||||||
|
//
|
||||||
|
// Supported formats:
|
||||||
|
// - PDF: sent directly to lp (CUPS)
|
||||||
|
// - Raster images: converted to PDF via img2pdf, then sent to lp
|
||||||
|
// - SVG: converted to PDF via pdftocairo -svg → pdftocairo -pdf, then sent to lp
|
||||||
|
//
|
||||||
|
// If img2pdf is not available, raster images are converted to PDF via
|
||||||
|
// ImageMagick's `convert` as a fallback.
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
/// Result of a print operation.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum PrintResult {
|
||||||
|
/// The document was sent to the printer successfully.
|
||||||
|
Success,
|
||||||
|
/// The operation was cancelled by the user.
|
||||||
|
Cancelled,
|
||||||
|
/// An error occurred during printing.
|
||||||
|
Error(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print the currently active document.
|
||||||
|
///
|
||||||
|
/// For PDF files the document is sent directly to CUPS via `lp`.
|
||||||
|
/// For raster images, the file is converted to PDF first (using `img2pdf` or
|
||||||
|
/// `convert` as fallback) and then sent to the printer.
|
||||||
|
/// For SVG files, the SVG is converted to PDF via `pdftocairo` and then sent
|
||||||
|
/// to the printer.
|
||||||
|
pub fn print_document(path: &PathBuf, kind: DocumentKind) -> PrintResult {
|
||||||
|
let pdf_path = match kind {
|
||||||
|
DocumentKind::Pdf => {
|
||||||
|
// PDFs can be sent directly to CUPS
|
||||||
|
path.clone()
|
||||||
|
}
|
||||||
|
DocumentKind::Raster => {
|
||||||
|
// Raster images need conversion to PDF first
|
||||||
|
convert_raster_to_pdf(path)
|
||||||
|
}
|
||||||
|
DocumentKind::Svg => {
|
||||||
|
// SVG needs conversion to PDF first
|
||||||
|
convert_svg_to_pdf(path)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match pdf_path {
|
||||||
|
Ok(pdf_path) => send_to_printer(&pdf_path),
|
||||||
|
Err(e) => PrintResult::Error(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a raster image to a temporary PDF file.
|
||||||
|
///
|
||||||
|
/// Uses `img2pdf` first, then falls back to `convert` (ImageMagick).
|
||||||
|
fn convert_raster_to_pdf(path: &PathBuf) -> Result<PathBuf, String> {
|
||||||
|
// Try img2pdf first (cleaner, no quality loss)
|
||||||
|
if let Ok(result) = convert_with_cmd("img2pdf", vec![path.to_string_lossy().into_owned()], path) {
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to ImageMagick convert
|
||||||
|
convert_with_cmd("convert", vec![path.to_string_lossy().into_owned()], path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert an SVG to a temporary PDF file.
|
||||||
|
///
|
||||||
|
/// Uses `pdftocairo` to render the SVG to a temporary PNG, then converts
|
||||||
|
/// that PNG to PDF. For a more direct approach, we try `svg2pdf` if available.
|
||||||
|
fn convert_svg_to_pdf(path: &PathBuf) -> Result<PathBuf, String> {
|
||||||
|
// Try svg2pdf first (direct SVG → PDF)
|
||||||
|
if let Ok(result) = convert_with_cmd(
|
||||||
|
"svg2pdf",
|
||||||
|
vec![
|
||||||
|
path.to_string_lossy().into_owned(),
|
||||||
|
"-".to_string(),
|
||||||
|
],
|
||||||
|
path,
|
||||||
|
) {
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: render SVG to PNG with pdftocairo, then convert PNG to PDF
|
||||||
|
// First render SVG to PNG
|
||||||
|
let prefix = temp_prefix(path, "svg");
|
||||||
|
let status = Command::new("pdftocairo")
|
||||||
|
.arg("-png")
|
||||||
|
.arg("-singlefile")
|
||||||
|
.arg("-f")
|
||||||
|
.arg("1")
|
||||||
|
.arg("-l")
|
||||||
|
.arg("1")
|
||||||
|
.arg("-scale-to")
|
||||||
|
.arg("2400")
|
||||||
|
.arg(path)
|
||||||
|
.arg(&prefix)
|
||||||
|
.status()
|
||||||
|
.map_err(|e| format!("Failed to run pdftocairo: {e}"))?;
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
return Err("Failed to render SVG with pdftocairo".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let png_path = PathBuf::from(format!("{}.png", prefix));
|
||||||
|
if !png_path.exists() {
|
||||||
|
return Err("pdftocairo did not produce output".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert PNG to PDF
|
||||||
|
convert_with_cmd("img2pdf", vec![png_path.to_string_lossy().into_owned()], path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a file to PDF using a command-line tool.
|
||||||
|
///
|
||||||
|
/// Writes the output to a temporary file and returns its path.
|
||||||
|
fn convert_with_cmd(
|
||||||
|
cmd: &str,
|
||||||
|
args: Vec<String>,
|
||||||
|
_source_path: &PathBuf,
|
||||||
|
) -> Result<PathBuf, String> {
|
||||||
|
let status = Command::new(cmd)
|
||||||
|
.args(&args)
|
||||||
|
.output()
|
||||||
|
.map_err(|e| format!("Failed to run {cmd}: {e}"))?;
|
||||||
|
|
||||||
|
if !status.status.success() {
|
||||||
|
let stderr = String::from_utf8_lossy(&status.stderr);
|
||||||
|
return Err(format!("{cmd} failed: {stderr}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// img2pdf writes to stdout by default, convert writes to a file
|
||||||
|
// For img2pdf, we need to capture stdout
|
||||||
|
// For convert, the output is specified in args
|
||||||
|
// Let's handle both:
|
||||||
|
// - img2pdf: write to temp file
|
||||||
|
// - convert: find the output file
|
||||||
|
|
||||||
|
// For img2pdf, we need to re-run with output redirect
|
||||||
|
// Actually, let's use a simpler approach: write to a known temp file
|
||||||
|
let output_path = temp_prefix(_source_path, "print");
|
||||||
|
|
||||||
|
// Re-run with explicit output
|
||||||
|
let mut cmd = Command::new(cmd);
|
||||||
|
if cmd.get_program().to_string() == "img2pdf" {
|
||||||
|
cmd.args(&args).arg("-o").arg(&output_path);
|
||||||
|
} else {
|
||||||
|
// For convert, the last arg is the output
|
||||||
|
if !args.is_empty() {
|
||||||
|
cmd.args(&args[..args.len().saturating_sub(1)]).arg(&output_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = cmd.status().map_err(|e| format!("Failed to run {cmd}: {e}"))?;
|
||||||
|
if !status.success() {
|
||||||
|
return Err(format!("{cmd} failed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if output_path.exists() {
|
||||||
|
Ok(output_path)
|
||||||
|
} else {
|
||||||
|
Err(format!("{cmd} did not produce output"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a PDF file to the system printer via CUPS (lp command).
|
||||||
|
fn send_to_printer(pdf_path: &PathBuf) -> PrintResult {
|
||||||
|
let status = Command::new("lp")
|
||||||
|
.arg(pdf_path)
|
||||||
|
.status()
|
||||||
|
.map_err(|e| format!("Failed to run lp: {e}"))?;
|
||||||
|
|
||||||
|
if status.success() {
|
||||||
|
PrintResult::Success
|
||||||
|
} else {
|
||||||
|
PrintResult::Error("Failed to send document to printer".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a temporary file path for printing.
|
||||||
|
fn temp_prefix(source_path: &PathBuf, prefix: &str) -> PathBuf {
|
||||||
|
std::env::temp_dir()
|
||||||
|
.join(format!(
|
||||||
|
"noctua-print-{}-{}",
|
||||||
|
path_hash(source_path),
|
||||||
|
prefix
|
||||||
|
))
|
||||||
|
.with_extension("pdf")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hash a path for use in temp file names.
|
||||||
|
fn path_hash(path: &PathBuf) -> u64 {
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||||
|
path.hash(&mut hasher);
|
||||||
|
hasher.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The document kind for print operations.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum DocumentKind {
|
||||||
|
Pdf,
|
||||||
|
Raster,
|
||||||
|
Svg,
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue