File Coverage

include/colouring.h
Criterion Covered Total %
statement 213 220 96.8
branch 99 122 81.1
condition n/a
subroutine n/a
pod n/a
total 312 342 91.2


line stmt bran cond sub pod time code
1             /*
2             * colouring.h - Pure C colour math library (header-only, C89 compatible)
3             *
4             * No Perl/XS dependencies. All functions are static.
5             *
6             * Usage from XS:
7             * #define COLOURING_FATAL(msg) croak("%s", (msg))
8             * #include "colouring.h"
9             *
10             * Usage from plain C:
11             * #include "colouring.h"
12             * // COLOURING_FATAL defaults to fprintf+abort
13             */
14              
15             #ifndef COLOURING_H
16             #define COLOURING_H
17              
18             #include
19             #include
20             #include
21             #include
22              
23             /* ── Error handling ────────────────────────────────────────────── */
24              
25             #ifndef COLOURING_FATAL
26             #define COLOURING_FATAL(msg) do { fprintf(stderr, "colouring: %s\n", (msg)); abort(); } while(0)
27             #endif
28              
29             /* ── Structs ───────────────────────────────────────────────────── */
30              
31             typedef struct {
32             double r; /* 0-255 */
33             double g;
34             double b;
35             double a; /* 0.0-1.0 */
36             } colouring_rgba_t;
37              
38             typedef struct {
39             int h; /* 0-360 */
40             double s; /* 0.0-1.0 */
41             double l; /* 0.0-1.0 */
42             double a; /* 0.0-1.0 */
43             } colouring_hsl_t;
44              
45             typedef struct {
46             double h; /* 0-360 */
47             double s; /* 0.0-1.0 */
48             double v; /* 0.0-1.0 */
49             } colouring_hsv_t;
50              
51             /* ── Scalar utilities ──────────────────────────────────────────── */
52              
53 30451           static double colouring_min(double a, double b) {
54 30451 100         return a < b ? a : b;
55             }
56              
57 30451           static double colouring_max(double a, double b) {
58 30451 100         return a > b ? a : b;
59             }
60              
61 30279           static double colouring_clamp(double val, double upper) {
62 30279           return colouring_min(colouring_max(val, 0), upper);
63             }
64              
65 0           static double colouring_round(double val, int dp) {
66 0           double factor = pow(10.0, dp);
67 0           return floor(val * factor + 0.5) / factor;
68             }
69              
70 65           static double colouring_depercent(const char *str) {
71 65           return atof(str) / 100.0;
72             }
73              
74             /* Format 0.0-1.0 as "NN%". buf must be >= 8 bytes. */
75 40           static void colouring_percent_buf(double num, char *buf, size_t bufsz) {
76 40           snprintf(buf, bufsz, "%.0f%%", num * 100.0);
77 40           }
78              
79             /* ── Hex parsing ───────────────────────────────────────────────── */
80              
81 145           static int colouring_hex2int(const char *hex) {
82 145           int val = 0;
83 433 100         while (*hex) {
84 289           int byte = *hex++;
85 289 50         if (byte >= '0' && byte <= '9') byte = byte - '0';
    100          
86 217 50         else if (byte >= 'a' && byte <= 'f') byte = byte - 'a' + 10;
    100          
87 1 50         else if (byte >= 'A' && byte <= 'F') byte = byte - 'A' + 10;
    50          
88 1           else { COLOURING_FATAL("Invalid hex character"); return 0; }
89 288           val = (val << 4) | (byte & 0xF);
90             }
91 144           return val;
92             }
93              
94 49           static colouring_rgba_t colouring_hex2rgb(const char *hex) {
95             colouring_rgba_t c;
96             int l;
97 49           c.a = 1.0;
98              
99             /* skip leading '#' if present */
100 49 50         if (hex[0] == '#') hex++;
101 49           l = (int)strlen(hex);
102              
103 49 100         if (l == 3) {
104             char h[3];
105 2           h[2] = '\0';
106 2           h[0] = hex[0]; h[1] = hex[0]; c.r = colouring_hex2int(h);
107 1           h[0] = hex[1]; h[1] = hex[1]; c.g = colouring_hex2int(h);
108 1           h[0] = hex[2]; h[1] = hex[2]; c.b = colouring_hex2int(h);
109 47 50         } else if (l == 6) {
110             char h[3];
111 47           h[2] = '\0';
112 47           h[0] = hex[0]; h[1] = hex[1]; c.r = colouring_hex2int(h);
113 47           h[0] = hex[2]; h[1] = hex[3]; c.g = colouring_hex2int(h);
114 47           h[0] = hex[4]; h[1] = hex[5]; c.b = colouring_hex2int(h);
115             } else {
116 0           COLOURING_FATAL("hex length must be 3 or 6");
117             c.r = c.g = c.b = 0;
118             }
119 48           return c;
120             }
121              
122             /* ── Number parsing from colour strings ────────────────────────── */
123              
124             /* Parse numeric values from strings like "rgb(255,0,128)" or "hsl(120,50%,50%)".
125             Writes up to max_out doubles, returns count written. */
126 10021           static int colouring_parse_numbers(const char *str, double *out, int max_out) {
127 10021           int count = 0;
128             char temp[32];
129 10021           int ti = 0;
130             int i;
131 10021           int len = (int)strlen(str);
132              
133 229310 100         for (i = 0; i < len; i++) {
134 219289 100         if ((str[i] >= '0' && str[i] <= '9') || str[i] == '.') {
    100          
    50          
135 99046 50         if (ti < 31) temp[ti++] = str[i];
136 120243 100         } else if (ti > 0) {
137 40070           temp[ti] = '\0';
138 40070 50         if (count < max_out) {
139 40070           out[count++] = atof(temp);
140             }
141 40070           ti = 0;
142             }
143             }
144             /* flush trailing number */
145 10021 50         if (ti > 0 && count < max_out) {
    0          
146 0           temp[ti] = '\0';
147 0           out[count++] = atof(temp);
148             }
149 10021           return count;
150             }
151              
152             /* ── HSL helper ────────────────────────────────────────────────── */
153              
154 30210           static double colouring__hue(double h, double m1, double m2) {
155 30210 100         h = h < 0 ? h + 1 : h > 1 ? h - 1 : h;
    100          
156 30210 100         if (h * 6.0 < 1.0) return m1 + (m2 - m1) * h * 6.0;
157 25151 100         if (h * 2.0 < 1.0) return m2;
158 15081 100         if (h * 3.0 < 2.0) return m1 + (m2 - m1) * ((2.0 / 3.0) - h) * 6.0;
159 10098           return m1;
160             }
161              
162             /* ── Colour space conversions ──────────────────────────────────── */
163              
164 10070           static colouring_rgba_t colouring_hsl2rgb(double h, double s, double l, double a) {
165             colouring_rgba_t c;
166             double m2, m1;
167 10070           int hi = (int)h;
168              
169 10070           h = (hi % 360) / 360.0;
170 10070 100         if (s > 1 || l > 1) {
    50          
171 10001           s = s / 100.0;
172 10001           l = l / 100.0;
173             }
174 10070 100         m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s;
175 10070           m1 = l * 2 - m2;
176              
177 10070           c.r = (int)(colouring_clamp(colouring__hue(h + (1.0 / 3.0), m1, m2), 1) * 255);
178 10070           c.g = (int)(colouring_clamp(colouring__hue(h, m1, m2), 1) * 255);
179 10070           c.b = (int)(colouring_clamp(colouring__hue(h - (1.0 / 3.0), m1, m2), 1) * 255);
180 10070           c.a = a;
181 10070           return c;
182             }
183              
184 78           static colouring_hsl_t colouring_rgb2hsl(double r, double g, double b, double a) {
185             colouring_hsl_t hsl;
186 78           double rn = r / 255.0;
187 78           double gn = g / 255.0;
188 78           double bn = b / 255.0;
189 78           double mx = colouring_max(colouring_max(rn, gn), bn);
190 78           double mn = colouring_min(colouring_min(rn, gn), bn);
191 78           double d = mx - mn;
192 78           double h = 0;
193 78           double s = 0;
194 78           double l = (mx + mn) / 2.0;
195              
196 78 100         if (mx != mn) {
197 10 100         s = l > 0.5 ? (d / (2.0 - mx - mn)) : (d / (mx + mn));
198 10 100         if (mx == rn)
199 4 100         h = (gn - bn) / d + (gn < bn ? 6 : 0);
200 6 100         else if (mx == gn)
201 5           h = (bn - rn) / d + 2;
202             else
203 1           h = (rn - gn) / d + 4;
204 10           h = h / 6.0;
205             }
206              
207 78           hsl.h = (int)(h * 360);
208 78           hsl.s = s;
209 78           hsl.l = l;
210 78           hsl.a = a;
211 78           return hsl;
212             }
213              
214 8           static colouring_hsv_t colouring_rgb2hsv(double r, double g, double b) {
215             colouring_hsv_t hsv;
216 8           double rn = r / 255.0;
217 8           double gn = g / 255.0;
218 8           double bn = b / 255.0;
219 8           double mx = colouring_max(colouring_max(rn, gn), bn);
220 8           double mn = colouring_min(colouring_min(rn, gn), bn);
221 8           double d = mx - mn;
222 8           double h = 0;
223              
224 8 100         hsv.s = (mx == 0) ? 0 : d / mx;
225 8           hsv.v = mx;
226              
227 8 100         if (mx != mn) {
228 6 100         if (mx == rn)
229 4 100         h = (gn - bn) / d + (gn < bn ? 6 : 0);
230 2 100         else if (mx == gn)
231 1           h = (bn - rn) / d + 2;
232             else
233 1           h = (rn - gn) / d + 4;
234 6           h = h / 6.0;
235             }
236              
237 8           hsv.h = h * 360.0;
238 8           return hsv;
239             }
240              
241             /* ── Colour manipulation ───────────────────────────────────────── */
242              
243 11           static colouring_rgba_t colouring_lighten(double r, double g, double b, double a,
244             double amount, int relative) {
245 11           colouring_hsl_t hsl = colouring_rgb2hsl(r, g, b, a);
246 11 100         if (relative) {
247             /* Original C code: double l = hsl.l || 1.; -- C logical OR
248             always yields 1 when hsl.l is nonzero, 1 when zero.
249             So relative lighten behaves same as absolute. */
250 2           hsl.l = hsl.l + colouring_clamp(amount, 1);
251             } else {
252 9           hsl.l = hsl.l + colouring_clamp(amount, 1);
253             }
254 11           return colouring_hsl2rgb(hsl.h, hsl.s, hsl.l, hsl.a);
255             }
256              
257 11           static colouring_rgba_t colouring_darken(double r, double g, double b, double a,
258             double amount, int relative) {
259 11           colouring_hsl_t hsl = colouring_rgb2hsl(r, g, b, a);
260 11 100         if (relative) {
261 1           hsl.l = hsl.l - colouring_clamp(hsl.l * amount, 1);
262             } else {
263 10           hsl.l = hsl.l - colouring_clamp(amount, 1);
264             }
265 11           return colouring_hsl2rgb(hsl.h, hsl.s, hsl.l, hsl.a);
266             }
267              
268 12           static colouring_rgba_t colouring_fade(double r, double g, double b, double a,
269             double amount) {
270 12           colouring_hsl_t hsl = colouring_rgb2hsl(r, g, b, a);
271 12           hsl.a = amount;
272 12           return colouring_hsl2rgb(hsl.h, hsl.s, hsl.l, hsl.a);
273             }
274              
275 14           static colouring_rgba_t colouring_fadeout(double r, double g, double b, double a,
276             double amount, int relative) {
277 14           colouring_hsl_t hsl = colouring_rgb2hsl(r, g, b, a);
278 14 100         hsl.a -= colouring_clamp(relative ? hsl.a * amount : amount, 1);
279 14           return colouring_hsl2rgb(hsl.h, hsl.s, hsl.l, hsl.a);
280             }
281              
282 14           static colouring_rgba_t colouring_fadein(double r, double g, double b, double a,
283             double amount, int relative) {
284 14           colouring_hsl_t hsl = colouring_rgb2hsl(r, g, b, a);
285 14 100         hsl.a += colouring_clamp(relative ? hsl.a * amount : amount, 1);
286 14           hsl.a = colouring_clamp(hsl.a, 1);
287 14           return colouring_hsl2rgb(hsl.h, hsl.s, hsl.l, hsl.a);
288             }
289              
290 1           static colouring_rgba_t colouring_saturate(double r, double g, double b, double a,
291             double amount, int relative) {
292 1           colouring_hsl_t hsl = colouring_rgb2hsl(r, g, b, a);
293 1 50         hsl.s += colouring_clamp(relative ? hsl.s * amount : amount, 1);
294 1           return colouring_hsl2rgb(hsl.h, hsl.s, hsl.l, hsl.a);
295             }
296              
297 2           static colouring_rgba_t colouring_desaturate(double r, double g, double b, double a,
298             double amount, int relative) {
299 2           colouring_hsl_t hsl = colouring_rgb2hsl(r, g, b, a);
300 2 50         hsl.s -= colouring_clamp(relative ? hsl.s * amount : amount, 1);
301 2           return colouring_hsl2rgb(hsl.h, hsl.s, hsl.l, hsl.a);
302             }
303              
304 1           static colouring_rgba_t colouring_greyscale(double r, double g, double b, double a) {
305 1           colouring_hsl_t hsl = colouring_rgb2hsl(r, g, b, a);
306 1           hsl.s -= 1.0;
307 1           return colouring_hsl2rgb(hsl.h, hsl.s, hsl.l, hsl.a);
308             }
309              
310 3           static colouring_rgba_t colouring_mix(colouring_rgba_t c1, colouring_rgba_t c2,
311             int weight) {
312             colouring_rgba_t out;
313 3           double w = weight / 100.0;
314 3           double a = c1.a - c2.a;
315             double w1, w2;
316              
317 3           w = (w * 2) - 1;
318 3 50         w1 = (((w * a == -1) ? w : (w + a) / (1 + w * a)) + 1) / 2.0;
319 3           w2 = 1 - w1;
320              
321 3           out.r = (c1.r * w1) + (c2.r * w2);
322 3           out.g = (c1.g * w1) + (c2.g * w2);
323 3           out.b = (c1.b * w1) + (c2.b * w2);
324 3           out.a = (c1.a * w) + (c2.a * (1 - w));
325 3           return out;
326             }
327              
328 1           static colouring_rgba_t colouring_tint(colouring_rgba_t c, int weight) {
329             colouring_rgba_t white;
330 1           white.r = 255; white.g = 255; white.b = 255; white.a = 1.0;
331 1           return colouring_mix(white, c, weight);
332             }
333              
334 1           static colouring_rgba_t colouring_shade(colouring_rgba_t c, int weight) {
335             colouring_rgba_t black;
336 1           black.r = 0; black.g = 0; black.b = 0; black.a = 1.0;
337 1           return colouring_mix(black, c, weight);
338             }
339              
340             /* ── Formatting ────────────────────────────────────────────────── */
341              
342 27           static void colouring_fmt_hex(colouring_rgba_t c, char *buf, size_t bufsz,
343             int force_long) {
344 27           int r = (int)c.r, g = (int)c.g, b = (int)c.b;
345 27           snprintf(buf, bufsz, "#%02x%02x%02x", r, g, b);
346 27 100         if (!force_long) {
347             /* shorten "#aabbcc" to "#abc" if possible */
348 22 100         if (buf[1] == buf[2] && buf[3] == buf[4] && buf[5] == buf[6]) {
    50          
    50          
349 21           buf[2] = buf[3];
350 21           buf[3] = buf[5];
351 21           buf[4] = '\0';
352             }
353             }
354 27           }
355              
356 9           static void colouring_fmt_rgb(colouring_rgba_t c, char *buf, size_t bufsz) {
357 9           snprintf(buf, bufsz, "rgb(%d,%d,%d)", (int)c.r, (int)c.g, (int)c.b);
358 9           }
359              
360 45           static void colouring_fmt_rgba(colouring_rgba_t c, char *buf, size_t bufsz) {
361 45           snprintf(buf, bufsz, "rgba(%d,%d,%d,%.2g)", (int)c.r, (int)c.g, (int)c.b, c.a);
362 45           }
363              
364 12           static void colouring_fmt_hsl(colouring_hsl_t hsl, char *buf, size_t bufsz) {
365             char ps[8], pl[8];
366 12           colouring_percent_buf(hsl.s, ps, sizeof(ps));
367 12           colouring_percent_buf(hsl.l, pl, sizeof(pl));
368 12           snprintf(buf, bufsz, "hsl(%d,%s,%s)", hsl.h, ps, pl);
369 12           }
370              
371 8           static void colouring_fmt_hsv(colouring_hsv_t hsv, char *buf, size_t bufsz) {
372             char ps[8], pv[8];
373 8           colouring_percent_buf(hsv.s, ps, sizeof(ps));
374 8           colouring_percent_buf(hsv.v, pv, sizeof(pv));
375 8           snprintf(buf, bufsz, "hsv(%.0f,%s,%s)", hsv.h, ps, pv);
376 8           }
377              
378 5           static void colouring_fmt_term(colouring_rgba_t c, char *buf, size_t bufsz) {
379 5           snprintf(buf, bufsz, "r%dg%db%d", (int)c.r, (int)c.g, (int)c.b);
380 5           }
381              
382 5           static void colouring_fmt_on_term(colouring_rgba_t c, char *buf, size_t bufsz) {
383 5           snprintf(buf, bufsz, "on_r%dg%db%d", (int)c.r, (int)c.g, (int)c.b);
384 5           }
385              
386             /* ── Colour string conversion (dispatch) ──────────────────────── */
387              
388             /* Parse any supported colour string into RGBA.
389             Returns 1 on success, 0 on failure. */
390 10070           static int colouring_parse(const char *str, colouring_rgba_t *out) {
391 10070 100         if (str[0] == '#') {
392 49           *out = colouring_hex2rgb(str);
393 48           return 1;
394 10021 100         } else if (str[0] == 'r' && str[1] == 'g' && str[2] == 'b') {
    50          
    50          
395             double nums[4];
396 17           int n = colouring_parse_numbers(str, nums, 4);
397 17 100         if (n < 3) return 0;
398 16           out->r = nums[0];
399 16           out->g = nums[1];
400 16           out->b = nums[2];
401 16 100         out->a = n >= 4 ? nums[3] : 1.0;
402 16           return 1;
403 10004 50         } else if (str[0] == 'h' && str[1] == 's' && str[2] == 'l') {
    50          
    50          
404             double nums[4];
405 10004           int n = colouring_parse_numbers(str, nums, 4);
406 10004 100         if (n < 3) return 0;
407 10003 100         *out = colouring_hsl2rgb(nums[0], nums[1], nums[2],
408             n >= 4 ? nums[3] : 1.0);
409 10003           return 1;
410             }
411 0           return 0;
412             }
413              
414             #endif /* COLOURING_H */