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 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, Length::Auto) = (style.size.width, style.size.height)
191                        {
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                        if global_id.is_some() && data.frame_count() > 1 {
203                            cx.request_animation_frame();
204                        }
205                    }
206
207                    cx.request_layout(style, [])
208                });
209
210            ((layout_id, frame_index), state)
211        })
212    }
213
214    fn prepaint(
215        &mut self,
216        global_id: Option<&GlobalElementId>,
217        bounds: Bounds<Pixels>,
218        _request_layout: &mut Self::RequestLayoutState,
219        cx: &mut WindowContext,
220    ) -> Option<Hitbox> {
221        self.interactivity
222            .prepaint(global_id, bounds, bounds.size, cx, |_, _, hitbox, _| hitbox)
223    }
224
225    fn paint(
226        &mut self,
227        global_id: Option<&GlobalElementId>,
228        bounds: Bounds<Pixels>,
229        frame_index: &mut Self::RequestLayoutState,
230        hitbox: &mut Self::PrepaintState,
231        cx: &mut WindowContext,
232    ) {
233        let source = self.source.clone();
234        self.interactivity
235            .paint(global_id, bounds, hitbox.as_ref(), cx, |style, cx| {
236                let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size());
237
238                if let Some(data) = source.use_data(cx) {
239                    let new_bounds = self.object_fit.get_bounds(bounds, data.size(*frame_index));
240                    cx.paint_image(
241                        new_bounds,
242                        corner_radii,
243                        data.clone(),
244                        *frame_index,
245                        self.grayscale,
246                    )
247                    .log_err();
248                }
249            })
250    }
251}
252
253impl IntoElement for Img {
254    type Element = Self;
255
256    fn into_element(self) -> Self::Element {
257        self
258    }
259}
260
261impl Styled for Img {
262    fn style(&mut self) -> &mut StyleRefinement {
263        &mut self.interactivity.base_style
264    }
265}
266
267impl InteractiveElement for Img {
268    fn interactivity(&mut self) -> &mut Interactivity {
269        &mut self.interactivity
270    }
271}
272
273impl ImageSource {
274    pub(crate) fn use_data(&self, cx: &mut WindowContext) -> Option<Arc<RenderImage>> {
275        match self {
276            ImageSource::Uri(_) | ImageSource::Embedded(_) | ImageSource::File(_) => {
277                let uri_or_path: UriOrPath = match self {
278                    ImageSource::Uri(uri) => uri.clone().into(),
279                    ImageSource::File(path) => path.clone().into(),
280                    ImageSource::Embedded(path) => UriOrPath::Embedded(path.clone()),
281                    _ => unreachable!(),
282                };
283
284                cx.use_asset::<ImageAsset>(&uri_or_path)?.log_err()
285            }
286
287            ImageSource::Render(data) => Some(data.to_owned()),
288            ImageSource::Image(data) => cx.use_asset::<ImageDecoder>(data)?.log_err(),
289        }
290    }
291
292    /// Fetch the data associated with this source, using GPUI's asset caching
293    pub async fn data(&self, cx: &mut AppContext) -> Option<Arc<RenderImage>> {
294        match self {
295            ImageSource::Uri(_) | ImageSource::Embedded(_) | ImageSource::File(_) => {
296                let uri_or_path: UriOrPath = match self {
297                    ImageSource::Uri(uri) => uri.clone().into(),
298                    ImageSource::File(path) => path.clone().into(),
299                    ImageSource::Embedded(path) => UriOrPath::Embedded(path.clone()),
300                    _ => unreachable!(),
301                };
302
303                cx.fetch_asset::<ImageAsset>(&uri_or_path).0.await.log_err()
304            }
305
306            ImageSource::Render(data) => Some(data.to_owned()),
307            ImageSource::Image(data) => cx.fetch_asset::<ImageDecoder>(data).0.await.log_err(),
308        }
309    }
310}
311
312#[derive(Clone)]
313enum ImageDecoder {}
314
315impl Asset for ImageDecoder {
316    type Source = Arc<Image>;
317    type Output = Result<Arc<RenderImage>, Arc<anyhow::Error>>;
318
319    fn load(
320        source: Self::Source,
321        cx: &mut AppContext,
322    ) -> impl Future<Output = Self::Output> + Send + 'static {
323        let result = source.to_image_data(cx).map_err(Arc::new);
324        async { result }
325    }
326}
327
328#[derive(Clone)]
329enum ImageAsset {}
330
331impl Asset for ImageAsset {
332    type Source = UriOrPath;
333    type Output = Result<Arc<RenderImage>, ImageCacheError>;
334
335    fn load(
336        source: Self::Source,
337        cx: &mut AppContext,
338    ) -> impl Future<Output = Self::Output> + Send + 'static {
339        let client = cx.http_client();
340        // TODO: Can we make SVGs always rescale?
341        // let scale_factor = cx.scale_factor();
342        let svg_renderer = cx.svg_renderer();
343        let asset_source = cx.asset_source().clone();
344        async move {
345            let bytes = match source.clone() {
346                UriOrPath::Path(uri) => fs::read(uri.as_ref())?,
347                UriOrPath::Uri(uri) => {
348                    let mut response = client.get(uri.as_ref(), ().into(), true).await?;
349                    let mut body = Vec::new();
350                    response.body_mut().read_to_end(&mut body).await?;
351                    if !response.status().is_success() {
352                        let mut body = String::from_utf8_lossy(&body).into_owned();
353                        let first_line = body.lines().next().unwrap_or("").trim_end();
354                        body.truncate(first_line.len());
355                        return Err(ImageCacheError::BadStatus {
356                            uri,
357                            status: response.status(),
358                            body,
359                        });
360                    }
361                    body
362                }
363                UriOrPath::Embedded(path) => {
364                    let data = asset_source.load(&path).ok().flatten();
365                    if let Some(data) = data {
366                        data.to_vec()
367                    } else {
368                        return Err(ImageCacheError::Asset(
369                            format!("not found: {}", path).into(),
370                        ));
371                    }
372                }
373            };
374
375            let data = if let Ok(format) = image::guess_format(&bytes) {
376                let data = match format {
377                    ImageFormat::Gif => {
378                        let decoder = GifDecoder::new(Cursor::new(&bytes))?;
379                        let mut frames = SmallVec::new();
380
381                        for frame in decoder.into_frames() {
382                            let mut frame = frame?;
383                            // Convert from RGBA to BGRA.
384                            for pixel in frame.buffer_mut().chunks_exact_mut(4) {
385                                pixel.swap(0, 2);
386                            }
387                            frames.push(frame);
388                        }
389
390                        frames
391                    }
392                    _ => {
393                        let mut data =
394                            image::load_from_memory_with_format(&bytes, format)?.into_rgba8();
395
396                        // Convert from RGBA to BGRA.
397                        for pixel in data.chunks_exact_mut(4) {
398                            pixel.swap(0, 2);
399                        }
400
401                        SmallVec::from_elem(Frame::new(data), 1)
402                    }
403                };
404
405                RenderImage::new(data)
406            } else {
407                let pixmap =
408                    // TODO: Can we make svgs always rescale?
409                    svg_renderer.render_pixmap(&bytes, SvgSize::ScaleFactor(1.0))?;
410
411                let buffer =
412                    ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).unwrap();
413
414                RenderImage::new(SmallVec::from_elem(Frame::new(buffer), 1))
415            };
416
417            Ok(Arc::new(data))
418        }
419    }
420}
421
422/// An error that can occur when interacting with the image cache.
423#[derive(Debug, Error, Clone)]
424pub enum ImageCacheError {
425    /// An error that occurred while fetching an image from a remote source.
426    #[error("http error: {0}")]
427    Client(#[from] http_client::Error),
428    /// An error that occurred while reading the image from disk.
429    #[error("IO error: {0}")]
430    Io(Arc<std::io::Error>),
431    /// An error that occurred while processing an image.
432    #[error("unexpected http status for {uri}: {status}, body: {body}")]
433    BadStatus {
434        /// The URI of the image.
435        uri: SharedUri,
436        /// The HTTP status code.
437        status: http_client::StatusCode,
438        /// The HTTP response body.
439        body: String,
440    },
441    /// An error that occurred while processing an asset.
442    #[error("asset error: {0}")]
443    Asset(SharedString),
444    /// An error that occurred while processing an image.
445    #[error("image error: {0}")]
446    Image(Arc<ImageError>),
447    /// An error that occurred while processing an SVG.
448    #[error("svg error: {0}")]
449    Usvg(Arc<usvg::Error>),
450}
451
452impl From<std::io::Error> for ImageCacheError {
453    fn from(error: std::io::Error) -> Self {
454        Self::Io(Arc::new(error))
455    }
456}
457
458impl From<ImageError> for ImageCacheError {
459    fn from(error: ImageError) -> Self {
460        Self::Image(Arc::new(error))
461    }
462}
463
464impl From<usvg::Error> for ImageCacheError {
465    fn from(error: usvg::Error) -> Self {
466        Self::Usvg(Arc::new(error))
467    }
468}