img.rs

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