Configuring ReportLab PageTemplate Frames for ISO 216 Sizes
ReportLab gives you absolute control over page geometry, which is exactly what a GIS report needs when a map frame must occupy a precise rectangle on an A3 plate and the methodology text must reflow inside an A4 body frame. The catch is that ReportLab works in points from a bottom-left origin, so getting ISO 216 sizes right means doing the millimetre-to-point conversion deliberately and computing every frame rectangle from the page box. This guide is a focused technique within the Print-Ready Page Sizing Standards for GIS Reports workflow, covering how to size Frame objects to A4 and A3, assemble a BaseDocTemplate with portrait and landscape PageTemplate objects, and reserve a dedicated map frame. For the broader programmatic layout toolkit, see the ReportLab Programmatic Layout section.
Prerequisites
- Python 3.10+ with
reportlab>=4.0installed (pip install reportlab) - A pre-rendered map image (PNG or PDF) sized to the intended map-frame dimensions
- Understanding that ReportLab measures in points (1 pt = 1/72 inch) from a bottom-left origin
- Familiarity with the ISO 216 series and target margins from Print-Ready Page Sizing Standards for GIS Reports
Step 1: Import ISO 216 Page Sizes and Points
Never hardcode ISO dimensions. reportlab.lib.pagesizes ships the exact point sizes, and reportlab.lib.units provides mm and inch so margin arithmetic stays readable.
from __future__ import annotations
from reportlab.lib.pagesizes import A4, A3, landscape
from reportlab.lib.units import mm
# A4 = (595.2755..., 841.8897...) points → 210 × 297 mm
# A3 = (841.8897..., 1190.5511...) points → 297 × 420 mm
print(A4, A3)
print(landscape(A3)) # (1190.55, 841.89) — width and height swapped
landscape() and its counterpart portrait() return a swapped (width, height) tuple, which is safer than manually reversing the values. Because A-series sizes share a 1:√2 ratio, an A4 body page pairs cleanly with an A3 fold-out map plate at the same aspect.
Step 2: Compute Frame Geometry from Margins
A Frame is defined by its lower-left corner (x1, y1), plus width and height. Derive all four from the page box and the margins, remembering that the origin is at the bottom.
from reportlab.platypus import Frame
def make_frames(page_size: tuple[float, float], margin: float) -> tuple[Frame, Frame]:
"""Split an ISO page into a top body frame and a lower map frame."""
page_w, page_h = page_size
usable_w = page_w - 2 * margin
usable_h = page_h - 2 * margin
map_h = usable_h * 0.62 # map frame takes 62% of usable height
body_h = usable_h - map_h - 6 * mm # gap between frames
body_frame = Frame(
x1=margin,
y1=margin + map_h + 6 * mm, # sits above the map frame
width=usable_w,
height=body_h,
id="body",
)
map_frame = Frame(
x1=margin,
y1=margin, # anchored at the bottom margin
width=usable_w,
height=map_h,
id="map",
)
return body_frame, map_frame
The critical subtraction is page_h - 2 * margin: using only the top margin is the single most common cause of clipped content, because the bottom margin then eats into the frame without being accounted for.
Step 3: Build a BaseDocTemplate with PageTemplates
BaseDocTemplate is the layout engine; each PageTemplate names a page layout and holds one or more frames. Register a portrait A4 template and a landscape A3 template so the report can switch between text pages and map plates.
from reportlab.platypus import BaseDocTemplate, PageTemplate
from reportlab.lib.units import mm
class SpatialReport(BaseDocTemplate):
def __init__(self, filename: str, **kw) -> None:
super().__init__(filename, pagesize=A4, **kw)
body_a4, map_a4 = make_frames(A4, margin=18 * mm)
body_a3, map_a3 = make_frames(landscape(A3), margin=15 * mm)
self.addPageTemplates([
PageTemplate(id="portrait_body", frames=[body_a4, map_a4],
pagesize=A4),
PageTemplate(id="landscape_plate", frames=[map_a3, body_a3],
pagesize=landscape(A3)),
])
Ordering frames inside a template matters: ReportLab fills them in list order, so on the landscape plate the map frame is listed first to receive the map Image before any caption flows into the secondary frame.
Step 4: Flow Content and Switch Templates
Content is a list of flowables. NextPageTemplate schedules which template the following page uses, and FrameBreak advances to the next frame within the current template.
from reportlab.platypus import Image, Paragraph, NextPageTemplate, FrameBreak, PageBreak
from reportlab.lib.styles import getSampleStyleSheet
styles = getSampleStyleSheet()
def build_story(map_a4_path: str, map_a3_path: str) -> list:
return [
Paragraph("Regional Flood Exposure Assessment", styles["Title"]),
Paragraph("Methodology and study area follow overleaf.", styles["BodyText"]),
FrameBreak(), # move into the A4 map frame
Image(map_a4_path, width=170 * mm, height=150 * mm),
NextPageTemplate("landscape_plate"), # next page is the A3 plate
PageBreak(),
Image(map_a3_path, width=380 * mm, height=250 * mm),
FrameBreak(),
Paragraph("Plate 1 — 1:25,000 hydrological catchment.", styles["BodyText"]),
]
doc = SpatialReport("iso216_report.pdf")
doc.build(build_story("catchment_a4.png", "catchment_a3.png"))
Sizing the Image explicitly in millimetres keeps the map inside its frame; ReportLab does not clip an oversized image, it overflows the frame silently, so the width and height must be smaller than the frame you computed in Step 2. This flowable-driven approach complements the table-flowing techniques in Iterating Through Shapefile Attributes in ReportLab.
Step 5: Verify the Emitted Page Size
Confirm ReportLab wrote the ISO sizes you intended by reading the media box back.
pdfinfo iso216_report.pdf | grep "Page size"
# Page 1: 595.28 x 841.89 pts (A4)
# Page 2: 1190.55 x 841.89 pts (A3 landscape)
Key Parameters / Configuration Reference
| Spec | Value | Notes |
|---|---|---|
| Point definition | 1 pt = 1/72 inch |
ReportLab’s base unit |
| A4 size | 595.28 × 841.89 pt |
Import as A4, do not hardcode |
| A3 size | 841.89 × 1190.55 pt |
Import as A3; use landscape(A3) to swap |
Frame origin |
bottom-left (x1, y1) |
y grows upward, unlike CSS |
Frame height |
page_h - 2 * margin |
Subtract both margins or content clips |
| Frame fill order | list order in PageTemplate |
First frame receives flowables first |
NextPageTemplate |
template id | Schedules the template for the next page |
FrameBreak |
— | Advances to the next frame on the current page |
Common Pitfalls
-
Hardcoding 595 × 842 as integers. The true A4 size is fractional (
595.28 × 841.89); rounding to integers shifts every frame by up to a point and can push a map past the trim. Import the constants instead. -
Forgetting the bottom-left origin. Positioning a header at
y1 = marginplaces it at the bottom of the page, not the top. Header frames anchor nearpage_h - margin - header_height. -
Oversized images overflowing the frame. ReportLab does not scale an
Imageto fit its frame automatically. Compute the image width and height to be smaller than the frame or the picture bleeds over the margin. -
Mismatched pagesize on the PageTemplate. Setting
pagesize=A4on the document butpagesize=landscape(A3)on a template without aNextPageTemplateproduces a page at the wrong size. Always schedule the template before thePageBreakthat should use it.
Verification
Assert the media box in CI with PyMuPDF so a geometry regression fails the build before a mis-sized report is distributed.
import fitz # pymupdf
A4_PT = (595.28, 841.89)
A3L_PT = (1190.55, 841.89)
def assert_iso_sizes(pdf_path: str) -> None:
doc = fitz.open(pdf_path)
r0, r1 = doc[0].rect, doc[1].rect
assert round(r0.width) == round(A4_PT[0]) and round(r0.height) == round(A4_PT[1]), \
f"page 1 not A4: {r0.width:.0f}×{r0.height:.0f}"
assert round(r1.width) == round(A3L_PT[0]) and round(r1.height) == round(A3L_PT[1]), \
f"page 2 not A3 landscape: {r1.width:.0f}×{r1.height:.0f}"
assert_iso_sizes("iso216_report.pdf")
Related
- Print-Ready Page Sizing Standards for GIS Reports — parent guide covering the ISO 216 and North American paper standards this frame math targets
- How to Set Exact Bleed Margins in WeasyPrint for GIS Maps — the HTML/CSS route to the same page-sizing goal for teams not using ReportLab
- ReportLab Programmatic Layout — the broader section on building map frames and flowing content with ReportLab
- Parent section: Print-Ready Page Sizing Standards for GIS Reports