diff options
author | gennyble <gen@nyble.dev> | 2024-01-02 19:12:42 -0600 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-01-02 19:12:42 -0600 |
commit | aafbf4d15989d409f0c2ed2cf13a70434a03d8ba (patch) | |
tree | 20fd7c8c3e4e42e8cba4a7d9db93f0a0ba87ba73 | |
parent | 0a6def135e61cc6ba43e533d4cb537fccb880262 (diff) | |
parent | 939eb07c2962a8dc20eb8963277f3f7359db50fb (diff) | |
download | gifed-aafbf4d15989d409f0c2ed2cf13a70434a03d8ba.tar.gz gifed-aafbf4d15989d409f0c2ed2cf13a70434a03d8ba.zip |
Merge pull request #20 from novedevo/main
VideoGif API
-rw-r--r-- | gifed/Cargo.toml | 9 | ||||
-rw-r--r-- | gifed/examples/streaming_write.rs | 39 | ||||
-rw-r--r-- | gifed/examples/write.rs | 35 | ||||
-rw-r--r-- | gifed/src/block/indexedimage.rs | 2 | ||||
-rw-r--r-- | gifed/src/block/palette.rs | 22 | ||||
-rw-r--r-- | gifed/src/gif.rs | 11 | ||||
-rw-r--r-- | gifed/src/lib.rs | 11 | ||||
-rw-r--r-- | gifed/src/videogif.rs | 143 | ||||
-rw-r--r-- | gifed/src/writer/imagebuilder.rs | 6 |
9 files changed, 181 insertions, 97 deletions
diff --git a/gifed/Cargo.toml b/gifed/Cargo.toml index 542526f..b4d6322 100644 --- a/gifed/Cargo.toml +++ b/gifed/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "gifed" version = "0.3.0" -authors = ["gennyble <gen@nyble.dev>"] -edition = "2018" +authors = ["gennyble <gen@nyble.dev>", "novedevo <devon@nove.dev>"] +edition = "2021" license = "ISC" description = "Gif encoding and decoding with fine control" repository = "https://github.com/genuinebyte/gifed" @@ -11,8 +11,13 @@ repository = "https://github.com/genuinebyte/gifed" bitvec = "1.0.1" weezl = "0.1.5" +color_quant = { version = "1.1.0", optional = true } +rgb = { version = "0.8", optional = true } + [features] weezl-encode = [] +videoish = ["color_quant", "rgb"] +default = [] [dev-dependencies] rand = "0.8.5" diff --git a/gifed/examples/streaming_write.rs b/gifed/examples/streaming_write.rs deleted file mode 100644 index c86f933..0000000 --- a/gifed/examples/streaming_write.rs +++ /dev/null @@ -1,39 +0,0 @@ -use std::fs::File; - -use gifed::{ - block::{LoopCount, Palette}, - writer::{ImageBuilder, Writer}, - Color, EncodeError, -}; - -fn main() -> Result<(), EncodeError> { - let gif_path = match std::env::args().nth(1) { - None => { - eprintln!("Expected a path to output the gif to"); - std::process::exit(-1); - } - Some(path) => path, - }; - - let mut palette = Palette::new(); - - // Fill the palette with every gray - for gray in 0..=255 { - palette.push(Color::new(gray, gray, gray)); - } - - let mut image = vec![0; 128 * 128]; - - // Create a file to write the gif to. We can try here, with the ?, because - // EncodeError has a From<std::io::Error> impl - let file = File::create(gif_path)?; - let mut writer = Writer::new(file, 128, 128, Some(palette))?; - for idx in 0..=255 { - image.fill(idx); - - writer.image(ImageBuilder::new(128, 128).delay(3).build(image.clone())?)?; - } - - writer.repeat(LoopCount::Forever)?; - writer.done() -} diff --git a/gifed/examples/write.rs b/gifed/examples/write.rs deleted file mode 100644 index 39e28d6..0000000 --- a/gifed/examples/write.rs +++ /dev/null @@ -1,35 +0,0 @@ -use gifed::{ - block::{LoopCount, Palette}, - writer::ImageBuilder, - Color, EncodeError, Gif, -}; - -fn main() -> Result<(), EncodeError> { - let gif_path = match std::env::args().nth(1) { - None => { - eprintln!("Expected a path to output the gif to"); - std::process::exit(-1); - } - Some(path) => path, - }; - - let mut palette = Palette::new(); - - // Fill the palette with every gray - for gray in 0..=255 { - palette.push(Color::new(gray, gray, gray)); - } - - let mut image = vec![0; 128 * 128]; - - let mut builder = Gif::builder(128, 128).palette(palette); - for idx in 0..=255 { - image.fill(idx); - - builder = builder.image(ImageBuilder::new(128, 128).delay(3).build(image.clone())?); - } - - builder.repeat(LoopCount::Forever).build()?.save(gif_path)?; - - Ok(()) -} diff --git a/gifed/src/block/indexedimage.rs b/gifed/src/block/indexedimage.rs index 659d0af..eb74aab 100644 --- a/gifed/src/block/indexedimage.rs +++ b/gifed/src/block/indexedimage.rs @@ -47,7 +47,7 @@ impl IndexedImage { let compressed = crate::LZW::new(mcs).encode(&self.indicies); #[cfg(feature = "weezl-encode")] - let compressed = Encoder::new(weezl::BitOrder::Lsb, mcs) + let compressed = weezl::encode::Encoder::new(weezl::BitOrder::Lsb, mcs) .encode(&self.indicies) .unwrap(); diff --git a/gifed/src/block/palette.rs b/gifed/src/block/palette.rs index 3cbcaaa..0dc1686 100644 --- a/gifed/src/block/palette.rs +++ b/gifed/src/block/palette.rs @@ -48,13 +48,11 @@ impl Palette { self.table.get(index as usize).copied() } - pub fn from_color<C: AsRef<Color>>(&self, color: C) -> Option<u8> { - for (i, &c) in self.table.iter().enumerate() { - if c == *color.as_ref() { - return Some(i as u8); - } - } - None + pub fn from_color(&self, color: Color) -> Option<u8> { + self.table + .iter() + .position(|c| *c == color) + .map(|idx| idx as u8) } /// How many padding bytes we need to write. @@ -179,7 +177,7 @@ mod test { vec_tuple_test(vec![(1, 2, 3), (4, 5, 6)], &[1, 2, 3, 4, 5, 6]) } - fn test_n_with_padding(real_count: usize, exected_padding_bytes: usize) { + fn test_n_with_padding(real_count: usize, expected_padding_bytes: usize) { let mut palette = Palette::new(); let mut expected = vec![]; @@ -189,11 +187,7 @@ mod test { expected.extend_from_slice(&[x, x, x]) } - // yes, this is really how I'm doing it. I have... trust issues with - // myself and iter::repeat. i hope you understand - for _ in 0..exected_padding_bytes { - expected.push(0x00); - } + expected.resize(expected.len() + expected_padding_bytes, 0x00); let bytes = palette.as_bytes(); assert_eq!(expected, bytes.as_slice()) @@ -201,7 +195,7 @@ mod test { fn test_n_with_padding_range(real_count_low: u8, real_count_high: u8, next_padstop: usize) { for x in real_count_low..=real_count_high { - test_n_with_padding(x as usize, (next_padstop as usize - x as usize) * 3) + test_n_with_padding(x as usize, (next_padstop - x as usize) * 3) } } diff --git a/gifed/src/gif.rs b/gifed/src/gif.rs index 5b1cfd7..f11694a 100644 --- a/gifed/src/gif.rs +++ b/gifed/src/gif.rs @@ -7,7 +7,6 @@ use crate::{ Block, CompressedImage, IndexedImage, Palette, ScreenDescriptor, Version, }, writer::EncodeBlock, - EncodeError, }; #[derive(Clone, Debug)] @@ -21,6 +20,16 @@ pub struct Gif { } impl Gif { + pub fn new(width: u16, height: u16) -> Self { + Self { + //TODO: gen- select lower when possible + version: Version::Gif89a, + descriptor: ScreenDescriptor::new(width, height), + palette: None, + blocks: vec![], + } + } + pub fn set_width(&mut self, width: u16) { self.descriptor.width = width; } diff --git a/gifed/src/lib.rs b/gifed/src/lib.rs index 438ab79..2ed3b9b 100644 --- a/gifed/src/lib.rs +++ b/gifed/src/lib.rs @@ -1,18 +1,25 @@ -mod color; mod gif; mod lzw; pub mod block; pub mod reader; +#[cfg(feature = "videoish")] +pub mod videogif; pub mod writer; pub use reader::DecodeError; pub use writer::EncodeError; -pub use color::Color; pub use gif::{Gif, Image}; pub use lzw::LZW; +#[cfg(feature = "rgb")] +pub type Color = rgb::RGB8; +#[cfg(not(feature = "rgb"))] +mod color; +#[cfg(not(feature = "rgb"))] +pub use color::Color; + /// Perform the algorithm to get the length of a color table from /// the value of the packed field. The max value here is 256 pub(crate) fn packed_to_color_table_length(packed: u8) -> usize { diff --git a/gifed/src/videogif.rs b/gifed/src/videogif.rs new file mode 100644 index 0000000..d066019 --- /dev/null +++ b/gifed/src/videogif.rs @@ -0,0 +1,143 @@ +use crate::{ + block::{LoopCount, Palette}, + writer::ImageBuilder, + Color, EncodeError, Gif, +}; + +use color_quant::NeuQuant; +use rgb::{ComponentBytes, FromSlice}; + +use std::convert::TryFrom; + +/// A Video-like GIF. +/// +/// All images must have the same dimensions. +pub struct VideoGif { + width: u16, + height: u16, + framerate: Option<u16>, + frames: Vec<Frame>, + looping: LoopCount, +} + +impl VideoGif { + pub fn new(width: u16, height: u16) -> Self { + Self { + width, + height, + framerate: None, + frames: vec![], + looping: LoopCount::Forever, + } + } + + /// Set the approximate frames per second. + /// + /// This struct uses a constant framerate and is only precise to hundreths + /// of a second, so you might not get exactly what you want. + pub fn set_framerate(&mut self, framerate: u16) { + self.framerate = Some(100 / framerate); + } + + /// Set the number of times this gif should loop. Defaults to forever. + /// + /// See [LoopCount] + pub fn set_looping(&mut self, count: LoopCount) { + self.looping = count; + } + + /// Adds a frame to the gif. + /// + /// # Panic + /// Panics if the provided [Frame]'s length is not the same as the gif's + /// width * height. + pub fn add_frame<F: Into<Frame>>(&mut self, frame: F) { + let frame = frame.into(); + let frame_area = frame.image_indices.len(); + let gif_area = self.width as usize * self.height as usize; + + if frame_area != gif_area { + //TODO: gen- Result instead of panic? + panic!( + "frame has a length of {frame_area} but VideoGif expected {gif_area} ({} * {})", + self.width, self.height + ); + } + + self.frames.push(frame) + } + + #[rustfmt::skip] // it was doing things i did not like + pub fn build(self) -> Result<Gif, EncodeError> { + let Self { width, height, framerate, frames, looping } = self; + + let mut gif = Gif::new(width, height); + + gif.push(looping); + + for Frame { image_indices, interval, palette } in frames { + //TODO: return error instead of defaulting to 10? or print warning? + // printing in a library is bad but perhaps so is assuming 10 fps? + let delay = interval.or(framerate).unwrap_or(10); + + gif.push( + ImageBuilder::new(width, height) + .delay(delay) + .palette(palette) + .build(image_indices)?, + ) + } + + Ok(gif) + } +} + +pub struct Frame { + /// indices into the palette + image_indices: Vec<u8>, + /// in hundredths of a second + interval: Option<u16>, + palette: Palette, +} + +impl From<Vec<Color>> for Frame { + fn from(flat: Vec<Color>) -> Self { + flat.as_slice().into() + } +} + +impl From<&[Color]> for Frame { + fn from(flat: &[Color]) -> Self { + let flat_rgba = flat.as_rgba(); + let quant = NeuQuant::new(1, 256, flat_rgba.as_bytes()); + + let mut indices = vec![0; flat.len()]; + for (image_idx, px) in flat.iter().enumerate() { + let color_idx = quant.index_of(&[px.r, px.g, px.b, 255]); + indices[image_idx] = color_idx as u8; + } + + let palette = Palette::try_from(quant.color_map_rgb().as_slice()).unwrap(); + + Self { + image_indices: indices, + interval: None, + palette, + } + } +} + +impl From<(&[Color], u16)> for Frame { + fn from(image_delay: (&[Color], u16)) -> Self { + let (flat, delay) = image_delay; + let mut this: Frame = flat.into(); + this.interval = Some(delay); + this + } +} + +impl Frame { + pub fn set_interval(&mut self, interval_hundredths: u16) { + self.interval = Some(interval_hundredths); + } +} diff --git a/gifed/src/writer/imagebuilder.rs b/gifed/src/writer/imagebuilder.rs index 2163477..2cbc0c2 100644 --- a/gifed/src/writer/imagebuilder.rs +++ b/gifed/src/writer/imagebuilder.rs @@ -44,9 +44,9 @@ impl ImageBuilder { self } - /// Time to wait, in hundreths of a second, before this image is drawn - pub fn delay(mut self, hundreths: u16) -> Self { - self.delay = hundreths; + /// Time to wait, in hundredths of a second, before this image is drawn + pub fn delay(mut self, hundredths: u16) -> Self { + self.delay = hundredths; self } |