fix(win-pdf): render PDF pages at the job resolution

Co-Authored-By: Justin Capella <4584655+b1tninja@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
committed 2026-10-18 15:35:28 +08:00
1 parent d053ceaf9d
commit a4da89b847
3 files changed
+110 -14

No files matched your search

+1
View File
@@ -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]
+6
View File
@@ -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,
+103 -14
View File
@@ -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
/// (<https://learn.microsoft.com/windows/win32/direct3d11/overviews-direct3d-11-resources-limits>)
/// and Direct2D printing limits a bitmap to 65535 device units
/// (<https://learn.microsoft.com/windows/win32/direct2d/d1163>). 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<u32, WinPdfPrinterError> {
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());
}
}