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:

  1. Media box — trim outset by the bleed. This is the sheet the RIP receives; map content fills it.
  2. Trim box — the finished size. This is where the guillotine cuts.
  3. Safe box — trim inset by the safety margin. Nothing readable may sit outside it.
  4. 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.

One derivation, three page sizes Three sheets at relative sizes for A5, A4 and A2. Each carries a 3 millimetre bleed drawn as the same absolute width on all three. The safety margin scales from 6 millimetres on A5 to 8 on A4 and 12 on A2, which holds the same visual proportion. A note records that a constant margin would look cramped on the large sheet and a scaled bleed would exceed the guillotine tolerance. A5 bleed 3 · safe 6 A4 bleed 3 · safe 8 A2 bleed 3 · safe 12 bleed constant — a mechanical tolerance margin scales — a visual proportion Reversing the two gives an A2 with a cramped margin and an A5 that loses content to the cut.
The two margins answer to different constraints, which is exactly why a per-size template ends up with one of them wrong.

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.
Why the neatline does not sit on the trim line Two magnified sheet edges. In the first, a 0.6 millimetre neatline is centred on the trim line, so the guillotine removes half of it and drift of half a millimetre removes the rest on some copies. In the second, the neatline sits 2 millimetres inside the safe box, so the full stroke survives and guillotine drift changes only the white margin beyond it. neatline on the trim line trim cut away half the stroke removed; drift removes the rest on some copies of the same run neatline inside the safe box trim cut away full stroke survives; drift changes only the white margin beyond it The left-hand case passes every digital check and varies visibly between copies of one print run.
Guillotine drift is a fact of production, not a defect. Geometry that tolerates it produces identical copies; geometry that ignores it produces a run with visibly different edges.

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.

Three guards, and the specification error each names Three guard conditions listed with the error each catches. A non-positive finished size catches a swapped or missing dimension. A safety margin consuming more than half the sheet catches a margin given in the wrong unit. A neatline offset larger than the safe box catches a template copied from a much larger page size. Each guard names the page size in its message. finished size must be positive catches a missing dimension or a size read from an empty config field safety margin under half the shorter side catches a margin given in centimetres or points instead of millimetres neatline offset fits inside the safe box catches a template copied from a much larger page size All three fail at the specification, naming the page size — not later, inside a renderer, naming a division by zero in a function that never heard of page sizes.
Each guard is one comparison. What they buy is that a unit mistake in a configuration file produces a message about that file rather than a stack trace three layers away.

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.


Back to Map Layout and Composition Automation