ezu
Painterly cartography — render vector tiles as paintings — on a pure-Rust, GPU-free CPU renderer with first-class MapLibre compatibility.

ezu (絵図) is a Rust map rendering engine that turns vector tiles (MVT /
PMTiles) into raster tiles on the CPU — no GPU, no headless browser. It
does this two ways, and does both at once:
- Painterly — a declarative node-graph style language, Ezu Style,
drives the
hokusaibrush engine and a library of image-processing ops (blur, blend, warp, dither, gradients, …) to render watercolor, ink wash, ukiyo-e, and beyond, while preserving the geographic data underneath. - MapLibre-compatible —
ezu translateconverts a MapLibre GL style into an ezu recipe, nodes carry raw MapLibre*-exprexpression fields for data-driven styling, and the expression engine is the sister cratemaplibre-expr(100 % conformance against MapLibre's official spec fixtures). A Protomaps basemap MapLibre style renders end to end, labels included.
Workspace
Each crate has its own README with API details and examples.
| Crate | crates.io | Description |
|---|---|---|
ezu |
Umbrella crate, re-exports + feature flags | |
ezu-core |
Tile / world coordinates, deterministic seeding | |
ezu-features |
GIS feature parsing (MVT via geozero, GeoJSON) — no remote fetch |
|
ezu-style |
Style spec parser (serde) — pure data, no rendering |
|
ezu-graph |
Typed node-DAG evaluator (Cache, Rayon parallel) | |
ezu-paint |
Painting primitives, built-in nodes, host glue (PNG / asset banks / fonts) | |
ezu-translate |
Translate map-engine styles into ezu recipes — MapLibre GL is the first frontend | |
ezu-cli |
Command-line tool — tile / bbox / tiles rendering, translate, check validator, graph, serve live editor + tile server |
|
ezu-wasm |
— (publish = false) |
WebAssembly bindings — scalar / SIMD / threads builds for in-browser rendering |
The expression engine lives in its own repository,
reearth/maplibre-expr-rs
(published as maplibre-expr). A
publish = false internal benchmark, ezu-compare, converts a MapLibre
style, renders it with ezu, and pixel-compares against a maplibre-gl-js
reference.
Try it
Install the CLI from crates.io:
That puts an ezu binary on your PATH. Point it at any style (URL or
local path) and it renders PNGs. A style declares its own tile sources in
a sources block (MVT, PMTiles, raster DEM, RGBA raster, GeoJSON), so
most commands need nothing but a --style and a tile address; CLI flags
override anything declared there for one-off swaps.
The painterly reference styles reference their brushes by relative file:
path, so render them from a checkout (the brush files live next to the
style JSON in
crates/ezu/examples/styles/brushes/):
&&
# Single tile to PNG (use `--out tile.webp` for lossless WebP).
Styles whose assets are all remote or inline need no checkout — pass a URL directly:
# Terrain style — pulls raster DEM tiles from terrain.reearth.land.
# bbox mosaic — stitch the tiles covering a lon/lat box into one PNG.
# XYZ pyramid — bulk-render `<out>/<z>/<x>/<y>.png`, parallel across cores.
# Validate a style (parse + build graph + resolve assets). Exits
# non-zero on error — drop into a pre-commit hook / CI step.
# Translate a MapLibre GL style into an ezu recipe. The recipe is
# zoom-independent, so one recipe renders at every zoom; skipped or
# approximated layers are reported on stderr.
|
# `--verbose` (or `-v`) enables per-node debug logs from the evaluator.
For deeper hacking, try the tokyo example, which renders a 2×2 batch
under the reference watercolor style with Rayon parallelism turned on:
# Output PNGs in ./out/tokyo/
ezu serve starts the browser-based live editor — edit the style JSON
and watch the map update, schema-validated as you type, with generated
controls for the style's params:
# Open http://127.0.0.1:8080
The full command reference and the editor's feature list live in the
ezu-cli README.
Features
- Painterly node-graph styling — a style is a typed node DAG, not an
ordered layer list, and the painting ops drive the
hokusaibrush engine: world-deterministic scatter-dab fills and brush strokes stay seamless across tile boundaries. Seeezu-paint. - ~80 built-in ops — sources, rasterization, composition, warp,
colour adjustment, morphology, palette / dither, vector geometry,
gradients, terrain (DEM → hillshade / slope / hypsometric tint) and
scalar fields, all statically type-checked across seven port kinds. Full
catalog in
ezu-paint; port kinds, caching, and pad propagation inezu-graph. - MapLibre style translation —
ezu translatelowers a MapLibre GL style into an ezu recipe:background/fill/line/circle/symbol/raster/hillshade/heatmap/fill-extrusionlayers map onto ezu nodes, and the recipe is zoom-independent — zoom and data functions are emitted as raw expressions and evaluated per tile. Mapping table and known gaps inezu-translate. - MapLibre expression engine — nodes accept raw MapLibre expressions
wherever a value varies per feature (
filter-expr,fill-expr,color-expr,width-expr,radius-expr, …), hand-written or inherited from a translated style. They run throughmaplibre-expr, a pure-Rust parser/evaluator at 100 % conformance against MapLibre's official spec fixtures, so evaluation matches MapLibre exactly. - Text labels — MapLibre's
symbollayer, ported:rustybuzzshaping, outline fonts or MapLibre glyph-PBF endpoints, point / line placement, SDF glyphs with expression-driven paint. Collision is deterministic across tile boundaries and shared across every label layer, with icons placing as one unit with their text (icon-text-fitincluded). Seeezu-paint. - Parametric styles — a
paramsblock declares typed knobs that resolve at render time, so one built graph serves every combination; override them per render from the CLI, a query string, or a library call. Styles also factor repeated patterns into user-defined functions. Both inezu-style. - Custom ops —
NodeFactoryis public: register your own ops on top of the built-in registry and they inherit the served JSON Schema, so editor autocomplete and live validation come for free. Seeezu-graph. - Brushes — nothing is bundled; a style names every brush through its
sourcesblock and any MyPaint.mybfile works. The example styles ship CC0 brushes by David Revoy alongside their style JSON. Seeezu-paint. - WASM + live editor — the renderer compiles to WebAssembly for
in-browser rendering, and
ezu servehosts a live style editor with a tile server. See WASM and theezu-cliREADME.
Performance
ezu renders on the CPU with a cache-aware, optionally Rayon-parallel
evaluator. On an Apple M1 (4 performance + 4 efficiency cores), a
Protomaps basemap MapLibre style converted with ezu translate and
rendered at 512 px evaluates in ~13–30 ms per tile single-threaded
across z12–z15 — pure graph evaluation, MVT fetch/decode excluded:
# eval: z12 ~22 ms, z13 ~30 ms, z14 ~19 ms, z15 ~13 ms
End to end, a 251-tile z13–z14 pyramid (ezu tiles, parallel evaluator
across all 8 cores) renders in ~9 s wall — ~37 ms/tile amortized
including HTTP tile fetch, MVT decode, and PNG encode, which dominate
the per-tile wall at this render cost. Numbers are from this machine on
the sample style; your mileage varies with hardware, style complexity,
and network.
WASM
ezu-wasm
compiles the renderer to WebAssembly for in-browser rendering. The JS
side owns all I/O and supplies decoded bytes — MVT/PMTiles tiles, brushes,
images, fonts, DEM/raster neighbours — to a stateful Renderer through
bindSource; renderTile returns PNG, lossless WebP, or raw RGBA (blit
straight to a <canvas> via putImageData). Three builds sit side by
side under target/wasm/: scalar and SIMD (+simd128) on stable
Rust, plus a threads build that renders across Web Workers via
wasm-bindgen-rayon
(nightly to build, cross-origin-isolated page to run). A self-contained
demo page and the routes it needs ship with ezu serve. See the
ezu-wasm README
for the full JS API, build commands, and benchmarks.
License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.