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 str.
696    ///
697    /// # Panics
698    ///
699    /// Panics if `media_type_suffixes` not ASCII.
700    #[deprecated = "use `from_static2`, it will replace this function next breaking release"]
701    pub const fn from_static(
702        display_name: &'static str,
703        media_type_suffixes: &'static str,
704        file_extensions: &'static str,
705        capabilities: ImageFormatCapability,
706    ) -> Self {
707        assert!(media_type_suffixes.is_ascii());
708        Self {
709            display_name: Txt::from_static(display_name),
710            media_type_suffixes: Txt::from_static(media_type_suffixes),
711            file_extensions: Txt::from_static(file_extensions),
712            magic_numbers: Txt::from_static(""),
713            capabilities,
714        }
715    }
716
717    /// From static strings.
718    ///
719    /// # Panics
720    ///
721    /// Panics if `media_type_suffixes` or `magic_numbers` are not ASCII.
722    pub const fn from_static2(
723        display_name: &'static str,
724        media_type_suffixes: &'static str,
725        file_extensions: &'static str,
726        magic_numbers: &'static str,
727        capabilities: ImageFormatCapability,
728    ) -> Self {
729        assert!(media_type_suffixes.is_ascii());
730        assert!(magic_numbers.is_ascii());
731        Self {
732            display_name: Txt::from_static(display_name),
733            media_type_suffixes: Txt::from_static(media_type_suffixes),
734            file_extensions: Txt::from_static(file_extensions),
735            magic_numbers: Txt::from_static(magic_numbers),
736            capabilities,
737        }
738    }
739
740    /// Iterate over media type suffixes.
741    pub fn media_type_suffixes_iter(&self) -> impl Iterator<Item = &str> {
742        self.media_type_suffixes.split(',').map(|e| e.trim())
743    }
744
745    /// Iterate over full media types, with `"image/"` prefix.
746    pub fn media_types(&self) -> impl Iterator<Item = Txt> {
747        self.media_type_suffixes_iter().map(Txt::from_str)
748    }
749
750    /// Iterate over extensions.
751    pub fn file_extensions_iter(&self) -> impl Iterator<Item = &str> {
752        self.file_extensions.split(',').map(|e| e.trim())
753    }
754
755    /// Checks if `f` matches any of the mime types or any of the file extensions.
756    ///
757    /// File extensions comparison ignores dot and ASCII case.
758    pub fn matches(&self, f: &str) -> bool {
759        let f = f.strip_prefix('.').unwrap_or(f);
760        let f = f.strip_prefix("image/").unwrap_or(f);
761        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))
762    }
763
764    /// Check if `file_prefix` matches any magic numbers.
765    ///
766    /// A good size for `file_prefix` is 24 bytes, it should cover all image formats.
767    pub fn matches_magic(&self, file_prefix: &[u8]) -> bool {
768        'search: for magic in self.magic_numbers.split(',') {
769            if magic.is_empty() || magic.len() > file_prefix.len() * 2 {
770                continue 'search;
771            }
772            'm: for (c, b) in magic.as_bytes().chunks_exact(2).zip(file_prefix) {
773                if c == b"xx" {
774                    continue 'm;
775                }
776                fn decode(c: u8) -> u8 {
777                    if c >= b'a' { c - b'a' + 10 } else { c - b'0' }
778                }
779                let c = (decode(c[0]) << 4) | decode(c[1]);
780                if c != *b {
781                    continue 'search;
782                }
783            }
784            return true;
785        }
786        false
787    }
788}
789
790/// Basic info about a color model.
791#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
792#[non_exhaustive]
793pub struct ColorType {
794    /// Color model name.
795    pub name: Txt,
796    /// Bits per channel.
797    pub bits: u8,
798    /// Channels per pixel.
799    pub channels: u8,
800}
801impl ColorType {
802    /// New.
803    pub const fn new(name: Txt, bits: u8, channels: u8) -> Self {
804        Self { name, bits, channels }
805    }
806
807    /// Bit length of a pixel.
808    pub fn bits_per_pixel(&self) -> u16 {
809        self.bits as u16 * self.channels as u16
810    }
811
812    /// Byte length of a pixel.
813    pub fn bytes_per_pixel(&self) -> u16 {
814        self.bits_per_pixel() / 8
815    }
816}
817impl ColorType {
818    /// BGRA8
819    pub const BGRA8: ColorType = ColorType::new(Txt::from_static("BGRA8"), 8, 4);
820    /// RGBA8
821    pub const RGBA8: ColorType = ColorType::new(Txt::from_static("RGBA8"), 8, 4);
822
823    /// A8
824    pub const A8: ColorType = ColorType::new(Txt::from_static("A8"), 8, 4);
825}
826
827/// Represent a image encode request.
828#[derive(Debug, Clone, Serialize, Deserialize)]
829#[non_exhaustive]
830pub struct ImageEncodeRequest {
831    /// Image to encode.
832    pub id: ImageId,
833
834    /// Optional entries to also encode.
835    ///
836    /// If set encodes the `id` as the first *page* followed by each entry in the order given.
837    pub entries: Vec<(ImageId, ImageEntryKind)>,
838
839    /// Format query, view-process uses [`ImageFormat::matches`] to find the format.
840    pub format: Txt,
841}
842impl ImageEncodeRequest {
843    /// New.
844    pub fn new(id: ImageId, format: Txt) -> Self {
845        Self {
846            id,
847            entries: vec![],
848            format,
849        }
850    }
851}