diff --git a/crates/automation/src/printing.rs b/crates/automation/src/printing.rs index 0af923ec25..55fec9ef55 100644 --- a/crates/automation/src/printing.rs +++ b/crates/automation/src/printing.rs @@ -227,6 +227,7 @@ impl Automation { s => return Err(bad(format!("unknown booklet_subset {s:?}"))), }, binding: if a.opt_str("binding")? == Some("right") { Binding::Right } else { Binding::Left }, + creep: a.opt_num("creep")?.unwrap_or(0.0), }, "poster" => Layout::Poster { scale: a.opt_num("scale")?.unwrap_or(200.0), @@ -291,7 +292,7 @@ mod tests { /// #2707: a booklet sets two-sided itself whatever `duplex` asks; a bad name is still an error. #[test] fn a_booklet_decides_two_sided_itself() { - let booklet = |subset| Layout::Booklet { subset, binding: Binding::Left }; + let booklet = |subset| Layout::Booklet { subset, binding: Binding::Left, creep: 0.0 }; for asked in ["printer", "off", "long-edge", "short-edge"] { assert_eq!(job_duplex(asked, &booklet(BookletSubset::BothSides)).ok(), Some(Some(spool::Duplex::ShortEdge)), "{asked}"); assert_eq!(job_duplex(asked, &booklet(BookletSubset::FrontOnly)).ok(), Some(Some(spool::Duplex::Off)), "{asked}"); diff --git a/crates/automation/src/tools.rs b/crates/automation/src/tools.rs index 186db56922..de650aea6e 100644 --- a/crates/automation/src/tools.rs +++ b/crates/automation/src/tools.rs @@ -764,6 +764,7 @@ pub fn tools() -> Vec { "auto_rotate": { "type": "boolean" }, "booklet_subset": { "type": "string", "enum": ["both", "front", "back"] }, "binding": { "type": "string", "enum": ["left", "right"] }, + "creep": { "type": "number", "minimum": 0, "maximum": 10, "description": "Booklet creep compensation: points each sheet's pages move toward the fold, times the sheet's place from the outside (default 0)." }, "overlap": { "type": "number", "minimum": 0 }, "cut_marks": { "type": "boolean" }, "orientation": { "type": "string", "enum": ["auto", "portrait", "landscape"] }, diff --git a/crates/automation/tests/it/automation.rs b/crates/automation/tests/it/automation.rs index bc9d2e9ee5..e0ceba48c0 100644 --- a/crates/automation/tests/it/automation.rs +++ b/crates/automation/tests/it/automation.rs @@ -6200,6 +6200,9 @@ fn printing_through_tools() { assert_eq!(ok(&mut a, "doc_info", json!({ "doc": printed }))["document"]["pages"].as_u64(), Some(n.div_ceil(4))); let r = ok(&mut a, "doc_print", json!({ "doc": doc, "pages": "1", "layout": "poster", "scale": 400, "path": "poster.pdf" })); assert!(r["sheets"].as_u64().unwrap() > 1); + let r = ok(&mut a, "doc_print", json!({ "doc": doc, "layout": "booklet", "creep": 1.5, "path": "booklet.pdf" })); + assert_eq!(r["sheets"].as_u64(), Some(n.div_ceil(4) * 2), "creep keeps the booklet's sheets"); + assert!(a.call("doc_print", &json!({ "doc": doc, "layout": "booklet", "creep": -1, "path": "x.pdf" })).is_err(), "negative creep"); assert!(matches!(a.call("doc_print", &json!({ "doc": doc })), Err(ToolError::InvalidArgs(_)))); assert!(matches!(a.call("doc_print", &json!({ "doc": doc, "pages": "99", "path": "x.pdf" })), Err(ToolError::InvalidArgs(_)))); } diff --git a/crates/print/README.md b/crates/print/README.md index 6b0aea45b0..e1bbe844cc 100644 --- a/crates/print/README.md +++ b/crates/print/README.md @@ -26,7 +26,9 @@ spool::submit(&pdf, &spool::Job { printer: None, copies: 2, ..Default::default() Headless: `doc_print` with `layout: "multiple"`, `order: "cut-stack"` and `per_sheet: 4` (or any supported grid size), plus `path` or `printer`. - **Booklet**: saddle-stitch imposition padded to a multiple of 4; both sides, front or back - only; left or right binding. + only; left or right binding; creep compensation, which moves each sheet's pages toward the + fold by its place from the outside times the creep (points per sheet, up to `MAX_CREEP`), + clipped at the fold. Headless: `doc_print` with `layout: "booklet"` and `creep`. - **Poster**: tile scale, overlap shared by neighbouring tiles, cut marks. - **Comments & forms**: document, + markups, + stamps, or form fields only; annotations print only with their Print flag, drawn from their appearance streams (Algorithm 8.1). diff --git a/crates/print/src/lib.rs b/crates/print/src/lib.rs index 855a4e2261..84cca070da 100644 --- a/crates/print/src/lib.rs +++ b/crates/print/src/lib.rs @@ -44,7 +44,7 @@ pub use settings::{ #[cfg(test)] use sheets::text_direction; pub use sheets::{ - LABEL_MARGIN, MARGIN, Placement, Sheet, add_cups_orientation, cups_orientation, layout, matching_paper, text_reads_up, texts_read_up, + LABEL_MARGIN, MARGIN, MAX_CREEP, Placement, Sheet, add_cups_orientation, cups_orientation, layout, matching_paper, text_reads_up, texts_read_up, }; /// The names of the Print dialog's choices, shared by the `doc_print` tool and the settings diff --git a/crates/print/src/settings.rs b/crates/print/src/settings.rs index a182948f57..2879b70994 100644 --- a/crates/print/src/settings.rs +++ b/crates/print/src/settings.rs @@ -75,9 +75,12 @@ pub enum Layout { /// portrait sheet that turns to landscape when that suits it. custom: bool, }, + /// `creep` (points, per sheet) moves sheet k's pages toward the fold by `k × creep`, sheet 0 + /// being the outermost, so the margins match once the nested sheets are folded and trimmed. Booklet { subset: BookletSubset, binding: Binding, + creep: f64, }, /// Tile scale (percent), overlap (points), cut marks. Poster { diff --git a/crates/print/src/sheets.rs b/crates/print/src/sheets.rs index cca4bdc07e..ccc0c1d737 100644 --- a/crates/print/src/sheets.rs +++ b/crates/print/src/sheets.rs @@ -168,6 +168,25 @@ fn place_in(page: usize, (dw, dh): (f64, f64), cell: [f64; 4], s: f64, rotate: b Placement { page, matrix, clip: [0.0, 0.0, dw, dh] } } +/// The most creep per sheet, in points. Paper adds a fraction of a point per sheet; more is a mistake. +pub const MAX_CREEP: f64 = 10.0; + +/// `placed` moved `shift` points across the sheet (positive is rightward) and clipped at the fold +/// at `fold`, so a page moved toward it never prints on the facing page. +fn creep_toward_fold(mut placed: Placement, shift: f64, fold: f64, s: f64) -> Placement { + if shift == 0.0 || s <= 0.0 { + return placed; + } + placed.matrix.0[4] += shift; + let at_fold = (fold - placed.matrix.0[4]) / s; + if shift > 0.0 { + placed.clip[2] = placed.clip[2].min(at_fold); + } else { + placed.clip[0] = placed.clip[0].max(at_fold); + } + placed +} + fn fit_scale((dw, dh): (f64, f64), (cw, ch): (f64, f64)) -> f64 { if dw <= 0.0 || dh <= 0.0 { 1.0 } else { (cw / dw).min(ch / dh) } } @@ -288,13 +307,19 @@ pub fn layout(sizes: &[(f64, f64)], settings: &Settings) -> Result, P sheets.push(sheet); } } - Layout::Booklet { subset, binding } => { + Layout::Booklet { subset, binding, creep } => { // Pad to a multiple of 4 with blank pages (None). let n4 = pages.len().div_ceil(4) * 4; let at = |i: usize| pages.get(i).copied(); let size = oriented(paper, Orientation::Landscape, true); let half = (size.0 - 2.0 * MARGIN) / 2.0; let cells = [[MARGIN, MARGIN, MARGIN + half, size.1 - MARGIN], [MARGIN + half, MARGIN, size.0 - MARGIN, size.1 - MARGIN]]; + // The innermost sheet moves furthest; it must stay within half its cell. `pages` is not + // empty (checked above), so there is at least one sheet. + let innermost = creep * (n4 / 4 - 1) as f64; + if !((0.0..=MAX_CREEP).contains(&creep) && innermost < half / 2.0) { + return Err(PrintError::Invalid("invalid booklet creep".into())); + } for k in 0..n4 / 4 { let front = (at(n4 - 1 - 2 * k), at(2 * k)); let back = (at(2 * k + 1), at(n4 - 2 - 2 * k)); @@ -308,11 +333,12 @@ pub fn layout(sizes: &[(f64, f64)], settings: &Settings) -> Result, P for (l, r) in sides { let (l, r) = if binding == Binding::Right { (r, l) } else { (l, r) }; let mut sheet = Sheet { size, ..Sheet::default() }; - for (p, cell) in [(l, cells[0]), (r, cells[1])] { + for (p, cell, toward) in [(l, cells[0], 1.0), (r, cells[1], -1.0)] { if let Some(p) = p { let d = sizes[p]; let s = fit_scale(d, (cell[2] - cell[0], cell[3] - cell[1])); - sheet.placed.push(place_in(p, d, cell, s, false)); + let placed = place_in(p, d, cell, s, false); + sheet.placed.push(creep_toward_fold(placed, toward * creep * k as f64, MARGIN + half, s)); } } sheets.push(sheet); diff --git a/crates/print/src/tests.rs b/crates/print/src/tests.rs index 75a88442ab..a80daf919b 100644 --- a/crates/print/src/tests.rs +++ b/crates/print/src/tests.rs @@ -288,15 +288,58 @@ fn imposing_lists_the_pages_once() { #[test] fn booklets_pair_pages_for_folding() { let sizes = vec![(200.0, 300.0); 6]; - let sheets = layout(&sizes, &settings((0..6).collect(), Layout::Booklet { subset: BookletSubset::BothSides, binding: Binding::Left })).unwrap(); + let sheets = + layout(&sizes, &settings((0..6).collect(), Layout::Booklet { subset: BookletSubset::BothSides, binding: Binding::Left, creep: 0.0 })) + .unwrap(); // 6 pages pad to 8: sheet 1 front [8,1] → [blank, 0], back [1, 6]; sheet 2 front [5, 2], back [3, 4]. assert_eq!(sheet_pages(&sheets), [vec![0], vec![1], vec![5, 2], vec![3, 4]]); assert!(sheets[0].placed[0].matrix.0[4] >= 395.9, "page 1 on the right half"); assert_eq!(sheets[0].size, (792.0, 612.0)); - let front = layout(&sizes, &settings((0..6).collect(), Layout::Booklet { subset: BookletSubset::FrontOnly, binding: Binding::Right })).unwrap(); + let front = + layout(&sizes, &settings((0..6).collect(), Layout::Booklet { subset: BookletSubset::FrontOnly, binding: Binding::Right, creep: 0.0 })) + .unwrap(); assert_eq!(sheet_pages(&front), [vec![0], vec![2, 5]], "right binding mirrors the spread"); } +/// #6777: creep moves each sheet's pages toward the fold by its place from the outside, and clips +/// them at the fold, for either binding. +#[test] +fn booklet_creep_moves_inner_sheets_toward_the_fold() { + let sizes = vec![(200.0, 300.0); 8]; + let booklet = + |binding, creep| layout(&sizes, &settings((0..8).collect(), Layout::Booklet { subset: BookletSubset::BothSides, binding, creep })).unwrap(); + for binding in [Binding::Left, Binding::Right] { + let (flat, crept) = (booklet(binding, 0.0), booklet(binding, 2.0)); + assert_eq!(sheet_pages(&flat), sheet_pages(&crept)); + let fold = 792.0 / 2.0; + for (side, (a, b)) in flat.iter().zip(&crept).enumerate() { + let shift = if side < 2 { 0.0 } else { 2.0 }; + let (left, right) = ((&a.placed[0], &b.placed[0]), (&a.placed[1], &b.placed[1])); + assert!((left.1.matrix.0[4] - left.0.matrix.0[4] - shift).abs() < 1e-9, "{binding:?} side {side}: the left page moves right"); + assert!((right.1.matrix.0[4] - right.0.matrix.0[4] + shift).abs() < 1e-9, "{binding:?} side {side}: the right page moves left"); + let s = left.1.matrix.0[0]; + assert!(left.1.matrix.0[4] + left.1.clip[2] * s <= fold + 1e-9, "{binding:?} side {side}: the left page stops at the fold"); + assert!(right.1.matrix.0[4] + right.1.clip[0] * s >= fold - 1e-9, "{binding:?} side {side}: the right page starts at the fold"); + } + } +} + +#[test] +fn booklet_creep_is_refused_when_it_is_not_a_small_positive_amount() { + let sizes = vec![(200.0, 300.0); 8]; + let booklet = |sizes: &[(f64, f64)], creep| { + layout(sizes, &settings((0..sizes.len()).collect(), Layout::Booklet { subset: BookletSubset::BothSides, binding: Binding::Left, creep })) + }; + for creep in [f64::NAN, f64::INFINITY, -1.0, MAX_CREEP + 0.1] { + assert!(matches!(booklet(&sizes, creep), Err(PrintError::Invalid(_))), "{creep}"); + } + assert!(booklet(&sizes, MAX_CREEP).is_ok()); + // 400 pages fold into 100 sheets: 5 pt each would move the innermost past half its cell. + let thick = vec![(200.0, 300.0); 400]; + assert!(booklet(&thick, 1.0).is_ok()); + assert!(matches!(booklet(&thick, 5.0), Err(PrintError::Invalid(_))), "the innermost sheet moves too far"); +} + #[test] fn posters_tile_with_overlap() { // A 200×300 page at 400% = 800×1200 on Letter with 18 pt margins (576×756 tiles), overlap 36. @@ -709,9 +752,11 @@ fn landscape_sheets_ask_cups_to_turn_the_page() { // A landscape Size sheet, and a booklet (always landscape sheets), are both turned. let page = layout(&[(300.0, 200.0)], &settings(vec![0], Layout::Size(SizeMode::Fit))).unwrap(); assert_eq!(cups_orientation(&page), ask); - let booklet = - layout(&[(200.0, 300.0); 4], &settings(vec![0, 1, 2, 3], Layout::Booklet { subset: BookletSubset::BothSides, binding: Binding::Left })) - .unwrap(); + let booklet = layout( + &[(200.0, 300.0); 4], + &settings(vec![0, 1, 2, 3], Layout::Booklet { subset: BookletSubset::BothSides, binding: Binding::Left, creep: 0.0 }), + ) + .unwrap(); assert_eq!(cups_orientation(&booklet), ask); // Portrait sheets, mixed jobs and nothing at all say nothing. let portrait = layout(&[(200.0, 300.0)], &settings(vec![0], Layout::Size(SizeMode::Fit))).unwrap(); diff --git a/crates/ui-egui/src/workflows/print_ui/body.rs b/crates/ui-egui/src/workflows/print_ui/body.rs index e4cc51916f..f2e288661c 100644 --- a/crates/ui-egui/src/workflows/print_ui/body.rs +++ b/crates/ui-egui/src/workflows/print_ui/body.rs @@ -93,7 +93,10 @@ pub(crate) fn body( ui.label(egui::RichText::new(tl!("Print")).font(theme::semibold(18.0))); ui.add_space(8.0); let settings = d.settings(sizes.len(), labels); - let sheets = settings.as_ref().ok().and_then(|s| print::layout(sizes, s).ok()).unwrap_or_default(); + let laid = settings.as_ref().ok().map(|s| print::layout(sizes, s)); + // Creep within its field's range can still be too much for a thick booklet. + let too_much_creep = d.handling == Handling::Booklet && d.creep > 0.0 && matches!(laid, Some(Err(_))); + let sheets = laid.and_then(Result::ok).unwrap_or_default(); d.sheet = d.sheet.min(sheets.len().saturating_sub(1)); // Cap the settings+preview area to the screen's available height (#4252, #5317): // on a short screen, or with the driver's own Properties… panel expanded below, this row can @@ -424,6 +427,10 @@ pub(crate) fn body( ); ui.label(tl!("Binding:")); combo(ui, "binding", tl!("Binding:"), &mut d.binding, &[(Binding::Left, tl!("Left")), (Binding::Right, tl!("Right"))], 80.0); + let creep_label = ui.label(tl!("Creep:")); + ui.add(egui::DragValue::new(&mut d.creep).range(0.0..=print::MAX_CREEP).speed(0.05).suffix(" pt")) + .labelled_by(creep_label.id) + .on_hover_text(tl!("Moves each sheet's pages toward the fold by this much more than the sheet outside it, so the margins match once the booklet is folded and trimmed.")); }); } } @@ -483,6 +490,10 @@ pub(crate) fn body( (Err(e), _) => { ui.put(area.shrink(16.0), egui::Label::new(egui::RichText::new(tl!(e)).color(t.text_muted)).wrap()); } + (Ok(_), None) if too_much_creep => { + let msg = tl!("The creep moves the innermost sheet's pages too far for this many pages. Reduce Creep."); + ui.put(area.shrink(16.0), egui::Label::new(egui::RichText::new(msg).color(t.text_muted)).wrap()); + } (Ok(_), Some(sheet)) => { let k = (PREVIEW_BOX.0 / sheet.size.0 as f32).min(PREVIEW_BOX.1 / sheet.size.1 as f32); let paper = Rect::from_center_size(area.center(), vec2(sheet.size.0 as f32 * k, sheet.size.1 as f32 * k)); diff --git a/crates/ui-egui/src/workflows/print_ui/draft.rs b/crates/ui-egui/src/workflows/print_ui/draft.rs index d70aba8ee8..4c3325401c 100644 --- a/crates/ui-egui/src/workflows/print_ui/draft.rs +++ b/crates/ui-egui/src/workflows/print_ui/draft.rs @@ -61,6 +61,8 @@ pub struct PrintDraft { pub auto_rotate: bool, pub booklet_subset: BookletSubset, pub binding: Binding, + /// Booklet creep compensation, points per sheet. + pub creep: f64, pub poster_scale: f64, pub overlap: f64, pub cut_marks: bool, @@ -131,6 +133,7 @@ impl Default for PrintDraft { auto_rotate: true, booklet_subset: BookletSubset::BothSides, binding: Binding::Left, + creep: 0.0, poster_scale: 200.0, overlap: 18.0, cut_marks: true, @@ -249,7 +252,7 @@ impl PrintDraft { other => other, } } - Handling::Booklet => Layout::Booklet { subset: self.booklet_subset, binding: self.binding }, + Handling::Booklet => Layout::Booklet { subset: self.booklet_subset, binding: self.binding, creep: self.creep }, Handling::Poster => Layout::Poster { scale: self.poster_scale, overlap: self.overlap, cut_marks: self.cut_marks }, Handling::Label => Layout::Label { flip: self.label_flip }, }; diff --git a/crates/ui-egui/tests/it/print.rs b/crates/ui-egui/tests/it/print.rs index e85a5b7099..13f74198cf 100644 --- a/crates/ui-egui/tests/it/print.rs +++ b/crates/ui-egui/tests/it/print.rs @@ -1125,6 +1125,10 @@ fn a_booklet_decides_two_sided_itself() { let d = &h.state().print_draft; assert_eq!(d.duplex, Some(Duplex::LongEdge), "the Two-sided choice is kept"); assert_eq!(d.job("x").duplex, Some(Duplex::ShortEdge), "both sides flip on the short edge"); + assert!(h.query_all_by_label("Creep:").next().is_some(), "a Creep field beside Binding"); + h.state_mut().print_draft.creep = 1.5; + let layout = h.state().print_draft.settings(4, &[]).map(|s| s.layout); + assert!(matches!(layout, Ok(pdfcraft_engine::print::Layout::Booklet { creep, .. }) if creep == 1.5), "the creep reaches the booklet (#6777)"); for subset in [BookletSubset::FrontOnly, BookletSubset::BackOnly] { h.state_mut().print_draft.booklet_subset = subset; assert_eq!(h.state().print_draft.job("x").duplex, Some(Duplex::Off), "{subset:?} is one pass of a manual duplex"); @@ -1135,6 +1139,29 @@ fn a_booklet_decides_two_sided_itself() { assert_eq!(h.state().print_draft.job("x").duplex, Some(Duplex::LongEdge), "back to the choice"); } +/// Creep the field allows can still move a thick booklet's innermost pages too far; the preview +/// says to reduce it rather than showing no sheets without a reason (#6777). +#[test] +fn too_much_creep_for_a_thick_booklet_is_explained_in_the_preview() { + let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { + let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); + app.open_bytes("thick.pdf", None, crate::support::fixtures::pdf_pages(400)).unwrap(); + app + }); + h.run_steps(4); + assert!(h.state_mut().execute("print.dialog")); + h.run_steps(3); + h.get_by_label("Booklet").click(); + h.run_steps(2); + h.state_mut().print_draft.creep = 5.0; + h.run_steps(2); + h.get_by_label_contains("Reduce Creep"); + h.state_mut().print_draft.creep = 0.5; + h.run_steps(2); + assert!(h.query_by_label_contains("Reduce Creep").is_none(), "a creep that fits lays the booklet out"); +} + /// An app restored from `saved` settings (if any), with a form open and its Print dialog shown. fn print_opened_with(saved: Option<&str>) -> PdfCraftApp { let mut app = PdfCraftApp::new(); diff --git a/crates/ui-egui/tests/it/reading_colors.rs b/crates/ui-egui/tests/it/reading_colors.rs index ac48d89d05..557696dbf4 100644 --- a/crates/ui-egui/tests/it/reading_colors.rs +++ b/crates/ui-egui/tests/it/reading_colors.rs @@ -9,41 +9,7 @@ use pdfcraft_ui_egui::PdfCraftApp; /// One valid page (proper xref, so no repair banner covers the dialog): black text on white. fn pdf() -> Vec { - pdf_pages(1) -} - -fn pdf_pages(count: usize) -> Vec { - let mut objs: Vec> = vec![ - b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(), - b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(), - b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 200] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>".to_vec(), - { - let body = b"BT /F1 14 Tf 20 150 Td (The quick brown fox) Tj ET"; - format!("<< /Length {} >>\nstream\n{}\nendstream", body.len(), String::from_utf8_lossy(body)).into_bytes() - }, - b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>".to_vec(), - ]; - let mut kids = vec!["3 0 R".to_owned()]; - for _ in 1..count { - kids.push(format!("{} 0 R", objs.len() + 1)); - objs.push(objs[2].clone()); - } - objs[1] = format!("<< /Type /Pages /Kids [{}] /Count {count} >>", kids.join(" ")).into_bytes(); - let mut out = b"%PDF-1.7\n".to_vec(); - let mut offsets = Vec::new(); - for (i, o) in objs.iter().enumerate() { - offsets.push(out.len()); - out.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes()); - out.extend_from_slice(o); - out.extend_from_slice(b"\nendobj\n"); - } - let xref = out.len(); - out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes()); - for o in offsets { - out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes()); - } - out.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objs.len() + 1).as_bytes()); - out + crate::support::fixtures::pdf_pages(1) } #[test] @@ -61,7 +27,7 @@ fn pending_pages_use_document_reading_background() { ] { let mut app = PdfCraftApp::new(); app.set_option("language", "en").unwrap(); - app.open_bytes("pending.pdf", None, pdf_pages(3)).unwrap(); + app.open_bytes("pending.pdf", None, crate::support::fixtures::pdf_pages(3)).unwrap(); let id = app.views[0].id; // Deliberately differ from the app preference: the document is the source of truth. app.session.set_reading_colors(id, colors); diff --git a/crates/ui-egui/tests/it/support/fixtures.rs b/crates/ui-egui/tests/it/support/fixtures.rs new file mode 100644 index 0000000000..d20cc1a3b1 --- /dev/null +++ b/crates/ui-egui/tests/it/support/fixtures.rs @@ -0,0 +1,37 @@ +//! Synthetic documents shared by the shell tests. + +/// `count` valid pages (proper xref, so no repair banner covers a dialog), each 300 x 200 pt with +/// black text on white. +pub fn pdf_pages(count: usize) -> Vec { + let mut objs: Vec> = vec![ + b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(), + b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(), + b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 200] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>".to_vec(), + { + let body = b"BT /F1 14 Tf 20 150 Td (The quick brown fox) Tj ET"; + format!("<< /Length {} >>\nstream\n{}\nendstream", body.len(), String::from_utf8_lossy(body)).into_bytes() + }, + b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>".to_vec(), + ]; + let mut kids = vec!["3 0 R".to_owned()]; + for _ in 1..count { + kids.push(format!("{} 0 R", objs.len() + 1)); + objs.push(objs[2].clone()); + } + objs[1] = format!("<< /Type /Pages /Kids [{}] /Count {count} >>", kids.join(" ")).into_bytes(); + let mut out = b"%PDF-1.7\n".to_vec(); + let mut offsets = Vec::new(); + for (i, o) in objs.iter().enumerate() { + offsets.push(out.len()); + out.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes()); + out.extend_from_slice(o); + out.extend_from_slice(b"\nendobj\n"); + } + let xref = out.len(); + out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes()); + for o in offsets { + out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes()); + } + out.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objs.len() + 1).as_bytes()); + out +} diff --git a/crates/ui-egui/tests/it/support/mod.rs b/crates/ui-egui/tests/it/support/mod.rs index 0d5f15b68a..e6993666dd 100644 --- a/crates/ui-egui/tests/it/support/mod.rs +++ b/crates/ui-egui/tests/it/support/mod.rs @@ -8,6 +8,7 @@ use egui::epaint::text::{Fonts, TextOptions}; use egui_kittest::Harness; use pdfcraft_ui_egui::{PdfCraftApp, i18n, theme}; +pub mod fixtures; pub mod input; /// Step frames until `ready` holds or 30 seconds pass, and say whether it held. For background work diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index c2b243b2f3..0bb6e07eac 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -8297,8 +8297,8 @@ milestone = "M10" status = "shipped" commands = ["print.dialog"] tools = ["doc_print"] -evidence = ["crates/print/src/tests.rs::booklets_pair_pages_for_folding", "crates/ui-egui/tests/it/print.rs::a_booklet_decides_two_sided_itself", "crates/automation/src/printing.rs::a_booklet_decides_two_sided_itself", "crates/print/src/tests.rs::a_booklet_prints_both_sides_on_the_short_edge"] -notes = "both/front/back, left/right binding; a booklet decides two-sided itself, as Acrobat does (its Print dialog hides Two-sided while Booklet is chosen; Using Acrobat X Pro: Acrobat automatically enables duplex printing for booklet printing): both sides flip on the short edge, front or back only prints one-sided, and the Two-sided choice is kept for the other layouts (#2707)" +evidence = ["crates/print/src/tests.rs::booklets_pair_pages_for_folding", "crates/ui-egui/tests/it/print.rs::a_booklet_decides_two_sided_itself", "crates/automation/src/printing.rs::a_booklet_decides_two_sided_itself", "crates/print/src/tests.rs::a_booklet_prints_both_sides_on_the_short_edge", "crates/print/src/tests.rs::booklet_creep_moves_inner_sheets_toward_the_fold"] +notes = "both/front/back, left/right binding; a booklet decides two-sided itself, as Acrobat does (its Print dialog hides Two-sided while Booklet is chosen; Using Acrobat X Pro: Acrobat automatically enables duplex printing for booklet printing): both sides flip on the short edge, front or back only prints one-sided, and the Two-sided choice is kept for the other layouts (#2707); creep compensation (points per sheet, default 0) moves each sheet's pages toward the fold by their sheet's place from the outside, clipped at the fold (#6777); the amount is entered, not worked out from the paper's thickness" [[feature]] id = "print.poster"