Color Picker & Converter — C source
Pick a color and convert between HEX, RGB, HSL, HSV, and CMYK with a live preview. Edit any format and copy the rest - runs entirely in your browser.
This is the C implementation — the same logic the interactive tool runs, in a shareable, citable form.
/*
* color-picker — color-space conversions, WCAG contrast, and named-color lookup.
*
* Language: C11 (ISO C standard library only).
*
* Source: CosmoDev polyglot showcase port of the color-picker tool, ported
* from src/lib/colorConvert.ts. Functionally equivalent: identical
* outputs for identical inputs, including clamping, NaN/Infinity
* handling, and failure (false / NULL) on invalid hex.
*
* RGB is the canonical hub — every other space converts through it — and
* normalize_color() re-derives the hex from its own clamped RGB so the five
* display formats can never disagree.
*
* License: display source — part of CosmoDev's polyglot tool pages.
*/
#include <ctype.h>
#include <math.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
/* A color resolved into every supported space, all mutually consistent. */
typedef struct {
char hex[8]; /* "#rrggbb" (lowercase) — the canonical handle */
double rgb[3]; /* 0–255 each */
double hsl[3]; /* h: 0–360, s/l: 0–100 */
double hsv[3]; /* h: 0–360, s/v: 0–100 */
double cmyk[4]; /* 0–100 each */
} ColorBundle;
/* A named CSS color with its hex. */
typedef struct {
const char *name;
const char *hex;
} NamedColor;
/* --- Internal clamp helpers ----------------------------------------------------
* Every public function is total: invalid hex → false; out-of-range numbers are
* clamped into their valid interval. C's fmin/fmax quietly DROP NaN operands,
* so NaN is handled explicitly first — corrupt input degrades to the lowest
* valid value instead of poisoning the result.
*/
static double clamp_range(double n, double lo, double hi)
{
if (isnan(n))
return lo;
if (n == INFINITY)
return hi;
if (n == -INFINITY)
return lo;
if (n < lo)
return lo;
if (n > hi)
return hi;
return n;
}
static double clamp01(double n)
{
return clamp_range(n, 0.0, 1.0);
}
/* Round half away from zero — mirrors JS Math.round for the non-negative
* values this module rounds. The explicit floor(n + 0.5) form documents the
* parity with the TypeScript source. */
static double round_half_up(double n)
{
return floor(n + 0.5);
}
/* Normalize any hue (negative, >360, or NaN) into [0, 360), matching
* JS ((Number(h) || 0) % 360 + 360) % 360. C's fmod takes the dividend's
* sign, like JS %.
*/
static double mod360(double h)
{
if (isnan(h))
h = 0.0;
return fmod(fmod(h, 360.0) + 360.0, 360.0);
}
/* --- HEX ↔ RGB ----------------------------------------------------------------- */
static bool is_hex_digits(const char *s, size_t len)
{
size_t i;
if (len == 0)
return false;
for (i = 0; i < len; i++) {
if (!isxdigit((unsigned char)s[i]))
return false;
}
return true;
}
/* Value of one ASCII hex digit (caller has validated it). */
static int hex_val(char c)
{
if (c >= '0' && c <= '9')
return c - '0';
if (c >= 'a' && c <= 'f')
return c - 'a' + 10;
return c - 'A' + 10;
}
/* Parse exactly two validated ASCII hex digits into 0–255. */
static double parse_hex_byte(const char *s)
{
return (double)((hex_val(s[0]) << 4) | hex_val(s[1]));
}
/* Parse "#rgb" / "#rrggbb" (case-insensitive, "#" optional) into out[3],
* or false for anything that is not a 3- or 6-digit hex color.
*/
bool hex_to_rgb(const char *hex, double out[3])
{
char expanded[7];
const char *s;
size_t len;
if (hex == NULL)
return false;
/* Trim ASCII whitespace from both ends, mirroring Python str.strip() /
* Rust str::trim() for the ASCII input this tool accepts. */
s = hex;
while (isspace((unsigned char)*s))
s++;
len = strlen(s);
while (len > 0 && isspace((unsigned char)s[len - 1]))
len--;
if (len > 0 && s[0] == '#') { /* TS strips exactly one leading '#'. */
s++;
len--;
}
/* Expand shorthand (#rgb → #rrggbb) only when it is exactly three hex digits. */
if (len == 3 && is_hex_digits(s, len)) {
expanded[0] = expanded[1] = s[0];
expanded[2] = expanded[3] = s[1];
expanded[4] = expanded[5] = s[2];
expanded[6] = '\0';
s = expanded;
len = 6;
}
if (len != 6 || !is_hex_digits(s, len))
return false;
out[0] = parse_hex_byte(s);
out[1] = parse_hex_byte(s + 2);
out[2] = parse_hex_byte(s + 4);
return true;
}
/* (r, g, b) (clamped to 0–255) → "#rrggbb" (lowercase, zero-padded).
* out must hold at least 8 bytes. */
void rgb_to_hex(double r, double g, double b, char out[8])
{
snprintf(out, 8, "#%02x%02x%02x",
(unsigned)round_half_up(clamp_range(r, 0.0, 255.0)),
(unsigned)round_half_up(clamp_range(g, 0.0, 255.0)),
(unsigned)round_half_up(clamp_range(b, 0.0, 255.0)));
}
/* --- RGB ↔ HSL ----------------------------------------------------------------- */
/* (r, g, b) (0–255) → out[3] = (h, s, l) with h: 0–360, s/l: 0–100. */
void rgb_to_hsl(double r, double g, double b, double out[3])
{
double rn = clamp01(r / 255.0);
double gn = clamp01(g / 255.0);
double bn = clamp01(b / 255.0);
double mx = fmax(fmax(rn, gn), bn);
double mn = fmin(fmin(rn, gn), bn);
double d = mx - mn;
double l = (mx + mn) / 2.0;
double h = 0.0;
double s = 0.0;
if (d != 0.0) {
/* Saturation formula branches on which half of the lightness axis we sit on. */
s = (l > 0.5) ? d / (2.0 - mx - mn) : d / (mx + mn);
if (mx == rn)
h = (gn - bn) / d + (gn < bn ? 6.0 : 0.0);
else if (mx == gn)
h = (bn - rn) / d + 2.0;
else
h = (rn - gn) / d + 4.0;
h *= 60.0;
}
out[0] = round_half_up(h);
out[1] = round_half_up(s * 100.0);
out[2] = round_half_up(l * 100.0);
}
/* (h, s, l) (h: 0–360, s/l: 0–100) → out[3] = (r, g, b) (0–255). */
void hsl_to_rgb(double h, double s, double l, double out[3])
{
double hn = mod360(h);
double sn = clamp01(s / 100.0);
double ln = clamp01(l / 100.0);
double c = (1.0 - fabs(2.0 * ln - 1.0)) * sn;
double x = c * (1.0 - fabs(fmod(hn / 60.0, 2.0) - 1.0));
double m = ln - c / 2.0;
double rr, gg, bb;
if (hn < 60.0) {
rr = c; gg = x; bb = 0.0;
} else if (hn < 120.0) {
rr = x; gg = c; bb = 0.0;
} else if (hn < 180.0) {
rr = 0.0; gg = c; bb = x;
} else if (hn < 240.0) {
rr = 0.0; gg = x; bb = c;
} else if (hn < 300.0) {
rr = x; gg = 0.0; bb = c;
} else {
rr = c; gg = 0.0; bb = x;
}
out[0] = (rr + m) * 255.0;
out[1] = (gg + m) * 255.0;
out[2] = (bb + m) * 255.0;
}
/* "#hex" → out[3] = (h, s, l), or false when the hex is invalid. */
bool hex_to_hsl(const char *hex, double out[3])
{
double rgb[3];
if (!hex_to_rgb(hex, rgb))
return false;
rgb_to_hsl(rgb[0], rgb[1], rgb[2], out);
return true;
}
/* (h, s, l) → "#rrggbb". out must hold at least 8 bytes. */
void hsl_to_hex(double h, double s, double l, char out[8])
{
double rgb[3];
hsl_to_rgb(h, s, l, rgb);
rgb_to_hex(rgb[0], rgb[1], rgb[2], out);
}
/* --- RGB ↔ HSV ----------------------------------------------------------------- */
/* (r, g, b) (0–255) → out[3] = (h, s, v) with h: 0–360, s/v: 0–100. */
void rgb_to_hsv(double r, double g, double b, double out[3])
{
double rn = clamp01(r / 255.0);
double gn = clamp01(g / 255.0);
double bn = clamp01(b / 255.0);
double mx = fmax(fmax(rn, gn), bn);
double mn = fmin(fmin(rn, gn), bn);
double d = mx - mn;
double h = 0.0;
if (d != 0.0) {
if (mx == rn)
h = (gn - bn) / d + (gn < bn ? 6.0 : 0.0);
else if (mx == gn)
h = (bn - rn) / d + 2.0;
else
h = (rn - gn) / d + 4.0;
h *= 60.0;
}
out[0] = round_half_up(h);
out[1] = round_half_up((mx == 0.0 ? 0.0 : d / mx) * 100.0);
out[2] = round_half_up(mx * 100.0);
}
/* (h, s, v) (h: 0–360, s/v: 0–100) → out[3] = (r, g, b) (0–255). */
void hsv_to_rgb(double h, double s, double v, double out[3])
{
double hn = mod360(h);
double sn = clamp01(s / 100.0);
double vn = clamp01(v / 100.0);
double c = vn * sn;
double x = c * (1.0 - fabs(fmod(hn / 60.0, 2.0) - 1.0));
double m = vn - c;
double rr, gg, bb;
if (hn < 60.0) {
rr = c; gg = x; bb = 0.0;
} else if (hn < 120.0) {
rr = x; gg = c; bb = 0.0;
} else if (hn < 180.0) {
rr = 0.0; gg = c; bb = x;
} else if (hn < 240.0) {
rr = 0.0; gg = x; bb = c;
} else if (hn < 300.0) {
rr = x; gg = 0.0; bb = c;
} else {
rr = c; gg = 0.0; bb = x;
}
out[0] = (rr + m) * 255.0;
out[1] = (gg + m) * 255.0;
out[2] = (bb + m) * 255.0;
}
/* (h, s, v) → "#rrggbb". out must hold at least 8 bytes. */
void hsv_to_hex(double h, double s, double v, char out[8])
{
double rgb[3];
hsv_to_rgb(h, s, v, rgb);
rgb_to_hex(rgb[0], rgb[1], rgb[2], out);
}
/* --- RGB ↔ CMYK ---------------------------------------------------------------- */
/* (r, g, b) (0–255) → out[4] = (c, m, y, k) (0–100 each). */
void rgb_to_cmyk(double r, double g, double b, double out[4])
{
double rn = clamp01(r / 255.0);
double gn = clamp01(g / 255.0);
double bn = clamp01(b / 255.0);
double k = 1.0 - fmax(fmax(rn, gn), bn);
if (k == 1.0) {
out[0] = 0.0; out[1] = 0.0; out[2] = 0.0; out[3] = 100.0; /* pure black — avoid divide-by-zero */
return;
}
out[0] = round_half_up((1.0 - rn - k) / (1.0 - k) * 100.0);
out[1] = round_half_up((1.0 - gn - k) / (1.0 - k) * 100.0);
out[2] = round_half_up((1.0 - bn - k) / (1.0 - k) * 100.0);
out[3] = round_half_up(k * 100.0);
}
/* (c, m, y, k) (0–100 each) → out[3] = (r, g, b) (0–255). */
void cmyk_to_rgb(double c, double m, double y, double k, double out[3])
{
double cn = clamp01(c / 100.0);
double mn = clamp01(m / 100.0);
double yn = clamp01(y / 100.0);
double kn = clamp01(k / 100.0);
out[0] = 255.0 * (1.0 - cn) * (1.0 - kn);
out[1] = 255.0 * (1.0 - mn) * (1.0 - kn);
out[2] = 255.0 * (1.0 - yn) * (1.0 - kn);
}
/* (c, m, y, k) → "#rrggbb". out must hold at least 8 bytes. */
void cmyk_to_hex(double c, double m, double y, double k, char out[8])
{
double rgb[3];
cmyk_to_rgb(c, m, y, k, rgb);
rgb_to_hex(rgb[0], rgb[1], rgb[2], out);
}
/* --- Round-robin normalizer ---------------------------------------------------- */
/* Resolve any hex into one consistent ColorBundle: the hex is re-derived from
* its own clamped RGB, then HSL/HSV/CMYK are all computed from that same RGB.
* This is the single funnel the UI routes every edit through, so the five
* display formats can never disagree. Returns false for invalid hex. */
bool normalize_color(const char *hex, ColorBundle *out)
{
double rgb[3];
if (out == NULL || !hex_to_rgb(hex, rgb))
return false;
rgb_to_hex(rgb[0], rgb[1], rgb[2], out->hex);
memcpy(out->rgb, rgb, sizeof rgb);
rgb_to_hsl(rgb[0], rgb[1], rgb[2], out->hsl);
rgb_to_hsv(rgb[0], rgb[1], rgb[2], out->hsv);
rgb_to_cmyk(rgb[0], rgb[1], rgb[2], out->cmyk);
return true;
}
/* --- WCAG luminance, contrast & text suggestion -------------------------------- */
/* Linearize a single sRGB channel (0–255) per the WCAG 2.x definition. */
static double srgb_channel(double c)
{
double s = clamp01(c / 255.0);
if (s <= 0.03928)
return s / 12.92;
return pow((s + 0.055) / 1.055, 2.4);
}
/* WCAG 2.x relative luminance of a hex (0 = black, 1 = white). */
bool relative_luminance(const char *hex, double *out)
{
double rgb[3];
if (!hex_to_rgb(hex, rgb))
return false;
*out = 0.2126 * srgb_channel(rgb[0])
+ 0.7152 * srgb_channel(rgb[1])
+ 0.0722 * srgb_channel(rgb[2]);
return true;
}
/* WCAG contrast ratio between two hexes (1–21), or false if either is invalid. */
bool contrast_ratio(const char *a, const char *b, double *out)
{
double la, lb, hi, lo;
if (!relative_luminance(a, &la) || !relative_luminance(b, &lb))
return false;
hi = (la >= lb) ? la : lb;
lo = (la >= lb) ? lb : la;
*out = (hi + 0.05) / (lo + 0.05);
return true;
}
/* Pick black or white text for maximum legibility on hex, or NULL if invalid. */
const char *suggest_text_hex(const char *hex)
{
double l;
if (!relative_luminance(hex, &l))
return NULL;
return l > 0.179 ? "#000000" : "#ffffff";
}
/* --- Closest named CSS color --------------------------------------------------- */
/* A curated set of well-known CSS named colors. Kept intentionally to entries
* whose hex is verifiable from memory — exhaustive tables typed by hand risk
* shipping wrong data, which unit tests cannot catch. */
static const struct {
const char *name;
const char *hex;
} NAMED_COLORS_RAW[] = {
{"black", "#000000"}, {"dim gray", "#696969"},
{"gray", "#808080"}, {"dark gray", "#a9a9a9"},
{"silver", "#c0c0c0"}, {"light gray", "#d3d3d3"},
{"gainsboro", "#dcdcdc"}, {"white smoke", "#f5f5f5"},
{"white", "#ffffff"}, {"snow", "#fffafa"},
{"ivory", "#fffff0"}, {"seashell", "#fff5ee"},
{"red", "#ff0000"}, {"crimson", "#dc143c"},
{"dark red", "#8b0000"},
{"firebrick", "#b22222"}, {"indian red", "#cd5c5c"},
{"salmon", "#fa8072"}, {"tomato", "#ff6347"},
{"coral", "#ff7f50"}, {"orange", "#ffa500"},
{"dark orange", "#ff8c00"}, {"gold", "#ffd700"},
{"chocolate", "#d2691e"}, {"brown", "#a52a2a"},
{"sienna", "#a0522d"}, {"tan", "#d2b48c"},
{"yellow", "#ffff00"}, {"khaki", "#f0e68c"},
{"lime", "#00ff00"}, {"lime green", "#32cd32"},
{"forest green", "#228b22"}, {"sea green", "#2e8b57"},
{"green", "#008000"}, {"dark green", "#006400"},
{"spring green", "#00ff7f"}, {"olive", "#808000"},
{"teal", "#008080"}, {"dark cyan", "#008b8b"},
{"turquoise", "#40e0d0"}, {"cyan", "#00ffff"},
{"sky blue", "#87ceeb"},
{"deep sky blue", "#00bfff"}, {"steel blue", "#4682b4"},
{"dodger blue", "#1e90ff"}, {"royal blue", "#4169e1"},
{"blue", "#0000ff"}, {"navy", "#000080"},
{"midnight blue", "#191970"}, {"indigo", "#4b0082"},
{"purple", "#800080"}, {"dark violet", "#9400d3"},
{"blue violet", "#8a2be2"}, {"medium purple", "#9370db"},
{"orchid", "#da70d6"}, {"violet", "#ee82ee"},
{"plum", "#dda0dd"}, {"magenta", "#ff00ff"},
{"deep pink", "#ff1493"}, {"hot pink", "#ff69b4"},
{"pink", "#ffc0cb"}, {"lavender", "#e6e6fa"},
};
#define NAMED_COUNT (sizeof NAMED_COLORS_RAW / sizeof NAMED_COLORS_RAW[0])
/* Pre-resolved named color: name, hex, and parsed RGB. */
typedef struct {
const char *name;
const char *hex;
double rgb[3];
} ResolvedNamed;
static ResolvedNamed g_named[NAMED_COUNT];
static bool g_named_ready = false;
/* Build the resolved named-color table once (lazy on first use) so nearest-match
* lookups never re-parse. Standard library only; single-threaded use. */
static const ResolvedNamed *named_colors(void)
{
size_t i;
if (!g_named_ready) {
for (i = 0; i < NAMED_COUNT; i++) {
double rgb[3];
if (hex_to_rgb(NAMED_COLORS_RAW[i].hex, rgb)) { /* curated hexes always parse */
g_named[i].name = NAMED_COLORS_RAW[i].name;
g_named[i].hex = NAMED_COLORS_RAW[i].hex;
memcpy(g_named[i].rgb, rgb, sizeof rgb);
}
}
g_named_ready = true;
}
return g_named;
}
/* Closest entry in the named-color table by squared RGB Euclidean distance.
* Returns false for invalid input. The table is a curated subset, so "closest"
* is approximate, not a guarantee of identity. */
bool nearest_named_color(const char *hex, NamedColor *out)
{
double rgb[3];
const ResolvedNamed *table;
size_t i, best = 0;
double best_d = INFINITY;
if (out == NULL || !hex_to_rgb(hex, rgb))
return false;
table = named_colors();
for (i = 0; i < NAMED_COUNT; i++) {
double dr = table[i].rgb[0] - rgb[0];
double dg = table[i].rgb[1] - rgb[1];
double db = table[i].rgb[2] - rgb[2];
double d = dr * dr + dg * dg + db * db;
if (d < best_d) {
best_d = d;
best = i;
}
}
out->name = table[best].name;
out->hex = table[best].hex;
return true;
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →