zng_ext_image/
types.rs

1use std::{
2    env, fmt, fs, io, mem, ops,
3    path::{Path, PathBuf},
4    sync::Arc,
5};
6
7use once_cell::sync::OnceCell;
8use parking_lot::Mutex;
9use zng_app::{
10    view_process::{EncodeError, ViewImage, ViewRenderer},
11    window::WindowId,
12};
13use zng_color::Rgba;
14use zng_layout::{
15    context::LayoutMetrics,
16    unit::{ByteLength, ByteUnits, PxRect, PxSize},
17};
18use zng_task::{self as task, SignalOnce};
19use zng_txt::Txt;
20use zng_var::{AnyVar, ReadOnlyArcVar, impl_from_and_into_var};
21use zng_view_api::{ViewProcessOffline, image::ImageTextureId};
22
23use crate::render::ImageRenderWindowRoot;
24
25pub use zng_view_api::image::{ImageDataFormat, ImageDownscale, ImageMaskMode, ImagePpi};
26
27/// A custom proxy in [`IMAGES`].
28///
29/// Implementers can intercept cache requests and redirect to another cache request or returns an image directly.
30///
31/// The methods on this API are synchronous, implementers that do any potential slow processing must output
32/// a *loading* [`ImageVar`] immediately and update it with the finished pixels when ready.
33///
34/// [`IMAGES`]: super::IMAGES
35pub trait ImageCacheProxy: Send + Sync {
36    /// Intercept a get request.
37    fn get(
38        &mut self,
39        key: &ImageHash,
40        source: &ImageSource,
41        mode: ImageCacheMode,
42        downscale: Option<ImageDownscale>,
43        mask: Option<ImageMaskMode>,
44    ) -> ProxyGetResult {
45        let r = match source {
46            ImageSource::Static(_, data, image_format) => self.data(key, data, image_format, mode, downscale, mask, false),
47            ImageSource::Data(_, data, image_format) => self.data(key, data, image_format, mode, downscale, mask, false),
48            _ => return ProxyGetResult::None,
49        };
50        match r {
51            Some(img) => ProxyGetResult::Image(img),
52            None => ProxyGetResult::None,
53        }
54    }
55
56    /// Intercept a [`Data`] or [`Static`] request.
57    ///
58    /// If [`is_data_proxy`] also intercept the [`Read`] or [`Download`] data.
59    ///
60    /// If `is_loaded` is `true` the data was read or downloaded and the return var will be bound to an existing var that may already be cached.
61    /// If it is `false` the data was already loaded on the source and the return var will be returned directly, without caching.
62    ///
63    ///
64    /// [`Data`]: ImageSource::Data
65    /// [`Static`]: ImageSource::Static
66    /// [`is_data_proxy`]: ImageCacheProxy::is_data_proxy
67    /// [`Read`]: ImageSource::Read
68    /// [`Download`]: ImageSource::Download
69    #[allow(clippy::too_many_arguments)]
70    fn data(
71        &mut self,
72        key: &ImageHash,
73        data: &[u8],
74        image_format: &ImageDataFormat,
75        mode: ImageCacheMode,
76        downscale: Option<ImageDownscale>,
77        mask: Option<ImageMaskMode>,
78        is_loaded: bool,
79    ) -> Option<ImageVar> {
80        let _ = (key, data, image_format, mode, downscale, mask, is_loaded);
81        None
82    }
83
84    /// Intercept a remove request.
85    fn remove(&mut self, key: &ImageHash, purge: bool) -> ProxyRemoveResult {
86        let _ = (key, purge);
87        ProxyRemoveResult::None
88    }
89
90    /// Called when the cache is cleaned or purged.
91    fn clear(&mut self, purge: bool) {
92        let _ = purge;
93    }
94
95    /// If this proxy only handles [`Data`] and [`Static`] sources.
96    ///
97    /// When this is `true` the [`get`] call is delayed to after [`Read`] and [`Download`] have loaded the data
98    /// and is skipped for [`Render`] and [`Image`].
99    ///
100    /// This is `false` by default.
101    ///
102    /// [`get`]: ImageCacheProxy::get
103    /// [`Data`]: ImageSource::Data
104    /// [`Static`]: ImageSource::Static
105    /// [`Read`]: ImageSource::Read
106    /// [`Download`]: ImageSource::Download
107    /// [`Render`]: ImageSource::Render
108    /// [`Image`]: ImageSource::Image
109    fn is_data_proxy(&self) -> bool {
110        false
111    }
112}
113
114/// Result of an [`ImageCacheProxy`] *get* redirect.
115pub enum ProxyGetResult {
116    /// Proxy does not intercept the request.
117    ///
118    /// The cache checks other proxies and fulfills the request if no proxy intercepts.
119    None,
120    /// Load and cache using the replacement source.
121    Cache(ImageSource, ImageCacheMode, Option<ImageDownscale>, Option<ImageMaskMode>),
122    /// Return the image instead of hitting the cache.
123    Image(ImageVar),
124}
125
126/// Result of an [`ImageCacheProxy`] *remove* redirect.
127pub enum ProxyRemoveResult {
128    /// Proxy does not intercept the request.
129    ///
130    /// The cache checks other proxies and fulfills the request if no proxy intercepts.
131    None,
132    /// Removes another cached entry.
133    ///
134    /// The `bool` indicates if the entry should be purged.
135    Remove(ImageHash, bool),
136    /// Consider the request fulfilled.
137    Removed,
138}
139
140/// Represents an [`Img`] tracked by the [`IMAGES`] cache.
141///
142/// The variable updates when the image updates.
143///
144/// [`IMAGES`]: super::IMAGES
145pub type ImageVar = ReadOnlyArcVar<Img>;
146
147/// State of an [`ImageVar`].
148///
149/// Each instance of this struct represent a single state,
150#[derive(Debug, Clone)]
151pub struct Img {
152    pub(super) view: OnceCell<ViewImage>,
153    render_ids: Arc<Mutex<Vec<RenderImage>>>,
154    pub(super) done_signal: SignalOnce,
155    pub(super) cache_key: Option<ImageHash>,
156}
157impl PartialEq for Img {
158    fn eq(&self, other: &Self) -> bool {
159        self.view == other.view
160    }
161}
162impl Img {
163    pub(super) fn new_none(cache_key: Option<ImageHash>) -> Self {
164        Img {
165            view: OnceCell::new(),
166            render_ids: Arc::default(),
167            done_signal: SignalOnce::new(),
168            cache_key,
169        }
170    }
171
172    /// New from existing `ViewImage`.
173    pub fn new(view: ViewImage) -> Self {
174        let sig = view.awaiter();
175        let v = OnceCell::new();
176        let _ = v.set(view);
177        Img {
178            view: v,
179            render_ids: Arc::default(),
180            done_signal: sig,
181            cache_key: None,
182        }
183    }
184
185    /// Create a dummy image in the loaded or error state.
186    pub fn dummy(error: Option<Txt>) -> Self {
187        Self::new(ViewImage::dummy(error))
188    }
189
190    /// Returns `true` if the is still acquiring or decoding the image bytes.
191    pub fn is_loading(&self) -> bool {
192        match self.view.get() {
193            Some(v) => !v.is_loaded() && !v.is_error(),
194            None => true,
195        }
196    }
197
198    /// If the image is successfully loaded in the view-process.
199    pub fn is_loaded(&self) -> bool {
200        match self.view.get() {
201            Some(v) => v.is_loaded(),
202            None => false,
203        }
204    }
205
206    /// If the image failed to load.
207    pub fn is_error(&self) -> bool {
208        match self.view.get() {
209            Some(v) => v.is_error(),
210            None => false,
211        }
212    }
213
214    /// Returns an error message if the image failed to load.
215    pub fn error(&self) -> Option<Txt> {
216        match self.view.get() {
217            Some(v) => v.error(),
218            None => None,
219        }
220    }
221
222    /// Returns a future that awaits until this image is loaded or encountered an error.
223    pub fn wait_done(&self) -> impl Future<Output = ()> + Send + Sync + 'static {
224        self.done_signal.clone()
225    }
226
227    /// Returns the image size in pixels, or zero if it is not loaded.
228    pub fn size(&self) -> PxSize {
229        self.view.get().map(|v| v.size()).unwrap_or_else(PxSize::zero)
230    }
231
232    /// Returns the image pixel-per-inch metadata if the image is loaded and the
233    /// metadata was retrieved.
234    pub fn ppi(&self) -> Option<ImagePpi> {
235        self.view.get().and_then(|v| v.ppi())
236    }
237
238    /// Returns `true` if the image is fully opaque or it is not loaded.
239    pub fn is_opaque(&self) -> bool {
240        self.view.get().map(|v| v.is_opaque()).unwrap_or(true)
241    }
242
243    /// Returns `true` if the image pixels are a single channel (A8).
244    pub fn is_mask(&self) -> bool {
245        self.view.get().map(|v| v.is_mask()).unwrap_or(false)
246    }
247
248    /// Connection to the image resource, if it is loaded.
249    pub fn view(&self) -> Option<&ViewImage> {
250        self.view.get().filter(|&v| v.is_loaded())
251    }
252
253    /// Calculate an *ideal* layout size for the image.
254    ///
255    /// The image is scaled considering the [`ppi`] and screen scale factor. If the
256    /// image has no [`ppi`] falls back to the [`screen_ppi`] in both dimensions.
257    ///
258    /// [`ppi`]: Self::ppi
259    /// [`screen_ppi`]: LayoutMetrics::screen_ppi
260    pub fn layout_size(&self, ctx: &LayoutMetrics) -> PxSize {
261        self.calc_size(ctx, ImagePpi::splat(ctx.screen_ppi().0), false)
262    }
263
264    /// Calculate a layout size for the image.
265    ///
266    /// # Parameters
267    ///
268    /// * `ctx`: Used to get the screen resolution.
269    /// * `fallback_ppi`: Resolution used if [`ppi`] is `None`.
270    /// * `ignore_image_ppi`: If `true` always uses the `fallback_ppi` as the resolution.
271    ///
272    /// [`ppi`]: Self::ppi
273    pub fn calc_size(&self, ctx: &LayoutMetrics, fallback_ppi: ImagePpi, ignore_image_ppi: bool) -> PxSize {
274        let dpi = if ignore_image_ppi {
275            fallback_ppi
276        } else {
277            self.ppi().unwrap_or(fallback_ppi)
278        };
279
280        let s_ppi = ctx.screen_ppi();
281        let mut size = self.size();
282
283        let fct = ctx.scale_factor().0;
284        size.width *= (s_ppi.0 / dpi.x) * fct;
285        size.height *= (s_ppi.0 / dpi.y) * fct;
286
287        size
288    }
289
290    /// Reference the decoded pre-multiplied BGRA8 pixel buffer or A8 if [`is_mask`].
291    ///
292    /// [`is_mask`]: Self::is_mask
293    pub fn pixels(&self) -> Option<zng_view_api::ipc::IpcBytes> {
294        self.view.get().and_then(|v| v.pixels())
295    }
296
297    /// Copy the `rect` selection from `pixels`.
298    ///
299    /// The `rect` is in pixels, with the origin (0, 0) at the top-left of the image.
300    ///
301    /// Returns the copied selection and the pixel buffer.
302    ///
303    /// Note that the selection can change if `rect` is not fully contained by the image area.
304    pub fn copy_pixels(&self, rect: PxRect) -> Option<(PxRect, Vec<u8>)> {
305        self.pixels().map(|pixels| {
306            let area = PxRect::from_size(self.size()).intersection(&rect).unwrap_or_default();
307            if area.size.width.0 == 0 || area.size.height.0 == 0 {
308                (area, vec![])
309            } else {
310                let x = area.origin.x.0 as usize;
311                let y = area.origin.y.0 as usize;
312                let width = area.size.width.0 as usize;
313                let height = area.size.height.0 as usize;
314                let pixel = if self.is_mask() { 1 } else { 4 };
315                let mut bytes = Vec::with_capacity(width * height * pixel);
316                let row_stride = self.size().width.0 as usize * pixel;
317                for l in y..y + height {
318                    let line_start = l * row_stride + x * pixel;
319                    let line_end = line_start + width * pixel;
320                    let line = &pixels[line_start..line_end];
321                    bytes.extend_from_slice(line);
322                }
323                (area, bytes)
324            }
325        })
326    }
327
328    /// Encode the image to the format.
329    pub async fn encode(&self, format: Txt) -> std::result::Result<zng_view_api::ipc::IpcBytes, EncodeError> {
330        self.done_signal.clone().await;
331        if let Some(e) = self.error() {
332            Err(EncodeError::Encode(e))
333        } else {
334            self.view.get().unwrap().encode(format).await
335        }
336    }
337
338    /// Encode and write the image to `path`.
339    ///
340    /// The image format is guessed from the file extension.
341    pub async fn save(&self, path: impl Into<PathBuf>) -> io::Result<()> {
342        let path = path.into();
343        if let Some(ext) = path.extension().and_then(|s| s.to_str()) {
344            self.save_impl(Txt::from_str(ext), path).await
345        } else {
346            Err(io::Error::new(
347                io::ErrorKind::InvalidInput,
348                "could not determinate image format from path extension",
349            ))
350        }
351    }
352
353    /// Encode and write the image to `path`.
354    ///
355    /// The image is encoded to the `format`, the file extension can be anything.
356    pub async fn save_with_format(&self, format: Txt, path: impl Into<PathBuf>) -> io::Result<()> {
357        self.save_impl(format, path.into()).await
358    }
359
360    async fn save_impl(&self, format: Txt, path: PathBuf) -> io::Result<()> {
361        let view = self.view.get().unwrap();
362        let data = view
363            .encode(format)
364            .await
365            .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
366        task::wait(move || fs::write(path, &data[..])).await
367    }
368}
369impl zng_app::render::Img for Img {
370    fn renderer_id(&self, renderer: &ViewRenderer) -> ImageTextureId {
371        if self.is_loaded() {
372            let mut rms = self.render_ids.lock();
373            if let Some(rm) = rms.iter().find(|k| &k.renderer == renderer) {
374                return rm.image_id;
375            }
376
377            let key = match renderer.use_image(self.view.get().unwrap()) {
378                Ok(k) => {
379                    if k == ImageTextureId::INVALID {
380                        tracing::error!("received INVALID from `use_image`");
381                        return k;
382                    }
383                    k
384                }
385                Err(ViewProcessOffline) => {
386                    tracing::debug!("respawned `add_image`, will return INVALID");
387                    return ImageTextureId::INVALID;
388                }
389            };
390
391            rms.push(RenderImage {
392                image_id: key,
393                renderer: renderer.clone(),
394            });
395            key
396        } else {
397            ImageTextureId::INVALID
398        }
399    }
400
401    fn size(&self) -> PxSize {
402        self.size()
403    }
404}
405
406struct RenderImage {
407    image_id: ImageTextureId,
408    renderer: ViewRenderer,
409}
410impl Drop for RenderImage {
411    fn drop(&mut self) {
412        // error here means the entire renderer was dropped.
413        let _ = self.renderer.delete_image_use(self.image_id);
414    }
415}
416impl fmt::Debug for RenderImage {
417    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
418        fmt::Debug::fmt(&self.image_id, f)
419    }
420}
421
422/// A 256-bit hash for image entries.
423///
424/// This hash is used to identify image files in the [`IMAGES`] cache.
425///
426/// Use [`ImageHasher`] to compute.
427///
428/// [`IMAGES`]: super::IMAGES
429#[derive(Clone, Copy)]
430pub struct ImageHash([u8; 32]);
431impl ImageHash {
432    /// Compute the hash for `data`.
433    pub fn compute(data: &[u8]) -> Self {
434        let mut h = Self::hasher();
435        h.update(data);
436        h.finish()
437    }
438
439    /// Start a new [`ImageHasher`].
440    pub fn hasher() -> ImageHasher {
441        ImageHasher::default()
442    }
443}
444impl fmt::Debug for ImageHash {
445    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
446        if f.alternate() {
447            f.debug_tuple("ImageHash").field(&self.0).finish()
448        } else {
449            use base64::*;
450            write!(f, "{}", base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(self.0))
451        }
452    }
453}
454impl fmt::Display for ImageHash {
455    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
456        write!(f, "{self:?}")
457    }
458}
459impl std::hash::Hash for ImageHash {
460    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
461        let h64 = [
462            self.0[0], self.0[1], self.0[2], self.0[3], self.0[4], self.0[5], self.0[6], self.0[7],
463        ];
464        state.write_u64(u64::from_ne_bytes(h64))
465    }
466}
467impl PartialEq for ImageHash {
468    fn eq(&self, other: &Self) -> bool {
469        self.0 == other.0
470    }
471}
472impl Eq for ImageHash {}
473
474/// Hasher that computes a [`ImageHash`].
475pub struct ImageHasher(sha2::Sha512_256);
476impl Default for ImageHasher {
477    fn default() -> Self {
478        use sha2::Digest;
479        Self(sha2::Sha512_256::new())
480    }
481}
482impl ImageHasher {
483    /// New default hasher.
484    pub fn new() -> Self {
485        Self::default()
486    }
487
488    /// Process data, updating the internal state.
489    pub fn update(&mut self, data: impl AsRef<[u8]>) {
490        use sha2::Digest;
491        self.0.update(data);
492    }
493
494    /// Finish computing the hash.
495    pub fn finish(self) -> ImageHash {
496        use sha2::Digest;
497        ImageHash(self.0.finalize().as_slice().try_into().unwrap())
498    }
499}
500impl std::hash::Hasher for ImageHasher {
501    fn finish(&self) -> u64 {
502        tracing::warn!("Hasher::finish called for ImageHasher");
503
504        use sha2::Digest;
505        let hash = self.0.clone().finalize();
506        u64::from_le_bytes(hash[..8].try_into().unwrap())
507    }
508
509    fn write(&mut self, bytes: &[u8]) {
510        self.update(bytes);
511    }
512}
513
514// We don't use Arc<dyn ..> because of this issue: https://github.com/rust-lang/rust/issues/69757
515type RenderFn = Arc<Box<dyn Fn(&ImageRenderArgs) -> Box<dyn ImageRenderWindowRoot> + Send + Sync>>;
516
517/// Arguments for the [`ImageSource::Render`] closure.
518///
519/// The arguments are set by the widget that will render the image.
520#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
521pub struct ImageRenderArgs {
522    /// Window that will render the image.
523    pub parent: Option<WindowId>,
524}
525
526/// The different sources of an image resource.
527#[derive(Clone)]
528pub enum ImageSource {
529    /// A path to an image file in the file system.
530    ///
531    /// Image equality is defined by the path, a copy of the image in another path is a different image.
532    Read(PathBuf),
533    /// A uri to an image resource downloaded using HTTP GET with an optional HTTP ACCEPT string.
534    ///
535    /// If the ACCEPT line is not given, all image formats supported by the view-process backend are accepted.
536    ///
537    /// Image equality is defined by the URI and ACCEPT string.
538    #[cfg(feature = "http")]
539    Download(crate::task::http::Uri, Option<Txt>),
540    /// Static bytes for an encoded or decoded image.
541    ///
542    /// Image equality is defined by the hash, it is usually the hash of the bytes but it does not need to be.
543    Static(ImageHash, &'static [u8], ImageDataFormat),
544    /// Shared reference to bytes for an encoded or decoded image.
545    ///
546    /// Image equality is defined by the hash, it is usually the hash of the bytes but it does not need to be.
547    ///
548    /// Inside [`IMAGES`] the reference to the bytes is held only until the image finishes decoding.
549    ///
550    /// [`IMAGES`]: super::IMAGES
551    Data(ImageHash, Arc<Vec<u8>>, ImageDataFormat),
552
553    /// A boxed closure that instantiates a `WindowRoot` that draws the image.
554    ///
555    /// Use the [`render`](Self::render) or [`render_node`](Self::render_node) functions to construct this variant.
556    ///
557    /// The closure is set by the image widget user, the args is set by the image widget.
558    Render(RenderFn, Option<ImageRenderArgs>),
559
560    /// Already resolved (loaded or loading) image.
561    ///
562    /// The image is passed-through, not cached.
563    Image(ImageVar),
564}
565impl ImageSource {
566    /// New image data from solid color.
567    pub fn flood(size: impl Into<PxSize>, color: impl Into<Rgba>, ppi: Option<ImagePpi>) -> Self {
568        let size = size.into();
569        let color = color.into();
570        let len = size.width.0 as usize * size.height.0 as usize * 4;
571        let mut data = vec![0; len];
572        for bgra in data.chunks_exact_mut(4) {
573            let rgba = color.to_bytes();
574            bgra[0] = rgba[2];
575            bgra[1] = rgba[1];
576            bgra[2] = rgba[0];
577            bgra[3] = rgba[3];
578        }
579        Self::from_data(Arc::new(data), ImageDataFormat::Bgra8 { size, ppi })
580    }
581
582    /// New source from data.
583    pub fn from_data(data: Arc<Vec<u8>>, format: ImageDataFormat) -> Self {
584        let mut hasher = ImageHasher::default();
585        hasher.update(&data[..]);
586        let hash = hasher.finish();
587        Self::Data(hash, data, format)
588    }
589
590    /// New source from static data.
591    pub fn from_static(data: &'static [u8], format: ImageDataFormat) -> Self {
592        let mut hasher = ImageHasher::default();
593        hasher.update(data);
594        let hash = hasher.finish();
595        Self::Static(hash, data, format)
596    }
597
598    /// Returns the image hash, unless the source is [`Img`].
599    pub fn hash128(&self, downscale: Option<ImageDownscale>, mask: Option<ImageMaskMode>) -> Option<ImageHash> {
600        match self {
601            ImageSource::Read(p) => Some(Self::hash128_read(p, downscale, mask)),
602            #[cfg(feature = "http")]
603            ImageSource::Download(u, a) => Some(Self::hash128_download(u, a, downscale, mask)),
604            ImageSource::Static(h, _, _) => Some(Self::hash128_data(*h, downscale, mask)),
605            ImageSource::Data(h, _, _) => Some(Self::hash128_data(*h, downscale, mask)),
606            ImageSource::Render(rfn, args) => Some(Self::hash128_render(rfn, args, downscale, mask)),
607            ImageSource::Image(_) => None,
608        }
609    }
610
611    /// Compute hash for a borrowed [`Static`] or [`Data`] image.
612    ///
613    /// [`Static`]: Self::Static
614    /// [`Data`]: Self::Data
615    pub fn hash128_data(data_hash: ImageHash, downscale: Option<ImageDownscale>, mask: Option<ImageMaskMode>) -> ImageHash {
616        if downscale.is_some() || mask.is_some() {
617            use std::hash::Hash;
618            let mut h = ImageHash::hasher();
619            data_hash.0.hash(&mut h);
620            downscale.hash(&mut h);
621            mask.hash(&mut h);
622            h.finish()
623        } else {
624            data_hash
625        }
626    }
627
628    /// Compute hash for a borrowed [`Read`] path.
629    ///
630    /// [`Read`]: Self::Read
631    pub fn hash128_read(path: &Path, downscale: Option<ImageDownscale>, mask: Option<ImageMaskMode>) -> ImageHash {
632        use std::hash::Hash;
633        let mut h = ImageHash::hasher();
634        0u8.hash(&mut h);
635        path.hash(&mut h);
636        downscale.hash(&mut h);
637        mask.hash(&mut h);
638        h.finish()
639    }
640
641    /// Compute hash for a borrowed [`Download`] URI and HTTP-ACCEPT.
642    ///
643    /// [`Download`]: Self::Download
644    #[cfg(feature = "http")]
645    pub fn hash128_download(
646        uri: &crate::task::http::Uri,
647        accept: &Option<Txt>,
648        downscale: Option<ImageDownscale>,
649        mask: Option<ImageMaskMode>,
650    ) -> ImageHash {
651        use std::hash::Hash;
652        let mut h = ImageHash::hasher();
653        1u8.hash(&mut h);
654        uri.hash(&mut h);
655        accept.hash(&mut h);
656        downscale.hash(&mut h);
657        mask.hash(&mut h);
658        h.finish()
659    }
660
661    /// Compute hash for a borrowed [`Render`] source.
662    ///
663    /// Pointer equality is used to identify the node closure.
664    ///
665    /// [`Render`]: Self::Render
666    pub fn hash128_render(
667        rfn: &RenderFn,
668        args: &Option<ImageRenderArgs>,
669        downscale: Option<ImageDownscale>,
670        mask: Option<ImageMaskMode>,
671    ) -> ImageHash {
672        use std::hash::Hash;
673        let mut h = ImageHash::hasher();
674        2u8.hash(&mut h);
675        (Arc::as_ptr(rfn) as usize).hash(&mut h);
676        args.hash(&mut h);
677        downscale.hash(&mut h);
678        mask.hash(&mut h);
679        h.finish()
680    }
681}
682impl PartialEq for ImageSource {
683    fn eq(&self, other: &Self) -> bool {
684        match (self, other) {
685            (Self::Read(l), Self::Read(r)) => l == r,
686            #[cfg(feature = "http")]
687            (Self::Download(lu, la), Self::Download(ru, ra)) => lu == ru && la == ra,
688            (Self::Render(lf, la), Self::Render(rf, ra)) => Arc::ptr_eq(lf, rf) && la == ra,
689            (Self::Image(l), Self::Image(r)) => l.var_ptr() == r.var_ptr(),
690            (l, r) => {
691                let l_hash = match l {
692                    ImageSource::Static(h, _, _) => h,
693                    ImageSource::Data(h, _, _) => h,
694                    _ => return false,
695                };
696                let r_hash = match r {
697                    ImageSource::Static(h, _, _) => h,
698                    ImageSource::Data(h, _, _) => h,
699                    _ => return false,
700                };
701
702                l_hash == r_hash
703            }
704        }
705    }
706}
707impl fmt::Debug for ImageSource {
708    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
709        if f.alternate() {
710            write!(f, "ImageSource::")?;
711        }
712        match self {
713            ImageSource::Read(p) => f.debug_tuple("Read").field(p).finish(),
714            #[cfg(feature = "http")]
715            ImageSource::Download(u, a) => f.debug_tuple("Download").field(u).field(a).finish(),
716            ImageSource::Static(key, _, fmt) => f.debug_tuple("Static").field(key).field(fmt).finish(),
717            ImageSource::Data(key, _, fmt) => f.debug_tuple("Data").field(key).field(fmt).finish(),
718            ImageSource::Render(_, _) => write!(f, "Render(_, _)"),
719            ImageSource::Image(_) => write!(f, "Image(_)"),
720        }
721    }
722}
723
724#[cfg(feature = "http")]
725impl_from_and_into_var! {
726    fn from(uri: crate::task::http::Uri) -> ImageSource {
727        ImageSource::Download(uri, None)
728    }
729    /// From (URI, HTTP-ACCEPT).
730    fn from((uri, accept): (crate::task::http::Uri, &'static str)) -> ImageSource {
731        ImageSource::Download(uri, Some(accept.into()))
732    }
733
734    /// Converts `http://` and `https://` to [`Download`], `file://` to
735    /// [`Read`] the path component, and the rest to [`Read`] the string as a path.
736    ///
737    /// [`Download`]: ImageSource::Download
738    /// [`Read`]: ImageSource::Read
739    fn from(s: &str) -> ImageSource {
740        use crate::task::http::uri::*;
741        if let Ok(uri) = Uri::try_from(s) {
742            if let Some(scheme) = uri.scheme() {
743                if scheme == &Scheme::HTTPS || scheme == &Scheme::HTTP {
744                    return ImageSource::Download(uri, None);
745                } else if scheme.as_str() == "file" {
746                    return PathBuf::from(uri.path()).into();
747                }
748            }
749        }
750        PathBuf::from(s).into()
751    }
752}
753
754#[cfg(not(feature = "http"))]
755impl_from_and_into_var! {
756    /// Converts to [`Read`].
757    ///
758    /// [`Read`]: ImageSource::Read
759    fn from(s: &str) -> ImageSource {
760        PathBuf::from(s).into()
761    }
762}
763
764impl_from_and_into_var! {
765    fn from(image: ImageVar) -> ImageSource {
766        ImageSource::Image(image)
767    }
768    fn from(path: PathBuf) -> ImageSource {
769        ImageSource::Read(path)
770    }
771    fn from(path: &Path) -> ImageSource {
772        path.to_owned().into()
773    }
774
775    /// Same as conversion from `&str`.
776    fn from(s: String) -> ImageSource {
777        s.as_str().into()
778    }
779    /// Same as conversion from `&str`.
780    fn from(s: Txt) -> ImageSource {
781        s.as_str().into()
782    }
783    /// From encoded data of [`Unknown`] format.
784    ///
785    /// [`Unknown`]: ImageDataFormat::Unknown
786    fn from(data: &'static [u8]) -> ImageSource {
787        ImageSource::Static(ImageHash::compute(data), data, ImageDataFormat::Unknown)
788    }
789    /// From encoded data of [`Unknown`] format.
790    ///
791    /// [`Unknown`]: ImageDataFormat::Unknown
792    fn from<const N: usize>(data: &'static [u8; N]) -> ImageSource {
793        (&data[..]).into()
794    }
795    /// From encoded data of [`Unknown`] format.
796    ///
797    /// [`Unknown`]: ImageDataFormat::Unknown
798    fn from(data: Arc<Vec<u8>>) -> ImageSource {
799        ImageSource::Data(ImageHash::compute(&data[..]), data, ImageDataFormat::Unknown)
800    }
801    /// From encoded data of [`Unknown`] format.
802    ///
803    /// [`Unknown`]: ImageDataFormat::Unknown
804    fn from(data: Vec<u8>) -> ImageSource {
805        Arc::new(data).into()
806    }
807    /// From encoded data of known format.
808    fn from<F: Into<ImageDataFormat>>((data, format): (&'static [u8], F)) -> ImageSource {
809        ImageSource::Static(ImageHash::compute(data), data, format.into())
810    }
811    /// From encoded data of known format.
812    fn from<F: Into<ImageDataFormat>, const N: usize>((data, format): (&'static [u8; N], F)) -> ImageSource {
813        (&data[..], format).into()
814    }
815    /// From encoded data of known format.
816    fn from<F: Into<ImageDataFormat>>((data, format): (Vec<u8>, F)) -> ImageSource {
817        (Arc::new(data), format).into()
818    }
819    /// From encoded data of known format.
820    fn from<F: Into<ImageDataFormat>>((data, format): (Arc<Vec<u8>>, F)) -> ImageSource {
821        ImageSource::Data(ImageHash::compute(&data[..]), data, format.into())
822    }
823}
824
825/// Cache mode of [`IMAGES`].
826///
827/// [`IMAGES`]: super::IMAGES
828#[derive(Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
829pub enum ImageCacheMode {
830    /// Don't hit the cache, just loads the image.
831    Ignore,
832    /// Gets a cached image or loads the image and caches it.
833    Cache,
834    /// Cache or reload if the cached image is an error.
835    Retry,
836    /// Reloads the cache image or loads the image and caches it.
837    ///
838    /// The [`ImageVar`] is not replaced, other references to the image also receive the update.
839    Reload,
840}
841impl fmt::Debug for ImageCacheMode {
842    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
843        if f.alternate() {
844            write!(f, "CacheMode::")?;
845        }
846        match self {
847            Self::Ignore => write!(f, "Ignore"),
848            Self::Cache => write!(f, "Cache"),
849            Self::Retry => write!(f, "Retry"),
850            Self::Reload => write!(f, "Reload"),
851        }
852    }
853}
854
855/// Represents a [`PathFilter`] and [`UriFilter`].
856#[derive(Clone)]
857pub enum ImageSourceFilter<U> {
858    /// Block all requests of this type.
859    BlockAll,
860    /// Allow all requests of this type.
861    AllowAll,
862    /// Custom filter, returns `true` to allow a request, `false` to block.
863    Custom(Arc<dyn Fn(&U) -> bool + Send + Sync>),
864}
865impl<U> ImageSourceFilter<U> {
866    /// New [`Custom`] filter.
867    ///
868    /// [`Custom`]: Self::Custom
869    pub fn custom(allow: impl Fn(&U) -> bool + Send + Sync + 'static) -> Self {
870        Self::Custom(Arc::new(allow))
871    }
872
873    /// Combine `self` with `other`, if they both are [`Custom`], otherwise is [`BlockAll`] if any is [`BlockAll`], else
874    /// is [`AllowAll`] if any is [`AllowAll`].
875    ///
876    /// If both are [`Custom`] both filters must allow a request to pass the new filter.
877    ///
878    /// [`Custom`]: Self::Custom
879    /// [`BlockAll`]: Self::BlockAll
880    /// [`AllowAll`]: Self::AllowAll
881    pub fn and(self, other: Self) -> Self
882    where
883        U: 'static,
884    {
885        use ImageSourceFilter::*;
886        match (self, other) {
887            (BlockAll, _) | (_, BlockAll) => BlockAll,
888            (AllowAll, _) | (_, AllowAll) => AllowAll,
889            (Custom(c0), Custom(c1)) => Custom(Arc::new(move |u| c0(u) && c1(u))),
890        }
891    }
892
893    /// Combine `self` with `other`, if they both are [`Custom`], otherwise is [`AllowAll`] if any is [`AllowAll`], else
894    /// is [`BlockAll`] if any is [`BlockAll`].
895    ///
896    /// If both are [`Custom`] at least one of the filters must allow a request to pass the new filter.
897    ///
898    /// [`Custom`]: Self::Custom
899    /// [`BlockAll`]: Self::BlockAll
900    /// [`AllowAll`]: Self::AllowAll
901    pub fn or(self, other: Self) -> Self
902    where
903        U: 'static,
904    {
905        use ImageSourceFilter::*;
906        match (self, other) {
907            (AllowAll, _) | (_, AllowAll) => AllowAll,
908            (BlockAll, _) | (_, BlockAll) => BlockAll,
909            (Custom(c0), Custom(c1)) => Custom(Arc::new(move |u| c0(u) || c1(u))),
910        }
911    }
912
913    /// Returns `true` if the filter allows the request.
914    pub fn allows(&self, item: &U) -> bool {
915        match self {
916            ImageSourceFilter::BlockAll => false,
917            ImageSourceFilter::AllowAll => true,
918            ImageSourceFilter::Custom(f) => f(item),
919        }
920    }
921}
922impl<U> fmt::Debug for ImageSourceFilter<U> {
923    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
924        match self {
925            Self::BlockAll => write!(f, "BlockAll"),
926            Self::AllowAll => write!(f, "AllowAll"),
927            Self::Custom(_) => write!(f, "Custom(_)"),
928        }
929    }
930}
931impl<U: 'static> ops::BitAnd for ImageSourceFilter<U> {
932    type Output = Self;
933
934    fn bitand(self, rhs: Self) -> Self::Output {
935        self.and(rhs)
936    }
937}
938impl<U: 'static> ops::BitOr for ImageSourceFilter<U> {
939    type Output = Self;
940
941    fn bitor(self, rhs: Self) -> Self::Output {
942        self.or(rhs)
943    }
944}
945impl<U: 'static> ops::BitAndAssign for ImageSourceFilter<U> {
946    fn bitand_assign(&mut self, rhs: Self) {
947        *self = mem::replace(self, Self::BlockAll).and(rhs);
948    }
949}
950impl<U: 'static> ops::BitOrAssign for ImageSourceFilter<U> {
951    fn bitor_assign(&mut self, rhs: Self) {
952        *self = mem::replace(self, Self::BlockAll).or(rhs);
953    }
954}
955impl<U> PartialEq for ImageSourceFilter<U> {
956    fn eq(&self, other: &Self) -> bool {
957        match (self, other) {
958            (Self::Custom(l0), Self::Custom(r0)) => Arc::ptr_eq(l0, r0),
959            _ => core::mem::discriminant(self) == core::mem::discriminant(other),
960        }
961    }
962}
963
964/// Represents a [`ImageSource::Read`] path request filter.
965///
966/// Only absolute, normalized paths are shared with the [`Custom`] filter, there is no relative paths or `..` components.
967///
968/// The paths are **not** canonicalized and existence is not verified, no system requests are made with unfiltered paths.
969///
970/// See [`ImageLimits::allow_path`] for more information.
971///
972/// [`Custom`]: ImageSourceFilter::Custom
973pub type PathFilter = ImageSourceFilter<PathBuf>;
974impl PathFilter {
975    /// Allow any file inside `dir` or sub-directories of `dir`.
976    pub fn allow_dir(dir: impl AsRef<Path>) -> Self {
977        let dir = crate::absolute_path(dir.as_ref(), || env::current_dir().expect("could not access current dir"), true);
978        PathFilter::custom(move |r| r.starts_with(&dir))
979    }
980
981    /// Allow any path with the `ext` extension.
982    pub fn allow_ext(ext: impl Into<std::ffi::OsString>) -> Self {
983        let ext = ext.into();
984        PathFilter::custom(move |r| r.extension().map(|e| e == ext).unwrap_or(false))
985    }
986
987    /// Allow any file inside the [`env::current_dir`] or sub-directories.
988    ///
989    /// Note that the current directory can be changed and the filter always uses the
990    /// *fresh* current directory, use [`allow_dir`] to create a filter the always points
991    /// to the current directory at the filter creation time.
992    ///
993    /// [`allow_dir`]: Self::allow_dir
994    pub fn allow_current_dir() -> Self {
995        PathFilter::custom(|r| env::current_dir().map(|d| r.starts_with(d)).unwrap_or(false))
996    }
997
998    /// Allow any file inside the current executable directory or sub-directories.
999    pub fn allow_exe_dir() -> Self {
1000        if let Ok(mut p) = env::current_exe().and_then(dunce::canonicalize) {
1001            if p.pop() {
1002                return Self::allow_dir(p);
1003            }
1004        }
1005
1006        // not `BlockAll` so this can still be composed using `or`.
1007        Self::custom(|_| false)
1008    }
1009
1010    /// Allow any file inside the [`zng::env::res`] directory or sub-directories.
1011    ///
1012    /// [`zng::env::res`]: zng_env::res
1013    pub fn allow_res() -> Self {
1014        Self::allow_dir(zng_env::res(""))
1015    }
1016}
1017
1018/// Represents a [`ImageSource::Download`] path request filter.
1019///
1020/// See [`ImageLimits::allow_uri`] for more information.
1021#[cfg(feature = "http")]
1022pub type UriFilter = ImageSourceFilter<crate::task::http::Uri>;
1023#[cfg(feature = "http")]
1024impl UriFilter {
1025    /// Allow any file from the `host` site.
1026    pub fn allow_host(host: impl Into<Txt>) -> Self {
1027        let host = host.into();
1028        UriFilter::custom(move |u| u.authority().map(|a| a.host() == host).unwrap_or(false))
1029    }
1030}
1031
1032impl<F: Fn(&PathBuf) -> bool + Send + Sync + 'static> From<F> for PathFilter {
1033    fn from(custom: F) -> Self {
1034        PathFilter::custom(custom)
1035    }
1036}
1037
1038#[cfg(feature = "http")]
1039impl<F: Fn(&task::http::Uri) -> bool + Send + Sync + 'static> From<F> for UriFilter {
1040    fn from(custom: F) -> Self {
1041        UriFilter::custom(custom)
1042    }
1043}
1044
1045/// Limits for image loading and decoding.
1046#[derive(Clone, Debug, PartialEq)]
1047pub struct ImageLimits {
1048    /// Maximum encoded file size allowed.
1049    ///
1050    /// An error is returned if the file size surpasses this value. If the size can read before
1051    /// read/download the validation happens before download starts, otherwise the error happens when this limit
1052    /// is reached and all already downloaded bytes are dropped.
1053    ///
1054    /// The default is `100mb`.
1055    pub max_encoded_len: ByteLength,
1056    /// Maximum decoded file size allowed.
1057    ///
1058    /// An error is returned if the decoded image memory would surpass the `width * height * 4`
1059    pub max_decoded_len: ByteLength,
1060
1061    /// Filter for [`ImageSource::Read`] paths.
1062    ///
1063    /// Only paths allowed by this filter are loaded
1064    pub allow_path: PathFilter,
1065
1066    /// Filter for [`ImageSource::Download`] URIs.
1067    #[cfg(feature = "http")]
1068    pub allow_uri: UriFilter,
1069}
1070impl ImageLimits {
1071    /// No size limits, allow all paths and URIs.
1072    pub fn none() -> Self {
1073        ImageLimits {
1074            max_encoded_len: ByteLength::MAX,
1075            max_decoded_len: ByteLength::MAX,
1076            allow_path: PathFilter::AllowAll,
1077            #[cfg(feature = "http")]
1078            allow_uri: UriFilter::AllowAll,
1079        }
1080    }
1081
1082    /// Set the [`max_encoded_len`].
1083    ///
1084    /// [`max_encoded_len`]: Self::max_encoded_len
1085    pub fn with_max_encoded_len(mut self, max_encoded_size: impl Into<ByteLength>) -> Self {
1086        self.max_encoded_len = max_encoded_size.into();
1087        self
1088    }
1089
1090    /// Set the [`max_decoded_len`].
1091    ///
1092    /// [`max_decoded_len`]: Self::max_encoded_len
1093    pub fn with_max_decoded_len(mut self, max_decoded_size: impl Into<ByteLength>) -> Self {
1094        self.max_decoded_len = max_decoded_size.into();
1095        self
1096    }
1097
1098    /// Set the [`allow_path`].
1099    ///
1100    /// [`allow_path`]: Self::allow_path
1101    pub fn with_allow_path(mut self, allow_path: impl Into<PathFilter>) -> Self {
1102        self.allow_path = allow_path.into();
1103        self
1104    }
1105
1106    /// Set the [`allow_uri`].
1107    ///
1108    /// [`allow_uri`]: Self::allow_uri
1109    #[cfg(feature = "http")]
1110    pub fn with_allow_uri(mut self, allow_url: impl Into<UriFilter>) -> Self {
1111        self.allow_uri = allow_url.into();
1112        self
1113    }
1114}
1115impl Default for ImageLimits {
1116    /// 100 megabytes encoded and 4096 megabytes decoded (BMP max).
1117    ///
1118    /// Allows only paths in `zng::env::res`, blocks all downloads.
1119    fn default() -> Self {
1120        Self {
1121            max_encoded_len: 100.megabytes(),
1122            max_decoded_len: 4096.megabytes(),
1123            allow_path: PathFilter::allow_res(),
1124            #[cfg(feature = "http")]
1125            allow_uri: UriFilter::BlockAll,
1126        }
1127    }
1128}
1129impl_from_and_into_var! {
1130    fn from(some: ImageLimits) -> Option<ImageLimits>;
1131}