mirror of
https://github.com/storytold/winprint.rs.git
synced 2026-11-15 06:44:28 +00:00
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:
3 files changed
+110
-14
No files matched your search
@@ -21,6 +21,7 @@ thiserror = "2.0"
|
|||||||
fmt-derive = "0.1"
|
fmt-derive = "0.1"
|
||||||
xml = "1.2"
|
xml = "1.2"
|
||||||
windows-core = "0.62"
|
windows-core = "0.62"
|
||||||
|
windows-numerics = "0.3"
|
||||||
windows-implement = "0.60.2"
|
windows-implement = "0.60.2"
|
||||||
|
|
||||||
[dependencies.windows]
|
[dependencies.windows]
|
||||||
|
|||||||
@@ -177,6 +177,10 @@ impl DxgiPrintContext {
|
|||||||
&print_ticket_stream,
|
&print_ticket_stream,
|
||||||
)
|
)
|
||||||
.map_err(DxgiPrintContextError::FailedToStartJob)?;
|
.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 {
|
let print_control_options = D2D1_PRINT_CONTROL_PROPERTIES {
|
||||||
rasterDPI: max(dpi.0, dpi.1) as f32,
|
rasterDPI: max(dpi.0, dpi.1) as f32,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -201,6 +205,8 @@ impl DxgiPrintContext {
|
|||||||
.Advise(&completion_source)
|
.Advise(&completion_source)
|
||||||
.map_err(DxgiPrintContextError::FailedToCreateEvent)?;
|
.map_err(DxgiPrintContextError::FailedToCreateEvent)?;
|
||||||
|
|
||||||
|
// The finished context owns the job from here on.
|
||||||
|
let _ = scopeguard::ScopeGuard::into_inner(job_guard);
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
d3d_device,
|
d3d_device,
|
||||||
d3d_context,
|
d3d_context,
|
||||||
|
|||||||
+103
-14
@@ -11,6 +11,7 @@ use windows::Storage::StorageFile;
|
|||||||
use windows::Win32::Graphics::Direct2D::Common::D2D_SIZE_F;
|
use windows::Win32::Graphics::Direct2D::Common::D2D_SIZE_F;
|
||||||
use windows::Win32::System::WinRT::Pdf::PdfCreateRenderer;
|
use windows::Win32::System::WinRT::Pdf::PdfCreateRenderer;
|
||||||
use windows::Win32::System::WinRT::Pdf::PDF_RENDER_PARAMS;
|
use windows::Win32::System::WinRT::Pdf::PDF_RENDER_PARAMS;
|
||||||
|
use windows_numerics::Matrix3x2;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
/// Represents an error from [`WinPdfPrinter`].
|
/// Represents an error from [`WinPdfPrinter`].
|
||||||
@@ -24,6 +25,31 @@ pub enum WinPdfPrinterError {
|
|||||||
/// Failed to open the document.
|
/// Failed to open the document.
|
||||||
#[error("Failed to open the document")]
|
#[error("Failed to open the document")]
|
||||||
FailedToOpenDocument(#[source] windows::core::Error),
|
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.
|
/// Render error.
|
||||||
#[error("Render error")]
|
#[error("Render error")]
|
||||||
RenderError(#[source] windows::core::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.
|
/// 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 {
|
pub struct WinPdfPrinter {
|
||||||
printer: PrinterDevice,
|
printer: PrinterDevice,
|
||||||
|
override_render_dpi: Option<(u32, u32)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WinPdfPrinter {
|
impl WinPdfPrinter {
|
||||||
/// Create a new [`WinPdfPrinter`] for the given printer device.
|
/// Create a new [`WinPdfPrinter`] for the given printer device.
|
||||||
pub fn new(printer: PrinterDevice) -> Self {
|
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 dxgi_device = &context.dxgi_device;
|
||||||
let print_control = &context.print_control;
|
let print_control = &context.print_control;
|
||||||
let d2d_context = &context.d2d_context;
|
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 {
|
unsafe {
|
||||||
let absolute_path =
|
let absolute_path =
|
||||||
std::path::absolute(path).map_err(WinPdfPrinterError::InvalidPath)?;
|
std::path::absolute(path).map_err(WinPdfPrinterError::InvalidPath)?;
|
||||||
@@ -71,10 +115,6 @@ impl FilePrinter for WinPdfPrinter {
|
|||||||
|
|
||||||
let pdf_renderer =
|
let pdf_renderer =
|
||||||
PdfCreateRenderer(dxgi_device).map_err(WinPdfPrinterError::RenderError)?;
|
PdfCreateRenderer(dxgi_device).map_err(WinPdfPrinterError::RenderError)?;
|
||||||
let pdf_render_options = PDF_RENDER_PARAMS {
|
|
||||||
IgnoreHighContrast: true.into(),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let page_count = pdf_document
|
let page_count = pdf_document
|
||||||
.PageCount()
|
.PageCount()
|
||||||
.map_err(WinPdfPrinterError::RenderError)?;
|
.map_err(WinPdfPrinterError::RenderError)?;
|
||||||
@@ -83,29 +123,47 @@ impl FilePrinter for WinPdfPrinter {
|
|||||||
.GetPage(i)
|
.GetPage(i)
|
||||||
.map_err(WinPdfPrinterError::RenderError)?;
|
.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
|
let command_list = d2d_context
|
||||||
.CreateCommandList()
|
.CreateCommandList()
|
||||||
.map_err(WinPdfPrinterError::RenderError)?;
|
.map_err(WinPdfPrinterError::RenderError)?;
|
||||||
d2d_context.SetTarget(&command_list);
|
d2d_context.SetTarget(&command_list);
|
||||||
d2d_context.BeginDraw();
|
d2d_context.BeginDraw();
|
||||||
pdf_renderer
|
d2d_context.SetTransform(&Matrix3x2::scale(scale.0.recip(), scale.1.recip()));
|
||||||
.RenderPageToDeviceContext(&pdf_page, d2d_context, Some(&pdf_render_options))
|
let rendered = pdf_renderer.RenderPageToDeviceContext(
|
||||||
.map_err(WinPdfPrinterError::RenderError)?;
|
&pdf_page,
|
||||||
d2d_context
|
d2d_context,
|
||||||
.EndDraw(None, None)
|
Some(&pdf_render_options),
|
||||||
.map_err(WinPdfPrinterError::RenderError)?;
|
);
|
||||||
|
d2d_context.SetTransform(&Matrix3x2::identity());
|
||||||
|
rendered.map_err(at_resolution)?;
|
||||||
|
d2d_context.EndDraw(None, None).map_err(at_resolution)?;
|
||||||
command_list
|
command_list
|
||||||
.Close()
|
.Close()
|
||||||
.map_err(WinPdfPrinterError::RenderError)?;
|
.map_err(WinPdfPrinterError::RenderError)?;
|
||||||
|
|
||||||
let pdf_size = pdf_page.Size().map_err(WinPdfPrinterError::RenderError)?;
|
|
||||||
let dx_size = D2D_SIZE_F {
|
let dx_size = D2D_SIZE_F {
|
||||||
width: pdf_size.Width as f32,
|
width: pdf_size.Width as f32,
|
||||||
height: pdf_size.Height as f32,
|
height: pdf_size.Height as f32,
|
||||||
};
|
};
|
||||||
print_control
|
print_control
|
||||||
.AddPage(&command_list, dx_size, None, None, None)
|
.AddPage(&command_list, dx_size, None, None, None)
|
||||||
.map_err(WinPdfPrinterError::RenderError)?;
|
.map_err(at_resolution)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
context.close_and_wait()?;
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::WinPdfPrinter;
|
use super::{scaled, WinPdfPrinter};
|
||||||
use crate::{printer::FilePrinter, test_utils::null_device};
|
use crate::{printer::FilePrinter, test_utils::null_device};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
@@ -126,4 +194,25 @@ mod tests {
|
|||||||
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("test_data/test_document.pdf");
|
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("test_data/test_document.pdf");
|
||||||
pdf.print(path.as_path(), Default::default()).unwrap();
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user