use super::DxgiPrintContext;
use super::DxgiPrintContextError;
use crate::printer::FilePrinter;
use crate::printer::PrintOutcome;
use crate::printer::PrintProgress;
use crate::printer::PrinterDevice;
use crate::ticket::PrintTicket;
use std::ops::ControlFlow;
use std::path::Path;
use thiserror::Error;
use windows::core::HSTRING;
use windows::Data::Pdf::PdfDocument;
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)]
pub enum WinPdfPrinterError {
#[error("DXGI print context error")]
DxgiPrintContextError(#[from] DxgiPrintContextError),
#[error("Invalid path")]
InvalidPath(#[source] std::io::Error),
#[error("Failed to open the document")]
FailedToOpenDocument(#[source] windows::core::Error),
#[error("The page ({dips} DIPs) cannot be rendered at {scale} times its size")]
PageSize {
dips: f32,
scale: f32,
},
#[error("The page could not be rendered at {render_dpi:?} dpi ({render_size_px:?} pixels)")]
RenderAtResolution {
render_dpi: (u32, u32),
render_size_px: (u32, u32),
#[source]
source: windows::core::Error,
},
#[error("Render error")]
RenderError(#[source] windows::core::Error),
}
pub struct WinPdfPrinter {
printer: PrinterDevice,
override_render_dpi: Option<(u32, u32)>,
}
impl WinPdfPrinter {
pub fn new(printer: PrinterDevice) -> Self {
Self {
printer,
override_render_dpi: None,
}
}
}
const DIP_PER_INCH: f32 = 96.0;
impl WinPdfPrinter {
pub fn override_render_dpi(&mut self, x: u32, y: u32) {
self.override_render_dpi = Some((x, y));
}
}
impl FilePrinter for WinPdfPrinter {
type Options = PrintTicket;
type Error = WinPdfPrinterError;
fn print_with_progress(
&self,
path: &Path,
options: PrintTicket,
on_progress: &mut dyn FnMut(&PrintProgress) -> ControlFlow<()>,
) -> std::result::Result<PrintOutcome, WinPdfPrinterError> {
let context = DxgiPrintContext::new(
&self.printer,
&options,
path.file_name().unwrap_or(path.as_ref()),
)?;
let dxgi_device = &context.dxgi_device;
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)?;
let file = StorageFile::GetFileFromPathAsync(&HSTRING::from(absolute_path.as_path()))
.map_err(WinPdfPrinterError::FailedToOpenDocument)?
.join()
.map_err(WinPdfPrinterError::FailedToOpenDocument)?;
let pdf_document = PdfDocument::LoadFromFileAsync(&file)
.map_err(WinPdfPrinterError::FailedToOpenDocument)?
.join()
.map_err(WinPdfPrinterError::FailedToOpenDocument)?;
let pdf_renderer =
PdfCreateRenderer(dxgi_device).map_err(WinPdfPrinterError::RenderError)?;
let page_count = pdf_document
.PageCount()
.map_err(WinPdfPrinterError::RenderError)?;
let mut checkpoint = |pages_added| {
context.checkpoint(on_progress(&PrintProgress::new(pages_added, page_count)))
};
if let ControlFlow::Break(outcome) = checkpoint(0)? {
return Ok(outcome);
}
for i in 0..page_count {
let pdf_page = pdf_document
.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();
d2d_context.SetTransform(&Matrix3x2::scale(
pdf_size.Width / pdf_render_options.DestinationWidth as f32,
pdf_size.Height / pdf_render_options.DestinationHeight as f32,
));
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 dx_size = D2D_SIZE_F {
width: pdf_size.Width as f32,
height: pdf_size.Height as f32,
};
if let ControlFlow::Break(outcome) = context
.add_page(&command_list, dx_size)
.map_err(at_resolution)?
{
return Ok(outcome);
}
if let ControlFlow::Break(outcome) = checkpoint(i + 1)? {
return Ok(outcome);
}
}
}
Ok(context.close_and_wait()?)
}
}
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::{scaled, WinPdfPrinter};
use crate::{
printer::{test_helpers, FilePrinter, PrintOutcome},
test_utils::null_device,
};
use std::path::Path;
#[test]
fn print_simple_pdf_document() {
let device = null_device::thread_local();
let pdf = WinPdfPrinter::new(device);
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("test_data/test_document.pdf");
assert_eq!(
pdf.print(path.as_path(), Default::default()).unwrap(),
PrintOutcome::Completed
);
}
#[test]
fn print_pdf_document_with_multiple_type_3_fonts() {
let device = null_device::thread_local();
let pdf = WinPdfPrinter::new(device);
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("test_data/type3_glyphs.pdf");
assert_eq!(
pdf.print(path.as_path(), Default::default()).unwrap(),
PrintOutcome::Completed
);
}
#[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);
assert_eq!(
pdf.print(path.as_path(), Default::default()).unwrap(),
PrintOutcome::Completed
);
}
#[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());
}
#[test]
fn reports_progress() {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("test_data/test_document.pdf");
test_helpers::assert_reports_progress(
&WinPdfPrinter::new(null_device::thread_local()),
&path,
);
}
#[test]
fn cancels_on_request() {
let device = null_device::thread_local();
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("test_data/test_document.pdf");
test_helpers::assert_cancels_on_request(
&WinPdfPrinter::new(device.clone()),
&device,
&path,
);
}
#[test]
fn observes_external_cancel() {
let device = null_device::thread_local();
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("test_data/test_document.pdf");
test_helpers::assert_observes_external_cancel(
&WinPdfPrinter::new(device.clone()),
&device,
&path,
);
}
#[test]
fn failure_leaves_no_job() {
let device = null_device::thread_local();
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("test_data/test_document.pdf");
test_helpers::assert_failure_leaves_no_job(
&WinPdfPrinter::new(device.clone()),
&device,
&path,
);
}
}