Export the Map Canvas to a PNG with PyQGIS
Saving what is on screen looks like a one-liner, and for a genuine screenshot it is. The trouble starts the moment a script changes the extent, adds a layer or toggles visibility and then immediately grabs the image — because the canvas renders asynchronously, what lands in the file is the previous frame. The result is an export that is correct on a slow machine and wrong on a fast one, or the reverse, which makes it maddening to debug.
This page is a focused recipe within Map Canvas Control and Image Export in PyQGIS. It covers the direct canvas save, waiting for the render to complete, and when to abandon the canvas entirely in favour of a render job that produces the same image at any resolution you ask for.
Prerequisites
- QGIS 3.34 LTR (bundled Python 3.12) or newer, running as a desktop application —
saveAsImage()needs a real canvas. - A project with visible layers, and write permission to the output directory.
- For the render-job alternative, no GUI is needed at all; see Render a Layer to an Image Headless in PyQGIS.
The direct save
When the canvas already shows what you want, one call is enough.
canvas = iface.mapCanvas()
canvas.saveAsImage("/data/exports/current_view.png")
Breakdown: The image is written at exactly the canvas widget's pixel size, so the resolution depends on the user's window and screen. A .pgw world file is written alongside it automatically, which means the PNG can be dropped back into QGIS or any other GIS as a georeferenced raster. The format is inferred from the extension — .jpg and .tif both work, though PNG is the right default for map output because it is lossless and handles flat colour well.
The world file is what makes the result more than a picture. It records the pixel size and the coordinates of the top-left corner, so any GIS can place the image back on the map at the right scale and position.
The y scale is negative because image rows run downward while map coordinates run upward — a sign error there is the usual reason a re-imported export appears mirrored. The file carries no CRS of its own, so a .prj alongside it is worth writing when the image will travel.
Wait for the render to finish
As soon as the script changes anything, the save has to wait. The canvas emits renderComplete when it has finished, and connecting to it once is the reliable pattern.
from qgis.core import QgsProject
canvas = iface.mapCanvas()
layer = QgsProject.instance().mapLayersByName("parcels")[0]
def save_once(painter):
canvas.renderComplete.disconnect(save_once)
canvas.saveAsImage("/data/exports/parcels_view.png")
print("written")
extent = layer.extent()
extent.scale(1.1)
canvas.setExtent(extent)
canvas.renderComplete.connect(save_once)
canvas.refreshAllLayers()
Breakdown: renderComplete passes a QPainter to its handlers, which is why save_once takes an argument it never uses. Disconnecting inside the handler is what stops the save firing on every subsequent pan and zoom for the rest of the session — a leaked connection here is a classic plugin bug. refreshAllLayers() rather than refresh() forces the layers to re-read from their providers, which matters when the data changed rather than just the view. Connecting after setting the extent avoids catching a repaint triggered by something else.
This is inherently asynchronous, so the script continues past refreshAllLayers() immediately. Anything that must happen after the file exists belongs inside save_once, not below the connect call — a distinction that trips up scripts written as a linear sequence.
Control the output size
Because saveAsImage() follows the widget, the only way to control resolution through the canvas is to resize the widget itself. That works and is ugly:
from qgis.PyQt.QtCore import QSize
canvas = iface.mapCanvas()
original = canvas.size()
canvas.resize(QSize(1600, 1200))
canvas.refresh()
# ... wait for renderComplete, then save ...
canvas.resize(original)
Breakdown: Resizing the canvas visibly disturbs the user's layout and forces two extra renders. It also cannot exceed the screen size in a meaningful way. Restoring the original size afterwards is essential but does not make the approach pleasant. This is the point at which the canvas stops being the right tool.
Use a render job instead
For any output whose size, DPI or background must be predictable, build the settings yourself. Nothing about this depends on the canvas, so the same code runs headless.
from qgis.core import QgsMapSettings, QgsMapRendererParallelJob, QgsProject
from qgis.PyQt.QtCore import QSize
from qgis.PyQt.QtGui import QColor
canvas = iface.mapCanvas()
settings = QgsMapSettings(canvas.mapSettings()) # start from what is on screen
settings.setOutputSize(QSize(1920, 1080))
settings.setOutputDpi(150)
settings.setBackgroundColor(QColor("#ffffff"))
extent = canvas.extent()
width, height = 1920, int(1920 * extent.height() / extent.width())
settings.setOutputSize(QSize(width, height))
settings.setExtent(extent)
job = QgsMapRendererParallelJob(settings)
job.start()
job.waitForFinished()
job.renderedImage().save("/data/exports/map.png", "png")
Breakdown: Copy-constructing QgsMapSettings from canvas.mapSettings() inherits the current layers, CRS and flags — including DrawLabeling, which is the usual reason a hand-built settings object loses its labels. Deriving the height from the extent's proportions stops QGIS silently expanding the extent to match a mismatched aspect ratio. waitForFinished() blocks until the render is genuinely complete, which is what removes the asynchrony problem entirely rather than working around it.
QGIS version compatibility
The examples target QGIS 3.34 LTR (Python 3.12).
| QGIS version | Python | Notes |
|---|---|---|
| 3.28 LTR | 3.9 | Identical API. saveAsImage() writes a world file in all 3.x. |
| 3.34 LTR | 3.12 | Baseline for this page. |
| 3.40 / 3.44 | 3.12 | Qgis.MapSettingsFlag replaces QgsMapSettings.Flag; the old spelling still resolves. |
saveAsImage(), renderComplete and QgsMapRendererParallelJob are unchanged across the 3.x line.
Troubleshooting
- The image shows the previous extent. The save ran before rendering finished. Connect to
renderComplete, or switch to a render job withwaitForFinished(). - The file is written but blank. The canvas had no visible layers, or the render job's
setLayers()list was empty. Print the layer count before rendering. - The background is transparent. A hand-built
QgsMapSettingsdefaults to transparent. CallsetBackgroundColor(). - Labels are missing. The settings object does not have
DrawLabelingset. Copy-construct fromcanvas.mapSettings()or set the flag explicitly. - The image resolution is too low.
saveAsImage()follows the widget size. Use a render job and setsetOutputSize(). - The export keeps firing on every pan. The
renderCompletehandler was never disconnected. Disconnect inside the handler.
Conclusion
canvas.saveAsImage() is right for a screenshot of what the user is already looking at. The moment a script changes the view, either wait for renderComplete or — better — build a QgsMapSettings and render it synchronously, which gives you control over size, DPI and background and produces the same file on every run.
Frequently Asked Questions
Why does my exported image show the wrong extent?
The canvas renders asynchronously, so a save called immediately after setExtent() captures the frame still on screen. Connect to the canvas's renderComplete signal and save from the handler, or use a render job and call waitForFinished().
How do I control the exported image size?
Not through saveAsImage(), which follows the canvas widget. Build a QgsMapSettings, call setOutputSize() and setOutputDpi(), and render it with QgsMapRendererParallelJob.
Does saveAsImage produce a georeferenced file?
Yes. It writes a world file — .pgw for a PNG — next to the image, so the result can be loaded back as a georeferenced raster.
Why is my exported map background transparent?
A QgsMapSettings built from scratch defaults to a transparent background. Call setBackgroundColor(QColor("#ffffff")), or copy-construct the settings from the canvas so the project background is inherited.
Can I export the canvas at a higher resolution without resizing the window?
Not through saveAsImage(), which is tied to the widget's pixel dimensions. Copy the canvas's map settings into a fresh QgsMapSettings, raise setOutputSize() and setOutputDpi() on the copy, and render that with a job. The result matches what is on screen but at whatever resolution you asked for.
What image formats can I write? Anything Qt's image writer supports — PNG, JPEG, TIFF and BMP among them. PNG is the right default for maps because it is lossless and handles large areas of flat colour efficiently. JPEG introduces visible artefacts around labels and thin lines, and cannot carry transparency at all.
Is there a size limit on a rendered image? Practically, yes: the image is held in memory uncompressed, so a 20 000 by 20 000 pixel render needs well over a gigabyte before it is written. For very large outputs, render in tiles and stitch them, or export from a layout to a format that streams.
Does the export include layers that are switched off?
No. canvas.layers() returns only the visible layers, so anything unticked in the layer panel is absent. Build the layer list explicitly from the project if the export should not depend on the panel state.