1pub mod algorithms;
2pub mod color_space;
3pub mod color_space_lab;
4pub mod enums;
5pub mod error;
6pub mod measured_palettes;
7pub mod palettes;
8pub mod tone_map;
9pub mod types;
10
11use crate::color_space::{linear_channel_to_srgb, srgb_channel_to_linear};
12use crate::enums::{DitherMode, GamutCompression, ToneCompression};
13use crate::palettes::Palette;
14use crate::types::ImageBuffer;
15
16#[derive(Debug, Clone, PartialEq)]
26pub struct DitherConfig {
27 pub mode: DitherMode,
29 pub serpentine: bool,
32 pub exposure: f64,
34 pub saturation: f64,
37 pub shadows: f64,
39 pub highlights: f64,
41 pub tone: ToneCompression,
43 pub gamut: GamutCompression,
45}
46
47impl Default for DitherConfig {
48 fn default() -> Self {
49 Self {
50 mode: DitherMode::Burkes,
51 serpentine: true,
52 exposure: 1.0,
53 saturation: 1.0,
54 shadows: 0.0,
55 highlights: 0.0,
56 tone: ToneCompression::Fixed(0.0),
57 gamut: GamutCompression::None,
58 }
59 }
60}
61
62fn dispatch(
63 img: &ImageBuffer,
64 p: &Palette,
65 canonical: &Palette,
66 mode: DitherMode,
67 serpentine: bool,
68 pin_exact_pixels: bool,
69) -> Vec<u8> {
70 match mode {
71 DitherMode::None => algorithms::direct_map(img.data, p, canonical),
72 DitherMode::Ordered if pin_exact_pixels => {
73 algorithms::ordered_dither_with_canonical(img.data, img.width, p, canonical)
74 }
75 DitherMode::Ordered => algorithms::ordered_dither(img.data, img.width, p),
76 DitherMode::FloydSteinberg if pin_exact_pixels => algorithms::error_diffusion_dither_with_canonical(
77 img.data,
78 img.width,
79 img.height,
80 p,
81 canonical,
82 &algorithms::FLOYD_STEINBERG,
83 serpentine,
84 ),
85 DitherMode::Burkes if pin_exact_pixels => algorithms::error_diffusion_dither_with_canonical(
86 img.data,
87 img.width,
88 img.height,
89 p,
90 canonical,
91 &algorithms::BURKES,
92 serpentine,
93 ),
94 DitherMode::Atkinson if pin_exact_pixels => algorithms::error_diffusion_dither_with_canonical(
95 img.data,
96 img.width,
97 img.height,
98 p,
99 canonical,
100 &algorithms::ATKINSON,
101 serpentine,
102 ),
103 DitherMode::Stucki if pin_exact_pixels => algorithms::error_diffusion_dither_with_canonical(
104 img.data,
105 img.width,
106 img.height,
107 p,
108 canonical,
109 &algorithms::STUCKI,
110 serpentine,
111 ),
112 DitherMode::Sierra if pin_exact_pixels => algorithms::error_diffusion_dither_with_canonical(
113 img.data,
114 img.width,
115 img.height,
116 p,
117 canonical,
118 &algorithms::SIERRA,
119 serpentine,
120 ),
121 DitherMode::SierraLite if pin_exact_pixels => algorithms::error_diffusion_dither_with_canonical(
122 img.data,
123 img.width,
124 img.height,
125 p,
126 canonical,
127 &algorithms::SIERRA_LITE,
128 serpentine,
129 ),
130 DitherMode::JarvisJudiceNinke if pin_exact_pixels => {
131 algorithms::error_diffusion_dither_with_canonical(
132 img.data,
133 img.width,
134 img.height,
135 p,
136 canonical,
137 &algorithms::JARVIS_JUDICE_NINKE,
138 serpentine,
139 )
140 }
141 DitherMode::FloydSteinberg => algorithms::error_diffusion_dither(
142 img.data,
143 img.width,
144 img.height,
145 p,
146 &algorithms::FLOYD_STEINBERG,
147 serpentine,
148 ),
149 DitherMode::Burkes => algorithms::error_diffusion_dither(
150 img.data,
151 img.width,
152 img.height,
153 p,
154 &algorithms::BURKES,
155 serpentine,
156 ),
157 DitherMode::Atkinson => algorithms::error_diffusion_dither(
158 img.data,
159 img.width,
160 img.height,
161 p,
162 &algorithms::ATKINSON,
163 serpentine,
164 ),
165 DitherMode::Stucki => algorithms::error_diffusion_dither(
166 img.data,
167 img.width,
168 img.height,
169 p,
170 &algorithms::STUCKI,
171 serpentine,
172 ),
173 DitherMode::Sierra => algorithms::error_diffusion_dither(
174 img.data,
175 img.width,
176 img.height,
177 p,
178 &algorithms::SIERRA,
179 serpentine,
180 ),
181 DitherMode::SierraLite => algorithms::error_diffusion_dither(
182 img.data,
183 img.width,
184 img.height,
185 p,
186 &algorithms::SIERRA_LITE,
187 serpentine,
188 ),
189 DitherMode::JarvisJudiceNinke => algorithms::error_diffusion_dither(
190 img.data,
191 img.width,
192 img.height,
193 p,
194 &algorithms::JARVIS_JUDICE_NINKE,
195 serpentine,
196 ),
197 }
198}
199
200fn needs_preprocess(c: &DitherConfig) -> bool {
201 (c.exposure - 1.0).abs() > 1e-9
202 || (c.saturation - 1.0).abs() > 1e-9
203 || c.shadows > 0.0
204 || c.highlights > 0.0
205 || !matches!(c.tone, ToneCompression::Fixed(s) if s <= 0.0)
206 || !matches!(c.gamut, GamutCompression::None)
207}
208
209pub fn dither(img: &ImageBuffer, palette: impl AsRef<Palette>, config: DitherConfig) -> Vec<u8> {
213 let p = palette.as_ref();
214 dither_impl(img, p, p, config, false)
215}
216
217pub fn dither_with_canonical(
223 img: &ImageBuffer,
224 matching_palette: impl AsRef<Palette>,
225 canonical_palette: impl AsRef<Palette>,
226 config: DitherConfig,
227) -> Vec<u8> {
228 let p = matching_palette.as_ref();
229 let canonical = canonical_palette.as_ref();
230 dither_impl(img, p, canonical, config, true)
231}
232
233fn dither_impl(
234 img: &ImageBuffer,
235 p: &Palette,
236 canonical: &Palette,
237 config: DitherConfig,
238 pin_exact_pixels: bool,
239) -> Vec<u8> {
240 if !needs_preprocess(&config) {
241 if config.mode != DitherMode::None
242 && let Some(indices) = algorithms::try_exact_palette_map(img.data, canonical)
243 {
244 return indices;
245 }
246 return dispatch(img, p, canonical, config.mode, config.serpentine, pin_exact_pixels);
247 }
248
249 let mut linear: Vec<[f64; 3]> = img
251 .data
252 .chunks_exact(3)
253 .map(|c| [
254 srgb_channel_to_linear(c[0]),
255 srgb_channel_to_linear(c[1]),
256 srgb_channel_to_linear(c[2]),
257 ])
258 .collect();
259
260 tone_map::apply_exposure(&mut linear, config.exposure);
261 tone_map::adjust_saturation(&mut linear, config.saturation);
262 tone_map::apply_shadows_highlights(&mut linear, config.shadows, config.highlights);
263 config.tone.apply(&mut linear, p);
264 config.gamut.apply(&mut linear, p);
265
266 let processed: Vec<u8> = linear
267 .iter()
268 .flat_map(|&[r, g, b]| [
269 linear_channel_to_srgb(r),
270 linear_channel_to_srgb(g),
271 linear_channel_to_srgb(b),
272 ])
273 .collect();
274
275 let processed_img = ImageBuffer::new(&processed, img.width);
276 dispatch(&processed_img, p, canonical, config.mode, config.serpentine, pin_exact_pixels)
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282 use crate::measured_palettes::SPECTRA_7_3_6COLOR;
283 use crate::palettes::ColorScheme;
284
285 fn pixels(rgb: [u8; 3], count: usize) -> Vec<u8> {
286 std::iter::repeat_n(rgb, count).flatten().collect()
287 }
288
289 #[test]
290 fn default_config_has_preprocessing_off() {
291 let config = DitherConfig::default();
292 assert!(matches!(config.tone, ToneCompression::Fixed(s) if s == 0.0));
293 assert_eq!(config.gamut, GamutCompression::None);
294 assert!(!needs_preprocess(&config));
295 }
296
297 #[test]
298 fn none_uses_canonical_exact_colors_with_measured_palette() {
299 let image = pixels([255, 0, 0], 4);
300 let img = ImageBuffer::new(&image, 2);
301 let output = dither_with_canonical(
302 &img,
303 &SPECTRA_7_3_6COLOR,
304 ColorScheme::Bwgbry.palette(),
305 DitherConfig { mode: DitherMode::None, ..Default::default() },
306 );
307 assert_eq!(output, vec![3, 3, 3, 3]);
308 }
309
310 #[test]
311 fn exact_canonical_colors_bypass_error_diffusion() {
312 let image = [
313 [0, 0, 0],
314 [255, 255, 255],
315 [255, 255, 0],
316 [255, 0, 0],
317 ]
318 .into_iter()
319 .flatten()
320 .collect::<Vec<_>>();
321 let img = ImageBuffer::new(&image, 2);
322 let output = dither_with_canonical(
323 &img,
324 &SPECTRA_7_3_6COLOR,
325 ColorScheme::Bwry.palette(),
326 DitherConfig { mode: DitherMode::Burkes, ..Default::default() },
327 );
328 assert_eq!(output, vec![0, 1, 2, 3]);
329 }
330
331 #[test]
332 fn exact_canonical_pixels_are_pinned_inside_mixed_error_diffusion_image() {
333 let mut image = pixels([128, 128, 128], 8);
334 image[0..3].copy_from_slice(&[0, 255, 0]);
335 image[9..12].copy_from_slice(&[0, 255, 0]);
336 let img = ImageBuffer::new(&image, 4);
337
338 for mode in [
339 DitherMode::Burkes,
340 DitherMode::FloydSteinberg,
341 DitherMode::Atkinson,
342 DitherMode::Stucki,
343 DitherMode::Sierra,
344 DitherMode::SierraLite,
345 DitherMode::JarvisJudiceNinke,
346 ] {
347 let output = dither_with_canonical(
348 &img,
349 &SPECTRA_7_3_6COLOR,
350 ColorScheme::Bwgbry.palette(),
351 DitherConfig { mode, ..Default::default() },
352 );
353 assert_eq!(output[0], 5, "{mode:?} should pin exact green at pixel 0");
354 assert_eq!(output[3], 5, "{mode:?} should pin exact green at pixel 3");
355 }
356 }
357
358 #[test]
359 fn exact_canonical_pixels_are_pinned_inside_mixed_ordered_image() {
360 let mut image = pixels([128, 128, 128], 8);
361 image[0..3].copy_from_slice(&[0, 255, 0]);
362 image[9..12].copy_from_slice(&[0, 255, 0]);
363 let img = ImageBuffer::new(&image, 4);
364
365 let output = dither_with_canonical(
366 &img,
367 &SPECTRA_7_3_6COLOR,
368 ColorScheme::Bwgbry.palette(),
369 DitherConfig { mode: DitherMode::Ordered, ..Default::default() },
370 );
371 assert_eq!(output[0], 5);
372 assert_eq!(output[3], 5);
373 }
374
375 #[test]
376 fn exact_bypass_is_skipped_when_preprocessing_is_enabled() {
377 let image = pixels([255, 0, 0], 4);
378 let img = ImageBuffer::new(&image, 2);
379 let output = dither_with_canonical(
380 &img,
381 &SPECTRA_7_3_6COLOR,
382 ColorScheme::Bwgbry.palette(),
383 DitherConfig {
384 mode: DitherMode::Burkes,
385 tone: ToneCompression::Fixed(1.0),
386 ..Default::default()
387 },
388 );
389 assert_eq!(output.len(), 4);
390 }
391}