From 5bf48e0f8c8fa95d58a359fb50043a3155e5392c Mon Sep 17 00:00:00 2001 From: Gert Pellin Date: Fri, 16 Oct 2026 12:40:39 +0200 Subject: [PATCH] M7: doc_header_footer takes shrink (#2140) The tool passes shrink through to the header and footer, with an end-to-end test that reads the moved text back from a saved copy, refuses margins that leave no room and checks that Remove brings the content back to full size. --- crates/automation/src/lib.rs | 1 + crates/automation/src/tools.rs | 3 ++- crates/automation/tests/automation.rs | 37 +++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/crates/automation/src/lib.rs b/crates/automation/src/lib.rs index 0e2e0dd698..52a397c48d 100644 --- a/crates/automation/src/lib.rs +++ b/crates/automation/src/lib.rs @@ -1207,6 +1207,7 @@ impl Automation { if let Some(s) = a.opt_int("start_number")? { hf.start_number = s.clamp(1, u32::MAX as i64) as u32; } + hf.shrink = a.opt_bool("shrink")?.unwrap_or(false); Edit::AddHeaderFooter { pages, settings: hf, replace } } "doc_watermark" => { diff --git a/crates/automation/src/tools.rs b/crates/automation/src/tools.rs index e154e1eebc..74a1941467 100644 --- a/crates/automation/src/tools.rs +++ b/crates/automation/src/tools.rs @@ -869,7 +869,7 @@ pub fn tools() -> Vec { t( "doc_header_footer", "Add header and footer", - "Add a header and/or footer to pages. Text boxes: header_left/center/right, footer_left/center/right. Tokens: <<1>> page number, <> page count, <<1 of n>>, <>, <<1/n>>, dates <> <> <> (and more), Bates <>. replace: true swaps out existing ones (Update). Undoable.", + "Add a header and/or footer to pages. Text boxes: header_left/center/right, footer_left/center/right. Tokens: <<1>> page number, <> page count, <<1 of n>>, <>, <<1/n>>, dates <> <> <> (and more), Bates <>. shrink: true scales the page content down evenly into the room the header and footer leave free, so they don't cover it. replace: true swaps out existing ones (Update). Undoable.", ) .cmd("edit.header_footer") .with(schema( @@ -882,6 +882,7 @@ pub fn tools() -> Vec { "color": { "type": "string", "description": "#RRGGBB or a colour name." }, "margins": { "type": "array", "items": { "type": "number", "minimum": 0 }, "minItems": 4, "maxItems": 4, "description": "Top, bottom, left, right in points (default 36, 36, 72, 72)." }, "start_number": { "type": "integer", "minimum": 1 }, + "shrink": { "type": "boolean", "description": "Shrink the page content so the header and footer don't cover it (default false)." }, "replace": { "type": "boolean" }, }), &["doc"], diff --git a/crates/automation/tests/automation.rs b/crates/automation/tests/automation.rs index cbf40f1962..2e1fdee4f2 100644 --- a/crates/automation/tests/automation.rs +++ b/crates/automation/tests/automation.rs @@ -2241,6 +2241,43 @@ fn headers_watermarks_and_backgrounds_through_tools() { ok(&mut a, "doc_save", json!({ "doc": doc, "path": "marked.pdf" })); } +#[test] +fn a_shrinking_header_and_footer_moves_the_page_content_out_of_their_way_through_tools() { + let dir = workdir("hf-shrink"); + let mut a = auto(&dir); + let doc = ok(&mut a, "doc_open", json!({ "path": "a.pdf" }))["doc"].as_u64().unwrap(); + // The box of "Page 1" (left, top, width, height in points), read back from a saved copy. + let mut saved = 0; + let mut text_box = |a: &mut Automation| -> [f64; 4] { + saved += 1; + let path = format!("shrunk{saved}.pdf"); + ok(a, "doc_save", json!({ "doc": doc, "path": path })); + let copy = ok(a, "doc_open", json!({ "path": path }))["doc"].as_u64().unwrap(); + let found = ok(a, "text_find", json!({ "doc": copy, "query": "Page 1" })); + let r = found["matches"][0]["rects"][0].as_array().unwrap_or_else(|| panic!("Page 1 is on the page: {found}")); + let v: Vec = r.iter().map(|x| x.as_f64().unwrap()).collect(); + [v[0], v[1], v[2] - v[0], v[3] - v[1]] + }; + let before = text_box(&mut a); + ok( + &mut a, + "doc_header_footer", + json!({ "doc": doc, "pages": [1], "header_center": "ACME", "footer_right": "<>", "shrink": true }), + ); + let after = text_box(&mut a); + // 200 × 300 page, 8 pt text and 36 pt margins: the header takes 48 pt, the footer 46.4 pt. + let k = (300.0 - 48.0 - 46.4) / 300.0; + assert!((after[2] / before[2] - k).abs() < 0.02 && (after[3] / before[3] - k).abs() < 0.02, "scaled by {k}: {before:?} → {after:?}"); + assert!((after[0] - (20.0 * k + (200.0 - 200.0 * k) / 2.0)).abs() < 1.0, "centred across: {before:?} → {after:?}"); + assert!(page_text(&mut a, doc)[0].contains("000001"), "the Bates number is drawn"); + // Margins that leave no room are refused; Remove brings the content back to full size. + let full = json!({ "doc": doc, "header_center": "X", "margins": [295, 36, 72, 72], "shrink": true, "replace": true }); + assert!(matches!(a.call("doc_header_footer", &full), Err(ToolError::Failed(_))), "no room"); + ok(&mut a, "doc_remove_marks", json!({ "doc": doc, "kind": "header_footer" })); + let restored = text_box(&mut a); + assert!(restored.iter().zip(before).all(|(r, b)| (r - b).abs() < 0.01), "{before:?} → {restored:?}"); +} + #[test] fn watermarks_move_from_the_page_centre_through_tools() { let dir = workdir("wm-offset");