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}