From f0fd9a237e19f95f95f123a9556ba1e60bbe94da Mon Sep 17 00:00:00 2001 From: gram Date: Fri, 16 Jan 2026 17:04:50 +0100 Subject: [PATCH 1/2] Make color swaps as simple as possible --- src/images.rs | 54 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/src/images.rs b/src/images.rs index bf71c7c..a9d4e27 100644 --- a/src/images.rs +++ b/src/images.rs @@ -15,19 +15,22 @@ pub fn convert_image(in_path: &Path, out_path: &Path, sys_pal: &Palette) -> Resu let mut img_pal = make_palette(&img, sys_pal).context("detect colors used in the image")?; let mut out = File::create(out_path).context("create output path")?; write_u8(&mut out, 0x21)?; - let colors = img_pal.len(); - if colors <= 2 { + let n_colors = img_pal.len(); + if n_colors <= 2 { + if n_colors <= 1 { + println!("⚠️ the image has only one color."); + } extend_palette(&mut img_pal, sys_pal, 2); write_image::<1, 8>(out, &img, &img_pal, sys_pal).context("write 1BPP image") - } else if colors <= 4 { + } else if n_colors <= 4 { extend_palette(&mut img_pal, sys_pal, 4); write_image::<2, 4>(out, &img, &img_pal, sys_pal).context("write 1BPP image") - } else if colors <= 16 { + } else if n_colors <= 16 { extend_palette(&mut img_pal, sys_pal, 16); write_image::<4, 2>(out, &img, &img_pal, sys_pal).context("write 1BPP image") } else { let has_transparency = img_pal.iter().any(Option::is_none); - if has_transparency && colors == 17 { + if has_transparency && n_colors == 17 { bail!("cannot use all 16 colors with transparency, remove one color"); } bail!("the image has too many colors"); @@ -75,7 +78,7 @@ fn write_image( Ok(()) } -/// Detect all colors used in the image +/// Detect all colors used in the image. fn make_palette(img: &RgbaImage, sys_pal: &Palette) -> Result> { let mut palette = Vec::new(); for (x, y, pixel) in img.enumerate_pixels() { @@ -99,12 +102,49 @@ fn make_palette(img: &RgbaImage, sys_pal: &Palette) -> Result> { /// Add empty colors at the end of the palette to match the BPP size. fn extend_palette(img_pal: &mut Vec, sys_pal: &Palette, size: usize) { + if img_pal.len() > size { + return; + } + // If the given image palette is fully contained within the system palette, + // place the colors in the image palette in the same positions as they are + // in the system palette. This will make it possible to read such images + // without worrying about applying color swaps. + if size == sys_pal.len() && is_subpalette(img_pal, sys_pal) { + let has_transp = img_pal.iter().any(Option::is_none); + if !has_transp { + img_pal.copy_from_slice(sys_pal); + return; + } + + let mut new_pal: Vec = Vec::new(); + let mut found_transp = false; + for c in sys_pal { + if found_transp || img_pal.contains(c) { + new_pal.push(*c); + } else { + new_pal.push(None); + found_transp = true; + } + } + img_pal.copy_from_slice(&new_pal[..]); + return; + } let n = size - img_pal.len(); for _ in 0..n { img_pal.push(sys_pal[0]); } } +/// Check if the image palette is fully contained within the given system palette. +fn is_subpalette(img_pal: &Vec, sys_pal: &Palette) -> bool { + for c in img_pal { + if c.is_some() && !sys_pal.contains(c) { + return false; + } + } + true +} + fn write_u8(f: &mut File, v: u8) -> std::io::Result<()> { f.write_all(&v.to_le_bytes()) } @@ -123,7 +163,7 @@ fn find_color(palette: &[Color], c: Color) -> u8 { panic!("color not in the palette") } -/// Make human-friendly hex representation of the color code. +/// Make human-readable hex representation of the color code. fn format_color(c: Color) -> String { match c { Some(c) => { From c55cd45e60eccd6e87d4db444347ce5ee23ca4c8 Mon Sep 17 00:00:00 2001 From: gram Date: Fri, 16 Jan 2026 18:49:43 +0100 Subject: [PATCH 2/2] test extend_palette --- src/images.rs | 96 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 71 insertions(+), 25 deletions(-) diff --git a/src/images.rs b/src/images.rs index a9d4e27..d6d80f6 100644 --- a/src/images.rs +++ b/src/images.rs @@ -14,6 +14,7 @@ pub fn convert_image(in_path: &Path, out_path: &Path, sys_pal: &Palette) -> Resu } let mut img_pal = make_palette(&img, sys_pal).context("detect colors used in the image")?; let mut out = File::create(out_path).context("create output path")?; + // The magic number. "2"=image, "1"=v1. write_u8(&mut out, 0x21)?; let n_colors = img_pal.len(); if n_colors <= 2 { @@ -101,42 +102,47 @@ fn make_palette(img: &RgbaImage, sys_pal: &Palette) -> Result> { } /// Add empty colors at the end of the palette to match the BPP size. +/// +/// If the given image palette is fully contained within the system palette +/// (after being cut to the expected swaps size), place the colors in the +/// image palette in the same positions as they are in the system palette. +/// This will make it possible to read such images without worrying about +/// applying color swaps. fn extend_palette(img_pal: &mut Vec, sys_pal: &Palette, size: usize) { if img_pal.len() > size { return; } - // If the given image palette is fully contained within the system palette, - // place the colors in the image palette in the same positions as they are - // in the system palette. This will make it possible to read such images - // without worrying about applying color swaps. - if size == sys_pal.len() && is_subpalette(img_pal, sys_pal) { - let has_transp = img_pal.iter().any(Option::is_none); - if !has_transp { - img_pal.copy_from_slice(sys_pal); - return; - } - let mut new_pal: Vec = Vec::new(); - let mut found_transp = false; - for c in sys_pal { - if found_transp || img_pal.contains(c) { - new_pal.push(*c); - } else { - new_pal.push(None); - found_transp = true; - } - } - img_pal.copy_from_slice(&new_pal[..]); + let sys_pal_prefix = &sys_pal[..size]; + if !is_subpalette(img_pal, sys_pal_prefix) { + img_pal.extend_from_slice(&sys_pal[img_pal.len()..size]); return; } - let n = size - img_pal.len(); - for _ in 0..n { - img_pal.push(sys_pal[0]); + + // No transparency? Just use the system palette. + let has_transp = img_pal.iter().any(Option::is_none); + if !has_transp { + img_pal.clear(); + img_pal.extend(sys_pal_prefix); + return; } + + // Has transparency? Then copy the system palette and poke one hole in it. + let mut new_pal: Vec = Vec::new(); + let mut found_transp = false; + for c in sys_pal_prefix { + if found_transp || img_pal.contains(c) { + new_pal.push(*c); + } else { + new_pal.push(None); + found_transp = true; + } + } + img_pal.copy_from_slice(&new_pal[..]); } /// Check if the image palette is fully contained within the given system palette. -fn is_subpalette(img_pal: &Vec, sys_pal: &Palette) -> bool { +fn is_subpalette(img_pal: &[Color], sys_pal: &[Color]) -> bool { for c in img_pal { if c.is_some() && !sys_pal.contains(c) { return false; @@ -231,4 +237,44 @@ mod tests { assert_eq!(pick_transparent(&[c1, c0, None], pal).unwrap(), 2); assert_eq!(pick_transparent(&[c0, c1, c2, c3, None], pal).unwrap(), 4); } + + #[test] + fn test_extend_palette() { + let pal = SWEETIE16; + let c0 = pal[0]; + let c1 = pal[1]; + let c2 = pal[2]; + let c3 = pal[3]; + let c4 = pal[4]; + + // Already the palette prefix, do nothing. + let mut img_pal = vec![c0, c1]; + extend_palette(&mut img_pal, pal, 2); + assert_eq!(img_pal, vec![c0, c1]); + + // A prefix but in a wrong order. Fix the order. + let mut img_pal = vec![c1, c0]; + extend_palette(&mut img_pal, pal, 2); + assert_eq!(img_pal, vec![c0, c1]); + + // Not a prefix and already full. Keep the given palette. + let mut img_pal = vec![c2, c1]; + extend_palette(&mut img_pal, pal, 2); + assert_eq!(img_pal, vec![c2, c1]); + + // A prefix but too short. Fill the rest. + let mut img_pal = vec![c0, c1]; + extend_palette(&mut img_pal, pal, 4); + assert_eq!(img_pal, vec![c0, c1, c2, c3]); + + // Within the palette prefix. + let mut img_pal = vec![c2, c1]; + extend_palette(&mut img_pal, pal, 4); + assert_eq!(img_pal, vec![c0, c1, c2, c3]); + + // Not a prefix but too short. Don't touch the given, fill the rest. + let mut img_pal = vec![c4, c2]; + extend_palette(&mut img_pal, pal, 4); + assert_eq!(img_pal, vec![c4, c2, c2, c3]); + } }