Skip to content

Commit

Permalink
feat: support YIQ color space
Browse files Browse the repository at this point in the history
  • Loading branch information
JiatLn committed Aug 29, 2023
1 parent 9938047 commit 1fdfa7b
Show file tree
Hide file tree
Showing 10 changed files with 153 additions and 25 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ You can use the `from_str` method to construct a color from a string.
<li><code>hwb</code></li>
<li><code>cmyk</code></li>
<li><code>xyz</code></li>
<li><code>yiq</code></li>
<li><code>yuv</code></li>
<li><code>YCbCr</code></li>
<li><code>lab</code></li>
Expand All @@ -81,6 +82,7 @@ let color = Color::from_str("hsi(60, 100%, 66.67%)").unwrap();
let color = Color::from_str("hwb(60, 0%, 0%)").unwrap();
let color = Color::from_str("cmyk(0%, 0%, 100%, 0%)").unwrap();
let color = Color::from_str("xyz(0.769975, 0.927808, 0.138526)").unwrap();
let color = Color::from_str("yiq(0.886, 0.32126, -0.31114)").unwrap();
let color = Color::from_str("yuv(0.886, -0.4359, 0.1)").unwrap();
let color = Color::from_str("YCbCr(225.93, 0.5755, 148.7269)").unwrap();
let color = Color::from_str("lab(97.14, -21.55, 94.48)").unwrap();
Expand Down
21 changes: 18 additions & 3 deletions src/color/from_str.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{ conversion, data::hex_of_name, parser, Color, ColorSpace };
use crate::{conversion, data::hex_of_name, parser, Color, ColorSpace};
use anyhow::Result;
use std::str::FromStr;

Expand Down Expand Up @@ -51,7 +51,6 @@ impl FromStr for Color {
} else {
let mut parser = parser::Parser::new();
parser.tokenize(&input).validate()?;
dbg!(parser.tokens);
(parser.color_space, parser.values)
};

Expand All @@ -60,7 +59,11 @@ impl FromStr for Color {
let r = color_vec[0];
let g = color_vec[1];
let b = color_vec[2];
let alpha = if color_vec.len() > 3 { color_vec[3] } else { 1.0 };
let alpha = if color_vec.len() > 3 {
color_vec[3]
} else {
1.0
};

Ok(Color::new(r, g, b, alpha))
}
Expand All @@ -74,6 +77,7 @@ fn convert_color_vec_by_color_space(color_vec: &[f64], color_space: &ColorSpace)
ColorSpace::HSV => conversion::hsv::hsv2rgb(color_vec),
ColorSpace::CMYK => conversion::cmyk::cmyk2rgb(color_vec),
ColorSpace::XYZ => conversion::xyz::xyz2rgb(color_vec),
ColorSpace::YIQ => conversion::yiq::yiq2rgb(color_vec),
ColorSpace::YUV => conversion::yuv::yuv2rgb(color_vec),
ColorSpace::YCbCr => conversion::ycbcr::ycbcr2rgb(color_vec),
ColorSpace::Lab => conversion::lab::lab2rgb(color_vec),
Expand Down Expand Up @@ -305,4 +309,15 @@ mod tests {
let color = Color::from_str("YCbCr(225.93, 0.5755, 148.7269)").unwrap();
assert_eq!(color.rgb(), "rgb(255, 255, 0)");
}

