Page Placement
Keyword page selectors and signature boxes — where the visible signature lands, resolved against the document's real page count
A visible signature needs two things: which pages it lands on and which box on each. Mudrit
expresses this as a list of Placements — a PageSelector paired with a
Rect. Selectors are resolved against the document's real page count at sign time, so
tokens like ODD, L, or 1,3,5-7,L work without you listing every page.
use mudrit_pdfsign::prelude::*;
let cfg = SignConfig::builder()
.place("1,3,5-7,L", [350, 60, 560, 160])? // compound selector + box
.placement(Placement::new(PageSelector::All, Rect::new(40, 40, 200, 100))) // a 2nd box on every page
.build();
let signed = sign_pdf(&pdf, &signer, &cfg)?;Page selectors
PageSelector is built programmatically or parsed from a string (case-insensitive, comma-separated).
A single token yields that variant; multiple tokens union into a compound selector.
| Token | Selects | PageSelector |
|---|---|---|
A / ALL | every page | PageSelector::All |
F / FIRST | the first page | PageSelector::First |
L / LAST | the last page | PageSelector::Last |
ODD | odd pages (1, 3, 5, …) | PageSelector::Odd |
EVEN | even pages (2, 4, 6, …) | PageSelector::Even |
3 | page 3 (1-based) | PageSelector::Page(3) |
2-5 | inclusive range | PageSelector::Range(2, 5) |
1,3,5-7,L | compound union | PageSelector::Any([...]) |
How selectors resolve
Every selector is resolved against the actual page count into a sorted, de-duplicated list of existing pages. Two rules keep this predictable:
- Out-of-range signs nothing. A selector that resolves past the end of the document (e.g.
9on a 4-page PDF, or an unbounded range) simply contributes no pages — it never errors and never signs a page that isn't there. A range like2-99is capped at the last page. - An unknown token errors. A token that is neither a keyword, a page number, nor a range (e.g.
"foo"or"2x") fails parsing with anInvalidInputerror, so a typo is caught loudly rather than silently dropped.
Because resolution is deferred to sign time, one SignConfig works across documents of different
lengths: .place("L", …) always lands on the true last page, whatever the page count turns out to be.
The Rect
A signature box is a rectangle in PDF user space — points, bottom-left origin. Rect names its
four corners so the numbers can't be transposed by accident, and it is interchangeable with a raw
[llx, lly, urx, ury] array via From, so pick whichever reads better at the call site.
Rect::new(350, 60, 560, 160) // (llx, lly) lower-left → (urx, ury) upper-right
Rect::from_xywh(350, 60, 210, 100) // lower-left origin + width/height → same box
[350, 60, 560, 160] // a bare [llx, lly, urx, ury] array works everywhere a Rect does
[350.0, 60.0, 559.9, 160.0] // f64 arrays round to the nearest point (559.9 → 560)| Constructor | Meaning |
|---|---|
Rect::new(llx, lly, urx, ury) | The two corner points. |
Rect::from_xywh(x, y, w, h) | Lower-left origin plus a width and height. |
[llx, lly, urx, ury] | An i64 (or f64) array, converted via From. |
Multiple boxes (multi-box)
A Placement is just one selector + one rect, and a SignConfig holds a list of them — so the
same page can carry several boxes with different rects, and different page sets can carry different
boxes. Add them with .place(selector, rect) (parses a string) or .placement(Placement) (a
pre-built one):
let cfg = SignConfig::builder()
.place("F", [350, 60, 560, 160])? // top-right box on page 1
.place("F", [40, 40, 250, 110])? // a 2nd box, also on page 1
.placement(Placement::new(PageSelector::Even, Rect::from_xywh(40, 40, 210, 100)))
.build();At sign time the placements are flattened to the (page, rect) list the engine stamps: order follows
the placements list, pages ascend within each placement, and exact (page, rect) duplicates are
dropped — but the same page with different boxes is kept.
Placement vs. Method
Placement decides where the widgets go; the signature Method
decides how many actual signatures those widgets represent — Single (one signature shown on every
resolved page), MultiShared (N fields sharing one value), or MultiChained (N independent
signatures). For MultiChained, the first resolved entry is the first revision.
.place(...) vs .placement(...)
| Method | Takes | Use when |
|---|---|---|
.place(selector, rect)? | a selector string + rect | the ergonomic default — "L", "ODD", "1,3,5-7,L" |
.placement(Placement) | a pre-built Placement | you already have a typed PageSelector (e.g. built in code) |
.place returns a Result because the selector string is parsed (an unknown token errors there);
.placement takes an already-valid Placement and is infallible. Placement::new(selector, rect)
builds one from a typed selector, and Placement::parse("ODD", rect)? from a string.
Signing into an existing field?
When you target a pre-existing empty signature field with .sign_existing_field(name), the field's own
page and rectangle are reused and any placements are ignored. See
Existing fields.