Export Atlas Pages to Individual PDFs

A single two-hundred-page PDF is the right output when somebody is going to print a book. It is the wrong output when each ward officer needs their own map, when the files feed a document management system, or when a web page links to one sheet per site. For those, the atlas should produce two hundred files with meaningful names — which is one call, and a handful of decisions around it.

This recipe belongs to Automating Atlas Map Series. It covers exporting one file per feature, controlling the names and the output settings, exporting a subset, and running the whole thing unattended with useful progress and error reporting.

One document, or one file eachA combined export produces a single multi-page PDF suited to printing and to reading as a book. A per-feature export produces one named file per coverage feature, suited to distribution, to linking from a web page, and to loading into a document system. The same atlas configuration drives both; only the export call differs.Same atlas, two export callsconfigured atlas200 pagesexportToPdf — one documentprinting, binding, reading throughone file, 180 MBexportToPdfs — one per featuredistribution, linking, filing200 named files

Prerequisites

  • QGIS 3.34 LTR (bundled Python 3.12) or newer.
  • A layout with a configured atlas — see Configure an Atlas Coverage Layer in PyQGIS.
  • A file name expression on the atlas, or every file will be called output_1, output_2 and so on.

Export one file per feature

from qgis.core import (QgsProject, QgsLayoutExporter)

project = QgsProject.instance()
layout = project.layoutManager().layoutByName("Ward maps")
atlas = layout.atlas()

settings = QgsLayoutExporter.PdfExportSettings()
settings.dpi = 300
settings.rasterizeWholeImage = False
settings.forceVectorOutput = True

exporter = QgsLayoutExporter(layout)
result, error = QgsLayoutExporter.exportToPdfs(
    atlas, "/data/exports/wards", settings)

if result != QgsLayoutExporter.Success:
    raise RuntimeError(f"atlas export failed: {error}")

Breakdown: exportToPdfs() — plural — takes the atlas rather than the layout and a directory rather than a file path, and writes one PDF per feature named from the atlas file name expression. dpi at 300 is print quality; 150 halves the file size and is fine for screen distribution. forceVectorOutput keeps text and lines as vectors so the PDF stays sharp and searchable, which is what you want unless the map contains blend modes or transparency that must be rasterised to render correctly. The result is an enumeration, not a boolean, and the error string names the file it stopped on — both worth surfacing in a scheduled job.

Get the file names right

The names come from the atlas configuration, not the export call:

atlas.setFilenameExpression(
    "'ward_' || lower(replace(\"ward_name\", ' ', '_')) || '_' || \"ward_code\"")
print(atlas.nameForPage(0))

Breakdown: Checking nameForPage(0) before exporting is the cheapest way to catch a name expression that produces something unusable — a NULL field yields an empty name, and every page then overwrites the same file, leaving one PDF where two hundred were expected. Including a code as well as a readable name guarantees uniqueness where two wards share a name, which happens more often than seems reasonable. Avoid characters that are illegal in file names on any platform your outputs might reach: slashes, colons and quotes are the usual offenders, and replace() chains handle them.

Export a subset

For a long-running series, exporting only what changed is often the difference between a two-minute job and an hour.

atlas.setFilterFeatures(True)
atlas.setFilterExpression("\"updated_on\" >= '2026-08-01'")

print(atlas.count(), "pages to export")
result, error = QgsLayoutExporter.exportToPdfs(
    atlas, "/data/exports/wards", settings)

atlas.setFilterFeatures(False)      # restore for the next caller

Breakdown: The atlas filter is the mechanism for a partial export — there is no "export pages 5 to 12" argument, and using the filter is better anyway because it works on meaning rather than position. Restoring the filter afterwards matters when the same project is used by other jobs or by a person; a script that leaves a filter enabled produces a puzzling half-empty atlas the next time somebody opens it. In a scheduled job it is usually cleaner to read the project into a standalone QgsProject() so the change is discarded with the object.

Exporting only what changedA scheduled export reads the timestamp of the previous run, filters the coverage layer to features updated since then, exports only those pages, and records the new timestamp. A full export runs on a weekly schedule so that any page missed by the incremental logic is eventually refreshed.Two hundred pages nightly is rarely necessaryread last run timefrom a state filefilter the atlasupdated since thenexport 6 pagesinstead of 200record the timefor the next runand a full export weeklyso a page the incremental logic missed is eventually refreshed

Report progress on a long run

exportToPdfs() blocks until every page is written, which on a large atlas can be many minutes with no output at all. Passing a feedback object gives progress:

from qgis.core import QgsFeedback

feedback = QgsFeedback()
feedback.progressChanged.connect(lambda p: print(f"{p:.0f}%"))

result, error = QgsLayoutExporter.exportToPdfs(
    atlas, "/data/exports/wards", settings, QgsLayoutExporter.PdfExportSettings(), feedback)

