libsixel_rs/dither/
method.rs

1//! Dither methods
2//!
3//! Functions for applying different dither algorithms.
4
5use crate::{dimension::SpaceDimension, quant::MethodForDiffuse, Error, Result};
6
7/// Convenience alias for a dither method callback.
8pub type DitherFn = fn(&mut [u8], usize) -> Result<()>;
9
10/// No-op dither method
11pub fn dither_func_none(_data: &mut [u8], _width: usize) -> Result<()> {
12    Ok(())
13}
14
15/// Floyd-Steinberg method
16///
17/// | Transform coefficients |       |       |      |
18/// |------------------------|-------|-------|------|
19/// |                        |       |  curr | 7/16 |
20/// |                        | 3/16  |  5/48 | 1/16 |
21pub fn dither_func_fs(data: &mut [u8], width: usize) -> Result<()> {
22    let data_len = data.len();
23    let max_offset = width * 3 + 3;
24
25    if max_offset > data_len {
26        Err(Error::Dither(format!(
27            "dither_func_fs: max offset({max_offset}) is out-of-bounds, max: {data_len}"
28        )))
29    } else {
30        let (error_r, error_g, error_b) = (data[0] & 0x7, data[1] & 0x7, data[2] & 0x7);
31
32        // FIXME: this is a direct port of the math from libsixel, refactored to idiomatic Rust.
33        //
34        // It only includes transforms for 5/16, 3/16, and 5/16.
35        //
36        // The coefficients should include 7/16, 3/16, 5/48, 1/16.
37        //
38        // Unclear why they were omitted from the original...
39
40        let mut offset = 3;
41
42        // r
43        data[offset] = data[offset].saturating_add((error_r * 5) >> 4);
44        // g
45        data[offset + 1] = data[offset + 1].saturating_add((error_g * 5) >> 4);
46        // b
47        data[offset + 2] = data[offset + 2].saturating_add((error_b * 5) >> 4);
48
49        offset = width * offset - 3;
50
51        // r
52        data[offset] = data[offset].saturating_add((error_r * 3) >> 4);
53        // g
54        data[offset + 1] = data[offset + 1].saturating_add((error_g * 3) >> 4);
55        // b
56        data[offset + 2] = data[offset + 2].saturating_add((error_b * 3) >> 4);
57
58        offset += 3;
59
60        // r
61        data[offset] = data[offset].saturating_add((error_r * 5) >> 4);
62        // g
63        data[offset + 1] = data[offset + 1].saturating_add((error_g * 5) >> 4);
64        // b
65        data[offset + 2] = data[offset + 2].saturating_add((error_b * 5) >> 4);
66
67        Ok(())
68    }
69}
70
71/// Atkinson's method
72///
73/// | Transform coefficients |     |       |     |     |
74/// |------------------------|-----|-------|-----|-----|
75/// |                        |     |  curr | 1/8 | 1/8 |
76/// |                        | 1/8 |  1/8  | 1/8 |     |
77/// |                        |     |  1/8  |     |     |
78pub fn dither_func_atkinson(data: &mut [u8], width: usize) -> Result<()> {
79    let data_len = data.len();
80    let max_offset = width * 6 + 3;
81    if max_offset > data_len {
82        Err(Error::Dither(format!(
83            "dither_func_fs: max offset({max_offset}) is out-of-bounds, max: {data_len}"
84        )))
85    } else {
86        let (error_r, error_g, error_b) = (
87            (data[0] & 0x7).saturating_add(4),
88            (data[1] & 0x7).saturating_add(4),
89            (data[2] & 0x7).saturating_add(4),
90        );
91
92        let (coeff_r, coeff_g, coeff_b) = (error_r >> 3, error_g >> 3, error_b >> 3);
93
94        let mut offset = 3;
95
96        // r
97        data[offset] = data[offset].saturating_add(coeff_r);
98        // g
99        data[offset + 1] = data[offset + 1].saturating_add(coeff_g);
100        // b
101        data[offset + 2] = data[offset + 2].saturating_add(coeff_b);
102
103        offset *= 2;
104
105        // r
106        data[offset] = data[offset].saturating_add(coeff_r);
107        // g
108        data[offset + 1] = data[offset + 1].saturating_add(coeff_g);
109        // b
110        data[offset + 2] = data[offset + 2].saturating_add(coeff_b);
111
112        offset += 3;
113
114        // r
115        data[offset] = data[offset].saturating_add(coeff_r);
116        // g
117        data[offset + 1] = data[offset + 1].saturating_add(coeff_g);
118        // b
119        data[offset + 2] = data[offset + 2].saturating_add(coeff_b);
120
121        offset = (width - 1) * 3;
122
123        // r
124        data[offset] = data[offset].saturating_add(coeff_r);
125        // g
126        data[offset + 1] = data[offset + 1].saturating_add(coeff_g);
127        // b
128        data[offset + 2] = data[offset + 2].saturating_add(coeff_b);
129
130        offset = width * 3;
131
132        // r
133        data[offset] = data[offset].saturating_add(coeff_r);
134        // g
135        data[offset + 1] = data[offset + 1].saturating_add(coeff_g);
136        // b
137        data[offset + 2] = data[offset + 2].saturating_add(coeff_b);
138
139        offset = (width + 1) * 3;
140
141        // r
142        data[offset] = data[offset].saturating_add(coeff_r);
143        // g
144        data[offset + 1] = data[offset + 1].saturating_add(coeff_g);
145        // b
146        data[offset + 2] = data[offset + 2].saturating_add(coeff_b);
147
148        offset = width * 6;
149
150        // r
151        data[offset] = data[offset].saturating_add(coeff_r);
152        // g
153        data[offset + 1] = data[offset + 1].saturating_add(coeff_g);
154        // b
155        data[offset + 2] = data[offset + 2].saturating_add(coeff_b);
156
157        Ok(())
158    }
159}
160
161/// Jajuni method
162///
163/// | Transform coefficients |      |      |       |      |      |
164/// |------------------------|------|------|-------|------|------|
165/// |                        |      |      |  curr | 7/48 | 5/48 |
166/// |                        | 3/48 | 5/48 |  7/48 | 5/48 | 3/48 |
167/// |                        | 1/48 | 3/48 |  5/48 | 3/48 | 1/48 |
168pub fn dither_func_jajuni(data: &mut [u8], width: usize) -> Result<()> {
169    let data_len = data.len();
170    let max_offset = (width * 2 + 2) * 3 + 3;
171    if max_offset > data_len {
172        Err(Error::Dither(format!(
173            "dither_func_fs: max offset({max_offset}) is out-of-bounds, max: {data_len}"
174        )))
175    } else {
176        let (error_r, error_g, error_b) = (data[0] & 0x7, data[1] & 0x7, data[2] & 0x7);
177
178        let (coeff_1_48_r, coeff_1_48_g, coeff_1_48_b) = (error_r / 48, error_g / 48, error_b / 48);
179        let (coeff_3_48_r, coeff_3_48_g, coeff_3_48_b) = (error_r / 16, error_g / 16, error_b / 16);
180        let (coeff_5_48_r, coeff_5_48_g, coeff_5_48_b) =
181            (error_r * 5 / 48, error_g * 5 / 48, error_b * 5 / 48);
182        let (coeff_7_48_r, coeff_7_48_g, coeff_7_48_b) =
183            (error_r * 7 / 48, error_g * 7 / 48, error_b * 7 / 48);
184
185        let mut offset = 3;
186
187        // r
188        data[offset] = data[offset].saturating_add(coeff_7_48_r);
189        // g
190        data[offset + 1] = data[offset + 1].saturating_add(coeff_7_48_g);
191        // b
192        data[offset + 2] = data[offset + 2].saturating_add(coeff_7_48_b);
193
194        offset *= 2;
195
196        // r
197        data[offset] = data[offset].saturating_add(coeff_5_48_r);
198        // g
199        data[offset + 1] = data[offset + 1].saturating_add(coeff_5_48_g);
200        // b
201        data[offset + 2] = data[offset + 2].saturating_add(coeff_5_48_b);
202
203        offset = (width - 2) * 3;
204
205        // r
206        data[offset] = data[offset].saturating_add(coeff_3_48_r);
207        // g
208        data[offset + 1] = data[offset + 1].saturating_add(coeff_3_48_g);
209        // b
210        data[offset + 2] = data[offset + 2].saturating_add(coeff_3_48_b);
211
212        offset = (width - 1) * 3;
213
214        // r
215        data[offset] = data[offset].saturating_add(coeff_5_48_r);
216        // g
217        data[offset + 1] = data[offset + 1].saturating_add(coeff_5_48_g);
218        // b
219        data[offset + 2] = data[offset + 2].saturating_add(coeff_5_48_b);
220
221        offset = width * 3;
222
223        // r
224        data[offset] = data[offset].saturating_add(coeff_7_48_r);
225        // g
226        data[offset + 1] = data[offset + 1].saturating_add(coeff_7_48_g);
227        // b
228        data[offset + 2] = data[offset + 2].saturating_add(coeff_7_48_b);
229
230        offset = (width + 1) * 3;
231
232        // r
233        data[offset] = data[offset].saturating_add(coeff_5_48_r);
234        // g
235        data[offset + 1] = data[offset + 1].saturating_add(coeff_5_48_g);
236        // b
237        data[offset + 2] = data[offset + 2].saturating_add(coeff_5_48_b);
238
239        offset = (width + 2) * 3;
240
241        // r
242        data[offset] = data[offset].saturating_add(coeff_3_48_r);
243        // g
244        data[offset + 1] = data[offset + 1].saturating_add(coeff_3_48_g);
245        // b
246        data[offset + 2] = data[offset + 2].saturating_add(coeff_3_48_b);
247
248        offset = (width * 2 - 2) * 3;
249
250        // r
251        data[offset] = data[offset].saturating_add(coeff_1_48_r);
252        // g
253        data[offset + 1] = data[offset + 1].saturating_add(coeff_1_48_g);
254        // b
255        data[offset + 2] = data[offset + 2].saturating_add(coeff_1_48_b);
256
257        offset = (width * 2 - 1) * 3;
258
259        // r
260        data[offset] = data[offset].saturating_add(coeff_3_48_r);
261        // g
262        data[offset + 1] = data[offset + 1].saturating_add(coeff_3_48_g);
263        // b
264        data[offset + 2] = data[offset + 2].saturating_add(coeff_3_48_b);
265
266        offset = width * 6;
267
268        // r
269        data[offset] = data[offset].saturating_add(coeff_5_48_r);
270        // g
271        data[offset + 1] = data[offset + 1].saturating_add(coeff_5_48_g);
272        // b
273        data[offset + 2] = data[offset + 2].saturating_add(coeff_5_48_b);
274
275        offset = (width * 2 + 1) * 3;
276
277        // r
278        data[offset] = data[offset].saturating_add(coeff_3_48_r);
279        // g
280        data[offset + 1] = data[offset + 1].saturating_add(coeff_3_48_g);
281        // b
282        data[offset + 2] = data[offset + 2].saturating_add(coeff_3_48_b);
283
284        offset = (width * 2 + 2) * 3;
285
286        // r
287        data[offset] = data[offset].saturating_add(coeff_1_48_r);
288        // g
289        data[offset + 1] = data[offset + 1].saturating_add(coeff_1_48_g);
290        // b
291        data[offset + 2] = data[offset + 2].saturating_add(coeff_1_48_b);
292
293        Ok(())
294    }
295}
296
297/// Stucki's method
298///
299/// | Transform coefficients |      |      |       |      |      |
300/// |------------------------|------|------|-------|------|------|
301/// |                        |      |      |  curr | 8/48 | 4/48 |
302/// |                        | 2/48 | 4/48 |  8/48 | 4/48 | 2/48 |
303/// |                        | 1/48 | 2/48 |  4/48 | 2/48 | 1/48 |
304pub fn dither_func_stucki(data: &mut [u8], width: usize) -> Result<()> {
305    let data_len = data.len();
306    let max_offset = (width * 2 + 2) * 3 + 3;
307    if max_offset > data_len {
308        Err(Error::Dither(format!(
309            "dither_func_fs: max offset({max_offset}) is out-of-bounds, max: {data_len}"
310        )))
311    } else {
312        let (error_r, error_g, error_b) = (data[0] & 0x7, data[1] & 0x7, data[2] & 0x7);
313
314        let (coeff_1_48_r, coeff_1_48_g, coeff_1_48_b) = (error_r / 48, error_g / 48, error_b / 48);
315        let (coeff_2_48_r, coeff_2_48_g, coeff_2_48_b) = (error_r / 24, error_g / 24, error_b / 24);
316        let (coeff_4_48_r, coeff_4_48_g, coeff_4_48_b) = (error_r / 12, error_g / 12, error_b / 12);
317        let (coeff_8_48_r, coeff_8_48_g, coeff_8_48_b) = (error_r / 6, error_g / 6, error_b / 6);
318
319        let mut offset = 3;
320
321        // r
322        data[offset] = data[offset].saturating_add(coeff_8_48_r);
323        // g
324        data[offset + 1] = data[offset + 1].saturating_add(coeff_8_48_g);
325        // b
326        data[offset + 2] = data[offset + 2].saturating_add(coeff_8_48_b);
327
328        offset *= 2;
329
330        // r
331        data[offset] = data[offset].saturating_add(coeff_4_48_r);
332        // g
333        data[offset + 1] = data[offset + 1].saturating_add(coeff_4_48_g);
334        // b
335        data[offset + 2] = data[offset + 2].saturating_add(coeff_4_48_b);
336
337        offset = (width - 2) * 3;
338
339        // r
340        data[offset] = data[offset].saturating_add(coeff_2_48_r);
341        // g
342        data[offset + 1] = data[offset + 1].saturating_add(coeff_2_48_g);
343        // b
344        data[offset + 2] = data[offset + 2].saturating_add(coeff_2_48_b);
345
346        offset = (width - 1) * 3;
347
348        // r
349        data[offset] = data[offset].saturating_add(coeff_4_48_r);
350        // g
351        data[offset + 1] = data[offset + 1].saturating_add(coeff_4_48_g);
352        // b
353        data[offset + 2] = data[offset + 2].saturating_add(coeff_4_48_b);
354
355        offset = width * 3;
356
357        // r
358        data[offset] = data[offset].saturating_add(coeff_8_48_r);
359        // g
360        data[offset + 1] = data[offset + 1].saturating_add(coeff_8_48_g);
361        // b
362        data[offset + 2] = data[offset + 2].saturating_add(coeff_8_48_b);
363
364        offset = (width + 1) * 3;
365
366        // r
367        data[offset] = data[offset].saturating_add(coeff_4_48_r);
368        // g
369        data[offset + 1] = data[offset + 1].saturating_add(coeff_4_48_g);
370        // b
371        data[offset + 2] = data[offset + 2].saturating_add(coeff_4_48_b);
372
373        offset = (width + 2) * 3;
374
375        // r
376        data[offset] = data[offset].saturating_add(coeff_2_48_r);
377        // g
378        data[offset + 1] = data[offset + 1].saturating_add(coeff_2_48_g);
379        // b
380        data[offset + 2] = data[offset + 2].saturating_add(coeff_2_48_b);
381
382        offset = (width * 2 - 2) * 3;
383
384        // r
385        data[offset] = data[offset].saturating_add(coeff_1_48_r);
386        // g
387        data[offset + 1] = data[offset + 1].saturating_add(coeff_1_48_g);
388        // b
389        data[offset + 2] = data[offset + 2].saturating_add(coeff_1_48_b);
390
391        offset = (width * 2 - 1) * 3;
392
393        // r
394        data[offset] = data[offset].saturating_add(coeff_2_48_r);
395        // g
396        data[offset + 1] = data[offset + 1].saturating_add(coeff_2_48_g);
397        // b
398        data[offset + 2] = data[offset + 2].saturating_add(coeff_2_48_b);
399
400        offset = width * 6;
401
402        // r
403        data[offset] = data[offset].saturating_add(coeff_4_48_r);
404        // g
405        data[offset + 1] = data[offset + 1].saturating_add(coeff_4_48_g);
406        // b
407        data[offset + 2] = data[offset + 2].saturating_add(coeff_4_48_b);
408
409        offset = (width * 2 + 1) * 3;
410
411        // r
412        data[offset] = data[offset].saturating_add(coeff_2_48_r);
413        // g
414        data[offset + 1] = data[offset + 1].saturating_add(coeff_2_48_g);
415        // b
416        data[offset + 2] = data[offset + 2].saturating_add(coeff_2_48_b);
417
418        offset = (width * 2 + 2) * 3;
419
420        // r
421        data[offset] = data[offset].saturating_add(coeff_1_48_r);
422        // g
423        data[offset + 1] = data[offset + 1].saturating_add(coeff_1_48_g);
424        // b
425        data[offset + 2] = data[offset + 2].saturating_add(coeff_1_48_b);
426
427        Ok(())
428    }
429}
430
431/// Burkes' method
432///
433/// | Transform coefficients |      |      |       |      |      |
434/// |------------------------|------|------|-------|------|------|
435/// |                        |      |      |  curr | 4/16 | 2/16 |
436/// |                        | 1/16 | 2/16 |  4/16 | 2/16 | 1/16 |
437pub fn dither_func_burkes(data: &mut [u8], width: usize) -> Result<()> {
438    let data_len = data.len();
439    let max_offset = (width + 2) * 3 + 3;
440    if max_offset > data_len {
441        Err(Error::Dither(format!(
442            "dither_func_fs: max offset({max_offset}) is out-of-bounds, max: {data_len}"
443        )))
444    } else {
445        let (error_r, error_g, error_b) = (data[0] & 0x7, data[1] & 0x7, data[2] & 0x7);
446
447        let (coeff_1_16_r, coeff_1_16_g, coeff_1_16_b) = (error_r / 16, error_g / 16, error_b / 16);
448        let (coeff_2_16_r, coeff_2_16_g, coeff_2_16_b) = (error_r / 8, error_g / 8, error_b / 8);
449        let (coeff_4_16_r, coeff_4_16_g, coeff_4_16_b) = (error_r / 4, error_g / 4, error_b / 4);
450
451        let mut offset = 3;
452
453        // r
454        data[offset] = data[offset].saturating_add(coeff_4_16_r);
455        // g
456        data[offset + 1] = data[offset + 1].saturating_add(coeff_4_16_g);
457        // b
458        data[offset + 2] = data[offset + 2].saturating_add(coeff_4_16_b);
459
460        offset *= 2;
461
462        // r
463        data[offset] = data[offset].saturating_add(coeff_2_16_r);
464        // g
465        data[offset + 1] = data[offset + 1].saturating_add(coeff_2_16_g);
466        // b
467        data[offset + 2] = data[offset + 2].saturating_add(coeff_2_16_b);
468
469        offset = (width - 2) * 3;
470
471        // r
472        data[offset] = data[offset].saturating_add(coeff_1_16_r);
473        // g
474        data[offset + 1] = data[offset + 1].saturating_add(coeff_1_16_g);
475        // b
476        data[offset + 2] = data[offset + 2].saturating_add(coeff_1_16_b);
477
478        offset = (width - 1) * 3;
479
480        // r
481        data[offset] = data[offset].saturating_add(coeff_2_16_r);
482        // g
483        data[offset + 1] = data[offset + 1].saturating_add(coeff_2_16_g);
484        // b
485        data[offset + 2] = data[offset + 2].saturating_add(coeff_2_16_b);
486
487        offset = width * 3;
488
489        // r
490        data[offset] = data[offset].saturating_add(coeff_4_16_r);
491        // g
492        data[offset + 1] = data[offset + 1].saturating_add(coeff_4_16_g);
493        // b
494        data[offset + 2] = data[offset + 2].saturating_add(coeff_4_16_b);
495
496        offset = (width + 1) * 3;
497
498        // r
499        data[offset] = data[offset].saturating_add(coeff_2_16_r);
500        // g
501        data[offset + 1] = data[offset + 1].saturating_add(coeff_2_16_g);
502        // b
503        data[offset + 2] = data[offset + 2].saturating_add(coeff_2_16_b);
504
505        offset = (width + 2) * 3;
506
507        // r
508        data[offset] = data[offset].saturating_add(coeff_1_16_r);
509        // g
510        data[offset + 1] = data[offset + 1].saturating_add(coeff_1_16_g);
511        // b
512        data[offset + 2] = data[offset + 2].saturating_add(coeff_1_16_b);
513
514        Ok(())
515    }
516}
517
518/// `A` dither method
519pub fn dither_func_a_dither(data: &mut [u8], _width: usize, x: usize, y: usize) -> Result<()> {
520    let data_len = data.len();
521
522    if data_len < 3 {
523        Err(Error::Dither(format!(
524            "depth(3) is out-of-bounds, max: {data_len}"
525        )))
526    } else {
527        for c in data.iter_mut().take(3) {
528            let mut mask = ((((x + *c as usize * 17) + y * 236) * 119) & 255) as f32;
529            mask = (mask - 128.0) / 256.0;
530            let value = (*c as f32) + mask;
531
532            *c = if (0.0..=255.0).contains(&value) {
533                value as u8
534            } else if value < 0.0
535                || value.is_nan()
536                || (value.is_infinite() && value.is_sign_negative())
537            {
538                0
539            } else if value > 255.0 || (value.is_infinite() && value.is_sign_positive()) {
540                255
541            } else {
542                0
543            };
544        }
545
546        Ok(())
547    }
548}
549
550/// `X` dither method
551pub fn dither_func_x_dither(data: &mut [u8], _width: usize, x: usize, y: usize) -> Result<()> {
552    let data_len = data.len();
553
554    if data_len < 3 {
555        Err(Error::Dither(format!(
556            "depth(3) is out-of-bounds, max: {data_len}"
557        )))
558    } else {
559        for c in data.iter_mut().take(3) {
560            let mut mask = ((((x + *c as usize * 17) + y * 236) * 1234) & 511) as f32;
561            mask = (mask - 128.0) / 512.0;
562            let value = (*c as f32) + mask;
563
564            *c = if (0.0..=255.0).contains(&value) {
565                value as u8
566            } else if value < 0.0
567                || value.is_nan()
568                || (value.is_infinite() && value.is_sign_negative())
569            {
570                0
571            } else if value > 255.0 || (value.is_infinite() && value.is_sign_positive()) {
572                255
573            } else {
574                0
575            };
576        }
577
578        Ok(())
579    }
580}
581
582/// Apply a dither method based on [`MethodForDiffuse`] variant.
583pub fn apply_15bpp_dither(
584    pixels: &mut [u8],
585    sd: SpaceDimension,
586    method_for_diffuse: MethodForDiffuse,
587) -> Result<()> {
588    let (x, y, width, height) = (sd.x, sd.y, sd.width, sd.height);
589
590    match method_for_diffuse {
591        MethodForDiffuse::Fs if x < width.saturating_sub(1) && y < height.saturating_sub(1) => {
592            dither_func_fs(pixels, width)
593        }
594        MethodForDiffuse::Atkinson
595            if x < width.saturating_sub(2) && y < height.saturating_sub(2) =>
596        {
597            dither_func_atkinson(pixels, width)
598        }
599        MethodForDiffuse::Jajuni if x < width.saturating_sub(2) && y < height.saturating_sub(2) => {
600            dither_func_jajuni(pixels, width)
601        }
602        MethodForDiffuse::Stucki if x < width.saturating_sub(2) && y < height.saturating_sub(2) => {
603            dither_func_stucki(pixels, width)
604        }
605        MethodForDiffuse::Burkes if x < width.saturating_sub(2) && y < height.saturating_sub(2) => {
606            dither_func_burkes(pixels, width)
607        }
608        MethodForDiffuse::ADither => dither_func_a_dither(pixels, width, x, y),
609        MethodForDiffuse::XDither => dither_func_x_dither(pixels, width, x, y),
610        _ => dither_func_none(pixels, width),
611    }
612}