diff --git a/agb-image-converter/src/colour.rs b/agb-image-converter/src/colour.rs new file mode 100644 index 00000000..4592a450 --- /dev/null +++ b/agb-image-converter/src/colour.rs @@ -0,0 +1,12 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Colour { + pub r: u8, + pub g: u8, + pub b: u8, +} + +impl Colour { + pub fn from_rgb(r: u8, g: u8, b: u8) -> Self { + Colour { r, g, b } + } +} diff --git a/agb-image-converter/src/image_loader.rs b/agb-image-converter/src/image_loader.rs new file mode 100644 index 00000000..aa3f5921 --- /dev/null +++ b/agb-image-converter/src/image_loader.rs @@ -0,0 +1,37 @@ +use std::path; + +use image::GenericImageView; + +use crate::colour::Colour; + +pub(crate) struct Image { + pub width: usize, + pub height: usize, + colour_data: Vec, +} + +impl Image { + pub fn load_from_file(image_path: &path::Path) -> Self { + let img = image::open(image_path).expect("Expected image to exist"); + let (width, height) = img.dimensions(); + + let width = width as usize; + let height = height as usize; + + let mut colour_data = Vec::with_capacity(width * height); + + for (_, _, pixel) in img.pixels() { + colour_data.push(Colour::from_rgb(pixel[0], pixel[1], pixel[2])); + } + + Image { + width, + height, + colour_data, + } + } + + pub fn colour(&self, x: usize, y: usize) -> Colour { + self.colour_data[x + y * self.width] + } +} diff --git a/agb-image-converter/src/lib.rs b/agb-image-converter/src/lib.rs index 10e07096..3f3905ca 100644 --- a/agb-image-converter/src/lib.rs +++ b/agb-image-converter/src/lib.rs @@ -1,6 +1,10 @@ -use std::path; +use std::path::PathBuf; mod colour; +mod image_loader; +mod palette16; + +use image_loader::Image; pub use colour::Colour; @@ -10,11 +14,59 @@ pub enum TileSize { Tile16, } +impl TileSize { + fn to_size(&self) -> usize { + match &self { + TileSize::Tile8 => 8, + TileSize::Tile16 => 16, + } + } +} + pub struct ImageConverterConfig { pub transparent_colour: Option, pub tile_size: TileSize, - pub input_file: path::PathBuf, - pub output_file: path::PathBuf, + pub input_image: PathBuf, + pub output_file: PathBuf, } -pub fn convert_image(setting: &ImageConverterConfig) {} +pub fn convert_image(settings: &ImageConverterConfig) { + let image = Image::load_from_file(&settings.input_image); + + let tile_size = settings.tile_size.to_size(); + if image.width % tile_size != 0 || image.height % tile_size != 0 { + panic!("Image size not a multiple of tile size"); + } + + let optimiser = optimiser_for_image(&image, tile_size); + let optimisation_results = optimiser.optimise_palettes(settings.transparent_colour); + + println!("{:?}", optimisation_results); +} + +fn optimiser_for_image(image: &Image, tile_size: usize) -> palette16::Palette16Optimiser { + let tiles_x = image.width / tile_size; + let tiles_y = image.height / tile_size; + + let mut palette_optimiser = palette16::Palette16Optimiser::new(); + + for y in 0..tiles_y { + for x in 0..tiles_x { + let mut palette = palette16::Palette16::new(); + + for j in 0..tile_size { + for i in 0..tile_size { + let colour = image.colour(x * tile_size + i, y * tile_size + j); + + if !palette.add_colour(colour) { + panic!("Tile contains more than 16 colours"); + } + } + } + + palette_optimiser.add_palette(palette); + } + } + + palette_optimiser +} diff --git a/agb-image-converter/src/palette16.rs b/agb-image-converter/src/palette16.rs new file mode 100644 index 00000000..5522c937 --- /dev/null +++ b/agb-image-converter/src/palette16.rs @@ -0,0 +1,170 @@ +use crate::colour::Colour; +use std::collections::HashSet; + +const MAX_COLOURS: usize = 256; +const MAX_COLOURS_PER_PALETTE: usize = 16; + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub(crate) struct Palette16 { + colours: Vec, +} + +impl Palette16 { + pub fn new() -> Self { + Palette16 { + colours: Vec::with_capacity(MAX_COLOURS_PER_PALETTE), + } + } + + pub fn add_colour(&mut self, colour: Colour) -> bool { + if self.colours.contains(&colour) { + return false; + } + + if self.colours.len() == MAX_COLOURS_PER_PALETTE { + panic!("Can have at most 16 colours in a single palette"); + } + self.colours.push(colour); + true + } + + fn union_length(&self, other: &Palette16) -> usize { + self.colours + .iter() + .chain(&other.colours) + .collect::>() + .len() + } + + fn is_satisfied_by(&self, other: &Palette16) -> bool { + self.colours + .iter() + .collect::>() + .is_subset(&other.colours.iter().collect::>()) + } +} + +pub(crate) struct Palette16Optimiser { + palettes: Vec, + colours: Vec, +} + +#[derive(Debug)] +pub(crate) struct Palette16OptimisationResults { + pub optimised_palettes: Vec, + pub assignments: Vec, +} + +impl Palette16Optimiser { + pub fn new() -> Self { + Palette16Optimiser { + palettes: vec![], + colours: Vec::new(), + } + } + + pub fn add_palette(&mut self, palette: Palette16) { + self.palettes.push(palette.clone()); + + for colour in palette.colours { + if self.colours.contains(&colour) { + continue; + } + + self.colours.push(colour); + } + + if self.colours.len() > MAX_COLOURS { + panic!("Cannot have over 256 colours"); + } + } + + pub fn optimise_palettes( + &self, + transparent_colour: Option, + ) -> Palette16OptimisationResults { + let mut assignments = vec![0; self.palettes.len()]; + let mut optimised_palettes = vec![]; + + let mut unsatisfied_palettes = self + .palettes + .iter() + .cloned() + .collect::>(); + + while unsatisfied_palettes.len() > 0 { + let palette = self.find_maximal_palette_for(&unsatisfied_palettes, transparent_colour); + + for test_palette in unsatisfied_palettes.clone() { + if test_palette.is_satisfied_by(&palette) { + unsatisfied_palettes.remove(&test_palette); + } + } + + for (i, overall_palette) in self.palettes.iter().enumerate() { + if overall_palette.is_satisfied_by(&palette) { + assignments[i] = optimised_palettes.len(); + } + } + + optimised_palettes.push(palette); + + if optimised_palettes.len() == MAX_COLOURS / MAX_COLOURS_PER_PALETTE { + panic!("Failed to find covering palettes"); + } + } + + Palette16OptimisationResults { + assignments, + optimised_palettes, + } + } + + fn find_maximal_palette_for( + &self, + unsatisfied_palettes: &HashSet, + transparent_colour: Option, + ) -> Palette16 { + let mut palette = Palette16::new(); + + if let Some(transparent_colour) = transparent_colour { + palette.add_colour(transparent_colour); + } + + loop { + let mut colour_usage = vec![0; MAX_COLOURS]; + let mut a_colour_is_used = false; + + for current_palette in unsatisfied_palettes { + if palette.union_length(¤t_palette) > MAX_COLOURS_PER_PALETTE { + continue; + } + + for colour in ¤t_palette.colours { + if let Some(colour_index) = self.colours.iter().position(|c| c == colour) { + colour_usage[colour_index] += 1; + a_colour_is_used = true; + } + } + } + + if !a_colour_is_used { + return palette; + } + + let best_index = colour_usage + .iter() + .enumerate() + .max_by(|(_, usage1), (_, usage2)| usage1.cmp(usage2)) + .unwrap() + .0; + + let best_colour = self.colours[best_index]; + + palette.add_colour(best_colour); + if palette.colours.len() == MAX_COLOURS_PER_PALETTE { + return palette; + } + } + } +}