Breakdown: The feedback object reports overall progress across the atlas and, importantly, can be cancelled — feedback.cancel() from another thread or a timer stops the export cleanly rather than leaving a half-written file. In a plugin this belongs on a background task so the interface stays responsive, following Run a Background Task with QgsTask in PyQGIS. In a scheduled script, printing progress to a log gives the operator something to look at when a run takes longer than usual, and it is the difference between "the job is slow" and "the job is stuck".

Three checks around a long exportBefore the run, the page count confirms the filter did what was intended. During the run, feedback reports progress so a slow export can be distinguished from a stuck one. After the run, counting the files written catches a collapsed file name expression, which is the one failure that produces a successful return and almost no output.Check before, during and after — each catches a different failurebefore — atlas.count()catches a filter thatexcludes everythingduring — feedbackdistinguishes slowfrom stuckafter — count the filescatches a collapsedname expressionOnly the third catches a run that reports success and wrote three files

Wrap it as an unattended job

from pathlib import Path
from qgis.core import QgsProject

def export_ward_maps(project_path, output_dir):
    project = QgsProject()
    if not project.read(project_path):
        raise RuntimeError(f"cannot read {project_path}")

    layout = project.layoutManager().layoutByName("Ward maps")
    if layout is None:
        raise RuntimeError("layout 'Ward maps' not found")

    Path(output_dir).mkdir(parents=True, exist_ok=True)

    settings = QgsLayoutExporter.PdfExportSettings()
    settings.dpi = 300

    result, error = QgsLayoutExporter.exportToPdfs(
        layout.atlas(), output_dir, settings)
    if result != QgsLayoutExporter.Success:
        raise RuntimeError(f"export failed: {error}")

    produced = sorted(Path(output_dir).glob("*.pdf"))
    print(f"wrote {len(produced)} files")
    return produced

Breakdown: Reading into a standalone QgsProject() keeps the job independent of anything else running, and means any filter it sets is discarded when the function returns. Creating the output directory first is necessary — the exporter does not, and the failure message about a directory is not always obvious. Counting the files afterwards is the check that matters in an unattended run: a successful return with three files where two hundred were expected means the name expression collapsed, and only the count reveals it. Logging that number alongside the run is what turns a silent regression into an alert, as described in Handle Errors and Logging in Unattended Scripts.

QGIS version compatibility

QGIS versionPythonNotes
3.22 LTR3.9exportToPdfs present with the same signature.
3.28 LTR3.9Identical.
3.34 LTR3.12Baseline for this page; geospatial PDF options available in the settings object.
3.40 / 3.443.12Identical; newer releases add more PDF metadata and layered-export options.

exportToImage() and exportToSvg() have matching plural forms for atlases with the same shape, so switching output format changes one call and one settings class.

Troubleshooting

  • All pages overwrite one file. The file name expression yields the same value — often because the field is NULL. Check nameForPage() first.
  • The export fails on one page. The error string names it. A NULL in a label expression or an unreachable remote layer is the usual cause.
  • Files land in the wrong place. The second argument is a directory for the plural form and a file path for the singular. Mixing them up produces a directory called wards.pdf.
  • Text is fuzzy in the output. forceVectorOutput is off, or a blend mode forced rasterisation. Check the layer styling.
  • The export takes far longer than expected. Remote layers are being fetched per page. Cache them locally first — see Web Services and Remote Data in PyQGIS.
  • Nothing is written and no error appears. The atlas filter excludes every feature. Print atlas.count() before exporting.

Conclusion

Use exportToPdfs() with the atlas and an output directory to get one named file per feature, and drive the names from an atlas expression that is unique and file-system safe. Check count() and nameForPage() before a long run, use the filter to export only what changed, pass a feedback object so a long export reports progress and can be cancelled, and count the files produced afterwards — that count is the only check that catches a silently collapsed name expression.

Frequently Asked Questions

Can I export to formats other than PDF? Yes — exportToImages() and exportToSvgs() take the same arguments with their own settings classes.

How do I add a page number or a total to each sheet? Use the @atlas_featurenumber and @atlas_totalfeatures variables in a label expression. They are populated during the export.

Can I export a single named feature? Set a filter expression matching that one feature and export as usual. It is the same mechanism as any other subset.

Does the export honour layer scale-based visibility? Yes, and at the atlas's per-page scale, which is worth remembering when Auto scaling puts some pages outside a layer's visible range.

Where do the files go if the name expression is empty? The exporter falls back to a numbered name, which is exactly the outcome the expression exists to avoid. Set one.

How large will the files be? Mostly determined by the raster content: a vector-only sheet is typically well under a megabyte, while one carrying aerial imagery at 300 dpi can be twenty times that. Lowering the dpi or clipping the imagery to the frame are the two levers that matter.

Can I produce a geospatial PDF? Yes — the export settings carry georeferencing options that embed the coordinate system and layer structure, so the result opens as a locatable map in readers that support it. It increases the file size and is worth it only when somebody downstream uses the feature.

Can I run this without QGIS desktop? Yes — it is an ordinary headless export. See Headless QGIS and Server Automation.