#[test]
fn test_color_from_yiq_str() {
let color = Color::from_str("yiq(0.42337, -0.07301, 0.17583)").unwrap();
assert_eq!(color.rgb(), "rgb(118, 84, 205)");
assert_eq!(color.hex(), "#7654cd");

let color = Color::from_str("yiq(0.886, 0.32126, -0.31114)").unwrap();
assert_eq!(color.rgb(), "rgb(255, 255, 0)");
assert_eq!(color.hex(), "#ff0");
}
}
28 changes: 24 additions & 4 deletions src/color/stringify.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
use crate::{
conversion::{
cmyk::rgb2cmyk,
hex::{ rgb2hex, rgba2hex },
hex::{rgb2hex, rgba2hex},
hsi::rgb2hsi,
hsl::rgb2hsl,
hsv::rgb2hsv,
hwb::rgb2hwb,
lab::rgb2lab,
xyz::rgb2xyz,
ycbcr::rgb2ycbcr,
yiq::rgb2yiq,
yuv::rgb2yuv,
},
data::name_of_hex,
utils::{ hex::simplify_hex, round },
utils::{hex::simplify_hex, round},
Color,
};

Expand Down Expand Up @@ -106,7 +107,6 @@ impl Color {
let h = round(hsl[0], 0);
let s = round(hsl[1] * 100.0, 0);
let l = round(hsl[2] * 100.0, 0);
dbg!(self.alpha());
format!("hsla({}, {}%, {}%, {})", h, s, l, self.alpha())
}
/// `hsv` string of the color
Expand Down Expand Up @@ -175,7 +175,10 @@ impl Color {
.iter()
.map(|&v| round(v * 100.0, 0))
.collect::<Vec<_>>();
format!("cmyk({}%, {}%, {}%, {}%)", cmyk[0], cmyk[1], cmyk[2], cmyk[3])
format!(
"cmyk({}%, {}%, {}%, {}%)",
cmyk[0], cmyk[1], cmyk[2], cmyk[3]
)
}
/// `xyz` string of the color
///
Expand All @@ -194,6 +197,23 @@ impl Color {
.collect::<Vec<_>>();
format!("xyz({}, {}, {})", xyz[0], xyz[1], xyz[2])
}
/// `yiq` string of the color
///
/// # Examples
///
/// ```rust
/// use color_art::Color;
///
/// let color = Color::new(255.0, 0.0, 0.0, 1.0);
/// assert_eq!(color.yiq(), "yiq(0.299, 0.59572, 0.21146)");
/// ```
pub fn yiq(self) -> String {
let yiq = rgb2yiq(&self.rgb)
.iter()
.map(|&v| round(v, 5))
.collect::<Vec<_>>();
format!("yiq({}, {}, {})", yiq[0], yiq[1], yiq[2])
}
/// `yuv` string of the color
///
/// # Examples
Expand Down
20 changes: 16 additions & 4 deletions src/color/vec_of.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{ conversion, Color, ColorSpace };
use crate::{conversion, Color, ColorSpace};

