zng_view_api/
image.rs

1//! Image types.
2
3use std::fmt;
4
5use serde::{Deserialize, Serialize};
6use zng_task::channel::IpcBytes;
7use zng_txt::Txt;
8
9use zng_unit::{Px, PxDensity2d, PxSize};
10
11crate::declare_id! {
12    /// Id of a decoded image in the cache.
13    ///
14    /// The View Process defines the ID.
15    pub struct ImageId(_);
16
17    /// Id of an image loaded in a renderer.
18    ///
19    /// The View Process defines the ID.
20    pub struct ImageTextureId(_);
21}
22
23/// Defines how the A8 image mask pixels are to be derived from a source mask image.
24#[derive(Debug, Copy, Clone, Serialize, PartialEq, Eq, Hash, Deserialize, Default)]
25#[non_exhaustive]
26pub enum ImageMaskMode {
27    /// Alpha channel.
28    ///
29    /// If the image has no alpha channel masks by `Luminance`.
30    #[default]
31    A,
32    /// Blue channel.
33    ///
34    /// If the image has no color channel fallback to monochrome channel, or `A`.
35    B,
36    /// Green channel.
37    ///
38    /// If the image has no color channel fallback to monochrome channel, or `A`.
39    G,
40    /// Red channel.
41    ///
42    /// If the image has no color channel fallback to monochrome channel, or `A`.
43    R,
44    /// Relative luminance.
45    ///
46    /// If the image has no color channel fallback to monochrome channel, or `A`.
47    Luminance,
48}
49
50/// Represent a image load/decode request.
51#[derive(Debug, Clone, Serialize, Deserialize)]
52#[non_exhaustive]
53pub struct ImageRequest<D> {
54    /// Image data format.
55    pub format: ImageDataFormat,
56    /// Image data.
57    ///
58    /// Bytes layout depends on the `format`, data structure is [`IpcBytes`] or [`IpcReceiver<IpcBytes>`] in the view API.
59    ///
60    /// [`IpcReceiver<IpcBytes>`]: crate::IpcReceiver
61    pub data: D,
62    /// Maximum allowed decoded size.
63    ///
64    /// View-process will avoid decoding and return an error if the image decoded to BGRA (4 bytes) exceeds this size.
65    /// This limit applies to the image before the `downscale`.
66    pub max_decoded_len: u64,
67    /// A size constraints to apply after the image is decoded. The image is resized to fit or fill the given size.
68    pub downscale: Option<ImageDownscale>,
69    /// Convert or decode the image into a single channel mask (R8).
70    pub mask: Option<ImageMaskMode>,
71}
72impl<D> ImageRequest<D> {
73    /// New request.
74    pub fn new(
75        format: ImageDataFormat,
76        data: D,
77        max_decoded_len: u64,
78        downscale: Option<ImageDownscale>,
79        mask: Option<ImageMaskMode>,
80    ) -> Self {
81        Self {
82            format,
83            data,
84            max_decoded_len,
85            downscale,
86            mask,
87        }
88    }
89}
90
91/// Defines how an image is downscaled after decoding.
92///
93/// The image aspect ratio is preserved in both modes, the image is not upscaled, if it already fits the size
94/// constraints if will not be resized.
95#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
96pub enum ImageDownscale {
97    /// Image is downscaled so that both dimensions fit inside the size.
98    Fit(PxSize),
99    /// Image is downscaled so that at least one dimension fits inside the size. The image is not clipped.
100    Fill(PxSize),
101}
102impl From<PxSize> for ImageDownscale {
103    /// Fit
104    fn from(fit: PxSize) -> Self {
105        ImageDownscale::Fit(fit)
106    }
107}
108impl From<Px> for ImageDownscale {
109    /// Fit splat
110    fn from(fit: Px) -> Self {
111        ImageDownscale::Fit(PxSize::splat(fit))
112    }
113}
114#[cfg(feature = "var")]
115zng_var::impl_from_and_into_var! {
116    fn from(fit: PxSize) -> ImageDownscale;
117    fn from(fit: Px) -> ImageDownscale;
118    fn from(some: ImageDownscale) -> Option<ImageDownscale>;
119}
120impl ImageDownscale {
121    /// Compute the expected final size if the downscale is applied on an image of `source_size`.
122    pub fn resize_dimensions(self, source_size: PxSize) -> PxSize {
123        let (new_size, fill) = match self {
124            ImageDownscale::Fill(s) => (s, true),
125            ImageDownscale::Fit(s) => (s, false),
126        };
127        let source_size = source_size.cast::<f64>();
128        let new_size = new_size.cast::<f64>();
129
130        let w_ratio = new_size.width / source_size.width;
131        let h_ratio = new_size.height / source_size.height;
132
133        let ratio = if fill {
134            f64::max(w_ratio, h_ratio)
135        } else {
136            f64::min(w_ratio, h_ratio)
137        };
138
139        let nw = u64::max((source_size.width * ratio).round() as _, 1);
140        let nh = u64::max((source_size.height * ratio).round() as _, 1);
141
142        const MAX: u64 = Px::MAX.0 as _;
143
144        if nw > MAX {
145            let ratio = MAX as f64 / source_size.width;
146            (Px::MAX, Px(i32::max((source_size.height * ratio).round() as _, 1))).into()
147        } else if nh > MAX {
148            let ratio = MAX as f64 / source_size.height;
149            (Px(i32::max((source_size.width * ratio).round() as _, 1)), Px::MAX).into()
150        } else {
151            (Px(nw as _), Px(nh as _)).into()
152        }
153    }
154}
155
156/// Format of the image bytes.
157#[derive(Debug, Clone, Serialize, Deserialize)]
158#[non_exhaustive]
159pub enum ImageDataFormat {
160    /// Decoded BGRA8.
161    ///
162    /// This is the internal image format, it indicates the image data
163    /// is already decoded and color managed (to sRGB).
164    Bgra8 {
165        /// Size in pixels.
166        size: PxSize,
167        /// Pixel density of the image.
168        density: Option<PxDensity2d>,
169    },
170
171    /// Decoded A8.
172    ///
173    /// This is the internal mask format it indicates the mask data
174    /// is already decoded.
175    A8 {
176        /// Size in pixels.
177        size: PxSize,
178    },
179
180    /// The image is encoded.
181    ///
182    /// This file extension maybe identifies the format. Fallback to `Unknown` handling if the file extension
183    /// is unknown or the file header does not match.
184    FileExtension(Txt),
185
186    /// The image is encoded.
187    ///
188    /// This MIME type maybe identifies the format. Fallback to `Unknown` handling if the file extension
189    /// is unknown or the file header does not match.
190    MimeType(Txt),
191
192    /// The image is encoded.
193    ///
194    /// A decoder will be selected using the "magic number" at the start of the bytes buffer.
195    Unknown,
196}
197impl From<Txt> for ImageDataFormat {
198    fn from(ext_or_mime: Txt) -> Self {
199        if ext_or_mime.contains('/') {
200            ImageDataFormat::MimeType(ext_or_mime)
201        } else {
202            ImageDataFormat::FileExtension(ext_or_mime)
203        }
204    }
205}
206impl From<&str> for ImageDataFormat {
207    fn from(ext_or_mime: &str) -> Self {
208        Txt::from_str(ext_or_mime).into()
209    }
210}
211impl From<PxSize> for ImageDataFormat {
212    fn from(bgra8_size: PxSize) -> Self {
213        ImageDataFormat::Bgra8 {
214            size: bgra8_size,
215            density: None,
216        }
217    }
218}
219impl PartialEq for ImageDataFormat {
220    fn eq(&self, other: &Self) -> bool {
221        match (self, other) {
222            (Self::FileExtension(l0), Self::FileExtension(r0)) => l0 == r0,
223            (Self::MimeType(l0), Self::MimeType(r0)) => l0 == r0,
224            (Self::Bgra8 { size: s0, density: p0 }, Self::Bgra8 { size: s1, density: p1 }) => {
225                s0 == s1 && density_key(*p0) == density_key(*p1)
226            }
227            (Self::Unknown, Self::Unknown) => true,
228            _ => false,
229        }
230    }
231}
232impl Eq for ImageDataFormat {}
233impl std::hash::Hash for ImageDataFormat {
234    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
235        core::mem::discriminant(self).hash(state);
236        match self {
237            ImageDataFormat::Bgra8 { size, density } => {
238                size.hash(state);
239                density_key(*density).hash(state);
240            }
241            ImageDataFormat::A8 { size } => {
242                size.hash(state);
243            }
244            ImageDataFormat::FileExtension(ext) => ext.hash(state),
245            ImageDataFormat::MimeType(mt) => mt.hash(state),
246            ImageDataFormat::Unknown => {}
247        }
248    }
249}
250
251fn density_key(density: Option<PxDensity2d>) -> Option<(u16, u16)> {
252    density.map(|s| ((s.width.ppcm() * 3.0) as u16, (s.height.ppcm() * 3.0) as u16))
253}
254
255/// Represents a successfully decoded image.
256///
257/// See [`Event::ImageLoaded`].
258///
259/// [`Event::ImageLoaded`]: crate::Event::ImageLoaded
260#[derive(Clone, PartialEq, Serialize, Deserialize)]
261#[non_exhaustive]
262pub struct ImageLoadedData {
263    /// Image ID.
264    pub id: ImageId,
265    /// Pixel size.
266    pub size: PxSize,
267    /// Pixel density metadata.
268    pub density: Option<PxDensity2d>,
269    /// If all pixels have an alpha value of 255.
270    pub is_opaque: bool,
271    /// If the `pixels` are in a single channel (A8).
272    pub is_mask: bool,
273    /// Reference to the BGRA8 pre-multiplied image pixels or the A8 pixels if `is_mask`.
274    pub pixels: IpcBytes,
275}
276impl ImageLoadedData {
277    /// New response.
278    pub fn new(id: ImageId, size: PxSize, density: Option<PxDensity2d>, is_opaque: bool, is_mask: bool, pixels: IpcBytes) -> Self {
279        Self {
280            id,
281            size,
282            density,
283            is_opaque,
284            is_mask,
285            pixels,
286        }
287    }
288}
289impl fmt::Debug for ImageLoadedData {
290    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291        f.debug_struct("ImageLoadedData")
292            .field("id", &self.id)
293            .field("size", &self.size)
294            .field("density", &self.density)
295            .field("is_opaque", &self.is_opaque)
296            .field("is_mask", &self.is_mask)
297            .field("pixels", &format_args!("<{} bytes shared memory>", self.pixels.len()))
298            .finish()
299    }
300}