summaryrefslogtreecommitdiffstats
path: root/dom/webgpu/tests/cts/checkout/src/webgpu/util/color_space_conversion.ts
blob: 4ab3679b23cc0b9736cecc23a1fad1969d4192db (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
import { assert, unreachable } from '../../common/util/util.js';

import { multiplyMatrices } from './math.js';

// These color space conversion function definitions are copied directly from
// CSS Color Module Level 4 Sample Code: https://drafts.csswg.org/css-color/#color-conversion-code
// *EXCEPT* the conversion matrices are replaced with exact rational forms computed here:
// https://github.com/kainino0x/exact_css_xyz_matrices
//   using this Rust crate: https://crates.io/crates/rgb_derivation
//   as described for sRGB on this page: https://mina86.com/2019/srgb-xyz-matrix/
//   but using the numbers from the CSS spec: https://www.w3.org/TR/css-color-4/#predefined

// Sample code for color conversions
// Conversion can also be done using ICC profiles and a Color Management System
// For clarity, a library is used for matrix multiplication (multiply-matrices.js)

// sRGB-related functions

/**
 * convert an array of sRGB values
 * where in-gamut values are in the range [0 - 1]
 * to linear light (un-companded) form.
 * https://en.wikipedia.org/wiki/SRGB
 * Extended transfer function:
 * for negative values,  linear portion is extended on reflection of axis,
 * then reflected power function is used.
 */
function lin_sRGB(RGB: Array<number>) {
  return RGB.map(val => {
    const sign = val < 0 ? -1 : 1;
    const abs = Math.abs(val);

    if (abs < 0.04045) {
      return val / 12.92;
    }

    return sign * Math.pow((abs + 0.055) / 1.055, 2.4);
  });
}

/**
 * convert an array of linear-light sRGB values in the range 0.0-1.0
 * to gamma corrected form
 * https://en.wikipedia.org/wiki/SRGB
 * Extended transfer function:
 * For negative values, linear portion extends on reflection
 * of axis, then uses reflected pow below that
 */
function gam_sRGB(RGB: Array<number>) {
  return RGB.map(val => {
    const sign = val < 0 ? -1 : 1;
    const abs = Math.abs(val);

    if (abs > 0.0031308) {
      return sign * (1.055 * Math.pow(abs, 1 / 2.4) - 0.055);
    }

    return 12.92 * val;
  });
}

/**
 * convert an array of linear-light sRGB values to CIE XYZ
 * using sRGB's own white, D65 (no chromatic adaptation)
 */
function lin_sRGB_to_XYZ(rgb: Array<Array<number>>) {
  /* prettier-ignore */
  const M = [
    [506752 / 1228815,  87881 / 245763,   12673 /   70218],
    [ 87098 /  409605, 175762 / 245763,   12673 /  175545],
    [  7918 /  409605,  87881 / 737289, 1001167 / 1053270],
  ];
  return multiplyMatrices(M, rgb);
}

/**
 * convert XYZ to linear-light sRGB
 * using sRGB's own white, D65 (no chromatic adaptation)
 */
function XYZ_to_lin_sRGB(XYZ: Array<Array<number>>) {
  /* prettier-ignore */
  const M = [
    [  12831 /   3959,    -329 /    214, -1974 /   3959],
    [-851781 / 878810, 1648619 / 878810, 36519 / 878810],
    [    705 /  12673,   -2585 /  12673,   705 /    667],
  ];

  return multiplyMatrices(M, XYZ);
}

//  display-p3-related functions

/**
 * convert an array of display-p3 RGB values in the range 0.0 - 1.0
 * to linear light (un-companded) form.
 */
function lin_P3(RGB: Array<number>) {
  return lin_sRGB(RGB); // same as sRGB
}

/**
 * convert an array of linear-light display-p3 RGB  in the range 0.0-1.0
 * to gamma corrected form
 */
function gam_P3(RGB: Array<number>) {
  return gam_sRGB(RGB); // same as sRGB
}

/**
 * convert an array of linear-light display-p3 values to CIE XYZ
 * using display-p3's D65 (no chromatic adaptation)
 */
function lin_P3_to_XYZ(rgb: Array<Array<number>>) {
  /* prettier-ignore */
  const M = [
    [608311 / 1250200, 189793 / 714400,  198249 / 1000160],
    [ 35783 /  156275, 247089 / 357200,  198249 / 2500400],
    [     0 /       1,  32229 / 714400, 5220557 / 5000800],
  ];

  return multiplyMatrices(M, rgb);
}

/**
 * convert XYZ to linear-light P3
 * using display-p3's own white, D65 (no chromatic adaptation)
 */
function XYZ_to_lin_P3(XYZ: Array<Array<number>>) {
  /* prettier-ignore */
  const M = [
    [446124 / 178915, -333277 / 357830, -72051 / 178915],
    [-14852 /  17905,   63121 /  35810,    423 /  17905],
    [ 11844 / 330415,  -50337 / 660830, 316169 / 330415],
  ];

  return multiplyMatrices(M, XYZ);
}

/**
 * @returns the converted pixels in `{R: number, G: number, B: number, A: number}`.
 *
 * Follow conversion steps in CSS Color Module Level 4
 * https://drafts.csswg.org/css-color/#predefined-to-predefined
 * display-p3 and sRGB share the same white points.
 */
export function displayP3ToSrgb(pixel: Readonly<RGBA>): RGBA {
  assert(
    pixel.R !== undefined && pixel.G !== undefined && pixel.B !== undefined,
    'color space conversion requires all of R, G and B components'
  );

  let rgbVec = [pixel.R, pixel.G, pixel.B];
  rgbVec = lin_P3(rgbVec);
  let rgbMatrix = [[rgbVec[0]], [rgbVec[1]], [rgbVec[2]]];
  rgbMatrix = XYZ_to_lin_sRGB(lin_P3_to_XYZ(rgbMatrix));
  rgbVec = [rgbMatrix[0][0], rgbMatrix[1][0], rgbMatrix[2][0]];
  rgbVec = gam_sRGB(rgbVec);

  return { R: rgbVec[0], G: rgbVec[1], B: rgbVec[2], A: pixel.A };
}
/**
 * @returns the converted pixels in `{R: number, G: number, B: number, A: number}`.
 *
 * Follow conversion steps in CSS Color Module Level 4
 * https://drafts.csswg.org/css-color/#predefined-to-predefined
 * display-p3 and sRGB share the same white points.
 */
export function srgbToDisplayP3(pixel: Readonly<RGBA>): RGBA {
  assert(
    pixel.R !== undefined && pixel.G !== undefined && pixel.B !== undefined,
    'color space conversion requires all of R, G and B components'
  );

  let rgbVec = [pixel.R, pixel.G, pixel.B];
  rgbVec = lin_sRGB(rgbVec);
  let rgbMatrix = [[rgbVec[0]], [rgbVec[1]], [rgbVec[2]]];
  rgbMatrix = XYZ_to_lin_P3(lin_sRGB_to_XYZ(rgbMatrix));
  rgbVec = [rgbMatrix[0][0], rgbMatrix[1][0], rgbMatrix[2][0]];
  rgbVec = gam_P3(rgbVec);

  return { R: rgbVec[0], G: rgbVec[1], B: rgbVec[2], A: pixel.A };
}

export type RGBA = { R: number; G: number; B: number; A: number };
type InPlaceColorConversion = (rgba: {
  R: number;
  G: number;
  B: number;
  readonly A: number; // Alpha never changes during a conversion.
}) => void;

/**
 * Returns a function which applies the specified colorspace/premultiplication conversion.
 * Does not clamp, so may return values outside of the `dstColorSpace` gamut, due to either
 * color space conversion or alpha premultiplication.
 */
export function makeInPlaceColorConversion({
  srcPremultiplied,
  dstPremultiplied,
  srcColorSpace = 'srgb',
  dstColorSpace = 'srgb',
}: {
  srcPremultiplied: boolean;
  dstPremultiplied: boolean;
  srcColorSpace?: PredefinedColorSpace;
  dstColorSpace?: PredefinedColorSpace;
}): InPlaceColorConversion {
  const requireColorSpaceConversion = srcColorSpace !== dstColorSpace;
  const requireUnpremultiplyAlpha =
    srcPremultiplied && (requireColorSpaceConversion || srcPremultiplied !== dstPremultiplied);
  const requirePremultiplyAlpha =
    dstPremultiplied && (requireColorSpaceConversion || srcPremultiplied !== dstPremultiplied);

  return rgba => {
    assert(rgba.A >= 0.0 && rgba.A <= 1.0, 'rgba.A out of bounds');

    if (requireUnpremultiplyAlpha) {
      if (rgba.A !== 0.0) {
        rgba.R /= rgba.A;
        rgba.G /= rgba.A;
        rgba.B /= rgba.A;
      } else {
        assert(
          rgba.R === 0.0 && rgba.G === 0.0 && rgba.B === 0.0 && rgba.A === 0.0,
          'Unpremultiply ops with alpha value 0.0 requires all channels equals to 0.0'
        );
      }
    }
    // It's possible RGB are now > 1.
    // This technically represents colors outside the src gamut, so no clamping yet.

    if (requireColorSpaceConversion) {
      if (srcColorSpace === 'display-p3' && dstColorSpace === 'srgb') {
        Object.assign(rgba, displayP3ToSrgb(rgba));
      } else if (srcColorSpace === 'srgb' && dstColorSpace === 'display-p3') {
        Object.assign(rgba, srgbToDisplayP3(rgba));
      } else {
        unreachable();
      }
    }
    // Now RGB may also be negative if the src gamut is larger than the dst gamut.

    if (requirePremultiplyAlpha) {
      rgba.R *= rgba.A;
      rgba.G *= rgba.A;
      rgba.B *= rgba.A;
    }
  };
}