#[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;
let key = src.strip_prefix('@').unwrap_or(src);
if let Some(b) = self.bank.get(key) {
return Ok(Asset::Brush(b.clone()));
}
if let Some(dir) = &self.brushes_dir {
let candidates = [dir.join(key), dir.join(format!("{key}.myb"))];
for path in &candidates {
if path.exists() {
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(Asset::Brush(Arc::new(brush)));
}
}
}
if let Some(img) = self.images.get(key) {
return Ok(Asset::Image(img.clone()));
}
if let Some(dir) = &self.images_dir {
let candidates = [
dir.join(key),
dir.join(format!("{key}.png")),
dir.join(format!("{key}.webp")),
];
for path in &candidates {
if path.exists() {
let raster = decode_image_file(path).map_err(|e| AssetError::Decode {
src: src.to_string(),
msg: e,
})?;
return Ok(Asset::Image(Arc::new(raster)));
}
}
}
Err(AssetError::NotFound(src.to_string()))
}
}
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.assets {
match decl.kind {
ezu_style::AssetKind::Brush => {
if loader.bank.contains_key(&decl.src) {
continue;
}
let json = fetch_asset_text(&decl.src, base_dir, "myb")
.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(decl.src.clone(), brush);
}
ezu_style::AssetKind::Image | ezu_style::AssetKind::MaskImage => {
if loader.images.contains_key(&decl.src) {
continue;
}
let bytes = fetch_asset_bytes(&decl.src, base_dir, "png")
.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(decl.src.clone(), raster);
}
ezu_style::AssetKind::Gradient => {}
}
}
Ok(())
}
#[cfg(feature = "http")]
async fn fetch_asset_text(
src: &str,
base_dir: &std::path::Path,
default_ext: &str,
) -> Result<String, String> {
if is_http_url(src) {
Ok(reqwest::get(src)
.await
.map_err(|e| e.to_string())?
.error_for_status()
.map_err(|e| e.to_string())?
.text()
.await
.map_err(|e| e.to_string())?)
} else {
let path = resolve_with_ext(base_dir, src, default_ext)
.ok_or_else(|| format!("no file at {}", base_dir.join(src).display()))?;
std::fs::read_to_string(&path).map_err(|e| format!("reading {}: {e}", path.display()))
}
}
#[cfg(feature = "http")]
async fn fetch_asset_bytes(
src: &str,
base_dir: &std::path::Path,
default_ext: &str,
) -> Result<Vec<u8>, String> {
if is_http_url(src) {
Ok(reqwest::get(src)
.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())
} else {
let path = resolve_with_ext(base_dir, src, default_ext)
.ok_or_else(|| format!("no file at {}", base_dir.join(src).display()))?;
std::fs::read(&path).map_err(|e| format!("reading {}: {e}", path.display()))
}
}
#[cfg(feature = "http")]
fn is_http_url(s: &str) -> bool {
s.starts_with("http://") || s.starts_with("https://")
}
#[cfg(feature = "http")]
fn resolve_with_ext(base: &std::path::Path, src: &str, ext: &str) -> Option<std::path::PathBuf> {
let direct = base.join(src);
if direct.exists() {
return Some(direct);
}
let with_ext = base.join(format!("{src}.{ext}"));
with_ext.exists().then_some(with_ext)
}