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