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