Source code

Revision control

Other Tools

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.
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.
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(target_os="macos")]
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
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.
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.
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.
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
275
pub fn table_count(&self) -> usize {
276
self.tables.len()
277
}
278
279
pub fn get_table(&self, color: u8) -> &[u8; 256] {
280
&self.tables[(color >> (8 - LUM_BITS)) as usize]
281
}
282
283
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.
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.
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")]
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.
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
370
fn over(dst: u32, src: u32, alpha: u32) -> u32 {
371
(src * alpha + dst * (255 - alpha))/255
372
}
373
374
fn overf(dst: f32, src: f32, alpha: f32) -> f32 {
375
((src * alpha + dst * (255. - alpha))/255.) as f32
376
}
377
378
379
fn absdiff(a: u32, b: u32) -> u32 {
380
if a < b { b - a } else { a - b }
381
}
382
383
#[test]
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