diff options
-rw-r--r-- | gifcheck/Cargo.toml | 1 | ||||
-rw-r--r-- | gifcheck/src/fix.rs | 103 | ||||
-rw-r--r-- | gifcheck/src/main.rs | 103 | ||||
-rw-r--r-- | gifed/src/block/extension/application.rs | 1 | ||||
-rw-r--r-- | gifed/src/block/indexedimage.rs | 24 | ||||
-rw-r--r-- | gifed/src/block/mod.rs | 2 | ||||
-rw-r--r-- | gifed/src/block/palette.rs | 160 | ||||
-rw-r--r-- | gifed/src/block/screendescriptor.rs | 1 | ||||
-rw-r--r-- | gifed/src/gif.rs | 154 | ||||
-rw-r--r-- | gifed/src/writer/mod.rs | 4 |
10 files changed, 406 insertions, 147 deletions
diff --git a/gifcheck/Cargo.toml b/gifcheck/Cargo.toml index 8ae0b40..5c63df7 100644 --- a/gifcheck/Cargo.toml +++ b/gifcheck/Cargo.toml @@ -7,3 +7,4 @@ license = "ISC" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +gifed = { path = "../gifed" } diff --git a/gifcheck/src/fix.rs b/gifcheck/src/fix.rs new file mode 100644 index 0000000..95f7ef2 --- /dev/null +++ b/gifcheck/src/fix.rs @@ -0,0 +1,103 @@ +use gifed::{ + block::{Block, Palette}, + Color, Gif, +}; + +use crate::PaletteReport; + +pub fn palette_errors(gif: &Gif, report: PaletteReport) -> Option<Gif> { + if report.local_matching_indicies { + let mut new = gif.clone(); + + for block in new.blocks.iter_mut() { + if let Block::CompressedImage(comp) = block { + comp.image_descriptor.packed.set_color_table(false); + comp.image_descriptor.packed.set_color_table_size(0); + + if let Some(plt) = comp.local_color_table.take() { + new.global_color_table.get_or_insert(plt); + } + } + } + + Some(new) + } else { + None + } +} + +pub fn images_match_exactly(gifa: &Gif, gifb: &Gif) -> bool { + let mut a_buf = vec![0; gifa.width() * gifa.height() * 4]; + let mut b_buf = vec![0; gifb.width() * gifb.height() * 4]; + + for (a, b) in gifa.images().zip(gifb.images()) { + if a.width() != b.width() || a.height() != b.height() { + return false; + } + + if a.left() != b.left() || a.top() != b.top() { + return false; + } + + let a_decomp = a.decompess(); + let b_decomp = b.decompess(); + + let a_size = deindex( + &a_decomp.indicies, + a.palette(), + a.transparent_index(), + &mut a_buf, + ); + + let b_size = deindex( + &b_decomp.indicies, + b.palette(), + b.transparent_index(), + &mut b_buf, + ); + + match (a_size, b_size) { + (None, _) | (_, None) => return false, + (Some(asize), Some(bsize)) => { + if asize != bsize { + return false; + } + + if a_buf[..asize] != b_buf[..bsize] { + return false; + } + } + } + } + + true +} + +fn deindex(indicies: &[u8], plt: &Palette, trns: Option<u8>, buffer: &mut [u8]) -> Option<usize> { + let mut rgba = |idx: usize, clr: Option<Color>| match clr { + None => { + buffer[idx] = 0; + buffer[idx + 1] = 0; + buffer[idx + 2] = 0; + buffer[idx + 3] = 0; + } + Some(clr) => { + buffer[idx] = clr.r; + buffer[idx + 1] = clr.g; + buffer[idx + 2] = clr.b; + buffer[idx + 3] = 255; + } + }; + + for (idx, color_idx) in indicies.iter().enumerate() { + match (trns, plt.get(*color_idx)) { + (Some(trns_idx), _) if trns_idx == *color_idx => rgba(idx * 4, None), + (_, Some(color)) => rgba(idx * 4, Some(color)), + (Some(_) | None, None) => { + return None; + } + } + } + + Some(indicies.len() * 4) +} diff --git a/gifcheck/src/main.rs b/gifcheck/src/main.rs index a30eb95..8eec4c1 100644 --- a/gifcheck/src/main.rs +++ b/gifcheck/src/main.rs @@ -1,3 +1,104 @@ +use std::{ops::Deref, path::PathBuf}; + +use gifed::{reader::Decoder, Gif}; + +mod fix; + fn main() { - println!("Hello, world!"); + let file = std::env::args().nth(1).unwrap(); + let arg = std::env::args().nth(2).map(|cmd| cmd.to_lowercase()); + + let gif = Decoder::file(&file).unwrap().read_all().unwrap(); + + let plt_report = same_palette(&gif); + match plt_report { + PaletteReport { + has_local: true, + local_redundant: true, + local_matching_indicies, + } => { + if local_matching_indicies { + println!("!!! LOCPLT_NORE. This could've been a global palette"); + } else { + println!("!! LOCPLT. This gif can be reindexed and have a global palette"); + } + } + PaletteReport { + has_local: true, + local_redundant: false, + .. + } => { + println!(" gif has local palettes and they differ"); + } + PaletteReport { + has_local: false, .. + } => { + println!(" gif only has a global palette"); + } + } + + if arg.as_deref() == Some("fix") { + if let Some(fix_gif) = fix::palette_errors(&gif, plt_report) { + if !fix::images_match_exactly(&gif, &fix_gif) { + panic!("fixed images did not exactly match, this is a hard error") + } + + println!("--- fixing, writing!"); + let mut path = PathBuf::from(file); + path.set_file_name(format!( + "{}_fix", + path.file_stem().unwrap().to_string_lossy() + )); + + fix_gif.save(path).unwrap(); + } + } +} + +pub struct PaletteReport { + // Does the gif even contain local color tables? + has_local: bool, + // ... do those color tables always contain the same colors? + local_redundant: bool, + // ... and do those colors all have matching inidices, making it possible + // to simply set the global palette and remove the locals? + local_matching_indicies: bool, +} + +fn same_palette(gif: &Gif) -> PaletteReport { + let mut palette = gif.global_color_table.as_ref(); + let mut report = PaletteReport { + has_local: false, + local_redundant: true, + local_matching_indicies: true, + }; + + for img in gif.images() { + if let Some(local_palette) = img.compressed.palette() { + report.has_local = true; + + match palette { + None => palette = Some(local_palette), + Some(known_palette) => { + if !local_palette.eq(known_palette) { + // Are the palletes equal, even? + report.local_redundant = false; + report.local_matching_indicies = false; + return report; + } else if report.local_matching_indicies { + // it's matching, but are the indicies the same? + for known_color in known_palette.deref() { + for local_color in local_palette.deref() { + if known_color != local_color { + report.local_matching_indicies = false; + } + } + } + } + } + } + } + } + + report } diff --git a/gifed/src/block/extension/application.rs b/gifed/src/block/extension/application.rs index 2244c35..f13047a 100644 --- a/gifed/src/block/extension/application.rs +++ b/gifed/src/block/extension/application.rs @@ -1,3 +1,4 @@ +#[derive(Clone, Debug)] pub struct Application { pub(crate) identifier: [u8; 8], pub(crate) authentication_code: [u8; 3], diff --git a/gifed/src/block/indexedimage.rs b/gifed/src/block/indexedimage.rs index 1868382..261ff38 100644 --- a/gifed/src/block/indexedimage.rs +++ b/gifed/src/block/indexedimage.rs @@ -34,7 +34,7 @@ impl IndexedImage { /// The `lzw_code_size` should be None if there is a local color table present. If /// this image is using the Global Color Table, you must provide an - /// LZW Minimum Code Size here. It is equal to the value of [Palette::packed_len], but + /// LZW Minimum Code Size here. It is equal to the value of [Palette::packed_len] + 1, but /// must also be at least 2. pub fn compress(self, lzw_code_size: Option<u8>) -> Result<CompressedImage, EncodeError> { // gen- The old code had a +1 here. Why? @@ -46,14 +46,12 @@ impl IndexedImage { Some(palette) => palette.lzw_code_size(), None => match lzw_code_size { None => return Err(EncodeError::InvalidCodeSize { lzw_code_size: 0 }), - Some(mcs) => mcs, + Some(mcs) => mcs.max(2), }, }; - let mcs = if mcs < 2 { 2 } else { mcs }; - //FIXME: gen- This seems broken - //let compressed = LZW::encode(mcs, &self.indicies); + //let compressed = crate::LZW::encode(mcs, &self.indicies); let compressed = Encoder::new(weezl::BitOrder::Lsb, mcs) .encode(&self.indicies) .unwrap(); @@ -86,7 +84,7 @@ impl CompressedImage { } pub fn top(&self) -> u16 { - self.image_descriptor.left + self.image_descriptor.top } pub fn width(&self) -> u16 { @@ -105,6 +103,11 @@ impl CompressedImage { let mut ret = vec![]; ret.extend_from_slice(&self.image_descriptor.as_bytes()); + + if let Some(palette) = &self.local_color_table { + ret.extend_from_slice(&palette.as_bytes()); + } + ret.push(self.lzw_code_size); for block in &self.blocks { @@ -128,8 +131,15 @@ impl CompressedImage { let data: Vec<u8> = blocks.into_iter().map(<_>::into_iter).flatten().collect(); + println!("lzw: {lzw_code_size}"); + + if local_color_table.is_some() { + let lct = local_color_table.as_ref().unwrap(); + println!("lct-lzw: {}", lct.lzw_code_size()); + } + //TODO: remove unwrap - let mut decompressor = weezl::decode::Decoder::new(weezl::BitOrder::Msb, lzw_code_size); + let mut decompressor = weezl::decode::Decoder::new(weezl::BitOrder::Lsb, lzw_code_size); let indicies = match decompressor.decode(&data) { Err(LzwError::InvalidCode) => Err(DecodeError::LzwInvalidCode), Ok(o) => Ok(o), diff --git a/gifed/src/block/mod.rs b/gifed/src/block/mod.rs index 36cc6fe..3a18a07 100644 --- a/gifed/src/block/mod.rs +++ b/gifed/src/block/mod.rs @@ -16,6 +16,7 @@ pub use version::Version; use self::extension::Application; use self::extension::GraphicControl; +#[derive(Clone, Debug)] pub enum Block { CompressedImage(CompressedImage), //TODO: Extension(Extension), @@ -26,6 +27,7 @@ pub enum Block { LoopingExtension(LoopCount), } +#[derive(Clone, Debug)] pub enum LoopCount { Forever, Number(u16), diff --git a/gifed/src/block/palette.rs b/gifed/src/block/palette.rs index d5cc696..67d9ae8 100644 --- a/gifed/src/block/palette.rs +++ b/gifed/src/block/palette.rs @@ -27,7 +27,8 @@ impl Palette { } pub fn lzw_code_size(&self) -> u8 { - self.packed_len() + 1 + let table_log = (self.table.len() as f32).log2() as u8; + table_log.max(2) } /// Returns the number of colours in the pallette @@ -62,17 +63,20 @@ impl Palette { /// How many padding bytes we need to write. /// We need to pad the colour table because the size must be a power of two. //TODO: gen- better docs - pub fn padding(&self) -> usize { + fn padding(&self) -> usize { let comp = self.computed_len(); (comp as usize - self.len()) * 3 } + /// The palette with padding if required pub fn as_bytes(&self) -> Vec<u8> { let mut bytes = Vec::with_capacity(self.table.len() * 3); for color in &self.table { bytes.extend_from_slice(&[color.r, color.g, color.b]); } + bytes.extend(std::iter::repeat(0u8).take(self.padding())); + bytes } } @@ -91,6 +95,22 @@ impl AsRef<Palette> for Palette { } } +impl PartialEq for Palette { + fn eq(&self, other: &Self) -> bool { + if self.len() != other.len() { + return false; + } + + for color in &other.table { + if !self.table.contains(color) { + return false; + } + } + + true + } +} + //TODO: TryFrom Vec<u8> (must be multiple of 3 len) and From Vec<Color> impl TryFrom<&[u8]> for Palette { type Error = (); @@ -134,3 +154,139 @@ impl TryFrom<Vec<(u8, u8, u8)>> for Palette { } } } + +#[cfg(test)] +mod test { + use super::*; + + fn vec_tuple_test(vec: Vec<(u8, u8, u8)>, expected: &[u8]) { + let plt: Palette = vec.try_into().unwrap(); + let bytes = plt.as_bytes(); + + assert_eq!(expected, bytes.as_slice()) + } + + #[test] + fn writes_one_with_padding() { + vec_tuple_test(vec![(1, 2, 3)], &[1, 2, 3, 0, 0, 0]) + } + + #[test] + fn writes_two_without_padding() { + vec_tuple_test(vec![(1, 2, 3), (4, 5, 6)], &[1, 2, 3, 4, 5, 6]) + } + + fn test_n_with_padding(real_count: usize, exected_padding_bytes: usize) { + let mut palette = Palette::new(); + let mut expected = vec![]; + + for x in 0..real_count { + let x = x as u8; + palette.push(Color { r: x, g: x, b: x }); + expected.extend_from_slice(&[x, x, x]) + } + + // yes, this is really how I'm doing it. I have... trust issues with + // myself and iter::repeat. i hope you understand + for _ in 0..exected_padding_bytes { + expected.push(0x00); + } + + let bytes = palette.as_bytes(); + assert_eq!(expected, bytes.as_slice()) + } + + fn test_n_with_padding_range(real_count_low: u8, real_count_high: u8, next_padstop: usize) { + for x in real_count_low..=real_count_high { + test_n_with_padding(x as usize, (next_padstop as usize - x as usize) * 3) + } + } + + #[test] + fn writes_three_with_padding() { + test_n_with_padding(3, 3); + } + + #[test] + fn writes_four_without_padding() { + test_n_with_padding(4, 0); + } + + #[test] + fn writes_five_to_seven_with_padding() { + test_n_with_padding_range(5, 7, 8); + } + + #[test] + fn writes_eight_without_padding() { + test_n_with_padding(8, 0); + } + + #[test] + fn writes_nine_to_fifteen_with_padding() { + test_n_with_padding_range(9, 15, 16); + } + + #[test] + fn writes_sixteen_without_padding() { + test_n_with_padding(16, 0); + } + + #[test] + fn writes_seventeen_to_thirtyone_with_padding() { + test_n_with_padding_range(17, 31, 32); + } + + #[test] + fn writes_thirtytwo_without_padding() { + test_n_with_padding(32, 0); + } + + #[test] + fn writes_thirtythree_to_sixtythree_with_padding() { + test_n_with_padding_range(33, 63, 64); + } + + #[test] + fn writes_sixtyfour_without_padding() { + test_n_with_padding(64, 0); + } + + #[test] + fn writes_sixtyfive_to_onehundredtwentyseven_with_padding() { + test_n_with_padding_range(65, 127, 128); + } + + #[test] + fn writes_onetwentyeight_without_padding() { + test_n_with_padding(128, 0); + } + + #[test] + fn writes_onetwentynine_to_twofiftyfive_with_padding() { + test_n_with_padding_range(129, 255, 256); + } + + #[test] + fn writes_256_without_padding() { + test_n_with_padding(256, 0); + } + + #[test] + fn packed_len_are_correct() { + let black = Color::new(0, 0, 0); + let mut palette = Palette::new(); + + // Nothing is nothing + assert_eq!(0, palette.packed_len()); + + // One color is still 0 because the formula is + // 2 ^ (len + 1) + // which means we should increase at 3 + palette.push(black); + assert_eq!(0, palette.packed_len()); + + palette.push(black); + assert_eq!(0, palette.packed_len()); + } +} diff --git a/gifed/src/block/screendescriptor.rs b/gifed/src/block/screendescriptor.rs index d44ca2f..a23bfdd 100644 --- a/gifed/src/block/screendescriptor.rs +++ b/gifed/src/block/screendescriptor.rs @@ -2,6 +2,7 @@ use std::convert::TryInto; use super::{packed::ScreenPacked, Palette}; +#[derive(Clone, Debug)] pub struct ScreenDescriptor { pub width: u16, pub height: u16, diff --git a/gifed/src/gif.rs b/gifed/src/gif.rs index 09b052e..4e70ffd 100644 --- a/gifed/src/gif.rs +++ b/gifed/src/gif.rs @@ -8,6 +8,8 @@ use crate::{ }, writer::GifBuilder, }; + +#[derive(Clone, Debug)] pub struct Gif { pub header: Version, pub screen_descriptor: ScreenDescriptor, @@ -20,6 +22,24 @@ impl Gif { GifBuilder::new(width, height) } + pub fn width(&self) -> usize { + self.screen_descriptor.width as usize + } + + pub fn height(&self) -> usize { + self.screen_descriptor.height as usize + } + + pub fn background_color(&self) -> Option<u8> { + // vii) Background Color Index - If the Global Color Table Flag is set + // to (zero), this field should be zero and should be ignored. + if self.screen_descriptor.has_color_table() { + Some(self.screen_descriptor.background_color_index) + } else { + None + } + } + pub fn as_bytes(&self) -> Vec<u8> { let mut out = vec![]; @@ -191,137 +211,3 @@ pub enum FrameControl { Input, InputOrDelay(Duration), } - -#[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()) - .build(indicies.clone()) - .unwrap(), - ) - .image(ImageBuilder::new(4, 4).build(indicies).unwrap()); - - let bytes = actual.build().unwrap().as_bytes(); - 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()) - .disposal_method(DisposalMethod::RestoreBackground) - .delay(64) - .build(indicies.clone()) - .unwrap(), - ) - .image(ImageBuilder::new(4, 4).build(indicies).unwrap()) - .build() - .unwrap() - .as_bytes(); - - 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/writer/mod.rs b/gifed/src/writer/mod.rs index 493514b..c2e8382 100644 --- a/gifed/src/writer/mod.rs +++ b/gifed/src/writer/mod.rs @@ -4,7 +4,7 @@ mod imagebuilder; use std::{error::Error, fmt, io::Write}; pub use gifbuilder::GifBuilder; -pub use imagebuilder::ImageBuilder; +pub use imagebuilder::{BuiltImage, ImageBuilder}; use crate::block::{encode_block, Block, LoopCount, Palette, ScreenDescriptor, Version}; @@ -47,9 +47,7 @@ impl<W: Write> Writer<W> { this.write_all(&screen_descriptor.as_bytes())?; if let Some(palette) = this.global_palette.as_ref() { - let padding: Vec<u8> = std::iter::repeat(0u8).take(palette.padding()).collect(); this.write_all(&palette.as_bytes())?; - this.write_all(&padding)?; } Ok(this) |