use crate::bindings::pdfium::*;
use crate::printer::FilePrinter;
use crate::printer::PrinterDevice;
use crate::ticket::PrintTicket;
use crate::ticket::ToDevModeError;
use crate::utils::emf::Emf;
use crate::utils::pdfium::PdfiumCustomDocument;
use crate::utils::pdfium::PdfiumGuard;
use crate::utils::wchar;
use scopeguard::defer;
use std::cell::Cell;
use std::path::Path;
use std::ptr;
use std::{fs::File, mem};
use thiserror::Error;
use windows::Win32::Foundation::RECT;
use windows::{
core::PCWSTR,
Win32::{
Graphics::Gdi::{
CreateDCW, DeleteDC, GetDeviceCaps, SetBrushOrgEx, SetGraphicsMode, SetStretchBltMode,
GET_DEVICE_CAPS_INDEX, GM_ADVANCED, HALFTONE, LOGPIXELSX, LOGPIXELSY, PHYSICALHEIGHT,
PHYSICALOFFSETX, PHYSICALOFFSETY, PHYSICALWIDTH,
},
Storage::Xps::{AbortDoc, EndDoc, EndPage, StartDocW, StartPage, DOCINFOW},
},
};
#[derive(Error, Debug)]
pub enum PdfiumPrinterError {
#[error("Failed to open printer")]
FailedToOpenPrinter,
#[error("File I/O error")]
FileIOError(#[source] std::io::Error),
#[error("Print Ticker Error")]
PrintTicketError(#[source] ToDevModeError),
#[error("StartDocW failed (returned {0})")]
StartDocFailed(i32),
#[error("StartPage failed for page {0} (returned {1})")]
StartPageFailed(i32, i32),
#[error("EndPage failed for page {0} (returned {1})")]
EndPageFailed(i32, i32),
#[error("EndDoc failed (returned {0})")]
EndDocFailed(i32),
#[error("PDFium failed to load the document (error {0})")]
PdfiumLoadFailed(u32),
#[error("Failed to create EMF for page {0}")]
EmfCreateFailed(i32),
#[error("Failed to playback EMF for page {0}")]
EmfPlaybackFailed(i32),
}
pub struct PdfiumPrinter {
printer: PrinterDevice,
}
impl PdfiumPrinter {
pub fn new(printer: PrinterDevice) -> Self {
Self { printer }
}
}
const PRINT_DRIVER: PCWSTR = PCWSTR(
[
'W' as u16, 'I' as u16, 'N' as u16, 'S' as u16, 'P' as u16, 'O' as u16, 'O' as u16,
'L' as u16, 0,
]
.as_ptr(),
);
impl FilePrinter for PdfiumPrinter {
type Options = PrintTicket;
type Error = PdfiumPrinterError;
fn print(
&self,
path: &Path,
options: PrintTicket,
) -> std::result::Result<(), PdfiumPrinterError> {
unsafe {
let dev_mode = options
.to_dev_mode(&self.printer)
.map_err(PdfiumPrinterError::PrintTicketError)?;
let hdc_print = CreateDCW(
PRINT_DRIVER,
PCWSTR(wchar::to_wide_chars(self.printer.os_name()).as_ptr()),
None,
Some(dev_mode.as_ptr() as *const _),
);
if hdc_print.is_invalid() {
return Err(PdfiumPrinterError::FailedToOpenPrinter);
}
defer! {
let _ = DeleteDC(hdc_print);
}
SetGraphicsMode(hdc_print, GM_ADVANCED);
SetStretchBltMode(hdc_print, HALFTONE);
let _ = SetBrushOrgEx(hdc_print, 0, 0, None);
let mut doc_name = wchar::to_wide_chars(path.file_name().unwrap_or(path.as_ref()));
let doc_info = DOCINFOW {
cbSize: mem::size_of::<DOCINFOW>() as i32,
fwType: 0,
lpszDocName: PCWSTR(doc_name.as_mut_ptr()),
lpszOutput: PCWSTR::null(),
lpszDatatype: PCWSTR::null(),
};
let start_doc_ret = StartDocW(hdc_print, &doc_info);
if start_doc_ret <= 0 {
return Err(PdfiumPrinterError::StartDocFailed(start_doc_ret));
}
let document_completed = Cell::new(false);
defer! {
if !document_completed.get() {
let _ = AbortDoc(hdc_print);
}
}
let _pdfium_guard = PdfiumGuard::guard();
let mut file = File::open(path).map_err(PdfiumPrinterError::FileIOError)?;
let mut file_delegation =
PdfiumCustomDocument::new(&mut file).map_err(PdfiumPrinterError::FileIOError)?;
let document = FPDF_LoadCustomDocument(file_delegation.as_mut(), ptr::null());
if document.is_null() {
return Err(PdfiumPrinterError::PdfiumLoadFailed(FPDF_GetLastError()));
}
defer! {
FPDF_CloseDocument(document);
}
let get_attr =
|kind: GET_DEVICE_CAPS_INDEX| -> i32 { GetDeviceCaps(Some(hdc_print), kind) };
let page_count = FPDF_GetPageCount(document);
for page_index in 0..page_count {
let page = FPDF_LoadPage(document, page_index);
defer! {
FPDF_ClosePage(page);
}
let start_page_ret = StartPage(hdc_print);
if start_page_ret <= 0 {
return Err(PdfiumPrinterError::StartPageFailed(
page_index,
start_page_ret,
));
}
let dpi_x = get_attr(LOGPIXELSX);
let dpi_y = get_attr(LOGPIXELSY);
let page_std_width = FPDF_GetPageWidth(page);
let page_std_height = FPDF_GetPageHeight(page);
let page_width = (page_std_width * dpi_x as f64 / 72.0).round() as i32;
let page_height = (page_std_height * dpi_y as f64 / 72.0).round() as i32;
let emf = Emf::new(
hdc_print,
(page_std_width * 2540.0 / 72.0).round() as i32,
(page_std_height * 2540.0 / 72.0).round() as i32,
|hdc_emf| {
SetGraphicsMode(hdc_emf, GM_ADVANCED);
FPDF_RenderPage(
hdc_emf,
page,
0,
0,
page_width,
page_height,
0,
FPDF_PRINTING,
);
true
},
)
.map_err(|_| PdfiumPrinterError::EmfCreateFailed(page_index))?;
let paper_width = get_attr(PHYSICALWIDTH);
let paper_height = get_attr(PHYSICALHEIGHT);
let scale = f64::min(
paper_width as f64 / page_width as f64,
paper_height as f64 / page_height as f64,
);
let actual_width = (page_width as f64 * scale).round() as i32;
let actual_height = (page_height as f64 * scale).round() as i32;
let left = -get_attr(PHYSICALOFFSETX) + (paper_width - actual_width) / 2;
let top = -get_attr(PHYSICALOFFSETY) + (paper_height - actual_height) / 2;
let target_rect = RECT {
left,
top,
right: actual_width + left,
bottom: actual_height + top,
};
let page_result = emf
.playback(hdc_print, target_rect)
.map_err(|_| PdfiumPrinterError::EmfPlaybackFailed(page_index));
let end_page_ret = EndPage(hdc_print);
page_result?;
if end_page_ret <= 0 {
return Err(PdfiumPrinterError::EndPageFailed(page_index, end_page_ret));
}
}
let end_doc_ret = EndDoc(hdc_print);
if end_doc_ret <= 0 {
return Err(PdfiumPrinterError::EndDocFailed(end_doc_ret));
}
document_completed.set(true);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::PdfiumPrinter;
use crate::{printer::FilePrinter, test_utils::null_device};
use std::path::Path;
#[test]
fn print_simple_pdf_document() {
let device = null_device::thread_local();
let pdf = PdfiumPrinter::new(device);
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("test_data/test_document.pdf");
pdf.print(path.as_path(), Default::default()).unwrap();
}
}