mirror of
https://github.com/wez/wezterm.git
synced 2024-12-23 13:21:38 +03:00
basic kitty img animation support
this is the bare minimum to squeak by with notcurses; it currently only supports editing single frame images. refs: #986
This commit is contained in:
parent
29a64e10b8
commit
e85e94248b
13
Cargo.lock
generated
13
Cargo.lock
generated
@ -977,15 +977,6 @@ dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deflate"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f95bf05dffba6e6cce8dfbb30def788154949ccd9aed761b472119c21e01c70"
|
||||
dependencies = [
|
||||
"adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derivative"
|
||||
version = "2.2.0"
|
||||
@ -3189,7 +3180,7 @@ checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"crc32fast",
|
||||
"deflate 0.8.6",
|
||||
"deflate",
|
||||
"miniz_oxide 0.3.7",
|
||||
]
|
||||
|
||||
@ -5131,13 +5122,13 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
"deflate 0.9.1",
|
||||
"hex",
|
||||
"image",
|
||||
"k9",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"lru",
|
||||
"miniz_oxide 0.4.4",
|
||||
"num-traits",
|
||||
"ordered-float",
|
||||
"palette",
|
||||
|
@ -16,7 +16,7 @@ use_serde = ["termwiz/use_serde"]
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
bitflags = "1.0"
|
||||
deflate = "0.9"
|
||||
miniz_oxide = "0.4"
|
||||
hex = "0.4"
|
||||
image = "0.23"
|
||||
lazy_static = "1.4"
|
||||
|
@ -1,14 +1,17 @@
|
||||
use crate::terminalstate::image::*;
|
||||
use crate::terminalstate::{ImageAttachParams, PlacementInfo};
|
||||
use crate::{StableRowIndex, TerminalState};
|
||||
use ::image::{DynamicImage, GenericImageView, RgbImage};
|
||||
use ::image::{
|
||||
DynamicImage, GenericImage, GenericImageView, ImageBuffer, RgbImage, Rgba, RgbaImage,
|
||||
};
|
||||
use anyhow::Context;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
use termwiz::escape::apc::KittyImageData;
|
||||
use termwiz::escape::apc::{
|
||||
KittyImage, KittyImageCompression, KittyImageDelete, KittyImageFormat, KittyImagePlacement,
|
||||
KittyImageTransmit, KittyImageVerbosity,
|
||||
KittyFrameCompositionMode, KittyImage, KittyImageCompression, KittyImageDelete,
|
||||
KittyImageFormat, KittyImageFrame, KittyImagePlacement, KittyImageTransmit,
|
||||
KittyImageVerbosity,
|
||||
};
|
||||
use termwiz::image::ImageDataType;
|
||||
use termwiz::surface::change::ImageData;
|
||||
@ -256,16 +259,13 @@ impl TerminalState {
|
||||
log::warn!("unhandled KittyImage::Delete {:?} {:?}", what, verbosity);
|
||||
}
|
||||
KittyImage::TransmitFrame {
|
||||
frame,
|
||||
transmit,
|
||||
frame,
|
||||
verbosity,
|
||||
} => {
|
||||
log::warn!(
|
||||
"unhandled KittyImage::TransmitFrame {:?} {:?} {:?}",
|
||||
frame,
|
||||
transmit,
|
||||
verbosity
|
||||
);
|
||||
if let Err(err) = self.kitty_frame_transmit(transmit, frame, verbosity) {
|
||||
log::error!("Error {:#} while handling KittyImage::TransmitFrame", err,);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -319,12 +319,127 @@ impl TerminalState {
|
||||
);
|
||||
}
|
||||
|
||||
fn kitty_img_transmit(
|
||||
fn kitty_frame_transmit(
|
||||
&mut self,
|
||||
transmit: KittyImageTransmit,
|
||||
frame: KittyImageFrame,
|
||||
verbosity: KittyImageVerbosity,
|
||||
) -> anyhow::Result<u32> {
|
||||
let (image_id, image_number) = match (transmit.image_id, transmit.image_number) {
|
||||
) -> anyhow::Result<()> {
|
||||
let (image_id, _image_number, img) = self.kitty_img_transmit_inner(transmit)?;
|
||||
|
||||
let img = match img.decode() {
|
||||
ImageDataType::Rgba8 {
|
||||
data,
|
||||
width,
|
||||
height,
|
||||
} => RgbaImage::from_vec(width, height, data)
|
||||
.ok_or_else(|| anyhow::anyhow!("data isn't rgba8"))?,
|
||||
wat => anyhow::bail!("data isn't rgba8 {:?}", wat),
|
||||
};
|
||||
|
||||
let anim = self
|
||||
.kitty_img
|
||||
.id_to_data
|
||||
.get(&image_id)
|
||||
.ok_or_else(|| anyhow::anyhow!("no matching image id"))?;
|
||||
|
||||
let mut anim = anim.data();
|
||||
|
||||
match &mut *anim {
|
||||
ImageDataType::EncodedFile(_) => {
|
||||
anyhow::bail!("Expected decoded image for image id {}", image_id)
|
||||
}
|
||||
ImageDataType::Rgba8 {
|
||||
data,
|
||||
width,
|
||||
height,
|
||||
} => {
|
||||
let base_frame = match frame.base_frame {
|
||||
Some(1) => Some(1),
|
||||
None => None,
|
||||
Some(n) => anyhow::bail!(
|
||||
"attempted to copy frame {} but there is only a single frame",
|
||||
n
|
||||
),
|
||||
};
|
||||
|
||||
match frame.frame_number {
|
||||
Some(1) => {
|
||||
// Edit in place
|
||||
let len = data.len();
|
||||
let mut anim: ImageBuffer<Rgba<u8>, &mut [u8]> =
|
||||
ImageBuffer::from_raw(*width, *height, data.as_mut_slice())
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"ImageBuffer::from_raw failed for single \
|
||||
frame of {}x{} ({} bytes)",
|
||||
width,
|
||||
height,
|
||||
len
|
||||
)
|
||||
})?;
|
||||
|
||||
match frame.composition_mode {
|
||||
KittyFrameCompositionMode::Overwrite => {
|
||||
// Notcurses can send an img with x,y position that overflows
|
||||
// the target frame, so we need to make a view that clips the
|
||||
// source image data.
|
||||
let x = frame.x.unwrap_or(0);
|
||||
let y = frame.y.unwrap_or(0);
|
||||
|
||||
let (src_w, src_h) = img.dimensions();
|
||||
|
||||
let w = src_w.min(width.saturating_sub(x));
|
||||
let h = src_h.min(height.saturating_sub(y));
|
||||
|
||||
let img = img.view(0, 0, w, h);
|
||||
|
||||
anim.copy_from(&img, frame.x.unwrap_or(0), frame.y.unwrap_or(0))
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"copying img with dims {:?} to frame \
|
||||
with dims {:?} @ offset {:?}x{:?}",
|
||||
img.dimensions(),
|
||||
anim.dimensions(),
|
||||
frame.x,
|
||||
frame.y
|
||||
)
|
||||
})?;
|
||||
}
|
||||
KittyFrameCompositionMode::AlphaBlending => {
|
||||
anyhow::bail!("alphablend compositing not implemented");
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// Create a second frame
|
||||
anyhow::bail!("crating frames not yet done");
|
||||
}
|
||||
Some(n) => anyhow::bail!(
|
||||
"attempted to edit frame {} but there is only a single frame",
|
||||
n
|
||||
),
|
||||
}
|
||||
}
|
||||
ImageDataType::AnimRgba8 {
|
||||
width,
|
||||
height,
|
||||
frames,
|
||||
durations,
|
||||
} => {
|
||||
anyhow::bail!("editing animations not yet done");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn kitty_img_transmit_inner(
|
||||
&mut self,
|
||||
transmit: KittyImageTransmit,
|
||||
) -> anyhow::Result<(u32, Option<u32>, ImageDataType)> {
|
||||
log::trace!("transmit {:?}", transmit);
|
||||
let (id, no) = match (transmit.image_id, transmit.image_number) {
|
||||
(Some(_), Some(_)) => {
|
||||
// TODO: send an EINVAL error back here
|
||||
anyhow::bail!("cannot use both i= and I= in the same request");
|
||||
@ -341,9 +456,6 @@ impl TerminalState {
|
||||
}
|
||||
};
|
||||
|
||||
self.kitty_img.max_image_id = self.kitty_img.max_image_id.max(image_id);
|
||||
log::trace!("transmit {:?}", transmit);
|
||||
|
||||
let data = transmit
|
||||
.data
|
||||
.load_data()
|
||||
@ -351,7 +463,10 @@ impl TerminalState {
|
||||
|
||||
let data = match transmit.compression {
|
||||
KittyImageCompression::None => data,
|
||||
KittyImageCompression::Deflate => deflate::deflate_bytes(&data),
|
||||
KittyImageCompression::Deflate => {
|
||||
miniz_oxide::inflate::decompress_to_vec_zlib(&data)
|
||||
.map_err(|e| anyhow::anyhow!("decompressing data: {:?}", e))?
|
||||
}
|
||||
};
|
||||
|
||||
let img = match transmit.format {
|
||||
@ -375,27 +490,45 @@ impl TerminalState {
|
||||
_ => data,
|
||||
};
|
||||
|
||||
let image_data = ImageDataType::Rgba8 {
|
||||
anyhow::ensure!(
|
||||
width * height * 4 == data.len() as u32,
|
||||
"transmit data len is {} but it doesn't match width*height*4 {}x{}x4 = {}",
|
||||
data.len(),
|
||||
width,
|
||||
height,
|
||||
width * height * 4
|
||||
);
|
||||
|
||||
ImageDataType::Rgba8 {
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
};
|
||||
|
||||
self.raw_image_to_image_data(image_data)
|
||||
}
|
||||
}
|
||||
Some(KittyImageFormat::Png) => {
|
||||
let decoded = image::load_from_memory(&data).context("decode png")?;
|
||||
let (width, height) = decoded.dimensions();
|
||||
let data = decoded.into_rgba8().into_vec();
|
||||
let image_data = ImageDataType::Rgba8 {
|
||||
ImageDataType::Rgba8 {
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
};
|
||||
self.raw_image_to_image_data(image_data)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok((id, no, img))
|
||||
}
|
||||
|
||||
fn kitty_img_transmit(
|
||||
&mut self,
|
||||
transmit: KittyImageTransmit,
|
||||
verbosity: KittyImageVerbosity,
|
||||
) -> anyhow::Result<u32> {
|
||||
let (image_id, image_number, img) = self.kitty_img_transmit_inner(transmit)?;
|
||||
self.kitty_img.max_image_id = self.kitty_img.max_image_id.max(image_id);
|
||||
|
||||
let img = self.raw_image_to_image_data(img);
|
||||
self.kitty_img.record_id_to_data(image_id, img);
|
||||
|
||||
if let Some(no) = image_number {
|
||||
|
@ -664,7 +664,7 @@ pub struct KittyImageFrame {
|
||||
|
||||
/// 1-based number of the frame which should be the base
|
||||
/// data for the new frame being created.
|
||||
/// If omitted, a black, fully-transparent background is used.
|
||||
/// If omitted, use background_pixel to specify color.
|
||||
/// c=...
|
||||
pub base_frame: Option<u32>,
|
||||
|
||||
@ -684,6 +684,7 @@ pub struct KittyImageFrame {
|
||||
pub composition_mode: KittyFrameCompositionMode,
|
||||
|
||||
/// Background color for pixels not specified in the frame data.
|
||||
/// If omitted, use a black, fully-transparent pixel (0)
|
||||
/// Y=...
|
||||
pub background_pixel: Option<u32>,
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user