Skip to content
← Back to Map layouts in QGIS: from template to automated map series
Map layouts · Part 16 of 18Intermediate· 7 min readSources

Export a PDF and a PNG with PyQGIS, and Check the Outputs

Export a layout as a PDF and as a PNG at a chosen resolution, check the layout before the export and the files after it, and learn which checks a script can do and which only a person can.

A script that says "Export succeeded" has told you that a file was written. It has not told you that the map is right. This lesson adds two kinds of checks, so that the script catches the mistakes that a machine can catch, and it names the ones that it cannot. It follows the export page of Part B, where you did the checking by eye.

Open scripts/lesson6_export_and_check.py.

Before the steps: imports

# Lesson 6: export a PDF and a PNG, and check the layout before and the files after.
from pathlib import Path
from qgis.core import (
    QgsProject,
    QgsPrintLayout,
    QgsReadWriteContext,
    QgsLayoutExporter,
    QgsExpressionContextUtils,
)
from qgis.PyQt.QtXml import QDomDocument

project = QgsProject.instance()
folder = Path(project.homePath())
output_folder = folder / "output" / "checked"
output_folder.mkdir(parents=True, exist_ok=True)

Step 1: A layout to export

# Step 1: a layout from the template, set to the East corporation
document = QDomDocument()
document.setContent((folder / "templates" / "parks_and_lakes_template.qpt").read_text(encoding="utf-8"))
layout = QgsPrintLayout(project)
layout.loadFromTemplate(document, QgsReadWriteContext(), True)
regions = project.mapLayersByName("Parks and lakes, share of area (%)")[0]
east = next(regions.getFeatures("\"short\" = 'East'"))
layout.itemById("map_title").setText(east["title"])
QgsExpressionContextUtils.setProjectVariable(project, "focus_name", "East")
extent = east.geometry().boundingBox()
extent.scale(1.25)
layout.itemById("main_map").zoomToExtent(extent)

REQUIRED_IDS = ["main_map", "map_title", "main_legend", "main_scale_bar"]

The script makes a layout from the template for the East corporation, as in the last two lessons.

Step 2: A check before the export

# Step 2: a check that runs BEFORE the export. It returns a list of problems; an empty list means "all present".
def check_layout(layout):
    return [item_id for item_id in REQUIRED_IDS if layout.itemById(item_id) is None]

REQUIRED_IDS is the list of items the page must have, and check_layout returns the ones that are missing. The list comprehension reads as: for each ID in the list, keep it if the layout has no item with that ID. An empty list means that nothing is missing. Returning the list, and not stopping at the first problem, tells you everything that is wrong in one go.

Step 3: A check after the export

# Step 3: a check that runs AFTER the export. A file that is missing or empty is a failed export.
def check_file(path):
    path = Path(path)
    if not path.exists():
        return f"{path.name} does not exist"
    if path.stat().st_size == 0:
        return f"{path.name} is empty"
    return None

A file that does not exist, or that is empty, is a failed export, whatever the status code says. This check costs two lines, and it is worth having, because a script that ran for ten minutes and produced empty files is no help.

Step 4: Export both formats

# Step 4: export both formats, but only if the layout passes its check
missing = check_layout(layout)
if missing:
    raise ValueError("Missing required layout items: " + ", ".join(missing))
print("Required layout items are present.")

exporter = QgsLayoutExporter(layout)

pdf_path = output_folder / "East.pdf"
result = exporter.exportToPdf(str(pdf_path), QgsLayoutExporter.PdfExportSettings())
print("PDF export status:", "Success" if result == QgsLayoutExporter.Success else result)

png_settings = QgsLayoutExporter.ImageExportSettings()
png_settings.dpi = 150                    # 300 for print; 150 is plenty for a screen or a web page
png_path = output_folder / "East.png"
result = exporter.exportToImage(str(png_path), png_settings)
print("PNG export status:", "Success" if result == QgsLayoutExporter.Success else result)

