ezu 0.6.1

Painterly cartography engine: render vector tiles as paintings (umbrella crate)
Documentation

ezu

Crates.io docs.rs CI License: MIT OR Apache-2.0

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

ezu pencil-sketch render of central Japan — © OpenStreetMap contributors, © Protomaps

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 hokusai brush 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-compatibleezu translate converts a MapLibre GL style into an ezu recipe, nodes carry raw MapLibre *-expr expression fields for data-driven styling, and the expression engine is the sister crate maplibre-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:

cargo install ezu-cli

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/):

git clone https://github.com/reearth/ezu && cd ezu

# Single tile to PNG (use `--out tile.webp` for lossless WebP).
ezu tile --style crates/ezu/examples/styles/watercolor.json \
  --tile 13/7276/3225 --out tile.png

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.
ezu tile \
  --style https://raw.githubusercontent.com/reearth/ezu/main/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`, parallel across cores.
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 (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

# 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.
ezu translate maplibre-style.json --out recipe.json
ezu translate https://example.com/style.json | ezu check /dev/stdin --no-fetch

# `--verbose` (or `-v`) enables per-node debug logs from the evaluator.
ezu --verbose tile --style style.json --tile 13/7276/3225 --out tile.png

For deeper hacking, try the tokyo example, which renders a 2×2 batch under the reference watercolor style with Rayon parallelism turned on:

cargo run --release --features parallel -p ezu --example tokyo
# 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:

ezu serve crates/ezu/examples/styles/pencil-sketch.json
# 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 hokusai brush engine: world-deterministic scatter-dab fills and brush strokes stay seamless across tile boundaries. See ezu-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 in ezu-graph.
  • MapLibre style translationezu translate lowers a MapLibre GL style into an ezu recipe: background / fill / line / circle / symbol / raster / hillshade / heatmap / fill-extrusion layers 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 in ezu-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 through maplibre-expr, a pure-Rust parser/evaluator at 100 % conformance against MapLibre's official spec fixtures, so evaluation matches MapLibre exactly.
  • Text labels — MapLibre's symbol layer, ported: rustybuzz shaping, 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-fit included). See ezu-paint.
  • Parametric styles — a params block 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 in ezu-style.
  • Custom opsNodeFactory is 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. See ezu-graph.
  • Brushes — nothing is bundled; a style names every brush through its sources block and any MyPaint .myb file works. The example styles ship CC0 brushes by David Revoy alongside their style JSON. See ezu-paint.
  • WASM + live editor — the renderer compiles to WebAssembly for in-browser rendering, and ezu serve hosts a live style editor with a tile server. See WASM and the ezu-cli README.

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:

cargo run --release -p ezu-compare -- \
  --style crates/ezu-compare/samples/protomaps-basemap.json \
  --tiles 12/3637/1613,13/7275/3225,14/14550/6452,15/29101/12904 \
  --bench --repeat 5
# 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.