use image::Pixel;
#[derive(Debug, PartialEq)]
pub struct Yiq {
y: f32, i: f32, q: f32, }
#[allow(clippy::excessive_precision)]
fn background_color(k: usize) -> (f32, f32, f32) {
let r = 48.0 + 159.0 * ((k % 2) as f32);
let g = 48.0 + 159.0 * ((k as f32 / 1.618033988749895).floor() as u32 % 2) as f32;
let b = 48.0 + 159.0 * ((k as f32 / 2.618033988749895).floor() as u32 % 2) as f32;
(r, g, b)
}
impl Yiq {
#[allow(clippy::excessive_precision)]
pub fn rgb2y(r: f32, g: f32, b: f32) -> f32 {
0.298_895_31 * r + 0.586_622_47 * g + 0.114_482_23 * b
}
#[allow(clippy::excessive_precision)]
pub fn rgb2i(r: f32, g: f32, b: f32) -> f32 {
0.595_977_99 * r - 0.274_171_6 * g - 0.321_801_89 * b
}
#[allow(clippy::excessive_precision)]
pub fn rgb2q(r: f32, g: f32, b: f32) -> f32 {
0.211_470_19 * r - 0.522_617_11 * g + 0.311_146_94 * b
}
pub fn from_rgba_with_pos(rgba: &image::Rgba<u8>, pos: usize) -> Self {
let rgba_channels = rgba.channels();
let r = f32::from(rgba_channels[0]);
let g = f32::from(rgba_channels[1]);
let b = f32::from(rgba_channels[2]);
let a = f32::from(rgba_channels[3]);
let (r, g, b) = if a < 255.0 {
let alpha = a / 255.0;
let (bg_r, bg_g, bg_b) = background_color(pos);
(
bg_r + (r - bg_r) * alpha,
bg_g + (g - bg_g) * alpha,
bg_b + (b - bg_b) * alpha,
)
} else {
(r, g, b)
};
let y = Self::rgb2y(r, g, b);
let i = Self::rgb2i(r, g, b);
let q = Self::rgb2q(r, g, b);
Self { y, i, q }
}
#[deprecated(
since = "0.8.0",
note = "Use from_rgba_with_pos instead for correct transparency handling"
)]
#[allow(dead_code)]
pub fn from_rgba(rgba: &image::Rgba<u8>) -> Self {
let rgb = rgba.to_rgb();
let rgb_channels = rgb.channels();
let r = f32::from(rgb_channels[0]);
let g = f32::from(rgb_channels[1]);
let b = f32::from(rgb_channels[2]);
let y = Self::rgb2y(r, g, b);
let i = Self::rgb2i(r, g, b);
let q = Self::rgb2q(r, g, b);
Self { y, i, q }
}
pub fn delta_y(left: &image::Rgb<u8>, right: &image::Rgb<u8>) -> f32 {
let left_channels = left.channels();
let (left_r, left_g, left_b) = (
f32::from(left_channels[0]),
f32::from(left_channels[1]),
f32::from(left_channels[2]),
);
let right_channels = right.channels();
let (right_r, right_g, right_b) = (
f32::from(right_channels[0]),
f32::from(right_channels[1]),
f32::from(right_channels[2]),
);
Self::rgb2y(left_r, left_g, left_b) - Self::rgb2y(right_r, right_g, right_b)
}
pub fn squared_distance(&self, other: &Self) -> f32 {
let delta_y = self.y - other.y;
let delta_i = self.i - other.i;
let delta_q = self.q - other.q;
let delta = 0.5053 * delta_y.powi(2) + 0.299 * delta_i.powi(2) + 0.195_7 * delta_q.powi(2);
if self.y > other.y { -delta } else { delta }
}
pub fn blend_with_white(&self, alpha: f32) -> f32 {
255.0 + (self.y - 255.0) * alpha
}
}
#[cfg(test)]
mod tests {
use super::Yiq;
#[test]
fn test_from_rgb() {
let expected = Yiq {
y: 0.0,
i: 0.0,
q: 0.0,
};
let actual = Yiq::from_rgba_with_pos(&image::Rgba([0, 0, 0, 255]), 0);
assert_eq!(expected, actual);
}
#[test]
fn test_squared_distance_same() {
let a = Yiq {
y: 0.5,
i: -0.1,
q: 0.1,
};
let b = Yiq {
y: 0.5,
i: -0.1,
q: 0.1,
};
assert_eq!(a.squared_distance(&b), 0.0);
}
#[test]
fn test_issue_32_transparent_vs_opaque_black() {
let opaque_black = Yiq::from_rgba_with_pos(&image::Rgba([0, 0, 0, 255]), 0);
let transparent_black = Yiq::from_rgba_with_pos(&image::Rgba([0, 0, 0, 0]), 0);
assert_ne!(
opaque_black.squared_distance(&transparent_black),
0.0,
"Transparent black and opaque black should have different YIQ values"
);
}
#[test]
fn test_semi_transparent_pixels() {
let opaque = Yiq::from_rgba_with_pos(&image::Rgba([100, 50, 25, 255]), 0);
let semi_transparent = Yiq::from_rgba_with_pos(&image::Rgba([100, 50, 25, 128]), 0);
let transparent = Yiq::from_rgba_with_pos(&image::Rgba([100, 50, 25, 0]), 0);
assert_ne!(opaque.y, semi_transparent.y);
assert_ne!(opaque.y, transparent.y);
assert_ne!(semi_transparent.y, transparent.y);
}
#[test]
fn test_position_dependent_background() {
let transparent_red_pos0 = Yiq::from_rgba_with_pos(&image::Rgba([255, 0, 0, 0]), 0);
let transparent_red_pos1 = Yiq::from_rgba_with_pos(&image::Rgba([255, 0, 0, 0]), 1);
assert_ne!(
transparent_red_pos0, transparent_red_pos1,
"Same transparent color at different positions should differ"
);
}
#[test]
fn test_fully_transparent_pixels_with_different_rgb_compare_equal() {
let pos = 42;
let transparent_black = Yiq::from_rgba_with_pos(&image::Rgba([0, 0, 0, 0]), pos);
let transparent_red = Yiq::from_rgba_with_pos(&image::Rgba([255, 0, 0, 0]), pos);
let transparent_white = Yiq::from_rgba_with_pos(&image::Rgba([255, 255, 255, 0]), pos);
assert_eq!(transparent_black, transparent_red);
assert_eq!(transparent_black, transparent_white);
assert_eq!(
transparent_black.squared_distance(&transparent_red),
0.0,
"Fully transparent pixels should have zero distance regardless of RGB"
);
}
#[test]
fn test_opaque_pixels_position_independent() {
let opaque_red_pos0 = Yiq::from_rgba_with_pos(&image::Rgba([255, 0, 0, 255]), 0);
let opaque_red_pos1 = Yiq::from_rgba_with_pos(&image::Rgba([255, 0, 0, 255]), 100);
assert_eq!(
opaque_red_pos0, opaque_red_pos1,
"Opaque pixels should be position-independent"
);
}
#[test]
fn test_various_colors_with_transparency() {
let color_rgb = [
[255, 0, 0], [0, 255, 0], [0, 0, 255], [255, 255, 255], ];
for rgb in color_rgb {
let transparent = Yiq::from_rgba_with_pos(&image::Rgba([rgb[0], rgb[1], rgb[2], 0]), 0);
let opaque = Yiq::from_rgba_with_pos(&image::Rgba([rgb[0], rgb[1], rgb[2], 255]), 0);
assert_ne!(
transparent, opaque,
"Transparent {:?} should differ from opaque",
rgb
);
}
}
}