imagequant/
pal.rs

1use crate::OrdFloat;
2use arrayvec::ArrayVec;
3use rgb::prelude::*;
4use std::ops::{Deref, DerefMut};
5
6/// 8-bit RGBA in sRGB. This is the only color format *publicly* used by the library.
7pub type RGBA = rgb::Rgba<u8>;
8
9#[allow(clippy::upper_case_acronyms)]
10pub type ARGBF = rgb::Argb<f32>;
11
12pub const INTERNAL_GAMMA: f64 = 0.57;
13pub const LIQ_WEIGHT_A: f32 = 0.625;
14pub const LIQ_WEIGHT_R: f32 = 0.5;
15pub const LIQ_WEIGHT_G: f32 = 1.;
16pub const LIQ_WEIGHT_B: f32 = 0.45;
17
18/// This is a fudge factor - reminder that colors are not in 0..1 range any more
19pub const LIQ_WEIGHT_MSE: f64 = 0.45;
20
21pub const MIN_OPAQUE_A: f32 = 1. / 256. * LIQ_WEIGHT_A;
22pub const MAX_TRANSP_A: f32 = 255. / 256. * LIQ_WEIGHT_A;
23
24/// 4xf32 color using internal gamma.
25///
26/// ARGB layout is important for x86 SIMD.
27/// I've created the newtype wrapper to try a 16-byte alignment, but it didn't improve perf :(
28#[cfg_attr(
29    any(target_arch = "x86_64", all(target_feature = "neon", target_arch = "aarch64")),
30    repr(C, align(16))
31)]
32#[derive(Debug, Copy, Clone, Default, PartialEq)]
33#[allow(non_camel_case_types)]
34pub struct f_pixel(pub ARGBF);
35
36impl f_pixel {
37    #[cfg(not(any(target_arch = "x86_64", all(target_feature = "neon", target_arch = "aarch64"))))]
38    #[inline(always)]
39    pub fn diff(&self, other: &f_pixel) -> f32 {
40        let alphas = other.0.a - self.0.a;
41        let black = self.0 - other.0;
42        let white = ARGBF {
43            a: 0.,
44            r: black.r + alphas,
45            g: black.g + alphas,
46            b: black.b + alphas,
47        };
48        (black.r * black.r).max(white.r * white.r) +
49        (black.g * black.g).max(white.g * white.g) +
50        (black.b * black.b).max(white.b * white.b)
51    }
52
53    #[cfg(all(target_feature = "neon", target_arch = "aarch64"))]
54    #[inline(always)]
55    pub fn diff(&self, other: &Self) -> f32 {
56        unsafe {
57            use std::arch::aarch64::*;
58
59            let px = vld1q_f32((self as *const Self).cast::<f32>());
60            let py = vld1q_f32((other as *const Self).cast::<f32>());
61
62            // y.a - x.a
63            let mut alphas = vsubq_f32(py, px);
64            alphas = vdupq_laneq_f32(alphas, 0); // copy first to all four
65
66            let mut onblack = vsubq_f32(px, py); // x - y
67            let mut onwhite = vaddq_f32(onblack, alphas); // x - y + (y.a - x.a)
68
69            onblack = vmulq_f32(onblack, onblack);
70            onwhite = vmulq_f32(onwhite, onwhite);
71
72            let max = vmaxq_f32(onwhite, onblack);
73
74            let mut max_r = [0.; 4];
75            vst1q_f32(max_r.as_mut_ptr(), max);
76
77            let mut max_gb = [0.; 4];
78            vst1q_f32(max_gb.as_mut_ptr(), vpaddq_f32(max, max));
79
80            // add rgb, not a
81
82            max_r[1] + max_gb[1]
83        }
84    }
85
86    #[cfg(target_arch = "x86_64")]
87    #[inline(always)]
88    pub fn diff(&self, other: &f_pixel) -> f32 {
89        unsafe {
90            use std::arch::x86_64::*;
91
92            let px = _mm_loadu_ps(self as *const f_pixel as *const f32);
93            let py = _mm_loadu_ps(other as *const f_pixel as *const f32);
94
95            // y.a - x.a
96            let mut alphas = _mm_sub_ss(py, px);
97            alphas = _mm_shuffle_ps(alphas, alphas, 0); // copy first to all four
98
99            let mut onblack = _mm_sub_ps(px, py); // x - y
100            let mut onwhite = _mm_add_ps(onblack, alphas); // x - y + (y.a - x.a)
101
102            onblack = _mm_mul_ps(onblack, onblack);
103            onwhite = _mm_mul_ps(onwhite, onwhite);
104            let max = _mm_max_ps(onwhite, onblack);
105
106            // the compiler is better at horizontal add than I am
107            let mut tmp = [0.; 4];
108            _mm_storeu_ps(tmp.as_mut_ptr(), max);
109
110            // add rgb, not a
111            let res = tmp[1] + tmp[2] + tmp[3];
112            res
113        }
114    }
115
116    #[inline]
117    pub(crate) fn to_rgb(self, gamma: f64) -> RGBA {
118        if self.a < MIN_OPAQUE_A {
119            return RGBA::new(0, 0, 0, 0);
120        }
121
122        let r = (LIQ_WEIGHT_A / LIQ_WEIGHT_R) * self.r / self.a;
123        let g = (LIQ_WEIGHT_A / LIQ_WEIGHT_G) * self.g / self.a;
124        let b = (LIQ_WEIGHT_A / LIQ_WEIGHT_B) * self.b / self.a;
125        let a = (256. / LIQ_WEIGHT_A) * self.a;
126
127        let gamma = (gamma / INTERNAL_GAMMA) as f32;
128        debug_assert!(gamma.is_finite());
129
130        // 256, because numbers are in range 1..255.9999… rounded down
131        RGBA {
132            r: (r.powf(gamma) * 256.) as u8,
133            g: (g.powf(gamma) * 256.) as u8,
134            b: (b.powf(gamma) * 256.) as u8,
135            a: a as u8,
136        }
137    }
138
139    pub fn from_rgba(gamma_lut: &[f32; 256], px: RGBA) -> Self {
140        let a = f32::from(px.a) / 255.;
141        Self(ARGBF {
142            a: a * LIQ_WEIGHT_A,
143            r: gamma_lut[px.r as usize] * LIQ_WEIGHT_R * a,
144            g: gamma_lut[px.g as usize] * LIQ_WEIGHT_G * a,
145            b: gamma_lut[px.b as usize] * LIQ_WEIGHT_B * a,
146        })
147    }
148}
149
150impl Deref for f_pixel {
151    type Target = ARGBF;
152
153    #[inline(always)]
154    fn deref(&self) -> &Self::Target {
155        &self.0
156    }
157}
158
159impl DerefMut for f_pixel {
160    #[inline(always)]
161    fn deref_mut(&mut self) -> &mut Self::Target {
162        &mut self.0
163    }
164}
165
166impl From<ARGBF> for f_pixel {
167    #[inline(always)]
168    fn from(x: ARGBF) -> Self {
169        Self(x)
170    }
171}
172
173/// To keep the data dense, `is_fixed` is stuffed into the sign bit
174#[derive(Copy, Clone, Debug)]
175pub(crate) struct PalPop(f32);
176
177impl PalPop {
178    #[inline(always)]
179    pub fn is_fixed(self) -> bool {
180        self.0 < 0.
181    }
182
183    #[must_use]
184    pub fn to_fixed(self) -> Self {
185        if self.0 < 0. {
186            return self;
187        }
188        Self(if self.0 > 0. { -self.0 } else { -1. })
189    }
190
191    #[inline]
192    #[cfg_attr(debug_assertions, track_caller)]
193    pub fn new(popularity: f32) -> Self {
194        debug_assert!(popularity >= 0.);
195        Self(popularity)
196    }
197
198    #[inline(always)]
199    #[must_use]
200    pub fn popularity(self) -> f32 {
201        self.0.abs()
202    }
203}
204
205#[cfg(feature = "large_palettes")]
206pub type PalIndex = u16;
207
208#[cfg(not(feature = "large_palettes"))]
209pub type PalIndex = u8;
210
211/// This could be increased to support > 256 colors in remapping too
212pub type PalIndexRemap = u8;
213pub type PalLen = u16;
214
215/// Palettes are stored on the stack, and really large ones will cause stack overflows
216pub(crate) const MAX_COLORS: usize = if PalIndex::MAX == 255 { 256 } else { 2048 };
217
218/// A palette of premultiplied ARGB 4xf32 colors in internal gamma
219#[derive(Clone)]
220pub(crate) struct PalF {
221    colors: ArrayVec<f_pixel, MAX_COLORS>,
222    pops: ArrayVec<PalPop, MAX_COLORS>,
223}
224
225impl PalF {
226    #[inline]
227    pub fn new() -> Self {
228        debug_assert!(PalIndex::MAX as usize + 1 >= MAX_COLORS);
229        debug_assert!(PalLen::MAX as usize >= MAX_COLORS);
230        Self {
231            colors: ArrayVec::default(),
232            pops: ArrayVec::default(),
233        }
234    }
235
236    #[inline(always)]
237    pub fn push(&mut self, color: f_pixel, popularity: PalPop) {
238        self.pops.push(popularity);
239        self.colors.push(color);
240    }
241
242    pub fn set(&mut self, idx: usize, color: f_pixel, popularity: PalPop) {
243        debug_assert!(idx < self.colors.len() && idx < self.pops.len());
244
245        if let Some(pops_idx) = self.pops.get_mut(idx) {
246            *pops_idx = popularity;
247        }
248        if let Some(colors_idx) = self.colors.get_mut(idx) {
249            *colors_idx = color;
250        }
251    }
252
253    #[inline(always)]
254    pub fn as_slice(&self) -> &[f_pixel] {
255        &self.colors
256    }
257
258    #[inline(always)]
259    pub fn pop_as_slice(&self) -> &[PalPop] {
260        &self.pops
261    }
262
263    // this is max colors allowed by the user, not just max in the current (candidate/low-quality) palette
264    pub(crate) fn with_fixed_colors(mut self, max_colors: PalLen, fixed_colors: &[f_pixel]) -> Self {
265        if fixed_colors.is_empty() {
266            return self;
267        }
268
269        // if using low quality, there's a chance mediancut won't create enough colors in the palette
270        let max_fixed_colors = fixed_colors.len().min(max_colors as usize);
271        if self.len() < max_fixed_colors {
272            let needs_extra = max_fixed_colors - self.len();
273            self.colors.extend(fixed_colors.iter().copied().take(needs_extra));
274            self.pops.extend(std::iter::repeat(PalPop::new(0.)).take(needs_extra));
275            debug_assert_eq!(self.len(), max_fixed_colors);
276        }
277
278        // since the fixed colors were in the histogram, expect them to be in the palette,
279        // and change closest existing one to be exact fixed
280        for (i, fixed_color) in fixed_colors.iter().enumerate().take(self.len()) {
281            let (best_idx, _) = self.colors.iter().enumerate().skip(i).min_by_key(|(_, pal_color)| {
282                // not using Nearest, because creation of the index may take longer than naive search once
283                OrdFloat::new(pal_color.diff(fixed_color))
284            }).expect("logic bug in fixed colors, please report a bug");
285            debug_assert!(best_idx >= i);
286            self.swap(i, best_idx);
287            self.set(i, *fixed_color, self.pops[i].to_fixed());
288        }
289
290        debug_assert!(self.colors.iter().zip(fixed_colors).all(|(p, f)| p == f));
291        debug_assert!(self.pops.iter().take(fixed_colors.len()).all(|pop| pop.is_fixed()));
292        self
293    }
294
295    #[inline(always)]
296    pub(crate) fn len(&self) -> usize {
297        debug_assert_eq!(self.colors.len(), self.pops.len());
298        self.colors.len()
299    }
300
301    #[inline(always)]
302    pub fn iter_mut(&mut self) -> impl Iterator<Item = (&mut f_pixel, &mut PalPop)> {
303        let c = &mut self.colors[..];
304        let pop = &mut self.pops[..c.len()];
305        c.iter_mut().zip(pop)
306    }
307
308    #[cfg_attr(debug_assertions, track_caller)]
309    pub(crate) fn swap(&mut self, a: usize, b: usize) {
310        self.colors.swap(a, b);
311        self.pops.swap(a, b);
312    }
313
314    /// Also rounds the input pal
315    pub(crate) fn init_int_palette(&mut self, int_palette: &mut Palette, gamma: f64, posterize: u8) {
316        let lut = gamma_lut(gamma);
317        for ((f_color, f_pop), int_pal) in self.iter_mut().zip(&mut int_palette.entries) {
318            let mut px = f_color.to_rgb(gamma)
319                .map(move |c| posterize_channel(c, posterize));
320            *f_color = f_pixel::from_rgba(&lut, px);
321            if px.a == 0 && !f_pop.is_fixed() {
322                px.r = 71u8;
323                px.g = 112u8;
324                px.b = 76u8;
325            }
326            *int_pal = px;
327        }
328        int_palette.count = self.len() as _;
329    }
330}
331
332#[inline]
333const fn posterize_channel(color: u8, bits: u8) -> u8 {
334    if bits == 0 {
335        color
336    } else {
337        (color & !((1 << bits) - 1)) | (color >> (8 - bits))
338    }
339}
340
341#[inline(always)]
342pub fn gamma_lut(gamma: f64) -> [f32; 256] {
343    debug_assert!(gamma > 0.);
344    let mut tmp = [0.; 256];
345    for (i, t) in tmp.iter_mut().enumerate() {
346        *t = ((i as f32) / 255.).powf((INTERNAL_GAMMA / gamma) as f32);
347    }
348    tmp
349}
350
351/// Not used in the Rust API.
352/// RGBA colors obtained from [`QuantizationResult`](crate::QuantizationResult)
353#[repr(C)]
354#[derive(Clone)]
355pub struct Palette {
356    /// Number of used colors in the `entries`
357    pub count: std::os::raw::c_uint,
358    /// The colors, up to `count`
359    pub entries: [RGBA; MAX_COLORS],
360}
361
362impl std::ops::Deref for Palette {
363    type Target = [RGBA];
364
365    #[inline(always)]
366    fn deref(&self) -> &Self::Target {
367        self.as_slice()
368    }
369}
370
371impl std::ops::DerefMut for Palette {
372    #[inline(always)]
373    fn deref_mut(&mut self) -> &mut Self::Target {
374        self.as_mut_slice()
375    }
376}
377
378impl Palette {
379    /// Palette colors
380    #[inline(always)]
381    #[must_use]
382    pub fn as_slice(&self) -> &[RGBA] {
383        &self.entries[..self.count as usize]
384    }
385
386    #[inline(always)]
387    pub(crate) fn as_mut_slice(&mut self) -> &mut [RGBA] {
388        &mut self.entries[..self.count as usize]
389    }
390}
391
392#[test]
393fn diff_test() {
394    let a = f_pixel(ARGBF {a: 1., r: 0.2, g: 0.3, b: 0.5});
395    let b = f_pixel(ARGBF {a: 1., r: 0.3, g: 0.3, b: 0.5});
396    let c = f_pixel(ARGBF {a: 1., r: 1., g: 0.3, b: 0.5});
397    let d = f_pixel(ARGBF {a: 0., r: 1., g: 0.3, b: 0.5});
398    assert!(a.diff(&b) < b.diff(&c));
399    assert!(c.diff(&b) < c.diff(&d));
400
401    let a = f_pixel(ARGBF {a: 1., b: 0.2, r: 0.3, g: 0.5});
402    let b = f_pixel(ARGBF {a: 1., b: 0.3, r: 0.3, g: 0.5});
403    let c = f_pixel(ARGBF {a: 1., b: 1., r: 0.3, g: 0.5});
404    let d = f_pixel(ARGBF {a: 0., b: 1., r: 0.3, g: 0.5});
405    assert!(a.diff(&b) < b.diff(&c));
406    assert!(c.diff(&b) < c.diff(&d));
407
408    let a = f_pixel(ARGBF {a: 1., g: 0.2, b: 0.3, r: 0.5});
409    let b = f_pixel(ARGBF {a: 1., g: 0.3, b: 0.3, r: 0.5});
410    let c = f_pixel(ARGBF {a: 1., g: 1., b: 0.3, r: 0.5});
411    let d = f_pixel(ARGBF {a: 0., g: 1., b: 0.3, r: 0.5});
412    assert!(a.diff(&b) < b.diff(&c));
413    assert!(c.diff(&b) < c.diff(&d));
414}
415
416#[test]
417fn pal_test() {
418    let mut p = PalF::new();
419    let gamma = gamma_lut(0.45455);
420    for i in 0..=255u8 {
421        let rgba = RGBA::new(i, i, i, 100 + i / 2);
422        p.push(f_pixel::from_rgba(&gamma, rgba), PalPop::new(1.));
423        assert_eq!(i as usize + 1, p.len());
424        assert_eq!(i as usize + 1, p.pop_as_slice().len());
425        assert_eq!(i as usize + 1, p.as_slice().len());
426        assert_eq!(i as usize + 1, p.colors.len());
427        assert_eq!(i as usize + 1, p.pops.len());
428        assert_eq!(i as usize + 1, p.iter_mut().count());
429    }
430
431    let mut int_pal = Palette {
432        count: 0,
433        entries: [RGBA::default(); MAX_COLORS],
434    };
435    p.init_int_palette(&mut int_pal, 0.45455, 0);
436
437    for i in 0..=255u8 {
438        let rgba = p.as_slice()[i as usize].to_rgb(0.45455);
439        assert_eq!(rgba, RGBA::new(i, i, i, 100 + i / 2));
440        assert_eq!(int_pal[i as usize], RGBA::new(i, i, i, 100 + i / 2));
441    }
442}
443
444#[test]
445#[cfg(feature = "large_palettes")]
446fn largepal() {
447    let gamma = gamma_lut(0.5);
448    let mut p = PalF::new();
449    for i in 0..1000 {
450        let rgba = RGBA::new(i as u8, (i/2) as u8, (i/4) as u8, 255);
451        p.push(f_pixel::from_rgba(&gamma, rgba), PalPop::new(1.));
452    }
453}