libsixel_rs/
quant.rs

1//! Types and methods for handling color map operations.
2
3use alloc::vec::Vec;
4
5use crate::std::{
6    cmp,
7    sync::atomic::{AtomicUsize, Ordering},
8};
9
10use crate::{Error, Result};
11
12mod box_vector;
13mod palette;
14mod tuple;
15
16pub use box_vector::*;
17pub use palette::*;
18pub use tuple::*;
19
20static COMPARE_PLANE: AtomicUsize = AtomicUsize::new(0);
21
22/// Gets the global comapre plane value.
23///
24/// This is a parameter to the [compare_plane] function.
25///
26/// Separated to allow [compare_plane] to be used as a function parameter for `sort_by`.
27pub fn global_compare_plane() -> usize {
28    COMPARE_PLANE.load(Ordering::Relaxed)
29}
30
31/// Gets the global comapre plane value.
32///
33/// This is a parameter to the [compare_plane] function.
34///
35/// Separated to allow [compare_plane] to be used as a function parameter for `sort_by`.
36pub fn set_global_compare_plane(plane: usize) {
37    COMPARE_PLANE.store(plane, Ordering::SeqCst)
38}
39
40/// Sort [TupleInt] by item selected by the `global_compare_plane` parameter.
41pub fn compare_plane(lhs: &TupleInt, rhs: &TupleInt) -> cmp::Ordering {
42    let plane = global_compare_plane();
43
44    let lhs_tuple = lhs.tuple.get(plane).unwrap_or(&0);
45    let rhs_tuple = rhs.tuple.get(plane).unwrap_or(&0);
46
47    lhs_tuple.cmp(rhs_tuple)
48}
49
50/// Histogram for [ColorMap] data.
51pub type Histogram = Vec<u16>;
52
53/// Built-in dither
54#[repr(u8)]
55#[derive(Clone, Copy, Debug, Default, PartialEq)]
56pub enum BuiltinDither {
57    /// Monochrome terminal with dark background
58    #[default]
59    MonoDark = 0,
60    /// Monochrome terminal with dark background
61    MonoLight,
62    /// XTerm 16color
63    Xterm16,
64    /// XTerm 256color
65    Xterm256,
66    /// VT340 monochrome
67    Vt340Mono,
68    /// VT340 color
69    Vt340Color,
70}
71
72/// Represents a map of colors and pixels.
73#[derive(Clone, Debug, PartialEq)]
74#[repr(C)]
75pub struct ColorMap {
76    pub table: Vec<TupleInt>,
77}
78
79impl ColorMap {
80    /// Creates a new [ColorMap].
81    pub const fn new() -> Self {
82        Self { table: Vec::new() }
83    }
84
85    /// Creates a new [ColorMap] with the provided capacity.
86    pub fn with_capacity(cap: usize) -> Self {
87        Self {
88            table: Vec::with_capacity(cap),
89        }
90    }
91
92    /// Creates a new [ColorMap] with `colors` [TupleInt]s, that each have `depth` entries.
93    pub fn with_colors_and_depth(colors: usize, depth: usize) -> Self {
94        Self {
95            table: vec![TupleInt::with_depth(depth); colors],
96        }
97    }
98
99    /// Gets a reference to the [ColorMap] table.
100    pub fn table(&self) -> &[TupleInt] {
101        self.table.as_ref()
102    }
103
104    /// Gets the length of the [ColorMap].
105    pub fn len(&self) -> usize {
106        self.table.len()
107    }
108
109    /// Gets whether the [ColorMap] is empty.
110    pub fn is_empty(&self) -> bool {
111        self.table.is_empty()
112    }
113
114    /// Produce a colormap containing the best colors to represent the
115    /// image stream in file 'ifP'.  Figure it out using the median cut
116    /// technique.
117    ///
118    /// The colormap will have 'reqcolors' or fewer colors in it, unless
119    /// 'allcolors' is true, in which case it will have all the colors that
120    /// are in the input.
121    ///
122    /// The colormap has the same maxval as the input.
123    ///
124    /// Put the colormap in newly allocated storage as a tupletable2
125    /// and return its address as *colormapP.  Return the number of colors in
126    /// it as *colorsP and its maxval as *colormapMaxvalP.
127    ///
128    /// Return the characteristics of the input file as
129    /// *formatP and *freqPamP.  (This information is not really
130    /// relevant to our colormap mission; just a fringe benefit).
131    pub fn compute_from_input(
132        data: &[u8],
133        depth: usize,
134        req_colors: usize,
135        method_for_largest: MethodForLargest,
136        method_for_rep: MethodForRep,
137        quality_mode: QualityMode,
138        orig_colors: &mut usize,
139    ) -> Result<Self> {
140        let mut color_map = Self::compute_histogram(data, depth, quality_mode)?;
141        *orig_colors = color_map.table.len();
142
143        if color_map.table.len() <= req_colors {
144            Ok(color_map)
145        } else {
146            color_map.median_cut(depth, req_colors, method_for_largest, method_for_rep)
147        }
148    }
149
150    /// Computes a histogram for a [ColorMap].
151    pub fn compute_histogram(data: &[u8], depth: usize, quality_mode: QualityMode) -> Result<Self> {
152        let len = data.len();
153
154        let max_sample: usize = match quality_mode {
155            QualityMode::Low | QualityMode::High => 18383,
156            _ => 4_003_079,
157        };
158
159        let mut step = if len < max_sample.saturating_mul(depth) {
160            depth.saturating_mul(6)
161        } else {
162            len.saturating_div(depth)
163                .saturating_div(max_sample)
164                .saturating_mul(depth)
165        };
166
167        if step == 0 {
168            step = depth;
169        }
170
171        let hist_cap = 1usize.wrapping_shl(depth.saturating_mul(5) as u32);
172        let mut histogram = vec![0u16; hist_cap];
173        let mut hist_ref: Vec<u16> = Vec::with_capacity(hist_cap);
174
175        for i in (0..len).step_by(step) {
176            let bucket_idx = compute_hash(data[i..].as_ref(), 3);
177            if bucket_idx >= hist_cap {
178                continue;
179            }
180
181            let hist_val = histogram[bucket_idx];
182            if hist_val == 0 {
183                hist_ref.push(bucket_idx as u16);
184            }
185
186            if hist_val < u16::MAX {
187                histogram[bucket_idx] = hist_val.saturating_add(1);
188            }
189        }
190
191        let map_len = hist_ref.len();
192        let mut color_map = Self::with_capacity(map_len);
193
194        for &i in hist_ref.iter() {
195            let hist_val = histogram[i as usize];
196            if hist_val > 0 {
197                let mut tuple = TupleInt::new();
198                tuple.set_value(hist_val as u32);
199                tuple.tuple = (0..map_len)
200                    .map(|n| ((i >> (n * 5) & 0x1f) << 3) as u32)
201                    .rev()
202                    .collect();
203
204                color_map.table.push(tuple);
205            }
206        }
207
208        Ok(color_map)
209    }
210
211    /// Compute a set of only `new_colors` colors that best represent an
212    /// image whose pixels are summarized by the histogram
213    /// `color_freq_table`.  Each tuple in that table has depth `depth`.
214    /// `color_freq_table.table[i]` tells the number of pixels in the subject image
215    /// have a particular color.
216    ///
217    /// As a side effect, sort `color_freq_table`.
218    pub fn median_cut(
219        &mut self,
220        depth: usize,
221        new_colors: usize,
222        method_for_largest: MethodForLargest,
223        method_for_rep: MethodForRep,
224    ) -> Result<Self> {
225        let sum = self.table.iter().map(|t| t.value as usize).sum();
226
227        // There is at least one box that contains at least 2 colors; ergo,
228        // there is more splitting we can do.
229        let mut bv = BoxVector::create(self.table.len(), new_colors, sum);
230
231        let mut boxes = 1usize;
232        let mut multicolor_boxes_exist = self.table.len() > 1;
233
234        // Main loop: split boxes until we have enough.
235        while boxes < new_colors && multicolor_boxes_exist {
236            let bi = bv.0.iter().filter(|b| b.colors >= 2).count();
237
238            if bi == 0 {
239                multicolor_boxes_exist = false;
240            } else {
241                boxes = self.split_box(&mut bv, boxes, bi, depth, method_for_largest)?;
242            }
243        }
244
245        self.from_bv(&bv, boxes, depth, new_colors, method_for_rep)
246    }
247
248    /// Split Box 'bi' in the box vector bv (so that bv contains one more box
249    /// than it did as input).  Split it so that each new box represents about
250    /// half of the pixels in the distribution given by 'colorfreqtable' for
251    /// the colors in the original box, but with distinct colors in each of the
252    /// two new boxes.
253    ///
254    /// Assume the box contains at least two colors.
255    pub fn split_box(
256        &mut self,
257        bv: &mut BoxVector,
258        mut boxes: usize,
259        bi: usize,
260        depth: usize,
261        method_for_largest: MethodForLargest,
262    ) -> Result<usize> {
263        const MAX_DEPTH: usize = 16;
264
265        if depth > MAX_DEPTH {
266            return Err(Error::Quant(format!(
267                "invalid depth, max: {MAX_DEPTH}, have: {depth}"
268            )));
269        }
270
271        let bv_len = bv.0.len();
272        if bi >= bv_len || boxes >= bv_len {
273            return Err(Error::Quant(format!("invalid BoxVector index and/or number, length: {bv_len}, index: {bi}, boxes: {boxes}")));
274        }
275
276        let box_start = bv[bi].ind;
277        let box_size = bv[bi].colors;
278        let sm = bv[bi].sum;
279
280        let mut min_val = [0u32; MAX_DEPTH];
281        let mut max_val = [0u32; MAX_DEPTH];
282
283        self.find_box_boundaries(depth, box_start, min_val.as_mut(), max_val.as_mut())?;
284
285        // From libsixel comments:
286        //
287        // ```
288        // Find the largest dimension, and sort by that component.  I have
289        // included two methods for determining the "largest" dimension;
290        // first by simply comparing the range in RGB space, and second by
291        // transforming into luminosities before the comparison.
292        // ```
293        //
294        // number of the plane with the largest spread
295        let largest_dimension = match method_for_largest {
296            MethodForLargest::Norm => largest_by_norm(&min_val[..depth], &max_val[..depth]),
297            MethodForLargest::Lum => largest_by_luminosity(&min_val[..depth], &max_val[..depth]),
298            _ => {
299                return Err(Error::Quant(format!(
300                    "internal: invalid valud for MethodForLargest, have: {method_for_largest}"
301                )));
302            }
303        };
304
305        // initial sum value
306        let mut lower_sum = self.table[box_start].value as usize;
307
308        // Set the gross global variable `compare_plane_plane` as a
309        // parameter to `compare_plane()`, which is called by `qsort()`.
310        let median_index = {
311            // Now find the median based on the counts, so that about half
312            // the pixels (not colors, pixels) are in each subdivision.
313
314            let mut i = 1usize;
315
316            while i < box_size && lower_sum < sm / 2 {
317                lower_sum = lower_sum.saturating_add(self.table[box_start + i].value as usize);
318                i += 1;
319            }
320
321            i
322        };
323
324        /* Split the box, and sort to bring the biggest boxes to the top.  */
325
326        bv[bi].colors = median_index;
327        bv[bi].sum = lower_sum;
328        bv[boxes].ind = box_start.saturating_add(median_index);
329        bv[boxes].colors = box_size.saturating_sub(median_index);
330        bv[boxes].sum = sm.saturating_sub(lower_sum);
331
332        boxes = boxes.saturating_add(1);
333
334        set_global_compare_plane(largest_dimension);
335
336        self.table[box_start..box_size]
337            .as_mut()
338            .sort_by(compare_plane);
339        bv.0[..boxes].as_mut().sort_by(sum_compare);
340
341        Ok(boxes)
342    }
343
344    /// Creates a [ColorMap] from a [BoxVector].
345    ///
346    /// From `libsixel` comments:
347    ///
348    ///
349    /// ```no_build, no_run
350    /// Ok, we've got enough boxes.  Now choose a representative color for
351    /// each box.  There are a number of possible ways to make this choice.
352    /// One would be to choose the center of the box; this ignores any structure
353    /// within the boxes.  Another method would be to average all the colors in
354    /// the box - this is the method specified in Heckbert's paper.  A third
355    /// method is to average all the pixels in the box.
356    /// ```
357    pub fn from_bv(
358        &self,
359        bv: &BoxVector,
360        boxes: usize,
361        depth: usize,
362        new_colors: usize,
363        method_for_rep: MethodForRep,
364    ) -> Result<Self> {
365        let mut colormap = ColorMap::with_colors_and_depth(new_colors, depth);
366        let bv_ref = bv.as_ref();
367
368        for (bi, table) in bv_ref[..boxes]
369            .iter()
370            .zip(colormap.table[..boxes].iter_mut())
371        {
372            table.tuple = match method_for_rep {
373                MethodForRep::CenterBox => self.center_box(bi.ind, bi.colors, depth)?,
374                MethodForRep::AverageColors => self.average_colors(bi.ind, bi.colors, depth)?,
375                MethodForRep::AveragePixels => self.average_pixels(bi.ind, bi.colors, depth)?,
376                _ => {
377                    return Err(Error::Quant(format!(
378                        "invalid value of method_for_rep: {method_for_rep}"
379                    )))
380                }
381            };
382        }
383
384        Ok(colormap)
385    }
386
387    /// Find the boundary of a box of pixels.
388    ///
389    /// From the `libsixel` docs:
390    ///
391    /// ```no_build, no_run
392    /// Go through the box finding the minimum and maximum of each
393    /// component - the boundaries of the box.
394    /// ```
395    pub fn find_box_boundaries(
396        &self,
397        depth: usize,
398        box_start: usize,
399        min_val: &mut [Sample],
400        max_val: &mut [Sample],
401    ) -> Result<()> {
402        let table_len = self.table.len();
403        let min_val_len = min_val.len();
404        let max_val_len = max_val.len();
405        let tuple_len = if let Some(tuple) = self.table.get(box_start) {
406            tuple.tuple.len()
407        } else {
408            0
409        };
410        if (box_start + 1) > table_len
411            || depth > tuple_len
412            || depth > min_val_len
413            || depth > max_val_len
414        {
415            return Err(Error::Quant(
416                format!("box_start and/or depth is out-of-bounds, table length: {table_len}, box_start: {box_start}, tuple length: {tuple_len}, min_val length: {min_val_len} max_val length: {max_val_len}")
417            ));
418        }
419
420        for (plane, &val) in self.table[box_start].tuple[..depth].iter().enumerate() {
421            min_val[plane] = val;
422            max_val[plane] = val;
423        }
424
425        for tuple in self.table[box_start + 1..].iter() {
426            for (plane, &val) in tuple.tuple[..depth].iter().enumerate() {
427                if val < min_val[plane] {
428                    min_val[plane] = val;
429                }
430
431                if val > max_val[plane] {
432                    max_val[plane] = val;
433                }
434            }
435        }
436
437        Ok(())
438    }
439
440    /// Finds the center of the box values.
441    pub fn center_box(&self, box_start: usize, box_size: usize, depth: usize) -> Result<Tuple> {
442        let table_len = self.table.len();
443        let tuple_len = if let Some(tuple) = self.table.get(box_start) {
444            tuple.tuple.len()
445        } else {
446            0
447        };
448
449        if box_start > table_len || box_size > table_len || depth > tuple_len {
450            Err(Error::Quant(format!("box_start, box_size, and/or depth is out-of-bounds: table length: {table_len}, box_start: {box_start}, box_size {box_size}, tuple length: {tuple_len}, depth: {depth}")))
451        } else {
452            let mut new_tuple = Tuple::with_capacity(depth);
453
454            for plane in 0..depth {
455                let val = self.table[box_start].tuple[plane];
456                let (mut min_val, mut max_val) = (val, val);
457
458                for tuple in self.table[box_start + 1..box_size].iter() {
459                    let v = tuple.tuple[plane];
460                    min_val = cmp::min(v, min_val);
461                    max_val = cmp::max(v, max_val);
462                }
463
464                new_tuple.push(min_val.saturating_add(max_val).saturating_div(2));
465            }
466
467            Ok(new_tuple)
468        }
469    }
470
471    /// Averages color values in a [ColorMap].
472    pub fn average_colors(&self, box_start: usize, box_size: usize, depth: usize) -> Result<Tuple> {
473        let table_len = self.table.len();
474        if box_start > table_len || box_size > table_len || box_start > box_size {
475            Err(Error::Quant(format!("box_start and/or box_size is out-of-bounds, table length: {table_len}, box start: {box_start}, box size: {box_size}")))
476        } else {
477            let mut new_tuple = Tuple::with_capacity(depth);
478
479            (0..depth).for_each(|plane| {
480                let sum: u32 = self.table[box_start..box_size]
481                    .iter()
482                    .map(|t| t.tuple.get(plane).unwrap_or(&0))
483                    .sum();
484
485                new_tuple.push(sum / box_size as u32);
486            });
487
488            Ok(new_tuple)
489        }
490    }
491
492    /// Averages pixel values in a [ColorMap].
493    pub fn average_pixels(&self, box_start: usize, box_size: usize, depth: usize) -> Result<Tuple> {
494        let table_len = self.table.len();
495        if box_start > table_len || box_size > table_len || box_start > box_size {
496            Err(Error::Quant(format!("box_start and/or box_size is out-of-bounds, table length: {table_len}, box start: {box_start}, box size: {box_size}")))
497        } else {
498            let n: u32 = self.table[box_start..box_size]
499                .iter()
500                .map(|t| t.value)
501                .sum();
502
503            let mut new_tuple = Tuple::with_capacity(depth);
504
505            (0..depth).for_each(|plane| {
506                let sum: u32 = self.table[box_start..box_size]
507                    .iter()
508                    .map(|t| t.tuple.get(plane).unwrap_or(&0).saturating_mul(t.value))
509                    .sum();
510
511                new_tuple.push(sum / n);
512            });
513
514            Ok(new_tuple)
515        }
516    }
517}
518
519#[cfg(test)]
520mod tests {
521    use super::*;
522
523    #[test]
524    fn test_1() -> Result<()> {
525        let min_val = [1u32];
526        let max_val = [2u32];
527
528        assert_eq!(largest_by_luminosity(min_val.as_ref(), max_val.as_ref()), 0);
529
530        // Should return the middle index, middle luminosity multiplier is significantly higher
531        let min_val = [1u32, 2u32, 3u32];
532        let max_val = [2u32, 4u32, 8u32];
533
534        assert_eq!(largest_by_luminosity(min_val.as_ref(), max_val.as_ref()), 1);
535
536        let min_val = [1u32, 3u32, 2u32];
537        let max_val = [2u32, 8u32, 4u32];
538
539        assert_eq!(largest_by_luminosity(min_val.as_ref(), max_val.as_ref()), 1);
540
541        // Should return the last index, 5x max_val is enough to overcome middle luminosity
542        // multiplier
543        let min_val = [1u32, 2u32, 3u32];
544        let max_val = [2u32, 4u32, 20u32];
545
546        assert_eq!(largest_by_luminosity(min_val.as_ref(), max_val.as_ref()), 2);
547
548        let min_val = [3u32, 1u32, 2u32];
549        let max_val = [8u32, 2u32, 4u32];
550
551        assert_eq!(largest_by_luminosity(min_val.as_ref(), max_val.as_ref()), 0);
552
553        // Luminosity only calculate for depth == 3
554        let min_val = [3u32, 1u32, 2u32, 0u32];
555        let max_val = [8u32, 2u32, 4u32, 0u32];
556
557        assert_eq!(largest_by_luminosity(min_val.as_ref(), max_val.as_ref()), 0);
558
559        let min_val = [3u32, 1u32];
560        let max_val = [8u32, 2u32];
561        assert_eq!(largest_by_luminosity(min_val.as_ref(), max_val.as_ref()), 0);
562
563        Ok(())
564    }
565}