Skip to main content

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