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