about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--squash/src/cli.rs85
-rw-r--r--squash/src/image.rs104
-rw-r--r--squash/src/main.rs163
3 files changed, 203 insertions, 149 deletions
diff --git a/squash/src/cli.rs b/squash/src/cli.rs
new file mode 100644
index 0000000..8a127b7
--- /dev/null
+++ b/squash/src/cli.rs
@@ -0,0 +1,85 @@
+use camino::Utf8PathBuf;
+
+pub struct Cli {
+	pub color_count: u8,
+	pub input: Utf8PathBuf,
+	pub in_type: InType,
+	pub output: Utf8PathBuf,
+	pub out_type: OutType,
+}
+
+pub enum InType {
+	Jpeg,
+	Png,
+}
+
+pub enum OutType {
+	Png,
+	Gif,
+}
+
+// Get's the CLI arguments or dies trying
+pub fn get() -> Cli {
+	let usage = || -> ! {
+		println!("usage: squash <color count> <input> <output>");
+		std::process::exit(0);
+	};
+	let mut argv = std::env::args().skip(1);
+
+	let color_count: u8 = if let Some(Ok(count)) = argv.next().map(|r| r.parse::<usize>()) {
+		if count > 256 {
+			eprintln!("max colour count must be 256 or below");
+			std::process::exit(1);
+		} else {
+			(count - 1) as u8
+		}
+	} else {
+		usage()
+	};
+
+	let input: Utf8PathBuf = if let Some(path) = argv.next() {
+		path.into()
+	} else {
+		usage();
+	};
+
+	let in_type = match input.extension() {
+		None => {
+			eprintln!("can't determine input filetype!\nSupported input types: PNG, JPG");
+			std::process::exit(1);
+		}
+		Some("png") => InType::Png,
+		Some("jpg") | Some("jpeg") => InType::Jpeg,
+		Some(ext) => {
+			eprintln!("unknown filetype '{ext}'!\nSupported input types: PNG, JPG");
+			std::process::exit(1);
+		}
+	};
+
+	let output: Utf8PathBuf = if let Some(path) = argv.next() {
+		path.into()
+	} else {
+		usage();
+	};
+
+	let out_type = match output.extension() {
+		None => {
+			eprintln!("can't determine output filetype!");
+			std::process::exit(1);
+		}
+		Some("png") => OutType::Png,
+		Some("gif") => OutType::Gif,
+		Some(ext) => {
+			eprintln!("unknown filetype '{ext}'!\nSupport output types are: GIF, PNG");
+			std::process::exit(1);
+		}
+	};
+
+	Cli {
+		color_count,
+		input,
+		in_type,
+		output,
+		out_type,
+	}
+}
diff --git a/squash/src/image.rs b/squash/src/image.rs
new file mode 100644
index 0000000..098e8a0
--- /dev/null
+++ b/squash/src/image.rs
@@ -0,0 +1,104 @@
+use std::{fs::File, io::BufWriter};
+
+use anyhow::{anyhow, bail};
+use camino::{Utf8Path, Utf8PathBuf};
+use colorsquash::Squasher;
+use gifed::writer::{GifBuilder, ImageBuilder};
+use png::{ColorType, Decoder, Encoder};
+use zune_jpeg::{zune_core::colorspace::ColorSpace, JpegDecoder};
+
+pub struct Image {
+	pub width: usize,
+	pub height: usize,
+	pub data: Vec<u8>,
+}
+
+pub fn get_png<P: AsRef<Utf8Path>>(path: P) -> Result<Image, anyhow::Error> {
+	let decoder = Decoder::new(File::open(path.as_ref())?);
+	let mut reader = decoder.read_info()?;
+
+	let mut data = vec![0; reader.output_buffer_size()];
+	let info = reader.next_frame(&mut data)?;
+	data.resize(info.buffer_size(), 0);
+
+	let colors = info.color_type;
+	match colors {
+		ColorType::Grayscale | ColorType::GrayscaleAlpha | ColorType::Indexed => {
+			bail!("colortype {colors:?} not supported")
+		}
+		ColorType::Rgba => {
+			let pixels = info.width as usize * info.height as usize;
+
+			// the first RGB is fine, we don't need to touch it
+			for idx in 1..pixels {
+				data[idx * 3] = data[idx * 4];
+				data[idx * 3 + 1] = data[idx * 4 + 1];
+				data[idx * 3 + 2] = data[idx * 4 + 2];
+			}
+			data.resize(pixels * 3, 0);
+
+			Ok(Image {
+				width: info.width as usize,
+				height: info.height as usize,
+				data,
+			})
+		}
+		ColorType::Rgb => Ok(Image {
+			width: info.width as usize,
+			height: info.height as usize,
+			data,
+		}),
+	}
+}
+
+pub fn get_jpg<P: AsRef<Utf8Path>>(path: P) -> Result<Image, anyhow::Error> {
+	let content = std::fs::read(path.as_ref())?;
+	let mut dec = JpegDecoder::new(&content);
+	let pixels = dec.decode()?;
+	let info = dec
+		.info()
+		.ok_or(anyhow!("image had no info; this should be impossible"))?;
+
+	let colorspace = dec.get_output_colorspace();
+	match colorspace {
+		Some(ColorSpace::RGB) => (),
+		_ => bail!("colorspace {colorspace:?} not supported"),
+	}
+
+	Ok(Image {
+		width: info.width as usize,
+		height: info.height as usize,
+		data: pixels,
+	})
+}
+
+pub fn save_png(
+	image: Image,
+	squasher: Squasher<u8>,
+	path: Utf8PathBuf,
+) -> Result<(), anyhow::Error> {
+	let file = File::create(path)?;
+	let bufw = BufWriter::new(file);
+
+	let mut enc = Encoder::new(bufw, image.width as u32, image.height as u32);
+	enc.set_color(ColorType::Indexed);
+	enc.set_depth(png::BitDepth::Eight);
+	enc.set_palette(squasher.palette_bytes());
+	enc.write_header()?.write_image_data(&image.data)?;
+
+	Ok(())
+}
+
+pub fn save_gif(
+	image: Image,
+	squasher: Squasher<u8>,
+	path: Utf8PathBuf,
+) -> Result<(), anyhow::Error> {
+	GifBuilder::new(image.width as u16, image.height as u16)
+		.palette(squasher.palette_bytes().as_slice().try_into().unwrap())
+		.image(ImageBuilder::new(image.width as u16, image.height as u16).build(image.data)?)
+		.build()?
+		.save(path)?;
+
+	Ok(())
+}
diff --git a/squash/src/main.rs b/squash/src/main.rs
index 80c7cb0..d5cd2d8 100644
--- a/squash/src/main.rs
+++ b/squash/src/main.rs
@@ -1,166 +1,31 @@
-use std::{fs::File, io::BufWriter};
-
-use anyhow::{anyhow, bail};
-use camino::{Utf8Path, Utf8PathBuf};
 use colorsquash::Squasher;
