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 {
new(gamma: f32) -> LuminanceColorSpace28     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 
to_luma(&self, luminance: f32) -> f3238     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 
from_luma(&self, luma: f32) -> f3254     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
round_to_u8(x : f32) -> u872 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  */
scale255(n: u8, mut base: u8) -> u884 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.
compute_luminance(r: u8, g: u8, b: u8) -> u899 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 {
quantize(&self) -> ColorU114     fn quantize(&self) -> ColorU;
quantized_floor(&self) -> ColorU115     fn quantized_floor(&self) -> ColorU;
quantized_ceil(&self) -> ColorU116     fn quantized_ceil(&self) -> ColorU;
luminance(&self) -> u8117     fn luminance(&self) -> u8;
luminance_color(&self) -> ColorU118     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.
quantize(&self) -> ColorU125     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.
quantized_floor(&self) -> ColorU135     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.
quantized_ceil(&self) -> ColorU145     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.
luminance(&self) -> u8156     fn luminance(&self) -> u8 {
157         compute_luminance(self.r, self.g, self.b)
158     }
159 
160     // Make a grayscale color from the computed luminance.
luminance_color(&self) -> ColorU161     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(target_os="macos")]
get_inverse_gamma_table_coregraphics_smoothing() -> [u8; 256]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.
apply_contrast(srca: f32, contrast: f32) -> f32187 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
build_gamma_correcting_lut(table: &mut [u8; 256], src: u8, contrast: f32, src_space: LuminanceColorSpace, dst_convert: LuminanceColorSpace)194 pub fn build_gamma_correcting_lut(table: &mut [u8; 256], src: u8, contrast: f32,
195                                   src_space: LuminanceColorSpace,
196                                   dst_convert: LuminanceColorSpace) {
197 
198     let src = src as f32 / 255.0;
199     let lin_src = src_space.to_luma(src);
200     // Guess at the dst. The perceptual inverse provides smaller visual
201     // discontinuities when slight changes to desaturated colors cause a channel
202     // to map to a different correcting lut with neighboring srcI.
203     // See https://code.google.com/p/chromium/issues/detail?id=141425#c59 .
204     let dst = 1.0 - src;
205     let lin_dst = dst_convert.to_luma(dst);
206 
207     // Contrast value tapers off to 0 as the src luminance becomes white
208     let adjusted_contrast = contrast * lin_dst;
209 
210     // Remove discontinuity and instability when src is close to dst.
211     // The value 1/256 is arbitrary and appears to contain the instability.
212     if (src - dst).abs() < (1.0 / 256.0) {
213         let mut ii : f32 = 0.0;
214         for v in table.iter_mut() {
215             let raw_srca = ii / 255.0;
216             let srca = apply_contrast(raw_srca, adjusted_contrast);
217 
218             *v = round_to_u8(255.0 * srca);
219             ii += 1.0;
220         }
221     } else {
222         // Avoid slow int to float conversion.
223         let mut ii : f32 = 0.0;
224         for v in table.iter_mut() {
225             // 'raw_srca += 1.0f / 255.0f' and even
226             // 'raw_srca = i * (1.0f / 255.0f)' can add up to more than 1.0f.
227             // When this happens the table[255] == 0x0 instead of 0xff.
228             // See http://code.google.com/p/chromium/issues/detail?id=146466
229             let raw_srca = ii / 255.0;
230             let srca = apply_contrast(raw_srca, adjusted_contrast);
231             assert!(srca <= 1.0);
232             let dsta = 1.0 - srca;
233 
234             // Calculate the output we want.
235             let lin_out = lin_src * srca + dsta * lin_dst;
236             assert!(lin_out <= 1.0);
237             let out = dst_convert.from_luma(lin_out);
238 
239             // Undo what the blit blend will do.
240             // i.e. given the formula for OVER: out = src * result + (1 - result) * dst
241             // solving for result gives:
242             let result = (out - dst) / (src - dst);
243 
244             *v = round_to_u8(255.0 * result);
245             debug!("Setting {:?} to {:?}", ii as u8, *v);
246 
247             ii += 1.0;
248         }
249     }
250 }
251 
252 pub struct GammaLut {
253     tables: [[u8; 256]; 1 << LUM_BITS],
254     #[cfg(target_os="macos")]
255     cg_inverse_gamma: [u8; 256],
256 }
257 
258 impl GammaLut {
259     // Skia actually makes 9 gamma tables, then based on the luminance color,
260     // fetches the RGB gamma table for that color.
generate_tables(&mut self, contrast: f32, paint_gamma: f32, device_gamma: f32)261     fn generate_tables(&mut self, contrast: f32, paint_gamma: f32, device_gamma: f32) {
262         let paint_color_space = LuminanceColorSpace::new(paint_gamma);
263         let device_color_space = LuminanceColorSpace::new(device_gamma);
264 
265         for (i, entry) in self.tables.iter_mut().enumerate() {
266             let luminance = scale255(LUM_BITS, i as u8);
267             build_gamma_correcting_lut(entry,
268                                        luminance,
269                                        contrast,
270                                        paint_color_space,
271                                        device_color_space);
272         }
273     }
274 
table_count(&self) -> usize275     pub fn table_count(&self) -> usize {
276         self.tables.len()
277     }
278 
get_table(&self, color: u8) -> &[u8; 256]279     pub fn get_table(&self, color: u8) -> &[u8; 256] {
280         &self.tables[(color >> (8 - LUM_BITS)) as usize]
281     }
282 
new(contrast: f32, paint_gamma: f32, device_gamma: f32) -> GammaLut283     pub fn new(contrast: f32, paint_gamma: f32, device_gamma: f32) -> GammaLut {
284         #[cfg(target_os="macos")]
285         let mut table = GammaLut {
286             tables: [[0; 256]; 1 << LUM_BITS],
287             cg_inverse_gamma: get_inverse_gamma_table_coregraphics_smoothing(),
288         };
289         #[cfg(not(target_os="macos"))]
290         let mut table = GammaLut {
291             tables: [[0; 256]; 1 << LUM_BITS],
292         };
293 
294         table.generate_tables(contrast, paint_gamma, device_gamma);
295 
296         table
297     }
298 
299     // Assumes pixels are in BGRA format. Assumes pixel values are in linear space already.
preblend(&self, pixels: &mut [u8], color: ColorU)300     pub fn preblend(&self, pixels: &mut [u8], color: ColorU) {
301         let table_r = self.get_table(color.r);
302         let table_g = self.get_table(color.g);
303         let table_b = self.get_table(color.b);
304 
305         for pixel in pixels.chunks_mut(4) {
306             let (b, g, r) = (table_b[pixel[0] as usize], table_g[pixel[1] as usize], table_r[pixel[2] as usize]);
307             pixel[0] = b;
308             pixel[1] = g;
309             pixel[2] = r;
310             pixel[3] = max(max(b, g), r);
311         }
312     }
313 
314     // Assumes pixels are in BGRA format. Assumes pixel values are in linear space already.
preblend_scaled(&self, pixels: &mut [u8], color: ColorU, percent: u8)315     pub fn preblend_scaled(&self, pixels: &mut [u8], color: ColorU, percent: u8) {
316         if percent >= 100 {
317             self.preblend(pixels, color);
318             return;
319         }
320 
321         let table_r = self.get_table(color.r);
322         let table_g = self.get_table(color.g);
323         let table_b = self.get_table(color.b);
324         let scale = (percent as i32 * 256) / 100;
325 
326         for pixel in pixels.chunks_mut(4) {
327             let (mut b, g, mut r) = (
328                 table_b[pixel[0] as usize] as i32,
329                 table_g[pixel[1] as usize] as i32,
330                 table_r[pixel[2] as usize] as i32,
331             );
332             b = g + (((b - g) * scale) >> 8);
333             r = g + (((r - g) * scale) >> 8);
334             pixel[0] = b as u8;
335             pixel[1] = g as u8;
336             pixel[2] = r as u8;
337             pixel[3] = max(max(b, g), r) as u8;
338         }
339     }
340 
341     #[cfg(target_os="macos")]
coregraphics_convert_to_linear(&self, pixels: &mut [u8])342     pub fn coregraphics_convert_to_linear(&self, pixels: &mut [u8]) {
343         for pixel in pixels.chunks_mut(4) {
344             pixel[0] = self.cg_inverse_gamma[pixel[0] as usize];
345             pixel[1] = self.cg_inverse_gamma[pixel[1] as usize];
346             pixel[2] = self.cg_inverse_gamma[pixel[2] as usize];
347         }
348     }
349 
350     // Assumes pixels are in BGRA format. Assumes pixel values are in linear space already.
preblend_grayscale(&self, pixels: &mut [u8], color: ColorU)351     pub fn preblend_grayscale(&self, pixels: &mut [u8], color: ColorU) {
352         let table_g = self.get_table(color.g);
353 
354         for pixel in pixels.chunks_mut(4) {
355             let luminance = compute_luminance(pixel[2], pixel[1], pixel[0]);
356             let alpha = table_g[luminance as usize];
357             pixel[0] = alpha;
358             pixel[1] = alpha;
359             pixel[2] = alpha;
360             pixel[3] = alpha;
361         }
362     }
363 
364 } // end impl GammaLut
365 
366 #[cfg(test)]
367 mod tests {
368     use super::*;
369 
over(dst: u32, src: u32, alpha: u32) -> u32370     fn over(dst: u32, src: u32, alpha: u32) -> u32 {
371         (src * alpha + dst * (255 - alpha))/255
372     }
373 
overf(dst: f32, src: f32, alpha: f32) -> f32374     fn overf(dst: f32, src: f32, alpha: f32) -> f32 {
375         ((src * alpha + dst * (255. - alpha))/255.) as f32
376     }
377 
378 
absdiff(a: u32, b: u32) -> u32379     fn absdiff(a: u32, b: u32) -> u32 {
380         if a < b  { b - a } else { a - b }
381     }
382 
383     #[test]
gamma()384     fn gamma() {
385         let mut table = [0u8; 256];
386         let g = 2.0;
387         let space = LuminanceColorSpace::Gamma(g);
388         let mut src : u32 = 131;
389         while src < 256 {
390             build_gamma_correcting_lut(&mut table, src as u8, 0., space, space);
391             let mut max_diff = 0;
392             let mut dst = 0;
393             while dst < 256 {
394                 for alpha in 0u32..256 {
395                     let preblend = table[alpha as usize];
396                     let lin_dst = (dst as f32 / 255.).powf(g) * 255.;
397                     let lin_src = (src as f32 / 255.).powf(g) * 255.;
398 
399                     let preblend_result = over(dst, src, preblend as u32);
400                     let true_result = ((overf(lin_dst, lin_src, alpha as f32) / 255.).powf(1. / g) * 255.) as u32;
401                     let diff = absdiff(preblend_result, true_result);
402                     //println!("{} -- {} {} = {}", alpha, preblend_result, true_result, diff);
403                     max_diff = max(max_diff, diff);
404                 }
405 
406                 //println!("{} {} max {}", src, dst, max_diff);
407                 assert!(max_diff <= 33);
408                 dst += 1;
409 
410             }
411             src += 1;
412         }
413     }
414 } // end mod
415