diff --git a/Cargo.lock b/Cargo.lock index c6016959f..26bc2a502 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/term/Cargo.toml b/term/Cargo.toml index df4e79464..d9e841310 100644 --- a/term/Cargo.toml +++ b/term/Cargo.toml @@ -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" diff --git a/term/src/terminalstate/kitty.rs b/term/src/terminalstate/kitty.rs index d20fd8b13..3fc93215f 100644 --- a/term/src/terminalstate/kitty.rs +++ b/term/src/terminalstate/kitty.rs @@ -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 { - 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, &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, 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 { + 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 { diff --git a/termwiz/src/escape/apc.rs b/termwiz/src/escape/apc.rs index f407dd641..7ac4967b2 100644 --- a/termwiz/src/escape/apc.rs +++ b/termwiz/src/escape/apc.rs @@ -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, @@ -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, }