From d5385e8bee605c51273f84a3983aa5f55e24f777 Mon Sep 17 00:00:00 2001 From: Devon Sawatsky Date: Sat, 23 Dec 2023 23:26:01 -0800 Subject: add experimental gif builder api --- gifed/Cargo.toml | 1 + gifed/src/gif_builder.rs | 131 +++++++++++++++++++++++++++++++++++++++++++++++ gifed/src/lib.rs | 1 + 3 files changed, 133 insertions(+) create mode 100644 gifed/src/gif_builder.rs diff --git a/gifed/Cargo.toml b/gifed/Cargo.toml index 542526f..528f670 100644 --- a/gifed/Cargo.toml +++ b/gifed/Cargo.toml @@ -9,6 +9,7 @@ repository = "https://github.com/genuinebyte/gifed" [dependencies] bitvec = "1.0.1" +colorsquash = { git = "https://github.com/gennyble/colorsquash", version = "0.1.0", optional = true } weezl = "0.1.5" [features] diff --git a/gifed/src/gif_builder.rs b/gifed/src/gif_builder.rs new file mode 100644 index 0000000..e701f6e --- /dev/null +++ b/gifed/src/gif_builder.rs @@ -0,0 +1,131 @@ +use std::convert::TryFrom; + +#[cfg(feature = "colorsquash")] +use colorsquash::{Squasher, SquasherBuilder}; + +use crate::{ + block::{packed::ScreenPacked, Palette, ScreenDescriptor, Version}, + writer::ImageBuilder, + Color, Gif, +}; + +pub struct GifBuilder { + width: u16, + height: u16, + framerate: Option, + frames: Vec, +} + +impl GifBuilder { + pub fn set_resolution(&mut self, width: u16, height: u16) { + self.width = width; + self.height = height; + } + pub fn set_framerate(&mut self, framerate: u16) { + self.framerate = Some(framerate) + } + pub fn add_frame(&mut self, frame: Frame) { + self.frames.push(frame) + } + pub fn build(self) -> Gif { + let Self { + width, + height, + framerate, + frames, + } = self; + + let descriptor = ScreenDescriptor::new(width, height); + let mut gif = Gif { + version: Version::Gif89a, + descriptor, + palette: None, + blocks: vec![], + }; + + let images = frames.into_iter().map(|frame| { + let Frame { + interval, + image, + palette, + } = frame; + + let palette = palette.unwrap(); + let delay = interval + .map(|interval| interval * 10) + .or(framerate.map(|fr| 100 / fr)) + .unwrap_or(10); + let image_bytes = image + .into_iter() + .flat_map(|row| { + row.into_iter().map(|c| { + palette + .from_color(c) + .expect("palette should be able to convert any color") + }) + }) + .collect::>(); + let ib = ImageBuilder::new(width, height) + .delay(delay) + .build(image_bytes) + .expect("image building should succeed"); + ib.image.compress(None).expect("compression should succeed") + }); + + for compressed_image in images { + gif.push(compressed_image); + } + + gif + } +} + +impl Default for GifBuilder { + fn default() -> Self { + Self { + width: 256, + height: 256, + framerate: Some(15), + frames: vec![], + } + } +} + +pub struct Frame { + image: Vec>, //row-major + interval: Option, + ///in hundredths of a second + palette: Option, +} + +impl From>> for Frame { + fn from(image: Vec>) -> Self { + Self { + image, + interval: None, + palette: None, + } + } +} + +impl Frame { + #[cfg(feature = "colorsquash")] + pub fn optimize_palette(&mut self) { + let image_bytes = self + .image + .iter() + .flat_map(|row| row.iter().flat_map(|color| [color.r, color.g, color.b])) + .collect::>(); + let squasher = SquasherBuilder::default() + .max_colors(255u8) + .build(image_bytes.as_slice()); + let pal = Palette::try_from(squasher.palette_bytes().as_slice()).unwrap(); + self.set_palette(pal) + } + pub fn set_palette(&mut self, palette: Palette) { + self.palette = Some(palette) + } + pub fn set_interval(&mut self, interval_hundredths: u16) { + self.interval = Some(interval_hundredths); + } +} diff --git a/gifed/src/lib.rs b/gifed/src/lib.rs index 438ab79..cc1e69f 100644 --- a/gifed/src/lib.rs +++ b/gifed/src/lib.rs @@ -3,6 +3,7 @@ mod gif; mod lzw; pub mod block; +pub mod gif_builder; pub mod reader; pub mod writer; -- cgit 1.4.1-3-g733a5 From 2230e8d37c16e864821db23636f2bad5c14d8d3c Mon Sep 17 00:00:00 2001 From: Devon Sawatsky Date: Sun, 24 Dec 2023 00:26:44 -0800 Subject: add optional rgb feature for improved interoperability with other crates --- gifed/Cargo.toml | 7 ++++++- gifed/src/block/palette.rs | 6 +++--- gifed/src/gif_builder.rs | 14 +++++++++----- gifed/src/lib.rs | 9 +++++++-- 4 files changed, 25 insertions(+), 11 deletions(-) diff --git a/gifed/Cargo.toml b/gifed/Cargo.toml index 528f670..9c9743c 100644 --- a/gifed/Cargo.toml +++ b/gifed/Cargo.toml @@ -9,11 +9,16 @@ repository = "https://github.com/genuinebyte/gifed" [dependencies] bitvec = "1.0.1" -colorsquash = { git = "https://github.com/gennyble/colorsquash", version = "0.1.0", optional = true } +colorsquash = { git = "https://github.com/novedevo/colorsquash", version = "0.2.0", optional = true } +rgb = {version="0.8.37", optional=true} weezl = "0.1.5" [features] weezl-encode = [] +default = [ + "colorsquash", + "rgb" +] [dev-dependencies] rand = "0.8.5" diff --git a/gifed/src/block/palette.rs b/gifed/src/block/palette.rs index 3cbcaaa..1c660df 100644 --- a/gifed/src/block/palette.rs +++ b/gifed/src/block/palette.rs @@ -48,9 +48,9 @@ impl Palette { self.table.get(index as usize).copied() } - pub fn from_color>(&self, color: C) -> Option { + pub fn from_color(&self, color: Color) -> Option { for (i, &c) in self.table.iter().enumerate() { - if c == *color.as_ref() { + if c == color { return Some(i as u8); } } @@ -201,7 +201,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_builder.rs b/gifed/src/gif_builder.rs index e701f6e..6951961 100644 --- a/gifed/src/gif_builder.rs +++ b/gifed/src/gif_builder.rs @@ -1,10 +1,10 @@ use std::convert::TryFrom; #[cfg(feature = "colorsquash")] -use colorsquash::{Squasher, SquasherBuilder}; +use colorsquash::Squasher; use crate::{ - block::{packed::ScreenPacked, Palette, ScreenDescriptor, Version}, + block::{Palette, ScreenDescriptor, Version}, writer::ImageBuilder, Color, Gif, }; @@ -111,14 +111,18 @@ impl From>> for Frame { impl Frame { #[cfg(feature = "colorsquash")] pub fn optimize_palette(&mut self) { + #[cfg(feature = "rgb")] + let image_bytes = self.image.clone().into_iter().flatten().collect::>(); + #[cfg(not(feature = "rgb"))] let image_bytes = self .image .iter() .flat_map(|row| row.iter().flat_map(|color| [color.r, color.g, color.b])) .collect::>(); - let squasher = SquasherBuilder::default() - .max_colors(255u8) - .build(image_bytes.as_slice()); + #[cfg(feature = "rgb")] + let squasher = Squasher::new(255u8, image_bytes.as_slice()); + #[cfg(not(feature = "rgb"))] + let squasher = Squasher::new_raw(255u8, image_bytes.as_slice()); let pal = Palette::try_from(squasher.palette_bytes().as_slice()).unwrap(); self.set_palette(pal) } diff --git a/gifed/src/lib.rs b/gifed/src/lib.rs index cc1e69f..9e4458f 100644 --- a/gifed/src/lib.rs +++ b/gifed/src/lib.rs @@ -1,4 +1,3 @@ -mod color; mod gif; mod lzw; @@ -10,10 +9,16 @@ 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 { -- cgit 1.4.1-3-g733a5 From 2a39c5e4360303d9565d51d30111ce028a1c960e Mon Sep 17 00:00:00 2001 From: Devon Sawatsky Date: Sun, 24 Dec 2023 22:40:43 -0800 Subject: change things that didn't really need to be changed --- gifed/src/block/palette.rs | 8 ++------ gifed/src/gif.rs | 1 - gifed/src/writer/imagebuilder.rs | 6 +++--- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/gifed/src/block/palette.rs b/gifed/src/block/palette.rs index 1c660df..b1414b4 100644 --- a/gifed/src/block/palette.rs +++ b/gifed/src/block/palette.rs @@ -179,7 +179,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 +189,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()) diff --git a/gifed/src/gif.rs b/gifed/src/gif.rs index 5b1cfd7..87a310f 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)] 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 } -- cgit 1.4.1-3-g733a5 From eed1868c122f3953699ee0b820e2dcaa9db842b3 Mon Sep 17 00:00:00 2001 From: Devon Sawatsky Date: Sun, 31 Dec 2023 15:19:45 -0800 Subject: silly rewrite of from_color to make it less readable and less lines --- gifed/src/block/palette.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/gifed/src/block/palette.rs b/gifed/src/block/palette.rs index b1414b4..f49442f 100644 --- a/gifed/src/block/palette.rs +++ b/gifed/src/block/palette.rs @@ -49,12 +49,11 @@ impl Palette { } pub fn from_color(&self, color: Color) -> Option { - for (i, &c) in self.table.iter().enumerate() { - if c == color { - return Some(i as u8); - } - } - None + self.table + .iter() + .enumerate() + .find(|(i, c)| **c == color) + .map(|(i, c)| i as u8) } /// How many padding bytes we need to write. -- cgit 1.4.1-3-g733a5 From e8acede7abe3f4bc7292053a5f6f144166a28afe Mon Sep 17 00:00:00 2001 From: Devon Sawatsky Date: Sun, 31 Dec 2023 15:20:29 -0800 Subject: fix bugs caused by sweeping comprehension errors --- gifed/src/gif_builder.rs | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/gifed/src/gif_builder.rs b/gifed/src/gif_builder.rs index 6951961..b376bf7 100644 --- a/gifed/src/gif_builder.rs +++ b/gifed/src/gif_builder.rs @@ -6,13 +6,14 @@ use colorsquash::Squasher; use crate::{ block::{Palette, ScreenDescriptor, Version}, writer::ImageBuilder, - Color, Gif, + Color, EncodeError, Gif, }; pub struct GifBuilder { width: u16, height: u16, framerate: Option, + global_palette: Option, frames: Vec, } @@ -27,19 +28,23 @@ impl GifBuilder { pub fn add_frame(&mut self, frame: Frame) { self.frames.push(frame) } - pub fn build(self) -> Gif { + pub fn add_global_palette(&mut self, palette: Palette) { + self.global_palette = Some(palette) + } + pub fn build(self) -> Result { let Self { width, height, framerate, frames, + global_palette, } = self; let descriptor = ScreenDescriptor::new(width, height); let mut gif = Gif { version: Version::Gif89a, descriptor, - palette: None, + palette: global_palette, blocks: vec![], }; @@ -50,33 +55,37 @@ impl GifBuilder { palette, } = frame; - let palette = palette.unwrap(); let delay = interval .map(|interval| interval * 10) .or(framerate.map(|fr| 100 / fr)) .unwrap_or(10); - let image_bytes = image + let image_indicies = image .into_iter() .flat_map(|row| { row.into_iter().map(|c| { + //if there is a palette for this frame, use that to encode the palette - .from_color(c) - .expect("palette should be able to convert any color") + .or(global_palette) + .map(|p| p.from_color(c)) //TODO: this is wrong. don't do this + .flatten() }) }) .collect::>(); - let ib = ImageBuilder::new(width, height) - .delay(delay) - .build(image_bytes) - .expect("image building should succeed"); - ib.image.compress(None).expect("compression should succeed") + let mut ib = ImageBuilder::new(width, height).delay(delay); + if let Some(p) = palette { + ib = ib.palette(p); + } + ib.build(image_indicies)?.image.compress(None) }); for compressed_image in images { - gif.push(compressed_image); + match compressed_image { + Ok(img) => gif.push(img), + Err(e) => return Err(e), + } } - gif + Ok(gif) } } -- cgit 1.4.1-3-g733a5 From 234eeebb8b51c32e6b7ba5aa19a64bee00ba52e8 Mon Sep 17 00:00:00 2001 From: Devon Sawatsky Date: Sun, 31 Dec 2023 16:11:13 -0800 Subject: fix clippy warning in palette --- gifed/src/block/palette.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gifed/src/block/palette.rs b/gifed/src/block/palette.rs index f49442f..436e698 100644 --- a/gifed/src/block/palette.rs +++ b/gifed/src/block/palette.rs @@ -52,8 +52,8 @@ impl Palette { self.table .iter() .enumerate() - .find(|(i, c)| **c == color) - .map(|(i, c)| i as u8) + .find(|(_, c)| **c == color) + .map(|(i, _)| i as u8) } /// How many padding bytes we need to write. -- cgit 1.4.1-3-g733a5 From 85f008479a1994486acd96b27fb5502839e6e893 Mon Sep 17 00:00:00 2001 From: Devon Sawatsky Date: Sun, 31 Dec 2023 16:11:35 -0800 Subject: require colorsquash and rgb features for the builder API --- gifed/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/gifed/src/lib.rs b/gifed/src/lib.rs index 9e4458f..508ef5c 100644 --- a/gifed/src/lib.rs +++ b/gifed/src/lib.rs @@ -2,6 +2,7 @@ mod gif; mod lzw; pub mod block; +#[cfg(all(feature = "colorsquash", feature = "rgb"))] pub mod gif_builder; pub mod reader; pub mod writer; -- cgit 1.4.1-3-g733a5 From ac3d9ffd2a913da2f578cc0dd908962ce23246bc Mon Sep 17 00:00:00 2001 From: Devon Sawatsky Date: Sun, 31 Dec 2023 16:13:34 -0800 Subject: rewrite the gif builder API to actually work --- gifed/src/gif_builder.rs | 82 ++++++++++++++++++------------------------------ 1 file changed, 31 insertions(+), 51 deletions(-) diff --git a/gifed/src/gif_builder.rs b/gifed/src/gif_builder.rs index b376bf7..323e63c 100644 --- a/gifed/src/gif_builder.rs +++ b/gifed/src/gif_builder.rs @@ -1,14 +1,14 @@ -use std::convert::TryFrom; - -#[cfg(feature = "colorsquash")] -use colorsquash::Squasher; - use crate::{ block::{Palette, ScreenDescriptor, Version}, writer::ImageBuilder, - Color, EncodeError, Gif, + EncodeError, Gif, }; +use colorsquash::Squasher; +use rgb::{ComponentBytes, RGB8}; + +use std::convert::TryFrom; + pub struct GifBuilder { width: u16, height: u16, @@ -51,7 +51,7 @@ impl GifBuilder { let images = frames.into_iter().map(|frame| { let Frame { interval, - image, + image_indices, palette, } = frame; @@ -59,23 +59,12 @@ impl GifBuilder { .map(|interval| interval * 10) .or(framerate.map(|fr| 100 / fr)) .unwrap_or(10); - let image_indicies = image - .into_iter() - .flat_map(|row| { - row.into_iter().map(|c| { - //if there is a palette for this frame, use that to encode the - palette - .or(global_palette) - .map(|p| p.from_color(c)) //TODO: this is wrong. don't do this - .flatten() - }) - }) - .collect::>(); - let mut ib = ImageBuilder::new(width, height).delay(delay); - if let Some(p) = palette { - ib = ib.palette(p); - } - ib.build(image_indicies)?.image.compress(None) + ImageBuilder::new(width, height) + .delay(delay) + .palette(palette) + .build(image_indices)? + .image + .compress(None) }); for compressed_image in images { @@ -96,48 +85,39 @@ impl Default for GifBuilder { height: 256, framerate: Some(15), frames: vec![], + global_palette: None, } } } pub struct Frame { - image: Vec>, //row-major - interval: Option, + ///indices into the palette + image_indices: Vec, ///in hundredths of a second - palette: Option, + interval: Option, + palette: Palette, } -impl From>> for Frame { - fn from(image: Vec>) -> Self { +impl From>> for Frame { + /// image: row-major ordering + fn from(image: Vec>) -> Self { + let flat = image.concat(); + + let squasher = Squasher::new(255u8, flat.as_slice()); + + let mut image_indices = vec![0; flat.len()]; + squasher.map_unsafe(flat.as_bytes(), &mut image_indices); + let palette = Palette::try_from(squasher.palette_bytes().as_slice()).unwrap(); Self { - image, + image_indices, interval: None, - palette: None, + palette, } } } impl Frame { - #[cfg(feature = "colorsquash")] - pub fn optimize_palette(&mut self) { - #[cfg(feature = "rgb")] - let image_bytes = self.image.clone().into_iter().flatten().collect::>(); - #[cfg(not(feature = "rgb"))] - let image_bytes = self - .image - .iter() - .flat_map(|row| row.iter().flat_map(|color| [color.r, color.g, color.b])) - .collect::>(); - #[cfg(feature = "rgb")] - let squasher = Squasher::new(255u8, image_bytes.as_slice()); - #[cfg(not(feature = "rgb"))] - let squasher = Squasher::new_raw(255u8, image_bytes.as_slice()); - let pal = Palette::try_from(squasher.palette_bytes().as_slice()).unwrap(); - self.set_palette(pal) - } - pub fn set_palette(&mut self, palette: Palette) { - self.palette = Some(palette) - } + /// pub fn set_interval(&mut self, interval_hundredths: u16) { self.interval = Some(interval_hundredths); } -- cgit 1.4.1-3-g733a5 From 76b719174d97dca088b1d647ce62c0e7bc126cd4 Mon Sep 17 00:00:00 2001 From: Devon Sawatsky Date: Sun, 31 Dec 2023 17:23:52 -0800 Subject: fix a bug that should never have gotten past rust-analyzer --- gifed/Cargo.toml | 6 +++--- gifed/src/gif_builder.rs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gifed/Cargo.toml b/gifed/Cargo.toml index 9c9743c..ae9f5a8 100644 --- a/gifed/Cargo.toml +++ b/gifed/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "gifed" version = "0.3.0" -authors = ["gennyble "] -edition = "2018" +authors = ["gennyble ", "novedevo "] +edition = "2021" license = "ISC" description = "Gif encoding and decoding with fine control" repository = "https://github.com/genuinebyte/gifed" @@ -10,7 +10,7 @@ repository = "https://github.com/genuinebyte/gifed" [dependencies] bitvec = "1.0.1" colorsquash = { git = "https://github.com/novedevo/colorsquash", version = "0.2.0", optional = true } -rgb = {version="0.8.37", optional=true} +rgb = {version="0.8", optional = true} weezl = "0.1.5" [features] diff --git a/gifed/src/gif_builder.rs b/gifed/src/gif_builder.rs index 323e63c..a6f9f95 100644 --- a/gifed/src/gif_builder.rs +++ b/gifed/src/gif_builder.rs @@ -5,7 +5,7 @@ use crate::{ }; use colorsquash::Squasher; -use rgb::{ComponentBytes, RGB8}; +use rgb::RGB8; use std::convert::TryFrom; @@ -106,7 +106,7 @@ impl From>> for Frame { let squasher = Squasher::new(255u8, flat.as_slice()); let mut image_indices = vec![0; flat.len()]; - squasher.map_unsafe(flat.as_bytes(), &mut image_indices); + squasher.map_unsafe(flat.as_slice(), &mut image_indices); let palette = Palette::try_from(squasher.palette_bytes().as_slice()).unwrap(); Self { image_indices, -- cgit 1.4.1-3-g733a5 From 789edc5d7c4781d5a9273b61d90762f469c4e171 Mon Sep 17 00:00:00 2001 From: gennyble Date: Tue, 2 Jan 2024 17:22:47 -0600 Subject: swith quantization methods --- gifed/Cargo.toml | 9 +++------ gifed/src/block/indexedimage.rs | 2 +- gifed/src/gif_builder.rs | 26 +++++++++++++++----------- gifed/src/lib.rs | 2 +- 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/gifed/Cargo.toml b/gifed/Cargo.toml index ae9f5a8..ce49947 100644 --- a/gifed/Cargo.toml +++ b/gifed/Cargo.toml @@ -9,16 +9,13 @@ repository = "https://github.com/genuinebyte/gifed" [dependencies] bitvec = "1.0.1" -colorsquash = { git = "https://github.com/novedevo/colorsquash", version = "0.2.0", optional = true } -rgb = {version="0.8", optional = true} +color_quant = "1.1.0" +rgb = { version = "0.8", optional = true } weezl = "0.1.5" [features] weezl-encode = [] -default = [ - "colorsquash", - "rgb" -] +default = ["rgb"] [dev-dependencies] rand = "0.8.5" 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/gif_builder.rs b/gifed/src/gif_builder.rs index a6f9f95..9d039bf 100644 --- a/gifed/src/gif_builder.rs +++ b/gifed/src/gif_builder.rs @@ -4,8 +4,8 @@ use crate::{ EncodeError, Gif, }; -use colorsquash::Squasher; -use rgb::RGB8; +use color_quant::NeuQuant; +use rgb::{ComponentBytes, FromSlice, RGB8}; use std::convert::TryFrom; @@ -56,15 +56,13 @@ impl GifBuilder { } = frame; let delay = interval - .map(|interval| interval * 10) + .map(|interval| interval) .or(framerate.map(|fr| 100 / fr)) .unwrap_or(10); ImageBuilder::new(width, height) .delay(delay) .palette(palette) - .build(image_indices)? - .image - .compress(None) + .build(image_indices) }); for compressed_image in images { @@ -103,13 +101,19 @@ impl From>> for Frame { fn from(image: Vec>) -> Self { let flat = image.concat(); - let squasher = Squasher::new(255u8, flat.as_slice()); + let flat_rgba = flat.as_rgba(); + let quant = NeuQuant::new(1, 256, &flat_rgba.as_bytes()); + + let mut indicies = 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]); + indicies[image_idx] = color_idx as u8; + } + + let palette = Palette::try_from(quant.color_map_rgb().as_slice()).unwrap(); - let mut image_indices = vec![0; flat.len()]; - squasher.map_unsafe(flat.as_slice(), &mut image_indices); - let palette = Palette::try_from(squasher.palette_bytes().as_slice()).unwrap(); Self { - image_indices, + image_indices: indicies, interval: None, palette, } diff --git a/gifed/src/lib.rs b/gifed/src/lib.rs index 508ef5c..3990e33 100644 --- a/gifed/src/lib.rs +++ b/gifed/src/lib.rs @@ -2,7 +2,7 @@ mod gif; mod lzw; pub mod block; -#[cfg(all(feature = "colorsquash", feature = "rgb"))] +#[cfg(all(feature = "rgb"))] pub mod gif_builder; pub mod reader; pub mod writer; -- cgit 1.4.1-3-g733a5 From 46e170f9c8fe432469b91f64cde2ce5f64d49091 Mon Sep 17 00:00:00 2001 From: gennyble Date: Tue, 2 Jan 2024 18:04:25 -0600 Subject: GifBuilder is now VideoGif and is behind videoish feature --- gifed/Cargo.toml | 8 +-- gifed/src/gif.rs | 10 ++++ gifed/src/gif_builder.rs | 124 +++++++++++++++++++---------------------------- gifed/src/lib.rs | 2 +- 4 files changed, 65 insertions(+), 79 deletions(-) diff --git a/gifed/Cargo.toml b/gifed/Cargo.toml index ce49947..b4d6322 100644 --- a/gifed/Cargo.toml +++ b/gifed/Cargo.toml @@ -9,13 +9,15 @@ repository = "https://github.com/genuinebyte/gifed" [dependencies] bitvec = "1.0.1" -color_quant = "1.1.0" -rgb = { version = "0.8", optional = true } weezl = "0.1.5" +color_quant = { version = "1.1.0", optional = true } +rgb = { version = "0.8", optional = true } + [features] weezl-encode = [] -default = ["rgb"] +videoish = ["color_quant", "rgb"] +default = [] [dev-dependencies] rand = "0.8.5" diff --git a/gifed/src/gif.rs b/gifed/src/gif.rs index 87a310f..f11694a 100644 --- a/gifed/src/gif.rs +++ b/gifed/src/gif.rs @@ -20,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/gif_builder.rs b/gifed/src/gif_builder.rs index 9d039bf..53b1929 100644 --- a/gifed/src/gif_builder.rs +++ b/gifed/src/gif_builder.rs @@ -1,90 +1,59 @@ -use crate::{ - block::{Palette, ScreenDescriptor, Version}, - writer::ImageBuilder, - EncodeError, Gif, -}; +use crate::{block::Palette, writer::ImageBuilder, Color, EncodeError, Gif}; use color_quant::NeuQuant; -use rgb::{ComponentBytes, FromSlice, RGB8}; +use rgb::{ComponentBytes, FromSlice}; use std::convert::TryFrom; -pub struct GifBuilder { +pub struct VideoGif { width: u16, height: u16, framerate: Option, - global_palette: Option, frames: Vec, } -impl GifBuilder { - pub fn set_resolution(&mut self, width: u16, height: u16) { - self.width = width; - self.height = height; +impl VideoGif { + pub fn new(width: u16, height: u16) -> Self { + Self { + width, + height, + framerate: None, + frames: vec![], + } } + + /// 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(framerate) + self.framerate = Some(100 / framerate); } - pub fn add_frame(&mut self, frame: Frame) { - self.frames.push(frame) - } - pub fn add_global_palette(&mut self, palette: Palette) { - self.global_palette = Some(palette) + + pub fn add_frame>(&mut self, frame: F) { + self.frames.push(frame.into()) } + + #[rustfmt::skip] // it was doing things i did not like pub fn build(self) -> Result { - let Self { - width, - height, - framerate, - frames, - global_palette, - } = self; - - let descriptor = ScreenDescriptor::new(width, height); - let mut gif = Gif { - version: Version::Gif89a, - descriptor, - palette: global_palette, - blocks: vec![], - }; - - let images = frames.into_iter().map(|frame| { - let Frame { - interval, - image_indices, - palette, - } = frame; - - let delay = interval - .map(|interval| interval) - .or(framerate.map(|fr| 100 / fr)) - .unwrap_or(10); - ImageBuilder::new(width, height) - .delay(delay) - .palette(palette) - .build(image_indices) - }); - - for compressed_image in images { - match compressed_image { - Ok(img) => gif.push(img), - Err(e) => return Err(e), - } - } + let Self { width, height, framerate, frames } = self; - Ok(gif) - } -} + let mut gif = Gif::new(width, height); -impl Default for GifBuilder { - fn default() -> Self { - Self { - width: 256, - height: 256, - framerate: Some(15), - frames: vec![], - global_palette: None, + 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) } } @@ -96,13 +65,10 @@ pub struct Frame { palette: Palette, } -impl From>> for Frame { - /// image: row-major ordering - fn from(image: Vec>) -> Self { - let flat = image.concat(); - +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 quant = NeuQuant::new(1, 256, flat_rgba.as_bytes()); let mut indicies = vec![0; flat.len()]; for (image_idx, px) in flat.iter().enumerate() { @@ -120,8 +86,16 @@ impl From>> for Frame { } } +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/lib.rs b/gifed/src/lib.rs index 3990e33..7d35dee 100644 --- a/gifed/src/lib.rs +++ b/gifed/src/lib.rs @@ -2,7 +2,7 @@ mod gif; mod lzw; pub mod block; -#[cfg(all(feature = "rgb"))] +#[cfg(feature = "videoish")] pub mod gif_builder; pub mod reader; pub mod writer; -- cgit 1.4.1-3-g733a5 From 4d7093c266bf9870648b9aaa4b9dd36561a69dcc Mon Sep 17 00:00:00 2001 From: gennyble Date: Tue, 2 Jan 2024 18:18:59 -0600 Subject: gif_build.rs renamed to videogif.rs also checking frame's are the right size now --- gifed/src/block/palette.rs | 5 +- gifed/src/gif_builder.rs | 102 -------------------------------------- gifed/src/lib.rs | 4 +- gifed/src/videogif.rs | 119 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 123 insertions(+), 107 deletions(-) delete mode 100644 gifed/src/gif_builder.rs create mode 100644 gifed/src/videogif.rs diff --git a/gifed/src/block/palette.rs b/gifed/src/block/palette.rs index 436e698..0dc1686 100644 --- a/gifed/src/block/palette.rs +++ b/gifed/src/block/palette.rs @@ -51,9 +51,8 @@ impl Palette { pub fn from_color(&self, color: Color) -> Option { self.table .iter() - .enumerate() - .find(|(_, c)| **c == color) - .map(|(i, _)| i as u8) + .position(|c| *c == color) + .map(|idx| idx as u8) } /// How many padding bytes we need to write. diff --git a/gifed/src/gif_builder.rs b/gifed/src/gif_builder.rs deleted file mode 100644 index 53b1929..0000000 --- a/gifed/src/gif_builder.rs +++ /dev/null @@ -1,102 +0,0 @@ -use crate::{block::Palette, writer::ImageBuilder, Color, EncodeError, Gif}; - -use color_quant::NeuQuant; -use rgb::{ComponentBytes, FromSlice}; - -use std::convert::TryFrom; - -pub struct VideoGif { - width: u16, - height: u16, - framerate: Option, - frames: Vec, -} - -impl VideoGif { - pub fn new(width: u16, height: u16) -> Self { - Self { - width, - height, - framerate: None, - frames: vec![], - } - } - - /// 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); - } - - pub fn add_frame>(&mut self, frame: F) { - self.frames.push(frame.into()) - } - - #[rustfmt::skip] // it was doing things i did not like - pub fn build(self) -> Result { - let Self { width, height, framerate, frames } = self; - - let mut gif = Gif::new(width, height); - - 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, - ///in hundredths of a second - interval: Option, - palette: Palette, -} - -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 indicies = 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]); - indicies[image_idx] = color_idx as u8; - } - - let palette = Palette::try_from(quant.color_map_rgb().as_slice()).unwrap(); - - Self { - image_indices: indicies, - 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/lib.rs b/gifed/src/lib.rs index 7d35dee..2ed3b9b 100644 --- a/gifed/src/lib.rs +++ b/gifed/src/lib.rs @@ -2,9 +2,9 @@ mod gif; mod lzw; pub mod block; -#[cfg(feature = "videoish")] -pub mod gif_builder; pub mod reader; +#[cfg(feature = "videoish")] +pub mod videogif; pub mod writer; pub use reader::DecodeError; diff --git a/gifed/src/videogif.rs b/gifed/src/videogif.rs new file mode 100644 index 0000000..27ec798 --- /dev/null +++ b/gifed/src/videogif.rs @@ -0,0 +1,119 @@ +use crate::{block::Palette, writer::ImageBuilder, Color, EncodeError, Gif}; + +use color_quant::NeuQuant; +use rgb::{ComponentBytes, FromSlice}; + +use std::convert::TryFrom; + +pub struct VideoGif { + width: u16, + height: u16, + framerate: Option, + frames: Vec, +} + +impl VideoGif { + pub fn new(width: u16, height: u16) -> Self { + Self { + width, + height, + framerate: None, + frames: vec![], + } + } + + /// 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); + } + + /// 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>(&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 { + let Self { width, height, framerate, frames } = self; + + let mut gif = Gif::new(width, height); + + 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, + /// in hundredths of a second + interval: Option, + palette: Palette, +} + +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); + } +} -- cgit 1.4.1-3-g733a5 From 865bbc58ff2d8589d74bef709afc4a1908624b2c Mon Sep 17 00:00:00 2001 From: gennyble Date: Tue, 2 Jan 2024 18:20:19 -0600 Subject: exampels gone; need to rewrite then anyway --- gifed/examples/streaming_write.rs | 39 --------------------------------------- gifed/examples/write.rs | 35 ----------------------------------- 2 files changed, 74 deletions(-) delete mode 100644 gifed/examples/streaming_write.rs delete mode 100644 gifed/examples/write.rs 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 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(()) -} -- cgit 1.4.1-3-g733a5 From 96878711b03ee026f57d69648953d603ed503f34 Mon Sep 17 00:00:00 2001 From: gennyble Date: Tue, 2 Jan 2024 18:32:06 -0600 Subject: Frame can now From Vec --- gifed/src/videogif.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/gifed/src/videogif.rs b/gifed/src/videogif.rs index 27ec798..a5475cc 100644 --- a/gifed/src/videogif.rs +++ b/gifed/src/videogif.rs @@ -82,6 +82,12 @@ pub struct Frame { palette: Palette, } +impl From> for Frame { + fn from(flat: Vec) -> Self { + flat.as_slice().into() + } +} + impl From<&[Color]> for Frame { fn from(flat: &[Color]) -> Self { let flat_rgba = flat.as_rgba(); -- cgit 1.4.1-3-g733a5 From 939eb07c2962a8dc20eb8963277f3f7359db50fb Mon Sep 17 00:00:00 2001 From: gennyble Date: Tue, 2 Jan 2024 19:11:20 -0600 Subject: VideoGif will now loop --- gifed/src/videogif.rs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/gifed/src/videogif.rs b/gifed/src/videogif.rs index a5475cc..d066019 100644 --- a/gifed/src/videogif.rs +++ b/gifed/src/videogif.rs @@ -1,15 +1,23 @@ -use crate::{block::Palette, writer::ImageBuilder, Color, EncodeError, Gif}; +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, frames: Vec, + looping: LoopCount, } impl VideoGif { @@ -19,6 +27,7 @@ impl VideoGif { height, framerate: None, frames: vec![], + looping: LoopCount::Forever, } } @@ -30,6 +39,13 @@ impl VideoGif { 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 @@ -53,10 +69,12 @@ impl VideoGif { #[rustfmt::skip] // it was doing things i did not like pub fn build(self) -> Result { - let Self { width, height, framerate, frames } = self; + 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? -- cgit 1.4.1-3-g733a5