chromaframe-sdk 0.1.1

Deterministic, privacy-preserving color measurement and ranking SDK
Documentation
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct Lab {
    pub l: f32,
    pub a: f32,
    pub b: f32,
}

#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct Lch {
    pub l: f32,
    pub c: f32,
    pub h: f32,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct DirectionalDelta {
    pub from: String,
    pub to: String,
    pub delta_l: f32,
    pub delta_a: f32,
    pub delta_b: f32,
    pub delta_e00: f32,
    pub michelson_lightness_contrast: f32,
}

pub fn srgb_to_lab(rgb: [u8; 3]) -> Lab {
    let [r, g, b] = rgb.map(|channel| srgb_channel_to_linear(f32::from(channel) / 255.0));
    let x = (0.412_456_4 * r + 0.357_576_1 * g + 0.180_437_5 * b) / 0.95047;
    let y = 0.212_672_9 * r + 0.715_152_2 * g + 0.072_175 * b;
    let z = (0.019_333_9 * r + 0.119_192 * g + 0.950_304_1 * b) / 1.08883;
    let fx = xyz_pivot(x);
    let fy = xyz_pivot(y);
    let fz = xyz_pivot(z);
    Lab {
        l: 116.0 * fy - 16.0,
        a: 500.0 * (fx - fy),
        b: 200.0 * (fy - fz),
    }
}

#[must_use]
pub fn lab_to_lch(lab: Lab) -> Lch {
    let c = (lab.a.mul_add(lab.a, lab.b * lab.b)).sqrt();
    let mut h = lab.b.atan2(lab.a).to_degrees();
    if h < 0.0 {
        h += 360.0;
    }
    Lch { l: lab.l, c, h }
}

#[must_use]
pub fn ita_degrees(lab: Lab) -> f32 {
    ((lab.l - 50.0) / lab.b.max(0.001)).atan().to_degrees()
}

#[must_use]
pub fn depth_proxy(lab: Lab) -> f32 {
    crate::score::clamp01(1.0 - ((ita_degrees(lab) + 55.0) / 110.0))
}

#[must_use]
pub fn michelson_lightness_contrast(first: Lab, second: Lab) -> f32 {
    ((first.l - second.l).abs() / (first.l + second.l).max(0.001)).clamp(0.0, 1.0)
}

#[must_use]
pub fn directional_delta(
    from_name: impl Into<String>,
    from: Lab,
    to_name: impl Into<String>,
    to: Lab,
) -> DirectionalDelta {
    DirectionalDelta {
        from: from_name.into(),
        to: to_name.into(),
        delta_l: to.l - from.l,
        delta_a: to.a - from.a,
        delta_b: to.b - from.b,
        delta_e00: delta_e00(from, to),
        michelson_lightness_contrast: michelson_lightness_contrast(from, to),
    }
}

#[must_use]
pub fn delta_e00(first: Lab, second: Lab) -> f32 {
    let c1 = (first.a.mul_add(first.a, first.b * first.b)).sqrt();
    let c2 = (second.a.mul_add(second.a, second.b * second.b)).sqrt();
    let c_bar = (c1 + c2) / 2.0;
    let c_bar7 = c_bar.powi(7);
    let g = 0.5 * (1.0 - (c_bar7 / (c_bar7 + 25_f32.powi(7))).sqrt());
    let a1p = (1.0 + g) * first.a;
    let a2p = (1.0 + g) * second.a;
    let c1p = (a1p.mul_add(a1p, first.b * first.b)).sqrt();
    let c2p = (a2p.mul_add(a2p, second.b * second.b)).sqrt();
    let h1p = hue_degrees(first.b, a1p);
    let h2p = hue_degrees(second.b, a2p);
    let dlp = second.l - first.l;
    let dcp = c2p - c1p;
    let dhp = hue_delta(h1p, h2p, c1p, c2p);
    let dh_big = 2.0 * (c1p * c2p).sqrt() * (dhp.to_radians() / 2.0).sin();
    let lp_bar = (first.l + second.l) / 2.0;
    let cp_bar = (c1p + c2p) / 2.0;
    let hp_bar = hue_average(h1p, h2p, c1p, c2p);
    let t = 1.0 - 0.17 * (hp_bar - 30.0).to_radians().cos()
        + 0.24 * (2.0 * hp_bar).to_radians().cos()
        + 0.32 * (3.0 * hp_bar + 6.0).to_radians().cos()
        - 0.20 * (4.0 * hp_bar - 63.0).to_radians().cos();
    let sl = 1.0 + (0.015 * (lp_bar - 50.0).powi(2)) / (20.0 + (lp_bar - 50.0).powi(2)).sqrt();
    let sc = 1.0 + 0.045 * cp_bar;
    let sh = 1.0 + 0.015 * cp_bar * t;
    let delta_theta = 30.0 * (-((hp_bar - 275.0) / 25.0).powi(2)).exp();
    let rc = 2.0 * (cp_bar.powi(7) / (cp_bar.powi(7) + 25_f32.powi(7))).sqrt();
    let rt = -rc * (2.0 * delta_theta).to_radians().sin();
    let l_term = dlp / sl;
    let c_term = dcp / sc;
    let h_term = dh_big / sh;
    (l_term.powi(2) + c_term.powi(2) + h_term.powi(2) + rt * c_term * h_term)
        .max(0.0)
        .sqrt()
}

#[must_use]
pub fn srgb_channel_to_linear(value: f32) -> f32 {
    if value <= 0.04045 {
        value / 12.92
    } else {
        ((value + 0.055) / 1.055).powf(2.4)
    }
}

fn xyz_pivot(value: f32) -> f32 {
    if value > 216.0 / 24389.0 {
        value.cbrt()
    } else {
        (841.0 / 108.0) * value + 4.0 / 29.0
    }
}

fn hue_degrees(b: f32, a: f32) -> f32 {
    if b == 0.0 && a == 0.0 {
        0.0
    } else {
        b.atan2(a).to_degrees().rem_euclid(360.0)
    }
}
fn hue_delta(h1: f32, h2: f32, c1: f32, c2: f32) -> f32 {
    if c1 * c2 == 0.0 {
        0.0
    } else if (h2 - h1).abs() <= 180.0 {
        h2 - h1
    } else if h2 <= h1 {
        h2 - h1 + 360.0
    } else {
        h2 - h1 - 360.0
    }
}
fn hue_average(h1: f32, h2: f32, c1: f32, c2: f32) -> f32 {
    if c1 * c2 == 0.0 {
        h1 + h2
    } else if (h1 - h2).abs() <= 180.0 {
        (h1 + h2) / 2.0
    } else if h1 + h2 < 360.0 {
        (h1 + h2 + 360.0) / 2.0
    } else {
        (h1 + h2 - 360.0) / 2.0
    }
}