zng_view_api/
image.rs

1//! Image types.
2
3use std::fmt;
4
5use serde::{Deserialize, Serialize};
6use zng_txt::Txt;
7
8use crate::ipc::IpcBytes;
9use zng_unit::{Px, 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 [`IpcBytesReceiver`] in the view API.
59    ///
60    /// [`IpcBytesReceiver`]: crate::IpcBytesReceiver
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 `resize_to_fit`.
66    pub max_decoded_len: u64,
67    /// A size constraints to apply after the image is decoded. The image is resized so both dimensions fit inside
68    /// the constraints, the image aspect ratio is preserved.
69    pub downscale: Option<ImageDownscale>,
70    /// Convert or decode the image into a single channel mask (R8).
71    pub mask: Option<ImageMaskMode>,
72}
73impl<D> ImageRequest<D> {
74    /// New request.
75    pub fn new(
76        format: ImageDataFormat,
77        data: D,
78        max_decoded_len: u64,
79        downscale: Option<ImageDownscale>,
80        mask: Option<ImageMaskMode>,
81    ) -> Self {
82        Self {
83            format,
84            data,
85            max_decoded_len,
86            downscale,
87            mask,
88        }
89    }
90}
91
92/// Defines how an image is downscaled after decoding.
93///
94/// The image aspect ratio is preserved in both modes, the image is not upscaled, if it already fits the size
95/// constraints if will not be resized.
96#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
97pub enum ImageDownscale {
98    /// Image is downscaled so that both dimensions fit inside the size.
99    Fit(PxSize),
100    /// Image is downscaled so that at least one dimension fits inside the size.
101    Fill(PxSize),
102}
103impl From<PxSize> for ImageDownscale {
104    /// Fit
105    fn from(fit: PxSize) -> Self {
106        ImageDownscale::Fit(fit)
107    }
108}
109impl From<Px> for ImageDownscale {
110    /// Fit splat
111    fn from(fit: Px) -> Self {
112        ImageDownscale::Fit(PxSize::splat(fit))
113    }
114}
115#[cfg(feature = "var")]
116zng_var::impl_from_and_into_var! {
117    fn from(fit: PxSize) -> ImageDownscale;
118    fn from(fit: Px) -> ImageDownscale;
119    fn from(some: ImageDownscale) -> Option<ImageDownscale>;
120}
121impl ImageDownscale {
122    /// Compute the expected final size if the downscale is applied on an image of `source_size`.
123    pub fn resize_dimensions(self, source_size: PxSize) -> PxSize {
124        // code from image crate
125        fn resize_dimensions(width: u32, height: u32, n_width: u32, n_height: u32, fill: bool) -> (u32, u32) {
126            use std::cmp::max;
127
128            let w_ratio = n_width as f64 / width as f64;
129            let h_ratio = n_height as f64 / height as f64;
130
131            let ratio = if fill {
132                f64::max(w_ratio, h_ratio)
133            } else {
134                f64::min(w_ratio, h_ratio)
135            };
136
137            let nw = max((width as f64 * ratio).round() as u64, 1);
138            let nh = max((height as f64 * ratio).round() as u64, 1);
139
140            if nw > u64::from(u32::MAX) {
141                let ratio = u32::MAX as f64 / width as f64;
142                (u32::MAX, max((height as f64 * ratio).round() as u32, 1))
143            } else if nh > u64::from(u32::MAX) {
144                let ratio = u32::MAX as f64 / height as f64;
145                (max((width as f64 * ratio).round() as u32, 1), u32::MAX)
146            } else {
147                (nw as u32, nh as u32)
148            }
149        }
150
151        let (x, y) = match self {
152            ImageDownscale::Fit(s) => resize_dimensions(
153                source_size.width.0.max(0) as _,
154                source_size.height.0.max(0) as _,
155                s.width.0.max(0) as _,
156                s.height.0.max(0) as _,
157                false,
158            ),
159            ImageDownscale::Fill(s) => resize_dimensions(
160                source_size.width.0.max(0) as _,
161                source_size.height.0.max(0) as _,
162                s.width.0.max(0) as _,
163                s.height.0.max(0) as _,
164                true,
165            ),
166        };
167        PxSize::new(Px(x as _), Px(y as _))
168    }
169}
170
171/// Format of the image bytes.
172#[derive(Debug, Clone, Serialize, Deserialize)]
173#[non_exhaustive]
174pub enum ImageDataFormat {
175    /// Decoded BGRA8.
176    ///
177    /// This is the internal image format, it indicates the image data
178    /// is already decoded and must only be entered into the cache.
179    Bgra8 {
180        /// Size in pixels.
181        size: PxSize,
182        /// Pixels-per-inch of the image.
183        ppi: Option<ImagePpi>,
184    },
185
186    /// Decoded A8.
187    ///
188    /// This is the internal mask format it indicates the mask data
189    /// is already decoded and must only be entered into the cache.
190    A8 {
191        /// Size in pixels.
192        size: PxSize,
193    },
194
195    /// The image is encoded, a file extension that maybe identifies
196    /// the format is known.
197    FileExtension(Txt),
198
199    /// The image is encoded, MIME type that maybe identifies the format is known.
200    MimeType(Txt),
201
202    /// The image is encoded, a decoder will be selected using the "magic number"
203    /// on the beginning of the bytes buffer.
204    Unknown,
205}
206impl From<Txt> for ImageDataFormat {
207    fn from(ext_or_mime: Txt) -> Self {
208        if ext_or_mime.contains('/') {
209            ImageDataFormat::MimeType(ext_or_mime)
210        } else {
211            ImageDataFormat::FileExtension(ext_or_mime)
212        }
213    }
214}
215impl From<&str> for ImageDataFormat {
216    fn from(ext_or_mime: &str) -> Self {
217        Txt::from_str(ext_or_mime).into()
218    }
219}
220impl From<PxSize> for ImageDataFormat {
221    fn from(bgra8_size: PxSize) -> Self {
222        ImageDataFormat::Bgra8 {
223            size: bgra8_size,
224            ppi: None,
225        }
226    }
227}
228impl PartialEq for ImageDataFormat {
229    fn eq(&self, other: &Self) -> bool {
230        match (self, other) {
231            (Self::FileExtension(l0), Self::FileExtension(r0)) => l0 == r0,
232            (Self::MimeType(l0), Self::MimeType(r0)) => l0 == r0,
233            (Self::Bgra8 { size: s0, ppi: p0 }, Self::Bgra8 { size: s1, ppi: p1 }) => s0 == s1 && ppi_key(*p0) == ppi_key(*p1),
234            (Self::Unknown, Self::Unknown) => true,
235            _ => false,
236        }
237    }
238}
239impl Eq for ImageDataFormat {}
240impl std::hash::Hash for ImageDataFormat {
241    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
242        core::mem::discriminant(self).hash(state);
243        match self {
244            ImageDataFormat::Bgra8 { size, ppi } => {
245                size.hash(state);
246                ppi_key(*ppi).hash(state);
247            }
248            ImageDataFormat::A8 { size } => {
249                size.hash(state);
250            }
251            ImageDataFormat::FileExtension(ext) => ext.hash(state),
252            ImageDataFormat::MimeType(mt) => mt.hash(state),
253            ImageDataFormat::Unknown => {}
254        }
255    }
256}
257
258fn ppi_key(ppi: Option<ImagePpi>) -> Option<(u16, u16)> {
259    ppi.map(|s| ((s.x * 3.0) as u16, (s.y * 3.0) as u16))
260}
261
262/// Represents a successfully decoded image.
263///
264/// See [`Event::ImageLoaded`].
265///
266/// [`Event::ImageLoaded`]: crate::Event::ImageLoaded
267#[derive(Clone, PartialEq, Serialize, Deserialize)]
268#[non_exhaustive]
269pub struct ImageLoadedData {
270    /// Image ID.
271    pub id: ImageId,
272    /// Pixel size.
273    pub size: PxSize,
274    /// Pixel-per-inch metadata.
275    pub ppi: Option<ImagePpi>,
276    /// If all pixels have an alpha value of 255.
277    pub is_opaque: bool,
278    /// If the `pixels` are in a single channel (A8).
279    pub is_mask: bool,
280    /// Reference to the BGRA8 pre-multiplied image pixels or the A8 pixels if `is_mask`.
281    pub pixels: IpcBytes,
282}
283impl ImageLoadedData {
284    /// New response.
285    pub fn new(id: ImageId, size: PxSize, ppi: Option<ImagePpi>, is_opaque: bool, is_mask: bool, pixels: IpcBytes) -> Self {
286        Self {
287            id,
288            size,
289            ppi,
290            is_opaque,
291            is_mask,
292            pixels,
293        }
294    }
295}
296impl fmt::Debug for ImageLoadedData {
297    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
298        f.debug_struct("ImageLoadedData")
299            .field("id", &self.id)
300            .field("size", &self.size)
301            .field("ppi", &self.ppi)
302            .field("is_opaque", &self.is_opaque)
303            .field("is_mask", &self.is_mask)
304            .field("pixels", &format_args!("<{} bytes shared memory>", self.pixels.len()))
305            .finish()
306    }
307}
308/// Pixels-per-inch of each dimension of an image.
309#[derive(Clone, Copy, PartialEq, Serialize, Deserialize)]
310pub struct ImagePpi {
311    /// Pixels-per-inch in the X dimension.
312    pub x: f32,
313    /// Pixels-per-inch in the Y dimension.
314    pub y: f32,
315}
316impl ImagePpi {
317    /// New from x, y.
318    pub const fn new(x: f32, y: f32) -> Self {
319        Self { x, y }
320    }
321
322    /// New equal in both dimensions.
323    pub const fn splat(xy: f32) -> Self {
324        Self::new(xy, xy)
325    }
326}
327impl Default for ImagePpi {
328    /// 96.0
329    fn default() -> Self {
330        Self::splat(96.0)
331    }
332}
333impl fmt::Debug for ImagePpi {
334    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
335        if f.alternate() || self.x != self.y {
336            f.debug_struct("ImagePpi").field("x", &self.x).field("y", &self.y).finish()
337        } else {
338            write!(f, "{}", self.x)
339        }
340    }
341}
342
343impl From<f32> for ImagePpi {
344    fn from(xy: f32) -> Self {
345        ImagePpi::splat(xy)
346    }
347}
348impl From<(f32, f32)> for ImagePpi {
349    fn from((x, y): (f32, f32)) -> Self {
350        ImagePpi::new(x, y)
351    }
352}
353
354#[cfg(feature = "var")]
355zng_var::impl_from_and_into_var! {
356    fn from(xy: f32) -> ImagePpi;
357    fn from(xy: (f32, f32)) -> ImagePpi;
358}