ezu-cli 0.1.0

Command-line renderer for the Ezu Style Spec
# 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`](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.

| Crate | Description |
|---|---|
| [`ezu`]crates/ezu | Umbrella crate, re-exports + feature flags |
| [`ezu-core`]crates/ezu-core | Tile / world coordinates, deterministic seeding |
| [`ezu-features`]crates/ezu-features | GIS feature parsing (MVT via `geozero`, GeoJSON) — no remote fetch |
| [`ezu-style`]crates/ezu-style | Style spec parser (`serde`) — pure data, no rendering |
| [`ezu-graph`]crates/ezu-graph | Typed node-DAG evaluator (Cache, Rayon parallel) |
| [`ezu-paint`]crates/ezu-paint | Painting primitives, built-in nodes, host glue (PNG / brush bank) |
| [`ezu-wasm`]crates/ezu-wasm | WebAssembly bindings (`wasm-bindgen`) |
| [`ezu-cli`]crates/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:

```sh
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:

```sh
# 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:

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

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

```sh
# 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`](crates/ezu-paint):

- **Sources**`solid`, `circle`, `noise` (white / value / perlin /
  simplex / worley, with fBm octaves and domain warp, world-anchored
  for seamless tile borders), `features`, `brush-file`
- **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)
- **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`, `hsl` (hue rotation +
  saturation/lightness shift), `invert`, `color-to-alpha` (chroma key)
- **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.

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

```json
{
  "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`](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`](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/`](crates/ezu-paint/src/builtin/),
attribution in
[`builtin/CREDITS.md`](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.