use crate::palette::Rgb;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Image {
pub width: usize,
pub height: usize,
pub pixels: Vec<u32>,
}
pub const fn solid(rgb: Rgb) -> u32 {
0xFF00_0000 | (rgb & 0x00FF_FFFF)
}
const MAX_SIZE: usize = 2048;
const DEFAULT: [(u32, u32, u32); 16] = [
(0, 0, 0),
(20, 20, 80),
(80, 13, 13),
(20, 80, 20),
(80, 20, 80),
(20, 80, 80),
(80, 80, 20),
(53, 53, 53),
(26, 26, 26),
(33, 33, 60),
(60, 26, 26),
(33, 60, 33),
(60, 33, 60),
(33, 60, 60),
(60, 60, 33),
(80, 80, 80),
];
fn percent(r: u32, g: u32, b: u32) -> Rgb {
let c = |v: u32| v.min(100) * 255 / 100;
c(r) << 16 | c(g) << 8 | c(b)
}
fn hls(h: u32, l: u32, s: u32) -> Rgb {
let h = ((h + 240) % 360) as f64;
let (l, s) = (l.min(100) as f64 / 100.0, s.min(100) as f64 / 100.0);
let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
let (r, g, b) = match h as u32 / 60 {
0 => (c, x, 0.0),
1 => (x, c, 0.0),
2 => (0.0, c, x),
3 => (0.0, x, c),
4 => (x, 0.0, c),
_ => (c, 0.0, x),
};
let m = l - c / 2.0;
let v = |f: f64| (((f + m) * 255.0).round().clamp(0.0, 255.0)) as u32;
v(r) << 16 | v(g) << 8 | v(b)
}
fn numbers(data: &[u8]) -> (Vec<u32>, usize) {
let mut out = Vec::new();
let mut cur: Option<u32> = None;
let mut i = 0;
while i < data.len() {
match data[i] {
d @ b'0'..=b'9' => cur = Some(cur.unwrap_or(0).saturating_mul(10).saturating_add((d - b'0') as u32)),
b';' => out.push(cur.take().unwrap_or(0)),
_ => break,
}
i += 1;
}
if let Some(v) = cur {
out.push(v);
}
(out, i)
}
pub fn decode(params: &[u32], data: &[u8], background: Rgb) -> Image {
let p1 = params.first().copied().unwrap_or(0);
let transparent = params.get(1).copied() == Some(1);
let mut aspect = match p1 {
2 => 5,
3 | 4 => 3,
7..=9 => 1,
_ => 2,
};
let mut palette: [Rgb; 256] = std::array::from_fn(|i| {
let (r, g, b) = DEFAULT[i % 16];
percent(r, g, b)
});
let mut color = palette[0];
let mut rows: Vec<Vec<u32>> = Vec::new();
let (mut x, mut band) = (0usize, 0usize);
let (mut width, mut height) = (0usize, 0usize);
let mut raster = (0usize, 0usize);
let mut drawn = false;
let mut i = 0;
while i < data.len() {
let c = data[i];
i += 1;
match c {
b'?'..=b'~' | b'!' => {
let (count, bits) = if c == b'!' {
let (n, used) = numbers(&data[i..]);
i += used;
let Some(&ch) = data.get(i).filter(|ch| (b'?'..=b'~').contains(ch)) else { continue };
i += 1;
(n.first().copied().unwrap_or(1).max(1) as usize, ch - b'?')
} else {
(1, c - b'?')
};
drawn = true;
let count = count.min(MAX_SIZE.saturating_sub(x));
if bits != 0 {
for bit in 0..6 {
if bits & (1 << bit) == 0 {
continue;
}
for dy in 0..aspect {
let y = (band * 6 + bit) * aspect + dy;
if y >= MAX_SIZE {
continue;
}
if rows.len() <= y {
rows.resize_with(y + 1, Vec::new);
}
let row = &mut rows[y];
if row.len() < x + count {
row.resize(x + count, 0);
}
row[x..x + count].fill(solid(color));
}
}
}
x += count;
width = width.max(x);
height = height.max(((band + 1) * 6 * aspect).min(MAX_SIZE));
}
b'#' => {
let (n, used) = numbers(&data[i..]);
i += used;
let Some(®) = n.first() else { continue };
let reg = (reg as usize).min(255);
if n.len() >= 5 {
palette[reg] = match n[1] {
1 => hls(n[2], n[3], n[4]),
_ => percent(n[2], n[3], n[4]),
};
}
color = palette[reg];
}
b'"' => {
let (n, used) = numbers(&data[i..]);
i += used;
if !drawn {
let (pan, pad) = (n.first().copied().unwrap_or(1), n.get(1).copied().unwrap_or(1).max(1));
aspect = ((pan as f64 / pad as f64).round() as usize).clamp(1, 10);
raster = (
n.get(2).copied().unwrap_or(0).min(MAX_SIZE as u32) as usize,
n.get(3).copied().unwrap_or(0).min(MAX_SIZE as u32) as usize,
);
}
}
b'$' => x = 0,
b'-' => {
x = 0;
band += 1;
}
_ => {}
}
}
if !transparent {
width = width.max(raster.0);
height = height.max(raster.1);
}
let fill = if transparent { 0 } else { solid(background) };
let mut pixels = vec![fill; width * height];
for (y, row) in rows.iter().enumerate().take(height) {
for (x, &p) in row.iter().enumerate().take(width) {
if p != 0 {
pixels[y * width + x] = p;
}
}
}
Image { width, height, pixels }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bands_colors_and_repeats() {
let img = decode(&[0, 1], b"#1;2;100;0;0#2;2;0;100;0\"1;1#1~#2!2~-#1~#2!2~", 0);
assert_eq!((img.width, img.height), (3, 12));
assert_eq!(img.pixels[0], solid(0xFF0000));
assert_eq!(img.pixels[1], solid(0x00FF00));
assert_eq!(img.pixels[11 * 3 + 2], solid(0x00FF00));
}
#[test]
fn passes_and_transparency() {
let img = decode(&[0, 1], b"\"1;1#1;2;100;0;0@$#2;2;0;0;100A", 0);
assert_eq!(img.pixels[..6], [solid(0xFF0000), solid(0x0000FF), 0, 0, 0, 0]);
let img = decode(&[], b"\"1;1;2;6@", 0x123456);
assert_eq!((img.width, img.height), (2, 6));
assert_eq!(img.pixels[1], solid(0x123456));
assert_eq!(img.pixels[0], solid(0));
}
#[test]
fn pixel_shape() {
let img = decode(&[], b"~", 0);
assert_eq!((img.width, img.height), (1, 12));
let img = decode(&[9], b"~", 0);
assert_eq!(img.height, 6);
}
#[test]
fn random_input_is_harmless() {
let mut seed = 0x1234_5678u32;
let alphabet = b"?@AB~!#$-\";0123456789;;;";
for _ in 0..200 {
let data: Vec<u8> = (0..2000)
.map(|_| {
seed ^= seed << 13;
seed ^= seed >> 17;
seed ^= seed << 5;
alphabet[seed as usize % alphabet.len()]
})
.collect();
let img = decode(&[0, 0], &data, 0);
assert!(img.width <= MAX_SIZE && img.height <= MAX_SIZE);
assert_eq!(img.pixels.len(), img.width * img.height);
}
let img = decode(&[9], b"\"1;1;99999;99999!999999~", 0);
assert_eq!((img.width, img.height), (MAX_SIZE, MAX_SIZE));
}
#[test]
fn hls_colors() {
assert_eq!(hls(0, 50, 100), 0x0000FF);
assert_eq!(hls(120, 50, 100), 0xFF0000);
assert_eq!(hls(240, 50, 100), 0x00FF00);
assert_eq!(hls(0, 100, 0), 0xFFFFFF);
}
}