Skip to main content

agg_rust/
gradient_lut.rs

1//! Gradient color lookup table.
2//!
3//! Port of `agg_gradient_lut.h` — builds a LUT (lookup table) from SVG-style
4//! color stops. Used by `SpanGradient` to map gradient distances to colors.
5
6use crate::basics::uround;
7use crate::color::Rgba8;
8use crate::dda_line::DdaLineInterpolator;
9
10// ============================================================================
11// ColorFunction trait
12// ============================================================================
13
14/// Trait for color lookup functions used by span_gradient.
15///
16/// Provides indexed access to a color palette of known size.
17pub trait ColorFunction {
18    type Color;
19
20    fn size(&self) -> usize;
21    fn get(&self, index: usize) -> Self::Color;
22}
23
24// ============================================================================
25// ColorInterpolator — generic version using gradient() method
26// ============================================================================
27
28/// Generic color interpolator using the color type's `gradient()` method.
29///
30/// Port of C++ `color_interpolator<ColorT>` (generic template).
31struct ColorInterpolatorGeneric<C> {
32    c1: C,
33    c2: C,
34    len: u32,
35    count: u32,
36}
37
38impl<C: Clone> ColorInterpolatorGeneric<C> {
39    fn new(c1: &C, c2: &C, len: u32) -> Self {
40        Self {
41            c1: c1.clone(),
42            c2: c2.clone(),
43            len,
44            count: 0,
45        }
46    }
47
48    fn inc(&mut self) {
49        self.count += 1;
50    }
51}
52
53impl ColorInterpolatorGeneric<Rgba8> {
54    fn color(&self) -> Rgba8 {
55        self.c1
56            .gradient(&self.c2, self.count as f64 / self.len as f64)
57    }
58}
59
60// ============================================================================
61// ColorInterpolatorRgba8 — fast DDA specialization for Rgba8
62// ============================================================================
63
64/// Fast RGBA8 color interpolator using 14-bit DDA interpolation.
65///
66/// Port of C++ `color_interpolator<rgba8>` specialization.
67struct ColorInterpolatorRgba8 {
68    r: DdaLineInterpolator<14, 0>,
69    g: DdaLineInterpolator<14, 0>,
70    b: DdaLineInterpolator<14, 0>,
71    a: DdaLineInterpolator<14, 0>,
72}
73
74impl ColorInterpolatorRgba8 {
75    fn new(c1: &Rgba8, c2: &Rgba8, len: u32) -> Self {
76        Self {
77            r: DdaLineInterpolator::new(c1.r as i32, c2.r as i32, len),
78            g: DdaLineInterpolator::new(c1.g as i32, c2.g as i32, len),
79            b: DdaLineInterpolator::new(c1.b as i32, c2.b as i32, len),
80            a: DdaLineInterpolator::new(c1.a as i32, c2.a as i32, len),
81        }
82    }
83
84    fn inc(&mut self) {
85        self.r.inc();
86        self.g.inc();
87        self.b.inc();
88        self.a.inc();
89    }
90
91    fn color(&self) -> Rgba8 {
92        Rgba8::new(
93            self.r.y() as u32,
94            self.g.y() as u32,
95            self.b.y() as u32,
96            self.a.y() as u32,
97        )
98    }
99}
100
101// ============================================================================
102// GradientLut
103// ============================================================================
104
105/// Color stop for gradient definition.
106#[derive(Clone)]
107struct ColorPoint {
108    offset: f64,
109    color: Rgba8,
110}
111
112impl ColorPoint {
113    fn new(offset: f64, color: Rgba8) -> Self {
114        Self {
115            offset: offset.clamp(0.0, 1.0),
116            color,
117        }
118    }
119}
120
121/// Gradient color lookup table.
122///
123/// Builds a 256-entry (or custom size) color LUT from SVG-style color stops.
124/// Supports arbitrary numbers of stops at positions [0..1].
125///
126/// Port of C++ `gradient_lut<ColorInterpolator, ColorLutSize>`.
127pub struct GradientLut {
128    color_profile: Vec<ColorPoint>,
129    color_lut: Vec<Rgba8>,
130    lut_size: usize,
131    use_fast_interpolator: bool,
132}
133
134impl GradientLut {
135    /// Create a new gradient LUT with the specified size (default 256).
136    pub fn new(lut_size: usize) -> Self {
137        Self {
138            color_profile: Vec::new(),
139            color_lut: vec![Rgba8::default(); lut_size],
140            lut_size,
141            use_fast_interpolator: true,
142        }
143    }
144
145    /// Create a new gradient LUT with default size of 256.
146    pub fn new_default() -> Self {
147        Self::new(256)
148    }
149
150    /// Set whether to use the fast DDA interpolator (default: true).
151    pub fn set_use_fast_interpolator(&mut self, fast: bool) {
152        self.use_fast_interpolator = fast;
153    }
154
155    /// Remove all color stops.
156    pub fn remove_all(&mut self) {
157        self.color_profile.clear();
158    }
159
160    /// Add a color stop at the given offset (clamped to [0..1]).
161    pub fn add_color(&mut self, offset: f64, color: Rgba8) {
162        self.color_profile.push(ColorPoint::new(offset, color));
163    }
164
165    /// Build the lookup table by interpolating between color stops.
166    ///
167    /// Must have at least 2 color stops. Stops are sorted by offset
168    /// and duplicates are removed.
169    pub fn build_lut(&mut self) {
170        // Sort by offset
171        self.color_profile
172            .sort_by(|a, b| a.offset.partial_cmp(&b.offset).unwrap());
173        // Remove duplicates (same offset)
174        self.color_profile
175            .dedup_by(|a, b| (a.offset - b.offset).abs() < 1e-10);
176
177        if self.color_profile.len() < 2 {
178            return;
179        }
180
181        let size = self.lut_size;
182        let mut start = uround(self.color_profile[0].offset * size as f64) as usize;
183
184        // Fill before first stop with first color
185        let c = self.color_profile[0].color;
186        for i in 0..start.min(size) {
187            self.color_lut[i] = c;
188        }
189
190        // Interpolate between stops
191        for i in 1..self.color_profile.len() {
192            let end = uround(self.color_profile[i].offset * size as f64) as usize;
193            // The loops below write entries `start..end` (that's `end - start` of them),
194            // calling `color()` then `inc()` each time, so the LAST written entry uses
195            // step index `end - start - 1`. A DdaLineInterpolator sized `count` only
196            // reaches its end color after `count` steps, so to land the segment's end
197            // color exactly on that last entry the interpolator must be sized
198            // `end - start - 1`. The previous `end - start + 1` sized it two steps too
199            // long, so every segment stopped ~2/255 short of its end color (a
200            // black->white ramp ended at 253, not 255).
201            let seg_len = if end > start { (end - start - 1).max(1) } else { 1 };
202
203            if self.use_fast_interpolator {
204                let mut ci = ColorInterpolatorRgba8::new(
205                    &self.color_profile[i - 1].color,
206                    &self.color_profile[i].color,
207                    seg_len as u32,
208                );
209                while start < end && start < size {
210                    self.color_lut[start] = ci.color();
211                    ci.inc();
212                    start += 1;
213                }
214            } else {
215                let mut ci = ColorInterpolatorGeneric::new(
216                    &self.color_profile[i - 1].color,
217                    &self.color_profile[i].color,
218                    seg_len as u32,
219                );
220                while start < end && start < size {
221                    self.color_lut[start] = ci.color();
222                    ci.inc();
223                    start += 1;
224                }
225            }
226        }
227
228        // Fill after last stop with last color
229        let c = self.color_profile.last().unwrap().color;
230        let mut end = start;
231        while end < size {
232            self.color_lut[end] = c;
233            end += 1;
234        }
235    }
236}
237
238impl ColorFunction for GradientLut {
239    type Color = Rgba8;
240
241    fn size(&self) -> usize {
242        self.lut_size
243    }
244
245    #[inline]
246    fn get(&self, index: usize) -> Rgba8 {
247        self.color_lut[index]
248    }
249}
250
251// ============================================================================
252// GradientLinearColor — simple 2-color linear interpolation
253// ============================================================================
254
255/// Simple 2-color linear gradient color function.
256///
257/// Interpolates between two colors based on index/size ratio.
258///
259/// Port of C++ `gradient_linear_color<ColorT>`.
260pub struct GradientLinearColor {
261    c1: Rgba8,
262    c2: Rgba8,
263    size: usize,
264}
265
266impl GradientLinearColor {
267    pub fn new(c1: Rgba8, c2: Rgba8, size: usize) -> Self {
268        Self { c1, c2, size }
269    }
270
271    pub fn colors(&mut self, c1: Rgba8, c2: Rgba8) {
272        self.c1 = c1;
273        self.c2 = c2;
274    }
275}
276
277impl ColorFunction for GradientLinearColor {
278    type Color = Rgba8;
279
280    fn size(&self) -> usize {
281        self.size
282    }
283
284    fn get(&self, index: usize) -> Rgba8 {
285        self.c1
286            .gradient(&self.c2, index as f64 / (self.size - 1).max(1) as f64)
287    }
288}
289
290// ============================================================================
291// Tests
292// ============================================================================
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297
298    #[test]
299    fn test_gradient_lut_new() {
300        let lut = GradientLut::new_default();
301        assert_eq!(lut.size(), 256);
302    }
303
304    #[test]
305    fn test_gradient_lut_two_stops() {
306        let mut lut = GradientLut::new_default();
307        lut.add_color(0.0, Rgba8::new(255, 0, 0, 255));
308        lut.add_color(1.0, Rgba8::new(0, 0, 255, 255));
309        lut.build_lut();
310
311        // First should be exactly red
312        let c0 = lut.get(0);
313        assert_eq!(c0.r, 255);
314        assert_eq!(c0.b, 0);
315
316        // Last entry must land EXACTLY on the end color (the interpolator is now sized
317        // so the ramp reaches its endpoint instead of stopping ~2/255 short).
318        let c255 = lut.get(255);
319        assert_eq!(c255.r, 0, "c255.r={}", c255.r);
320        assert_eq!(c255.b, 255, "c255.b={}", c255.b);
321
322        // Middle should be roughly equal
323        let c128 = lut.get(128);
324        assert!(c128.r > 50 && c128.r < 200, "Mid r={}", c128.r);
325        assert!(c128.b > 50 && c128.b < 200, "Mid b={}", c128.b);
326    }
327
328    /// Regression test for the gradient-LUT interpolator length off-by-one.
329    ///
330    /// `build_lut` fills entries `start..end` (that's `end - start` of them), writing the
331    /// interpolator's current color then stepping it once per entry — so the LAST entry
332    /// is written at step index `end - start - 1`. A `DdaLineInterpolator` reaches its end
333    /// color only after `count` steps, so `count` must be `end - start - 1` for that last
334    /// entry to land on the end color.
335    ///
336    /// The interpolator used to be sized `end - start + 1`. For a single black→white
337    /// segment over the whole 256-entry table (`start = 0`, `end = 256`) that made the
338    /// last entry land at step 255 of a 257-step ramp:
339    ///
340    ///     255 * 255 / 257 ≈ 253
341    ///
342    /// so `get(255)` came back as (253, 253, 253) instead of pure white — every
343    /// multi-stop gradient's endpoint was tinted ~2/255 toward the start color. With the
344    /// interpolator sized `end - start - 1`, a segment whose length divides the color
345    /// range (here 255 over 255 steps) reaches the endpoint exactly.
346    #[test]
347    fn gradient_lut_two_stop_ramp_reaches_its_end_color_exactly() {
348        let mut lut = GradientLut::new_default();
349        lut.add_color(0.0, Rgba8::new(0, 0, 0, 255)); // black
350        lut.add_color(1.0, Rgba8::new(255, 255, 255, 255)); // white
351        lut.build_lut();
352
353        // Start is pure black.
354        let first = lut.get(0);
355        assert_eq!((first.r, first.g, first.b), (0, 0, 0), "first = {first:?}");
356
357        // End is pure white. Pre-fix this was ~(253, 253, 253) — the bug this guards.
358        let last = lut.get(255);
359        assert_eq!(
360            (last.r, last.g, last.b),
361            (255, 255, 255),
362            "the ramp must reach pure white; got {last:?}",
363        );
364    }
365
366    #[test]
367    fn test_gradient_lut_three_stops() {
368        let mut lut = GradientLut::new_default();
369        lut.add_color(0.0, Rgba8::new(255, 0, 0, 255));
370        lut.add_color(0.5, Rgba8::new(0, 255, 0, 255));
371        lut.add_color(1.0, Rgba8::new(0, 0, 255, 255));
372        lut.build_lut();
373
374        // Start: exactly red
375        assert_eq!(lut.get(0).r, 255);
376        // End: blue within 1 LSB. Here the last segment spans 127 entries over a range
377        // of 255, which integer DDA can't divide exactly, so it lands at 254 — but that
378        // is the residual rounding error, not the old ~2/255 endpoint shortfall.
379        assert!(lut.get(255).b >= 254, "last.b={}", lut.get(255).b);
380        // Middle: should be mostly green
381        let mid = lut.get(128);
382        assert!(mid.g > 128, "Mid green={}", mid.g);
383    }
384
385    #[test]
386    fn test_gradient_lut_remove_all() {
387        let mut lut = GradientLut::new_default();
388        lut.add_color(0.0, Rgba8::new(255, 0, 0, 255));
389        lut.remove_all();
390        lut.add_color(0.0, Rgba8::new(0, 255, 0, 255));
391        lut.add_color(1.0, Rgba8::new(0, 255, 0, 255));
392        lut.build_lut();
393        assert_eq!(lut.get(0).g, 255);
394    }
395
396    #[test]
397    fn test_gradient_lut_generic_interpolator() {
398        let mut lut = GradientLut::new_default();
399        lut.set_use_fast_interpolator(false);
400        lut.add_color(0.0, Rgba8::new(255, 0, 0, 255));
401        lut.add_color(1.0, Rgba8::new(0, 0, 255, 255));
402        lut.build_lut();
403
404        let c0 = lut.get(0);
405        assert_eq!(c0.r, 255);
406        let c255 = lut.get(255);
407        // Regression guard for the seg_len endpoint fix on the generic interpolator path:
408        // upstream C++ agg_gradient_lut.h sized segments end - start + 1, so ramps stopped
409        // ~2/255 short of the end color. The endpoint must now be exactly the end color.
410        assert_eq!(c255.b, 255);
411        assert_eq!(c255.r, 0);
412    }
413
414    #[test]
415    fn test_gradient_lut_custom_size() {
416        let mut lut = GradientLut::new(64);
417        lut.add_color(0.0, Rgba8::new(0, 0, 0, 255));
418        lut.add_color(1.0, Rgba8::new(255, 255, 255, 255));
419        lut.build_lut();
420        assert_eq!(lut.size(), 64);
421        assert_eq!(lut.get(0).r, 0);
422        assert!(lut.get(63).r >= 244, "last.r={}", lut.get(63).r);
423    }
424
425    #[test]
426    fn test_gradient_linear_color() {
427        let gc = GradientLinearColor::new(
428            Rgba8::new(0, 0, 0, 255),
429            Rgba8::new(255, 255, 255, 255),
430            256,
431        );
432        assert_eq!(gc.size(), 256);
433
434        let c0 = gc.get(0);
435        assert_eq!(c0.r, 0);
436
437        let c255 = gc.get(255);
438        assert_eq!(c255.r, 255);
439
440        let c128 = gc.get(128);
441        assert!(c128.r > 100 && c128.r < 160, "c128.r={}", c128.r);
442    }
443
444    #[test]
445    fn test_gradient_lut_unsorted_stops() {
446        let mut lut = GradientLut::new_default();
447        lut.add_color(1.0, Rgba8::new(0, 0, 255, 255));
448        lut.add_color(0.0, Rgba8::new(255, 0, 0, 255));
449        lut.build_lut();
450
451        // Should still work — stops are sorted internally
452        assert_eq!(lut.get(0).r, 255);
453        // Endpoint must equal the end color (seg_len fix; C++ ramps stopped ~2/255 short).
454        assert_eq!(lut.get(255).b, 255);
455        assert_eq!(lut.get(255).r, 0);
456    }
457
458    #[test]
459    fn test_color_interpolator_rgba8_fast() {
460        let c1 = Rgba8::new(0, 0, 0, 255);
461        let c2 = Rgba8::new(255, 255, 255, 255);
462        let mut ci = ColorInterpolatorRgba8::new(&c1, &c2, 10);
463
464        let first = ci.color();
465        assert_eq!(first.r, 0);
466
467        for _ in 0..10 {
468            ci.inc();
469        }
470        let last = ci.color();
471        assert_eq!(last.r, 255);
472    }
473
474    #[test]
475    fn test_gradient_linear_color_set_colors() {
476        let mut gc = GradientLinearColor::new(
477            Rgba8::new(0, 0, 0, 255),
478            Rgba8::new(255, 255, 255, 255),
479            256,
480        );
481        gc.colors(Rgba8::new(255, 0, 0, 255), Rgba8::new(0, 255, 0, 255));
482        assert_eq!(gc.get(0).r, 255);
483        assert_eq!(gc.get(0).g, 0);
484        assert_eq!(gc.get(255).r, 0);
485        assert_eq!(gc.get(255).g, 255);
486    }
487}