Handling Multi-Page Landscape vs Portrait Switches in Automated Spatial Reports
Direct Answer: Handling multi-page landscape vs portrait switches requires explicit @page named rules in CSS Paged Media, triggered via break-before: page on dedicated wrapper elements, paired with pre-scaled GIS assets that match exact printable dimensions. Do not rely on viewport scaling or automatic renderer heuristics. Instead, treat each orientation as an isolated rendering context, calculate aspect ratios before export, and enforce break-inside: avoid on map frames, legends, and scale bars to prevent clipping or margin drift.
Core Architecture & Orientation Context
Spatial reporting pipelines routinely alternate between wide map canvases (landscape) and dense attribute tables, methodology narratives, or cross-section profiles (portrait). When orientation switches are left implicit, PDF engines stretch content, clip bleed zones, or force uniform page sizes that break visual hierarchy and violate cartographic standards.
As outlined in foundational Document Architecture & Layout Rules for Spatial Reports, reliable automation requires separating content generation from layout directives. Each orientation must be declared as a distinct rendering context. This aligns with modern CSS Grid Systems for Report Layouts, where structural containers dictate flow rather than relying on post-render adjustments.
Adopt a strict three-layer pipeline:
- Data Layer: Raw GeoJSON, raster tiles, or spatial database queries.
- Asset Layer: Pre-rendered map images, SVG legends, and tabular outputs sized to target print dimensions at fixed DPI (typically 150–300).
- Layout Layer: HTML/CSS wrappers with explicit
@pageassignments and break directives injected before rendering.
CSS Paged Media Implementation
The W3C CSS Paged Media Module Level 3 specification defines named pages that allow precise orientation control. Modern renderers (WeasyPrint, PrinceXML, DocRaptor) fully support this pattern.
/* Default fallback */
@page {
size: letter portrait;
margin: 20mm;
}
/* Named orientation pages */
@page landscape {
size: letter landscape;
margin: 15mm;
}
@page portrait {
size: letter portrait;
margin: 20mm;
}
/* Orientation triggers */
.page-portrait {
page: portrait;
break-before: page;
}
.page-landscape {
page: landscape;
break-before: page;
}
/* Prevent spatial elements from splitting */
.map-canvas, .legend-container, .scale-bar {
break-inside: avoid;
page-break-inside: avoid; /* Legacy fallback */
}
Key implementation notes:
size: letter landscapeinstructs the PDF engine to rotate the media box and adjust margins accordingly.break-before: pageforces a hard page break and applies the named@pagerule to the new sheet.- Always pair modern
break-inside: avoidwithpage-break-inside: avoidfor cross-renderer compatibility. - Margins should differ slightly between orientations to account for binding offsets and map bleed zones.
Python Pipeline Integration
In Python, assemble HTML fragments dynamically, inject the CSS, and pass the complete document to a Paged Media renderer. Below is a production-ready pattern using WeasyPrint:
from weasyprint import HTML
from jinja2 import Template
REPORT_CSS = """
<style>
@page { size: letter portrait; margin: 20mm; }
@page landscape { size: letter landscape; margin: 15mm; }
@page portrait { size: letter portrait; margin: 20mm; }
.page-portrait { page: portrait; break-before: page; }
.page-landscape { page: landscape; break-before: page; }
.map-canvas, .legend-container { break-inside: avoid; page-break-inside: avoid; }
</style>
"""
def assemble_spatial_report(portrait_blocks, landscape_blocks):
html_parts = ["<html><head>", REPORT_CSS, "</head><body>"]
for block in portrait_blocks:
html_parts.append(f'<div class="page-portrait">{block}</div>')
for block in landscape_blocks:
html_parts.append(f'<div class="page-landscape">{block}</div>')
html_parts.append("</body></html>")
return "".join(html_parts)
def render_to_pdf(html_string, output_path):
HTML(string=html_string).write_pdf(output_path)
# Usage example:
# portrait_html = ["<h1>Executive Summary</h1><p>Methodology details...</p>"]
# landscape_html = ['<div class="map-canvas"><img src="exported_map_300dpi.png" width="100%"></div>']
# render_to_pdf(assemble_spatial_report(portrait_html, landscape_html), "spatial_report.pdf")
Refer to the official WeasyPrint documentation for environment setup and font embedding requirements. Note that Jinja2 or similar templating engines are recommended for complex spatial reports, but string concatenation remains viable for lightweight pipelines.
Spatial Asset Scaling & Reflow Rules
GIS exports rarely match PDF page ratios natively. To prevent clipping, margin drift, or distorted scale bars, enforce these pre-render constraints:
- Match Aspect Ratios Exactly: Calculate the usable print area (
page_width - 2*margin,page_height - 2*margin) and export maps at that exact ratio. A US Letter landscape page (11" × 8.5" = 279.4mm × 215.9mm) with 15mm margins yields ~249mm × 186mm usable space. Export maps at roughly a 1.34:1 ratio. - Fixed DPI Export: Render maps at 150–300 DPI before HTML injection. Never rely on CSS
width: 100%to upscale low-resolution tiles; this causes pixelation and breaks scale bar accuracy. - Legend & Scale Bar Isolation: Wrap legends and scale bars in their own flex or grid containers with
break-inside: avoid. If a map spans a page break, duplicate the scale bar on the new page or convert it to a vector overlay. - Coordinate System Consistency: Ensure exported map images retain their projection metadata in accompanying captions. Automated pipelines should inject CRS strings dynamically to avoid cartographic ambiguity.
Renderer Quirks & Validation Checklist
Different PDF engines interpret Paged Media rules with varying strictness. Validate your pipeline against these common failure points:
| Issue | Cause | Fix |
|---|---|---|
| Orientation ignored | Missing @page name or incorrect page: assignment |
Verify CSS selector matches @page landscape exactly |
| Map splits across pages | Missing break-inside: avoid on container |
Apply to parent wrapper, not just <img> |
| Margins shift on landscape | Overriding @page margins with inline styles |
Keep margins in @page blocks only |
| Scale bar misaligned | CSS transform or object-fit applied |
Use native image dimensions + fixed container |
Pre-flight validation steps:
- Render a 2-page test (portrait → landscape → portrait) and verify media box rotation in a PDF inspector.
- Check that
@pagemargins do not overlap with map bleed zones. - Confirm that
break-before: pagedoes not create blank pages when consecutive sections share the same orientation. - Embed all custom fonts and verify SVG legend paths render at native stroke widths.
By treating orientation as a declarative layout property rather than a runtime viewport adjustment, spatial reporting pipelines achieve consistent, print-ready output across GIS exports, tabular data, and narrative sections.