cataract 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. checksums.yaml +7 -0
  2. data/.clang-tidy +30 -0
  3. data/.github/workflows/ci-macos.yml +12 -0
  4. data/.github/workflows/ci.yml +77 -0
  5. data/.github/workflows/test.yml +76 -0
  6. data/.gitignore +45 -0
  7. data/.overcommit.yml +38 -0
  8. data/.rubocop.yml +83 -0
  9. data/BENCHMARKS.md +201 -0
  10. data/CHANGELOG.md +1 -0
  11. data/Gemfile +27 -0
  12. data/LICENSE +21 -0
  13. data/RAGEL_MIGRATION.md +60 -0
  14. data/README.md +292 -0
  15. data/Rakefile +209 -0
  16. data/benchmarks/benchmark_harness.rb +193 -0
  17. data/benchmarks/benchmark_merging.rb +121 -0
  18. data/benchmarks/benchmark_optimization_comparison.rb +168 -0
  19. data/benchmarks/benchmark_parsing.rb +153 -0
  20. data/benchmarks/benchmark_ragel_removal.rb +56 -0
  21. data/benchmarks/benchmark_runner.rb +70 -0
  22. data/benchmarks/benchmark_serialization.rb +180 -0
  23. data/benchmarks/benchmark_shorthand.rb +109 -0
  24. data/benchmarks/benchmark_shorthand_expansion.rb +176 -0
  25. data/benchmarks/benchmark_specificity.rb +124 -0
  26. data/benchmarks/benchmark_string_allocation.rb +151 -0
  27. data/benchmarks/benchmark_stylesheet_to_s.rb +62 -0
  28. data/benchmarks/benchmark_to_s_cached.rb +55 -0
  29. data/benchmarks/benchmark_value_splitter.rb +54 -0
  30. data/benchmarks/benchmark_yjit.rb +158 -0
  31. data/benchmarks/benchmark_yjit_workers.rb +61 -0
  32. data/benchmarks/profile_to_s.rb +23 -0
  33. data/benchmarks/speedup_calculator.rb +83 -0
  34. data/benchmarks/system_metadata.rb +81 -0
  35. data/benchmarks/templates/benchmarks.md.erb +221 -0
  36. data/benchmarks/yjit_tests.rb +141 -0
  37. data/cataract.gemspec +34 -0
  38. data/cliff.toml +92 -0
  39. data/examples/color_conversion_visual_test/color_conversion_test.html +3603 -0
  40. data/examples/color_conversion_visual_test/generate.rb +202 -0
  41. data/examples/color_conversion_visual_test/template.html.erb +259 -0
  42. data/examples/css_analyzer/analyzer.rb +164 -0
  43. data/examples/css_analyzer/analyzers/base.rb +33 -0
  44. data/examples/css_analyzer/analyzers/colors.rb +133 -0
  45. data/examples/css_analyzer/analyzers/important.rb +88 -0
  46. data/examples/css_analyzer/analyzers/properties.rb +61 -0
  47. data/examples/css_analyzer/analyzers/specificity.rb +68 -0
  48. data/examples/css_analyzer/templates/report.html.erb +575 -0
  49. data/examples/css_analyzer.rb +69 -0
  50. data/examples/github_analysis.html +5343 -0
  51. data/ext/cataract/cataract.c +1086 -0
  52. data/ext/cataract/cataract.h +174 -0
  53. data/ext/cataract/css_parser.c +1435 -0
  54. data/ext/cataract/extconf.rb +48 -0
  55. data/ext/cataract/import_scanner.c +174 -0
  56. data/ext/cataract/merge.c +973 -0
  57. data/ext/cataract/shorthand_expander.c +902 -0
  58. data/ext/cataract/specificity.c +213 -0
  59. data/ext/cataract/value_splitter.c +116 -0
  60. data/ext/cataract_color/cataract_color.c +16 -0
  61. data/ext/cataract_color/color_conversion.c +1687 -0
  62. data/ext/cataract_color/color_conversion.h +136 -0
  63. data/ext/cataract_color/color_conversion_lab.c +571 -0
  64. data/ext/cataract_color/color_conversion_named.c +259 -0
  65. data/ext/cataract_color/color_conversion_oklab.c +547 -0
  66. data/ext/cataract_color/extconf.rb +23 -0
  67. data/ext/cataract_old/cataract.c +393 -0
  68. data/ext/cataract_old/cataract.h +250 -0
  69. data/ext/cataract_old/css_parser.c +933 -0
  70. data/ext/cataract_old/extconf.rb +67 -0
  71. data/ext/cataract_old/import_scanner.c +174 -0
  72. data/ext/cataract_old/merge.c +776 -0
  73. data/ext/cataract_old/shorthand_expander.c +902 -0
  74. data/ext/cataract_old/specificity.c +213 -0
  75. data/ext/cataract_old/stylesheet.c +290 -0
  76. data/ext/cataract_old/value_splitter.c +116 -0
  77. data/lib/cataract/at_rule.rb +97 -0
  78. data/lib/cataract/color_conversion.rb +18 -0
  79. data/lib/cataract/declarations.rb +332 -0
  80. data/lib/cataract/import_resolver.rb +210 -0
  81. data/lib/cataract/rule.rb +131 -0
  82. data/lib/cataract/stylesheet.rb +716 -0
  83. data/lib/cataract/stylesheet_scope.rb +257 -0
  84. data/lib/cataract/version.rb +5 -0
  85. data/lib/cataract.rb +107 -0
  86. data/lib/tasks/gem.rake +158 -0
  87. data/scripts/fuzzer/run.rb +828 -0
  88. data/scripts/fuzzer/worker.rb +99 -0
  89. data/scripts/generate_benchmarks_md.rb +155 -0
  90. metadata +135 -0
