Skip to content

Use from Rust

Cargo.toml
[dependencies]
ezu = { version = "0.6", features = ["parallel"] }

The umbrella crate re-exports the workspace: ezu::style, ezu::graph, ezu::paint, ezu::features, ezu::core. API detail lives on docs.rs; this page is the shape of a host.

Parse once, build once, render per tile:

let doc = ezu::style::Document::from_json(&json)?;
let registry = ezu::paint::nodes::default_registry();
let graph = ezu::graph::build_graph(&doc, &registry)?; // once per style
let cache = ezu::graph::Cache::new(); // shared across tiles
let base = ezu::paint::host::BrushBankLoader::default();
let tile_id = ezu::graph::TileId { z: 13, x: 7276, y: 3225 };
// Per-tile bindings overlay the document-scoped bank.
let mut tile_loader = ezu::paint::host::TileLoader::new(&base, tile_id);
tile_loader.bind_mvt(ezu::features::mvt::decode(&bytes)?);
// The style's `pad` is a floor; the graph knows how far its filters
// reach. Sizing the canvas from both is what the CLI and the wasm host do.
let canvas = ezu::graph::CanvasInfo {
tile_size: doc.tile_size,
pad: doc.pad.max(graph.required_pad()?),
};
let ev = ezu::graph::Evaluator::new(&graph, &cache, &tile_loader);
let out = ev.render_parallel(
tile_id,
canvas,
&ezu::graph::ParamValues::new(),
/* rng_seed */ 0,
)?;

build_graph is where everything static is checked: every @ref resolves, ports type-check, there are no cycles, and every field that decides padding carries a static bound. Failures come back as BuildGraphError with the offending node id attached — surface that string, it is the useful part.

ezu never fetches anything itself. External data enters through one trait:

pub trait AssetLoader: Send + Sync {
fn load(&self, name: &str) -> Result<Asset, AssetError>;
fn hash(&self, _name: &str) -> u128 { 0 } // for cache invalidation
}

Think of it as shader uniforms: the document declares which bindings its source nodes sample, your host fills them, the evaluator stitches it together. Names beginning with tile. are tile-scoped by convention and rebound per render; bare names are document-scoped (brush and image banks).

Return a real hash() for anything that can change. The evaluator folds each binding’s hash into the consuming node’s cache key, so a changed binding invalidates every dependent entry without the node having to think about it.

For the common cases you do not need to implement the trait yourself: ezu::paint::host ships BrushBankLoader, TileLoader, prefetch_doc_assets, DEM/raster source registries, and PNG/WebP encoders.

Bind and render, as the JS and Go packages do

Section titled “Bind and render, as the JS and Go packages do”

The lifecycle above is the whole machine. When your host already does its own fetching and only wants to bind bytes and get pixels back, ezu::renderer is the same embedding API the JS and Go packages wrap, with the same calls:

use ezu::renderer::{BindOptions, RenderOptions, Renderer};
let mut r = Renderer::new(&style_json)?;
r.bind_source("basemap", mvt_bytes, &BindOptions::default())?;
let png = r.render_tile(13, 7276, 3225, RenderOptions::default())?;
r.clear_sources();

A page at a print scale is compute_frame and render_frame, which render the tiles the page covers at the tile size that meets the scale and crop them into one RGBA page, the same as ezu frame:

use ezu::renderer::FrameRequest;
let f = r.compute_frame(&FrameRequest {
center_lon: 139.70, center_lat: 35.68,
scale: 1000.0, dpi: 200.0,
width: 1929, height: 1370, // 245 × 174 mm at 200 dpi
zoom: None,
style_tile_size: r.tile_size(),
})?;
let page = r.render_frame(&f, RenderOptions::default(), |b, tile| {
let mvt = fetch_mvt(tile.z, tile.x, tile.y)?; // your fetch, your error type
b.bind_source("basemap", mvt, &BindOptions::default())?;
Ok::<_, MyError>(())
})?;

The bind gets a FrameBinder for the tile it is binding. render_frame holds the renderer by &mut for the whole frame, so that binder is the only way to bind while it runs — the borrow checker does what the JS and Go packages check at run time.

Evaluator::render walks the topological order sequentially. render_parallel (behind the parallel feature) buckets nodes by longest-path depth — no two nodes in a bucket share an edge — and fans each bucket across Rayon’s pool. On a wide watercolour graph that is the difference between 6.3 s and 1.3 s of wall clock for four tiles.

Leave parallel off for wasm; turn it on for servers and CLIs.

One built graph serves every parameter combination:

let mut params = ezu::graph::ParamValues::new();
params.set("softness".into(), ezu::graph::parse_param_value(&doc.params, "softness", "2")?);
let out = ev.render_parallel(tile_id, canvas, &params, 0)?;

The cache keys on the values each node actually reads, so flipping one param re-evaluates only the nodes downstream of it.

let cache = ezu::graph::Cache::with_capacity(4096); // entries, not bytes

A Mutex-protected LRU keyed by a Merkle-style content hash over (canvas, tile, node param hash + asset hashes, input hashes). Hits are cheap to clone because heavy payloads sit behind Arc. Keep one cache per style for the life of that style; throw it away when the style changes.

NodeFactory is public, so a downstream crate can register ops on top of the built-in registry and inherit the JSON Schema (and therefore editor completion) automatically. See custom ops.

Map renders on this site are made fromOpenStreetMap data viaProtomaps (© OpenStreetMap contributors), elevation from Re:Earth Terrain,Mapterhorn andEGM2008 (NGA), and aerial imagery from GSI Japan(© 国土地理院). The painterly styles use CC0 brushes byDavid Revoy.