mirror of
https://github.com/storytold/winprint.rs.git
synced 2026-11-15 06:44:28 +00:00
Merge pull request #15 from songzhihui/fix/pdf-stream-loading
fix: load PDF print jobs on demand through a read-only file stream
This commit is contained in:
3 files changed
+147
-11
No files matched your search
@@ -48,6 +48,7 @@ features = [
|
||||
"Win32_System_Ole",
|
||||
"Win32_System_Threading",
|
||||
"Win32_System_Variant",
|
||||
"Win32_System_WinRT",
|
||||
"Win32_System_WinRT_Pdf",
|
||||
"Win32_UI_Shell",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
|
||||
@@ -74,6 +74,11 @@ The narrow changes used by craft-print are:
|
||||
An unsuccessful job requests cancellation; the status connection is unadvised
|
||||
before its COM apartment is released. The timeout does not bound calls into a
|
||||
printer driver or an interactive output-file dialog.
|
||||
- PDF loading uses a read-only file-backed stream, but `LoadFromStreamAsync` is
|
||||
currently joined without a deadline. The completion timeout above does not bound
|
||||
document loading. Adding cancellation requires keeping the stream and temporary
|
||||
file alive until the native operation has actually stopped; returning from a
|
||||
timeout alone is not sufficient. This remains a known limitation of the loader.
|
||||
- `WinPdfPrinter::print_with_progress(path, ticket, callback)` reports `(processed,
|
||||
total)` after each successful Direct2D AddPage, preserving one job and its ticket.
|
||||
These counts measure page processing, never physical printing or terminal success.
|
||||
|
||||
+141
-11
@@ -7,8 +7,10 @@ use std::path::Path;
|
||||
use thiserror::Error;
|
||||
use windows::core::HSTRING;
|
||||
use windows::Data::Pdf::PdfDocument;
|
||||
use windows::Storage::StorageFile;
|
||||
use windows::Storage::FileAccessMode;
|
||||
use windows::Storage::Streams::IRandomAccessStream;
|
||||
use windows::Win32::Graphics::Direct2D::Common::D2D_SIZE_F;
|
||||
use windows::Win32::System::WinRT::CreateRandomAccessStreamOnFile;
|
||||
use windows::Win32::System::WinRT::Pdf::PdfCreateRenderer;
|
||||
use windows::Win32::System::WinRT::Pdf::PDF_RENDER_PARAMS;
|
||||
|
||||
@@ -70,16 +72,7 @@ impl WinPdfPrinter {
|
||||
let print_control = &context.print_control;
|
||||
let d2d_context = &context.d2d_context;
|
||||
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_document = load_document(path)?;
|
||||
|
||||
let pdf_renderer =
|
||||
PdfCreateRenderer(dxgi_device).map_err(WinPdfPrinterError::RenderError)?;
|
||||
@@ -127,8 +120,145 @@ impl WinPdfPrinter {
|
||||
}
|
||||
}
|
||||
|
||||
// StorageFile's broker can refuse otherwise readable desktop job files. Open a
|
||||
// read-only, file-backed WinRT stream directly: no whole-file copy and no per-chunk
|
||||
// DataWriter/StoreAsync operations. The PDF renderer reads the file on demand.
|
||||
fn open_document_stream(path: &Path) -> Result<IRandomAccessStream, WinPdfPrinterError> {
|
||||
// On Windows, absolute also rejects embedded NULs before any native stream call.
|
||||
let absolute_path = std::path::absolute(path).map_err(WinPdfPrinterError::InvalidPath)?;
|
||||
let file_path = HSTRING::from(absolute_path.as_path());
|
||||
// SAFETY: HSTRING owns a NUL-terminated path for the duration of the call.
|
||||
// The windows binding supplies IRandomAccessStream's IID and owns the returned
|
||||
// COM reference. Read access does not create or modify the input file.
|
||||
unsafe { CreateRandomAccessStreamOnFile(&file_path, FileAccessMode::Read.0 as u32) }
|
||||
.map_err(WinPdfPrinterError::FailedToOpenDocument)
|
||||
}
|
||||
|
||||
fn load_document(path: &Path) -> Result<PdfDocument, WinPdfPrinterError> {
|
||||
let stream = open_document_stream(path)?;
|
||||
PdfDocument::LoadFromStreamAsync(&stream)
|
||||
.map_err(WinPdfPrinterError::FailedToOpenDocument)?
|
||||
.join()
|
||||
.map_err(WinPdfPrinterError::FailedToOpenDocument)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn loads_a_hidden_job_file_without_storage_broker_path_access() {
|
||||
use std::io::Write;
|
||||
use std::os::windows::fs::OpenOptionsExt;
|
||||
let _com = crate::utils::com::ComInitializer::new();
|
||||
let path =
|
||||
std::env::temp_dir().join(format!("winprint-hidden-{}.pdf", uuid::Uuid::new_v4()));
|
||||
let file = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.attributes(2)
|
||||
.open(&path)
|
||||
.unwrap(); // FILE_ATTRIBUTE_HIDDEN
|
||||
let path = scopeguard::guard(path, |path| {
|
||||
let _ = std::fs::remove_file(path);
|
||||
});
|
||||
// Handles created below the guard are released before its cleanup on a panic.
|
||||
{
|
||||
let mut output = file;
|
||||
output
|
||||
.write_all(include_bytes!("../../test_data/test_document.pdf"))
|
||||
.unwrap();
|
||||
}
|
||||
{
|
||||
let document = super::load_document(&path).unwrap();
|
||||
assert!(document.PageCount().unwrap() > 0);
|
||||
let page = document.GetPage(0).unwrap();
|
||||
let size = page.Size().unwrap();
|
||||
assert!(size.Width > 0.0 && size.Height > 0.0);
|
||||
}
|
||||
// Verify normal cleanup only after the page and document have been released.
|
||||
std::fs::remove_file(&*path).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_stream_reads_the_end_of_a_large_file_on_demand() {
|
||||
use std::io::{Seek, SeekFrom, Write};
|
||||
use std::time::{Duration, Instant};
|
||||
use windows::Storage::Streams::{Buffer, DataReader, InputStreamOptions};
|
||||
let _com = crate::utils::com::ComInitializer::new();
|
||||
let path =
|
||||
std::env::temp_dir().join(format!("winprint-large-{}.bin", uuid::Uuid::new_v4()));
|
||||
let file = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&path)
|
||||
.unwrap();
|
||||
let path = scopeguard::guard(path, |path| {
|
||||
let _ = std::fs::remove_file(path);
|
||||
});
|
||||
let size = 256 * 1024 * 1024;
|
||||
{
|
||||
let mut output = file;
|
||||
output.set_len(size).unwrap();
|
||||
output.seek(SeekFrom::End(-1)).unwrap();
|
||||
output.write_all(&[0xa5]).unwrap();
|
||||
output.sync_all().unwrap();
|
||||
}
|
||||
{
|
||||
let stream = super::open_document_stream(&path).unwrap();
|
||||
assert_eq!(stream.Size().unwrap(), size);
|
||||
assert!(stream.CanRead().unwrap());
|
||||
assert!(!stream.CanWrite().unwrap());
|
||||
let input = stream.GetInputStreamAt(size - 1).unwrap();
|
||||
let buffer = Buffer::Create(1).unwrap();
|
||||
let read = input
|
||||
.ReadAsync(&buffer, 1, InputStreamOptions::None)
|
||||
.unwrap();
|
||||
let started = Instant::now();
|
||||
// AsyncStatus::Started is 0. Poll the native operation rather than waiting
|
||||
// indefinitely for a completion callback: this test previously hung in CI.
|
||||
while read.Status().unwrap().0 == 0 {
|
||||
if started.elapsed() >= Duration::from_secs(30) {
|
||||
let _ = read.Cancel();
|
||||
panic!("the one-byte file-backed read did not complete within 30 seconds");
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
let bytes = read.GetResults().unwrap();
|
||||
assert_eq!(bytes.Length().unwrap(), 1);
|
||||
let reader = DataReader::FromBuffer(&bytes).unwrap();
|
||||
assert_eq!(reader.ReadByte().unwrap(), 0xa5);
|
||||
}
|
||||
std::fs::remove_file(&*path).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_missing_job_is_an_error_without_creating_a_file() {
|
||||
let _com = crate::utils::com::ComInitializer::new();
|
||||
let path =
|
||||
std::env::temp_dir().join(format!("winprint-missing-{}.pdf", uuid::Uuid::new_v4()));
|
||||
assert!(super::load_document(&path).is_err());
|
||||
assert!(!path.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_path_normalization_rejects_embedded_nul() {
|
||||
let _com = crate::utils::com::ComInitializer::new();
|
||||
let path = std::path::Path::new("job.pdf\0ignored");
|
||||
assert!(matches!(
|
||||
super::open_document_stream(path),
|
||||
Err(super::WinPdfPrinterError::InvalidPath(error))
|
||||
if error.kind() == std::io::ErrorKind::InvalidInput
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_stream_is_read_only() {
|
||||
let _com = crate::utils::com::ComInitializer::new();
|
||||
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("test_data/test_document.pdf");
|
||||
let stream = super::open_document_stream(&path).unwrap();
|
||||
assert!(stream.CanRead().unwrap());
|
||||
assert!(!stream.CanWrite().unwrap());
|
||||
}
|
||||
|
||||
use super::WinPdfPrinter;
|
||||
use crate::{printer::FilePrinter, test_utils::null_device};
|
||||
use std::path::Path;
|
||||
|
||||
Reference in new issue
Block a user