use std::fmt;
use crate::colors::Myrgb;
use palette::convert::FromColorUnclamped;
use palette::cast::ComponentsAs;
use palette::IntoColor;
use palette::Clamp;
use palette::Srgb;
use palette::Mix;
use serde::{Serialize, Deserialize};
use owo_colors::AnsiColors;
use itertools::Itertools;
pub use fallback_generator::FallbackGenerator;
use salience::Salient;
mod lab;
mod lch;
pub mod salience;
mod lchansi;
mod util;
mod fallback_generator;
use fallback_generator::FallbackGenerator as G;
pub const MIN_COLS: u8 = 6;
pub const MAX_COLS: u8 = 16;
pub enum ColorOrder {
LightFirst,
DarkFirst,
}
use self::ColorSpace as Cs;
#[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone, Copy, Default, clap::ValueEnum)]
#[cfg_attr(feature = "doc" , derive(documented::Documented, documented::DocumentedFields))]
#[cfg_attr(feature = "iter", derive(strum::EnumIter))]
#[cfg_attr(feature = "schema" , derive(schemars::JsonSchema))]
#[serde(rename_all = "lowercase")]
pub enum ColorSpace {
Lab,
#[clap(alias = "lab-mixed", name = "labmixed")] #[serde(alias = "lab-mixed")]
LabMixed,
#[default]
Lch,
#[clap(alias = "lch-mixed", name = "lchmixed")] #[serde(alias = "lch-mixed")]
LchMixed,
#[clap(alias = "salience", name = "salience")] #[serde(alias = "salience")]
Salience,
#[clap(alias = "lch-ansi", name = "lchansi")] #[serde(alias = "lch-ansi")]
LchAnsi,
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Histo<T: ColorTrait> {
color: T,
count: usize,
}
impl<T: ColorTrait> Histo<T> {
pub fn new(color: T, count: usize) -> Self { Self { color, count } }
pub fn new_no_count(color: T) -> Self { Self { color, count: usize::MAX } }
}
impl<T: ColorTrait> From<HistoScore<T>> for Histo<T> {
fn from(hs: HistoScore<T>) -> Self {
Self { color: hs.histo.color, count: hs.histo.count }
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct HistoScore<T: ColorTrait> {
histo: Histo<T>,
score: f32,
}
impl<T: ColorTrait> HistoScore<T> {
pub fn new(histo:Histo<T>, score: f32) -> Self { Self { histo, score } }
}
impl<T: ColorTrait> From<Histo<T>> for HistoScore<T> {
fn from(h: Histo<T>) -> Self {
Self { histo: h, score: 0.0 }
}
}
pub fn sort_histogram_by_score<T: ColorTrait + Salient>(histo: Vec<Histo<T>>, bg: T) -> Vec<Histo<T>> {
let mut histoscore: Vec<HistoScore<T>> = histo.into_iter().map(HistoScore::from).collect();
let cnt_sclr = 4.0;
let sal_factor = 1.0 / 20.0 * 1.5;
histoscore.iter_mut().for_each(|hs| {
let score_log_scaled = (hs.histo.count as f32).powf(1.0/cnt_sclr);
let col = hs.histo.color;
hs.score = score_log_scaled * (1.0 + col.improved_salience(&bg)*sal_factor);
});
histoscore.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Greater));
histoscore.into_iter().map(Histo::from).collect()
}
pub fn run_dynamic<C: BuildHisto<U>, U: ColorTrait + std::marker::Send> (
bytes: &[u8],
_threshold: u8,
gen: &G,
mix: bool,
ord: &ColorOrder,
dedup: bool,
) -> Option<(Vec<Srgb>, Vec<Srgb>, bool)> {
use std::thread;
use std::collections::HashMap;
use std::sync::mpsc;
let mut warn = false;
let mut fallback = false;
let mut threshold = 20;
let (txfinal, rxfinal) = mpsc::channel();
thread::scope(|s| {
let mut histo = vec![];
let min_threshold = 2;
let mut hash: HashMap<usize, Vec<u8>> = HashMap::from([(0, vec![0])]);
let idx = [14, 16, 13, 17, 12, 18, 11, 19, 10, 20, 9, 21, 8, 22, 7, 23, 6, 24, 5, 25, 4,
26, 3, 27, 2, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44];
'running: for i in 0..idx.len() {
let th1 = idx[i];
let myread1 = s.spawn(move || C::init(bytes, th1, mix, ord));
let th2 = idx[i+1];
let myread2 = s.spawn(move || C::init(bytes, th2, mix, ord));
let th3 = idx[i+2];
let myread3 = s.spawn(move || C::init(bytes, th3, mix, ord));
for (storage, th) in [(myread1, th1), (myread2, th2), (myread3, th3)] {
threshold = th;
match storage.join().expect("Waiting for the thread.") {
Some(s) => {
let s = if dedup {
let s = C::dedup_cols(s, threshold, ord);
let s = C::process_deduped(s, ord, bytes);
C::clip_cols(s, ord)
} else { s };
let len = s.len();
if len >= MIN_COLS as usize
&& len <= MAX_COLS as usize {
histo = s;
break 'running
}
let len = s.len();
if len >= MIN_COLS as usize && len <= MAX_COLS as usize {
histo = s;
break 'running;
}
hash.entry(len).or_default().push(threshold);
},
None => 'nocolor: {
let max = *hash.iter().max_by(|a, b| a.0.cmp(b.0)).expect("not empty").0;
if max < 2 { break 'nocolor }
if threshold == 2 && max < 2 { return }
if threshold < 10 && max < MIN_COLS.into()
{
let possible_ths = hash.get(&max).expect("not empty");
let median = possible_ths[possible_ths.len() / 2]; threshold = median;
fallback = true;
}
},
}
if threshold == min_threshold { break 'running }
}
}
txfinal.send(histo).expect("Sending message MPSC");
});
let mut histo = rxfinal.recv().expect("Receiving message MPSC");
let len = histo.len();
if len < 2 { return None }
if len == 2 {
warn = true;
histo = C::fallback_monochromatic(histo, gen);
} else if fallback || len < MIN_COLS.into() {
warn = true;
histo = C::fallback(histo, threshold, gen);
}
let orig = C::to_rgb(&histo);
let top = C::sort_col(histo, ord);
let top = C::to_rgb(&top);
Some( (top, orig, warn) )
}
pub fn run_once<C: BuildHisto<U>, U: ColorTrait>(
bytes: &[u8],
threshold: u8,
gen: &G,
mix: bool,
ord: &ColorOrder,
dedup: bool,
) -> Option<(Vec<Srgb>, Vec<Srgb>, bool)> {
let mut warn = false;
let ret = match C::init(bytes, threshold, mix, ord) {
Some(s) => {
let s = if dedup {
let s = C::dedup_cols(s, threshold, ord);
let s = C::process_deduped(s, ord, bytes);
C::clip_cols(s, ord)
} else {
s
};
let len = s.len();
if len == 2 { warn = true;
Some(C::fallback_monochromatic(s, gen))
} else if len < MIN_COLS.into() { warn = true;
Some(C::fallback(s, threshold, gen))
} else if len < 2 { warn = true;
None
} else { warn = false;
Some(s)
}
},
None => None,
};
let ret = ret?;
let orig = C::to_rgb(&ret);
let top = C::sort_col(ret, ord);
let top = C::to_rgb(&top);
Some( (top, orig, warn) )
}
impl ColorSpace {
pub fn run(&self, dynamic: bool, bytes_rgb8: &[u8], threshold: u8, gen: &G, ord: &ColorOrder) -> Option<(Vec<Srgb>, Vec<Srgb>, bool)> {
match dynamic {
true => self.run_dynamic(bytes_rgb8, threshold, gen, ord),
false => self.run_once (bytes_rgb8, threshold, gen, ord),
}
}
pub fn run_once(&self, bytes_rgb8: &[u8], threshold: u8 , gen: &G, ord: &ColorOrder) -> Option<(Vec<Srgb>, Vec<Srgb>, bool)> {
let mix = self.mixed();
let dedup = self.to_dedup();
let f = match self {
Cs::Lab => run_once::<lab::Lab, lab::Spec>,
Cs::LabMixed => run_once::<lab::Lab, lab::Spec>,
Cs::Lch => run_once::<lch::Lch, lch::Spec>,
Cs::LchMixed => run_once::<lch::Lch, lch::Spec>,
Cs::LchAnsi => run_once::<lchansi::LchAnsi, lch::Spec>,
Cs::Salience => run_once::<salience::Salience, salience::Spec>,
};
f(bytes_rgb8, threshold, gen, mix, ord, dedup)
}
pub fn run_dynamic(&self, bytes_rgb8: &[u8], threshold: u8, gen: &G, ord: &ColorOrder) -> Option<(Vec<Srgb>, Vec<Srgb>, bool)> {
let mix = self.mixed();
let dedup = self.to_dedup();
match self {
Cs::Lab => run_dynamic::<lab::Lab, lab::Spec>(bytes_rgb8, threshold, gen, mix, ord, dedup),
Cs::LabMixed => run_dynamic::<lab::Lab, lab::Spec>(bytes_rgb8, threshold, gen, mix, ord, dedup),
Cs::Lch => run_dynamic::<lch::Lch, lch::Spec>(bytes_rgb8, threshold, gen, mix, ord, dedup),
Cs::LchMixed => run_dynamic::<lch::Lch, lch::Spec>(bytes_rgb8, threshold, gen, mix, ord, dedup),
Cs::LchAnsi => run_dynamic::<lchansi::LchAnsi, lch::Spec>(bytes_rgb8, threshold, gen, mix, ord, dedup),
Cs::Salience => run_dynamic::<salience::Salience, salience::Spec>(bytes_rgb8, threshold, gen, mix, ord, dedup),
}
}
pub fn mixed(&self) -> bool {
match self {
Cs::LabMixed | Cs::LchMixed => true,
Cs::Lch | Cs::Lab | Cs::Salience | Cs::LchAnsi => false,
}
}
pub fn to_dedup(&self) -> bool {
match self {
Cs::LabMixed | Cs::LchMixed | Cs::Lch | Cs::Salience | Cs::Lab => true,
Cs::LchAnsi => false,
}
}
pub fn col(&self) -> AnsiColors {
match self {
Cs::Lab => AnsiColors::Blue,
Cs::LabMixed => AnsiColors::Green,
Cs::Lch => AnsiColors::Magenta,
Cs::LchMixed => AnsiColors::Magenta,
Cs::Salience => AnsiColors::White,
Cs::LchAnsi => AnsiColors::Cyan,
}
}
}
impl<T: ColorTrait> From<Histo<T>> for Myrgb {
fn from(h: Histo<T>) -> Self {
h.color.into()
}
}
impl From<Srgb<u8>> for Myrgb {
fn from(c: Srgb<u8>) -> Self {
Self(c.into_format())
}
}
impl From<Myrgb> for Srgb<u8> {
fn from(c: Myrgb) -> Self {
c.0.into_format()
}
}
pub trait Difference {
fn col_diff(&self, a: &Self, threshold: u8) -> bool;
}
impl<T: ColorTrait> From<T> for Myrgb {
fn from(lab: T) -> Self {
let a: Srgb = lab.into_color();
Self(a)
}
}
pub trait ColorTrait:
Copy
+ std::fmt::Debug
+ Difference
+ Into<Myrgb>
+ IntoColor<Srgb>
+ Mix<Scalar = f32>
+ FromColorUnclamped<Srgb>
+ Clamp
+ palette::convert::FromColorUnclamped<palette::rgb::Rgb<palette::encoding::Linear<palette::encoding::Srgb>>>
{}
pub trait BuildHisto<C: ColorTrait> {
fn init(bytes: &[u8], threshold: u8, mix: bool, _cs: &ColorOrder) -> Option<Vec<Histo<C>>> {
let b = Self::read(bytes);
let ret = Self::gather_cols(b, threshold, mix);
if ret.len() < 2 { None } else { Some(ret) }
}
fn fallback(histo: Vec<Histo<C>>, threshold: u8, gen: &G) -> Vec<Histo<C>> {
let mut histo = histo;
let mut new = Self::color_generator(&histo, threshold, gen);
histo.append(&mut new);
histo.sort_by(|a, b| b.count.cmp(&a.count));
histo.truncate(MAX_COLS.into());
histo
}
fn fallback_monochromatic(histo: Vec<Histo<C>>, gen: &G) -> Vec<Histo<C>> {
let mut histo = histo;
let mut new = gen.gen()(histo[0].color.into_color(), histo[1].color.into_color(), MIN_COLS)
.iter()
.map(|&x| {
let c: C = x.into_color();
Histo { color: c, count: 1 }
})
.collect::<Vec<Histo<C>>>();
histo.append(&mut new);
histo.sort_by(|a, b| b.count.cmp(&a.count));
histo.truncate(MAX_COLS.into());
histo
}
fn dedup_cols(histo: Vec<Histo<C>>, threshold: u8, _cs: &ColorOrder) -> Vec<Histo<C>> {
let mut histo = histo;
histo.sort_by_key(|&a| Self::sort_by_key_fn(a));
histo
.iter_mut()
.dedup_by_with_count(|a, b| a.color.col_diff(&b.color, threshold))
.for_each(|x| x.1.count += x.0);
histo
}
fn process_deduped(histo: Vec<Histo<C>>, _ord: &ColorOrder, _bytes: &[u8])-> Vec<Histo<C>> {
let mut histo = histo;
histo.sort_by(|a, b| b.count.cmp(&a.count));
histo
}
fn clip_cols(histo: Vec<Histo<C>>, _ord: &ColorOrder)-> Vec<Histo<C>> {
let mut histo = histo;
histo.truncate(MAX_COLS.into());
histo
}
fn read(bytes: &[u8]) -> Vec<C> { read(bytes) }
fn filter_cols(histo: Vec<C>) -> Vec<C>;
fn sort_col(histo: Vec<Histo<C>>, cs: &ColorOrder) -> Vec<Histo<C>>;
fn sort_by_key_fn(a: Histo<C>) -> impl Ord;
fn color_generator(histo: &[Histo<C>], threshold: u8, gen: &G) -> Vec<Histo<C>> {
let mut new_cols = vec![];
for comb in histo.iter().combinations(2) {
let color_a: Srgb = comb[0].color.into_color();
let color_b: Srgb = comb[1].color.into_color();
let rgbs = gen.gen()(color_a, color_b, MAX_COLS)
.iter().map(|&x| x.into_color()).collect();
new_cols.append(&mut Self::gather_cols(rgbs, threshold, false));
let len = histo.len() + new_cols.len();
if len >= MIN_COLS.into() { break; } }
new_cols
}
fn gather_cols(colors: Vec<C>, threshold: u8, mix: bool) -> Vec<Histo<C>> {
let mut histogram: Vec<Histo<C>> = vec![];
let colors: Vec<C> = Self::filter_cols(colors);
'outter: for c in colors {
for hist in &mut histogram {
if c.col_diff(&hist.color, threshold) {
if mix { hist.color = hist.color.mix(c, 0.5); }
hist.count += 1;
continue 'outter;
}
}
histogram.push(Histo { color: c, count: 1 });
}
histogram.into()
}
fn to_rgb(histo: &[Histo<C>]) -> Vec<Srgb> { histo.iter().map(|x| x.color.into_color()).collect() }
}
impl fmt::Display for Cs {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Cs::Lab => write!(f, "Lab"),
Cs::LabMixed => write!(f, "LabMixed"),
Cs::Lch => write!(f, "Lch"),
Cs::LchMixed => write!(f, "LchMixed"),
Cs::Salience => write!(f, "Salience"),
Cs::LchAnsi => write!(f, "LchAnsi"),
}
}
}
fn read<T: ColorTrait>(bytes: &[u8]) -> Vec<T> {
let s: &[Srgb<u8>] = bytes.components_as();
s
.iter()
.map(|x| x.into_linear().into_color())
.collect::<Vec<T>>()
}