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