img.rs

  1use crate::{
  2    point, px, size, AbsoluteLength, Asset, Bounds, DefiniteLength, DevicePixels, Element,
  3    ElementId, GlobalElementId, Hitbox, ImageData, InteractiveElement, Interactivity, IntoElement,
  4    LayoutId, Length, Pixels, SharedString, SharedUri, Size, StyleRefinement, Styled, SvgSize,
  5    UriOrPath, WindowContext,
  6};
  7use futures::{AsyncReadExt, Future};
  8use http_client;
  9use image::{
 10    codecs::gif::GifDecoder, AnimationDecoder, Frame, ImageBuffer, ImageError, ImageFormat,
 11};
 12#[cfg(target_os = "macos")]
 13use media::core_video::CVImageBuffer;
 14use smallvec::SmallVec;
 15use std::{
 16    fs,
 17    io::Cursor,
 18    path::PathBuf,
 19    sync::Arc,
 20    time::{Duration, Instant},
 21};
 22use thiserror::Error;
 23use util::ResultExt;
 24
 25/// A source of image content.
 26#[derive(Clone, Debug)]
 27pub enum ImageSource {
 28    /// Image content will be loaded from provided URI at render time.
 29    Uri(SharedUri),
 30    /// Image content will be loaded from the provided file at render time.
 31    File(Arc<PathBuf>),
 32    /// Cached image data
 33    Data(Arc<ImageData>),
 34    /// Image content will be loaded from Asset at render time.
 35    Asset(SharedString),
 36    // TODO: move surface definitions into mac platform module
 37    /// A CoreVideo image buffer
 38    #[cfg(target_os = "macos")]
 39    Surface(CVImageBuffer),
 40}
 41
 42fn is_uri(uri: &str) -> bool {
 43    uri.contains("://")
 44}
 45
 46impl From<SharedUri> for ImageSource {
 47    fn from(value: SharedUri) -> Self {
 48        Self::Uri(value)
 49    }
 50}
 51
 52impl From<&'static str> for ImageSource {
 53    fn from(s: &'static str) -> Self {
 54        if is_uri(&s) {
 55            Self::Uri(s.into())
 56        } else {
 57            Self::Asset(s.into())
 58        }
 59    }
 60}
 61
 62impl From<String> for ImageSource {
 63    fn from(s: String) -> Self {
 64        if is_uri(&s) {
 65            Self::Uri(s.into())
 66        } else {
 67            Self::Asset(s.into())
 68        }
 69    }
 70}
 71
 72impl From<SharedString> for ImageSource {
 73    fn from(s: SharedString) -> Self {
 74        if is_uri(&s) {
 75            Self::Uri(s.into())
 76        } else {
 77            Self::Asset(s)
 78        }
 79    }
 80}
 81
 82impl From<Arc<PathBuf>> for ImageSource {
 83    fn from(value: Arc<PathBuf>) -> Self {
 84        Self::File(value)
 85    }
 86}
 87
 88impl From<PathBuf> for ImageSource {
 89    fn from(value: PathBuf) -> Self {
 90        Self::File(value.into())
 91    }
 92}
 93
 94impl From<Arc<ImageData>> for ImageSource {
 95    fn from(value: Arc<ImageData>) -> Self {
 96        Self::Data(value)
 97    }
 98}
 99
