diff --git a/Cargo.toml b/Cargo.toml index 7ff2009..9cb6ae5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ thiserror = "2.0" fmt-derive = "0.1" xml = "1.2" windows-core = "0.62" +windows-numerics = "0.3" windows-implement = "0.60.2" [dependencies.windows] diff --git a/src/printer/dxgi.rs b/src/printer/dxgi.rs index cc6fdbf..1800912 100644 --- a/src/printer/dxgi.rs +++ b/src/printer/dxgi.rs @@ -177,6 +177,10 @@ impl DxgiPrintContext { &print_ticket_stream, ) .map_err(DxgiPrintContextError::FailedToStartJob)?; + // The job exists from here on: cancel it on any early return (or panic) below. + let job_guard = scopeguard::guard(document_target.clone(), |target| { + let _ = target.Cancel(); + }); let print_control_options = D2D1_PRINT_CONTROL_PROPERTIES { rasterDPI: max(dpi.0, dpi.1) as f32, ..Default::default() @@ -201,6 +205,8 @@ impl DxgiPrintContext { .Advise(&completion_source) .map_err(DxgiPrintContextError::FailedToCreateEvent)?; + // The finished context owns the job from here on. + let _ = scopeguard::ScopeGuard::into_inner(job_guard); Ok(Self { d3d_device, d3d_context, diff --git a/src/printer/win_pdf.rs b/src/printer/win_pdf.rs index c32a224..f9a8e87 100644 --- a/src/printer/win_pdf.rs +++ b/src/printer/win_pdf.rs @@ -11,6 +11,7 @@ use windows::Storage::StorageFile; use windows::Win32::Graphics::Direct2D::Common::D2D_SIZE_F; use windows::Win32::System::WinRT::Pdf::PdfCreateRenderer; use windows::Win32::System::WinRT::Pdf::PDF_RENDER_PARAMS; +use windows_numerics::Matrix3x2; #[derive(Error, Debug)] /// Represents an error from [`WinPdfPrinter`]. @@ -24,6 +25,31 @@ pub enum WinPdfPrinterError { /// Failed to open the document. #[error("Failed to open the document")] FailedToOpenDocument(#[source] windows::core::Error), + /// The page does not fit the requested resolution. + #[error("The page ({dips} DIPs) cannot be rendered at {scale} times its size")] + PageSize { + /// The side of the page in device-independent pixels. + dips: f32, + /// The factor of the requested resolution over 96 dpi. + scale: f32, + }, + /// A page could not be rendered or recorded at the requested resolution. + /// + /// Direct3D 11 guarantees textures only up to 16384 texels a side + /// () + /// and Direct2D printing limits a bitmap to 65535 device units + /// (). Neither was reached when + /// pages were rendered whole up to A4 at 9600 dpi, but a driver or GPU may have its own limits. + #[error("The page could not be rendered at {render_dpi:?} dpi ({render_size_px:?} pixels)")] + RenderAtResolution { + /// The render resolution. + render_dpi: (u32, u32), + /// The render size in pixels at that resolution. + render_size_px: (u32, u32), + /// The failing call's error. + #[source] + source: windows::core::Error, + }, /// Render error. #[error("Render error")] RenderError(#[source] windows::core::Error), @@ -32,12 +58,25 @@ pub enum WinPdfPrinterError { /// A printer that uses [`Windows.Data.Pdf`](https://learn.microsoft.com/en-us/uwp/api/windows.data.pdf?view=winrt-26100) to to print PDF documents. pub struct WinPdfPrinter { printer: PrinterDevice, + override_render_dpi: Option<(u32, u32)>, } impl WinPdfPrinter { /// Create a new [`WinPdfPrinter`] for the given printer device. pub fn new(printer: PrinterDevice) -> Self { - Self { printer } + Self { + printer, + override_render_dpi: None, + } + } +} + +const DIP_PER_INCH: f32 = 96.0; + +impl WinPdfPrinter { + /// Override the resolution`Windows.Data.Pdf` draws a page at. + pub fn override_render_dpi(&mut self, x: u32, y: u32) { + self.override_render_dpi = Some((x, y)); } } @@ -57,6 +96,11 @@ impl FilePrinter for WinPdfPrinter { let dxgi_device = &context.dxgi_device; let print_control = &context.print_control; let d2d_context = &context.d2d_context; + let render_dpi = self.override_render_dpi.unwrap_or(context.dpi); + let scale = ( + render_dpi.0 as f32 / DIP_PER_INCH, + render_dpi.1 as f32 / DIP_PER_INCH, + ); unsafe { let absolute_path = std::path::absolute(path).map_err(WinPdfPrinterError::InvalidPath)?; @@ -71,10 +115,6 @@ impl FilePrinter for WinPdfPrinter { let pdf_renderer = PdfCreateRenderer(dxgi_device).map_err(WinPdfPrinterError::RenderError)?; - let pdf_render_options = PDF_RENDER_PARAMS { - IgnoreHighContrast: true.into(), - ..Default::default() - }; let page_count = pdf_document .PageCount() .map_err(WinPdfPrinterError::RenderError)?; @@ -83,29 +123,47 @@ impl FilePrinter for WinPdfPrinter { .GetPage(i) .map_err(WinPdfPrinterError::RenderError)?; + let pdf_size = pdf_page.Size().map_err(WinPdfPrinterError::RenderError)?; + let render_size_px = ( + scaled(pdf_size.Width, scale.0)?, + scaled(pdf_size.Height, scale.1)?, + ); + let at_resolution = |source| WinPdfPrinterError::RenderAtResolution { + render_dpi, + render_size_px, + source, + }; + let pdf_render_options = PDF_RENDER_PARAMS { + DestinationWidth: render_size_px.0, + DestinationHeight: render_size_px.1, + IgnoreHighContrast: true.into(), + ..Default::default() + }; let command_list = d2d_context .CreateCommandList() .map_err(WinPdfPrinterError::RenderError)?; d2d_context.SetTarget(&command_list); d2d_context.BeginDraw(); - pdf_renderer - .RenderPageToDeviceContext(&pdf_page, d2d_context, Some(&pdf_render_options)) - .map_err(WinPdfPrinterError::RenderError)?; - d2d_context - .EndDraw(None, None) - .map_err(WinPdfPrinterError::RenderError)?; + d2d_context.SetTransform(&Matrix3x2::scale(scale.0.recip(), scale.1.recip())); + let rendered = pdf_renderer.RenderPageToDeviceContext( + &pdf_page, + d2d_context, + Some(&pdf_render_options), + ); + d2d_context.SetTransform(&Matrix3x2::identity()); + rendered.map_err(at_resolution)?; + d2d_context.EndDraw(None, None).map_err(at_resolution)?; command_list .Close() .map_err(WinPdfPrinterError::RenderError)?; - let pdf_size = pdf_page.Size().map_err(WinPdfPrinterError::RenderError)?; let dx_size = D2D_SIZE_F { width: pdf_size.Width as f32, height: pdf_size.Height as f32, }; print_control .AddPage(&command_list, dx_size, None, None, None) - .map_err(WinPdfPrinterError::RenderError)?; + .map_err(at_resolution)?; } } context.close_and_wait()?; @@ -113,9 +171,19 @@ impl FilePrinter for WinPdfPrinter { } } +/// A page side in pixels at `scale` times its size in DIPs. +fn scaled(dips: f32, scale: f32) -> Result { + let pixels = (dips * scale).ceil(); + if pixels >= 1.0 && pixels < u32::MAX as f32 { + Ok(pixels as u32) + } else { + Err(WinPdfPrinterError::PageSize { dips, scale }) + } +} + #[cfg(test)] mod tests { - use super::WinPdfPrinter; + use super::{scaled, WinPdfPrinter}; use crate::{printer::FilePrinter, test_utils::null_device}; use std::path::Path; @@ -126,4 +194,25 @@ mod tests { let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("test_data/test_document.pdf"); pdf.print(path.as_path(), Default::default()).unwrap(); } + + /// A4 at 1600 dpi is 13229 x 18709 px, taller than the 16384 texels Direct3D 11 allows a + /// texture. The page is still rendered whole, in one pass: `RenderPageToDeviceContext` records + /// into a command list and never allocates a page-sized texture. + #[test] + fn a_page_larger_than_a_texture_prints_at_a_high_resolution() { + let device = null_device::thread_local(); + let mut pdf = WinPdfPrinter::new(device); + let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("test_data/test_document.pdf"); + pdf.override_render_dpi(1600, 1600); + pdf.print(path.as_path(), Default::default()).unwrap(); + } + + #[test] + fn a_page_side_is_scaled_and_never_overflows() { + assert_eq!(scaled(793.7, 1600.0 / 96.0).unwrap(), 13229); + assert_eq!(scaled(1122.5, 1.0).unwrap(), 1123); + assert!(scaled(0.0, 1.0).is_err()); + assert!(scaled(f32::NAN, 1.0).is_err()); + assert!(scaled(1e12, 100.0).is_err()); + } }