about summary refs log tree commit diff
diff options
context:
space:
mode:
authorgennyble <gen@nyble.dev>2024-01-02 19:12:42 -0600
committerGitHub <noreply@github.com>2024-01-02 19:12:42 -0600
commitaafbf4d15989d409f0c2ed2cf13a70434a03d8ba (patch)
tree20fd7c8c3e4e42e8cba4a7d9db93f0a0ba87ba73
parent0a6def135e61cc6ba43e533d4cb537fccb880262 (diff)
parent939eb07c2962a8dc20eb8963277f3f7359db50fb (diff)
downloadgifed-aafbf4d15989d409f0c2ed2cf13a70434a03d8ba.tar.gz
gifed-aafbf4d15989d409f0c2ed2cf13a70434a03d8ba.zip
Merge pull request #20 from novedevo/main
VideoGif API
-rw-r--r--gifed/Cargo.toml9
-rw-r--r--gifed/examples/streaming_write.rs39
-rw-r--r--gifed/examples/write.rs35
-rw-r--r--gifed/src/block/indexedimage.rs2
-rw-r--r--gifed/src/block/palette.rs22
-rw-r--r--gifed/src/gif.rs11
-rw-r--r--gifed/src/lib.rs11
-rw-r--r--gifed/src/videogif.rs143
-rw-r--r--gifed/src/writer/imagebuilder.rs6
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
 	}