image.rs

  1use anyhow::Result;
  2use base64::{
  3    Engine as _, alphabet,
  4    engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig},
  5};
  6use gpui::{App, ClipboardItem, Image, ImageFormat, Pixels, RenderImage, Window, img};
  7use settings::Settings as _;
  8use std::sync::Arc;
  9use ui::{IntoElement, Styled, prelude::*};
 10
 11use crate::outputs::{OutputContent, plain};
 12use crate::repl_settings::ReplSettings;
 13
 14/// ImageView renders an image inline in an editor, adapting to the line height to fit the image.
 15pub struct ImageView {
 16    clipboard_image: Arc<Image>,
 17    height: u32,
 18    width: u32,
 19    image: Arc<RenderImage>,
 20}
 21
 22pub const STANDARD_INDIFFERENT: GeneralPurpose = GeneralPurpose::new(
 23    &alphabet::STANDARD,
 24    GeneralPurposeConfig::new()
 25        .with_encode_padding(false)
 26        .with_decode_padding_mode(DecodePaddingMode::Indifferent),
 27);
 28
 29impl ImageView {
 30    pub fn from(base64_encoded_data: &str) -> Result<Self> {
 31        let filtered =
 32            base64_encoded_data.replace(&[' ', '\n', '\t', '\r', '\x0b', '\x0c'][..], "");
 33        let bytes = STANDARD_INDIFFERENT.decode(filtered)?;
 34
 35        let format = image::guess_format(&bytes)?;
 36
 37        let mut data = image::load_from_memory_with_format(&bytes, format)?.into_rgba8();
 38
 39        // Convert from RGBA to BGRA.
 40        for pixel in data.chunks_exact_mut(4) {
 41            pixel.swap(0, 2);
 42        }
 43
 44        let height = data.height();
 45        let width = data.width();
 46
 47        let gpui_image_data = RenderImage::new(vec![image::Frame::new(data)]);
 48
 49        let format = match format {
 50            image::ImageFormat::Png => ImageFormat::Png,
 51            image::ImageFormat::Jpeg => ImageFormat::Jpeg,
 52            image::ImageFormat::Gif => ImageFormat::Gif,
 53            image::ImageFormat::WebP => ImageFormat::Webp,
 54            image::ImageFormat::Tiff => ImageFormat::Tiff,
 55            image::ImageFormat::Bmp => ImageFormat::Bmp,
 56            image::ImageFormat::Ico => ImageFormat::Ico,
 57            format => {
 58                anyhow::bail!("unsupported image format {format:?}");
 59            }
 60        };
 61
 62        // Convert back to a GPUI image for use with the clipboard
 63        let clipboard_image = Arc::new(Image::from_bytes(format, bytes));
 64
 65        Ok(ImageView {
 66            clipboard_image,
 67            height,
 68            width,
 69            image: Arc::new(gpui_image_data),
 70        })
 71    }
 72
 73    fn scaled_size(
 74        &self,
 75        line_height: Pixels,
 76        max_width: Option<Pixels>,
 77        max_height: Option<Pixels>,
 78    ) -> (Pixels, Pixels) {
 79        let (mut height, mut width) = if self.height as f32 / f32::from(line_height)
 80            == u8::MAX as f32
 81        {
 82            let height = u8::MAX as f32 * line_height;
 83            let width = Pixels::from(self.width as f32 * f32::from(height) / self.height as f32);
 84            (height, width)
 85        } else {
 86            (self.height.into(), self.width.into())
 87        };
 88
 89        let mut scale: f32 = 1.0;
 90        if let Some(max_width) = max_width {
 91            if width > max_width {
 92                scale = scale.min(max_width / width);
 93            }
 94        }
 95
 96        if let Some(max_height) = max_height {
 97            if height > max_height {
 98                scale = scale.min(max_height / height);
 99            }
100        }
101
102        if scale < 1.0 {
103            width *= scale;
104            height *= scale;
105        }
106
107        (height, width)
108    }
109}
110
111impl Render for ImageView {
112    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
113        let settings = ReplSettings::get_global(cx);
114        let line_height = window.line_height();
115
116        let max_width = plain::max_width_for_columns(settings.max_columns, window, cx);
117
118        let max_height = if settings.output_max_height_lines > 0 {
119            Some(line_height * settings.output_max_height_lines as f32)
120        } else {
121            None
122        };
123
124        let (height, width) = self.scaled_size(line_height, max_width, max_height);
125
126        let image = self.image.clone();
127
128        img(image).w(width).h(height)
129    }
130}
131
132impl OutputContent for ImageView {
133    fn clipboard_content(&self, _window: &Window, _cx: &App) -> Option<ClipboardItem> {
134        Some(ClipboardItem::new_image(self.clipboard_image.as_ref()))
135    }
136
137    fn has_clipboard_content(&self, _window: &Window, _cx: &App) -> bool {
138        true
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    fn encode_test_image(width: u32, height: u32) -> String {
147        let image_buffer =
148            image::ImageBuffer::from_pixel(width, height, image::Rgba([0, 0, 0, 255]));
149        let image = image::DynamicImage::ImageRgba8(image_buffer);
150
151        let mut bytes = Vec::new();
152        let mut cursor = std::io::Cursor::new(&mut bytes);
153        if let Err(error) = image.write_to(&mut cursor, image::ImageFormat::Png) {
154            panic!("failed to encode test image: {error}");
155        }
156
157        base64::engine::general_purpose::STANDARD.encode(bytes)
158    }
159
160    #[test]
161    fn test_image_view_scaled_size_respects_limits() {
162        let encoded = encode_test_image(200, 120);
163        let image_view = match ImageView::from(&encoded) {
164            Ok(view) => view,
165            Err(error) => panic!("failed to decode image view: {error}"),
166        };
167
168        let line_height = Pixels::from(10.0);
169        let max_width = Pixels::from(50.0);
170        let max_height = Pixels::from(40.0);
171        let (height, width) =
172            image_view.scaled_size(line_height, Some(max_width), Some(max_height));
173
174        assert_eq!(f32::from(width), 50.0);
175        assert_eq!(f32::from(height), 30.0);
176    }
177
178    #[test]
179    fn test_image_view_scaled_size_unbounded() {
180        let encoded = encode_test_image(200, 120);
181        let image_view = match ImageView::from(&encoded) {
182            Ok(view) => view,
183            Err(error) => panic!("failed to decode image view: {error}"),
184        };
185
186        let line_height = Pixels::from(10.0);
187        let (height, width) = image_view.scaled_size(line_height, None, None);
188
189        assert_eq!(f32::from(width), 200.0);
190        assert_eq!(f32::from(height), 120.0);
191    }
192}