use indexmap::IndexSet;
use rgb::RGBA8;
use crate::{
colors::{BitDepth, ColorType},
headers::IhdrData,
png::{PngImage, scan_lines::ScanLine},
};
#[must_use]
pub fn reduced_palette(png: &PngImage, optimize_alpha: bool) -> Option<PngImage> {
if png.ihdr.bit_depth != BitDepth::Eight {
return None;
}
let ColorType::Indexed { palette } = &png.ihdr.color_type else {
return None;
};
let mut used = [false; 256];
for &byte in &png.data {
used[byte as usize] = true;
}
let black = RGBA8::new(0, 0, 0, 255);
let mut condensed = IndexSet::with_capacity(palette.len());
let mut byte_map = [0; 256];
let mut did_change = false;
for (i, used) in used.iter().enumerate() {
if !used {
continue;
}
let color = *palette.get(i).unwrap_or(&black);
byte_map[i] = add_color_to_set(color, &mut condensed, optimize_alpha);
if byte_map[i] as usize != i {
did_change = true;
}
}
let data = if did_change {
png.data.iter().map(|b| byte_map[*b as usize]).collect()
} else if condensed.len() != palette.len() {
png.data.clone()
} else {
return None;
};
let palette: Vec<_> = condensed.into_iter().collect();
Some(PngImage {
ihdr: IhdrData {
color_type: ColorType::Indexed { palette },
..png.ihdr
},
data,
})
}
fn add_color_to_set(mut color: RGBA8, set: &mut IndexSet<RGBA8>, optimize_alpha: bool) -> u8 {
if optimize_alpha && color.a == 0 {
color.r = 0;
color.g = 0;
color.b = 0;
}
let (idx, _) = set.insert_full(color);
idx as u8
}
#[must_use]
pub fn sorted_palette(png: &PngImage) -> Option<PngImage> {
if png.ihdr.bit_depth != BitDepth::Eight {
return None;
}
let palette = match &png.ihdr.color_type {
ColorType::Indexed { palette } if palette.len() > 1 => palette,
_ => return None,
};
let mut enumerated: Vec<_> = palette.iter().enumerate().collect();
let keep_first = most_popular_edge_color(palette.len(), png);
let first = keep_first.map(|f| enumerated.remove(f));
enumerated.sort_by(|a, b| {
let color_val = |color: &RGBA8| {
let a = i32::from(color.a);
((a & 0xFE) << 18) + (a & 0x01)
- i32::from(color.r) * 299
- i32::from(color.g) * 587
- i32::from(color.b) * 114
};
color_val(a.1).cmp(&color_val(b.1))
});
if let Some(first) = first {
enumerated.insert(0, first);
}
let (remapping, palette): (Vec<_>, Vec<RGBA8>) = enumerated.into_iter().unzip();
if remapping.iter().enumerate().all(|(a, b)| a == *b) {
return None;
}
let mut byte_map = [0; 256];
for (i, &v) in remapping.iter().enumerate() {
byte_map[v] = i as u8;
}
let data = png.data.iter().map(|&b| byte_map[b as usize]).collect();
Some(PngImage {
ihdr: IhdrData {
color_type: ColorType::Indexed { palette },
..png.ihdr
},
data,
})
}
#[must_use]
pub fn sorted_palette_mzeng(png: &PngImage, matrix: &CoOccurrenceMatrix) -> Option<PngImage> {
let mut remapping = mzeng_reindex(matrix);
apply_most_popular_color(&mut remapping, matrix, png.data.len() * 3 / 20);
apply_palette_reorder(png, &remapping)
}
#[must_use]
pub fn sorted_palette_ezeng(
png: &PngImage,
matrix: &CoOccurrenceMatrix,
max_swap_dist: u8,
) -> Option<PngImage> {
let mut remapping = ezeng_reindex(matrix);
pairwise_swap_search(&mut remapping, matrix, max_swap_dist);
apply_most_popular_color(&mut remapping, matrix, png.data.len() * 3 / 20);
apply_palette_reorder(png, &remapping)
}
#[must_use]
pub fn sorted_palette_battiato(png: &PngImage, matrix: &CoOccurrenceMatrix) -> Option<PngImage> {
let mut remapping = battiato_reindex(matrix);
apply_most_popular_color(&mut remapping, matrix, 0);
apply_palette_reorder(png, &remapping)
}
fn apply_palette_reorder(png: &PngImage, remapping: &[usize]) -> Option<PngImage> {
let ColorType::Indexed { palette } = &png.ihdr.color_type else {
return None;
};
assert!(remapping.len() == palette.len());
if remapping.iter().enumerate().all(|(a, b)| a == *b) {
return None;
}
let mut new_palette = Vec::new();
let mut byte_map = [0; 256];
for (i, &v) in remapping.iter().enumerate() {
new_palette.push(palette[v]);
byte_map[v] = i as u8;
}
let data = png.data.iter().map(|&b| byte_map[b as usize]).collect();
Some(PngImage {
ihdr: IhdrData {
color_type: ColorType::Indexed {
palette: new_palette,
},
..png.ihdr
},
data,
})
}
fn most_popular_edge_color(num_colors: usize, png: &PngImage) -> Option<usize> {
let mut counts = [0_u32; 256];
for line in png.scan_lines(false) {
if let &[first, .., last] = line.data {
counts[first as usize] += 1;
counts[last as usize] += 1;
}
}
let max = counts
.iter()
.take(num_colors)
.enumerate()
.max_by_key(|&(_, v)| v)
.unwrap();
let max_equal = counts.iter().filter(|&v| v == max.1).count();
if max_equal > 1 {
return None;
}
Some(max.0)
}
fn apply_most_popular_color(remapping: &mut [usize], matrix: &CoOccurrenceMatrix, min: usize) {
let most_popular = matrix.most_popular_color();
if most_popular.1 < min as u32 {
return;
}
let first_idx = remapping.iter().position(|&i| i == most_popular.0).unwrap();
if first_idx >= remapping.len() / 2 {
remapping.reverse();
remapping.rotate_right(first_idx + 1);
} else {
remapping.rotate_left(first_idx);
}
}
fn mzeng_reindex(matrix: &CoOccurrenceMatrix) -> Vec<usize> {
let edges = &matrix.weighted_edges;
let mut remapping = vec![edges[0].0 as usize, edges[0].1 as usize];
let mut sums = Vec::new();
let mut best_sum_pos = 0;
let mut best_sum = (0, 0);
for i in 0..matrix.num_colors {
let m_row = matrix.row(i);
if i == remapping[0] || i == remapping[1] {
continue;
}
let sum = (i, m_row[remapping[0]] + m_row[remapping[1]]);
if sum.1 > best_sum.1 {
best_sum_pos = sums.len();
best_sum = sum;
}
sums.push(sum);
}
while !sums.is_empty() {
let best_index = best_sum.0;
let mut delta: isize = 0;
let n = (matrix.num_colors - sums.len()) as isize;
let best_row = matrix.row(best_index);
for (i, &index) in remapping.iter().enumerate() {
delta += (n - 1 - 2 * i as isize) * best_row[index] as isize;
}
if delta > 0 {
remapping.insert(0, best_index);
} else {
remapping.push(best_index);
}
sums.swap_remove(best_sum_pos);
if !sums.is_empty() {
best_sum_pos = 0;
best_sum = (0, 0);
for (i, sum) in sums.iter_mut().enumerate() {
sum.1 += best_row[sum.0];
if sum.1 > best_sum.1 {
best_sum_pos = i;
best_sum = *sum;
}
}
}
}
remapping
}
fn ezeng_reindex(matrix: &CoOccurrenceMatrix) -> Vec<usize> {
let edges = &matrix.weighted_edges;
let mut remapping = vec![edges[0].0 as usize, edges[0].1 as usize];
let mut sums = Vec::new();
let mut best_sum_pos = 0;
let mut best_sum = (0, 0);
for i in 0..matrix.num_colors {
let m_row = matrix.row(i);
if i == remapping[0] || i == remapping[1] {
continue;
}
let sum = (i, m_row[remapping[0]] + m_row[remapping[1]]);
if sum.1 > best_sum.1 {
best_sum_pos = sums.len();
best_sum = sum;
}
sums.push(sum);
}
while !sums.is_empty() {
let best_index = best_sum.0;
let m = remapping.len();
let mut best_pos = 0;
let mut best_cost = i64::MAX;
let mut cross_cost: i64 = 0;
let best_row = matrix.row(best_index);
for p in 0..=m {
let new_cost: i64 = (0..m)
.map(|k| {
let dist = if k < p { p - k } else { k + 1 - p };
best_row[remapping[k]] as i64 * dist as i64
})
.sum();
let total = new_cost + cross_cost;
if total < best_cost {
best_cost = total;
best_pos = p;
}
if p < m {
let row_p = matrix.row(remapping[p]);
for &rb in &remapping[(p + 1)..m] {
cross_cost += row_p[rb] as i64;
}
for &ra in &remapping[..p] {
cross_cost -= row_p[ra] as i64;
}
}
}
remapping.insert(best_pos, best_index);
sums.swap_remove(best_sum_pos);
if !sums.is_empty() {
best_sum_pos = 0;
best_sum = (0, 0);
for (i, sum) in sums.iter_mut().enumerate() {
sum.1 += best_row[sum.0];
if sum.1 > best_sum.1 {
best_sum_pos = i;
best_sum = *sum;
}
}
}
}
remapping
}
fn battiato_reindex(matrix: &CoOccurrenceMatrix) -> Vec<usize> {
let mut chains = Vec::new();
let mut vx = vec![(0, 0); matrix.num_colors];
for &(i, j, _) in &matrix.weighted_edges {
let i = i as usize;
let j = j as usize;
let vi = vx[i];
let vj = vx[j];
if vi.0 == 0 && vj.0 == 0 {
vx[i].0 = 1;
vx[i].1 = chains.len();
vx[j].0 = 1;
vx[j].1 = chains.len();
chains.push(vec![i, j]);
} else if vi.0 == 0 && vj.0 == 1 {
vx[i].0 = 1;
vx[i].1 = vj.1;
vx[j].0 = 2;
let chain = &mut chains[vj.1];
if chain[0] == j {
chain.insert(0, i);
} else {
chain.push(i);
}
} else if vi.0 == 1 && vj.0 == 0 {
vx[j].0 = 1;
vx[j].1 = vi.1;
vx[i].0 = 2;
let chain = &mut chains[vi.1];
if chain[0] == i {
chain.insert(0, j);
} else {
chain.push(j);
}
} else if vi.0 == 1 && vj.0 == 1 && vi.1 != vj.1 {
vx[i].0 = 2;
vx[j].0 = 2;
let (a, b) = if vi.1 < vj.1 { (i, j) } else { (j, i) };
let ca = vx[a].1;
let cb = vx[b].1;
let chainb = std::mem::take(&mut chains[cb]);
for &v in &chainb {
vx[v].1 = ca;
}
let chaina = &mut chains[ca];
if chaina[0] == a && chainb[0] == b {
for v in chainb {
chaina.insert(0, v);
}
} else if chaina[0] == a {
chaina.splice(0..0, chainb);
} else if chainb[0] == b {
chaina.extend(chainb);
} else {
let pos = chaina.len();
for v in chainb {
chaina.insert(pos, v);
}
}
}
if chains[0].len() == matrix.num_colors {
break;
}
}
chains
.into_iter()
.flatten()
.chain(
vx.into_iter()
.enumerate()
.filter_map(|(i, v)| if v.0 == 0 { Some(i) } else { None }),
)
.collect()
}
fn pairwise_swap_search(remapping: &mut [usize], matrix: &CoOccurrenceMatrix, max_dist: u8) {
let num_colors = remapping.len();
let b_limit = max_dist as usize + 1;
let mut swaps = 2;
while swaps >= 2 {
swaps = 0;
for a in 0..num_colors - 1 {
for b in (a + 1)..(a + b_limit).min(num_colors) {
let va = remapping[a];
let vb = remapping[b];
let row_a = matrix.row(va);
let row_b = matrix.row(vb);
let mut delta: i64 = 0;
let dist = (b - a) as i64;
for &vi in &remapping[..a] {
let weight_diff = row_a[vi] as i64 - row_b[vi] as i64;
delta += weight_diff * dist;
}
for (off, &vi) in remapping[(a + 1)..b].iter().enumerate() {
let i = (a + 1 + off) as i64;
let weight_diff = row_a[vi] as i64 - row_b[vi] as i64;
let dist_diff = (a + b) as i64 - 2 * i;
delta += weight_diff * dist_diff;
}
for &vi in &remapping[(b + 1)..] {
let weight_diff = row_a[vi] as i64 - row_b[vi] as i64;
delta -= weight_diff * dist;
}
if delta < 0 {
remapping.swap(a, b);
swaps += 1;
}
}
}
}
}
#[derive(Debug)]
pub struct CoOccurrenceMatrix {
num_colors: usize,
data: Vec<u32>,
weighted_edges: Vec<(u8, u8, u32)>,
}
impl CoOccurrenceMatrix {
pub fn from(png: &PngImage) -> Option<Self> {
if png.ihdr.bit_depth != BitDepth::Eight {
return None;
}
let num_colors = match &png.ihdr.color_type {
ColorType::Indexed { palette } if palette.len() > 2 => palette.len(),
_ => return None,
};
let data = Self::build(num_colors, png);
let weighted_edges = Self::weighted_edges(num_colors, &data);
Some(Self {
num_colors,
data,
weighted_edges,
})
}
fn build(num_colors: usize, png: &PngImage) -> Vec<u32> {
let mut data = vec![0; num_colors * num_colors];
let mut prev: Option<ScanLine> = None;
for line in png.scan_lines(false) {
let mut prev_val = None;
for i in 0..line.data.len() {
let val = line.data[i] as usize;
if let Some(prev_val) = prev_val.replace(val) {
data[prev_val * num_colors + val] += 1;
}
if let Some(&prev_val) = prev.as_ref().and_then(|l| l.data.get(i)) {
data[prev_val as usize * num_colors + val] += 1;
}
}
prev = Some(line);
}
for i in 0..num_colors {
let row_start = i * num_colors;
for j in 0..=i {
let opposite = j * num_colors + i;
data[row_start + j] += data[opposite];
data[opposite] = data[row_start + j];
}
}
data
}
fn weighted_edges(num_colors: usize, data: &[u32]) -> Vec<(u8, u8, u32)> {
let mut edges = Vec::new();
for i in 0..num_colors {
let row = &data[(i * num_colors)..];
for (j, &val) in row.iter().enumerate().take(i) {
if val > 0 {
edges.push((j as u8, i as u8, val));
}
}
}
edges.sort_by(|(_, _, w1), (_, _, w2)| w2.cmp(w1));
edges
}
#[inline]
fn row(&self, row: usize) -> &[u32] {
let start = row * self.num_colors;
&self.data[start..(start + self.num_colors)]
}
fn most_popular_color(&self) -> (usize, u32) {
let mut best = (0, 0);
for i in 0..self.num_colors {
let sum: u32 = self.row(i).iter().sum();
if sum > best.1 {
best = (i, sum);
}
}
best.1 /= 4;
best
}
}