about summary refs log tree commit diff
diff options
context:
space:
mode:
authorGenny <gen@nyble.dev>2022-11-25 18:40:52 -0600
committerGenny <gen@nyble.dev>2022-11-25 18:40:52 -0600
commit080f6297f28a9800186376742827de60d17e6160 (patch)
tree8a5030ba3a8949ee030926451c9c59c040b7f565
parentd19e7b983ec034004cb27ab647634474ada10a18 (diff)
downloadgifed-080f6297f28a9800186376742827de60d17e6160.tar.gz
gifed-080f6297f28a9800186376742827de60d17e6160.zip
gifed: streaming writer
-rw-r--r--README.md5
-rw-r--r--gifed/examples/streaming_write.rs39
-rw-r--r--gifed/examples/write.rs20
-rw-r--r--gifed/src/block/screendescriptor.rs12
-rw-r--r--gifed/src/lib.rs27
-rw-r--r--gifed/src/writer/gifbuilder.rs4
-rw-r--r--gifed/src/writer/mod.rs120
7 files changed, 185 insertions, 42 deletions
diff --git a/README.md b/README.md
index c0b631e..f2c625f 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@ This crate is still pretty rough. I hope to make it feature complete and intuiti
 - [ ] Reader
 - [x] Streaming Reader
 - [x] Writer
-- [ ] Streaming Writer
+- [x] Streaming Writer
 - [ ] Feature to allow using the [weezl][weezl-crates] crate for LZW compression instead of the built-in
 - [ ] Feature to allow using the [rgb][rgb-crates] crate for the color type.
 - [ ] Well written and easy to understand docs! `bitvec` quality, but who can match that?
@@ -38,6 +38,9 @@ These are part of the 89a spec, but are kept separate as they're not "core" to t
 [gif89a]: https://www.w3.org/Graphics/GIF/spec-gif89a.txt
 [netscape]: http://www.vurdalakov.net/misc/gif/netscape-looping-application-extension
 
+## gif-frames
+GIF Frames helps you manage the weird disposal methods that the Graphic Control Extension introduced. Also provides Indexed -> Rgb/Rgba conversion.
+
 ## gifprobe
 Similar to FFMPEG's ffprobe, gifprobe will print details of a gif to stdout.
 
diff --git a/gifed/examples/streaming_write.rs b/gifed/examples/streaming_write.rs
new file mode 100644
index 0000000..7fab196
--- /dev/null
+++ b/gifed/examples/streaming_write.rs
@@ -0,0 +1,39 @@
+use std::fs::File;
+
+use gifed::{
+	block::{LoopCount, Palette},
+	writer::{ImageBuilder, Writer},
+	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];
+
+	// 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
index ff8809f..39e28d6 100644
--- a/gifed/examples/write.rs
+++ b/gifed/examples/write.rs
@@ -1,10 +1,10 @@
 use gifed::{
 	block::{LoopCount, Palette},
 	writer::ImageBuilder,
-	Color, Gif,
+	Color, EncodeError, Gif,
 };
 
