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    /// The [`ImageFormat::display_name`] that was decoded or the [`ColorType::name`] if the image was not decoded.
491    pub format_name: Txt,
492    /// Extra metadata if this image is an entry in another image.
493    ///
494    /// When this is `None` the is the first [`ImageEntryKind::Page`] in the container, usually the only page.
495    pub parent: Option<ImageEntryMetadata>,
496
497    /// Custom metadata.
498    pub extensions: Vec<(ApiExtensionId, ApiExtensionPayload)>,
499}
500impl ImageMetadata {
501    /// New.
502    pub fn new(id: ImageId, size: PxSize, is_mask: bool, original_color_type: ColorType) -> Self {
503        Self {
504            id,
505            size,
506            density: None,
507            is_mask,
508            original_color_type,
509            parent: None,
510            extensions: vec![],
511            format_name: Txt::default(),
512        }
513    }
514}
515impl Default for ImageMetadata {
516    fn default() -> Self {
517        Self {
518            id: ImageId::INVALID,
519            size: Default::default(),
520            density: Default::default(),
521            is_mask: Default::default(),
522            original_color_type: ColorType::BGRA8,
523            parent: Default::default(),
524            extensions: vec![],
525            format_name: Txt::default(),
526        }
527    }
528}
529
530/// Kind of image container entry an image was decoded from.
531#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
532#[non_exhaustive]
533pub enum ImageEntryKind {
534    /// Full sized image in the container.
535    Page,
536    /// Reduced resolution alternate of the other image.
537    ///
538    /// Can be mip levels, a thumbnail or a smaller symbolic alternative designed to be more readable at smaller scale.
539    Reduced {
540        /// If reduced image was generated, not part of the image container file.
541        synthetic: bool,
542    },
543    /// Custom kind identifier.
544    Other {
545        /// Custom identifier.
546        ///
547        /// This is an implementation specific value that can be parsed.
548        kind: Txt,
549    },
550}
551impl ImageEntryKind {
552    fn discriminant(&self) -> u8 {
553        match self {
554            ImageEntryKind::Page => 0,
555            ImageEntryKind::Reduced { .. } => 1,
556            ImageEntryKind::Other { .. } => 2,
557        }
558    }
559}
560impl std::cmp::Ord for ImageEntryKind {
561    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
562        self.discriminant().cmp(&other.discriminant())
563    }
564}
565impl std::cmp::PartialOrd for ImageEntryKind {
566    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
567        Some(self.cmp(other))
568    }
569}
570
571/// Represents a partial or fully decoded image.
572///
573/// See [`Event::ImageDecoded`] for more details.
574///
575/// [`Event::ImageDecoded`]: crate::Event::ImageDecoded
576#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
577#[non_exhaustive]
578pub struct ImageDecoded {
579    /// Image metadata.
580    pub meta: ImageMetadata,
581
582    /// If the [`pixels`] only represent a partial image.
583    ///
584    /// When this is `None` the image has fully loaded.
585    ///
586    /// [`pixels`]: Self::pixels
587    pub partial: Option<PartialImageKind>,
588
589    /// Decoded pixels.
590    ///
591    /// Is BGRA8 pre-multiplied if `!is_mask` or is `A8` if `is_mask`.
592    pub pixels: IpcBytes,
593    /// If all pixels have an alpha value of 255.
594    pub is_opaque: bool,
595}
596impl Default for ImageDecoded {
597    fn default() -> Self {
598        Self {
599            meta: Default::default(),
600            partial: Default::default(),
601            pixels: Default::default(),
602            is_opaque: true,
603        }
604    }
605}
606impl ImageDecoded {
607    /// New.
608    pub fn new(meta: ImageMetadata, pixels: IpcBytes, is_opaque: bool) -> Self {
609        Self {
610            meta,
611            partial: None,
612            pixels,
613            is_opaque,
614        }
615    }
616}
617
618/// Represents what kind of partial data was decoded.
619#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
620#[non_exhaustive]
621pub enum PartialImageKind {
622    /// The [`pixels`] are a placeholder image that must fill the actual image size.
623    ///
624    /// [`pixels`]: ImageDecoded::pixels
625    Placeholder {
626        /// The placeholder size.
627        pixel_size: PxSize,
628    },
629    /// The [`pixels`] is an image with the image full width but with only `height`.
630    ///
631    /// [`pixels`]: ImageDecoded::pixels
632    Rows {
633        /// Offset of the decoded rows.
634        ///
635        /// This is 0 if the image decodes from top to bottom or is `actual_height - height` if it decodes bottom to top.
636        y: Px,
637        /// The actual height of the pixels.
638        height: Px,
639    },
640}
641
642bitflags! {
643    /// Capabilities of an [`ImageFormat`] implementation.
644    ///
645    /// Note that `DECODE` capability is omitted because the view-process can always decode formats.
646    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
647    pub struct ImageFormatCapability: u32 {
648        /// View-process can encode images in this format.
649        const ENCODE = 1 << 0;
650        /// View-process can decode multiple containers of the format with multiple image entries.
651        const DECODE_ENTRIES = 1 << 1;
652        /// View-process can encode multiple images into a single container of the format.
653        const ENCODE_ENTRIES = (1 << 2) | ImageFormatCapability::ENCODE.bits();
654        /// View-process can decode pixels as they are received.
655        ///
656        /// Note that the view-process can always handle progressive data by accumulating it and then decoding.
657        /// The decoder can also decode the metadata before receiving all data, that does not count as progressive decoding either.
658        const DECODE_PROGRESSIVE = 1 << 3;
659    }
660}
661
662/// Represents an image codec capability.
663///
664/// This type will be used in the next breaking release of the view API.
665#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
666#[non_exhaustive]
667pub struct ImageFormat {
668    /// Display name of the format.
669    pub display_name: Txt,
670
671    /// Media types (MIME) associated with the format.
672    ///
673    /// Lowercase, without `"image/"` prefix, comma separated if there is more than one.
674    pub media_type_suffixes: Txt,
675
676    /// Common file extensions associated with the format.
677    ///
678    /// Lowercase, without dot, comma separated if there is more than one.
679    pub file_extensions: Txt,
680
681    /// Identifier file prefixes.
682    ///
683    /// Lower case ASCII hexadecimals, comma separated if there is more than one, `"xx"` matches any byte.
684    pub magic_numbers: Txt,
685
686    /// Capabilities of this format.
687    pub capabilities: ImageFormatCapability,
688}
689impl ImageFormat {
690    /// From static str.
691    ///
692    /// # Panics
693    ///
694    /// Panics if `media_type_suffixes` not ASCII.
695    #[deprecated = "use `from_static2`, it will replace this function next breaking release"]
696    pub const fn from_static(
697        display_name: &'static str,
698        media_type_suffixes: &'static str,
699        file_extensions: &'static str,
700        capabilities: ImageFormatCapability,
701    ) -> Self {
702        assert!(media_type_suffixes.is_ascii());
703        Self {
704            display_name: Txt::from_static(display_name),
705            media_type_suffixes: Txt::from_static(media_type_suffixes),
706            file_extensions: Txt::from_static(file_extensions),
707            magic_numbers: Txt::from_static(""),
708            capabilities,
709        }
710    }
711
712    /// From static strings.
713    ///
714    /// # Panics
715    ///
716    /// Panics if `media_type_suffixes` or `magic_numbers` are not ASCII.
717    pub const fn from_static2(
718        display_name: &'static str,
719        media_type_suffixes: &'static str,
720        file_extensions: &'static str,
721        magic_numbers: &'static str,
722        capabilities: ImageFormatCapability,
723    ) -> Self {
724        assert!(media_type_suffixes.is_ascii());
725        assert!(magic_numbers.is_ascii());
726        Self {
727            display_name: Txt::from_static(display_name),
728            media_type_suffixes: Txt::from_static(media_type_suffixes),
729            file_extensions: Txt::from_static(file_extensions),
730            magic_numbers: Txt::from_static(magic_numbers),
731            capabilities,
732        }
733    }
734
735    /// Iterate over media type suffixes.
736    pub fn media_type_suffixes_iter(&self) -> impl Iterator<Item = &str> {
737        self.media_type_suffixes.split(',').map(|e| e.trim())
738    }
739
740    /// Iterate over full media types, with `"image/"` prefix.
741    pub fn media_types(&self) -> impl Iterator<Item = Txt> {
742        self.media_type_suffixes_iter().map(Txt::from_str)
743    }
744
745    /// Iterate over extensions.
746    pub fn file_extensions_iter(&self) -> impl Iterator<Item = &str> {
747        self.file_extensions.split(',').map(|e| e.trim())
748    }
749
750    /// Checks if `f` matches any of the mime types or any of the file extensions.
751    ///
752    /// File extensions comparison ignores dot and ASCII case.
753    pub fn matches(&self, f: &str) -> bool {
754        let f = f.strip_prefix('.').unwrap_or(f);
755        let f = f.strip_prefix("image/").unwrap_or(f);
756        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))
757    }
758
759    /// Check if `file_prefix` matches any magic numbers.
760    ///
761    /// A good size for `file_prefix` is 24 bytes, it should cover all image formats.
762    pub fn matches_magic(&self, file_prefix: &[u8]) -> bool {
763        'search: for magic in self.magic_numbers.split(',') {
764            if magic.is_empty() || magic.len() > file_prefix.len() * 2 {
765                continue 'search;
766            }
767            'm: for (c, b) in magic.as_bytes().chunks_exact(2).zip(file_prefix) {
768                if c == b"xx" {
769                    continue 'm;
770                }
771                fn decode(c: u8) -> u8 {
772                    if c >= b'a' { c - b'a' + 10 } else { c - b'0' }
773                }
774                let c = (decode(c[0]) << 4) | decode(c[1]);
775                if c != *b {
776                    continue 'search;
777                }
778            }
779            return true;
780        }
781        false
782    }
783}
784
785/// Basic info about a color model.
786#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
787#[non_exhaustive]
788pub struct ColorType {
789    /// Color model name.
790    pub name: Txt,
791    /// Bits per channel.
792    pub bits: u8,
793    /// Channels per pixel.
794    pub channels: u8,
795}
796impl ColorType {
797    /// New.
798    pub const fn new(name: Txt, bits: u8, channels: u8) -> Self {
799        Self { name, bits, channels }
800    }
801
802    /// Bit length of a pixel.
803    pub fn bits_per_pixel(&self) -> u16 {
804        self.bits as u16 * self.channels as u16
805    }
806
807    /// Byte length of a pixel.
808    pub fn bytes_per_pixel(&self) -> u16 {
809        self.bits_per_pixel() / 8
810    }
811}
812impl ColorType {
813    /// BGRA8
814    pub const BGRA8: ColorType = ColorType::new(Txt::from_static("BGRA8"), 8, 4);
815    /// RGBA8
816    pub const RGBA8: ColorType = ColorType::new(Txt::from_static("RGBA8"), 8, 4);
817
818    /// A8
819    pub const A8: ColorType = ColorType::new(Txt::from_static("A8"), 8, 4);
820}
821
822/// Represent a image encode request.
823#[derive(Debug, Clone, Serialize, Deserialize)]
824#[non_exhaustive]
825pub struct ImageEncodeRequest {
826    /// Image to encode.
827    pub id: ImageId,
828
829    /// Optional entries to also encode.
830    ///
831    /// If set encodes the `id` as the first *page* followed by each entry in the order given.
832    pub entries: Vec<(ImageId, ImageEntryKind)>,
833
834    /// Format query, view-process uses [`ImageFormat::matches`] to find the format.
835    pub format: Txt,
836}
837impl ImageEncodeRequest {
838    /// New.
839    pub fn new(id: ImageId, format: Txt) -> Self {
840        Self {
841            id,
842            entries: vec![],
843            format,
844        }
845    }
846}