Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add `expression_type`, a media type identifying the dialect of `expression` (e.g. `text/x-numexpr`,
`application/vnd.maplibre.expression+json`), and a `rel: "stylesheet"` link relation for referencing external
style documents (SLD, Mapbox/MapLibre Style, OpenLayers Flatstyle, QGIS QML), sharing the same media-type
vocabulary. A stylesheet link may also address a specific named layer/style inside a multi-layer document via
a URI fragment on `href` ([#17](https://github.com/stac-extensions/render/issues/17),
[#21](https://github.com/stac-extensions/render/issues/21))
- Add a vector data example (`examples/item-vector.json`), showing the extension used on non-raster data with
a MapLibre expression and stylesheet link
- Add an `assets` cross-reference attribute to `rel: "stylesheet"` links, so a stylesheet can be tied
directly to one or more assets without requiring a `renders` entry — useful for vector styling, where
`render` (raster-focused) often doesn't apply at all

### Fixed

- Require `expression` whenever `expression_type` is set (the reverse is not required, so existing
documents using `expression` alone remain valid)
- Correct the `stylesheet` link documentation: STAC Asset Objects have no `links` field, so a stylesheet
link always lives at the item/collection level, never directly on an asset

## [2.1.0] - 2026-09-22

### Added
Expand Down
106 changes: 105 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Rendering extension aims at providings consumers with the possible rendering of
- Examples:
- [Landsat-8 example](examples/item-landsat8.json): Shows the basic usage of the extension in a landsat-8 STAC Item
- [Sentinel-2 example](examples/item-sentinel2.json): Shows the basic usage of the extension in a Sentinel-2 STAC Item
- [Vector example](examples/item-vector.json): Shows the extension used on vector data, styled with a MapLibre expression and stylesheet link
- [Collection example](examples/collection.json): Shows the basic usage of the extension in a collection
- [JSON Schema](json-schema/schema.json)
- [Changelog](./CHANGELOG.md)
Expand Down Expand Up @@ -44,7 +45,8 @@ The fields in the table below can be used in these parts of STAC documents:
| colormap | object | [Color map JSON definition](https://developmentseed.org/titiler/user_guide/rendering/#custom-colormaps) that must be applied for a raster band |
| color_formula | string | [Color formula](https://developmentseed.org/titiler/user_guide/rendering/#color-formula) that must be applied for a raster band |
| resampling | string | Resampling algorithm to apply to the referenced assets. See [GDAL resampling algorithm](https://gdal.org/programs/gdalwarp.html#cmdoption-gdalwarp-r) for some examples. |
| expression | string, object, array | Band arithmetic formula to apply to the referenced assets. The format is defined by the rendering application, e.g. a [TiTiler](https://developmentseed.org/titiler/) band math string or a [MapLibre](https://maplibre.org/maplibre-style-spec/expressions/) style expression array. |
| expression | string, object, array | Expression to derive the rendered value(s) from the referenced assets, e.g. a band-math formula or a style expression (which may also cover conditionals, interpolation, or other non-arithmetic operations). See [Expression and stylesheet formats](#expression-and-stylesheet-formats) for how to identify its dialect with `expression_type`. |
| expression_type | string | Media type identifying the dialect of `expression` (e.g. `text/x-numexpr`, `application/vnd.maplibre.expression+json`). If `expression_type` is set, `expression` MUST also be set — but not the reverse: `expression` alone remains valid. See [Expression and stylesheet formats](#expression-and-stylesheet-formats). If not set, a string `expression` SHOULD be assumed to be `text/x-numexpr` for backwards compatibility; an object or array `expression` SHOULD NOT be assumed to be any particular dialect. |
| minmax_zoom | \[int] | Zoom levels range applicable for the visualization |
| asset_as_band | boolean | Treat assets in `expression` as single bands. Required when expression uses multiple assets |

Expand Down Expand Up @@ -86,6 +88,43 @@ It is specified as a 2 dimensions array of delimited Min,Max range per band.
A prescaling can also be performed according to the `offset` and `scale` fields value of the
[raster](https://github.com/stac-extensions/raster) extension.

## Expression and stylesheet formats

The `render` object is intentionally implementation-agnostic: `expression` and, via
[stylesheet links](#stylesheet-links), a whole external stylesheet, can each be written in more than one
dialect (band-math strings, JSON style-expression arrays, XML style documents, ...). Without saying which
dialect a given value uses, a client has no reliable way to parse it. For example, all of the following are
different, mutually incompatible ways to express the same NDVI formula:

- `numexpr` (used by [TiTiler](https://github.com/developmentseed/titiler)/rio-tiler): a Python-like string,
e.g. `"(B08-B04)/(B08+B04)"`.
- [MapLibre GL / Mapbox GL style expressions](https://maplibre.org/maplibre-style-spec/expressions/): a JSON
array, e.g. `["/", ["-", ["band", 2], ["band", 1]], ["+", ["band", 2], ["band", 1]]]`.
- [OpenLayers style expressions](https://openlayers.org/en/latest/apidoc/module-ol_expr_expression.html): also
JSON-array-based, but a **distinct grammar** from MapLibre/Mapbox's, despite the superficial similarity
(OpenLayers bridges to actual Mapbox/MapLibre style documents only via the separate
[`ol-mapbox-style`](https://github.com/openlayers/ol-mapbox-style) adapter package, not natively).

None of these dialects has a formally IANA-registered media type. Where possible this extension reuses the
same informal `vnd.` media types already used by [OGC API - Styles](https://docs.ogc.org/DRAFTS/20-009.html)
for whole stylesheet documents; for expression fragments and dialects OGC API - Styles doesn't cover, this
extension defines its own, following the same `vnd.`/`x-` conventions:

| Format | Media type | Scope |
| --- | --- | --- |
| `numexpr` band math | `text/x-numexpr` | `expression` fragment (string) |
| MapLibre/Mapbox GL style expression | `application/vnd.maplibre.expression+json` | `expression` fragment (array) |
| OpenLayers style expression | `application/vnd.openlayers.expression+json` | `expression` fragment (array) |
| Mapbox/MapLibre Style (full stylesheet) | `application/vnd.mapbox.style+json` | stylesheet document (reused from OGC API - Styles) |
| OGC Styled Layer Descriptor (SLD) | `application/vnd.ogc.sld+xml` | stylesheet document (reused from OGC API - Styles) |
| OpenLayers Flatstyle (full stylesheet) | `application/vnd.openlayers.flatstyle+json` | stylesheet document |
| QGIS QML style | `application/vnd.qgis.qml+xml` | stylesheet document |

`expression_type` uses the "expression fragment" rows to disambiguate the `expression` field. A
[stylesheet link](#stylesheet-links)'s `type` uses the "stylesheet document" rows, since it points at a
whole, standalone style document rather than a single formula. This table is not exhaustive: additional
formats can be added following the same convention as new renderers need to be supported.

## Dynamic tile servers integration

The render objects are designed to be used by dynamic tile servers to produce RGB tiles from a STAC Item.
Expand Down Expand Up @@ -225,6 +264,7 @@ Obviously, the same rendering can be applied to local source assets without usin
"resampling": "average",
"colormap_name": "ylgn",
"expression": "(B08-B04)/(B08+B04)",
"expression_type": "text/x-numexpr",
"rescale": [[-1,1]]
}
}
Expand Down Expand Up @@ -253,6 +293,70 @@ in order to provide a cross link to the render object.
}
```

### Stylesheet links

To reference an external, standalone style document (as opposed to the inline `expression` field), add a
link with `rel: "stylesheet"` to the item or collection's `links` array (STAC Asset Objects have no
`links` field of their own, so a stylesheet link always lives at the item/collection level, even when it
styles a single asset). The link MUST carry a `type` identifying the stylesheet's format, using the
"stylesheet document" media types from
[Expression and stylesheet formats](#expression-and-stylesheet-formats) (e.g. SLD, Mapbox/MapLibre Style,
OpenLayers Flatstyle, QGIS QML).

A stylesheet link does not require the `render` extension. It can cross-reference what it styles two
ways, independently of each other:

- `render`, the same attribute used by [web map links](#additional-attributes), naming which `renders`
entry it styles. Useful for fine-tuning a specific render's presentation (see the examples below).
- `assets`, an array of asset keys the stylesheet applies to directly. Useful when there is no `render`
entry at all — e.g. a vector item that has no raster pixels to rescale/colormap, only a style to
apply. `render` primarily targets raster data, so this is the expected path for vector styling.

```json
{
"rel": "stylesheet",
"type": "application/vnd.mapbox.style+json",
"href": "https://example.com/styles/ndvi.json",
"render": "ndvi"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also make this usable without the render extension? So that we can just connect an asset and a style? Especially in the vector case it doesn't make a lot of sense to use render.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It actually make sense when you want to fine tune the rendering. Check the examples.
Besides that note, I do not see what prevent a user to add a stylesheet link as of today.

@m-mohr m-mohr Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like a misunderstanding.

The way it is is fine for render, but there's no way to directly make a connection between asset (or another link) and the stylesheet. For vector you may just want t a stylesheet without the render extension, as render is primarily targeting raster data.

}
```

```json
{
"rel": "stylesheet",
"type": "application/vnd.mapbox.style+json",
"href": "https://example.com/styles/roads.json",
"assets": [ "roads" ]
}
```

#### Addressing a layer within a stylesheet

A single stylesheet document can define more than one named layer or style. When that is the case, `render`
alone is not enough to disambiguate, because it names which `renders` entry the link is for, not which part
of the document to use. Append the layer/style's own name, as defined by that stylesheet format, as a URI
fragment on `href`. This reuses each format's native naming instead of inventing a new addressing scheme:

- SLD: the `<NamedLayer>`/`<UserStyle>` element's `<Name>` text, e.g. `styles.sld#ndvi`.
- Mapbox/MapLibre Style: a top-level `layers[].id`, e.g. `style.json#ndvi-layer`.

```json
"links": [
{
"rel": "stylesheet",
"type": "application/vnd.ogc.sld+xml",
"href": "https://example.com/styles/multi.sld#ndvi",
"render": "ndvi"
},
{
"rel": "stylesheet",
"type": "application/vnd.ogc.sld+xml",
"href": "https://example.com/styles/multi.sld#sir",
"render": "sir"
}
]
```

## Contributing

All contributions are subject to the
Expand Down
7 changes: 7 additions & 0 deletions examples/item-landsat8.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@
"href": "https://landsat-stac.s3.amazonaws.com/collections/landsat-8-l1.json",
"type": "application/json",
"title": "The full collection"
},
{
"rel": "stylesheet",
"type": "application/vnd.mapbox.style+json",
"href": "https://example.com/styles/ndvi.json",
"title": "NDVI stylesheet",
"render": "ndvi"
}
],
"assets": {
Expand Down
84 changes: 84 additions & 0 deletions examples/item-vector.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
{
"type": "Feature",
"stac_version": "1.0.0",
"stac_extensions": [
"https://stac-extensions.github.io/render/v2.1.0/schema.json"
],
"id": "roads-example",
"bbox": [
2.25,
48.82,
2.42,
48.9
],
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
2.25,
48.82
],
[
2.25,
48.9
],
[
2.42,
48.9
],
[
2.42,
48.82
],
[
2.25,
48.82
]
]
]
},
"properties": {
"datetime": "2024-01-01T00:00:00Z",
"renders": {
"roads-by-class": {
"title": "Roads colored by class",
"assets": [
"roads"
],
"expression": [
"match",
[
"get",
"class"
],
"motorway",
"#e15c5c",
"primary",
"#f2b46d",
"#cccccc"
],
"expression_type": "application/vnd.maplibre.expression+json"
}
}
},
"links": [
{
"rel": "stylesheet",
"type": "application/vnd.mapbox.style+json",
"href": "https://example.com/styles/roads.json",
"title": "Roads MapLibre style",
"render": "roads-by-class"
}
],
"assets": {
"roads": {
"title": "Road network",
"type": "application/geo+json",
"roles": [
"data"
],
"href": "https://example.com/data/roads.geojson"
}
}
}
35 changes: 35 additions & 0 deletions json-schema/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,16 @@
}
}
}
},
{
"properties": {
"links": {
"type": "array",
"items": {
"$ref": "#/definitions/stylesheet_link"
}
}
}
}
]
},
Expand Down Expand Up @@ -128,6 +138,22 @@
}
],
"definitions": {
"stylesheet_link": {
"$comment": "If a link's rel is 'stylesheet', it MUST carry a media type identifying the stylesheet format.",
"if": {
"type": "object",
"required": ["rel"],
"properties": {
"rel": {
"const": "stylesheet"
}
}
},
"then": {
"type": "object",
"required": ["type"]
}
},
"stac_extensions": {
"type": "object",
"required": [
Expand Down Expand Up @@ -225,6 +251,12 @@
"required": [
"assets"
],
"if": {
"required": ["expression_type"]
},
"then": {
"required": ["expression"]
},
"properties": {
"assets": {
"type": "array",
Expand Down Expand Up @@ -265,6 +297,9 @@
"expression": {
"type": ["string", "object", "array"]
},
"expression_type": {
"type": "string"
},
Comment on lines 297 to +302

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the schema should ensure that expression_type is required when expression is set and vice versa. They can't really be provided alone.

"minmax_zoom": {
"type": "array",
"items": {
Expand Down
Loading