Skip to content

Commit dde6c41

Browse files
committed
scaled image support for jupyter outputs
1 parent d1a9e1b commit dde6c41

File tree

3 files changed

+99
-6
lines changed

3 files changed

+99
-6
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/runtimes/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@ doctest = false
1414

1515
[dependencies]
1616
anyhow.workspace = true
17-
# the terminal crate doesn't add alacritty to the workspace, so we directly depend on it here
17+
# Note: terminal crate doesn't add alacritty to the workspace, so we directly depend on it here
1818
alacritty_terminal = "0.23"
19+
base64.workspace = true
1920
collections.workspace = true
2021
editor.workspace = true
2122
gpui.workspace = true
2223
futures.workspace = true
24+
# Note: gpui crate doesn't pull image from the workspace
25+
image = "0.23"
2326
language.workspace = true
2427
log.workspace = true
2528
project.workspace = true

crates/runtimes/src/outputs.rs

Lines changed: 93 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,78 @@
1+
use std::sync::Arc;
2+
13
use crate::stdio::TerminalOutput;
24
use crate::ExecutionId;
3-
use gpui::{AnyElement, FontWeight, Render, View};
5+
use anyhow::{anyhow, Result};
6+
use gpui::{img, AnyElement, FontWeight, ImageData, Render, View};
47
use runtimelib::{ExecutionState, JupyterMessageContent, MimeType};
58
use serde_json::Value;
69
use ui::{div, prelude::*, v_flex, IntoElement, Styled, ViewContext};
710

11+
pub struct ImageView {
12+
height: u32,
13+
width: u32,
14+
image: Arc<ImageData>,
15+
}
16+
17+
impl ImageView {
18+
fn render(&self, cx: &ViewContext<ExecutionView>) -> AnyElement {
19+
let line_height = cx.line_height();
20+
21+
let (height, width) = if self.height as f32 / line_height.0 == u8::MAX as f32 {
22+
let height = u8::MAX as f32 * line_height.0;
23+
let width = self.width as f32 * height / self.height as f32;
24+
(height, width)
25+
} else {
26+
(self.height as f32, self.width as f32)
27+
};
28+
29+
let image = self.image.clone();
30+
31+
div()
32+
.h(Pixels(height as f32))
33+
.w(Pixels(width as f32))
34+
.child(img(image))
35+
.into_any_element()
36+
}
37+
}
38+
39+
impl LineHeight for ImageView {
40+
fn num_lines(&self, cx: &mut WindowContext) -> u8 {
41+
let line_height = cx.line_height();
42+
43+
let lines = self.height as f32 / line_height.0;
44+
45+
if lines > u8::MAX as f32 {
46+
return u8::MAX;
47+
}
48+
lines as u8
49+
}
50+
}
51+
852
pub enum OutputType {
953
Plain(TerminalOutput),
1054
Media((MimeType, Value)),
1155
Stream(TerminalOutput),
56+
Image(ImageView),
1257
ErrorOutput {
1358
ename: String,
1459
evalue: String,
1560
traceback: TerminalOutput,
1661
},
62+
Message(String),
1763
}
1864

1965
pub trait LineHeight: Sized {
2066
fn num_lines(&self, cx: &mut WindowContext) -> u8;
2167
}
2268

2369
// Priority order goes from highest to lowest (plaintext is the common fallback)
24-
const PRIORITY_ORDER: &[MimeType] = &[MimeType::Markdown, MimeType::Plain];
70+
const PRIORITY_ORDER: &[MimeType] = &[
71+
MimeType::Png,
72+
MimeType::Jpeg,
73+
MimeType::Markdown,
74+
MimeType::Plain,
75+
];
2576

2677
impl OutputType {
2778
fn render(&self, cx: &ViewContext<ExecutionView>) -> Option<AnyElement> {
@@ -32,6 +83,8 @@ impl OutputType {
3283
// Self::Markdown(markdown) => Some(markdown.render(theme)),
3384
Self::Media((mimetype, value)) => render_rich(mimetype, value),
3485
Self::Stream(stdio) => Some(stdio.render(cx)),
86+
Self::Image(image) => Some(image.render(cx)),
87+
Self::Message(message) => Some(div().child(message.clone()).into_any_element()),
3588
Self::ErrorOutput {
3689
ename,
3790
evalue,
@@ -50,6 +103,8 @@ impl LineHeight for OutputType {
50103
Self::Plain(stdio) => stdio.num_lines(cx),
51104
Self::Media((_mimetype, value)) => value.as_str().unwrap_or("").lines().count() as u8,
52105
Self::Stream(stdio) => stdio.num_lines(cx),
106+
Self::Image(image) => image.num_lines(cx),
107+
Self::Message(message) => message.lines().count() as u8,
53108
Self::ErrorOutput {
54109
ename,
55110
evalue,
@@ -119,15 +174,40 @@ pub enum ExecutionStatus {
119174
Finished,
120175
}
121176

122-
// Cell has status that's dependent on the runtime
123-
// The runtime itself has status
124-
125177
pub struct ExecutionView {
126178
pub execution_id: ExecutionId,
127179
pub outputs: Vec<OutputType>,
128180
pub status: ExecutionStatus,
129181
}
130182

183+
pub fn extract_image_output(mimetype: &MimeType, value: &Value) -> Result<OutputType> {
184+
let media_type = match mimetype {
185+
// TODO: Introduce From<MimeType> for str in runtimelib
186+
// We don't necessarily need it since we use guess_format, however we could skip
187+
// it if we wanted to.
188+
MimeType::Png => "image/png",
189+
MimeType::Jpeg => "image/jpeg",
190+
_ => return Err(anyhow::anyhow!("Unsupported image format")),
191+
};
192+
193+
let bytes = value.as_str().ok_or(anyhow!("Invalid image data"))?;
194+
let bytes = base64::decode(bytes)?;
195+
196+
let format = image::guess_format(&bytes)?;
197+
let data = image::load_from_memory_with_format(&bytes, format)?.into_bgra8();
198+
199+
let height = data.height();
200+
let width = data.width();
201+
202+
let gpui_image_data = ImageData::new(data);
203+
204+
return Ok(OutputType::Image(ImageView {
205+
height,
206+
width,
207+
image: Arc::new(gpui_image_data),
208+
}));
209+
}
210+
131211
impl ExecutionView {
132212
pub fn new(execution_id: ExecutionId, _cx: &mut ViewContext<Self>) -> Self {
133213
Self {
@@ -156,6 +236,14 @@ impl ExecutionView {
156236
MimeType::Markdown => {
157237
OutputType::Plain(TerminalOutput::from(value.as_str().unwrap_or("")))
158238
}
239+
MimeType::Png | MimeType::Jpeg => {
240+
match extract_image_output(&mimetype, &value) {
241+
Ok(output) => output,
242+
Err(error) => {
243+
OutputType::Message(format!("Failed to load image: {}", error))
244+
}
245+
}
246+
}
159247
// We don't handle this type, but ok
160248
_ => OutputType::Media((mimetype, value)),
161249
}

0 commit comments

Comments
 (0)