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