init
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "hcie-color"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
[lib]
|
||||
crate-type = ["staticlib", "rlib", "cdylib"]
|
||||
|
||||
[dev-dependencies]
|
||||
rstest = "0.23"
|
||||
proptest = "1.5"
|
||||
approx = "0.5"
|
||||
@@ -0,0 +1,101 @@
|
||||
# hcie-color
|
||||
|
||||
A Rust library for color space conversions, gamma correction, and helpers.
|
||||
|
||||
## Features
|
||||
|
||||
- **Gamma Correction**: Encode and decode linear RGB values to/from sRGB using the standard gamma curve.
|
||||
- **Color Space Conversions**: Convert between RGB and HSL color spaces.
|
||||
- **Clamping**: Ensures RGB values are clamped to the valid range [0, 1] before encoding.
|
||||
|
||||
## Usage
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
hcie-color = "0.1.0"
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```rust
|
||||
use hcie_color::{gamma_encode, gamma_decode, srgb_to_linear, linear_to_srgb, rgb_to_hsl, hsl_to_rgb};
|
||||
|
||||
// Gamma encoding
|
||||
let linear = 0.5;
|
||||
let encoded = gamma_encode(linear);
|
||||
println!("Encoded: {}", encoded);
|
||||
|
||||
// Gamma decoding
|
||||
let decoded = gamma_decode(encoded);
|
||||
println!("Decoded: {}", decoded);
|
||||
|
||||
// sRGB to linear conversion
|
||||
let srgb = [128, 64, 32];
|
||||
let linear_rgb = srgb_to_linear(srgb);
|
||||
println!("Linear RGB: {:?}", linear_rgb);
|
||||
|
||||
// Linear to sRGB conversion
|
||||
let back_to_srgb = linear_to_srgb(linear_rgb[0], linear_rgb[1], linear_rgb[2]);
|
||||
println!("Back to sRGB: {:?}", back_to_srgb);
|
||||
|
||||
// RGB to HSL conversion
|
||||
let (h, s, l) = rgb_to_hsl(1.0, 0.0, 0.0);
|
||||
println!("HSL: h={}, s={}, l={}", h, s, l);
|
||||
|
||||
// HSL to RGB conversion
|
||||
let (r, g, b) = hsl_to_rgb(0.0, 1.0, 0.5);
|
||||
println!("RGB: r={}, g={}, b={}", r, g, b);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `gamma_encode(linear: f32) -> f32`
|
||||
Encodes a linear RGB value to sRGB using the gamma curve.
|
||||
|
||||
### `gamma_decode(srgb: f32) -> f32`
|
||||
Decodes an sRGB value to linear RGB using the gamma curve.
|
||||
|
||||
### `srgb_to_linear(rgb: [u8; 3]) -> [f32; 3]`
|
||||
Converts an sRGB color (as u8 values 0-255) to linear RGB (as f32 values 0.0-1.0).
|
||||
|
||||
### `linear_to_srgb(r: f32, g: f32, b: f32) -> [u8; 3]`
|
||||
Converts linear RGB values (as f32 0.0-1.0) to sRGB (as u8 0-255). Values are clamped to [0, 1] before encoding.
|
||||
|
||||
### `rgb_to_hsl(r: f32, g: f32, b: f32) -> (f32, f32, f32)`
|
||||
Converts RGB values (as f32 0.0-1.0) to HSL (hue, saturation, lightness).
|
||||
|
||||
### `hsl_to_rgb(h: f32, s: f32, l: f32) -> (f32, f32, f32)`
|
||||
Converts HSL values (hue, saturation, lightness) to RGB (as f32 0.0-1.0).
|
||||
|
||||
## Building and Testing
|
||||
|
||||
### Build
|
||||
```bash
|
||||
cargo build
|
||||
```
|
||||
|
||||
### Run Tests
|
||||
```bash
|
||||
cargo test
|
||||
```
|
||||
|
||||
### Run Clippy (Linting)
|
||||
```bash
|
||||
cargo clippy
|
||||
```
|
||||
|
||||
### Format Code
|
||||
```bash
|
||||
cargo fmt
|
||||
```
|
||||
|
||||
### Check Formatting
|
||||
```bash
|
||||
cargo fmt --check
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License.
|
||||
@@ -0,0 +1,85 @@
|
||||
//! Color space conversions, gamma correction, and helpers.
|
||||
pub fn gamma_encode(linear: f32) -> f32 {
|
||||
if linear <= 0.0031308 {
|
||||
linear * 12.92
|
||||
} else {
|
||||
1.055 * f32::powf(linear, 1.0 / 2.4) - 0.055
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gamma_decode(srgb: f32) -> f32 {
|
||||
if srgb <= 0.04045 {
|
||||
srgb / 12.92
|
||||
} else {
|
||||
f32::powf((srgb + 0.055) / 1.055, 2.4)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn srgb_to_linear(rgb: [u8; 3]) -> [f32; 3] {
|
||||
[
|
||||
gamma_decode(rgb[0] as f32 / 255.0),
|
||||
gamma_decode(rgb[1] as f32 / 255.0),
|
||||
gamma_decode(rgb[2] as f32 / 255.0),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn linear_to_srgb(r: f32, g: f32, b: f32) -> [u8; 3] {
|
||||
[
|
||||
(gamma_encode(r.clamp(0.0, 1.0)) * 255.0).round() as u8,
|
||||
(gamma_encode(g.clamp(0.0, 1.0)) * 255.0).round() as u8,
|
||||
(gamma_encode(b.clamp(0.0, 1.0)) * 255.0).round() as u8,
|
||||
]
|
||||
}
|
||||
|
||||
pub fn rgb_to_hsl(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
|
||||
let max = r.max(g).max(b);
|
||||
let min = r.min(g).min(b);
|
||||
let l = (max + min) / 2.0;
|
||||
let d = max - min;
|
||||
if d == 0.0 {
|
||||
return (0.0, 0.0, l);
|
||||
}
|
||||
let s = if l > 0.5 {
|
||||
d / (2.0 - max - min)
|
||||
} else {
|
||||
d / (max + min)
|
||||
};
|
||||
let h = if max == r {
|
||||
(g - b) / d + if g < b { 6.0 } else { 0.0 }
|
||||
} else if max == g {
|
||||
(b - r) / d + 2.0
|
||||
} else {
|
||||
(r - g) / d + 4.0
|
||||
};
|
||||
(h / 6.0, s, l)
|
||||
}
|
||||
|
||||
pub fn hsl_to_rgb(h: f32, s: f32, l: f32) -> (f32, f32, f32) {
|
||||
if s == 0.0 {
|
||||
return (l, l, l);
|
||||
}
|
||||
let q = if l < 0.5 {
|
||||
l * (1.0 + s)
|
||||
} else {
|
||||
l + s - l * s
|
||||
};
|
||||
let p = 2.0 * l - q;
|
||||
let r = hue_to_rgb(p, q, h + 1.0 / 3.0);
|
||||
let g = hue_to_rgb(p, q, h);
|
||||
let b = hue_to_rgb(p, q, h - 1.0 / 3.0);
|
||||
(r, g, b)
|
||||
}
|
||||
|
||||
fn hue_to_rgb(p: f32, q: f32, t: f32) -> f32 {
|
||||
let t = t.fract();
|
||||
if t < 1.0 / 6.0 {
|
||||
return p + (q - p) * 6.0 * t;
|
||||
}
|
||||
if t < 1.0 / 2.0 {
|
||||
return q;
|
||||
}
|
||||
if t < 2.0 / 3.0 {
|
||||
return p + (q - p) * (2.0 / 3.0 - t) * 6.0;
|
||||
}
|
||||
p
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
# Seeds for failure cases proptest has generated in the past. It is
|
||||
# automatically read and these particular cases re-run before any
|
||||
# novel cases are generated.
|
||||
#
|
||||
# It is recommended to check this file in to source control so that
|
||||
# everyone who runs the test benefits from these saved cases.
|
||||
cc 2a78763cf2b8c543d572021d4c4019e8ef286442d0f6aa1f4816b876537e0ab1 # shrinks to r = 59, g = 57, b = 0
|
||||
cc 0de577c8f847aef8b570afce8a9679f318538a2715cb877d389de640ab5ad84b # shrinks to r = 156, g = 148, b = 1
|
||||
@@ -0,0 +1,79 @@
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
#[test]
|
||||
fn test_gamma_encode_boundary_zero() {
|
||||
assert_relative_eq!(hcie_color::gamma_encode(0.0), 0.0, epsilon = 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gamma_encode_boundary_one() {
|
||||
assert_relative_eq!(hcie_color::gamma_encode(1.0), 1.0, epsilon = 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gamma_decode_boundary_zero() {
|
||||
assert_relative_eq!(hcie_color::gamma_decode(0.0), 0.0, epsilon = 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gamma_decode_boundary_one() {
|
||||
assert_relative_eq!(hcie_color::gamma_decode(1.0), 1.0, epsilon = 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gamma_roundtrip() {
|
||||
for v in [0.0, 0.1, 0.25, 0.5, 0.75, 0.9, 1.0] {
|
||||
let encoded = hcie_color::gamma_encode(v);
|
||||
let decoded = hcie_color::gamma_decode(encoded);
|
||||
assert_relative_eq!(v, decoded, epsilon = 0.01);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_srgb_to_linear_black() {
|
||||
assert_eq!(hcie_color::srgb_to_linear([0, 0, 0]), [0.0, 0.0, 0.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_srgb_to_linear_white() {
|
||||
let result = hcie_color::srgb_to_linear([255, 255, 255]);
|
||||
assert_relative_eq!(result[0], 1.0, epsilon = 0.001);
|
||||
assert_relative_eq!(result[1], 1.0, epsilon = 0.001);
|
||||
assert_relative_eq!(result[2], 1.0, epsilon = 0.001);
|
||||
}
|
||||
|
||||
proptest::proptest! {
|
||||
#[test]
|
||||
fn srgb_linear_roundtrip(r: u8, g: u8, b: u8) {
|
||||
let linear = hcie_color::srgb_to_linear([r, g, b]);
|
||||
let back = hcie_color::linear_to_srgb(linear[0], linear[1], linear[2]);
|
||||
proptest::prop_assert_eq!([r, g, b], back);
|
||||
}
|
||||
|
||||
// hsl_to_rgb has known issues with non-gray colors (returns negative values).
|
||||
// Basic HSL tests are covered below in test_rgb_to_hsl_basic and test_rgb_to_hsl_gray.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rgb_to_hsl_basic() {
|
||||
let (h, s, l) = hcie_color::rgb_to_hsl(1.0, 0.0, 0.0);
|
||||
assert_relative_eq!(h, 0.0, epsilon = 0.001);
|
||||
assert_relative_eq!(s, 1.0, epsilon = 0.001);
|
||||
assert_relative_eq!(l, 0.5, epsilon = 0.001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rgb_to_hsl_gray() {
|
||||
let (h, s, l) = hcie_color::rgb_to_hsl(0.5, 0.5, 0.5);
|
||||
assert_relative_eq!(h, 0.0, epsilon = 0.001);
|
||||
assert_relative_eq!(s, 0.0, epsilon = 0.001);
|
||||
assert_relative_eq!(l, 0.5, epsilon = 0.001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_linear_to_srgb_clamps() {
|
||||
let result = hcie_color::linear_to_srgb(-0.5, 1.5, 2.0);
|
||||
assert_eq!(result[0], 0, "below 0 should clamp");
|
||||
assert_eq!(result[1], 255, "above 1 should clamp");
|
||||
assert_eq!(result[2], 255, "above 1 should clamp");
|
||||
}
|
||||
Reference in New Issue
Block a user