Skip to content

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 →