Skip to content

Commit 03d881b

Browse files
authored
Merge pull request #81 from firefly-zero/image-v2
New image format (v2)
2 parents 581f2b1 + 23f9681 commit 03d881b

2 files changed

Lines changed: 115 additions & 293 deletions

File tree

src/images.rs

Lines changed: 55 additions & 187 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::palettes::{Color, Palette};
22
use anyhow::{Context, Result, bail};
3-
use image::{Pixel, Rgba, RgbaImage};
3+
use image::{Pixel, Rgb, Rgba, RgbaImage};
44
use std::fs::File;
55
use std::io::Write;
66
use std::path::Path;
@@ -12,65 +12,30 @@ pub fn convert_image(in_path: &Path, out_path: &Path, sys_pal: &Palette) -> Resu
1212
if img.width() % 8 != 0 {
1313
bail!("image width must be divisible by 8");
1414
}
15-
let mut img_pal = make_palette(&img, sys_pal).context("detect colors used in the image")?;
16-
let mut out = File::create(out_path).context("create output path")?;
17-
// The magic number. "2"=image, "1"=v1.
18-
write_u8(&mut out, 0x21)?;
19-
let n_colors = img_pal.len();
20-
if n_colors <= 2 {
21-
if n_colors <= 1 {
22-
println!("⚠️ the image has only one color.");
23-
}
24-
extend_palette(&mut img_pal, sys_pal, 2);
25-
write_image::<1, 8>(out, &img, &img_pal, sys_pal).context("write 1BPP image")
26-
} else if n_colors <= 4 {
27-
extend_palette(&mut img_pal, sys_pal, 4);
28-
write_image::<2, 4>(out, &img, &img_pal, sys_pal).context("write 1BPP image")
29-
} else if n_colors <= 16 {
30-
extend_palette(&mut img_pal, sys_pal, 16);
31-
write_image::<4, 2>(out, &img, &img_pal, sys_pal).context("write 1BPP image")
32-
} else {
33-
let has_transparency = img_pal.iter().any(Option::is_none);
34-
if has_transparency && n_colors == 17 {
35-
bail!("cannot use all 16 colors with transparency, remove one color");
36-
}
37-
bail!("the image has too many colors");
38-
}
15+
let transp = find_unused_color(&img, sys_pal).context("detect colors used in the image")?;
16+
let out = File::create(out_path).context("create output path")?;
17+
write_image(out, &img, sys_pal, transp).context("write image")
3918
}
4019

