//! Images. //! This module has some helpers for modeling terminal cells that are filled //! with image data. //! We're targeting the iTerm image protocol initially, with sixel as an obvious //! follow up. //! Kitty has an extensive and complex graphics protocol //! whose docs are here: //! //! Both iTerm2 and Sixel appear to have semantics that allow replacing the //! contents of a single chararcter cell with image data, whereas the kitty //! protocol appears to track the images out of band as attachments with //! z-order. use ordered_float::NotNan; #[cfg(feature = "use_serde")] use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::sync::{Arc, Mutex, MutexGuard}; use std::time::Duration; #[cfg(feature = "use_serde")] fn deserialize_notnan<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { let value = f32::deserialize(deserializer)?; NotNan::new(value).map_err(|e| serde::de::Error::custom(format!("{:?}", e))) } #[cfg(feature = "use_serde")] #[cfg_attr(feature = "cargo-clippy", allow(clippy::trivially_copy_pass_by_ref))] fn serialize_notnan(value: &NotNan, serializer: S) -> Result where S: Serializer, { value.into_inner().serialize(serializer) } #[cfg_attr(feature = "use_serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct TextureCoordinate { #[cfg_attr( feature = "use_serde", serde( deserialize_with = "deserialize_notnan", serialize_with = "serialize_notnan" ) )] pub x: NotNan, #[cfg_attr( feature = "use_serde", serde( deserialize_with = "deserialize_notnan", serialize_with = "serialize_notnan" ) )] pub y: NotNan, } impl TextureCoordinate { pub fn new(x: NotNan, y: NotNan) -> Self { Self { x, y } } pub fn new_f32(x: f32, y: f32) -> Self { let x = NotNan::new(x).unwrap(); let y = NotNan::new(y).unwrap(); Self::new(x, y) } } /// Tracks data for displaying an image in the place of the normal cell /// character data. Since an Image can span multiple cells, we need to logically /// carve up the image and track each slice of it. Each cell needs to know /// its "texture coordinates" within that image so that we can render the /// right slice. #[cfg_attr(feature = "use_serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq)] pub struct ImageCell { /// Texture coordinate for the top left of this cell. /// (0,0) is the top left of the ImageData. (1, 1) is /// the bottom right. top_left: TextureCoordinate, /// Texture coordinates for the bottom right of this cell. bottom_right: TextureCoordinate, /// References the underlying image data data: Arc, z_index: i32, /// When rendering in the cell, use this offset from the top left /// of the cell padding_left: u16, padding_top: u16, padding_right: u16, padding_bottom: u16, image_id: Option, placement_id: Option, } impl ImageCell { pub fn new( top_left: TextureCoordinate, bottom_right: TextureCoordinate, data: Arc, ) -> Self { Self::with_z_index(top_left, bottom_right, data, 0, 0, 0, 0, 0, None, None) } pub fn with_z_index( top_left: TextureCoordinate, bottom_right: TextureCoordinate, data: Arc, z_index: i32, padding_left: u16, padding_top: u16, padding_right: u16, padding_bottom: u16, image_id: Option, placement_id: Option, ) -> Self { Self { top_left, bottom_right, data, z_index, padding_left, padding_top, padding_right, padding_bottom, image_id, placement_id, } } pub fn matches_placement(&self, image_id: u32, placement_id: Option) -> bool { self.image_id == Some(image_id) && self.placement_id == placement_id } pub fn has_placement_id(&self) -> bool { self.placement_id.is_some() } pub fn top_left(&self) -> TextureCoordinate { self.top_left } pub fn bottom_right(&self) -> TextureCoordinate { self.bottom_right } pub fn image_data(&self) -> &Arc { &self.data } /// negative z_index is rendered beneath the text layer. /// >= 0 is rendered above the text. /// negative z_index < INT32_MIN/2 will be drawn under cells /// with non-default background colors pub fn z_index(&self) -> i32 { self.z_index } /// Returns padding (left, top, right, bottom) pub fn padding(&self) -> (u16, u16, u16, u16) { ( self.padding_left, self.padding_top, self.padding_right, self.padding_bottom, ) } } #[cfg_attr(feature = "use_serde", derive(Serialize, Deserialize))] #[derive(Clone, PartialEq, Eq)] pub enum ImageDataType { /// Data is in the native image file format /// (best for file formats that have animated content) EncodedFile(Vec), /// Data is RGBA u8 data Rgba8 { data: Vec, width: u32, height: u32, hash: [u8; 32], }, /// Data is an animated sequence AnimRgba8 { width: u32, height: u32, durations: Vec, frames: Vec>, hashes: Vec<[u8; 32]>, }, } impl std::fmt::Debug for ImageDataType { fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Self::EncodedFile(data) => fmt .debug_struct("EncodedFile") .field("data_of_len", &data.len()) .finish(), Self::Rgba8 { data, width, height, hash, } => fmt .debug_struct("Rgba8") .field("data_of_len", &data.len()) .field("width", &width) .field("height", &height) .field("hash", &hash) .finish(), Self::AnimRgba8 { frames, width, height, durations, hashes, } => fmt .debug_struct("AnimRgba8") .field("frames_of_len", &frames.len()) .field("width", &width) .field("height", &height) .field("durations", durations) .field("hashes", hashes) .finish(), } } } impl ImageDataType { pub fn new_single_frame(width: u32, height: u32, data: Vec) -> Self { let hash = Self::hash_bytes(&data); assert_eq!( width * height * 4, data.len() as u32, "invalid dimensions {}x{} for pixel data of length {}", width, height, data.len() ); Self::Rgba8 { width, height, data, hash, } } pub fn hash_bytes(bytes: &[u8]) -> [u8; 32] { use sha2::Digest; let mut hasher = sha2::Sha256::new(); hasher.update(bytes); hasher.finalize().into() } pub fn compute_hash(&self) -> [u8; 32] { use sha2::Digest; let mut hasher = sha2::Sha256::new(); match self { ImageDataType::EncodedFile(data) => hasher.update(data), ImageDataType::Rgba8 { data, .. } => hasher.update(data), ImageDataType::AnimRgba8 { frames, .. } => { for data in frames { hasher.update(data); } } }; hasher.finalize().into() } /// Decode an encoded file into either an Rgba8 or AnimRgba8 variant /// if we recognize the file format, otherwise the EncodedFile data /// is preserved as is. #[cfg(feature = "use_image")] pub fn decode(self) -> Self { use image::{AnimationDecoder, ImageFormat}; match self { Self::EncodedFile(data) => { let format = match image::guess_format(&data) { Ok(format) => format, Err(err) => { log::warn!("Unable to decode raw image data: {:#}", err); return Self::EncodedFile(data); } }; match format { ImageFormat::Gif => image::gif::GifDecoder::new(&*data) .and_then(|decoder| decoder.into_frames().collect_frames()) .and_then(|frames| Ok(Self::decode_frames(frames))) .unwrap_or_else(|err| { log::error!( "Unable to parse animated gif: {:#}, trying as single frame", err ); Self::decode_single(data) }), ImageFormat::Png => { let decoder = match image::png::PngDecoder::new(&*data) { Ok(d) => d, _ => return Self::EncodedFile(data), }; if decoder.is_apng() { match decoder.apng().into_frames().collect_frames() { Ok(frames) => Self::decode_frames(frames), _ => Self::EncodedFile(data), } } else { Self::decode_single(data) } } _ => Self::decode_single(data), } } data => data, } } #[cfg(not(feature = "use_image"))] pub fn decode(self) -> Self { self } #[cfg(feature = "use_image")] fn decode_frames(img_frames: Vec) -> Self { let mut width = 0; let mut height = 0; let mut frames = vec![]; let mut durations = vec![]; let mut hashes = vec![]; for frame in img_frames.into_iter() { let duration: Duration = frame.delay().into(); durations.push(duration); let image = image::DynamicImage::ImageRgba8(frame.into_buffer()).to_rgba8(); let (w, h) = image.dimensions(); width = w; height = h; let data = image.into_vec(); hashes.push(Self::hash_bytes(&data)); frames.push(data); } Self::AnimRgba8 { width, height, frames, durations, hashes, } } #[cfg(feature = "use_image")] fn decode_single(data: Vec) -> Self { match image::load_from_memory(&data) { Ok(image) => { let image = image.to_rgba8(); let (width, height) = image.dimensions(); let data = image.into_vec(); let hash = Self::hash_bytes(&data); Self::Rgba8 { width, height, data, hash, } } _ => Self::EncodedFile(data), } } } static IMAGE_ID: ::std::sync::atomic::AtomicUsize = ::std::sync::atomic::AtomicUsize::new(0); #[cfg_attr(feature = "use_serde", derive(Serialize, Deserialize))] #[derive(Debug)] pub struct ImageData { id: usize, data: Mutex, } impl Eq for ImageData {} impl PartialEq for ImageData { fn eq(&self, rhs: &Self) -> bool { self.id == rhs.id } } impl ImageData { /// Create a new ImageData struct with the provided raw data. pub fn with_raw_data(data: Vec) -> Self { Self::with_data(ImageDataType::EncodedFile(data).decode()) } pub fn with_data(data: ImageDataType) -> Self { let id = IMAGE_ID.fetch_add(1, ::std::sync::atomic::Ordering::Relaxed); Self { id, data: Mutex::new(data), } } /// Returns the in-memory footprint pub fn len(&self) -> usize { match &*self.data() { ImageDataType::EncodedFile(d) => d.len(), ImageDataType::Rgba8 { data, .. } => data.len(), ImageDataType::AnimRgba8 { frames, .. } => frames.len() * frames[0].len(), } } pub fn data(&self) -> MutexGuard { self.data.lock().unwrap() } pub fn id(&self) -> usize { self.id } pub fn hash(&self) -> [u8; 32] { self.data().compute_hash() } }