about summary refs log tree commit diff
diff options
context:
space:
mode:
authorgennyble <gen@nyble.dev>2023-11-15 21:14:49 -0600
committergennyble <gen@nyble.dev>2023-11-15 21:14:49 -0600
commit98f750ffaaa65d6e903d23a472a8356cc255ba0d (patch)
tree8873469602c2c15c641167e8c42550b6d6d08d7b
parent9fe0ced38710de9c9f4c36e182555ea80c8be20b (diff)
parent2106c47cc9b16aaf4831d9005dfeafdd3b078db2 (diff)
downloadgifed-98f750ffaaa65d6e903d23a472a8356cc255ba0d.tar.gz
gifed-98f750ffaaa65d6e903d23a472a8356cc255ba0d.zip
Merge branch 'gen-merge'
-rw-r--r--audio-extension.md39
-rw-r--r--gaudio/README.md2
-rw-r--r--gaudio/src/main.rs1
-rw-r--r--gaudio/src/mp3/bitrate.rs (renamed from gaudio/src/mp3.rs)169
-rw-r--r--gaudio/src/mp3/mod.rs304
-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.rs22
-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.rs20
-rw-r--r--gifed/src/writer/mod.rs4
15 files changed, 747 insertions, 185 deletions
diff --git a/audio-extension.md b/audio-extension.md
index a213a8f..dd8503c 100644
--- a/audio-extension.md
+++ b/audio-extension.md
@@ -1,11 +1,38 @@
 # Audio Extension DRAFT
 Add an MP3 audio stream to a gif. 
 
-An application extension with the identifier "GENNYBLE" and auth code "AUD". The data is simply MP3 frames.
+An application extension with the identifier "GENNYBLE" and auth code "AUD".
 
-Questions yet answered:
-- what do we do if the animation and audio length differ?
-- what if there is no graphics extension and thus no length? do we behave differently?
-- what if audio data starts before image data? do we play audio before we display?
+Rough idea:
+- we need an "Audio Control Extension", which is similar to a "Graphic Control Extension". It will provide detail on the upcoming audio data and where it appears so it may inform the decoder.
+- two version:
+	- one that's more era appropriate with MP3
+	- one with Opus which is just cuter
 
-What I'd like to do is just say "all we're doing is shoving MP3 frames in the extension, the rest is on you" and like, the decoder is just supposed to buffer and play the audio when it's received, but that seems.. not great.
\ No newline at end of file
+## Audio Control Extension
+Application Extension. Ident "GENNYBLE" auth code "ACE" *(audio control extension)*.
+
+problems:
+- a decoder may stop reading blocks after it draws an image that has a graphic control with delay. if there is supposed to be audio playing with this frame, it won't know.
+
+## ahh
+- a fixed timescale counting up from the first image every hundreth of a second. audio may not play first.
+
+The stream is driven by the gif video and assumed to be in sync from when it starts.
+
+for audio to be played, there **must** be an ACN extension before the image it's to be played with. this informs the decoder that it's to continue processing after it draws the image. directly after the image should appear the ADT extension
+
+The gif image data drives the audio. The audio **must not** extend the time of
+the file. 
+
+Because the minimal length of an MP3 frame is 1152 samples *(something about size)* the buffer **must** be able to contain a frame of MP3 data. 
+
+## Audio Data Block Extension
+Application Extension. Ident "GENNYBLE" auth code "ADT" *(audio data)*.
+
+
+## Example Data Stream
+GCE - delay 0.1
+ACE - audio after image
+IMG - image
+ADT - audio, dur 0.09, delay 0.01
\ No newline at end of file
diff --git a/gaudio/README.md b/gaudio/README.md
new file mode 100644
index 0000000..b8b16b2
--- /dev/null
+++ b/gaudio/README.md
@@ -0,0 +1,2 @@
+# gaudio
+shove mp3 into a gif.
\ No newline at end of file
diff --git a/gaudio/src/main.rs b/gaudio/src/main.rs
index 7d881b8..5632889 100644
--- a/gaudio/src/main.rs
+++ b/gaudio/src/main.rs
@@ -5,4 +5,5 @@ fn main() {
 	let data = std::fs::read(file).unwrap();
 	let mut breaker = mp3::Breaker::new();
 	breaker.split(data).unwrap();
+	println!("{} frames", breaker.frames.len());
 }
