zng_view_api/
image.rs

1//! Image types.
2
3use bitflags::bitflags;
4use serde::{Deserialize, Serialize};
5use zng_task::channel::IpcBytes;
6use zng_txt::Txt;
7
8use zng_unit::{Px, PxDensity2d, PxSize};
9
10crate::declare_id! {
11    /// Id of a decoded image in the cache.
12    ///
13    /// The View Process defines the ID.
14    pub struct ImageId(_);
15
16    /// Id of an image loaded in a renderer.
17    ///
18    /// The View Process defines the ID.
19    pub struct ImageTextureId(_);
20
21    /// Id of an image encode task.
22    ///
23    /// The View Process defines the ID.
24    pub struct ImageEncodeId(_);
25}
26
27/// Defines how the A8 image mask pixels are to be derived from a source mask image.
28#[derive(Debug, Copy, Clone, Serialize, PartialEq, Eq, Hash, Deserialize, Default)]
29#[non_exhaustive]
30pub enum ImageMaskMode {
31    /// Alpha channel.
32    ///
33    /// If the image has no alpha channel masks by `Luminance`.
34    #[default]
35    A,
36    /// Blue channel.
37    ///
38    /// If the image has no color channel fallback to monochrome channel, or `A`.
39    B,
40    /// Green channel.
41    ///
42    /// If the image has no color channel fallback to monochrome channel, or `A`.
43    G,
44    /// Red channel.
45    ///
46    /// If the image has no color channel fallback to monochrome channel, or `A`.
47    R,
48    /// Relative luminance.
49    ///
50    /// If the image has no color channel fallback to monochrome channel, or `A`.
51    Luminance,
52}
53
54bitflags! {
55    /// Defines what images are decoded from multi image containers.
56    ///
57    /// These flags represent the different [`ImageEntryKind`].
58    #[derive(Copy, Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize)]
59    pub struct ImageEntriesMode: u8 {
60        /// Decodes all pages.
61        const PAGES = 0b0001;
62        /// Decodes reduced alternates of the selected pages.
63        const REDUCED = 0b0010;
64        /// Decodes only the first page, or the page explicitly marked as primary by the container format.
65        ///
66        /// Note that this is 0, empty.
67        const PRIMARY = 0;
68
69        /// Decodes any other images that are not considered pages nor reduced alternates.
70        const OTHER = 0b1000;
71    }
72}
73#[cfg(feature = "var")]
74zng_var::impl_from_and_into_var! {
75    fn from(kind: ImageEntryKind) -> ImageEntriesMode {
76        match kind {
77            ImageEntryKind::Page => ImageEntriesMode::PAGES,
78            ImageEntryKind::Reduced { .. } => ImageEntriesMode::REDUCED,
79            ImageEntryKind::Other { .. } => ImageEntriesMode::OTHER,
80        }
81    }
82}
83
84/// Represent a image load/decode request.
85#[derive(Debug, Clone, Serialize, Deserialize)]
86#[non_exhaustive]
87pub struct ImageRequest<D> {
88    /// Image data format.
89    pub format: ImageDataFormat,
90    /// Image data.
91    ///
92    /// Bytes layout depends on the `format`, data structure is [`IpcBytes`] or [`IpcReceiver<IpcBytes>`] in the view API.
93    ///
94    /// [`IpcReceiver<IpcBytes>`]: crate::IpcReceiver
95    pub data: D,
96    /// Maximum allowed decoded size.
97    ///
98    /// View-process will avoid decoding and return an error if the image decoded to BGRA (4 bytes) exceeds this size.
99    /// This limit applies to the image before the `downscale`.
100    pub max_decoded_len: u64,
101
102    /// A size constraints to apply after the image is decoded. The image is resized to fit or fill the given size.
103    /// Optionally generate multiple reduced entries.
104    ///
105    /// If the image contains multiple images selects the nearest *reduced alternate* that can be downscaled.
106    ///
107    /// If `entries` requests `REDUCED` only the alternates smaller than the requested downscale are included.
108    pub downscale: Option<ImageDownscaleMode>,
109
110    /// Convert or decode the image into a single channel mask (R8).
111    pub mask: Option<ImageMaskMode>,
112
113    /// Defines what images are decoded from multi image containers.
114    pub entries: ImageEntriesMode,
115
116    /// Image is an entry (or subtree) of this other image.
117    ///
118    /// This value is now used by the view-process, it is just returned with the metadata. This is useful when
119    /// an already decoded image is requested after a respawn to maintain the original container structure.
120    pub parent: Option<ImageEntryMetadata>,
121}
122impl<D> ImageRequest<D> {
123    /// New request.
124    pub fn new(
125        format: ImageDataFormat,
126        data: D,
127        max_decoded_len: u64,
128        downscale: Option<ImageDownscaleMode>,
129        mask: Option<ImageMaskMode>,
130    ) -> Self {
131        Self {
132            format,
133            data,
134            max_decoded_len,
135            downscale,
136            mask,
137            entries: ImageEntriesMode::PRIMARY,
138            parent: None,
139        }
140    }
141}
142
143/// Defines how an image is downscaled after decoding.
144///
145/// The image aspect ratio is preserved in all modes. The image is never upscaled. If the image container
146/// contains reduced alternative the nearest to the requested size is used as source.
147#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
148#[non_exhaustive]
149pub enum ImageDownscaleMode {
150    /// Image is downscaled so that both dimensions fit inside the size.
151    Fit(PxSize),
152    /// Image is downscaled so that at least one dimension fits inside the size. The image is not clipped.
153    Fill(PxSize),
154    /// Generate synthetic [`ImageEntryKind::Reduced`] entries each half the size of the image until the sample that is
155    /// nearest `min_size` and greater or equal to it. If the image container already has alternates that are equal to
156    /// or *near* a mip size that size is used instead.
157    MipMap {
158        /// Minimum sample size.
159        min_size: PxSize,
160        /// Maximum `Fill` size.
161        max_size: PxSize,
162    },
163    /// Generate multiple synthetic [`ImageEntryKind::Reduced`] entries. The entry sizes are sorted by largest first,
164    /// if the image full size already fits in the largest downscale requested the full image is returned and any
165    /// downscale actually smaller becomes a synthetic entry. If the image is larger than all requested sizes it is downscaled as well.
166    Entries(Vec<ImageDownscaleMode>),
167}
168impl From<PxSize> for ImageDownscaleMode {
169    /// Fit
170    fn from(fit: PxSize) -> Self {
171        ImageDownscaleMode::Fit(fit)
172    }
173}
174impl From<Px> for ImageDownscaleMode {
175    /// Fit splat
176    fn from(fit: Px) -> Self {
177        ImageDownscaleMode::Fit(PxSize::splat(fit))
178    }
179}
180#[cfg(feature = "var")]
181zng_var::impl_from_and_into_var! {
182    fn from(fit: PxSize) -> ImageDownscaleMode;
183    fn from(fit: Px) -> ImageDownscaleMode;
184    fn from(some: ImageDownscaleMode) -> Option<ImageDownscaleMode>;
185}
186impl ImageDownscaleMode {
187    /// Default mipmap min/max when the objective of the mipmap is optimizing dynamically resizing massive images.
188    pub fn mip_map() -> Self {
189        Self::MipMap {
190            min_size: PxSize::splat(Px(512)),
191            max_size: PxSize::splat(Px::MAX),
192        }
193    }
194
195    /// Append entry downscale request.
196    pub fn with_entry(self, other: impl Into<ImageDownscaleMode>) -> Self {
197        self.with_impl(other.into())
198    }
199    fn with_impl(self, other: Self) -> Self {
200        let mut v = match self {
201            Self::Entries(e) => e,
202            s => vec![s],
203        };
204        match other {
205            Self::Entries(o) => v.extend(o),
206            o => v.push(o),
207        }
208        Self::Entries(v)
209    }
210
211    /// Get downscale sizes that need to be generated.
212    ///
213    /// The `page_size` is the image full size, the `reduced_sizes` are
214    /// sizes of reduced alternates that are already provided by the image  container.
215    ///
216    /// Returns the downscale for the image full size, if needed and a list of reduced entries that must be synthesized,
217    /// sorted largest to smallest.
218    pub fn sizes(&self, page_size: PxSize, reduced_sizes: &[PxSize]) -> (Option<PxSize>, Vec<PxSize>) {
219        match self {
220            ImageDownscaleMode::Fit(s) => (fit_fill(page_size, *s, false), vec![]),
221            ImageDownscaleMode::Fill(s) => (fit_fill(page_size, *s, true), vec![]),
222            ImageDownscaleMode::MipMap { min_size, max_size } => Self::collect_mip_map(page_size, reduced_sizes, &[], *min_size, *max_size),
223            ImageDownscaleMode::Entries(modes) => {
224                let mut include_full_size = false;
225                let mut sizes = vec![];
226                let mut mip_map = None;
227                for m in modes {
228                    m.collect_entries(page_size, &mut sizes, &mut mip_map, &mut include_full_size);
229                }
230                if let Some([min_size, max_size]) = mip_map {
231                    let (first, mips) = Self::collect_mip_map(page_size, reduced_sizes, &sizes, min_size, max_size);
232                    include_full_size |= first.is_some();
233                    sizes.extend(first);
234                    sizes.extend(mips);
235                }
236
237                sizes.sort_by_key(|s| s.width.0 * s.height.0);
238                sizes.dedup();
239
240                let full_downscale = if include_full_size { None } else { sizes.pop() };
241                sizes.reverse();
242
243                (full_downscale, sizes)
244            }
245        }
246    }
247
248    fn collect_mip_map(
249        page_size: PxSize,
250        reduced_sizes: &[PxSize],
251        entry_sizes: &[PxSize],
252        min_size: PxSize,
253        max_size: PxSize,
254    ) -> (Option<PxSize>, Vec<PxSize>) {
255        let page_downscale = fit_fill(page_size, max_size, true);
256        let mut size = page_downscale.unwrap_or(page_size) / Px(2);
257        let mut entries = vec![];
258        while min_size.width < size.width && min_size.height < size.height {
259            if let Some(entry) = fit_fill(page_size, size, true)
260                && !reduced_sizes.iter().any(|s| Self::near(entry, *s))
261                && !entry_sizes.iter().any(|s| Self::near(entry, *s))
262            {
263                entries.push(entry);
264            }
265            size /= Px(2);
266        }
267        (page_downscale, entries)
268    }
269    fn near(candidate: PxSize, existing: PxSize) -> bool {
270        let dist = (candidate - existing).abs();
271        dist.width < Px(10) && dist.height <= Px(10)
272    }
273
274    fn collect_entries(&self, page_size: PxSize, sizes: &mut Vec<PxSize>, mip_map: &mut Option<[PxSize; 2]>, include_full_size: &mut bool) {
275        match self {
276            ImageDownscaleMode::Fit(s) => match fit_fill(page_size, *s, false) {
277                Some(s) => sizes.push(s),
278                None => *include_full_size = true,
279            },
280            ImageDownscaleMode::Fill(s) => match fit_fill(page_size, *s, true) {
281                Some(s) => sizes.push(s),
282                None => *include_full_size = true,
283            },
284            ImageDownscaleMode::MipMap { min_size, max_size } => {
285                *include_full_size = true;
286                if let Some([min, max]) = mip_map {
287                    *min = min.min(*min_size);
288                    *max = max.min(*min_size);
289                } else {
290                    *mip_map = Some([*min_size, *max_size]);
291                }
292            }
293            ImageDownscaleMode::Entries(modes) => {
294                for m in modes {
295                    m.collect_entries(page_size, sizes, mip_map, include_full_size);
296                }
297            }
298        }
299    }
300}
301
302fn fit_fill(source_size: PxSize, new_size: PxSize, fill: bool) -> Option<PxSize> {
303    let source_size = source_size.cast::<f64>();
304    let new_size = new_size.cast::<f64>();
305
306    let w_ratio = new_size.width / source_size.width;
307    let h_ratio = new_size.height / source_size.height;
308
309    let ratio = if fill {
310        f64::max(w_ratio, h_ratio)
311    } else {
312        f64::min(w_ratio, h_ratio)
313    };
314
315    if ratio >= 1.0 {
316        return None;
317    }
318
319    let nw = u64::max((source_size.width * ratio).round() as _, 1);
320    let nh = u64::max((source_size.height * ratio).round() as _, 1);
321
322    const MAX: u64 = Px::MAX.0 as _;
323
324    let r = if nw > MAX {
325        let ratio = MAX as f64 / source_size.width;
326        (Px::MAX, Px(i32::max((source_size.height * ratio).round() as _, 1)))
327    } else if nh > MAX {
328        let ratio = MAX as f64 / source_size.height;
329        (Px(i32::max((source_size.width * ratio).round() as _, 1)), Px::MAX)
330    } else {
331        (Px(nw as _), Px(nh as _))
332    }
333    .into();
334
335    Some(r)
336}
337
338/// Format of the image bytes.
339#[derive(Debug, Clone, Serialize, Deserialize)]
340#[non_exhaustive]
341pub enum ImageDataFormat {
342    /// Decoded BGRA8.
343    ///
344    /// This is the internal image format, it indicates the image data
345    /// is already decoded and color managed (to sRGB).
346    Bgra8 {
347        /// Size in pixels.
348        size: PxSize,
349        /// Pixel density of the image.
350        density: Option<PxDensity2d>,
351        /// Original color type of the image.
352        original_color_type: ColorType,
353    },
354
355    /// Decoded A8.
356    ///
357    /// This is the internal mask format it indicates the mask data
358    /// is already decoded.
359    A8 {
360        /// Size in pixels.
361        size: PxSize,
362    },
363
364    /// The image is encoded.
365    ///
366    /// This file extension maybe identifies the format. Fallback to `Unknown` handling if the file extension
367    /// is unknown or the file header does not match.
368    FileExtension(Txt),
369
370    /// The image is encoded.
371    ///
372    /// This MIME type maybe identifies the format. Fallback to `Unknown` handling if the file extension
373    /// is unknown or the file header does not match.
374    MimeType(Txt),
375
376    /// The image is encoded.
377    ///
378    /// A decoder will be selected using the "magic number" at the start of the bytes buffer.
379    Unknown,
380}
381impl From<Txt> for ImageDataFormat {
382    fn from(ext_or_mime: Txt) -> Self {
383        if ext_or_mime.contains('/') {
384            ImageDataFormat::MimeType(ext_or_mime)
385        } else {
386            ImageDataFormat::FileExtension(ext_or_mime)
387        }
388    }
389}
390impl From<&str> for ImageDataFormat {
391    fn from(ext_or_mime: &str) -> Self {
392        Txt::from_str(ext_or_mime).into()
393    }
394}
395impl From<PxSize> for ImageDataFormat {
396    fn from(bgra8_size: PxSize) -> Self {
397        ImageDataFormat::Bgra8 {
398            size: bgra8_size,
399            density: None,
400            original_color_type: ColorType::BGRA8,
401        }
402    }
403}
404impl PartialEq for ImageDataFormat {
405    fn eq(&self, other: &Self) -> bool {
406        match (self, other) {
407            (Self::FileExtension(l0), Self::FileExtension(r0)) => l0 == r0,
408            (Self::MimeType(l0), Self::MimeType(r0)) => l0 == r0,
409            (
410                Self::Bgra8 {
411                    size: s0,
412                    density: p0,
413                    original_color_type: oc0,
414                },
415                Self::Bgra8 {
416                    size: s1,
417                    density: p1,
418                    original_color_type: oc1,
419                },
420            ) => s0 == s1 && density_key(*p0) == density_key(*p1) && oc0 == oc1,
421            (Self::Unknown, Self::Unknown) => true,
422            _ => false,
423        }
424    }
425}
426impl Eq for ImageDataFormat {}
427impl std::hash::Hash for ImageDataFormat {
428    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
429        core::mem::discriminant(self).hash(state);
430        match self {
431            ImageDataFormat::Bgra8 {
432                size,
433                density,
434                original_color_type,
435            } => {
436                size.hash(state);
437                density_key(*density).hash(state);
438                original_color_type.hash(state)
439            }
440            ImageDataFormat::A8 { size } => {
441                size.hash(state);
442            }
443            ImageDataFormat::FileExtension(ext) => ext.hash(state),
444            ImageDataFormat::MimeType(mt) => mt.hash(state),
445            ImageDataFormat::Unknown => {}
446        }
447    }
448}
449
450fn density_key(density: Option<PxDensity2d>) -> Option<(u16, u16)> {
451    density.map(|s| ((s.width.ppcm() * 3.0) as u16, (s.height.ppcm() * 3.0) as u16))
452}
453
454/// Represents decoded header metadata about an image position in a container represented by another image.
455#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
456#[non_exhaustive]
457pub struct ImageEntryMetadata {
458    /// Image this one belongs too.
459    ///
460    /// The view-process always sends the parent image metadata first, so this id should be known by the app-process.
461    pub parent: ImageId,
462    /// Sort index of the image in the list of entries.
463    pub index: usize,
464    /// Kind of entry.
465    pub kind: ImageEntryKind,
466}
467impl ImageEntryMetadata {
468    /// New.
469    pub fn new(parent: ImageId, index: usize, kind: ImageEntryKind) -> Self {
470        Self { parent, index, kind }
471    }
472}
473
474/// Represents decoded header metadata about an image.
475#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
476#[non_exhaustive]
477pub struct ImageMetadata {
478    /// Image ID.
479    pub id: ImageId,
480    /// Pixel size.
481    pub size: PxSize,
482    /// Pixel density metadata.
483    pub density: Option<PxDensity2d>,
484    /// If the `pixels` are in a single channel (A8).
485    pub is_mask: bool,
486    /// Image color type before it was converted to BGRA8 or A8.
487    pub original_color_type: ColorType,
488    /// Extra metadata if this image is an entry in another image.
489    ///
490    /// When this is `None` the is the first [`ImageEntryKind::Page`] in the container, usually the only page.
491    pub parent: Option<ImageEntryMetadata>,
492}
493impl ImageMetadata {
494    /// New.
495    pub fn new(id: ImageId, size: PxSize, is_mask: bool, original_color_type: ColorType) -> Self {
496        Self {
497            id,
498            size,
499            density: None,
500            is_mask,
501            original_color_type,
502            parent: None,
503        }
504    }
505}
506impl Default for ImageMetadata {
507    fn default() -> Self {
508        Self {
509            id: ImageId::INVALID,
510            size: Default::default(),
511            density: Default::default(),
512            is_mask: Default::default(),
513            original_color_type: ColorType::BGRA8,
514            parent: Default::default(),
515        }
516    }
517}
518
519/// Kind of image container entry an image was decoded from.
520#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
521#[non_exhaustive]
522pub enum ImageEntryKind {
523    /// Full sized image in the container.
524    Page,
525    /// Reduced resolution alternate of the other image.
526    ///
527    /// Can be mip levels, a thumbnail or a smaller symbolic alternative designed to be more readable at smaller scale.
528    Reduced {
529        /// If reduced image was generated, not part of the image container file.
530        synthetic: bool,
531    },
532    /// Custom kind identifier.
533    Other {
534        /// Custom identifier.
535        ///
536        /// This is an implementation specific value that can be parsed.
537        kind: Txt,
538    },
539}
540impl ImageEntryKind {
541    fn discriminant(&self) -> u8 {
542        match self {
543            ImageEntryKind::Page => 0,
544            ImageEntryKind::Reduced { .. } => 1,
545            ImageEntryKind::Other { .. } => 2,
546        }
547    }
548}
549impl std::cmp::Ord for ImageEntryKind {
550    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
551        self.discriminant().cmp(&other.discriminant())
552    }
553}
554impl std::cmp::PartialOrd for ImageEntryKind {
555    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
556        Some(self.cmp(other))
557    }
558}
559
560/// Represents a partial or fully decoded image.
561///
562/// See [`Event::ImageDecoded`] for more details.
563///
564/// [`Event::ImageDecoded`]: crate::Event::ImageDecoded
565#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
566#[non_exhaustive]
567pub struct ImageDecoded {
568    /// Image metadata.
569    pub meta: ImageMetadata,
570
571    /// If the [`pixels`] only represent a partial image.
572    ///
573    /// When this is `None` the image has fully loaded.
574    ///
575    /// [`pixels`]: Self::pixels
576    pub partial: Option<PartialImageKind>,
577
578    /// Decoded pixels.
579    ///
580    /// Is BGRA8 pre-multiplied if `!is_mask` or is `A8` if `is_mask`.
581    pub pixels: IpcBytes,
582    /// If all pixels have an alpha value of 255.
583    pub is_opaque: bool,
584}
585impl Default for ImageDecoded {
586    fn default() -> Self {
587        Self {
588            meta: Default::default(),
589            partial: Default::default(),
590            pixels: Default::default(),
591            is_opaque: true,
592        }
593    }
594}
595impl ImageDecoded {
596    /// New.
597    pub fn new(meta: ImageMetadata, pixels: IpcBytes, is_opaque: bool) -> Self {
598        Self {
599            meta,
600            partial: None,
601            pixels,
602            is_opaque,
603        }
604    }
605}
606
607/// Represents what kind of partial data was decoded.
608#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
609#[non_exhaustive]
610pub enum PartialImageKind {
611    /// The [`pixels`] are a placeholder image that must fill the actual image size.
612    ///
613    /// [`pixels`]: ImageDecoded::pixels
614    Placeholder {
615        /// The placeholder size.
616        pixel_size: PxSize,
617    },
618    /// The [`pixels`] is an image with the image full width but with only `height`.
619    ///
620    /// [`pixels`]: ImageDecoded::pixels
621    Rows {
622        /// Offset of the decoded rows.
623        ///
624        /// This is 0 if the image decodes from top to bottom or is `actual_height - height` if it decodes bottom to top.
625        y: Px,
626        /// The actual height of the pixels.
627        height: Px,
628    },
629}
630
631bitflags! {
632    /// Capabilities of an [`ImageFormat`] implementation.
633    ///
634    /// Note that `DECODE` capability is omitted because the view-process can always decode formats.
635    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
636    pub struct ImageFormatCapability: u32 {
637        /// View-process can encode images in this format.
638        const ENCODE = 1 << 0;
639        /// View-process can decode multiple containers of the format with multiple image entries.
640        const DECODE_ENTRIES = 1 << 1;
641        /// View-process can encode multiple images into a single container of the format.
642        const ENCODE_ENTRIES = (1 << 2) | ImageFormatCapability::ENCODE.bits();
643        /// View-process can decode pixels as they are received.
644        ///
645        /// Note that the view-process can always handle progressive data by accumulating it and then decoding.
646        /// The decoder can also decode the metadata before receiving all data, that does not count as progressive decoding either.
647        const DECODE_PROGRESSIVE = 1 << 3;
648    }
649}
650
651/// Represents an image codec capability.
652///
653/// This type will be used in the next breaking release of the view API.
654#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
655#[non_exhaustive]
656pub struct ImageFormat {
657    /// Display name of the format.
658    pub display_name: Txt,
659
660    /// Media types (MIME) associated with the format.
661    ///
662    /// Lowercase, without `"image/"` prefix, comma separated if there is more than one.
663    pub media_type_suffixes: Txt,
664
665    /// Common file extensions associated with the format.
666    ///
667    /// Lowercase, without dot, comma separated if there is more than one.
668    pub file_extensions: Txt,
669
670    /// Capabilities of this format.
671    pub capabilities: ImageFormatCapability,
672}
673impl ImageFormat {
674    /// From static str.
675    ///
676    /// # Panics
677    ///
678    /// Panics if `media_type_suffixes` not ASCII.
679    pub const fn from_static(
680        display_name: &'static str,
681        media_type_suffixes: &'static str,
682        file_extensions: &'static str,
683        capabilities: ImageFormatCapability,
684    ) -> Self {
685        assert!(media_type_suffixes.is_ascii());
686        Self {
687            display_name: Txt::from_static(display_name),
688            media_type_suffixes: Txt::from_static(media_type_suffixes),
689            file_extensions: Txt::from_static(file_extensions),
690            capabilities,
691        }
692    }
693
694    /// Iterate over media type suffixes.
695    pub fn media_type_suffixes_iter(&self) -> impl Iterator<Item = &str> {
696        self.media_type_suffixes.split(',').map(|e| e.trim())
697    }
698
699    /// Iterate over full media types, with `"image/"` prefix.
700    pub fn media_types(&self) -> impl Iterator<Item = Txt> {
701        self.media_type_suffixes_iter().map(Txt::from_str)
702    }
703
704    /// Iterate over extensions.
705    pub fn file_extensions_iter(&self) -> impl Iterator<Item = &str> {
706        self.file_extensions.split(',').map(|e| e.trim())
707    }
708
709    /// Checks if `f` matches any of the mime types or any of the file extensions.
710    ///
711    /// File extensions comparison ignores dot and ASCII case.
712    pub fn matches(&self, f: &str) -> bool {
713        let f = f.strip_prefix('.').unwrap_or(f);
714        let f = f.strip_prefix("image/").unwrap_or(f);
715        self.media_type_suffixes_iter().any(|e| e.eq_ignore_ascii_case(f)) || self.file_extensions_iter().any(|e| e.eq_ignore_ascii_case(f))
716    }
717}
718
719/// Basic info about a color model.
720#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
721#[non_exhaustive]
722pub struct ColorType {
723    /// Color model name.
724    pub name: Txt,
725    /// Bits per channel.
726    pub bits: u8,
727    /// Channels per pixel.
728    pub channels: u8,
729}
730impl ColorType {
731    /// New.
732    pub const fn new(name: Txt, bits: u8, channels: u8) -> Self {
733        Self { name, bits, channels }
734    }
735
736    /// Bit length of a pixel.
737    pub fn bits_per_pixel(&self) -> u16 {
738        self.bits as u16 * self.channels as u16
739    }
740
741    /// Byte length of a pixel.
742    pub fn bytes_per_pixel(&self) -> u16 {
743        self.bits_per_pixel() / 8
744    }
745}
746impl ColorType {
747    /// BGRA8
748    pub const BGRA8: ColorType = ColorType::new(Txt::from_static("BGRA8"), 8, 4);
749    /// RGBA8
750    pub const RGBA8: ColorType = ColorType::new(Txt::from_static("RGBA8"), 8, 4);
751
752    /// A8
753    pub const A8: ColorType = ColorType::new(Txt::from_static("A8"), 8, 4);
754}
755
756/// Represent a image encode request.
757#[derive(Debug, Clone, Serialize, Deserialize)]
758#[non_exhaustive]
759pub struct ImageEncodeRequest {
760    /// Image to encode.
761    pub id: ImageId,
762
763    /// Optional entries to also encode.
764    ///
765    /// If set encodes the `id` as the first *page* followed by each entry in the order given.
766    pub entries: Vec<(ImageId, ImageEntryKind)>,
767
768    /// Format query, view-process uses [`ImageFormat::matches`] to find the format.
769    pub format: Txt,
770}
771impl ImageEncodeRequest {
772    /// New.
773    pub fn new(id: ImageId, format: Txt) -> Self {
774        Self {
775            id,
776            entries: vec![],
777            format,
778        }
779    }
780}