palette_extract/mmcq_impl/
mod.rs

1mod config;
2mod histogram;
3mod pixel_encoding;
4mod types;
5mod util;
6mod vbox;
7
8pub use types::Color;
9pub use pixel_encoding::PixelEncoding;
10
11use std::cmp::{self, Ordering};
12
13use histogram::create_histogram_and_vbox;
14use config::{FRACTION_BY_POPULATION, MAX_ITERATIONS, VBOX_LENGTH};
15use util::color_index_from;
16use vbox::VBox;
17use types::ColorChannel;
18
19pub fn extract_colors(
20    pixels: &[u8],
21    encoding: PixelEncoding,
22    quality: u8,
23    max_colors: u8,
24    ignore_white: bool,
25) -> Vec<Color> {
26    let vbox = create_histogram_and_vbox(
27        pixels,
28        encoding,
29        quality,
30        ignore_white,
31    );
32
33    // priority queue
34    let mut pq = vec![vbox];
35
36    // Round up to have the same behaviour as in JavaScript
37    let target = (FRACTION_BY_POPULATION * max_colors as f32).ceil() as u32;
38
39    iterate(&mut pq, sort_by_count, target);
40
41    pq.sort_by(sort_by_product);
42
43    let len_before = pq.len() as u32;
44
45    iterate(&mut pq, sort_by_product, max_colors as u32 - len_before);
46
47    pq.reverse();
48
49    pq.iter().map(|v| v.get_average()).collect()
50}
51
52fn apply_median_cut(vbox: VBox) -> Vec<VBox> {
53    if vbox.get_count() == 0 {
54        return vec![];
55    }
56
57    // only one pixel, no split
58    if vbox.get_count() == 1 {
59        return vec![vbox];
60    }
61
62    let histogram = &vbox.histogram;
63
64    // Find the partial sum arrays along the selected axis.
65    let mut total: u32 = 0;
66    let mut partial_sum: Vec<i32> = vec![-1; VBOX_LENGTH.into()]; // -1 = not set / 0 = 0
67    let axis = vbox.widest_color_channel();
68
69    match axis {
70        ColorChannel::R => {
71            for r in vbox.r_range() {
72                let mut sum: u32 = 0;
73                for g in vbox.g_range() {
74                    for b in vbox.b_range() {
75                        let index = color_index_from(r, g, b);
76                        sum += histogram[index as usize];
77                    }
78                }
79                total += sum;
80                partial_sum[r as usize] = total as i32;
81            }
82        }
83        ColorChannel::G => {
84            for g in vbox.g_range() {
85                let mut sum: u32 = 0;
86                for r in vbox.r_range() {
87                    for b in vbox.b_range() {
88                        let index = color_index_from(r, g, b);
89                        sum += histogram[index as usize];
90                    }
91                }
92                total += sum;
93                partial_sum[g as usize] = total as i32;
94            }
95        }
96        ColorChannel::B => {
97            for b in vbox.b_range() {
98                let mut sum: u32 = 0;
99                for r in vbox.r_range() {
100                    for g in vbox.g_range() {
101                        let index = color_index_from(r, g, b);
102                        sum += histogram[index as usize];
103                    }
104                }
105                total += sum;
106                partial_sum[b as usize] = total as i32;
107            }
108        }
109    }
110
111    let mut look_ahead_sum: Vec<i32> = vec![-1; VBOX_LENGTH.into()]; // -1 = not set / 0 = 0
112    for (i, sum) in partial_sum.iter().enumerate().filter(|(_, &sum)| sum != -1) {
113        look_ahead_sum[i] = total as i32 - sum
114    }
115
116    return cut(axis, &vbox, &partial_sum, &look_ahead_sum, total);
117}
118
119fn cut(
120    axis: ColorChannel,
121    vbox: &VBox,
122    partial_sum: &[i32],
123    look_ahead_sum: &[i32],
124    total: u32,
125) -> Vec<VBox> {
126    let vbox_min: i32;
127    let vbox_max: i32;
128
129    match axis {
130        ColorChannel::R => {
131            vbox_min = vbox.get_r_min().into();
132            vbox_max = vbox.get_r_max().into();
133        }
134        ColorChannel::G => {
135            vbox_min = vbox.get_g_min().into();
136            vbox_max = vbox.get_g_max().into();
137        }
138        ColorChannel::B => {
139            vbox_min = vbox.get_b_min().into();
140            vbox_max = vbox.get_b_max().into();
141        }
142    }
143
144    for l in (vbox_min..(vbox_max + 1)).filter(|&i| partial_sum[i as usize] > (total / 2) as i32) {
145        let mut vbox1 = VBox::new_from(&vbox);
146        let mut vbox2 = VBox::new_from(&vbox);
147
148        let left = l - vbox_min;
149        let right = vbox_max - l;
150
151        let mut d2 = if left <= right {
152            cmp::min(vbox_max - 1, l + right / 2)
153        } else {
154            // 2.0 and cast to int is necessary to have the same
155            // behaviour as in JavaScript
156            cmp::max(vbox_min, (((l - 1) as f32) - (left as f32 / 2.0)) as i32)
157        };
158
159        while d2 < 0 || partial_sum[d2 as usize] <= 0 {
160            d2 += 1;
161        }
162
163        let mut count2: i32 = look_ahead_sum[d2 as usize];
164        while count2 == 0 && d2 > 0 && partial_sum[(d2 - 1) as usize] > 0 {
165            d2 -= 1;
166            count2 = look_ahead_sum[d2 as usize];
167        }
168
169        vbox1.set_max(d2 as u8, &axis);
170        vbox2.set_min(d2 as u8 + 1, &axis);
171
172        return vec![vbox1, vbox2];
173    }
174
175    panic!("VBox can't be cut")
176}
177
178fn sort_by_count(l: &VBox, r: &VBox) -> Ordering {
179    l.get_count().cmp(&r.get_count())
180}
181
182fn sort_by_product(a: &VBox, b: &VBox) -> Ordering {
183    let a_count = a.get_count();
184    let b_count = b.get_count();
185    let a_volume = a.get_volume();
186    let b_volume = b.get_volume();
187
188    if a_count == b_count {
189        // If count is 0 for both (or the same), sort by volume
190        return a_volume.cmp(&b_volume);
191    } else {
192        // Otherwise sort by products
193        let a_product = a_count as u64 * a_volume as u64;
194        let b_product = b_count as u64 * b_volume as u64;
195        return a_product.cmp(&b_product);
196    }
197}
198
199fn iterate(queue: &mut Vec<VBox>, comp: fn(&VBox, &VBox) -> Ordering, target: u32) {
200    let mut color = 1;
201
202    for _ in 0..MAX_ITERATIONS {
203        let last_item = match queue.last() {
204            Some(v) => v,
205            None => return,
206        };
207
208        if last_item.get_count() == 0 {
209            queue.sort_by(comp);
210            continue;
211        }
212        let vbox = queue.pop().unwrap();
213        let mut new_boxes = apply_median_cut(vbox);
214        queue.push(new_boxes.remove(0));
215        if new_boxes.len() == 1 {
216            queue.push(new_boxes.remove(0));
217            color += 1
218        }
219        queue.sort_by(comp);
220
221        if color >= target {
222            return;
223        }
224    }
225}