mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-02 22:41:47 -06:00
133 lines
6.9 KiB
Rust
133 lines
6.9 KiB
Rust
//! Printing tools: list printers, and print (or save the print-ready PDF) with Acrobat's Print
|
|
//! dialog options.
|
|
|
|
use pdfcraft_engine::print::{self, Binding, BookletSubset, Content, Layout, Orientation, PAPERS, PageOrder, SizeMode, Subset, spool};
|
|
use serde_json::{Value, json};
|
|
|
|
use crate::{Args, Automation, Result, ToolError, failed, write_atomic};
|
|
|
|
fn bad(m: impl Into<String>) -> ToolError {
|
|
ToolError::InvalidArgs(m.into())
|
|
}
|
|
|
|
impl Automation {
|
|
pub(crate) fn printers(&self) -> Result<Value> {
|
|
let list: Vec<Value> = spool::printers().into_iter().map(|p| json!({ "name": p.name, "default": p.default })).collect();
|
|
Ok(json!({ "count": list.len(), "printers": list }))
|
|
}
|
|
|
|
pub(crate) fn doc_print(&mut self, a: &Args) -> Result<Value> {
|
|
let doc = self.doc(a)?;
|
|
let (id, count) = (doc.id, doc.info.pages.len());
|
|
let labels: Vec<String> = doc.info.pages.iter().map(|p| p.label.clone()).collect();
|
|
let subset = match a.opt_str("subset")?.unwrap_or("all") {
|
|
"all" => Subset::All,
|
|
"odd" => Subset::Odd,
|
|
"even" => Subset::Even,
|
|
s => return Err(bad(format!("unknown subset {s:?}"))),
|
|
};
|
|
let pages = print::select_pages(count, a.opt_str("pages")?, &labels, subset, a.opt_bool("reverse")?.unwrap_or(false))
|
|
.map_err(|e| bad(e.to_string()))?;
|
|
let layout = match a.opt_str("layout")?.unwrap_or("fit") {
|
|
"fit" => Layout::Size(SizeMode::Fit),
|
|
"actual" => Layout::Size(SizeMode::Actual),
|
|
"shrink" => Layout::Size(SizeMode::Shrink),
|
|
"custom" => Layout::Size(SizeMode::Custom(a.opt_num("scale")?.ok_or_else(|| bad("custom needs scale (percent)"))?)),
|
|
"multiple" => {
|
|
let per = a.opt_int("per_sheet")?.unwrap_or(2).clamp(1, 256) as usize;
|
|
let order = match a.opt_str("order")?.unwrap_or("horizontal") {
|
|
"horizontal" => PageOrder::Horizontal,
|
|
"horizontal-reversed" => PageOrder::HorizontalReversed,
|
|
"vertical" => PageOrder::Vertical,
|
|
"vertical-reversed" => PageOrder::VerticalReversed,
|
|
"cut-stack" => PageOrder::CutStack,
|
|
o => return Err(bad(format!("unknown order {o:?}"))),
|
|
};
|
|
match Layout::multiple(per) {
|
|
Layout::Multiple { cols, rows, .. } => Layout::Multiple {
|
|
cols,
|
|
rows,
|
|
order,
|
|
border: a.opt_bool("border")?.unwrap_or(false),
|
|
auto_rotate: a.opt_bool("auto_rotate")?.unwrap_or(true),
|
|
},
|
|
l => l,
|
|
}
|
|
}
|
|
"booklet" => Layout::Booklet {
|
|
subset: match a.opt_str("booklet_subset")?.unwrap_or("both") {
|
|
"both" => BookletSubset::BothSides,
|
|
"front" => BookletSubset::FrontOnly,
|
|
"back" => BookletSubset::BackOnly,
|
|
s => return Err(bad(format!("unknown booklet_subset {s:?}"))),
|
|
},
|
|
binding: if a.opt_str("binding")? == Some("right") { Binding::Right } else { Binding::Left },
|
|
},
|
|
"poster" => Layout::Poster {
|
|
scale: a.opt_num("scale")?.unwrap_or(200.0),
|
|
overlap: a.opt_num("overlap")?.unwrap_or(18.0),
|
|
cut_marks: a.opt_bool("cut_marks")?.unwrap_or(true),
|
|
},
|
|
l => return Err(bad(format!("unknown layout {l:?}"))),
|
|
};
|
|
if matches!(layout, Layout::Multiple { order: PageOrder::CutStack, .. }) && a.opt_str("duplex")?.unwrap_or("off") != "off" {
|
|
return Err(bad("cut-and-stack printing needs duplex off (single-sided sheets)"));
|
|
}
|
|
if a.opt_str("order")? == Some("cut-stack") && !matches!(layout, Layout::Multiple { .. }) {
|
|
return Err(bad("cut-stack order needs layout multiple"));
|
|
}
|
|
let orientation = match a.opt_str("orientation")?.unwrap_or("auto") {
|
|
"auto" => Orientation::Auto,
|
|
"portrait" => Orientation::Portrait,
|
|
"landscape" => Orientation::Landscape,
|
|
o => return Err(bad(format!("unknown orientation {o:?}"))),
|
|
};
|
|
let content = match a.opt_str("comments_forms")?.unwrap_or("document-and-markups") {
|
|
"document" => Content::Document,
|
|
"document-and-markups" => Content::DocumentAndMarkups,
|
|
"document-and-stamps" => Content::DocumentAndStamps,
|
|
"form-fields-only" => Content::FormFieldsOnly,
|
|
c => return Err(bad(format!("unknown comments_forms {c:?}"))),
|
|
};
|
|
let paper = match a.opt_str("paper")? {
|
|
None => PAPERS[0].1,
|
|
Some(p) => PAPERS
|
|
.iter()
|
|
.find(|(n, _)| n.eq_ignore_ascii_case(p) || n.replace("US ", "").eq_ignore_ascii_case(p))
|
|
.map(|x| x.1)
|
|
.ok_or_else(|| bad(format!("unknown paper {p:?} (Letter, Legal, Tabloid, A3, A4, A5)")))?,
|
|
};
|
|
let settings = print::Settings { pages, paper, orientation, layout, content };
|
|
let sizes: Vec<(f64, f64)> = self.doc(a)?.info.pages.iter().map(|p| (p.width as f64, p.height as f64)).collect();
|
|
let sheets = print::layout(&sizes, &settings).map_err(|e| bad(e.to_string()))?.len();
|
|
let bytes = self.session.print_pdf(id, &settings).map_err(failed)?;
|
|
let mut out = json!({ "sheets": sheets, "pages": settings.pages.len(), "bytes": bytes.len() });
|
|
match (a.opt_str("path")?, a.opt_str("printer")?) {
|
|
(Some(p), None) => {
|
|
let path = self.resolve(p, true)?;
|
|
write_atomic(&path, &bytes)?;
|
|
out["path"] = json!(path.to_string_lossy());
|
|
}
|
|
(None, Some(printer)) => {
|
|
let name = self.doc(a)?.name.clone();
|
|
let job = spool::Job {
|
|
printer: if printer == "default" { None } else { Some(printer.to_string()) },
|
|
copies: a.opt_int("copies")?.unwrap_or(1).clamp(1, 999) as u32,
|
|
collate: a.opt_bool("collate")?.unwrap_or(true),
|
|
duplex: match a.opt_str("duplex")?.unwrap_or("off") {
|
|
"off" => spool::Duplex::Off,
|
|
"long-edge" => spool::Duplex::LongEdge,
|
|
"short-edge" => spool::Duplex::ShortEdge,
|
|
d => return Err(bad(format!("unknown duplex {d:?}"))),
|
|
},
|
|
grayscale: a.opt_bool("grayscale")?.unwrap_or(false),
|
|
title: name,
|
|
};
|
|
out["job"] = json!(spool::submit(&bytes, &job).map_err(|e| failed(e.to_string()))?);
|
|
}
|
|
_ => return Err(bad("pass either path (save the print-ready PDF) or printer (a name from printers, or \"default\")")),
|
|
}
|
|
Ok(out)
|
|
}
|
|
}
|