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) => (downscale_fit_fill(page_size, *s, false), vec![]),
225            ImageDownscaleMode::Fill(s) => (downscale_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 = 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) = downscale_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 downscale_fit_fill(page_size, *s, false) {
281                Some(s) => sizes.push(s),
282                None => *include_full_size = true,
283            },
284            ImageDownscaleMode::Fill(s) => match downscale_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
306/// Calculate the fit or fill size.
307///
308/// Returns `None` if the `source_size` is already smaller or equal to the `constraints`.
309pub fn downscale_fit_fill(source_size: PxSize, constraints: PxSize, fill: bool) -> Option<PxSize> {
310    let source_size = source_size.cast::<f64>();
311    let new_size = constraints.cast::<f64>();
312
313    let w_ratio = new_size.width / source_size.width;
314    let h_ratio = new_size.height / source_size.height;
315
316    let ratio = if fill {
317        f64::max(w_ratio, h_ratio)
318    } else {
319        f64::min(w_ratio, h_ratio)
320    };
321
322    if ratio >= 1.0 {
323        return None;
324    }
325
326    let nw = u64::max((source_size.width * ratio).round() as _, 1);
327    let nh = u64::max((source_size.height * ratio).round() as _, 1);
328
329    const MAX: u64 = Px::MAX.0 as _;
330
331    let r = if nw > MAX {
332        let ratio = MAX as f64 / source_size.width;
333        (Px::MAX, Px(i32::max((source_size.height * ratio).round() as _, 1)))
334    } else if nh > MAX {
335        let ratio = MAX as f64 / source_size.height;
336        (Px(i32::max((source_size.width * ratio).round() as _, 1)), Px::MAX)
337    } else {
338        (Px(nw as _), Px(nh as _))
339    }
340    .into();
341
342    Some(r)
343}
344
345/// Format of the image bytes.
346#[derive(Debug, Clone, Serialize, Deserialize)]
347#[non_exhaustive]
348pub enum ImageDataFormat {
349    /// Decoded BGRA8.
350    ///
351    /// This is the internal image format, it indicates the image data
352    /// is already decoded and color managed (to sRGB).
353    Bgra8 {
354        /// Size in pixels.
355        size: PxSize,
356        /// Pixel density of the image.
357        density: Option<PxDensity2d>,
358        /// Original color type of the image.
359        original_color_type: ColorType,
360    },
361
362    /// Decoded A8.
363    ///
364    /// This is the internal mask format it indicates the mask data
365    /// is already decoded.
366    A8 {
367        /// Size in pixels.
368        size: PxSize,
369    },
370
371    /// The image is encoded.
372    ///
373    /// This file extension maybe identifies the format. Fallback to `Unknown` handling if the file extension
374    /// is unknown or the file header does not match.
375    FileExtension(Txt),
376
377    /// The image is encoded.
378    ///
379    /// This MIME type maybe identifies the format. Fallback to `Unknown` handling if the file extension
380    /// is unknown or the file header does not match.
381    MimeType(Txt),
382
383    /// The image is encoded.
384    ///
385    /// A decoder will be selected using the "magic number" at the start of the bytes buffer.
386    Unknown,
387}
388impl From<Txt> for ImageDataFormat {
389    fn from(ext_or_mime: Txt) -> Self {
390        if ext_or_mime.contains('/') {
391            ImageDataFormat::MimeType(ext_or_mime)
392        } else {
393            ImageDataFormat::FileExtension(ext_or_mime)
394        }
395    }
396}
397impl From<&str> for ImageDataFormat {
398    fn from(ext_or_mime: &str) -> Self {
399        Txt::from_str(ext_or_mime).into()
400    }
401}
402impl From<PxSize> for ImageDataFormat {
403    fn from(bgra8_size: PxSize) -> Self {
404        ImageDataFormat::Bgra8 {
405            size: bgra8_size,
406            density: None,
407            original_color_type: ColorType::BGRA8,
408        }
409    }
410}
411impl PartialEq for ImageDataFormat {
412    fn eq(&self, other: &Self) -> bool {
413        match (self, other) {
414            (Self::FileExtension(l0), Self::FileExtension(r0)) => l0 == r0,
415            (Self::MimeType(l0), Self::MimeType(r0)) => l0 == r0,
416            (
417                Self::Bgra8 {
418                    size: s0,
419                    density: p0,
420                    original_color_type: oc0,
421                },
422                Self::Bgra8 {
423                    size: s1,
424                    density: p1,
425                    original_color_type: oc1,
426                },
427            ) => s0 == s1 && density_key(*p0) == density_key(*p1) && oc0 == oc1,
428            (Self::Unknown, Self::Unknown) => true,
429            _ => false,
430        }
431    }
432}
433impl Eq for ImageDataFormat {}
434impl std::hash::Hash for ImageDataFormat {
435    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
436        core::mem::discriminant(self).hash(state);
437        match self {
438            ImageDataFormat::Bgra8 {
439                size,
440                density,
441                original_color_type,
442            } => {
443                size.hash(state);
444                density_key(*density).hash(state);
445                original_color_type.hash(state)
446            }
447            ImageDataFormat::A8 { size } => {
448                size.hash(state);
449            }
450            ImageDataFormat::FileExtension(ext) => ext.hash(state),
451            ImageDataFormat::MimeType(mt) => mt.hash(state),
452            ImageDataFormat::Unknown => {}
453        }
454    }
455}
456
457fn density_key(density: Option<PxDensity2d>) -> Option<(u16, u16)> {
458    density.map(|s| ((s.width.ppcm() * 3.0) as u16, (s.height.ppcm() * 3.0) as u16))
459}
460
461/// Represents decoded header metadata about an image position in a container represented by another image.
462#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
463#[non_exhaustive]
464pub struct ImageEntryMetadata {
465    /// Image this one belongs too.
466    ///
467    /// The view-process always sends the parent image metadata first, so this id should be known by the app-process.
468    pub parent: ImageId,
469    /// Sort index of the image in the list of entries.
470    pub index: usize,
471    /// Kind of entry.
472    pub kind: ImageEntryKind,
473}
474impl ImageEntryMetadata {
475    /// New.
476    pub fn new(parent: ImageId, index: usize, kind: ImageEntryKind) -> Self {
477        Self { parent, index, kind }
478    }
479}
480
481/// Represents decoded header metadata about an image.
482#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
483#[non_exhaustive]
484pub struct ImageMetadata {
485    /// Image ID.
486    pub id: ImageId,
487    /// Pixel size.
488    pub size: PxSize,
489    /// Pixel density metadata.
490    pub density: Option<PxDensity2d>,
491    /// If the `pixels` are in a single channel (A8).
492    pub is_mask: bool,
493    /// Image color type before it was converted to BGRA8 or A8.
494    pub original_color_type: ColorType,
495    /// The [`ImageFormat::display_name`] that was decoded or the [`ColorType::name`] if the image was not decoded.
496    pub format_name: Txt,
497    /// Extra metadata if this image is an entry in another image.
498    ///
499    /// When this is `None` the is the first [`ImageEntryKind::Page`] in the container, usually the only page.
500    pub parent: Option<ImageEntryMetadata>,
501
502    /// Custom metadata.
503    pub extensions: Vec<(ApiExtensionId, ApiExtensionPayload)>,
504}
505impl ImageMetadata {
506    /// New.
507    pub fn new(id: ImageId, size: PxSize, is_mask: bool, original_color_type: ColorType) -> Self {
508        Self {
509            id,
510            size,
511            density: None,
512            is_mask,
513            original_color_type,
514            parent: None,
515            extensions: vec![],
516            format_name: Txt::default(),
517        }
518    }
519}
520impl Default for ImageMetadata {
521    fn default() -> Self {
522        Self {
523            id: ImageId::INVALID,
524            size: Default::default(),
525            density: Default::default(),
526            is_mask: Default::default(),
527            original_color_type: ColorType::BGRA8,
528            parent: Default::default(),
529            extensions: vec![],
530            format_name: Txt::default(),
531        }
532    }
533}
534
535/// Kind of image container entry an image was decoded from.
536#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
537#[non_exhaustive]
538pub enum ImageEntryKind {
539    /// Full sized image in the container.
540    Page,
541    /// Reduced resolution alternate of the other image.
542    ///
543    /// Can be mip levels, a thumbnail or a smaller symbolic alternative designed to be more readable at smaller scale.
544    Reduced {
545        /// If reduced image was generated, not part of the image container file.
546        synthetic: bool,
547    },
548    /// Custom kind identifier.
549    Other {
550        /// Custom identifier.
551        ///
552        /// This is an implementation specific value that can be parsed.
553        kind: Txt,
554    },
555}
556impl ImageEntryKind {
557    fn discriminant(&self) -> u8 {
558        match self {
559            ImageEntryKind::Page => 0,
560            ImageEntryKind::Reduced { .. } => 1,
561            ImageEntryKind::Other { .. } => 2,
562        }
563    }
564}
565impl std::cmp::Ord for ImageEntryKind {
566    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
567        self.discriminant().cmp(&other.discriminant())
568    }
569}
570impl std::cmp::PartialOrd for ImageEntryKind {
571    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
572        Some(self.cmp(other))
573    }
574}
575
576/// Represents a partial or fully decoded image.
577///
578/// See [`Event::ImageDecoded`] for more details.
579///
580/// [`Event::ImageDecoded`]: crate::Event::ImageDecoded
581#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
582#[non_exhaustive]
583pub struct ImageDecoded {
584    /// Image metadata.
585    pub meta: ImageMetadata,
586
587    /// If the [`pixels`] only represent a partial image.
588    ///
589    /// When this is `None` the image has fully loaded.
590    ///
591    /// [`pixels`]: Self::pixels
592    pub partial: Option<PartialImageKind>,
593
594    /// Decoded pixels.
595    ///
596    /// Is BGRA8 pre-multiplied if `!is_mask` or is `A8` if `is_mask`.
597    pub pixels: IpcBytes,
598    /// If all pixels have an alpha value of 255.
599    pub is_opaque: bool,
600}
601impl Default for ImageDecoded {
602    fn default() -> Self {
603        Self {
604            meta: Default::default(),
605            partial: Default::default(),
606            pixels: Default::default(),
607            is_opaque: true,
608        }
609    }
610}
611impl ImageDecoded {
612    /// New.
613    pub fn new(meta: ImageMetadata, pixels: IpcBytes, is_opaque: bool) -> Self {
614        Self {
615            meta,
616            partial: None,
617            pixels,
618            is_opaque,
619        }
620    }
621}
622
623/// Represents what kind of partial data was decoded.
624#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
625#[non_exhaustive]
626pub enum PartialImageKind {
627    /// The [`pixels`] are a placeholder image that must fill the actual image size.
628    ///
629    /// [`pixels`]: ImageDecoded::pixels
630    Placeholder {
631        /// The placeholder size.
632        pixel_size: PxSize,
633    },
634    /// The [`pixels`] is an image with the image full width but with only `height`.
635    ///
636    /// [`pixels`]: ImageDecoded::pixels
637    Rows {
638        /// Offset of the decoded rows.
639        ///
640        /// This is 0 if the image decodes from top to bottom or is `actual_height - height` if it decodes bottom to top.
641        y: Px,
642        /// The actual height of the pixels.
643        height: Px,
644    },
645}
646
647bitflags! {
648    /// Capabilities of an [`ImageFormat`] implementation.
649    ///
650    /// Note that `DECODE` capability is omitted because the view-process can always decode formats.
651    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
652    pub struct ImageFormatCapability: u32 {
653        /// View-process can encode images in this format.
654        const ENCODE = 1 << 0;
655        /// View-process can decode multiple containers of the format with multiple image entries.
656        const DECODE_ENTRIES = 1 << 1;
657        /// View-process can encode multiple images into a single container of the format.
658        const ENCODE_ENTRIES = (1 << 2) | ImageFormatCapability::ENCODE.bits();
659        /// View-process can decode pixels as they are received.
660        ///
661        /// Note that the view-process can always handle progressive data by accumulating it and then decoding.
662        /// The decoder can also decode the metadata before receiving all data, that does not count as progressive decoding either.
663        const DECODE_PROGRESSIVE = 1 << 3;
664    }
665}
666
667/// Represents an image codec capability.
668///
669/// This type will be used in the next breaking release of the view API.
670#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
671#[non_exhaustive]
672pub struct ImageFormat {
673    /// Display name of the format.
674    pub display_name: Txt,
675
676    /// Media types (MIME) associated with the format.
677    ///
678    /// Lowercase, without `"image/"` prefix, comma separated if there is more than one.
679    pub media_type_suffixes: Txt,
680
681    /// Common file extensions associated with the format.
682    ///
683    /// Lowercase, without dot, comma separated if there is more than one.
684    pub file_extensions: Txt,
685
686    /// Identifier file prefixes.
687    ///
688    /// Lower case ASCII hexadecimals, comma separated if there is more than one, `"xx"` matches any byte.
689    pub magic_numbers: Txt,
690
691    /// Capabilities of this format.
692    pub capabilities: ImageFormatCapability,
693}
694impl ImageFormat {
695    /// From static strings.
696    ///
697    /// # Panics
698    ///
699    /// Panics if `media_type_suffixes` or `magic_numbers` are not ASCII.
700    pub const fn from_static(
701        display_name: &'static str,
702        media_type_suffixes: &'static str,
703        file_extensions: &'static str,
704        magic_numbers: &'static str,
705        capabilities: ImageFormatCapability,
706    ) -> Self {
707        assert!(media_type_suffixes.is_ascii());
708        assert!(magic_numbers.is_ascii());
709        Self {
710            display_name: Txt::from_static(display_name),
711            media_type_suffixes: Txt::from_static(media_type_suffixes),
712            file_extensions: Txt::from_static(file_extensions),
713            magic_numbers: Txt::from_static(magic_numbers),
714            capabilities,
715        }
716    }
717
718    /// Iterate over media type suffixes.
719    pub fn media_type_suffixes_iter(&self) -> impl Iterator<Item = &str> {
720        self.media_type_suffixes.split(',').map(|e| e.trim())
721    }
722
723    /// Iterate over full media types, with `"image/"` prefix.
724    pub fn media_types(&self) -> impl Iterator<Item = Txt> {
725        self.media_type_suffixes_iter().map(Txt::from_str)
726    }
727
728    /// Iterate over extensions.
729    pub fn file_extensions_iter(&self) -> impl Iterator<Item = &str> {
730        self.file_extensions.split(',').map(|e| e.trim())
731    }
732
733    /// Checks if `f` matches any of the mime types or any of the file extensions.
734    ///
735    /// File extensions comparison ignores dot and ASCII case.
736    pub fn matches(&self, f: &str) -> bool {
737        let f = f.strip_prefix('.').unwrap_or(f);
738        let f = f.strip_prefix("image/").unwrap_or(f);
739        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))
740    }
741
742    /// Check if `file_prefix` matches any magic numbers.
743    ///
744    /// A good size for `file_prefix` is 24 bytes, it should cover all image formats.
745    pub fn matches_magic(&self, file_prefix: &[u8]) -> bool {
746        'search: for magic in self.magic_numbers.split(',') {
747            if magic.is_empty() || magic.len() > file_prefix.len() * 2 {
748                continue 'search;
749            }
750            'm: for (c, b) in magic.as_bytes().chunks_exact(2).zip(file_prefix) {
751                if c == b"xx" {
752                    continue 'm;
753                }
754                fn decode(c: u8) -> u8 {
755                    if c >= b'a' { c - b'a' + 10 } else { c - b'0' }
756                }
757                let c = (decode(c[0]) << 4) | decode(c[1]);
758                if c != *b {
759                    continue 'search;
760                }
761            }
762            return true;
763        }
764        false
765    }
766}
767
768/// Basic info about a color model.
769#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
770#[non_exhaustive]
771pub struct ColorType {
772    /// Color model name.
773    pub name: Txt,
774    /// Bits per channel.
775    pub bits: u8,
776    /// Channels per pixel.
777    pub channels: u8,
778}
779impl ColorType {
780    /// New.
781    pub const fn new(name: Txt, bits: u8, channels: u8) -> Self {
782        Self { name, bits, channels }
783    }
784
785    /// Bit length of a pixel.
786    pub fn bits_per_pixel(&self) -> u16 {
787        self.bits as u16 * self.channels as u16
788    }
789
790    /// Byte length of a pixel.
791    pub fn bytes_per_pixel(&self) -> u16 {
792        self.bits_per_pixel() / 8
793    }
794}
795impl ColorType {
796    /// BGRA8
797    pub const BGRA8: ColorType = ColorType::new(Txt::from_static("BGRA8"), 8, 4);
798    /// RGBA8
799    pub const RGBA8: ColorType = ColorType::new(Txt::from_static("RGBA8"), 8, 4);
800
801    /// A8
802    pub const A8: ColorType = ColorType::new(Txt::from_static("A8"), 8, 4);
803}
804
805/// Represent a image encode request.
806#[derive(Debug, Clone, Serialize, Deserialize)]
807#[non_exhaustive]
808pub struct ImageEncodeRequest {
809    /// Image to encode.
810    pub id: ImageId,
811
812    /// Optional entries to also encode.
813    ///
814    /// If set encodes the `id` as the first *page* followed by each entry in the order given.
815    pub entries: Vec<(ImageId, ImageEntryKind)>,
816
817    /// Format query, view-process uses [`ImageFormat::matches`] to find the format.
818    pub format: Txt,
819}
820impl ImageEncodeRequest {
821    /// New.
822    pub fn new(id: ImageId, format: Txt) -> Self {
823        Self {
824            id,
825            entries: vec![],
826            format,
827        }
828    }
829}