#![doc(html_favicon_url = "https://zng-ui.github.io/res/zng-logo-icon.png")]
#![doc(html_logo_url = "https://zng-ui.github.io/res/zng-logo.png")]
#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
#![warn(unused_extern_crates)]
#![warn(missing_docs)]
use std::{
any::Any,
mem,
path::{Path, PathBuf},
pin::Pin,
};
use parking_lot::Mutex;
use zng_app::{
static_id,
update::UPDATES,
view_process::{
VIEW_PROCESS, VIEW_PROCESS_INITED_EVENT, ViewImageHandle,
raw_events::{
RAW_FRAME_RENDERED_EVENT, RAW_HEADLESS_OPEN_EVENT, RAW_IMAGE_DECODE_ERROR_EVENT, RAW_IMAGE_DECODED_EVENT,
RAW_IMAGE_METADATA_DECODED_EVENT, RAW_WINDOW_OR_HEADLESS_OPEN_ERROR_EVENT,
},
},
widget::{
WIDGET,
node::{IntoUiNode, UiNode, UiNodeOp, match_node},
},
window::{WINDOW, WindowId},
};
use zng_app_context::app_local;
use zng_clone_move::clmv;
use zng_layout::unit::{ByteLength, ByteUnits};
use zng_state_map::StateId;
use zng_task::channel::IpcBytes;
use zng_txt::ToTxt;
use zng_unique_id::{IdEntry, IdMap};
use zng_var::{IntoVar, Var, VarHandle, var};
use zng_view_api::{
image::{ImageDecoded, ImageRequest},
window::RenderMode,
};
mod types;
pub use types::*;
app_local! {
static IMAGES_SV: ImagesService = ImagesService::new();
}
struct ImagesService {
load_in_headless: Var<bool>,
limits: Var<ImageLimits>,
extensions: Vec<Box<dyn ImagesExtension>>,
render_windows: Option<Box<dyn ImageRenderWindowsService>>,
cache: IdMap<ImageHash, ImageVar>,
}
impl ImagesService {
pub fn new() -> Self {
Self {
load_in_headless: var(false),
limits: var(ImageLimits::default()),
extensions: vec![],
render_windows: None,
cache: IdMap::new(),
}
}
pub fn render_windows(&self) -> Box<dyn ImageRenderWindowsService> {
self.render_windows
.as_ref()
.expect("WINDOWS service not integrated with IMAGES service")
.clone_boxed()
}
}
pub struct IMAGES;
impl IMAGES {
pub fn load_in_headless(&self) -> Var<bool> {
IMAGES_SV.read().load_in_headless.clone()
}
pub fn limits(&self) -> Var<ImageLimits> {
IMAGES_SV.read().limits.clone()
}
pub fn read(&self, path: impl Into<PathBuf>) -> ImageVar {
self.image_impl(path.into().into(), ImageOptions::cache(), None)
}
#[cfg(feature = "http")]
pub fn download<U>(&self, uri: U, accept: Option<zng_txt::Txt>) -> ImageVar
where
U: TryInto<zng_task::http::Uri>,
<U as TryInto<zng_task::http::Uri>>::Error: ToTxt,
{
match uri.try_into() {
Ok(uri) => self.image_impl(ImageSource::Download(uri, accept), ImageOptions::cache(), None),
Err(e) => {
let e = e.to_txt();
tracing::debug!("cannot convert into download URI, {e}");
zng_var::const_var(ImageEntry::new_error(e))
}
}
}
pub fn from_static(&self, data: &'static [u8], format: impl Into<ImageDataFormat>) -> ImageVar {
self.image_impl((data, format.into()).into(), ImageOptions::cache(), None)
}
pub fn from_data(&self, data: IpcBytes, format: impl Into<ImageDataFormat>) -> ImageVar {
self.image_impl((data, format.into()).into(), ImageOptions::cache(), None)
}
pub fn image(&self, source: impl Into<ImageSource>, options: ImageOptions, limits: Option<ImageLimits>) -> ImageVar {
self.image_impl(source.into(), options, limits)
}
fn image_impl(&self, source: ImageSource, options: ImageOptions, limits: Option<ImageLimits>) -> ImageVar {
tracing::trace!("image request ({source:?}, {options:?}, {limits:?})");
let r = var(ImageEntry::new_loading());
let ri = r.read_only();
UPDATES.once_update("IMAGES.image", move || {
image(source, options, limits, r);
});
ri
}
pub fn image_task<F>(&self, source: impl IntoFuture<IntoFuture = F>, options: ImageOptions, limits: Option<ImageLimits>) -> ImageVar
where
F: Future<Output = ImageSource> + Send + 'static,
{
self.image_task_impl(Box::pin(source.into_future()), options, limits)
}
fn image_task_impl(
&self,
source: Pin<Box<dyn Future<Output = ImageSource> + Send + 'static>>,
options: ImageOptions,
limits: Option<ImageLimits>,
) -> ImageVar {
let r = var(ImageEntry::new_loading());
let ri = r.read_only();
zng_task::spawn(async move {
let source = source.await;
image(source, options, limits, r);
});
ri
}
pub fn register(&self, key: Option<ImageHash>, image: (ViewImageHandle, ImageDecoded)) -> ImageVar {
let r = var(ImageEntry::new_loading());
let rr = r.read_only();
UPDATES.once_update("IMAGES.register", move || {
image_view(key, image.0, image.1, None, r);
});
rr
}
pub fn clean(&self, key: ImageHash) {
UPDATES.once_update("IMAGES.clean", move || {
if let IdEntry::Occupied(e) = IMAGES_SV.write().cache.entry(key)
&& e.get().strong_count() == 1
{
e.remove();
}
});
}
pub fn purge(&self, key: ImageHash) {
UPDATES.once_update("IMAGES.purge", move || {
IMAGES_SV.write().cache.remove(&key);
});
}
pub fn cache_key(&self, image: &ImageEntry) -> Option<ImageHash> {
let key = image.cache_key?;
if IMAGES_SV.read().cache.contains_key(&key) {
Some(key)
} else {
None
}
}
pub fn is_cached(&self, image: &ImageEntry) -> bool {
match &image.cache_key {
Some(k) => IMAGES_SV.read().cache.contains_key(k),
None => false,
}
}
pub fn clean_all(&self) {
UPDATES.once_update("IMAGES.clean_all", || {
IMAGES_SV.write().cache.retain(|_, v| v.strong_count() > 1);
});
}
pub fn purge_all(&self) {
UPDATES.once_update("IMAGES.purge_all", || {
IMAGES_SV.write().cache.clear();
});
}
pub fn extend(&self, extension: Box<dyn ImagesExtension>) {
UPDATES.once_update("IMAGES.extend", move || {
IMAGES_SV.write().extensions.push(extension);
});
}
pub fn available_formats(&self) -> Vec<ImageFormat> {
let mut formats = VIEW_PROCESS.info().image.clone();
let mut exts = mem::take(&mut IMAGES_SV.write().extensions);
for ext in exts.iter_mut() {
ext.available_formats(&mut formats);
}
let mut s = IMAGES_SV.write();
exts.append(&mut s.extensions);
s.extensions = exts;
formats
}
#[cfg(feature = "http")]
fn http_accept(&self) -> zng_txt::Txt {
let mut s = String::new();
let mut sep = "";
for f in self.available_formats() {
for f in f.media_type_suffixes_iter() {
s.push_str(sep);
s.push_str("image/");
s.push_str(f);
sep = ",";
}
}
s.into()
}
}
fn image(mut source: ImageSource, mut options: ImageOptions, limits: Option<ImageLimits>, r: Var<ImageEntry>) {
let mut s = IMAGES_SV.write();
let limits = limits.unwrap_or_else(|| s.limits.get());
let mut exts = mem::take(&mut s.extensions);
drop(s); if !exts.is_empty() {
tracing::trace!("process image with {} extensions", exts.len());
}
for ext in &mut exts {
ext.image(&limits, &mut source, &mut options);
}
let mut s = IMAGES_SV.write();
exts.append(&mut s.extensions);
s.extensions = exts;
if let ImageSource::Image(var) = source {
var.set_bind(&r).perm();
r.hold(var).perm();
return;
}
if !VIEW_PROCESS.is_available() && !s.load_in_headless.get() {
tracing::debug!("ignoring image request due headless mode");
return;
}
let key = source.hash128(&options).unwrap();
match options.cache_mode {
ImageCacheMode::Ignore => (),
ImageCacheMode::Cache => {
match s.cache.entry(key) {
IdEntry::Occupied(e) => {
let var = e.get();
var.set_bind(&r).perm();
r.hold(var.clone()).perm();
return;
}
IdEntry::Vacant(e) => {
e.insert(r.clone());
}
}
}
ImageCacheMode::Retry => {
match s.cache.entry(key) {
IdEntry::Occupied(mut e) => {
let var = e.get();
if var.with(ImageEntry::is_error) {
r.set_bind(var).perm();
var.hold(r.clone()).perm();
e.insert(r.clone());
} else {
var.set_bind(&r).perm();
r.hold(var.clone()).perm();
return;
}
}
IdEntry::Vacant(e) => {
e.insert(r.clone());
}
}
}
ImageCacheMode::Reload => {
match s.cache.entry(key) {
IdEntry::Occupied(mut e) => {
let var = e.get();
r.set_bind(var).perm();
var.hold(r.clone()).perm();
e.insert(r.clone());
}
IdEntry::Vacant(e) => {
e.insert(r.clone());
}
}
}
}
drop(s);
match source {
ImageSource::Read(path) => {
fn read(path: &Path, limit: ByteLength) -> std::io::Result<IpcBytes> {
let file = std::fs::File::open(path)?;
if file.metadata()?.len() > limit.bytes() as u64 {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "file length exceeds limit"));
}
IpcBytes::from_file_blocking(file)
}
let limit = limits.max_encoded_len;
let data_format = match path.extension() {
Some(ext) => ImageDataFormat::FileExtension(ext.to_string_lossy().to_txt()),
None => ImageDataFormat::Unknown,
};
zng_task::spawn_wait(move || match read(&path, limit) {
Ok(data) => {
tracing::trace!("read {path:?}, len: {:?}, fmt: {data_format:?}", data.len().bytes());
image_data(false, Some(key), data_format, data, options, limits, r)
}
Err(e) => {
r.set(ImageEntry::new_error(e.to_txt()));
}
});
}
#[cfg(feature = "http")]
ImageSource::Download(uri, accept) => {
let accept = accept.unwrap_or_else(|| IMAGES.http_accept());
use zng_task::http::*;
async fn download(uri: Uri, accept: zng_txt::Txt, limit: ByteLength) -> Result<(ImageDataFormat, IpcBytes), Error> {
let request = Request::get(uri)?.max_length(limit).header(header::ACCEPT, accept.as_str())?;
let mut response = send(request).await?;
let data_format = match response.header().get(&header::CONTENT_TYPE).and_then(|m| m.to_str().ok()) {
Some(m) => ImageDataFormat::MimeType(m.to_txt()),
None => ImageDataFormat::Unknown,
};
let data = response.body().await?;
Ok((data_format, data))
}
let limit = limits.max_encoded_len;
zng_task::spawn(async move {
match download(uri, accept, limit).await {
Ok((fmt, data)) => {
image_data(false, Some(key), fmt, data, options, limits, r);
}
Err(e) => r.set(ImageEntry::new_error(e.to_txt())),
}
});
}
ImageSource::Data(_, data, format) => image_data(false, Some(key), format, data, options, limits, r),
ImageSource::Render(render_fn, args) => image_render(Some(key), render_fn, args, options, r),
_ => unreachable!(),
}
}
fn image_data(
is_respawn: bool,
cache_key: Option<ImageHash>,
format: ImageDataFormat,
data: IpcBytes,
options: ImageOptions,
limits: ImageLimits,
r: Var<ImageEntry>,
) {
if !is_respawn && let Some(key) = cache_key {
let mut replaced = false;
let mut exts = mem::take(&mut IMAGES_SV.write().extensions);
if !exts.is_empty() {
tracing::trace!("process image_data with {} extensions", exts.len());
}
for ext in &mut exts {
if let Some(replacement) = ext.image_data(limits.max_decoded_len, &key, &data, &format, &options) {
replacement.set_bind(&r).perm();
r.hold(replacement).perm();
replaced = true;
break;
}
}
{
let mut s = IMAGES_SV.write();
exts.append(&mut s.extensions);
s.extensions = exts;
if replaced {
tracing::trace!("extension replaced image_data");
return;
}
}
}
if !VIEW_PROCESS.is_available() {
tracing::debug!("ignoring image view request after test load due to headless mode");
return;
}
let mut request = ImageRequest::new(
format.clone(),
data.clone(),
limits.max_decoded_len.bytes() as u64,
options.downscale.clone(),
options.mask,
);
request.entries = options.entries;
if is_respawn {
request.parent = r.with(|r| r.data.meta.parent.clone());
}
if VIEW_PROCESS.is_connected()
&& let Ok(view_img) = VIEW_PROCESS.add_image(request)
{
image_view(
cache_key,
view_img,
ImageDecoded::default(),
Some((format, data, options, limits)),
r,
);
} else {
tracing::debug!("image view request failed, will retry on respawn");
let mut once = Some((format, data, options, limits, r));
VIEW_PROCESS_INITED_EVENT
.hook(move |_| {
let (format, data, options, limits, r) = once.take().unwrap();
image_data(true, cache_key, format, data, options, limits, r);
false
})
.perm();
}
}
fn image_view(
cache_key: Option<ImageHash>,
handle: ViewImageHandle,
decoded: ImageDecoded,
respawn_data: Option<(ImageDataFormat, IpcBytes, ImageOptions, ImageLimits)>,
r: Var<ImageEntry>,
) {
let mut img = r.get();
img.cache_key = cache_key;
img.handle = handle;
img.data = decoded;
let is_loaded = img.is_loaded();
let is_dummy = img.view_handle().is_dummy();
r.set(img);
if is_loaded {
image_decoded(r);
return;
}
if is_dummy {
tracing::error!("tried to register dummy handle");
return;
}
let decoding_respawn_handle = if respawn_data.is_some() {
let r_weak = r.downgrade();
let mut respawn_data = respawn_data;
VIEW_PROCESS_INITED_EVENT.hook(move |_| {
if let Some(r) = r_weak.upgrade() {
let (format, data, options, limits) = respawn_data.take().unwrap();
image_data(true, cache_key, format, data, options, limits, r);
}
false
})
} else {
VarHandle::dummy()
};
let r_weak = r.downgrade();
let decode_error_handle = RAW_IMAGE_DECODE_ERROR_EVENT.hook(move |args| match r_weak.upgrade() {
Some(r) => {
if r.with(|img| img.view_handle() == &args.handle.upgrade().unwrap()) {
tracing::debug!("image view error, {}", args.error);
r.set(ImageEntry::new_error(args.error.clone()));
false
} else {
r.with(ImageEntry::is_loading)
}
}
None => false,
});
let r_weak = r.downgrade();
let decode_meta_handle = RAW_IMAGE_METADATA_DECODED_EVENT.hook(move |args| match r_weak.upgrade() {
Some(r) => {
if r.with(|img| img.view_handle() == &args.handle.upgrade().unwrap()) {
let meta = args.meta.clone();
tracing::trace!("image view metadata decoded for request");
r.modify(move |i| i.data.meta = meta);
} else if let Some(p) = &args.meta.parent
&& p.parent == r.with(|img| img.view_handle().image_id())
{
tracing::trace!("image view metadata decoded for entry of request");
let mut entry_d = ImageDecoded::default();
entry_d.meta = args.meta.clone();
let entry = var(ImageEntry::new(None, args.handle.upgrade().unwrap(), entry_d.clone()));
r.modify(clmv!(entry, |i| i.insert_entry(entry)));
image_view(None, args.handle.upgrade().unwrap(), entry_d, None, entry);
}
r.with(ImageEntry::is_loading)
}
None => false,
});
let r_weak = r.downgrade();
RAW_IMAGE_DECODED_EVENT
.hook(move |args| {
let _hold = [&decoding_respawn_handle, &decode_error_handle, &decode_meta_handle];
match r_weak.upgrade() {
Some(r) => {
if r.with(|img| img.view_handle() == &args.handle.upgrade().unwrap()) {
let data = args.image.upgrade().unwrap();
let is_loading = data.partial.is_some();
tracing::trace!("image view decoded, partial={:?}", is_loading);
r.modify(move |i| i.data = (*data.0).clone());
if !is_loading {
image_decoded(r);
}
is_loading
} else {
r.with(ImageEntry::is_loading)
}
}
None => false,
}
})
.perm();
}
fn image_decoded(r: Var<ImageEntry>) {
let r_weak = r.downgrade();
VIEW_PROCESS_INITED_EVENT
.hook(move |_| {
if let Some(r) = r_weak.upgrade() {
let img = r.get();
if !img.is_loaded() {
return false;
}
let size = img.size();
let mut options = ImageOptions::none();
let format = match img.is_mask() {
true => {
options.mask = Some(ImageMaskMode::A);
ImageDataFormat::A8 { size }
}
false => ImageDataFormat::Bgra8 {
size,
density: img.density(),
original_color_type: img.original_color_type(),
},
};
image_data(
true,
img.cache_key,
format,
img.data.pixels.clone(),
options,
ImageLimits::none(),
r,
);
}
false
})
.perm();
}
fn image_render(
cache_key: Option<ImageHash>,
render_fn: crate::RenderFn,
args: Option<ImageRenderArgs>,
options: ImageOptions,
r: Var<ImageEntry>,
) {
let s = IMAGES_SV.read();
let windows = s.render_windows();
let windows_ctx = windows.clone_boxed();
let mask = options.mask;
windows.open_headless_window(Box::new(move || {
let ctx = ImageRenderCtx::new();
let retain = ctx.retain.clone();
WINDOW.set_state(*IMAGE_RENDER_ID, ctx);
let w = render_fn(&args.unwrap_or_default());
windows_ctx.enable_frame_capture_in_window_context(mask);
image_render_open(cache_key, WINDOW.id(), retain, r);
w
}));
}
fn image_render_open(cache_key: Option<ImageHash>, win_id: WindowId, retain: Var<bool>, r: Var<ImageEntry>) {
let r_weak = r.downgrade();
let error_handle = RAW_WINDOW_OR_HEADLESS_OPEN_ERROR_EVENT.hook(move |args| {
if args.window_id == win_id {
if let Some(r) = r_weak.upgrade() {
r.set(ImageEntry::new_error(args.error.clone()));
}
false
} else {
true
}
});
RAW_HEADLESS_OPEN_EVENT
.hook(move |args| {
let _hold = &error_handle;
args.window_id != win_id
})
.perm();
let r_weak = r.downgrade();
RAW_FRAME_RENDERED_EVENT
.hook(move |args| {
if args.window_id == win_id {
if let Some(r) = r_weak.upgrade() {
match args.frame_image.clone() {
Some(h) => {
let h = h.upgrade().unwrap();
let handle = h.0.0.clone();
let data = h.1.clone();
let retain = retain.get();
r.set(ImageEntry::new(cache_key, handle, data));
if !retain {
IMAGES_SV.read().render_windows().close_window(win_id);
image_decoded(r);
}
retain
}
None => {
r.set(ImageEntry::new_error("image render window did not capture a frame".to_txt()));
false
}
}
} else {
false
}
} else {
true
}
})
.perm();
}
impl IMAGES {
pub fn render<N, R>(&self, mask: Option<ImageMaskMode>, render: N) -> ImageVar
where
N: FnOnce() -> R + Send + Sync + 'static,
R: ImageRenderWindowRoot,
{
let render = Mutex::new(Some(render));
let source = ImageSource::render(move |_| render.lock().take().expect("IMAGES.render closure called more than once")());
let options = ImageOptions::new(ImageCacheMode::Ignore, None, mask, ImageEntriesMode::empty());
self.image_impl(source, options, None)
}
pub fn render_node(
&self,
render_mode: RenderMode,
mask: Option<ImageMaskMode>,
render: impl FnOnce() -> UiNode + Send + Sync + 'static,
) -> ImageVar {
let render = Mutex::new(Some(render));
let source = ImageSource::render_node(render_mode, move |_| {
render.lock().take().expect("IMAGES.render closure called more than once")()
});
let options = ImageOptions::new(ImageCacheMode::Ignore, None, mask, ImageEntriesMode::empty());
self.image_impl(source, options, None)
}
}
#[expect(non_camel_case_types)]
pub struct IMAGES_WINDOW;
impl IMAGES_WINDOW {
pub fn hook_render_windows_service(&self, service: Box<dyn ImageRenderWindowsService>) {
let mut img = IMAGES_SV.write();
img.render_windows = Some(service);
}
}
pub trait ImageRenderWindowsService: Send + Sync + 'static {
fn clone_boxed(&self) -> Box<dyn ImageRenderWindowsService>;
fn new_window_root(&self, node: UiNode, render_mode: RenderMode) -> Box<dyn ImageRenderWindowRoot>;
fn set_parent_in_window_context(&self, parent_id: WindowId);
fn enable_frame_capture_in_window_context(&self, mask: Option<ImageMaskMode>);
fn open_headless_window(&self, new_window_root: Box<dyn FnOnce() -> Box<dyn ImageRenderWindowRoot> + Send>);
fn close_window(&self, window_id: WindowId);
}
pub trait ImageRenderWindowRoot: Send + Any + 'static {}
#[expect(non_camel_case_types)]
pub struct IMAGE_RENDER;
impl IMAGE_RENDER {
pub fn is_in_render(&self) -> bool {
WINDOW.contains_state(*IMAGE_RENDER_ID)
}
pub fn retain(&self) -> Var<bool> {
WINDOW.req_state(*IMAGE_RENDER_ID).retain
}
}
#[zng_app::widget::property(CONTEXT, default(false))]
pub fn render_retain(child: impl IntoUiNode, retain: impl IntoVar<bool>) -> UiNode {
let retain = retain.into_var();
match_node(child, move |_, op| {
if let UiNodeOp::Init = op {
if IMAGE_RENDER.is_in_render() {
let actual_retain = IMAGE_RENDER.retain();
actual_retain.set_from(&retain);
let handle = actual_retain.bind(&retain);
WIDGET.push_var_handle(handle);
} else {
tracing::error!("can only set `render_retain` in render widgets")
}
}
})
}
#[derive(Clone)]
struct ImageRenderCtx {
retain: Var<bool>,
}
impl ImageRenderCtx {
fn new() -> Self {
Self { retain: var(false) }
}
}
static_id! {
static ref IMAGE_RENDER_ID: StateId<ImageRenderCtx>;
}