From 9219dbb327c0408e51a4849082e0a9be00127846 Mon Sep 17 00:00:00 2001 From: Wez Furlong Date: Wed, 8 Aug 2018 09:00:07 -0700 Subject: [PATCH] We can now take iterm images into the wezterm terminalstate We don't yet have any code to render them, and the vte parser seems to truncate incoming image sequences ~1kb in size, so more work is needed to make this useful. --- term/Cargo.toml | 2 + term/src/lib.rs | 2 + term/src/terminalstate.rs | 139 +++++++++++++++++++++++++++++++++++-- termwiz/src/escape/osc.rs | 15 ++++ termwiz/src/surface/mod.rs | 2 +- 5 files changed, 155 insertions(+), 5 deletions(-) diff --git a/term/Cargo.toml b/term/Cargo.toml index cf00dc4f3..526a72879 100644 --- a/term/Cargo.toml +++ b/term/Cargo.toml @@ -6,6 +6,8 @@ version = "0.1.0" [dependencies] bitflags = "~1.0" failure = "~0.1" +image = "~0.19" +ordered-float = "~0.5" unicode-segmentation = "~1.2" unicode-width = "~0.1" diff --git a/term/src/lib.rs b/term/src/lib.rs index 6c5d5b873..fa611de26 100644 --- a/term/src/lib.rs +++ b/term/src/lib.rs @@ -3,6 +3,8 @@ extern crate bitflags; #[macro_use] extern crate failure; +extern crate image; +extern crate ordered_float; extern crate termwiz; extern crate unicode_segmentation; extern crate unicode_width; diff --git a/term/src/terminalstate.rs b/term/src/terminalstate.rs index d7353b629..a87de02fe 100644 --- a/term/src/terminalstate.rs +++ b/term/src/terminalstate.rs @@ -1,11 +1,15 @@ use super::*; +use image::{self, GenericImage}; +use ordered_float::NotNaN; use std::fmt::Write; use termwiz::escape::csi::{ Cursor, DecPrivateMode, DecPrivateModeCode, Device, Edit, EraseInDisplay, EraseInLine, Mode, Sgr, }; +use termwiz::escape::osc::{ITermFileData, ITermProprietary}; use termwiz::escape::{Action, ControlCode, Esc, EscCode, OperatingSystemCommand, CSI}; use termwiz::hyperlink::Rule as HyperlinkRule; +use termwiz::image::{ImageCell, ImageData, TextureCoordinate}; use unicode_segmentation::UnicodeSegmentation; struct TabStop { @@ -1122,6 +1126,133 @@ impl TerminalState { } } + fn set_image(&mut self, image: ITermFileData) { + if !image.inline { + eprintln!( + "Ignoring file download request name={:?} size={}", + image.name, + image.data.len() + ); + return; + } + + // Decode the image data + let decoded_image = match image::load_from_memory(&image.data) { + Ok(im) => im, + Err(e) => { + eprintln!( + "Unable to decode image: {}: size={} {:?}", + e, + image.data.len(), + image + ); + return; + } + }; + + // Figure out the dimensions. + // TODO: we need to understand pixels here, and we don't today, + // so "guess" using the values that I see in my setup. + let cell_pixel_width = 8; + let cell_pixel_height = 15; + + let width = image + .width + .to_pixels(cell_pixel_width, self.screen().physical_cols); + let height = image + .height + .to_pixels(cell_pixel_height, self.screen().physical_rows); + + // Compute any Automatic dimensions + let (width, height) = match (width, height) { + (None, None) => ( + decoded_image.width() as usize, + decoded_image.height() as usize, + ), + (Some(w), None) => { + let scale = decoded_image.width() as f32 / w as f32; + let h = decoded_image.height() as f32 * scale; + (w, h as usize) + } + (None, Some(h)) => { + let scale = decoded_image.height() as f32 / h as f32; + let w = decoded_image.width() as f32 * scale; + (w as usize, h) + } + (Some(w), Some(h)) => (w, h), + }; + + let width_in_cells = width / cell_pixel_width; + let height_in_cells = height / cell_pixel_height; + + let available_pixel_width = width_in_cells * cell_pixel_width; + let available_pixel_height = height_in_cells * cell_pixel_height; + + // TODO: defer this to the actual renderer + /* + let resized_image = if image.preserve_aspect_ratio { + let resized = decoded_image.resize( + available_pixel_width as u32, + available_pixel_height as u32, + image::FilterType::Lanczos3, + ); + // Pad with black bars to preserve aspect ratio + // Assumption: new_rgba8 returns black/transparent pixels by default. + let dest = DynamicImage::new_rgba8(available_pixel_width, available_pixel_height); + dest.copy_from(resized, 0, 0); + dest + } else { + decoded_image.resize_exact( + available_pixel_width as u32, + available_pixel_height as u32, + image::FilterType::Lanczos3, + ) + }; + */ + + let image_data = Rc::new(ImageData::with_raw_data(image.data)); + + let mut ypos = NotNaN::new(0.0).unwrap(); + let cursor_x = self.cursor.x; + let cursor_y = self.cursor.y; + let x_delta = 1.0 / available_pixel_width as f32; + let y_delta = 1.0 / available_pixel_height as f32; + eprintln!( + "image is {}x{} cells, {}x{} pixels", + width_in_cells, height_in_cells, width, height + ); + for y in 0..height_in_cells { + let mut xpos = NotNaN::new(0.0).unwrap(); + for x in 0..width_in_cells { + self.screen_mut().set_cell( + cursor_x + x, + cursor_y + y as VisibleRowIndex, + &Cell::new( + ' ', + CellAttributes::default() + .set_image(Some(Box::new(ImageCell::new( + TextureCoordinate::new(xpos, ypos), + TextureCoordinate::new( + xpos + cell_pixel_width as f32, + ypos + cell_pixel_height as f32, + ), + image_data.clone(), + )))) + .clone(), + ), + ); + xpos += x_delta; + } + ypos += y_delta; + } + + // FIXME: check cursor positioning in iterm + self.set_cursor_pos( + &Position::Relative(width_in_cells as i64), + &Position::Relative(0), + ); + } + fn perform_device(&mut self, dev: Device, host: &mut TerminalHost) { match dev { Device::DeviceAttributes(a) => eprintln!("unhandled: {:?}", a), @@ -1679,10 +1810,10 @@ impl<'a> Performer<'a> { Ok(_) => (), Err(err) => eprintln!("failed to set clipboard in response to OSC 52: {:?}", err), }, - OperatingSystemCommand::ITermProprietary(iterm) => { - // TODO: handle some iTerm2 sequences - eprintln!("unhandled iterm2: {:?}", iterm); - } + OperatingSystemCommand::ITermProprietary(iterm) => match iterm { + ITermProprietary::File(image) => self.set_image(*image), + _ => eprintln!("unhandled iterm2: {:?}", iterm), + }, OperatingSystemCommand::SystemNotification(message) => { eprintln!("Application sends SystemNotification: {}", message); } diff --git a/termwiz/src/escape/osc.rs b/termwiz/src/escape/osc.rs index a4cd6f156..c477484ea 100644 --- a/termwiz/src/escape/osc.rs +++ b/termwiz/src/escape/osc.rs @@ -446,6 +446,21 @@ impl ITermDimension { Ok(ITermDimension::Cells(num)) } } + + /// Convert the dimension into a number of pixels based on the provided + /// size of a cell and number of cells in that dimension. + /// Returns None for the Automatic variant. + pub fn to_pixels(&self, cell_size: usize, num_cells: usize) -> Option { + match self { + ITermDimension::Automatic => None, + ITermDimension::Cells(n) => Some((*n).max(0) as usize * cell_size), + ITermDimension::Pixels(n) => Some((*n).max(0) as usize), + ITermDimension::Percent(n) => Some( + (((*n).max(0).min(100) as f32 / 100.0) * num_cells as f32 * cell_size as f32) + as usize, + ), + } + } } impl ITermProprietary { diff --git a/termwiz/src/surface/mod.rs b/termwiz/src/surface/mod.rs index 22e93bdac..d1913a5eb 100644 --- a/termwiz/src/surface/mod.rs +++ b/termwiz/src/surface/mod.rs @@ -1410,7 +1410,7 @@ mod test { #[test] fn images() { // a dummy image blob with nonsense content - let data = Rc::new(ImageData::with_raw_data(0, 0, vec![])); + let data = Rc::new(ImageData::with_raw_data(vec![])); let mut s = Surface::new(2, 2); s.add_change(Change::Image(Image { top_left: TextureCoordinate::new_f32(0.0, 0.0),