1use image::{DynamicImage, Rgba, RgbaImage};
2
3use crate::types::pixel_format::PixelFormat;
4
5pub fn dither(image: &DynamicImage, format: PixelFormat) -> DynamicImage {
11 match format {
12 PixelFormat::Rgba8888 => image.clone(),
13 PixelFormat::Rgb888 => dither_rgb888(image),
14 PixelFormat::Rgb565 => dither_rgb565(image),
15 PixelFormat::Rgba4444 => dither_rgba4444(image),
16 PixelFormat::Rgba5551 => dither_rgba5551(image),
17 PixelFormat::Alpha8 => dither_alpha8(image),
18 }
19}
20
21fn dither_rgb565(image: &DynamicImage) -> DynamicImage {
22 let src = image.to_rgba8();
23 let (w, h) = src.dimensions();
24 let mut err = vec![[0i32; 4]; (w * h) as usize];
25
26 let mut dst = RgbaImage::new(w, h);
27 for y in 0..h {
28 for x in 0..w {
29 let p = src.get_pixel(x, y);
30 let idx = (y * w + x) as usize;
31
32 let r_in = (p[0] as i32 + err[idx][0]).clamp(0, 255) as u8;
33 let g_in = (p[1] as i32 + err[idx][1]).clamp(0, 255) as u8;
34 let b_in = (p[2] as i32 + err[idx][2]).clamp(0, 255) as u8;
35
36 let r_q = quantize5(r_in);
37 let g_q = quantize6(g_in);
38 let b_q = quantize5(b_in);
39
40 dst.put_pixel(x, y, Rgba([r_q, g_q, b_q, 255]));
41
42 diffuse(&mut err, w, h, x, y, r_in as i32 - r_q as i32, 0);
43 diffuse(&mut err, w, h, x, y, g_in as i32 - g_q as i32, 1);
44 diffuse(&mut err, w, h, x, y, b_in as i32 - b_q as i32, 2);
45 }
46 }
47 DynamicImage::ImageRgba8(dst)
48}
49
50fn dither_rgb888(image: &DynamicImage) -> DynamicImage {
51 let src = image.to_rgba8();
53 let (w, h) = src.dimensions();
54 let mut dst = RgbaImage::new(w, h);
55 for y in 0..h {
56 for x in 0..w {
57 let p = src.get_pixel(x, y);
58 dst.put_pixel(x, y, Rgba([p[0], p[1], p[2], 255]));
59 }
60 }
61 DynamicImage::ImageRgba8(dst)
62}
63
64fn dither_rgba4444(image: &DynamicImage) -> DynamicImage {
65 let src = image.to_rgba8();
66 let (w, h) = src.dimensions();
67 let mut err = vec![[0i32; 4]; (w * h) as usize];
68
69 let mut dst = RgbaImage::new(w, h);
70 for y in 0..h {
71 for x in 0..w {
72 let p = src.get_pixel(x, y);
73 let idx = (y * w + x) as usize;
74
75 let r_in = (p[0] as i32 + err[idx][0]).clamp(0, 255) as u8;
76 let g_in = (p[1] as i32 + err[idx][1]).clamp(0, 255) as u8;
77 let b_in = (p[2] as i32 + err[idx][2]).clamp(0, 255) as u8;
78 let a_in = (p[3] as i32 + err[idx][3]).clamp(0, 255) as u8;
79
80 let r_q = quantize4(r_in);
81 let g_q = quantize4(g_in);
82 let b_q = quantize4(b_in);
83 let a_q = quantize4(a_in);
84
85 dst.put_pixel(x, y, Rgba([r_q, g_q, b_q, a_q]));
86
87 diffuse(&mut err, w, h, x, y, r_in as i32 - r_q as i32, 0);
88 diffuse(&mut err, w, h, x, y, g_in as i32 - g_q as i32, 1);
89 diffuse(&mut err, w, h, x, y, b_in as i32 - b_q as i32, 2);
90 diffuse(&mut err, w, h, x, y, a_in as i32 - a_q as i32, 3);
91 }
92 }
93 DynamicImage::ImageRgba8(dst)
94}
95
96fn dither_rgba5551(image: &DynamicImage) -> DynamicImage {
97 let src = image.to_rgba8();
98 let (w, h) = src.dimensions();
99 let mut err = vec![[0i32; 4]; (w * h) as usize];
100
101 let mut dst = RgbaImage::new(w, h);
102 for y in 0..h {
103 for x in 0..w {
104 let p = src.get_pixel(x, y);
105 let idx = (y * w + x) as usize;
106
107 let r_in = (p[0] as i32 + err[idx][0]).clamp(0, 255) as u8;
108 let g_in = (p[1] as i32 + err[idx][1]).clamp(0, 255) as u8;
109 let b_in = (p[2] as i32 + err[idx][2]).clamp(0, 255) as u8;
110 let a_in = (p[3] as i32 + err[idx][3]).clamp(0, 255) as u8;
111
112 let r_q = quantize5(r_in);
113 let g_q = quantize5(g_in);
114 let b_q = quantize5(b_in);
115 let a_q = if a_in >= 128 { 255u8 } else { 0u8 };
116
117 dst.put_pixel(x, y, Rgba([r_q, g_q, b_q, a_q]));
118
119 diffuse(&mut err, w, h, x, y, r_in as i32 - r_q as i32, 0);
120 diffuse(&mut err, w, h, x, y, g_in as i32 - g_q as i32, 1);
121 diffuse(&mut err, w, h, x, y, b_in as i32 - b_q as i32, 2);
122 diffuse(&mut err, w, h, x, y, a_in as i32 - a_q as i32, 3);
123 }
124 }
125 DynamicImage::ImageRgba8(dst)
126}
127
128fn dither_alpha8(image: &DynamicImage) -> DynamicImage {
129 let src = image.to_rgba8();
131 let (w, h) = src.dimensions();
132 let mut dst = RgbaImage::new(w, h);
133 for y in 0..h {
134 for x in 0..w {
135 let p = src.get_pixel(x, y);
136 dst.put_pixel(x, y, Rgba([0, 0, 0, p[3]]));
137 }
138 }
139 DynamicImage::ImageRgba8(dst)
140}
141
142fn diffuse(err: &mut [[i32; 4]], w: u32, h: u32, x: u32, y: u32, error: i32, ch: usize) {
146 if error == 0 {
147 return;
148 }
149 let (x, y, w, h) = (x as usize, y as usize, w as usize, h as usize);
150
151 if x + 1 < w {
152 err[y * w + (x + 1)][ch] += error * 7 / 16;
153 }
154 if y + 1 < h {
155 if x > 0 {
156 err[(y + 1) * w + (x - 1)][ch] += error * 3 / 16;
157 }
158 err[(y + 1) * w + x][ch] += error * 5 / 16;
159 if x + 1 < w {
160 err[(y + 1) * w + (x + 1)][ch] += error / 16;
161 }
162 }
163}
164
165fn quantize5(v: u8) -> u8 {
168 let v5 = v >> 3;
169 (v5 << 3) | (v5 >> 2)
170}
171
172fn quantize6(v: u8) -> u8 {
174 let v6 = v >> 2;
175 (v6 << 2) | (v6 >> 4)
176}
177
178fn quantize4(v: u8) -> u8 {
181 let v4 = v >> 4;
182 (v4 << 4) | v4
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn rgba8888_is_noop() {
191 let img = DynamicImage::new_rgba8(4, 4);
192 let out = dither(&img, PixelFormat::Rgba8888);
193 assert_eq!(out.width(), 4);
194 assert_eq!(out.height(), 4);
195 }
196
197 #[test]
198 fn rgb565_alpha_forced_to_255() {
199 let mut src = RgbaImage::new(1, 1);
200 src.put_pixel(0, 0, Rgba([255, 128, 64, 200]));
201 let out = dither(&DynamicImage::ImageRgba8(src), PixelFormat::Rgb565);
202 let p = out.to_rgba8().get_pixel(0, 0).0;
203 assert_eq!(p[3], 255, "rgb565 forces alpha to 255");
204 }
205
206 #[test]
207 fn rgba5551_threshold_opaque() {
208 let mut src = RgbaImage::new(1, 1);
209 src.put_pixel(0, 0, Rgba([255, 255, 255, 200]));
210 let out = dither(&DynamicImage::ImageRgba8(src), PixelFormat::Rgba5551);
211 let p = out.to_rgba8().get_pixel(0, 0).0;
212 assert_eq!(p[3], 255, "alpha >= 128 rounds to 255");
213 }
214
215 #[test]
216 fn rgba5551_threshold_transparent() {
217 let mut src = RgbaImage::new(1, 1);
218 src.put_pixel(0, 0, Rgba([255, 255, 255, 50]));
219 let out = dither(&DynamicImage::ImageRgba8(src), PixelFormat::Rgba5551);
220 let p = out.to_rgba8().get_pixel(0, 0).0;
221 assert_eq!(p[3], 0, "alpha < 128 rounds to 0");
222 }
223
224 #[test]
225 fn quantize5_round_trips() {
226 assert_eq!(quantize5(255), 255);
228 assert_eq!(quantize5(0), 0);
230 }
231
232 #[test]
233 fn quantize4_round_trips() {
234 assert_eq!(quantize4(255), 255);
235 assert_eq!(quantize4(0), 0);
236 }
237
238 #[test]
239 fn rgba4444_preserves_dimensions() {
240 let img = DynamicImage::new_rgba8(8, 8);
241 let out = dither(&img, PixelFormat::Rgba4444);
242 assert_eq!((out.width(), out.height()), (8, 8));
243 }
244}