1
1
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:
Wez Furlong 2021-08-03 21:57:27 -07:00
parent 0c5753eb58
commit 02b5defac1
2 changed files with 273 additions and 54 deletions

View File

@ -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(())
}

View File

@ -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);
}
}
}
}