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