Skip to main content

zng_ext_image/
lib.rs

1#![doc(html_favicon_url = "https://zng-ui.github.io/res/zng-logo-icon.png")]
2#![doc(html_logo_url = "https://zng-ui.github.io/res/zng-logo.png")]
3//!
4//! Image loading, rendering and cache.
5//!
6//! # Services
7//!
8//! Services this extension provides.
9//!
10//! * [`IMAGES`]
11//!
12//! # Crate
13//!
14#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
15#![warn(unused_extern_crates)]
16#![warn(missing_docs)]
17
18use std::{any::Any, path::PathBuf, pin::Pin};
19
20use zng_app::{
21    static_id,
22    update::UPDATES,
23    view_process::{
24        VIEW_PROCESS, VIEW_PROCESS_INITED_EVENT, ViewImageHandle,
25        raw_events::{
26            RAW_FRAME_RENDERED_EVENT, RAW_HEADLESS_OPEN_EVENT, RAW_IMAGE_DECODE_ERROR_EVENT, RAW_IMAGE_DECODED_EVENT,
27            RAW_IMAGE_METADATA_DECODED_EVENT, RAW_WINDOW_OR_HEADLESS_OPEN_ERROR_EVENT,
28        },
29    },
30    widget::{
31        WIDGET,
32        node::{IntoUiNode, UiNode, UiNodeOp, match_node},
33    },
34    window::{WINDOW, WindowId},
35};
36use zng_app_context::app_local;
37use zng_clone_move::clmv;
38use zng_layout::unit::{ByteLength, ByteUnits};
39use zng_state_map::StateId;
40use zng_task::channel::{IpcBytes, IpcReadHandle};
41use zng_task::parking_lot::Mutex;
42use zng_txt::ToTxt;
43use zng_unique_id::{IdEntry, IdMap};
44use zng_var::{IntoVar, Var, VarHandle, var};
45use zng_view_api::{
46    image::{ImageDecoded, ImageRequest},
47    window::RenderMode,
48};
49
50mod types;
51pub use types::*;
52
53app_local! {
54    static IMAGES_SV: ImagesService = ImagesService::new();
55    static IMAGES_EXTENSIONS: Vec<Box<dyn ImagesExtension>> = vec![];
56}
57
58struct ImagesService {
59    load_in_headless: Var<bool>,
60    limits: Var<ImageLimits>,
61
62    render_windows: Option<Box<dyn ImageRenderWindowsService>>,
63
64    cache: IdMap<ImageHash, ImageVar>,
65}
66impl ImagesService {
67    pub fn new() -> Self {
68        Self {
69            load_in_headless: var(false),
70            limits: var(ImageLimits::default()),
71
72            render_windows: None,
73
74            cache: IdMap::new(),
75        }
76    }
77
78    pub fn render_windows(&self) -> Box<dyn ImageRenderWindowsService> {
79        self.render_windows
80            .as_ref()
81            .expect("WINDOWS service not integrated with IMAGES service")
82            .clone_boxed()
83    }
84}
85
86/// Image loading, cache and render service.
87///
88/// If the app is running without a [`VIEW_PROCESS`] all images are dummy, see [`load_in_headless`] for
89/// details.
90///
91/// [`load_in_headless`]: IMAGES::load_in_headless
92/// [`VIEW_PROCESS`]: zng_app::view_process::VIEW_PROCESS
93pub struct IMAGES;
94impl IMAGES {
95    /// If should still download/read image bytes in headless/renderless mode.
96    ///
97    /// When an app is in headless mode without renderer no [`VIEW_PROCESS`] is available, so
98    /// images cannot be decoded, in this case all images are dummy loading and no attempt
99    /// to download/read the image files is made. You can enable loading in headless tests to detect
100    /// IO errors, in this case if there is an error acquiring the image file the image will be a
101    /// dummy with error.
102    ///
103    /// [`VIEW_PROCESS`]: zng_app::view_process::VIEW_PROCESS
104    pub fn load_in_headless(&self) -> Var<bool> {
105        IMAGES_SV.read().load_in_headless.clone()
106    }
107
108    /// Default loading and decoding limits for each image.
109    pub fn limits(&self) -> Var<ImageLimits> {
110        IMAGES_SV.read().limits.clone()
111    }
112
113    /// Request an image, reads from a `path` and caches it.
114    ///
115    /// This is shorthand for calling [`IMAGES.image`] with [`ImageSource::Read`] and [`ImageOptions::cache`].
116    ///
117    /// [`IMAGES.image`]: IMAGES::image
118    pub fn read(&self, path: impl Into<PathBuf>) -> ImageVar {
119        self.image_impl(path.into().into(), ImageOptions::cache(), None)
120    }
121
122    /// Request an image, downloads from an `uri` and caches it.
123    ///
124    /// Optionally define the HTTP ACCEPT header, if not set all image formats supported by the view-process
125    /// backend are accepted.
126    ///
127    /// This is shorthand for calling [`IMAGES.image`] with [`ImageSource::Download`] and [`ImageOptions::cache`].
128    ///
129    /// [`IMAGES.image`]: IMAGES::image
130    #[cfg(feature = "http")]
131    pub fn download<U>(&self, uri: U, accept: Option<zng_txt::Txt>) -> ImageVar
132    where
133        U: TryInto<zng_task::http::Uri>,
134        <U as TryInto<zng_task::http::Uri>>::Error: ToTxt,
135    {
136        match uri.try_into() {
137            Ok(uri) => self.image_impl(ImageSource::Download(uri, accept), ImageOptions::cache(), None),
138            Err(e) => {
139                let e = e.to_txt();
140                tracing::debug!("cannot convert into download URI, {e}");
141                zng_var::const_var(ImageEntry::new_error(e))
142            }
143        }
144    }
145
146    /// Request an image from `&'static [u8]` data.
147    ///
148    /// The data can be any of the formats described in [`ImageDataFormat`].
149    ///
150    /// This is shorthand for calling [`IMAGES.image`] with [`ImageSource::Data`] and [`ImageOptions::cache`].
151    ///
152    /// # Examples
153    ///
154    /// Get an image from a PNG file embedded in the app executable using [`include_bytes!`].
155    ///
156    /// ```
157    /// # use zng_ext_image::*;
158    /// # macro_rules! include_bytes { ($tt:tt) => { &[] } }
159    /// # fn demo() {
160    /// let image_var = IMAGES.from_static(include_bytes!("ico.png"), "png");
161    /// # }
162    /// ```
163    ///
164    /// [`IMAGES.image`]: IMAGES::image
165    pub fn from_static(&self, data: &'static [u8], format: impl Into<ImageDataFormat>) -> ImageVar {
166        self.image_impl((data, format.into()).into(), ImageOptions::cache(), None)
167    }
168
169    /// Get a cached image from shared data.
170    ///
171    /// The data can be any of the formats described in [`ImageDataFormat`].
172    ///
173    /// This is shorthand for calling [`IMAGES.image`] with [`ImageSource::Data`] and [`ImageOptions::cache`].
174    ///
175    /// [`IMAGES.image`]: IMAGES::image
176    pub fn from_data(&self, data: IpcBytes, format: impl Into<ImageDataFormat>) -> ImageVar {
177        self.image_impl((data, format.into()).into(), ImageOptions::cache(), None)
178    }
179
180    /// Request an image, with full load and cache configuration.
181    ///
182    /// If `limits` is `None` the [`IMAGES.limits`] is used.
183    ///
184    /// Always returns a *loading* image due to the deferred nature of services. If the image is already in cache
185    /// it will be set and bound to it once the current update finishes.
186    ///
187    /// [`IMAGES.limits`]: IMAGES::limits
188    pub fn image(&self, source: impl Into<ImageSource>, options: ImageOptions, limits: Option<ImageLimits>) -> ImageVar {
189        self.image_impl(source.into(), options, limits)
190    }
191    fn image_impl(&self, source: ImageSource, options: ImageOptions, limits: Option<ImageLimits>) -> ImageVar {
192        tracing::trace!("image request ({source:?}, {options:?}, {limits:?})");
193        let r = var(ImageEntry::new_loading());
194        let ri = r.read_only();
195        UPDATES.once_update("IMAGES.image", move || {
196            image(source, options, limits, r);
197        });
198        ri
199    }
200
201    /// Await for an image source, then get or load the image.
202    ///
203    /// If `limits` is `None` the [`IMAGES.limits`] is used.
204    ///
205    /// This method returns immediately with a loading [`ImageVar`], when `source` is ready it
206    /// is used to get the actual [`ImageVar`] and binds it to the returned image.
207    ///
208    /// Note that the [`cache_mode`] always applies to the inner image, and only to the return image if `cache_key` is set.
209    ///
210    /// [`IMAGES.limits`]: IMAGES::limits
211    /// [`cache_mode`]: ImageOptions::cache_mode
212    pub fn image_task<F>(&self, source: impl IntoFuture<IntoFuture = F>, options: ImageOptions, limits: Option<ImageLimits>) -> ImageVar
213    where
214        F: Future<Output = ImageSource> + Send + 'static,
215    {
216        self.image_task_impl(Box::pin(source.into_future()), options, limits)
217    }
218    fn image_task_impl(
219        &self,
220        source: Pin<Box<dyn Future<Output = ImageSource> + Send + 'static>>,
221        options: ImageOptions,
222        limits: Option<ImageLimits>,
223    ) -> ImageVar {
224        let r = var(ImageEntry::new_loading());
225        let ri = r.read_only();
226        zng_task::spawn(async move {
227            let source = source.await;
228            image(source, options, limits, r);
229        });
230        ri
231    }
232
233    /// Associate the `image` produced by direct interaction with the view-process with the `key` in the cache.
234    ///
235    /// Returns an image var that tracks the image, note that if the `key` is already known does not use the `image` data.
236    ///
237    /// Note that you can register entries in [`ImageEntry::insert_entry`], this method is only for tracking a new entry.
238    ///
239    /// Note that the image will not automatically restore on respawn if the view-process fails while decoding.
240    pub fn register(&self, key: Option<ImageHash>, image: (ViewImageHandle, ImageDecoded)) -> ImageVar {
241        let r = var(ImageEntry::new_loading());
242        let rr = r.read_only();
243        UPDATES.once_update("IMAGES.register", move || {
244            image_view(key, image.0, image.1, None, r);
245        });
246        rr
247    }
248
249    /// Remove the image from the cache, if it is only held by the cache.
250    ///
251    /// You can use [`ImageSource::hash128_read`] and [`ImageSource::hash128_download`] to get the `key`
252    /// for files or downloads.
253    pub fn clean(&self, key: ImageHash) {
254        UPDATES.once_update("IMAGES.clean", move || {
255            if let IdEntry::Occupied(e) = IMAGES_SV.write().cache.entry(key)
256                && e.get().strong_count() == 1
257            {
258                e.remove();
259            }
260        });
261    }
262
263    /// Remove the image from the cache, even if it is still referenced outside of the cache.
264    ///
265    /// You can use [`ImageSource::hash128_read`] and [`ImageSource::hash128_download`] to get the `key`
266    /// for files or downloads.
267    pub fn purge(&self, key: ImageHash) {
268        UPDATES.once_update("IMAGES.purge", move || {
269            IMAGES_SV.write().cache.remove(&key);
270        });
271    }
272
273    /// Gets the cache key of an image.
274    pub fn cache_key(&self, image: &ImageEntry) -> Option<ImageHash> {
275        let key = image.cache_key?;
276        if IMAGES_SV.read().cache.contains_key(&key) {
277            Some(key)
278        } else {
279            None
280        }
281    }
282
283    /// If the image is cached.
284    pub fn is_cached(&self, image: &ImageEntry) -> bool {
285        match &image.cache_key {
286            Some(k) => IMAGES_SV.read().cache.contains_key(k),
287            None => false,
288        }
289    }
290
291    /// Clear cached images that are not referenced outside of the cache.
292    pub fn clean_all(&self) {
293        UPDATES.once_update("IMAGES.clean_all", || {
294            IMAGES_SV.write().cache.retain(|_, v| v.strong_count() > 1);
295        });
296    }
297
298    /// Clear all cached images, including images that are still referenced outside of the cache.
299    ///
300    /// Image memory only drops when all strong references are removed, so if an image is referenced
301    /// outside of the cache it will merely be disconnected from the cache by this method.
302    pub fn purge_all(&self) {
303        UPDATES.once_update("IMAGES.purge_all", || {
304            IMAGES_SV.write().cache.clear();
305        });
306    }
307
308    /// Add an images service extension.
309    ///
310    /// See [`ImagesExtension`] for extension capabilities.
311    pub fn extend(&self, extension: Box<dyn ImagesExtension>) {
312        UPDATES.once_update("IMAGES.extend", move || {
313            IMAGES_EXTENSIONS.write().push(extension);
314        });
315    }
316
317    /// Image formats implemented by the current view-process and extensions.
318    ///
319    /// Note that if the view-process is not connected this returns empty. If
320    /// you need the formats on app startup await on `zng::APP.wait_view_process()`
321    /// before requesting the formats.
322    pub fn available_formats(&self) -> Vec<ImageFormat> {
323        let mut formats = VIEW_PROCESS.info().image.clone();
324
325        for ext in IMAGES_EXTENSIONS.read().iter() {
326            ext.available_formats(&mut formats);
327        }
328
329        formats
330    }
331
332    #[cfg(feature = "http")]
333    fn http_accept(&self) -> zng_txt::Txt {
334        let mut s = String::new();
335        let mut sep = "";
336        for f in self.available_formats() {
337            for f in f.media_type_suffixes_iter() {
338                s.push_str(sep);
339                s.push_str("image/");
340                s.push_str(f);
341                sep = ",";
342            }
343        }
344        s.into()
345    }
346}
347
348fn image(mut source: ImageSource, mut options: ImageOptions, limits: Option<ImageLimits>, r: Var<ImageEntry>) {
349    let limits = limits.unwrap_or_else(|| IMAGES_SV.read().limits.get());
350
351    // apply extensions
352    {
353        let mut exts = IMAGES_EXTENSIONS.write();
354        if !exts.is_empty() {
355            tracing::trace!("process image with {} extensions", exts.len());
356        }
357        for ext in exts.iter_mut() {
358            ext.image(&limits, &mut source, &mut options);
359        }
360    }
361
362    // only lock service after extensions
363    let mut s = IMAGES_SV.write();
364
365    if let ImageSource::Image(var) = source {
366        // Image is passthrough, cache config is ignored
367        var.set_bind(&r).perm();
368        r.hold(var).perm();
369        return;
370    } else if let ImageSource::Entries { primary, entries } = source {
371        let entries: Vec<_> = entries
372            .into_iter()
373            .map(|(k, e)| (k, IMAGES.image(e, options.clone(), Some(limits.clone()))))
374            .collect();
375        let r_weak = r.downgrade();
376        let binding = move |mut primary: ImageEntry| -> bool {
377            let primary_id = primary.handle.image_id();
378            for (i, (kind, entry)) in entries.iter().enumerate() {
379                let kind = kind.clone();
380                primary.insert_entry(entry.map(move |e| {
381                    let mut e = e.clone();
382                    e.data.meta.parent = Some(zng_view_api::image::ImageEntryMetadata::new(primary_id, i, kind.clone()));
383                    e
384                }));
385            }
386            if let Some(r) = r_weak.upgrade() {
387                r.set(primary);
388                true
389            } else {
390                false
391            }
392        };
393        let primary = IMAGES.image(*primary, options.clone(), Some(limits.clone()));
394        binding(primary.get());
395        primary.hook(move |a| binding(a.value().clone())).perm();
396        r.hold(primary).perm();
397        return;
398    }
399
400    if !VIEW_PROCESS.is_available() && !s.load_in_headless.get() {
401        tracing::debug!("ignoring image request due headless mode");
402        return;
403    }
404
405    let key = source.hash128(&options).unwrap();
406
407    // setup cache and drop service lock
408    match options.cache_mode {
409        ImageCacheMode::Ignore => (),
410        ImageCacheMode::Cache => {
411            match s.cache.entry(key) {
412                IdEntry::Occupied(e) => {
413                    // already cached
414                    let var = e.get();
415                    var.set_bind(&r).perm();
416                    r.hold(var.clone()).perm();
417                    return;
418                }
419                IdEntry::Vacant(e) => {
420                    // cache
421                    e.insert(r.clone());
422                }
423            }
424        }
425        ImageCacheMode::Retry => {
426            match s.cache.entry(key) {
427                IdEntry::Occupied(mut e) => {
428                    let var = e.get();
429                    if var.with(ImageEntry::is_error) {
430                        // already cached with error
431
432                        // bind old entry to new, in case there are listeners to it,
433                        // can't use `strong_count` to optimize here because it might have weak refs out there
434                        r.set_bind(var).perm();
435                        var.hold(r.clone()).perm();
436
437                        // new var `r` becomes the entry
438                        e.insert(r.clone());
439                    } else {
440                        // already cached ok
441                        var.set_bind(&r).perm();
442                        r.hold(var.clone()).perm();
443                        return;
444                    }
445                }
446                IdEntry::Vacant(e) => {
447                    // cache
448                    e.insert(r.clone());
449                }
450            }
451        }
452        ImageCacheMode::Reload => {
453            match s.cache.entry(key) {
454                IdEntry::Occupied(mut e) => {
455                    let var = e.get();
456                    r.set_bind(var).perm();
457                    var.hold(r.clone()).perm();
458
459                    e.insert(r.clone());
460                }
461                IdEntry::Vacant(e) => {
462                    // cache
463                    e.insert(r.clone());
464                }
465            }
466        }
467    }
468    drop(s);
469
470    match source {
471        ImageSource::Read(path) => {
472            fn read(path: &PathBuf, limit: (&ImageSourceFilter<PathBuf>, ByteLength)) -> std::io::Result<(IpcReadHandle, u64)> {
473                if !limit.0.allows(path) {
474                    return Err(std::io::Error::new(
475                        std::io::ErrorKind::PermissionDenied,
476                        "file path no allowed by limit",
477                    ));
478                }
479                let file = std::fs::File::open(path)?;
480                let len = file.metadata()?.len();
481                if len > limit.1.bytes() {
482                    return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "file length exceeds limit"));
483                }
484                Ok((IpcReadHandle::best_read_blocking(file)?, len))
485            }
486            let data_format = match path.extension() {
487                Some(ext) => ImageDataFormat::FileExtension(ext.to_string_lossy().to_txt()),
488                None => ImageDataFormat::Unknown,
489            };
490            zng_task::spawn_wait(move || match read(&path, (&limits.allow_path, limits.max_encoded_len)) {
491                Ok((data, len)) => {
492                    tracing::trace!("read {path:?}, len: {:?}, fmt: {data_format:?}", len.bytes());
493                    image_data(false, Some(key), data_format, data, options, limits, r)
494                }
495                Err(e) => {
496                    tracing::debug!("cannot read {path:?}, {e}");
497                    r.set(ImageEntry::new_error(e.to_txt()));
498                }
499            });
500        }
501        #[cfg(feature = "http")]
502        ImageSource::Download(uri, accept) => {
503            let accept = accept.unwrap_or_else(|| IMAGES.http_accept());
504
505            use zng_task::http::*;
506            async fn download(
507                uri: Uri,
508                accept: zng_txt::Txt,
509                limit: (ImageSourceFilter<Uri>, ByteLength),
510            ) -> Result<(ImageDataFormat, IpcBytes), Error> {
511                if !limit.0.allows(&uri) {
512                    return Err(Box::new(std::io::Error::new(
513                        std::io::ErrorKind::PermissionDenied,
514                        "uri no allowed by limit",
515                    )));
516                }
517
518                let request = Request::get(uri)?.max_length(limit.1).header(header::ACCEPT, accept.as_str())?;
519                let mut response = send(request).await?;
520                response.error().await?;
521                let data_format = match response.header().get(&header::CONTENT_TYPE).and_then(|m| m.to_str().ok()) {
522                    Some(m) => ImageDataFormat::MimeType(m.to_txt()),
523                    None => ImageDataFormat::Unknown,
524                };
525                let data = response.body().await?;
526
527                Ok((data_format, data))
528            }
529
530            zng_task::spawn(async move {
531                match download(uri.clone(), accept, (limits.allow_uri.clone(), limits.max_encoded_len)).await {
532                    Ok((fmt, data)) => {
533                        tracing::trace!("download {uri:?}, len: {:?}, fmt: {fmt:?}", (data.len() as u64).bytes());
534                        image_data(false, Some(key), fmt, data.into(), options, limits, r);
535                    }
536                    Err(e) => {
537                        tracing::debug!("cannot download {uri:?}, {e}");
538                        r.set(ImageEntry::new_error(e.to_txt()));
539                    }
540                }
541            });
542        }
543        ImageSource::Data(_, data, format) => image_data(false, Some(key), format, data.into(), options, limits, r),
544        ImageSource::Render(render_fn, args) => image_render(Some(key), render_fn, args, options, r),
545        _ => unreachable!(),
546    }
547}
548
549// source data acquired, setup view-process handle
550fn image_data(
551    is_respawn: bool,
552    cache_key: Option<ImageHash>,
553    format: ImageDataFormat,
554    data: IpcReadHandle,
555    options: ImageOptions,
556    limits: ImageLimits,
557    r: Var<ImageEntry>,
558) {
559    if !is_respawn && let Some(key) = cache_key {
560        let mut exts = IMAGES_EXTENSIONS.write();
561        if !exts.is_empty() {
562            tracing::trace!("process image_data with {} extensions", exts.len());
563        }
564        for ext in exts.iter_mut() {
565            if let Some(replacement) = ext.image_data(limits.max_decoded_len, &key, &data, &format, &options) {
566                replacement.set_bind(&r).perm();
567                r.hold(replacement).perm();
568
569                tracing::trace!("extension replaced image_data");
570                return;
571            }
572        }
573    }
574
575    if !VIEW_PROCESS.is_available() {
576        tracing::debug!("ignoring image view request after test load due to headless mode");
577        return;
578    }
579
580    let mut data = data;
581    let data_clone = match data.duplicate_or_read_blocking() {
582        Ok(d) => d,
583        Err(e) => {
584            tracing::error!("image data lost, {e}");
585            return;
586        }
587    };
588    let data = data;
589
590    let mut request = ImageRequest::new(
591        format.clone(),
592        data_clone,
593        limits.max_decoded_len.bytes(),
594        options.downscale.clone(),
595        options.mask,
596    );
597    request.entries = options.entries;
598
599    if is_respawn {
600        request.parent = r.with(|r| r.data.meta.parent.clone());
601    }
602
603    if VIEW_PROCESS.is_connected()
604        && let Ok(view_img) = VIEW_PROCESS.add_image(request)
605    {
606        // explicitly checking for connected to avoid logging an error
607        image_view(
608            cache_key,
609            view_img,
610            ImageDecoded::default(),
611            Some((format, data, options, limits)),
612            r,
613        );
614    } else {
615        tracing::debug!("image view request failed, will retry on respawn");
616        let mut once = Some((format, data, options, limits, r));
617        VIEW_PROCESS_INITED_EVENT
618            .hook(move |_| {
619                let (format, data, options, limits, r) = once.take().unwrap();
620                image_data(true, cache_key, format, data, options, limits, r);
621                false
622            })
623            .perm();
624    }
625}
626// monitor view-process handle until it is loaded
627fn image_view(
628    cache_key: Option<ImageHash>,
629    handle: ViewImageHandle,
630    decoded: ImageDecoded,
631    respawn_data: Option<(ImageDataFormat, IpcReadHandle, ImageOptions, ImageLimits)>,
632    r: Var<ImageEntry>,
633) {
634    // reuse value to keep entry vars alive in case of respawn
635    let mut img = r.get();
636    img.cache_key = cache_key;
637    img.handle = handle;
638    img.data = decoded;
639
640    let is_loaded = img.is_loaded();
641    let is_dummy = img.view_handle().is_dummy();
642    r.set(img);
643
644    if is_loaded {
645        image_decoded(r);
646        return;
647    }
648
649    if is_dummy {
650        tracing::error!("tried to register dummy handle");
651        return;
652    }
653
654    // handle respawn during image decode
655    let decoding_respawn_handle = if respawn_data.is_some() {
656        let r_weak = r.downgrade();
657        let mut respawn_data = respawn_data;
658        VIEW_PROCESS_INITED_EVENT.hook(move |_| {
659            if let Some(r) = r_weak.upgrade() {
660                let (format, data, options, limits) = respawn_data.take().unwrap();
661                image_data(true, cache_key, format, data, options, limits, r);
662            }
663            false
664        })
665    } else {
666        // image registered (without source info), respawn is the responsibility of the caller
667        VarHandle::dummy()
668    };
669
670    // handle decode error
671    let r_weak = r.downgrade();
672    let decode_error_handle = RAW_IMAGE_DECODE_ERROR_EVENT.hook(move |args| match r_weak.upgrade() {
673        Some(r) => {
674            if let Some(handle) = args.handle.upgrade()
675                && r.with(|img| img.view_handle() == &handle)
676            {
677                tracing::debug!("image view error, {}", args.error);
678                r.set(ImageEntry::new_error(args.error.clone()));
679                false
680            } else {
681                r.with(ImageEntry::is_loading)
682            }
683        }
684        None => false,
685    });
686
687    // handle metadata decoded
688    let r_weak = r.downgrade();
689    let decode_meta_handle = RAW_IMAGE_METADATA_DECODED_EVENT.hook(move |args| match r_weak.upgrade() {
690        Some(r) => {
691            let handle = match args.handle.upgrade() {
692                Some(h) => h,
693                None => return r.with(ImageEntry::is_loading),
694            };
695            if r.with(|img| img.view_handle() == &handle) {
696                let meta = args.meta.clone();
697                tracing::trace!("image view metadata decoded for request");
698                r.modify(move |i| i.data.meta = meta);
699            } else if let Some(p) = &args.meta.parent
700                && p.parent == r.with(|img| img.view_handle().image_id())
701            {
702                // discovered an entry for this image, start tracking it
703                tracing::trace!("image view metadata decoded for entry of request");
704                let mut entry_d = ImageDecoded::default();
705                entry_d.meta = args.meta.clone();
706                let entry = var(ImageEntry::new(None, handle.clone(), entry_d.clone()));
707                r.modify(clmv!(entry, |i| i.insert_entry(entry)));
708                image_view(None, handle, entry_d, None, entry);
709            }
710            r.with(ImageEntry::is_loading)
711        }
712        None => false,
713    });
714
715    // handle pixels decoded
716    let r_weak = r.downgrade();
717    RAW_IMAGE_DECODED_EVENT
718        .hook(move |args| {
719            let _hold = [&decoding_respawn_handle, &decode_error_handle, &decode_meta_handle];
720            match r_weak.upgrade() {
721                Some(r) => {
722                    if let Some(handle) = args.handle.upgrade()
723                        && r.with(|img| img.view_handle() == &handle)
724                    {
725                        let data = args.image.upgrade().unwrap();
726                        let is_loading = data.partial.is_some();
727                        tracing::trace!("image view decoded, partial={:?}", is_loading);
728                        r.modify(move |i| i.data = (*data.0).clone());
729                        if !is_loading {
730                            image_decoded(r);
731                        }
732                        is_loading
733                    } else {
734                        r.with(ImageEntry::is_loading)
735                    }
736                }
737                None => false,
738            }
739        })
740        .perm();
741}
742// image decoded ok, setup respawn handle
743fn image_decoded(r: Var<ImageEntry>) {
744    let r_weak = r.downgrade();
745    VIEW_PROCESS_INITED_EVENT
746        .hook(move |_| {
747            if let Some(r) = r_weak.upgrade() {
748                let img = r.get();
749                if !img.is_loaded() {
750                    // image rebound, maybe due to cache refresh
751                    return false;
752                }
753
754                // respawn the image as decoded data
755                let size = img.size();
756                let mut options = ImageOptions::none();
757                let format = match img.is_mask() {
758                    true => {
759                        options.mask = Some(ImageMaskMode::A);
760                        ImageDataFormat::A8 { size }
761                    }
762                    false => ImageDataFormat::Bgra8 {
763                        size,
764                        density: img.density(),
765                        original_color_type: img.original_color_type(),
766                    },
767                };
768                image_data(
769                    true,
770                    img.cache_key,
771                    format,
772                    img.data.pixels.clone().into(),
773                    options,
774                    ImageLimits::none(),
775                    r,
776                );
777            }
778            false
779        })
780        .perm();
781}
782
783// image render request, respawn errors during rendering are handled by the WINDOWS service
784fn image_render(
785    cache_key: Option<ImageHash>,
786    render_fn: crate::RenderFn,
787    args: Option<ImageRenderArgs>,
788    options: ImageOptions,
789    r: Var<ImageEntry>,
790) {
791    let s = IMAGES_SV.read();
792    let windows = s.render_windows();
793    let windows_ctx = windows.clone_boxed();
794    let mask = options.mask;
795    windows.open_headless_window(Box::new(move || {
796        let ctx = ImageRenderCtx::new();
797        let retain = ctx.retain.clone();
798        WINDOW.set_state(*IMAGE_RENDER_ID, ctx);
799        let w = render_fn(&args.unwrap_or_default());
800        windows_ctx.enable_frame_capture_in_window_context(mask);
801        image_render_open(cache_key, WINDOW.id(), retain, r);
802        w
803    }));
804}
805
806fn image_render_open(cache_key: Option<ImageHash>, win_id: WindowId, retain: Var<bool>, r: Var<ImageEntry>) {
807    // handle window open error
808    let r_weak = r.downgrade();
809    let error_handle = RAW_WINDOW_OR_HEADLESS_OPEN_ERROR_EVENT.hook(move |args| {
810        if args.window_id == win_id {
811            if let Some(r) = r_weak.upgrade() {
812                r.set(ImageEntry::new_error(args.error.clone()));
813            }
814            false
815        } else {
816            true
817        }
818    });
819    // hold error handle until open ok
820    RAW_HEADLESS_OPEN_EVENT
821        .hook(move |args| {
822            let _hold = &error_handle;
823            args.window_id != win_id
824        })
825        .perm();
826
827    // handle frame(s)
828    let r_weak = r.downgrade();
829    RAW_FRAME_RENDERED_EVENT
830        .hook(move |args| {
831            if args.window_id == win_id {
832                if let Some(r) = r_weak.upgrade() {
833                    match args.frame_image.clone() {
834                        Some(h) => {
835                            let h = h.upgrade().unwrap();
836                            let handle = h.0.0.clone();
837                            let data = h.1.clone();
838                            let retain = retain.get();
839                            r.set(ImageEntry::new(cache_key, handle, data));
840                            if !retain {
841                                IMAGES_SV.read().render_windows().close_window(win_id);
842                                // image_decoded setup a normal respawn recovery for the image
843                                image_decoded(r);
844                            }
845                            // else if it is retained on respawn the window will render again
846                            retain
847                        }
848                        None => {
849                            r.set(ImageEntry::new_error("image render window did not capture a frame".to_txt()));
850                            false
851                        }
852                    }
853                } else {
854                    false
855                }
856            } else {
857                true
858            }
859        })
860        .perm();
861}
862
863impl IMAGES {
864    /// Render the *window* generated by `render` to an image.
865    ///
866    /// The *window* is created as a headless surface and rendered to the returned image. You can set the
867    /// [`IMAGE_RENDER.retain`] var inside `render` to create an image that updates with new frames. By default it will only render once.
868    ///
869    /// The closure runs in the [`WINDOW`] context of the headless window.
870    ///
871    /// This is shorthand for calling [`IMAGES.image`] with [`ImageSource::render`] and [`ImageOptions::none`].
872    ///
873    /// [`IMAGE_RENDER.retain`]: IMAGE_RENDER::retain
874    /// [`WINDOW`]: zng_app::window::WINDOW
875    /// [`IMAGES.image`]: IMAGES::image
876    pub fn render<N, R>(&self, mask: Option<ImageMaskMode>, render: N) -> ImageVar
877    where
878        N: FnOnce() -> R + Send + Sync + 'static,
879        R: ImageRenderWindowRoot,
880    {
881        let render = Mutex::new(Some(render));
882        let source = ImageSource::render(move |_| render.lock().take().expect("IMAGES.render closure called more than once")());
883        let options = ImageOptions::new(ImageCacheMode::Ignore, None, mask, ImageEntriesMode::empty());
884        self.image_impl(source, options, None)
885    }
886
887    /// Render an [`UiNode`] to an image.
888    ///
889    /// This method is a shortcut to [`render`] a node without needing to declare the headless window, note that
890    /// a headless window is still used, the node does not have the same context as the calling widget.
891    ///
892    /// This is shorthand for calling [`IMAGES.image`] with [`ImageSource::render_node`] and [`ImageOptions::none`].
893    ///
894    /// [`render`]: Self::render
895    /// [`UiNode`]: zng_app::widget::node::UiNode
896    /// [`IMAGES.image`]: IMAGES::image
897    pub fn render_node(
898        &self,
899        render_mode: RenderMode,
900        mask: Option<ImageMaskMode>,
901        render: impl FnOnce() -> UiNode + Send + Sync + 'static,
902    ) -> ImageVar {
903        let render = Mutex::new(Some(render));
904        let source = ImageSource::render_node(render_mode, move |_| {
905            render.lock().take().expect("IMAGES.render closure called more than once")()
906        });
907        let options = ImageOptions::new(ImageCacheMode::Ignore, None, mask, ImageEntriesMode::empty());
908        self.image_impl(source, options, None)
909    }
910}
911
912/// Images render window hook.
913#[expect(non_camel_case_types)]
914pub struct IMAGES_WINDOW;
915impl IMAGES_WINDOW {
916    /// Sets the windows service used to manage the headless windows used to render images.
917    ///
918    /// This must be called by the windows implementation only.
919    pub fn hook_render_windows_service(&self, service: Box<dyn ImageRenderWindowsService>) {
920        let mut img = IMAGES_SV.write();
921        img.render_windows = Some(service);
922    }
923}
924
925/// Reference to a windows manager service that [`IMAGES`] can use to render images.
926///
927/// This service must be implemented by the window implementer, the `WINDOWS` service implements it.
928pub trait ImageRenderWindowsService: Send + Sync + 'static {
929    /// Clone the service reference.
930    fn clone_boxed(&self) -> Box<dyn ImageRenderWindowsService>;
931
932    /// Create a window root that presents the node.
933    ///
934    /// This is to produce a window wrapper for [`ImageSource::render_node`].
935    fn new_window_root(&self, node: UiNode, render_mode: RenderMode) -> Box<dyn ImageRenderWindowRoot>;
936
937    /// Set parent window for the headless render window.
938    ///
939    /// Called inside the [`WINDOW`] context for the new window.
940    fn set_parent_in_window_context(&self, parent_id: WindowId);
941
942    /// Enable frame capture for the window.
943    ///
944    /// If `mask` is set captures only the given channel, if not set will capture the full BGRA image.
945    ///
946    /// Called inside the [`WINDOW`] context for the new window.
947    fn enable_frame_capture_in_window_context(&self, mask: Option<ImageMaskMode>);
948
949    /// Open the window.
950    ///
951    /// The `new_window_root` must be called inside the [`WINDOW`] context for the new window.
952    fn open_headless_window(&self, new_window_root: Box<dyn FnOnce() -> Box<dyn ImageRenderWindowRoot> + Send>);
953
954    /// Close the window, does nothing if the window is not found.
955    fn close_window(&self, window_id: WindowId);
956}
957
958/// Implemented for the root window type.
959///
960/// This is implemented for the `WindowRoot` type.
961pub trait ImageRenderWindowRoot: Send + Any + 'static {}
962
963/// Controls properties of the render window used by [`IMAGES.render`].
964///
965/// [`IMAGES.render`]: IMAGES::render
966#[expect(non_camel_case_types)]
967pub struct IMAGE_RENDER;
968impl IMAGE_RENDER {
969    /// If the current context is an [`IMAGES.render`] closure, window or widget.
970    ///
971    /// [`IMAGES.render`]: IMAGES::render
972    pub fn is_in_render(&self) -> bool {
973        WINDOW.contains_state(*IMAGE_RENDER_ID)
974    }
975
976    /// If the render task is kept alive after a frame is produced, this is `false` by default
977    /// meaning the image only renders once, if set to `true` the image will automatically update
978    /// when the render widget requests a new frame.
979    pub fn retain(&self) -> Var<bool> {
980        WINDOW.req_state(*IMAGE_RENDER_ID).retain
981    }
982}
983
984/// If the render task is kept alive after a frame is produced, this is `false` by default
985/// meaning the image only renders once, if set to `true` the image will automatically update
986/// when the render widget requests a new frame.
987///
988/// This property sets and binds `retain` to [`IMAGE_RENDER.retain`].
989///
990/// [`IMAGE_RENDER.retain`]: IMAGE_RENDER::retain
991#[zng_app::widget::property(CONTEXT, default(false))]
992pub fn render_retain(child: impl IntoUiNode, retain: impl IntoVar<bool>) -> UiNode {
993    let retain = retain.into_var();
994    match_node(child, move |_, op| {
995        if let UiNodeOp::Init = op {
996            if IMAGE_RENDER.is_in_render() {
997                let actual_retain = IMAGE_RENDER.retain();
998                actual_retain.set_from(&retain);
999                let handle = actual_retain.bind(&retain);
1000                WIDGET.push_var_handle(handle);
1001            } else {
1002                tracing::error!("can only set `render_retain` in render widgets")
1003            }
1004        }
1005    })
1006}
1007
1008#[derive(Clone)]
1009struct ImageRenderCtx {
1010    retain: Var<bool>,
1011}
1012impl ImageRenderCtx {
1013    fn new() -> Self {
1014        Self { retain: var(false) }
1015    }
1016}
1017
1018static_id! {
1019    static ref IMAGE_RENDER_ID: StateId<ImageRenderCtx>;
1020}