Exporting Multiple QGIS Layouts to PDF with PyQGIS
When a project holds a dozen print layouts — one per district, one per theme, one per client — exporting each by hand through the layout dialog is slow and error-prone. The reliable approach is to iterate the active project's layout manager with PyQGIS and call QgsLayoutExporter.exportToPdf() on every print layout in turn. This removes manual UI clicks, enforces consistent file naming and DPI, and slots straight into a scripted Automated Map Layout Generation pipeline.
This page covers the batch pattern for separate layouts in one project. If instead a single layout fans out over many features from a coverage layer, the atlas PDF recipe is the right tool — the two techniques are complementary and often run in the same pipeline.
Prerequisites
- QGIS 3.34 LTR (Python 3.12) recommended; the code also runs on 3.28 LTR and the 3.40+ line.
- A saved project (
.qgzor.qgs) that already contains one or more print layouts — this script exports existing layouts, it does not build them. - Write access to an output directory.
- Familiarity with running code in the QGIS Python Console, or a headless setup if you are automating outside the desktop.
Everything below relies only on classes from qgis.core, so it works identically in the console and in a standalone script.
Concrete Recipe: Export Every Layout
Run this directly in the QGIS Python Console, or embed the function in a standalone script:
import os
import re
from qgis.core import QgsProject, QgsLayoutExporter
def export_all_layouts_to_pdf(output_dir: str) -> None:
"""Exports every print layout in the active QGIS project to individual PDFs."""
os.makedirs(output_dir, exist_ok=True)
project = QgsProject.instance()
manager = project.layoutManager()
layouts = manager.printLayouts()
if not layouts:
print("No print layouts found in the current project.")
return
for layout in layouts:
name = layout.name()
# Sanitize the layout name for cross-platform-safe filenames
safe_name = re.sub(r'[^\w\-_ ]', '', name).strip()
pdf_path = os.path.join(output_dir, f"{safe_name}.pdf")
exporter = QgsLayoutExporter(layout)
settings = QgsLayoutExporter.PdfExportSettings()
settings.dpi = 300
settings.forceVectorOutput = True
result = exporter.exportToPdf(pdf_path, settings)
if result == QgsLayoutExporter.Success:
print(f"Exported: {pdf_path}")
else:
print(f"Failed: {name} | Error code: {result}")
# Usage in the QGIS Python Console:
# export_all_layouts_to_pdf(r"C:\QGIS_Exports\PDFs")
Breakdown: project.layoutManager().printLayouts() returns only the print layouts (it filters out report objects), so new layouts added to the project are picked up automatically — there are no hardcoded names to maintain. Each layout gets its own QgsLayoutExporter, its own PdfExportSettings, and its own output file named after the layout. Reading layout.name() and sanitizing it means the on-disk filenames stay predictable and match what a cartographer sees in the layout manager.
exportToPdf() returns an enum result, not a boolean. Comparing it against QgsLayoutExporter.Success distinguishes a clean export from the various failure codes (FileError, PrintError, Canceled, MemoryError, IteratorError), which is what makes batch runs auditable rather than silently partial.
One combined PDF instead of many files
To merge every layout into a single multi-page document, collect the layouts and call the static exportLayoutsToPdf() helper once:
from qgis.core import QgsProject, QgsLayoutExporter
layouts = QgsProject.instance().layoutManager().printLayouts()
settings = QgsLayoutExporter.PdfExportSettings()
settings.dpi = 300
settings.forceVectorOutput = True
result, error = QgsLayoutExporter.exportLayoutsToPdf(
layouts, "/path/to/combined_atlas.pdf", settings
)
if result != QgsLayoutExporter.Success:
print(f"Combined export failed: {error}")
Use one-file-per-layout when each map is a standalone deliverable; use the combined document when the layouts form a single report or booklet.
Key Export Settings
A few PdfExportSettings fields decide print quality and file size:
settings.dpi = 300— 300 DPI is the standard for print-grade output. Drop to 150 for quick previews to speed up rendering and shrink files.settings.forceVectorOutput = True— keeps geometry, text and line work as true vectors, so typography stays crisp at any zoom. Raster layers (satellite imagery, hillshades) still rasterize at the configureddpi.settings.rasterizeWholeImage = False— leave this off unless blend modes or layer effects render incorrectly as vectors; rasterizing the whole page inflates file size.settings.exportMetadata = True— writes the layout's title, author and keywords into the PDF metadata, useful for archival deliverables.
Standardizing DPI and vector output across the whole batch is what guarantees cartographic consistency between team members and export runs.
Running Headless (CI/CD, Docker, Cron)
For automated pipelines, GitHub Actions or scheduled jobs, initialize the QGIS application environment before loading the project. This is the same bootstrap used to run a Processing algorithm from a standalone script:
from qgis.core import QgsApplication, QgsProject
# 1. Bootstrap QGIS (adjust the prefix path to your OS/install)
qgs = QgsApplication([], False)
qgs.setPrefixPath("/usr", True)
qgs.initQgis()
# 2. Load the project and run the export
project = QgsProject.instance()
if not project.read("/path/to/your/project.qgz"):
raise RuntimeError("Could not read project file")
export_all_layouts_to_pdf("/path/to/output")
# 3. Clean exit
qgs.exitQgis()
The QgsApplication([], False) call with False starts QGIS without a GUI, which is exactly what a Docker container or CI runner needs. Without this initialization a standalone script cannot access the layout classes at all.
One PDF or many
The export loop can produce a file per layout or a single combined document, and the choice affects how the result is used far more than how it is produced.
Vector or raster inside the PDF
A PDF can hold real vector geometry or a rasterised image of the map, and the setting that decides is easy to leave wrong.
QGIS Version Compatibility
- QGIS 3.10+ is the floor.
QgsLayoutExporterreplaces the legacyQgsComposition/QgsComposerAPI from QGIS 2.x — code written against composers will not run here. - QGIS 3.34 LTR / 3.40+ is recommended and what the examples target. The
printLayouts(),PdfExportSettingsandexportToPdf()signatures have been stable across the 3.x LTR line, so the recipe carries forward without changes. - Path handling: Windows needs raw strings (
r"C:\path") or escaped backslashes; Linux and macOS take standard POSIX paths. Resolve relatives withos.path.abspath()before exporting. - Python: the console ships Python 3.9+ depending on the QGIS build (3.12 on 3.34 LTR). The
reandoscalls used here are version-agnostic.
Troubleshooting
| Error / symptom | Likely cause | Fix |
|---|---|---|
FileError / Canceled despite a valid path | Target file locked by antivirus, a cloud-sync client, or a PDF viewer holding the previous output | Export to a local temp folder, then move the files. Close viewers on Windows; disable real-time scanning for the target path. |
PrintError | Uninitialized layout or missing page dimensions | Call layout.initializeDefaults() on dynamically created layouts before exporting. |
| Blank or partial PDF | Broken data sources or missing system fonts | Validate layers with layer.isValid() before export; install required fonts or enable font fallback in QGIS settings. |
MemoryError on high-DPI pages | Very complex symbology at 300+ DPI | Lower settings.dpi to 150 for tests; simplify heavy symbology; insert QgsApplication.processEvents() between iterations to flush the Qt event queue and free RAM. |
| Distorted scale bars or grids | Map items in mixed coordinate reference systems | Align all map items to one projected CRS; mixed CRS definitions force on-the-fly transforms that skew scale. |
For batch throughput, resolve paths up front with os.path.abspath(), and when running inside the desktop call iface.mapCanvas().setRenderFlag(False) to free CPU/GPU cycles for the background export. In production, replace the print() calls with structured CSV or JSON logging so each run leaves an audit trail.
Conclusion
Iterating the layout manager and calling QgsLayoutExporter.exportToPdf() turns a manual, per-layout chore into a single repeatable function. Reading layouts from printLayouts() rather than a hardcoded list means the script keeps working as maps are added or renamed, and checking the enum result against QgsLayoutExporter.Success keeps every run auditable. Pin the code to a QGIS LTR release, standardize DPI and vector output, and the same script serves the desktop console and a headless CI job without change — the delivery layer of a fully automated spatial pipeline.
Frequently Asked Questions
How do I get every print layout in a project to export at once?
Call QgsProject.instance().layoutManager().printLayouts() to retrieve the list of layouts, then loop over it and call QgsLayoutExporter.exportToPdf() for each one. Using the layout manager instead of hardcoded names means new layouts are picked up automatically without editing the script.
What does forceVectorOutput = True actually do?
It tells QgsLayoutExporter to keep vector geometry, text and line work as true vectors in the PDF instead of rasterizing them, preserving crisp typography at any zoom — which matters for print-grade deliverables. Raster layers such as satellite imagery or hillshades still render at the configured dpi value.
Why does export return FileError even though my path looks correct?
The target file is usually locked by antivirus, a cloud-sync client, or a PDF viewer that still has a previous output open, or the directory lacks write permissions. Export to a local temporary folder first and move the files afterward, close any viewer holding the output on Windows, and wrap the export call in try/except so you can log and skip these cases.
How do I run the export without the QGIS desktop, for CI or a cron job?
Bootstrap the application before loading the project: create QgsApplication([], False), set the prefix path, and call initQgis(), then read the project and run the export, finishing with exitQgis(). This headless initialization gives a standalone script access to the same PyQGIS classes available in the console.
How should I sanitize layout names before using them as filenames?
Layout names can contain characters that are illegal in file paths, so strip them with a regex such as re.sub(r'[^\w\-_ ]', '', name).strip() before joining the name to your output directory. Creating the directory with os.makedirs(..., exist_ok=True) and resolving it with os.path.abspath() further prevents silent export failures.
Related
- Up: Automated Map Layout Generation with PyQGIS — the parent guide to the layout object model this recipe drives.
- Generate an Atlas PDF in PyQGIS — when one layout fans out over a coverage layer instead of many separate layouts.
- Run a Processing Algorithm from a Script in PyQGIS — the standalone bootstrap pattern used for headless exports.
- Chain Buffer and Clip in PyQGIS — building the analysis outputs that feed these layouts.