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