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 and cache.
5//!
6//! # Crate
7//!
8#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
9#![warn(unused_extern_crates)]
10#![warn(missing_docs)]
11
12use std::{
13    env, mem,
14    path::{Path, PathBuf},
15    sync::atomic::{AtomicBool, Ordering},
16};
17
18use parking_lot::Mutex;
19use task::io::AsyncReadExt;
20use zng_app::{
21    APP, AppExtension,
22    update::EventUpdate,
23    view_process::{
24        VIEW_PROCESS, VIEW_PROCESS_INITED_EVENT, ViewImage,
25        raw_events::{LOW_MEMORY_EVENT, RAW_IMAGE_LOAD_ERROR_EVENT, RAW_IMAGE_LOADED_EVENT, RAW_IMAGE_METADATA_LOADED_EVENT},
26    },
27    widget::UiTaskWidget,
28};
29use zng_app_context::app_local;
30use zng_clone_move::{async_clmv, clmv};
31use zng_task as task;
32
33mod types;
34pub use types::*;
35
36mod render;
37#[doc(inline)]
38pub use render::{IMAGE_RENDER, IMAGES_WINDOW, ImageRenderWindowRoot, ImageRenderWindowsService, render_retain};
39use zng_layout::unit::{ByteLength, ByteUnits};
40use zng_task::{UiTask, channel::IpcBytes};
41use zng_txt::{ToTxt, Txt, formatx};
42use zng_unique_id::{IdEntry, IdMap};
43use zng_var::{Var, WeakVar, var};
44use zng_view_api::image::ImageRequest;
45
46/// Application extension that provides an image cache.
47///
48/// # Services
49///
50/// Services this extension provides.
51///
52/// * [`IMAGES`]
53#[derive(Default)]
54#[non_exhaustive]
55pub struct ImageManager {}
56impl AppExtension for ImageManager {
57    fn event_preview(&mut self, update: &mut EventUpdate) {
58        if let Some(args) = RAW_IMAGE_METADATA_LOADED_EVENT.on(update) {
59            let images = IMAGES_SV.read();
60
61            if let Some(var) = images
62                .decoding
63                .iter()
64                .map(|t| &t.image)
65                .find(|v| v.with(|img| img.view.get().unwrap() == &args.image))
66            {
67                var.update();
68            }
69        } else if let Some(args) = RAW_IMAGE_LOADED_EVENT.on(update) {
70            let image = &args.image;
71
72            // image finished decoding, remove from `decoding`
73            // and notify image var value update.
74            let mut images = IMAGES_SV.write();
75
76            if let Some(i) = images
77                .decoding
78                .iter()
79                .position(|t| t.image.with(|img| img.view.get().unwrap() == image))
80            {
81                let ImageDecodingTask { image, .. } = images.decoding.swap_remove(i);
82                image.update();
83                image.with(|img| img.done_signal.set());
84            }
85        } else if let Some(args) = RAW_IMAGE_LOAD_ERROR_EVENT.on(update) {
86            let image = &args.image;
87
88            // image failed to decode, remove from `decoding`
89            // and notify image var value update.
90            let mut images = IMAGES_SV.write();
91
92            if let Some(i) = images
93                .decoding
94                .iter()
95                .position(|t| t.image.with(|img| img.view.get().unwrap() == image))
96            {
97                let ImageDecodingTask { image, .. } = images.decoding.swap_remove(i);
98                image.update();
99                image.with(|img| {
100                    img.done_signal.set();
101
102                    if let Some(k) = &img.cache_key
103                        && let Some(e) = images.cache.get(k)
104                    {
105                        e.error.store(true, Ordering::Relaxed);
106                    }
107
108                    tracing::error!("decode error: {:?}", img.error().unwrap());
109                });
110            }
111        } else if let Some(args) = VIEW_PROCESS_INITED_EVENT.on(update) {
112            let mut images = IMAGES_SV.write();
113            let images = &mut *images;
114            images.cleanup_not_cached(true);
115            images.download_accept.clear();
116
117            let mut decoding_interrupted = mem::take(&mut images.decoding);
118            for (img_var, max_decoded_len, downscale, mask) in images
119                .cache
120                .values()
121                .map(|e| (e.image.clone(), e.max_decoded_len, e.downscale, e.mask))
122                .chain(
123                    images
124                        .not_cached
125                        .iter()
126                        .filter_map(|e| e.image.upgrade().map(|v| (v, e.max_decoded_len, e.downscale, e.mask))),
127                )
128            {
129                let img = img_var.get();
130
131                if let Some(view) = img.view.get() {
132                    if view.generation() == args.generation {
133                        continue; // already recovered, can this happen?
134                    }
135                    if let Some(e) = view.error() {
136                        // respawned, but image was an error.
137                        img_var.set(Img::dummy(Some(e.to_owned())));
138                    } else if let Some(task_i) = decoding_interrupted
139                        .iter()
140                        .position(|e| e.image.with(|img| img.view() == Some(view)))
141                    {
142                        let task = decoding_interrupted.swap_remove(task_i);
143                        // respawned, but image was decoding, need to restart decode.
144                        match VIEW_PROCESS.add_image(ImageRequest::new(
145                            task.format.clone(),
146                            task.data.clone(),
147                            max_decoded_len.0 as u64,
148                            downscale,
149                            mask,
150                        )) {
151                            Ok(img) => {
152                                img_var.set(Img::new(img));
153                            }
154                            Err(_) => { /*will receive another event.*/ }
155                        }
156                        images.decoding.push(ImageDecodingTask {
157                            format: task.format.clone(),
158                            data: task.data.clone(),
159                            image: img_var,
160                        });
161                    } else {
162                        // respawned and image was loaded.
163
164                        let img_format = if view.is_mask() {
165                            ImageDataFormat::A8 { size: view.size() }
166                        } else {
167                            ImageDataFormat::Bgra8 {
168                                size: view.size(),
169                                density: view.density(),
170                            }
171                        };
172
173                        let data = view.pixels().unwrap();
174                        let img = match VIEW_PROCESS.add_image(ImageRequest::new(
175                            img_format.clone(),
176                            data.clone(),
177                            max_decoded_len.0 as u64,
178                            downscale,
179                            mask,
180                        )) {
181                            Ok(img) => img,
182                            Err(_) => return, // we will receive another event.
183                        };
184
185                        img_var.set(Img::new(img));
186
187                        images.decoding.push(ImageDecodingTask {
188                            format: img_format,
189                            data,
190                            image: img_var,
191                        });
192                    }
193                } else if let Some(task_i) = decoding_interrupted.iter().position(|e| e.image.var_eq(&img_var)) {
194                    // respawned, but image had not started decoding, start it now.
195                    let task = decoding_interrupted.swap_remove(task_i);
196                    match VIEW_PROCESS.add_image(ImageRequest::new(
197                        task.format.clone(),
198                        task.data.clone(),
199                        max_decoded_len.0 as u64,
200                        downscale,
201                        mask,
202                    )) {
203                        Ok(img) => {
204                            img_var.set(Img::new(img));
205                        }
206                        Err(_) => { /*will receive another event.*/ }
207                    }
208                    images.decoding.push(ImageDecodingTask {
209                        format: task.format.clone(),
210                        data: task.data.clone(),
211                        image: img_var,
212                    });
213                }
214                // else { *is loading, will continue normally in self.update_preview()* }
215            }
216        } else if LOW_MEMORY_EVENT.on(update).is_some() {
217            IMAGES.clean_all();
218        } else {
219            self.event_preview_render(update);
220        }
221    }
222
223    fn update_preview(&mut self) {
224        // update loading tasks:
225
226        let mut images = IMAGES_SV.write();
227        let mut loading = Vec::with_capacity(images.loading.len());
228        let loading_tasks = mem::take(&mut images.loading);
229        let mut proxies = mem::take(&mut images.proxies);
230        drop(images); // proxies can use IMAGES
231
232        'loading_tasks: for t in loading_tasks {
233            t.task.lock().update();
234            match t.task.into_inner().into_result() {
235                Ok(d) => {
236                    match d.r {
237                        Ok(data) => {
238                            if let Some((key, mode)) = &t.is_data_proxy_source {
239                                for proxy in &mut proxies {
240                                    if proxy.is_data_proxy()
241                                        && let Some(replaced) = proxy.data(key, &data, &d.format, *mode, t.downscale, t.mask, true)
242                                    {
243                                        replaced.set_bind(&t.image).perm();
244                                        t.image.hold(replaced).perm();
245                                        continue 'loading_tasks;
246                                    }
247                                }
248                            }
249
250                            if VIEW_PROCESS.is_available() {
251                                // success and we have a view-process.
252                                match VIEW_PROCESS.add_image(ImageRequest::new(
253                                    d.format.clone(),
254                                    data.clone(),
255                                    t.max_decoded_len.0 as u64,
256                                    t.downscale,
257                                    t.mask,
258                                )) {
259                                    Ok(img) => {
260                                        // request sent, add to `decoding` will receive
261                                        // `RawImageLoadedEvent` or `RawImageLoadErrorEvent` event
262                                        // when done.
263                                        t.image.modify(move |v| {
264                                            v.inner_set_or_replace(img, false);
265                                        });
266                                    }
267                                    Err(_) => {
268                                        // will recover in VIEW_PROCESS_INITED_EVENT
269                                    }
270                                }
271                                IMAGES_SV.write().decoding.push(ImageDecodingTask {
272                                    format: d.format,
273                                    data,
274                                    image: t.image,
275                                });
276                            } else {
277                                // success, but we are only doing `load_in_headless` validation.
278                                let img = ViewImage::dummy(None);
279                                t.image.modify(move |v| {
280                                    v.inner_set_or_replace(img, true);
281                                });
282                            }
283                        }
284                        Err(e) => {
285                            tracing::error!("load error: {e:?}");
286                            // load error.
287                            let img = ViewImage::dummy(Some(e));
288                            t.image.modify(move |v| {
289                                v.inner_set_or_replace(img, true);
290                            });
291
292                            // flag error for user retry
293                            if let Some(k) = &t.image.with(|img| img.cache_key)
294                                && let Some(e) = IMAGES_SV.read().cache.get(k)
295                            {
296                                e.error.store(true, Ordering::Relaxed);
297                            }
298                        }
299                    }
300                }
301                Err(task) => {
302                    loading.push(ImageLoadingTask {
303                        task: Mutex::new(task),
304                        image: t.image,
305                        max_decoded_len: t.max_decoded_len,
306                        downscale: t.downscale,
307                        mask: t.mask,
308                        is_data_proxy_source: t.is_data_proxy_source,
309                    });
310                }
311            }
312        }
313        let mut images = IMAGES_SV.write();
314        images.loading = loading;
315        images.proxies = proxies;
316    }
317
318    fn update(&mut self) {
319        self.update_render();
320    }
321}
322
323app_local! {
324    static IMAGES_SV: ImagesService = {
325        APP.extensions().require::<ImageManager>();
326        ImagesService::new()
327    };
328}
329
330struct ImageLoadingTask {
331    task: Mutex<UiTask<ImageData>>,
332    image: Var<Img>,
333    max_decoded_len: ByteLength,
334    downscale: Option<ImageDownscale>,
335    mask: Option<ImageMaskMode>,
336    is_data_proxy_source: Option<(ImageHash, ImageCacheMode)>,
337}
338
339struct ImageDecodingTask {
340    format: ImageDataFormat,
341    data: IpcBytes,
342    image: Var<Img>,
343}
344
345struct CacheEntry {
346    image: Var<Img>,
347    error: AtomicBool,
348    max_decoded_len: ByteLength,
349    downscale: Option<ImageDownscale>,
350    mask: Option<ImageMaskMode>,
351}
352
353struct NotCachedEntry {
354    image: WeakVar<Img>,
355    max_decoded_len: ByteLength,
356    downscale: Option<ImageDownscale>,
357    mask: Option<ImageMaskMode>,
358}
359
360struct ImagesService {
361    load_in_headless: Var<bool>,
362    limits: Var<ImageLimits>,
363
364    download_accept: Txt,
365    proxies: Vec<Box<dyn ImageCacheProxy>>,
366
367    loading: Vec<ImageLoadingTask>,
368    decoding: Vec<ImageDecodingTask>,
369    cache: IdMap<ImageHash, CacheEntry>,
370    not_cached: Vec<NotCachedEntry>,
371
372    render: render::ImagesRender,
373}
374impl ImagesService {
375    fn new() -> Self {
376        Self {
377            load_in_headless: var(false),
378            limits: var(ImageLimits::default()),
379            proxies: vec![],
380            loading: vec![],
381            decoding: vec![],
382            download_accept: Txt::from_static(""),
383            cache: IdMap::new(),
384            not_cached: vec![],
385            render: render::ImagesRender::default(),
386        }
387    }
388
389    fn register(
390        &mut self,
391        key: ImageHash,
392        image: ViewImage,
393        downscale: Option<ImageDownscale>,
394    ) -> std::result::Result<ImageVar, (ViewImage, ImageVar)> {
395        let limits = self.limits.get();
396        let limits = ImageLimits {
397            max_encoded_len: limits.max_encoded_len,
398            max_decoded_len: limits.max_decoded_len.max(image.pixels().map(|b| b.len()).unwrap_or(0).bytes()),
399            allow_path: PathFilter::BlockAll,
400            #[cfg(feature = "http")]
401            allow_uri: UriFilter::BlockAll,
402        };
403
404        match self.cache.entry(key) {
405            IdEntry::Occupied(e) => Err((image, e.get().image.read_only())),
406            IdEntry::Vacant(e) => {
407                let is_error = image.is_error();
408                let is_loading = !is_error && !image.is_loaded();
409                let is_mask = image.is_mask();
410                let format = if is_mask {
411                    ImageDataFormat::A8 { size: image.size() }
412                } else {
413                    ImageDataFormat::Bgra8 {
414                        size: image.size(),
415                        density: image.density(),
416                    }
417                };
418                let img_var = var(Img::new(image));
419                if is_loading {
420                    self.decoding.push(ImageDecodingTask {
421                        format,
422                        data: IpcBytes::default(),
423                        image: img_var.clone(),
424                    });
425                }
426
427                Ok(e.insert(CacheEntry {
428                    error: AtomicBool::new(is_error),
429                    image: img_var,
430                    max_decoded_len: limits.max_decoded_len,
431                    downscale,
432                    mask: if is_mask { Some(ImageMaskMode::A) } else { None },
433                })
434                .image
435                .read_only())
436            }
437        }
438    }
439
440    fn detach(&mut self, image: ImageVar) -> ImageVar {
441        if let Some(key) = &image.with(|i| i.cache_key) {
442            let decoded_size = image.with(|img| img.pixels().map(|b| b.len()).unwrap_or(0).bytes());
443            let mut max_decoded_len = self.limits.with(|l| l.max_decoded_len.max(decoded_size));
444            let mut downscale = None;
445            let mut mask = None;
446
447            if let Some(e) = self.cache.get(key) {
448                max_decoded_len = e.max_decoded_len;
449                downscale = e.downscale;
450                mask = e.mask;
451
452                // is cached, `clean` if is only external reference.
453                if image.strong_count() == 2 {
454                    self.cache.remove(key);
455                }
456            }
457
458            // remove `cache_key` from image, this clones the `Img` only-if is still in cache.
459            let mut img = image.get();
460            img.cache_key = None;
461            let img = var(img);
462            self.not_cached.push(NotCachedEntry {
463                image: img.downgrade(),
464                max_decoded_len,
465                downscale,
466                mask,
467            });
468            img.read_only()
469        } else {
470            // already not cached
471            image
472        }
473    }
474
475    fn proxy_then_remove(mut proxies: Vec<Box<dyn ImageCacheProxy>>, key: &ImageHash, purge: bool) -> bool {
476        for proxy in &mut proxies {
477            let r = proxy.remove(key, purge);
478            match r {
479                ProxyRemoveResult::None => continue,
480                ProxyRemoveResult::Remove(r, p) => return IMAGES_SV.write().proxied_remove(proxies, &r, p),
481                ProxyRemoveResult::Removed => {
482                    IMAGES_SV.write().proxies.append(&mut proxies);
483                    return true;
484                }
485            }
486        }
487        IMAGES_SV.write().proxied_remove(proxies, key, purge)
488    }
489    fn proxied_remove(&mut self, mut proxies: Vec<Box<dyn ImageCacheProxy>>, key: &ImageHash, purge: bool) -> bool {
490        self.proxies.append(&mut proxies);
491        if purge || self.cache.get(key).map(|v| v.image.strong_count() > 1).unwrap_or(false) {
492            self.cache.remove(key).is_some()
493        } else {
494            false
495        }
496    }
497
498    fn proxy_then_get(
499        mut proxies: Vec<Box<dyn ImageCacheProxy>>,
500        source: ImageSource,
501        mode: ImageCacheMode,
502        limits: ImageLimits,
503        downscale: Option<ImageDownscale>,
504        mask: Option<ImageMaskMode>,
505    ) -> ImageVar {
506        let source = match source {
507            ImageSource::Read(path) => {
508                let path = crate::absolute_path(&path, || env::current_dir().expect("could not access current dir"), true);
509                if !limits.allow_path.allows(&path) {
510                    let error = formatx!("limits filter blocked `{}`", path.display());
511                    tracing::error!("{error}");
512                    IMAGES_SV.write().proxies.append(&mut proxies);
513                    return var(Img::dummy(Some(error))).read_only();
514                }
515                ImageSource::Read(path)
516            }
517            #[cfg(feature = "http")]
518            ImageSource::Download(uri, accepts) => {
519                if !limits.allow_uri.allows(&uri) {
520                    let error = formatx!("limits filter blocked `{uri}`");
521                    tracing::error!("{error}");
522                    IMAGES_SV.write().proxies.append(&mut proxies);
523                    return var(Img::dummy(Some(error))).read_only();
524                }
525                ImageSource::Download(uri, accepts)
526            }
527            ImageSource::Image(r) => {
528                IMAGES_SV.write().proxies.append(&mut proxies);
529                return r;
530            }
531            source => source,
532        };
533
534        let key = source.hash128(downscale, mask).unwrap();
535        for proxy in &mut proxies {
536            if proxy.is_data_proxy() && !matches!(source, ImageSource::Data(_, _, _)) {
537                continue;
538            }
539
540            let r = proxy.get(&key, &source, mode, downscale, mask);
541            match r {
542                ProxyGetResult::None => continue,
543                ProxyGetResult::Cache(source, mode, downscale, mask) => {
544                    return IMAGES_SV.write().proxied_get(
545                        proxies,
546                        source.hash128(downscale, mask).unwrap(),
547                        source,
548                        mode,
549                        limits,
550                        downscale,
551                        mask,
552                    );
553                }
554                ProxyGetResult::Image(img) => {
555                    IMAGES_SV.write().proxies.append(&mut proxies);
556                    return img;
557                }
558            }
559        }
560        IMAGES_SV.write().proxied_get(proxies, key, source, mode, limits, downscale, mask)
561    }
562    #[allow(clippy::too_many_arguments)]
563    fn proxied_get(
564        &mut self,
565        mut proxies: Vec<Box<dyn ImageCacheProxy>>,
566        key: ImageHash,
567        source: ImageSource,
568        mode: ImageCacheMode,
569        limits: ImageLimits,
570        downscale: Option<ImageDownscale>,
571        mask: Option<ImageMaskMode>,
572    ) -> ImageVar {
573        self.proxies.append(&mut proxies);
574        match mode {
575            ImageCacheMode::Cache => {
576                if let Some(v) = self.cache.get(&key) {
577                    return v.image.read_only();
578                }
579            }
580            ImageCacheMode::Retry => {
581                if let Some(e) = self.cache.get(&key)
582                    && !e.error.load(Ordering::Relaxed)
583                {
584                    return e.image.read_only();
585                }
586            }
587            ImageCacheMode::Ignore | ImageCacheMode::Reload => {}
588        }
589
590        if !VIEW_PROCESS.is_available() && !self.load_in_headless.get() {
591            tracing::warn!("loading dummy image, set `load_in_headless=true` to actually load without renderer");
592
593            let dummy = var(Img::new(ViewImage::dummy(None)));
594            self.cache.insert(
595                key,
596                CacheEntry {
597                    image: dummy.clone(),
598                    error: AtomicBool::new(false),
599                    max_decoded_len: limits.max_decoded_len,
600                    downscale,
601                    mask,
602                },
603            );
604            return dummy.read_only();
605        }
606
607        let max_encoded_size = limits.max_encoded_len;
608
609        match source {
610            ImageSource::Read(path) => self.load_task(
611                key,
612                mode,
613                limits.max_decoded_len,
614                downscale,
615                mask,
616                true,
617                task::run(async move {
618                    let mut r = ImageData {
619                        format: path
620                            .extension()
621                            .and_then(|e| e.to_str())
622                            .map(|s| ImageDataFormat::FileExtension(Txt::from_str(s)))
623                            .unwrap_or(ImageDataFormat::Unknown),
624                        r: Err(Txt::from_static("")),
625                    };
626
627                    let mut file = match task::fs::File::open(path).await {
628                        Ok(f) => f,
629                        Err(e) => {
630                            r.r = Err(e.to_txt());
631                            return r;
632                        }
633                    };
634
635                    let len = match file.metadata().await {
636                        Ok(m) => m.len() as usize,
637                        Err(e) => {
638                            r.r = Err(e.to_txt());
639                            return r;
640                        }
641                    };
642
643                    if len > max_encoded_size.0 {
644                        r.r = Err(formatx!("file size `{}` exceeds the limit of `{max_encoded_size}`", len.bytes()));
645                        return r;
646                    }
647
648                    let mut data = Vec::with_capacity(len);
649                    r.r = match file.read_to_end(&mut data).await {
650                        Ok(_) => match IpcBytes::from_vec_blocking(data) {
651                            Ok(r) => Ok(r),
652                            Err(e) => Err(e.to_txt()),
653                        },
654                        Err(e) => Err(e.to_txt()),
655                    };
656
657                    r
658                }),
659            ),
660            #[cfg(feature = "http")]
661            ImageSource::Download(uri, accept) => {
662                let accept = accept.unwrap_or_else(|| self.download_accept());
663
664                self.load_task(
665                    key,
666                    mode,
667                    limits.max_decoded_len,
668                    downscale,
669                    mask,
670                    true,
671                    task::run(async move {
672                        let mut r = ImageData {
673                            format: ImageDataFormat::Unknown,
674                            r: Err(Txt::from_static("")),
675                        };
676
677                        let request = task::http::Request::get(uri)
678                            .unwrap()
679                            .header(task::http::header::ACCEPT, accept.as_str())
680                            .unwrap()
681                            .max_length(max_encoded_size);
682
683                        match task::http::send(request).await {
684                            Ok(mut rsp) => {
685                                if let Some(m) = rsp.header().get(&task::http::header::CONTENT_TYPE).and_then(|v| v.to_str().ok()) {
686                                    let m = m.to_lowercase();
687                                    if m.starts_with("image/") {
688                                        r.format = ImageDataFormat::MimeType(Txt::from_str(&m));
689                                    }
690                                }
691
692                                r.r = rsp.body().await.map_err(|e| formatx!("download error: {e}"));
693                            }
694                            Err(e) => {
695                                r.r = Err(formatx!("request error: {e}"));
696                            }
697                        }
698
699                        r
700                    }),
701                )
702            }
703            ImageSource::Data(_, bytes, fmt) => {
704                let r = ImageData { format: fmt, r: Ok(bytes) };
705                self.load_task(key, mode, limits.max_decoded_len, downscale, mask, false, async { r })
706            }
707            ImageSource::Render(rfn, args) => {
708                let img = self.new_cache_image(key, mode, limits.max_decoded_len, downscale, mask);
709                self.render_img(mask, clmv!(rfn, || rfn(&args.unwrap_or_default())), &img);
710                img.read_only()
711            }
712            ImageSource::Image(_) => unreachable!(),
713        }
714    }
715
716    #[cfg(feature = "http")]
717    fn download_accept(&mut self) -> Txt {
718        if self.download_accept.is_empty() {
719            if VIEW_PROCESS.is_available() {
720                let mut r = String::new();
721                let mut sep = "";
722                for fmt in VIEW_PROCESS.image_decoders().unwrap_or_default() {
723                    r.push_str(sep);
724                    r.push_str("image/");
725                    r.push_str(&fmt);
726                    sep = ",";
727                }
728            }
729            if self.download_accept.is_empty() {
730                self.download_accept = "image/*".into();
731            }
732        }
733        self.download_accept.clone()
734    }
735
736    fn cleanup_not_cached(&mut self, force: bool) {
737        if force || self.not_cached.len() > 1000 {
738            self.not_cached.retain(|c| c.image.strong_count() > 0);
739        }
740    }
741
742    fn new_cache_image(
743        &mut self,
744        key: ImageHash,
745        mode: ImageCacheMode,
746        max_decoded_len: ByteLength,
747        downscale: Option<ImageDownscale>,
748        mask: Option<ImageMaskMode>,
749    ) -> Var<Img> {
750        self.cleanup_not_cached(false);
751
752        if let ImageCacheMode::Reload = mode {
753            self.cache
754                .entry(key)
755                .or_insert_with(|| CacheEntry {
756                    image: var(Img::new_none(Some(key))),
757                    error: AtomicBool::new(false),
758                    max_decoded_len,
759                    downscale,
760                    mask,
761                })
762                .image
763                .clone()
764        } else if let ImageCacheMode::Ignore = mode {
765            let img = var(Img::new_none(None));
766            self.not_cached.push(NotCachedEntry {
767                image: img.downgrade(),
768                max_decoded_len,
769                downscale,
770                mask,
771            });
772            img
773        } else {
774            let img = var(Img::new_none(Some(key)));
775            self.cache.insert(
776                key,
777                CacheEntry {
778                    image: img.clone(),
779                    error: AtomicBool::new(false),
780                    max_decoded_len,
781                    downscale,
782                    mask,
783                },
784            );
785            img
786        }
787    }
788
789    /// The `fetch_bytes` future is polled in the UI thread, use `task::run` for futures that poll a lot.
790    #[allow(clippy::too_many_arguments)]
791    fn load_task(
792        &mut self,
793        key: ImageHash,
794        mode: ImageCacheMode,
795        max_decoded_len: ByteLength,
796        downscale: Option<ImageDownscale>,
797        mask: Option<ImageMaskMode>,
798        is_data_proxy_source: bool,
799        fetch_bytes: impl Future<Output = ImageData> + Send + 'static,
800    ) -> ImageVar {
801        let img = self.new_cache_image(key, mode, max_decoded_len, downscale, mask);
802        let r = img.read_only();
803
804        self.loading.push(ImageLoadingTask {
805            task: Mutex::new(UiTask::new(None, fetch_bytes)),
806            image: img,
807            max_decoded_len,
808            downscale,
809            mask,
810            is_data_proxy_source: if is_data_proxy_source { Some((key, mode)) } else { None },
811        });
812        zng_app::update::UPDATES.update(None);
813
814        r
815    }
816}
817
818/// Image loading, cache and render service.
819///
820/// If the app is running without a [`VIEW_PROCESS`] all images are dummy, see [`load_in_headless`] for
821/// details.
822///
823/// # Provider
824///
825/// This service is provided by the [`ImageManager`] extension, it will panic if used in an app not extended.
826///
827/// [`load_in_headless`]: IMAGES::load_in_headless
828/// [`VIEW_PROCESS`]: zng_app::view_process::VIEW_PROCESS
829pub struct IMAGES;
830impl IMAGES {
831    /// If should still download/read image bytes in headless/renderless mode.
832    ///
833    /// When an app is in headless mode without renderer no [`VIEW_PROCESS`] is available, so
834    /// images cannot be decoded, in this case all images are the [`dummy`] image and no attempt
835    /// to download/read the image files is made. You can enable loading in headless tests to detect
836    /// IO errors, in this case if there is an error acquiring the image file the image will be a
837    /// [`dummy`] with error.
838    ///
839    /// [`dummy`]: IMAGES::dummy
840    /// [`VIEW_PROCESS`]: zng_app::view_process::VIEW_PROCESS
841    pub fn load_in_headless(&self) -> Var<bool> {
842        IMAGES_SV.read().load_in_headless.clone()
843    }
844
845    /// Default loading and decoding limits for each image.
846    pub fn limits(&self) -> Var<ImageLimits> {
847        IMAGES_SV.read().limits.clone()
848    }
849
850    /// Returns a dummy image that reports it is loaded or an error.
851    pub fn dummy(&self, error: Option<Txt>) -> ImageVar {
852        var(Img::dummy(error)).read_only()
853    }
854
855    /// Cache or load an image file from a file system `path`.
856    pub fn read(&self, path: impl Into<PathBuf>) -> ImageVar {
857        self.cache(path.into())
858    }
859
860    /// Get a cached `uri` or download it.
861    ///
862    /// Optionally define the HTTP ACCEPT header, if not set all image formats supported by the view-process
863    /// backend are accepted.
864    #[cfg(feature = "http")]
865    pub fn download<U>(&self, uri: U, accept: Option<Txt>) -> ImageVar
866    where
867        U: TryInto<task::http::Uri>,
868        <U as TryInto<task::http::Uri>>::Error: ToTxt,
869    {
870        match uri.try_into() {
871            Ok(uri) => self.cache(ImageSource::Download(uri, accept)),
872            Err(e) => self.dummy(Some(e.to_txt())),
873        }
874    }
875
876    /// Get a cached image from `&'static [u8]` data.
877    ///
878    /// The data can be any of the formats described in [`ImageDataFormat`].
879    ///
880    /// The image key is a [`ImageHash`] of the image data.
881    ///
882    /// # Examples
883    ///
884    /// Get an image from a PNG file embedded in the app executable using [`include_bytes!`].
885    ///
886    /// ```
887    /// # use zng_ext_image::*;
888    /// # macro_rules! include_bytes { ($tt:tt) => { &[] } }
889    /// # fn demo() {
890    /// let image_var = IMAGES.from_static(include_bytes!("ico.png"), "png");
891    /// # }
892    pub fn from_static(&self, data: &'static [u8], format: impl Into<ImageDataFormat>) -> ImageVar {
893        self.cache((data, format.into()))
894    }
895
896    /// Get a cached image from shared data.
897    ///
898    /// The image key is a [`ImageHash`] of the image data. The data reference is held only until the image is decoded.
899    ///
900    /// The data can be any of the formats described in [`ImageDataFormat`].
901    pub fn from_data(&self, data: IpcBytes, format: impl Into<ImageDataFormat>) -> ImageVar {
902        self.cache((data, format.into()))
903    }
904
905    /// Get a cached image or add it to the cache.
906    pub fn cache(&self, source: impl Into<ImageSource>) -> ImageVar {
907        self.image(source, ImageCacheMode::Cache, None, None, None)
908    }
909
910    /// Get a cached image or add it to the cache or retry if the cached image is an error.
911    pub fn retry(&self, source: impl Into<ImageSource>) -> ImageVar {
912        self.image(source, ImageCacheMode::Retry, None, None, None)
913    }
914
915    /// Load an image, if it was already cached update the cached image with the reloaded data.
916    pub fn reload(&self, source: impl Into<ImageSource>) -> ImageVar {
917        self.image(source, ImageCacheMode::Reload, None, None, None)
918    }
919
920    /// Get or load an image.
921    ///
922    /// If `limits` is `None` the [`IMAGES.limits`] is used.
923    ///
924    /// [`IMAGES.limits`]: IMAGES::limits
925    pub fn image(
926        &self,
927        source: impl Into<ImageSource>,
928        cache_mode: impl Into<ImageCacheMode>,
929        limits: Option<ImageLimits>,
930        downscale: Option<ImageDownscale>,
931        mask: Option<ImageMaskMode>,
932    ) -> ImageVar {
933        let limits = limits.unwrap_or_else(|| IMAGES_SV.read().limits.get());
934        let proxies = mem::take(&mut IMAGES_SV.write().proxies);
935        ImagesService::proxy_then_get(proxies, source.into(), cache_mode.into(), limits, downscale, mask)
936    }
937
938    /// Await for an image source, then get or load the image.
939    ///
940    /// If `limits` is `None` the [`IMAGES.limits`] is used.
941    ///
942    /// This method returns immediately with a loading [`ImageVar`], when `source` is ready it
943    /// is used to get the actual [`ImageVar`] and binds it to the returned image.
944    ///
945    /// Note that the `cache_mode` always applies to the inner image, and only to the return image if `cache_key` is set.
946    ///
947    /// [`IMAGES.limits`]: IMAGES::limits
948    pub fn image_task<F>(
949        &self,
950        source: impl IntoFuture<IntoFuture = F>,
951        cache_mode: impl Into<ImageCacheMode>,
952        cache_key: Option<ImageHash>,
953        limits: Option<ImageLimits>,
954        downscale: Option<ImageDownscale>,
955        mask: Option<ImageMaskMode>,
956    ) -> ImageVar
957    where
958        F: Future<Output = ImageSource> + Send + 'static,
959    {
960        let cache_mode = cache_mode.into();
961
962        if let Some(key) = cache_key {
963            match cache_mode {
964                ImageCacheMode::Cache => {
965                    if let Some(v) = IMAGES_SV.read().cache.get(&key) {
966                        return v.image.read_only();
967                    }
968                }
969                ImageCacheMode::Retry => {
970                    if let Some(e) = IMAGES_SV.read().cache.get(&key)
971                        && !e.error.load(Ordering::Relaxed)
972                    {
973                        return e.image.read_only();
974                    }
975                }
976                ImageCacheMode::Ignore | ImageCacheMode::Reload => {}
977            }
978        }
979
980        let source = source.into_future();
981        let img = var(Img::new_none(cache_key));
982
983        task::spawn(async_clmv!(img, {
984            let source = source.await;
985            let actual_img = IMAGES.image(source, cache_mode, limits, downscale, mask);
986            actual_img.set_bind(&img).perm();
987            img.hold(actual_img).perm();
988        }));
989        img.read_only()
990    }
991
992    /// Associate the `image` with the `key` in the cache.
993    ///
994    /// Returns `Ok(ImageVar)` with the new image var that tracks `image`, or `Err(ViewImage, ImageVar)`
995    /// that returns the `image` and a clone of the var already associated with the `key`.
996    pub fn register(&self, key: ImageHash, image: ViewImage) -> std::result::Result<ImageVar, (ViewImage, ImageVar)> {
997        IMAGES_SV.write().register(key, image, None)
998    }
999
1000    /// Remove the image from the cache, if it is only held by the cache.
1001    ///
1002    /// You can use [`ImageSource::hash128_read`] and [`ImageSource::hash128_download`] to get the `key`
1003    /// for files or downloads.
1004    ///
1005    /// Returns `true` if the image was removed.
1006    pub fn clean(&self, key: ImageHash) -> bool {
1007        ImagesService::proxy_then_remove(mem::take(&mut IMAGES_SV.write().proxies), &key, false)
1008    }
1009
1010    /// Remove the image from the cache, even if it is still referenced outside of the cache.
1011    ///
1012    /// You can use [`ImageSource::hash128_read`] and [`ImageSource::hash128_download`] to get the `key`
1013    /// for files or downloads.
1014    ///
1015    /// Returns `true` if the image was cached.
1016    pub fn purge(&self, key: &ImageHash) -> bool {
1017        ImagesService::proxy_then_remove(mem::take(&mut IMAGES_SV.write().proxies), key, true)
1018    }
1019
1020    /// Gets the cache key of an image.
1021    pub fn cache_key(&self, image: &Img) -> Option<ImageHash> {
1022        if let Some(key) = &image.cache_key
1023            && IMAGES_SV.read().cache.contains_key(key)
1024        {
1025            return Some(*key);
1026        }
1027        None
1028    }
1029
1030    /// If the image is cached.
1031    pub fn is_cached(&self, image: &Img) -> bool {
1032        image
1033            .cache_key
1034            .as_ref()
1035            .map(|k| IMAGES_SV.read().cache.contains_key(k))
1036            .unwrap_or(false)
1037    }
1038
1039    /// Returns an image that is not cached.
1040    ///
1041    /// If the `image` is the only reference returns it and removes it from the cache. If there are other
1042    /// references a new [`ImageVar`] is generated from a clone of the image.
1043    pub fn detach(&self, image: ImageVar) -> ImageVar {
1044        IMAGES_SV.write().detach(image)
1045    }
1046
1047    /// Clear cached images that are not referenced outside of the cache.
1048    pub fn clean_all(&self) {
1049        let mut img = IMAGES_SV.write();
1050        img.proxies.iter_mut().for_each(|p| p.clear(false));
1051        img.cache.retain(|_, v| v.image.strong_count() > 1);
1052    }
1053
1054    /// Clear all cached images, including images that are still referenced outside of the cache.
1055    ///
1056    /// Image memory only drops when all strong references are removed, so if an image is referenced
1057    /// outside of the cache it will merely be disconnected from the cache by this method.
1058    pub fn purge_all(&self) {
1059        let mut img = IMAGES_SV.write();
1060        img.cache.clear();
1061        img.proxies.iter_mut().for_each(|p| p.clear(true));
1062    }
1063
1064    /// Add a cache proxy.
1065    ///
1066    /// Proxies can intercept cache requests and map to a different request or return an image directly.
1067    pub fn install_proxy(&self, proxy: Box<dyn ImageCacheProxy>) {
1068        IMAGES_SV.write().proxies.push(proxy);
1069    }
1070
1071    /// Image format decoders implemented by the current view-process.
1072    ///
1073    /// Each text is the lower-case file extension, without the dot.
1074    pub fn available_decoders(&self) -> Vec<Txt> {
1075        VIEW_PROCESS.image_decoders().unwrap_or_default()
1076    }
1077
1078    /// Image format encoders implemented by the current view-process.
1079    ///
1080    /// Each text is the lower-case file extension, without the dot.
1081    pub fn available_encoders(&self) -> Vec<Txt> {
1082        VIEW_PROCESS.image_encoders().unwrap_or_default()
1083    }
1084}
1085struct ImageData {
1086    format: ImageDataFormat,
1087    r: std::result::Result<IpcBytes, Txt>,
1088}
1089
1090fn absolute_path(path: &Path, base: impl FnOnce() -> PathBuf, allow_escape: bool) -> PathBuf {
1091    if path.is_absolute() {
1092        normalize_path(path)
1093    } else {
1094        let mut dir = base();
1095        if allow_escape {
1096            dir.push(path);
1097            normalize_path(&dir)
1098        } else {
1099            dir.push(normalize_path(path));
1100            dir
1101        }
1102    }
1103}
1104/// Resolves `..` components, without any system request.
1105///
1106/// Source: https://github.com/rust-lang/cargo/blob/fede83ccf973457de319ba6fa0e36ead454d2e20/src/cargo/util/paths.rs#L61
1107fn normalize_path(path: &Path) -> PathBuf {
1108    use std::path::Component;
1109
1110    let mut components = path.components().peekable();
1111    let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
1112        components.next();
1113        PathBuf::from(c.as_os_str())
1114    } else {
1115        PathBuf::new()
1116    };
1117
1118    for component in components {
1119        match component {
1120            Component::Prefix(..) => unreachable!(),
1121            Component::RootDir => {
1122                ret.push(component.as_os_str());
1123            }
1124            Component::CurDir => {}
1125            Component::ParentDir => {
1126                ret.pop();
1127            }
1128            Component::Normal(c) => {
1129                ret.push(c);
1130            }
1131        }
1132    }
1133    ret
1134}