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.
Prerequisites
- A licensed PrinceXML 15+ binary on the
PATH(prince --versionshould print the build) - Python 3.10+ for the orchestration script (Prince itself needs no Python, but the pipeline drives it via
subprocess) - Map plates exported slightly larger than the trim so they can reach the bleed edge
- Familiarity with the trim/bleed vocabulary from Margin and Bleed Alignment in Automated PDFs and the paper standards in Print-Ready Page Sizing Standards for GIS Reports
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.
@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.
/* 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.
.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.
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.
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-trimis omitted or set equal toprince-bleedincorrectly. Setprince-trimto 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
bleedandmarksproperties, while Prince addsprince-bleedandprince-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 crossdoubles 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.
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")
Related
- Margin and Bleed Alignment in Automated PDFs — parent guide covering trim, bleed, and safe-area geometry across renderers
- How to Set Exact Bleed Margins in WeasyPrint for GIS Maps — the WeasyPrint equivalent this guide contrasts against
- Converting QGIS Layout Templates to Automated CSS Grids — upstream step for turning a QGIS print composer layout into the bleed-aware HTML this page renders
- Parent section: Margin and Bleed Alignment in Automated PDFs