Add Pictures and Logos to a Print Layout in PyQGIS

Most map layouts carry images besides the map: an organisation's logo, a north arrow, a site photograph, a chart, a QR code linking to the online version. The picture item displays any SVG or raster image at a position and size on the page, scales it in one of several ways, and can switch images per atlas page through an expression. Placed from Python, pictures become part of an automated layout like any other item.

This recipe belongs to Automated Map Layout Generation. It adds a logo with the right resize mode, chooses between SVG and raster images, recolours SVG symbols through parameters, adds a north arrow linked to the map's rotation, switches pictures per atlas page, and keeps image paths working on other machines.

Resize modesFour resize modes for a picture in a fixed frame. Zoom scales the image to fit inside the frame keeping its aspect ratio. Stretch fills the frame exactly, distorting the image. Clip shows the image at its natural size, cut by the frame. Zoom and resize frame scales the image and shrinks the frame to match. Zoom is right for logos and photos.How the image meets its frameZoomfit insidekeep aspectlogos, photosStretchfill exactlydistortsrarely rightClipnatural sizecut by framecropsZoomResizeFramefit, thenshrink frametidy layouts

Prerequisites

  • QGIS 3.34 LTR or newer, or the QGIS 4 series.
  • Image files — SVG for logos and symbols wherever possible, PNG or JPEG for photographs.
  • A layout to add to, built in code or loaded from a template.

A logo needs a path, a position, a size and a resize mode that keeps its proportions. The anchor point decides which corner stays fixed when the image is fitted into the frame.

from qgis.core import (QgsProject, QgsLayoutItemPicture, QgsLayoutItem, QgsLayoutPoint,
                       QgsLayoutSize, QgsUnitTypes)

layout = QgsProject.instance().layoutManager().layoutByName("District report")

logo = QgsLayoutItemPicture(layout)
logo.setId("logo")
logo.setPicturePath("/data/branding/city_logo.svg")
logo.setResizeMode(QgsLayoutItemPicture.Zoom)
logo.setPictureAnchor(QgsLayoutItem.UpperRight)
logo.attemptMove(QgsLayoutPoint(250, 8, QgsUnitTypes.LayoutMillimeters))
logo.attemptResize(QgsLayoutSize(38, 16, QgsUnitTypes.LayoutMillimeters))
layout.addLayoutItem(logo)

Breakdown: Zoom scales the image to fit inside the 38 × 16 mm frame without distortion; whichever dimension does not fill leaves empty space, and the anchor decides where — UpperRight keeps the logo tight against the top-right corner, matching a right-aligned title block. SVG logos stay crisp at any print size and keep file sizes small. Give the item an id so templates and scripts can find and update it.

SVG or raster?

The format of the image file matters for quality, file size and what you can change from QGIS.

SVG versus raster imagesSVG images are vector: crisp at any size, small files, and SVGs with QGIS parameters can be recoloured from the layout. Raster images such as PNG and JPEG have a fixed number of pixels: fine for photographs, but they blur when enlarged and large ones inflate PDFs. Use SVG for logos, arrows and symbols, raster for photos, and size raster images for their printed size at about 300 dpi.Vector for marks, raster for photosSVGcrisp at any sizesmall filesrecolour via parametersPNG / JPEGphotos, scansblur when enlargedsize for ~300 dpi

For raster images, the rule of thumb is pixels = printed size in inches × 300. A photo printed 60 mm wide needs about 700 pixels across; a 4,000-pixel camera original adds megabytes to every PDF for no visible gain, so downscale first. Check effective resolution before export:

from qgis.PyQt.QtGui import QImage

photo_path = "/data/photos/site_014.jpg"
img = QImage(photo_path)
frame_mm = 60
dpi = img.width() / (frame_mm / 25.4)
print(f"{img.width()} px over {frame_mm} mm ≈ {dpi:.0f} dpi")
if dpi > 450:
    small = img.scaledToWidth(int(frame_mm / 25.4 * 300))
    small.save("/data/photos/site_014_print.jpg", quality=88)

