Booklet printing: creep compensation (#6790)

This commit is contained in:
Ken Mantle authored and GitHub committed 2026-11-10 10:00:36 +07:00
1 parent c6d9c13002
commit 5b67f8d021
15 files changed
+177 -51

No files matched your search

+2 -1
View File
@@ -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}");
+1
View File
@@ -764,6 +764,7 @@ pub fn tools() -> Vec<ToolDef> {
"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"] },
+3
View File
@@ -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(_))));
}
+3 -1
View File
@@ -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).
+1 -1
View File
@@ -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
+3
View File
@@ -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 {
+29 -3
View File
@@ -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<Vec<Sheet>, 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<Vec<Sheet>, 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);
+50 -5
View File
@@ -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();
+12 -1
View File
@@ -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));
@@ -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 },
};
+27
View File
@@ -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();
+2 -36
View File
@@ -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<u8> {
pdf_pages(1)
}
fn pdf_pages(count: usize) -> Vec<u8> {
let mut objs: Vec<Vec<u8>> = 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);
@@ -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<u8> {
let mut objs: Vec<Vec<u8>> = 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
}
+1
View File
@@ -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
+2 -2
View File
@@ -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"