How to Set Exact Bleed Margins in WeasyPrint for GIS Maps

To set exact bleed margins in WeasyPrint for GIS maps, configure the @page rule to match your final trim dimensions plus bleed, set CSS margins to zero, and render map imagery inside a full-bleed container that extends beyond the trim line using negative positioning or calc(). Because WeasyPrint implements CSS Paged Media rather than native PDF/X bleed box metadata, visual bleed must be engineered through precise HTML/CSS layout and explicit DPI scaling for spatial assets.

The following workflow guarantees a precise 3 mm bleed on all sides while preserving exact trim boundaries for map extents, scale bars, north arrows, and legends.

Core Implementation Pattern

WeasyPrint renders HTML/CSS to PDF using a fixed layout engine. The most reliable bleed strategy expands the @page canvas to trim + bleed, removes all default margins, and offsets the map image negatively so it extends past the trim edge.

Python
import weasyprint

# 1. Define trim size and bleed in millimeters
TRIM_WIDTH = 210   # A4 width
TRIM_HEIGHT = 297  # A4 height
BLEED = 3          # Standard commercial print bleed

# 2. Calculate canvas dimensions (trim + bleed on both axes)
PAGE_W = TRIM_WIDTH + (BLEED * 2)
PAGE_H = TRIM_HEIGHT + (BLEED * 2)

# 3. Build HTML with CSS Paged Media layout
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<style>
  @page {{
    size: {PAGE_W}mm {PAGE_H}mm;
    margin: 0;
  }}
  body {{
    margin: 0;
    padding: 0;
    font-family: system-ui, -apple-system, sans-serif;
  }}
  .bleed-canvas {{
    width: 100%;
    height: 100%;
    position: relative;
    background: #fafafa; /* Proof background for empty margins */
  }}
  .map-image {{
    position: absolute;
    top: -{BLEED}mm;
    left: -{BLEED}mm;
    width: calc(100% + {BLEED*2}mm);
    height: calc(100% + {BLEED*2}mm);
    object-fit: cover;
    /* Prevent interpolation artifacts on crisp cartographic lines */
    image-rendering: crisp-edges;
  }}
  .trim-boundary {{
    position: absolute;
    top: {BLEED}mm;
    left: {BLEED}mm;
    width: {TRIM_WIDTH}mm;
    height: {TRIM_HEIGHT}mm;
    border: 0.5pt dashed #ff0000; /* Visual proof only; remove before final print */
    pointer-events: none;
    box-sizing: border-box;
  }}
  .gis-overlay {{
    position: absolute;
    top: {BLEED}mm;
    left: {BLEED}mm;
    width: {TRIM_WIDTH}mm;
    height: {TRIM_HEIGHT}mm;
    padding: 10mm;
    box-sizing: border-box;
    color: #111;
  }}
  @media print {{
    .trim-boundary {{ display: none; }}
  }}
</style>
</head>
<body>
  <div class="bleed-canvas">
    <!-- Replace with your exported GIS raster (GeoTIFF/PNG at 300+ DPI) -->
    <img class="map-image" src="map_export_300dpi.png" alt="GIS Map">
    <div class="trim-boundary"></div>
    <div class="gis-overlay">
      <h1>Regional Hydrology Analysis</h1>
      <p>Scale: 1:24,000 | CRS: EPSG:32612 | Datum: WGS84</p>
    </div>
  </div>
</body>
</html>
"""

# 4. Render the PDF. The @page size is declared in millimetres, so WeasyPrint
#    emits a physically-sized, vector PDF — print fidelity depends on the source
#    raster's own resolution (see the DPI math below), not a write_pdf() flag.
weasyprint.HTML(string=html_content).write_pdf("gis_map_bleed.pdf")

Why This Works

  • @page size defines the physical PDF canvas. WeasyPrint treats this as the absolute page boundary.
  • margin: 0 eliminates browser-default gutters that would otherwise clip bleed content.
  • Negative positioning (top: -3mm; left: -3mm) pushes the map raster into the bleed zone while keeping the .gis-overlay strictly within the trim area.
  • Physical units (mm in the @page rule) make WeasyPrint emit an absolutely-sized, vector PDF. Because the output is vector, there is no dpi argument on write_pdf() — print sharpness comes from the source raster being exported at the pixel dimensions computed below, not from a render-time flag.

Spatial Asset Preparation & DPI Math

GIS exports rarely align perfectly with standard print dimensions. When generating maps via QGIS, ArcGIS Pro, or geopandas + contextily, export at 300 DPI minimum and calculate exact pixel dimensions using:

For a 210×297 mm A4 page with 3 mm bleed at 300 DPI:

  • Width:
  • Height:

Always export your map canvas at these exact pixel dimensions. WeasyPrint does not resample rasters intelligently; it maps 1 CSS pixel to 1 image pixel at the specified DPI. If your source image is undersized, the PDF will stretch it, degrading contour lines, parcel boundaries, and label legibility.

When working with vector overlays (SVG legends, scale bars, coordinate grids), embed them directly in the HTML rather than rasterizing them. WeasyPrint renders SVG paths at native resolution, preserving crisp typography and precise geometric alignment regardless of output DPI.

Automated PDF generation for cartographic outputs requires strict adherence to commercial printing tolerances. Before handing off to a press or large-format printer, verify the following:

  1. Bleed Consistency: Measure the PDF in Acrobat Pro or pdfinfo. The media box should match PAGE_W × PAGE_H, while the trim box (if added via post-processing) should match TRIM_WIDTH × TRIM_HEIGHT. WeasyPrint outputs a single MediaBox; professional RIPs will crop to trim during imposition.
  2. Color Space: WeasyPrint outputs RGB by default. Most offset and digital presses expect CMYK. Convert your final PDF using ghostscript or qpdf before submission, or configure your GIS export to sRGB with embedded ICC profiles to minimize gamut shifts.
  3. Typography & Scale: Use @font-face with static .woff2 files. System fonts vary across CI/CD runners and can shift text blocks, breaking alignment with map features. Lock scale bar dimensions in CSS mm units so they remain proportional regardless of screen preview.
  4. Trim Mark Generation: If your printer requires crop marks, generate them programmatically using @page { marks: crop; } (supported in WeasyPrint ≥56) or overlay SVG crosshairs at the exact trim corners.

For teams building automated reporting pipelines, aligning these CSS rules with established Print-Ready Page Sizing Standards for GIS Reports ensures consistent output across regional templates, multi-page atlases, and dynamic dashboard exports.

Troubleshooting Common WeasyPrint Bleed Issues

  • Clipped Edges: Caused by non-zero body margins or padding on the root container. Reset with margin: 0; padding: 0; explicitly.
  • Blurry Map Features: Usually a DPI mismatch in the source asset. Confirm your source raster was exported at the calculated pixel dimensions (300 DPI at the final physical size); WeasyPrint embeds it at the CSS box size without resampling.
  • Unexpected Page Breaks: WeasyPrint respects break-inside: avoid; and page-break-inside: avoid;. Apply these to .gis-overlay or legend blocks to prevent fragmentation across spreads.
  • Font Substitution in CI: Containerized runners often lack system fonts. Bundle .ttf/.woff2 assets and reference them via @font-face with format('woff2').

When scaling this pattern across enterprise reporting systems, review your broader Document Architecture & Layout Rules for Spatial Reports to standardize grid systems, metadata placement, and automated legend generation. Properly structured HTML templates combined with WeasyPrint’s deterministic rendering pipeline eliminate manual layout adjustments and guarantee press-ready output on every run.