zng_ext_image/
lib.rs

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