Using PrinceXML Named Pages for Report Bleed

When a spatial report ships to a commercial printer, edge-to-edge map imagery must extend past the trim line into a bleed area so that guillotine tolerance never leaves a white sliver at the page edge. PrinceXML implements CSS Paged Media with a set of prince- extensions that make press-ready bleed and crop marks a first-class feature, and named @page rules let a single stylesheet give the cover, map plates, and body text different bleed behaviour. This guide is a focused technique inside the Margin and Bleed Alignment in Automated PDFs workflow, and it deliberately contrasts Prince with the WeasyPrint approach documented in How to Set Exact Bleed Margins in WeasyPrint for GIS Maps.

PrinceXML bleed box, trim box, and crop marks Concentric rectangles: the outer bleed box, the trim box inside it, and the content margin inside that. Corner crop marks sit on the trim line and a shaded full-bleed map fills to the bleed edge. bleed box (prince-bleed) full-bleed map negative margin trim box content margin marks: crop cross draws corner marks on the trim line

Prerequisites

Step 1: Declare Size, Bleed, and Marks in @page

Prince reads prince-bleed to size the bleed box and marks: crop cross to draw printer marks. Set them on the default @page so every page inherits press-ready geometry unless a named page overrides it.

CSS
@page {
  size: 210mm 297mm;      /* A4 trim */
  prince-bleed: 3mm;      /* bleed box extends 3mm beyond trim on all sides */
  prince-trim: 3mm;       /* distance from paper edge to trim — positions marks */
  marks: crop cross;      /* corner crop marks + centre registration crosses */
  margin: 18mm;
}

prince-bleed and prince-trim together define both boxes: the bleed box is the trim plus the bleed distance, and prince-trim positions where the crop marks are drawn relative to the physical sheet. Keeping both explicit avoids the common error of marks landing on the bleed line instead of the trim line.

Step 2: Define Named Pages per Section Type

A report mixes section types with different edge requirements: a cover that bleeds on all sides, map plates that bleed, and body pages that do not. Named @page rules let each type carry its own settings, selected with the page property.

CSS
/* Cover — full bleed, no visible margin box */
@page cover {
  prince-bleed: 3mm;
  margin: 0;
  marks: crop cross;
}

/* Map plate — bleed retained, wider content margin for a caption strip */
@page map {
  prince-bleed: 3mm;
  margin: 12mm 12mm 20mm 12mm;
  marks: crop cross;
}

/* Body text — no bleed needed, tighter marks-free page */
@page body {
  prince-bleed: 0;
  margin: 22mm 18mm;
  marks: none;
}

.section-cover { page: cover; break-before: page; }
.section-map   { page: map;   break-before: page; }
.section-body  { page: body;  break-before: page; }

Selecting the named page from a wrapper class keeps the orientation and break logic aligned with the pattern used for Handling Multi-Page Landscape vs Portrait Switches: the page name selects geometry, break-before: page starts the section on a fresh sheet.

Step 3: Push Full-Bleed Maps into the Bleed Area

Declaring prince-bleed only creates room; the map must actually extend into it. Use negative margins equal to the page margin plus the bleed so the image reaches the bleed box edge.

CSS
.section-map .plate {
  /* pull the image out to the bleed box on the three outer edges */
  margin: -12mm -12mm 0 -12mm;   /* cancel page margin */
  margin-top: -15mm;             /* page margin (12mm) + bleed (3mm) */
  width: calc(100% + 24mm + 6mm);/* content width + both margins + both bleeds */
}

.section-map .plate img {
  display: block;
  width: 100%;
  height: auto;
}

Export the map plate at a pixel width that covers the full bleed-box width, not just the trim width. If the export only covers the trim, Prince upscales it to fill the bleed and softens the edge exactly where sharpness matters most.

Step 4: Invoke Prince from the Python Pipeline

Prince is a standalone binary. Drive it from the pipeline with subprocess, passing the assembled HTML and the print stylesheet, and capture the log so CI can surface warnings.

