ezu-cli 0.1.0

Command-line renderer for the Ezu Style Spec
ezu-cli-0.1.0 is not a library.

ezu

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 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.

Crate 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 / brush bank)
ezu-wasm WebAssembly bindings (wasm-bindgen)
ezu-cli Command-line tool — tile / bbox / tiles rendering, check style validator, serve live editor + tile server

Try it

Install the CLI directly from GitHub — no clone, no git, just one command:

cargo install --git https://github.com/reearth/ezu ezu-cli

That puts an ezu binary on your PATH. Point it at any style (URL or local path) and any tile source (PMTiles URL/file, an {z}/{x}/{y} MVT URL/path, or a TileJSON) and it spits out PNGs:

# Single tile to PNG (use `--out tile.webp` for lossless WebP).
# The reference style uses built-in brushes bundled with `ezu-paint`,
# so no `--assets-dir` is needed.
ezu tile \
  --style https://raw.githubusercontent.com/reearth/ezu/main/crates/ezu/examples/styles/watercolor-basic.json \
  --pmtiles https://build.protomaps.com/20260520.pmtiles \
  --tile 13/7276/3225 --out tile.png

# bbox mosaic — stitch the tiles covering a lon/lat box into one PNG
ezu bbox --style URL_OR_PATH --pmtiles 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 --pmtiles 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

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:

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

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).

The WASM demo (single-tile render in the browser, scalar vs SIMD switch):

# Build both flavors
cd crates/ezu-wasm
wasm-pack build --target web --release --out-dir ../../target/wasm/scalar
RUSTFLAGS="-C target-feature=+simd128" \
  wasm-pack build --target web --release --out-dir ../../target/wasm/simd

# Serve everything (editor, demo, brushes, /mvt) and open the demo
ezu serve
# http://127.0.0.1:8080/wasm-demo/

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 (Features / Raster / Brush / Scalar); 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:

  • Sourcessolid, circle, noise (white / value / perlin / simplex / worley, with fBm octaves and domain warp, world-anchored for seamless tile borders), features, brush-file
  • Rasterizationfill-solid (tiny-skia + libblur), fill-dabs (hokusai scatter-dab fill, world-deterministic so dabs stay seamless across tile boundaries), line (hokusai stroke along polylines)
  • Compositionblur (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)
  • Warpdisplace (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
  • Adjustmentbrightness-contrast, hsl (hue rotation + saturation/lightness shift), invert, color-to-alpha (chroma key)
  • Gradientsgradient-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.

Example: a watercolor water layer with a brushed road on top of an earth-tone background.

{
  "name": "demo",
  "tile-size": 512,
  "pad": 24,
  "assets": { "glazing": { "type": "brush", "src": "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.

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 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, bundled into ezu-paint at compile time (crates/ezu-paint/src/builtin/, attribution in 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 or MIT license at your option.