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.
- checksums.yaml +7 -0
- data/.clang-tidy +30 -0
- data/.github/workflows/ci-macos.yml +12 -0
- data/.github/workflows/ci.yml +77 -0
- data/.github/workflows/test.yml +76 -0
- data/.gitignore +45 -0
- data/.overcommit.yml +38 -0
- data/.rubocop.yml +83 -0
- data/BENCHMARKS.md +201 -0
- data/CHANGELOG.md +1 -0
- data/Gemfile +27 -0
- data/LICENSE +21 -0
- data/RAGEL_MIGRATION.md +60 -0
- data/README.md +292 -0
- data/Rakefile +209 -0
- data/benchmarks/benchmark_harness.rb +193 -0
- data/benchmarks/benchmark_merging.rb +121 -0
- data/benchmarks/benchmark_optimization_comparison.rb +168 -0
- data/benchmarks/benchmark_parsing.rb +153 -0
- data/benchmarks/benchmark_ragel_removal.rb +56 -0
- data/benchmarks/benchmark_runner.rb +70 -0
- data/benchmarks/benchmark_serialization.rb +180 -0
- data/benchmarks/benchmark_shorthand.rb +109 -0
- data/benchmarks/benchmark_shorthand_expansion.rb +176 -0
- data/benchmarks/benchmark_specificity.rb +124 -0
- data/benchmarks/benchmark_string_allocation.rb +151 -0
- data/benchmarks/benchmark_stylesheet_to_s.rb +62 -0
- data/benchmarks/benchmark_to_s_cached.rb +55 -0
- data/benchmarks/benchmark_value_splitter.rb +54 -0
- data/benchmarks/benchmark_yjit.rb +158 -0
- data/benchmarks/benchmark_yjit_workers.rb +61 -0
- data/benchmarks/profile_to_s.rb +23 -0
- data/benchmarks/speedup_calculator.rb +83 -0
- data/benchmarks/system_metadata.rb +81 -0
- data/benchmarks/templates/benchmarks.md.erb +221 -0
- data/benchmarks/yjit_tests.rb +141 -0
- data/cataract.gemspec +34 -0
- data/cliff.toml +92 -0
- data/examples/color_conversion_visual_test/color_conversion_test.html +3603 -0
- data/examples/color_conversion_visual_test/generate.rb +202 -0
- data/examples/color_conversion_visual_test/template.html.erb +259 -0
- data/examples/css_analyzer/analyzer.rb +164 -0
- data/examples/css_analyzer/analyzers/base.rb +33 -0
- data/examples/css_analyzer/analyzers/colors.rb +133 -0
- data/examples/css_analyzer/analyzers/important.rb +88 -0
- data/examples/css_analyzer/analyzers/properties.rb +61 -0
- data/examples/css_analyzer/analyzers/specificity.rb +68 -0
- data/examples/css_analyzer/templates/report.html.erb +575 -0
- data/examples/css_analyzer.rb +69 -0
- data/examples/github_analysis.html +5343 -0
- data/ext/cataract/cataract.c +1086 -0
- data/ext/cataract/cataract.h +174 -0
- data/ext/cataract/css_parser.c +1435 -0
- data/ext/cataract/extconf.rb +48 -0
- data/ext/cataract/import_scanner.c +174 -0
- data/ext/cataract/merge.c +973 -0
- data/ext/cataract/shorthand_expander.c +902 -0
- data/ext/cataract/specificity.c +213 -0
- data/ext/cataract/value_splitter.c +116 -0
- data/ext/cataract_color/cataract_color.c +16 -0
- data/ext/cataract_color/color_conversion.c +1687 -0
- data/ext/cataract_color/color_conversion.h +136 -0
- data/ext/cataract_color/color_conversion_lab.c +571 -0
- data/ext/cataract_color/color_conversion_named.c +259 -0
- data/ext/cataract_color/color_conversion_oklab.c +547 -0
- data/ext/cataract_color/extconf.rb +23 -0
- data/ext/cataract_old/cataract.c +393 -0
- data/ext/cataract_old/cataract.h +250 -0
- data/ext/cataract_old/css_parser.c +933 -0
- data/ext/cataract_old/extconf.rb +67 -0
- data/ext/cataract_old/import_scanner.c +174 -0
- data/ext/cataract_old/merge.c +776 -0
- data/ext/cataract_old/shorthand_expander.c +902 -0
- data/ext/cataract_old/specificity.c +213 -0
- data/ext/cataract_old/stylesheet.c +290 -0
- data/ext/cataract_old/value_splitter.c +116 -0
- data/lib/cataract/at_rule.rb +97 -0
- data/lib/cataract/color_conversion.rb +18 -0
- data/lib/cataract/declarations.rb +332 -0
- data/lib/cataract/import_resolver.rb +210 -0
- data/lib/cataract/rule.rb +131 -0
- data/lib/cataract/stylesheet.rb +716 -0
- data/lib/cataract/stylesheet_scope.rb +257 -0
- data/lib/cataract/version.rb +5 -0
- data/lib/cataract.rb +107 -0
- data/lib/tasks/gem.rake +158 -0
- data/scripts/fuzzer/run.rb +828 -0
- data/scripts/fuzzer/worker.rb +99 -0
- data/scripts/generate_benchmarks_md.rb +155 -0
- 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')
|