SEC-IMS
Style Guide: Exporting QGIS Styles to OpenLayers via SLDReader
A practical guide for authoring styles in QGIS so they survive export to SLD and render correctly in OpenLayers using NieuwlandGeo/SLDReader.
1. The pipeline and its constraints
QGIS Symbology/Labeling → SLD 1.0/1.1 export → SLDReader parse → createOlStyleFunction → OpenLayers canvas
Your style must survive three lossy steps:
- QGIS → SLD: QGIS can express far more than SLD can (draw effects, blending, shapeburst fills, geometry generators, callouts, data-defined overrides). Anything without an SLD equivalent is silently dropped or exported as a QGIS-specific
ogc:Functionthat nothing else understands. - SLD → SLDReader: SLDReader reads SLD 1.0 and 1.1, but implements a subset of the spec (documented in its README “Restrictions” section).
- SLDReader → OpenLayers: OL’s canvas renderer has its own limits (e.g., no fractional text anchors, displacement applied before rotation, no built-in label collision engine equivalent to QGIS’s).
Golden rule: author in QGIS using only the intersection of all three. Design “SLD-first” — if you can’t express it in hand-written SLD, don’t build it in QGIS.
2. Recommended QGIS setup
- Use QGIS 3.28 LTR or newer — SLD export (especially label export) improved substantially across the 3.x series.
- Export via Layer Properties → Symbology → Style ▾ → Save Style… → As SLD style file. This includes both symbology and (simple) labeling.
- Keep renderers to: Single Symbol, Categorized, Graduated, or Rule-based. All four export cleanly to SLD
<Rule>elements with<ogc:Filter>s. Avoid heatmap, point cluster, point displacement, 2.5D, and inverted polygon renderers. - Always open the exported SLD in a text editor and review it. Treat the export as a draft, not a final artifact. Search for:
ogc:Function— QGIS-specific functions SLDReader won’t know (it supports only a handful of built-ins, though you can register your own viaregisterFunction).ExternalGraphic/OnlineResourcehrefs pointing to local file paths.uom=attributes (map-unit sizes) you didn’t intend.- Empty or suspicious
<se:Name>/ parameter values.
3. Symbology rules (points, lines, polygons)
Units and sizes
- Set all sizes in pixels in QGIS (symbol size, stroke width, font size). Millimetres get converted on export (1 mm ≈ 3.78 px at 96 dpi), which works but makes the SLD harder to reason about and pixel-perfect matching harder.
- Avoid “map units” / metres-at-scale unless you specifically want ground-unit sizing. SLDReader does support
uom="…metre", but for correct results in Web Mercator you must wire up theconvertResolutionoption withgetPointResolution()so sizes are right at your latitude:
vectorLayer.setStyle(SLDReader.createOlStyleFunction(featureTypeStyle, {
convertResolution: viewResolution => {
const center = map.getView().getCenter();
return getPointResolution(map.getView().getProjection(), viewResolution, center);
},
imageLoadedCallback: () => vectorLayer.changed(), // re-render when ExternalGraphic icons load
}));Point symbols
- Use Simple Marker with one of:
circle, square, triangle, star, cross, x, hexagon, octagon(the well-known names SLDReader implements; it also supports thehorlineandbackslashcustom marks). - Avoid Font Markers — they export as
ttf://Font#codewell-known names, which SLDReader explicitly does not support. - SVG markers export as
ExternalGraphic. The exported href is usually a local file path — you must edit it to a web-accessible URL (host the SVG/PNG alongside your app). PNG is the safer bet; test SVG icons carefully. - One
GraphicperPointSymbolizer, oneMark/ExternalGraphicperGraphic. If you need stacked markers, use multiple symbol layers in QGIS — they export as multiplePointSymbolizers in the same rule, which SLDReader handles. - Size and rotation can be data-driven via a plain field (
PropertyName) — that survives. Complex data-defined expressions do not.
Lines
- Simple stroke: color, width, opacity, dash pattern (
stroke-dasharray), line cap/join all export and render. - Marker lines (symbols along a line) export as
GraphicStroke. SLDReader supports this, but you need OpenLayers ≥ 8.3 to avoid visual artifacts, and ≥ 10.8 for perpendicular stroke offsets. Keep marker-line symbols to the supported well-known marks. - Avoid arrow symbol layers, interpolated lines, and draw effects.
Polygons
- Simple fill + outline: fully supported (fill, fill-opacity, stroke params).
- Hatching: use a Line Pattern Fill restricted to horizontal or one diagonal direction, or hand-edit the SLD to use a
GraphicFillwith markhorlineorbackslash(usebackslashfor diagonal hashing — it tiles cleanly). Marks as polygon fill are supported by SLDReader. - Avoid: shapeburst, gradient fills, random marker fill, point-pattern fills with exotic spacing, raster image fills (unless you post-edit to an
ExternalGraphicfill with a hosted image).
Things that never survive — don’t use them
Blending modes, layer/draw effects (shadows, glow), geometry generators, symbol-level opacity tricks, live layer effects, paint effects, and most data-defined overrides. If you need a property-driven style, model it as Categorized/Rule-based classes (which become discrete SLD rules with filters) instead of a data-defined override.
Scale-dependent rendering
- Rule-based renderer scale ranges export as
MinScaleDenominator/MaxScaleDenominatorand SLDReader honors them. - SLDReader converts OL resolution → scale denominator assuming ~96 dpi. In EPSG:3857 at higher latitudes, use the
convertResolutionhook (above) so QGIS scale ranges and browser scale ranges line up.
4. Labels — making QGIS labels work in SLDReader
Labels are the most fragile part of the export. QGIS’s labeling engine (PAL) is dramatically more capable than SLD’s TextSymbolizer, and SLDReader/OL add their own constraints.
4.1 What survives the trip
| QGIS labeling feature | SLD element | SLDReader/OL support |
|---|---|---|
| Label with a single field | <Label><ogc:PropertyName> |
✅ Yes (dynamic labels supported) |
| Font family / size / bold / italic | <Font> CssParameters |
✅ (font must exist in the browser) |
| Text color, opacity | fill / fill-opacity |
✅ |
| Buffer (halo) | <Halo> radius + fill |
✅ |
| Offset from point (X/Y) | PointPlacement > Displacement |
✅ (see rotation caveat) |
| Rotation (fixed) | PointPlacement > Rotation |
✅ ⚠ OL applies displacement before rotation, opposite of the SLD spec — avoid combining the two |
| Anchor / quadrant | AnchorPoint (0–1) |
⚠ Snapped to left/center/right & top/middle/bottom — OL has no fractional anchors |
| Labels along lines / curved | <LinePlacement> |
✅ rendered as OL placement: 'line'; PerpendicularOffset is NOT supported |
| Scale-based label visibility | Min/MaxScaleDenominator | ✅ (use rule-based labeling in QGIS) |
4.2 What does NOT survive — avoid in QGIS
- Complex label expressions.
concat("name", ' (', "type", ')')style expressions export as QGISogc:Functioncalls that SLDReader doesn’t know. Workarounds: (a) precompute the label into a real attribute or virtual field and label with that single field; or (b) register a custom function in SLDReader withSLDReader.registerFunction(name, fn)matching what QGIS emitted. - Background shapes, shadows, callouts/leader lines — no SLD equivalent; dropped.
- Data-defined overrides on any label property — dropped or exported as unknown functions.
- Multiline / wrap-on-character, letter spacing, blend modes, kerning tweaks — dropped (GeoServer
VendorOptions are not interpreted by SLDReader). - Curved placement fine-tuning (max angle, repeat distance, overrun) — collapses to plain
LinePlacement. - Collision avoidance / priority — QGIS’s PAL engine doesn’t export. In OL, approximate it with
declutter: trueon the vector layer (and optionallytext-overflowsettings). Expect different label density than in QGIS.
4.3 Recipe for “safe” QGIS labels
- Single Labels mode, Value = one plain field (create a virtual/precomputed field if you need composition).
- Font: choose a font you will actually load in the browser (web-safe, or a webfont loaded via CSS before the layer renders). Hand-edit the SLD’s
font-familyto add fallbacks:Open Sans, Arial, sans-serif. - Size in pixels, not points or millimetres.
- Buffer: enable QGIS “Draw text buffer” → exports as
Halo; keep radius small (1–2 px) — OL draws halos as a text stroke, so very large radii look chunky. - Placement (points): use Offset from point with an explicit quadrant and X/Y offsets, rather than Around point (cartographic). Remember anchors get snapped to 9 positions in OL.
- Placement (lines): Parallel or Curved both become
LinePlacement; don’t rely on a perpendicular offset — it’s ignored. If you need the label off the line, you currently can’t via SLD here; consider styling labels separately in OL. - Don’t combine rotation + offset in one symbolizer (spec/OL order mismatch).
- Scale ranges: use rule-based labeling with scale visibility per rule → exports clean min/max scale denominators.
- On the OL side, create the layer with
declutter: trueto get basic collision handling.
4.4 A known-good TextSymbolizer to aim for
After export, your label symbolizer should look essentially like this — if it’s much more complicated, something will likely break:
<se:TextSymbolizer>
<se:Label><ogc:PropertyName>name</ogc:PropertyName></se:Label>
<se:Font>
<se:SvgParameter name="font-family">Open Sans</se:SvgParameter>
<se:SvgParameter name="font-size">13</se:SvgParameter>
<se:SvgParameter name="font-weight">bold</se:SvgParameter>
</se:Font>
<se:LabelPlacement>
<se:PointPlacement>
<se:AnchorPoint>
<se:AnchorPointX>0</se:AnchorPointX> <!-- left-aligned -->
<se:AnchorPointY>0.5</se:AnchorPointY> <!-- vertically centered -->
</se:AnchorPoint>
<se:Displacement>
<se:DisplacementX>8</se:DisplacementX>
<se:DisplacementY>0</se:DisplacementY>
</se:Displacement>
</se:PointPlacement>
</se:LabelPlacement>
<se:Halo>
<se:Radius>1.5</se:Radius>
<se:Fill><se:SvgParameter name="fill">#ffffff</se:SvgParameter></se:Fill>
</se:Halo>
<se:Fill><se:SvgParameter name="fill">#232323</se:SvgParameter></se:Fill>
</se:TextSymbolizer>5. OpenLayers integration checklist
- OL versions: ≥ 6.15 for most features; ≥ 8.3 for clean
GraphicStroke; ≥ 10.8 for custom marks (horline/backslash) and perpendicular stroke offsets. - Use
Reader→getLayer/getStyle(match the<se:Name>in the SLD, or pass no name to take the first) →createOlStyleFunction(featureTypeStyle, options). - Pass
imageLoadedCallbackso the layer re-renders whenExternalGraphicicons finish loading. - Pass
convertResolution(point resolution at view center) if you use scale-dependent rules or ground-unit (uom) sizes. - Register custom functions with
SLDReader.registerFunction()if your SLD contains function expressions you can’t eliminate. - Set
declutter: trueon label-bearing vector layers. - Load webfonts before first render (e.g., FontFaceObserver), or labels will draw in a fallback font and stay that way until the layer redraws.
6. Testing & iteration workflow
- Style in QGIS → export SLD → inspect the XML.
- Paste the SLD into the SLDReader playground (in the project’s live demos) with sample features — fastest way to see exactly what SLDReader makes of it.
- Optionally load the same SLD into GeoServer as a sanity check; if GeoServer rejects it, browsers definitely won’t be happier.
- Fix issues at the source in QGIS where possible; keep a small, documented set of post-export hand edits (icon URLs, font fallbacks) — ideally scripted (a few lines of XSLT/Python) so re-exports stay cheap.
- Keep the SLD files in version control next to the app; treat hand-edits as code.
7. Quick reference: do / don’t
Do: single/categorized/graduated/rule-based renderers · simple markers from the supported well-known list · pixel units · single-field labels · text buffers · explicit offsets and anchors · scale ranges via rules · hosted icon URLs · OL declutter.
Don’t: font markers (ttf://) · data-defined overrides · label expressions · shapeburst/gradient fills · draw effects & blending · background shapes/callouts · perpendicular offsets on line labels · rotation+displacement combos · map-unit sizes without convertResolution.