image.rs

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