@@ -0,0 +1,547 @@
1
+ // color_conversion_oklab.c - Oklab color space conversions
2
+ //
3
+ // Implementation of Oklab color space conversions based on Björn Ottosson's work:
4
+ // https://bottosson.github.io/posts/oklab/
5
+ //
6
+ // ABOUT OKLAB:
7
+ // Oklab is a perceptually uniform color space designed for image processing.
8
+ // It predicts lightness (L), green-red axis (a), and blue-yellow axis (b) with
9
+ // better accuracy than existing alternatives like CIELAB. Key properties:
10
+ // - Perceptually uniform: equal distances in Oklab correspond to equal perceived differences
11
+ // - Better for color interpolation than HSL or RGB
12
+ // - Scale-invariant: changing exposure scales all coordinates proportionally
13
+ // - Improved hue prediction versus CIELAB or CIELUV
14
+ // - Simpler computation than CAM16-UCS while maintaining uniformity
15
+ //
16
+ // LICENSE NOTE:
17
+ // The reference C++ implementation from Björn Ottosson is provided under public domain
18
+ // with optional MIT licensing. This implementation is derived from that reference code.
19
+ //
20
+ // COLOR CONVERSION PIPELINE:
21
+ // Parse: oklab(L a b) → linear RGB → sRGB (0-255) → struct color_ir
22
+ // Format: struct color_ir → sRGB (0-255) → linear RGB → oklab(L a b)
23
+ //
24
+ // REFERENCES:
25
+ // - Main Oklab post: https://bottosson.github.io/posts/oklab/
26
+ // - Color processing context: https://bottosson.github.io/posts/colorwrong/
27
+ // - CSS Color Module Level 4: https://www.w3.org/TR/css-color-4/#ok-lab
28
+
29
+ #include "color_conversion.h"
30
+ #include <math.h>
31
+ #include <stdlib.h>
32
+ #include <ctype.h>
33
+
34
+ // Forward declarations for internal helpers
35
+ static void srgb_to_linear_rgb(int r, int g, int b, double *lr, double *lg, double *lb);
36
+ static void linear_rgb_to_srgb(double lr, double lg, double lb, int *r, int *g, int *b);
37
+ static void linear_rgb_to_oklab(double lr, double lg, double lb, double *L, double *a, double *b);
38
+ static void oklab_to_linear_rgb(double L, double a, double b, double *lr, double *lg, double *lb);
39
+ static void oklab_to_oklch(double L, double a, double b, double *out_L, double *out_C, double *out_H);
40
+ static void oklch_to_oklab(double L, double C, double H, double *out_L, double *out_a, double *out_b);
41
+ static double parse_float(const char **p, double percent_max);
42
+
43
+ // =============================================================================
44
+ // CONSTANTS
45
+ // =============================================================================
46
+
47
+ // sRGB gamma correction constants (IEC 61966-2-1:1999)
48
+ // These values appear multiple times in gamma correction functions
49
+ #define SRGB_GAMMA_THRESHOLD_INV 0.04045 // Inverse transform threshold
50
+ #define SRGB_GAMMA_THRESHOLD_FWD 0.0031308 // Forward transform threshold
51
+ #define SRGB_GAMMA_LINEAR_SLOPE 12.92 // Linear segment slope
52
+ #define SRGB_GAMMA_OFFSET 0.055 // Gamma function offset
53
+ #define SRGB_GAMMA_SCALE 1.055 // Gamma function scale (1 + offset)
54
+ #define SRGB_GAMMA_EXPONENT 2.4 // Gamma exponent
55
+
56
+ // OKLCh powerless hue threshold (per W3C CSS Color Module Level 4)
57
+ // When chroma is below this value, hue is considered "powerless" (missing)
58
+ #define OKLCH_CHROMA_EPSILON 0.000004
59
+
60
+ // =============================================================================
61
+ // GAMMA CORRECTION: sRGB ↔ Linear RGB
62
+ // =============================================================================
63
+ //
64
+ // From https://bottosson.github.io/posts/colorwrong/:
65
+ // "The sRGB standard defines a nonlinear transfer function that relates the
66
+ // numerical values in the image to the actual light intensity."
67
+ //
68
+ // The sRGB transfer function applies gamma correction (γ ≈ 2.2) to compress
69
+ // the dynamic range for better perceptual distribution in 8-bit storage.
70
+ // We must undo this before color space conversions.
71
+
72
+ // Convert sRGB (0-255) to linear RGB (0.0-1.0)
73
+ // Applies inverse gamma: removes the sRGB nonlinearity
74
+ static void srgb_to_linear_rgb(int r, int g, int b, double *lr, double *lg, double *lb) {
75
+ double rs = r / 255.0;
76
+ double gs = g / 255.0;
77
+ double bs = b / 255.0;
78
+
79
+ // sRGB inverse transfer function (IEC 61966-2-1:1999)
80
+ *lr = (rs <= SRGB_GAMMA_THRESHOLD_INV) ? rs / SRGB_GAMMA_LINEAR_SLOPE
81
+ : pow((rs + SRGB_GAMMA_OFFSET) / SRGB_GAMMA_SCALE, SRGB_GAMMA_EXPONENT);
82
+ *lg = (gs <= SRGB_GAMMA_THRESHOLD_INV) ? gs / SRGB_GAMMA_LINEAR_SLOPE
83
+ : pow((gs + SRGB_GAMMA_OFFSET) / SRGB_GAMMA_SCALE, SRGB_GAMMA_EXPONENT);
84
+ *lb = (bs <= SRGB_GAMMA_THRESHOLD_INV) ? bs / SRGB_GAMMA_LINEAR_SLOPE
85
+ : pow((bs + SRGB_GAMMA_OFFSET) / SRGB_GAMMA_SCALE, SRGB_GAMMA_EXPONENT);
86
+ }
87
+
88
+ // Convert linear RGB (0.0-1.0) to sRGB (0-255)
89
+ // Applies gamma: adds the sRGB nonlinearity
90
+ static void linear_rgb_to_srgb(double lr, double lg, double lb, int *r, int *g, int *b) {
91
+ // Clamp to valid range [0.0, 1.0]
92
+ if (lr < 0.0) lr = 0.0;
93
+ if (lr > 1.0) lr = 1.0;
94
+ if (lg < 0.0) lg = 0.0;
95
+ if (lg > 1.0) lg = 1.0;
96
+ if (lb < 0.0) lb = 0.0;
97
+ if (lb > 1.0) lb = 1.0;
98
+
99
+ // sRGB forward transfer function (IEC 61966-2-1:1999)
100
+ double rs = (lr <= SRGB_GAMMA_THRESHOLD_FWD) ? lr * SRGB_GAMMA_LINEAR_SLOPE
101
+ : SRGB_GAMMA_SCALE * pow(lr, 1.0/SRGB_GAMMA_EXPONENT) - SRGB_GAMMA_OFFSET;
102
+ double gs = (lg <= SRGB_GAMMA_THRESHOLD_FWD) ? lg * SRGB_GAMMA_LINEAR_SLOPE
103
+ : SRGB_GAMMA_SCALE * pow(lg, 1.0/SRGB_GAMMA_EXPONENT) - SRGB_GAMMA_OFFSET;
104
+ double bs = (lb <= SRGB_GAMMA_THRESHOLD_FWD) ? lb * SRGB_GAMMA_LINEAR_SLOPE
105
+ : SRGB_GAMMA_SCALE * pow(lb, 1.0/SRGB_GAMMA_EXPONENT) - SRGB_GAMMA_OFFSET;
106
+
107
+ // Convert to 0-255 range and round
108
+ *r = (int)(rs * 255.0 + 0.5);
109
+ *g = (int)(gs * 255.0 + 0.5);
110
+ *b = (int)(bs * 255.0 + 0.5);
111
+
112
+ // Clamp to valid byte range
113
+ if (*r < 0) *r = 0;
114
+ if (*r > 255) *r = 255;
115
+ if (*g < 0) *g = 0;
116
+ if (*g > 255) *g = 255;
117
+ if (*b < 0) *b = 0;
118
+ if (*b > 255) *b = 255;
119
+ }
120
+
121
+ // =============================================================================
122
+ // OKLAB CONVERSIONS: Linear RGB ↔ Oklab
123
+ // =============================================================================
124
+ //
125
+ // From https://bottosson.github.io/posts/oklab/:
126
+ // "Oklab is a perceptual color space that uses a cube root transfer function
127
+ // and optimized transformation matrices to achieve perceptual uniformity."
128
+ //
129
+ // ALGORITHM (Linear RGB → Oklab):
130
+ // 1. Convert linear RGB to LMS cone response using matrix M₁
131
+ // 2. Apply cube root nonlinearity: l' = ∛l, m' = ∛m, s' = ∛s
132
+ // 3. Transform to Lab coordinates using matrix M₂
133
+ //
134
+ // The matrices below are the optimized versions from the reference implementation
135
+ // (public domain / MIT licensed) at https://bottosson.github.io/posts/oklab/
136
+
137
+ // Convert linear RGB to Oklab
138
+ // Inputs: lr, lg, lb in range [0.0, 1.0]
139
+ // Outputs: L (lightness), a (green-red), b (blue-yellow)
140
+ static void linear_rgb_to_oklab(double lr, double lg, double lb, double *L, double *a, double *b) {
141
+ // Step 1: Linear RGB → LMS cone response (matrix M₁)
142
+ // Matrix coefficients from https://bottosson.github.io/posts/oklab/
143
+ // Transforms sRGB to LMS cone response (approximating human vision)
144
+ double l = lr * 0.4122214708 // M₁[0][0]
145
+ + lg * 0.5363325363 // M₁[0][1]
146
+ + lb * 0.0514459929; // M₁[0][2]
147
+ double m = lr * 0.2119034982 // M₁[1][0]
148
+ + lg * 0.6806995451 // M₁[1][1]
149
+ + lb * 0.1073969566; // M₁[1][2]
150
+ double s = lr * 0.0883024619 // M₁[2][0]
151
+ + lg * 0.2817188376 // M₁[2][1]
152
+ + lb * 0.6299787005; // M₁[2][2]
153
+
154
+ // Step 2: Apply cube root nonlinearity
155
+ // From the post: "The cube root is applied to make the space more perceptually uniform"
156
+ // Using cbrt() for better numerical accuracy than pow(x, 1.0/3.0)
157
+ double l_ = cbrt(l);
158
+ double m_ = cbrt(m);
159
+ double s_ = cbrt(s);
160
+
161
+ // Step 3: Transform to Lab coordinates (matrix M₂)
162
+ // Matrix coefficients from https://bottosson.github.io/posts/oklab/
163
+ // Final transformation to perceptually uniform Oklab coordinates
164
+ *L = l_ * 0.2104542553 // M₂[0][0]
165
+ + m_ * 0.7936177850 // M₂[0][1]
166
+ + s_ * -0.0040720468; // M₂[0][2]
167
+ *a = l_ * 1.9779984951 // M₂[1][0]
168
+ + m_ * -2.4285922050 // M₂[1][1]
169
+ + s_ * 0.4505937099; // M₂[1][2]
170
+ *b = l_ * 0.0259040371 // M₂[2][0]
171
+ + m_ * 0.7827717662 // M₂[2][1]
172
+ + s_ * -0.8086757660; // M₂[2][2]
173
+ }
174
+
175
+ // Convert Oklab to linear RGB (inverse of above)
176
+ // Inputs: L, a, b (Oklab coordinates)
177
+ // Outputs: lr, lg, lb in range [0.0, 1.0] (may exceed range, caller should clamp)
178
+ static void oklab_to_linear_rgb(double L, double a, double b, double *lr, double *lg, double *lb) {
179
+ // Step 1: Invert M₂ to get l', m', s' from Lab
180
+ // Inverse M₂ matrix coefficients from https://bottosson.github.io/posts/oklab/
181
+ double l_ = L + a * 0.3963377774 // M₂⁻¹[0][1]
182
+ + b * 0.2158037573; // M₂⁻¹[0][2]
183
+ double m_ = L + a * -0.1055613458 // M₂⁻¹[1][1]
184
+ + b * -0.0638541728; // M₂⁻¹[1][2]
185
+ double s_ = L + a * -0.0894841775 // M₂⁻¹[2][1]
186
+ + b * -1.2914855480; // M₂⁻¹[2][2]
187
+
188
+ // Step 2: Invert cube root (cube the values)
189
+ double l = l_ * l_ * l_;
190
+ double m = m_ * m_ * m_;
191
+ double s = s_ * s_ * s_;
192
+
193
+ // Step 3: Invert M₁ to get linear RGB from LMS
194
+ // Inverse M₁ matrix coefficients from https://bottosson.github.io/posts/oklab/
195
+ *lr = l * 4.0767416621 // M₁⁻¹[0][0]
196
+ + m * -3.3077115913 // M₁⁻¹[0][1]
197
+ + s * 0.2309699292; // M₁⁻¹[0][2]
198
+ *lg = l * -1.2684380046 // M₁⁻¹[1][0]
199
+ + m * 2.6097574011 // M₁⁻¹[1][1]
200
+ + s * -0.3413193965; // M₁⁻¹[1][2]
201
+ *lb = l * -0.0041960863 // M₁⁻¹[2][0]
202
+ + m * -0.7034186147 // M₁⁻¹[2][1]
203
+ + s * 1.7076147010; // M₁⁻¹[2][2]
204
+ }
205
+
206
+ // =============================================================================
207
+ // PARSING: oklab() CSS syntax → struct color_ir
208
+ // =============================================================================
209
+
210
+ // Parse a floating-point number from CSS (with optional percentage)
211
+ // Supports: integers, decimals, negative values, percentages
212
+ // percent_max: value that 100% maps to (1.0 for standard, 0.4 for chroma)
213
+ static double parse_float(const char **p, double percent_max) {
214
+ int sign = 1;
215
+ if (**p == '-') {
216
+ sign = -1;
217
+ (*p)++;
218
+ } else if (**p == '+') {
219
+ (*p)++;
220
+ }
221
+
222
+ double result = 0.0;
223
+
224
+ // Parse integer part
225
+ while (**p >= '0' && **p <= '9') {
226
+ result = result * 10.0 + (**p - '0');
227
+ (*p)++;
228
+ }
229
+
230
+ // Parse decimal part
231
+ if (**p == '.') {
232
+ (*p)++;
233
+ double fraction = 0.1;
234
+ while (**p >= '0' && **p <= '9') {
235
+ result += (**p - '0') * fraction;
236
+ fraction *= 0.1;
237
+ (*p)++;
238
+ }
239
+ }
240
+
241
+ // Check for percentage sign
242
+ if (**p == '%') {
243
+ (*p)++;
244
+ result = (result / 100.0) * percent_max;
245
+ }
246
+
247
+ return sign * result;
248
+ }
249
+
250
+ // =============================================================================
251
+ // OKLCH COORDINATE CONVERSION: Oklab (Cartesian) ↔ OKLCh (Cylindrical/Polar)
252
+ // =============================================================================
253
+ //
254
+ // OKLCh is the cylindrical/polar representation of Oklab, similar to how
255
+ // HSL relates to RGB. The conversion is straightforward:
256
+ //
257
+ // Oklab → OKLCh (Cartesian to Polar):
258
+ // L (lightness) stays the same
259
+ // C (chroma) = sqrt(a² + b²)
260
+ // H (hue) = atan2(b, a) converted to degrees
261
+ //
262
+ // OKLCh → Oklab (Polar to Cartesian):
263
+ // L (lightness) stays the same
264
+ // a = C * cos(H)
265
+ // b = C * sin(H)
266
+ //
267
+ // W3C Spec: https://www.w3.org/TR/css-color-4/#the-oklch-notation
268
+ // - L: 0% = 0.0, 100% = 1.0 (same as Oklab)
269
+ // - C: 0% = 0.0, 100% = 0.4 (chroma)
270
+ // - H: hue angle in degrees (0-360)
271
+ // - 0° = purplish red (positive a axis)
272
+ // - 90° = mustard yellow (positive b axis)
273
+ // - 180° = greenish cyan (negative a axis)
274
+ // - 270° = sky blue (negative b axis)
275
+ // - Powerless hue: when C ≤ 0.000004 (epsilon), hue is powerless
276
+
277
+ // Convert Oklab (L, a, b) to OKLCh (L, C, H)
278
+ static void oklab_to_oklch(double L, double a, double b, double *out_L, double *out_C, double *out_H) {
279
+ *out_L = L;
280
+ *out_C = sqrt(a * a + b * b);
281
+
282
+ // Calculate hue angle in degrees
283
+ // atan2 returns radians in range [-π, π]
284
+ double h_rad = atan2(b, a);
285
+ *out_H = h_rad * 180.0 / M_PI;
286
+
287
+ // Normalize to [0, 360) range
288
+ if (*out_H < 0.0) {
289
+ *out_H += 360.0;
290
+ }
291
+
292
+ // Per W3C spec: if chroma is very small (near zero), hue is powerless
293
+ // and should be treated as missing/0
294
+ if (*out_C <= OKLCH_CHROMA_EPSILON) {
295
+ *out_H = 0.0; // Powerless hue
296
+ }
297
+ }
298
+
299
+ // Convert OKLCh (L, C, H) to Oklab (L, a, b)
300
+ static void oklch_to_oklab(double L, double C, double H, double *out_L, double *out_a, double *out_b) {
301
+ *out_L = L;
302
+
303
+ // Clamp negative chroma to 0 (per W3C spec)
304
+ if (C < 0.0) {
305
+ C = 0.0;
306
+ }
307
+
308
+ // Convert hue angle from degrees to radians
309
+ double h_rad = H * M_PI / 180.0;
310
+
311
+ // Convert polar to Cartesian
312
+ *out_a = C * cos(h_rad);
313
+ *out_b = C * sin(h_rad);
314
+ }
315
+
316
+ // Parse oklab() CSS function into IR (sRGB 0-255)
317
+ // Syntax: oklab(L a b) or oklab(L a b / alpha)
318
+ // Example: oklab(0.628 0.225 0.126) or oklab(0.5 -0.1 0.2 / 0.8)
319
+ struct color_ir parse_oklab(VALUE oklab_value) {
320
+ struct color_ir color;
321
+ INIT_COLOR_IR(color);
322
+
323
+ const char *str = StringValueCStr(oklab_value);
324
+ const char *p = str;
325
+
326
+ // Skip "oklab("
327
+ while (*p && *p != '(') p++;
328
+ if (*p != '(') {
329
+ rb_raise(rb_eArgError, "Invalid oklab() syntax");
330
+ }
331
+ p++; // Skip '('
332
+
333
+ SKIP_WHITESPACE(p);
334
+
335
+ // Parse L (lightness): typically 0.0 to 1.0
336
+ double L = parse_float(&p, 1.0);
337
+ SKIP_WHITESPACE(p);
338
+
339
+ // Parse a (green-red axis): typically -0.4 to 0.4
340
+ double a = parse_float(&p, 1.0);
341
+ SKIP_WHITESPACE(p);
342
+
343
+ // Parse b (blue-yellow axis): typically -0.4 to 0.4
344
+ double b = parse_float(&p, 1.0);
345
+ SKIP_WHITESPACE(p);
346
+
347
+ // Check for alpha: oklab(L a b / alpha)
348
+ if (*p == '/') {
349
+ p++;
350
+ SKIP_WHITESPACE(p);
351
+ color.alpha = parse_float(&p, 1.0);
352
+ SKIP_WHITESPACE(p);
353
+ }
354
+
355
+ // Verify closing parenthesis
356
+ if (*p != ')') {
357
+ rb_raise(rb_eArgError, "Invalid oklab() syntax: missing closing parenthesis");
358
+ }
359
+
360
+ // Convert Oklab → linear RGB
361
+ double lr, lg, lb;
362
+ oklab_to_linear_rgb(L, a, b, &lr, &lg, &lb);
363
+
364
+ // Store high-precision linear RGB in IR (avoids quantization loss)
365
+ color.has_linear_rgb = 1;
366
+ color.linear_r = lr;
367
+ color.linear_g = lg;
368
+ color.linear_b = lb;
369
+
370
+ // Also populate sRGB (0-255) for compatibility with other formatters
371
+ linear_rgb_to_srgb(lr, lg, lb, &color.red, &color.green, &color.blue);
372
+
373
+ return color;
374
+ }
375
+
376
+ // =============================================================================
377
+ // FORMATTING: struct color_ir → oklab() CSS syntax
378
+ // =============================================================================
379
+
380
+ // Format IR as oklab() CSS function
381
+ // Syntax: oklab(L a b) or oklab(L a b / alpha)
382
+ // Prefers high-precision linear RGB if available to avoid quantization errors
383
+ VALUE format_oklab(struct color_ir color, int use_modern_syntax) {
384
+ double lr, lg, lb;
385
+
386
+ // Prefer linear RGB for precision if available
387
+ // Otherwise fall back to sRGB → linear RGB conversion
388
+ if (color.has_linear_rgb) {
389
+ // Use high-precision linear RGB directly (avoids sRGB quantization)
390
+ lr = color.linear_r;
391
+ lg = color.linear_g;
392
+ lb = color.linear_b;
393
+ } else {
394
+ // Convert sRGB (0-255) → linear RGB
395
+ srgb_to_linear_rgb(color.red, color.green, color.blue, &lr, &lg, &lb);
396
+ }
397
+
398
+ // Convert linear RGB → Oklab
399
+ double L, a, b;
400
+ linear_rgb_to_oklab(lr, lg, lb, &L, &a, &b);
401
+
402
+ char buf[128];
403
+
404
+ if (color.alpha >= 0.0) {
405
+ // With alpha: oklab(L a b / alpha)
406
+ FORMAT_OKLAB_ALPHA(buf, L, a, b, color.alpha);
407
+ } else {
408
+ // No alpha: oklab(L a b)
409
+ FORMAT_OKLAB(buf, L, a, b);
410
+ }
411
+
412
+ return rb_str_new_cstr(buf);
413
+ }
414
+
415
+ // =============================================================================
416
+ // OKLCH PARSING AND FORMATTING
417
+ // =============================================================================
418
+
419
+ // Parse oklch() CSS function into IR (sRGB 0-255)
420
+ // Syntax: oklch(L C H) or oklch(L C H / alpha)
421
+ // Example: oklch(51.975% 0.17686 142.495) or oklch(50% 0.2 270 / 0.8)
422
+ //
423
+ // Per W3C spec:
424
+ // - L: 0% = 0.0, 100% = 1.0
425
+ // - C: 0% = 0.0, 100% = 0.4 (chroma), negative values clamped to 0
426
+ // - H: hue angle in degrees, normalized to [0, 360)
427
+ // - Alpha: 0-1.0 or percentage
428
+ struct color_ir parse_oklch(VALUE oklch_value) {
429
+ struct color_ir color;
430
+ INIT_COLOR_IR(color);
431
+
432
+ const char *str = StringValueCStr(oklch_value);
433
+ const char *p = str;
434
+
435
+ // Skip whitespace
436
+ while (*p == ' ' || *p == '\t') p++;
437
+
438
+ // Expect "oklch("
439
+ if (!(p[0] == 'o' && p[1] == 'k' && p[2] == 'l' && p[3] == 'c' && p[4] == 'h' && p[5] == '(')) {
440
+ rb_raise(rb_eArgError, "Invalid oklch() syntax: expected 'oklch(', got '%s'", str);
441
+ }
442
+ p += 6;
443
+
444
+ // Skip whitespace
445
+ while (*p == ' ' || *p == '\t') p++;
446
+
447
+ // Parse L (lightness): 0-1.0 or percentage
448
+ const char *before_L = p;
449
+ double L = parse_float(&p, 1.0);
450
+ if (p == before_L) {
451
+ rb_raise(rb_eArgError, "Invalid oklch() syntax: missing lightness value in '%s'", str);
452
+ }
453
+ while (*p == ' ' || *p == '\t') p++;
454
+
455
+ // Parse C (chroma): 0-0.4 or percentage (100% = 0.4)
456
+ const char *before_C = p;
457
+ double C = parse_float(&p, 0.4);
458
+ if (p == before_C) {
459
+ rb_raise(rb_eArgError, "Invalid oklch() syntax: missing chroma value in '%s'", str);
460
+ }
461
+ while (*p == ' ' || *p == '\t' || *p == ',') p++;
462
+
463
+ // Parse H (hue): angle in degrees
464
+ const char *before_H = p;
465
+ double H = parse_float(&p, 1.0);
466
+ if (p == before_H) {
467
+ rb_raise(rb_eArgError, "Invalid oklch() syntax: missing hue value in '%s'", str);
468
+ }
469
+ // Normalize hue to [0, 360) range
470
+ H = fmod(H, 360.0);
471
+ if (H < 0.0) {
472
+ H += 360.0;
473
+ }
474
+
475
+ // Skip whitespace
476
+ while (*p == ' ' || *p == '\t') p++;
477
+
478
+ // Check for alpha channel (slash separator)
479
+ if (*p == '/') {
480
+ p++;
481
+ while (*p == ' ' || *p == '\t') p++;
482
+ color.alpha = parse_float(&p, 1.0);
483
+ while (*p == ' ' || *p == '\t') p++;
484
+ }
485
+
486
+ // Expect closing paren
487
+ if (*p != ')') {
488
+ rb_raise(rb_eArgError, "Invalid oklch() syntax: missing closing parenthesis in '%s'", str);
489
+ }
490
+
491
+ // Convert OKLCh → Oklab
492
+ double oklab_L, oklab_a, oklab_b;
493
+ oklch_to_oklab(L, C, H, &oklab_L, &oklab_a, &oklab_b);
494
+
495
+ // Convert Oklab → linear RGB → sRGB
496
+ double lr, lg, lb;
497
+ oklab_to_linear_rgb(oklab_L, oklab_a, oklab_b, &lr, &lg, &lb);
498
+ linear_rgb_to_srgb(lr, lg, lb, &color.red, &color.green, &color.blue);
499
+
500
+ // Store linear RGB for precision (in case we convert back to oklch/oklab later)
501
+ color.has_linear_rgb = 1;
502
+ color.linear_r = lr;
503
+ color.linear_g = lg;
504
+ color.linear_b = lb;
505
+
506
+ RB_GC_GUARD(oklch_value);
507
+ return color;
508
+ }
509
+
510
+ // Format IR (sRGB 0-255) as oklch() CSS function
511
+ // Returns: oklch(L C H) or oklch(L C H / alpha)
512
+ // use_modern_syntax parameter is ignored (oklch always uses modern syntax)
513
+ VALUE format_oklch(struct color_ir color, int use_modern_syntax) {
514
+ (void)use_modern_syntax; // Unused, oklch always uses modern syntax
515
+
516
+ double lr, lg, lb;
517
+
518
+ // Use high-precision linear RGB if available
519
+ if (color.has_linear_rgb) {
520
+ lr = color.linear_r;
521
+ lg = color.linear_g;
522
+ lb = color.linear_b;
523
+ } else {
524
+ // Convert sRGB (0-255) → linear RGB
525
+ srgb_to_linear_rgb(color.red, color.green, color.blue, &lr, &lg, &lb);
526
+ }
527
+
528
+ // Convert linear RGB → Oklab
529
+ double oklab_L, oklab_a, oklab_b;
530
+ linear_rgb_to_oklab(lr, lg, lb, &oklab_L, &oklab_a, &oklab_b);
531
+
532
+ // Convert Oklab → OKLCh
533
+ double L, C, H;
534
+ oklab_to_oklch(oklab_L, oklab_a, oklab_b, &L, &C, &H);
535
+
536
+ char buf[128];
537
+
538
+ if (color.alpha >= 0.0) {
539
+ // With alpha: oklch(L C H / alpha)
540
+ snprintf(buf, sizeof(buf), "oklch(%.4f %.4f %.3f / %.2f)", L, C, H, color.alpha);
541
+ } else {
542
+ // No alpha: oklch(L C H)
543
+ snprintf(buf, sizeof(buf), "oklch(%.4f %.4f %.3f)", L, C, H);
544
+ }
545
+
546
+ return rb_str_new_cstr(buf);
547
+ }
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mkmf'
4
+
5
+ # Add include path for cataract.h from main extension
6
+ $INCFLAGS << ' -I$(srcdir)/../cataract'
7
+
8
+ # Color conversion extension - separate from core parser
9
+ # Compile C files:
10
+ # - cataract_color.c (extension entry point)
11
+ # - color_conversion.c (main conversion dispatcher)
12
+ # - color_conversion_oklab.c (Oklab color space conversions)
13
+ # - color_conversion_lab.c (CIE L*a*b* color space conversions)
14
+ # - color_conversion_named.c (CSS named colors)
15
+ $objs = ['cataract_color.o', 'color_conversion.o', 'color_conversion_oklab.o', 'color_conversion_lab.o',
16
+ 'color_conversion_named.o']
17
+
18
+ # Suppress warnings
19
+ $CFLAGS << ' -Wno-unused-const-variable' if RUBY_PLATFORM.match?(/darwin|linux/)
20
+ $CFLAGS << ' -Wno-shorten-64-to-32' if RUBY_PLATFORM.include?('darwin')
21
+ $CFLAGS << ' -Wno-unused-variable'
22
+
23
+ create_makefile('cataract/cataract_color')