From 1de64a3818875947f7f1044b1d4cfdf271b04fd3 Mon Sep 17 00:00:00 2001 From: Genny Date: Sun, 21 Nov 2021 18:35:57 -0600 Subject: Bring gifprobe into this repository --- .gitignore | 2 +- Cargo.toml | 24 +-- README.md | 10 +- benches/lzw_encode.rs | 23 --- examples/read.rs | 22 -- gifed/Cargo.toml | 19 ++ gifed/benches/lzw_encode.rs | 23 +++ gifed/examples/read.rs | 22 ++ gifed/src/block/colortable.rs | 117 +++++++++++ gifed/src/block/extension/application.rs | 15 ++ gifed/src/block/extension/graphiccontrol.rs | 116 +++++++++++ gifed/src/block/extension/mod.rs | 49 +++++ gifed/src/block/imagedescriptor.rs | 73 +++++++ gifed/src/block/indexedimage.rs | 70 +++++++ gifed/src/block/mod.rs | 25 +++ gifed/src/block/screendescriptor.rs | 79 +++++++ gifed/src/block/version.rs | 25 +++ gifed/src/color.rs | 38 ++++ gifed/src/colorimage.rs | 61 ++++++ gifed/src/gif.rs | 306 ++++++++++++++++++++++++++++ gifed/src/lib.rs | 51 +++++ gifed/src/lzw.rs | 173 ++++++++++++++++ gifed/src/reader/mod.rs | 274 +++++++++++++++++++++++++ gifed/src/writer/gifbuilder.rs | 106 ++++++++++ gifed/src/writer/imagebuilder.rs | 133 ++++++++++++ gifed/src/writer/mod.rs | 5 + gifprobe/Cargo.toml | 14 ++ gifprobe/src/main.rs | 107 ++++++++++ src/block/colortable.rs | 117 ----------- src/block/extension/application.rs | 15 -- src/block/extension/graphiccontrol.rs | 116 ----------- src/block/extension/mod.rs | 49 ----- src/block/imagedescriptor.rs | 73 ------- src/block/indexedimage.rs | 70 ------- src/block/mod.rs | 25 --- src/block/screendescriptor.rs | 79 ------- src/block/version.rs | 25 --- src/color.rs | 38 ---- src/colorimage.rs | 61 ------ src/gif.rs | 290 -------------------------- src/lib.rs | 51 ----- src/lzw.rs | 173 ---------------- src/reader/mod.rs | 274 ------------------------- src/writer/gifbuilder.rs | 106 ---------- src/writer/imagebuilder.rs | 133 ------------ src/writer/mod.rs | 5 - 46 files changed, 1915 insertions(+), 1767 deletions(-) delete mode 100644 benches/lzw_encode.rs delete mode 100644 examples/read.rs create mode 100644 gifed/Cargo.toml create mode 100644 gifed/benches/lzw_encode.rs create mode 100644 gifed/examples/read.rs create mode 100644 gifed/src/block/colortable.rs create mode 100644 gifed/src/block/extension/application.rs create mode 100644 gifed/src/block/extension/graphiccontrol.rs create mode 100644 gifed/src/block/extension/mod.rs create mode 100644 gifed/src/block/imagedescriptor.rs create mode 100644 gifed/src/block/indexedimage.rs create mode 100644 gifed/src/block/mod.rs create mode 100644 gifed/src/block/screendescriptor.rs create mode 100644 gifed/src/block/version.rs create mode 100644 gifed/src/color.rs create mode 100644 gifed/src/colorimage.rs create mode 100644 gifed/src/gif.rs create mode 100644 gifed/src/lib.rs create mode 100644 gifed/src/lzw.rs create mode 100644 gifed/src/reader/mod.rs create mode 100644 gifed/src/writer/gifbuilder.rs create mode 100644 gifed/src/writer/imagebuilder.rs create mode 100644 gifed/src/writer/mod.rs create mode 100644 gifprobe/Cargo.toml create mode 100644 gifprobe/src/main.rs delete mode 100644 src/block/colortable.rs delete mode 100644 src/block/extension/application.rs delete mode 100644 src/block/extension/graphiccontrol.rs delete mode 100644 src/block/extension/mod.rs delete mode 100644 src/block/imagedescriptor.rs delete mode 100644 src/block/indexedimage.rs delete mode 100644 src/block/mod.rs delete mode 100644 src/block/screendescriptor.rs delete mode 100644 src/block/version.rs delete mode 100644 src/color.rs delete mode 100644 src/colorimage.rs delete mode 100644 src/gif.rs delete mode 100644 src/lib.rs delete mode 100644 src/lzw.rs delete mode 100644 src/reader/mod.rs delete mode 100644 src/writer/gifbuilder.rs delete mode 100644 src/writer/imagebuilder.rs delete mode 100644 src/writer/mod.rs diff --git a/.gitignore b/.gitignore index e3b0c5d..4699ed7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ /.vscode -/target +**/target Cargo.lock *.gif *.txt diff --git a/Cargo.toml b/Cargo.toml index 295a943..2a5d22b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,5 @@ -[package] -name = "gifed" -version = "0.1.0" -authors = ["Brad Alfirevic "] -edition = "2018" -license = "CC0-1.0" -description = "Gif encoding and decoding with fine control" -repository = "https://github.com/genuinebyte/gifed" - -[dev-dependencies] -criterion = "0.3" -png = "0.17.2" - -[dependencies] -weezl = "0.1.5" - -[[bench]] -name = "lzw_encode" -harness = false \ No newline at end of file +[workspace] +members = [ + "gifed", + "gifprobe" +] \ No newline at end of file diff --git a/README.md b/README.md index b65f891..ca9d927 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ This crate is still pretty rough. I hope to make it feature complete and intuitive to use while still allowing fine grained control over the file itself, which has been and always will be, one of the main goals of this crate. -### TODO +### Gifed TODO - [x] Writing GIF87a - [x] Writing GIF89a - [x] Automatically select the lowest version possible when writing @@ -18,4 +18,10 @@ always will be, one of the main goals of this crate. - [ ] Well written and easy to understand docs! `bitvec` quality, but who can match that? [weezl-crates]: https://crates.io/crates/weezl -[rgb-crates]: https://crates.io/crates/rgb \ No newline at end of file +[rgb-crates]: https://crates.io/crates/rgb + +## gifprobe +Similar to FFMPEG's ffprobe, gifprobe will print details of a gif to stdout. + +# License +gided and gifprobe are licensed under Creative Commons Zero 1.0; they're in the public domain. Attribution is appreciated, but not required. \ No newline at end of file diff --git a/benches/lzw_encode.rs b/benches/lzw_encode.rs deleted file mode 100644 index 69ed275..0000000 --- a/benches/lzw_encode.rs +++ /dev/null @@ -1,23 +0,0 @@ -use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use gifed::LZW; -use rand::{thread_rng, Rng}; -use weezl::{encode::Encoder, BitOrder}; - -pub fn criterion_benchmark(c: &mut Criterion) { - let mut random = [0u8; 2048]; - thread_rng().fill(&mut random[..]); - - c.bench_function("lzw encode 255bytes", |b| { - b.iter(|| LZW::encode(8, black_box(&random))) - }); - c.bench_function("weezl encode 255bytes", |b| { - b.iter(|| { - Encoder::new(BitOrder::Msb, 8) - .encode(black_box(&random)) - .unwrap() - }) - }); -} - -criterion_group!(benches, criterion_benchmark); -criterion_main!(benches); diff --git a/examples/read.rs b/examples/read.rs deleted file mode 100644 index 3c40bfe..0000000 --- a/examples/read.rs +++ /dev/null @@ -1,22 +0,0 @@ -use gifed::{ - reader::{self, GifReader}, - writer::ImageBuilder, - Gif, -}; - -fn main() { - let reader = GifReader::file("examples/simulation.gif").unwrap(); - let first = reader.images().next().unwrap(); - - Gif::builder(first.width(), first.height()) - .palette(first.palette().clone()) - .image( - ImageBuilder::new(first.width(), first.height()) - .transparent_index(first.transparent_index()) - .indicies(first.indicies()), - ) - .build() - .unwrap() - .save("first.gif") - .unwrap(); -} diff --git a/gifed/Cargo.toml b/gifed/Cargo.toml new file mode 100644 index 0000000..08974f5 --- /dev/null +++ b/gifed/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "gifed" +version = "0.1.0" +authors = ["Genevive Alfirevic "] +edition = "2018" +license = "CC0-1.0" +description = "Gif encoding and decoding with fine control" +repository = "https://github.com/genuinebyte/gifed" + +[dev-dependencies] +criterion = "0.3" +png = "0.17.2" + +[dependencies] +weezl = "0.1.5" + +[[bench]] +name = "lzw_encode" +harness = false \ No newline at end of file diff --git a/gifed/benches/lzw_encode.rs b/gifed/benches/lzw_encode.rs new file mode 100644 index 0000000..69ed275 --- /dev/null +++ b/gifed/benches/lzw_encode.rs @@ -0,0 +1,23 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use gifed::LZW; +use rand::{thread_rng, Rng}; +use weezl::{encode::Encoder, BitOrder}; + +pub fn criterion_benchmark(c: &mut Criterion) { + let mut random = [0u8; 2048]; + thread_rng().fill(&mut random[..]); + + c.bench_function("lzw encode 255bytes", |b| { + b.iter(|| LZW::encode(8, black_box(&random))) + }); + c.bench_function("weezl encode 255bytes", |b| { + b.iter(|| { + Encoder::new(BitOrder::Msb, 8) + .encode(black_box(&random)) + .unwrap() + }) + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/gifed/examples/read.rs b/gifed/examples/read.rs new file mode 100644 index 0000000..3c40bfe --- /dev/null +++ b/gifed/examples/read.rs @@ -0,0 +1,22 @@ +use gifed::{ + reader::{self, GifReader}, + writer::ImageBuilder, + Gif, +}; + +fn main() { + let reader = GifReader::file("examples/simulation.gif").unwrap(); + let first = reader.images().next().unwrap(); + + Gif::builder(first.width(), first.height()) + .palette(first.palette().clone()) + .image( + ImageBuilder::new(first.width(), first.height()) + .transparent_index(first.transparent_index()) + .indicies(first.indicies()), + ) + .build() + .unwrap() + .save("first.gif") + .unwrap(); +} 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, +} + +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 { + self.table.get(index as usize).map(|v| v.clone()) + } + + 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 + } +} + +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 (must be multiple of 3 len) and From Vec +impl TryFrom<&[u8]> for ColorTable { + type Error = (); + + fn try_from(value: &[u8]) -> Result { + 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::>(), + }) + } + } +} + +impl TryFrom> for ColorTable { + type Error = EncodingError; + + fn try_from(value: Vec) -> Result { + if value.len() > 256 { + Err(EncodingError::TooManyColors) + } else { + Ok(Self { table: value }) + } + } +} + +impl TryFrom> for ColorTable { + type Error = EncodingError; + + fn try_from(value: Vec<(u8, u8, u8)>) -> Result { + 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, +} + +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 { + 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), // 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 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, + pub indicies: Vec, +} + +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, + pub lzw_minimum_code_size: u8, + pub blocks: Vec>, +} 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, +} + +impl ColorImage { + pub(crate) fn from_indicies( + width: u16, + height: u16, + indicies: &[u8], + table: &ColorTable, + transindex: Option, + ) -> Result { + 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> for ColorImage { + type Error = DecodingError; + + fn try_from(img: Image<'a>) -> Result { + 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, + pub blocks: Vec, // 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 { + 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>(&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 { + 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, +} + +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, + 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 { + self.transparent_index + } + + pub fn indicies(&self) -> &[u8] { + self.indicies + } +} + +pub struct Frame { + width: u16, + height: u16, + palette: ColorTable, + transparent_index: Option, + indicies: Vec, + 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 { + let mut dictionary: HashMap, 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, + 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 { + 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>(path: P) -> Result { + 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 { + 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 { + 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, 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 { + 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 { + 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 for DecodingError { + fn from(ioerror: std::io::Error) -> Self { + DecodingError::IoError(ioerror) + } +} + +struct SmartReader { + inner: Vec, + position: usize, +} + +impl SmartReader { + pub fn u8(&mut self) -> Option { + self.position += 1; + self.inner.get(self.position - 1).map(|b| *b) + } + + pub fn u16(&mut self) -> Option { + 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> { + 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> { + self.take(size).map(|bytes| String::from_utf8_lossy(bytes)) + } + + pub fn take_data_subblocks(&mut self) -> Vec> { + 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 { + 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, + blocks: Vec, + error: Option, +} + +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 { + 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, + + delay: u16, + disposal_method: DisposalMethod, + transparent_index: Option, + + indicies: Vec, +} + +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) -> 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 { + 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 { + 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; diff --git a/gifprobe/Cargo.toml b/gifprobe/Cargo.toml new file mode 100644 index 0000000..75047b8 --- /dev/null +++ b/gifprobe/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "gifprobe" +version = "0.1.0" +authors = ["Genevive Alfirevic { + describe_image(&img); + img_count += 1; + } + gifed::block::Block::Extension(ext) => match ext { + gifed::block::extension::Extension::GraphicControl(gce) => { + hundreths += gce.delay_time() as usize; + + println!( + "Graphic Control Extension\n\tDelay Time {}\n\tDispose {}", + format!("{}s", gce.delay_time() as f32 / 100.0).yellow(), + gce.disposal_method().unwrap().yellow() + ) + } + gifed::block::extension::Extension::Looping(_) => todo!(), + gifed::block::extension::Extension::Comment(cmt) => { + println!("Comment Extension\n\tLength {}", cmt.len()) + } + gifed::block::extension::Extension::Application(app) => { + let auth = app.authentication_code(); + println!("Application Extension\n\tIdentifier {}\n\tAuthentication {:02X} {:02X} {:02X}",app.identifier().yellow(), auth[0].yellow(), auth[1].yellow(), auth[2].yellow()); + } + }, + } + } + + println!( + "{} is {}.{}s long and has {} frames", + file, + hundreths / 100, + hundreths % 100, + img_count + ); +} + +fn describe_image(bli: &IndexedImage) { + println!( + "Image\n\tOffset {}x{}\n\tDimensions {}x{}", + bli.image_descriptor.left.yellow(), + bli.image_descriptor.top.yellow(), + bli.image_descriptor.width.yellow(), + bli.image_descriptor.height.yellow(), + ); + + if bli.image_descriptor.color_table_present() { + println!( + "\tLocal Color Table Present {}\n\tLocal Color Table Size {}", + "Yes".green(), + bli.image_descriptor.color_table_size().green() + ); + } else { + println!( + "\tLocal Color Table Present {}\n\tLocal Color Table Size {}", + "No".red(), + bli.image_descriptor.color_table_size().red() + ); + } +} diff --git a/src/block/colortable.rs b/src/block/colortable.rs deleted file mode 100644 index 01fe00b..0000000 --- a/src/block/colortable.rs +++ /dev/null @@ -1,117 +0,0 @@ -pub use crate::Color; -use crate::EncodingError; -use std::{ - convert::{TryFrom, TryInto}, - ops::Deref, -}; - -#[derive(Clone, Debug)] -pub struct ColorTable { - table: Vec, -} - -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 { - self.table.get(index as usize).map(|v| v.clone()) - } - - 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 - } -} - -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 (must be multiple of 3 len) and From Vec -impl TryFrom<&[u8]> for ColorTable { - type Error = (); - - fn try_from(value: &[u8]) -> Result { - 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::>(), - }) - } - } -} - -impl TryFrom> for ColorTable { - type Error = EncodingError; - - fn try_from(value: Vec) -> Result { - if value.len() > 256 { - Err(EncodingError::TooManyColors) - } else { - Ok(Self { table: value }) - } - } -} - -impl TryFrom> for ColorTable { - type Error = EncodingError; - - fn try_from(value: Vec<(u8, u8, u8)>) -> Result { - if value.len() > 256 { - Err(EncodingError::TooManyColors) - } else { - Ok(Self { - table: value.into_iter().map(|c| c.into()).collect(), - }) - } - } -} diff --git a/src/block/extension/application.rs b/src/block/extension/application.rs deleted file mode 100644 index 9ec1814..0000000 --- a/src/block/extension/application.rs +++ /dev/null @@ -1,15 +0,0 @@ -pub struct Application { - pub(crate) identifier: String, // max len 8 - pub(crate) authentication_code: [u8; 3], - pub(crate) data: Vec, -} - -impl Application { - pub fn identifier(&self) -> &str { - &self.identifier - } - - pub fn authentication_code(&self) -> &[u8] { - &self.authentication_code - } -} diff --git a/src/block/extension/graphiccontrol.rs b/src/block/extension/graphiccontrol.rs deleted file mode 100644 index b595554..0000000 --- a/src/block/extension/graphiccontrol.rs +++ /dev/null @@ -1,116 +0,0 @@ -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 { - 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/src/block/extension/mod.rs b/src/block/extension/mod.rs deleted file mode 100644 index fb5eb20..0000000 --- a/src/block/extension/mod.rs +++ /dev/null @@ -1,49 +0,0 @@ -mod application; -mod graphiccontrol; - -pub use graphiccontrol::{DisposalMethod, GraphicControl}; - -pub use self::application::Application; - -pub enum Extension { - GraphicControl(GraphicControl), - Looping(u16), - Comment(Vec), // 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 for Extension { - fn from(gce: GraphicControl) -> Self { - Extension::GraphicControl(gce) - } -} diff --git a/src/block/imagedescriptor.rs b/src/block/imagedescriptor.rs deleted file mode 100644 index 25567b2..0000000 --- a/src/block/imagedescriptor.rs +++ /dev/null @@ -1,73 +0,0 @@ -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/src/block/indexedimage.rs b/src/block/indexedimage.rs deleted file mode 100644 index 8ed0319..0000000 --- a/src/block/indexedimage.rs +++ /dev/null @@ -1,70 +0,0 @@ -use std::convert::TryFrom; - -use super::{ColorTable, ImageDescriptor}; -use crate::LZW; - -pub struct IndexedImage { - pub image_descriptor: ImageDescriptor, - pub local_color_table: Option, - pub indicies: Vec, -} - -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, - pub lzw_minimum_code_size: u8, - pub blocks: Vec>, -} diff --git a/src/block/mod.rs b/src/block/mod.rs deleted file mode 100644 index e35224b..0000000 --- a/src/block/mod.rs +++ /dev/null @@ -1,25 +0,0 @@ -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/src/block/screendescriptor.rs b/src/block/screendescriptor.rs deleted file mode 100644 index dc0257d..0000000 --- a/src/block/screendescriptor.rs +++ /dev/null @@ -1,79 +0,0 @@ -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/src/block/version.rs b/src/block/version.rs deleted file mode 100644 index 0171ad4..0000000 --- a/src/block/version.rs +++ /dev/null @@ -1,25 +0,0 @@ -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/src/color.rs b/src/color.rs deleted file mode 100644 index e18ce58..0000000 --- a/src/color.rs +++ /dev/null @@ -1,38 +0,0 @@ -#[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/src/colorimage.rs b/src/colorimage.rs deleted file mode 100644 index 69dac1e..0000000 --- a/src/colorimage.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::convert::TryFrom; - -use crate::{block::ColorTable, gif::Image, reader::DecodingError, Color}; - -pub struct ColorImage { - width: u16, - height: u16, - data: Vec, -} - -impl ColorImage { - pub(crate) fn from_indicies( - width: u16, - height: u16, - indicies: &[u8], - table: &ColorTable, - transindex: Option, - ) -> Result { - 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> for ColorImage { - type Error = DecodingError; - - fn try_from(img: Image<'a>) -> Result { - 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/src/gif.rs b/src/gif.rs deleted file mode 100644 index de84764..0000000 --- a/src/gif.rs +++ /dev/null @@ -1,290 +0,0 @@ -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, - pub blocks: Vec, // 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 { - 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>(&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 { - 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 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, - 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 { - self.transparent_index - } - - pub fn indicies(&self) -> &[u8] { - self.indicies - } -} - -#[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/src/lib.rs b/src/lib.rs deleted file mode 100644 index 0a11fdc..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,51 +0,0 @@ -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/src/lzw.rs b/src/lzw.rs deleted file mode 100644 index dce6a5d..0000000 --- a/src/lzw.rs +++ /dev/null @@ -1,173 +0,0 @@ -use std::collections::HashMap; - -pub struct LZW {} -impl LZW { - pub fn encode(minimum_size: u8, indicies: &[u8]) -> Vec { - let mut dictionary: HashMap, 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, - 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 { - 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/src/reader/mod.rs b/src/reader/mod.rs deleted file mode 100644 index 41494df..0000000 --- a/src/reader/mod.rs +++ /dev/null @@ -1,274 +0,0 @@ -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>(path: P) -> Result { - 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 { - 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 { - 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, 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 { - 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 { - 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 for DecodingError { - fn from(ioerror: std::io::Error) -> Self { - DecodingError::IoError(ioerror) - } -} - -struct SmartReader { - inner: Vec, - position: usize, -} - -impl SmartReader { - pub fn u8(&mut self) -> Option { - self.position += 1; - self.inner.get(self.position - 1).map(|b| *b) - } - - pub fn u16(&mut self) -> Option { - 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> { - 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> { - self.take(size).map(|bytes| String::from_utf8_lossy(bytes)) - } - - pub fn take_data_subblocks(&mut self) -> Vec> { - 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 { - let blocks = self.take_data_subblocks(); - let mut ret = vec![]; - for block in blocks { - ret.extend_from_slice(&block) - } - - ret - } -} diff --git a/src/writer/gifbuilder.rs b/src/writer/gifbuilder.rs deleted file mode 100644 index 57a62e3..0000000 --- a/src/writer/gifbuilder.rs +++ /dev/null @@ -1,106 +0,0 @@ -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, - blocks: Vec, - error: Option, -} - -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 { - 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/src/writer/imagebuilder.rs b/src/writer/imagebuilder.rs deleted file mode 100644 index f5c9e2b..0000000 --- a/src/writer/imagebuilder.rs +++ /dev/null @@ -1,133 +0,0 @@ -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, - - delay: u16, - disposal_method: DisposalMethod, - transparent_index: Option, - - indicies: Vec, -} - -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) -> 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 { - 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 { - 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/src/writer/mod.rs b/src/writer/mod.rs deleted file mode 100644 index 88311fc..0000000 --- a/src/writer/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod gifbuilder; -mod imagebuilder; - -pub use gifbuilder::GifBuilder; -pub use imagebuilder::ImageBuilder; -- cgit 1.4.1-3-g733a5