use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use crate::image_cache::{ImageCache, LoadedImage, decode};
pub trait RepaintSignal: Clone + Send + 'static {
fn notify(&self);
}
fn resolve_local_image(doc_dir: Option<&Path>, url: &str) -> Option<PathBuf> {
let path = Path::new(url.strip_prefix("file://").unwrap_or(url));
if path.is_absolute() {
Some(path.to_path_buf())
} else {
doc_dir.map(|d| d.join(path))
}
}
static IMAGE_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(reqwest::Client::new);
fn load_local_image(doc_dir: Option<&Path>, url: &str) -> Option<LoadedImage> {
resolve_local_image(doc_dir, url)
.and_then(|p| std::fs::read(p).ok())
.and_then(|bytes| decode(&bytes))
}
async fn fetch_remote_image(url: &str) -> Option<Vec<u8>> {
let resp = match IMAGE_CLIENT
.get(url)
.header("User-Agent", "writ")
.send()
.await
.and_then(|r| r.error_for_status())
{
Ok(r) => r,
Err(e) => {
eprintln!("[writ] image fetch failed ({url}): {e}");
return None;
}
};
resp.bytes().await.ok().map(|b| b.to_vec())
}
fn decode_image_bytes(url: &str, bytes: &[u8]) -> Option<LoadedImage> {
let img = decode(bytes);
if img.is_none() {
eprintln!(
"[writ] image decode failed ({url}): {} bytes, unsupported or malformed image",
bytes.len()
);
}
img
}
async fn load_remote_image(url: &str) -> Option<LoadedImage> {
let bytes = fetch_remote_image(url).await?;
decode_image_bytes(url, &bytes)
}
pub fn spawn_image_loads(
doc_dir: Option<PathBuf>,
urls: &[String],
cache: &ImageCache,
runtime: &tokio::runtime::Handle,
signal: impl RepaintSignal,
) {
for url in urls {
if cache.contains(url) {
continue; }
cache.mark_loading(url);
let cache = cache.clone();
let signal = signal.clone();
let url = url.clone();
if url.starts_with("http://") || url.starts_with("https://") {
runtime.spawn(async move {
let loaded = match fetch_remote_image(&url).await {
Some(bytes) => {
let u = url.clone();
tokio::task::spawn_blocking(move || decode_image_bytes(&u, &bytes))
.await
.ok()
.flatten()
}
None => None,
};
cache.store_render(&url, loaded.ok_or(None));
signal.notify();
});
} else {
let doc_dir = doc_dir.clone();
runtime.spawn_blocking(move || {
cache.store_render(&url, load_local_image(doc_dir.as_deref(), &url).ok_or(None));
signal.notify();
});
}
}
}
pub fn load_local_images_blocking(doc_dir: Option<&Path>, urls: &[String], cache: &ImageCache) {
for url in urls {
let loaded = if url.starts_with("http://") || url.starts_with("https://") {
tokio::runtime::Handle::current().block_on(load_remote_image(url))
} else {
load_local_image(doc_dir, url)
};
cache.store_render(url, loaded.ok_or(None));
}
}