about summary refs log tree commit diff
path: root/gifed/src
diff options
context:
space:
mode:
authorGenny <gen@nyble.dev>2021-11-21 18:35:57 -0600
committerGenny <gen@nyble.dev>2021-11-21 18:35:57 -0600
commit1de64a3818875947f7f1044b1d4cfdf271b04fd3 (patch)
tree3a5bfbb237d67832dfa1beeb3d28566173484b63 /gifed/src
parentf35e18cb0531e7d6a3544560746d592aa47ed555 (diff)
downloadgifed-1de64a3818875947f7f1044b1d4cfdf271b04fd3.tar.gz
gifed-1de64a3818875947f7f1044b1d4cfdf271b04fd3.zip
Bring gifprobe into this repository
Diffstat (limited to 'gifed/src')
-rw-r--r--gifed/src/block/colortable.rs117
-rw-r--r--gifed/src/block/extension/application.rs15
-rw-r--r--gifed/src/block/extension/graphiccontrol.rs116
-rw-r--r--gifed/src/block/extension/mod.rs49
-rw-r--r--gifed/src/block/imagedescriptor.rs73
-rw-r--r--gifed/src/block/indexedimage.rs70
-rw-r--r--gifed/src/block/mod.rs25
-rw-r--r--gifed/src/block/screendescriptor.rs79
-rw-r--r--gifed/src/block/version.rs25
-rw-r--r--gifed/src/color.rs38
-rw-r--r--gifed/src/colorimage.rs61
-rw-r--r--gifed/src/gif.rs306
-rw-r--r--gifed/src/lib.rs51
-rw-r--r--gifed/src/lzw.rs173
-rw-r--r--gifed/src/reader/mod.rs274
-rw-r--r--gifed/src/writer/gifbuilder.rs106
-rw-r--r--gifed/src/writer/imagebuilder.rs133
-rw-r--r--gifed/src/writer/mod.rs5
18 files changed, 1716 insertions, 0 deletions
diff --git a/gifed/src/block/colortable.rs b/gifed/src/block/colortable.rs
new file mode 100644
index 0000000..01fe00b
--- /dev/null
+++ b/gifed/src/block/colortable.rs
@@ -0,0 +1,117 @@
+pub use crate::Color;
+use crate::EncodingError;
+use std::{
+	convert::{TryFrom, TryInto},
+	ops::Deref,
+};
+
+#[derive(Clone, Debug)]
+pub struct ColorTable {
+	table: Vec<Color>,
+}
+
+impl ColorTable {
+	pub fn new() -> Self {
+		Self { table: vec![] }
+	}
+
+	/// Returns the number of colors in the color table as used by the packed
+	/// fields in the Logical Screen Descriptor and Image Descriptor. You can
+	/// get the actual size with the [`len`](struct.ColorTable.html#method.len) method.
+	pub fn packed_len(&self) -> u8 {
+		((self.table.len() as f32).log2().ceil() - 1f32) as u8
+	}
+
+	/// Returns the number of items in the table
+	pub fn len(&self) -> usize {
+		self.table.len()
+	}
+
+	/// Pushes a color on to the end of the table
+	pub fn push(&mut self, color: Color) {
+		self.table.push(color);
+	}
+
+	pub fn get(&self, index: u8) -> Option<Color> {
+		self.table.get(index as usize).map(|v| v.clone())
+	}
+
+	pub fn from_color(&self, color: Color) -> Option<u8> {
+		for (i, &c) in self.table.iter().enumerate() {
+			if c == color {
+				return Some(i as u8);
+			}
+		}
+		None
+	}
+}
+
+impl Deref for ColorTable {
+	type Target = [Color];
+
+	fn deref(&self) -> &Self::Target {
+		&self.table
+	}
+}
+
+impl From<&ColorTable> for Box<[u8]> {
+	fn from(table: &ColorTable) -> Self {
+		let mut vec = vec![];
+
+		for color in table.iter() {
+			vec.extend_from_slice(&[color.r, color.g, color.b]);
+		}
+
+		let packed_len = 2usize.pow(table.packed_len() as u32 + 1);
+		let padding = (packed_len as usize - table.len()) * 3;
+		if padding > 0 {
+			vec.extend_from_slice(&vec![0; padding]);
+		}
+
+		vec.into_boxed_slice()
+	}
+}
+
+//TODO: TryFrom Vec<u8> (must be multiple of 3 len) and From Vec<Color>
+impl TryFrom<&[u8]> for ColorTable {
+	type Error = ();
+
+	fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
+		if value.len() % 3 != 0 {
+			return Err(());
+		} else {
+			Ok(Self {
+				table: value
+					.chunks(3)
+					.map(|slice| Color::from(TryInto::<[u8; 3]>::try_into(slice).unwrap()))
+					.collect::<Vec<Color>>(),
+			})
+		}
+	}
+}
+
+impl TryFrom<Vec<Color>> for ColorTable {
+	type Error = EncodingError;
+
+	fn try_from(value: Vec<Color>) -> Result<Self, Self::Error> {
+		if value.len() > 256 {
+			Err(EncodingError::TooManyColors)
+		} else {
+			Ok(Self { table: value })
+		}
+	}
+}
+
+impl TryFrom<Vec<(u8, u8, u8)>> for ColorTable {
+	type Error = EncodingError;
+
+	fn try_from(value: Vec<(u8, u8, u8)>) -> Result<Self, Self::Error> {
+		if value.len() > 256 {
+			Err(EncodingError::TooManyColors)
+		} else {
+			Ok(Self {
+				table: value.into_iter().map(|c| c.into()).collect(),
+			})
+		}
+	}
+}
diff --git a/gifed/src/block/extension/application.rs b/gifed/src/block/extension/application.rs
new file mode 100644
index 0000000..9ec1814
--- /dev/null
+++ b/gifed/src/block/extension/application.rs
@@ -0,0 +1,15 @@
+pub struct Application {
+	pub(crate) identifier: String, // max len 8
+	pub(crate) authentication_code: [u8; 3],
+	pub(crate) data: Vec<u8>,
+}
+
+impl Application {
+	pub fn identifier(&self) -> &str {
+		&self.identifier
+	}
+
+	pub fn authentication_code(&self) -> &[u8] {
+		&self.authentication_code
+	}
+}
diff --git a/gifed/src/block/extension/graphiccontrol.rs b/gifed/src/block/extension/graphiccontrol.rs
new file mode 100644
index 0000000..b595554
--- /dev/null
+++ b/gifed/src/block/extension/graphiccontrol.rs
@@ -0,0 +1,116 @@
+use std::{convert::TryInto, fmt};
+
+#[derive(Clone, Debug)]
+pub struct GraphicControl {
+	pub(crate) packed: u8,
+	pub(crate) delay_time: u16,
+	pub(crate) transparency_index: u8,
+}
+
+impl GraphicControl {
+	pub fn new(
+		disposal_method: DisposalMethod,
+		user_input_flag: bool,
+		transparency_flag: bool,
+		delay_time: u16,
+		transparency_index: u8,
+	) -> Self {
+		let mut ret = Self {
+			packed: 0,
+			delay_time,
+			transparency_index,
+		};
+
+		ret.set_disposal_method(disposal_method);
+		ret.user_input(user_input_flag);
+		ret.transparency(transparency_flag);
+
+		ret
+	}
+
+	pub fn disposal_method(&self) -> Option<DisposalMethod> {
+		match self.packed & 0b000_111_00 {
+			0b000_000_00 => Some(DisposalMethod::NoAction),
+			0b000_100_00 => Some(DisposalMethod::DoNotDispose),
+			0b000_010_00 => Some(DisposalMethod::RestoreBackground),
+			0b000_110_00 => Some(DisposalMethod::RestorePrevious),
+			_ => None,
+		}
+	}
+
+	pub fn set_disposal_method(&mut self, method: DisposalMethod) {
+		match method {
+			DisposalMethod::NoAction => self.packed &= 0b111_000_1_1,
+			DisposalMethod::DoNotDispose => self.packed |= 0b000_100_0_0,
+			DisposalMethod::RestoreBackground => self.packed |= 0b000_010_0_0,
+			DisposalMethod::RestorePrevious => self.packed |= 0b000_110_0_0,
+		};
+	}
+
+	pub fn transparency_index(&self) -> u8 {
+		self.transparency_index
+	}
+
+	pub fn user_input(&mut self, flag: bool) {
+		if flag {
+			self.packed |= 0b000_000_1_0;
+		} else {
+			self.packed &= 0b111_111_0_1;
+		}
+	}
+
+	pub fn transparency(&mut self, flag: bool) {
+		if flag {
+			self.packed |= 0b000_000_0_1;
+		} else {
+			self.packed &= 0b111_111_1_0;
+		}
+	}
+
+	pub fn delay_time(&self) -> u16 {
+		self.delay_time
+	}
+
+	pub fn delay_time_mut(&mut self) -> &mut u16 {
+		&mut self.delay_time
+	}
+
+	pub fn is_transparent(&self) -> bool {
+		self.packed & 0b000_000_0_1 > 0
+	}
+}
+
+impl From<[u8; 4]> for GraphicControl {
+	fn from(arr: [u8; 4]) -> Self {
+		let packed = arr[0];
+		let delay_time = u16::from_le_bytes(arr[1..3].try_into().unwrap());
+		let transparency_index = arr[3];
+
+		Self {
+			packed,
+			delay_time,
+			transparency_index,
+		}
+	}
+}
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum DisposalMethod {
+	NoAction,
+	DoNotDispose,
+	RestoreBackground,
+	RestorePrevious,
+}
+
+impl fmt::Display for DisposalMethod {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		let st = match self {
+			DisposalMethod::NoAction => "Dispose as Normal",
+			DisposalMethod::DoNotDispose => "No Dispose",
+			DisposalMethod::RestoreBackground => "Restore to background",
+			DisposalMethod::RestorePrevious => "Restore previous image",
+		};
+
+		write!(f, "{}", st)
+	}
+}
diff --git a/gifed/src/block/extension/mod.rs b/gifed/src/block/extension/mod.rs
new file mode 100644
index 0000000..fb5eb20
--- /dev/null
+++ b/gifed/src/block/extension/mod.rs
@@ -0,0 +1,49 @@
+mod application;
+mod graphiccontrol;
+
+pub use graphiccontrol::{DisposalMethod, GraphicControl};
+
+pub use self::application::Application;
+
+pub enum Extension {
+	GraphicControl(GraphicControl),
+	Looping(u16),
+	Comment(Vec<u8>), // Plain Text
+	Application(Application),
+}
+
+impl From<&Extension> for Box<[u8]> {
+	fn from(ext: &Extension) -> Self {
+		let mut vec = vec![];
+		vec.push(0x21); // Push the extension introducer
+
+		match ext {
+			Extension::GraphicControl(gc) => {
+				vec.push(0xF9); // Graphic control label
+				vec.push(0x04); // Block size for this extension is always 4
+				vec.push(gc.packed);
+				vec.extend_from_slice(&gc.delay_time.to_le_bytes());
+				vec.push(gc.transparency_index);
+			}
+			Extension::Looping(count) => {
+				vec.push(0xFF); // Application extension label
+				vec.push(0x0B); // 11 bytes in this block
+				vec.extend_from_slice(b"NETSCAPE2.0"); // App. ident. and "auth code"
+				vec.push(0x03); // Sub-block length
+				vec.push(0x01); // Identifies netscape looping extension
+				vec.extend_from_slice(&count.to_le_bytes());
+			}
+			Extension::Comment(_) => todo!(),
+			Extension::Application(_) => todo!(),
+		}
+
+		vec.push(0x00); // Zero-length data block indicates end of extension
+		vec.into_boxed_slice()
+	}
+}
+
+impl From<GraphicControl> for Extension {
+	fn from(gce: GraphicControl) -> Self {
+		Extension::GraphicControl(gce)
+	}
+}
diff --git a/gifed/src/block/imagedescriptor.rs b/gifed/src/block/imagedescriptor.rs
new file mode 100644
index 0000000..25567b2
--- /dev/null
+++ b/gifed/src/block/imagedescriptor.rs
@@ -0,0 +1,73 @@
+use std::convert::TryInto;
+
+pub struct ImageDescriptor {
+	// Image Seperator 0x2C is the first byte //
+	pub left: u16,
+	pub top: u16,
+	pub width: u16,
+	pub height: u16,
+	pub packed: u8,
+}
+
+impl ImageDescriptor {
+	pub fn set_color_table_present(&mut self, is_present: bool) {
+		if is_present {
+			self.packed |= 0b1000_0000;
+		} else {
+			self.packed &= 0b0111_1111;
+		}
+	}
+
+	pub fn set_color_table_size(&mut self, size: u8) {
+		// GCT size is calulated by raising two to this number plus one,
+		// so we have to work backwards.
+		let size = (size as f32).log2().ceil() - 1f32;
+		self.packed |= size as u8;
+	}
+
+	//TODO: Setter for sort flag in packed field
+	//TODO: Setter for interlace flag in packed field
+
+	pub fn color_table_present(&self) -> bool {
+		self.packed & 0b1000_0000 != 0
+	}
+
+	pub fn color_table_size(&self) -> usize {
+		crate::packed_to_color_table_length(self.packed & 0b0000_0111)
+	}
+}
+
+impl From<&ImageDescriptor> for Box<[u8]> {
+	fn from(desc: &ImageDescriptor) -> Self {
+		let mut vec = vec![];
+
+		vec.push(0x2C); // Image Seperator
+		vec.extend_from_slice(&desc.left.to_le_bytes());
+		vec.extend_from_slice(&desc.top.to_le_bytes());
+		vec.extend_from_slice(&desc.width.to_le_bytes());
+		vec.extend_from_slice(&desc.height.to_le_bytes());
+		vec.push(desc.packed);
+
+		vec.into_boxed_slice()
+	}
+}
+
+impl From<[u8; 9]> for ImageDescriptor {
+	fn from(arr: [u8; 9]) -> Self {
+		let left = u16::from_le_bytes(arr[0..2].try_into().unwrap());
+		let top = u16::from_le_bytes(arr[2..4].try_into().unwrap());
+		let width = u16::from_le_bytes(arr[4..6].try_into().unwrap());
+		let height = u16::from_le_bytes(arr[6..8].try_into().unwrap());
+		let packed = arr[8];
+
+		Self {
+			left,
+			top,
+			width,
+			height,
+			packed,
+		}
+	}
+}
+
+//TODO: Impl to allow changing the packed field easier
diff --git a/gifed/src/block/indexedimage.rs b/gifed/src/block/indexedimage.rs
new file mode 100644
index 0000000..8ed0319
--- /dev/null
+++ b/gifed/src/block/indexedimage.rs
@@ -0,0 +1,70 @@
+use std::convert::TryFrom;
+
+use super::{ColorTable, ImageDescriptor};
+use crate::LZW;
+
+pub struct IndexedImage {
+	pub image_descriptor: ImageDescriptor,
+	pub local_color_table: Option<ColorTable>,
+	pub indicies: Vec<u8>,
+}
+
+impl IndexedImage {
+	pub fn left(&self) -> u16 {
+		self.image_descriptor.left
+	}
+
+	pub fn top(&self) -> u16 {
+		self.image_descriptor.left
+	}
+
+	pub fn width(&self) -> u16 {
+		self.image_descriptor.width
+	}
+
+	pub fn height(&self) -> u16 {
+		self.image_descriptor.height
+	}
+
+	pub fn as_boxed_slice(&self, minimum_code_size: u8) -> Box<[u8]> {
+		let mut out = vec![];
+
+		let mut boxed: Box<[u8]> = (&self.image_descriptor).into();
+		out.extend_from_slice(&*boxed);
+
+		// Get the mcs while we write out the color table
+		let mut mcs = if let Some(lct) = &self.local_color_table {
+			boxed = lct.into();
+			out.extend_from_slice(&*boxed);
+
+			lct.packed_len()
+		} else {
+			minimum_code_size + 1
+		};
+
+		if mcs < 2 {
+			mcs = 2; // Must be true: 0 <= mcs <= 8
+		}
+
+		// First write out the MCS
+		out.push(mcs);
+
+		let compressed = LZW::encode(mcs, &self.indicies);
+
+		for chunk in compressed.chunks(255) {
+			out.push(chunk.len() as u8);
+			out.extend_from_slice(chunk);
+		}
+		// Data block length 0 to indicate an end
+		out.push(0x00);
+
+		out.into_boxed_slice()
+	}
+}
+
+pub struct CompressedImage {
+	pub image_descriptor: ImageDescriptor,
+	pub local_color_table: Option<ColorTable>,
+	pub lzw_minimum_code_size: u8,
+	pub blocks: Vec<Vec<u8>>,
+}
diff --git a/gifed/src/block/mod.rs b/gifed/src/block/mod.rs
new file mode 100644
index 0000000..e35224b
--- /dev/null
+++ b/gifed/src/block/mod.rs
@@ -0,0 +1,25 @@
+mod colortable;
+pub mod extension;
+mod imagedescriptor;
+mod indexedimage;
+mod screendescriptor;
+mod version;
+
+pub use colortable::ColorTable;
+pub use imagedescriptor::ImageDescriptor;
+pub use indexedimage::CompressedImage;
+pub use indexedimage::IndexedImage;
+pub use screendescriptor::ScreenDescriptor;
+pub use version::Version;
+
+use crate::writer::ImageBuilder;
+
+pub enum Block {
+	IndexedImage(IndexedImage),
+	Extension(extension::Extension),
+}
+
+enum WriteBlock {
+	ImageBuilder(ImageBuilder),
+	Block(Block),
+}
diff --git a/gifed/src/block/screendescriptor.rs b/gifed/src/block/screendescriptor.rs
new file mode 100644
index 0000000..dc0257d
--- /dev/null
+++ b/gifed/src/block/screendescriptor.rs
@@ -0,0 +1,79 @@
+use std::convert::TryInto;
+
+pub struct ScreenDescriptor {
+	pub width: u16,
+	pub height: u16,
+	pub packed: u8,
+	pub background_color_index: u8,
+	pub pixel_aspect_ratio: u8,
+}
+
+impl ScreenDescriptor {
+	pub fn new(width: u16, height: u16) -> Self {
+		Self {
+			width,
+			height,
+			packed: 0,
+			background_color_index: 0,
+			pixel_aspect_ratio: 0,
+		}
+	}
+
+	pub fn set_color_table_present(&mut self, is_present: bool) {
+		if is_present {
+			self.packed |= 0b1000_0000;
+		} else {
+			self.packed &= 0b0111_1111;
+		}
+	}
+
+	pub fn set_color_table_size(&mut self, size: u8) {
+		println!("scts: {}", size);
+		// GCT size is calulated by raising two to this number plus one,
+		// so we have to work backwards.
+		let size = (size as f32).log2().ceil() - 1f32;
+		self.packed |= size as u8;
+	}
+
+	//TODO: Setter for sort flag in packed field
+	//TODO: Setter for color resolution in packed field
+
+	pub fn color_table_present(&self) -> bool {
+		self.packed & 0b1000_0000 != 0
+	}
+
+	pub fn color_table_len(&self) -> usize {
+		crate::packed_to_color_table_length(self.packed & 0b0000_0111)
+	}
+}
+
+impl From<&ScreenDescriptor> for Box<[u8]> {
+	fn from(lsd: &ScreenDescriptor) -> Self {
+		let mut vec = vec![];
+		vec.extend_from_slice(&lsd.width.to_le_bytes());
+		vec.extend_from_slice(&lsd.height.to_le_bytes());
+		vec.push(lsd.packed);
+		vec.push(lsd.background_color_index);
+		vec.push(lsd.pixel_aspect_ratio);
+
+		vec.into_boxed_slice()
+	}
+}
+
+impl From<[u8; 7]> for ScreenDescriptor {
+	fn from(arr: [u8; 7]) -> Self {
+		let width = u16::from_le_bytes(arr[0..2].try_into().unwrap());
+		let height = u16::from_le_bytes(arr[2..4].try_into().unwrap());
+		let packed = arr[4];
+		let background_color_index = arr[5];
+		let pixel_aspect_ratio = arr[6];
+
+		Self {
+			width,
+			height,
+			packed,
+			background_color_index,
+			pixel_aspect_ratio,
+		}
+	}
+}
diff --git a/gifed/src/block/version.rs b/gifed/src/block/version.rs
new file mode 100644
index 0000000..0171ad4
--- /dev/null
+++ b/gifed/src/block/version.rs
@@ -0,0 +1,25 @@
+use std::fmt;
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum Version {
+	Gif87a,
+	Gif89a,
+}
+
+impl From<&Version> for &[u8] {
+	fn from(version: &Version) -> Self {
+		match version {
+			Version::Gif87a => b"GIF87a",
+			Version::Gif89a => b"GIF89a",
+		}
+	}
+}
+
+impl fmt::Display for Version {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		match self {
+			Version::Gif87a => write!(f, "GIF87a"),
+			Version::Gif89a => write!(f, "GIF89a"),
+		}
+	}
+}
diff --git a/gifed/src/color.rs b/gifed/src/color.rs
new file mode 100644
index 0000000..e18ce58
--- /dev/null
+++ b/gifed/src/color.rs
@@ -0,0 +1,38 @@
+#[derive(Copy, Clone, Debug, PartialEq)]
+pub struct Color {
+	pub r: u8,
+	pub g: u8,
+	pub b: u8,
+}
+
+impl Color {
+	pub fn new(r: u8, g: u8, b: u8) -> Self {
+		Self { r, g, b }
+	}
+}
+
+impl From<[u8; 3]> for Color {
+	fn from(arr: [u8; 3]) -> Self {
+		Self {
+			r: arr[0],
+			g: arr[1],
+			b: arr[2],
+		}
+	}
+}
+
+impl From<(u8, u8, u8)> for Color {
+	fn from(t: (u8, u8, u8)) -> Self {
+		Self {
+			r: t.0,
+			g: t.1,
+			b: t.2,
+		}
+	}
+}
+
+impl Into<[u8; 3]> for Color {
+	fn into(self) -> [u8; 3] {
+		[self.r, self.g, self.b]
+	}
+}
diff --git a/gifed/src/colorimage.rs b/gifed/src/colorimage.rs
new file mode 100644
index 0000000..69dac1e
--- /dev/null
+++ b/gifed/src/colorimage.rs
@@ -0,0 +1,61 @@
+use std::convert::TryFrom;
+
+use crate::{block::ColorTable, gif::Image, reader::DecodingError, Color};
+
+pub struct ColorImage {
+	width: u16,
+	height: u16,
+	data: Vec<Pixel>,
+}
+
+impl ColorImage {
+	pub(crate) fn from_indicies(
+		width: u16,
+		height: u16,
+		indicies: &[u8],
+		table: &ColorTable,
+		transindex: Option<u8>,
+	) -> Result<Self, DecodingError> {
+		let mut data = vec![Pixel::Transparent; (width * height) as usize];
+
+		for (image_index, color_index) in indicies.into_iter().enumerate() {
+			if let Some(trans) = transindex {
+				if trans == *color_index {
+					data[image_index] = Pixel::Transparent;
+				}
+			} else {
+				data[image_index] = Pixel::Color(
+					table
+						.get(*color_index)
+						.ok_or(DecodingError::ColorIndexOutOfBounds)?,
+				);
+			}
+		}
+
+		Ok(ColorImage {
+			width,
+			height,
+			data,
+		})
+	}
+}
+
+impl<'a> TryFrom<Image<'a>> for ColorImage {
+	type Error = DecodingError;
+
+	fn try_from(img: Image<'a>) -> Result<Self, Self::Error> {
+		ColorImage::from_indicies(
+			img.width,
+			img.height,
+			img.indicies,
+			img.palette,
+			img.transparent_index,
+		)
+	}
+}
+
+#[derive(Copy, Clone, Debug, PartialEq)]
+pub enum Pixel {
+	Color(Color),
+	Transparent,
+}
diff --git a/gifed/src/gif.rs b/gifed/src/gif.rs
new file mode 100644
index 0000000..89aaa64
--- /dev/null
+++ b/gifed/src/gif.rs
@@ -0,0 +1,306 @@
+use std::{fs::File, io::Write, path::Path};
+
+use crate::{
+	block::{extension::Extension, Block, ColorTable, ScreenDescriptor, Version},
+	colorimage,
+	writer::GifBuilder,
+	ColorImage,
+};
+pub struct Gif {
+	pub header: Version,
+	pub screen_descriptor: ScreenDescriptor,
+	pub global_color_table: Option<ColorTable>,
+	pub blocks: Vec<Block>, // Trailer at the end of this struct is 0x3B //
+}
+
+impl Gif {
+	pub fn builder(width: u16, height: u16) -> GifBuilder {
+		GifBuilder::new(width, height)
+	}
+
+	pub fn to_vec(&self) -> Vec<u8> {
+		let mut out = vec![];
+
+		out.extend_from_slice((&self.header).into());
+
+		let mut boxed: Box<[u8]> = (&self.screen_descriptor).into();
+		out.extend_from_slice(&*boxed);
+
+		// While we output the color table, grab it's length to use when
+		// outputting the image, or 0 if we don't have a GCT
+		let mcs = if let Some(gct) = &self.global_color_table {
+			boxed = gct.into();
+			out.extend_from_slice(&*boxed);
+
+			gct.packed_len()
+		} else {
+			0
+		};
+
+		for block in self.blocks.iter() {
+			match block {
+				Block::IndexedImage(image) => {
+					boxed = image.as_boxed_slice(mcs);
+					out.extend_from_slice(&*boxed);
+				}
+				//Block::BlockedImage(_) => unreachable!(),
+				Block::Extension(ext) => {
+					boxed = ext.into();
+					out.extend_from_slice(&*boxed);
+				}
+			}
+		}
+
+		// Write Trailer
+		out.push(0x3B);
+
+		out
+	}
+
+	pub fn save<P: AsRef<Path>>(&self, path: P) -> std::io::Result<()> {
+		File::create(path.as_ref())?.write_all(&self.to_vec())
+	}
+
+	pub fn images<'a>(&'a self) -> ImageIterator<'a> {
+		ImageIterator {
+			gif: self,
+			veciter: self.blocks.iter(),
+		}
+	}
+}
+
+pub struct ImageIterator<'a> {
+	gif: &'a Gif,
+	veciter: std::slice::Iter<'a, Block>,
+}
+
+impl<'a> Iterator for ImageIterator<'a> {
+	type Item = Image<'a>;
+
+	fn next(&mut self) -> Option<Self::Item> {
+		let mut transparent = None;
+
+		let img = loop {
+			match self.veciter.next() {
+				Some(block) => match block {
+					Block::IndexedImage(img) => break img,
+					Block::Extension(Extension::GraphicControl(gce)) => {
+						if gce.is_transparent() {
+							transparent = Some(gce.transparency_index());
+						} else {
+							transparent = None;
+						}
+					}
+					_ => (),
+				},
+				None => return None,
+			}
+		};
+
+		if img.image_descriptor.color_table_present() {
+			Some(Image {
+				width: img.image_descriptor.width,
+				height: img.image_descriptor.height,
+				left_offset: img.image_descriptor.left,
+				top_offset: img.image_descriptor.top,
+				palette: &img.local_color_table.as_ref().unwrap(),
+				transparent_index: transparent,
+				indicies: &img.indicies,
+			})
+		} else {
+			Some(Image {
+				width: img.image_descriptor.width,
+				height: img.image_descriptor.height,
+				left_offset: img.image_descriptor.left,
+				top_offset: img.image_descriptor.top,
+				palette: self.gif.global_color_table.as_ref().unwrap(),
+				transparent_index: transparent,
+				indicies: &img.indicies,
+			})
+		}
+	}
+}
+
+pub struct FrameIterator<'a> {
+	gif: &'a Gif,
+	veciter: std::slice::Iter<'a, Block>,
+	buffer: Vec<u8>,
+}
+
+pub struct Image<'a> {
+	pub(crate) width: u16,
+	pub(crate) height: u16,
+	left_offset: u16,
+	top_offset: u16,
+	pub(crate) palette: &'a ColorTable,
+	pub(crate) transparent_index: Option<u8>,
+	pub(crate) indicies: &'a [u8],
+}
+
+impl<'a> Image<'a> {
+	pub fn width(&self) -> u16 {
+		self.width
+	}
+
+	pub fn height(&self) -> u16 {
+		self.height
+	}
+
+	pub fn position(&self) -> (u16, u16) {
+		(self.left_offset, self.top_offset)
+	}
+
+	pub fn palette(&self) -> &ColorTable {
+		self.palette
+	}
+
+	pub fn transparent_index(&self) -> Option<u8> {
+		self.transparent_index
+	}
+
+	pub fn indicies(&self) -> &[u8] {
+		self.indicies
+	}
+}
+
+pub struct Frame {
+	width: u16,
+	height: u16,
+	palette: ColorTable,
+	transparent_index: Option<u8>,
+	indicies: Vec<u8>,
+	delay_after_draw: u16,
+	user_input_flag: bool,
+}
+
+#[cfg(test)]
+pub mod gif {
+	use std::convert::TryInto;
+	use std::io::Write;
+
+	use crate::block::extension::DisposalMethod;
+	use crate::writer::{GifBuilder, ImageBuilder};
+	use crate::Color;
+
+	#[test]
+	fn to_vec_gif87a() {
+		let gct = vec![Color::new(1, 2, 3), Color::new(253, 254, 255)];
+		let colortable = vec![Color::new(0, 0, 0), Color::new(128, 0, 255)];
+		let indicies = vec![0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0];
+
+		let expected_out = vec![
+			0x47,
+			0x49,
+			0x46,
+			0x38,
+			0x37,
+			0x61, // Version - GIF87a
+			0x04,
+			0x00,
+			0x04,
+			0x00,
+			0b1000_0000,
+			0x00,
+			0x00, // Logical Screen Descriptor
+			1,
+			2,
+			3,
+			253,
+			254,
+			255, // Global Color Table
+			0x2C,
+			0x00,
+			0x00,
+			0x00,
+			0x00,
+			0x04,
+			0x00,
+			0x04,
+			0x00,
+			0b1000_0000, // Image Descriptor 1
+			0,
+			0,
+			0,
+			128,
+			0,
+			255, // Color Table
+			0x02,
+			0x05,
+			0x84,
+			0x1D,
+			0x81,
+			0x7A,
+			0x50,
+			0x00, // Image Data 1
+			0x2C,
+			0x00,
+			0x00,
+			0x00,
+			0x00,
+			0x04,
+			0x00,
+			0x04,
+			0x00,
+			0b0000_0000, // Image Descriptor 2
+			0x02,
+			0x05,
+			0x84,
+			0x1D,
+			0x81,
+			0x7A,
+			0x50,
+			0x00, // Image Data 2
+			0x3B, // Trailer
+		];
+
+		let actual = GifBuilder::new(4, 4)
+			.palette(gct.try_into().unwrap())
+			.image(
+				ImageBuilder::new(4, 4)
+					.palette(colortable.try_into().unwrap())
+					.indicies(&indicies),
+			)
+			.image(ImageBuilder::new(4, 4).indicies(&indicies));
+
+		let bytes = actual.build().unwrap().to_vec();
+		assert_eq!(bytes, expected_out);
+	}
+
+	#[test]
+	fn to_vec_gif89a() {
+		let gct = vec![Color::new(1, 2, 3), Color::new(253, 254, 255)];
+		let colortable = vec![Color::new(0, 0, 0), Color::new(128, 0, 255)];
+		let indicies = vec![0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0];
+
+		let expected_out = vec![
+			71, 73, 70, 56, 57, 97, 4, 0, 4, 0, 128, 0, 0, 1, 2, 3, 253, 254, 255, 33, 249, 4, 8,
+			64, 0, 0, 0, 44, 0, 0, 0, 0, 4, 0, 4, 0, 128, 0, 0, 0, 128, 0, 255, 2, 5, 132, 29, 129,
+			122, 80, 0, 44, 0, 0, 0, 0, 4, 0, 4, 0, 0, 2, 5, 132, 29, 129, 122, 80, 0, 59,
+		];
+
+		let actual_out = GifBuilder::new(4, 4)
+			.palette(gct.try_into().unwrap())
+			.image(
+				ImageBuilder::new(4, 4)
+					.palette(colortable.try_into().unwrap())
+					.indicies(&indicies)
+					.disposal_method(DisposalMethod::RestoreBackground)
+					.delay(64),
+			)
+			.image(ImageBuilder::new(4, 4).indicies(&indicies))
+			.build()
+			.unwrap()
+			.to_vec();
+
+		std::fs::File::create("ah.gif")
+			.unwrap()
+			.write_all(&actual_out)
+			.unwrap();
+		std::fs::File::create("ah_hand.gif")
+			.unwrap()
+			.write_all(&expected_out)
+			.unwrap();
+
+		assert_eq!(actual_out, expected_out);
+	}
+}
diff --git a/gifed/src/lib.rs b/gifed/src/lib.rs
new file mode 100644
index 0000000..0a11fdc
--- /dev/null
+++ b/gifed/src/lib.rs
@@ -0,0 +1,51 @@
+mod color;
+mod colorimage;
+mod gif;
+mod lzw;
+
+pub mod block;
+pub mod reader;
+pub mod writer;
+
+use core::fmt;
+use std::error::Error;
+
+pub use color::Color;
+pub use colorimage::ColorImage;
+pub use gif::Gif;
+pub use lzw::LZW;
+
+/// 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 {
+	2usize.pow(packed as u32 + 1)
+}
+
+//TODO: Be sure to check that fields in LSD and Img. Desc. that were reserved
+//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 EncodingError {
+	TooManyColors,
+	NoColorTable,
+	IndicieSizeMismatch { expected: usize, got: usize },
+}
+
+impl fmt::Display for EncodingError {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		match self {
+			Self::TooManyColors => write!(f, "A palette is limited to 256 colors"),
+			Self::NoColorTable => write!(
+				f,
+				"Refusing to set the background color index when no color table is set!"
+			),
+			Self::IndicieSizeMismatch { expected, got } => {
+				write!(f, "Expected to have {} indicies but got {}", expected, got)
+			}
+		}
+	}
+}
+
+impl Error for EncodingError {}
diff --git a/gifed/src/lzw.rs b/gifed/src/lzw.rs
new file mode 100644
index 0000000..dce6a5d
--- /dev/null
+++ b/gifed/src/lzw.rs
@@ -0,0 +1,173 @@
+use std::collections::HashMap;
+
+pub struct LZW {}
+impl LZW {
+	pub fn encode(minimum_size: u8, indicies: &[u8]) -> Vec<u8> {
+		let mut dictionary: HashMap<Vec<u8>, u16> = HashMap::new();
+
+		let cc = 2u16.pow(minimum_size as u32);
+		let eoi = cc + 1;
+
+		// Fill dictionary with self-descriptive values
+		for value in 0..cc {
+			dictionary.insert(vec![value as u8], value);
+		}
+
+		let mut next_code = eoi + 1;
+		let mut code_size = minimum_size + 1;
+
+		let mut iter = indicies.into_iter();
+		let mut out = BitStream::new();
+		let mut buffer = vec![*iter.next().unwrap()];
+
+		out.push_bits(code_size, cc);
+
+		for &indicie in iter {
+			buffer.push(indicie);
+
+			if !dictionary.contains_key(&buffer) {
+				buffer.pop();
+
+				if let Some(&code) = dictionary.get(&buffer) {
+					out.push_bits(code_size, code);
+
+					// Put the code back and add the vec to the dict
+					buffer.push(indicie);
+					dictionary.insert(buffer.clone(), next_code);
+					next_code += 1;
+
+					// If the next_code can't fit in the code_size, we have to increase it
+					if next_code - 1 == 2u16.pow(code_size as u32) {
+						code_size += 1;
+					}
+
+					buffer.clear();
+					buffer.push(indicie);
+				} else {
+					unreachable!()
+				}
+			}
+		}
+
+		if buffer.len() > 0 {
+			match dictionary.get(&buffer) {
+				Some(&code) => out.push_bits(code_size, code),
+				None => {
+					panic!("Codes left in the buffer but the buffer is not a valid dictionary key!")
+				}
+			}
+		}
+		out.push_bits(code_size, eoi);
+
+		out.vec()
+	}
+}
+
+#[cfg(test)]
+mod lzw_test {
+	use super::*;
+
+	#[test]
+	fn encode() {
+		let indicies = vec![0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0];
+		let output = vec![0x84, 0x1D, 0x81, 0x7A, 0x50];
+
+		let lzout = LZW::encode(2, &indicies);
+
+		assert_eq!(lzout, output);
+	}
+
+	#[test]
+	fn against_weezl() {
+		let indicies = vec![0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0];
+		let weezl = weezl::encode::Encoder::new(weezl::BitOrder::Lsb, 2)
+			.encode(&indicies)
+			.unwrap();
+		let us = LZW::encode(2, &indicies);
+
+		assert_eq!(weezl, us);
+	}
+}
+
+struct BitStream {
+	formed: Vec<u8>,
+	current: u8,
+	index: u8,
+}
+
+impl BitStream {
+	fn new() -> Self {
+		Self {
+			formed: vec![],
+			current: 0,
+			index: 0,
+		}
+	}
+
+	fn push_bits(&mut self, count: u8, data: u16) {
+		let mut new_index = self.index + count;
+		let mut current32 = (self.current as u32) | ((data as u32) << self.index);
+
+		loop {
+			if new_index >= 8 {
+				self.formed.push(current32 as u8);
+				current32 = current32 >> 8;
+				new_index -= 8;
+			} else {
+				self.current = current32 as u8;
+				self.index = new_index;
+
+				break;
+			}
+		}
+	}
+
+	fn vec(self) -> Vec<u8> {
+		let mut out = self.formed;
+
+		if self.index != 0 {
+			out.push(self.current);
+		}
+
+		out
+	}
+}
+
+#[cfg(test)]
+mod bitstream_test {
+	use super::*;
+
+	#[test]
+	fn short_push() {
+		let mut bs = BitStream::new();
+		bs.push_bits(2, 3);
+		bs.push_bits(2, 3);
+		bs.push_bits(3, 1);
+		bs.push_bits(2, 3);
+
+		let bsvec = bs.vec();
+
+		for byte in &bsvec {
+			print!("{:b} ", byte);
+		}
+		println!("");
+
+		assert_eq!(bsvec, vec![0b1001_1111, 0b0000_0001]);
+	}
+
+	#[test]
+	fn long_push() {
+		let mut bs = BitStream::new();
+		bs.push_bits(1, 1);
+		bs.push_bits(12, 2049);
+
+		let bsvec = bs.vec();
+
+		for byte in &bsvec {
+			print!("{:b} ", byte);
+		}
+		println!("");
+
+		assert_eq!(bsvec, vec![0b0000_0011, 0b0001_0000]);
+	}
+}
diff --git a/gifed/src/reader/mod.rs b/gifed/src/reader/mod.rs
new file mode 100644
index 0000000..41494df
--- /dev/null
+++ b/gifed/src/reader/mod.rs
@@ -0,0 +1,274 @@
+use std::{
+	borrow::Cow,
+	convert::{TryFrom, TryInto},
+	error::Error,
+	fmt,
+	fs::File,
+	io::{BufRead, BufReader, Read},
+	path::Path,
+};
+
+use crate::{
+	block::{
+		extension::{Application, Extension, GraphicControl},
+		Block, ColorTable, CompressedImage, ImageDescriptor, IndexedImage, ScreenDescriptor,
+		Version,
+	},
+	color, Gif,
+};
+
+pub struct GifReader {}
+
+impl GifReader {
+	pub fn file<P: AsRef<Path>>(path: P) -> Result<Gif, DecodingError> {
+		let mut file = File::open(path)?;
+		let mut reader = SmartReader {
+			inner: vec![],
+			position: 0,
+		};
+		file.read_to_end(&mut reader.inner)?;
+
+		let mut gif = Self::read_required(&mut reader)?;
+
+		if gif.screen_descriptor.color_table_present() {
+			let gct_size = gif.screen_descriptor.color_table_len() * 3;
+			gif.global_color_table = Some(Self::read_color_table(&mut reader, gct_size)?);
+		}
+
+		loop {
+			match Self::read_block(&mut reader)? {
+				Some(block) => gif.blocks.push(block),
+				None => return Ok(gif),
+			}
+		}
+	}
+
+	fn read_required(reader: &mut SmartReader) -> Result<Gif, DecodingError> {
+		let version = match reader.take_lossy_utf8(6).as_deref() {
+			Some("GIF87a") => Version::Gif87a,
+			Some("GIF89a") => Version::Gif89a,
+			_ => return Err(DecodingError::UnknownVersionString),
+		};
+
+		let mut lsd_buffer: [u8; 7] = [0; 7];
+		reader
+			.read_exact(&mut lsd_buffer)
+			.ok_or(DecodingError::UnexpectedEof)?;
+
+		let lsd = ScreenDescriptor::from(lsd_buffer);
+
+		Ok(Gif {
+			header: version,
+			screen_descriptor: lsd,
+			global_color_table: None,
+			blocks: vec![],
+		})
+	}
+
+	fn read_color_table(
+		reader: &mut SmartReader,
+		size: usize,
+	) -> Result<ColorTable, DecodingError> {
+		let buffer = reader
+			.take(size as usize)
+			.ok_or(DecodingError::UnexpectedEof)?;
+
+		// We get the size from the screen descriptor. This should never return Err
+		Ok(ColorTable::try_from(&buffer[..]).unwrap())
+	}
+
+	fn read_block(reader: &mut SmartReader) -> Result<Option<Block>, DecodingError> {
+		let block_id = reader.u8().ok_or(DecodingError::UnexpectedEof)?;
+
+		//TODO: remove panic
+		match block_id {
+			0x21 => Self::read_extension(reader).map(|block| Some(block)),
+			0x2C => Self::read_image(reader).map(|block| Some(block)),
+			0x3B => Ok(None),
+			_ => panic!(
+				"Unknown block identifier {:X} {:X}",
+				block_id, reader.position
+			),
+		}
+	}
+
+	fn read_extension(reader: &mut SmartReader) -> Result<Block, DecodingError> {
+		let extension_id = reader.u8().expect("File ended early");
+
+		match extension_id {
+			0xF9 => {
+				reader.skip(1); // Skip block length, we know it
+				let mut data = [0u8; 4];
+				reader
+					.read_exact(&mut data)
+					.ok_or(DecodingError::UnexpectedEof)?;
+				reader.skip(1); // Skip block terminator
+
+				Ok(Block::Extension(Extension::GraphicControl(
+					GraphicControl::from(data),
+				)))
+			}
+			0xFE => Ok(Block::Extension(Extension::Comment(
+				reader.take_and_collapse_subblocks(),
+			))),
+			0x01 => todo!(), //TODO: do; plain text extension
+			0xFF => {
+				//TODO: error instead of unwraps
+				assert_eq!(Some(11), reader.u8());
+				let identifier = reader.take_lossy_utf8(8).unwrap().to_string();
+				let authentication_code: [u8; 3] =
+					TryInto::try_into(reader.take(3).unwrap()).unwrap();
+				let data = reader.take_and_collapse_subblocks();
+
+				Ok(Block::Extension(Extension::Application(Application {
+					identifier,
+					authentication_code,
+					data,
+				})))
+			}
+			_ => panic!("Unknown Extension Identifier!"),
+		}
+	}
+
+	fn read_image(mut reader: &mut SmartReader) -> Result<Block, DecodingError> {
+		let mut buffer = [0u8; 9];
+		reader
+			.read_exact(&mut buffer)
+			.ok_or(DecodingError::UnexpectedEof)?;
+		let descriptor = ImageDescriptor::from(buffer);
+
+		let color_table = if descriptor.color_table_present() {
+			let size = descriptor.color_table_size() * 3;
+			Some(Self::read_color_table(&mut reader, size)?)
+		} else {
+			None
+		};
+
+		let lzw_csize = reader.u8().ok_or(DecodingError::UnexpectedEof)?;
+
+		let compressed_data = reader.take_and_collapse_subblocks();
+
+		let mut decompress = weezl::decode::Decoder::new(weezl::BitOrder::Lsb, lzw_csize);
+		//TODO: remove unwrap
+		let mut decompressed_data = decompress.decode(&compressed_data).unwrap();
+
+		Ok(Block::IndexedImage(IndexedImage {
+			image_descriptor: descriptor,
+			local_color_table: color_table,
+			indicies: decompressed_data,
+		}))
+	}
+}
+
+#[derive(Debug)]
+pub enum DecodingError {
+	IoError(std::io::Error),
+	UnknownVersionString,
+	UnexpectedEof,
+	ColorIndexOutOfBounds,
+}
+
+impl Error for DecodingError {}
+impl fmt::Display for DecodingError {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		match self {
+			DecodingError::IoError(error) => write!(f, "{}", error),
+			DecodingError::UnknownVersionString => {
+				write!(f, "File did not start with a valid header")
+			}
+			DecodingError::UnexpectedEof => {
+				write!(f, "Found the end of the data at a weird spot")
+			}
+			DecodingError::ColorIndexOutOfBounds => {
+				write!(
+					f,
+					"The image contained an index not found in the color table"
+				)
+			}
+		}
+	}
+}
+
+impl From<std::io::Error> for DecodingError {
+	fn from(ioerror: std::io::Error) -> Self {
+		DecodingError::IoError(ioerror)
+	}
+}
+
+struct SmartReader {
+	inner: Vec<u8>,
+	position: usize,
+}
+
+impl SmartReader {
+	pub fn u8(&mut self) -> Option<u8> {
+		self.position += 1;
+		self.inner.get(self.position - 1).map(|b| *b)
+	}
+
+	pub fn u16(&mut self) -> Option<u16> {
+		self.position += 2;
+		self.inner
+			.get(self.position - 2..self.position)
+			.map(|bytes| u16::from_le_bytes(bytes.try_into().unwrap()))
+	}
+
+	pub fn skip(&mut self, size: usize) {
+		self.position += size;
+	}
+
+	pub fn take(&mut self, size: usize) -> Option<&[u8]> {
+		self.position += size;
+		self.inner.get(self.position - size..self.position)
+	}
+
+	//TODO: Result not Option when buffer len
+	pub fn read_exact(&mut self, buf: &mut [u8]) -> Option<()> {
+		if self.position + buf.len() > self.inner.len() {
+			None
+		} else {
+			self.position += buf.len();
+			buf.copy_from_slice(&self.inner[self.position - buf.len()..self.position]);
+			Some(())
+		}
+	}
+
+	pub fn take_vec(&mut self, size: usize) -> Option<Vec<u8>> {
+		self.position += size;
+		self.inner
+			.get(self.position - size..self.position)
+			.map(|bytes| bytes.to_vec())
+	}
+
+	pub fn take_lossy_utf8(&mut self, size: usize) -> Option<Cow<'_, str>> {
+		self.take(size).map(|bytes| String::from_utf8_lossy(bytes))
+	}
+
+	pub fn take_data_subblocks(&mut self) -> Vec<Vec<u8>> {
+		let mut blocks = vec![];
+
+		loop {
+			let block_size = self.u8().expect("Failed to read length of sublock");
+
+			if block_size == 0 {
+				return blocks;
+			}
+
+			let block = self
+				.take_vec(block_size as usize)
+				.expect("Failed to read sublock");
+
+			blocks.push(block);
+		}
+	}
+
+	pub fn take_and_collapse_subblocks(&mut self) -> Vec<u8> {
+		let blocks = self.take_data_subblocks();
+		let mut ret = vec![];
+		for block in blocks {
+			ret.extend_from_slice(&block)
+		}
+
+		ret
+	}
+}
diff --git a/gifed/src/writer/gifbuilder.rs b/gifed/src/writer/gifbuilder.rs
new file mode 100644
index 0000000..57a62e3
--- /dev/null
+++ b/gifed/src/writer/gifbuilder.rs
@@ -0,0 +1,106 @@
+use std::convert::TryInto;
+
+use crate::block::{extension::Extension, Block, ColorTable, ScreenDescriptor, Version};
+use crate::writer::ImageBuilder;
+use crate::{EncodingError, Gif};
+
+pub struct GifBuilder {
+	version: Version,
+	width: u16,
+	height: u16,
+	background_color_index: u8,
+	global_color_table: Option<ColorTable>,
+	blocks: Vec<Block>,
+	error: Option<EncodingError>,
+}
+
+impl GifBuilder {
+	pub fn new(width: u16, height: u16) -> Self {
+		Self {
+			version: Version::Gif87a,
+			width,
+			height,
+			background_color_index: 0,
+			global_color_table: None,
+			blocks: vec![],
+			error: None,
+		}
+	}
+
+	pub fn palette(mut self, palette: ColorTable) -> Self {
+		self.global_color_table = Some(palette);
+		self
+	}
+
+	pub fn background_index(mut self, ind: u8) -> Self {
+		if self.error.is_some() {
+			return self;
+		}
+
+		if self.global_color_table.is_none() {
+			self.error = Some(EncodingError::NoColorTable);
+		} else {
+			self.background_color_index = ind;
+		}
+		self
+	}
+
+	pub fn image(mut self, ib: ImageBuilder) -> Self {
+		if self.error.is_some() {
+			return self;
+		}
+
+		if ib.required_version() == Version::Gif89a {
+			self.version = Version::Gif89a;
+		}
+
+		if let Some(gce) = ib.get_graphic_control() {
+			self.blocks.push(Block::Extension(gce.into()));
+		}
+
+		match ib.build() {
+			Ok(image) => self.blocks.push(Block::IndexedImage(image)),
+			Err(e) => self.error = Some(e),
+		}
+
+		self
+	}
+
+	/*pub fn extension(mut self, ext: Extension) -> Self {
+		self.blocks.push(Block::Extension(ext));
+		self
+	}*/
+
+	pub fn repeat(mut self, count: u16) -> Self {
+		self.blocks
+			.push(Block::Extension(Extension::Looping(count)));
+		self
+	}
+
+	pub fn build(self) -> Result<Gif, EncodingError> {
+		if let Some(error) = self.error {
+			return Err(error);
+		}
+
+		let mut lsd = ScreenDescriptor {
+			width: self.width,
+			height: self.height,
+			packed: 0, // Set later
+			background_color_index: self.background_color_index,
+			pixel_aspect_ratio: 0, //TODO: Allow configuring
+		};
+
+		if let Some(gct) = &self.global_color_table {
+			println!("build {}", gct.len());
+			lsd.set_color_table_present(true);
+			lsd.set_color_table_size((gct.len() - 1) as u8);
+		}
+
+		Ok(Gif {
+			header: self.version,
+			screen_descriptor: lsd,
+			global_color_table: self.global_color_table,
+			blocks: self.blocks,
+		})
+	}
+}
diff --git a/gifed/src/writer/imagebuilder.rs b/gifed/src/writer/imagebuilder.rs
new file mode 100644
index 0000000..f5c9e2b
--- /dev/null
+++ b/gifed/src/writer/imagebuilder.rs
@@ -0,0 +1,133 @@
+use crate::{
+	block::{
+		extension::{DisposalMethod, GraphicControl},
+		ColorTable, ImageDescriptor, IndexedImage, Version,
+	},
+	EncodingError,
+};
+
+pub struct ImageBuilder {
+	left_offset: u16,
+	top_offset: u16,
+	width: u16,
+	height: u16,
+	color_table: Option<ColorTable>,
+
+	delay: u16,
+	disposal_method: DisposalMethod,
+	transparent_index: Option<u8>,
+
+	indicies: Vec<u8>,
+}
+
+impl ImageBuilder {
+	pub fn new(width: u16, height: u16) -> Self {
+		Self {
+			left_offset: 0,
+			top_offset: 0,
+			width,
+			height,
+			color_table: None,
+			delay: 0,
+			disposal_method: DisposalMethod::NoAction,
+			transparent_index: None,
+			indicies: vec![],
+		}
+	}
+
+	pub fn offset(mut self, left: u16, top: u16) -> Self {
+		self.left_offset = left;
+		self.top_offset = top;
+		self
+	}
+
+	pub fn palette(mut self, table: ColorTable) -> Self {
+		self.color_table = Some(table);
+		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;
+		self
+	}
+
+	pub fn disposal_method(mut self, method: DisposalMethod) -> Self {
+		self.disposal_method = method;
+		self
+	}
+
+	pub fn transparent_index(mut self, index: Option<u8>) -> Self {
+		self.transparent_index = index;
+		self
+	}
+
+	pub fn required_version(&self) -> Version {
+		if self.delay > 0
+			|| self.disposal_method != DisposalMethod::NoAction
+			|| self.transparent_index.is_some()
+		{
+			Version::Gif89a
+		} else {
+			Version::Gif87a
+		}
+	}
+
+	pub fn get_graphic_control(&self) -> Option<GraphicControl> {
+		if self.required_version() == Version::Gif89a {
+			if let Some(transindex) = self.transparent_index {
+				Some(GraphicControl::new(
+					self.disposal_method,
+					false,
+					true,
+					self.delay,
+					transindex,
+				))
+			} else {
+				Some(GraphicControl::new(
+					self.disposal_method,
+					false,
+					false,
+					self.delay,
+					0,
+				))
+			}
+		} else {
+			None
+		}
+	}
+
+	pub fn indicies(mut self, indicies: &[u8]) -> Self {
+		self.indicies = indicies.to_vec();
+		self
+	}
+
+	pub fn build(self) -> Result<IndexedImage, EncodingError> {
+		let expected_len = self.width as usize * self.height as usize;
+		if self.indicies.len() != expected_len {
+			return Err(EncodingError::IndicieSizeMismatch {
+				expected: expected_len,
+				got: self.indicies.len(),
+			});
+		}
+
+		let mut imgdesc = ImageDescriptor {
+			left: self.left_offset,
+			top: self.top_offset,
+			width: self.width,
+			height: self.height,
+			packed: 0, // Set later
+		};
+
+		if let Some(lct) = &self.color_table {
+			imgdesc.set_color_table_present(true);
+			imgdesc.set_color_table_size(lct.packed_len());
+		}
+
+		Ok(IndexedImage {
+			image_descriptor: imgdesc,
+			local_color_table: self.color_table,
+			indicies: self.indicies,
+		})
+	}
+}
diff --git a/gifed/src/writer/mod.rs b/gifed/src/writer/mod.rs
new file mode 100644
index 0000000..88311fc
--- /dev/null
+++ b/gifed/src/writer/mod.rs
@@ -0,0 +1,5 @@
+mod gifbuilder;
+mod imagebuilder;
+
+pub use gifbuilder::GifBuilder;
+pub use imagebuilder::ImageBuilder;