Python
from __future__ import annotations

import subprocess
from pathlib import Path


def render_with_prince(
    html_path: Path,
    css_path: Path,
    output_path: Path,
) -> None:
    """Render a press-ready PDF with PrinceXML, raising on any Prince error."""
    result = subprocess.run(
        [
            "prince",
            str(html_path),
            "--style", str(css_path),
            "--output", str(output_path),
            "--pdf-profile", "PDF/X-4",   # press-oriented profile with colour intent
        ],
        capture_output=True,
        text=True,
    )
    if result.returncode != 0:
        raise RuntimeError(f"Prince failed:\n{result.stderr}")
    print(result.stderr)  # Prince writes warnings to stderr even on success


render_with_prince(
    Path("report.html"),
    Path("print_bleed.css"),
    Path("report_press.pdf"),
)

The --pdf-profile PDF/X-4 flag makes Prince emit an output-intent block and enforce the trim/bleed boxes that prepress software expects — something WeasyPrint does not offer. If you need CMYK separations, Prince can also honour prince-color-mode, another capability absent from the WeasyPrint path.

Step 5: Verify Trim Box and Bleed Box

Confirm Prince wrote distinct boxes. pdfinfo from poppler-utils reports the boxes; the bleed box should exceed the trim box by twice the bleed on each dimension.

Bash
pdfinfo -box report_press.pdf
# TrimBox:  0.00  0.00  595.28  841.89   (A4 in points)
# BleedBox: -8.50 -8.50 603.78  850.39   (3mm ≈ 8.5pt larger on every side)

Key Parameters / Configuration Reference

Property Value Notes
size 210mm 297mm Trim size; the sheet Prince cuts to
prince-bleed 3mm Distance the bleed box extends beyond trim on all sides
prince-trim 3mm Positions crop marks relative to the paper edge
marks crop cross Draws corner crop marks and centre registration crosses
page cover / map / body Selects a named @page for a section wrapper
--pdf-profile PDF/X-4 Emits output intent and enforces press boxes
3mm in points ≈ 8.5pt Handy for reading pdfinfo -box output

Common Pitfalls

  • Marks drawn on the bleed line, not the trim line. This happens when prince-trim is omitted or set equal to prince-bleed incorrectly. Set prince-trim to the intended trim-to-paper distance so marks register on the cut line.

  • Maps that stop at the trim edge. A negative margin only reveals bleed if the image is wide enough to fill it. Export plates at the full bleed-box dimensions, then pull them out with negative margins.

  • Assuming WeasyPrint CSS transfers unchanged. WeasyPrint uses the standard bleed and marks properties, while Prince adds prince-bleed and prince-trim. Maintain a Prince-specific stylesheet rather than sharing one file; see the WeasyPrint variant in How to Set Exact Bleed Margins in WeasyPrint for GIS Maps.

  • Delivering marked and imposed PDFs. If the vendor imposes the job, marks: crop cross doubles the marks after imposition. Confirm whether the printer wants a marked press sheet or a clean file with only a BleedBox.

Verification

Assert the box geometry in CI with a small parser so a stylesheet regression cannot ship a report with a missing bleed box.

Python
import subprocess

def assert_bleed(pdf_path: str, min_bleed_pt: float = 8.0) -> None:
    """Confirm the BleedBox exceeds the TrimBox by at least the expected bleed."""
    out = subprocess.run(
        ["pdfinfo", "-box", pdf_path], capture_output=True, text=True
    ).stdout
    trim = [float(x) for x in _row(out, "TrimBox")]
    bleed = [float(x) for x in _row(out, "BleedBox")]
    assert (trim[0] - bleed[0]) >= min_bleed_pt, "left bleed too small"
    assert (bleed[2] - trim[2]) >= min_bleed_pt, "right bleed too small"

def _row(text: str, key: str) -> list[str]:
    line = next(l for l in text.splitlines() if l.startswith(key))
    return line.split(":", 1)[1].split()

assert_bleed("report_press.pdf")