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 std::sync::Arc;
 8use ui::{IntoElement, Styled, div, prelude::*};
 9
10use crate::outputs::OutputContent;
11
12/// ImageView renders an image inline in an editor, adapting to the line height to fit the image.
13pub struct ImageView {
14    clipboard_image: Arc<Image>,
15    height: u32,
16    width: u32,
17    image: Arc<RenderImage>,
18}
19
20pub const STANDARD_INDIFFERENT: GeneralPurpose = GeneralPurpose::new(
21    &alphabet::STANDARD,
22    GeneralPurposeConfig::new()
23        .with_encode_padding(false)
24        .with_decode_padding_mode(DecodePaddingMode::Indifferent),
25);
26
27impl ImageView {
28    pub fn from(base64_encoded_data: &str) -> Result<Self> {
29        let filtered =
30            base64_encoded_data.replace(&[' ', '\n', '\t', '\r', '\x0b', '\x0c'][..], "");
31        let bytes = STANDARD_INDIFFERENT.decode(filtered)?;
32
33        let format = image::guess_format(&bytes)?;
34
35        let mut data = image::load_from_memory_with_format(&bytes, format)?.into_rgba8();
36
37        // Convert from RGBA to BGRA.
38        for pixel in data.chunks_exact_mut(4) {
39            pixel.swap(0, 2);
40        }
41
42        let height = data.height();
43        let width = data.width();
44
45        let gpui_image_data = RenderImage::new(vec![image::Frame::new(data)]);
46
47        let format = match format {
48            image::ImageFormat::Png => ImageFormat::Png,
49            image::ImageFormat::Jpeg => ImageFormat::Jpeg,
50            image::ImageFormat::Gif => ImageFormat::Gif,
51            image::ImageFormat::WebP => ImageFormat::Webp,
52            image::ImageFormat::Tiff => ImageFormat::Tiff,
53            image::ImageFormat::Bmp => ImageFormat::Bmp,
54            format => {
55                anyhow::bail!("unsupported image format {format:?}");
56            }
57        };
58
59        // Convert back to a GPUI image for use with the clipboard
60        let clipboard_image = Arc::new(Image::from_bytes(format, bytes));
61
62        Ok(ImageView {
63            clipboard_image,
64            height,
65            width,
66            image: Arc::new(gpui_image_data),
67        })
68    }
69}
70
71impl Render for ImageView {
72    fn render(&mut self, window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
73        let line_height = window.line_height();
74
75        let (height, width) = if self.height as f32 / line_height.0 == u8::MAX as f32 {
76            let height = u8::MAX as f32 * line_height.0;
77            let width = self.width as f32 * height / self.height as f32;
78            (height, width)
79        } else {
80            (self.height as f32, self.width as f32)
81        };
82
83        let image = self.image.clone();
84
85        div().h(Pixels(height)).w(Pixels(width)).child(img(image))
86    }
87}
88
89impl OutputContent for ImageView {
90    fn clipboard_content(&self, _window: &Window, _cx: &App) -> Option<ClipboardItem> {
91        Some(ClipboardItem::new_image(self.clipboard_image.as_ref()))
92    }
93
94    fn has_clipboard_content(&self, _window: &Window, _cx: &App) -> bool {
95        true
96    }
97}