From e9b727e7a9b6163ae137ea2998c230ee810f62d6 Mon Sep 17 00:00:00 2001 From: Beinsezii Date: Mon, 17 Jun 2024 18:56:52 -0700 Subject: [PATCH] Merge branch `4chan` Adds an optional 4th channel to all functions Squashed commit of the following: commit 75d3922419a80065ae542b1249b361e797edf733 Author: Beinsezii Date: Mon Jun 17 18:46:30 2024 -0700 Update docs/readme for generics commit 521219bc68c1d2e4b37ecfb3cc5f24ee72893b52 Author: Beinsezii Date: Mon Jun 17 18:36:55 2024 -0700 Convert str2col into DType and const N It's getting hectic. I'll want to mildly refactor the tests later. commit 511b29273b9fc59c041731d912f15e61dfbb23c9 Author: Beinsezii Date: Mon Jun 17 17:46:45 2024 -0700 Move DEFAULT const into separate function Much cleaner. This is the way. commit 01cd2f11f3e5ed7b4d58dd005c695f8c6d3226d3 Author: Beinsezii Date: Mon Jun 17 17:34:31 2024 -0700 Rough const N hex functions Pretty ugly ngl. You can't const N: usize = 255 for some stupid reason commit 5066cf8b2dc671fb794b9792252ada9329dea983 Author: Beinsezii Date: Sat Jun 15 00:53:18 2024 -0700 Allow tests on non-master pushes commit 4cf16b97ce30fd96fe0ec78a2545b02e7aaa6fe2 Author: Beinsezii Date: Sat Jun 15 00:14:52 2024 -0700 4 channel HK and extern "C" functions Should cover everything except hex commit 825a789d5b6b5f120c5259fa10a6987c147492c9 Author: Beinsezii Date: Fri Jun 14 23:57:11 2024 -0700 Missing `where` commit bb5d4ce48cb75da85e468d94094eafc9f0fb8c17 Author: Beinsezii Date: Fri Jun 14 23:54:17 2024 -0700 Switch irgb functions to No hex yet because it'd be not `const`, yet I still want an array if I can... commit 4675887a78c7e590db4d8c8e7a047c52a2d66e83 Author: Beinsezii Date: Fri Jun 14 19:08:28 2024 -0700 Update benches for 4-width commit d9380e460d820ddd666e2df821930d19a25a1587 Author: Beinsezii Date: Fri Jun 14 18:28:16 2024 -0700 Move `convert_space` fns to N channel Remove `convert_space_alpha` commit a2b1913b9137fff77061fcc1db8428ba5694d96e Author: Beinsezii Date: Fri Jun 14 18:09:49 2024 -0700 alpha_untouch test commit 39400cf55f9e5dc1cd5d43d5065e5873049a9c3b Author: Beinsezii Date: Fri Jun 14 18:00:50 2024 -0700 N channels on most forward/backward functions Integer/hex could work too but needs tests commit 3475859059b896e9c74c59756eb9c60207e068f4 Author: Beinsezii Date: Thu Jun 13 19:28:53 2024 -0700 remove unsafe commit e20941309d87f549d06b1f207535ba2896ec970e Merge: 85c64cb c133495 Author: Beinsezii Date: Thu Jun 13 18:56:27 2024 -0700 Merge branch 'master' into 4chan -X theirs commit 85c64cbff49cc6985ab272bb2b68a99ec71e5d19 Author: Beinsezii Date: Tue Jun 11 17:08:12 2024 -0700 4 Channel function variants proof-of-concept I probably need to refactor the benchmark functions to be less copy paste hell --- README.md | 2 +- benches/conversions.rs | 45 +++- scripts/test_ctypes.py | 82 ++++--- src/lib.rs | 489 ++++++++++++++++++++++++++++++----------- src/tests.rs | 212 ++++++++++++++---- 5 files changed, 615 insertions(+), 215 deletions(-) diff --git a/README.md b/README.md index 41583fd..e235004 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # ColCon 0.9.0 -Simple colorspace conversions in Rust. +Comprehensive colorspace conversions in Rust. ## Features * Pure Rust, no dependencies. diff --git a/benches/conversions.rs b/benches/conversions.rs index fbf8fb9..c44b267 100644 --- a/benches/conversions.rs +++ b/benches/conversions.rs @@ -1,13 +1,13 @@ use colcon::Space; use criterion::{black_box, criterion_group, criterion_main, Criterion}; -fn pixels() -> Vec { +fn pixels() -> Vec { let size = 512; let mut result = Vec::::with_capacity(size * size * 3); for x in 1..=size { for y in 1..=size { let n = (x as f32 / size as f32 / 2.0) + (y as f32 / size as f32 / 2.0); - result.extend_from_slice(&[n; 3]); + result.extend_from_slice(&[n; N]); } } result @@ -58,51 +58,74 @@ macro_rules! bench_convert_generic { $c.bench_function(concat!($id, "_", $n, $ts, "_slice"), |b| { b.iter(|| { let mut pixels = $ps.clone(); - black_box(colcon::convert_space_sliced($from, $to, &mut pixels)); + black_box(colcon::convert_space_sliced::<_, 3>($from, $to, &mut pixels)); }) }); }; } pub fn conversions(c: &mut Criterion) { - let pix_slice_f32: Box<[f32]> = pixels().into_boxed_slice(); + let pix_slice_3f32: Box<[f32]> = pixels::<3>().into_boxed_slice(); + let pix_slice_4f32: Box<[f32]> = pixels::<4>().into_boxed_slice(); - let pix_slice_f64: Box<[f64]> = pixels() + let pix_slice_3f64: Box<[f64]> = pixels::<3>() .into_iter() .map(|c| c.into()) .collect::>() .into_boxed_slice(); - let pix_chunk_3f32: Box<[[f32; 3]]> = pixels() + let pix_slice_4f64: Box<[f64]> = pixels::<4>() + .into_iter() + .map(|c| c.into()) + .collect::>() + .into_boxed_slice(); + + let pix_chunk_3f32: Box<[[f32; 3]]> = pixels::<3>() .chunks_exact(3) .map(|c| c.try_into().unwrap()) .collect::>() .into_boxed_slice(); - let pix_chunk_3f64: Box<[[f64; 3]]> = pixels() + let pix_chunk_4f32: Box<[[f32; 4]]> = pixels::<4>() + .chunks_exact(4) + .map(|c| c.try_into().unwrap()) + .collect::>() + .into_boxed_slice(); + + let pix_chunk_3f64: Box<[[f64; 3]]> = pixels::<3>() .chunks_exact(3) .map(|c| TryInto::<[f32; 3]>::try_into(c).unwrap().map(|n| n.into())) .collect::>() .into_boxed_slice(); + let pix_chunk_4f64: Box<[[f64; 4]]> = pixels::<4>() + .chunks_exact(4) + .map(|c| TryInto::<[f32; 4]>::try_into(c).unwrap().map(|n| n.into())) + .collect::>() + .into_boxed_slice(); + macro_rules! bench_three { ($f: path, $id:literal) => { bench_three_generic!(c, pix_chunk_3f32, $f, $id, 3, f32, "f32"); bench_three_generic!(c, pix_chunk_3f64, $f, $id, 3, f64, "f64"); + bench_three_generic!(c, pix_chunk_4f32, $f, $id, 4, f32, "f32"); + bench_three_generic!(c, pix_chunk_4f64, $f, $id, 4, f64, "f64"); }; } macro_rules! bench_one { ($f: path, $id:literal) => { - bench_one_generic!(c, pix_slice_f32, $f, $id, f32, "f32"); - bench_one_generic!(c, pix_slice_f32, $f, $id, f64, "f64"); + bench_one_generic!(c, pix_slice_3f32, $f, $id, f32, "f32"); + bench_one_generic!(c, pix_slice_3f32, $f, $id, f64, "f64"); }; } macro_rules! bench_convert { ($from: expr, $to:expr, $id:literal) => { - bench_convert_generic!(c, pix_slice_f32, pix_chunk_3f32, $from, $to, $id, 3, f32, "f32"); - bench_convert_generic!(c, pix_slice_f64, pix_chunk_3f64, $from, $to, $id, 3, f64, "f64"); + bench_convert_generic!(c, pix_slice_3f32, pix_chunk_3f32, $from, $to, $id, 3, f32, "f32"); + bench_convert_generic!(c, pix_slice_3f64, pix_chunk_3f64, $from, $to, $id, 3, f64, "f64"); + bench_convert_generic!(c, pix_slice_4f32, pix_chunk_4f32, $from, $to, $id, 4, f32, "f32"); + bench_convert_generic!(c, pix_slice_4f64, pix_chunk_4f64, $from, $to, $id, 4, f64, "f64"); }; } diff --git a/scripts/test_ctypes.py b/scripts/test_ctypes.py index c293272..8294f86 100755 --- a/scripts/test_ctypes.py +++ b/scripts/test_ctypes.py @@ -1,6 +1,7 @@ #! /usr/bin/env python3 import ctypes from sys import platform + cpixel = ctypes.c_float * 3 cpixels = ctypes.POINTER(ctypes.c_float) @@ -13,26 +14,31 @@ colcon = ctypes.CDLL(f"./target/release/{LIBRARY}") -colcon.convert_space_ffi.argtypes = [ctypes.c_char_p, ctypes.c_char_p, cpixels, ctypes.c_uint] +colcon.convert_space_ffi.argtypes = [ + ctypes.c_char_p, + ctypes.c_char_p, + cpixels, + ctypes.c_uint, +] colcon.convert_space_ffi.restype = ctypes.c_int32 # up -colcon.srgb_to_hsv_f32.argtypes = [cpixel] -colcon.srgb_to_lrgb_f32.argtypes = [cpixel] -colcon.lrgb_to_xyz_f32.argtypes = [cpixel] -colcon.xyz_to_cielab_f32.argtypes = [cpixel] -colcon.xyz_to_oklab_f32.argtypes = [cpixel] -colcon.xyz_to_jzazbz_f32.argtypes = [cpixel] -colcon.lab_to_lch_f32.argtypes = [cpixel] +colcon.srgb_to_hsv_3f32.argtypes = [cpixel] +colcon.srgb_to_lrgb_3f32.argtypes = [cpixel] +colcon.lrgb_to_xyz_3f32.argtypes = [cpixel] +colcon.xyz_to_cielab_3f32.argtypes = [cpixel] +colcon.xyz_to_oklab_3f32.argtypes = [cpixel] +colcon.xyz_to_jzazbz_3f32.argtypes = [cpixel] +colcon.lab_to_lch_3f32.argtypes = [cpixel] # down -colcon.lch_to_lab_f32.argtypes = [cpixel] -colcon.jzazbz_to_xyz_f32.argtypes = [cpixel] -colcon.oklab_to_xyz_f32.argtypes = [cpixel] -colcon.cielab_to_xyz_f32.argtypes = [cpixel] -colcon.xyz_to_lrgb_f32.argtypes = [cpixel] -colcon.lrgb_to_srgb_f32.argtypes = [cpixel] -colcon.srgb_to_hsv_f32.argtypes = [cpixel] +colcon.lch_to_lab_3f32.argtypes = [cpixel] +colcon.jzazbz_to_xyz_3f32.argtypes = [cpixel] +colcon.oklab_to_xyz_3f32.argtypes = [cpixel] +colcon.cielab_to_xyz_3f32.argtypes = [cpixel] +colcon.xyz_to_lrgb_3f32.argtypes = [cpixel] +colcon.lrgb_to_srgb_3f32.argtypes = [cpixel] +colcon.srgb_to_hsv_3f32.argtypes = [cpixel] # extra colcon.srgb_eotf_f32.argtypes = [ctypes.c_float] @@ -47,8 +53,14 @@ colcon.pq_oetf_f32.restype = ctypes.c_float colcon.pqz_oetf_f32.argtypes = [ctypes.c_float] colcon.pqz_oetf_f32.restype = ctypes.c_float -colcon.hk_high2023_f32.argtypes = [cpixel] -colcon.hk_high2023_comp_f32.argtypes = [cpixel] +colcon.hk_high2023_3f32.argtypes = [cpixel] +colcon.hk_high2023_comp_3f32.argtypes = [cpixel] + +# other dtypes +colcon.srgb_to_lrgb_4f32.argtypes = [ctypes.c_float * 4] +colcon.srgb_to_lrgb_3f64.argtypes = [ctypes.c_double * 3] +colcon.srgb_to_lrgb_4f64.argtypes = [ctypes.c_double * 4] + SRGB = [0.20000000, 0.35000000, 0.95000000] LRGB = [0.03310477, 0.10048151, 0.89000541] @@ -59,69 +71,73 @@ OKLAB = [0.53893206, -0.01239956, -0.23206808] JZAZBZ = [0.00601244, -0.00145433, -0.01984568] + def pixcmp(a, b): epsilon = 1e-5 - for (ac, bc) in zip(a, b): + for ac, bc in zip(a, b): if abs(ac - bc) > epsilon: - print(f"\nFAIL:\n[{a[0]:.8f}, {a[1]:.8f}, {a[2]:.8f}]\n[{b[0]:.8f}, {b[1]:.8f}, {b[2]:.8f}]\n") + print( + f"\nFAIL:\n[{a[0]:.8f}, {a[1]:.8f}, {a[2]:.8f}]\n[{b[0]:.8f}, {b[1]:.8f}, {b[2]:.8f}]\n" + ) break + # up pix = cpixel(*SRGB) -colcon.srgb_to_hsv_f32(pix) +colcon.srgb_to_hsv_3f32(pix) pixcmp(list(pix), HSV) pix = cpixel(*SRGB) -colcon.srgb_to_lrgb_f32(pix) +colcon.srgb_to_lrgb_3f32(pix) pixcmp(list(pix), LRGB) pix = cpixel(*LRGB) -colcon.lrgb_to_xyz_f32(pix) +colcon.lrgb_to_xyz_3f32(pix) pixcmp(list(pix), XYZ) pix = cpixel(*XYZ) -colcon.xyz_to_cielab_f32(pix) +colcon.xyz_to_cielab_3f32(pix) pixcmp(list(pix), LAB) pix = cpixel(*XYZ) -colcon.xyz_to_oklab_f32(pix) +colcon.xyz_to_oklab_3f32(pix) pixcmp(list(pix), OKLAB) pix = cpixel(*XYZ) -colcon.xyz_to_jzazbz_f32(pix) +colcon.xyz_to_jzazbz_3f32(pix) pixcmp(list(pix), JZAZBZ) pix = cpixel(*LAB) -colcon.lab_to_lch_f32(pix) +colcon.lab_to_lch_3f32(pix) pixcmp(list(pix), LCH) # down pix = cpixel(*LCH) -colcon.lch_to_lab_f32(pix) +colcon.lch_to_lab_3f32(pix) pixcmp(list(pix), LAB) pix = cpixel(*LAB) -colcon.cielab_to_xyz_f32(pix) +colcon.cielab_to_xyz_3f32(pix) pixcmp(list(pix), XYZ) pix = cpixel(*JZAZBZ) -colcon.jzazbz_to_xyz_f32(pix) +colcon.jzazbz_to_xyz_3f32(pix) pixcmp(list(pix), XYZ) pix = cpixel(*OKLAB) -colcon.oklab_to_xyz_f32(pix) +colcon.oklab_to_xyz_3f32(pix) pixcmp(list(pix), XYZ) pix = cpixel(*XYZ) -colcon.xyz_to_lrgb_f32(pix) +colcon.xyz_to_lrgb_3f32(pix) pixcmp(list(pix), LRGB) pix = cpixel(*LRGB) -colcon.lrgb_to_srgb_f32(pix) +colcon.lrgb_to_srgb_3f32(pix) pixcmp(list(pix), SRGB) pix = cpixel(*SRGB) -colcon.srgb_to_hsv_f32(pix) +colcon.srgb_to_hsv_3f32(pix) pixcmp(list(pix), HSV) pix = (ctypes.c_float * len(SRGB))(*SRGB) diff --git a/src/lib.rs b/src/lib.rs index 6ef84ed..02c629b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,11 @@ #![warn(missing_docs)] -//! Simple colorspace conversions in pure Rust. +//! Comprehensive colorspace conversions in pure Rust +//! +//! The working data structure is `[DType; ValidChannels]`, where DType is one of +//! `f32` or `f64` and ValidChannels is either 3 or 4, with the 4th channel representing +//! alpha and being unprocessed outside of typing conversions //! -//! All conversions are in-place, except when converting to/from integer and hexadecimal. //! Formulae are generally taken from their research papers or Wikipedia and validated against //! colour-science //! @@ -17,6 +20,15 @@ use core::ops::{Add, Div, Mul, Neg, Rem, Sub}; // DType {{{ +/// 3 channels, or 4 with alpha. +/// Alpha ignored during space conversions. +pub struct Channels; +/// 3 channels, or 4 with alpha. +/// Alpha ignored during space conversions. +pub trait ValidChannels {} +impl ValidChannels for Channels<3> {} +impl ValidChannels for Channels<4> {} + #[allow(missing_docs)] /// Convert an F32 ito any supported DType pub trait FromF32: Sized { @@ -422,7 +434,10 @@ pub const HIGH2023_MEAN: f32 = 20.956442; /// Returns difference in perceptual lightness based on hue, aka the Helmholtz-Kohlrausch effect. /// High et al 2023 implementation. -pub fn hk_high2023(lch: &[T; 3]) -> T { +pub fn hk_high2023(lch: &[T; N]) -> T +where + Channels: ValidChannels, +{ let fby: T = T::ff32(K_HIGH2022[0]).fma( ((lch[2] - 90.0.to_dt()) / 2.0.to_dt()).to_radians().sin().abs(), K_HIGH2022[1].to_dt(), @@ -439,7 +454,10 @@ pub fn hk_high2023(lch: &[T; 3]) -> T { /// Compensates CIE LCH's L value for the Helmholtz-Kohlrausch effect. /// High et al 2023 implementation. -pub fn hk_high2023_comp(lch: &mut [T; 3]) { +pub fn hk_high2023_comp(lch: &mut [T; N]) +where + Channels: ValidChannels, +{ lch[0] = lch[0] + (T::ff32(HIGH2023_MEAN) - hk_high2023(lch)) * (lch[1] / 100.0.to_dt()) } @@ -754,9 +772,10 @@ macro_rules! op_chunk { macro_rules! op_inter { ($func:ident, $data:expr) => { - $data - .chunks_exact_mut(3) - .for_each(|pixel| $func(pixel.try_into().unwrap())) + $data.chunks_exact_mut(N).for_each(|pixel| { + let pixel: &mut [T; N] = pixel.try_into().unwrap(); + $func(pixel); + }) }; } @@ -813,7 +832,10 @@ macro_rules! graph { /// Runs conversion functions to convert `pixel` from one `Space` to another /// in the least possible moves. -pub fn convert_space(from: Space, to: Space, pixel: &mut [T; 3]) { +pub fn convert_space(from: Space, to: Space, pixel: &mut [T; N]) +where + Channels: ValidChannels, +{ graph!(convert_space, pixel, from, to, op_single); } @@ -821,7 +843,10 @@ pub fn convert_space(from: Space, to: Space, pixel: &mut [T; 3]) { /// in the least possible moves. /// /// Caches conversion graph for faster iteration. -pub fn convert_space_chunked(from: Space, to: Space, pixels: &mut [[T; 3]]) { +pub fn convert_space_chunked(from: Space, to: Space, pixels: &mut [[T; N]]) +where + Channels: ValidChannels, +{ graph!(convert_space_chunked, pixels, from, to, op_chunk); } @@ -829,7 +854,10 @@ pub fn convert_space_chunked(from: Space, to: Space, pixels: &mut [[T; /// in the least possible moves. /// /// Caches conversion graph for faster iteration and ignores remainder values in slice. -pub fn convert_space_sliced(from: Space, to: Space, pixels: &mut [T]) { +pub fn convert_space_sliced(from: Space, to: Space, pixels: &mut [T]) +where + Channels: ValidChannels, +{ graph!(convert_space_sliced, pixels, from, to, op_inter); } @@ -877,15 +905,10 @@ pub extern "C" fn convert_space_ffi(from: *const c_char, to: *const c_char, pixe core::slice::from_raw_parts_mut(pixels, len) } }; - convert_space_sliced(from, to, pixels); + convert_space_sliced::<_, 3>(from, to, pixels); 0 } -/// Same as `convert_space`, ignores the 4th value in `pixel`. -pub fn convert_space_alpha(from: Space, to: Space, pixel: &mut [f32; 4]) { - unsafe { convert_space(from, to, pixel.get_unchecked_mut(0..3).try_into().unwrap_unchecked()) } -} - // ### Convert Space ### }}} // ### Str2Col ### {{{ @@ -903,22 +926,25 @@ fn rm_paren<'a>(s: &'a str) -> &'a str { /// /// Can additionally be set as a % of SDR range. /// -/// Does not support alpha channel. +/// Alpha will be NaN if only 3 values are provided. /// /// # Examples /// /// ``` /// use colcon::{str2col, Space}; /// -/// assert_eq!(str2col("0.2, 0.5, 0.6"), Some((Space::SRGB, [0.2, 0.5, 0.6]))); -/// assert_eq!(str2col("lch:50;20;120"), Some((Space::CIELCH, [50.0, 20.0, 120.0]))); -/// assert_eq!(str2col("oklab(0.2, 0.6, -0.5)"), Some((Space::OKLAB, [0.2, 0.6, -0.5]))); -/// assert_eq!(str2col("srgb 100% 50% 25%"), Some((Space::SRGB, [1.0, 0.5, 0.25]))); +/// assert_eq!(str2col("0.2, 0.5, 0.6"), Some((Space::SRGB, [0.2f32, 0.5, 0.6]))); +/// assert_eq!(str2col("lch:50;20;120"), Some((Space::CIELCH, [50.0f32, 20.0, 120.0]))); +/// assert_eq!(str2col("oklab(0.2, 0.6, -0.5)"), Some((Space::OKLAB, [0.2f32, 0.6, -0.5]))); +/// assert_eq!(str2col("srgb 100% 50% 25%"), Some((Space::SRGB, [1.0f32, 0.5, 0.25]))); /// ``` -pub fn str2col(mut s: &str) -> Option<(Space, [f32; 3])> { +pub fn str2col(mut s: &str) -> Option<(Space, [T; N])> +where + Channels: ValidChannels, +{ s = rm_paren(s.trim()); let mut space = Space::SRGB; - let mut result = [f32::NAN; 3]; + let mut result = [f32::NAN; N]; // Return hex if valid if let Ok(irgb) = hex_to_irgb(s) { @@ -941,19 +967,26 @@ pub fn str2col(mut s: &str) -> Option<(Space, [f32; 3])> { .filter(|s| !s.is_empty()) .enumerate() { - if n > 2 { + if n > 3 { return None; + } else if n >= result.len() { + continue; } else if let Ok(value) = split.parse::() { result[n] = value; } else if split.ends_with('%') { - if let Ok(value) = split[0..(split.len() - 1)].parse::() { + if let Ok(percent) = split[0..(split.len() - 1)].parse::() { + // alpha + if n == 3 { + result[n] = percent / 100.0; + continue; + } let (q0, q100) = (space.srgb_quant0()[n], space.srgb_quant100()[n]); if q0.is_finite() && q100.is_finite() { - result[n] = value / 100.0 * (q100 - q0) + q0; + result[n] = percent / 100.0 * (q100 - q0) + q0; } else if Space::UCS_POLAR.contains(&space) { - result[n] = value / 100.0 * 360.0 + result[n] = percent / 100.0 * 360.0 } else if space == Space::HSV { - result[n] = value / 100.0 + result[n] = percent / 100.0 } else { return None; } @@ -964,8 +997,8 @@ pub fn str2col(mut s: &str) -> Option<(Space, [f32; 3])> { return None; } } - if result.iter().all(|v| v.is_finite()) { - Some((space, result)) + if result.iter().take(3).all(|v| v.is_finite()) { + Some((space, result.map(|c| c.to_dt()))) } else { None } @@ -974,7 +1007,10 @@ pub fn str2col(mut s: &str) -> Option<(Space, [f32; 3])> { /// Convert a string into a pixel of the requested Space. /// /// Shorthand for str2col() -> convert_space() -pub fn str2space(s: &str, to: Space) -> Option<[f32; 3]> { +pub fn str2space(s: &str, to: Space) -> Option<[T; N]> +where + Channels: ValidChannels, +{ str2col(s).map(|(from, mut col)| { convert_space(from, to, &mut col); col @@ -985,17 +1021,20 @@ pub fn str2space(s: &str, to: Space) -> Option<[f32; 3]> { // ### FORWARD ### {{{ /// Convert floating (0.0..1.0) RGB to integer (0..255) RGB. -pub fn srgb_to_irgb(pixel: [f32; 3]) -> [u8; 3] { - [ - ((pixel[0] * 255.0).max(0.0).min(255.0) as u8), - ((pixel[1] * 255.0).max(0.0).min(255.0) as u8), - ((pixel[2] * 255.0).max(0.0).min(255.0) as u8), - ] +pub fn srgb_to_irgb(pixel: [f32; N]) -> [u8; N] +where + Channels: ValidChannels, +{ + pixel.map(|c| ((c * 255.0).max(0.0).min(255.0) as u8)) } /// Create a hexadecimal string from integer RGB. -pub fn irgb_to_hex(pixel: [u8; 3]) -> String { - let mut hex = String::from("#"); +pub fn irgb_to_hex(pixel: [u8; N]) -> String +where + Channels: ValidChannels, +{ + let mut hex = String::with_capacity(N * 2 + 1); + hex.push('#'); pixel.into_iter().for_each(|c| { [c / 16, c % 16] @@ -1007,7 +1046,10 @@ pub fn irgb_to_hex(pixel: [u8; 3]) -> String { } /// Convert from sRGB to HSV. -pub fn srgb_to_hsv(pixel: &mut [T; 3]) { +pub fn srgb_to_hsv(pixel: &mut [T; N]) +where + Channels: ValidChannels, +{ let vmin = pixel[0].min(pixel[1]).min(pixel[2]); let vmax = pixel[0].max(pixel[1]).max(pixel[2]); let dmax = vmax - vmin; @@ -1019,7 +1061,9 @@ pub fn srgb_to_hsv(pixel: &mut [T; 3]) { } else { let s = dmax / vmax; - let [dr, dg, db] = pixel.map(|c| (((vmax - c) / 6.0.to_dt()) + (dmax / 2.0.to_dt())) / dmax); + let dr = (((vmax - pixel[0]) / 6.0.to_dt()) + (dmax / 2.0.to_dt())) / dmax; + let dg = (((vmax - pixel[1]) / 6.0.to_dt()) + (dmax / 2.0.to_dt())) / dmax; + let db = (((vmax - pixel[2]) / 6.0.to_dt()) + (dmax / 2.0.to_dt())) / dmax; let h = if pixel[0] == vmax { db - dg @@ -1031,31 +1075,42 @@ pub fn srgb_to_hsv(pixel: &mut [T; 3]) { .rem_euclid(1.0.to_dt()); (h, s) }; - *pixel = [h, s, v]; + pixel[0] = h; + pixel[1] = s; + pixel[2] = v; } /// Convert from sRGB to Linear RGB by applying the sRGB EOTF /// /// -pub fn srgb_to_lrgb(pixel: &mut [T; 3]) { - pixel.iter_mut().for_each(|c| *c = srgb_eotf(*c)); +pub fn srgb_to_lrgb(pixel: &mut [T; N]) +where + Channels: ValidChannels, +{ + pixel.iter_mut().take(3).for_each(|c| *c = srgb_eotf(*c)); } /// Convert from Linear Light RGB to CIE XYZ, D65 standard illuminant /// /// -pub fn lrgb_to_xyz(pixel: &mut [T; 3]) { - *pixel = matmul3(XYZ65_MAT, *pixel) +pub fn lrgb_to_xyz(pixel: &mut [T; N]) +where + Channels: ValidChannels, +{ + [pixel[0], pixel[1], pixel[2]] = matmul3(XYZ65_MAT, [pixel[0], pixel[1], pixel[2]]) } /// Convert from CIE XYZ to CIE LAB. /// /// -pub fn xyz_to_cielab(pixel: &mut [T; 3]) { +pub fn xyz_to_cielab(pixel: &mut [T; N]) +where + Channels: ValidChannels, +{ // Reverse D65 standard illuminant - pixel.iter_mut().zip(D65).for_each(|(c, d)| *c = *c / d.to_dt()); + pixel.iter_mut().take(3).zip(D65).for_each(|(c, d)| *c = *c / d.to_dt()); - pixel.iter_mut().for_each(|c| { + pixel.iter_mut().take(3).for_each(|c| { if *c > T::ff32(LAB_DELTA).powi(3) { *c = c.cbrt() } else { @@ -1063,7 +1118,7 @@ pub fn xyz_to_cielab(pixel: &mut [T; 3]) { } }); - *pixel = [ + [pixel[0], pixel[1], pixel[2]] = [ T::ff32(116.0).fma(pixel[1], T::ff32(-16.0)), T::ff32(500.0) * (pixel[0] - pixel[1]), T::ff32(200.0) * (pixel[1] - pixel[2]), @@ -1073,16 +1128,22 @@ pub fn xyz_to_cielab(pixel: &mut [T; 3]) { /// Convert from CIE XYZ to OKLAB. /// /// -pub fn xyz_to_oklab(pixel: &mut [T; 3]) { - let mut lms = matmul3t(*pixel, OKLAB_M1); +pub fn xyz_to_oklab(pixel: &mut [T; N]) +where + Channels: ValidChannels, +{ + let mut lms = matmul3t([pixel[0], pixel[1], pixel[2]], OKLAB_M1); lms.iter_mut().for_each(|c| *c = c.cbrt()); - *pixel = matmul3t(lms, OKLAB_M2); + [pixel[0], pixel[1], pixel[2]] = matmul3t(lms, OKLAB_M2); } /// Convert CIE XYZ to JzAzBz /// /// -pub fn xyz_to_jzazbz(pixel: &mut [T; 3]) { +pub fn xyz_to_jzazbz(pixel: &mut [T; N]) +where + Channels: ValidChannels, +{ let mut lms = matmul3( JZAZBZ_M1, [ @@ -1096,11 +1157,9 @@ pub fn xyz_to_jzazbz(pixel: &mut [T; 3]) { let lab = matmul3(JZAZBZ_M2, lms); - *pixel = [ - (T::ff32(1.0 + JZAZBZ_D) * lab[0]) / lab[0].fma(JZAZBZ_D.to_dt(), 1.0.to_dt()) - JZAZBZ_D0.to_dt(), - lab[1], - lab[2], - ] + pixel[0] = (T::ff32(1.0 + JZAZBZ_D) * lab[0]) / lab[0].fma(JZAZBZ_D.to_dt(), 1.0.to_dt()) - JZAZBZ_D0.to_dt(); + pixel[1] = lab[1]; + pixel[2] = lab[2]; } // Disabled for now as all the papers are paywalled @@ -1113,7 +1172,10 @@ pub fn xyz_to_jzazbz(pixel: &mut [T; 3]) { /// Convert LRGB to ICtCp. Unvalidated, WIP /// /// -pub fn _lrgb_to_ictcp(pixel: &mut [T; 3]) { +pub fn _lrgb_to_ictcp(pixel: &mut [T; N]) +where + Channels: ValidChannels, +{ // // let alpha = 1.09929682680944; // let beta = 0.018053968510807; @@ -1123,17 +1185,20 @@ pub fn _lrgb_to_ictcp(pixel: &mut [T; 3]) { // }; // pixel.iter_mut().for_each(|c| bt2020(c)); - let mut lms = matmul3(ICTCP_M1, *pixel); + let mut lms = matmul3(ICTCP_M1, [pixel[0], pixel[1], pixel[2]]); // lms prime lms.iter_mut().for_each(|c| *c = pq_oetf(*c)); - *pixel = matmul3(ICTCP_M2, lms); + [pixel[0], pixel[1], pixel[2]] = matmul3(ICTCP_M2, lms); } /// Converts an LAB based space to a cylindrical representation. /// /// -pub fn lab_to_lch(pixel: &mut [T; 3]) { - *pixel = [ +pub fn lab_to_lch(pixel: &mut [T; N]) +where + Channels: ValidChannels, +{ + [pixel[0], pixel[1], pixel[2]] = [ pixel[0], (pixel[1].powi(2) + pixel[2].powi(2)).sqrt(), pixel[2].atan2(pixel[1]).to_degrees().rem_euclid(360.0.to_dt()), @@ -1145,51 +1210,72 @@ pub fn lab_to_lch(pixel: &mut [T; 3]) { // ### BACKWARD ### {{{ /// Convert integer (0..255) RGB to floating (0.0..1.0) RGB. -pub fn irgb_to_srgb(pixel: [u8; 3]) -> [f32; 3] { - [ - pixel[0] as f32 / 255.0, - pixel[1] as f32 / 255.0, - pixel[2] as f32 / 255.0, - ] +pub fn irgb_to_srgb(pixel: [u8; N]) -> [T; N] +where + Channels: ValidChannels, +{ + pixel.map(|c| T::ff32(c as f32 / 255.0)) } /// Create integer RGB set from hex string. -pub fn hex_to_irgb(hex: &str) -> Result<[u8; 3], String> { - let hex = hex.trim().to_ascii_uppercase(); - - let mut chars = hex.chars(); +/// `DEFAULT` is only used when 4 channels are requested but 3 is given. +pub fn hex_to_irgb_default(hex: &str) -> Result<[u8; N], String> +where + Channels: ValidChannels, +{ + let mut chars = hex.trim().chars(); if chars.as_str().starts_with('#') { chars.next(); } - let ids: Vec = if chars.as_str().len() == 6 { - chars + let ids: Vec = match chars.as_str().len() { + 6 | 8 => chars .map(|c| { let u = c as u32; + // numeric if 57 >= u && u >= 48 { Ok(u - 48) + // uppercase } else if 70 >= u && u >= 65 { Ok(u - 55) + // lowercase + } else if 102 >= u && u >= 97 { + Ok(u - 87) } else { - Err(String::from("Hex character ") + &String::from(c) + " out of bounds") + Err(String::from("Hex character '") + &String::from(c) + "' out of bounds") } }) - .collect() - } else { - Err(String::from("Incorrect hex length!")) + .collect(), + n => Err(String::from("Incorrect hex length ") + &n.to_string()), }?; - Ok([ - ((ids[0]) * 16 + ids[1]) as u8, - ((ids[2]) * 16 + ids[3]) as u8, - ((ids[4]) * 16 + ids[5]) as u8, - ]) + let mut result = [DEFAULT; N]; + + ids.chunks(2) + .take(result.len()) + .enumerate() + .for_each(|(n, chunk)| result[n] = ((chunk[0]) * 16 + chunk[1]) as u8); + + Ok(result) +} + +/// Create integer RGB set from hex string. +/// Will default to 255 for alpha if 4 channels requested but hex length is 6. +/// Use `hex_to_irgb_default` to customize this. +pub fn hex_to_irgb(hex: &str) -> Result<[u8; N], String> +where + Channels: ValidChannels, +{ + hex_to_irgb_default::(hex) } /// Convert from HSV to sRGB. -pub fn hsv_to_srgb(pixel: &mut [T; 3]) { +pub fn hsv_to_srgb(pixel: &mut [T; N]) +where + Channels: ValidChannels, +{ if pixel[1] == 0.0.to_dt() { - *pixel = [pixel[2]; 3]; + [pixel[0], pixel[1]] = [pixel[2]; 2]; } else { let mut var_h = pixel[0] * 6.0.to_dt(); if var_h == 6.0.to_dt() { @@ -1200,7 +1286,7 @@ pub fn hsv_to_srgb(pixel: &mut [T; 3]) { let var_2 = pixel[2] * (-var_h + var_i).fma(pixel[1], 1.0.to_dt()); let var_3 = pixel[2] * (T::ff32(-1.0) + (var_h - var_i)).fma(pixel[1], T::ff32(1.0)); - *pixel = if var_i == 0.0.to_dt() { + [pixel[0], pixel[1], pixel[2]] = if var_i == 0.0.to_dt() { [pixel[2], var_3, var_1] } else if var_i == 1.0.to_dt() { [var_2, pixel[2], var_1] @@ -1219,28 +1305,37 @@ pub fn hsv_to_srgb(pixel: &mut [T; 3]) { /// Convert from Linear RGB to sRGB by applying the inverse sRGB EOTF /// /// -pub fn lrgb_to_srgb(pixel: &mut [T; 3]) { - pixel.iter_mut().for_each(|c| *c = srgb_oetf(*c)); +pub fn lrgb_to_srgb(pixel: &mut [T; N]) +where + Channels: ValidChannels, +{ + pixel.iter_mut().take(3).for_each(|c| *c = srgb_oetf(*c)); } /// Convert from CIE XYZ to Linear Light RGB. /// /// -pub fn xyz_to_lrgb(pixel: &mut [T; 3]) { - *pixel = matmul3(XYZ65_MAT_INV, *pixel) +pub fn xyz_to_lrgb(pixel: &mut [T; N]) +where + Channels: ValidChannels, +{ + [pixel[0], pixel[1], pixel[2]] = matmul3(XYZ65_MAT_INV, [pixel[0], pixel[1], pixel[2]]) } /// Convert from CIE LAB to CIE XYZ. /// /// -pub fn cielab_to_xyz(pixel: &mut [T; 3]) { - *pixel = [ +pub fn cielab_to_xyz(pixel: &mut [T; N]) +where + Channels: ValidChannels, +{ + [pixel[0], pixel[1], pixel[2]] = [ (pixel[0] + 16.0.to_dt()) / 116.0.to_dt() + pixel[1] / 500.0.to_dt(), (pixel[0] + 16.0.to_dt()) / 116.0.to_dt(), (pixel[0] + 16.0.to_dt()) / 116.0.to_dt() - pixel[2] / 200.0.to_dt(), ]; - pixel.iter_mut().for_each(|c| { + pixel.iter_mut().take(3).for_each(|c| { if *c > LAB_DELTA.to_dt() { *c = c.powi(3) } else { @@ -1248,22 +1343,28 @@ pub fn cielab_to_xyz(pixel: &mut [T; 3]) { } }); - pixel.iter_mut().zip(D65).for_each(|(c, d)| *c = *c * d.to_dt()); + pixel.iter_mut().take(3).zip(D65).for_each(|(c, d)| *c = *c * d.to_dt()); } /// Convert from OKLAB to CIE XYZ. /// /// -pub fn oklab_to_xyz(pixel: &mut [T; 3]) { - let mut lms = matmul3t(*pixel, OKLAB_M2_INV); +pub fn oklab_to_xyz(pixel: &mut [T; N]) +where + Channels: ValidChannels, +{ + let mut lms = matmul3t([pixel[0], pixel[1], pixel[2]], OKLAB_M2_INV); lms.iter_mut().for_each(|c| *c = c.powi(3)); - *pixel = matmul3t(lms, OKLAB_M1_INV); + [pixel[0], pixel[1], pixel[2]] = matmul3t(lms, OKLAB_M1_INV); } /// Convert JzAzBz to CIE XYZ /// /// -pub fn jzazbz_to_xyz(pixel: &mut [T; 3]) { +pub fn jzazbz_to_xyz(pixel: &mut [T; N]) +where + Channels: ValidChannels, +{ let mut lms = matmul3( JZAZBZ_M2_INV, [ @@ -1276,7 +1377,7 @@ pub fn jzazbz_to_xyz(pixel: &mut [T; 3]) { lms.iter_mut().for_each(|c| *c = pqz_eotf(*c)); - *pixel = matmul3(JZAZBZ_M1_INV, lms); + [pixel[0], pixel[1], pixel[2]] = matmul3(JZAZBZ_M1_INV, lms); pixel[0] = pixel[2].fma((JZAZBZ_B - 1.0).to_dt(), pixel[0]) / JZAZBZ_B.to_dt(); pixel[1] = pixel[0].fma((JZAZBZ_G - 1.0).to_dt(), pixel[1]) / JZAZBZ_G.to_dt(); @@ -1293,19 +1394,25 @@ pub fn jzazbz_to_xyz(pixel: &mut [T; 3]) { /// /// // #[no_mangle] -pub fn _ictcp_to_lrgb(pixel: &mut [T; 3]) { +pub fn _ictcp_to_lrgb(pixel: &mut [T; N]) +where + Channels: ValidChannels, +{ // lms prime - let mut lms = matmul3(ICTCP_M2_INV, *pixel); + let mut lms = matmul3(ICTCP_M2_INV, [pixel[0], pixel[1], pixel[2]]); // non-prime lms lms.iter_mut().for_each(|c| *c = pq_eotf(*c)); - *pixel = matmul3(ICTCP_M1_INV, lms); + [pixel[0], pixel[1], pixel[2]] = matmul3(ICTCP_M1_INV, lms); } /// Retrieves an LAB based space from its cylindrical representation. /// /// -pub fn lch_to_lab(pixel: &mut [T; 3]) { - *pixel = [ +pub fn lch_to_lab(pixel: &mut [T; N]) +where + Channels: ValidChannels, +{ + [pixel[0], pixel[1], pixel[2]] = [ pixel[0], pixel[1] * pixel[2].to_radians().cos(), pixel[1] * pixel[2].to_radians().sin(), @@ -1330,26 +1437,42 @@ macro_rules! cdef1 { } macro_rules! cdef3 { - ($base:ident, $f32:ident, $f64:ident) => { + ($base:ident, $f32_3:ident, $f64_3:ident, $f32_4:ident, $f64_4:ident) => { + #[no_mangle] + extern "C" fn $f32_3(pixel: &mut [f32; 3]) { + $base(pixel) + } + #[no_mangle] + extern "C" fn $f64_3(pixel: &mut [f64; 3]) { + $base(pixel) + } #[no_mangle] - extern "C" fn $f32(pixel: &mut [f32; 3]) { + extern "C" fn $f32_4(pixel: &mut [f32; 4]) { $base(pixel) } #[no_mangle] - extern "C" fn $f64(pixel: &mut [f64; 3]) { + extern "C" fn $f64_4(pixel: &mut [f64; 4]) { $base(pixel) } }; } macro_rules! cdef31 { - ($base:ident, $f32:ident, $f64:ident) => { + ($base:ident, $f32_3:ident, $f64_3:ident, $f32_4:ident, $f64_4:ident) => { + #[no_mangle] + extern "C" fn $f32_3(pixel: &[f32; 3]) -> f32 { + $base(pixel) + } + #[no_mangle] + extern "C" fn $f64_3(pixel: &[f64; 3]) -> f64 { + $base(pixel) + } #[no_mangle] - extern "C" fn $f32(pixel: &[f32; 3]) -> f32 { + extern "C" fn $f32_4(pixel: &[f32; 4]) -> f32 { $base(pixel) } #[no_mangle] - extern "C" fn $f64(pixel: &[f64; 3]) -> f64 { + extern "C" fn $f64_4(pixel: &[f64; 4]) -> f64 { $base(pixel) } }; @@ -1364,25 +1487,135 @@ cdef1!(pq_oetf, pq_oetf_f32, pq_oetf_f64); cdef1!(pqz_oetf, pqz_oetf_f32, pqz_oetf_f64); // Helmholtz-Kohlrausch -cdef31!(hk_high2023, hk_high2023_f32, hk_high2023_f64); -cdef3!(hk_high2023_comp, hk_high2023_comp_f32, hk_high2023_comp_f64); +cdef31!( + hk_high2023, + hk_high2023_3f32, + hk_high2023_3f64, + hk_high2023_4f32, + hk_high2023_4f64 +); +cdef3!( + hk_high2023_comp, + hk_high2023_comp_3f32, + hk_high2023_comp_3f64, + hk_high2023_comp_4f32, + hk_high2023_comp_4f64 +); // Forward -cdef3!(srgb_to_hsv, srgb_to_hsv_f32, srgb_to_hsv_f64); -cdef3!(srgb_to_lrgb, srgb_to_lrgb_f32, srgb_to_lrgb_f64); -cdef3!(lrgb_to_xyz, lrgb_to_xyz_f32, lrgb_to_xyz_f64); -cdef3!(xyz_to_cielab, xyz_to_cielab_f32, xyz_to_cielab_f64); -cdef3!(xyz_to_oklab, xyz_to_oklab_f32, xyz_to_oklab_f64); -cdef3!(xyz_to_jzazbz, xyz_to_jzazbz_f32, xyz_to_jzazbz_f64); -cdef3!(lab_to_lch, lab_to_lch_f32, lab_to_lch_f64); +cdef3!( + srgb_to_hsv, + srgb_to_hsv_3f32, + srgb_to_hsv_3f64, + srgb_to_hsv_4f32, + srgb_to_hsv_4f64 +); +cdef3!( + srgb_to_lrgb, + srgb_to_lrgb_3f32, + srgb_to_lrgb_3f64, + srgb_to_lrgb_4f32, + srgb_to_lrgb_4f64 +); +cdef3!( + lrgb_to_xyz, + lrgb_to_xyz_3f32, + lrgb_to_xyz_3f64, + lrgb_to_xyz_4f32, + lrgb_to_xyz_4f64 +); +cdef3!( + xyz_to_cielab, + xyz_to_cielab_3f32, + xyz_to_cielab_3f64, + xyz_to_cielab_4f32, + xyz_to_cielab_4f64 +); +cdef3!( + xyz_to_oklab, + xyz_to_oklab_3f32, + xyz_to_oklab_3f64, + xyz_to_oklab_4f32, + xyz_to_oklab_4f64 +); +cdef3!( + xyz_to_jzazbz, + xyz_to_jzazbz_3f32, + xyz_to_jzazbz_3f64, + xyz_to_jzazbz_4f32, + xyz_to_jzazbz_4f64 +); +cdef3!( + lab_to_lch, + lab_to_lch_3f32, + lab_to_lch_3f64, + lab_to_lch_4f32, + lab_to_lch_4f64 +); +cdef3!( + _lrgb_to_ictcp, + _lrgb_to_ictcp_3f32, + _lrgb_to_ictcp_3f64, + _lrgb_to_ictcp_4f32, + _lrgb_to_ictcp_4f64 +); // Backward -cdef3!(hsv_to_srgb, hsv_to_srgb_f32, hsv_to_srgb_f64); -cdef3!(lrgb_to_srgb, lrgb_to_srgb_f32, lrgb_to_srgb_f64); -cdef3!(xyz_to_lrgb, xyz_to_lrgb_f32, xyz_to_lrgb_f64); -cdef3!(cielab_to_xyz, cielab_to_xyz_f32, cielab_to_xyz_f64); -cdef3!(oklab_to_xyz, oklab_to_xyz_f32, oklab_to_xyz_f64); -cdef3!(jzazbz_to_xyz, jzazbz_to_xyz_f32, jzazbz_to_xyz_f64); -cdef3!(lch_to_lab, lch_to_lab_f32, lch_to_lab_f64); +cdef3!( + hsv_to_srgb, + hsv_to_srgb_3f32, + hsv_to_srgb_3f64, + hsv_to_srgb_4f32, + hsv_to_srgb_4f64 +); +cdef3!( + lrgb_to_srgb, + lrgb_to_srgb_3f32, + lrgb_to_srgb_3f64, + lrgb_to_srgb_4f32, + lrgb_to_srgb_4f64 +); +cdef3!( + xyz_to_lrgb, + xyz_to_lrgb_3f32, + xyz_to_lrgb_3f64, + xyz_to_lrgb_4f32, + xyz_to_lrgb_4f64 +); +cdef3!( + cielab_to_xyz, + cielab_to_xyz_3f32, + cielab_to_xyz_3f64, + cielab_to_xyz_4f32, + cielab_to_xyz_4f64 +); +cdef3!( + oklab_to_xyz, + oklab_to_xyz_3f32, + oklab_to_xyz_3f64, + oklab_to_xyz_4f32, + oklab_to_xyz_4f64 +); +cdef3!( + jzazbz_to_xyz, + jzazbz_to_xyz_3f32, + jzazbz_to_xyz_3f64, + jzazbz_to_xyz_4f32, + jzazbz_to_xyz_4f64 +); +cdef3!( + lch_to_lab, + lch_to_lab_3f32, + lch_to_lab_3f64, + lch_to_lab_4f32, + lch_to_lab_4f64 +); +cdef3!( + _ictcp_to_lrgb, + _ictcp_to_lrgb_3f32, + _ictcp_to_lrgb_3f64, + _ictcp_to_lrgb_4f32, + _ictcp_to_lrgb_4f64 +); // }}} diff --git a/src/tests.rs b/src/tests.rs index fc00616..57dabf5 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -3,6 +3,9 @@ use super::*; const HEX: &str = "#3359F2"; const IRGB: [u8; 3] = [51, 89, 242]; +const HEXA: &str = "#3359F259"; +const IRGBA: [u8; 4] = [51, 89, 242, 89]; + // ### COLOUR-REFS ### {{{ const SRGB: &'static [[f64; 3]] = &[ @@ -213,21 +216,73 @@ fn irgb_to() { assert_eq!(IRGB, srgb_to_irgb([0.2, 0.35, 0.95])) } +#[test] +fn irgb_to_alpha() { + assert_eq!(IRGBA, srgb_to_irgb([0.2, 0.35, 0.95, 0.35])) +} + #[test] fn irgb_from() { - let mut srgb = irgb_to_srgb(IRGB); + let mut srgb = irgb_to_srgb::(IRGB); + // Round decimal to hundredths srgb.iter_mut().for_each(|c| *c = (*c * 100.0).round() / 100.0); assert_eq!([0.2, 0.35, 0.95], srgb) } +#[test] +fn irgb_from_alpha() { + let mut srgb = irgb_to_srgb::(IRGBA); + // Round decimal to hundredths + srgb.iter_mut().for_each(|c| *c = (*c * 100.0).round() / 100.0); + assert_eq!([0.2, 0.35, 0.95, 0.35], srgb) +} + #[test] fn hex_to() { assert_eq!(HEX, irgb_to_hex(IRGB)) } +#[test] +fn hex_to_alpha() { + assert_eq!(HEXA, irgb_to_hex(IRGBA)) +} + #[test] fn hex_from() { - assert_eq!(IRGB, hex_to_irgb(HEX).unwrap()) + assert_eq!(IRGB, hex_to_irgb(HEX).unwrap()); + assert_eq!(IRGB, hex_to_irgb(HEXA).unwrap()); +} + +#[test] +fn hex_from_alpha() { + assert_eq!( + [IRGB[0], IRGB[1], IRGB[2], 123], + hex_to_irgb_default::<4, 123>(HEX).unwrap() + ); + assert_eq!(IRGBA, hex_to_irgb(HEXA).unwrap()); +} + +#[test] +fn hex_validations() { + for hex in [ + "#ABCDEF", + "#abcdef", + "#ABCDEF01", + "#abcdef01", + "#ABCDEF", + "ABCDEF", + " ABCDEF ", + " #ABCDEF ", + ] { + assert!(hex_to_irgb::<3>(hex).is_ok(), "NOT VALID 3: '{}'", hex); + assert!(hex_to_irgb::<4>(hex).is_ok(), "NOT VALID 4: '{}'", hex); + } + for hex in [ + "", "#", "#5F", "#ABCDEG", "#abcdeg", "#ABCDEFF", "#abcdeg", "##ABCDEF", "ABCDEF#", + ] { + assert!(hex_to_irgb::<3>(hex).is_err(), "NOT INVALID 3: '{}'", hex); + assert!(hex_to_irgb::<4>(hex).is_err(), "NOT INVALID 4: '{}'", hex); + } } #[test] @@ -347,13 +402,56 @@ fn tree_jump() { conv_cmp(Space::CIELCH, LCH, Space::HSV, HSV); } +#[test] +fn alpha_untouch() { + let mut pixel = [1.0, 2.0, 3.0, 4.0f64]; + for f in [ + srgb_to_hsv, + hsv_to_srgb, + srgb_to_lrgb, + lrgb_to_xyz, + xyz_to_cielab, + xyz_to_oklab, + xyz_to_jzazbz, + lab_to_lch, + _lrgb_to_ictcp, + _ictcp_to_lrgb, + lrgb_to_srgb, + xyz_to_lrgb, + cielab_to_xyz, + oklab_to_xyz, + jzazbz_to_xyz, + lch_to_lab, + ] { + f(&mut pixel); + assert_eq!(pixel[3].to_bits(), 4.0_f64.to_bits(), "{:?}", f); + } + convert_space(Space::SRGB, Space::CIELCH, &mut pixel); + assert_eq!(pixel[3].to_bits(), 4.0_f64.to_bits()); + let mut chunks = [pixel, pixel, pixel]; + convert_space_chunked(Space::CIELCH, Space::SRGB, &mut chunks); + chunks + .iter() + .for_each(|c| assert_eq!(c[3].to_bits(), 4.0_f64.to_bits(), "alpha_untouch_chunked")); + let mut slice = [pixel, pixel, pixel].iter().fold(Vec::::new(), |mut acc, it| { + acc.extend_from_slice(it.as_slice()); + acc + }); + convert_space_sliced::<_, 4>(Space::CIELCH, Space::SRGB, &mut slice); + slice + .iter() + .skip(3) + .step_by(4) + .for_each(|n| assert_eq!(n.to_bits(), 4.0_f64.to_bits(), "alpha_untouch_sliced")); +} + #[test] fn sliced() { let mut pixel: Vec = SRGB.iter().fold(Vec::new(), |mut acc, it| { acc.extend_from_slice(it); acc }); - convert_space_sliced(Space::SRGB, Space::CIELCH, &mut pixel); + convert_space_sliced::<_, 3>(Space::SRGB, Space::CIELCH, &mut pixel); pix_cmp( &pixel .chunks_exact(3) @@ -372,7 +470,7 @@ fn sliced_odd() { acc }); pixel.push(1234.5678); - convert_space_sliced(Space::SRGB, Space::CIELCH, &mut pixel); + convert_space_sliced::<_, 3>(Space::SRGB, Space::CIELCH, &mut pixel); pix_cmp( &pixel .chunks_exact(3) @@ -389,7 +487,7 @@ fn sliced_odd() { fn sliced_smol() { let pixels = [1.0, 0.0]; let mut smol = pixels.clone(); - convert_space_sliced(Space::SRGB, Space::CIELCH, &mut smol); + convert_space_sliced::<_, 3>(Space::SRGB, Space::CIELCH, &mut smol); assert_eq!(pixels, smol); } @@ -485,100 +583,112 @@ fn space_strings() { // ### Str2Col ### {{{ #[test] fn str2col_base() { - assert_eq!(str2col("0.2, 0.5, 0.6"), Some((Space::SRGB, [0.2, 0.5, 0.6]))) + assert_eq!(str2col("0.2, 0.5, 0.6"), Some((Space::SRGB, [0.2f32, 0.5, 0.6]))) } #[test] fn str2col_base_tight() { - assert_eq!(str2col("0.2,0.5,0.6"), Some((Space::SRGB, [0.2, 0.5, 0.6]))) + assert_eq!(str2col("0.2,0.5,0.6"), Some((Space::SRGB, [0.2f32, 0.5, 0.6]))) } #[test] fn str2col_base_lop() { - assert_eq!(str2col("0.2,0.5, 0.6"), Some((Space::SRGB, [0.2, 0.5, 0.6]))) + assert_eq!(str2col("0.2,0.5, 0.6"), Some((Space::SRGB, [0.2f32, 0.5, 0.6]))) } #[test] fn str2col_base_bare() { - assert_eq!(str2col("0.2 0.5 0.6"), Some((Space::SRGB, [0.2, 0.5, 0.6]))) + assert_eq!(str2col("0.2 0.5 0.6"), Some((Space::SRGB, [0.2f32, 0.5, 0.6]))) } #[test] fn str2col_base_bare_fat() { - assert_eq!(str2col(" 0.2 0.5 0.6 "), Some((Space::SRGB, [0.2, 0.5, 0.6]))) + assert_eq!(str2col(" 0.2 0.5 0.6 "), Some((Space::SRGB, [0.2f32, 0.5, 0.6]))) } #[test] fn str2col_base_paren() { - assert_eq!(str2col("(0.2 0.5 0.6)"), Some((Space::SRGB, [0.2, 0.5, 0.6]))) + assert_eq!(str2col("(0.2 0.5 0.6)"), Some((Space::SRGB, [0.2f32, 0.5, 0.6]))) } #[test] fn str2col_base_paren2() { - assert_eq!(str2col("{ 0.2 : 0.5 : 0.6 }"), Some((Space::SRGB, [0.2, 0.5, 0.6]))) + assert_eq!(str2col("{ 0.2 : 0.5 : 0.6 }"), Some((Space::SRGB, [0.2f32, 0.5, 0.6]))) } #[test] fn str2col_base_none() { - assert_eq!(str2col(" 0.2 0.5 f"), None) + assert_eq!(str2col::(" 0.2 0.5 f"), None) } #[test] fn str2col_base_none2() { - assert_eq!(str2col("0.2*0.5 0.6"), None) + assert_eq!(str2col::("0.2*0.5 0.6"), None) } #[test] fn str2col_base_paren_none() { - assert_eq!(str2col("(0.2 0.5 0.6"), None) + assert_eq!(str2col::("(0.2 0.5 0.6"), None) } #[test] fn str2col_base_paren_none2() { - assert_eq!(str2col("0.2 0.5 0.6}"), None) + assert_eq!(str2col::("0.2 0.5 0.6}"), None) } #[test] fn str2col_lch() { - assert_eq!(str2col("lch(50, 30, 160)"), Some((Space::CIELCH, [50.0, 30.0, 160.0]))) + assert_eq!( + str2col("lch(50, 30, 160)"), + Some((Space::CIELCH, [50.0f32, 30.0, 160.0])) + ) } #[test] fn str2col_lch_space() { - assert_eq!(str2col("lch 50, 30, 160"), Some((Space::CIELCH, [50.0, 30.0, 160.0]))) + assert_eq!( + str2col("lch 50, 30, 160"), + Some((Space::CIELCH, [50.0f32, 30.0, 160.0])) + ) } #[test] fn str2col_lch_colon() { - assert_eq!(str2col("lch:50:30:160"), Some((Space::CIELCH, [50.0, 30.0, 160.0]))) + assert_eq!(str2col("lch:50:30:160"), Some((Space::CIELCH, [50.0f32, 30.0, 160.0]))) } #[test] fn str2col_lch_semicolon() { - assert_eq!(str2col("lch;50;30;160"), Some((Space::CIELCH, [50.0, 30.0, 160.0]))) + assert_eq!(str2col("lch;50;30;160"), Some((Space::CIELCH, [50.0f32, 30.0, 160.0]))) } #[test] fn str2col_lch_mixed() { - assert_eq!(str2col("lch; (50,30,160)"), Some((Space::CIELCH, [50.0, 30.0, 160.0]))) + assert_eq!( + str2col("lch; (50,30,160)"), + Some((Space::CIELCH, [50.0f32, 30.0, 160.0])) + ) } #[test] fn str2col_lch_mixed2() { - assert_eq!(str2col("lch(50; 30; 160)"), Some((Space::CIELCH, [50.0, 30.0, 160.0]))) + assert_eq!( + str2col("lch(50; 30; 160)"), + Some((Space::CIELCH, [50.0f32, 30.0, 160.0])) + ) } #[test] fn str2col_lch_mixed3() { assert_eq!( str2col("lch (50 30 160)"), - Some((Space::CIELCH, [50.0, 30.0, 160.0])) + Some((Space::CIELCH, [50.0f32, 30.0, 160.0])) ) } #[test] fn str2col_hex() { - assert_eq!(str2col(HEX), Some((Space::SRGB, irgb_to_srgb(IRGB)))) + assert_eq!(str2col(HEX), Some((Space::SRGB, irgb_to_srgb::(IRGB)))) } #[test] @@ -587,7 +697,11 @@ fn str2col_perc100() { str2col("oklch 100% 100% 100%"), Some(( Space::OKLCH, - [Space::OKLCH.srgb_quant100()[0], Space::OKLCH.srgb_quant100()[1], 360.0] + [ + Space::OKLCH.srgb_quant100()[0], + Space::OKLCH.srgb_quant100()[1], + 360.0f32 + ] )) ) } @@ -601,7 +715,7 @@ fn str2col_perc50() { [ (Space::OKLCH.srgb_quant0()[0] + Space::OKLCH.srgb_quant100()[0]) / 2.0, (Space::OKLCH.srgb_quant0()[1] + Space::OKLCH.srgb_quant100()[1]) / 2.0, - 180.0, + 180.0f32, ] )) ) @@ -613,7 +727,7 @@ fn str2col_perc0() { str2col("oklch 0% 0% 0%"), Some(( Space::OKLCH, - [Space::OKLCH.srgb_quant0()[0], Space::OKLCH.srgb_quant0()[1], 0.0] + [Space::OKLCH.srgb_quant0()[0], Space::OKLCH.srgb_quant0()[1], 0.0f32] )) ) } @@ -624,40 +738,54 @@ fn str2col_perc_mix() { str2col("oklab 0.5 100.000% 0%"), Some(( Space::OKLAB, - [0.5, Space::OKLAB.srgb_quant100()[1], Space::OKLAB.srgb_quant0()[2]] + [0.5f32, Space::OKLAB.srgb_quant100()[1], Space::OKLAB.srgb_quant0()[2]] )) ) } #[test] fn str2col_perc_inval() { - assert_eq!(str2col("oklab 0.5 100 % 0%"), None) + assert_eq!(str2col::("oklab 0.5 100 % 0%"), None); + assert_eq!(str2col::("oklab 0.5% %100% 0%"), None); + assert_eq!(str2col::("oklab 0.5 100%% 0%"), None); } #[test] -fn str2col_perc_inval2() { - assert_eq!(str2col("oklab 0.5% %100% 0%"), None) -} - -#[test] -fn str2col_perc_inval3() { - assert_eq!(str2col("oklab 0.5 100%% 0%"), None) +fn str2col_alpha() { + assert_eq!( + str2col("srgb 0, 0.5, 0.75, 1.0"), + Some((Space::SRGB, [0f32, 0.5, 0.75, 1.0])) + ); + assert_eq!( + str2col("srgb 0, 0.5, 0.75, 1.0"), + Some((Space::SRGB, [0f32, 0.5, 0.75])) + ); + assert_eq!( + str2col("srgb 10%, 20%, 50%, 80%"), + Some((Space::SRGB, [0.1f32, 0.2, 0.5, 0.8])) + ); + assert_eq!( + str2col("srgb 10%, 20%, 50%, 80%"), + Some((Space::SRGB, [0.1f32, 0.2, 0.5])) + ); + let mut will_nan = str2col::("srgb 0, 0.5, 0.75").unwrap(); + if will_nan.1[3].is_nan() { + will_nan.1[3] = 0.12345 + } + assert_eq!(will_nan, (Space::SRGB, [0f32, 0.5, 0.75, 0.12345])); } #[test] fn str2space_base() { - let pix: [f64; 3] = str2space("oklch : 0.62792590, 0.25768453, 29.22319405", Space::SRGB) - .expect("STR2SPACE_BASE FAIL") - .map(|v| v.into()); + let pix: [f64; 3] = + str2space("oklch : 0.62792590, 0.25768453, 29.22319405", Space::SRGB).expect("STR2SPACE_BASE FAIL"); let reference = [1.00000000, 0.00000000, 0.00000000]; pix_cmp(&[pix], &[reference], 1e-3, &[]); } #[test] fn str2space_hex() { - let pix: [f64; 3] = str2space(" { #FF0000 } ", Space::OKLCH) - .expect("STR2SPACE_HEX FAIL") - .map(|v| v.into()); + let pix: [f64; 3] = str2space(" { #FF0000 } ", Space::OKLCH).expect("STR2SPACE_HEX FAIL"); let reference = [0.62792590, 0.25768453, 29.22319405]; pix_cmp(&[pix], &[reference], 1e-3, &[]); }