diff options
-rw-r--r-- | squash/src/cli.rs | 85 | ||||
-rw-r--r-- | squash/src/image.rs | 104 | ||||
-rw-r--r-- | squash/src/main.rs | 163 |
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(()) -} |