about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--gifcheck/Cargo.toml1
-rw-r--r--gifcheck/src/fix.rs103
-rw-r--r--gifcheck/src/main.rs103
-rw-r--r--gifed/src/block/extension/application.rs1
-rw-r--r--gifed/src/block/indexedimage.rs24
-rw-r--r--gifed/src/block/mod.rs2
-rw-r--r--gifed/src/block/palette.rs160
-rw-r--r--gifed/src/block/screendescriptor.rs1
-rw-r--r--gifed/src/gif.rs154
-rw-r--r--gifed/src/writer/mod.rs4
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)