Guide · Coordinates & anchors
PDF Positioning, hands on
Last updated: 2026-07-02
Where exactly does a stamp land when you say (x, y)? This page is the
coordinate contract of rust-pdf, live: drag the sliders, flip the anchors, and watch
both the page and the API call update. Everything here maps 1:1 to
PlaceText, PlaceParagraph, MaskedText and
DrawImage in all ten languages.
The contract in one line: units are points (1 pt = 1/72"), the origin is the
bottom-left, y grows up, rotation is counter-clockwise
about the anchor, and what y means is an explicit,
documented choice (the VerticalAnchor).
1. Vertical anchors: what y means
Libraries disagree about the vertical meaning of y: a raw PDF text matrix anchors
the baseline, the legacy engine's fixed-position layout lays text down from the top of its
layout box, legacy PDF libraries hangs it from a rectangle edge. rust-pdf makes the choice explicit.
Pick an anchor and watch the text move while the anchor point ● stays put.
Anchor playground
Top/Bottom are clean font geometry (ascender and descender lines).
LineTop/LineBottom reproduce the legacy engine's layout box (OS/2 win metrics
plus a 0.21 em half-leading each side), calibrated to match fixed-position layout within
a tenth of a point. The legacy layout engines model never leaks into the defaults.
2. Rotation pivots at the anchor
rotationDeg rotates counter-clockwise about the anchor. Offsets from
alignment or vertical anchors rotate together with the text, so the anchor remains the same
physical point of the text box at every angle. Spin it.
Pivot playground
Note how with LineBottom the box-bottom point is the thing pinned to the dot:
the baseline offset swings with the text instead of being pre-added to y.
That is how legacy layout engines composes it, so ports line up at every angle.
3. Rotated scans: StampSpace
Scanned documents usually carry /Rotate 90. There are two honest ways to interpret
your coordinates on such a page, and rust-pdf lets you pick per document instead of guessing.
Same call, two spaces (page has /Rotate 90)
Visible: coordinates in the page as displayed, the library compensates
/Rotate, a 0° stamp reads upright on screen. Media: the raw PDF
user space, nothing composed, which is what legacy raw-space drawing and
fixed-position layout use. Porting coordinates computed for legacy PDF libraries? Use Media.
4. Paragraphs: wrapping, bottom-pinning and the ceiling
PlaceParagraph wraps by word inside a width. With the Bottom anchors the
block is bottom-pinned: its bottom rests on y and it grows upward by
the real content height. maxHeight is a ceiling, not a box to fill: overflowing lines
are cut from the top and the last lines stay pinned. The call reports the consumed height.
Block anchor playground
Watch the cut direction: with the bottom-pin, a too-small ceiling removes the first
lines and keeps the last ones on y (footers, signature blocks). With
Top, the cut removes the last lines below y − maxHeight.
5. MaskedText: a box, a mask, aligned text
MaskedText paints an opaque box and writes a value over it, the classic
"cover the placeholder" move. The vertical alignment and the horizontal inset are explicit.
Box alignment playground
Top matches the top line-alignment of rectangle-based text APIs; padding: 0
starts flush at the box edge like rectangle-based DrawString APIs. The default keeps a small inset
of 0.15 × size.
6. Images: corner pivot or bounding box
A rotated image can be anchored two ways: Corner (default) keeps
(x, y) as the image's own lower-left corner and the image sweeps around it;
BoundingBox lands the rotated image's bounding box at (x, y),
which is how legacy layout engines lays out rotated images.
Image anchor playground
With BoundingBox the pixels always land at or above and to the right of the
anchor: a 90° image occupies [x, x+height] × [y, y+width].
The same contract in every language
One Rust core, ten bindings, identical semantics. A signature block stamped at the bottom of a scanned page looks like this:
# pip install rustpdf
import rustpdf
from rustpdf import EditableDoc, StampSpace, VerticalAnchor, Align
doc = EditableDoc.load(open("scan.pdf", "rb").read())
doc.set_stamp_space(StampSpace.MEDIA) # legacy-style raw coordinates
times = doc.add_font_file("Times New Roman.ttf")
# Bottom-pinned signature block: the block bottom rests on y = 96.
doc.place_paragraph(0, 72, 96, 220,
"Digitally signed by Maria Silva\nReason: Approval\n2026-07-02 14:31 UTC",
size=10, font_id=times, max_height=64,
anchor=VerticalAnchor.LINE_BOTTOM)
open("signed-stamped.pdf", "wb").write(doc.to_bytes())
// dotnet add package RustPdf
using RustPdf;
using var doc = EditableDoc.Load(File.ReadAllBytes("scan.pdf"));
doc.StampSpace = StampSpace.Media; // legacy-style raw coordinates
int times = doc.AddFontFile("Times New Roman.ttf");
// Bottom-pinned signature block: the block bottom rests on y = 96.
doc.PlaceParagraph(0, 72, 96, 220,
"Digitally signed by Maria Silva\nReason: Approval\n2026-07-02 14:31 UTC",
size: 10, fontId: times, maxHeight: 64,
anchor: VerticalAnchor.LineBottom);
File.WriteAllBytes("signed-stamped.pdf", doc.ToBytes());
// go get github.com/rustpdf/rustpdf-go@latest
doc, _ := rustpdf.Load(data)
defer doc.Close()
doc.SetStampSpace(rustpdf.StampMedia) // legacy-style raw coordinates
times, _ := doc.AddFontFile("Times New Roman.ttf")
// Bottom-pinned signature block: the block bottom rests on y = 96.
doc.PlaceParagraph(0, 72, 96, 220,
"Digitally signed by Maria Silva\nReason: Approval\n2026-07-02 14:31 UTC",
10, 0, 0, 0, rustpdf.AlignLeft, times,
64, 1.0, rustpdf.AnchorLineBottom, 0)
out, _ := doc.Bytes()
// npm install rustpdf
const { EditableDoc, StampSpace, VerticalAnchor } = require("rustpdf");
const doc = EditableDoc.load(fs.readFileSync("scan.pdf"));
doc.setStampSpace(StampSpace.Media); // legacy-style raw coordinates
const times = doc.addFontFile("Times New Roman.ttf");
// Bottom-pinned signature block: the block bottom rests on y = 96.
doc.placeParagraph(0, 72, 96, 220,
"Digitally signed by Maria Silva\nReason: Approval\n2026-07-02 14:31 UTC",
{ size: 10, fontId: times, maxHeight: 64,
anchor: VerticalAnchor.LineBottom });
fs.writeFileSync("signed-stamped.pdf", doc.toBytes());
Full API reference per language: Python, C#, Go, Node, Java, PHP, Ruby, Swift, Delphi.
Positioning FAQ
What does the y coordinate mean when I place text?
By default it is the text baseline. You can also anchor by the geometric
top (ascender line), the geometric bottom (descender line), or the top and bottom of the
layout line box (LineTop/LineBottom), which is the right pair
when matching output from legacy layout engines's fixed-position layout.
Around which point does rotated content pivot?
Always the anchor (x, y). Alignment shifts and vertical-anchor offsets rotate
with the text, so the anchor stays the same physical point of the box at any angle.
Images pivot at their lower-left corner by default, or anchor the rotated bounding box
with ImageAnchor.BoundingBox.
Why does my stamp land wrong on scanned pages?
Scans carry /Rotate. The default Visible space compensates it so
coordinates match what you see. Code ported from legacy layout engines computes coordinates in the raw
user space instead: set StampSpace.Media and your math is used untouched.
How do I bottom-align a block like legacy PDF engines's fixed-position layout?
PlaceParagraph with anchor: LineBottom: the block's bottom rests
on y, maxHeight is a ceiling that cuts from the top, and the call
returns the consumed height so you can stack blocks without re-measuring.
Positioning you can reason about
Basic generation and stamping are free. PDF/A, encryption, signatures, redaction and rendering are licensed features on the same core.
Related guides: Render PDF to image · Watermark a PDF · PAdES signatures