pub mod dem_decode;
pub use dem_decode::{decode_dem_tile, stitch_padded_field, DemDecodeError, DemTile};
#[cfg(feature = "http")]
pub mod dem;
#[cfg(feature = "http")]
pub use dem::{bind_dem_sources, build_dem_sources, DemFetchError, DemSourceRegistry};
use std::any::Any;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use ezu_features::{mvt::DecodedTile, FeatureLayer};
use ezu_graph::{Asset, AssetError, AssetLoader, OpaqueValue, RasterBuf, ScalarField, TileId};
use hokusai::Brush;
use tiny_skia::{Pixmap, PixmapPaint, Transform};
use xxhash_rust::xxh3::Xxh3;
use crate::PaintError;
pub struct BrushBankLoader {
pub bank: HashMap<String, Arc<Brush>>,
pub brushes_dir: Option<PathBuf>,
pub images: HashMap<String, Arc<RasterBuf>>,
pub images_dir: Option<PathBuf>,
}
impl BrushBankLoader {
pub fn new() -> Self {
let mut this = Self::empty();
this.register_builtins();
this
}
pub fn empty() -> Self {
Self {
bank: HashMap::new(),
brushes_dir: None,
images: HashMap::new(),
images_dir: None,
}
}
pub fn register_builtins(&mut self) -> &mut Self {
for (name, myb_json) in crate::builtin::BUILTIN_BRUSHES {
if let Ok(brush) = hokusai::myb::from_str(myb_json) {
self.bank.insert((*name).to_string(), Arc::new(brush));
}
}
self
}
pub fn with_dir(mut self, dir: PathBuf) -> Self {
self.brushes_dir = Some(dir);
self
}
pub fn with_images_dir(mut self, dir: PathBuf) -> Self {
self.images_dir = Some(dir);
self
}
pub fn insert(&mut self, name: impl Into<String>, brush: Brush) {
self.bank.insert(name.into(), Arc::new(brush));
}
pub fn insert_image(&mut self, name: impl Into<String>, image: RasterBuf) {
self.images.insert(name.into(), Arc::new(image));
}
}
impl Default for BrushBankLoader {
fn default() -> Self {
Self::new()
}
}
impl AssetLoader for BrushBankLoader {
fn load(&self, name: &str) -> Result<Asset, AssetError> {
let src = name;
match parse_src_scheme(src)? {
SrcScheme::Builtin(key) => {
if let Some(b) = self.bank.get(key) {
return Ok(Asset::Brush(b.clone()));
}
if let Some(b) = self.bank.get(src) {
return Ok(Asset::Brush(b.clone()));
}
if let Some(img) = self.images.get(key) {
return Ok(Asset::Image(img.clone()));
}
if let Some(img) = self.images.get(src) {
return Ok(Asset::Image(img.clone()));
}
Err(AssetError::NotFound(src.to_string()))
}
SrcScheme::File(path) => {
if let Some(b) = self.bank.get(src) {
return Ok(Asset::Brush(b.clone()));
}
if let Some(img) = self.images.get(src) {
return Ok(Asset::Image(img.clone()));
}
if let Some(asset) = load_brush_file(self.brushes_dir.as_deref(), path, src)? {
return Ok(asset);
}
if let Some(asset) = load_image_file(self.images_dir.as_deref(), path, src)? {
return Ok(asset);
}
Err(AssetError::NotFound(src.to_string()))
}
SrcScheme::Http(_) => {
if let Some(b) = self.bank.get(src) {
return Ok(Asset::Brush(b.clone()));
}
if let Some(img) = self.images.get(src) {
return Ok(Asset::Image(img.clone()));
}
Err(AssetError::NotFound(src.to_string()))
}
}
}
}
#[derive(Debug, Clone, Copy)]
enum SrcScheme<'a> {
Builtin(&'a str),
File(&'a str),
Http(#[allow(dead_code)] &'a str),
}
fn parse_src_scheme(src: &str) -> Result<SrcScheme<'_>, AssetError> {
if let Some(rest) = src.strip_prefix("builtin:") {
Ok(SrcScheme::Builtin(rest))
} else if let Some(rest) = src.strip_prefix("file:") {
Ok(SrcScheme::File(rest))
} else if src.starts_with("http://") || src.starts_with("https://") {
Ok(SrcScheme::Http(src))
} else {
Err(AssetError::Other(format!(
"src `{src}` is missing a scheme — use `builtin:NAME`, `file:PATH`, or `http(s)://URL`"
)))
}
}
fn load_brush_file(
dir: Option<&std::path::Path>,
path: &str,
src: &str,
) -> Result<Option<Asset>, AssetError> {
let abs = std::path::Path::new(path);
let candidates: Vec<std::path::PathBuf> = if abs.is_absolute() {
vec![abs.to_path_buf()]
} else {
match dir {
Some(d) => {
let base = d.join(path);
vec![base.clone(), base.with_extension("myb")]
}
None => return Ok(None),
}
};
for path in &candidates {
if !path.exists() || !is_brush_extension(path) {
continue;
}
let bytes = std::fs::read_to_string(path).map_err(|e| AssetError::Decode {
src: src.to_string(),
msg: e.to_string(),
})?;
let brush = hokusai::myb::from_str(&bytes).map_err(|e| AssetError::Decode {
src: src.to_string(),
msg: e.to_string(),
})?;
return Ok(Some(Asset::Brush(Arc::new(brush))));
}
Ok(None)
}
fn load_image_file(
dir: Option<&std::path::Path>,
path: &str,
src: &str,
) -> Result<Option<Asset>, AssetError> {
let abs = std::path::Path::new(path);
let candidates: Vec<std::path::PathBuf> = if abs.is_absolute() {
vec![abs.to_path_buf()]
} else {
match dir {
Some(d) => {
let base = d.join(path);
vec![
base.clone(),
base.with_extension("png"),
base.with_extension("webp"),
]
}
None => return Ok(None),
}
};
for path in &candidates {
if !path.exists() || is_brush_extension(path) {
continue;
}
let raster = decode_image_file(path).map_err(|e| AssetError::Decode {
src: src.to_string(),
msg: e,
})?;
return Ok(Some(Asset::Image(Arc::new(raster))));
}
Ok(None)
}
fn is_brush_extension(path: &std::path::Path) -> bool {
matches!(path.extension().and_then(|s| s.to_str()), Some("myb"))
}
pub struct TileLoader<'a> {
base: &'a dyn AssetLoader,
bindings: HashMap<String, Binding>,
tile: TileId,
}
struct Binding {
asset: Asset,
hash: u128,
}
impl<'a> TileLoader<'a> {
pub fn new(base: &'a dyn AssetLoader, tile: TileId) -> Self {
Self {
base,
bindings: HashMap::new(),
tile,
}
}
pub fn bind_features(&mut self, name: impl Into<String>, layer: FeatureLayer) -> &mut Self {
let name = name.into();
let hash = self.binding_hash(&name);
let opaque: OpaqueValue = Arc::new(layer) as Arc<dyn Any + Send + Sync>;
self.bindings.insert(
name,
Binding {
asset: Asset::Features(opaque),
hash,
},
);
self
}
pub fn bind_scalar_field(&mut self, name: impl Into<String>, field: ScalarField) -> &mut Self {
let name = name.into();
let hash = self.binding_hash(&name);
self.bindings.insert(
name,
Binding {
asset: Asset::ScalarField(Arc::new(field)),
hash,
},
);
self
}
pub fn bind_mvt(&mut self, tile: DecodedTile) -> &mut Self {
for layer in tile.layers {
let key = format!("tile.{}", layer.name);
self.bind_features(key, layer);
}
self
}
fn binding_hash(&self, name: &str) -> u128 {
let mut h = Xxh3::new();
h.update(&self.tile.z.to_le_bytes());
h.update(&self.tile.x.to_le_bytes());
h.update(&self.tile.y.to_le_bytes());
h.update(name.as_bytes());
h.digest128()
}
}
impl AssetLoader for TileLoader<'_> {
fn load(&self, name: &str) -> Result<Asset, AssetError> {
if let Some(b) = self.bindings.get(name) {
return Ok(b.asset.clone());
}
self.base.load(name)
}
fn hash(&self, name: &str) -> u128 {
if let Some(b) = self.bindings.get(name) {
return b.hash;
}
self.base.hash(name)
}
}
fn decode_image_file(path: &std::path::Path) -> Result<RasterBuf, String> {
let img = image::open(path).map_err(|e| e.to_string())?.to_rgba8();
Ok(rgba_to_premul_raster(img))
}
pub fn decode_image_bytes(bytes: &[u8]) -> Result<RasterBuf, String> {
let img = image::load_from_memory(bytes)
.map_err(|e| e.to_string())?
.to_rgba8();
Ok(rgba_to_premul_raster(img))
}
fn rgba_to_premul_raster(img: image::RgbaImage) -> RasterBuf {
let (w, h) = img.dimensions();
let mut pixels = Vec::with_capacity((w * h * 4) as usize);
for px in img.pixels() {
let [r, g, b, a] = px.0;
let af = a as f32 / 255.0;
pixels.push((r as f32 * af).round() as u8);
pixels.push((g as f32 * af).round() as u8);
pixels.push((b as f32 * af).round() as u8);
pixels.push(a);
}
RasterBuf {
width: w,
height: h,
pixels,
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum PngCompression {
Fast,
#[default]
Default,
Best,
}
pub fn raster_to_png(buf: &RasterBuf, tile_size: u32, pad: u32) -> Result<Vec<u8>, PaintError> {
raster_to_png_with(buf, tile_size, pad, PngCompression::Default)
}
pub fn raster_to_png_with(
buf: &RasterBuf,
tile_size: u32,
pad: u32,
compression: PngCompression,
) -> Result<Vec<u8>, PaintError> {
if matches!(compression, PngCompression::Default) {
let padded = pixmap_from_raster(buf)?;
if pad == 0 && padded.width() == tile_size && padded.height() == tile_size {
return padded.encode_png().map_err(|_| PaintError::PngEncode);
}
let mut out = Pixmap::new(tile_size, tile_size).ok_or(PaintError::PngEncode)?;
out.draw_pixmap(
-(pad as i32),
-(pad as i32),
padded.as_ref(),
&PixmapPaint::default(),
Transform::identity(),
None,
);
return out.encode_png().map_err(|_| PaintError::PngEncode);
}
let rgba = raster_to_rgba8(buf, tile_size, pad);
encode_rgba8_png(tile_size, tile_size, &rgba, compression)
}
fn encode_rgba8_png(
width: u32,
height: u32,
straight_rgba: &[u8],
compression: PngCompression,
) -> Result<Vec<u8>, PaintError> {
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
let ct = match compression {
PngCompression::Fast => CompressionType::Fast,
PngCompression::Default => CompressionType::Default,
PngCompression::Best => CompressionType::Best,
};
let mut out = Vec::new();
let encoder = PngEncoder::new_with_quality(&mut out, ct, FilterType::Adaptive);
image::ImageEncoder::write_image(
encoder,
straight_rgba,
width,
height,
image::ExtendedColorType::Rgba8,
)
.map_err(|_| PaintError::PngEncode)?;
Ok(out)
}
fn pixmap_from_raster(buf: &RasterBuf) -> Result<Pixmap, PaintError> {
let mut p = Pixmap::new(buf.width, buf.height).ok_or(PaintError::PngEncode)?;
p.data_mut().copy_from_slice(&buf.pixels);
Ok(p)
}
pub fn raster_to_webp(buf: &RasterBuf, tile_size: u32, pad: u32) -> Result<Vec<u8>, PaintError> {
let rgba = raster_to_rgba8(buf, tile_size, pad);
encode_rgba8_webp(tile_size, tile_size, &rgba)
}
pub fn pixmap_to_webp(pixmap: &Pixmap) -> Result<Vec<u8>, PaintError> {
let (w, h) = (pixmap.width(), pixmap.height());
let mut rgba = Vec::with_capacity((w * h * 4) as usize);
for p in pixmap.pixels() {
let p = p.demultiply();
rgba.extend_from_slice(&[p.red(), p.green(), p.blue(), p.alpha()]);
}
encode_rgba8_webp(w, h, &rgba)
}
fn encode_rgba8_webp(width: u32, height: u32, straight_rgba: &[u8]) -> Result<Vec<u8>, PaintError> {
let mut out = Vec::new();
let encoder = image::codecs::webp::WebPEncoder::new_lossless(&mut out);
image::ImageEncoder::write_image(
encoder,
straight_rgba,
width,
height,
image::ExtendedColorType::Rgba8,
)
.map_err(|e| PaintError::WebpEncode(e.to_string()))?;
Ok(out)
}
pub fn raster_to_rgba8(buf: &RasterBuf, tile_size: u32, pad: u32) -> Vec<u8> {
let padded = match pixmap_from_raster(buf) {
Ok(p) => p,
Err(_) => return vec![0; (tile_size * tile_size * 4) as usize],
};
let tile_pixmap = if pad == 0 && padded.width() == tile_size && padded.height() == tile_size {
padded
} else {
let mut out = match Pixmap::new(tile_size, tile_size) {
Some(p) => p,
None => return vec![0; (tile_size * tile_size * 4) as usize],
};
out.draw_pixmap(
-(pad as i32),
-(pad as i32),
padded.as_ref(),
&PixmapPaint::default(),
Transform::identity(),
None,
);
out
};
let mut rgba = Vec::with_capacity((tile_size * tile_size * 4) as usize);
for p in tile_pixmap.pixels() {
let p = p.demultiply();
rgba.extend_from_slice(&[p.red(), p.green(), p.blue(), p.alpha()]);
}
rgba
}
#[cfg(feature = "http")]
pub async fn prefetch_doc_assets(
doc: &ezu_style::Document,
base_dir: &std::path::Path,
loader: &mut BrushBankLoader,
) -> Result<(), String> {
for (name, decl) in &doc.sources {
let _ = base_dir; match decl {
ezu_style::SourceDecl::Brush(file) => {
if !is_http_url(&file.src) {
continue;
}
if loader.bank.contains_key(&file.src) {
continue;
}
let json = http_text(&file.src)
.await
.map_err(|e| format!("brush `{name}`: {e}"))?;
let brush = hokusai::myb::from_str(&json)
.map_err(|e| format!("brush `{name}` parse: {e}"))?;
loader.insert(file.src.clone(), brush);
}
ezu_style::SourceDecl::Image(file) => {
if !is_http_url(&file.src) {
continue;
}
if loader.images.contains_key(&file.src) {
continue;
}
let bytes = http_bytes(&file.src)
.await
.map_err(|e| format!("image `{name}`: {e}"))?;
let raster = decode_image_bytes(&bytes)
.map_err(|e| format!("image `{name}` decode: {e}"))?;
loader.insert_image(file.src.clone(), raster);
}
ezu_style::SourceDecl::Mvt(_)
| ezu_style::SourceDecl::Pmtiles(_)
| ezu_style::SourceDecl::Dem(_) => {}
}
}
Ok(())
}
#[cfg(feature = "http")]
async fn http_text(url: &str) -> Result<String, String> {
reqwest::get(url)
.await
.map_err(|e| e.to_string())?
.error_for_status()
.map_err(|e| e.to_string())?
.text()
.await
.map_err(|e| e.to_string())
}
#[cfg(feature = "http")]
async fn http_bytes(url: &str) -> Result<Vec<u8>, String> {
Ok(reqwest::get(url)
.await
.map_err(|e| e.to_string())?
.error_for_status()
.map_err(|e| e.to_string())?
.bytes()
.await
.map_err(|e| e.to_string())?
.to_vec())
}
#[cfg(feature = "http")]
fn is_http_url(s: &str) -> bool {
s.starts_with("http://") || s.starts_with("https://")
}