Skip to main content

winprint_ext/printer/
image.rs

1use super::DxgiPrintContext;
2use super::DxgiPrintContextError;
3use crate::printer::FilePrinter;
4use crate::printer::PrinterDevice;
5use crate::ticket::PrintTicket;
6use crate::ticket::ToDevModeError;
7use crate::utils::wchar;
8use std::path::Path;
9use thiserror::Error;
10use windows::core::PCWSTR;
11use windows::Win32::Foundation::GENERIC_READ;
12use windows::Win32::Graphics::Direct2D::Common::D2D_RECT_F;
13use windows::Win32::Graphics::Direct2D::Common::D2D_SIZE_F;
14use windows::Win32::Graphics::Direct2D::D2D1_INTERPOLATION_MODE_HIGH_QUALITY_CUBIC;
15use windows::Win32::Graphics::Imaging::GUID_WICPixelFormat32bppPBGRA;
16use windows::Win32::Graphics::Imaging::WICBitmapDitherTypeNone;
17use windows::Win32::Graphics::Imaging::WICBitmapPaletteTypeMedianCut;
18use windows::Win32::Graphics::Imaging::WICDecodeMetadataCacheOnDemand;
19use windows::Win32::Graphics::Gdi::{
20    CreateDCW, DeleteDC, GetDeviceCaps, LOGPIXELSX,
21    PHYSICALHEIGHT, PHYSICALOFFSETX, PHYSICALOFFSETY, PHYSICALWIDTH,
22};
23use windows_numerics::Matrix3x2;
24
25#[derive(Error, Debug)]
26/// Represents an error from [`ImagePrinter`].
27pub enum ImagePrinterError {
28    /// DXGI print context error.
29    #[error("DXGI print context error")]
30    DxgiPrintContextError(#[from] DxgiPrintContextError),
31    /// Print ticket error.
32    #[error("Print ticket error")]
33    PrintTicketError(#[from] ToDevModeError),
34    /// Invalid path.
35    #[error("Invalid path")]
36    InvalidPath(#[source] std::io::Error),
37    /// Failed to open the document.
38    #[error("Failed to open the document")]
39    FailedToOpenDocument(#[source] windows::core::Error),
40    /// Render error.
41    #[error("Render error")]
42    RenderError(#[source] windows::core::Error),
43}
44
45/// A printer that prints images. Multiple frames in a single image file will be printed as separate pages.
46pub struct ImagePrinter {
47    printer: PrinterDevice,
48}
49
50impl ImagePrinter {
51    /// Create a new [`ImagePrinter`] for the given printer device.
52    pub fn new(printer: PrinterDevice) -> Self {
53        Self { printer }
54    }
55
56    /// Print an image with additional options.
57    ///
58    /// When `auto_rotate` is true, the image will be automatically rotated 90°
59    /// to best fit the paper orientation (only when the image and paper aspect
60    /// ratios mismatch). This is useful when no explicit orientation parameter
61    /// is provided by the caller.
62    pub fn print_with_options(
63        &self,
64        path: &Path,
65        options: PrintTicket,
66        auto_rotate: bool,
67    ) -> std::result::Result<(), ImagePrinterError> {
68        let context = DxgiPrintContext::new(
69            &self.printer,
70            &options,
71            path.file_name().unwrap_or(path.as_ref()),
72        )?;
73        let wic_factory = &context.wic_factory;
74        let print_control = &context.print_control;
75        let d2d_context = &context.d2d_context;
76        unsafe {
77            let absolute_path =
78                std::path::absolute(path).map_err(ImagePrinterError::InvalidPath)?;
79            let image_decoder = wic_factory
80                .CreateDecoderFromFilename(
81                    PCWSTR(wchar::to_wide_chars(absolute_path.as_os_str()).as_ptr()),
82                    None,
83                    GENERIC_READ,
84                    WICDecodeMetadataCacheOnDemand,
85                )
86                .map_err(ImagePrinterError::FailedToOpenDocument)?;
87
88            let page_count = image_decoder
89                .GetFrameCount()
90                .map_err(ImagePrinterError::RenderError)?;
91            for i in 0..page_count {
92                let frame = image_decoder
93                    .GetFrame(i)
94                    .map_err(ImagePrinterError::RenderError)?;
95
96                let mut image_width = 0;
97                let mut image_height = 0;
98                frame
99                    .GetSize(&mut image_width, &mut image_height)
100                    .map_err(ImagePrinterError::RenderError)?;
101                let mut image_dpi_x = 0.0;
102                let mut image_dpi_y = 0.0;
103                frame
104                    .GetResolution(&mut image_dpi_x, &mut image_dpi_y)
105                    .map_err(ImagePrinterError::RenderError)?;
106
107                let natural_page_size = D2D_SIZE_F {
108                    width: (image_width as f64 * 96.0 / image_dpi_x) as f32,
109                    height: (image_height as f64 * 96.0 / image_dpi_y) as f32,
110                };
111
112                // Determine if auto-rotation is needed (before scaling)
113                let need_rotate = auto_rotate
114                    && natural_page_size.width > natural_page_size.height;
115
116                // Query physical paper dimensions via GDI for fit-to-page scaling
117                let dev_mode_for_dc = options
118                    .to_dev_mode(&self.printer)
119                    .map_err(ImagePrinterError::PrintTicketError)?;
120                let print_driver = PCWSTR(
121                    ['W' as u16, 'I' as u16, 'N' as u16, 'S' as u16, 'P' as u16,
122                     'O' as u16, 'O' as u16, 'L' as u16, 0].as_ptr(),
123                );
124                let hdc = CreateDCW(
125                    print_driver,
126                    PCWSTR(wchar::to_wide_chars(self.printer.os_name()).as_ptr()),
127                    None,
128                    Some(dev_mode_for_dc.as_ptr() as *const _),
129                );
130                let (page_size, translate_x, translate_y, dest_w, dest_h, scale, actual_rotate) =
131                    if hdc.is_invalid() {
132                        (natural_page_size, 0.0f64, 0.0f64,
133                         natural_page_size.width as f64, natural_page_size.height as f64,
134                         1.0f64, false)
135                    } else {
136                        let paper_w = GetDeviceCaps(Some(hdc), PHYSICALWIDTH);
137                        let paper_h = GetDeviceCaps(Some(hdc), PHYSICALHEIGHT);
138                        let offset_x = GetDeviceCaps(Some(hdc), PHYSICALOFFSETX);
139                        let offset_y = GetDeviceCaps(Some(hdc), PHYSICALOFFSETY);
140                        let dpi_x = GetDeviceCaps(Some(hdc), LOGPIXELSX);
141                        let _ = DeleteDC(hdc);
142
143                        // Convert device units to DIPs (96 DPI)
144                        let paper_w_dips = paper_w as f64 * 96.0 / dpi_x as f64;
145                        let paper_h_dips = paper_h as f64 * 96.0 / dpi_x as f64;
146                        let offset_x_dips = offset_x as f64 * 96.0 / dpi_x as f64;
147                        let offset_y_dips = offset_y as f64 * 96.0 / dpi_x as f64;
148
149                        // Check if rotation gives a better fit
150                        let paper_landscape = paper_w_dips > paper_h_dips;
151                        let should_rotate = need_rotate && paper_landscape != (natural_page_size.width > natural_page_size.height);
152
153                        let (eff_w, eff_h) = if should_rotate {
154                            (natural_page_size.height as f64, natural_page_size.width as f64)
155                        } else {
156                            (natural_page_size.width as f64, natural_page_size.height as f64)
157                        };
158
159                        let scale = f64::min(paper_w_dips / eff_w, paper_h_dips / eff_h);
160                        let scaled_w = eff_w * scale;
161                        let scaled_h = eff_h * scale;
162
163                        // Center on printable area (compensate for physical offset)
164                        let tx = -offset_x_dips + (paper_w_dips - scaled_w) / 2.0;
165                        let ty = -offset_y_dips + (paper_h_dips - scaled_h) / 2.0;
166
167                        let page_size = D2D_SIZE_F {
168                            width: paper_w_dips as f32,
169                            height: paper_h_dips as f32,
170                        };
171                        // dest rect uses ORIGINAL bitmap dimensions;
172                        // the transform matrix handles scale + rotate + translate
173                        (page_size, tx, ty,
174                         natural_page_size.width as f64, natural_page_size.height as f64,
175                         scale, should_rotate)
176                    };
177
178                let format_converter = wic_factory
179                    .CreateFormatConverter()
180                    .map_err(ImagePrinterError::RenderError)?;
181                format_converter
182                    .Initialize(
183                        &frame,
184                        &GUID_WICPixelFormat32bppPBGRA,
185                        WICBitmapDitherTypeNone,
186                        None,
187                        0.0,
188                        WICBitmapPaletteTypeMedianCut,
189                    )
190                    .map_err(ImagePrinterError::RenderError)?;
191                let bitmap = d2d_context
192                    .CreateBitmapFromWicBitmap(&format_converter, None)
193                    .map_err(ImagePrinterError::RenderError)?;
194
195                let command_list = d2d_context
196                    .CreateCommandList()
197                    .map_err(ImagePrinterError::RenderError)?;
198                d2d_context.SetTarget(&command_list);
199
200                d2d_context.BeginDraw();
201
202                // Apply combined transform: scale + optional 90° CW rotation + translate
203                // For no rotation: standard scale then translate
204                // For 90° CW rotation: scale, rotate, then translate
205                //   Rotated bitmap: x-axis → (0, scale), y-axis → (scale, 0)
206                //   i.e., image width maps to vertical, image height maps to horizontal
207                let s = scale as f32;
208                let matrix = if actual_rotate {
209                    Matrix3x2 {
210                        M11: 0.0, M12: s,
211                        M21: s,   M22: 0.0,
212                        M31: translate_x as f32, M32: translate_y as f32,
213                    }
214                } else {
215                    Matrix3x2 {
216                        M11: s,   M12: 0.0,
217                        M21: 0.0, M22: s,
218                        M31: translate_x as f32, M32: translate_y as f32,
219                    }
220                };
221                d2d_context.SetTransform(&matrix);
222
223                d2d_context.DrawBitmap(
224                    &bitmap,
225                    Some(&D2D_RECT_F {
226                        left: 0.0,
227                        top: 0.0,
228                        right: dest_w as f32,
229                        bottom: dest_h as f32,
230                    }),
231                    1.0,
232                    D2D1_INTERPOLATION_MODE_HIGH_QUALITY_CUBIC,
233                    None,
234                    None,
235                );
236                d2d_context
237                    .EndDraw(None, None)
238                    .map_err(ImagePrinterError::RenderError)?;
239                command_list
240                    .Close()
241                    .map_err(ImagePrinterError::RenderError)?;
242                print_control
243                    .AddPage(&command_list, page_size, None, None, None)
244                    .map_err(ImagePrinterError::RenderError)?;
245            }
246        }
247        context.close_and_wait()?;
248        Ok(())
249    }
250}
251
252impl FilePrinter for ImagePrinter {
253    type Options = PrintTicket;
254    type Error = ImagePrinterError;
255    fn print(
256        &self,
257        path: &Path,
258        options: PrintTicket,
259    ) -> std::result::Result<(), ImagePrinterError> {
260        self.print_with_options(path, options, false)
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::ImagePrinter;
267    use crate::{printer::FilePrinter, test_utils::null_device};
268    use std::path::Path;
269
270    #[test]
271    fn print_simple_tiff_document() {
272        let device = null_device::thread_local();
273        let image = ImagePrinter::new(device);
274        let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("test_data/test_document.tiff");
275        image.print(path.as_path(), Default::default()).unwrap();
276    }
277}