Merge PR #2832: M7: Header & footer: shrink the page content out of their way (#2140)

ATTRIBUTION.toml: sha256 lines taken from the base side; recomputed after all merges.
This commit is contained in:
aiongg committed 2026-10-22 01:45:14 +00:00
commit 29ad20342c
31 files changed
+258 -7

No files matched your search

+1
View File
@@ -205,6 +205,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md
- **2026-10-16 (M10, #1201 partial):** Lossless image export applies each component's Decode pair. Indexed values round and clip before palette lookup. Color-key masks keep raw samples. The existing 16-bit precision limit remains. JPEG and mask Decode, ICC transforms, and calibrated colors remain outside this fix. Estimates unchanged.
- **2026-10-16 (M3, #2739):** Wrapped revision buttons stay above Binding in Document Properties. A synthetic regression checks control bounds and opens a saved revision at two viewport widths. Estimates unchanged.
- **2026-10-19 (M7, tracked text):** Editable paragraphs and object labels distinguish repeated glyph tracking from word gaps in TJ arrays and separately positioned glyphs. Synthetic regressions cover character spacing, horizontal scaling, rotated baselines, larger word gaps, and rows of one-glyph words or cells (inferred tracking is capped at half an em, so `a b c` and digit or Y/N columns keep their spaces). Estimates unchanged.
- **2026-10-16 (M7, #2140):** Header & footer (and Bates numbering, which uses the same dialog) can shrink the page content so the header and footer don't cover it: the original content is scaled evenly into the band they leave free, centred across, also on rotated pages; Remove and Update take the scaling away, and margins that leave no room are refused. Dialog checkbox with a preview of the band, `shrink` on `doc_header_footer`, all catalogs. Annotations and added text and images are not moved yet. Estimates unchanged.
- **2026-10-15 (M2, gap 12):** `cargo xtask text-oracle` scores reading order as well as word F1: the longest common subsequence of the two word sequences over the longer one. F1 is a bag of words and cannot see order at all -- measuring #2431, a change that moves text and nothing else, it called 26 of the 29 files whose output changed identical, where the order score separated 18 improvements from 6 regressions. Reports, does not gate; a disagreement with Poppler, not a verdict. Estimates unchanged.
- **2026-10-15 (M2, #2305 follow-up):** WinAnsiEncoding 0xA0 and 0xAD and MacRomanEncoding 0xCA extract as the ordinary space and hyphen Annex D.2 names them, in the vendored hayro tables (patch 30) as well as `crates/fonts`, so search, copy and redaction see characters that were hidden behind U+00A0 and U+00AD. Across the pdf.js corpus the soft hyphens we extract fall 117 to 13 and the non-breaking spaces 30 to 26; the rest come from `/ToUnicode`. An explicit `/Differences` name keeps its glyph list value, as pdftotext does. Estimates unchanged.
- **2026-10-15 (M12, XFA, #2574):** A `pageArea` with no `<occur>` repeats without limit instead of taking a subform's default of one use, so a later page area with `occur max="1"` is no longer drawn (with a copy of its fields) over every body page. A page area's `occur min` is honoured too: one the body never reached gets the pages it still needs after the body (the once-only closing page of a form, with its button, comes last). Acrobat Pro also puts that page last, and when printing keeps it but leaves it blank (`relevant="-print"` on the page area), which PdfCraft does not do yet; both re-observed on a synthetic fixture, see the 2026-10-19 line. Synthetic regression tests. Estimates unchanged.
+1
View File
@@ -1208,6 +1208,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
@@ -875,7 +875,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(
@@ -888,6 +888,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");
+71 -1
View File
@@ -65,12 +65,24 @@ pub struct HeaderFooter {
pub margins: [f64; 4],
/// The number `<<1>>` shows on the first page of the range.
pub start_number: u32,
/// Shrink the page content so the header and footer don't cover it (Acrobat's Appearance
/// Options): the original content is scaled down evenly into the band the header and footer
/// leave free, centred across the page. Remove and Update take the scaling away again.
pub shrink: bool,
}
impl Default for HeaderFooter {
/// Acrobat's defaults: 8 pt black, margins 0.5 in top and bottom, 1 in left and right.
fn default() -> Self {
Self { text: Default::default(), font_size: 8.0, color: [0.0; 3], underline: false, margins: [36.0, 36.0, 72.0, 72.0], start_number: 1 }
Self {
text: Default::default(),
font_size: 8.0,
color: [0.0; 3],
underline: false,
margins: [36.0, 36.0, 72.0, 72.0],
start_number: 1,
shrink: false,
}
}
}
@@ -83,6 +95,42 @@ impl HeaderFooter {
pub fn first_undrawable(&self) -> Option<char> {
self.text.iter().flat_map(|t| t.lines()).find_map(pdfcraft_fonts::first_non_win_ansi)
}
/// The room the header and the footer take at the top and the bottom of a page, in points:
/// the margin, the lines of the tallest box, and a clearance of half the font size. A side
/// without text takes none. This is the room [`HeaderFooter::shrink`] keeps free.
pub fn bands(&self) -> (f64, f64) {
let size = self.font_size;
let line_h = size * 1.2;
let lines = |boxes: &[String]| boxes.iter().filter(|t| !t.trim().is_empty()).map(|t| t.lines().count().max(1)).max();
let (header, footer) = self.text.split_at(3);
// Header lines hang below the top margin (a full line each); the footer's last line sits
// on the bottom margin, so only its capitals (0.8 of the size) rise above it.
let top = lines(header).map_or(0.0, |n| self.margins[0] + size + (n - 1) as f64 * line_h + size / 2.0);
let bottom = lines(footer).map_or(0.0, |n| self.margins[1] + size * 0.8 + (n - 1) as f64 * line_h + size / 2.0);
(top, bottom)
}
}
/// The `cm` that shrinks a page's original content (user space) into the band `hf` leaves free,
/// for a page shown `size` (display space) with view matrix `view` (display to user space): an
/// even scale, centred across, the content's bottom on the footer's band.
fn shrink_matrix(hf: &HeaderFooter, size: (f64, f64), view: [f64; 6]) -> Result<[f64; 6], EditError> {
let (w, h) = size;
let (top, bottom) = hf.bands();
let free = h - top - bottom;
if !(free.is_finite() && free >= 1.0 && w.is_finite() && w > 0.0) {
return Err(EditError::Invalid("the header and footer margins leave no room for the page content to shrink into".into()));
}
let k = free / h;
let v = kurbo::Affine::new(view);
let det = v.determinant();
if !(det.is_finite() && det.abs() > 1e-12) {
return Err(EditError::Invalid("the page can't be shrunk: its view matrix is degenerate".into()));
}
let shrink = kurbo::Affine::new([k, 0.0, 0.0, k, (w - w * k) / 2.0, bottom]);
// User space to display space, shrink there, and back.
Ok((v * shrink * v.inverse()).as_coeffs())
}
/// A picture for a background or watermark (Acrobat: Source ▸ File): an XObject already in
@@ -552,6 +600,18 @@ fn place_tagged(doc: &mut Document, page: &pdfcraft_model::Page, tag: &str, cont
Ok(())
}
/// Put content tagged `tag` first inside the q/Q wrapper, so it applies to the page's original
/// content and nothing else (a header's shrink). A page without a wrapper has no original content
/// on it and is left alone.
fn place_inside_wrapper(doc: &mut Document, page: &pdfcraft_model::Page, tag: &str, content: Vec<u8>) -> Result<(), EditError> {
let mut list = contents(doc, page)?;
let Some(open) = list.iter().position(|o| stream_tag(doc, o).as_deref() == Some("Wrap")) else { return Ok(()) };
let stream = Object::Ref(doc.add(Object::Stream(Stream::flate(tagged(tag), &content))));
list.insert(open + 1, stream);
doc.update_dict(page.obj, |d| d.set(b"Contents".to_vec(), Object::Array(list)))?;
Ok(())
}
/// Draw `content` on top of a page (0-based) as permanent page content, with standard Helvetica
/// available as `/PCHelv`. The stream is tagged `tag` (not a mark kind, so Remove never takes
/// it away); the original content is wrapped in q/Q first. Used for applied redaction boxes.
@@ -632,6 +692,11 @@ pub fn add_header_footer(doc: &mut Document, pages: &[usize], hf: &HeaderFooter,
u64::MAX
)));
}
// Shrinking needs room on every page: refuse before anything is drawn or replaced.
let shrinks = pages
.iter()
.map(|&i| hf.shrink.then(|| shrink_matrix(hf, all[i].display_size(doc), all[i].view_matrix(doc))).transpose())
.collect::<Result<Vec<_>, _>>()?;
if replace {
remove_marks(doc, pages, MarkKind::HeaderFooter)?;
}
@@ -679,6 +744,11 @@ pub fn add_header_footer(doc: &mut Document, pages: &[usize], hf: &HeaderFooter,
add_resources(doc, page, None, None)?;
let page = &page_list(doc)[i];
place(doc, page, MarkKind::HeaderFooter, content, false)?;
if let Some(Some(m)) = shrinks.get(k) {
let cm = format!("{} cm\n", m.map(matrix_number).join(" "));
let page = &page_list(doc)[i];
place_inside_wrapper(doc, page, MarkKind::HeaderFooter.tag(), cm.into_bytes())?;
}
}
Ok(())
}
+94
View File
@@ -156,6 +156,100 @@ fn replace_and_remove_restore_the_original_content() {
assert_eq!(streams(&reopen(&doc), 0), original, "back to exactly the original content");
}
/// The six numbers of the first `… cm` in `content`.
fn first_cm(content: &str) -> [f64; 6] {
let line = content.lines().find(|l| l.ends_with(" cm")).unwrap_or_else(|| panic!("no cm in {content:?}"));
let v: Vec<f64> = line.split_whitespace().take(6).map(|x| x.parse().unwrap()).collect();
<[f64; 6]>::try_from(v).unwrap()
}
fn map_point(m: [f64; 6], x: f64, y: f64) -> (f64, f64) {
(m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5])
}
fn near(a: (f64, f64), b: (f64, f64)) -> bool {
(a.0 - b.0).abs() < 0.01 && (a.1 - b.1).abs() < 0.01
}
fn shrinking(header: &str, footer: &str) -> HeaderFooter {
let mut hf = HeaderFooter { shrink: true, ..HeaderFooter::default() };
hf.text[1] = header.into();
hf.text[5] = footer.into();
hf
}
#[test]
fn shrink_scales_the_original_content_into_the_band_between_header_and_footer() {
let mut doc = fixture();
// 8 pt text, margins 36 top and bottom: the header takes 36 + 8 (+ 4 clearance) at the top,
// the footer 36 + 6.4 (its capitals above the baseline, + 4) at the bottom.
add_header_footer(&mut doc, &[0], &shrinking("ACME", "<<Bates Number#6#1##>>"), false, &cx()).unwrap();
let doc = reopen(&doc);
let s = streams(&doc, 0);
// q-wrapper, shrink, original, Q-wrapper, header/footer.
assert_eq!(s.len(), 5, "{s:?}");
assert_eq!((s[0].as_str(), s[3].as_str()), ("q %PdfCraft\n", "\nQ %PdfCraft\n"));
assert!(s[2].contains("(Hi) Tj"), "the original content follows the shrink: {s:?}");
let m = first_cm(&s[1]);
let k = (800.0 - 48.0 - 46.4) / 800.0;
assert!((m[0] - k).abs() < 1e-3 && (m[3] - k).abs() < 1e-3 && m[1] == 0.0 && m[2] == 0.0, "uniform scale {k}: {m:?}");
// The page's corners land on the band, centred across.
assert!(near(map_point(m, 0.0, 0.0), ((600.0 - 600.0 * k) / 2.0, 46.4)), "{m:?}");
assert!(near(map_point(m, 600.0, 800.0), ((600.0 + 600.0 * k) / 2.0, 800.0 - 48.0)), "{m:?}");
assert!(s[4].contains("(000001) Tj"), "{}", s[4]);
}
#[test]
fn shrink_follows_the_display_space_of_a_rotated_page() {
let mut doc = fixture();
// Page 2 is rotated 90°: shown 800 wide and 600 high. A footer alone keeps the top.
add_header_footer(&mut doc, &[1], &shrinking("", "<<1>>"), false, &cx()).unwrap();
let s = streams(&doc, 1);
let m = first_cm(&s[1]);
let page = &pdfcraft_model::pages(&doc)[1];
let v = page.view_matrix(&doc);
let k = (600.0 - 46.4) / 600.0;
// Display (x, y) is user v·(x, y): the shrunk content's display corners.
let to_user = |x: f64, y: f64| map_point(v, x, y);
assert!(near(map_point(m, to_user(0.0, 0.0).0, to_user(0.0, 0.0).1), to_user((800.0 - 800.0 * k) / 2.0, 46.4)), "{m:?}");
assert!(near(map_point(m, to_user(800.0, 600.0).0, to_user(800.0, 600.0).1), to_user((800.0 + 800.0 * k) / 2.0, 600.0)), "{m:?}");
}
#[test]
fn shrink_is_undone_by_remove_and_not_doubled_by_update() {
let mut doc = fixture();
let original = streams(&doc, 0);
add_background(&mut doc, &[0], &Background::default(), false).unwrap();
add_header_footer(&mut doc, &[0], &shrinking("First", ""), false, &cx()).unwrap();
add_header_footer(&mut doc, &[0], &shrinking("Second", ""), true, &cx()).unwrap();
let s = streams(&doc, 0);
assert_eq!(s.iter().filter(|x| x.contains(" cm") && !x.contains("/PCMark")).count(), 2, "one shrink besides the original's own cm: {s:?}");
assert!(s[0].contains("/PCMark /Background"), "the background stays behind, full size: {s:?}");
// Update without shrinking: back to full size.
let mut plain = shrinking("Third", "");
plain.shrink = false;
add_header_footer(&mut doc, &[0], &plain, true, &cx()).unwrap();
assert_eq!(streams(&doc, 0).len(), 5, "background, wrapper, original, wrapper, header: {:?}", streams(&doc, 0));
add_header_footer(&mut doc, &[0], &shrinking("Fourth", ""), true, &cx()).unwrap();
assert_eq!(remove_marks(&mut doc, &[0], MarkKind::HeaderFooter).unwrap(), 2, "the header and its shrink");
assert_eq!(remove_marks(&mut doc, &[0], MarkKind::Background).unwrap(), 1);
assert_eq!(streams(&reopen(&doc), 0), original, "back to exactly the original content");
}
#[test]
fn shrink_with_no_room_left_is_refused_without_drawing() {
let mut doc = fixture();
let mut hf = shrinking("Top", "Bottom");
hf.margins = [400.0, 400.0, 72.0, 72.0];
let err = add_header_footer(&mut doc, &[0, 1], &hf, false, &cx()).unwrap_err();
assert!(err.to_string().contains("no room"), "{err}");
assert!(!doc.is_modified());
// A page without content has nothing to shrink: it just gets the header.
let mut doc = fixture();
add_header_footer(&mut doc, &[2], &shrinking("Top", ""), false, &cx()).unwrap();
assert_eq!(streams(&doc, 2).len(), 1);
}
#[test]
fn watermarks_rotate_fade_and_can_go_behind() {
let mut doc = fixture();
+1
View File
@@ -841,6 +841,7 @@
Insert Page Number إدراج رقم الصفحة
Insert Date إدراج التاريخ
Start page number رقم صفحة البداية
Shrink the page content to make room for the header and footer تصغير محتوى الصفحة لإفساح المجال للرأس والتذييل
Source المصدر
Fit to page ملاءمة الصفحة
Rotation التدوير
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -514,6 +514,7 @@
Insert Page Number Вмъкни номер на страница
Insert Date Вмъкни дата
Start page number Начален номер на страница
Shrink the page content to make room for the header and footer Смаляване на съдържанието на страницата, за да има място за горния и долния колонтитул
Source Източник
Fit to page Побери в страницата
Rotation Завъртане
Can't render this file because it contains an unexpected character in line 8 and column 16.
+1
View File
@@ -916,6 +916,7 @@ key Space Mezerník
Insert Page Number Vložit číslo stránky
Insert Date Vložit datum
Start page number Počáteční číslo stránky
Shrink the page content to make room for the header and footer Zmenšit obsah stránky, aby se vešlo záhlaví a zápatí
Source Zdroj
Fit to page Přizpůsobit stránce
Rotation Otočení
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -819,6 +819,7 @@
Insert Page Number Seitenzahl einfügen
Insert Date Datum einfügen
Start page number Erste Seitenzahl
Shrink the page content to make room for the header and footer Seiteninhalt verkleinern, damit Kopf- und Fußzeile Platz haben
Source Quelle
Fit to page An Seite anpassen
Rotation Drehung
Can't render this file because it contains an unexpected character in line 6 and column 51.
+1
View File
@@ -424,6 +424,7 @@
Insert Page Number Εισαγωγή αριθμού σελίδας
Insert Date Εισαγωγή ημερομηνίας
Start page number Αρχικός αριθμός σελίδας
Shrink the page content to make room for the header and footer Σμίκρυνση του περιεχομένου της σελίδας ώστε να χωρούν η κεφαλίδα και το υποσέλιδο
Source Πηγή
Appear behind page Εμφάνιση πίσω από τη σελίδα
Appear on top of page Εμφάνιση πάνω από τη σελίδα
1 # Greek (el) interface catalog. Columns: context <TAB> English source <TAB> Greek translation.
424
425
426
427
428
429
430
+1
View File
@@ -835,6 +835,7 @@
Insert Page Number Insertar número de página
Insert Date Insertar fecha
Start page number Número de página inicial
Shrink the page content to make room for the header and footer Reducir el contenido de la página para dejar espacio al encabezado y al pie de página
Source Origen
Fit to page Ajustar a la página
Rotation Rotación
Can't render this file because it contains an unexpected character in line 6 and column 60.
+1
View File
@@ -815,6 +815,7 @@
Insert Page Number Insérer le numéro de page
Insert Date Insérer la date
Start page number Numéro de page de départ
Shrink the page content to make room for the header and footer Réduire le contenu de la page pour laisser de la place à l'en-tête et au pied de page
Source Source
Fit to page Ajuster à la page
Rotation Rotation
Can't render this file because it contains an unexpected character in line 6 and column 39.
+1
View File
@@ -812,6 +812,7 @@
Insert Page Number Oldalszám beszúrása
Insert Date Dátum beszúrása
Start page number Kezdő oldalszám
Shrink the page content to make room for the header and footer Az oldal tartalmának kicsinyítése, hogy elférjen az élőfej és az élőláb
Source Forrás
Fit to page Oldalhoz igazítás
Rotation Elforgatás
Can't render this file because it contains an unexpected character in line 7 and column 44.
+1
View File
@@ -838,6 +838,7 @@
Insert Page Number Inserisci numero di pagina
Insert Date Inserisci data
Start page number Numero di pagina iniziale
Shrink the page content to make room for the header and footer Riduci il contenuto della pagina per fare spazio a intestazione e piè di pagina
Source Origine
Fit to page Adatta alla pagina
Rotation Rotazione
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -842,6 +842,7 @@
Insert Page Number ページ番号を挿入
Insert Date 日付を挿入
Start page number 開始ページ番号
Shrink the page content to make room for the header and footer ヘッダーとフッターの領域を空けるためにページの内容を縮小
Source ソース
Fit to page ページに合わせる
Rotation 回転
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -798,6 +798,7 @@
Insert Page Number Бет нөмірін енгізу
Insert Date Күнді енгізу
Start page number Бастапқы бет нөмірі
Shrink the page content to make room for the header and footer Жоғарғы және төменгі колонтитулға орын босату үшін бет мазмұнын кішірейту
Source Дереккөз
Fit to page Бетке сыйдыру
Rotation Бұрылу
1 Menu Мәзір
798 Insert Page Number Бет нөмірін енгізу
799 Insert Date Күнді енгізу
800 Start page number Бастапқы бет нөмірі
801 Shrink the page content to make room for the header and footer Жоғарғы және төменгі колонтитулға орын босату үшін бет мазмұнын кішірейту
802 Source Дереккөз
803 Fit to page Бетке сыйдыру
804 Rotation Бұрылу
+1
View File
@@ -1145,6 +1145,7 @@
Start Pradėti
Start Checking Pradėti tikrinimą
Start page number Pradinio puslapio numeris
Shrink the page content to make room for the header and footer Sumažinti puslapio turinį, kad tilptų antraštė ir poraštė
Status Būsena
Steps Veiksmai
Strength: {s} Stiprumas: {s}
Can't render this file because it contains an unexpected character in line 404 and column 9.
+1
View File
@@ -834,6 +834,7 @@
Insert Page Number Wstaw numer strony
Insert Date Wstaw datę
Start page number Początkowy numer strony
Shrink the page content to make room for the header and footer Zmniejsz zawartość strony, aby zrobić miejsce na nagłówek i stopkę
Source Źródło
Fit to page Dopasuj do strony
Rotation Obrót
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -469,6 +469,7 @@
Insert Page Number Inserir número de página
Insert Date Inserir data
Start page number Número da página inicial
Shrink the page content to make room for the header and footer Reduzir o conteúdo da página para dar espaço ao cabeçalho e ao rodapé
Source Origem
Fit to page Ajustar à página
Rotation Rotação
Can't render this file because it contains an unexpected character in line 9 and column 16.
+1
View File
@@ -835,6 +835,7 @@
Insert Page Number Вставить номер страницы
Insert Date Вставить дату
Start page number Начальный номер страницы
Shrink the page content to make room for the header and footer Уменьшить содержимое страницы, чтобы освободить место для колонтитулов
Source Источник
Fit to page По странице
Rotation Поворот
Can't render this file because it contains an unexpected character in line 9 and column 16.
+1
View File
@@ -1764,6 +1764,7 @@
Start Začetek
Start Checking Začni preverjanje
Start page number Številka začetne strani
Shrink the page content to make room for the header and footer Pomanjšaj vsebino strani, da bo prostor za glavo in nogo
Status Stanje
Steps Koraki
Sticky note Opomba
Can't render this file because it contains an unexpected character in line 9 and column 16.
+1
View File
@@ -816,6 +816,7 @@ comment menu Edit సవరించు
Insert Page Number పేజీ సంఖ్యను చొప్పించు
Insert Date తేదీని చొప్పించు
Start page number ప్రారంభ పేజీ సంఖ్య
Shrink the page content to make room for the header and footer హెడర్ మరియు ఫుటర్‌కు చోటు కల్పించేందుకు పేజీ కంటెంట్‌ను కుదించు
Source మూలం
Fit to page పేజీకి సరిపోయేలా
Rotation భ్రమణం
Can't render this file because it contains an unexpected character in line 13 and column 16.
+1
View File
@@ -797,6 +797,7 @@
Insert Page Number Sayfa Numarası Ekle
Insert Date Tarih Ekle
Start page number Başlangıç sayfa numarası
Shrink the page content to make room for the header and footer Üst bilgi ve alt bilgiye yer açmak için sayfa içeriğini küçült
Source Kaynak
Fit to page Sayfaya sığdır
Rotation Döndürme
1 # Turkish (tr) catalog. Format: see the header of ja.tsv
797
798
799
800
801
802
803
+1
View File
@@ -839,6 +839,7 @@
Insert Page Number Вставити номер сторінки
Insert Date Вставити дату
Start page number Початковий номер сторінки
Shrink the page content to make room for the header and footer Зменшити вміст сторінки, щоб звільнити місце для колонтитулів
Source Джерело
Fit to page За розміром сторінки
Rotation Поворот
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -1136,6 +1136,7 @@ theme Appearance Giao diện
Start Bắt đầu
Start Checking Bắt đầu kiểm tra
Start page number Số trang bắt đầu
Shrink the page content to make room for the header and footer Thu nhỏ nội dung trang để chừa chỗ cho đầu trang và chân trang
Status Trạng thái
Steps Các bước
Strength: {s} Độ mạnh: {s}
Can't render this file because it contains an unexpected character in line 491 and column 12.
+1
View File
@@ -521,6 +521,7 @@
Insert Page Number 插入页码
Insert Date 插入日期
Start page number 起始页码
Shrink the page content to make room for the header and footer 缩小页面内容,为页眉和页脚留出空间
Source 来源
Fit to page 适合页面
Rotation 旋转
Can't render this file because it contains an unexpected character in line 9 and column 16.
+1
View File
@@ -474,6 +474,7 @@
Insert Page Number 插入頁碼
Insert Date 插入日期
Start page number 起始頁碼
Shrink the page content to make room for the header and footer 縮小頁面內容,為頁首和頁尾留出空間
Source 來源
Fit to page 適合頁面
Rotation 旋轉
Can't render this file because it contains an unexpected character in line 9 and column 16.
+10
View File
@@ -223,6 +223,7 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: M
ui.label(tl!("Start page number"));
ui.add(egui::DragValue::new(&mut d.hf.start_number).range(1..=999_999));
});
ui.add(widgets::checkbox(&mut d.hf.shrink, tl!("Shrink the page content to make room for the header and footer")));
}
MarkKind::Watermark => {
ui.horizontal(|ui| {
@@ -306,11 +307,20 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: M
ui.add_space(8.0);
// Preview of the current page.
let hf = d.hf.clone();
let size_h = page.1.max(1.0);
let (wm, bg, fit) = (d.wm.clone(), d.bg.clone(), d.fit);
let file_name = d.use_file.then(|| d.file.as_ref().map(|f| f.0.clone())).flatten();
ui.horizontal(|ui| {
preview(ui, t, page, |p, r, s| match kind {
MarkKind::HeaderFooter => {
if hf.shrink {
// The band the page content shrinks into, centred across.
let (top, bottom) = hf.bands();
let k = ((size_h - top - bottom) / size_h).clamp(0.0, 1.0) as f32;
let band = Rect::from_min_max(pos2(r.left(), r.top() + top as f32 * s), pos2(r.right(), r.bottom() - bottom as f32 * s));
let content = Rect::from_center_size(band.center(), vec2(r.width() * k, band.height().max(0.0)));
p.rect_filled(content, CornerRadius::ZERO, Color32::from_gray(232));
}
for (k, text) in hf.text.iter().enumerate() {
if text.trim().is_empty() {
continue;
+15
View File
@@ -58,6 +58,21 @@ fn header_and_footer_dialog_inserts_tokens_and_applies_to_a_subset() {
assert!(marks(&h).is_empty());
}
#[test]
fn header_and_footer_dialog_can_shrink_the_page_content() {
let mut h = harness();
assert!(h.state_mut().execute("edit.bates"));
h.run_steps(3);
assert!(!h.state().marks_draft.hf.shrink, "off by default, as in Acrobat");
h.get_by_label("Shrink the page content to make room for the header and footer").click();
h.run_steps(2);
assert!(h.state().marks_draft.hf.shrink);
h.get_by_label("OK").click();
h.run_steps(4);
assert_eq!(h.state().dialog, None);
assert_eq!(marks(&h), [MarkKind::HeaderFooter]);
}
#[test]
fn watermark_and_background_dialogs() {
let mut h = harness();
+5 -5
View File
@@ -2433,11 +2433,11 @@ title = "Shrink document to avoid overwriting content"
area = "C"
tier = "P2"
milestone = "M7"
status = "planned"
commands = []
tools = []
evidence = []
notes = ""
status = "shipped"
commands = ["edit.header_footer"]
tools = ["doc_header_footer"]
evidence = ["crates/edit/src/tests.rs::shrink_scales_the_original_content_into_the_band_between_header_and_footer", "crates/edit/src/tests.rs::shrink_follows_the_display_space_of_a_rotated_page", "crates/edit/src/tests.rs::shrink_is_undone_by_remove_and_not_doubled_by_update", "crates/automation/tests/automation.rs::a_shrinking_header_and_footer_moves_the_page_content_out_of_their_way_through_tools", "crates/ui-egui/tests/ui_group/marks.rs::header_and_footer_dialog_can_shrink_the_page_content"]
notes = "#2140: the page's original content is scaled evenly into the band the header and footer leave free (margin, tallest box, half the font size of clearance), centred across, in display space; Remove and Update undo it. Not moved yet: annotations (links, comments, form fields) and PdfCraft's own added text and images; not compared side by side with Acrobat's exact scale"
[[feature]]
id = "edit.watermark-add-text"