Breakdown: Effective dpi is image pixels divided by the printed width in inches. Below about 150 dpi a photo looks soft in print; far above 300 it only adds file size. Downscaling to 300 dpi at the printed width and saving at a reasonable JPEG quality keeps PDFs small without visible loss.

Recolour SVG symbols

SVG files prepared with QGIS parameters — fill="param(fill)" and similar — can be recoloured from the picture item, so one north arrow or logo variant serves light and dark layouts.

from qgis.PyQt.QtGui import QColor

arrow_svg = "/usr/share/qgis/svg/arrows/NorthArrow_11.svg"
north = QgsLayoutItemPicture(layout)
north.setId("north_arrow")
north.setPicturePath(arrow_svg)
north.setSvgFillColor(QColor("#17211d"))
north.setSvgStrokeColor(QColor("#17211d"))
north.setSvgStrokeWidth(0.2)
north.setResizeMode(QgsLayoutItemPicture.Zoom)
north.attemptMove(QgsLayoutPoint(262, 170, QgsUnitTypes.LayoutMillimeters))
north.attemptResize(QgsLayoutSize(14, 18, QgsUnitTypes.LayoutMillimeters))
layout.addLayoutItem(north)

Breakdown: QGIS's bundled SVGs, including the north arrows, use parameters, so fill and stroke colours set on the item replace the defaults. The bundled SVG folder differs by platform — find it with QgsApplication.svgPaths() rather than hard-coding a Linux path. SVGs without parameters ignore these settings; adding param(fill) placeholders to an organisation's own logo makes it recolourable in the same way.

A north arrow should point to grid north as the map shows it. Linking it to the map item makes it follow map rotation automatically.

map_item = layout.itemById("main_map")
north.setLinkedMap(map_item)
north.setNorthMode(QgsLayoutItemPicture.GridNorth)
north.setNorthOffset(0)
map_item.setMapRotation(12)            # e.g. a map rotated to fit a long valley
north.refreshPicture()
print("arrow rotation:", round(north.pictureRotation(), 1))

Breakdown: With a linked map, the picture's rotation follows the map's rotation, so a map turned 12 degrees gets an arrow turned to match. GridNorth points up the map's grid; TrueNorth also corrects for the difference between grid north and true north, which matters in projections where meridians converge noticeably. Most layouts never rotate the map, but linking costs nothing and keeps the arrow correct if someone later does. Scale bars and north arrows are covered for the map canvas in adding a scale bar and north arrow.

Position pictures relative to the page and map

Hard-coded coordinates break when the page size or map frame changes. Computing positions from the page and the map item keeps pictures in the right place on A4 and A3 alike.

page = layout.pageCollection().page(0)
pw, ph = page.pageSize().width(), page.pageSize().height()
margin = 8

logo.attemptResize(QgsLayoutSize(38, 16, QgsUnitTypes.LayoutMillimeters))
logo.attemptMove(QgsLayoutPoint(pw - margin - 38, margin, QgsUnitTypes.LayoutMillimeters))

m = map_item.rectWithFrame()
north.attemptMove(QgsLayoutPoint(m.right() - 18, m.bottom() - 24, QgsUnitTypes.LayoutMillimeters))

Breakdown: Placing the logo from the page width and a margin keeps it in the top-right corner for any page size. Positioning the north arrow from the map item's frame puts it inside the map's lower-right corner wherever the map sits. Layout coordinates are in layout units — millimetres by default — and the map rectangle is in the same units, so the arithmetic is direct. Templates designed for one size can then be reused for another by changing only the page size before positioning.

Switch pictures per atlas page

In an atlas, each page can show its own photo, plan or chart. A data-defined picture source builds the path from the current feature.

