Server-side raster tile rendering
Martin can render a style into raster images server-side: as XYZ tiles, or as a single static image at a chosen camera.
Warning
This feature is included in the default build on Linux. Its behaviour may change in patch releases.
Limitations of our current implementation:
- Rendering support is currently only available on Linux. To add support for macOS/Windows, please see https://github.com/maplibre/maplibre-native-rs.
- Currently, martin does not cache style rendered requests and
- does not support concurrency for this feature.
We welcome contributions to help stabilise this feature!
To enable rendering, turn it on in the configuration file:
Rendered XYZ tiles
We support generating a rasterised image for an XYZ tile of a given style.
After enabling rendering, you can use the /style/<style_id>/{z}/{x}/{y}.{filetype} API to get a <style_id>'s rendered png/jpeg content.
Static images
Info
We currently do not have the same capabilities as Tileserver-GL to layout images. We are working on adding this feature and are very open to contributions if you want to help!
Warning
Static rendering shares the limitations listed above (Linux only, no caching, no concurrency). The HTTP shape may still change in patch releases.
Martin can render a single PNG/JPEG/WebP of a style at a chosen camera. The same URL is served by two methods:
GET /style/{style_id}/static/{camera}/{size}.{ext}
POST /style/{style_id}/static/{camera}/{size}.{ext}
GET returns the base map alone.
POST additionally accepts a GeoJSON FeatureCollection in the body, with
styling on each feature's properties, and overlays those features on top
of the base style for that single render.
An empty or missing body is equivalent to GET.
Camera
The {camera} segment chooses what the image looks at:
| Form | Meaning |
|---|---|
lon,lat,zoom |
Center at (lon, lat) and zoom (north up, flat). |
lon,lat,zoom@bearing |
Center + bearing in degrees (clockwise from north). |
lon,lat,zoom@bearing,pitch |
Center + bearing + pitch in degrees. |
minLon,minLat,maxLon,maxLat |
Fit the given bounding box to the requested size. |
Info
The image is always rendered INSIDE of the requested size.
So if the bbox is [-10°,-1°,10°,1°] and size 500x500 is requested, the image will be centered on 0,0 with a 500x500 box that is fully inside the bbox, so the left-top most pixel is approximately at 1°,-1°.
We will not expand the image outside the bbox.
Size and format
{size}.{ext} follows the WIDTHxHEIGHT[@{scale}x].{ext} pattern, e.g.
800x600.png, 400x300@2x.jpg.
Allowed extensions are png, jpg, and webp.
Width and height are capped at 2048 px each; scale is capped at 4x.
Overlay body (POST)
The overlay body is a GeoJSON FeatureCollection. Each feature carries its
style on properties using
MapLibre Style Spec paint/layout
property names (circle-*, line-*, fill-*). Any property left unset falls
back to MapLibre's own default.
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": { "type": "LineString",
"coordinates": [[-10, 5], [10, 5]] },
"properties": { "line-color": "#f00", "line-width": 3, "line-cap": "round" }
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [0, 0] },
"properties": { "circle-color": "#00f", "circle-radius": 8 }
},
{
"type": "Feature",
"geometry": { "type": "Polygon",
"coordinates": [[[-5,-5],[5,-5],[5,5],[-5,5],[-5,-5]]] },
"properties": { "fill-color": "#0a0", "fill-opacity": 0.5 }
}
]
}
How features become layers:
- Point / MultiPoint -> one
circlelayer. - LineString / MultiLineString -> one
linelayer. - Polygon / MultiPolygon -> a
filllayer (unless onlyline-*properties are set), plus alinelayer when anyline-*property is present (so a polygon withline-colorgets an outline ofline-width). GeometryCollectionand features withgeometry: nullare silently skipped.
Anything the server can't make sense of is rejected with 400 Bad
Request: a malformed body, a top-level type that isn't
FeatureCollection, a member type that isn't Feature, an invalid CSS
color, a non-numeric width, or an unknown line-cap/line-join enum value.
Unknown property keys on feature.properties are silently ignored, so
GeoJSON files that already carry application metadata (id, name,
title, description, …) work without modification.
Supported style properties
Every property is optional; an unset property uses the MapLibre Style Spec default.
| MapLibre property | Applies to | MapLibre default |
|---|---|---|
circle-color |
Point | #000000 |
circle-opacity |
Point | 1 |
circle-radius |
Point | 5 |
circle-stroke-color |
Point | #000000 |
circle-stroke-opacity |
Point | 1 |
circle-stroke-width |
Point | 0 |
line-color |
LineString / Polygon | #000000 |
line-opacity |
LineString / Polygon | 1 |
line-width |
LineString / Polygon | 1 |
line-cap |
LineString / Polygon | butt |
line-join |
LineString / Polygon | miter |
fill-color |
Polygon | #000000 |
fill-opacity |
Polygon | 1 |
fill-outline-color |
Polygon | matches fill-color |
line-cap is one of butt, round, square; line-join is one of
miter, bevel, round. Colors accept any CSS color string.
Range checks are not enforced - *-opacity values outside 0..=1 and
negative widths are passed through to MapLibre verbatim.
Out of scope
- Data-driven expressions on individual feature properties (each feature becomes its own GeoJSON source and layer with literal paint values).
- Layer types beyond
fill/line/circle(nosymbol,heatmap,raster,fill-extrusion). - Externally-referenced GeoJSON URLs (every feature is inline).
- Symbol/icon markers (no
symbollayer) - use acircleinstead. - Z-ordering relative to base style layers - all overlay layers are drawn on top of the base style, in feature-array order.
All examples below render the same camera (/static/0,0,2/200x200.png)
against the maplibre_demo style, so the visual differences come only from
the overlay body.
Line
{
"type": "FeatureCollection",
"features": [{
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [[-10.0, -10.0], [10.0, 10.0]]
},
"properties": {
"line-color": "#95BEFA",
"line-width": 5,
"line-cap": "round",
"line-join": "round"
}
}]
}

Fill
{
"type": "FeatureCollection",
"features": [{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [[
[-10.0, -10.0], [10.0, -10.0],
[10.0, 10.0], [-10.0, 10.0],
[-10.0, -10.0]
]]
},
"properties": {
"fill-color": "red",
"fill-opacity": 1.0
}
}]
}

Fill opacity (alpha blending)
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": { "type": "Polygon",
"coordinates": [[[-40, -20], [10, -20], [10, 20], [-40, 20], [-40, -20]]] },
"properties": {
"fill-color": "#285DAA",
"fill-opacity": 0.5
}
},
{
"type": "Feature",
"geometry": { "type": "Polygon",
"coordinates": [[[-10, -20], [40, -20], [40, 20], [-10, 20], [-10, -20]]] },
"properties": {
"fill-color": "#95BEFA",
"fill-opacity": 0.5
}
}
]
}

Circle (marker)
{
"type": "FeatureCollection",
"features": [{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [0.0, 0.0]
},
"properties": {
"circle-color": "#285DAA",
"circle-radius": 8
}
}]
}
