use resvg::tiny_skia;
use std::collections::HashMap;
type ColorBox = Vec<([u8; 3], u32)>;
const HEADER_LEN: u32 = 40;
const PALETTE_LEN: usize = 256;
const ALPHA_THRESHOLD: u8 = 128;
pub fn encode_bmp_8bpp(pixmap: &tiny_skia::Pixmap) -> Vec<u8> {
let (w, h) = (pixmap.width() as usize, pixmap.height() as usize);
let mut rgb: Vec<[u8; 3]> = Vec::with_capacity(w * h);
let mut opaque: Vec<bool> = Vec::with_capacity(w * h);
for px in pixmap.pixels() {
let c = px.demultiply();
rgb.push([c.red(), c.green(), c.blue()]);
opaque.push(c.alpha() >= ALPHA_THRESHOLD);
}
let palette = quantize(&rgb, &opaque);
let indices: Vec<u8> = rgb
.iter()
.zip(&opaque)
.map(|(c, &o)| if o { nearest(&palette, *c) } else { 0 })
.collect();
let color_stride = w.div_ceil(4) * 4;
let mask_stride = w.div_ceil(8).div_ceil(4) * 4;
let mut data = Vec::with_capacity(
HEADER_LEN as usize + 4 * PALETTE_LEN + h * (color_stride + mask_stride),
);
let (w32, h32) = (
i32::try_from(w).expect("icon width is at most 256"),
i32::try_from(h).expect("icon height is at most 256"),
);
data.extend_from_slice(&HEADER_LEN.to_le_bytes());
data.extend_from_slice(&w32.to_le_bytes());
data.extend_from_slice(&(2 * h32).to_le_bytes());
data.extend_from_slice(&1u16.to_le_bytes()); data.extend_from_slice(&8u16.to_le_bytes()); data.extend_from_slice(&0u32.to_le_bytes()); data.extend_from_slice(&0u32.to_le_bytes()); data.extend_from_slice(&0i32.to_le_bytes()); data.extend_from_slice(&0i32.to_le_bytes()); data.extend_from_slice(&0u32.to_le_bytes()); data.extend_from_slice(&0u32.to_le_bytes());
for c in &palette {
data.extend_from_slice(&[c[2], c[1], c[0], 0]);
}
for _ in palette.len()..PALETTE_LEN {
data.extend_from_slice(&[0, 0, 0, 0]);
}
for y in (0..h).rev() {
let row = &indices[y * w..(y + 1) * w];
data.extend_from_slice(row);
data.extend(std::iter::repeat_n(0u8, color_stride - w));
}
for y in (0..h).rev() {
let start = data.len();
for chunk in opaque[y * w..(y + 1) * w].chunks(8) {
let mut byte = 0u8;
for (bit, &o) in chunk.iter().enumerate() {
if !o {
byte |= 0x80 >> bit;
}
}
data.push(byte);
}
data.extend(std::iter::repeat_n(0u8, mask_stride - (data.len() - start)));
}
data
}
fn quantize(rgb: &[[u8; 3]], opaque: &[bool]) -> Vec<[u8; 3]> {
let mut counts: HashMap<[u8; 3], u32> = HashMap::new();
for (c, &o) in rgb.iter().zip(opaque) {
if o {
*counts.entry(*c).or_default() += 1;
}
}
let mut colors: ColorBox = counts.into_iter().collect();
colors.sort_unstable();
if colors.is_empty() {
return vec![[0, 0, 0]];
}
if colors.len() <= PALETTE_LEN {
return colors.into_iter().map(|(c, _)| c).collect();
}
let mut boxes = vec![colors];
while boxes.len() < PALETTE_LEN {
let Some(i) = boxes
.iter()
.enumerate()
.filter(|(_, b)| b.len() > 1)
.max_by_key(|(_, b)| widest_channel(b).1)
.map(|(i, _)| i)
else {
break;
};
let (channel, _) = widest_channel(&boxes[i]);
let (lo, hi) = split_at_median(boxes.swap_remove(i), channel);
boxes.push(lo);
boxes.push(hi);
}
boxes.iter().map(average).collect()
}
fn widest_channel(b: &ColorBox) -> (usize, u8) {
(0..3)
.map(|ch| {
let min = b.iter().map(|(c, _)| c[ch]).min().unwrap_or(0);
let max = b.iter().map(|(c, _)| c[ch]).max().unwrap_or(0);
(ch, max - min)
})
.max_by_key(|&(_, range)| range)
.unwrap_or((0, 0))
}
fn split_at_median(mut b: ColorBox, channel: usize) -> (ColorBox, ColorBox) {
b.sort_unstable_by_key(|(c, _)| c[channel]);
let half = b.iter().map(|(_, n)| u64::from(*n)).sum::<u64>() / 2;
let mut running = 0u64;
let mut at = 0;
for (i, (_, n)) in b.iter().enumerate() {
running += u64::from(*n);
if running > half {
at = i;
break;
}
}
let at = at.clamp(1, b.len() - 1);
let hi = b.split_off(at);
(b, hi)
}
fn average(b: &ColorBox) -> [u8; 3] {
let total: u64 = b.iter().map(|(_, n)| u64::from(*n)).sum();
if total == 0 {
return [0, 0, 0];
}
let mut out = [0u8; 3];
for (ch, slot) in out.iter_mut().enumerate() {
let sum: u64 = b
.iter()
.map(|(c, n)| u64::from(c[ch]) * u64::from(*n))
.sum();
*slot = u8::try_from(sum / total).expect("a mean of bytes is a byte");
}
out
}
fn nearest(palette: &[[u8; 3]], c: [u8; 3]) -> u8 {
let mut best = (0usize, i32::MAX);
for (i, p) in palette.iter().enumerate() {
let d: i32 = (0..3)
.map(|ch| {
let delta = i32::from(p[ch]) - i32::from(c[ch]);
delta * delta
})
.sum();
if d < best.1 {
best = (i, d);
}
}
u8::try_from(best.0).expect("palette holds at most 256 entries")
}