mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-14 01:53:11 +00:00
* M10: Windows prints a large image whole: drawn as tiles under Windows.Data.Pdf's cap (#5131) Windows prints each page through Windows.Data.Pdf, which draws any image of more than 2^22 pixels from a copy shrunk to that size. With the page at the printer's resolution (craft-libs#396, #5023), a 6000 x 6000 scan still printed as 2048 x 2048, its one-pixel detail gone. pdfcraft_optimize::tile_large_images replaces each such image XObject with a form XObject of the same object number that draws it as tiles of at most 2000 pixels a side, so whatever drew the image draws the tiles. Samples are copied as stored (any bit depth and colour space, decode array, interpolation, colour key); a grey or RGB JPEG is decoded and stored with Flate; a soft or stencil mask of the image's size is cut into the same tiles. Tiles overlap by a pixel against drivers that soften image edges, except under a soft mask, where the overlap would blend see-through pixels twice. JPEG 2000, CMYK or ambiguous JPEGs, masks of another size, more than 80 million pixels, short data and images past the decode allowance are left whole. pdfcraft_engine::spooler::spooler_copy applies it to the copy that goes to the spooler on Windows, from the Print dialog's worker and doc_print; elsewhere, or when the copy can't be read or written, the bytes go as they are. The saved print-ready PDF is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M10: a test that the spooler gets the tiles on Windows; tiles are up to 2001 px (#5131) spooler_copy was only tested with prints that pass through unchanged, so one that skipped tiling on Windows would have passed. A test now checks that on Windows it sends the tiled copy and elsewhere the bytes as they are. The optimize README said tiles have at most 2000 pixels a side; with the overlap they have 2001. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M10: the tiles' samples are StoredSamples, apart from the crate's Samples (#5131) main has since added a public `Samples` (8-bit grey or RGB, for tools that repaint an image), so two different things in one crate shared a name. The tiles keep an image's samples as stored, in its own colour space and depth, so each tile redraws exactly what it replaces; the type now says so in its name and doc comment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: austin62jake <340374338+austin62jake@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
111 lines
4.6 KiB
Rust
111 lines
4.6 KiB
Rust
//! The print-ready PDF as the system spooler gets it.
|
||
|
||
use std::sync::Arc;
|
||
|
||
use pdfcraft_cos::{SaveOptions, write_full};
|
||
|
||
use crate::guard;
|
||
|
||
/// The print-ready PDF `bytes` ([`crate::Session::print_pdf`]) as they go to the system spooler.
|
||
/// On Windows, whose PDF renderer prints an image of more than 2²² pixels from a shrunk copy,
|
||
/// large images are drawn as tiles ([`pdfcraft_optimize::tile_large_images`], #5131), so that a
|
||
/// scan prints at the printer's resolution. Elsewhere, with no large image, and when the copy
|
||
/// can't be read or written, the bytes go as they are: tiling never stops a print.
|
||
pub fn spooler_copy(bytes: Vec<u8>) -> Vec<u8> {
|
||
if !cfg!(windows) {
|
||
return bytes;
|
||
}
|
||
let bytes = Arc::new(bytes);
|
||
// Once `tiled` returns, its document is gone and `bytes` is the only owner left: no copy.
|
||
tiled(bytes.clone()).unwrap_or_else(|| Arc::try_unwrap(bytes).unwrap_or_else(|shared| (*shared).clone()))
|
||
}
|
||
|
||
/// `bytes` with their large images drawn as tiles, or `None` when there is none, or the copy
|
||
/// can't be read or written.
|
||
pub fn tiled(bytes: Arc<Vec<u8>>) -> Option<Vec<u8>> {
|
||
guard(|| {
|
||
let mut doc = pdfcraft_cos::Document::open(bytes).ok()?;
|
||
let report = pdfcraft_optimize::tile_large_images(&mut doc);
|
||
(report.tiled > 0).then(|| write_full(&doc, &SaveOptions::default()).ok()).flatten()
|
||
})
|
||
.ok()
|
||
.flatten()
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use pdfcraft_cos::{Dict, Document, Object, Stream};
|
||
|
||
use super::*;
|
||
|
||
/// A one-page PDF drawing a `w` × `h` grey image over the page.
|
||
fn page_with_image(w: i64, h: i64) -> Vec<u8> {
|
||
let mut doc = Document::new_empty();
|
||
let pages = doc.root().and_then(|r| doc.get(r).as_dict().and_then(|d| d.reference(b"Pages"))).unwrap();
|
||
let mut image = Dict::new();
|
||
image.set("Type", Object::name("XObject"));
|
||
image.set("Subtype", Object::name("Image"));
|
||
image.set("Width", Object::Int(w));
|
||
image.set("Height", Object::Int(h));
|
||
image.set("ColorSpace", Object::name("DeviceGray"));
|
||
image.set("BitsPerComponent", Object::Int(8));
|
||
let samples: Vec<u8> = (0..w * h).map(|i| (i % 251) as u8).collect();
|
||
let image = doc.add(Object::Stream(Stream::flate(image, &samples)));
|
||
let mut xobjects = Dict::new();
|
||
xobjects.set("Im", Object::Ref(image));
|
||
let mut resources = Dict::new();
|
||
resources.set("XObject", Object::Dict(xobjects));
|
||
let content = doc.add(Object::Stream(Stream::from_raw(Dict::new(), b"q 612 0 0 792 0 0 cm /Im Do Q".to_vec())));
|
||
let mut page = Dict::new();
|
||
page.set("Type", Object::name("Page"));
|
||
page.set("Parent", Object::Ref(pages));
|
||
page.set("MediaBox", Object::Array([0, 0, 612, 792].map(Object::Int).to_vec()));
|
||
page.set("Resources", Object::Dict(resources));
|
||
page.set("Contents", Object::Ref(content));
|
||
let page = doc.add(Object::Dict(page));
|
||
doc.update_dict(pages, |d| {
|
||
d.set("Count", Object::Int(1));
|
||
d.set("Kids", Object::Array(vec![Object::Ref(page)]));
|
||
})
|
||
.unwrap();
|
||
write_full(&doc, &SaveOptions::default()).unwrap()
|
||
}
|
||
|
||
#[test]
|
||
fn a_large_image_goes_to_the_spooler_as_tiles() {
|
||
let tiled = tiled(Arc::new(page_with_image(2100, 2100))).expect("tiled");
|
||
let doc = Document::open(Arc::new(tiled)).unwrap();
|
||
let largest = doc
|
||
.scan_objects()
|
||
.filter_map(|(_, o)| match &*o {
|
||
Object::Stream(s) if s.dict.name(b"Subtype") == Some(b"Image") => Some(s.dict.int(b"Width")? * s.dict.int(b"Height")?),
|
||
_ => None,
|
||
})
|
||
.max();
|
||
// The page's image is a form now; its tiles are the images it draws.
|
||
assert_eq!(largest, Some(2001 * 2001));
|
||
}
|
||
|
||
#[test]
|
||
fn a_print_with_nothing_to_tile_or_unreadable_goes_as_it_is() {
|
||
assert_eq!(tiled(Arc::new(page_with_image(100, 100))), None);
|
||
assert_eq!(tiled(Arc::new(b"not a PDF".to_vec())), None);
|
||
assert_eq!(spooler_copy(b"not a PDF".to_vec()), b"not a PDF");
|
||
let small = page_with_image(100, 100);
|
||
assert_eq!(spooler_copy(small.clone()), small);
|
||
}
|
||
|
||
/// On Windows the spooler gets the tiled copy ([`tiled`]); elsewhere the bytes as they are.
|
||
#[test]
|
||
fn the_spooler_gets_tiles_on_windows_only() {
|
||
let large = page_with_image(2100, 2100);
|
||
let sent = spooler_copy(large.clone());
|
||
if cfg!(windows) {
|
||
assert_eq!(Some(&sent), tiled(Arc::new(large.clone())).as_ref());
|
||
assert_ne!(sent, large);
|
||
} else {
|
||
assert_eq!(sent, large);
|
||
}
|
||
}
|
||
}
|