Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

68 changes: 49 additions & 19 deletions src/terminal/image/protocols/kitty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -43,7 +46,7 @@ const DIACRITICS: &[u32] = &[

enum GenericResource<B> {
Image(B),
Gif(Vec<GifFrame<B>>),
Animated { frames: Vec<AnimationFrame<B>>, loop_count: LoopCount },
}

type RawResource = GenericResource<RgbaImage>;
Expand All @@ -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 }
}
}
Expand All @@ -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) => {
Expand Down Expand Up @@ -111,7 +114,7 @@ impl Drop for KittyBuffer {
}
}

struct GifFrame<T> {
struct AnimationFrame<T> {
delay: Delay,
buffer: T,
}
Expand Down Expand Up @@ -143,24 +146,24 @@ impl KittyPrinter {
Ok(resource)
}

fn persist_gif(&self, frames: Vec<GifFrame<RgbaImage>>) -> io::Result<KittyImage> {
fn persist_animation(&self, frames: Vec<AnimationFrame<RgbaImage>>, loop_count: LoopCount) -> io::Result<KittyImage> {
let mut persisted_frames = Vec::new();
let mut dimensions = (0, 0);
for frame in frames {
let path = self.allocate_tempfile();
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<KittyImage> {
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),
}
}

Expand Down Expand Up @@ -205,20 +208,25 @@ impl KittyPrinter {
Ok(())
}

fn print_gif<T>(
fn print_animation<T>(
&self,
dimensions: (u32, u32),
frames: &[GifFrame<KittyBuffer>],
frames: &[AnimationFrame<KittyBuffer>],
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),
Expand Down Expand Up @@ -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() })?;
Expand All @@ -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)?;
}
Expand All @@ -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();
Expand Down Expand Up @@ -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 {
Expand All @@ -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(())
}
Expand Down