Files
ElyOffice/crates/ely-platform/src/print.rs
T
ZacharyZhang-NY b7fca0146d Ely Office: six offline-first desktop apps in Rust and GPUI
ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git),
ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
2026-10-08 21:20:08 -04:00

105 lines
4.5 KiB
Rust

//! Prints a PDF through the system print panel.
use std::path::Path;
/// Shows the print panel for `pdf`; false when cancelled.
#[cfg(target_os = "macos")]
pub fn print_pdf(pdf: &Path) -> anyhow::Result<bool> {
use objc2::{AllocAnyThread, MainThreadMarker};
use objc2_app_kit::{NSPaperOrientation, NSPrintInfo, NSPrintJobSavingURL, NSPrintSaveJob};
use objc2_foundation::{NSCopying, NSSize, NSString, NSURL};
use objc2_pdf_kit::{PDFDisplayBox, PDFDocument, PDFPrintScalingMode};
let mtm =
MainThreadMarker::new().ok_or_else(|| anyhow::anyhow!("printing needs the main thread"))?;
let url = NSURL::fileURLWithPath(&NSString::from_str(&pdf.to_string_lossy()));
let document = unsafe { PDFDocument::initWithURL(PDFDocument::alloc(), &url) }
.ok_or_else(|| anyhow::anyhow!("{} is no readable PDF", pdf.display()))?;
let sizes: Vec<(f64, f64)> = (0..unsafe { document.pageCount() })
.filter_map(|n| unsafe { document.pageAtIndex(n) })
.map(|page| {
let size = unsafe { page.boundsForBox(PDFDisplayBox::MediaBox) }.size;
// A page turned a quarter shows its other side up.
match unsafe { page.rotation() }.rem_euclid(180) == 90 {
true => (size.height, size.width),
false => (size.width, size.height),
}
})
.collect();
let (width, height) = paper_for(&sizes).ok_or_else(|| anyhow::anyhow!("{} has no pages", pdf.display()))?;
let paper = NSSize::new(width, height);
if let Some(scale) = crate::env::value("ELY_PRINT_SHARED_SCALE") {
// Tests start from a shared scale the copy must undo.
NSPrintInfo::sharedPrintInfo().setScalingFactor(scale.parse()?);
}
// The PDF already holds the document's paper, margins and scale.
let info = NSPrintInfo::sharedPrintInfo().copy();
info.setScalingFactor(1.0);
info.setOrientation(match paper.width > paper.height {
true => NSPaperOrientation::Landscape,
false => NSPaperOrientation::Portrait,
});
info.setPaperSize(paper);
info.setTopMargin(0.0);
info.setBottomMargin(0.0);
info.setLeftMargin(0.0);
info.setRightMargin(0.0);
log::info!("print: paper {}x{} points", paper.width, paper.height);
let operation = unsafe {
document.printOperationForPrintInfo_scalingMode_autoRotate(
Some(&info),
PDFPrintScalingMode::PageScaleNone,
true,
mtm,
)
}
.ok_or_else(|| anyhow::anyhow!("no print operation for {}", pdf.display()))?;
// Tests read the real job: saved to a file.
let saved = crate::env::value("ELY_PRINT_TO");
if let Some(to) = &saved {
let info = operation.printInfo();
info.setJobDisposition(unsafe { NSPrintSaveJob });
let to = NSURL::fileURLWithPath(&NSString::from_str(to));
unsafe { info.dictionary().insert(NSPrintJobSavingURL, &to) };
}
operation.setShowsPrintPanel(saved.is_none());
operation.setShowsProgressPanel(saved.is_none());
let printed = operation.runOperation();
log::info!("print: the panel closed, printed {printed}");
Ok(printed)
}
/// One paper holding every page, turned as needed.
fn paper_for(sizes: &[(f64, f64)]) -> Option<(f64, f64)> {
let long = sizes.iter().map(|(w, h)| w.max(*h)).fold(None, |most: Option<f64>, side| Some(most.map_or(side, |m| m.max(side))))?;
let short = sizes.iter().map(|(w, h)| w.min(*h)).fold(0.0, f64::max);
// The largest page sets the paper's way up.
let (w, h) = sizes.iter().copied().fold((0.0, 0.0), |big, size| if size.0 * size.1 > big.0 * big.1 { size } else { big });
// A square page leaves it to the others.
let wide = match w == h {
true => sizes.iter().any(|(w, h)| w > h),
false => w > h,
};
Some(if wide { (long, short) } else { (short, long) })
}
/// Printing needs a system panel this platform build lacks.
#[cfg(not(target_os = "macos"))]
pub fn print_pdf(pdf: &Path) -> anyhow::Result<bool> {
anyhow::bail!("printing {} is not built on this platform", pdf.display())
}
#[cfg(test)]
mod tests {
use super::paper_for;
#[test]
fn the_paper_holds_every_page() {
assert_eq!(paper_for(&[(595.0, 420.0)]), Some((595.0, 420.0)));
assert_eq!(paper_for(&[(420.0, 595.0), (842.0, 1191.0)]), Some((842.0, 1191.0)));
// Wide and square pages: neither side may be cut.
assert_eq!(paper_for(&[(1000.0, 200.0), (600.0, 600.0)]), Some((1000.0, 600.0)));
assert_eq!(paper_for(&[]), None);
}
}