Use SVG and Font Markers in PyQGIS

Sooner or later a point layer needs to be something other than a circle: a hospital cross, a wind turbine, a numbered pin, a directional chevron. QGIS offers three routes — an SVG file, a glyph from an installed font, or a raster image — and the choice affects portability, recolouring and how fast the map draws far more than it affects how the marker looks.

This recipe belongs to Symbol Layers & Advanced Symbology in PyQGIS. It covers creating each marker type from Python, making the path survive a move to another machine, recolouring an SVG per feature, and choosing between the three when performance matters.

Three marker sources comparedAn SVG marker reads a file from the QGIS search path and can be recoloured per feature if it uses the param convention. A font marker draws one glyph from an installed font, is cached and therefore fast, and recolours freely. A raster marker places a bitmap, cannot be recoloured, and is the only option when the artwork is a photograph.What the marker is made of decides what you can do with itSvgMarkera file on diskrecolours if it usesparam(fill)re-rasterised persize and colourFontMarkerone glyph from a fontrecolours freelyfont must be installedglyph cache — fastestRasterMarkera bitmapcannot be recolouredblurs when scaled uponly for photographsall three are marker symbol layers and stack with everything else

Prerequisites

  • QGIS 3.34 LTR (bundled Python 3.12) or newer.
  • A point layer in the project; the examples use one called facilities.
  • For SVG markers, either an SVG in the QGIS SVG search paths or a file you can reference. For font markers, the font installed on every machine that will open the project.

Place an SVG marker

QgsSvgMarkerSymbolLayer takes a path and a size, and every other property is optional.

from qgis.core import QgsProject, QgsMarkerSymbol, QgsSvgMarkerSymbolLayer
from qgis.PyQt.QtGui import QColor

layer = QgsProject.instance().mapLayersByName("facilities")[0]

marker = QgsSvgMarkerSymbolLayer("gpsicons/hospital.svg")
marker.setSize(6.0)
marker.setFillColor(QColor("#b91c1c"))
marker.setStrokeColor(QColor("#7f1d1d"))
marker.setStrokeWidth(0.3)

symbol = QgsMarkerSymbol()
symbol.deleteSymbolLayer(0)
symbol.appendSymbolLayer(marker)
layer.renderer().setSymbol(symbol)
layer.triggerRepaint()

Breakdown: The path given here is relative, and that is deliberate — QGIS resolves relative SVG paths against its SVG search paths, so gpsicons/hospital.svg finds the bundled icon on every installation regardless of where QGIS was installed. An absolute path works on your machine and breaks on everyone else's, which is the single most common reason a shared project shows empty squares where the markers should be. setFillColor() and setStrokeColor() only do anything if the SVG uses the QGIS parameter convention; on a fixed-colour SVG they are silently ignored.

To see which directories QGIS is searching, ask it:

from qgis.core import QgsApplication

for path in QgsApplication.svgPaths():
    print(path)

Breakdown: svgPaths() returns the resolved list — the bundled svg/ directory, anything added under Settings → Options → System, and the project's own directory when the project has been saved. Dropping your organisation's icon set into a folder listed here, and referencing it relatively, is what makes a style portable across a team.

Make an SVG recolourable

An SVG only accepts colours from QGIS if its fill and stroke attributes are written as parameters.

<path d="M12 2 L22 22 L2 22 Z"
      fill="param(fill) #cccccc"
      stroke="param(outline) #333333"
      stroke-width="param(outline-width) 1" />

Breakdown: That element sits inside an ordinary SVG root with a square viewBox. The param(name) fallback syntax is QGIS-specific: the parameter is used when QGIS supplies one, and the literal after it is used by any other SVG renderer, so the file still opens correctly in a browser or Inkscape. The parameter names fill, outline and outline-width are the ones wired to setFillColor(), setStrokeColor() and setStrokeWidth(); arbitrary names are exposed through setParameters() instead and are how a marker can take, say, a per-feature label colour.

With that in place, a data-defined colour becomes possible:

from qgis.core import QgsProperty, QgsSymbolLayer

marker.setDataDefinedProperty(
    QgsSymbolLayer.PropertyFillColor,
    QgsProperty.fromExpression(
        "case when \"beds\" > 200 then '#b91c1c' else '#2563eb' end"
    ),
)

Breakdown: This works, and it is also the most expensive thing you can ask an SVG marker to do. Each distinct colour produces a separate rasterisation of the file, and the cache is keyed on the combination of size and colour — so a continuous colour ramp on fifty thousand points effectively disables caching. Where the colour varies continuously, a font marker or a simple marker will draw the same map in a fraction of the time.

The SVG marker render pathA relative path is resolved against the QGIS SVG search paths. The file is parsed once. For each distinct combination of size and colour the parameters are substituted and the artwork is rasterised into a cache entry. Features reusing a combination hit the cache; a per-feature colour produces a new entry each time.The cache is keyed on size and colour togetherrelative pathgpsicons/hospital.svgsearch pathsbundled · user · projectparam substitutionfill · outline · widthraster cacheone entry per combofour fixed classes4 cache entries, reused 50 000 timesdraws instantlycontinuous colour ramp≈50 000 cache entriesthe cache stops being a cache

Font markers, and why they are usually faster

A font marker draws a single character. Because Qt already caches rendered glyphs, changing colour or size costs almost nothing.

from qgis.core import QgsFontMarkerSymbolLayer
from qgis.PyQt.QtGui import QColor

marker = QgsFontMarkerSymbolLayer("Noto Sans Symbols 2")
marker.setCharacter("▲")           # black up-pointing triangle
marker.setSize(5.0)
marker.setColor(QColor("#0f766e"))
marker.setAngle(0)

