color_thief/
lib.rs

1// Copyright 2017, Reizner Evgeniy <razrfalcon@gmail.com>.
2// See the COPYRIGHT file at the top-level directory of this distribution.
3// Licensed under the MIT license, see the LICENSE file or <http://opensource.org/licenses/MIT>
4
5/*!
6*color-thief-rs* is a [color-thief](https://github.com/lokesh/color-thief)
7algorithm reimplementation in Rust.
8
9The implementation itself is a heavily modified
10[Swift version](https://github.com/yamoridon/ColorThiefSwift) of the same algorithm.
11*/
12
13#![forbid(unsafe_code)]
14#![warn(missing_docs)]
15
16extern crate rgb;
17
18use std::cmp;
19use std::fmt;
20use std::error;
21use std::u8;
22
23pub use rgb::RGB8 as Color;
24
25const SIGNAL_BITS: i32              = 5; // Use only upper 5 bits of 8 bits.
26const RIGHT_SHIFT: i32              = 8 - SIGNAL_BITS;
27const MULTIPLIER: i32               = 1 << RIGHT_SHIFT;
28const MULTIPLIER_64: f64            = MULTIPLIER as f64;
29const HISTOGRAM_SIZE: usize         = 1 << (3 * SIGNAL_BITS);
30const VBOX_LENGTH: usize            = 1 << SIGNAL_BITS;
31const FRACTION_BY_POPULATION: f64   = 0.75;
32const MAX_ITERATIONS: i32           = 1000;
33
34/// Represent a color format of an underlying image data.
35#[allow(missing_docs)]
36#[derive(Clone,Copy,PartialEq,Debug)]
37pub enum ColorFormat {
38    Rgb,
39    Rgba,
40    Argb,
41    Bgr,
42    Bgra,
43}
44
45/// List of all errors.
46#[allow(missing_docs)]
47#[derive(Clone,Copy,PartialEq,Debug)]
48pub enum Error {
49    InvalidVBox,
50    VBoxCutFailed,
51}
52
53impl fmt::Display for Error {
54    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
55        let msg = match *self {
56            Error::InvalidVBox => "an invalid VBox",
57            Error::VBoxCutFailed => "failed to cut a VBox",
58        };
59
60        write!(f, "{}", msg)
61    }
62}
63
64impl error::Error for Error {}
65
66/// Returns a representative color palette of an image.
67///
68/// * `pixels` - A raw image data.
69///
70///   We do not use any existing image representing crate for a better portability.
71/// * `color_format` - Represent a color format of an underlying image data.
72/// * `quality` - Quality of an output colors.
73///
74///   Basically, a step in pixels to improve performance.
75///
76///   Range: 1..10.
77/// * `max_colors` - A number of colors in the output palette.
78///   Actual colors count can be lower depending on the image.
79///
80///   Range: 2..255.
81pub fn get_palette(
82    pixels: &[u8],
83    color_format: ColorFormat,
84    quality: u8,
85    max_colors: u8,
86) -> Result<Vec<Color>, Error> {
87    assert!(quality > 0 && quality <= 10);
88    assert!(max_colors > 1);
89
90    quantize(&pixels, color_format, quality, max_colors)
91}
92
93enum ColorChannel {
94    Red,
95    Green,
96    Blue,
97}
98
99#[derive(Clone)]
100struct VBox {
101    r_min: u8,
102    r_max: u8,
103    g_min: u8,
104    g_max: u8,
105    b_min: u8,
106    b_max: u8,
107    average: Color,
108    volume: i32,
109    count: i32,
110}
111
112impl VBox {
113    fn new(
114        r_min: u8, r_max: u8,
115        g_min: u8, g_max: u8,
116        b_min: u8, b_max: u8,
117    ) -> VBox {
118        VBox {
119            r_min: r_min,
120            r_max: r_max,
121            g_min: g_min,
122            g_max: g_max,
123            b_min: b_min,
124            b_max: b_max,
125            average: Color::new(0, 0, 0),
126            volume: 0,
127            count: 0,
128        }
129
130        // `recalc()` should be called right after `new()`.
131    }
132
133    fn recalc(&mut self, histogram: &[i32]) {
134        self.average = self.calc_average(histogram);
135        self.count = self.calc_count(histogram);
136        self.volume = self.calc_volume();
137    }
138
139    /// Get 3 dimensional volume of the color space.
140    fn calc_volume(&self) -> i32 {
141          (self.r_max as i32 - self.r_min as i32 + 1)
142        * (self.g_max as i32 - self.g_min as i32 + 1)
143        * (self.b_max as i32 - self.b_min as i32 + 1)
144    }
145
146    /// Get total count of histogram samples.
147    fn calc_count(&self, histogram: &[i32]) -> i32 {
148        let mut count = 0;
149        for i in self.r_min..(self.r_max + 1) {
150            for j in self.g_min..(self.g_max + 1) {
151                for k in self.b_min..(self.b_max + 1) {
152                    let index = make_color_index_of(i, j, k);
153                    count += histogram[index];
154                }
155            }
156        }
157
158        count
159    }
160
161    fn calc_average(&self, histogram: &[i32]) -> Color {
162        let mut ntot = 0;
163
164        let mut r_sum = 0;
165        let mut g_sum = 0;
166        let mut b_sum = 0;
167
168        for i in self.r_min..(self.r_max + 1) {
169            for j in self.g_min..(self.g_max + 1) {
170                for k in self.b_min..(self.b_max + 1) {
171                    let index = make_color_index_of(i, j, k);
172                    let hval = histogram[index] as f64;
173                    ntot += hval as i32;
174                    r_sum += (hval * (i as f64 + 0.5) * MULTIPLIER_64) as i32;
175                    g_sum += (hval * (j as f64 + 0.5) * MULTIPLIER_64) as i32;
176                    b_sum += (hval * (k as f64 + 0.5) * MULTIPLIER_64) as i32;
177                }
178            }
179        }
180
181        if ntot > 0 {
182            let r = r_sum / ntot;
183            let g = g_sum / ntot;
184            let b = b_sum / ntot;
185            Color::new(r as u8, g as u8, b as u8)
186        } else {
187            let r = MULTIPLIER * (self.r_min as i32 + self.r_max as i32 + 1) / 2;
188            let g = MULTIPLIER * (self.g_min as i32 + self.g_max as i32 + 1) / 2;
189            let b = MULTIPLIER * (self.b_min as i32 + self.b_max as i32 + 1) / 2;
190            Color::new(cmp::min(r, 255) as u8,
191                       cmp::min(g, 255) as u8,
192                       cmp::min(b, 255) as u8)
193        }
194    }
195
196    fn widest_color_channel(&self) -> ColorChannel {
197        let r_width = self.r_max - self.r_min;
198        let g_width = self.g_max - self.g_min;
199        let b_width = self.b_max - self.b_min;
200
201        let max = cmp::max(cmp::max(r_width, g_width), b_width);
202
203        if max == r_width {
204            ColorChannel::Red
205        } else if max == g_width {
206            ColorChannel::Green
207        } else {
208            ColorChannel::Blue
209        }
210    }
211}
212
213fn make_histogram_and_vbox(
214    pixels: &[u8],
215    color_format: ColorFormat,
216    step: u8,
217) -> (VBox, Vec<i32>) {
218    let mut histogram: Vec<i32> = (0..HISTOGRAM_SIZE).map(|_| 0).collect();
219
220    let mut r_min = u8::MAX;
221    let mut r_max = u8::MIN;
222    let mut g_min = u8::MAX;
223    let mut g_max = u8::MIN;
224    let mut b_min = u8::MAX;
225    let mut b_max = u8::MIN;
226
227    let colors_count = match color_format {
228        ColorFormat::Rgb => 3,
229        ColorFormat::Rgba => 4,
230        ColorFormat::Argb => 4,
231        ColorFormat::Bgr => 3,
232        ColorFormat::Bgra => 4,
233    };
234
235    let pixel_count = pixels.len() / colors_count;
236    let mut i = 0;
237    while i < pixel_count {
238        let pos = i * colors_count;
239
240        let (r, g, b, a) = color_parts(pixels, color_format, pos);
241
242        i += colors_count * step as usize;
243
244        // If pixel is mostly opaque or white.
245        if a < 125 || (r > 250 && g > 250 && b > 250) {
246            continue;
247        }
248
249        let shifted_r = r >> RIGHT_SHIFT as u8;
250        let shifted_b = b >> RIGHT_SHIFT as u8;
251        let shifted_g = g >> RIGHT_SHIFT as u8;
252
253        r_min = cmp::min(r_min, shifted_r);
254        r_max = cmp::max(r_max, shifted_r);
255        g_min = cmp::min(g_min, shifted_g);
256        g_max = cmp::max(g_max, shifted_g);
257        b_min = cmp::min(b_min, shifted_b);
258        b_max = cmp::max(b_max, shifted_b);
259
260        // Increment histogram.
261        let index = make_color_index_of(shifted_r, shifted_g, shifted_b);
262        histogram[index] += 1;
263    }
264
265    let mut vbox = VBox::new(r_min, r_max, g_min, g_max, b_min, b_max);
266    vbox.recalc(&histogram);
267
268    (vbox, histogram)
269}
270
271
272/// Extracts r, g, b, a color parts.
273fn color_parts(
274    pixels: &[u8],
275    color_format: ColorFormat,
276    pos: usize,
277) -> (u8, u8, u8, u8) {
278    match color_format {
279        ColorFormat::Rgb => {
280            (pixels[pos + 0],
281             pixels[pos + 1],
282             pixels[pos + 2],
283             255)
284        }
285        ColorFormat::Rgba => {
286            (pixels[pos + 0],
287             pixels[pos + 1],
288             pixels[pos + 2],
289             pixels[pos + 3])
290        }
291        ColorFormat::Argb => {
292            (pixels[pos + 1],
293             pixels[pos + 2],
294             pixels[pos + 3],
295             pixels[pos + 0])
296        },
297        ColorFormat::Bgr => {
298            (pixels[pos + 2],
299             pixels[pos + 1],
300             pixels[pos + 0],
301             255)
302        }
303        ColorFormat::Bgra => {
304            (pixels[pos + 2],
305             pixels[pos + 1],
306             pixels[pos + 0],
307             pixels[pos + 3])
308        }
309    }
310}
311
312fn apply_median_cut(
313    histogram: &[i32],
314    vbox: &mut VBox,
315) -> Result<(VBox, Option<VBox>), Error> {
316    if vbox.count == 0 {
317        return Err(Error::InvalidVBox);
318    }
319
320    // Only one pixel, no split.
321    if vbox.count == 1 {
322        return Ok((vbox.clone(), None));
323    }
324
325    // Find the partial sum arrays along the selected axis.
326    let mut total = 0;
327    let mut partial_sum: Vec<i32> = (0..VBOX_LENGTH).map(|_| -1).collect();
328
329    let axis = vbox.widest_color_channel();
330    match axis {
331        ColorChannel::Red => {
332            for i in vbox.r_min..(vbox.r_max + 1) {
333                let mut sum = 0;
334                for j in vbox.g_min..(vbox.g_max + 1) {
335                    for k in vbox.b_min..(vbox.b_max + 1) {
336                        let index = make_color_index_of(i, j, k);
337                        sum += histogram[index];
338                    }
339                }
340                total += sum;
341                partial_sum[i as usize] = total;
342            }
343        }
344        ColorChannel::Green => {
345            for i in vbox.g_min..(vbox.g_max + 1) {
346                let mut sum = 0;
347                for j in vbox.r_min..(vbox.r_max + 1) {
348                    for k in vbox.b_min..(vbox.b_max + 1) {
349                        let index = make_color_index_of(j, i, k);
350                        sum += histogram[index];
351                    }
352                }
353                total += sum;
354                partial_sum[i as usize] = total;
355            }
356        }
357        ColorChannel::Blue => {
358            for i in vbox.b_min..(vbox.b_max + 1) {
359                let mut sum = 0;
360                for j in vbox.r_min..(vbox.r_max + 1) {
361                    for k in vbox.g_min..(vbox.g_max + 1) {
362                        let index = make_color_index_of(j, k, i);
363                        sum += histogram[index];
364                    }
365                }
366                total += sum;
367                partial_sum[i as usize] = total;
368            }
369        }
370    }
371
372    let mut look_ahead_sum: Vec<i32> = (0..VBOX_LENGTH).map(|_| -1).collect();
373    for (i, sum) in partial_sum.iter().enumerate().filter(|&(_, sum)| *sum != -1) {
374        look_ahead_sum[i] = total - sum;
375    }
376
377    cut(axis, vbox, histogram, &partial_sum, &look_ahead_sum, total)
378}
379
380fn cut(
381    axis: ColorChannel,
382    vbox: &VBox,
383    histogram: &[i32],
384    partial_sum: &[i32],
385    look_ahead_sum: &[i32],
386    total: i32,
387) -> Result<(VBox, Option<VBox>), Error> {
388    let (vbox_min, vbox_max) = match axis {
389        ColorChannel::Red =>   (vbox.r_min as i32, vbox.r_max as i32),
390        ColorChannel::Green => (vbox.g_min as i32, vbox.g_max as i32),
391        ColorChannel::Blue =>  (vbox.b_min as i32, vbox.b_max as i32),
392    };
393
394    for i in vbox_min..vbox_max + 1 {
395        if partial_sum[i as usize] <= total / 2 {
396            continue;
397        }
398
399        let mut vbox1 = vbox.clone();
400        let mut vbox2 = vbox.clone();
401
402        let left = i - vbox_min;
403        let right = vbox_max - i;
404
405        let mut d2 = if left <= right {
406            cmp::min(vbox_max - 1, i + right / 2)
407        } else {
408            // 2.0 and cast to int is necessary to have the same
409            // behavior as in JavaScript.
410            cmp::max(vbox_min, ((i - 1) as f64 - left as f64 / 2.0) as i32)
411        };
412
413        // Avoid 0-count.
414        while d2 < 0 || partial_sum[d2 as usize] <= 0 {
415            d2 += 1;
416        }
417        let mut count2 = look_ahead_sum[d2 as usize];
418        while count2 == 0 && d2 > 0 && partial_sum[d2 as usize - 1] > 0 {
419            d2 -= 1;
420            count2 = look_ahead_sum[d2 as usize];
421        }
422
423        // Set dimensions.
424        match axis {
425            ColorChannel::Red => {
426                vbox1.r_max = d2 as u8;
427                vbox2.r_min = (d2 + 1) as u8;
428            }
429            ColorChannel::Green => {
430                vbox1.g_max = d2 as u8;
431                vbox2.g_min = (d2 + 1) as u8;
432            }
433            ColorChannel::Blue => {
434                vbox1.b_max = d2 as u8;
435                vbox2.b_min = (d2 + 1) as u8;
436            }
437        }
438
439        vbox1.recalc(histogram);
440        vbox2.recalc(histogram);
441
442        return Ok((vbox1, Some(vbox2)));
443    }
444
445    Err(Error::VBoxCutFailed)
446}
447
448fn quantize(
449    pixels: &[u8],
450    color_format: ColorFormat,
451    quality: u8,
452    max_colors: u8,
453) -> Result<Vec<Color>, Error> {
454    // Get the histogram and the beginning vbox from the colors.
455    let (vbox, histogram) = make_histogram_and_vbox(pixels, color_format, quality);
456
457    // Priority queue.
458    let mut pq = vec![vbox.clone()];
459
460    // Round up to have the same behavior as in JavaScript
461    let target = (FRACTION_BY_POPULATION * max_colors as f64).ceil() as u8;
462
463    // First set of colors, sorted by population.
464    iterate(&mut pq, compare_by_count, target, &histogram)?;
465
466    // Re-sort by the product of pixel occupancy times the size in color space.
467    pq.sort_by(compare_by_product);
468
469    // next set - generate the median cuts using the (npix * vol) sorting.
470    let len = pq.len() as u8;
471    iterate(&mut pq, compare_by_product, max_colors - len, &histogram)?;
472
473    // Reverse to put the highest elements first into the color map.
474    pq.reverse();
475
476    // Keep at most `max_colors` in the resulting vector.
477    let mut colors: Vec<Color> = pq.iter().map(|v| v.average).collect();
478    colors.truncate(max_colors as usize);
479
480    Ok(colors)
481}
482
483// Inner function to do the iteration.
484fn iterate<P>(
485    queue: &mut Vec<VBox>,
486    comparator: P,
487    target: u8,
488    histogram: &[i32],
489) -> Result<(), Error>
490    where P: FnMut(&VBox, &VBox) -> cmp::Ordering + Copy
491{
492    let mut color = 1;
493
494    for _ in 0..MAX_ITERATIONS {
495        if let Some(mut vbox) = queue.last().cloned() {
496            if vbox.count == 0 {
497                queue.sort_by(comparator);
498                continue;
499            }
500            queue.pop();
501
502            // Do the cut.
503            let vboxes = apply_median_cut(histogram, &mut vbox)?;
504            queue.push(vboxes.0.clone());
505            if let Some(ref vb) = vboxes.1 {
506                queue.push(vb.clone());
507                color += 1;
508            }
509
510            queue.sort_by(comparator);
511
512            if color >= target {
513               break;
514            }
515        }
516    }
517
518    Ok(())
519}
520
521fn compare_by_count(a: &VBox, b: &VBox) -> cmp::Ordering {
522    a.count.cmp(&b.count)
523}
524
525fn compare_by_product(a: &VBox, b: &VBox) -> cmp::Ordering {
526    if a.count == b.count {
527        // If count is 0 for both (or the same), sort by volume.
528        a.volume.cmp(&b.volume)
529    } else {
530        // Otherwise sort by products.
531        let a_product = a.count as i64 * a.volume as i64;
532        let b_product = b.count as i64 * b.volume as i64;
533        a_product.cmp(&b_product)
534    }
535}
536
537/// Get reduced-space color index for a pixel.
538fn make_color_index_of(red: u8, green: u8, blue: u8) -> usize {
539    (   ((red as i32) << (2 * SIGNAL_BITS))
540      + ((green as i32) << SIGNAL_BITS)
541      +   blue as i32
542    ) as usize
543}