Use from Rust
[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.
The lifecycle
Section titled “The lifecycle”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, ®istry)?; // once per stylelet cache = ezu::graph::Cache::new(); // shared across tileslet 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::square( doc.tile_size, 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.
You own the I/O
Section titled “You own the I/O”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();The renderer builds the style’s graph once and keeps it: warnings() lists what
the build warned about, and graph() hands over the graph itself — what it
reads, the canvas margin it needs — so a host that reports on the style does not
build it a second time. The graph is Rust-only; the JS and Go packages have
warnings and their own calls for the rest.
Built with the http feature, a native host can also leave the style’s own
assets to the renderer. resolve_assets(base_dir) reads or fetches every brush,
image, sprite and font the style declares, file: paths against base_dir, and
gives each glyphs source a stack that reads a range the first time a label needs
it, during the render, which is how the ezu CLI renders. Tile data is still
yours to fetch and load. The JS and Go packages have no such call: a browser or
a wasm module cannot do I/O from inside a render, so there every source is
bound.
let mut r = Renderer::new(&style_json)?;r.resolve_assets(Path::new("styles/")).await?;A retina tile is one option away. pixel_ratio: 2.0 renders the same map at
twice the density — twice the tile size and margin, every width, text size and
blur the style gives in pixels doubled, the same labels in the same spots — as
MapLibre’s @2x tiles are. It takes 1 to 4, fractions included. tile_size is
a different knob: a larger tile at the same pixel sizes, which draws finer.
let retina = r.render_tile(13, 7276, 3225, RenderOptions { pixel_ratio: 2.0, ..RenderOptions::default()})?;Driving the graph yourself, the same comes from a canvas that says so: its sizes
in device pixels and the ratio beside them, as
CanvasInfo::square(2 * tile_size, 2 * pad).with_pixel_ratio(2.0). Nodes scale
their pixel fields by ctx.px, so an op of your own does the same.
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 std::collections::HashMap;use ezu::renderer::{FrameRequest, SourceSet};
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 mut loaded = HashMap::new(); // each fetched tile, decoded oncelet page = r.render_frame(&f, RenderOptions::default(), |b, tile| { let mut set = SourceSet::new(); for (dx, dy) in [(0, 0)].into_iter().chain(b.requested_neighbor_offsets("basemap")?) { let at = (tile.x as i32 + dx, tile.y as i32 + dy); if !loaded.contains_key(&at) { let mvt = fetch_mvt(tile.z, at.0, at.1)?; // your fetch, your error type loaded.insert(at, b.load_source("basemap", &mvt)?); } set.bind(&loaded[&at], &BindOptions { coord: (dx, dy), ..BindOptions::default() })?; } Ok::<_, MyError>(set)})?;Each tile renders from the SourceSet its callback returns, as
render_tile_with does. The tiles of a frame border each other, so keeping
each SourceData and putting it into the set of every tile that reads it
decodes each fetched tile once. The callback also gets a FrameBinder, which
loads data, answers the queries a fetch needs, and binds into the renderer what
a set turns out to need — the glyphs needed_codepoints_in lists.
render_frame holds the renderer by &mut for the whole frame, so that binder
is the only way to change it while the frame runs — the borrow checker does
what the JS and Go packages check at run time.
A render can also hand back what a features node held — the labels and the
boxes placement gave them, the features a filter kept — next to the image.
render_tile_with_features takes the ids of the nodes to report on and returns
one NodeFeatures per id, computed on the way to the pixels rather than by a
second render. Each holds the node’s FilteredFeatures in tile units, and
to_geojson() writes it as a GeoJSON FeatureCollection in those units, which
is what the JS and Go packages hand back:
let (png, features) = r.render_tile_with_features(z, x, y, &set, RenderOptions::default(), &["boxes"])?;let geojson = features[0].to_geojson(); // x right, y down, 0..extent across the tileto_geojson_in writes the same collection in other coordinates:
TileFeatureCoords::LonLat in WGS84, the numbers a page holding the tile
writes, or TileFeatureCoords::Px in the rendered tile’s pixels, at the tile
size the render drew at. TileFeatureCoords::Tile is to_geojson itself.
use ezu::renderer::TileFeatureCoords;let geojson = features[0].to_geojson_in(TileFeatureCoords::LonLat);render_frame_with_features does the same over a page: it gathers each node’s
features from every tile, drops the ones wholly off the page, and, given a key —
properties that identify a feature, label-id and part for label-boxes —
keeps one copy of a feature that several tiles saw. Each PageFeatures writes
the file ezu frame --features writes, in longitude and latitude or page
pixels, byte for byte:
use ezu::renderer::{FeatureCoords, FrameFeatureNode, FrameFeatures};
let req = FrameFeatures { nodes: vec![FrameFeatureNode::keyed("boxes", ["label-id", "part"])], coords: FeatureCoords::LonLat,};let (page, features) = r.render_frame_with_features(&f, RenderOptions::default(), &req, sources)?;std::fs::write("labels.geojson", features[0].to_geojson())?;A host that renders a frame’s tiles its own way — several at once over one
&Renderer — gets the same assembly from frame_features: a collector it
feeds each tile’s render_tile_with_features result into, in any order, and
finishes into the same pages.
Serial or parallel
Section titled “Serial or parallel”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.
Params at render time
Section titled “Params at render time”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, ¶ms, 0)?;The cache keys on the values each node actually reads, so flipping one param re-evaluates only the nodes downstream of it.
Cache sizing
Section titled “Cache sizing”let cache = ezu::graph::Cache::with_capacity(4096); // entries, not bytesA 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. ezu::renderer keeps
its own, 8 MiB of pixels unless set_cache_budget says otherwise.
Your own ops
Section titled “Your own ops”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.