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.
This commit is contained in:
Gert Pellin
2026-10-21 09:02:09 +02:00
parent 01149dea77
commit 5bf48e0f8c
3 changed files with 40 additions and 1 deletions
+1
View File
@@ -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" => {
+2 -1
View File
@@ -869,7 +869,7 @@ pub fn tools() -> Vec<ToolDef> {
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, <<n>> page count, <<1 of n>>, <<Page 1 of n>>, <<1/n>>, dates <<m/d/yyyy>> <<yyyy-mm-dd>> <<mmmm d, yyyy>> (and more), Bates <<Bates Number#6#1#PREFIX#SUFFIX>>. 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, <<n>> page count, <<1 of n>>, <<Page 1 of n>>, <<1/n>>, dates <<m/d/yyyy>> <<yyyy-mm-dd>> <<mmmm d, yyyy>> (and more), Bates <<Bates Number#6#1#PREFIX#SUFFIX>>. 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<ToolDef> {
"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"],
+37
View File
@@ -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<f64> = 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": "<<Bates Number#6#1##>>", "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");