Skip to main content

winprint_ext/printer/
pdfium.rs

1use crate::bindings::pdfium::*;
2use crate::printer::FilePrinter;
3use crate::printer::PrinterDevice;
4use crate::ticket::PrintTicket;
5use crate::ticket::ToDevModeError;
6use crate::utils::emf::Emf;
7use crate::utils::pdfium::PdfiumCustomDocument;
8use crate::utils::pdfium::PdfiumGuard;
9use crate::utils::wchar;
10use scopeguard::defer;
11use std::cell::Cell;
12use std::path::Path;
13use std::ptr;
14use std::{fs::File, mem};
15use thiserror::Error;
16use windows::Win32::Foundation::RECT;
17use windows::{
18    core::PCWSTR,
19    Win32::{
20        Graphics::Gdi::{
21            CreateDCW, DeleteDC, GetDeviceCaps, SetBrushOrgEx, SetGraphicsMode, SetStretchBltMode,
22            GET_DEVICE_CAPS_INDEX, GM_ADVANCED, HALFTONE, LOGPIXELSX, LOGPIXELSY, PHYSICALHEIGHT,
23            PHYSICALOFFSETX, PHYSICALOFFSETY, PHYSICALWIDTH,
24        },
25        Storage::Xps::{AbortDoc, EndDoc, EndPage, StartDocW, StartPage, DOCINFOW},
26    },
27};
28
29#[derive(Error, Debug)]
30/// Represents an error from [`PdfiumPrinter`].
31pub enum PdfiumPrinterError {
32    /// Failed to open printer.
33    #[error("Failed to open printer")]
34    FailedToOpenPrinter,
35    /// File I/O error.
36    #[error("File I/O error")]
37    FileIOError(#[source] std::io::Error),
38    /// Print ticket error.
39    #[error("Print Ticker Error")]
40    PrintTicketError(#[source] ToDevModeError),
41    /// StartDoc failed.
42    #[error("StartDocW failed (returned {0})")]
43    StartDocFailed(i32),
44    /// StartPage failed.
45    #[error("StartPage failed for page {0} (returned {1})")]
46    StartPageFailed(i32, i32),
47    /// EndPage failed.
48    #[error("EndPage failed for page {0} (returned {1})")]
49    EndPageFailed(i32, i32),
50    /// EndDoc failed.
51    #[error("EndDoc failed (returned {0})")]
52    EndDocFailed(i32),
53    /// PDFium failed to load the document.
54    #[error("PDFium failed to load the document (error {0})")]
55    PdfiumLoadFailed(u32),
56    /// EMF creation failed for a page.
57    #[error("Failed to create EMF for page {0}")]
58    EmfCreateFailed(i32),
59    /// EMF playback failed for a page.
60    #[error("Failed to playback EMF for page {0}")]
61    EmfPlaybackFailed(i32),
62}
63
64/// A printer that uses Pdfium to print PDF documents.
65pub struct PdfiumPrinter {
66    printer: PrinterDevice,
67}
68
69impl PdfiumPrinter {
70    /// Create a new [`PdfiumPrinter`] for the given printer device.
71    pub fn new(printer: PrinterDevice) -> Self {
72        Self { printer }
73    }
74}
75
76const PRINT_DRIVER: PCWSTR = PCWSTR(
77    [
78        'W' as u16, 'I' as u16, 'N' as u16, 'S' as u16, 'P' as u16, 'O' as u16, 'O' as u16,
79        'L' as u16, 0,
80    ]
81    .as_ptr(),
82);
83
84impl FilePrinter for PdfiumPrinter {
85    type Options = PrintTicket;
86    type Error = PdfiumPrinterError;
87
88    fn print(
89        &self,
90        path: &Path,
91        options: PrintTicket,
92    ) -> std::result::Result<(), PdfiumPrinterError> {
93        unsafe {
94            let dev_mode = options
95                .to_dev_mode(&self.printer)
96                .map_err(PdfiumPrinterError::PrintTicketError)?;
97            // According to https://learn.microsoft.com/en-us/windows/win32/printdocs/retrieving-a-printer-device-context:
98            // > To render to a specific printer, you must specify "WINSPOOL" as the device.
99            //
100            // However, according to https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-createdcw
101            // > For printing, we recommend that you pass NULL to lpszDriver because GDI ignores lpszDriver for printer devices.
102            //
103            // We check the Chromium source code and it seems that they are using "WINSPOOL" as the driver name, so we will do the same.
104            // https://github.com/chromium/chromium/blob/749ad837ac3e74e3988f4a079979e3ea7e926f25/printing/printing_context_win.cc#L489
105            let hdc_print = CreateDCW(
106                PRINT_DRIVER,
107                PCWSTR(wchar::to_wide_chars(self.printer.os_name()).as_ptr()),
108                None,
109                Some(dev_mode.as_ptr() as *const _),
110            );
111            if hdc_print.is_invalid() {
112                return Err(PdfiumPrinterError::FailedToOpenPrinter);
113            }
114            defer! {
115                let _ = DeleteDC(hdc_print);
116            }
117
118            SetGraphicsMode(hdc_print, GM_ADVANCED);
119            SetStretchBltMode(hdc_print, HALFTONE);
120            // After setting the HALFTONE stretching mode,
121            // an application must call the SetBrushOrgEx function to set the brush origin.
122            // If it fails to do so, brush misalignment occurs.
123            let _ = SetBrushOrgEx(hdc_print, 0, 0, None);
124
125            let mut doc_name = wchar::to_wide_chars(path.file_name().unwrap_or(path.as_ref()));
126            let doc_info = DOCINFOW {
127                cbSize: mem::size_of::<DOCINFOW>() as i32,
128                fwType: 0,
129                lpszDocName: PCWSTR(doc_name.as_mut_ptr()),
130                lpszOutput: PCWSTR::null(),
131                lpszDatatype: PCWSTR::null(),
132            };
133
134            let start_doc_ret = StartDocW(hdc_print, &doc_info);
135            if start_doc_ret <= 0 {
136                return Err(PdfiumPrinterError::StartDocFailed(start_doc_ret));
137            }
138
139            let document_completed = Cell::new(false);
140            defer! {
141                if !document_completed.get() {
142                    let _ = AbortDoc(hdc_print);
143                }
144            }
145
146            let _pdfium_guard = PdfiumGuard::guard();
147            let mut file = File::open(path).map_err(PdfiumPrinterError::FileIOError)?;
148            let mut file_delegation =
149                PdfiumCustomDocument::new(&mut file).map_err(PdfiumPrinterError::FileIOError)?;
150            let document = FPDF_LoadCustomDocument(file_delegation.as_mut(), ptr::null());
151            if document.is_null() {
152                return Err(PdfiumPrinterError::PdfiumLoadFailed(FPDF_GetLastError()));
153            }
154            defer! {
155                FPDF_CloseDocument(document);
156            }
157
158            let get_attr =
159                |kind: GET_DEVICE_CAPS_INDEX| -> i32 { GetDeviceCaps(Some(hdc_print), kind) };
160            let page_count = FPDF_GetPageCount(document);
161            for page_index in 0..page_count {
162                let page = FPDF_LoadPage(document, page_index);
163                defer! {
164                    FPDF_ClosePage(page);
165                }
166
167                let start_page_ret = StartPage(hdc_print);
168                if start_page_ret <= 0 {
169                    return Err(PdfiumPrinterError::StartPageFailed(
170                        page_index,
171                        start_page_ret,
172                    ));
173                }
174                let dpi_x = get_attr(LOGPIXELSX);
175                let dpi_y = get_attr(LOGPIXELSY);
176                let page_std_width = FPDF_GetPageWidth(page);
177                let page_std_height = FPDF_GetPageHeight(page);
178                if page_std_width <= 0.0 || page_std_height <= 0.0 {
179                    return Err(PdfiumPrinterError::EmfCreateFailed(page_index));
180                }
181                let page_width = (page_std_width * dpi_x as f64 / 72.0).round() as i32;
182                let page_height = (page_std_height * dpi_y as f64 / 72.0).round() as i32;
183                let emf = Emf::new(
184                    hdc_print,
185                    (page_std_width * 2540.0 / 72.0).round() as i32,
186                    (page_std_height * 2540.0 / 72.0).round() as i32,
187                    |hdc_emf| {
188                        SetGraphicsMode(hdc_emf, GM_ADVANCED);
189                        FPDF_RenderPage(
190                            hdc_emf,
191                            page,
192                            0,
193                            0,
194                            page_width,
195                            page_height,
196                            0,
197                            FPDF_PRINTING,
198                        );
199                        true
200                    },
201                )
202                .map_err(|_| PdfiumPrinterError::EmfCreateFailed(page_index))?;
203
204                let paper_width = get_attr(PHYSICALWIDTH);
205                let paper_height = get_attr(PHYSICALHEIGHT);
206                let scale = f64::min(
207                    paper_width as f64 / page_width as f64,
208                    paper_height as f64 / page_height as f64,
209                );
210                let actual_width = (page_width as f64 * scale).round() as i32;
211                let actual_height = (page_height as f64 * scale).round() as i32;
212                let left = -get_attr(PHYSICALOFFSETX) + (paper_width - actual_width) / 2;
213                let top = -get_attr(PHYSICALOFFSETY) + (paper_height - actual_height) / 2;
214                let target_rect = RECT {
215                    left,
216                    top,
217                    right: actual_width + left,
218                    bottom: actual_height + top,
219                };
220                let page_result = emf
221                    .playback(hdc_print, target_rect)
222                    .map_err(|_| PdfiumPrinterError::EmfPlaybackFailed(page_index));
223                let end_page_ret = EndPage(hdc_print);
224                page_result?;
225                if end_page_ret <= 0 {
226                    return Err(PdfiumPrinterError::EndPageFailed(page_index, end_page_ret));
227                }
228            }
229
230            let end_doc_ret = EndDoc(hdc_print);
231            if end_doc_ret <= 0 {
232                return Err(PdfiumPrinterError::EndDocFailed(end_doc_ret));
233            }
234            document_completed.set(true);
235        }
236        Ok(())
237    }
238}
239
240#[cfg(all(test, feature = "test-utils"))]
241mod tests {
242    use super::PdfiumPrinter;
243    use crate::{printer::FilePrinter, test_utils::null_device};
244    use std::path::Path;
245
246    #[test]
247    fn print_simple_pdf_document() {
248        let device = null_device::thread_local();
249        let pdf = PdfiumPrinter::new(device);
250        let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("test_data/test_document.pdf");
251        pdf.print(path.as_path(), Default::default()).unwrap();
252    }
253}