Skip to content
Snippets Groups Projects
Verified Commit da178981 authored by Louis's avatar Louis :fire:
Browse files

Create library module and factor out utilities into library functions

parent 826157d0
No related branches found
No related tags found
No related merge requests found
This diff is collapsed.
...@@ -16,17 +16,22 @@ authors = [ ...@@ -16,17 +16,22 @@ authors = [
name = "crunch" name = "crunch"
path = "src/main.rs" path = "src/main.rs"
[features]
default = ["serialise_types", "filesystem"]
serialise_types = []
filesystem = []
[dependencies] [dependencies]
clap = { version = "4.5", features = ["derive"] } clap = { version = "4.5", features = ["derive"] }
env_logger = "0.11" env_logger = "0.11"
log = "0.4" log = "0.4"
anyhow = "1.0" anyhow = "1.0"
thiserror = "1.0" thiserror = "2.0"
deltae = "0.3" deltae = "0.3"
glam = "0.29" glam = "0.29"
image = "0.24" image = "0.25"
lab = "0.11" lab = "0.11"
num-traits = "0.2" num-traits = "0.2"
......
hard_tabs = true hard_tabs = true
#group_imports = "StdExternalCrate"
use_field_init_shorthand = true use_field_init_shorthand = true
use_try_shorthand = true use_try_shorthand = true
\ No newline at end of file
use crate::utils::{new_image, BasicRgba, OutputFormat, PaletteMap};
use image::{GenericImage, Pixel, Rgba};
use num_traits::ToPrimitive;
pub fn apply_colour_map(colour_map: &PaletteMap, image: &impl GenericImage) -> OutputFormat {
let mut output = new_image(image.width(), image.height());
for (x, y, pixel) in image.pixels() {
let pixel = pixel.to_rgba();
if pixel.0[3].to_u8().unwrap() == 0 {
output.put_pixel(x, y, BasicRgba::transparent().into());
continue;
}
let data = Rgba::from([
pixel.0[0].to_u8().unwrap(),
pixel.0[1].to_u8().unwrap(),
pixel.0[2].to_u8().unwrap(),
pixel.0[3].to_u8().unwrap(),
]);
let basic = BasicRgba::from(data);
match colour_map.get(&basic) {
Some(mapped_data) => output.put_pixel(
x,
y,
Rgba::from(BasicRgba {
a: basic.a,
..*mapped_data
}),
),
None => output.put_pixel(x, y, data),
};
}
output
}
mod colours;
pub use colours::apply_colour_map;
use clap::Parser;
use image::ImageFormat;
use serde::{Deserialize, Serialize};
use crate::commands::{ use crate::commands::{
Atlas, Extract, Extrude, Flip, Info, Palette, Pipeline, Reduce, Remap, Rotate, Scale, Split, Atlas, Extract, Extrude, Flip, Info, Palette, Pipeline, Reduce, Remap, Rotate, Scale, Split,
Swap, Swap,
}; };
use clap::Parser;
use crate::load_image; use crunch_cli::utils::{load_image, ColourPalette, PaletteMap};
use image::ImageFormat;
use serde::{Deserialize, Serialize};
/// Crunch is a set of utilities for quickly and easily processing a batch of files, either directly /// Crunch is a set of utilities for quickly and easily processing a batch of files, either directly
/// or by defining pipelines /// or by defining pipelines
...@@ -98,10 +96,10 @@ impl Args { ...@@ -98,10 +96,10 @@ impl Args {
let image_data = load_image(&remap.input, None)?; let image_data = load_image(&remap.input, None)?;
let palette_data = load_image(&remap.palette, None)?; let palette_data = load_image(&remap.palette, None)?;
let image_palette = Palette::extract_from(&image_data)?; let image_palette = ColourPalette::from(&image_data);
let target_palette = Palette::extract_from(&palette_data)?; let target_palette = ColourPalette::from(&palette_data);
let mappings = Palette::calculate_mapping(&image_palette, &target_palette); let mappings = PaletteMap::calculate_mapping(&image_palette, &target_palette);
let output = Remap::remap_image(image_data, mappings)?; let output = Remap::remap_image(image_data, mappings)?;
output output
...@@ -111,7 +109,7 @@ impl Args { ...@@ -111,7 +109,7 @@ impl Args {
Args::Swap(swap) => { Args::Swap(swap) => {
let image_data = load_image(&swap.input, None)?; let image_data = load_image(&swap.input, None)?;
let palette_data = load_image(&swap.palette, None)?; let palette_data = load_image(&swap.palette, None)?;
let palette_swap = Palette::from_columns(&palette_data)?; let palette_swap = PaletteMap::from_column_image(&palette_data)?;
let output = Swap::swap_pixels(image_data, palette_swap)?; let output = Swap::swap_pixels(image_data, palette_swap)?;
output output
.save_with_format(&swap.output, ImageFormat::Png) .save_with_format(&swap.output, ImageFormat::Png)
......
use clap::Parser; use clap::Parser;
use std::cmp::{min, Ordering}; use std::cmp::min;
use std::collections::hash_map::RandomState;
use std::collections::{HashMap, HashSet};
use anyhow::Error; use crunch_cli::utils::{new_image, BasicRgba, ColourPalette, PaletteFormat, PaletteMap};
use deltae::{Delta, LabValue, DE2000};
use image::{GenericImage, Pixel, Rgba}; use image::{GenericImage, Pixel, Rgba};
use num_traits::ToPrimitive;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::io::Write; use std::io::Write;
use crate::format::PaletteFormat;
use crate::utils::{new_image, BasicRgba};
pub type PixelPalette = Vec<BasicRgba>;
pub type ColourMapping = HashMap<BasicRgba, BasicRgba>;
pub fn sort_by_hue(palette: &mut PixelPalette) {
palette.sort_by(|pa, pb| {
let hue_a = pa.hue();
let hue_b = pb.hue();
if hue_a.is_nan() && hue_b.is_nan() {
Ordering::Equal
} else if hue_a.is_nan() {
Ordering::Less
} else if hue_b.is_nan() || hue_a > hue_b {
Ordering::Greater
} else if hue_b > hue_a {
Ordering::Less
} else {
Ordering::Equal
}
});
}
/// Create a palette file containing every distinct colour from the input image /// Create a palette file containing every distinct colour from the input image
#[derive(Parser, Clone, Serialize, Deserialize, Debug)] #[derive(Parser, Clone, Serialize, Deserialize, Debug)]
#[clap(author, version = "0.9.0")] #[clap(author, version = "0.9.0")]
...@@ -55,27 +26,12 @@ pub struct Palette { ...@@ -55,27 +26,12 @@ pub struct Palette {
impl Palette { impl Palette {
pub fn run(&self, image: &impl GenericImage) -> anyhow::Result<()> { pub fn run(&self, image: &impl GenericImage) -> anyhow::Result<()> {
let palette = Palette::extract_from(image)?; let palette = ColourPalette::from(image);
self.write_palette(palette) self.write_palette(palette)
} }
pub fn extract_from(image: &impl GenericImage) -> anyhow::Result<PixelPalette> { pub fn write_palette(&self, mut colours: ColourPalette) -> anyhow::Result<()> {
let mut colours = HashSet::new(); colours.sort_by_hue();
for (_, _, pixel) in image.pixels() {
let pixel = pixel.to_rgba();
colours.insert(Rgba::from([
pixel.0[0].to_u8().unwrap(),
pixel.0[1].to_u8().unwrap(),
pixel.0[2].to_u8().unwrap(),
pixel.0[3].to_u8().unwrap(),
]));
}
Ok(colours.iter().map(BasicRgba::from).collect())
}
pub fn write_palette(&self, mut colours: PixelPalette) -> anyhow::Result<()> {
sort_by_hue(&mut colours);
match self.format { match self.format {
PaletteFormat::Png => { PaletteFormat::Png => {
...@@ -121,96 +77,4 @@ impl Palette { ...@@ -121,96 +77,4 @@ impl Palette {
Ok(()) Ok(())
} }
pub fn from_columns(image: &impl GenericImage) -> anyhow::Result<ColourMapping> {
if image.width() != 2 {
return Err(Error::msg("Image must have a pixel width of 2"))?;
}
let mut mapping = ColourMapping::with_capacity(image.height() as usize);
for y in 0..image.height() {
let left = image.get_pixel(0, y);
let right = image.get_pixel(1, y);
let left = left.to_rgba();
let right = right.to_rgba();
let data = Rgba::from([
left.0[0].to_u8().unwrap(),
left.0[1].to_u8().unwrap(),
left.0[2].to_u8().unwrap(),
left.0[3].to_u8().unwrap(),
]);
let left_pixel = BasicRgba::from(data);
let data = Rgba::from([
right.0[0].to_u8().unwrap(),
right.0[1].to_u8().unwrap(),
right.0[2].to_u8().unwrap(),
right.0[3].to_u8().unwrap(),
]);
let right_pixel = BasicRgba::from(data);
mapping.insert(left_pixel, right_pixel);
}
Ok(mapping)
}
pub fn calculate_mapping(from: &PixelPalette, to: &PixelPalette) -> ColourMapping {
let colour_labs = Vec::from_iter(to.iter().map(LabValue::from));
let to_palette_vectors: HashMap<usize, &BasicRgba, RandomState> =
HashMap::from_iter(to.iter().enumerate());
let mut out_map: ColourMapping = HashMap::with_capacity(from.len());
for colour in from {
let closest = to_palette_vectors
.keys()
.fold(None, |lowest, idx| match lowest {
Some(num) => {
let current = colour_labs[*idx];
let previous: LabValue = colour_labs[num];
if colour.delta(current, DE2000) < colour.delta(previous, DE2000) {
Some(*idx)
} else {
Some(num)
}
}
None => Some(*idx),
});
match closest {
Some(idx) => match to_palette_vectors.get(&idx) {
Some(col) => {
out_map.insert(*colour, **col);
}
None => {
log::warn!("No matching set for {} with col {:?}", idx, &colour);
out_map.insert(
*colour,
BasicRgba {
r: 0,
g: 0,
b: 0,
a: 0,
},
);
}
},
None => {
log::warn!("No closest for {:?}", &colour);
out_map.insert(
*colour,
BasicRgba {
r: 0,
g: 0,
b: 0,
a: 0,
},
);
}
}
}
out_map
}
} }
use crate::commands::palette::ColourMapping;
use clap::Parser; use clap::Parser;
use image::{GenericImage, Pixel, Rgba}; use crunch_cli::actions::apply_colour_map;
use num_traits::ToPrimitive; use crunch_cli::utils::{OutputFormat, PaletteMap};
use image::GenericImage;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::utils::{new_image, BasicRgba, OutputFormat};
/// Use an existing colour mapping file to swap colours in the input sprite /// Use an existing colour mapping file to swap colours in the input sprite
#[derive(Debug, Clone, Parser, Serialize, Deserialize)] #[derive(Debug, Clone, Parser, Serialize, Deserialize)]
#[clap(author, version = "0.9.0")] #[clap(author, version = "0.9.0")]
...@@ -25,38 +23,8 @@ pub struct Swap { ...@@ -25,38 +23,8 @@ pub struct Swap {
impl Swap { impl Swap {
pub fn swap_pixels( pub fn swap_pixels(
image: impl GenericImage, image: impl GenericImage,
mappings: ColourMapping, mappings: PaletteMap,
) -> anyhow::Result<OutputFormat> { ) -> anyhow::Result<OutputFormat> {
let mut output = new_image(image.width(), image.height()); Ok(apply_colour_map(&mappings, &image))
for (x, y, pixel) in image.pixels() {
let pixel = pixel.to_rgba();
if pixel.0[3].to_u8().unwrap() == 0 {
output.put_pixel(x, y, Rgba::from(BasicRgba::transparent()));
continue;
}
let data = Rgba::from([
pixel.0[0].to_u8().unwrap(),
pixel.0[1].to_u8().unwrap(),
pixel.0[2].to_u8().unwrap(),
pixel.0[3].to_u8().unwrap(),
]);
let basic = BasicRgba::from(data);
match mappings.get(&basic) {
Some(mapped_data) => output.put_pixel(
x,
y,
Rgba::from(BasicRgba {
a: basic.a,
..*mapped_data
}),
),
None => output.put_pixel(x, y, data),
};
}
Ok(output)
} }
} }
pub mod actions;
pub mod utils;
#[cfg(feature = "filesystem")]
mod cli_args; mod cli_args;
mod commands; mod commands;
mod format;
mod utils;
use clap::Parser; use clap::Parser;
use crate::cli_args::Args; use crate::cli_args::Args;
use crate::format::load_image;
// This might look silly, but we want compilation of the CLI to fail if we disable filesystem support
#[cfg(feature = "filesystem")]
fn main() -> anyhow::Result<(), anyhow::Error> { fn main() -> anyhow::Result<(), anyhow::Error> {
env_logger::Builder::from_env("LOG_LEVEL").init(); env_logger::Builder::from_env("LOG_LEVEL").init();
let args: Args = Args::parse(); let args: Args = Args::parse();
......
...@@ -33,9 +33,8 @@ pub type RgbaOutputFormat = TypedOutputFormat<RgbaImage>; ...@@ -33,9 +33,8 @@ pub type RgbaOutputFormat = TypedOutputFormat<RgbaImage>;
pub fn new_image(new_width: u32, new_height: u32) -> OutputFormat { pub fn new_image(new_width: u32, new_height: u32) -> OutputFormat {
let mut new_image = RgbaImage::new(new_width, new_height); let mut new_image = RgbaImage::new(new_width, new_height);
let (new_image_x, new_image_y, new_image_width, new_image_height) = new_image.bounds(); for x in 0..new_width {
for x in new_image_x..new_image_width { for y in 0..new_height {
for y in new_image_y..new_image_height {
new_image.put_pixel(x, y, Rgba::from([0, 0, 0, 0])); new_image.put_pixel(x, y, Rgba::from([0, 0, 0, 0]));
} }
} }
...@@ -43,6 +42,10 @@ pub fn new_image(new_width: u32, new_height: u32) -> OutputFormat { ...@@ -43,6 +42,10 @@ pub fn new_image(new_width: u32, new_height: u32) -> OutputFormat {
} }
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
#[cfg_attr(
feature = "serialise_types",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct BasicRgba { pub struct BasicRgba {
pub r: u8, pub r: u8,
pub g: u8, pub g: u8,
...@@ -86,6 +89,17 @@ impl BasicRgba { ...@@ -86,6 +89,17 @@ impl BasicRgba {
} }
} }
impl From<[u8; 4]> for BasicRgba {
fn from(value: [u8; 4]) -> Self {
Self {
r: value[0],
g: value[1],
b: value[2],
a: value[3],
}
}
}
impl From<Rgba<u8>> for BasicRgba { impl From<Rgba<u8>> for BasicRgba {
fn from(other: Rgba<u8>) -> Self { fn from(other: Rgba<u8>) -> Self {
Self { Self {
......
use crate::utils::Format;
use image::io::Reader;
use image::{ImageError, RgbaImage};
use std::path::Path;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ImageLoadingError {
#[error("Failed to load image data; {0}")]
IoError(#[from] std::io::Error),
#[error("Failed to process image; {0}")]
ImageError(#[from] ImageError),
}
pub fn load_image(
path: impl ToString,
format: Option<Format>,
) -> anyhow::Result<RgbaImage, ImageLoadingError> {
let mut image = Reader::open(path.to_string())?;
let file = match format {
Some(format) => {
image.set_format(format.as_image_format());
image.decode()?.into_rgba8()
}
None => image.with_guessed_format()?.decode()?.into_rgba8(),
};
Ok(file)
}
pub fn make_paths<T: AsRef<Path>>(path: T) -> anyhow::Result<(), std::io::Error> {
if let Some(target) = path.as_ref().parent() {
std::fs::create_dir_all(target)
} else {
Ok(())
}
}
...@@ -30,6 +30,30 @@ pub enum Format { ...@@ -30,6 +30,30 @@ pub enum Format {
Bmp, Bmp,
} }
impl From<Format> for ImageFormat {
fn from(value: Format) -> Self {
value.as_image_format()
}
}
impl TryFrom<ImageFormat> for Format {
type Error = anyhow::Error;
fn try_from(value: ImageFormat) -> Result<Self, Self::Error> {
use Format::*;
match value {
ImageFormat::Png => Ok(Png),
ImageFormat::Jpeg => Ok(Jpg),
ImageFormat::Gif => Ok(Gif),
ImageFormat::Ico => Ok(Ico),
ImageFormat::Tga => Ok(Tga),
ImageFormat::Tiff => Ok(Tiff),
ImageFormat::Bmp => Ok(Bmp),
_ => Err(anyhow::anyhow!("Unsupported image format")),
}
}
}
impl Format { impl Format {
pub fn as_image_format(&self) -> ImageFormat { pub fn as_image_format(&self) -> ImageFormat {
use Format::*; use Format::*;
...@@ -44,35 +68,3 @@ impl Format { ...@@ -44,35 +68,3 @@ impl Format {
} }
} }
} }
#[derive(Error, Debug)]
pub enum ImageLoadingError {
#[error("Failed to load image data; {0}")]
IoError(#[from] std::io::Error),
#[error("Failed to process image; {0}")]
ImageError(#[from] ImageError),
}
pub fn load_image(
path: impl ToString,
format: Option<Format>,
) -> anyhow::Result<RgbaImage, ImageLoadingError> {
let mut image = Reader::open(path.to_string())?;
let file = match format {
Some(format) => {
image.set_format(format.as_image_format());
image.decode()?.into_rgba8()
}
None => image.with_guessed_format()?.decode()?.into_rgba8(),
};
Ok(file)
}
pub fn make_paths<T: AsRef<Path>>(path: T) -> anyhow::Result<(), std::io::Error> {
if let Some(target) = path.as_ref().parent() {
std::fs::create_dir_all(target)
} else {
Ok(())
}
}
use crate::utils::palette::ColourPalette;
use crate::utils::BasicRgba;
use anyhow::Error;
use deltae::{Delta, LabValue, DE2000};
use image::{GenericImage, Pixel, Rgba};
use num_traits::ToPrimitive;
use std::collections::HashMap;
use std::hash::RandomState;
use std::ops::{Deref, DerefMut};
#[derive(Clone, Debug)]
#[cfg_attr(
feature = "serialise_types",
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(feature = "serialise_types", serde(transparent))]
pub struct PaletteMap(HashMap<BasicRgba, BasicRgba>);
impl Deref for PaletteMap {
type Target = HashMap<BasicRgba, BasicRgba>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for PaletteMap {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl From<HashMap<BasicRgba, BasicRgba>> for PaletteMap {
fn from(other: HashMap<BasicRgba, BasicRgba>) -> Self {
PaletteMap(other)
}
}
impl FromIterator<(BasicRgba, BasicRgba)> for PaletteMap {
fn from_iter<T: IntoIterator<Item = (BasicRgba, BasicRgba)>>(iter: T) -> Self {
<PaletteMap as From<HashMap<BasicRgba, BasicRgba>>>::from(iter.into_iter().collect())
}
}
impl PaletteMap {
/// Create a new mapping between colours with no entries, and no space allocated.
pub fn empty() -> Self {
PaletteMap(HashMap::new())
}
/// Create a new, empty mapping between colours with a given amount of preallocated space.
pub fn with_capacity(capacity: usize) -> Self {
PaletteMap(HashMap::with_capacity(capacity))
}
pub fn from_column_image(image: &impl GenericImage) -> anyhow::Result<PaletteMap> {
if image.width() != 2 {
return Err(Error::msg("Image must have a pixel width of 2"))?;
}
let mut mapping = PaletteMap::with_capacity(image.height() as usize);
for y in 0..image.height() {
let left = image.get_pixel(0, y).to_rgba();
let right = image.get_pixel(1, y).to_rgba();
let left_pixel = BasicRgba::from([
left[0].to_u8().unwrap(),
left[1].to_u8().unwrap(),
left[2].to_u8().unwrap(),
left[3].to_u8().unwrap(),
]);
let right_pixel = BasicRgba::from([
right[0].to_u8().unwrap(),
right[1].to_u8().unwrap(),
right[2].to_u8().unwrap(),
right[3].to_u8().unwrap(),
]);
mapping.insert(left_pixel, right_pixel);
}
Ok(mapping)
}
/// Safe method for creating a palette map from two colour palettes. The colour palettes will be truncated to the length of the smaller palette,
/// possibly losing some colour information
pub fn from_palettes_lossy(from: &ColourPalette, to: &ColourPalette) -> PaletteMap {
let smallest = from.len().min(to.len());
let iter = from
.iter()
.take(smallest)
.copied()
.zip(to.iter().take(smallest).copied());
PaletteMap::from_iter(iter)
}
pub fn calculate_mapping(from: &ColourPalette, to: &ColourPalette) -> PaletteMap {
let colour_labs = Vec::from_iter(to.iter().map(LabValue::from));
let to_palette_vectors: HashMap<usize, &BasicRgba> =
HashMap::from_iter(to.iter().enumerate());
let mut out_map = PaletteMap::with_capacity(from.len());
for colour in from.iter() {
// Iterate all the possible output colours to find the "closest" match. Closeness is determined by the smallest Delta E2000 distance.
let closest = to_palette_vectors
.keys()
.fold(None, |lowest, idx| match lowest {
Some(num) => {
let current = colour_labs[*idx];
let previous: LabValue = colour_labs[num];
if colour.delta(current, DE2000) < colour.delta(previous, DE2000) {
Some(*idx)
} else {
Some(num)
}
}
None => Some(*idx),
});
// We perform a check just in case the output palette is empty. As long as there is at least
// one colour in the output palette, 'closest' will be `Some`, so we output a transparent pixel
// for the `None` edge case
match closest {
Some(idx) => match to_palette_vectors.get(&idx) {
Some(col) => {
out_map.insert(*colour, **col);
}
None => {
log::warn!("No matching set for {} with col {:?}", idx, &colour);
out_map.insert(
*colour,
BasicRgba {
r: 0,
g: 0,
b: 0,
a: 0,
},
);
}
},
None => {
log::warn!("No closest for {:?}", &colour);
out_map.insert(
*colour,
BasicRgba {
r: 0,
g: 0,
b: 0,
a: 0,
},
);
}
}
}
out_map
}
}
mod colours;
#[cfg(feature = "filesystem")]
mod files;
mod formats;
mod mapping;
mod palette;
pub use colours::*;
#[cfg(feature = "filesystem")]
pub use files::*;
pub use formats::*;
pub use mapping::*;
pub use palette::*;
use crate::utils::BasicRgba;
use image::{GenericImage, Pixel, Rgba};
use num_traits::ToPrimitive;
use std::cmp::Ordering;
use std::collections::HashSet;
use std::ops::{Deref, DerefMut, Index};
#[derive(Clone, Debug)]
#[cfg_attr(
feature = "serialise_types",
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(feature = "serialise_types", serde(transparent))]
pub struct ColourPalette(Vec<BasicRgba>);
impl Deref for ColourPalette {
type Target = Vec<BasicRgba>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for ColourPalette {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl From<Vec<BasicRgba>> for ColourPalette {
fn from(other: Vec<BasicRgba>) -> Self {
ColourPalette(other)
}
}
impl FromIterator<BasicRgba> for ColourPalette {
fn from_iter<T: IntoIterator<Item = BasicRgba>>(iter: T) -> Self {
<ColourPalette as From<Vec<BasicRgba>>>::from(iter.into_iter().collect())
}
}
impl ColourPalette {
/// Create a new colour palette with no entries, and no space allocated.
pub fn empty() -> Self {
ColourPalette(Vec::new())
}
/// Create a new, empty colour palette with a given amount of preallocated space.
pub fn with_capacity(capacity: usize) -> Self {
ColourPalette(Vec::with_capacity(capacity))
}
/// Mutate the palette in place, sorting entries by hue. Hue is sorted in ascending order by degree
pub fn sort_by_hue(&mut self) {
self.sort_by(|pa, pb| {
let hue_a = pa.hue();
let hue_b = pb.hue();
if hue_a.is_nan() && hue_b.is_nan() {
Ordering::Equal
} else if hue_a.is_nan() {
Ordering::Less
} else if hue_b.is_nan() || hue_a > hue_b {
Ordering::Greater
} else if hue_b > hue_a {
Ordering::Less
} else {
Ordering::Equal
}
});
}
}
impl<I: GenericImage> From<&I> for ColourPalette {
fn from(image: &I) -> Self {
let mut colours = HashSet::new();
for (_, _, pixel) in image.pixels() {
let pixel = pixel.to_rgba();
let parts = (
pixel[0].to_u8(),
pixel[1].to_u8(),
pixel[2].to_u8(),
pixel[3].to_u8(),
);
match parts {
(Some(r), Some(g), Some(b), Some(a)) => {
colours.insert(BasicRgba::from([r, g, b, a]));
}
_ => continue, // Ignore pixels with missing or invalid components
}
}
ColourPalette::from_iter(colours.into_iter())
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment