diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | Cargo.toml | 9 | ||||
-rw-r--r-- | src/common/mod.rs | 20 | ||||
-rw-r--r-- | src/lib.rs | 5 | ||||
-rw-r--r-- | src/writer/gifbuilder.rs | 238 | ||||
-rw-r--r-- | src/writer/imagebuilder.rs | 196 | ||||
-rw-r--r-- | src/writer/lzw.rs | 175 | ||||
-rw-r--r-- | src/writer/mod.rs | 9 | ||||
-rw-r--r-- | src/writer/outvec.rs | 64 |
9 files changed, 718 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..dc07e9a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "gifed" +version = "0.1.0" +authors = ["Brad Alfirevic <brad@genbyte.dev>"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/src/common/mod.rs b/src/common/mod.rs new file mode 100644 index 0000000..1080e29 --- /dev/null +++ b/src/common/mod.rs @@ -0,0 +1,20 @@ +pub enum Version { + Gif87a, + Gif89a +} + +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 + } + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..187decb --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,5 @@ +mod common; +mod writer; + +pub use common::Version; +pub use writer::GifBuilder; \ No newline at end of file diff --git a/src/writer/gifbuilder.rs b/src/writer/gifbuilder.rs new file mode 100644 index 0000000..ff98ba7 --- /dev/null +++ b/src/writer/gifbuilder.rs @@ -0,0 +1,238 @@ +use crate::common::{Color, Version}; +use super::OutVec; +use super::ImageBuilder; + +pub struct GifBuilder { + version: Version, + width: u16, + height: u16, + global_color_table: Option<Vec<Color>>, + background_color_index: u8, + imagebuilders: Vec<ImageBuilder> +} + +impl GifBuilder { + pub fn new(version: Version, width: u16, height: u16) -> Self { + Self { + version, + width, + height, + global_color_table: None, + background_color_index: 0, + imagebuilders: vec![] + } + } + + pub fn global_color_table(mut self, vec: Vec<Color>) -> Self { + if vec.len() == 0 || vec.len() > 256 { + //TODO: Throw error instead of panic + panic!("GCT has to be less than or 256 colors in size, and at least one"); + } + + self.global_color_table = Some(vec); + + self + } + + pub fn background_color_index(mut self, ind: u8) -> Self { + if self.global_color_table.is_none() { + //TODO: Throw error or let it go by, who knows + panic!("Setting background color index with noGCT!"); + } + + self.background_color_index = ind; + self + } + + pub fn image(mut self, ib: ImageBuilder) -> Self { + self.imagebuilders.push(ib); + self + } + + pub fn write_to_vec(&self) -> Vec<u8> { + let mut out = OutVec::new(); + + self + .write_header(&mut out) + .write_logical_screen_descriptor(&mut out) + .write_global_color_table(&mut out) + .write_images(&mut out) + .write_trailer(&mut out); + + out.vec() + } + + fn write_header(&self, out: &mut OutVec) -> &Self { + out.push_slice(b"GIF"); + + //TODO: Automatically detect version + match self.version { + Version::Gif87a => out.push_slice(b"87a"), + Version::Gif89a => out.push_slice(b"89a") + }; + + self + } + + fn write_logical_screen_descriptor(&self, out: &mut OutVec) -> &Self { + out.push_u16(self.width).push_u16(self.height); + + let mut packed: u8 = 0; + + if let Some(gct) = &self.global_color_table { + packed |= 0b1000_0000; // Set the GCT flag + + // GCT size is calulated by raising two to this number plus one, + // so we have to work backwards. + let size = (gct.len() as f32).log2().ceil() - 1f32; + + packed |= size as u8; + } + //TODO: Color Resolution and Sort Flag fields in packed. + out.push_u8(packed); + + out.push_u8(self.background_color_index) + .push_u8(0x00); //TOOD: Allow setting pixel aspect ratio + + self + } + + fn write_global_color_table(&self, out: &mut OutVec) -> &Self { + if let Some(gct) = &self.global_color_table { + out.push_colors(&gct); + } + + self + } + + fn write_images(&self, out: &mut OutVec) -> &Self { + //TODO: Deduplicate color table size code + let mcs = if let Some(gct) = &self.global_color_table { + ((gct.len() as f32).log2().ceil() - 1f32) as u8 + } else { + 0 + }; + + for ib in &self.imagebuilders { + let image = ib.write_to_vec(mcs); + out.push_slice(&image); + } + + self + } + + fn write_trailer(&self, out: &mut OutVec) { + /* + "This block is a single-field block indicating the end of the GIF Data Stream. + It contains the fixed value 0x3B." + */ + out.push_u8(0x3B); + } +} + +#[cfg(test)] +pub mod gifbuilder_test { + use super::*; + + #[test] + pub fn writer_header() { + let mut out = OutVec::new(); + GifBuilder::new(Version::Gif87a, 0, 0).write_header(&mut out); + + assert_eq!(out.vec(), b"GIF87a"); + } + + #[test] + pub fn write_logical_screen_descriptor() { + let mut out = OutVec::new(); + GifBuilder::new(Version::Gif87a, 4, 4).write_logical_screen_descriptor(&mut out); + + assert_eq!(out.vec(), vec![0x04, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00]); + + let mut out = OutVec::new(); + let gct = vec![ + Color::new(0, 0, 0), Color::new(0, 0, 0), Color::new(0, 0, 0), + Color::new(0, 0, 0), Color::new(0, 0, 0) + ]; + GifBuilder::new(Version::Gif87a, 4, 4).global_color_table(gct).write_logical_screen_descriptor(&mut out); + + assert_eq!(out.vec(), vec![0x04, 0x00, 0x04, 0x00, 0b1000_0010, 0x00, 0x00]); + } + + #[test] + pub fn write_global_color_table() { + let mut out = OutVec::new(); + let gct = vec![ + Color::new(1, 2, 3), Color::new(253, 254, 255) + ]; + GifBuilder::new(Version::Gif87a, 4, 4).global_color_table(gct).write_global_color_table(&mut out); + + assert_eq!(out.vec(), vec![1, 2, 3, 253, 254, 255]); + } + + #[test] + fn write_images() { + 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![ + 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 + ]; + + let mut out = OutVec::new(); + GifBuilder::new(Version::Gif87a, 4, 4) + .global_color_table(gct) + .image(ImageBuilder::new(4, 4) + .color_table(colortable) + .indicies(indicies.clone()) + ).image(ImageBuilder::new(4, 4) + .indicies(indicies) + ).write_images(&mut out); + + assert_eq!(out.vec(), expected_out); + } + + #[test] + fn write_to_vec() { + 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 mut out = OutVec::new(); + let actual_out = GifBuilder::new(Version::Gif87a, 4, 4) + .global_color_table(gct) + .image(ImageBuilder::new(4, 4) + .color_table(colortable) + .indicies(indicies.clone()) + ).image(ImageBuilder::new(4, 4) + .indicies(indicies) + ).write_to_vec(); + + assert_eq!(actual_out, expected_out); + } +} \ No newline at end of file diff --git a/src/writer/imagebuilder.rs b/src/writer/imagebuilder.rs new file mode 100644 index 0000000..cd96908 --- /dev/null +++ b/src/writer/imagebuilder.rs @@ -0,0 +1,196 @@ +use crate::common::Color; +use super::OutVec; +use super::LZW; + +pub struct ImageBuilder { + left_offset: u16, + top_offset: u16, + width: u16, + height: u16, + color_table: Option<Vec<Color>>, + 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, + indicies: vec![] + } + } + + pub fn offsets(mut self, left_offset: u16, top_offset: u16) -> Self { + self.left_offset = left_offset; + self.top_offset = top_offset; + self + } + + pub fn left_offset(mut self, offset: u16) -> Self { + self.left_offset = offset; + self + } + + pub fn top_offset(mut self, offset: u16) -> Self { + self.top_offset = offset; + self + } + + pub fn color_table(mut self, vec: Vec<Color>) -> Self { + if vec.len() == 0 || vec.len() > 256 { + //TODO: Throw error instead of panic + panic!("Color table has to be less than or 256 colors in size, and at least one"); + } + + self.color_table = Some(vec); + + self + } + + pub fn indicies(mut self, vec: Vec<u8>) -> Self { + self.indicies = vec; + self + } + + //TODO: Make lzw_minimum_code_size optional. ONly needed with global color tables + pub fn write_to_vec(&self, lzw_minimum_code_size: u8) -> Vec<u8> { + let mut out = OutVec::new(); + + self.write_image_descriptor(&mut out) + .write_color_table(&mut out) + .write_image_data(&mut out, lzw_minimum_code_size); + + out.vec() + } + + fn write_image_descriptor(&self, out: &mut OutVec) -> &Self { + // Image seperator. At the start of every image descriptor + out.push_u8(0x2C) + .push_u16(self.left_offset) + .push_u16(self.top_offset) + .push_u16(self.width) + .push_u16(self.height); + + // Taken from gifbuilder.rs + //TODO: deduplciate code + let mut packed: u8 = 0; + if let Some(ct) = &self.color_table { + packed |= 0b1000_0000; // Set the color table flag + + let size = (ct.len() as f32).log2().ceil() - 1f32; + + packed |= size as u8; + } + //TODO: Interlace and Sort flags in packed + out.push_u8(packed); + + self + } + + fn write_color_table(&self, out: &mut OutVec) -> &Self { + if let Some(ct) = &self.color_table { + out.push_colors(&ct); + } + + self + } + + fn write_image_data(&self, out: &mut OutVec, minimum_code_size: u8) -> &Self { + let mut mcs = minimum_code_size; + + //TODO: Deduplicate color table size code + if let Some(ct) = &self.color_table { + mcs = ((ct.len() as f32).log2().ceil() - 1f32) as u8; + } + + if mcs < 2 { + // Must always be true: 2 <= mcs <= 8 + mcs = 2; + } + + // First write out the MCS + out.push_u8(mcs); + + let compressed = LZW::encode(mcs, &self.indicies); + + for chunk in compressed.chunks(255) { + out.push_u8(chunk.len() as u8); + out.push_slice(chunk); + } + // Data block length 0 to indicate an end + out.push_u8(0x00); + + self + } +} + +#[cfg(test)] +mod imagebuilder_test { + use super::*; + + #[test] + fn write_to_vec() { + 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![ + 0x2C, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x04, 0x00, 0b1000_0000, // Image Descriptor + 0, 0, 0, 128, 0, 255, // Color Table + 0x02, 0x05, 0x84, 0x1D, 0x81, 0x7A, 0x50, 0x00 // Image Data + ]; + let actual_out = ImageBuilder::new(4, 4) + .color_table(colortable) + .indicies(indicies) + .write_to_vec(0); + + assert_eq!(actual_out, expected_out); + } + + #[test] + fn write_image_descriptor() { + let mut out = OutVec::new(); + ImageBuilder::new(16, 16).offsets(1, 6).write_image_descriptor(&mut out); + + assert_eq!(out.vec(), vec![0x2C, 0x01, 0x00, 0x06, 0x00, 0x10, 0x00, 0x10, 0x00, 0x00]); + + let mut out = OutVec::new(); + ImageBuilder::new(16, 16) + .offsets(1, 6) + .color_table(vec![Color::new(0, 0, 0)]) + .write_image_descriptor(&mut out); + + assert_eq!(out.vec(), vec![0x2C, 0x01, 0x00, 0x06, 0x00, 0x10, 0x00, 0x10, 0x00, 0b1000_0000]); + } + + #[test] + fn write_color_table() { + let mut out = OutVec::new(); + ImageBuilder::new(16, 16) + .color_table(vec![Color::new(1, 2, 3), Color::new(253, 254, 255)]) + .write_color_table(&mut out); + + assert_eq!(out.vec(), vec![0x01, 0x02, 0x03, 0xFD, 0xFE, 0xFF]); + } + + #[test] + fn write_image_data() { + #[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![0x02, 0x05, 0x84, 0x1D, 0x81, 0x7A, 0x50, 0x00]; + + let mut out = OutVec::new(); + ImageBuilder::new(16, 16) + .indicies(indicies) + .write_image_data(&mut out, 2); + + assert_eq!(out.vec(), output); + } + } +} \ No newline at end of file diff --git a/src/writer/lzw.rs b/src/writer/lzw.rs new file mode 100644 index 0000000..1970617 --- /dev/null +++ b/src/writer/lzw.rs @@ -0,0 +1,175 @@ +use std::collections::HashMap; + +pub struct LZW {} +impl LZW { + //TODO: Cleanup and make not awful + pub fn encode(minimum_size: u8, indicies: &[u8]) -> Vec<u8> { + let mut dictionary: HashMap<Vec<u8>, u16> = HashMap::new(); + + let cc: u16 = 2u16.pow(minimum_size as u32); + let eoi: u16 = cc+1; + + let mut index: u16 = eoi+1; // Next code number + let mut codesize: u8 = minimum_size+1; // Current code size + + //println!("cc: {} - eoi: {}", cc, eoi); + + // Populate starting codes + for code in 0..cc { + dictionary.insert(vec![code as u8], code); + } + + let mut iter = indicies.iter(); + let mut out = BitStream::new(); + let mut buffer = vec![*iter.next().unwrap()]; //TODO: Remove unwrap + + //"Encoders should output a Clear code as the first code of each image data stream." + out.push_bits(codesize, cc); + + //println!("Before Loop\n\tBuffer: {:?}\n\tCodesize:{}\n\tIndex:{}", buffer, codesize, index); + + for next_code in iter { + buffer.push(*next_code); + + //println!("Buffer: {:?} - Codesize:{} - Index:{}\n\tDict: {:?}", buffer, codesize, index, dictionary); + + match dictionary.get(&buffer) { + Some(_code) => { + //println!("\tPresent!"); + continue; + }, + None => { + buffer.pop(); + match dictionary.get(&buffer) { + Some(code) => { + out.push_bits(codesize, *code); + //println!("\tOutputting {} with {} bits. Buffer is {:?}", *code, codesize, buffer); + + // Add new entry for buffer and increase the index + buffer.push(*next_code); + dictionary.insert(buffer, index); + index += 1; + + // Increase code size if we should + if index-1 == 2u16.pow(codesize as u32) { + codesize += 1; + } + + // Reset the buffer to only contain next_code + buffer = vec![*next_code]; + }, + None => panic!("No dictionary entry when there should be! Something is wrong!") + } + } + } + } + + if buffer.len() > 0 { + match dictionary.get(&buffer) { + None => panic!("No codes left but not in the dictionary!"), + Some(code) => out.push_bits(codesize, *code) + } + } + out.push_bits(codesize, 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); + } +} + +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]); + } +} \ No newline at end of file diff --git a/src/writer/mod.rs b/src/writer/mod.rs new file mode 100644 index 0000000..965dcfa --- /dev/null +++ b/src/writer/mod.rs @@ -0,0 +1,9 @@ +mod outvec; +mod gifbuilder; +mod lzw; +mod imagebuilder; + +use outvec::OutVec; +pub use gifbuilder::GifBuilder; +use lzw::LZW; +pub use imagebuilder::ImageBuilder; \ No newline at end of file diff --git a/src/writer/outvec.rs b/src/writer/outvec.rs new file mode 100644 index 0000000..eb04934 --- /dev/null +++ b/src/writer/outvec.rs @@ -0,0 +1,64 @@ +use crate::common::Color; + +#[derive(Debug, PartialEq)] +pub struct OutVec { + data: Vec<u8> +} + +impl OutVec { + pub fn new() -> Self { + Self { + data: vec![] + } + } + + pub fn push_u8(&mut self, n: u8) -> &mut Self { + self.data.push(n); + self + } + + pub fn push_u16(&mut self, n: u16) -> &mut Self { + self.data.extend_from_slice(&n.to_le_bytes()); + self + } + + pub fn push_u32(&mut self, n: u32) -> &mut Self { + self.data.extend_from_slice(&n.to_le_bytes()); + self + } + + pub fn push_u64(&mut self, n: u64) -> &mut Self { + self.data.extend_from_slice(&n.to_le_bytes()); + self + } + + pub fn push_slice(&mut self, slice: &[u8]) -> &mut Self { + self.data.extend_from_slice(slice); + self + } + + pub fn push_color(&mut self, color: &Color) -> &mut Self { + self.data.extend_from_slice(&[color.r, color.g, color.b]); + self + } + + pub fn push_colors(&mut self, colors: &[Color]) -> &mut Self { + for color in colors { + self.push_color(color); + } + self + } + + pub fn vec(self) -> Vec<u8> { + self.data + } +} + +impl From<Vec<u8>> for OutVec { + fn from(v: Vec<u8>) -> Self { + let mut outvec = Self::new(); + outvec.push_slice(&v); + + outvec + } +} \ No newline at end of file |