llm 1.3.8

A Rust library unifying multiple LLM backends.
Documentation
use super::Rgb;

#[derive(Clone, Copy)]
pub(super) struct Lab {
    l: f32,
    a: f32,
    b: f32,
}

pub(super) fn blend_lab(a: Rgb, b: Rgb, t: f32) -> Rgb {
    let a_lab = rgb_to_lab(a);
    let b_lab = rgb_to_lab(b);
    let lab = Lab {
        l: lerp(a_lab.l, b_lab.l, t),
        a: lerp(a_lab.a, b_lab.a, t),
        b: lerp(a_lab.b, b_lab.b, t),
    };
    lab_to_rgb(lab)
}

pub(super) fn rgb_to_lab(rgb: Rgb) -> Lab {
    let (x, y, z) = rgb_to_xyz(rgb);
    xyz_to_lab(x, y, z)
}

pub(super) fn lab_distance(a: Lab, b: Lab) -> f32 {
    let dl = a.l - b.l;
    let da = a.a - b.a;
    let db = a.b - b.b;
    (dl * dl + da * da + db * db).sqrt()
}

fn lerp(a: f32, b: f32, t: f32) -> f32 {
    a + (b - a) * t
}

fn lab_to_rgb(lab: Lab) -> Rgb {
    let (x, y, z) = lab_to_xyz(lab);
    xyz_to_rgb(x, y, z)
}

fn rgb_to_xyz(rgb: Rgb) -> (f32, f32, f32) {
    let r = srgb_to_linear(rgb.r);
    let g = srgb_to_linear(rgb.g);
    let b = srgb_to_linear(rgb.b);
    let x = (r * RGB_TO_XYZ[0]) + (g * RGB_TO_XYZ[1]) + (b * RGB_TO_XYZ[2]);
    let y = (r * RGB_TO_XYZ[3]) + (g * RGB_TO_XYZ[4]) + (b * RGB_TO_XYZ[5]);
    let z = (r * RGB_TO_XYZ[6]) + (g * RGB_TO_XYZ[7]) + (b * RGB_TO_XYZ[8]);
    (x, y, z)
}

fn xyz_to_rgb(x: f32, y: f32, z: f32) -> Rgb {
    let r = (x * XYZ_TO_RGB[0]) + (y * XYZ_TO_RGB[1]) + (z * XYZ_TO_RGB[2]);
    let g = (x * XYZ_TO_RGB[3]) + (y * XYZ_TO_RGB[4]) + (z * XYZ_TO_RGB[5]);
    let b = (x * XYZ_TO_RGB[6]) + (y * XYZ_TO_RGB[7]) + (z * XYZ_TO_RGB[8]);
    let r = linear_to_srgb(r);
    let g = linear_to_srgb(g);
    let b = linear_to_srgb(b);
    Rgb::new(r, g, b)
}

fn srgb_to_linear(value: u8) -> f32 {
    let normalized = value as f32 / SRGB_MAX;
    if normalized <= SRGB_EPS {
        normalized / SRGB_DIV
    } else {
        ((normalized + SRGB_OFF) / SRGB_SCALE).powf(SRGB_GAMMA)
    }
}

fn linear_to_srgb(value: f32) -> u8 {
    let clamped = value.clamp(0.0, 1.0);
    let encoded = if clamped <= LIN_EPS {
        clamped * SRGB_DIV
    } else {
        SRGB_SCALE * clamped.powf(1.0 / SRGB_GAMMA) - SRGB_OFF
    };
    (encoded * SRGB_MAX).round().clamp(0.0, SRGB_MAX) as u8
}

fn xyz_to_lab(x: f32, y: f32, z: f32) -> Lab {
    let x = lab_f(x / D65_WHITE[0]);
    let y = lab_f(y / D65_WHITE[1]);
    let z = lab_f(z / D65_WHITE[2]);
    Lab {
        l: (LAB_SCALE * y) - LAB_OFF,
        a: LAB_A_SCALE * (x - y),
        b: LAB_B_SCALE * (y - z),
    }
}

fn lab_to_xyz(lab: Lab) -> (f32, f32, f32) {
    let y = (lab.l + LAB_OFF) / LAB_SCALE;
    let x = lab.a / LAB_A_SCALE + y;
    let z = y - lab.b / LAB_B_SCALE;
    (
        D65_WHITE[0] * lab_inv_f(x),
        D65_WHITE[1] * lab_inv_f(y),
        D65_WHITE[2] * lab_inv_f(z),
    )
}

fn lab_f(value: f32) -> f32 {
    if value > LAB_EPS {
        value.powf(1.0 / LAB_KAPPA)
    } else {
        (LAB_KAPPA * value + LAB_OFF) / LAB_SCALE
    }
}

fn lab_inv_f(value: f32) -> f32 {
    let cube = value.powi(3);
    if cube > LAB_EPS {
        cube
    } else {
        (LAB_SCALE * value - LAB_OFF) / LAB_KAPPA
    }
}

const SRGB_MAX: f32 = 255.0;
const SRGB_EPS: f32 = 0.04045;
const SRGB_DIV: f32 = 12.92;
const SRGB_OFF: f32 = 0.055;
const SRGB_SCALE: f32 = 1.055;
const SRGB_GAMMA: f32 = 2.4;
const LIN_EPS: f32 = 0.0031308;

const D65_WHITE: [f32; 3] = [95.047, 100.0, 108.883];
const LAB_EPS: f32 = 0.008856;
const LAB_KAPPA: f32 = 7.787;
const LAB_SCALE: f32 = 116.0;
const LAB_OFF: f32 = 16.0;
const LAB_A_SCALE: f32 = 500.0;
const LAB_B_SCALE: f32 = 200.0;

const RGB_TO_XYZ: [f32; 9] = [41.24, 35.76, 18.05, 21.26, 71.52, 7.22, 1.93, 11.92, 95.05];
const XYZ_TO_RGB: [f32; 9] = [
    3.2406, -1.5372, -0.4986, -0.9689, 1.8758, 0.0415, 0.0557, -0.2040, 1.0570,
];