ThinkGeo.com    |     Documentation    |     Premium Support

Using SVG files in Blazor Maps

We are in the process of creating new icons for the use on our Maps and are looking at making those .svg files. Currently we use .png files and load those as a GeoImage which only supports raster file types. Other that converting the .svg to a .png at load time, is there a better method for loading them.

Thanks,
James R.

Hi James,

You’re right that GeoImage is raster-only — and as you’ve likely noticed, that path renders on the server side: the SDK draws your features into raster map tiles and sends the tiles to the browser.

It helps to separate two independent choices — where the icon is drawn and the icon format — which gives four combinations:

Server-side (PointStyle + GeoImage on a LayerOverlay):

  • PNG — :white_check_mark: this is what you do today.
  • SVG — :warning: no built-in SVG input; you’d rasterize the SVG once yourself (e.g. with Svg.Skia).

Client-side (MarkerOverlay + PointMarkerStyle, rendered in the browser):

  • PNG — :white_check_mark: built-in.
  • SVG — :white_check_mark: built-in; just point VirtualPath at an .svg and OpenLayers renders it as vector.

Recommendation: if your icons are a manageable number of point markers you want to interact with (click / hover / popup) — the typical “custom map icons” case — use the client-side MarkerOverlay. You get true native SVG (crisp at every zoom, no conversion) plus per-marker events, at the cost of pushing each point to the browser. Stay on the server-side path when you have very large datasets, need server-side data-driven styling/labels, or the data must remain on the server — there, rasterize the SVG once at your target size.

Client-side, VirtualPath takes either format, so PNG↔SVG is a one-line swap:

// put the file under wwwroot, e.g. wwwroot/images/pin.svg (or pin.png)
var style = new PointMarkerStyle("images/pin.svg", 0.5, 1.0, 1.0);
// args: path, offsetX, offsetY, scale

razor
<MarkerOverlaySetting>
    <SimpleMarkerOverlay MarkerSource="@markers">
        <MarkersSetting>
            @{
                var m = context as MyMarkerModel;
                <Marker Id="@m.Id" Position="@m.Position" Style="@m.Style" />
            }
        </MarkersSetting>
    </SimpleMarkerOverlay>
</MarkerOverlaySetting>

Let us know if you have any questions — or if you’d rather stay on the server-side PointStyle path, and I can share a small helper that rasterizes the SVG once at your target size.

Thanks,
Ben

Ben,

We definitely need to stay on the server side as the icons will be placed interactively by the users dragging them onto the map from a “toolbox” of sorts or loaded from saved data. Similar to the previous sample I sent for a reported bug. They are not fixed icons as they will need to be added, deleted and moved as needed by the users. The code to rasterize the SVG icons I think I understand but any samples are always appreciated.

Thanks,
James R.

Hi James,

Here is a method you can use.

First reference Svg.Skia (MIT-licensed).

<PackageReference Include="Svg.Skia" Version="3.0.4" />

And here is the new method:

using SkiaSharp;
using Svg.Skia;
using ThinkGeo.Core;

// Rasterize the .svg ONCE, at the pixel height you want the icon drawn.
static GeoImage SvgToGeoImage(string svgPath, float targetHeightPx)
{
    using var svg = new SKSvg();
    svg.Load(svgPath);

    var bounds = svg.Picture.CullRect;
    float scale = targetHeightPx / bounds.Height;
    var info = new SKImageInfo((int)MathF.Ceiling(bounds.Width * scale),
                               (int)MathF.Ceiling(bounds.Height * scale));

    using var surface = SKSurface.Create(info);
    surface.Canvas.Clear(SKColors.Transparent);
    surface.Canvas.Scale(scale);
    surface.Canvas.DrawPicture(svg.Picture);

    using var img  = surface.Snapshot();
    using var data = img.Encode(SKEncodedImageFormat.Png, 100);
    return new GeoImage(data.ToArray());
}

// Build the PointStyle once per icon and reuse it for every feature that uses it:
var pinStyle = new PointStyle(SvgToGeoImage("icons/pin.svg", 48));

// the rest of the code would be the same
var layer = new InMemoryFeatureLayer();   // your existing editable layer
layer.ZoomLevelSet.ZoomLevel01.DefaultPointStyle = pinStyle;
layer.ZoomLevelSet.ZoomLevel01.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level20;

A couple of things worth knowing:

  • Rasterize once per distinct icon and reuse the GeoImage/PointStyle — don’t rasterize per feature.
  • targetHeightPx is the on-screen icon size; if you render high-DPI (2×) tiles, rasterize at 2× so it stays crisp.

One alternative, if you’d rather not add a runtime dependency: since the server rasterizes to a tile anyway, you can pre-convert the SVGs to appropriately-sized PNGs offline and keep loading them as GeoImage exactly like today. Runtime Svg.Skia is worth it when the SVG isn’t known until runtime (e.g. user-supplied) or you want to re-rasterize per DPI.

Thanks,
Ben

Ben,

Thanks, we are still determining our new icons, and I can possibly convert them pre-use. However, this is helpful just in case I cannot.

James R.

No problem! Just let us know if you have any more questions.