# ezu
[](https://crates.io/crates/ezu)
[](https://docs.rs/ezu)
[](https://github.com/reearth/ezu/actions/workflows/ci.yml)
[](#license)
**Painterly cartography** — render vector tiles as paintings.

`ezu` (絵図) is a Rust map rendering engine that turns vector tiles (MVT /
PMTiles) into painterly raster tiles via the
[`hokusai`](https://github.com/reearth/hokusai) brush engine and a
declarative style language called **Ezu Style**. Where conventional map
engines aim for cartographic accuracy, ezu aims for artistic
interpretation — watercolor, ink wash, ukiyo-e, and beyond — while
preserving the geographic data underneath.
## Workspace
Each crate has its own README with API details and examples.
| [`ezu`](https://github.com/reearth/ezu/tree/main/crates/ezu) | [](https://crates.io/crates/ezu) | Umbrella crate, re-exports + feature flags |
| [`ezu-core`](https://github.com/reearth/ezu/tree/main/crates/ezu-core) | [](https://crates.io/crates/ezu-core) | Tile / world coordinates, deterministic seeding |
| [`ezu-features`](https://github.com/reearth/ezu/tree/main/crates/ezu-features) | [](https://crates.io/crates/ezu-features) | GIS feature parsing (MVT via `geozero`, GeoJSON) — no remote fetch |
| [`ezu-style`](https://github.com/reearth/ezu/tree/main/crates/ezu-style) | [](https://crates.io/crates/ezu-style) | Style spec parser (`serde`) — pure data, no rendering |
| [`ezu-graph`](https://github.com/reearth/ezu/tree/main/crates/ezu-graph) | [](https://crates.io/crates/ezu-graph) | Typed node-DAG evaluator (Cache, Rayon parallel) |
| [`ezu-paint`](https://github.com/reearth/ezu/tree/main/crates/ezu-paint) | [](https://crates.io/crates/ezu-paint) | Painting primitives, built-in nodes, host glue (PNG / brush bank) |
| [`ezu-cli`](https://github.com/reearth/ezu/tree/main/crates/ezu-cli) | [](https://crates.io/crates/ezu-cli) | Command-line tool — `tile` / `bbox` / `tiles` rendering, `check` style validator, `serve` live editor + tile server |
## Try it
Install the CLI from crates.io:
```sh
cargo install ezu-cli
```
That puts an `ezu` binary on your `PATH`. Point it at any style (URL
or local path) and a tile source (PMTiles URL/file, an `{z}/{x}/{y}`
MVT URL/path, or a TileJSON) and it spits out PNGs. The style can
declare its own tile sources in a `sources` block (MVT, PMTiles, or
raster DEM); CLI flags override anything declared there for one-off
swaps:
```sh
# Single tile to PNG (use `--out tile.webp` for lossless WebP). The
# reference styles bundle their own `sources` block (Protomaps daily
# build + Re:Earth Terrain), so no `--pmtiles` / `--mvt` is needed —
# pass them to override what the style declares.
ezu tile \
--style https://raw.githubusercontent.com/reearth/ezu/main/crates/ezu/examples/styles/watercolor-basic.json \
--tile 13/7276/3225 --out tile.png
# Terrain style — pulls raster DEM tiles from terrain.reearth.land.
ezu tile --style crates/ezu/examples/styles/hillshade.json \
--tile 11/1813/807 --out fuji.png
# bbox mosaic — stitch the tiles covering a lon/lat box into one PNG.
ezu bbox --style URL_OR_PATH \
--bbox 139.74,35.65,139.78,35.69 --zoom 13 --out tokyo.png
# XYZ pyramid — bulk-render `<out>/<z>/<x>/<y>.png` for a zoom range.
ezu tiles --style URL_OR_PATH \
--bbox 139.74,35.65,139.78,35.69 \
--min-zoom 10 --max-zoom 14 --out pyramid
# Validate a style document (parse + build graph + resolve assets).
# Exits non-zero on error — drop into a pre-commit hook / CI step.
ezu check style.json
ezu check style.json --no-fetch # parse + graph only, offline
# `--verbose` (or `-v`) enables per-node debug logs from the
# evaluator: op name, cache hit/miss, output shape, eval duration.
ezu --verbose tile --style style.json --tile 13/7276/3225 --out tile.png
```
The reference style references brushes by name (`watercolor_glazing`,
`2B_pencil`, …) — these are CC0 MyPaint brushes bundled into the binary,
so they resolve without any host-side file staging. To bring your own
`.myb` brush, declare it in the style's `assets` block (with an
`http(s)://` URL or a path relative to `--assets-dir`).
For deeper hacking, clone the repo and try the `tokyo` example, which
renders a 2×2 batch under the reference watercolor style with Rayon
parallelism turned on:
```sh
cargo run --release --features parallel -p ezu --example tokyo
# Output PNGs in ./out/tokyo/
```
The live editor (browser-based, edit JSON → see the map update,
schema-validated as you type):
```sh
ezu serve # default example style
ezu serve crates/ezu/examples/styles/pencil-sketch.json # open a specific style
ezu serve https://example.com/style.json # or fetch one over http(s)
# Open http://127.0.0.1:8080
```
The editor (MapLibre GL based) supports:
- **Open / URL / Save** — load a style from a local file or http(s) URL,
save the current buffer as `<name>.json`. Open on Chromium browsers
uses the File System Access API so Save writes back in place.
- **Apply** with `⌘↵` / `Ctrl+↵` (works anywhere on the page).
- **Live preview** — when enabled, auto-applies on every keystroke that
parses + schema-validates + server-validates clean.
- **External-edit reload** — when launched with a local path
(`ezu serve foo.json`), the server polls the file and pushes
Server-Sent Events on every change. The editor swaps the buffer
silently when clean, or surfaces a Reload banner when the user has
unsaved edits. The `↻ HH:MM:SS` indicator in the toolbar shows the
last auto-reload. On Chromium, the same watch also runs against
files opened via the in-browser file picker. Opening a different
file via `Open…` / `URL…` detaches the server watch for that
session.
- **Source MVT inspector** — toggle a vector overlay of the underlying
MVT, with per-layer ON/OFF and click-to-inspect feature properties.
Layers are discovered from the tile at the map center; pan/zoom
rescans automatically.
- **Tile grid + zoom indicator** — toggle a `z/x/y` boundary overlay
(drawn per tile via `maplibregl.addProtocol`), and read the live
zoom value (click to copy `z @ lat,lng`).
## How it paints
A style is a **typed node DAG**, not an ordered layer list. Every
operation is a node; ports are statically type-checked across six
kinds — `Features` (geometry + props), `Raster` (canvas-sized RGBA),
`Sprite` (image at native dimensions, consumed by placement ops),
`Brush` (hokusai brush handle), `Scalar` (constants),
`ScalarField` (per-pixel `f32` grid — elevation, distance, scalar
noise; carries optional geographic scaling). Ports list the kinds
they accept, so polymorphic ops (e.g. `blur` over `Raster`/`Sprite`)
pass the input kind straight through. Intermediate buffers are
cached and reusable across tiles.
External inputs — images, brushes, per-tile MVT/GeoJSON feature
layers — enter through one uniform `AssetLoader` trait. The style
references each binding by name (`tile.<layer>` for per-tile feature
data, bare names for document-scoped assets); the host fills the
bindings before rendering. Asset `src` entries can be local file
paths or `http(s)://` URLs — native hosts (CLI, server, examples)
prefetch URLs via `ezu_paint::host::prefetch_doc_assets` at startup
(gated behind the `http` feature). Source-format choice (MVT vs
GeoJSON vs synthesized) is a host concern, not a node concern.
The minimum op set ships in [`ezu-paint`](https://github.com/reearth/ezu/tree/main/crates/ezu-paint):
- **Sources** — `solid`, `circle` (both with optional `kind: sprite`
for synthetic placement/tiling source), `noise` (white / value /
perlin / simplex / worley, with fBm octaves and domain warp,
world-anchored for seamless tile borders; `kind: scalar` emits raw
fBm as a `ScalarField` for terrain stylization), `features`,
`brush-file`, `image` (load a PNG/WebP asset as a `Sprite` for
placement / tiling ops)
- **Rasterization** — `fill-solid` (tiny-skia + libblur), `fill-dabs`
(hokusai scatter-dab fill, **world-deterministic** so dabs stay
seamless across tile boundaries), `line` (hokusai stroke along
polylines), `stamp` (paint an image per feature point — accepts a
`Sprite` or canvas-sized `Raster`), `place` (composite one image at
fixed canvas coordinates with `fit: none/cover/contain/stretch`),
`tiling` (repeat an image across the canvas, world-anchored for
seamless tile borders)
- **Composition** — `blur` (libblur Gaussian), `blend` (W3C 16 blend
modes — multiply / screen / overlay / soft-light / hue / luminosity
etc., plus `composite` operators (`destination-out` for brush-style
eraser), `clip` for Photoshop-style clipping masks, and an optional
alpha-`mask` input)
- **Warp** — `displace` (Photoshop-style displacement map: R/G channels
of a second raster drive per-pixel offsets), `warp` (domain warp via
built-in noise; world-anchored for seamless tile borders). Both grow
upstream pad by `amp-px` and expose `clamp` / `transparent` /
`mirror` boundary modes
- **Adjustment** — `brightness-contrast`, `levels` (Photoshop-style
in/out black/white + gamma), `hsl` (hue rotation +
saturation/lightness shift), `invert`, `color-to-alpha` (chroma key)
- **Morphology / edges** — `erode` / `dilate` (per-channel min/max box
filter, for mask cleanup), `edge-detect` (Sobel gradient magnitude),
`sharpen` (4-neighbour Laplacian)
- **Channel ops** — `channel-shuffle` (rearrange RGBA, or stamp
constants `0` / `1` into channels), `posterize` (per-channel
quantisation)
- **Geometry (Voronoi family)** — `voronoi` (point set → diagram
edges), `voronoi-fracture` (split polygons into Voronoi sub-cells
via seed points), `medial-axis` (polygon → skeleton polylines for
river / lake centrelines and similar), `triangulate` (Delaunay)
- **Geometry (set + transform)** — `feature-boolean`
(union / intersection / difference / xor over polygons),
`transform` (translate / rotate / scale), `bbox` (axis-aligned
envelope), `smooth` (Chaikin), `densify`, `resample`
- **Utility** — `switch` (build-time A/B selection over any port
kind; great for param-driven variants), `pick-channel` (extract
R/G/B/A/luminance from a Raster as a `ScalarField`, bridging into
`map-range` / `threshold` / `color-ramp`)
- **Scalar math** — `map-range` (linear remap with optional clamp on a
`ScalarField`), `threshold` (binarise with optional soft ramp)
- **Gradients** — `gradient-linear`, `gradient-radial` (elliptical via
`aspect`), `gradient-conic`, `gradient-diamond`. All take color stops
and an `anchor: "tile" | "world"` for tile-local or world-anchored
(seamless across tiles) patterns.
- **Terrain** — `dem` (sample a host-bound raster-DEM mosaic as a
`ScalarField` with `geo_scale` populated; the host declares the tile pyramid in `sources` and
handles fetch / decode / 3×3 stitch / overzoom upsampling for
terrarium and mapbox-rgb encodings), `hillshade` (Horn-method
analytical shade with `shade` or multiply-friendly `relief` mode,
optional ESRI multidirectional), `slope`, `color-ramp` (any scalar
field → colour via a stops table; canonical use is hypsometric
tinting of a DEM).
Example: a watercolor water layer with a brushed road on top of an
earth-tone background.
```json
{
"name": "demo",
"tile-size": 512,
"pad": 24,
"sources": { "glazing": { "type": "brush", "src": "builtin:watercolor_glazing" } },
"nodes": {
"bg": { "op": "solid", "color": "#fbf6e6" },
"earth": { "op": "features", "name": "tile.earth" },
"earth_p":{ "op": "fill-solid", "features": "@earth", "fill": "#e8d9b0" },
"water": { "op": "features", "name": "tile.water" },
"water_p":{ "op": "fill-dabs", "features": "@water",
"color": "#5876a0", "opacity": 0.22,
"radius-px": 7, "spacing-px": 3 },
"roads": { "op": "features", "name": "tile.roads",
"filter": { "kind_detail": "motorway" } },
"brush": { "op": "brush-file", "src": "@glazing" },
"roads_p":{ "op": "line", "features": "@roads", "brush": "@brush",
"color": "#4a3424", "radius-px": 2.6 },
"c1": { "op": "blend", "base": "@bg", "over": "@earth_p" },
"c2": { "op": "blend", "base": "@c1", "over": "@water_p" },
"out": { "op": "blend", "base": "@c2", "over": "@roads_p" }
},
"output": "@out"
}
```
The full reference watercolor style is in
[`crates/ezu/examples/styles/watercolor-basic.json`](https://github.com/reearth/ezu/tree/main/crates/ezu/examples/styles/watercolor-basic.json).
All painting happens on a **padded canvas** (`tile_size + 2 * pad`) so
gaussian blurs and MVT buffer geometry that overflows `[0, extent]`
land inside the buffer; the output is cropped to the tile by
[`ezu-paint::host`](https://github.com/reearth/ezu/tree/main/crates/ezu-paint) before encoding.
## Custom ops
`NodeFactory` is a public trait — any downstream crate can register
its own ops on top of `ezu-paint::nodes::default_registry()` and feed
the registry to `ezu-graph::build_graph`. The JSON Schema served at
`/schemas/ezu-style.json` by `ezu serve` is derived from the live
registry, so custom ops get editor autocomplete (and as-you-type
validation in the live editor) out of the box.
## Brushes
The reference styles consume CC0 brushes by David Revoy from
[`mypaint/mypaint-brushes`](https://github.com/mypaint/mypaint-brushes),
bundled into `ezu-paint` at compile time
([`crates/ezu-paint/src/builtin/`](https://github.com/reearth/ezu/tree/main/crates/ezu-paint/src/builtin/),
attribution in
[`builtin/CREDITS.md`](https://github.com/reearth/ezu/tree/main/crates/ezu-paint/src/builtin/CREDITS.md)). Any
MyPaint `.myb` brush works — declare it in the style's `assets` block
and the host loads it from disk or HTTP.
## License
Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) or
[MIT license](LICENSE-MIT) at your option.