diff --git a/gaudio/src/mp3.rs b/gaudio/src/mp3/bitrate.rs
index 3d643dc..34d332f 100644
--- a/gaudio/src/mp3.rs
+++ b/gaudio/src/mp3/bitrate.rs
@@ -1,169 +1,4 @@
-use std::io::{BufRead, BufReader, Cursor, Read};
-
-/// Destroy an MP3, ripping it's frames apart. Also removes any ID3v2 tags
-/// because who needs metadata?
-pub struct Breaker {
-	frames: Vec<Frame>,
-}
-
-impl Breaker {
-	pub fn new() -> Self {
-		Self { frames: vec![] }
-	}
-
-	pub fn split(&mut self, mut data: Vec<u8>) -> Result<(), std::io::Error> {
-		let cursor = Cursor::new(data);
-		let mut reader = BufReader::new(cursor);
-
-		let mut consumed = 0;
-		loop {
-			let mut three = [0x00, 0x00, 0x00];
-			reader.read_exact(&mut three)?;
-			consumed += 3;
-
-			if &three == b"ID3" {
-				println!("ID3v2 offset {:X}", consumed);
-				Self::skip_id3v2(&mut reader)?
-			} else if three[0] == 0xFF && three[2] & 0b1110_0000 == 0b1110_0000 {
-				let mut one_more = [0x00];
-				reader.read_exact(&mut one_more)?;
-			}
-		}
-
-		todo!()
-	}
-
-	/// Assumes the ident "TAG" was already consumed
-	fn skip_id3v2<R: BufRead>(reader: &mut R) -> Result<(), std::io::Error> {
-		// We don't actually want this, but want to get rid of it.
-		let mut version_and_flags = [0x00, 0x00, 0x00];
-		reader.read_exact(&mut version_and_flags)?;
-
-		println!(
-			"Version {} Revision {}",
-			version_and_flags[0], version_and_flags[1]
-		);
-
-		let mut syncsafe_size = [0x00, 0x00, 0x00, 0x00];
-		reader.read_exact(&mut syncsafe_size)?;
-
-		// Size is MSB
-		let mut size = syncsafe_size[3] as u32;
-		// Shift right eight, but back one because most significant bit is 0 due to syncsafe
-		size |= (syncsafe_size[2] as u32) << 7;
-		size |= (syncsafe_size[1] as u32) << 14;
-		size |= (syncsafe_size[0] as u32) << 21;
-
-		let human = if size > 1024 * 1024 {
-			format!("{:.2}MiB", size as f32 / (1024.0 * 1024.0))
-		} else if size > 1024 {
-			format!("{:.2}KiB", size as f32 / 1024.0)
-		} else {
-			format!("{size}B")
-		};
-
-		println!("ID3v2 size is {human} bytes");
-
-		// Make a vec size big. We're not here to be efficient, sorry if this dissapoint you.
-		let mut skip = vec![0x00; size as usize];
-		reader.read_exact(&mut skip)
-	}
-}
-
-pub struct Frame {
-	header: Header,
-	data: Vec<u8>,
-}
-
-pub struct Header {
-	// I only want to parse what i need, but we need this for writing out, so
-	raw: [u8; 4],
-	version: Version,
-	layer: Layer,
-	crc: bool,
-}
-
-impl Header {
-	pub fn from_bytes(raw: [u8; 4]) -> Result<Self, Error> {
-		if raw[0] != 0xFF || raw[1] & 0b1110_0000 != 0b1110_0000 {
-			return Err(Error::HeaderUnsync);
-		}
-
-		let version = Version::from(raw[1]);
-		let layer = Layer::from(raw[1]);
-		let crc = raw[1] & 1 == 0;
-
-		let bitrate = Bitrate::resolve(raw[2], version, layer);
-
-		//TODO: gen- love, you were trying to get the size of the data field. We need
-		//to know the sampling rate and the pad bit for that, which happen to be the
-		//next three bits.
-
-		todo!()
-	}
-
-	// Algorithm taken from:
-	// http://www.multiweb.cz/twoinches/mp3inside.htm
-	/// The length of the header and data
-	pub fn length(&self) -> usize {
-		todo!()
-	}
-
-	/// The length of the audio data. This is just the length - 4
-	pub fn data_length(&self) -> usize {
-		self.length() - 4
-	}
-}
-
-#[derive(Debug, thiserror::Error)]
-pub enum Error {
-	#[error("tried to parse header, but first 11 bits were not 1; not synced!")]
-	HeaderUnsync,
-	#[error("The version or the layer was a reserved value")]
-	CannotResolveBitrate,
-	#[error("Bitrate bits were all 1")]
-	BitrateBad,
-}
-
-#[derive(Copy, Clone, Debug)]
-pub enum Version {
-	Mpeg2_5,
-	Reserved,
-	Mpeg2,
-	Mpeg1,
-}
-
-impl From<u8> for Version {
-	fn from(byte: u8) -> Self {
-		match byte & 0b000_11_000 {
-			0b000_00_000 => Version::Mpeg2_5,
-			0b000_01_000 => Version::Reserved,
-			0b000_10_000 => Version::Mpeg2,
-			0b000_11_000 => Version::Mpeg1,
-			_ => unreachable!(),
-		}
-	}
-}
-
-#[derive(Copy, Clone, Debug)]
-pub enum Layer {
-	Reserved,
-	Layer3,
-	Layer2,
-	Layer1,
-}
-
-impl From<u8> for Layer {
-	fn from(byte: u8) -> Self {
-		match byte & 0b000_00_110 {
-			0b000_00_000 => Layer::Reserved,
-			0b000_00_010 => Layer::Layer3,
-			0b000_00_100 => Layer::Layer2,
-			0b000_00_110 => Layer::Layer1,
-			_ => unreachable!(),
-		}
-	}
-}
+use super::{Error, Layer, Version};
 
 /// What do you want from me it's hard to name a thing that's just number.
 #[derive(Copy, Clone, Debug, PartialEq)]
