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                        let mut body = String::from_utf8_lossy(&body).into_owned();
355                        let first_line = body.lines().next().unwrap_or("").trim_end();
356                        body.truncate(first_line.len());
357                        return Err(ImageCacheError::BadStatus {
358                            uri,
359                            status: response.status(),
360                            body,
361                        });
362                    }
363                    body
364                }
365                UriOrPath::Embedded(path) => {
366                    let data = asset_source.load(&path).ok().flatten();
367                    if let Some(data) = data {
368                        data.to_vec()
369                    } else {
370                        return Err(ImageCacheError::Asset(
371                            format!("not found: {}", path).into(),
372                        ));
373                    }
374                }
375            };
376
377            let data = if let Ok(format) = image::guess_format(&bytes) {
378                let data = match format {
379                    ImageFormat::Gif => {
380                        let decoder = GifDecoder::new(Cursor::new(&bytes))?;
381                        let mut frames = SmallVec::new();
382
383                        for frame in decoder.into_frames() {
384                            let mut frame = frame?;
385                            // Convert from RGBA to BGRA.
386                            for pixel in frame.buffer_mut().chunks_exact_mut(4) {
387                                pixel.swap(0, 2);
388                            }
389                            frames.push(frame);
390                        }
391
392                        frames
393                    }
394                    _ => {
395                        let mut data =
396                            image::load_from_memory_with_format(&bytes, format)?.into_rgba8();
397
398                        // Convert from RGBA to BGRA.
399                        for pixel in data.chunks_exact_mut(4) {
400                            pixel.swap(0, 2);
401                        }
402
403                        SmallVec::from_elem(Frame::new(data), 1)
404                    }
405                };
406
407                RenderImage::new(data)
408            } else {
409                let pixmap =
410                    // TODO: Can we make svgs always rescale?
411                    svg_renderer.render_pixmap(&bytes, SvgSize::ScaleFactor(1.0))?;
412
413                let buffer =
414                    ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).unwrap();
415
416                RenderImage::new(SmallVec::from_elem(Frame::new(buffer), 1))
417            };
418
419            Ok(Arc::new(data))
420        }
421    }
422}
423
424/// An error that can occur when interacting with the image cache.
425#[derive(Debug, Error, Clone)]
426pub enum ImageCacheError {
427    /// An error that occurred while fetching an image from a remote source.
428    #[error("http error: {0}")]
429    Client(#[from] http_client::Error),
430    /// An error that occurred while reading the image from disk.
431    #[error("IO error: {0}")]
432    Io(Arc<std::io::Error>),
433    /// An error that occurred while processing an image.
434    #[error("unexpected http status for {uri}: {status}, body: {body}")]
435    BadStatus {
436        /// The URI of the image.
437        uri: SharedUri,
438        /// The HTTP status code.
439        status: http_client::StatusCode,
440        /// The HTTP response body.
441        body: String,
442    },
443    /// An error that occurred while processing an asset.
444    #[error("asset error: {0}")]
445    Asset(SharedString),
446    /// An error that occurred while processing an image.
447    #[error("image error: {0}")]
448    Image(Arc<ImageError>),
449    /// An error that occurred while processing an SVG.
450    #[error("svg error: {0}")]
451    Usvg(Arc<usvg::Error>),
452}
453
454impl From<std::io::Error> for ImageCacheError {
455    fn from(error: std::io::Error) -> Self {
456        Self::Io(Arc::new(error))
457    }
458}
459
460impl From<ImageError> for ImageCacheError {
461    fn from(error: ImageError) -> Self {
462        Self::Image(Arc::new(error))
463    }
464}
465
466impl From<usvg::Error> for ImageCacheError {
467    fn from(error: usvg::Error) -> Self {
468        Self::Usvg(Arc::new(error))
469    }
470}