fix(print): list the source pages once per imposition

page_form listed every page of the document to take the one it
printed, so imposing P pages walked the page tree and copied its leaf
dictionaries P times over: P^2 page copies. impose passes the list it
already holds. A page index outside it is an error, not a panic.
This commit is contained in:
aiongg
2026-10-22 17:43:04 +00:00
parent a0e82da864
commit 24701fe959
3 changed files with 33 additions and 4 deletions
+20 -3
View File
@@ -770,13 +770,16 @@ fn appearance_matrix(doc: &Document, form: &Dict, rect: [f64; 4]) -> Option<Matr
}
/// A Form XObject holding page `index` as it prints (user space; BBox = crop box).
/// `pages` are the document's pages, listed once by the caller: listing them for each page
/// printed would make printing quadratic in the page count.
fn page_form(
doc: &mut Document,
pages: &[pdfcraft_model::Page],
index: usize,
content: Content,
promoted: &mut std::collections::HashMap<ObjRef, ObjRef>,
) -> Result<ObjRef, PrintError> {
let page = pdfcraft_model::pages(doc).swap_remove(index);
let page = pages.get(index).ok_or_else(|| PrintError::Invalid(format!("there is no page {}", index.saturating_add(1))))?;
let crop = page.crop(doc);
let mut res = page.dict.get(b"Resources").and_then(|r| doc.resolve(r).as_dict().cloned()).unwrap_or_default();
let mut body = if content == Content::FormFieldsOnly { Vec::new() } else { decoded_contents(doc, &page.dict) };
@@ -841,10 +844,24 @@ fn page_form(
Ok(doc.add(Object::Stream(Stream::flate(d, &body))))
}
#[cfg(test)]
thread_local! {
/// Pages listed by [`source_pages`] on this thread.
static PAGES_LISTED: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
/// The source document's pages, for printing.
fn source_pages(doc: &Document) -> Vec<pdfcraft_model::Page> {
let pages = pdfcraft_model::pages(doc);
#[cfg(test)]
PAGES_LISTED.with(|c| c.set(c.get() + pages.len()));
pages
}
/// Build the print-ready PDF for `settings`. Encryption is not carried over (the file goes to a
/// printer or is the user's own copy); callers must check the print permission first.
pub fn impose(src: &Document, settings: &Settings) -> Result<Vec<u8>, PrintError> {
let pages = pdfcraft_model::pages(src);
let pages = source_pages(src);
let sizes: Vec<(f64, f64)> = pages.iter().map(|p| p.display_size(src)).collect();
let sheets = layout(&sizes, settings)?;
let mut doc = src.clone();
@@ -861,7 +878,7 @@ pub fn impose(src: &Document, settings: &Settings) -> Result<Vec<u8>, PrintError
let form = match forms.get(&pl.page) {
Some(f) => *f,
None => {
let f = page_form(&mut doc, pl.page, settings.content, &mut promoted)?;
let f = page_form(&mut doc, &pages, pl.page, settings.content, &mut promoted)?;
forms.insert(pl.page, f);
f
}
+11
View File
@@ -192,6 +192,17 @@ fn custom_pages_per_sheet_never_overfill_the_grid() {
assert_eq!(pdfcraft_model::pages(&Document::open(Arc::new(out)).unwrap()).len(), 3);
}
#[test]
fn imposing_lists_the_pages_once() {
let n = 150;
let doc = fixture(n);
PAGES_LISTED.with(|c| c.set(0));
let out = impose(&doc, &settings((0..n).collect(), Layout::multiple(2))).unwrap();
let listed = PAGES_LISTED.with(|c| c.get());
assert!(listed <= 2 * n, "{listed} pages listed to print {n}");
assert_eq!(pdfcraft_model::pages(&Document::open(Arc::new(out)).unwrap()).len(), n / 2);
}
#[test]
fn booklets_pair_pages_for_folding() {
let sizes = vec![(200.0, 300.0); 6];
+2 -1
View File
@@ -147,7 +147,8 @@ fn promotion_preserves_streams_and_explicit_subtypes() {
for subtype in [None, Some(Object::name("Form")), Some(Object::name("Image")), Some(Object::Null), Some(Object::Int(7))] {
let (mut doc, ap, _) = widget_fixture(subtype.clone(), Some([2.0, 0.0, 0.0, 3.0, 5.0, 7.0]));
let original = doc.get(ap);
let form = page_form(&mut doc, 0, Content::Document, &mut std::collections::HashMap::new()).unwrap();
let pages = pdfcraft_model::pages(&doc);
let form = page_form(&mut doc, &pages, 0, Content::Document, &mut std::collections::HashMap::new()).unwrap();
assert_eq!(doc.get(ap), original, "original in the print clone");
let promoted = xobject(&doc, doc.get(form).as_dict().unwrap(), b"PCPrA0");
if subtype.is_some() {