diff --git a/Cargo.lock b/Cargo.lock index 33319347..20b65d2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -492,9 +492,9 @@ dependencies = [ [[package]] name = "gif" -version = "0.13.3" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" dependencies = [ "color_quant", "weezl", @@ -536,9 +536,9 @@ dependencies = [ [[package]] name = "image" -version = "0.25.8" +version = "0.25.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" dependencies = [ "bytemuck", "byteorder-lite", @@ -715,9 +715,9 @@ dependencies = [ [[package]] name = "moxcms" -version = "0.7.9" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fbdd3d7436f8b5e892b8b7ea114271ff0fa00bc5acae845d53b07d498616ef6" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" dependencies = [ "num-traits", "pxfm", @@ -1966,15 +1966,15 @@ dependencies = [ [[package]] name = "zune-core" -version = "0.4.12" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" [[package]] name = "zune-jpeg" -version = "0.4.21" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" dependencies = [ "zune-core", ] diff --git a/src/terminal/image/protocols/kitty.rs b/src/terminal/image/protocols/kitty.rs index bf092361..5dbb03b2 100644 --- a/src/terminal/image/protocols/kitty.rs +++ b/src/terminal/image/protocols/kitty.rs @@ -6,7 +6,10 @@ use crate::{ }, }; use base64::{Engine, engine::general_purpose::STANDARD}; -use image::{AnimationDecoder, Delay, EncodableLayout, ImageReader, RgbaImage, codecs::gif::GifDecoder}; +use image::{ + AnimationDecoder, Delay, EncodableLayout, ImageReader, RgbaImage, codecs::gif::GifDecoder, codecs::png::PngDecoder, + metadata::LoopCount, +}; use std::{ fmt, fs::{self, File}, @@ -43,7 +46,7 @@ const DIACRITICS: &[u32] = &[ enum GenericResource { Image(B), - Gif(Vec>), + Animated { frames: Vec>, loop_count: LoopCount }, } type RawResource = GenericResource; @@ -55,13 +58,13 @@ impl RawResource { dimensions: image.dimensions(), resource: GenericResource::Image(KittyBuffer::Memory(image.into_raw())), }, - Self::Gif(frames) => { + Self::Animated { frames, loop_count } => { let dimensions = frames[0].buffer.dimensions(); let frames = frames .into_iter() - .map(|frame| GifFrame { delay: frame.delay, buffer: KittyBuffer::Memory(frame.buffer.into_raw()) }) + .map(|frame| AnimationFrame { delay: frame.delay, buffer: KittyBuffer::Memory(frame.buffer.into_raw()) }) .collect(); - let resource = GenericResource::Gif(frames); + let resource = GenericResource::Animated { frames, loop_count }; KittyImage { dimensions, resource } } } @@ -77,7 +80,7 @@ impl KittyImage { pub(crate) fn as_rgba8(&self) -> RgbaImage { let first_frame = match &self.resource { GenericResource::Image(buffer) => buffer, - GenericResource::Gif(gif_frames) => &gif_frames[0].buffer, + GenericResource::Animated { frames: animation_frames, .. } => &animation_frames[0].buffer, }; let buffer = match first_frame { KittyBuffer::Filesystem(path) => { @@ -111,7 +114,7 @@ impl Drop for KittyBuffer { } } -struct GifFrame { +struct AnimationFrame { delay: Delay, buffer: T, } @@ -143,7 +146,7 @@ impl KittyPrinter { Ok(resource) } - fn persist_gif(&self, frames: Vec>) -> io::Result { + fn persist_animation(&self, frames: Vec>, loop_count: LoopCount) -> io::Result { let mut persisted_frames = Vec::new(); let mut dimensions = (0, 0); for frame in frames { @@ -151,16 +154,16 @@ impl KittyPrinter { fs::write(&path, frame.buffer.as_bytes())?; dimensions = frame.buffer.dimensions(); - let frame = GifFrame { delay: frame.delay, buffer: KittyBuffer::Filesystem(path) }; + let frame = AnimationFrame { delay: frame.delay, buffer: KittyBuffer::Filesystem(path) }; persisted_frames.push(frame); } - Ok(KittyImage { dimensions, resource: GenericResource::Gif(persisted_frames) }) + Ok(KittyImage { dimensions, resource: GenericResource::Animated { frames: persisted_frames, loop_count } }) } fn persist_resource(&self, resource: RawResource) -> io::Result { match resource { RawResource::Image(image) => self.persist_image(image), - RawResource::Gif(frames) => self.persist_gif(frames), + RawResource::Animated { frames, loop_count } => self.persist_animation(frames, loop_count), } } @@ -205,20 +208,25 @@ impl KittyPrinter { Ok(()) } - fn print_gif( + fn print_animation( &self, dimensions: (u32, u32), - frames: &[GifFrame], + frames: &[AnimationFrame], + loop_count: LoopCount, terminal: &mut T, print_options: &PrintOptions, ) -> Result<(), PrintImageError> where T: TerminalIo, { + let loops = match loop_count { + LoopCount::Infinite => 1u32, + LoopCount::Finite(n) => n.get() + 1, + }; let image_id = Self::generate_image_id(); + for (frame_id, frame) in frames.iter().enumerate() { let (num, denom) = frame.delay.numer_denom_ms(); - // default to 100ms in case somehow the denominator is 0 let delay = num.checked_div(denom).unwrap_or(100); let mut options = vec![ ControlOption::Format(ImageFormat::Rgba), @@ -252,7 +260,7 @@ impl KittyPrinter { ControlOption::Action(Action::Animate), ControlOption::ImageId(image_id), ControlOption::FrameId(1), - ControlOption::Loops(1), + ControlOption::Loops(loops), ]; let command = self.make_command(options, "").to_string(); terminal.execute(&TerminalCommand::PrintText { content: &command, style: Default::default() })?; @@ -267,6 +275,7 @@ impl KittyPrinter { terminal.execute(&TerminalCommand::PrintText { content: &command, style: Default::default() })?; } } + if self.tmux { self.print_unicode_placeholders(terminal, print_options, image_id)?; } @@ -275,7 +284,7 @@ impl KittyPrinter { ControlOption::ImageId(image_id), ControlOption::FrameId(1), ControlOption::AnimationState(3), - ControlOption::Loops(1), + ControlOption::Loops(loops), ControlOption::Quiet(2), ]; let command = self.make_command(options, "").to_string(); @@ -380,16 +389,35 @@ impl KittyPrinter { let mut frames = Vec::new(); for frame in decoder.into_frames() { let frame = frame?; - let frame = GifFrame { delay: frame.delay(), buffer: frame.into_buffer() }; + let frame = AnimationFrame { delay: frame.delay(), buffer: frame.into_buffer() }; frames.push(frame); } - Ok(RawResource::Gif(frames)) + Ok(RawResource::Animated { frames, loop_count: LoopCount::Infinite }) } else { + let reader = BufReader::new(file); + let png_decoder = PngDecoder::new(reader); + if let Ok(decoder) = png_decoder { + if decoder.is_apng().unwrap_or(false) { + let apng_decoder = decoder.apng()?; + let loop_count = apng_decoder.loop_count(); + let mut frames = Vec::new(); + for frame in apng_decoder.into_frames() { + let frame = frame?; + let frame = AnimationFrame { delay: frame.delay(), buffer: frame.into_buffer() }; + frames.push(frame); + } + if !frames.is_empty() { + return Ok(RawResource::Animated { frames, loop_count }); + } + } + } + let file = File::open(path)?; let reader = ImageReader::new(BufReader::new(file)).with_guessed_format()?; let image = reader.decode()?; Ok(RawResource::Image(image.into_rgba8())) } } + } impl PrintImage for KittyPrinter { @@ -413,7 +441,9 @@ impl PrintImage for KittyPrinter { { match &image.resource { GenericResource::Image(resource) => self.print_image(image.dimensions, resource, terminal, options)?, - GenericResource::Gif(frames) => self.print_gif(image.dimensions, frames, terminal, options)?, + GenericResource::Animated { frames, loop_count } => { + self.print_animation(image.dimensions, frames, *loop_count, terminal, options)? + } }; Ok(()) }