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:

  1. 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:Function that nothing else understands.
  2. SLD → SLDReader: SLDReader reads SLD 1.0 and 1.1, but implements a subset of the spec (documented in its README “Restrictions” section).
  3. 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.


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 the convertResolution option with getPointResolution() 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 the horline and backslash custom marks).
  • Avoid Font Markers — they export as ttf://Font#code well-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 Graphic per PointSymbolizer, one Mark/ExternalGraphic per Graphic. If you need stacked markers, use multiple symbol layers in QGIS — they export as multiple PointSymbolizers 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 GraphicFill with mark horline or backslash (use backslash for 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 ExternalGraphic fill 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/MaxScaleDenominator and SLDReader honors them.
  • SLDReader converts OL resolution → scale denominator assuming ~96 dpi. In EPSG:3857 at higher latitudes, use the convertResolution hook (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 QGIS ogc:Function calls 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 with SLDReader.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: true on the vector layer (and optionally text-overflow settings). Expect different label density than in QGIS.

4.3 Recipe for “safe” QGIS labels

  1. Single Labels mode, Value = one plain field (create a virtual/precomputed field if you need composition).
  2. 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-family to add fallbacks: Open Sans, Arial, sans-serif.
  3. Size in pixels, not points or millimetres.
  4. 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.
  5. 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.
  6. 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.
  7. Don’t combine rotation + offset in one symbolizer (spec/OL order mismatch).
  8. Scale ranges: use rule-based labeling with scale visibility per rule → exports clean min/max scale denominators.
  9. On the OL side, create the layer with declutter: true to 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 ReadergetLayer/getStyle (match the <se:Name> in the SLD, or pass no name to take the first) → createOlStyleFunction(featureTypeStyle, options).
  • Pass imageLoadedCallback so the layer re-renders when ExternalGraphic icons 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: true on 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

  1. Style in QGIS → export SLD → inspect the XML.
  2. 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.
  3. Optionally load the same SLD into GeoServer as a sanity check; if GeoServer rejects it, browsers definitely won’t be happier.
  4. 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.
  5. 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.