@@ -368,7 +203,7 @@ impl Bitrate {
 			v2_l23!(b1110) => Ok(Bitrate::Rate160),
 
 			(br!(b1111), _, _) => Err(Error::BitrateBad),
-			(_, Version::Reserved, _) | (_, _, Layer::Reserved) => Err(Error::CannotResolveBitrate),
+			(_, Version::Reserved, _) | (_, _, Layer::Reserved) => Err(Error::BitrateReserve),
 		}
 	}
 
diff --git a/gaudio/src/mp3/mod.rs b/gaudio/src/mp3/mod.rs
new file mode 100644
index 0000000..fe6433f
--- /dev/null
+++ b/gaudio/src/mp3/mod.rs
@@ -0,0 +1,304 @@
+use std::{
+	io::{BufRead, BufReader, Cursor, ErrorKind, Read},
+	time::Duration,
+};
+
+use crate::mp3::bitrate::Bitrate;
+
+mod bitrate;
+
+/// Destroy an MP3, ripping it's frames apart. Also removes any ID3v2 tags
+/// because who needs metadata?
+pub struct Breaker {
+	pub frames: Vec<Frame>,
+}
+
+impl Breaker {
+	pub fn new() -> Self {
+		Self { frames: vec![] }
+	}
+
+	pub fn split(&mut self, data: Vec<u8>) -> Result<(), std::io::Error> {
+		let cursor = Cursor::new(data);
+		let mut reader = BufReader::new(cursor);
+
+		let mut consumed = 0;
+		loop {
+			print!("[{consumed:06X}] reading... ");
+			let mut three = [0x00, 0x00, 0x00];
+			if let Err(e) = reader.read_exact(&mut three) {
+				if e.kind() == ErrorKind::UnexpectedEof {
+					println!("out of bytes!");
+					break;
+				} else {
+					println!("failed!");
+					return Err(e);
+				}
+			}
+			consumed += 3;
+
+			if &three == b"ID3" {
+				println!("found ID3v2!");
+				Self::skip_id3v2(&mut reader, &mut consumed)?
+			} else if three[0] == 0xFF && three[1] & 0b1110_0000 == 0b1110_0000 {
+				print!("Have header - ");
+				let mut one_more = [0x00];
+				reader.read_exact(&mut one_more)?;
+				consumed += 1;
+
+				let header =
+					Header::from_bytes([three[0], three[1], three[2], one_more[0]]).unwrap();
+				let dat_len = header.data_length();
+				let mut data = vec![0; dat_len];
+				reader.read_exact(&mut data)?;
+				consumed += dat_len;
+				let frame = Frame { header, data };
+
+				println!(
+					"{}kbps {}kHz {:<4}bytes [{}ms]",
+					frame.header.bitrate.kbps().unwrap(),
+					frame.header.samplerate.freq() / 1000,
+					frame.header.length(),
+					frame.duration().as_millis()
+				);
+
+				self.frames.push(frame);
+			} else {
+				println!("unsynced!");
+				panic!()
+			}
+		}
+
+		Ok(())
+	}
+
+	/// Assumes the ident "TAG" was already consumed
+	fn skip_id3v2<R: BufRead>(reader: &mut R, consumed: &mut usize) -> Result<(), std::io::Error> {
+		// We don't actually want this, but want to get rid of it.
+		let mut version_and_flags = [0x00, 0x00, 0x00];
+		reader.read_exact(&mut version_and_flags)?;
+		*consumed += 3;
+
+		println!(
+			"Version {} Revision {}",
+			version_and_flags[0], version_and_flags[1]
+		);
+
+		let mut syncsafe_size = [0x00, 0x00, 0x00, 0x00];
+		reader.read_exact(&mut syncsafe_size)?;
+		*consumed += 4;
+
+		// Size is MSB
+		let mut size = syncsafe_size[3] as u32;
+		// Shift right eight, but back one because most significant bit is 0 due to syncsafe
+		size |= (syncsafe_size[2] as u32) << 7;
+		size |= (syncsafe_size[1] as u32) << 14;
+		size |= (syncsafe_size[0] as u32) << 21;
+
+		let human = if size > 1024 * 1024 {
+			format!("{:.2}MiB", size as f32 / (1024.0 * 1024.0))
+		} else if size > 1024 {
+			format!("{:.2}KiB", size as f32 / 1024.0)
+		} else {
+			format!("{size}B")
+		};
+
+		println!("ID3v2 size is {human} bytes");
+
+		// Make a vec size big. We're not here to be efficient, sorry if this dissapoint you.
+		let mut skip = vec![0x00; size as usize];
+		reader.read_exact(&mut skip)?;
+		*consumed += size as usize;
+
+		Ok(())
+	}
+}
+
+pub struct Frame {
+	pub header: Header,
+	pub data: Vec<u8>,
+}
+
+impl Frame {
+	/// The number of moments-in-time this frame represents. This is constant
+	/// and related to the [Layer]
+	pub fn sample_count(&self) -> usize {
+		// http://www.datavoyage.com/mpgscript/mpeghdr.htm
+		// > Frame size is the number of samples contained in a frame. It is
+		// > constant and always 384 samples for Layer I and 1152 samples for
+		// > Layer II and Layer III.
+		match self.header.layer {
+			Layer::Reserved => panic!(),
+			Layer::Layer1 => 384,
+			Layer::Layer2 | Layer::Layer3 => 1152,
+		}
+	}
+
+	/// Compute the duration of this audio frame
+	pub fn duration(&self) -> Duration {
+		let millis = (self.sample_count() * 1000) / self.header.samplerate.freq();
+		Duration::from_millis(millis as u64)
+	}
+}
+
+pub struct Header {
+	// I only want to parse what i need, but we need this for writing out, so
+	pub raw: [u8; 4],
+	pub version: Version,
+	pub layer: Layer,
+	pub crc: bool,
+	pub bitrate: Bitrate,
+	pub samplerate: SampleRate,
+	pub pad: bool,
+}
+
+impl Header {
+	pub fn from_bytes(raw: [u8; 4]) -> Result<Self, Error> {
+		if raw[0] != 0xFF || raw[1] & 0b1110_0000 != 0b1110_0000 {
+			return Err(Error::HeaderUnsync);
+		}
+
+		//TODO: gen- yell if the version and layer aren't V1 L3?
+		let version = Version::from_packed(raw[1]);
+		let layer = Layer::from_packed(raw[1]);
+		// CRC is 2bytes and directly follows the frame header
+		let crc = raw[1] & 1 == 0;
+		let bitrate = Bitrate::resolve(raw[2], version, layer)?;
+		let samplerate = SampleRate::from_packed(raw[2]);
+
+		if let SampleRate::Reserved = samplerate {
+			return Err(Error::SampleRateReserve);
+		}
+
+		let pad = raw[2] & 2 > 0;
+
+		//TODO: gen- love, you were trying to get the size of the data field. We need
+		//to know the sampling rate and the pad bit for that, which happen to be the
+		//next three bits.
+
+		//Things i did not parse because i do not care about them:
+		// - private bit
+		// - channels
+		// - mode extension
+		// - copyright (lol)
+		// - original (lmfao)
+		// - emphasis
+
+		Ok(Self {
+			raw,
+			version,
+			layer,
+			crc,
+			bitrate,
+			samplerate,
+			pad,
+		})
+	}
+
+	// Algorithm taken from:
+	// http://www.multiweb.cz/twoinches/mp3inside.htm
+	/// The length of the header and data
+	pub fn length(&self) -> usize {
+		// what, do we not care about crc? won't it add 2 bytes?
+		let size = (144 * self.bitrate.bitrate().unwrap()) / self.samplerate.freq();
+		if self.pad {
+			size + 1
+		} else {
+			size
+		}
+	}
+
+	/// The length of the audio data. This is just the length - 4
+	pub fn data_length(&self) -> usize {
+		self.length() - 4
+	}
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+	#[error("tried to parse header, but first 11 bits were not 1; not synced!")]
+	HeaderUnsync,
+	#[error("The version or the layer was a reserved value")]
+	BitrateReserve,
+	#[error("Bitrate bits were all 1")]
+	BitrateBad,
+	#[error("SampleRate was a reserved value")]
+	SampleRateReserve,
+}
+
+#[derive(Copy, Clone, Debug)]
+pub enum Version {
+	Mpeg2_5,
+	Reserved,
+	Mpeg2,
+	Mpeg1,
+}
+
+impl Version {
+	/// Parse the Version from the second byte of the frame header
+	fn from_packed(byte: u8) -> Self {
+		#[allow(clippy::unusual_byte_groupings)]
+		match byte & 0b000_11_000 {
+			0b000_00_000 => Version::Mpeg2_5,
+			0b000_01_000 => Version::Reserved,
+			0b000_10_000 => Version::Mpeg2,
+			0b000_11_000 => Version::Mpeg1,
+			_ => unreachable!(),
+		}
+	}
+}
+
+#[derive(Copy, Clone, Debug)]
+pub enum Layer {
+	Reserved,
+	Layer3,
+	Layer2,
+	Layer1,
+}
+
+impl Layer {
+	/// Parse the Layer from the second byte of the frame header.
+	fn from_packed(byte: u8) -> Self {
+		#[allow(clippy::unusual_byte_groupings)]
+		match byte & 0b000_00_110 {
+			0b000_00_000 => Layer::Reserved,
+			0b000_00_010 => Layer::Layer3,
+			0b000_00_100 => Layer::Layer2,
+			0b000_00_110 => Layer::Layer1,
+			_ => unreachable!(),
+		}
+	}
+}
+
+#[derive(Copy, Clone, Debug)]
+pub enum SampleRate {
+	Hz44100,
+	Hz48000,
+	Hz32000,
+	Reserved,
+}
+
+impl SampleRate {
+	/// Parse the SampleRate from the third byte of the frame header
+	fn from_packed(byte: u8) -> Self {
+		#[allow(clippy::unusual_byte_groupings)]
+		match byte & 0b0000_11_0_0 {
+			0b0000_00_0_0 => SampleRate::Hz44100,
+			0b0000_01_0_0 => SampleRate::Hz48000,
+			0b0000_10_0_0 => SampleRate::Hz32000,
+			0b0000_11_0_0 => SampleRate::Reserved,
+			_ => unreachable!(),
+		}
+	}
+
+	pub fn freq(&self) -> usize {
+		match self {
+			SampleRate::Hz44100 => 44100,
+			SampleRate::Hz48000 => 48000,
+			SampleRate::Hz32000 => 32000,
+			SampleRate::Reserved => {
+				panic!("sample rate was a reserved value; unable to determien a frequency")
+			}
+		}
+	}
+}
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 03c2119..c98e753 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();
@@ -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().flat_map(<_>::into_iter).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 9c4a5a9..8a06883 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
@@ -66,17 +67,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 - 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
 	}
 }
@@ -101,6 +105,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 = ();
@@ -144,3 +164,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 90354a1..46807a9 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![];
 
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)