1
1
mirror of https://github.com/wez/wezterm.git synced 2024-11-22 13:16:39 +03:00
wezterm/termwiz/src/image.rs
Wez Furlong c0649ad4bb
remove cfg_attr(feature = "cargo-clippy" noise
I never liked clippy for dropping turds like this throughout the
code. I'm glad it is no longer required, but now rust is warning
about this syntax being redundant.
2024-09-14 06:49:13 -07:00

575 lines
18 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 crate::error::InternalError;
use ordered_float::NotNan;
#[cfg(feature = "use_serde")]
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::hash::{Hash, Hasher};
use std::sync::{Arc, Mutex, MutexGuard};
use std::time::Duration;
use wezterm_blob_leases::{BlobLease, BlobManager};
#[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")]
#[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, Hash)]
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 compute_shape_hash<H: Hasher>(&self, hasher: &mut H) {
self.top_left.hash(hasher);
self.bottom_right.hash(hasher);
self.data.hash.hash(hasher);
self.z_index.hash(hasher);
self.padding_left.hash(hasher);
self.padding_top.hash(hasher);
self.padding_right.hash(hasher);
self.padding_bottom.hash(hasher);
self.image_id.hash(hasher);
self.placement_id.hash(hasher);
}
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 image_id(&self) -> Option<u32> {
self.image_id
}
pub fn placement_id(&self) -> Option<u32> {
self.placement_id
}
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 in the native image file format,
/// (best for file formats that have animated content)
/// and is stored as a blob via the blob manager.
EncodedLease(
#[cfg_attr(
feature = "use_serde",
serde(with = "wezterm_blob_leases::lease_bytes")
)]
BlobLease,
),
/// 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::EncodedLease(lease) => lease.fmt(fmt),
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,
}
}
/// Black pixels
pub fn placeholder() -> Self {
let mut data = vec![];
let size = 8;
for _ in 0..size * size {
data.extend_from_slice(&[0, 0, 0, 0xff]);
}
ImageDataType::new_single_frame(size, size, data)
}
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::EncodedLease(lease) => return lease.content_id().as_hash_bytes(),
ImageDataType::Rgba8 { data, .. } => hasher.update(data),
ImageDataType::AnimRgba8 {
frames, durations, ..
} => {
for data in frames {
hasher.update(data);
}
for d in durations {
let d = d.as_secs_f32();
let b = d.to_ne_bytes();
hasher.update(b);
}
}
};
hasher.finalize().into()
}
/// Divides the animation frame durations by the provided
/// speed_factor, so a factor of 2 will halve the duration.
/// # Panics
/// if the speed_factor is negative, non-finite or the result
/// overflows the allow Duration range.
pub fn adjust_speed(&mut self, speed_factor: f32) {
match self {
Self::AnimRgba8 { durations, .. } => {
for d in durations {
*d = d.mul_f32(1. / speed_factor);
}
}
_ => {}
}
}
#[cfg(feature = "use_image")]
pub fn dimensions(&self) -> Result<(u32, u32), InternalError> {
fn dimensions_for_data(data: &[u8]) -> image::ImageResult<(u32, u32)> {
let reader =
image::ImageReader::new(std::io::Cursor::new(data)).with_guessed_format()?;
let (width, height) = reader.into_dimensions()?;
Ok((width, height))
}
match self {
ImageDataType::EncodedFile(data) => Ok(dimensions_for_data(data)?),
ImageDataType::EncodedLease(lease) => Ok(dimensions_for_data(&lease.get_data()?)?),
ImageDataType::AnimRgba8 { width, height, .. }
| ImageDataType::Rgba8 { width, height, .. } => Ok((*width, *height)),
}
}
/// Migrate an in-memory encoded image blob to on-disk to reduce
/// the memory footprint
pub fn swap_out(self) -> Result<Self, InternalError> {
match self {
Self::EncodedFile(data) => match BlobManager::store(&data) {
Ok(lease) => Ok(Self::EncodedLease(lease)),
Err(wezterm_blob_leases::Error::StorageNotInit) => Ok(Self::EncodedFile(data)),
Err(err) => Err(err.into()),
},
other => Ok(other),
}
}
/// 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);
}
};
let cursor = std::io::Cursor::new(&*data);
match format {
ImageFormat::Gif => image::codecs::gif::GifDecoder::new(cursor)
.and_then(|decoder| decoder.into_frames().collect_frames())
.and_then(|frames| {
if frames.is_empty() {
log::error!("decoded image has 0 frames, using placeholder");
Ok(Self::placeholder())
} else {
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::codecs::png::PngDecoder::new(cursor) {
Ok(d) => d,
_ => return Self::EncodedFile(data),
};
if decoder.is_apng().unwrap_or(false) {
match decoder
.apng()
.and_then(|d| d.into_frames().collect_frames())
{
Ok(frames) if frames.is_empty() => {
log::error!("decoded image has 0 frames, using placeholder");
Self::placeholder()
}
Ok(frames) => Self::decode_frames(frames),
_ => Self::EncodedFile(data),
}
} else {
Self::decode_single(data)
}
}
ImageFormat::WebP => {
let decoder = match image::codecs::webp::WebPDecoder::new(cursor) {
Ok(d) => d,
_ => return Self::EncodedFile(data),
};
match decoder.into_frames().collect_frames() {
Ok(frames) if frames.is_empty() => {
log::error!("decoded image has 0 frames, using placeholder");
Self::placeholder()
}
Ok(frames) => Self::decode_frames(frames),
_ => Self::EncodedFile(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),
}
}
}
#[cfg_attr(feature = "use_serde", derive(Serialize, Deserialize))]
pub struct ImageData {
data: Mutex<ImageDataType>,
hash: [u8; 32],
}
struct HexSlice<'a>(&'a [u8]);
impl<'a> std::fmt::Display for HexSlice<'a> {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
for byte in self.0 {
write!(fmt, "{byte:x}")?;
}
Ok(())
}
}
impl std::fmt::Debug for ImageData {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
fmt.debug_struct("ImageData")
.field("data", &self.data)
.field("hash", &format_args!("{}", HexSlice(&self.hash)))
.finish()
}
}
impl Eq for ImageData {}
impl PartialEq for ImageData {
fn eq(&self, rhs: &Self) -> bool {
self.hash == rhs.hash
}
}
impl ImageData {
/// Create a new ImageData struct with the provided raw data.
pub fn with_raw_data(data: Vec<u8>) -> Self {
let hash = ImageDataType::hash_bytes(&data);
Self::with_data_and_hash(ImageDataType::EncodedFile(data).decode(), hash)
}
fn with_data_and_hash(data: ImageDataType, hash: [u8; 32]) -> Self {
Self {
data: Mutex::new(data),
hash,
}
}
pub fn with_data(data: ImageDataType) -> Self {
let hash = data.compute_hash();
Self {
data: Mutex::new(data),
hash,
}
}
/// Returns the in-memory footprint
pub fn len(&self) -> usize {
match &*self.data() {
ImageDataType::EncodedFile(d) => d.len(),
ImageDataType::EncodedLease(_) => 0,
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 hash(&self) -> [u8; 32] {
self.hash
}
}