-use gifed::writer::{GifBuilder, ImageBuilder};
-use png::{ColorType, Decoder, Encoder};
-use zune_jpeg::{zune_core::colorspace::ColorSpace, JpegDecoder};
-
-fn main() -> Result<(), anyhow::Error> {
-	// I should use clap or at least getopt, but this is fine. It's 20LOC.
-	let usage = || -> ! {
-		println!("usage: squash <color count> <input> <output>");
-		std::process::exit(0);
-	};
-	let mut argv = std::env::args().skip(1);
 
-	let color_count: u8 = if let Some(Ok(count)) = argv.next().map(|r| r.parse::<usize>()) {
-		if count > 256 {
-			eprintln!("max colour count must be 256 or below");
-			std::process::exit(1);
-		} else {
-			(count - 1) as u8
-		}
-	} else {
-		usage()
-	};
+use crate::cli::{InType, OutType};
 
-	let input_path: Utf8PathBuf = if let Some(path) = argv.next() {
-		path.into()
-	} else {
-		usage();
-	};
+mod cli;
+mod image;
 
-	let output_path: Utf8PathBuf = if let Some(path) = argv.next() {
-		path.into()
-	} else {
-		usage();
-	};
+fn main() -> Result<(), anyhow::Error> {
+	//gen: I should use clap or at least getopt, but this is fine.
+	let cli = cli::get();
 
-	let mut image = match input_path.extension() {
-		None => {
-			eprintln!("can't determine input filetype!\nSupported input types: PNG, JPG");
-			std::process::exit(1);
-		}
-		Some("png") => get_png(input_path)?,
-		Some("jpg") | Some("jpeg") => get_jpg(input_path)?,
-		Some(ext) => {
-			eprintln!("unknown filetype '{ext}'!\nSupported input types: PNG, JPG");
-			std::process::exit(1);
-		}
+	let mut image = match cli.in_type {
+		InType::Png => image::get_png(cli.input)?,
+		InType::Jpeg => image::get_jpg(cli.input)?,
 	};
 
-	let mut squasher = Squasher::new(color_count, &image.data);
+	let mut squasher = Squasher::new(cli.color_count, &image.data);
 	let size = squasher.map_over(&mut image.data);
 	image.data.resize(size, 0);
 
 	println!(
 		"selected {} colours of max {}",
 		squasher.palette().len(),
-		color_count
+		cli.color_count
 	);
 
-	match output_path.extension() {
-		None => {
-			eprintln!("can't determine output filetype! defaulting to png");
-			save_png(image, squasher, output_path)
-		}
-		Some("png") => save_png(image, squasher, output_path),
-		Some("gif") => save_gif(image, squasher, output_path),
-		Some(ext) => {
-			eprintln!("unknown filetype '{ext}'!\nSupport output types are: GIF, PNG");
-			std::process::exit(1);
-		}
+	match cli.out_type {
+		OutType::Png => image::save_png(image, squasher, cli.output),
+		OutType::Gif => image::save_gif(image, squasher, cli.output),
 	}
 }
-
-struct Image {
-	width: usize,
-	height: usize,
-	data: Vec<u8>,
-}
-
-fn get_png<P: AsRef<Utf8Path>>(path: P) -> Result<Image, anyhow::Error> {
-	let decoder = Decoder::new(File::open(path.as_ref())?);
-	let mut reader = decoder.read_info()?;
-
-	let mut data = vec![0; reader.output_buffer_size()];
-	let info = reader.next_frame(&mut data)?;
-	data.resize(info.buffer_size(), 0);
-
-	let colors = info.color_type;
-	match colors {
-		ColorType::Grayscale | ColorType::GrayscaleAlpha | ColorType::Indexed => {
-			bail!("colortype {colors:?} not supported")
-		}
-		ColorType::Rgba => {
-			let pixels = info.width as usize * info.height as usize;
-
-			// the first RGB is fine, we don't need to touch it
-			for idx in 1..pixels {
-				data[idx * 3] = data[idx * 4];
-				data[idx * 3 + 1] = data[idx * 4 + 1];
-				data[idx * 3 + 2] = data[idx * 4 + 2];
-			}
-			data.resize(pixels * 3, 0);
-
-			Ok(Image {
-				width: info.width as usize,
-				height: info.height as usize,
-				data,
-			})
-		}
-		ColorType::Rgb => Ok(Image {
-			width: info.width as usize,
-			height: info.height as usize,
-			data,
-		}),
-	}
-}
-
-fn get_jpg<P: AsRef<Utf8Path>>(path: P) -> Result<Image, anyhow::Error> {
-	let content = std::fs::read(path.as_ref())?;
-	let mut dec = JpegDecoder::new(&content);
-	let pixels = dec.decode()?;
-	let info = dec
-		.info()
-		.ok_or(anyhow!("image had no info; this should be impossible"))?;
-
-	let colorspace = dec.get_output_colorspace();
-	match colorspace {
-		Some(ColorSpace::RGB) => (),
-		_ => bail!("colorspace {colorspace:?} not supported"),
-	}
-
-	Ok(Image {
-		width: info.width as usize,
-		height: info.height as usize,
-		data: pixels,
-	})
-}
-
-fn save_png(image: Image, squasher: Squasher<u8>, path: Utf8PathBuf) -> Result<(), anyhow::Error> {
-	let file = File::create(path)?;
-	let bufw = BufWriter::new(file);
-
-	let mut enc = Encoder::new(bufw, image.width as u32, image.height as u32);
-	enc.set_color(ColorType::Indexed);
-	enc.set_depth(png::BitDepth::Eight);
-	enc.set_palette(squasher.palette_bytes());
-	enc.write_header()?.write_image_data(&image.data)?;
-
-	Ok(())
-}
-
-fn save_gif(image: Image, squasher: Squasher<u8>, path: Utf8PathBuf) -> Result<(), anyhow::Error> {
-	// I don't think I like this API anymore. It's a builder API, that's fine.
-	// I should make it so you can mutate the Gif directly.
-	GifBuilder::new(image.width as u16, image.height as u16)
-		.palette(squasher.palette_bytes().as_slice().try_into().unwrap())
-		.image(ImageBuilder::new(image.width as u16, image.height as u16).build(image.data)?)
-		.build()?
-		.save(path)?;
-
-	Ok(())
-}