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,571 @@
|
|
|
1
|
+
// color_conversion_lab.c - CIE L*a*b* and LCH color space conversions
|
|
2
|
+
//
|
|
3
|
+
// Implementation of CIE 1976 (L*, a*, b*) and LCH color space conversions.
|
|
4
|
+
//
|
|
5
|
+
// ABOUT LAB:
|
|
6
|
+
// CIE L*a*b* (CIELAB) is a color space designed to be perceptually uniform,
|
|
7
|
+
// meaning that a change of the same amount in a color value should produce
|
|
8
|
+
// a change of about the same visual importance. Key properties:
|
|
9
|
+
// - Device-independent (unlike RGB which depends on display characteristics)
|
|
10
|
+
// - L* represents lightness (0 = black, 100 = white)
|
|
11
|
+
// - a* represents green-red axis (negative = green, positive = red)
|
|
12
|
+
// - b* represents blue-yellow axis (negative = blue, positive = yellow)
|
|
13
|
+
// - Covers entire range of human color perception
|
|
14
|
+
//
|
|
15
|
+
// ABOUT LCH:
|
|
16
|
+
// CIE LCH (cylindrical Lab) uses polar coordinates for the same color space:
|
|
17
|
+
// - L (lightness) is identical to Lab
|
|
18
|
+
// - C (chroma) = sqrt(a² + b²), represents "amount of color"
|
|
19
|
+
// - H (hue) = atan2(b, a), hue angle in degrees
|
|
20
|
+
// LCH is often more intuitive than Lab for color manipulation.
|
|
21
|
+
//
|
|
22
|
+
// COLOR CONVERSION PIPELINE:
|
|
23
|
+
// Parse: lab(L a b) → XYZ → linear RGB → sRGB (0-255) → struct color_ir
|
|
24
|
+
// lch(L C H) → Lab → XYZ → linear RGB → sRGB (0-255) → struct color_ir
|
|
25
|
+
// Format: struct color_ir → sRGB (0-255) → linear RGB → XYZ → lab(L a b)
|
|
26
|
+
// struct color_ir → sRGB (0-255) → linear RGB → XYZ → Lab → lch(L C H)
|
|
27
|
+
//
|
|
28
|
+
// REFERENCES:
|
|
29
|
+
// - CSS Color Module Level 4: https://www.w3.org/TR/css-color-4/#lab-colors
|
|
30
|
+
// - CIE 1976 L*a*b*: https://en.wikipedia.org/wiki/CIELAB_color_space
|
|
31
|
+
// - Bruce Lindbloom: http://www.brucelindbloom.com/
|
|
32
|
+
|
|
33
|
+
#include "color_conversion.h"
|
|
34
|
+
#include <math.h>
|
|
35
|
+
#include <stdlib.h>
|
|
36
|
+
#include <ctype.h>
|
|
37
|
+
|
|
38
|
+
// Forward declarations for internal helpers
|
|
39
|
+
static void srgb_to_linear_rgb(int r, int g, int b, double *lr, double *lg, double *lb);
|
|
40
|
+
static void linear_rgb_to_srgb(double lr, double lg, double lb, int *r, int *g, int *b);
|
|
41
|
+
static void linear_rgb_to_xyz_d65(double lr, double lg, double lb, double *x, double *y, double *z);
|
|
42
|
+
static void xyz_d65_to_linear_rgb(double x, double y, double z, double *lr, double *lg, double *lb);
|
|
43
|
+
static void xyz_d50_to_d65(double x50, double y50, double z50, double *x65, double *y65, double *z65);
|
|
44
|
+
static void xyz_d65_to_d50(double x65, double y65, double z65, double *x50, double *y50, double *z50);
|
|
45
|
+
static void xyz_to_lab(double x, double y, double z, double *L, double *a, double *b);
|
|
46
|
+
static void lab_to_xyz(double L, double a, double b, double *x, double *y, double *z);
|
|
47
|
+
static void lab_to_lch(double L, double a, double b, double *out_L, double *out_C, double *out_H);
|
|
48
|
+
static void lch_to_lab(double L, double C, double H, double *out_L, double *out_a, double *out_b);
|
|
49
|
+
static double parse_float(const char **p, double percent_max);
|
|
50
|
+
|
|
51
|
+
// =============================================================================
|
|
52
|
+
// GAMMA CORRECTION: sRGB ↔ Linear RGB
|
|
53
|
+
// =============================================================================
|
|
54
|
+
|
|
55
|
+
// sRGB gamma correction constants (IEC 61966-2-1:1999)
|
|
56
|
+
#define SRGB_GAMMA_THRESHOLD_INV 0.04045 // Inverse transform threshold
|
|
57
|
+
#define SRGB_GAMMA_THRESHOLD_FWD 0.0031308 // Forward transform threshold
|
|
58
|
+
#define SRGB_GAMMA_LINEAR_SLOPE 12.92 // Linear segment slope
|
|
59
|
+
#define SRGB_GAMMA_OFFSET 0.055 // Gamma function offset
|
|
60
|
+
#define SRGB_GAMMA_SCALE 1.055 // Gamma function scale (1 + offset)
|
|
61
|
+
#define SRGB_GAMMA_EXPONENT 2.4 // Gamma exponent
|
|
62
|
+
|
|
63
|
+
// Convert sRGB (0-255) to linear RGB (0.0-1.0)
|
|
64
|
+
// Applies inverse gamma: removes the sRGB nonlinearity
|
|
65
|
+
static void srgb_to_linear_rgb(int r, int g, int b, double *lr, double *lg, double *lb) {
|
|
66
|
+
double rs = r / 255.0;
|
|
67
|
+
double gs = g / 255.0;
|
|
68
|
+
double bs = b / 255.0;
|
|
69
|
+
|
|
70
|
+
// sRGB inverse transfer function (IEC 61966-2-1:1999)
|
|
71
|
+
*lr = (rs <= SRGB_GAMMA_THRESHOLD_INV) ? rs / SRGB_GAMMA_LINEAR_SLOPE
|
|
72
|
+
: pow((rs + SRGB_GAMMA_OFFSET) / SRGB_GAMMA_SCALE, SRGB_GAMMA_EXPONENT);
|
|
73
|
+
*lg = (gs <= SRGB_GAMMA_THRESHOLD_INV) ? gs / SRGB_GAMMA_LINEAR_SLOPE
|
|
74
|
+
: pow((gs + SRGB_GAMMA_OFFSET) / SRGB_GAMMA_SCALE, SRGB_GAMMA_EXPONENT);
|
|
75
|
+
*lb = (bs <= SRGB_GAMMA_THRESHOLD_INV) ? bs / SRGB_GAMMA_LINEAR_SLOPE
|
|
76
|
+
: pow((bs + SRGB_GAMMA_OFFSET) / SRGB_GAMMA_SCALE, SRGB_GAMMA_EXPONENT);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Convert linear RGB (0.0-1.0) to sRGB (0-255)
|
|
80
|
+
// Applies gamma: adds the sRGB nonlinearity
|
|
81
|
+
static void linear_rgb_to_srgb(double lr, double lg, double lb, int *r, int *g, int *b) {
|
|
82
|
+
// Clamp to valid range [0.0, 1.0]
|
|
83
|
+
if (lr < 0.0) lr = 0.0;
|
|
84
|
+
if (lr > 1.0) lr = 1.0;
|
|
85
|
+
if (lg < 0.0) lg = 0.0;
|
|
86
|
+
if (lg > 1.0) lg = 1.0;
|
|
87
|
+
if (lb < 0.0) lb = 0.0;
|
|
88
|
+
if (lb > 1.0) lb = 1.0;
|
|
89
|
+
|
|
90
|
+
// sRGB forward transfer function (IEC 61966-2-1:1999)
|
|
91
|
+
double rs = (lr <= SRGB_GAMMA_THRESHOLD_FWD) ? lr * SRGB_GAMMA_LINEAR_SLOPE
|
|
92
|
+
: SRGB_GAMMA_SCALE * pow(lr, 1.0/SRGB_GAMMA_EXPONENT) - SRGB_GAMMA_OFFSET;
|
|
93
|
+
double gs = (lg <= SRGB_GAMMA_THRESHOLD_FWD) ? lg * SRGB_GAMMA_LINEAR_SLOPE
|
|
94
|
+
: SRGB_GAMMA_SCALE * pow(lg, 1.0/SRGB_GAMMA_EXPONENT) - SRGB_GAMMA_OFFSET;
|
|
95
|
+
double bs = (lb <= SRGB_GAMMA_THRESHOLD_FWD) ? lb * SRGB_GAMMA_LINEAR_SLOPE
|
|
96
|
+
: SRGB_GAMMA_SCALE * pow(lb, 1.0/SRGB_GAMMA_EXPONENT) - SRGB_GAMMA_OFFSET;
|
|
97
|
+
|
|
98
|
+
// Convert to 0-255 range and round
|
|
99
|
+
*r = (int)(rs * 255.0 + 0.5);
|
|
100
|
+
*g = (int)(gs * 255.0 + 0.5);
|
|
101
|
+
*b = (int)(bs * 255.0 + 0.5);
|
|
102
|
+
|
|
103
|
+
// Clamp to valid byte range
|
|
104
|
+
if (*r < 0) *r = 0;
|
|
105
|
+
if (*r > 255) *r = 255;
|
|
106
|
+
if (*g < 0) *g = 0;
|
|
107
|
+
if (*g > 255) *g = 255;
|
|
108
|
+
if (*b < 0) *b = 0;
|
|
109
|
+
if (*b > 255) *b = 255;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// =============================================================================
|
|
113
|
+
// LINEAR RGB ↔ XYZ CONVERSIONS
|
|
114
|
+
// =============================================================================
|
|
115
|
+
|
|
116
|
+
// Convert linear RGB to CIE XYZ (D65 illuminant, sRGB primaries)
|
|
117
|
+
// Matrix from http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
|
|
118
|
+
static void linear_rgb_to_xyz_d65(double lr, double lg, double lb, double *x, double *y, double *z) {
|
|
119
|
+
// sRGB to XYZ-D65 transformation matrix (M = sRGB primaries × D65 white point)
|
|
120
|
+
*x = lr * 0.4124564 + lg * 0.3575761 + lb * 0.1804375; // X from R,G,B
|
|
121
|
+
*y = lr * 0.2126729 + lg * 0.7151522 + lb * 0.0721750; // Y from R,G,B (luminance)
|
|
122
|
+
*z = lr * 0.0193339 + lg * 0.1191920 + lb * 0.9503041; // Z from R,G,B
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Convert CIE XYZ to linear RGB (D65 illuminant, sRGB primaries)
|
|
126
|
+
// Matrix from http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
|
|
127
|
+
static void xyz_d65_to_linear_rgb(double x, double y, double z, double *lr, double *lg, double *lb) {
|
|
128
|
+
// XYZ-D65 to sRGB transformation matrix (M^-1)
|
|
129
|
+
*lr = x * 3.2404542 + y * -1.5371385 + z * -0.4985314; // R from X,Y,Z
|
|
130
|
+
*lg = x * -0.9692660 + y * 1.8760108 + z * 0.0415560; // G from X,Y,Z
|
|
131
|
+
*lb = x * 0.0556434 + y * -0.2040259 + z * 1.0572252; // B from X,Y,Z
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// =============================================================================
|
|
135
|
+
// CHROMATIC ADAPTATION: D50 ↔ D65
|
|
136
|
+
// =============================================================================
|
|
137
|
+
// Lab uses D50, sRGB uses D65, so we need chromatic adaptation
|
|
138
|
+
// Bradford matrices from CSS Color Module Level 4 spec
|
|
139
|
+
|
|
140
|
+
// Convert XYZ D50 to XYZ D65
|
|
141
|
+
static void xyz_d50_to_d65(double x50, double y50, double z50, double *x65, double *y65, double *z65) {
|
|
142
|
+
// Bradford chromatic adaptation matrix D50→D65
|
|
143
|
+
*x65 = x50 * 0.9554734527042182 // M[0][0]
|
|
144
|
+
+ y50 * -0.023098536874261423 // M[0][1]
|
|
145
|
+
+ z50 * 0.0632593086610217; // M[0][2]
|
|
146
|
+
*y65 = x50 * -0.028369706963208136 // M[1][0]
|
|
147
|
+
+ y50 * 1.0099954580058226 // M[1][1]
|
|
148
|
+
+ z50 * 0.021041398966943008; // M[1][2]
|
|
149
|
+
*z65 = x50 * 0.012314001688319899 // M[2][0]
|
|
150
|
+
+ y50 * -0.020507696433477912 // M[2][1]
|
|
151
|
+
+ z50 * 1.3303659366080753; // M[2][2]
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Convert XYZ D65 to XYZ D50
|
|
155
|
+
static void xyz_d65_to_d50(double x65, double y65, double z65, double *x50, double *y50, double *z50) {
|
|
156
|
+
// Bradford chromatic adaptation matrix D65→D50 (inverse of above)
|
|
157
|
+
*x50 = x65 * 1.0479298208405488 // M^-1[0][0]
|
|
158
|
+
+ y65 * 0.022946793341019088 // M^-1[0][1]
|
|
159
|
+
+ z65 * -0.05019222954313557; // M^-1[0][2]
|
|
160
|
+
*y50 = x65 * 0.029627815688159344 // M^-1[1][0]
|
|
161
|
+
+ y65 * 0.990434484573249 // M^-1[1][1]
|
|
162
|
+
+ z65 * -0.01707382502938514; // M^-1[1][2]
|
|
163
|
+
*z50 = x65 * -0.009243058152591178 // M^-1[2][0]
|
|
164
|
+
+ y65 * 0.015055144896577895 // M^-1[2][1]
|
|
165
|
+
+ z65 * 0.7518742899580008; // M^-1[2][2]
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// =============================================================================
|
|
169
|
+
// XYZ ↔ LAB CONVERSIONS
|
|
170
|
+
// =============================================================================
|
|
171
|
+
|
|
172
|
+
// CIE Standard Illuminant D50 white point (used for Lab in CSS)
|
|
173
|
+
// From CSS Color Module Level 4 spec
|
|
174
|
+
#define XYZ_WHITE_X 0.96422
|
|
175
|
+
#define XYZ_WHITE_Y 1.00000
|
|
176
|
+
#define XYZ_WHITE_Z 0.82521
|
|
177
|
+
|
|
178
|
+
// CIE Lab constants from CSS Color Module Level 4
|
|
179
|
+
#define LAB_EPSILON (216.0 / 24389.0) // 6^3 / 29^3
|
|
180
|
+
#define LAB_KAPPA (24389.0 / 27.0) // 29^3 / 3^3
|
|
181
|
+
|
|
182
|
+
// LCH powerless hue threshold (per W3C CSS Color Module Level 4)
|
|
183
|
+
// When chroma is below this value, hue is considered "powerless" (missing)
|
|
184
|
+
#define LCH_CHROMA_EPSILON 0.0015
|
|
185
|
+
|
|
186
|
+
// Convert XYZ to CIE L*a*b* (CSS Color Module Level 4 algorithm)
|
|
187
|
+
static void xyz_to_lab(double x, double y, double z, double *L, double *a, double *b) {
|
|
188
|
+
// Normalize by D65 white point
|
|
189
|
+
double xn = x / XYZ_WHITE_X;
|
|
190
|
+
double yn = y / XYZ_WHITE_Y;
|
|
191
|
+
double zn = z / XYZ_WHITE_Z;
|
|
192
|
+
|
|
193
|
+
// Apply f function to each component
|
|
194
|
+
double fx = (xn > LAB_EPSILON) ? pow(xn, 1.0/3.0) : (LAB_KAPPA * xn + 16.0) / 116.0;
|
|
195
|
+
double fy = (yn > LAB_EPSILON) ? pow(yn, 1.0/3.0) : (LAB_KAPPA * yn + 16.0) / 116.0;
|
|
196
|
+
double fz = (zn > LAB_EPSILON) ? pow(zn, 1.0/3.0) : (LAB_KAPPA * zn + 16.0) / 116.0;
|
|
197
|
+
|
|
198
|
+
*L = 116.0 * fy - 16.0;
|
|
199
|
+
*a = 500.0 * (fx - fy);
|
|
200
|
+
*b = 200.0 * (fy - fz);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Convert CIE L*a*b* to XYZ (CSS Color Module Level 4 algorithm)
|
|
204
|
+
static void lab_to_xyz(double L, double a, double b, double *x, double *y, double *z) {
|
|
205
|
+
double fy = (L + 16.0) / 116.0;
|
|
206
|
+
double fx = a / 500.0 + fy;
|
|
207
|
+
double fz = fy - b / 200.0;
|
|
208
|
+
|
|
209
|
+
// Apply inverse f function
|
|
210
|
+
double xn = (pow(fx, 3) > LAB_EPSILON) ? pow(fx, 3) : (116.0 * fx - 16.0) / LAB_KAPPA;
|
|
211
|
+
double yn = (L > LAB_KAPPA * LAB_EPSILON) ? pow((L + 16.0) / 116.0, 3) : L / LAB_KAPPA;
|
|
212
|
+
double zn = (pow(fz, 3) > LAB_EPSILON) ? pow(fz, 3) : (116.0 * fz - 16.0) / LAB_KAPPA;
|
|
213
|
+
|
|
214
|
+
*x = xn * XYZ_WHITE_X;
|
|
215
|
+
*y = yn * XYZ_WHITE_Y;
|
|
216
|
+
*z = zn * XYZ_WHITE_Z;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// =============================================================================
|
|
220
|
+
// LAB ↔ LCH COORDINATE CONVERSION (Cartesian ↔ Polar)
|
|
221
|
+
// =============================================================================
|
|
222
|
+
//
|
|
223
|
+
// LCH is the cylindrical/polar representation of Lab, similar to how
|
|
224
|
+
// HSL relates to RGB. The conversion is straightforward:
|
|
225
|
+
//
|
|
226
|
+
// Lab → LCH (Cartesian to Polar):
|
|
227
|
+
// L (lightness) stays the same
|
|
228
|
+
// C (chroma) = sqrt(a² + b²)
|
|
229
|
+
// H (hue) = atan2(b, a) converted to degrees
|
|
230
|
+
//
|
|
231
|
+
// LCH → Lab (Polar to Cartesian):
|
|
232
|
+
// L (lightness) stays the same
|
|
233
|
+
// a = C * cos(H)
|
|
234
|
+
// b = C * sin(H)
|
|
235
|
+
//
|
|
236
|
+
// W3C Spec: https://www.w3.org/TR/css-color-4/#lch-to-lab
|
|
237
|
+
// - L: 0% = 0.0, 100% = 100.0 (same as Lab)
|
|
238
|
+
// - C: 0% = 0, 100% = 150 (chroma), negative values clamped to 0
|
|
239
|
+
// - H: hue angle in degrees (0-360)
|
|
240
|
+
// - 0° = purplish red (positive a axis)
|
|
241
|
+
// - 90° = mustard yellow (positive b axis)
|
|
242
|
+
// - 180° = greenish cyan (negative a axis)
|
|
243
|
+
// - 270° = sky blue (negative b axis)
|
|
244
|
+
// - Powerless hue: when C <= 0.0015, hue is powerless
|
|
245
|
+
|
|
246
|
+
// Convert Lab (L, a, b) to LCH (L, C, H)
|
|
247
|
+
static void lab_to_lch(double L, double a, double b, double *out_L, double *out_C, double *out_H) {
|
|
248
|
+
*out_L = L;
|
|
249
|
+
*out_C = sqrt(a * a + b * b);
|
|
250
|
+
|
|
251
|
+
// Calculate hue angle in degrees
|
|
252
|
+
// atan2 returns radians in range [-π, π]
|
|
253
|
+
double h_rad = atan2(b, a);
|
|
254
|
+
*out_H = h_rad * 180.0 / M_PI; // Convert to degrees
|
|
255
|
+
|
|
256
|
+
// Normalize to [0, 360) range
|
|
257
|
+
if (*out_H < 0.0) {
|
|
258
|
+
*out_H += 360.0;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Per W3C spec: if chroma is very small (near zero), hue is powerless
|
|
262
|
+
// and should be treated as missing/0
|
|
263
|
+
if (*out_C <= LCH_CHROMA_EPSILON) {
|
|
264
|
+
*out_H = 0.0; // Powerless hue
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Convert LCH (L, C, H) to Lab (L, a, b)
|
|
269
|
+
static void lch_to_lab(double L, double C, double H, double *out_L, double *out_a, double *out_b) {
|
|
270
|
+
*out_L = L;
|
|
271
|
+
|
|
272
|
+
// Clamp negative chroma to 0 (per W3C spec)
|
|
273
|
+
if (C < 0.0) {
|
|
274
|
+
C = 0.0;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Convert hue angle from degrees to radians
|
|
278
|
+
double h_rad = H * M_PI / 180.0;
|
|
279
|
+
|
|
280
|
+
// Convert polar to Cartesian
|
|
281
|
+
*out_a = C * cos(h_rad);
|
|
282
|
+
*out_b = C * sin(h_rad);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// =============================================================================
|
|
286
|
+
// PARSING HELPERS
|
|
287
|
+
// =============================================================================
|
|
288
|
+
|
|
289
|
+
// Parse a floating point number with optional percentage
|
|
290
|
+
// Returns the value, or raises error on invalid syntax
|
|
291
|
+
// If percentage is found, value is scaled by percent_max
|
|
292
|
+
// e.g., "50%" with percent_max=100.0 returns 50.0
|
|
293
|
+
static double parse_float(const char **p, double percent_max) {
|
|
294
|
+
char *end;
|
|
295
|
+
double value = strtod(*p, &end);
|
|
296
|
+
|
|
297
|
+
if (end == *p) {
|
|
298
|
+
rb_raise(rb_eArgError, "Expected number in color value");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
*p = end;
|
|
302
|
+
SKIP_WHITESPACE(*p);
|
|
303
|
+
|
|
304
|
+
// Check for percentage
|
|
305
|
+
if (**p == '%') {
|
|
306
|
+
value = (value / 100.0) * percent_max;
|
|
307
|
+
(*p)++;
|
|
308
|
+
SKIP_WHITESPACE(*p);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return value;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// =============================================================================
|
|
315
|
+
// PUBLIC API: Parse and Format Lab
|
|
316
|
+
// =============================================================================
|
|
317
|
+
|
|
318
|
+
// Parse lab() CSS function to intermediate representation
|
|
319
|
+
// Format: lab(L a b) or lab(L a b / alpha)
|
|
320
|
+
// L: 0-100 or 0%-100% (lightness)
|
|
321
|
+
// a, b: typically -125 to 125 (but unbounded)
|
|
322
|
+
// alpha: 0-1 or 0%-100%
|
|
323
|
+
struct color_ir parse_lab(VALUE lab_value) {
|
|
324
|
+
struct color_ir color;
|
|
325
|
+
INIT_COLOR_IR(color);
|
|
326
|
+
|
|
327
|
+
const char *str = StringValueCStr(lab_value);
|
|
328
|
+
const char *p = str;
|
|
329
|
+
|
|
330
|
+
// Skip "lab("
|
|
331
|
+
if (strncmp(p, "lab(", 4) != 0) {
|
|
332
|
+
rb_raise(rb_eArgError, "Invalid lab() syntax: must start with 'lab('");
|
|
333
|
+
}
|
|
334
|
+
p += 4;
|
|
335
|
+
SKIP_WHITESPACE(p);
|
|
336
|
+
|
|
337
|
+
// Parse L (lightness): 0-100 or 0%-100%
|
|
338
|
+
double L = parse_float(&p, 100.0);
|
|
339
|
+
|
|
340
|
+
// Clamp L to [0, 100] per spec
|
|
341
|
+
if (L < 0.0) L = 0.0;
|
|
342
|
+
if (L > 100.0) L = 100.0;
|
|
343
|
+
|
|
344
|
+
SKIP_SEPARATOR(p);
|
|
345
|
+
|
|
346
|
+
// Parse a (green-red axis)
|
|
347
|
+
// a is typically -125 to 125, but can be percentage relative to -125/125
|
|
348
|
+
double a = parse_float(&p, 125.0);
|
|
349
|
+
|
|
350
|
+
SKIP_SEPARATOR(p);
|
|
351
|
+
|
|
352
|
+
// Parse b (blue-yellow axis)
|
|
353
|
+
// b is typically -125 to 125, but can be percentage relative to -125/125
|
|
354
|
+
double b = parse_float(&p, 125.0);
|
|
355
|
+
|
|
356
|
+
SKIP_WHITESPACE(p);
|
|
357
|
+
|
|
358
|
+
// Check for optional alpha
|
|
359
|
+
if (*p == '/') {
|
|
360
|
+
p++;
|
|
361
|
+
SKIP_WHITESPACE(p);
|
|
362
|
+
color.alpha = parse_float(&p, 1.0);
|
|
363
|
+
|
|
364
|
+
// Clamp alpha to [0, 1]
|
|
365
|
+
if (color.alpha < 0.0) color.alpha = 0.0;
|
|
366
|
+
if (color.alpha > 1.0) color.alpha = 1.0;
|
|
367
|
+
|
|
368
|
+
SKIP_WHITESPACE(p);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Expect closing paren
|
|
372
|
+
if (*p != ')') {
|
|
373
|
+
rb_raise(rb_eArgError, "Invalid lab() syntax: expected closing ')'");
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Convert Lab → XYZ D50 → XYZ D65 → linear RGB → sRGB
|
|
377
|
+
double x_d50, y_d50, z_d50;
|
|
378
|
+
lab_to_xyz(L, a, b, &x_d50, &y_d50, &z_d50);
|
|
379
|
+
|
|
380
|
+
// Chromatic adaptation D50 → D65
|
|
381
|
+
double x_d65, y_d65, z_d65;
|
|
382
|
+
xyz_d50_to_d65(x_d50, y_d50, z_d50, &x_d65, &y_d65, &z_d65);
|
|
383
|
+
|
|
384
|
+
double lr, lg, lb;
|
|
385
|
+
xyz_d65_to_linear_rgb(x_d65, y_d65, z_d65, &lr, &lg, &lb);
|
|
386
|
+
|
|
387
|
+
linear_rgb_to_srgb(lr, lg, lb, &color.red, &color.green, &color.blue);
|
|
388
|
+
|
|
389
|
+
// Store linear RGB for high precision
|
|
390
|
+
color.has_linear_rgb = 1;
|
|
391
|
+
color.linear_r = lr;
|
|
392
|
+
color.linear_g = lg;
|
|
393
|
+
color.linear_b = lb;
|
|
394
|
+
|
|
395
|
+
RB_GC_GUARD(lab_value);
|
|
396
|
+
return color;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Format intermediate representation to lab() CSS function
|
|
400
|
+
// Returns Ruby string like "lab(L a b)" or "lab(L a b / alpha)"
|
|
401
|
+
VALUE format_lab(struct color_ir color, int use_modern_syntax) {
|
|
402
|
+
(void)use_modern_syntax; // Lab only has one syntax
|
|
403
|
+
|
|
404
|
+
double lr, lg, lb;
|
|
405
|
+
|
|
406
|
+
// Use high-precision linear RGB if available, otherwise convert from sRGB
|
|
407
|
+
if (color.has_linear_rgb) {
|
|
408
|
+
lr = color.linear_r;
|
|
409
|
+
lg = color.linear_g;
|
|
410
|
+
lb = color.linear_b;
|
|
411
|
+
} else {
|
|
412
|
+
srgb_to_linear_rgb(color.red, color.green, color.blue, &lr, &lg, &lb);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Convert linear RGB → XYZ D65 → XYZ D50 → Lab
|
|
416
|
+
double x_d65, y_d65, z_d65;
|
|
417
|
+
linear_rgb_to_xyz_d65(lr, lg, lb, &x_d65, &y_d65, &z_d65);
|
|
418
|
+
|
|
419
|
+
// Chromatic adaptation D65 → D50
|
|
420
|
+
double x_d50, y_d50, z_d50;
|
|
421
|
+
xyz_d65_to_d50(x_d65, y_d65, z_d65, &x_d50, &y_d50, &z_d50);
|
|
422
|
+
|
|
423
|
+
double L, a, b;
|
|
424
|
+
xyz_to_lab(x_d50, y_d50, z_d50, &L, &a, &b);
|
|
425
|
+
|
|
426
|
+
char buf[128];
|
|
427
|
+
if (color.alpha >= 0.0) {
|
|
428
|
+
FORMAT_LAB_ALPHA(buf, L, a, b, color.alpha);
|
|
429
|
+
} else {
|
|
430
|
+
FORMAT_LAB(buf, L, a, b);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return rb_str_new_cstr(buf);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// =============================================================================
|
|
437
|
+
// PUBLIC API: Parse and Format LCH
|
|
438
|
+
// =============================================================================
|
|
439
|
+
|
|
440
|
+
// Parse lch() CSS function to intermediate representation
|
|
441
|
+
// Format: lch(L C H) or lch(L C H / alpha)
|
|
442
|
+
// L: 0-100 or 0%-100% (lightness)
|
|
443
|
+
// C: 0-150 or 0%-100% (chroma, where 100% = 150)
|
|
444
|
+
// H: hue angle in degrees (0-360, wraps)
|
|
445
|
+
// alpha: 0-1 or 0%-100%
|
|
446
|
+
struct color_ir parse_lch(VALUE lch_value) {
|
|
447
|
+
struct color_ir color;
|
|
448
|
+
INIT_COLOR_IR(color);
|
|
449
|
+
|
|
450
|
+
const char *str = StringValueCStr(lch_value);
|
|
451
|
+
const char *p = str;
|
|
452
|
+
|
|
453
|
+
// Skip "lch("
|
|
454
|
+
if (strncmp(p, "lch(", 4) != 0) {
|
|
455
|
+
rb_raise(rb_eArgError, "Invalid lch() syntax: must start with 'lch('");
|
|
456
|
+
}
|
|
457
|
+
p += 4;
|
|
458
|
+
SKIP_WHITESPACE(p);
|
|
459
|
+
|
|
460
|
+
// Parse L (lightness): 0-100 or 0%-100%
|
|
461
|
+
double L = parse_float(&p, 100.0);
|
|
462
|
+
|
|
463
|
+
// Clamp L to [0, 100] per spec
|
|
464
|
+
if (L < 0.0) L = 0.0;
|
|
465
|
+
if (L > 100.0) L = 100.0;
|
|
466
|
+
|
|
467
|
+
SKIP_SEPARATOR(p);
|
|
468
|
+
|
|
469
|
+
// Parse C (chroma): 0-150 or 0%-100% (where 100% = 150)
|
|
470
|
+
double C = parse_float(&p, 150.0);
|
|
471
|
+
|
|
472
|
+
// Clamp negative chroma to 0 per spec
|
|
473
|
+
if (C < 0.0) C = 0.0;
|
|
474
|
+
|
|
475
|
+
SKIP_SEPARATOR(p);
|
|
476
|
+
|
|
477
|
+
// Parse H (hue): degrees, can be any value (wraps around)
|
|
478
|
+
double H = parse_float(&p, 1.0); // Hue is not a percentage typically
|
|
479
|
+
|
|
480
|
+
// Normalize hue to [0, 360) range
|
|
481
|
+
H = fmod(H, 360.0);
|
|
482
|
+
if (H < 0.0) {
|
|
483
|
+
H += 360.0;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
SKIP_WHITESPACE(p);
|
|
487
|
+
|
|
488
|
+
// Check for optional alpha
|
|
489
|
+
if (*p == '/') {
|
|
490
|
+
p++;
|
|
491
|
+
SKIP_WHITESPACE(p);
|
|
492
|
+
color.alpha = parse_float(&p, 1.0);
|
|
493
|
+
|
|
494
|
+
// Clamp alpha to [0, 1]
|
|
495
|
+
if (color.alpha < 0.0) color.alpha = 0.0;
|
|
496
|
+
if (color.alpha > 1.0) color.alpha = 1.0;
|
|
497
|
+
|
|
498
|
+
SKIP_WHITESPACE(p);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Expect closing paren
|
|
502
|
+
if (*p != ')') {
|
|
503
|
+
rb_raise(rb_eArgError, "Invalid lch() syntax: expected closing ')'");
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Convert LCH → Lab → XYZ D50 → XYZ D65 → linear RGB → sRGB
|
|
507
|
+
double lab_L, lab_a, lab_b;
|
|
508
|
+
lch_to_lab(L, C, H, &lab_L, &lab_a, &lab_b);
|
|
509
|
+
|
|
510
|
+
double x_d50, y_d50, z_d50;
|
|
511
|
+
lab_to_xyz(lab_L, lab_a, lab_b, &x_d50, &y_d50, &z_d50);
|
|
512
|
+
|
|
513
|
+
// Chromatic adaptation D50 → D65
|
|
514
|
+
double x_d65, y_d65, z_d65;
|
|
515
|
+
xyz_d50_to_d65(x_d50, y_d50, z_d50, &x_d65, &y_d65, &z_d65);
|
|
516
|
+
|
|
517
|
+
double lr, lg, lb;
|
|
518
|
+
xyz_d65_to_linear_rgb(x_d65, y_d65, z_d65, &lr, &lg, &lb);
|
|
519
|
+
|
|
520
|
+
linear_rgb_to_srgb(lr, lg, lb, &color.red, &color.green, &color.blue);
|
|
521
|
+
|
|
522
|
+
// Store linear RGB for high precision
|
|
523
|
+
color.has_linear_rgb = 1;
|
|
524
|
+
color.linear_r = lr;
|
|
525
|
+
color.linear_g = lg;
|
|
526
|
+
color.linear_b = lb;
|
|
527
|
+
|
|
528
|
+
RB_GC_GUARD(lch_value);
|
|
529
|
+
return color;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Format intermediate representation to lch() CSS function
|
|
533
|
+
// Returns Ruby string like "lch(L C H)" or "lch(L C H / alpha)"
|
|
534
|
+
VALUE format_lch(struct color_ir color, int use_modern_syntax) {
|
|
535
|
+
(void)use_modern_syntax; // LCH only has one syntax
|
|
536
|
+
|
|
537
|
+
double lr, lg, lb;
|
|
538
|
+
|
|
539
|
+
// Use high-precision linear RGB if available, otherwise convert from sRGB
|
|
540
|
+
if (color.has_linear_rgb) {
|
|
541
|
+
lr = color.linear_r;
|
|
542
|
+
lg = color.linear_g;
|
|
543
|
+
lb = color.linear_b;
|
|
544
|
+
} else {
|
|
545
|
+
srgb_to_linear_rgb(color.red, color.green, color.blue, &lr, &lg, &lb);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Convert linear RGB → XYZ D65 → XYZ D50 → Lab → LCH
|
|
549
|
+
double x_d65, y_d65, z_d65;
|
|
550
|
+
linear_rgb_to_xyz_d65(lr, lg, lb, &x_d65, &y_d65, &z_d65);
|
|
551
|
+
|
|
552
|
+
// Chromatic adaptation D65 → D50
|
|
553
|
+
double x_d50, y_d50, z_d50;
|
|
554
|
+
xyz_d65_to_d50(x_d65, y_d65, z_d65, &x_d50, &y_d50, &z_d50);
|
|
555
|
+
|
|
556
|
+
double lab_L, lab_a, lab_b;
|
|
557
|
+
xyz_to_lab(x_d50, y_d50, z_d50, &lab_L, &lab_a, &lab_b);
|
|
558
|
+
|
|
559
|
+
// Convert Lab to LCH
|
|
560
|
+
double L, C, H;
|
|
561
|
+
lab_to_lch(lab_L, lab_a, lab_b, &L, &C, &H);
|
|
562
|
+
|
|
563
|
+
char buf[128];
|
|
564
|
+
if (color.alpha >= 0.0) {
|
|
565
|
+
FORMAT_LCH_ALPHA(buf, L, C, H, color.alpha);
|
|
566
|
+
} else {
|
|
567
|
+
FORMAT_LCH(buf, L, C, H);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return rb_str_new_cstr(buf);
|
|
571
|
+
}
|