tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

gamma_lut.rs (14129B)


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