Look at the structure. If the layout is not complete, the script raises an error and nothing is exported. If it is complete, the exporter writes a PDF and then a PNG.

The PNG uses ImageExportSettings, and dpi = 150 sets its resolution. A PDF keeps text and lines as vectors, so it has no dpi for them. For an image, 300 dpi suits printing, and 150 is plenty for a web page or a slide. The Part B page on export shows how the sizes compare: the same page is 0.4 MB at 150 dpi and 0.9 MB at 300 dpi.

Each export returns a status code. The code is compared with QgsLayoutExporter.Success, and the number is printed otherwise. In QGIS 3.44 Success is 0 and FileError is 3.

Step 5: Check the files

# Step 5: check the files that were written
for path in (pdf_path, png_path):
    problem = check_file(path)
    print(path.name, "->", problem or f"ok, {path.stat().st_size:,} bytes")

For each file, the check is called and the size is printed. {size:,} writes the number with thousands separators, which is easier to read.

Step 6: See the first check catch a mistake

# Step 6: see the first check catch a mistake. Remove the legend, as if the template had changed.
layout.removeLayoutItem(layout.itemById("main_legend"))
print("After removing the legend, missing:", check_layout(layout))

The script removes the legend from the layout, as if the template had been edited, and runs the check again. Here is everything it printed:

Required layout items are present.
PDF export status: Success
PNG export status: Success
East.pdf -> ok, 611,366 bytes
East.png -> ok, 347,870 bytes
After removing the legend, missing: ['main_legend']

The last line shows the check doing its job: ['main_legend'] is the item that went missing. In a real script you would run the check before every export.

What a script can check, and what it cannot

Kind of checkCan a script do it?Examples
StructureUsuallyThe required items exist, the layer is valid, the template loaded
FilesUsuallyThe file exists, it is not empty, its size is plausible
ContentPartlyThe title mentions the right area, the extent contains the feature
AppearanceHardlyText is not clipped, the legend is readable, the balance is good
MeaningNoThe classification is sensible, the sources are right, the map answers the question

A script is good at the first rows and poor at the last. So the sequence is always the same: let the script check what it can, and then look at the maps. Look at the first map of a batch, any map that is unusual, and every map after you change the template. The export and final checks note explains why, and the map quality checklist gives you a list to go through.

The checklist for publication:

  • The title and the date match the data on the map.
  • The extent includes the whole area of interest.
  • The right layers and styles are shown.
  • The legend explains the symbols that are used.
  • The scale bar is linked to the right map.
  • Nothing is clipped: title, labels, legend and source note.
  • The page size and the export settings suit the use.
  • The sources and the dates are documented.
  • The PDF or image opens, and a person has looked at it.

When it breaks

What you seeWhyWhat to do
ValueError: Missing required layout items: main_legendThe check found a missing itemFix the template, or the ID, then run again
East.pdf -> East.pdf is emptyThe export wrote an empty fileCheck the layout, then export again
PDF export status: 3FileError: QGIS could not write the fileCheck the folder and permissions, and close the file if it is open
The PNG is blurryThe dpi is too lowUse 300 for print
The PDF is much larger than expectedA large raster or a very high dpiCheck the layers, and the settings

Make it your own

  1. Add main_scale_bar and map_credits to REQUIRED_IDS. Does the layout from the template pass?
  2. Export the PNG at 300 dpi and compare its size with the 150 dpi one.
  3. Add a check that the title text is not empty: layout.itemById("map_title").text().
  4. Write the results of each check to a small CSV file. Lesson 7 shows how.

Common mistakes

  • Trusting the status code and never opening the files.
  • Checking after the export and not before it.
  • Stopping at the first problem and hiding the others.
  • Using the highest dpi for everything.
  • Believing that a script can check that a map is good.

Key takeaways

Next: A complete automation pipeline.