mirror of
https://github.com/mfontanini/presenterm.git
synced 2026-09-01 23:33:21 -06:00
feat: add APNG animation support with loop count
This commit is contained in:
parent
3326cf7be6
commit
e67ac9c6ab
20
Cargo.lock
generated
20
Cargo.lock
generated
@ -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",
|
||||
]
|
||||
|
||||
@ -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<B> {
|
||||
Image(B),
|
||||
Gif(Vec<GifFrame<B>>),
|
||||
Animated { frames: Vec<AnimationFrame<B>>, loop_count: LoopCount },
|
||||
}
|
||||
|
||||
type RawResource = GenericResource<RgbaImage>;
|
||||
@ -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<T> {
|
||||
struct AnimationFrame<T> {
|
||||
delay: Delay,
|
||||
buffer: T,
|
||||
}
|
||||
@ -143,7 +146,7 @@ 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 {
|
||||
@ -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<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),
|
||||
}
|
||||
}
|
||||
|
||||
@ -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),
|
||||
@ -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(())
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user