Computing Neatline and Margin Geometry for Variable Page Sizes
Derive every page box from the finished size and the printer’s bleed specification in one function, so that adding A3 to a catalogue of A4 sheets is a call with different arguments rather than a second template with its own constants.
Core Algorithm and Workflow
Four boxes describe a print sheet, and each is derived from the one before it:
- Media box — trim outset by the bleed. This is the sheet the RIP receives; map content fills it.
- Trim box — the finished size. This is where the guillotine cuts.
- Safe box — trim inset by the safety margin. Nothing readable may sit outside it.
- Neatline — a drawn rectangle inside the safe box, at a documented offset.
The distinction that matters is between the two margins. Bleed is a mechanical tolerance: guillotines drift by a millimetre or two regardless of the sheet size, so an A0 poster needs the same three millimetres as an A6 card. The safety margin is a visual proportion, and eight millimetres that looks generous on A5 looks cramped on A1.
That asymmetry is the whole reason a single derivation beats a per-size template. Encoded once, a new page size gets a correct bleed and a proportionally correct margin without anybody deciding either.
Production-Ready Python Implementation
import math
from dataclasses import dataclass
@dataclass(frozen=True)
class Box:
"""A rectangle in millimetres of finished output."""
x: float
y: float
w: float
h: float
def inset(self, m: float) -> "Box":
if self.w - 2 * m <= 0 or self.h - 2 * m <= 0:
raise ValueError(f"inset of {m} mm collapses a {self.w}×{self.h} mm box")
return Box(self.x + m, self.y + m, self.w - 2 * m, self.h - 2 * m)
def outset(self, m: float) -> "Box":
return Box(self.x - m, self.y - m, self.w + 2 * m, self.h + 2 * m)
A4_AREA = 210.0 * 297.0
def sheet_geometry(finished_w: float, finished_h: float,
bleed: float = 3.0,
safe_at_a4: float = 8.0,
neatline_offset: float = 2.0) -> dict:
"""Every page box derived from one finished size.
Bleed is a fixed mechanical tolerance. The safety margin is scaled by the
square root of the area ratio against A4, which holds the visual proportion
without letting it grow without bound on large formats.
"""
if finished_w <= 0 or finished_h <= 0:
raise ValueError("finished size must be positive")
scale = math.sqrt((finished_w * finished_h) / A4_AREA)
safety = round(safe_at_a4 * scale, 1)
trim = Box(0.0, 0.0, finished_w, finished_h)
safe = trim.inset(safety) # raises on an over-large margin
return {
"media": trim.outset(bleed),
"trim": trim,
"safe": safe,
"neatline": safe.inset(neatline_offset),
"safety_margin": safety,
"bleed": bleed,
}
inset raising rather than returning a degenerate box is the guard that matters. A safety margin larger than half the sheet is always a specification error, and catching it here produces a message naming the page size. Letting it through returns a negative-width box, and the failure then surfaces wherever something first divides by that width — usually deep in a renderer, with a message that names nothing useful.
Performance Tuning and Cartographic Best Practices
- Keep everything in millimetres until rasterisation. Every value here is a physical length. Converting to pixels once, at the end, means a change of output DPI touches one line rather than the whole derivation — the argument set out in DPI and Resolution Management.
- Put the neatline inside the safe box. A neatline stroked on the trim line loses half its weight to the guillotine, and loses a different half on each copy. Inside the safe box, the full stroke survives and its distance from the cut edge is a deliberate figure.
- Round the derived margin. A safety margin of 11.3137 millimetres is arithmetically correct and impossible to discuss with a printer. Round to one decimal place at the point of derivation, not at the point of use.
- Cache per page size, not per sheet. In an atlas every sheet shares a size, so the geometry is computed once and passed in — the same hoisting that applies to legends and terrain derivatives.
- Record the numbers in the output. Stamping the bleed and safety margin into the PDF metadata or a sidecar makes a later question about a printed sheet answerable without re-running the pipeline.
Integration and Next Steps
These boxes are the input to the marginalia allocation in Map Layout and Composition Automation: the safe box is what gets divided between title, legend, credits and the map frame. The media box is what the export must fill with map content, which connects directly to the bleed requirement discussed under Print-Ready Export and Batch Generation Workflows. In a series, computing the geometry once per page size and passing it into every sheet is what guarantees that all sheets have identical frames — the precondition for holding scale constant across an atlas.
Frequently Asked Questions
Should margins scale with the page size?
The safety margin should; the bleed should not. Bleed compensates for guillotine drift, a fixed mechanical tolerance of two to three millimetres regardless of sheet size — an A0 poster and an A6 card need the same allowance, and scaling it up on the poster simply wastes paper. The safety margin is a visual proportion: eight millimetres reads as generous on A5 and cramped on A1. Scaling it by the square root of the area ratio holds the proportion while keeping the growth sublinear, so an A0 sheet gets a margin around four times A4’s rather than sixteen times.
Where exactly does the neatline sit?
Inside the safe box, at a small documented offset. A neatline stroked on the trim line loses half its weight to the cut, and because guillotines drift it loses a different amount on each copy — so a single print run produces sheets with visibly different borders. Placing it inside the safe box means the full stroke survives everywhere and the only thing drift changes is the width of white paper beyond it, which nobody notices.
What should happen when the margins do not fit?
Raise, and name the page size and the offending margin. A safety margin larger than half the shorter side is always a specification error, most often a unit mistake — centimetres or points entered where millimetres were expected. Catching it in the geometry function produces a message that identifies the input; allowing a negative-width box through defers the failure to whichever renderer first divides by that width, and that error names nothing that helps.
Do I need separate geometry for landscape and portrait?
No. Pass the finished width and height and let one derivation handle both — orientation is two numbers in a different order, not a special case. What can legitimately differ is the marginalia arrangement downstream: a legend that works as a right-hand column on a portrait sheet may work better as a footer band in landscape. That is a layout decision made against the safe box these functions return, and keeping it separate is what lets the box geometry stay orientation-agnostic.
Related
- Automating Inset Map Placement and Extent Indicators — placing elements within the frame these boxes define.
- Generating North Arrows and Graticules Programmatically — the marginal band the graticule labels occupy.