1
1
mirror of https://github.com/wez/wezterm.git synced 2024-12-25 14:22:37 +03:00
wezterm/termwiz/src/image.rs
Wez Furlong 294579fd31 term: introduce right&bottom padding to ImageCell
This helps us correctly set the size of the image cell
for the case where we have a partial cell at the right/bottom
edge of an image being mapped across cells.

refs: #1270
2021-12-23 07:26:49 -07:00

425 lines
13 KiB
Rust

//! 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:
//! <https://github.com/kovidgoyal/kitty/blob/master/docs/graphics-protocol.rst>
//! 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<NotNan<f32>, 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<S>(value: &NotNan<f32>, serializer: S) -> Result<S::Ok, S::Error>
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<f32>,
#[cfg_attr(
feature = "use_serde",
serde(
deserialize_with = "deserialize_notnan",
serialize_with = "serialize_notnan"
)
)]
pub y: NotNan<f32>,
}
impl TextureCoordinate {
pub fn new(x: NotNan<f32>, y: NotNan<f32>) -> 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<ImageData>,
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<u32>,
placement_id: Option<u32>,
}
impl ImageCell {
pub fn new(
top_left: TextureCoordinate,
bottom_right: TextureCoordinate,
data: Arc<ImageData>,
) -> 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<ImageData>,
z_index: i32,
padding_left: u16,
padding_top: u16,
padding_right: u16,
padding_bottom: u16,
image_id: Option<u32>,
placement_id: Option<u32>,
) -> 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<u32>) -> 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<ImageData> {
&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<u8>),
/// Data is RGBA u8 data
Rgba8 {
data: Vec<u8>,
width: u32,
height: u32,
hash: [u8; 32],
},
/// Data is an animated sequence
AnimRgba8 {
width: u32,
height: u32,
durations: Vec<Duration>,
frames: Vec<Vec<u8>>,
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<u8>) -> 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<image::Frame>) -> 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<u8>) -> 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<ImageDataType>,
}
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<u8>) -> 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<ImageDataType> {
self.data.lock().unwrap()
}
pub fn id(&self) -> usize {
self.id
}
pub fn hash(&self) -> [u8; 32] {
self.data().compute_hash()
}
}