impl Color {
/// Get the color space vector of the color instance.
Expand Down Expand Up @@ -36,6 +36,7 @@ impl Color {
ColorSpace::HWB => conversion::hwb::rgb2hwb(&color),
ColorSpace::CMYK => conversion::cmyk::rgb2cmyk(&color),
ColorSpace::XYZ => conversion::xyz::rgb2xyz(&color),
ColorSpace::YIQ => conversion::yiq::rgb2yiq(&color),
ColorSpace::YUV => conversion::yuv::rgb2yuv(&color),
ColorSpace::YCbCr => conversion::ycbcr::rgb2ycbcr(&color),
ColorSpace::Lab => conversion::lab::rgb2lab(&color),
Expand All @@ -46,7 +47,7 @@ impl Color {

#[cfg(test)]
mod tests {
use crate::{ *, utils::round };
use crate::{utils::round, *};

#[test]
fn test_vec_of_hsl() {
Expand All @@ -57,7 +58,7 @@ mod tests {

#[test]
fn test_vec_of_lab() {
let color = color!(#7654CD);
let color = color!(#7654cd);
let vec = color
.vec_of(ColorSpace::Lab)
.iter()
Expand All @@ -68,12 +69,23 @@ mod tests {

#[test]
fn test_vec_of_xyz() {
let color = color!(#7654CD);
let color = color!(#7654cd);
let vec = color
.vec_of(ColorSpace::XYZ)
.iter()
.map(|&v| round(v, 5))
.collect::<Vec<_>>();
assert_eq!(vec, vec![0.2166, 0.146, 0.59437]);
}

#[test]
fn test_vec_of_yiq() {
let color = color!(#7654cd);
let vec = color
.vec_of(ColorSpace::YIQ)
.iter()
.map(|&v| round(v, 5))
.collect::<Vec<_>>();
assert_eq!(vec, vec![0.42337, -0.07301, 0.17583]);
}
}
11 changes: 10 additions & 1 deletion src/color_space/color_space.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ pub enum ColorSpace {
///
/// XYZ stands for X, Y, and Z.
XYZ,
/// [YIQ](https://en.wikipedia.org/wiki/YIQ) color space.
///
/// YIQ stands for luminance (Y), and the chrominance components I and Q.
YIQ,
/// YUV color Space.
///
/// YUV stands for luminance (Y), and the chrominance components U and V.
Expand All @@ -65,7 +69,10 @@ impl ColorSpace {
}
}

impl<T> From<T> for ColorSpace where T: ToString {
impl<T> From<T> for ColorSpace
where
T: ToString,
{
fn from(s: T) -> Self {
match s.to_string().to_lowercase().as_str() {
"rgb" => ColorSpace::RGB,
Expand All @@ -78,6 +85,7 @@ impl<T> From<T> for ColorSpace where T: ToString {
"hwb" => ColorSpace::HWB,
"cmyk" => ColorSpace::CMYK,
"xyz" => ColorSpace::XYZ,
"yiq" => ColorSpace::YIQ,
"yuv" => ColorSpace::YUV,
"ycbcr" => ColorSpace::YCbCr,
"lab" => ColorSpace::Lab,
Expand All @@ -99,6 +107,7 @@ impl ColorSpace {
ColorSpace::HWB => 3,
ColorSpace::CMYK => 4,
ColorSpace::XYZ => 3,
ColorSpace::YIQ => 3,
ColorSpace::YUV => 3,
ColorSpace::YCbCr => 3,
ColorSpace::Lab => 3,
Expand Down
30 changes: 20 additions & 10 deletions src/color_space/valid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,9 @@ impl ColorSpace {
}
}
}
ColorSpace::HEX =>
todo!(
"HEX color space not implemented yet, please use `ColorSpace::valid_hex` instead"
),
ColorSpace::HEX => todo!(
"HEX color space not implemented yet, please use `ColorSpace::valid_hex` instead"
),
ColorSpace::HWB => {
if vec.len() != 3 {
anyhow::bail!("HWB color space requires 3 values");
Expand Down Expand Up @@ -162,6 +161,22 @@ impl ColorSpace {
}
}
}
ColorSpace::YIQ => {
if vec.len() != 3 {
anyhow::bail!("YIQ color space requires 3 values");
}
if let [y, i, q] = vec[..] {
if y < 0.0 || y > 1.0 {
anyhow::bail!("Y must be between 0.0 and 1.0, got {}", y);
}
if i < -0.5957 || i > 0.5957 {
anyhow::bail!("I must be between -0.5957 and 0.5957, got {}", i);
}
if q < -0.5226 || q > 0.5226 {
anyhow::bail!("Q must be between -0.5226 and 0.5226, got {}", q);
}
}
}
ColorSpace::YUV => {
if vec.len() != 3 {
anyhow::bail!("YUV color space requires 3 values");
Expand Down Expand Up @@ -216,12 +231,7 @@ impl ColorSpace {
}
/// Validate a hex color string
pub(crate) fn valid_hex(hex: &str) -> Result<()> {
if
!hex
.chars()
.skip(1)
.all(|c| c.is_ascii_hexdigit())
{
if !hex.chars().skip(1).all(|c| c.is_ascii_hexdigit()) {
anyhow::bail!("Hex color must be a valid hex string");
}
if hex.len() != 4 && hex.len() != 5 && hex.len() != 7 && hex.len() != 9 {
Expand Down
1 change: 1 addition & 0 deletions src/conversion/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ pub(crate) mod lab;
pub(crate) mod utils;
pub(crate) mod xyz;
pub(crate) mod ycbcr;
pub(crate) mod yiq;
pub(crate) mod yuv;
60 changes: 60 additions & 0 deletions src/conversion/yiq.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use crate::utils::{normalize_color, round};

/// Convert `YIQ` to `RGB`
///
/// reference: [From YIQ to RGB](https://en.wikipedia.org/wiki/YIQ#From_YIQ_to_RGB)
pub fn yiq2rgb(color: &[f64]) -> Vec<f64> {
let y = color[0];
let i = color[1];
let q = color[2];
let r = y + 0.956 * i + 0.619 * q;
let g = y - 0.272 * i - 0.647 * q;
let b = y - 1.106 * i + 1.703 * q;
vec![
round(r * 255.0, 0),
round(g * 255.0, 0),
round(b * 255.0, 0),
]
}

/// Convert `RGB` to `YIQ`
///
/// reference: [From RGB to YIQ](https://en.wikipedia.org/wiki/YIQ#From_RGB_to_YIQ)
pub fn rgb2yiq(color: &[f64]) -> Vec<f64> {
let color = normalize_color(color);
let r = color[0];
let g = color[1];
let b = color[2];
let y = 0.299 * r + 0.587 * g + 0.114 * b;
let i = 0.595716 * r - 0.274453 * g - 0.321263 * b;
let q = 0.211456 * r - 0.522591 * g + 0.311135 * b;
vec![y, i, q]
}

#[cfg(test)]
mod tests {

use super::*;

fn round5_vec(vec: Vec<f64>) -> Vec<f64> {
vec.iter().map(|&v| round(v, 5)).collect::<Vec<_>>()
}

#[test]
fn test_yiq2rgb() {
let color = yiq2rgb(&[1.0, 0.0, 0.0]);
assert_eq!(color, vec![255.0, 255.0, 255.0]);

let color = yiq2rgb(&[0.42337, -0.07301, 0.17583]);
assert_eq!(color, vec![118.0, 84.0, 205.0]);
}

#[test]
fn test_rgb2yiq() {
let color = rgb2yiq(&[255.0, 255.0, 255.0]);
assert_eq!(round5_vec(color), vec![1.0, 0.0, 0.0]);

let color = rgb2yiq(&[118.0, 84.0, 205.0]);
assert_eq!(round5_vec(color), vec![0.42337, -0.07301, 0.17583]);
}
}
4 changes: 1 addition & 3 deletions src/parser/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,6 @@ impl Parser {
}

pub fn validate(&mut self) -> Result<()> {
dbg!(&self.tokens);

let mut stack = Vec::new();

while let Some(token) = self.tokens.get(self.current) {
Expand Down Expand Up @@ -242,7 +240,7 @@ mod tests {
assert_eq!(values, vec![255.0, 255.0, 255.0]);

let input = "hsl(60, 80%, 50%)";

let mut parser = Parser::new();
parser.tokenize(input);

Expand Down
1 change: 1 addition & 0 deletions tests/color_stringify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ fn test_color_stringify() {
assert_eq!(color.cmyk(), "cmyk(0%, 0%, 100%, 0%)");
assert_eq!(color.name(), "yellow");
assert_eq!(color.xyz(), "xyz(0.769975, 0.927808, 0.138526)");
assert_eq!(color.yiq(), "yiq(0.886, 0.32126, -0.31114)");
assert_eq!(color.yuv(), "yuv(0.886, -0.4359, 0.1)");
assert_eq!(color.lab(), "lab(97.61, -15.75, 93.39)");
}

0 comments on commit 1fdfa7b

Please sign in to comment.