Skip to main content

agg_rust/
gamma.rs

1//! Gamma correction functions and lookup tables.
2//!
3//! Port of `agg_gamma_functions.h` and `agg_gamma_lut.h` — various gamma
4//! correction strategies used throughout AGG for anti-aliasing quality.
5
6use crate::basics::uround;
7
8// ============================================================================
9// Gamma function trait
10// ============================================================================
11
12/// Trait for gamma correction functions.
13/// Port of C++ gamma function objects (operator() overload).
14pub trait GammaFunction {
15    fn call(&self, x: f64) -> f64;
16}
17
18// ============================================================================
19// Gamma none (identity)
20// ============================================================================
21
22/// No gamma correction — returns input unchanged.
23/// Port of C++ `gamma_none`.
24#[derive(Debug, Clone, Copy, Default)]
25pub struct GammaNone;
26
27impl GammaFunction for GammaNone {
28    #[inline]
29    fn call(&self, x: f64) -> f64 {
30        x
31    }
32}
33
34// ============================================================================
35// Gamma power
36// ============================================================================
37
38/// Power-law gamma correction: `x^gamma`.
39/// Port of C++ `gamma_power`.
40#[derive(Debug, Clone, Copy)]
41pub struct GammaPower {
42    gamma: f64,
43}
44
45impl GammaPower {
46    pub fn new(gamma: f64) -> Self {
47        Self { gamma }
48    }
49
50    pub fn gamma(&self) -> f64 {
51        self.gamma
52    }
53
54    pub fn set_gamma(&mut self, g: f64) {
55        self.gamma = g;
56    }
57}
58
59impl Default for GammaPower {
60    fn default() -> Self {
61        Self { gamma: 1.0 }
62    }
63}
64
65impl GammaFunction for GammaPower {
66    #[inline]
67    fn call(&self, x: f64) -> f64 {
68        x.powf(self.gamma)
69    }
70}
71
72// ============================================================================
73// Gamma threshold
74// ============================================================================
75
76/// Threshold gamma: returns 0 if x < threshold, 1 otherwise.
77/// Port of C++ `gamma_threshold`.
78#[derive(Debug, Clone, Copy)]
79pub struct GammaThreshold {
80    threshold: f64,
81}
82
83impl GammaThreshold {
84    pub fn new(threshold: f64) -> Self {
85        Self { threshold }
86    }
87
88    pub fn threshold(&self) -> f64 {
89        self.threshold
90    }
91
92    pub fn set_threshold(&mut self, t: f64) {
93        self.threshold = t;
94    }
95}
96
97impl Default for GammaThreshold {
98    fn default() -> Self {
99        Self { threshold: 0.5 }
100    }
101}
102
103impl GammaFunction for GammaThreshold {
104    #[inline]
105    fn call(&self, x: f64) -> f64 {
106        if x < self.threshold {
107            0.0
108        } else {
109            1.0
110        }
111    }
112}
113
114// ============================================================================
115// Gamma linear
116// ============================================================================
117
118/// Linear ramp gamma: 0 below `start`, 1 above `end`, linear between.
119/// Port of C++ `gamma_linear`.
120#[derive(Debug, Clone, Copy)]
121pub struct GammaLinear {
122    start: f64,
123    end: f64,
124}
125
126impl GammaLinear {
127    pub fn new(start: f64, end: f64) -> Self {
128        Self { start, end }
129    }
130
131    pub fn start(&self) -> f64 {
132        self.start
133    }
134
135    pub fn end(&self) -> f64 {
136        self.end
137    }
138
139    pub fn set_start(&mut self, s: f64) {
140        self.start = s;
141    }
142
143    pub fn set_end(&mut self, e: f64) {
144        self.end = e;
145    }
146
147    pub fn set(&mut self, s: f64, e: f64) {
148        self.start = s;
149        self.end = e;
150    }
151}
152
153impl Default for GammaLinear {
154    fn default() -> Self {
155        Self {
156            start: 0.0,
157            end: 1.0,
158        }
159    }
160}
161
162impl GammaFunction for GammaLinear {
163    #[inline]
164    fn call(&self, x: f64) -> f64 {
165        if x < self.start {
166            0.0
167        } else if x > self.end {
168            1.0
169        } else {
170            (x - self.start) / (self.end - self.start)
171        }
172    }
173}
174
175// ============================================================================
176// Gamma multiply
177// ============================================================================
178
179/// Multiplicative gamma: `min(x * multiplier, 1.0)`.
180/// Port of C++ `gamma_multiply`.
181#[derive(Debug, Clone, Copy)]
182pub struct GammaMultiply {
183    mul: f64,
184}
185
186impl GammaMultiply {
187    pub fn new(mul: f64) -> Self {
188        Self { mul }
189    }
190
191    pub fn value(&self) -> f64 {
192        self.mul
193    }
194
195    pub fn set_value(&mut self, v: f64) {
196        self.mul = v;
197    }
198}
199
200impl Default for GammaMultiply {
201    fn default() -> Self {
202        Self { mul: 1.0 }
203    }
204}
205
206impl GammaFunction for GammaMultiply {
207    #[inline]
208    fn call(&self, x: f64) -> f64 {
209        let y = x * self.mul;
210        if y > 1.0 {
211            1.0
212        } else {
213            y
214        }
215    }
216}
217
218// ============================================================================
219// sRGB conversion functions
220// ============================================================================
221
222/// Convert sRGB value (0..1) to linear.
223#[inline]
224pub fn srgb_to_linear(x: f64) -> f64 {
225    if x <= 0.04045 {
226        x / 12.92
227    } else {
228        ((x + 0.055) / 1.055).powf(2.4)
229    }
230}
231
232/// Convert linear value (0..1) to sRGB.
233#[inline]
234pub fn linear_to_srgb(x: f64) -> f64 {
235    if x <= 0.0031308 {
236        x * 12.92
237    } else {
238        1.055 * x.powf(1.0 / 2.4) - 0.055
239    }
240}
241
242// ============================================================================
243// Gamma LUT (Lookup Table)
244// ============================================================================
245
246/// Gamma correction using pre-computed lookup tables.
247/// Port of C++ `gamma_lut<LoResT, HiResT, GammaShift, HiResShift>`.
248///
249/// Default parameters match C++ defaults: u8 for both low/high resolution,
250/// shift=8 for both, giving 256-entry tables.
251pub struct GammaLut {
252    gamma: f64,
253    #[allow(dead_code)]
254    gamma_shift: u32,
255    gamma_size: usize,
256    gamma_mask: f64,
257    #[allow(dead_code)]
258    hi_res_shift: u32,
259    hi_res_size: usize,
260    hi_res_mask: f64,
261    dir_gamma: Vec<u8>,
262    inv_gamma: Vec<u8>,
263}
264
265impl GammaLut {
266    /// Create a gamma LUT with identity gamma (1.0).
267    /// Uses the default 8-bit/8-bit configuration.
268    pub fn new() -> Self {
269        Self::with_shifts(8, 8)
270    }
271
272    /// Create a gamma LUT with the specified gamma value.
273    pub fn new_with_gamma(g: f64) -> Self {
274        let mut lut = Self::new();
275        lut.set_gamma(g);
276        lut
277    }
278
279    /// Create a gamma LUT with custom shift parameters.
280    pub fn with_shifts(gamma_shift: u32, hi_res_shift: u32) -> Self {
281        let gamma_size = 1usize << gamma_shift;
282        let hi_res_size = 1usize << hi_res_shift;
283
284        let mut dir_gamma = vec![0u8; gamma_size];
285        let mut inv_gamma = vec![0u8; hi_res_size];
286
287        // Identity gamma: direct mapping
288        for (i, entry) in dir_gamma.iter_mut().enumerate() {
289            *entry = (i << (hi_res_shift - gamma_shift)) as u8;
290        }
291        for (i, entry) in inv_gamma.iter_mut().enumerate() {
292            *entry = (i >> (hi_res_shift - gamma_shift)) as u8;
293        }
294
295        Self {
296            gamma: 1.0,
297            gamma_shift,
298            gamma_size,
299            gamma_mask: (gamma_size - 1) as f64,
300            hi_res_shift,
301            hi_res_size,
302            hi_res_mask: (hi_res_size - 1) as f64,
303            dir_gamma,
304            inv_gamma,
305        }
306    }
307
308    /// Set the gamma value and rebuild lookup tables.
309    pub fn set_gamma(&mut self, g: f64) {
310        self.gamma = g;
311
312        for i in 0..self.gamma_size {
313            self.dir_gamma[i] =
314                uround((i as f64 / self.gamma_mask).powf(self.gamma) * self.hi_res_mask) as u8;
315        }
316
317        let inv_g = 1.0 / g;
318        for i in 0..self.hi_res_size {
319            self.inv_gamma[i] =
320                uround((i as f64 / self.hi_res_mask).powf(inv_g) * self.gamma_mask) as u8;
321        }
322    }
323
324    /// Get the current gamma value.
325    pub fn gamma(&self) -> f64 {
326        self.gamma
327    }
328
329    /// Forward (direct) gamma correction: low-res → high-res.
330    #[inline]
331    pub fn dir(&self, v: u8) -> u8 {
332        self.dir_gamma[v as usize]
333    }
334
335    /// Inverse gamma correction: high-res → low-res.
336    #[inline]
337    pub fn inv(&self, v: u8) -> u8 {
338        self.inv_gamma[v as usize]
339    }
340}
341
342impl Default for GammaLut {
343    fn default() -> Self {
344        Self::new()
345    }
346}
347
348// ============================================================================
349// Gamma Spline — interactive gamma curve via bicubic spline
350// ============================================================================
351
352/// Spline-based gamma correction curve.
353///
354/// Port of C++ `gamma_spline` from `ctrl/agg_gamma_spline.h`.
355/// Takes 4 control parameters `(kx1, ky1, kx2, ky2)` each in `[0.001..1.999]`
356/// and generates a smooth gamma curve through 4 interpolation points:
357///   - `(0, 0)`
358///   - `(kx1 * 0.25, ky1 * 0.25)`
359///   - `(1 - kx2 * 0.25, 1 - ky2 * 0.25)`
360///   - `(1, 1)`
361///
362/// Produces a 256-entry lookup table (`gamma()`) and supports evaluation (`y()`).
363/// Also implements a vertex source interface for rendering the curve.
364pub struct GammaSpline {
365    gamma: [u8; 256],
366    x: [f64; 4],
367    y_pts: [f64; 4],
368    spline: crate::bspline::Bspline,
369    x1: f64,
370    y1: f64,
371    x2: f64,
372    y2: f64,
373    cur_x: f64,
374}
375
376impl GammaSpline {
377    pub fn new() -> Self {
378        let mut gs = Self {
379            gamma: [0; 256],
380            x: [0.0; 4],
381            y_pts: [0.0; 4],
382            spline: crate::bspline::Bspline::new(),
383            x1: 0.0,
384            y1: 0.0,
385            x2: 10.0,
386            y2: 10.0,
387            cur_x: 0.0,
388        };
389        gs.set_values(1.0, 1.0, 1.0, 1.0);
390        gs
391    }
392
393    /// Set the 4 control parameters and rebuild the spline and gamma table.
394    ///
395    /// Each parameter should be in `[0.001..1.999]`; values are clamped.
396    pub fn set_values(&mut self, kx1: f64, ky1: f64, kx2: f64, ky2: f64) {
397        let kx1 = kx1.clamp(0.001, 1.999);
398        let ky1 = ky1.clamp(0.001, 1.999);
399        let kx2 = kx2.clamp(0.001, 1.999);
400        let ky2 = ky2.clamp(0.001, 1.999);
401
402        self.x[0] = 0.0;
403        self.y_pts[0] = 0.0;
404        self.x[1] = kx1 * 0.25;
405        self.y_pts[1] = ky1 * 0.25;
406        self.x[2] = 1.0 - kx2 * 0.25;
407        self.y_pts[2] = 1.0 - ky2 * 0.25;
408        self.x[3] = 1.0;
409        self.y_pts[3] = 1.0;
410
411        self.spline.init(&self.x, &self.y_pts);
412
413        for i in 0..256 {
414            self.gamma[i] = (self.y(i as f64 / 255.0) * 255.0) as u8;
415        }
416    }
417
418    /// Get the 4 control parameters back from the stored spline points.
419    pub fn get_values(&self) -> (f64, f64, f64, f64) {
420        (
421            self.x[1] * 4.0,
422            self.y_pts[1] * 4.0,
423            (1.0 - self.x[2]) * 4.0,
424            (1.0 - self.y_pts[2]) * 4.0,
425        )
426    }
427
428    /// Get the 256-entry gamma lookup table.
429    pub fn gamma(&self) -> &[u8; 256] {
430        &self.gamma
431    }
432
433    /// Evaluate the spline curve at `x` (0..1) → result clamped to `[0..1]`.
434    pub fn y(&self, x: f64) -> f64 {
435        let x = x.clamp(0.0, 1.0);
436        let val = self.spline.get(x);
437        val.clamp(0.0, 1.0)
438    }
439
440    /// Set the bounding box for vertex source rendering.
441    pub fn set_box(&mut self, x1: f64, y1: f64, x2: f64, y2: f64) {
442        self.x1 = x1;
443        self.y1 = y1;
444        self.x2 = x2;
445        self.y2 = y2;
446    }
447
448    /// Rewind vertex source iteration.
449    pub fn rewind(&mut self, _idx: u32) {
450        self.cur_x = 0.0;
451    }
452
453    /// Get the next vertex of the gamma curve path.
454    pub fn vertex(&mut self, vx: &mut f64, vy: &mut f64) -> u32 {
455        use crate::basics::{PATH_CMD_LINE_TO, PATH_CMD_MOVE_TO, PATH_CMD_STOP};
456
457        if self.cur_x == 0.0 {
458            *vx = self.x1;
459            *vy = self.y1;
460            self.cur_x += 1.0 / (self.x2 - self.x1);
461            return PATH_CMD_MOVE_TO;
462        }
463
464        if self.cur_x > 1.0 {
465            return PATH_CMD_STOP;
466        }
467
468        *vx = self.x1 + self.cur_x * (self.x2 - self.x1);
469        *vy = self.y1 + self.y(self.cur_x) * (self.y2 - self.y1);
470
471        self.cur_x += 1.0 / (self.x2 - self.x1);
472        PATH_CMD_LINE_TO
473    }
474}
475
476impl Default for GammaSpline {
477    fn default() -> Self {
478        Self::new()
479    }
480}
481
482impl GammaFunction for GammaSpline {
483    fn call(&self, x: f64) -> f64 {
484        self.y(x)
485    }
486}
487
488// ============================================================================
489// Tests
490// ============================================================================
491
492#[cfg(test)]
493mod tests {
494    use super::*;
495
496    const EPSILON: f64 = 1e-10;
497
498    #[test]
499    fn test_gamma_none() {
500        let g = GammaNone;
501        assert!((g.call(0.0) - 0.0).abs() < EPSILON);
502        assert!((g.call(0.5) - 0.5).abs() < EPSILON);
503        assert!((g.call(1.0) - 1.0).abs() < EPSILON);
504    }
505
506    #[test]
507    fn test_gamma_power_identity() {
508        let g = GammaPower::new(1.0);
509        assert!((g.call(0.5) - 0.5).abs() < EPSILON);
510    }
511
512    #[test]
513    fn test_gamma_power_square() {
514        let g = GammaPower::new(2.0);
515        assert!((g.call(0.5) - 0.25).abs() < EPSILON);
516        assert!((g.call(0.0) - 0.0).abs() < EPSILON);
517        assert!((g.call(1.0) - 1.0).abs() < EPSILON);
518    }
519
520    #[test]
521    fn test_gamma_threshold() {
522        let g = GammaThreshold::new(0.5);
523        assert_eq!(g.call(0.3), 0.0);
524        assert_eq!(g.call(0.5), 1.0);
525        assert_eq!(g.call(0.7), 1.0);
526    }
527
528    #[test]
529    fn test_gamma_linear() {
530        let g = GammaLinear::new(0.2, 0.8);
531        assert_eq!(g.call(0.1), 0.0);
532        assert_eq!(g.call(0.9), 1.0);
533        assert!((g.call(0.5) - 0.5).abs() < EPSILON);
534    }
535
536    #[test]
537    fn test_gamma_multiply() {
538        let g = GammaMultiply::new(2.0);
539        assert!((g.call(0.3) - 0.6).abs() < EPSILON);
540        assert_eq!(g.call(0.7), 1.0); // Clamped
541        assert_eq!(g.call(1.0), 1.0);
542    }
543
544    #[test]
545    fn test_srgb_roundtrip() {
546        for i in 0..=10 {
547            let x = i as f64 / 10.0;
548            let linear = srgb_to_linear(x);
549            let back = linear_to_srgb(linear);
550            assert!(
551                (x - back).abs() < 1e-6,
552                "sRGB roundtrip failed for x={}: got {}",
553                x,
554                back
555            );
556        }
557    }
558
559    #[test]
560    fn test_srgb_endpoints() {
561        assert!((srgb_to_linear(0.0)).abs() < EPSILON);
562        assert!((srgb_to_linear(1.0) - 1.0).abs() < EPSILON);
563        assert!((linear_to_srgb(0.0)).abs() < EPSILON);
564        assert!((linear_to_srgb(1.0) - 1.0).abs() < EPSILON);
565    }
566
567    #[test]
568    fn test_gamma_lut_identity() {
569        let lut = GammaLut::new();
570        assert_eq!(lut.gamma(), 1.0);
571        // Identity: dir and inv should be identity mappings
572        assert_eq!(lut.dir(0), 0);
573        assert_eq!(lut.dir(128), 128);
574        assert_eq!(lut.dir(255), 255);
575        assert_eq!(lut.inv(0), 0);
576        assert_eq!(lut.inv(128), 128);
577        assert_eq!(lut.inv(255), 255);
578    }
579
580    #[test]
581    fn test_gamma_lut_roundtrip() {
582        let lut = GammaLut::new_with_gamma(2.2);
583        // Forward then inverse should be approximately identity
584        for v in [0u8, 64, 128, 192, 255] {
585            let forward = lut.dir(v);
586            let back = lut.inv(forward);
587            assert!(
588                (v as i32 - back as i32).unsigned_abs() <= 1,
589                "Roundtrip failed for v={}: dir={}, inv={}",
590                v,
591                forward,
592                back
593            );
594        }
595    }
596
597    #[test]
598    fn test_gamma_lut_gamma_2() {
599        let lut = GammaLut::new_with_gamma(2.0);
600        // At gamma=2.0, dir(128) should be pow(128/255, 2.0) * 255 ≈ 64
601        let d = lut.dir(128);
602        assert!(
603            (d as i32 - 64).unsigned_abs() <= 1,
604            "dir(128) at gamma 2.0 should be ~64, got {}",
605            d
606        );
607    }
608
609    // ====================================================================
610    // GammaSpline tests
611    // ====================================================================
612
613    #[test]
614    fn test_gamma_spline_default() {
615        let gs = GammaSpline::new();
616        // Default values(1,1,1,1): straight line
617        let (kx1, ky1, kx2, ky2) = gs.get_values();
618        assert!((kx1 - 1.0).abs() < 0.01);
619        assert!((ky1 - 1.0).abs() < 0.01);
620        assert!((kx2 - 1.0).abs() < 0.01);
621        assert!((ky2 - 1.0).abs() < 0.01);
622    }
623
624    #[test]
625    fn test_gamma_spline_identity_curve() {
626        let gs = GammaSpline::new();
627        // With default values, the curve should approximate identity
628        assert!((gs.y(0.0)).abs() < 0.01);
629        assert!((gs.y(1.0) - 1.0).abs() < 0.01);
630        assert!((gs.y(0.5) - 0.5).abs() < 0.05);
631    }
632
633    #[test]
634    fn test_gamma_spline_gamma_table() {
635        let gs = GammaSpline::new();
636        let gamma = gs.gamma();
637        // First and last entries
638        assert_eq!(gamma[0], 0);
639        assert_eq!(gamma[255], 255);
640        // Middle should be close to 128
641        assert!((gamma[128] as i32 - 128).unsigned_abs() <= 5);
642    }
643
644    #[test]
645    fn test_gamma_spline_roundtrip_values() {
646        let mut gs = GammaSpline::new();
647        gs.set_values(0.5, 1.5, 0.8, 1.2);
648        let (kx1, ky1, kx2, ky2) = gs.get_values();
649        assert!((kx1 - 0.5).abs() < 0.001);
650        assert!((ky1 - 1.5).abs() < 0.001);
651        assert!((kx2 - 0.8).abs() < 0.001);
652        assert!((ky2 - 1.2).abs() < 0.001);
653    }
654
655    #[test]
656    fn test_gamma_spline_vertex_source() {
657        let mut gs = GammaSpline::new();
658        gs.set_box(0.0, 0.0, 100.0, 100.0);
659        gs.rewind(0);
660
661        let (mut x, mut y) = (0.0, 0.0);
662        let cmd = gs.vertex(&mut x, &mut y);
663        assert_eq!(cmd, crate::basics::PATH_CMD_MOVE_TO);
664        assert!((x - 0.0).abs() < 0.01);
665
666        // Should produce line_to vertices until > 1.0
667        let mut count = 0;
668        loop {
669            let cmd = gs.vertex(&mut x, &mut y);
670            if cmd == crate::basics::PATH_CMD_STOP {
671                break;
672            }
673            assert_eq!(cmd, crate::basics::PATH_CMD_LINE_TO);
674            count += 1;
675        }
676        assert!(count >= 99 && count <= 101); // ~100 pixels wide box
677    }
678
679    #[test]
680    fn test_gamma_spline_clamping() {
681        let mut gs = GammaSpline::new();
682        gs.set_values(0.0, 3.0, -1.0, 2.5);
683        let (kx1, ky1, kx2, ky2) = gs.get_values();
684        // Should be clamped to [0.001, 1.999]
685        assert!((kx1 - 0.001).abs() < 0.001);
686        assert!((ky1 - 1.999).abs() < 0.001);
687        assert!((kx2 - 0.001).abs() < 0.001);
688        assert!((ky2 - 1.999).abs() < 0.001);
689    }
690
691    #[test]
692    fn test_gamma_spline_as_gamma_function() {
693        let gs = GammaSpline::new();
694        // GammaSpline implements GammaFunction
695        assert!((gs.call(0.0)).abs() < 0.01);
696        assert!((gs.call(1.0) - 1.0).abs() < 0.01);
697    }
698}