use crate::color::{ColorError, ColorResult};
use crate::core::{Box, Pix, PixelDepth, pixel};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PaintType {
#[default]
Light,
Dark,
}
#[derive(Debug, Clone)]
pub struct ColorGrayOptions {
pub paint_type: PaintType,
pub threshold: u8,
pub target_color: (u8, u8, u8),
}
impl Default for ColorGrayOptions {
fn default() -> Self {
Self {
paint_type: PaintType::Light,
threshold: 0,
target_color: (255, 0, 0), }
}
}
pub fn pixel_fractional_shift(r: u8, g: u8, b: u8, fract: f32) -> ColorResult<(u8, u8, u8)> {
if !(-1.0..=1.0).contains(&fract) {
return Err(ColorError::InvalidParameters(
"fraction must be in range [-1.0, 1.0]".to_string(),
));
}
let (nr, ng, nb) = if fract < 0.0 {
let factor = 1.0 + fract;
(
(factor * r as f32 + 0.5) as u8,
(factor * g as f32 + 0.5) as u8,
(factor * b as f32 + 0.5) as u8,
)
} else {
(
r + (fract * (255.0 - r as f32) + 0.5) as u8,
g + (fract * (255.0 - g as f32) + 0.5) as u8,
b + (fract * (255.0 - b as f32) + 0.5) as u8,
)
};
Ok((nr, ng, nb))
}
pub fn pixel_shift_by_component(
r: u8,
g: u8,
b: u8,
src_color: u32,
dst_color: u32,
) -> (u8, u8, u8) {
let (rs, gs, bs) = extract_rgb_from_color(src_color);
let (rd, gd, bd) = extract_rgb_from_color(dst_color);
let nr = shift_component(r, rs, rd);
let ng = shift_component(g, gs, gd);
let nb = shift_component(b, bs, bd);
(nr, ng, nb)
}
#[inline]
fn shift_component(val: u8, src: u8, dst: u8) -> u8 {
if dst == src {
val
} else if dst < src {
if src == 0 {
0
} else {
((val as u32 * dst as u32) / src as u32) as u8
}
} else {
if src == 255 {
val
} else {
255 - (((255 - dst) as u32 * (255 - val) as u32) / (255 - src) as u32) as u8
}
}
}
pub fn pixel_linear_map_to_target_color(pixel: u32, src_map: u32, dst_map: u32) -> u32 {
let (r, g, b, a) = pixel::extract_rgba(pixel);
let (sr, sg, sb) = extract_rgb_from_color(src_map);
let (dr, dg, db) = extract_rgb_from_color(dst_map);
let sr = sr.clamp(1, 254);
let sg = sg.clamp(1, 254);
let sb = sb.clamp(1, 254);
let nr = linear_map_component(r, sr, dr);
let ng = linear_map_component(g, sg, dg);
let nb = linear_map_component(b, sb, db);
pixel::compose_rgba(nr, ng, nb, a)
}
#[inline]
fn linear_map_component(val: u8, src: u8, dst: u8) -> u8 {
if val <= src {
((val as u32 * dst as u32) / src as u32) as u8
} else {
dst + (((255 - dst) as u32 * (val - src) as u32) / (255 - src) as u32) as u8
}
}
#[inline]
fn extract_rgb_from_color(color: u32) -> (u8, u8, u8) {
let r = ((color >> 24) & 0xff) as u8;
let g = ((color >> 16) & 0xff) as u8;
let b = ((color >> 8) & 0xff) as u8;
(r, g, b)
}
#[inline]
fn compose_color_from_rgb(r: u8, g: u8, b: u8) -> u32 {
((r as u32) << 24) | ((g as u32) << 16) | ((b as u32) << 8)
}
pub fn pix_color_gray(
pix: &Pix,
region: Option<&Box>,
options: &ColorGrayOptions,
) -> ColorResult<Pix> {
if pix.depth() != PixelDepth::Bit32 {
return Err(ColorError::UnsupportedDepth {
expected: "32 bpp",
actual: pix.depth().bits(),
});
}
match options.paint_type {
PaintType::Light => {
if options.threshold == 255 {
return Err(ColorError::InvalidParameters(
"threshold must be < 255 for Light paint type".to_string(),
));
}
}
PaintType::Dark => {
if options.threshold == 0 {
return Err(ColorError::InvalidParameters(
"threshold must be > 0 for Dark paint type".to_string(),
));
}
}
}
let w = pix.width();
let h = pix.height();
let (x1, y1, x2, y2) = if let Some(b) = region {
let bx = b.x.max(0) as u32;
let by = b.y.max(0) as u32;
let bw = b.w as u32;
let bh = b.h as u32;
(bx, by, (bx + bw).min(w), (by + bh).min(h))
} else {
(0, 0, w, h)
};
let out_pix = Pix::new(w, h, PixelDepth::Bit32)?;
let mut out_mut = out_pix.try_into_mut().unwrap();
out_mut.set_spp(pix.spp());
for y in 0..h {
for x in 0..w {
let pixel = pix.get_pixel_unchecked(x, y);
let new_pixel = if x >= x1 && x < x2 && y >= y1 && y < y2 {
colorize_pixel(pixel, options)
} else {
pixel
};
out_mut.set_pixel_unchecked(x, y, new_pixel);
}
}
Ok(out_mut.into())
}
pub fn pix_color_gray_masked(
pix: &Pix,
mask: &Pix,
options: &ColorGrayOptions,
) -> ColorResult<Pix> {
if pix.depth() != PixelDepth::Bit32 {
return Err(ColorError::UnsupportedDepth {
expected: "32 bpp",
actual: pix.depth().bits(),
});
}
if mask.depth() != PixelDepth::Bit1 {
return Err(ColorError::UnsupportedDepth {
expected: "1 bpp mask",
actual: mask.depth().bits(),
});
}
match options.paint_type {
PaintType::Light => {
if options.threshold == 255 {
return Err(ColorError::InvalidParameters(
"threshold must be < 255 for Light paint type".to_string(),
));
}
}
PaintType::Dark => {
if options.threshold == 0 {
return Err(ColorError::InvalidParameters(
"threshold must be > 0 for Dark paint type".to_string(),
));
}
}
}
let w = pix.width();
let h = pix.height();
let mask_w = mask.width();
let mask_h = mask.height();
let w_min = w.min(mask_w);
let h_min = h.min(mask_h);
let out_pix = Pix::new(w, h, PixelDepth::Bit32)?;
let mut out_mut = out_pix.try_into_mut().unwrap();
out_mut.set_spp(pix.spp());
for y in 0..h {
for x in 0..w {
let pixel = pix.get_pixel_unchecked(x, y);
let new_pixel = if x < w_min && y < h_min {
let mask_val = mask.get_pixel_unchecked(x, y);
if mask_val != 0 {
colorize_pixel(pixel, options)
} else {
pixel
}
} else {
pixel
};
out_mut.set_pixel_unchecked(x, y, new_pixel);
}
}
Ok(out_mut.into())
}
#[inline]
fn colorize_pixel(pixel: u32, options: &ColorGrayOptions) -> u32 {
let (r, g, b, a) = pixel::extract_rgba(pixel);
let (tr, tg, tb) = options.target_color;
let avg = ((r as u32 + g as u32 + b as u32) / 3) as u8;
match options.paint_type {
PaintType::Light => {
if avg <= options.threshold {
return pixel;
}
let factor = avg as f32 / 255.0;
let nr = (tr as f32 * factor) as u8;
let ng = (tg as f32 * factor) as u8;
let nb = (tb as f32 * factor) as u8;
pixel::compose_rgba(nr, ng, nb, a)
}
PaintType::Dark => {
if avg >= options.threshold {
return pixel;
}
let factor = avg as f32 / 255.0;
let nr = tr + ((255.0 - tr as f32) * factor) as u8;
let ng = tg + ((255.0 - tg as f32) * factor) as u8;
let nb = tb + ((255.0 - tb as f32) * factor) as u8;
pixel::compose_rgba(nr, ng, nb, a)
}
}
}
pub fn pix_snap_color(pix: &Pix, src_color: u32, dst_color: u32, diff: u8) -> ColorResult<Pix> {
match pix.depth() {
PixelDepth::Bit8 => pix_snap_color_8bpp(pix, src_color, dst_color, diff),
PixelDepth::Bit32 => pix_snap_color_32bpp(pix, src_color, dst_color, diff),
_ => Err(ColorError::UnsupportedDepth {
expected: "8 or 32 bpp",
actual: pix.depth().bits(),
}),
}
}
fn pix_snap_color_8bpp(pix: &Pix, src_color: u32, dst_color: u32, diff: u8) -> ColorResult<Pix> {
let w = pix.width();
let h = pix.height();
let src_val = (src_color & 0xff) as u8;
let dst_val = (dst_color & 0xff) as u8;
let out_pix = Pix::new(w, h, PixelDepth::Bit8)?;
let mut out_mut = out_pix.try_into_mut().unwrap();
for y in 0..h {
for x in 0..w {
let val = pix.get_pixel_unchecked(x, y) as u8;
let new_val = if (val as i16 - src_val as i16).unsigned_abs() as u8 <= diff {
dst_val
} else {
val
};
out_mut.set_pixel_unchecked(x, y, new_val as u32);
}
}
Ok(out_mut.into())
}
fn pix_snap_color_32bpp(pix: &Pix, src_color: u32, dst_color: u32, diff: u8) -> ColorResult<Pix> {
let w = pix.width();
let h = pix.height();
let (sr, sg, sb) = extract_rgb_from_color(src_color);
let out_pix = Pix::new(w, h, PixelDepth::Bit32)?;
let mut out_mut = out_pix.try_into_mut().unwrap();
out_mut.set_spp(pix.spp());
for y in 0..h {
for x in 0..w {
let pixel = pix.get_pixel_unchecked(x, y);
let (r, g, b, a) = pixel::extract_rgba(pixel);
let new_pixel = if (r as i16 - sr as i16).unsigned_abs() as u8 <= diff
&& (g as i16 - sg as i16).unsigned_abs() as u8 <= diff
&& (b as i16 - sb as i16).unsigned_abs() as u8 <= diff
{
let (dr, dg, db) = extract_rgb_from_color(dst_color);
pixel::compose_rgba(dr, dg, db, a)
} else {
pixel
};
out_mut.set_pixel_unchecked(x, y, new_pixel);
}
}
Ok(out_mut.into())
}
pub fn pix_linear_map_to_target_color(
pix: &Pix,
src_color: u32,
dst_color: u32,
) -> ColorResult<Pix> {
if pix.depth() != PixelDepth::Bit32 {
return Err(ColorError::UnsupportedDepth {
expected: "32 bpp",
actual: pix.depth().bits(),
});
}
let w = pix.width();
let h = pix.height();
let (sr, sg, sb) = extract_rgb_from_color(src_color);
let (dr, dg, db) = extract_rgb_from_color(dst_color);
let sr = sr.clamp(1, 254);
let sg = sg.clamp(1, 254);
let sb = sb.clamp(1, 254);
let mut rtab = [0u8; 256];
let mut gtab = [0u8; 256];
let mut btab = [0u8; 256];
for i in 0..256 {
rtab[i] = linear_map_component(i as u8, sr, dr);
gtab[i] = linear_map_component(i as u8, sg, dg);
btab[i] = linear_map_component(i as u8, sb, db);
}
let out_pix = Pix::new(w, h, PixelDepth::Bit32)?;
let mut out_mut = out_pix.try_into_mut().unwrap();
out_mut.set_spp(pix.spp());
for y in 0..h {
for x in 0..w {
let pixel = pix.get_pixel_unchecked(x, y);
let (r, g, b, a) = pixel::extract_rgba(pixel);
let nr = rtab[r as usize];
let ng = gtab[g as usize];
let nb = btab[b as usize];
let new_pixel = pixel::compose_rgba(nr, ng, nb, a);
out_mut.set_pixel_unchecked(x, y, new_pixel);
}
}
Ok(out_mut.into())
}
pub fn pix_shift_by_component(pix: &Pix, src_color: u32, dst_color: u32) -> ColorResult<Pix> {
if pix.depth() != PixelDepth::Bit32 {
return Err(ColorError::UnsupportedDepth {
expected: "32 bpp",
actual: pix.depth().bits(),
});
}
let w = pix.width();
let h = pix.height();
let (sr, sg, sb) = extract_rgb_from_color(src_color);
let (dr, dg, db) = extract_rgb_from_color(dst_color);
let mut rtab = [0u8; 256];
let mut gtab = [0u8; 256];
let mut btab = [0u8; 256];
for i in 0..256 {
rtab[i] = shift_component(i as u8, sr, dr);
gtab[i] = shift_component(i as u8, sg, dg);
btab[i] = shift_component(i as u8, sb, db);
}
let out_pix = Pix::new(w, h, PixelDepth::Bit32)?;
let mut out_mut = out_pix.try_into_mut().unwrap();
out_mut.set_spp(pix.spp());
for y in 0..h {
for x in 0..w {
let pixel = pix.get_pixel_unchecked(x, y);
let (r, g, b, a) = pixel::extract_rgba(pixel);
let nr = rtab[r as usize];
let ng = gtab[g as usize];
let nb = btab[b as usize];
let new_pixel = pixel::compose_rgba(nr, ng, nb, a);
out_mut.set_pixel_unchecked(x, y, new_pixel);
}
}
Ok(out_mut.into())
}
pub fn pix_map_with_invariant_hue(pix: &Pix, src_color: u32, fract: f32) -> ColorResult<Pix> {
if pix.depth() != PixelDepth::Bit32 {
return Err(ColorError::UnsupportedDepth {
expected: "32 bpp",
actual: pix.depth().bits(),
});
}
if !(-1.0..=1.0).contains(&fract) {
return Err(ColorError::InvalidParameters(
"fraction must be in range [-1.0, 1.0]".to_string(),
));
}
let (r, g, b) = extract_rgb_from_color(src_color);
let (dr, dg, db) = pixel_fractional_shift(r, g, b, fract)?;
let dst_color = compose_color_from_rgb(dr, dg, db);
pix_linear_map_to_target_color(pix, src_color, dst_color)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pixel_fractional_shift_to_black() {
let (r, g, b) = pixel_fractional_shift(200, 150, 100, -1.0).unwrap();
assert_eq!((r, g, b), (0, 0, 0));
}
#[test]
fn test_pixel_fractional_shift_to_white() {
let (r, g, b) = pixel_fractional_shift(100, 150, 200, 1.0).unwrap();
assert_eq!((r, g, b), (255, 255, 255));
}
#[test]
fn test_pixel_fractional_shift_no_change() {
let (r, g, b) = pixel_fractional_shift(100, 150, 200, 0.0).unwrap();
assert_eq!((r, g, b), (100, 150, 200));
}
#[test]
fn test_pixel_fractional_shift_invalid_fract() {
assert!(pixel_fractional_shift(100, 150, 200, 1.5).is_err());
assert!(pixel_fractional_shift(100, 150, 200, -1.5).is_err());
}
#[test]
fn test_shift_component() {
assert_eq!(shift_component(100, 128, 128), 100);
assert_eq!(shift_component(200, 255, 128), 100);
let result = shift_component(200, 128, 200);
assert!(result > 200); }
#[test]
fn test_linear_map_component() {
assert_eq!(linear_map_component(128, 128, 64), 64);
assert_eq!(linear_map_component(0, 128, 200), 0);
assert_eq!(linear_map_component(255, 128, 200), 255);
}
#[test]
fn test_extract_rgb_from_color() {
let (r, g, b) = extract_rgb_from_color(0xFF804000);
assert_eq!((r, g, b), (255, 128, 64));
}
#[test]
fn test_compose_color_from_rgb() {
let color = compose_color_from_rgb(255, 128, 64);
assert_eq!(color, 0xFF804000);
}
#[test]
fn test_pix_color_gray_light() {
let pix = Pix::new(10, 10, PixelDepth::Bit32).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
for y in 0..10 {
for x in 0..10 {
let pixel = pixel::compose_rgb(128, 128, 128);
pix_mut.set_pixel_unchecked(x, y, pixel);
}
}
let options = ColorGrayOptions {
paint_type: PaintType::Light,
threshold: 0,
target_color: (255, 0, 0), };
let result = pix_color_gray(&pix_mut.into(), None, &options).unwrap();
let pixel = result.get_pixel_unchecked(5, 5);
let (r, g, b, _) = pixel::extract_rgba(pixel);
assert!(r > g);
assert!(r > b);
assert!((r as i32 - 128).abs() < 10);
}
#[test]
fn test_pix_color_gray_dark() {
let pix = Pix::new(10, 10, PixelDepth::Bit32).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
for y in 0..10 {
for x in 0..10 {
let pixel = pixel::compose_rgb(64, 64, 64);
pix_mut.set_pixel_unchecked(x, y, pixel);
}
}
let options = ColorGrayOptions {
paint_type: PaintType::Dark,
threshold: 255,
target_color: (0, 0, 255), };
let result = pix_color_gray(&pix_mut.into(), None, &options).unwrap();
let pixel = result.get_pixel_unchecked(5, 5);
let (r, g, b, _) = pixel::extract_rgba(pixel);
assert!(b > r);
assert!(b > g);
}
#[test]
fn test_pix_snap_color_32bpp() {
let pix = Pix::new(10, 10, PixelDepth::Bit32).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
for y in 0..10 {
for x in 0..10 {
let pixel = pixel::compose_rgb(250, 252, 248);
pix_mut.set_pixel_unchecked(x, y, pixel);
}
}
let result = pix_snap_color(&pix_mut.into(), 0xFFFFFF00, 0xFFFFFF00, 10).unwrap();
let pixel = result.get_pixel_unchecked(5, 5);
let (r, g, b, _) = pixel::extract_rgba(pixel);
assert_eq!((r, g, b), (255, 255, 255));
}
#[test]
fn test_pix_snap_color_8bpp() {
let pix = Pix::new(10, 10, PixelDepth::Bit8).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
for y in 0..10 {
for x in 0..10 {
pix_mut.set_pixel_unchecked(x, y, 250);
}
}
let result = pix_snap_color(&pix_mut.into(), 0x000000FF, 0x000000FF, 10).unwrap();
let val = result.get_pixel_unchecked(5, 5) as u8;
assert_eq!(val, 255);
}
#[test]
fn test_pix_linear_map_to_target_color() {
let pix = Pix::new(10, 10, PixelDepth::Bit32).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
for y in 0..10 {
for x in 0..10 {
let pixel = pixel::compose_rgb(128, 128, 128);
pix_mut.set_pixel_unchecked(x, y, pixel);
}
}
let result =
pix_linear_map_to_target_color(&pix_mut.into(), 0x80808000, 0x40404000).unwrap();
let pixel = result.get_pixel_unchecked(5, 5);
let (r, g, b, _) = pixel::extract_rgba(pixel);
assert_eq!(r, 64);
assert_eq!(g, 64);
assert_eq!(b, 64);
}
#[test]
fn test_pix_shift_by_component() {
let pix = Pix::new(10, 10, PixelDepth::Bit32).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
for y in 0..10 {
for x in 0..10 {
let pixel = pixel::compose_rgb(255, 255, 255);
pix_mut.set_pixel_unchecked(x, y, pixel);
}
}
let result = pix_shift_by_component(&pix_mut.into(), 0xFFFFFF00, 0xFF808000).unwrap();
let pixel = result.get_pixel_unchecked(5, 5);
let (r, g, b, _) = pixel::extract_rgba(pixel);
assert_eq!(r, 255);
assert!(g < 255);
assert!(b < 255);
}
#[test]
fn test_pix_map_with_invariant_hue() {
let pix = Pix::new(10, 10, PixelDepth::Bit32).unwrap();
let mut pix_mut = pix.try_into_mut().unwrap();
for y in 0..10 {
for x in 0..10 {
let pixel = pixel::compose_rgb(200, 100, 50);
pix_mut.set_pixel_unchecked(x, y, pixel);
}
}
let result = pix_map_with_invariant_hue(&pix_mut.into(), 0xC8643200, 0.5).unwrap();
let pixel = result.get_pixel_unchecked(5, 5);
let (r, g, b, _) = pixel::extract_rgba(pixel);
assert!(r > 200 || (r == 255));
assert!(g > 100);
assert!(b > 50);
}
#[test]
fn test_color_gray_threshold_validation() {
let pix = Pix::new(10, 10, PixelDepth::Bit32).unwrap();
let options = ColorGrayOptions {
paint_type: PaintType::Light,
threshold: 255,
target_color: (255, 0, 0),
};
assert!(pix_color_gray(&pix, None, &options).is_err());
let options = ColorGrayOptions {
paint_type: PaintType::Dark,
threshold: 0,
target_color: (255, 0, 0),
};
assert!(pix_color_gray(&pix, None, &options).is_err());
}
}