mirror of
https://github.com/wez/wezterm.git
synced 2024-12-23 21:32:13 +03:00
kitty img: parse and implement basic anim frame composition
This is just enough for notcurses-demo to run, and is definitely missing various cases. refs: #986
This commit is contained in:
parent
0c5753eb58
commit
02b5defac1
@ -11,8 +11,8 @@ use std::time::Duration;
|
||||
use termwiz::escape::apc::KittyImageData;
|
||||
use termwiz::escape::apc::{
|
||||
KittyFrameCompositionMode, KittyImage, KittyImageCompression, KittyImageDelete,
|
||||
KittyImageFormat, KittyImageFrame, KittyImagePlacement, KittyImageTransmit,
|
||||
KittyImageVerbosity,
|
||||
KittyImageFormat, KittyImageFrame, KittyImageFrameCompose, KittyImagePlacement,
|
||||
KittyImageTransmit, KittyImageVerbosity,
|
||||
};
|
||||
use termwiz::image::ImageDataType;
|
||||
use termwiz::surface::change::ImageData;
|
||||
@ -269,6 +269,11 @@ impl TerminalState {
|
||||
log::error!("Error {:#} while handling KittyImage::TransmitFrame", err,);
|
||||
}
|
||||
}
|
||||
KittyImage::ComposeFrame { frame, verbosity } => {
|
||||
if let Err(err) = self.kitty_frame_compose(frame, verbosity) {
|
||||
log::error!("Error {:#} while handling KittyImage::ComposeFrame", err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
@ -321,6 +326,115 @@ impl TerminalState {
|
||||
);
|
||||
}
|
||||
|
||||
fn kitty_frame_compose(
|
||||
&mut self,
|
||||
frame: KittyImageFrameCompose,
|
||||
verbosity: KittyImageVerbosity,
|
||||
) -> anyhow::Result<()> {
|
||||
let image_id = match frame.image_number {
|
||||
Some(no) => match self.kitty_img.number_to_id.get(&no) {
|
||||
Some(id) => *id,
|
||||
None => anyhow::bail!("no such image_number {}", no),
|
||||
},
|
||||
None => frame
|
||||
.image_id
|
||||
.ok_or_else(|| anyhow::anyhow!("no image_id"))?,
|
||||
};
|
||||
|
||||
let src_frame = frame
|
||||
.source_frame
|
||||
.ok_or_else(|| anyhow::anyhow!("missing source frame"))?
|
||||
as usize;
|
||||
let target_frame = frame
|
||||
.target_frame
|
||||
.ok_or_else(|| anyhow::anyhow!("missing target frame"))?
|
||||
as usize;
|
||||
|
||||
let img = self
|
||||
.kitty_img
|
||||
.id_to_data
|
||||
.get(&image_id)
|
||||
.ok_or_else(|| anyhow::anyhow!("invalid image id {}", image_id))?;
|
||||
|
||||
let mut img = img.data();
|
||||
match &mut *img {
|
||||
ImageDataType::EncodedFile(_) => anyhow::bail!("invalid image type"),
|
||||
ImageDataType::Rgba8 {
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
hash,
|
||||
} => {
|
||||
anyhow::ensure!(
|
||||
src_frame == target_frame && src_frame == 1,
|
||||
"src_frame={} target_frame={} but there is only a single frame",
|
||||
src_frame,
|
||||
target_frame
|
||||
);
|
||||
|
||||
let mut anim_img: ImageBuffer<Rgba<u8>, &mut [u8]> =
|
||||
ImageBuffer::from_raw(*width, *height, data.as_mut_slice())
|
||||
.ok_or_else(|| anyhow::anyhow!("ill formed image"))?;
|
||||
|
||||
anyhow::bail!("TODO: finish this case in frame compose");
|
||||
}
|
||||
ImageDataType::AnimRgba8 {
|
||||
width,
|
||||
height,
|
||||
frames,
|
||||
hashes,
|
||||
..
|
||||
} => {
|
||||
anyhow::ensure!(
|
||||
src_frame > 0 && src_frame <= frames.len(),
|
||||
"src_frame {} is out of range",
|
||||
src_frame
|
||||
);
|
||||
anyhow::ensure!(
|
||||
target_frame > 0 && target_frame <= frames.len(),
|
||||
"target_frame {} is out of range",
|
||||
target_frame
|
||||
);
|
||||
|
||||
if src_frame != target_frame {
|
||||
let src = RgbaImage::from_vec(*width, *height, frames[src_frame - 1].clone())
|
||||
.ok_or_else(|| anyhow::anyhow!("ill formed image"))?;
|
||||
|
||||
let src = src.view(
|
||||
frame.src_x.unwrap_or(0),
|
||||
frame.src_y.unwrap_or(0),
|
||||
frame.w.unwrap_or(*width),
|
||||
frame.h.unwrap_or(*height),
|
||||
);
|
||||
|
||||
let mut dest: ImageBuffer<Rgba<u8>, &mut [u8]> = ImageBuffer::from_raw(
|
||||
*width,
|
||||
*height,
|
||||
frames[target_frame - 1].as_mut_slice(),
|
||||
)
|
||||
.ok_or_else(|| anyhow::anyhow!("ill formed image"))?;
|
||||
|
||||
blit(
|
||||
&mut dest,
|
||||
*width,
|
||||
*height,
|
||||
&src,
|
||||
frame.x.unwrap_or(0),
|
||||
frame.y.unwrap_or(0),
|
||||
frame.composition_mode,
|
||||
)?;
|
||||
|
||||
drop(dest);
|
||||
hashes[target_frame - 1] = ImageDataType::hash_bytes(&frames[target_frame - 1]);
|
||||
} else {
|
||||
anyhow::bail!("TODO: editing in place not yet impl");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn kitty_frame_transmit(
|
||||
&mut self,
|
||||
mut transmit: KittyImageTransmit,
|
||||
@ -373,50 +487,6 @@ impl TerminalState {
|
||||
Some(n) => n.into(),
|
||||
});
|
||||
|
||||
fn blit<D, S, P>(
|
||||
dest: &mut D,
|
||||
dest_width: u32,
|
||||
dest_height: u32,
|
||||
src: &S,
|
||||
x: u32,
|
||||
y: u32,
|
||||
mode: KittyFrameCompositionMode,
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
D: GenericImage<Pixel = P>,
|
||||
S: GenericImageView<Pixel = P>,
|
||||
{
|
||||
// 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 (src_w, src_h) = src.dimensions();
|
||||
|
||||
let w = src_w.min(dest_width.saturating_sub(x));
|
||||
let h = src_h.min(dest_height.saturating_sub(y));
|
||||
|
||||
let src = src.view(0, 0, w, h);
|
||||
match mode {
|
||||
KittyFrameCompositionMode::Overwrite => {
|
||||
dest.copy_from(&src, x, y).with_context(|| {
|
||||
format!(
|
||||
"copying img with dims {:?} to frame \
|
||||
with dims {}x{} @ offset {:?}x{:?}",
|
||||
src.dimensions(),
|
||||
dest_width,
|
||||
dest_height,
|
||||
x,
|
||||
y
|
||||
)
|
||||
})?;
|
||||
}
|
||||
KittyFrameCompositionMode::AlphaBlending => {
|
||||
::image::imageops::overlay(dest, &src, x, y);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
match &mut *anim {
|
||||
ImageDataType::EncodedFile(_) => {
|
||||
anyhow::bail!("Expected decoded image for image id {}", image_id)
|
||||
@ -465,7 +535,7 @@ impl TerminalState {
|
||||
drop(anim_img);
|
||||
*hash = ImageDataType::hash_bytes(data);
|
||||
}
|
||||
None => {
|
||||
Some(2) | None => {
|
||||
// Create a second frame
|
||||
|
||||
let mut new_frame = if base_frame.is_some() {
|
||||
@ -488,7 +558,7 @@ impl TerminalState {
|
||||
let new_frame_hash = ImageDataType::hash_bytes(&new_frame_data);
|
||||
|
||||
let frames = vec![std::mem::take(data), new_frame_data];
|
||||
let durations = vec![Duration::from_millis(40), frame_gap];
|
||||
let durations = vec![Duration::from_millis(0), frame_gap];
|
||||
let hashes = vec![*hash, new_frame_hash];
|
||||
|
||||
*anim = ImageDataType::AnimRgba8 {
|
||||
@ -691,11 +761,6 @@ impl TerminalState {
|
||||
}
|
||||
|
||||
fn coalesce_kitty_accumulation(&mut self, img: KittyImage) -> anyhow::Result<KittyImage> {
|
||||
log::trace!(
|
||||
"coalesce: accumulator={:#?} img:{:#?}",
|
||||
self.kitty_img.accumulator,
|
||||
img
|
||||
);
|
||||
if self.kitty_img.accumulator.is_empty() {
|
||||
Ok(img)
|
||||
} else {
|
||||
@ -765,3 +830,47 @@ impl TerminalState {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn blit<D, S, P>(
|
||||
dest: &mut D,
|
||||
dest_width: u32,
|
||||
dest_height: u32,
|
||||
src: &S,
|
||||
x: u32,
|
||||
y: u32,
|
||||
mode: KittyFrameCompositionMode,
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
D: GenericImage<Pixel = P>,
|
||||
S: GenericImageView<Pixel = P>,
|
||||
{
|
||||
// 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 (src_w, src_h) = src.dimensions();
|
||||
|
||||
let w = src_w.min(dest_width.saturating_sub(x));
|
||||
let h = src_h.min(dest_height.saturating_sub(y));
|
||||
|
||||
let src = src.view(0, 0, w, h);
|
||||
match mode {
|
||||
KittyFrameCompositionMode::Overwrite => {
|
||||
dest.copy_from(&src, x, y).with_context(|| {
|
||||
format!(
|
||||
"copying img with dims {:?} to frame \
|
||||
with dims {}x{} @ offset {:?}x{:?}",
|
||||
src.dimensions(),
|
||||
dest_width,
|
||||
dest_height,
|
||||
x,
|
||||
y
|
||||
)
|
||||
})?;
|
||||
}
|
||||
KittyFrameCompositionMode::AlphaBlending => {
|
||||
::image::imageops::overlay(dest, &src, x, y);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -655,6 +655,101 @@ pub enum KittyFrameCompositionMode {
|
||||
Overwrite,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct KittyImageFrameCompose {
|
||||
/// i=...
|
||||
pub image_id: Option<u32>,
|
||||
/// I=...
|
||||
pub image_number: Option<u32>,
|
||||
|
||||
/// 1-based number of the frame which should be the base
|
||||
/// data for the new frame being created.
|
||||
/// If omitted, use background_pixel to specify color.
|
||||
/// c=...
|
||||
pub target_frame: Option<u32>,
|
||||
|
||||
/// 1-based number of the frame which should be edited.
|
||||
/// If omitted, a new frame is created.
|
||||
/// r=...
|
||||
pub source_frame: Option<u32>,
|
||||
|
||||
/// Left edge in pixels to update
|
||||
/// x=...
|
||||
pub x: Option<u32>,
|
||||
/// Top edge in pixels to update
|
||||
/// y=...
|
||||
pub y: Option<u32>,
|
||||
|
||||
/// Width (in pixels) of the source and destination rectangles.
|
||||
/// By default the full width is used.
|
||||
/// w=...
|
||||
pub w: Option<u32>,
|
||||
|
||||
/// Height (in pixels) of the source and destination rectangles.
|
||||
/// By default the full height is used.
|
||||
/// h=...
|
||||
pub h: Option<u32>,
|
||||
|
||||
/// Left edge in pixels of the source rectangle
|
||||
/// X=...
|
||||
pub src_x: Option<u32>,
|
||||
/// Top edge in pixels of the source rectangle
|
||||
/// Y=...
|
||||
pub src_y: Option<u32>,
|
||||
|
||||
/// Composition mode.
|
||||
/// Default is AlphaBlending
|
||||
/// C=...
|
||||
pub composition_mode: KittyFrameCompositionMode,
|
||||
}
|
||||
|
||||
impl KittyImageFrameCompose {
|
||||
fn from_keys(keys: &BTreeMap<&str, &str>) -> Option<Self> {
|
||||
Some(Self {
|
||||
image_id: geti(keys, "i"),
|
||||
image_number: geti(keys, "I"),
|
||||
x: geti(keys, "x"),
|
||||
y: geti(keys, "y"),
|
||||
src_x: geti(keys, "X"),
|
||||
src_y: geti(keys, "Y"),
|
||||
w: geti(keys, "w"),
|
||||
h: geti(keys, "h"),
|
||||
target_frame: match geti(keys, "c") {
|
||||
None | Some(0) => None,
|
||||
n => n,
|
||||
},
|
||||
source_frame: match geti(keys, "r") {
|
||||
None | Some(0) => None,
|
||||
n => n,
|
||||
},
|
||||
composition_mode: match geti(keys, "C") {
|
||||
None | Some(0) => KittyFrameCompositionMode::AlphaBlending,
|
||||
Some(1) => KittyFrameCompositionMode::Overwrite,
|
||||
_ => return None,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn to_keys(&self, keys: &mut BTreeMap<&'static str, String>) {
|
||||
set(keys, "i", &self.image_id);
|
||||
set(keys, "I", &self.image_number);
|
||||
set(keys, "w", &self.w);
|
||||
set(keys, "h", &self.h);
|
||||
set(keys, "x", &self.x);
|
||||
set(keys, "y", &self.y);
|
||||
set(keys, "X", &self.src_x);
|
||||
set(keys, "Y", &self.src_y);
|
||||
set(keys, "c", &self.target_frame);
|
||||
set(keys, "r", &self.source_frame);
|
||||
match &self.composition_mode {
|
||||
KittyFrameCompositionMode::AlphaBlending => {}
|
||||
KittyFrameCompositionMode::Overwrite => {
|
||||
keys.insert("C", "1".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct KittyImageFrame {
|
||||
/// Left edge in pixels to update
|
||||
@ -719,7 +814,7 @@ impl KittyImageFrame {
|
||||
set(keys, "x", &self.x);
|
||||
set(keys, "y", &self.y);
|
||||
set(keys, "c", &self.base_frame);
|
||||
set(keys, "r", &self.base_frame);
|
||||
set(keys, "r", &self.frame_number);
|
||||
set(keys, "Z", &self.duration_ms);
|
||||
match &self.composition_mode {
|
||||
KittyFrameCompositionMode::AlphaBlending => {}
|
||||
@ -764,6 +859,11 @@ pub enum KittyImage {
|
||||
frame: KittyImageFrame,
|
||||
verbosity: KittyImageVerbosity,
|
||||
},
|
||||
/// a='c'
|
||||
ComposeFrame {
|
||||
frame: KittyImageFrameCompose,
|
||||
verbosity: KittyImageVerbosity,
|
||||
},
|
||||
}
|
||||
|
||||
impl KittyImage {
|
||||
@ -775,6 +875,7 @@ impl KittyImage {
|
||||
Self::Display { verbosity, .. } => *verbosity,
|
||||
Self::Delete { verbosity, .. } => *verbosity,
|
||||
Self::TransmitFrame { verbosity, .. } => *verbosity,
|
||||
Self::ComposeFrame { verbosity, .. } => *verbosity,
|
||||
}
|
||||
}
|
||||
|
||||
@ -824,6 +925,10 @@ impl KittyImage {
|
||||
frame: KittyImageFrame::from_keys(&keys)?,
|
||||
verbosity,
|
||||
}),
|
||||
"c" => Some(Self::ComposeFrame {
|
||||
frame: KittyImageFrameCompose::from_keys(&keys)?,
|
||||
verbosity,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@ -883,6 +988,11 @@ impl KittyImage {
|
||||
frame.to_keys(keys);
|
||||
verbosity.to_keys(keys);
|
||||
}
|
||||
Self::ComposeFrame { frame, verbosity } => {
|
||||
keys.insert("a", "c".to_string());
|
||||
frame.to_keys(keys);
|
||||
verbosity.to_keys(keys);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user