use colorthief::{
Algorithm, Dominant, Mmcq, Rgb48Frame, RgbFrame, RgbFrameError, extract, extract_rgb48,
};
fn solid_color_frame(rgb: [u8; 3], width: u32, height: u32) -> Vec<u8> {
let mut buf = Vec::with_capacity((width * height) as usize * 3);
for _ in 0..width * height {
buf.extend_from_slice(&rgb);
}
buf
}
#[test]
fn extract_on_solid_red_returns_a_red_named_color() {
let buf = solid_color_frame([255, 0, 0], 8, 8);
let frame = RgbFrame::try_new(&buf, 8, 8, 24).expect("frame");
let dominants = extract(frame, 5);
assert!(!dominants.is_empty(), "expected at least one dominant");
let top = dominants[0];
assert!(
top.color().family().as_str().contains("red") || top.color().name().contains("red"),
"top dominant on solid red was rgb={:?} name={:?} family={:?}",
top.rgb(),
top.color().name(),
top.color().family().as_str(),
);
assert!(top.population() > 0, "population must be non-zero");
}
#[test]
fn extract_on_solid_blue_returns_a_blue_named_color() {
let buf = solid_color_frame([0, 0, 255], 8, 8);
let frame = RgbFrame::try_new(&buf, 8, 8, 24).expect("frame");
let dominants = extract(frame, 5);
assert!(!dominants.is_empty());
let top = dominants[0];
assert!(
top.color().family().as_str().contains("blue") || top.color().name().contains("blue"),
"top dominant on solid blue was rgb={:?} name={:?} family={:?}",
top.rgb(),
top.color().name(),
top.color().family().as_str(),
);
}
#[test]
fn extract_count_zero_returns_empty() {
let buf = solid_color_frame([128, 128, 128], 4, 4);
let frame = RgbFrame::try_new(&buf, 4, 4, 12).expect("frame");
let dominants = extract(frame, 0);
assert!(dominants.is_empty());
}
#[test]
fn extract_on_red_blue_split_recovers_both_hues() {
let mut buf = Vec::with_capacity(64 * 3);
for row in 0..8 {
for _ in 0..8 {
let rgb = if row < 4 { [255, 0, 0] } else { [0, 0, 255] };
buf.extend_from_slice(&rgb);
}
}
let frame = RgbFrame::try_new(&buf, 8, 8, 24).expect("frame");
let dominants = extract(frame, 5);
assert!(dominants.len() >= 2);
let has_red = dominants
.iter()
.any(|d| d.color().family().as_str().contains("red") || d.color().name().contains("red"));
let has_blue = dominants
.iter()
.any(|d| d.color().family().as_str().contains("blue") || d.color().name().contains("blue"));
assert!(
has_red && has_blue,
"expected red and blue named entries, got: {:?}",
dominants
.iter()
.map(|d| (d.color().name(), d.rgb(), d.population()))
.collect::<Vec<_>>()
);
}
#[test]
fn extract_dominants_sorted_by_population_descending() {
let mut buf = Vec::with_capacity(64 * 3);
for row in 0..8 {
for _ in 0..8 {
let rgb = if row < 6 { [255, 0, 0] } else { [0, 0, 255] };
buf.extend_from_slice(&rgb);
}
}
let frame = RgbFrame::try_new(&buf, 8, 8, 24).expect("frame");
let dominants = extract(frame, 5);
assert!(dominants.len() >= 2);
for window in dominants.windows(2) {
assert!(
window[0].population() >= window[1].population(),
"dominants must be sorted by descending population: {:?}",
dominants
.iter()
.map(|d| (d.color().name(), d.population()))
.collect::<Vec<_>>()
);
}
let top = dominants[0];
assert!(
top.color().family().as_str().contains("red") || top.color().name().contains("red"),
"75%-red frame: top dominant should be red, got {:?}",
top.color().name()
);
}
#[test]
fn extract_count_one_returns_at_most_one() {
let mut buf = Vec::with_capacity(16 * 3);
for i in 0..16 {
let rgb = if i < 8 { [255, 0, 0] } else { [0, 0, 255] };
buf.extend_from_slice(&rgb);
}
let frame = RgbFrame::try_new(&buf, 4, 4, 12).expect("frame");
let dominants = extract(frame, 1);
assert!(
dominants.len() <= 1,
"extract(_, 1) must return at most 1 entry, got {}: {:?}",
dominants.len(),
dominants.iter().map(|d| d.rgb()).collect::<Vec<_>>(),
);
}
fn striped_palette_frame(palette: &[[u8; 3]], width: u32, height: u32) -> Vec<u8> {
let total = (width * height) as usize;
let mut buf = Vec::with_capacity(total * 3);
for i in 0..total {
buf.extend_from_slice(&palette[i % palette.len()]);
}
buf
}
#[test]
fn extract_count_3_returns_full_count() {
let palette = [[200u8, 30, 30], [30, 200, 30], [30, 30, 200]];
let buf = striped_palette_frame(&palette, 8, 8);
let frame = RgbFrame::try_new(&buf, 8, 8, 24).expect("frame");
let dominants = extract(frame, 3);
assert_eq!(dominants.len(), 3);
for d in &dominants {
assert!(d.population() > 0);
}
}
#[test]
fn extract_count_7_returns_full_count() {
let palette = [
[200u8, 30, 30],
[30, 200, 30],
[30, 30, 200],
[200, 200, 30],
[30, 200, 200],
[200, 30, 200],
[128, 128, 128],
];
let buf = striped_palette_frame(&palette, 8, 8);
let frame = RgbFrame::try_new(&buf, 8, 8, 24).expect("frame");
let dominants = extract(frame, 7);
assert_eq!(dominants.len(), 7);
for d in &dominants {
assert!(d.population() > 0);
}
}
#[test]
fn extract_with_count_distinct_colors_returns_full_count() {
let palette: [[u8; 3]; 5] = [
[10, 200, 10], [10, 10, 200], [200, 10, 200], [10, 200, 200], [255, 10, 10], ];
let mut buf = Vec::with_capacity(64 * 3);
for i in 0..64 {
let rgb = palette[i % 5];
buf.extend_from_slice(&rgb);
}
let frame = RgbFrame::try_new(&buf, 8, 8, 24).expect("frame");
assert_eq!(frame.rgb(), buf.as_slice());
assert_eq!(frame.width(), 8);
assert_eq!(frame.height(), 8);
assert_eq!(frame.stride(), 24);
let dominants = extract(frame, 5);
assert_eq!(
dominants.len(),
5,
"5 distinct colors but extract returned {} dominants: {:?}",
dominants.len(),
dominants
.iter()
.map(|d| (d.rgb(), d.population()))
.collect::<Vec<_>>(),
);
for d in &dominants {
assert!(
d.population() > 0,
"zero-population dominant: rgb={:?}",
d.rgb()
);
}
}
#[test]
fn extract_no_zero_population_dominants_below_distinct_color_floor() {
let mut buf = Vec::with_capacity(64 * 3);
for i in 0..64 {
let rgb = if i < 32 { [255, 0, 0] } else { [0, 0, 255] };
buf.extend_from_slice(&rgb);
}
let frame = RgbFrame::try_new(&buf, 8, 8, 24).expect("frame");
let dominants = extract(frame, 5);
assert!(
dominants.len() <= 2,
"frame has 2 distinct colors but extract returned {} dominants: {:?}",
dominants.len(),
dominants
.iter()
.map(|d| (d.rgb(), d.population()))
.collect::<Vec<_>>(),
);
for d in &dominants {
assert!(
d.population() > 0,
"zero-population dominant in result: rgb={:?} name={:?}",
d.rgb(),
d.color().name(),
);
}
}
fn solid_color_frame_u16(rgb: [u16; 3], width: u32, height: u32) -> Vec<u16> {
let mut buf = Vec::with_capacity((width * height) as usize * 3);
for _ in 0..width * height {
buf.extend_from_slice(&rgb);
}
buf
}
#[test]
fn extract_rgb48_on_solid_red_returns_a_red_named_color() {
let buf = solid_color_frame_u16([0xFF00, 0x0000, 0x0000], 8, 8);
let frame = Rgb48Frame::try_new(&buf, 8, 8, 24).expect("frame");
assert_eq!(frame.width(), 8);
assert_eq!(frame.height(), 8);
assert_eq!(frame.stride(), 24);
assert_eq!(frame.rgb16(), buf.as_slice());
let dominants = extract_rgb48(frame, 5);
assert!(!dominants.is_empty(), "expected at least one dominant");
let top = dominants[0];
assert!(
top.color().family().as_str().contains("red") || top.color().name().contains("red"),
"top dominant on solid red u16 was rgb={:?} name={:?} family={:?}",
top.rgb(),
top.color().name(),
top.color().family().as_str(),
);
}
#[test]
fn extract_rgb48_widened_matches_extract_u8() {
let mut buf_u8 = Vec::with_capacity(64 * 3);
for i in 0..64 {
let rgb = match i % 4 {
0 => [200, 30, 30],
1 => [30, 30, 200],
2 => [30, 200, 30],
_ => [180, 180, 180],
};
buf_u8.extend_from_slice(&rgb);
}
let buf_u16: Vec<u16> = buf_u8.iter().map(|&b| (b as u16) << 8).collect();
let frame_u8 = RgbFrame::try_new(&buf_u8, 8, 8, 24).expect("u8 frame");
let frame_u16 = Rgb48Frame::try_new(&buf_u16, 8, 8, 24).expect("u16 frame");
let d_u8 = extract(frame_u8, 5);
let d_u16 = extract_rgb48(frame_u16, 5);
assert_eq!(d_u8.len(), d_u16.len(), "dominant counts must match");
for (a, b) in d_u8.iter().zip(d_u16.iter()) {
assert_eq!(
a.rgb(),
b.rgb(),
"u8 and u16-widened paths produced different RGBs"
);
assert_eq!(a.population(), b.population(), "populations diverged");
assert_eq!(
a.color().name(),
b.color().name(),
"named colors diverged: u8={} u16={}",
a.color().name(),
b.color().name(),
);
}
}
#[test]
fn rgb48_try_new_rejects_zero_dimension() {
let buf = vec![0u16; 12];
let err = Rgb48Frame::try_new(&buf, 0, 4, 12).unwrap_err();
assert!(matches!(err, RgbFrameError::ZeroDimension { .. }));
let err = Rgb48Frame::try_new(&buf, 4, 0, 12).unwrap_err();
assert!(matches!(err, RgbFrameError::ZeroDimension { .. }));
}
#[test]
fn rgb48_try_new_rejects_width_overflow() {
let buf: Vec<u16> = Vec::new();
let err = Rgb48Frame::try_new(&buf, u32::MAX / 2, 1, u32::MAX).unwrap_err();
assert!(
matches!(err, RgbFrameError::WidthOverflow { .. }),
"expected WidthOverflow, got {err:?}",
);
}
#[test]
#[cfg(target_pointer_width = "32")]
fn rgb48_try_new_rejects_geometry_overflow() {
let buf: Vec<u16> = Vec::new();
let err = Rgb48Frame::try_new(&buf, 1, u32::MAX, u32::MAX).unwrap_err();
assert!(
matches!(err, RgbFrameError::GeometryOverflow { .. }),
"expected GeometryOverflow, got {err:?}",
);
}
#[test]
fn rgb_try_new_rejects_zero_dimension() {
let buf = vec![0u8; 12];
let err = RgbFrame::try_new(&buf, 0, 4, 12).unwrap_err();
assert!(matches!(err, RgbFrameError::ZeroDimension { .. }));
let err = RgbFrame::try_new(&buf, 4, 0, 12).unwrap_err();
assert!(matches!(err, RgbFrameError::ZeroDimension { .. }));
}
#[test]
fn rgb_try_new_rejects_stride_too_small() {
let buf = vec![0u8; 16];
let err = RgbFrame::try_new(&buf, 4, 2, 8).unwrap_err();
assert!(
matches!(
err,
RgbFrameError::StrideTooSmall {
min_stride: 12,
stride: 8
}
),
"expected StrideTooSmall, got {err:?}",
);
}
#[test]
fn rgb_try_new_rejects_plane_too_short() {
let buf = vec![0u8; 30];
let err = RgbFrame::try_new(&buf, 4, 4, 12).unwrap_err();
assert!(
matches!(
err,
RgbFrameError::PlaneTooShort {
expected: 48,
actual: 30
}
),
"expected PlaneTooShort, got {err:?}",
);
}
#[test]
fn rgb_try_new_rejects_width_overflow() {
let buf: Vec<u8> = Vec::new();
let err = RgbFrame::try_new(&buf, u32::MAX / 2, 1, u32::MAX).unwrap_err();
assert!(
matches!(err, RgbFrameError::WidthOverflow { .. }),
"expected WidthOverflow, got {err:?}",
);
}
#[test]
#[cfg(target_pointer_width = "32")]
fn rgb_try_new_rejects_geometry_overflow() {
let buf: Vec<u8> = Vec::new();
let err = RgbFrame::try_new(&buf, 1, u32::MAX, u32::MAX).unwrap_err();
assert!(
matches!(err, RgbFrameError::GeometryOverflow { .. }),
"expected GeometryOverflow, got {err:?}",
);
}
#[test]
fn rgb48_try_new_rejects_stride_too_small() {
let buf = vec![0u16; 16];
let err = Rgb48Frame::try_new(&buf, 4, 2, 8).unwrap_err();
assert!(
matches!(
err,
RgbFrameError::StrideTooSmall {
min_stride: 12,
stride: 8
}
),
"expected StrideTooSmall, got {err:?}",
);
}
#[test]
fn rgb48_try_new_rejects_plane_too_short() {
let buf = vec![0u16; 30];
let err = Rgb48Frame::try_new(&buf, 4, 4, 12).unwrap_err();
assert!(
matches!(
err,
RgbFrameError::PlaneTooShort {
expected: 48,
actual: 30
}
),
"expected PlaneTooShort, got {err:?}",
);
}
#[test]
fn extract_rgb48_count_zero_returns_empty() {
let buf = solid_color_frame_u16([0x8000, 0x4000, 0x2000], 4, 4);
let frame = Rgb48Frame::try_new(&buf, 4, 4, 12).expect("frame");
let dominants = extract_rgb48(frame, 0);
assert!(dominants.is_empty());
}
#[test]
fn mmcq_extract_into_array_buffer_recovers_red() {
let buf = solid_color_frame([200, 50, 50], 8, 8);
let frame = RgbFrame::try_new(&buf, 8, 8, 24).expect("frame");
let mut mmcq = Mmcq::new_boxed();
let mut out: [Option<Dominant>; 5] = [const { None }; 5];
mmcq.extract(frame.pixels(), 5, Algorithm::default(), &mut out);
let first = out
.iter()
.find_map(|o| o.as_ref())
.expect("expected at least one dominant");
assert!(
first.color().family().as_str().contains("red") || first.color().name().contains("red"),
"expected red-family dominant, got {:?}",
first.color().name(),
);
assert!(first.population() > 0);
}
#[test]
fn mmcq_reuse_resets_state_between_calls() {
let mut mmcq = Mmcq::new_boxed();
let red_buf = solid_color_frame([220, 20, 20], 8, 8);
let red_frame = RgbFrame::try_new(&red_buf, 8, 8, 24).expect("frame");
let mut red_out: [Option<Dominant>; 3] = [const { None }; 3];
mmcq.extract(red_frame.pixels(), 3, Algorithm::default(), &mut red_out);
let red_first = red_out
.iter()
.find_map(|o| o.as_ref())
.expect("red dominant");
assert!(
red_first.color().family().as_str().contains("red") || red_first.color().name().contains("red")
);
let blue_buf = solid_color_frame([20, 20, 220], 8, 8);
let blue_frame = RgbFrame::try_new(&blue_buf, 8, 8, 24).expect("frame");
let mut blue_out: [Option<Dominant>; 3] = [const { None }; 3];
mmcq.extract(blue_frame.pixels(), 3, Algorithm::default(), &mut blue_out);
let blue_first = blue_out
.iter()
.find_map(|o| o.as_ref())
.expect("blue dominant");
assert!(
blue_first.color().family().as_str().contains("blue")
|| blue_first.color().name().contains("blue"),
"second-call dominant should be blue, got {:?}",
blue_first.color().name(),
);
}
#[test]
fn stack_mmcq() {
static mut MMCQ: Mmcq = Mmcq::new();
const W: u32 = 16;
const H: u32 = 16;
const STRIDE: u32 = W * 3;
let mut buf = [0u8; (STRIDE * H) as usize];
for row in 0..H as usize {
let rgb = if row < 12 {
[220, 30, 30]
} else {
[30, 30, 220]
};
for col in 0..W as usize {
let off = row * STRIDE as usize + col * 3;
buf[off..off + 3].copy_from_slice(&rgb);
}
}
let frame = RgbFrame::try_new(&buf, W, H, STRIDE).expect("frame");
let mut out: [Option<Dominant>; 5] = [const { None }; 5];
#[allow(static_mut_refs)]
unsafe {
(*core::ptr::addr_of_mut!(MMCQ)).extract(frame.pixels(), 5, Algorithm::default(), &mut out);
}
for slot in out.iter().flatten() {
println!(
"rgb={:?} name={:?} family={:?} pop={} ({:.1}%)",
slot.rgb(),
slot.color().name(),
slot.color().family().as_str(),
slot.population(),
slot.percentage(),
);
}
}