Breakdown: setCharacter() takes the character itself, not a code point number, so a Python escape or a literal both work. The font family string must match what Qt reports, and Qt silently substitutes a default when the family is missing — which is why a project that looked right on the author's machine shows the wrong glyph elsewhere. Guard against that by checking availability before applying the style:

from qgis.PyQt.QtGui import QFontDatabase

if "Noto Sans Symbols 2" not in QFontDatabase().families():
    raise RuntimeError("required marker font is not installed")

Breakdown: Failing loudly at style time is much better than shipping a map where every wind turbine is the letter that Qt happened to substitute. In a plugin, a check like this belongs next to the other startup validation rather than inside the render path.

Anchors, offsets and pins

A circle is symmetric, so it does not matter where QGIS considers its centre. A pin does — the point of the pin has to land on the coordinate, not the middle of its head.

from qgis.core import QgsMarkerSymbolLayer
from qgis.PyQt.QtCore import QPointF

marker.setVerticalAnchorPoint(QgsMarkerSymbolLayer.Bottom)
marker.setHorizontalAnchorPoint(QgsMarkerSymbolLayer.HCenter)
marker.setOffset(QPointF(0, -0.4))

Breakdown: The anchor decides which part of the artwork is placed on the coordinate: Bottom plus HCenter puts the bottom-centre of the marker on the point, which is exactly what a map pin, a tree symbol or a flag wants. The offset is applied afterwards in the layer's output unit and is the right tool for nudging a marker clear of a line it sits on, or for building the offset copy that acts as a drop shadow in a two-layer stack.

Anchoring and offsetting interact with rotation in a way worth knowing: rotation happens about the anchor point, so a chevron anchored at its base rotates like a weather vane, while the same chevron anchored at its centre spins in place. When a rotated marker drifts as the angle changes, the anchor is almost always the reason.

Raster markers, and when they are the right answer

QgsRasterMarkerSymbolLayer places a PNG or JPEG at each point. It cannot be recoloured and it softens when scaled beyond its native resolution, so it is a poor general-purpose marker — but for a layer of photographs, a set of national flags or a client's logo with gradients that must not be redrawn, it is the only faithful option.

from qgis.core import QgsRasterMarkerSymbolLayer

marker = QgsRasterMarkerSymbolLayer("/data/icons/turbine_128.png")
marker.setSize(7.0)
marker.setOpacity(0.9)

Breakdown: Size is again in the output unit, and the image is scaled to fit it, so supply artwork at roughly twice the largest size you will print at and no more — an oversized PNG per point is memory the renderer holds for no visible gain. setOpacity() here is per symbol layer, distinct from the whole-layer opacity discussed in opacity and blend modes, and it is the one to use when only the icon should fade while its casing stays solid.

Which one to choose

Use a simple marker when the shape is a shape — circle, square, triangle, star are all built in and cost nothing. Use a font marker when you need a symbol library and control over colour, and you can guarantee the font. Use an SVG marker when the artwork is genuinely bespoke, when it must match a house style, or when it needs the two-colour treatment that param(fill) plus param(outline) gives. Reach for a raster marker only when the artwork is a photograph or a logo whose gradients will not survive being redrawn.

QGIS version compatibility

The examples target QGIS 3.34 LTR (Python 3.12).

QGIS versionPythonNotes
3.16 LTR3.7param() convention and all three marker classes present.
3.22 LTR3.9QgsSvgMarkerSymbolLayer.setParameters() accepts arbitrary named parameters.
3.28 LTR3.9Remote SVG paths (http) supported with a network cache.
3.34 LTR3.12Baseline for this page.
3.40+3.12QgsApplication.svgPaths() unchanged; SVG cache sizing exposed in settings.

Troubleshooting

  • Markers render as empty squares. The SVG path did not resolve. Print QgsApplication.svgPaths() and use a path relative to one of them.
  • setFillColor() does nothing. The SVG has literal colours. Rewrite its fill as param(fill) #cccccc.
  • The SVG is black. Same cause — no parameters, so QGIS draws the file's own colours, and a file authored with fill="#000000" stays black.
  • The wrong glyph appears on another machine. The font is not installed there and Qt substituted one. Check QFontDatabase().families() before applying the style.
  • The canvas crawls after switching to SVG markers. A data-defined colour or size is defeating the raster cache. Reduce to a handful of discrete classes, or switch to a font marker.
  • The marker is offset from the point. The SVG's viewBox is not centred on the artwork. Either fix the file or compensate with setOffset(QPointF(x, y)).

Conclusion

Reference SVGs by a path relative to the QGIS search paths so styles travel, write the artwork with param(fill) if it must be recoloured, and check the font exists before relying on a font marker. When a marker's colour or size varies per feature, remember that SVG markers pay for it in rasterisation and font markers do not.

Frequently Asked Questions

Can I load an SVG marker from a URL? Yes — QGIS 3.28 and newer accept an http or https path and cache the fetched file. It makes styles portable without shipping icons, at the cost of a network dependency at first render, which is a poor trade for anything that must run headless on a schedule.

How do I use my own parameter names in an SVG? Write them as param(mycolour) in the file and set them with marker.setParameters({"mycolour": QgsProperty.fromExpression("...")}). The values are QgsProperty objects, so each one can be static or data-defined.

Why does the marker size not match the SVG's own dimensions?setSize() is the size of the rendered marker in the layer's output unit, and the SVG's viewBox is scaled to fit it. The file's internal units are irrelevant, which is why a well-made marker SVG uses a square viewBox.

Can a marker rotate to follow a bearing field? Yes. Set a data-defined QgsSymbolLayer.PropertyAngle from the field. Combined with a triangle glyph this turns a point layer into a flow map without touching the geometry.