41-
fn write_image<const BPP: u8, const PPB: usize>(
42-
mut out: File,
43-
img: &RgbaImage,
44-
img_pal: &[Color],
45-
sys_pal: &Palette,
46-
) -> Result<()> {
47-
write_u8(&mut out, BPP)?; // BPP
20+
fn write_image(mut out: File, img: &RgbaImage, sys_pal: &Palette, transp: u8) -> Result<()> {
21+
const BPP: u8 = 4;
22+
const PPB: usize = 2;
23+
4824
let Ok(width) = u16::try_from(img.width()) else {
4925
bail!("the image is too big")
5026
};
27+
write_u8(&mut out, 0x22)?; // magic number
5128
write_u16(&mut out, width)?; // image width
52-
let transparent = pick_transparent(img_pal, sys_pal)?;
53-
write_u8(&mut out, transparent)?; // transparent color
29+
write_u8(&mut out, transp)?; // transparent color
5430

55-
// palette swaps
56-
let mut byte = 0;
57-
debug_assert!(img_pal.len() == 2 || img_pal.len() == 4 || img_pal.len() == 16);
58-
for (i, color) in img_pal.iter().enumerate() {
59-
let index = match color {
60-
Some(color) => find_color(sys_pal, Some(*color)),
61-
None => transparent,
62-
};
63-
byte = (byte << 4) | index;
64-
if i % 2 == 1 {
65-
write_u8(&mut out, byte)?;
66-
}
67-
}
68-
69-
// image raw packed bytes
31+
// Pixel values.
7032
let mut byte: u8 = 0;
7133
for (i, pixel) in img.pixels().enumerate() {
7234
let color = convert_color(*pixel);
73-
let raw_color = find_color(img_pal, color);
35+
let raw_color = match color {
36+
Some(color) => find_color(sys_pal, color),
37+
None => transp,
38+
};
7439
byte = (byte << BPP) | raw_color;
7540
if (i + 1) % PPB == 0 {
7641
write_u8(&mut out, byte)?;
@@ -79,77 +44,33 @@ fn write_image<const BPP: u8, const PPB: usize>(
7944
Ok(())
8045
}
8146

82-
/// Detect all colors used in the image.
83-
fn make_palette(img: &RgbaImage, sys_pal: &Palette) -> Result<Vec<Color>> {
84-
let mut palette = Vec::new();
47+
/// Find color from the palette not used on the image.
48+
///
49+
/// Additionally ensures that the image uses the given color palette.
50+
fn find_unused_color(img: &RgbaImage, sys_pal: &Palette) -> Result<u8> {
51+
let mut used_colors: Vec<Color> = Vec::new();
52+
let mut has_transp = false;
8553
for (x, y, pixel) in img.enumerate_pixels() {
86-
let color = convert_color(*pixel);
87-
if !palette.contains(&color) {
88-
if color.is_some() && !sys_pal.contains(&color) {
89-
bail!(
90-
"found a color not present in the color palette: {} (at x={x}, y={y})",
91-
format_color(color),
92-
);
93-
}
94-
palette.push(color);
54+
let Some(color) = convert_color(*pixel) else {
55+
has_transp = true;
56+
continue;
57+
};
58+
if !sys_pal.contains(&color) {
59+
bail!(
60+
"found a color not present in the color palette: {} (at x={x}, y={y})",
61+
format_color(color),
62+
);
9563
}
96-
}
97-
palette.sort_by_key(|c| match c {
98-
Some(c) => find_color(sys_pal, Some(*c)),
99-
None => 20,
100-
});
101-
Ok(palette)
102-
}
103-
104-
/// Add empty colors at the end of the palette to match the BPP size.
105-
///
106-
/// If the given image palette is fully contained within the system palette
107-
/// (after being cut to the expected swaps size), place the colors in the
108-
/// image palette in the same positions as they are in the system palette.
109-
/// This will make it possible to read such images without worrying about
110-
/// applying color swaps.
111-
fn extend_palette(img_pal: &mut Vec<Color>, sys_pal: &Palette, size: usize) {
112-
if img_pal.len() > size {
113-
return;
114-
}
115-
116-
let sys_pal_prefix = &sys_pal[..size];
117-
if !is_subpalette(img_pal, sys_pal_prefix) {
118-
img_pal.extend_from_slice(&sys_pal[img_pal.len()..size]);
119-
return;
120-
}
121-
122-
// No transparency? Just use the system palette.
123-
let has_transp = img_pal.iter().any(Option::is_none);
124-
if !has_transp {
125-
img_pal.clear();
126-
img_pal.extend(sys_pal_prefix);
127-
return;
128-
}
129-
130-
// Has transparency? Then copy the system palette and poke one hole in it.
131-
let mut new_pal: Vec<Color> = Vec::new();
132-
let mut found_transp = false;
133-
for c in sys_pal_prefix {
134-
if found_transp || img_pal.contains(c) {
135-
new_pal.push(*c);
136-
} else {
137-
new_pal.push(None);
138-
found_transp = true;
64+
if !used_colors.contains(&color) {
65+
used_colors.push(color);
13966
}
14067
}
141-
img_pal.clear();
142-
img_pal.extend(new_pal);
143-
}
14468

145-
/// Check if the image palette is fully contained within the given system palette.
146-
fn is_subpalette(img_pal: &[Color], sys_pal: &[Color]) -> bool {
147-
for c in img_pal {
148-
if c.is_some() && !sys_pal.contains(c) {
149-
return false;
150-
}
69+
if has_transp {
70+
pick_transparent(&used_colors, sys_pal)
71+
} else {
72+
Ok(0xff)
15173
}
152-
true
15374
}
15475

15576
fn write_u8(f: &mut File, v: u8) -> std::io::Result<()> {
@@ -161,7 +82,7 @@ fn write_u16(f: &mut File, v: u16) -> std::io::Result<()> {
16182
}
16283

16384
/// Find the index of the given color in the given palette.
164-
fn find_color(palette: &[Color], c: Color) -> u8 {
85+
fn find_color(palette: &Palette, c: Rgb<u8>) -> u8 {
16586
for (color, i) in palette.iter().zip(0u8..) {
16687
if *color == c {
16788
return i;
@@ -172,45 +93,35 @@ fn find_color(palette: &[Color], c: Color) -> u8 {
17293

17394
/// Make human-readable hex representation of the color code.
17495
fn format_color(c: Color) -> String {
175-
match c {
176-
Some(c) => {
177-
let c = c.0;
178-
format!("#{:02X}{:02X}{:02X}", c[0], c[1], c[2])
179-
}
180-
None => "ALPHA".to_string(),
181-
}
96+
let c = c.0;
97+
format!("#{:02X}{:02X}{:02X}", c[0], c[1], c[2])
18298
}
18399

184-
fn convert_color(c: Rgba<u8>) -> Color {
185-
if is_transparent(c) {
100+
fn convert_color(c: Rgba<u8>) -> Option<Color> {
101+
let alpha = c.0[3];
102+
let is_transparent = alpha < 128;
103+
if is_transparent {
186104
return None;
187105
}
188106
Some(c.to_rgb())
189107
}
190108

191-
const fn is_transparent(c: Rgba<u8>) -> bool {
192-
let alpha = c.0[3];
193-
alpha < 128
194-
}
195-
196109
/// Pick the color to be used to represent transparency
197110
fn pick_transparent(img_pal: &[Color], sys_pal: &Palette) -> Result<u8> {
198-
if img_pal.iter().all(Option::is_some) {
199-
// no transparency needed
200-
return Ok(17);
201-
}
111+
assert!(img_pal.len() <= sys_pal.len());
112+
assert!(sys_pal.len() <= 16);
202113
for (color, i) in sys_pal.iter().zip(0u8..) {
203114
if !img_pal.contains(color) {
204115
return Ok(i);
205116
}
206117
}
207-
if img_pal.len() > 16 {
208-
bail!("the image cannot contain more than 16 colors")
118+
if sys_pal.len() == 16 {
119+
bail!("cannot use all 16 colors with transparency, remove one color");
209120
}
210-
if img_pal.len() == 16 {
211-
bail!("an image cannot contain all 16 colors and transparency")
212-
}
213-
bail!("image contains colors not from the palette")
121+
// If the system palette has less than 16 colors,
122+
// any of the colors outside the palette
123+
// can be used for transparency. We use 15.
124+
Ok(0xf)
214125
}
215126

216127
#[cfg(test)]
@@ -221,8 +132,7 @@ mod tests {
221132

222133
#[test]
223134
fn test_format_color() {
224-
assert_eq!(format_color(None), "ALPHA");
225-
assert_eq!(format_color(Some(Rgb([0x89, 0xab, 0xcd]))), "#89ABCD");
135+
assert_eq!(format_color(Rgb([0x89, 0xab, 0xcd])), "#89ABCD");
226136
}
227137

228138
#[test]
@@ -232,50 +142,8 @@ mod tests {
232142
let c1 = pal[1];
233143
let c2 = pal[2];
234144
let c3 = pal[3];
235-
assert_eq!(pick_transparent(&[c0, c1], pal).unwrap(), 17);
236-
assert_eq!(pick_transparent(&[c0, c1, None], pal).unwrap(), 2);
237-
assert_eq!(pick_transparent(&[c0, None, c1], pal).unwrap(), 2);
238-
assert_eq!(pick_transparent(&[c1, c0, None], pal).unwrap(), 2);
239-
assert_eq!(pick_transparent(&[c0, c1, c2, c3, None], pal).unwrap(), 4);
240-
}
241-
242-
#[test]
243-
fn test_extend_palette() {
244-
let pal = SWEETIE16;
245-
let c0 = pal[0];
246-
let c1 = pal[1];
247-
let c2 = pal[2];
248-
let c3 = pal[3];
249-
let c4 = pal[4];
250-
251-
// Already the palette prefix, do nothing.
252-
let mut img_pal = vec![c0, c1];
253-
extend_palette(&mut img_pal, pal, 2);
254-
assert_eq!(img_pal, vec![c0, c1]);
255-
256-
// A prefix but in a wrong order. Fix the order.
257-
let mut img_pal = vec![c1, c0];
258-
extend_palette(&mut img_pal, pal, 2);
259-
assert_eq!(img_pal, vec![c0, c1]);
260-
261-
// Not a prefix and already full. Keep the given palette.
262-
let mut img_pal = vec![c2, c1];
263-
extend_palette(&mut img_pal, pal, 2);
264-
assert_eq!(img_pal, vec![c2, c1]);
265-
266-
// A prefix but too short. Fill the rest.
267-
let mut img_pal = vec![c0, c1];
268-
extend_palette(&mut img_pal, pal, 4);
269-
assert_eq!(img_pal, vec![c0, c1, c2, c3]);
270-
271-
// Within the palette prefix.
272-
let mut img_pal = vec![c2, c1];
273-
extend_palette(&mut img_pal, pal, 4);
274-
assert_eq!(img_pal, vec![c0, c1, c2, c3]);
275-
276-
// Not a prefix but too short. Don't touch the given, fill the rest.
277-
let mut img_pal = vec![c4, c2];
278-
extend_palette(&mut img_pal, pal, 4);
279-
assert_eq!(img_pal, vec![c4, c2, c2, c3]);
145+
assert_eq!(pick_transparent(&[c0, c1], pal).unwrap(), 2);
146+
assert_eq!(pick_transparent(&[c1, c0], pal).unwrap(), 2);
147+
assert_eq!(pick_transparent(&[c0, c1, c2, c3], pal).unwrap(), 4);
280148
}
281149
}

0 commit comments

Comments
 (0)