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:

  1. Data Layer: Raw GeoJSON, raster tiles, or spatial database queries.
  2. Asset Layer: Pre-rendered map images, SVG legends, and tabular outputs sized to target print dimensions at fixed DPI (typically 150–300).
  3. Layout Layer: HTML/CSS wrappers with explicit @page assignments 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.

CSS
/* 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 landscape instructs the PDF engine to rotate the media box and adjust margins accordingly.
  • break-before: page forces a hard page break and applies the named @page rule to the new sheet.
  • Always pair modern break-inside: avoid with page-break-inside: avoid for 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:

Python
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:

  1. Render a 2-page test (portrait → landscape → portrait) and verify media box rotation in a PDF inspector.
  2. Check that @page margins do not overlap with map bleed zones.
  3. Confirm that break-before: page does not create blank pages when consecutive sections share the same orientation.
  4. 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.