gamma_lut/
lib.rs

1/*!
2Gamma correction lookup tables.
3
4This is a port of Skia gamma LUT logic into Rust, used by WebRender.
5*/
6//#![warn(missing_docs)] //TODO
7
8#[macro_use]
9extern crate log;
10
11/// Color space responsible for converting between lumas and luminances.
12#[derive(Clone, Copy, Debug, PartialEq)]
13pub enum LuminanceColorSpace {
14    /// Linear space - no conversion involved.
15    Linear,
16    /// Simple gamma space - uses the `luminance ^ gamma` function.
17    Gamma(f32),
18    /// Srgb space.
19    Srgb,
20}
21
22impl LuminanceColorSpace {
23    pub fn new(gamma: f32) -> LuminanceColorSpace {
24        if gamma == 1.0 {
25            LuminanceColorSpace::Linear
26        } else if gamma == 0.0 {
27            LuminanceColorSpace::Srgb
28        } else {
29            LuminanceColorSpace::Gamma(gamma)
30        }
31    }
32
33    pub fn to_luma(&self, luminance: f32) -> f32 {
34        match *self {
35            LuminanceColorSpace::Linear => luminance,
36            LuminanceColorSpace::Gamma(gamma) => luminance.powf(gamma),
37            LuminanceColorSpace::Srgb => {
38                //The magic numbers are derived from the sRGB specification.
39                //See http://www.color.org/chardata/rgb/srgb.xalter .
40                if luminance <= 0.04045 {
41                    luminance / 12.92
42                } else {
43                    ((luminance + 0.055) / 1.055).powf(2.4)
44                }
45            }
46        }
47    }
48
49    pub fn from_luma(&self, luma: f32) -> f32 {
50        match *self {
51            LuminanceColorSpace::Linear => luma,
52            LuminanceColorSpace::Gamma(gamma) => luma.powf(1. / gamma),
53            LuminanceColorSpace::Srgb => {
54                //The magic numbers are derived from the sRGB specification.
55                //See http://www.color.org/chardata/rgb/srgb.xalter .
56                if luma <= 0.0031308 {
57                    luma * 12.92
58                } else {
59                    1.055 * luma.powf(1./2.4) - 0.055
60                }
61            }
62        }
63    }
64}
65
66//TODO: tests
67fn round_to_u8(x : f32) -> u8 {
68    let v = (x + 0.5).floor() as i32;
69    assert!(0 <= v && v < 0x100);
70    v as u8
71}
72
73//TODO: tests
74/*
75 * Scales base <= 2^N-1 to 2^8-1
76 * @param N [1, 8] the number of bits used by base.
77 * @param base the number to be scaled to [0, 255].
78 */
79fn scale255(n: u8, mut base: u8) -> u8 {
80    base <<= 8 - n;
81    let mut lum = base;
82    let mut i = n;
83
84    while i < 8 {
85        lum |= base >> i;
86        i += n;
87    }
88
89    lum
90}
91
92// Computes the luminance from the given r, g, and b in accordance with
93// SK_LUM_COEFF_X. For correct results, r, g, and b should be in linear space.
94fn compute_luminance(r: u8, g: u8, b: u8) -> u8 {
95    // The following is
96    // r * SK_LUM_COEFF_R + g * SK_LUM_COEFF_G + b * SK_LUM_COEFF_B
97    // with SK_LUM_COEFF_X in 1.8 fixed point (rounding adjusted to sum to 256).
98    let val: u32 = r as u32 * 54 + g as u32 * 183 + b as u32 * 19;
99    assert!(val < 0x10000);
100    (val >> 8) as u8
101}
102
103// Skia uses 3 bits per channel for luminance.
104pub const LUM_BITS: u8 = 3;
105
106#[derive(Copy, Clone)]
107pub struct Color {
108    pub r: u8,
109    pub g: u8,
110    pub b: u8,
111    pub a: u8,
112}
113
114impl Color {
115    pub fn new(r: u8, g: u8, b: u8, a: u8) -> Color {
116        Color {
117            r: r,
118            g: g,
119            b: b,
120            a: a,
121        }
122    }
123
124    // Compute a canonical color that is equivalent to the input color
125    // for preblend table lookups.
126    pub fn quantize(&self) -> Color {
127        Color::new(
128            scale255(LUM_BITS, self.r >> (8 - LUM_BITS)),
129            scale255(LUM_BITS, self.g >> (8 - LUM_BITS)),
130            scale255(LUM_BITS, self.b >> (8 - LUM_BITS)),
131            self.a,
132        )
133    }
134
135    // Compute a luminance value suitable for grayscale preblend table
136    // lookups.
137    pub fn luminance(&self) -> u8 {
138        compute_luminance(self.r, self.g, self.b)
139    }
140
141    // Make a grayscale color from the computed luminance.
142    pub fn luminance_color(&self) -> Color {
143        let lum = self.luminance();
144        Color::new(lum, lum, lum, self.a)
145    }
146}
147
148// This will invert the gamma applied by CoreGraphics,
149// so we can get linear values.
150// CoreGraphics obscurely defaults to 2.0 as the smoothing gamma value.
151// The color space used does not appear to affect this choice.
152#[cfg(target_os="macos")]
153fn get_inverse_gamma_table_coregraphics_smoothing() -> [u8; 256] {
154    let mut table = [0u8; 256];
155
156    for (i, v) in table.iter_mut().enumerate() {
157        let x = i as f32 / 255.0;
158        *v = round_to_u8(x * x * 255.0);
159    }
160
161    table
162}
163
164// A value of 0.5 for SK_GAMMA_CONTRAST appears to be a good compromise.
165// With lower values small text appears washed out (though correctly so).
166// With higher values lcd fringing is worse and the smoothing effect of
167// partial coverage is diminished.
168fn apply_contrast(srca: f32, contrast: f32) -> f32 {
169    srca + ((1.0 - srca) * contrast * srca)
170}
171
172// The approach here is not necessarily the one with the lowest error
173// See https://bel.fi/alankila/lcd/alpcor.html for a similar kind of thing
174// that just search for the adjusted alpha value
175pub fn build_gamma_correcting_lut(table: &mut [u8; 256], src: u8, contrast: f32,
176                                  src_space: LuminanceColorSpace,
177                                  dst_convert: LuminanceColorSpace) {
178
179    let src = src as f32 / 255.0;
180    let lin_src = src_space.to_luma(src);
181    // Guess at the dst. The perceptual inverse provides smaller visual
182    // discontinuities when slight changes to desaturated colors cause a channel
183    // to map to a different correcting lut with neighboring srcI.
184    // See https://code.google.com/p/chromium/issues/detail?id=141425#c59 .
185    let dst = 1.0 - src;
186    let lin_dst = dst_convert.to_luma(dst);
187
188    // Contrast value tapers off to 0 as the src luminance becomes white
189    let adjusted_contrast = contrast * lin_dst;
190
191    // Remove discontinuity and instability when src is close to dst.
192    // The value 1/256 is arbitrary and appears to contain the instability.
193    if (src - dst).abs() < (1.0 / 256.0) {
194        let mut ii : f32 = 0.0;
195        for v in table.iter_mut() {
196            let raw_srca = ii / 255.0;
197            let srca = apply_contrast(raw_srca, adjusted_contrast);
198
199            *v = round_to_u8(255.0 * srca);
200            ii += 1.0;
201        }
202    } else {
203        // Avoid slow int to float conversion.
204        let mut ii : f32 = 0.0;
205        for v in table.iter_mut() {
206            // 'raw_srca += 1.0f / 255.0f' and even
207            // 'raw_srca = i * (1.0f / 255.0f)' can add up to more than 1.0f.
208            // When this happens the table[255] == 0x0 instead of 0xff.
209            // See http://code.google.com/p/chromium/issues/detail?id=146466
210            let raw_srca = ii / 255.0;
211            let srca = apply_contrast(raw_srca, adjusted_contrast);
212            assert!(srca <= 1.0);
213            let dsta = 1.0 - srca;
214
215            // Calculate the output we want.
216            let lin_out = lin_src * srca + dsta * lin_dst;
217            assert!(lin_out <= 1.0);
218            let out = dst_convert.from_luma(lin_out);
219
220            // Undo what the blit blend will do.
221            // i.e. given the formula for OVER: out = src * result + (1 - result) * dst
222            // solving for result gives:
223            let result = (out - dst) / (src - dst);
224
225            *v = round_to_u8(255.0 * result);
226            debug!("Setting {:?} to {:?}", ii as u8, *v);
227
228            ii += 1.0;
229        }
230    }
231}
232
233pub struct GammaLut {
234    tables: [[u8; 256]; 1 << LUM_BITS],
235    #[cfg(target_os="macos")]
236    cg_inverse_gamma: [u8; 256],
237}
238
239impl GammaLut {
240    // Skia actually makes 9 gamma tables, then based on the luminance color,
241    // fetches the RGB gamma table for that color.
242    fn generate_tables(&mut self, contrast: f32, paint_gamma: f32, device_gamma: f32) {
243        let paint_color_space = LuminanceColorSpace::new(paint_gamma);
244        let device_color_space = LuminanceColorSpace::new(device_gamma);
245
246        for (i, entry) in self.tables.iter_mut().enumerate() {
247            let luminance = scale255(LUM_BITS, i as u8);
248            build_gamma_correcting_lut(entry,
249                                       luminance,
250                                       contrast,
251                                       paint_color_space,
252                                       device_color_space);
253        }
254    }
255
256    pub fn table_count(&self) -> usize {
257        self.tables.len()
258    }
259
260    pub fn get_table(&self, color: u8) -> &[u8; 256] {
261        &self.tables[(color >> (8 - LUM_BITS)) as usize]
262    }
263
264    pub fn new(contrast: f32, paint_gamma: f32, device_gamma: f32) -> GammaLut {
265        #[cfg(target_os="macos")]
266        let mut table = GammaLut {
267            tables: [[0; 256]; 1 << LUM_BITS],
268            cg_inverse_gamma: get_inverse_gamma_table_coregraphics_smoothing(),
269        };
270        #[cfg(not(target_os="macos"))]
271        let mut table = GammaLut {
272            tables: [[0; 256]; 1 << LUM_BITS],
273        };
274
275        table.generate_tables(contrast, paint_gamma, device_gamma);
276
277        table
278    }
279
280    // Skia normally preblends based on what the text color is.
281    // If we can't do that, use Skia default colors.
282    pub fn preblend_default_colors_bgra(&self, pixels: &mut [u8], width: usize, height: usize) {
283        let preblend_color = Color::new(0x7f, 0x80, 0x7f, 0xff);
284        self.preblend_bgra(pixels, width, height, preblend_color);
285    }
286
287    fn replace_pixels_bgra(&self, pixels: &mut [u8], width: usize, height: usize,
288                           table_r: &[u8; 256], table_g: &[u8; 256], table_b: &[u8; 256]) {
289         for y in 0..height {
290            let current_height = y * width * 4;
291
292            for pixel in pixels[current_height..current_height + (width * 4)].chunks_mut(4) {
293                pixel[0] = table_b[pixel[0] as usize];
294                pixel[1] = table_g[pixel[1] as usize];
295                pixel[2] = table_r[pixel[2] as usize];
296                // Don't touch alpha
297            }
298        }
299    }
300
301    // Mostly used by windows and GlyphRunAnalysis::GetAlphaTexture
302    fn replace_pixels_rgb(&self, pixels: &mut [u8], width: usize, height: usize,
303                          table_r: &[u8; 256], table_g: &[u8; 256], table_b: &[u8; 256]) {
304         for y in 0..height {
305            let current_height = y * width * 3;
306
307            for pixel in pixels[current_height..current_height + (width * 3)].chunks_mut(3) {
308                pixel[0] = table_r[pixel[0] as usize];
309                pixel[1] = table_g[pixel[1] as usize];
310                pixel[2] = table_b[pixel[2] as usize];
311            }
312        }
313    }
314
315    // Assumes pixels are in BGRA format. Assumes pixel values are in linear space already.
316    pub fn preblend_bgra(&self, pixels: &mut [u8], width: usize, height: usize, color: Color) {
317        let table_r = self.get_table(color.r);
318        let table_g = self.get_table(color.g);
319        let table_b = self.get_table(color.b);
320
321        self.replace_pixels_bgra(pixels, width, height, table_r, table_g, table_b);
322    }
323
324    // Assumes pixels are in RGB format. Assumes pixel values are in linear space already. NOTE:
325    // there is no alpha here.
326    pub fn preblend_rgb(&self, pixels: &mut [u8], width: usize, height: usize, color: Color) {
327        let table_r = self.get_table(color.r);
328        let table_g = self.get_table(color.g);
329        let table_b = self.get_table(color.b);
330
331        self.replace_pixels_rgb(pixels, width, height, table_r, table_g, table_b);
332    }
333
334    #[cfg(target_os="macos")]
335    pub fn coregraphics_convert_to_linear_bgra(&self, pixels: &mut [u8], width: usize, height: usize) {
336        self.replace_pixels_bgra(pixels, width, height,
337                                 &self.cg_inverse_gamma,
338                                 &self.cg_inverse_gamma,
339                                 &self.cg_inverse_gamma);
340    }
341
342    // Assumes pixels are in BGRA format. Assumes pixel values are in linear space already.
343    pub fn preblend_grayscale_bgra(&self, pixels: &mut [u8], width: usize, height: usize, color: Color) {
344        let table_g = self.get_table(color.g);
345
346         for y in 0..height {
347            let current_height = y * width * 4;
348
349            for pixel in pixels[current_height..current_height + (width * 4)].chunks_mut(4) {
350                let luminance = compute_luminance(pixel[2], pixel[1], pixel[0]);
351                pixel[0] = table_g[luminance as usize];
352                pixel[1] = table_g[luminance as usize];
353                pixel[2] = table_g[luminance as usize];
354                pixel[3] = table_g[luminance as usize];
355            }
356        }
357    }
358
359} // end impl GammaLut
360
361#[cfg(test)]
362mod tests {
363    use std::cmp;
364    use super::*;
365
366    fn over(dst: u32, src: u32, alpha: u32) -> u32 {
367        (src * alpha + dst * (255 - alpha))/255
368    }
369
370    fn overf(dst: f32, src: f32, alpha: f32) -> f32 {
371        ((src * alpha + dst * (255. - alpha))/255.) as f32
372    }
373
374
375    fn absdiff(a: u32, b: u32) -> u32 {
376        if a < b  { b - a } else { a - b }
377    }
378
379    #[test]
380    fn gamma() {
381        let mut table = [0u8; 256];
382        let g = 2.0;
383        let space = LuminanceColorSpace::Gamma(g);
384        let mut src : u32 = 131;
385        while src < 256 {
386            build_gamma_correcting_lut(&mut table, src as u8, 0., space, space);
387            let mut max_diff = 0;
388            let mut dst = 0;
389            while dst < 256 {
390                for alpha in 0u32..256 {
391                    let preblend = table[alpha as usize];
392                    let lin_dst = (dst as f32 / 255.).powf(g) * 255.;
393                    let lin_src = (src as f32 / 255.).powf(g) * 255.;
394
395                    let preblend_result = over(dst, src, preblend as u32);
396                    let true_result = ((overf(lin_dst, lin_src, alpha as f32) / 255.).powf(1. / g) * 255.) as u32;
397                    let diff = absdiff(preblend_result, true_result);
398                    //println!("{} -- {} {} = {}", alpha, preblend_result, true_result, diff);
399                    max_diff = cmp::max(max_diff, diff);
400                }
401
402                //println!("{} {} max {}", src, dst, max_diff);
403                assert!(max_diff <= 33);
404                dst += 1;
405
406            }
407            src += 1;
408        }
409    }
410} // end mod