Generating an Atlas Map Series Programmatically
Drive the QGIS atlas engine in Python with QgsLayoutAtlas: bind a coverage layer, set a page-name expression, and export one map sheet per feature as PDF or…
TL;DR: Get the layout’s atlas() (a QgsLayoutAtlas), call setCoverageLayer() and setEnabled(True), set a filename/page-name expression, then export with QgsLayoutExporter.exportToPdfs(atlas, ...) for one file per feature — or drive beginRender() / next() yourself for full control over each sheet.
This page is part of the automating print layouts and map export guide, which sits inside the broader Plugin Development & UI Integration in QGIS reference. Where exporting a single print layout to PDF with QgsLayoutExporter produces one document, the atlas engine turns a single layout into a series — one map sheet per feature in a coverage layer. Think map books, per-parcel cadastral prints, or a survey report with one page per site.
Complete Runnable Script
The script below loads a project, resolves a print layout by name, wires up its QgsLayoutAtlas, and exports one PDF per coverage feature. It also demonstrates the manual beginRender() / next() iteration path, which you need whenever you want to inspect, log, or post-process each individual sheet. Both approaches share the same atlas configuration; only the export mechanism differs.
"""
atlas_export.py
Configure and export a QGIS atlas map series from an existing print layout.
Compatible with QGIS 3.28+ LTR. Runs headless or inside the QGIS Python console.
"""
from __future__ import annotations
from pathlib import Path
from qgis.core import (
QgsApplication,
QgsLayout,
QgsLayoutAtlas,
QgsLayoutExporter,
QgsLayoutItemMap,
QgsPrintLayout,
QgsProject,
QgsVectorLayer,
)
def configure_atlas(
layout: QgsPrintLayout,
coverage_layer: QgsVectorLayer,
*,
page_name_expr: str = '"name"',
filter_expr: str | None = None,
map_margin: float = 0.10,
) -> QgsLayoutAtlas:
"""
Bind a coverage layer to a layout's atlas and mark its map item as
atlas-driven.
Args:
layout: The print layout carrying at least one map item.
coverage_layer: Vector layer whose features drive the iteration.
page_name_expr: QGIS expression yielding a per-feature page name;
doubles as the default filename token.
filter_expr: Optional expression to restrict which features render.
map_margin: Fractional padding around each feature (0.10 = 10%).
Returns:
The configured, enabled QgsLayoutAtlas.
Raises:
ValueError: if the layout has no QgsLayoutItemMap to drive.
"""
atlas: QgsLayoutAtlas = layout.atlas()
atlas.setCoverageLayer(coverage_layer)
atlas.setEnabled(True)
atlas.setHideCoverage(False)
atlas.setPageNameExpression(page_name_expr)
atlas.setFilenameExpression("'sheet_' || @atlas_featurenumber")
if filter_expr:
atlas.setFilterFeatures(True)
atlas.setFilterExpression(filter_expr)
# Find the first map item and let the atlas frame each feature.
map_item: QgsLayoutItemMap | None = next(
(item for item in layout.items() if isinstance(item, QgsLayoutItemMap)),
None,
)
if map_item is None:
raise ValueError("Layout contains no QgsLayoutItemMap to drive.")
map_item.setAtlasDriven(True)
map_item.setAtlasScalingMode(QgsLayoutItemMap.Auto)
map_item.setAtlasMargin(map_margin)
return atlas
def export_atlas_to_pdfs(
layout: QgsPrintLayout,
atlas: QgsLayoutAtlas,
out_dir: Path,
) -> None:
"""
Export one PDF per feature using the QgsLayoutExporter atlas overload.
Raises:
RuntimeError: if the exporter reports a non-Success result.
"""
out_dir.mkdir(parents=True, exist_ok=True)
exporter = QgsLayoutExporter(layout)
settings = QgsLayoutExporter.PdfExportSettings()
settings.dpi = 300
settings.rasterizeWholeImage = False
result, error = exporter.exportToPdfs(atlas, str(out_dir), settings)
if result != QgsLayoutExporter.Success:
raise RuntimeError(f"Atlas PDF export failed: {error} (code {result})")
def export_atlas_manually(
layout: QgsPrintLayout,
atlas: QgsLayoutAtlas,
out_dir: Path,
) -> list[Path]:
"""
Iterate the atlas by hand with beginRender()/next().
Use this path when you need per-sheet logging, conditional export, or
custom filenames beyond what the filename expression supports.
Returns:
The list of files written, in iteration order.
"""
out_dir.mkdir(parents=True, exist_ok=True)
exporter = QgsLayoutExporter(layout)
written: list[Path] = []
if not atlas.beginRender():
raise RuntimeError("atlas.beginRender() returned False — no features?")
# first() positions the atlas on feature 0 and returns True while valid.
keep_going = atlas.first()
while keep_going:
# currentFilename() is resolved from setFilenameExpression().
target = out_dir / f"{atlas.currentFilename()}.png"
result = exporter.exportToImage(
str(target), QgsLayoutExporter.ImageExportSettings()
)
if result != QgsLayoutExporter.Success:
raise RuntimeError(f"Failed on {target.name}: code {result}")
written.append(target)
keep_going = atlas.next()
atlas.endRender()
return written
def main() -> None:
"""Resolve a layout, configure its atlas, and export the series."""
project = QgsProject.instance()
project.read("/data/projects/parcels.qgz")
layout_obj: QgsLayout | None = (
project.layoutManager().layoutByName("Parcel Map Book")
)
if not isinstance(layout_obj, QgsPrintLayout):
raise RuntimeError("Print layout 'Parcel Map Book' not found.")
coverage = project.mapLayersByName("parcels")[0]
atlas = configure_atlas(layout_obj, coverage, page_name_expr='"parcel_id"')
export_atlas_to_pdfs(layout_obj, atlas, Path("/data/output/atlas_pdfs"))
if __name__ == "__main__":
QgsApplication.setPrefixPath("/usr", True)
qgs = QgsApplication([], False)
qgs.initQgis()
try:
main()
finally:
qgs.exitQgis()
Run it headless with python atlas_export.py, or paste configure_atlas and export_atlas_to_pdfs into the QGIS Python console and call them against an already-open project — the atlas configuration is identical either way.
How the Atlas Iteration Works
At its core the atlas engine is a controller that walks a coverage layer’s features, and for each one repositions every atlas-driven map item, resolves the page-name and filename expressions, and renders the layout as a self-contained sheet. The diagram below shows the fan-out from a single coverage layer to N output files.
One iteration is a short, fixed sequence — and the map item step is the one people leave out:
Architecture Breakdown
QgsLayoutAtlas — the contract
Every QgsPrintLayout owns exactly one QgsLayoutAtlas, retrieved with layout.atlas(). You never construct it directly; you configure the instance the layout already carries. Three properties do the heavy lifting:
setCoverageLayer(layer)binds the vector layer whose features drive iteration.coverageLayer()reads it back. The feature count of this layer equals the number of sheets the atlas will emit (minus any filtered out).setPageNameExpression(expr)accepts a QGIS expression string evaluated per feature. It names each page in the layout designer’s page list and, importantly, is available as a token for output naming. A bare field reference like"parcel_id"is a valid expression.setFilterExpression(expr)combined withsetFilterFeatures(True)restricts iteration to features matching a boolean expression — for example"status" = 'approved'. WithoutsetFilterFeatures(True), the filter string is ignored entirely.
Call setEnabled(True) to make the atlas active; a disabled atlas exports as a single ordinary layout. Inside expressions you also gain atlas-specific variables: @atlas_featurenumber (1-based), @atlas_totalfeatures, @atlas_pagename, and @atlas_feature (the current QgsFeature). These are the building blocks for deterministic filenames — 'sheet_' || @atlas_featurenumber guarantees a stable ordering that does not depend on attribute uniqueness.
The atlas-driven map item
An atlas that iterates features but never moves the map produces N identical sheets. The link between the two is QgsLayoutItemMap.setAtlasDriven(True): it tells that map item to recentre on the current feature’s geometry each iteration. A layout can contain several maps; only those flagged atlas-driven follow the coverage feature, so an overview inset can stay fixed while the main map pans.
When every page comes out the same, it is one of three causes:
How the map frames each feature is governed by the scaling mode set with setAtlasScalingMode():
QgsLayoutItemMap.Auto— pick the best scale so the feature plus its margin fills the frame. Combine withsetAtlasMargin(0.10)for 10% padding.QgsLayoutItemMap.Fixed— hold the map’s own scale constant and simply centre on the feature. Ideal for consistent-scale map books.QgsLayoutItemMap.Predefined— snap to the nearest scale in the project’s predefined scale list.
Set the scaling mode before exporting; changing it mid-render has no effect on sheets already produced.
Export overloads: single-file vs multi
QgsLayoutExporter exposes atlas-aware overloads that accept the QgsLayoutAtlas as their first argument and manage beginRender()/next() internally:
exportToPdfs(atlas, baseFilePath, settings)writes one PDF per feature, naming each from the filename expression. This is the map-book-as-loose-pages case.exportToPdf(atlas, filePath, settings)(singular) concatenates every sheet into one multi-page PDF — the bound map book. Note the singularexportToPdfversus pluralexportToPdfs; the distinction is the entire difference between a folder of files and one document.exportToImage(atlas, dir, extension, settings)writes one raster (PNG, JPEG, TIFF) per feature into a directory.
All of these return a QgsLayoutExporter.ExportResult enum. Compare against QgsLayoutExporter.Success and surface anything else — common non-success codes are FileError (unwritable path), PrintError, and Canceled. When you need behaviour the overloads do not offer — per-sheet logging, conditional skipping, uploading each file as it lands — fall back to the manual first()/next()/endRender() loop shown in the runnable script, calling a plain single-layout exporter inside the loop.
Running the Export Off the UI Thread
Atlas exports are CPU- and I/O-heavy: a coverage layer with a few hundred features can take minutes at 300 DPI. Inside a plugin, running that synchronously freezes the QGIS window and risks the OS marking it unresponsive. Wrap the export in a QgsTask so it runs on a background thread and reports progress back to the UI. The asynchronous task execution with QgsTask guide covers the full pattern; the skeleton below shows the atlas-specific wiring.
"""atlas_task.py — run an atlas export inside a background QgsTask."""
from __future__ import annotations
from pathlib import Path
from qgis.core import QgsLayoutAtlas, QgsPrintLayout, QgsTask
class AtlasExportTask(QgsTask):
"""Export an atlas series without blocking the QGIS UI thread."""
def __init__(
self, layout: QgsPrintLayout, atlas: QgsLayoutAtlas, out_dir: Path
) -> None:
super().__init__("Atlas export", QgsTask.CanCancel)
self._layout = layout
self._atlas = atlas
self._out_dir = out_dir
self._error: str | None = None
def run(self) -> bool:
"""Executed on the worker thread. Never touch the GUI here."""
try:
from atlas_export import export_atlas_to_pdfs
export_atlas_to_pdfs(self._layout, self._atlas, self._out_dir)
return True
except Exception as exc: # noqa: BLE001 — report to finished()
self._error = str(exc)
return False
def finished(self, ok: bool) -> None:
"""Executed back on the main thread — safe to update UI here."""
if not ok:
print(f"Atlas export failed: {self._error}")
Add the task with QgsApplication.taskManager().addTask(task) and it runs to completion in the background. Because rendering happens off-thread, keep every QGIS-object mutation inside run() and confine UI updates to finished().
Production Best Practices
- Set
setEnabled(True)explicitly. A layout whose atlas is configured but not enabled exports as a single sheet with no warning. - Flag at least one map item as atlas-driven. Otherwise the map never pans and you get N copies of the same view.
- Prefer
@atlas_featurenumberfor filenames. Attribute-derived names can collide or contain path-hostile characters; the feature number is unique and monotonic. Sanitise attribute-based names if you must use them. - Always check the
ExportResult. Treat anything other thanQgsLayoutExporter.Successas a hard failure and log the code — silent partial exports are worse than a loud stop. - Choose the scaling mode deliberately.
Fixedfor uniform map books,Autowith a margin for varied feature sizes. Mixing them across a series confuses readers. - Use
setFilterFeatures(True)alongsidesetFilterExpression(). The filter string is inert without the boolean flag. - Export long series inside a
QgsTaskso the QGIS UI stays responsive and the job is cancellable. - Balance DPI against volume. 300 DPI is print-quality; for a proof run or hundreds of sheets, drop to 150 DPI to cut render time and file size.
Related
- Automating Print Layouts and Map Export — parent guide to the layout and export subsystem, from map items to exporters
- Exporting a Print Layout to PDF with
QgsLayoutExporter— the single-document export path that the atlas overloads build on - Asynchronous Task Execution with
QgsTask— run heavy atlas exports on a background thread without freezing the QGIS UI