img.rs

  1use std::fs;
  2use std::path::PathBuf;
  3use std::sync::Arc;
  4
  5use crate::{
  6    point, px, size, AbsoluteLength, Asset, Bounds, DefiniteLength, DevicePixels, Element,
  7    ElementContext, Hitbox, ImageData, InteractiveElement, Interactivity, IntoElement, LayoutId,
  8    Length, Pixels, SharedUri, Size, StyleRefinement, Styled, SvgSize, UriOrPath, WindowContext,
  9};
 10use futures::{AsyncReadExt, Future};
 11use image::{ImageBuffer, ImageError};
 12#[cfg(target_os = "macos")]
 13use media::core_video::CVImageBuffer;
 14
 15use thiserror::Error;
 16use util::{http, ResultExt};
 17
 18/// A source of image content.
 19#[derive(Clone, Debug)]
 20pub enum ImageSource {
 21    /// Image content will be loaded from provided URI at render time.
 22    Uri(SharedUri),
 23    /// Image content will be loaded from the provided file at render time.
 24    File(Arc<PathBuf>),
 25    /// Cached image data
 26    Data(Arc<ImageData>),
 27    // TODO: move surface definitions into mac platform module
 28    /// A CoreVideo image buffer
 29    #[cfg(target_os = "macos")]
 30    Surface(CVImageBuffer),
 31}
 32
 33impl From<SharedUri> for ImageSource {
 34    fn from(value: SharedUri) -> Self {
 35        Self::Uri(value)
 36    }
 37}
 38
 39impl From<&'static str> for ImageSource {
 40    fn from(uri: &'static str) -> Self {
 41        Self::Uri(uri.into())
 42    }
 43}
 44
 45impl From<String> for ImageSource {
 46    fn from(uri: String) -> Self {
 47        Self::Uri(uri.into())
 48    }
 49}
 50
 51impl From<Arc<PathBuf>> for ImageSource {
 52    fn from(value: Arc<PathBuf>) -> Self {
 53        Self::File(value)
 54    }
 55}
 56
 57impl From<PathBuf> for ImageSource {
 58    fn from(value: PathBuf) -> Self {
 59        Self::File(value.into())
 60    }
 61}
 62
 63impl From<Arc<ImageData>> for ImageSource {
 64    fn from(value: Arc<ImageData>) -> Self {
 65        Self::Data(value)
 66    }
 67}
 68
 69#[cfg(target_os = "macos")]
 70impl From<CVImageBuffer> for ImageSource {
 71    fn from(value: CVImageBuffer) -> Self {
 72        Self::Surface(value)
 73    }
 74}
 75
 76/// An image element.
 77pub struct Img {
 78    interactivity: Interactivity,
 79    source: ImageSource,
 80    grayscale: bool,
 81    object_fit: ObjectFit,
 82}
 83
 84/// Create a new image element.
 85pub fn img(source: impl Into<ImageSource>) -> Img {
 86    Img {
 87        interactivity: Interactivity::default(),
 88        source: source.into(),
 89        grayscale: false,
 90        object_fit: ObjectFit::Contain,
 91    }
 92}
 93
 94/// How to fit the image into the bounds of the element.
 95pub enum ObjectFit {
 96    /// The image will be stretched to fill the bounds of the element.
 97    Fill,
 98    /// The image will be scaled to fit within the bounds of the element.
 99    Contain,
100    /// The image will be scaled to cover the bounds of the element.
101    Cover,
102    /// The image will maintain its original size.
103    None,
104}
105
106impl ObjectFit {
107    /// Get the bounds of the image within the given bounds.
108    pub fn get_bounds(
109        &self,
110        bounds: Bounds<Pixels>,
111        image_size: Size<DevicePixels>,
112    ) -> Bounds<Pixels> {
113        let image_size = image_size.map(|dimension| Pixels::from(u32::from(dimension)));
114        let image_ratio = image_size.width / image_size.height;
115        let bounds_ratio = bounds.size.width / bounds.size.height;
116
117        match self {
118            ObjectFit::Fill => bounds,
119            ObjectFit::Contain => {
120                let new_size = if bounds_ratio > image_ratio {
121                    size(
122                        image_size.width * (bounds.size.height / image_size.height),
123                        bounds.size.height,
124                    )
125                } else {
126                    size(
127                        bounds.size.width,
128                        image_size.height * (bounds.size.width / image_size.width),
129                    )
130                };
131
132                Bounds {
133                    origin: point(
134                        bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
135                        bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
136                    ),
137                    size: new_size,
138                }
139            }
140            ObjectFit::Cover => {
141                let new_size = if bounds_ratio > image_ratio {
142                    size(
143                        bounds.size.width,
144                        image_size.height * (bounds.size.width / image_size.width),
145                    )
146                } else {
147                    size(
148                        image_size.width * (bounds.size.height / image_size.height),
149                        bounds.size.height,
150                    )
151                };
152
153                Bounds {
154                    origin: point(
155                        bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
156                        bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
157                    ),
158                    size: new_size,
159                }
160            }
161            ObjectFit::None => Bounds {
162                origin: bounds.origin,
163                size: image_size,
164            },
165        }
166    }
167}
168
169impl Img {
170    /// A list of all format extensions currently supported by this img element
171    pub fn extensions() -> &'static [&'static str] {
172        // This is the list in [image::ImageFormat::from_extension] + `svg`
173        &[
174            "avif", "jpg", "jpeg", "png", "gif", "webp", "tif", "tiff", "tga", "dds", "bmp", "ico",
175            "hdr", "exr", "pbm", "pam", "ppm", "pgm", "ff", "farbfeld", "qoi", "svg",
176        ]
177    }
178
179    /// Set the image to be displayed in grayscale.
180    pub fn grayscale(mut self, grayscale: bool) -> Self {
181        self.grayscale = grayscale;
182        self
183    }
184    /// Set the object fit for the image.
185    pub fn object_fit(mut self, object_fit: ObjectFit) -> Self {
186        self.object_fit = object_fit;
187        self
188    }
189}
190
191impl Element for Img {
192    type BeforeLayout = ();
193    type AfterLayout = Option<Hitbox>;
194
195    fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) {
196        let layout_id = self.interactivity.before_layout(cx, |mut style, cx| {
197            if let Some(data) = self.source.data(cx) {
198                let image_size = data.size();
199                match (style.size.width, style.size.height) {
200                    (Length::Auto, Length::Auto) => {
201                        style.size = Size {
202                            width: Length::Definite(DefiniteLength::Absolute(
203                                AbsoluteLength::Pixels(px(image_size.width.0 as f32)),
204                            )),
205                            height: Length::Definite(DefiniteLength::Absolute(
206                                AbsoluteLength::Pixels(px(image_size.height.0 as f32)),
207                            )),
208                        }
209                    }
210                    _ => {}
211                }
212            }
213
214            cx.request_layout(&style, [])
215        });
216        (layout_id, ())
217    }
218
219    fn after_layout(
220        &mut self,
221        bounds: Bounds<Pixels>,
222        _before_layout: &mut Self::BeforeLayout,
223        cx: &mut ElementContext,
224    ) -> Option<Hitbox> {
225        self.interactivity
226            .after_layout(bounds, bounds.size, cx, |_, _, hitbox, _| hitbox)
227    }
228
229    fn paint(
230        &mut self,
231        bounds: Bounds<Pixels>,
232        _: &mut Self::BeforeLayout,
233        hitbox: &mut Self::AfterLayout,
234        cx: &mut ElementContext,
235    ) {
236        let source = self.source.clone();
237        self.interactivity
238            .paint(bounds, hitbox.as_ref(), cx, |style, cx| {
239                let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size());
240
241                if let Some(data) = source.data(cx) {
242                    let new_bounds = self.object_fit.get_bounds(bounds, data.size());
243                    cx.paint_image(new_bounds, corner_radii, data.clone(), self.grayscale)
244                        .log_err();
245                }
246
247                match source {
248                    #[cfg(target_os = "macos")]
249                    ImageSource::Surface(surface) => {
250                        let size = size(surface.width().into(), surface.height().into());
251                        let new_bounds = self.object_fit.get_bounds(bounds, size);
252                        // TODO: Add support for corner_radii and grayscale.
253                        cx.paint_surface(new_bounds, surface);
254                    }
255                    _ => {}
256                }
257            })
258    }
259}
260
261impl IntoElement for Img {
262    type Element = Self;
263
264    fn into_element(self) -> Self::Element {
265        self
266    }
267}
268
269impl Styled for Img {
270    fn style(&mut self) -> &mut StyleRefinement {
271        &mut self.interactivity.base_style
272    }
273}
274
275impl InteractiveElement for Img {
276    fn interactivity(&mut self) -> &mut Interactivity {
277        &mut self.interactivity
278    }
279}
280
281impl ImageSource {
282    fn data(&self, cx: &mut ElementContext) -> Option<Arc<ImageData>> {
283        match self {
284            ImageSource::Uri(_) | ImageSource::File(_) => {
285                let uri_or_path: UriOrPath = match self {
286                    ImageSource::Uri(uri) => uri.clone().into(),
287                    ImageSource::File(path) => path.clone().into(),
288                    _ => unreachable!(),
289                };
290
291                cx.use_cached_asset::<Image>(&uri_or_path)?.log_err()
292            }
293
294            ImageSource::Data(data) => Some(data.to_owned()),
295            #[cfg(target_os = "macos")]
296            ImageSource::Surface(_) => None,
297        }
298    }
299}
300
301#[derive(Clone)]
302enum Image {}
303
304impl Asset for Image {
305    type Source = UriOrPath;
306    type Output = Result<Arc<ImageData>, ImageCacheError>;
307
308    fn load(
309        source: Self::Source,
310        cx: &mut WindowContext,
311    ) -> impl Future<Output = Self::Output> + Send + 'static {
312        let client = cx.http_client();
313        let scale_factor = cx.scale_factor();
314        let svg_renderer = cx.svg_renderer();
315        async move {
316            let bytes = match source.clone() {
317                UriOrPath::Path(uri) => fs::read(uri.as_ref())?,
318                UriOrPath::Uri(uri) => {
319                    let mut response = client.get(uri.as_ref(), ().into(), true).await?;
320                    let mut body = Vec::new();
321                    response.body_mut().read_to_end(&mut body).await?;
322                    if !response.status().is_success() {
323                        return Err(ImageCacheError::BadStatus {
324                            status: response.status(),
325                            body: String::from_utf8_lossy(&body).into_owned(),
326                        });
327                    }
328                    body
329                }
330            };
331
332            let data = if let Ok(format) = image::guess_format(&bytes) {
333                let data = image::load_from_memory_with_format(&bytes, format)?.into_bgra8();
334                ImageData::new(data)
335            } else {
336                let pixmap =
337                    svg_renderer.render_pixmap(&bytes, SvgSize::ScaleFactor(scale_factor))?;
338
339                let buffer =
340                    ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).unwrap();
341
342                ImageData::new(buffer)
343            };
344
345            Ok(Arc::new(data))
346        }
347    }
348}
349
350/// An error that can occur when interacting with the image cache.
351#[derive(Debug, Error, Clone)]
352pub enum ImageCacheError {
353    /// An error that occurred while fetching an image from a remote source.
354    #[error("http error: {0}")]
355    Client(#[from] http::Error),
356    /// An error that occurred while reading the image from disk.
357    #[error("IO error: {0}")]
358    Io(Arc<std::io::Error>),
359    /// An error that occurred while processing an image.
360    #[error("unexpected http status: {status}, body: {body}")]
361    BadStatus {
362        /// The HTTP status code.
363        status: http::StatusCode,
364        /// The HTTP response body.
365        body: String,
366    },
367    /// An error that occurred while processing an image.
368    #[error("image error: {0}")]
369    Image(Arc<ImageError>),
370    /// An error that occurred while processing an SVG.
371    #[error("svg error: {0}")]
372    Usvg(Arc<usvg::Error>),
373}
374
375impl From<std::io::Error> for ImageCacheError {
376    fn from(error: std::io::Error) -> Self {
377        Self::Io(Arc::new(error))
378    }
379}
380
381impl From<ImageError> for ImageCacheError {
382    fn from(error: ImageError) -> Self {
383        Self::Image(Arc::new(error))
384    }
385}
386
387impl From<usvg::Error> for ImageCacheError {
388    fn from(error: usvg::Error) -> Self {
389        Self::Usvg(Arc::new(error))
390    }
391}