-fn main() {
+fn main() -> Result<(), EncodeError> {
 	let gif_path = match std::env::args().nth(1) {
 		None => {
 			eprintln!("Expected a path to output the gif to");
@@ -26,18 +26,10 @@ fn main() {
 	for idx in 0..=255 {
 		image.fill(idx);
 
-		builder = builder.image(
-			ImageBuilder::new(128, 128)
-				.delay(3)
-				.build(image.clone())
-				.unwrap(),
-		);
+		builder = builder.image(ImageBuilder::new(128, 128).delay(3).build(image.clone())?);
 	}
 
-	builder
-		.repeat(LoopCount::Forever)
-		.build()
-		.unwrap()
-		.save(gif_path)
-		.unwrap();
+	builder.repeat(LoopCount::Forever).build()?.save(gif_path)?;
+
+	Ok(())
 }
diff --git a/gifed/src/block/screendescriptor.rs b/gifed/src/block/screendescriptor.rs
index aaeea53..d44ca2f 100644
--- a/gifed/src/block/screendescriptor.rs
+++ b/gifed/src/block/screendescriptor.rs
@@ -43,6 +43,18 @@ impl ScreenDescriptor {
 		crate::packed_to_color_table_length(self.packed.color_table_size())
 	}
 
+	/// Returns the background color index. Defaults to zero.
+	pub fn background_color(&self) -> u8 {
+		self.background_color_index
+	}
+
+	/// Set the color used to fill the space in the gif's canvas that no image
+	/// has occupied yet. If there is no palette associated with this
+	/// Screen Descriptor, this value should be ignored by the decoder.
+	pub fn set_background_color(&mut self, index: u8) {
+		self.background_color_index = index;
+	}
+
 	pub fn as_bytes(&self) -> Vec<u8> {
 		let mut vec = vec![];
 		vec.extend_from_slice(&self.width.to_le_bytes());
diff --git a/gifed/src/lib.rs b/gifed/src/lib.rs
index f2dfba5..966b46c 100644
--- a/gifed/src/lib.rs
+++ b/gifed/src/lib.rs
@@ -6,8 +6,8 @@ pub mod block;
 pub mod reader;
 pub mod writer;
 
-use core::fmt;
-use std::error::Error;
+pub use reader::DecodeError;
+pub use writer::EncodeError;
 
 pub use color::Color;
 pub use gif::{Gif, Image};
@@ -23,26 +23,3 @@ pub(crate) fn packed_to_color_table_length(packed: u8) -> usize {
 //in 87a aren't set if version is 87a, or that we return a warning, etc. Just
 //remember about this.
 //bottom of page 24 in 89a
-
-#[derive(Clone, Copy, Debug)]
-pub enum EncodeError {
-	TooManyColors,
-	IndicieSizeMismatch { expected: usize, got: usize },
-	InvalidCodeSize { lzw_code_size: u8 },
-}
-
-impl fmt::Display for EncodeError {
-	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-		match self {
-			Self::TooManyColors => write!(f, "A palette is limited to 256 colors"),
-			Self::IndicieSizeMismatch { expected, got } => {
-				write!(f, "Expected to have {expected} indicies but got {got}")
-			}
-			Self::InvalidCodeSize { lzw_code_size } => {
-				write!(f, "InvalidCodeSize => {lzw_code_size}")
-			}
-		}
-	}
-}
-
-impl Error for EncodeError {}
diff --git a/gifed/src/writer/gifbuilder.rs b/gifed/src/writer/gifbuilder.rs
index 1d3ff1a..2aaf51f 100644
--- a/gifed/src/writer/gifbuilder.rs
+++ b/gifed/src/writer/gifbuilder.rs
@@ -66,14 +66,14 @@ impl GifBuilder {
 				.push(BuildBlock::Block(Block::CompressedImage(ci))),
 			EncodeImage::IndexedImage(ii) => self.blocks.push(BuildBlock::Indexed(ii)),
 			EncodeImage::BuiltImage(BuiltImage { image, gce }) => {
-				self.blocks.push(BuildBlock::Indexed(image));
-
 				if let Some(gce) = gce {
 					self.version = Version::Gif89a;
 
 					self.blocks
 						.push(BuildBlock::Block(Block::GraphicControlExtension(gce)));
 				}
+
+				self.blocks.push(BuildBlock::Indexed(image));
 			}
 		}
 
diff --git a/gifed/src/writer/mod.rs b/gifed/src/writer/mod.rs
index 88311fc..ad380a8 100644
--- a/gifed/src/writer/mod.rs
+++ b/gifed/src/writer/mod.rs
@@ -1,5 +1,125 @@
 mod gifbuilder;
 mod imagebuilder;
 
+use std::{error::Error, fmt, io::Write};
+
 pub use gifbuilder::GifBuilder;
 pub use imagebuilder::ImageBuilder;
+
+use crate::block::{encode_block, Block, LoopCount, Palette, ScreenDescriptor, Version};
+
+use self::gifbuilder::EncodeImage;
+
+pub struct Writer<W: Write> {
+	writer: W,
+	global_palette: Option<Palette>,
+}
+
+impl<W: Write> Writer<W> {
+	/// Write the required bits of a GIF. The version written to the stream is
+	/// [Version::Gif89a]. If for some reason you need [Version::Gif87a] I
+	/// recommend you use [Writer::from_parts].
+	pub fn new(
+		writer: W,
+		width: u16,
+		height: u16,
+		global_palette: Option<Palette>,
+	) -> Result<Self, EncodeError> {
+		let screen_descriptor = ScreenDescriptor::new(width, height);
+		Self::from_parts(writer, Version::Gif89a, screen_descriptor, global_palette)
+	}
+
+	//FIXME: gen- This name sucks
+	/// Create a new [Writer] from the provided required blocks.
+	pub fn from_parts(
+		writer: W,
+		version: Version,
+		screen_descriptor: ScreenDescriptor,
+		global_palette: Option<Palette>,
+	) -> Result<Self, EncodeError> {
+		let mut this = Self {
+			writer,
+			global_palette,
+		};
+		this.write_all(version.as_bytes())?;
+		this.write_all(&screen_descriptor.as_bytes())?;
+
+		if let Some(palette) = this.global_palette.as_ref() {
+			this.write_all(&palette.as_bytes())?;
+		}
+
+		Ok(this)
+	}
+
+	fn write_all(&mut self, buf: &[u8]) -> Result<(), EncodeError> {
+		self.writer
+			.write_all(buf)
+			.map_err(|error| EncodeError::IoError { error })
+	}
+
+	pub fn block(&mut self, block: Block) -> Result<(), EncodeError> {
+		self.write_all(&encode_block(&block))
+	}
+
+	pub fn repeat(&mut self, count: LoopCount) -> Result<(), EncodeError> {
+		self.write_all(&encode_block(&Block::LoopingExtension(count)))
+	}
+
+	pub fn image<I: Into<EncodeImage>>(&mut self, image: I) -> Result<(), EncodeError> {
+		match image.into() {
+			EncodeImage::CompressedImage(compressed) => self.write_all(&compressed.as_bytes()),
+			EncodeImage::IndexedImage(indexed) => {
+				let lzw_code_size = self.global_palette.as_ref().map(|p| p.lzw_code_size());
+
+				let compressed = indexed.compress(lzw_code_size)?;
+				self.write_all(&compressed.as_bytes())
+			}
+			EncodeImage::BuiltImage(built) => {
+				if let Some(gce) = built.gce {
+					self.write_all(&encode_block(&Block::GraphicControlExtension(gce)))?;
+				}
+
+				let lzw_code_size = self.global_palette.as_ref().map(|p| p.lzw_code_size());
+
+				let compressed = built.image.compress(lzw_code_size)?;
+				self.write_all(&compressed.as_bytes())
+			}
+		}
+	}
+
+	pub fn done(mut self) -> Result<(), EncodeError> {
+		self.write_all(&[0x3B])
+	}
+}
+
+#[derive(Debug)]
+pub enum EncodeError {
+	IoError { error: std::io::Error },
+	TooManyColors,
+	IndicieSizeMismatch { expected: usize, got: usize },
+	InvalidCodeSize { lzw_code_size: u8 },
+}
+
+impl Error for EncodeError {}
+impl fmt::Display for EncodeError {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		match self {
+			Self::IoError { error } => {
+				write!(f, "{error}")
+			}
+			Self::TooManyColors => write!(f, "A palette is limited to 256 colors"),
+			Self::IndicieSizeMismatch { expected, got } => {
+				write!(f, "Expected to have {expected} indicies but got {got}")
+			}
+			Self::InvalidCodeSize { lzw_code_size } => {
+				write!(f, "InvalidCodeSize => {lzw_code_size}")
+			}
+		}
+	}
+}
+
+impl From<std::io::Error> for EncodeError {
+	fn from(error: std::io::Error) -> Self {
+		EncodeError::IoError { error }
+	}
+}