100#[cfg(target_os = "macos")]
101impl From<CVImageBuffer> for ImageSource {
102    fn from(value: CVImageBuffer) -> Self {
103        Self::Surface(value)
104    }
105}
106
107/// An image element.
108pub struct Img {
109    interactivity: Interactivity,
110    source: ImageSource,
111    grayscale: bool,
112    object_fit: ObjectFit,
113}
114
115/// Create a new image element.
116pub fn img(source: impl Into<ImageSource>) -> Img {
117    Img {
118        interactivity: Interactivity::default(),
119        source: source.into(),
120        grayscale: false,
121        object_fit: ObjectFit::Contain,
122    }
123}
124
125/// How to fit the image into the bounds of the element.
126pub enum ObjectFit {
127    /// The image will be stretched to fill the bounds of the element.
128    Fill,
129    /// The image will be scaled to fit within the bounds of the element.
130    Contain,
131    /// The image will be scaled to cover the bounds of the element.
132    Cover,
133    /// The image will be scaled down to fit within the bounds of the element.
134    ScaleDown,
135    /// The image will maintain its original size.
136    None,
137}
138
139impl ObjectFit {
140    /// Get the bounds of the image within the given bounds.
141    pub fn get_bounds(
142        &self,
143        bounds: Bounds<Pixels>,
144        image_size: Size<DevicePixels>,
145    ) -> Bounds<Pixels> {
146        let image_size = image_size.map(|dimension| Pixels::from(u32::from(dimension)));
147        let image_ratio = image_size.width / image_size.height;
148        let bounds_ratio = bounds.size.width / bounds.size.height;
149
150        let result_bounds = match self {
151            ObjectFit::Fill => bounds,
152            ObjectFit::Contain => {
153                let new_size = if bounds_ratio > image_ratio {
154                    size(
155                        image_size.width * (bounds.size.height / image_size.height),
156                        bounds.size.height,
157                    )
158                } else {
159                    size(
160                        bounds.size.width,
161                        image_size.height * (bounds.size.width / image_size.width),
162                    )
163                };
164
165                Bounds {
166                    origin: point(
167                        bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
168                        bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
169                    ),
170                    size: new_size,
171                }
172            }
173            ObjectFit::ScaleDown => {
174                // Check if the image is larger than the bounds in either dimension.
175                if image_size.width > bounds.size.width || image_size.height > bounds.size.height {
176                    // If the image is larger, use the same logic as Contain to scale it down.
177                    let new_size = if bounds_ratio > image_ratio {
178                        size(
179                            image_size.width * (bounds.size.height / image_size.height),
180                            bounds.size.height,
181                        )
182                    } else {
183                        size(
184                            bounds.size.width,
185                            image_size.height * (bounds.size.width / image_size.width),
186                        )
187                    };
188
189                    Bounds {
190                        origin: point(
191                            bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
192                            bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
193                        ),
194                        size: new_size,
195                    }
196                } else {
197                    // If the image is smaller than or equal to the container, display it at its original size,
198                    // centered within the container.
199                    let original_size = size(image_size.width, image_size.height);
200                    Bounds {
201                        origin: point(
202                            bounds.origin.x + (bounds.size.width - original_size.width) / 2.0,
203                            bounds.origin.y + (bounds.size.height - original_size.height) / 2.0,
204                        ),
205                        size: original_size,
206                    }
207                }
208            }
209            ObjectFit::Cover => {
210                let new_size = if bounds_ratio > image_ratio {
211                    size(
212                        bounds.size.width,
213                        image_size.height * (bounds.size.width / image_size.width),
214                    )
215                } else {
216                    size(
217                        image_size.width * (bounds.size.height / image_size.height),
218                        bounds.size.height,
219                    )
220                };
221
222                Bounds {
223                    origin: point(
224                        bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
225                        bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
226                    ),
227                    size: new_size,
228                }
229            }
230            ObjectFit::None => Bounds {
231                origin: bounds.origin,
232                size: image_size,
233            },
234        };
235
236        result_bounds
237    }
238}
239
240impl Img {
241    /// A list of all format extensions currently supported by this img element
242    pub fn extensions() -> &'static [&'static str] {
243        // This is the list in [image::ImageFormat::from_extension] + `svg`
244        &[
245            "avif", "jpg", "jpeg", "png", "gif", "webp", "tif", "tiff", "tga", "dds", "bmp", "ico",
246            "hdr", "exr", "pbm", "pam", "ppm", "pgm", "ff", "farbfeld", "qoi", "svg",
247        ]
248    }
249
250    /// Set the image to be displayed in grayscale.
251    pub fn grayscale(mut self, grayscale: bool) -> Self {
252        self.grayscale = grayscale;
253        self
254    }
255    /// Set the object fit for the image.
256    pub fn object_fit(mut self, object_fit: ObjectFit) -> Self {
257        self.object_fit = object_fit;
258        self
259    }
260}
261
262/// The image state between frames
263struct ImgState {
264    frame_index: usize,
265    last_frame_time: Option<Instant>,
266}
267
268impl Element for Img {
269    type RequestLayoutState = usize;
270    type PrepaintState = Option<Hitbox>;
271
272    fn id(&self) -> Option<ElementId> {
273        self.interactivity.element_id.clone()
274    }
275
276    fn request_layout(
277        &mut self,
278        global_id: Option<&GlobalElementId>,
279        cx: &mut WindowContext,
280    ) -> (LayoutId, Self::RequestLayoutState) {
281        cx.with_optional_element_state(global_id, |state, cx| {
282            let mut state = state.map(|state| {
283                state.unwrap_or(ImgState {
284                    frame_index: 0,
285                    last_frame_time: None,
286                })
287            });
288
289            let frame_index = state.as_ref().map(|state| state.frame_index).unwrap_or(0);
290
291            let layout_id = self
292                .interactivity
293                .request_layout(global_id, cx, |mut style, cx| {
294                    if let Some(data) = self.source.data(cx) {
295                        if let Some(state) = &mut state {
296                            let frame_count = data.frame_count();
297                            if frame_count > 1 {
298                                let current_time = Instant::now();
299                                if let Some(last_frame_time) = state.last_frame_time {
300                                    let elapsed = current_time - last_frame_time;
301                                    let frame_duration =
302                                        Duration::from(data.delay(state.frame_index));
303
304                                    if elapsed >= frame_duration {
305                                        state.frame_index = (state.frame_index + 1) % frame_count;
306                                        state.last_frame_time =
307                                            Some(current_time - (elapsed - frame_duration));
308                                    }
309                                } else {
310                                    state.last_frame_time = Some(current_time);
311                                }
312                            }
313                        }
314
315                        let image_size = data.size(frame_index);
316                        match (style.size.width, style.size.height) {
317                            (Length::Auto, Length::Auto) => {
318                                style.size = Size {
319                                    width: Length::Definite(DefiniteLength::Absolute(
320                                        AbsoluteLength::Pixels(px(image_size.width.0 as f32)),
321                                    )),
322                                    height: Length::Definite(DefiniteLength::Absolute(
323                                        AbsoluteLength::Pixels(px(image_size.height.0 as f32)),
324                                    )),
325                                }
326                            }
327                            _ => {}
328                        }
329
330                        if global_id.is_some() && data.frame_count() > 1 {
331                            cx.request_animation_frame();
332                        }
333                    }
334
335                    cx.request_layout(style, [])
336                });
337
338            ((layout_id, frame_index), state)
339        })
340    }
341
342    fn prepaint(
343        &mut self,
344        global_id: Option<&GlobalElementId>,
345        bounds: Bounds<Pixels>,
346        _request_layout: &mut Self::RequestLayoutState,
347        cx: &mut WindowContext,
348    ) -> Option<Hitbox> {
349        self.interactivity
350            .prepaint(global_id, bounds, bounds.size, cx, |_, _, hitbox, _| hitbox)
351    }
352
353    fn paint(
354        &mut self,
355        global_id: Option<&GlobalElementId>,
356        bounds: Bounds<Pixels>,
357        frame_index: &mut Self::RequestLayoutState,
358        hitbox: &mut Self::PrepaintState,
359        cx: &mut WindowContext,
360    ) {
361        let source = self.source.clone();
362        self.interactivity
363            .paint(global_id, bounds, hitbox.as_ref(), cx, |style, cx| {
364                let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size());
365
366                if let Some(data) = source.data(cx) {
367                    let new_bounds = self.object_fit.get_bounds(bounds, data.size(*frame_index));
368                    cx.paint_image(
369                        new_bounds,
370                        corner_radii,
371                        data.clone(),
372                        *frame_index,
373                        self.grayscale,
374                    )
375                    .log_err();
376                }
377
378                match source {
379                    #[cfg(target_os = "macos")]
380                    ImageSource::Surface(surface) => {
381                        let size = size(surface.width().into(), surface.height().into());
382                        let new_bounds = self.object_fit.get_bounds(bounds, size);
383                        // TODO: Add support for corner_radii and grayscale.
384                        cx.paint_surface(new_bounds, surface);
385                    }
386                    _ => {}
387                }
388            })
389    }
390}
391
392impl IntoElement for Img {
393    type Element = Self;
394
395    fn into_element(self) -> Self::Element {
396        self
397    }
398}
399
400impl Styled for Img {
401    fn style(&mut self) -> &mut StyleRefinement {
402        &mut self.interactivity.base_style
403    }
404}
405
406impl InteractiveElement for Img {
407    fn interactivity(&mut self) -> &mut Interactivity {
408        &mut self.interactivity
409    }
410}
411
412impl ImageSource {
413    fn data(&self, cx: &mut WindowContext) -> Option<Arc<ImageData>> {
414        match self {
415            ImageSource::Uri(_) | ImageSource::Asset(_) | ImageSource::File(_) => {
416                let uri_or_path: UriOrPath = match self {
417                    ImageSource::Uri(uri) => uri.clone().into(),
418                    ImageSource::File(path) => path.clone().into(),
419                    ImageSource::Asset(path) => UriOrPath::Asset(path.clone()),
420                    _ => unreachable!(),
421                };
422
423                cx.use_cached_asset::<Image>(&uri_or_path)?.log_err()
424            }
425
426            ImageSource::Data(data) => Some(data.to_owned()),
427            #[cfg(target_os = "macos")]
428            ImageSource::Surface(_) => None,
429        }
430    }
431}
432
433#[derive(Clone)]
434enum Image {}
435
436impl Asset for Image {
437    type Source = UriOrPath;
438    type Output = Result<Arc<ImageData>, ImageCacheError>;
439
440    fn load(
441        source: Self::Source,
442        cx: &mut WindowContext,
443    ) -> impl Future<Output = Self::Output> + Send + 'static {
444        let client = cx.http_client();
445        let scale_factor = cx.scale_factor();
446        let svg_renderer = cx.svg_renderer();
447        let asset_source = cx.asset_source().clone();
448        async move {
449            let bytes = match source.clone() {
450                UriOrPath::Path(uri) => fs::read(uri.as_ref())?,
451                UriOrPath::Uri(uri) => {
452                    let mut response = client.get(uri.as_ref(), ().into(), true).await?;
453                    let mut body = Vec::new();
454                    response.body_mut().read_to_end(&mut body).await?;
455                    if !response.status().is_success() {
456                        return Err(ImageCacheError::BadStatus {
457                            uri,
458                            status: response.status(),
459                            body: String::from_utf8_lossy(&body).into_owned(),
460                        });
461                    }
462                    body
463                }
464                UriOrPath::Asset(path) => {
465                    let data = asset_source.load(&path).ok().flatten();
466                    if let Some(data) = data {
467                        data.to_vec()
468                    } else {
469                        return Err(ImageCacheError::Asset(
470                            format!("not found: {}", path).into(),
471                        ));
472                    }
473                }
474            };
475
476            let data = if let Ok(format) = image::guess_format(&bytes) {
477                let data = match format {
478                    ImageFormat::Gif => {
479                        let decoder = GifDecoder::new(Cursor::new(&bytes))?;
480                        let mut frames = SmallVec::new();
481
482                        for frame in decoder.into_frames() {
483                            let mut frame = frame?;
484                            // Convert from RGBA to BGRA.
485                            for pixel in frame.buffer_mut().chunks_exact_mut(4) {
486                                pixel.swap(0, 2);
487                            }
488                            frames.push(frame);
489                        }
490
491                        frames
492                    }
493                    _ => {
494                        let mut data =
495                            image::load_from_memory_with_format(&bytes, format)?.into_rgba8();
496
497                        // Convert from RGBA to BGRA.
498                        for pixel in data.chunks_exact_mut(4) {
499                            pixel.swap(0, 2);
500                        }
501
502                        SmallVec::from_elem(Frame::new(data), 1)
503                    }
504                };
505
506                ImageData::new(data)
507            } else {
508                let pixmap =
509                    svg_renderer.render_pixmap(&bytes, SvgSize::ScaleFactor(scale_factor))?;
510
511                let buffer =
512                    ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).unwrap();
513
514                ImageData::new(SmallVec::from_elem(Frame::new(buffer), 1))
515            };
516
517            Ok(Arc::new(data))
518        }
519    }
520}
521
522/// An error that can occur when interacting with the image cache.
523#[derive(Debug, Error, Clone)]
524pub enum ImageCacheError {
525    /// An error that occurred while fetching an image from a remote source.
526    #[error("http error: {0}")]
527    Client(#[from] http_client::Error),
528    /// An error that occurred while reading the image from disk.
529    #[error("IO error: {0}")]
530    Io(Arc<std::io::Error>),
531    /// An error that occurred while processing an image.
532    #[error("unexpected http status for {uri}: {status}, body: {body}")]
533    BadStatus {
534        /// The URI of the image.
535        uri: SharedUri,
536        /// The HTTP status code.
537        status: http_client::StatusCode,
538        /// The HTTP response body.
539        body: String,
540    },
541    /// An error that occurred while processing an asset.
542    #[error("asset error: {0}")]
543    Asset(SharedString),
544    /// An error that occurred while processing an image.
545    #[error("image error: {0}")]
546    Image(Arc<ImageError>),
547    /// An error that occurred while processing an SVG.
548    #[error("svg error: {0}")]
549    Usvg(Arc<usvg::Error>),
550}
551
552impl From<std::io::Error> for ImageCacheError {
553    fn from(error: std::io::Error) -> Self {
554        Self::Io(Arc::new(error))
555    }
556}
557
558impl From<ImageError> for ImageCacheError {
559    fn from(error: ImageError) -> Self {
560        Self::Image(Arc::new(error))
561    }
562}
563
564impl From<usvg::Error> for ImageCacheError {
565    fn from(error: usvg::Error) -> Self {
566        Self::Usvg(Arc::new(error))
567    }
568}