A different picture on every pageThe picture item's source is data-defined by an expression combining a photo folder with the current atlas feature's photo field. On each atlas page the expression is evaluated for that page's feature and the matching image is loaded. Features without a photo can fall back to a placeholder image through coalesce.Path from the feature, image per pageatlas featuresite_id, photopicture source'/photos/' ||coalesce("photo", …)page imageor placeholder

from qgis.core import QgsLayoutObject, QgsProperty

photo = QgsLayoutItemPicture(layout)
photo.setId("site_photo")
photo.setResizeMode(QgsLayoutItemPicture.Zoom)
photo.attemptMove(QgsLayoutPoint(200, 30, QgsUnitTypes.LayoutMillimeters))
photo.attemptResize(QgsLayoutSize(80, 60, QgsUnitTypes.LayoutMillimeters))
photo.dataDefinedProperties().setProperty(
    QgsLayoutObject.PictureSource,
    QgsProperty.fromExpression(
        "'/data/photos/' || coalesce(attribute(@atlas_feature, 'photo'), 'no_photo.png')"))
layout.addLayoutItem(photo)

Breakdown: The expression joins a folder with the feature's photo file name; coalesce substitutes a placeholder image for features without one, so pages never show a broken picture. The same mechanism displays a chart generated per feature, as in adding a chart to a print layout. Store only file names in the field and build the folder in the expression or a project variable, so moving the photo folder means changing one value.

Keep picture paths portable

Absolute paths break when a project moves to another machine or a colleague opens it from a different drive. Two techniques keep pictures working.

from qgis.core import QgsExpressionContextUtils

project = QgsProject.instance()
QgsExpressionContextUtils.setProjectVariable(project, "assets_dir", "/data/branding")
logo.dataDefinedProperties().setProperty(
    QgsLayoutObject.PictureSource,
    QgsProperty.fromExpression("@assets_dir || '/city_logo.svg'"))

for item in layout.items():
    if isinstance(item, QgsLayoutItemPicture):
        print(item.id() or "(no id)", item.picturePath())

Breakdown: A project variable holding the asset folder, used in data-defined picture sources, means one change relocates every picture in every layout. Alternatively, keep images in a folder next to the project and save the project with relative paths, which QGIS applies to picture items too. Listing every picture item with its resolved path is a quick audit before sending a project to someone else. For images that must never go missing, embedding them in the project (base64: picture sources on recent releases) is also possible, at the cost of a larger project file.

QGIS version compatibility

QgsLayoutItemPicture, resize modes, SVG parameter colours, north arrow linking and data-defined picture sources work on QGIS 3.34 LTR, 3.40 LTR and QGIS 4. On QGIS 4, enums are scoped: QgsLayoutItemPicture.ResizeMode.Zoom, QgsLayoutItemPicture.NorthMode.GridNorth, QgsLayoutItem.ReferencePoint.UpperRight; units use Qgis.LayoutUnit.Millimeters.

Troubleshooting

  • The logo is distorted. The resize mode is Stretch; use Zoom.
  • SVG colours do not change. The SVG has no param() placeholders.
  • Pictures are missing on another machine. Paths are absolute; use a project variable or relative paths.
  • PDFs are huge. Full-resolution photos were placed; downscale to about 300 dpi at printed size.

Conclusion

Place logos as SVG pictures with Zoom and a sensible anchor, size raster photos for about 300 dpi at their printed size, recolour parameterised SVGs from the item, link north arrows to the map, build per-page picture paths with data-defined expressions and placeholders, and keep paths portable with project variables.

Frequently Asked Questions

Can I add a QR code? Generate it as an SVG with a Python library such as segno and place it as a picture, or use a data-defined source to create one per atlas page.

Can a picture be a web URL? Yes; QGIS downloads remote images, but local files are faster and reliable in offline exports.

How do I put a picture behind the map? Change stacking order with layout.lowerItem or moveItemToBottom.

Do pictures appear in SVG exports as vectors? SVG pictures stay vector; raster pictures are embedded as images.