use crate::{
BoundedSlice, ColorComponents, ImageRef, IndexedImage, IndexedImageCounts, LengthOutOfRange,
MAX_PIXELS, PaletteCounts,
};
use alloc::{boxed::Box, vec, vec::Vec};
use bitvec::vec::BitVec;
use bytemuck::Zeroable;
use core::ops::Range;
use palette::cast::{self, AsArrays as _};
use wide::{CmpEq as _, u8x32};
const RADIX: usize = u8::MAX as usize + 1;
#[inline]
fn chunk_range(chunks: &[u32], i: usize) -> Range<usize> {
(chunks[i] as usize)..(chunks[i + 1] as usize)
}
fn chunked_u8_counts<const CHUNKS: usize>(
pixels: &[[u8; 3]],
counts: &mut [[u32; RADIX + 1]; CHUNKS],
) {
let (chunks, remainder) = pixels.as_chunks::<CHUNKS>();
for chunk in chunks {
for (counts, &[r, ..]) in counts.iter_mut().zip(chunk) {
counts[usize::from(r)] += 1;
}
}
for &[r, ..] in remainder {
counts[0][usize::from(r)] += 1;
}
#[allow(clippy::expect_used)]
let (counts, partial_counts) = counts.split_first_mut().expect("CHUNKS != 0");
for i in 0..RADIX {
for partial in &*partial_counts {
counts[i] += partial[i];
}
}
}
#[inline]
fn prefix_sum<const N: usize>(counts: &mut [u32; N]) {
for i in 1..N {
counts[i] += counts[i - 1];
}
}
fn dedup_colors_u8_3_bounded<Color: ColorComponents<u8, 3>>(
slice: &BoundedSlice<Color>,
) -> Vec<Color> {
let mut bitmask: BitVec = BitVec::repeat(false, RADIX * RADIX * RADIX);
for color in slice.as_arrays() {
let [r, g, b] = color.map(usize::from);
bitmask.set(r * RADIX * RADIX + g * RADIX + b, true);
}
let mut colors = Vec::new();
for i in bitmask.iter_ones() {
let r = i / (RADIX * RADIX);
let g = i % (RADIX * RADIX) / RADIX;
let b = i % (RADIX * RADIX) % RADIX;
#[allow(clippy::cast_possible_truncation)]
let color = cast::from_array([r as u8, g as u8, b as u8]);
colors.push(color);
}
colors
}
#[inline]
pub fn dedup_colors_u8_3<Color: ColorComponents<u8, 3>>(
slice: &[Color],
) -> Result<Vec<Color>, LengthOutOfRange> {
LengthOutOfRange::check_u32(slice, 0, MAX_PIXELS)?;
Ok(BoundedSlice::new(slice)
.map(dedup_colors_u8_3_bounded)
.unwrap_or_default())
}
fn create_prefix_lookup(palette: &[[u8; 3]]) -> Box<[[u32; RADIX]; RADIX]> {
let mut lookup = bytemuck::zeroed_box::<[[u32; RADIX]; RADIX]>();
let mut i = 0;
for chunk in palette.chunk_by(|&[r1, g1, _], &[r2, g2, _]| r1 == r2 && g1 == g2) {
let [r, g, _] = chunk[0];
lookup[usize::from(r)][usize::from(g)] = i;
#[allow(clippy::cast_possible_truncation)]
{
i += chunk.len() as u32;
}
}
lookup
}
#[inline]
fn find_index([r, g, b]: [u8; 3], lookup: &[[u32; RADIX]; RADIX], blue: &[u8]) -> u32 {
let index = lookup[usize::from(r)][usize::from(g)];
let i = index as usize;
#[allow(clippy::expect_used)]
let slice: &[u8; RADIX] = blue[i..(i + RADIX)]
.try_into()
.expect("slice of exactly length RADIX");
let slice: &[[u8; 32]; RADIX / 32] = bytemuck::cast_ref(slice);
let b = u8x32::splat(b);
for (chunk_i, &chunk) in slice.iter().enumerate() {
let mask = u8x32::new(chunk).simd_eq(b).to_bitmask();
if mask != 0 {
#[cfg(target_endian = "big")]
let mask = mask.swap_bytes();
#[allow(clippy::cast_possible_truncation)]
return chunk_i as u32 * 32 + mask.trailing_zeros() + index;
}
}
#[allow(clippy::unreachable)]
{
unreachable!("target byte should exist")
}
}
fn indices_from_palette_u8_3<Color: ColorComponents<u8, 3>>(
palette: &[Color],
pixels: &[Color],
) -> Vec<u32> {
let lookup = create_prefix_lookup(palette.as_arrays());
let lookup = lookup.as_ref();
let blue = palette
.as_arrays()
.iter()
.map(|&[_, _, b]| b)
.chain(core::iter::repeat_n(0, RADIX))
.collect::<Vec<_>>();
pixels
.as_arrays()
.iter()
.map(|&rgb| find_index(rgb, lookup, &blue))
.collect()
}
#[must_use]
pub fn dedup_image_u8_3<Color: ColorComponents<u8, 3>>(
image: ImageRef<'_, Color>,
) -> IndexedImage<Color, u32> {
if image.is_empty() {
return IndexedImage::default();
}
let pixels = image.as_slice();
let palette = dedup_colors_u8_3_bounded(BoundedSlice::new_unchecked(pixels));
let indices = indices_from_palette_u8_3(&palette, pixels);
IndexedImage::new_unchecked(image.width(), image.height(), palette, indices)
}
pub(crate) fn dedup_colors_u8_3_counts_bounded<Color: ColorComponents<u8, 3>>(
slice: &BoundedSlice<Color>,
) -> PaletteCounts<Color> {
let total_count = slice.length();
let mut colors = Vec::new();
let mut counts = Vec::new();
let mut green_blue = vec![[0; 2]; slice.len()];
let mut lower_counts = bytemuck::zeroed_box::<[[u32; RADIX]; RADIX]>();
let mut bitmask: BitVec = BitVec::repeat(false, RADIX * RADIX);
let mut red_prefix = bytemuck::zeroed_box();
chunked_u8_counts::<4>(slice.as_arrays(), &mut red_prefix);
let red_prefix = &mut red_prefix[0];
prefix_sum(red_prefix);
for &[r, g, b] in slice.as_arrays() {
let r = usize::from(r);
let j = red_prefix[r] - 1;
green_blue[j as usize] = [g, b];
red_prefix[r] = j;
}
red_prefix[RADIX] = total_count;
for r in 0..RADIX {
let chunk = chunk_range(red_prefix, r);
if !chunk.is_empty() {
let green_blue = &green_blue[chunk.clone()];
if chunk.len() < RADIX * RADIX / 4 {
for gb in green_blue {
let [g, b] = gb.map(usize::from);
lower_counts[g][b] += 1;
bitmask.set(g * RADIX + b, true);
}
for i in bitmask.iter_ones() {
let g = i / RADIX;
let b = i % RADIX;
#[allow(clippy::cast_possible_truncation)]
let color = cast::from_array([r as u8, g as u8, b as u8]);
colors.push(color);
counts.push(lower_counts[g][b]);
lower_counts[g][b] = 0;
}
bitmask.fill(false);
} else {
for &[g, b] in green_blue {
lower_counts[usize::from(g)][usize::from(b)] += 1;
}
for (g, count) in lower_counts.iter().enumerate() {
for (b, &count) in count.iter().enumerate() {
if count > 0 {
#[allow(clippy::cast_possible_truncation)]
let color = cast::from_array([r as u8, g as u8, b as u8]);
colors.push(color);
counts.push(count);
}
}
}
*lower_counts = Zeroable::zeroed();
}
}
}
PaletteCounts::new_unchecked(colors, counts, total_count)
}
#[inline]
pub fn dedup_colors_u8_3_counts<Color: ColorComponents<u8, 3>>(
slice: &[Color],
) -> Result<PaletteCounts<Color>, LengthOutOfRange> {
LengthOutOfRange::check_u32(slice, 0, MAX_PIXELS)?;
Ok(BoundedSlice::new(slice)
.map(dedup_colors_u8_3_counts_bounded)
.unwrap_or_default())
}
#[must_use]
pub fn dedup_image_u8_3_counts<Color: ColorComponents<u8, 3>>(
image: ImageRef<'_, Color>,
) -> IndexedImageCounts<Color, u32> {
if image.is_empty() {
return IndexedImageCounts::default();
}
let pixels = image.as_slice();
let palette_counts = dedup_colors_u8_3_counts_bounded(BoundedSlice::new_unchecked(pixels));
let indices = indices_from_palette_u8_3(palette_counts.palette(), pixels);
IndexedImageCounts::from_palette_counts_unchecked(
image.width(),
image.height(),
palette_counts,
indices,
)
}
#[cfg(feature = "threads")]
mod parallel {
use super::{RADIX, chunk_range, chunked_u8_counts};
use crate::{
BoundedSlice, ColorComponents, ImageRef, IndexedImage, IndexedImageCounts,
LengthOutOfRange, MAX_PIXELS, PaletteCounts,
dedup::{create_prefix_lookup, find_index},
};
use bitvec::vec::BitVec;
use palette::cast::{self, AsArrays as _};
use rayon::prelude::*;
#[allow(unsafe_code)]
mod sync_unsafe {
#[cfg(test)]
use core::sync::atomic::{AtomicBool, Ordering};
use core::{cell::UnsafeCell, ops::Range};
pub struct SyncUnsafeSlice<'a, T> {
cell: UnsafeCell<&'a mut [T]>,
#[cfg(test)]
written: Vec<AtomicBool>,
}
unsafe impl<T: Send + Sync> Send for SyncUnsafeSlice<'_, T> {}
unsafe impl<T: Send + Sync> Sync for SyncUnsafeSlice<'_, T> {}
impl<'a, T> SyncUnsafeSlice<'a, T> {
pub fn new(slice: &'a mut [T]) -> Self {
Self {
#[cfg(test)]
written: slice.iter().map(|_| AtomicBool::new(false)).collect(),
cell: UnsafeCell::new(slice),
}
}
unsafe fn get(&self) -> &'a mut [T] {
unsafe { *self.cell.get() }
}
}
impl<T: Copy> SyncUnsafeSlice<'_, T> {
#[inline]
pub unsafe fn write_slice(&self, range: Range<usize>, slice: &[T]) {
#[cfg(test)]
{
assert!(
!self.written[range.clone()]
.iter()
.any(|b| b.swap(true, Ordering::SeqCst))
);
}
(unsafe { self.get() })[range].copy_from_slice(slice);
}
}
}
use sync_unsafe::SyncUnsafeSlice;
fn u8_counts(slice: &[[u8; 3]], chunk_size: usize) -> Vec<[u32; RADIX + 1]> {
let mut counts = slice
.par_chunks(chunk_size)
.map(|chunk| {
let mut counts = bytemuck::zeroed_box();
chunked_u8_counts::<4>(chunk, &mut counts);
counts[0]
})
.collect::<Vec<_>>();
let mut carry = 0;
for i in 0..RADIX {
counts[0][i] += carry;
for j in 1..counts.len() {
counts[j][i] += counts[j - 1][i];
}
carry = counts[counts.len() - 1][i];
}
debug_assert_eq!(carry as usize, slice.len());
counts
}
fn sorted_green_blue(
slice: &[[u8; 3]],
chunk_size: usize,
red_prefixes: Vec<[u32; RADIX + 1]>,
) -> Vec<[u8; 2]> {
let mut green_blue = vec![[0; 2]; slice.len()];
let gb = SyncUnsafeSlice::new(&mut green_blue);
slice
.par_chunks(chunk_size)
.zip(red_prefixes)
.for_each(|(chunk, mut red_prefix)| {
const BUF_LEN: u8 = 128;
let mut buffer = bytemuck::zeroed_box::<[[[u8; 2]; BUF_LEN as usize]; RADIX]>();
let mut lengths = [0u8; RADIX];
#[allow(unsafe_code)]
for &[r, g, b] in chunk {
let r = usize::from(r);
let len = lengths[r];
let len = if len >= BUF_LEN {
let i = red_prefix[r] - u32::from(BUF_LEN);
let j = i as usize;
unsafe {
gb.write_slice(j..(j + usize::from(BUF_LEN)), &buffer[r]);
}
red_prefix[r] = i;
0
} else {
len
};
buffer[r][usize::from(len)] = [g, b];
lengths[r] = len + 1;
}
#[allow(unsafe_code)]
for (r, buf) in buffer.iter().enumerate() {
let len = lengths[r];
let i = red_prefix[r] - u32::from(len);
let len = usize::from(len);
let j = i as usize;
unsafe {
gb.write_slice(j..(j + len), &buf[..len]);
}
}
});
green_blue
}
#[inline]
fn dedup_colors_u8_3_bounded_par<Color: ColorComponents<u8, 3>>(
slice: &BoundedSlice<Color>,
) -> Vec<Color> {
let chunk_size = slice.len().div_ceil(rayon::current_num_threads());
let red_prefixes = u8_counts(slice.as_arrays(), chunk_size);
let red_prefix = {
let mut prefix = [0; RADIX + 1];
prefix[1..].copy_from_slice(&red_prefixes[red_prefixes.len() - 1][..RADIX]);
prefix
};
let green_blue = sorted_green_blue(slice.as_arrays(), chunk_size, red_prefixes);
(0..RADIX)
.into_par_iter()
.map(|r| {
let chunk = chunk_range(&red_prefix, r);
let mut colors = Vec::new();
if !chunk.is_empty() {
let green_blue = &green_blue[chunk];
let mut bitmask: BitVec = BitVec::repeat(false, RADIX * RADIX);
for gb in green_blue {
let [g, b] = gb.map(usize::from);
bitmask.set(g * RADIX + b, true);
}
for i in bitmask.iter_ones() {
let g = i / RADIX;
let b = i % RADIX;
#[allow(clippy::cast_possible_truncation)]
let color = cast::from_array([r as u8, g as u8, b as u8]);
colors.push(color);
}
}
colors
})
.collect::<Vec<_>>()
.concat()
}
#[inline]
pub fn dedup_colors_u8_3_par<Color: ColorComponents<u8, 3>>(
slice: &[Color],
) -> Result<Vec<Color>, LengthOutOfRange> {
LengthOutOfRange::check_u32(slice, 0, MAX_PIXELS)?;
Ok(BoundedSlice::new(slice)
.map(dedup_colors_u8_3_bounded_par)
.unwrap_or_default())
}
fn indices_from_palette_u8_3_par<Color: ColorComponents<u8, 3>>(
palette: &[Color],
pixels: &[Color],
) -> Vec<u32> {
let lookup = create_prefix_lookup(palette.as_arrays());
let lookup = lookup.as_ref();
let blue = palette
.as_arrays()
.iter()
.map(|&[_, _, b]| b)
.chain(core::iter::repeat_n(0, RADIX))
.collect::<Vec<_>>();
let blue = blue.as_slice();
pixels
.as_arrays()
.into_par_iter()
.map(|&rgb| find_index(rgb, lookup, blue))
.collect()
}
#[must_use]
pub fn dedup_image_u8_3_par<Color: ColorComponents<u8, 3>>(
image: ImageRef<'_, Color>,
) -> IndexedImage<Color, u32> {
if image.is_empty() {
return IndexedImage::default();
}
let pixels = image.as_slice();
let palette = dedup_colors_u8_3_bounded_par(BoundedSlice::new_unchecked(pixels));
let indices = indices_from_palette_u8_3_par(&palette, pixels);
IndexedImage::new_unchecked(image.width(), image.height(), palette, indices)
}
pub(crate) fn dedup_colors_u8_3_counts_bounded_par<Color: ColorComponents<u8, 3>>(
slice: &BoundedSlice<Color>,
) -> PaletteCounts<Color> {
let chunk_size = slice.len().div_ceil(rayon::current_num_threads());
let red_prefixes = u8_counts(slice.as_arrays(), chunk_size);
let red_prefix = {
let mut prefix = [0; RADIX + 1];
prefix[1..].copy_from_slice(&red_prefixes[red_prefixes.len() - 1][..RADIX]);
prefix
};
let green_blue = sorted_green_blue(slice.as_arrays(), chunk_size, red_prefixes);
let (colors, counts): (Vec<_>, Vec<_>) = (0..RADIX)
.into_par_iter()
.map(|r| {
let chunk = chunk_range(&red_prefix, r);
let mut colors = Vec::new();
let mut counts = Vec::new();
if !chunk.is_empty() {
let green_blue = &green_blue[chunk.clone()];
let mut lower_counts = bytemuck::zeroed_box::<[[u32; RADIX]; RADIX]>();
if chunk.len() < RADIX * RADIX / 4 {
let mut bitmask: BitVec = BitVec::repeat(false, RADIX * RADIX);
for gb in green_blue {
let [g, b] = gb.map(usize::from);
lower_counts[g][b] += 1;
bitmask.set(g * RADIX + b, true);
}
for i in bitmask.iter_ones() {
let g = i / RADIX;
let b = i % RADIX;
#[allow(clippy::cast_possible_truncation)]
let color = cast::from_array([r as u8, g as u8, b as u8]);
colors.push(color);
counts.push(lower_counts[g][b]);
}
} else {
for &[g, b] in green_blue {
lower_counts[usize::from(g)][usize::from(b)] += 1;
}
for (g, count) in lower_counts.iter().enumerate() {
for (b, &count) in count.iter().enumerate() {
if count > 0 {
#[allow(clippy::cast_possible_truncation)]
let color = cast::from_array([r as u8, g as u8, b as u8]);
colors.push(color);
counts.push(count);
}
}
}
}
}
(colors, counts)
})
.unzip();
PaletteCounts::new_unchecked(colors.concat(), counts.concat(), slice.length())
}
#[inline]
pub fn dedup_colors_u8_3_counts_par<Color: ColorComponents<u8, 3>>(
slice: &[Color],
) -> Result<PaletteCounts<Color>, LengthOutOfRange> {
LengthOutOfRange::check_u32(slice, 0, MAX_PIXELS)?;
Ok(BoundedSlice::new(slice)
.map(dedup_colors_u8_3_counts_bounded_par)
.unwrap_or_default())
}
#[must_use]
pub fn dedup_image_u8_3_counts_par<Color: ColorComponents<u8, 3>>(
image: ImageRef<'_, Color>,
) -> IndexedImageCounts<Color, u32> {
if image.is_empty() {
return IndexedImageCounts::default();
}
let pixels = image.as_slice();
let palette_counts =
dedup_colors_u8_3_counts_bounded_par(BoundedSlice::new_unchecked(pixels));
let indices = indices_from_palette_u8_3_par(palette_counts.palette(), pixels);
IndexedImageCounts::from_palette_counts_unchecked(
image.width(),
image.height(),
palette_counts,
indices,
)
}
}
#[cfg(feature = "threads")]
pub use parallel::*;
#[cfg(test)]
mod tests {
use super::*;
use crate::{Image, tests::*};
use palette::Srgb;
fn assert_valid_palette_counts(palette_counts: &PaletteCounts<Srgb<u8>>, colors: &[Srgb<u8>]) {
assert_eq!(palette_counts.total_count() as usize, colors.len());
let palette = palette_counts.palette();
for i in 1..palette.len() {
assert!(palette[i - 1].into_components() < palette[i].into_components());
}
}
fn assert_valid_indexed_counts(image: &IndexedImageCounts<Srgb<u8>, u32>, colors: &[Srgb<u8>]) {
assert_eq!(image.total_count() as usize, colors.len());
let palette = image.palette();
let mut counts = vec![0; palette.len()];
for (&i, &color) in image.indices().iter().zip(colors) {
assert_eq!(palette[i as usize], color);
counts[i as usize] += 1;
}
assert_eq!(&counts, image.counts());
for i in 1..palette.len() {
assert!(palette[i - 1].into_components() < palette[i].into_components());
}
}
#[allow(clippy::cast_possible_truncation)]
fn image_1d<T: AsRef<[U]>, U>(container: T) -> Image<U, T> {
Image::new_unchecked(container.as_ref().len() as u32, 1, container)
}
#[allow(clippy::too_many_lines)]
fn add_duplicate_color_with_data(colors: Vec<Srgb<u8>>) {
#[allow(clippy::cast_possible_truncation)]
fn index_of(colors: &[Srgb<u8>], color: Srgb<u8>) -> usize {
colors.iter().position(|&c| c == color).unwrap()
}
let with_duplicate = {
let mut pixels = colors;
let len = pixels.len();
pixels.as_mut_slice()[len - 1] = pixels[0];
image_1d(pixels)
};
let duplicate = with_duplicate.as_slice()[0];
let without_duplicate =
image_1d(&with_duplicate.as_slice()[..with_duplicate.as_slice().len() - 1]);
let expected = dedup_colors_u8_3(without_duplicate.as_slice()).unwrap();
let actual = dedup_colors_u8_3(with_duplicate.as_slice()).unwrap();
assert_eq!(actual, expected);
let expected = {
let image = dedup_image_u8_3(without_duplicate.as_ref()).into_indexed_image_counts();
let (width, height) = image.dimensions();
let (image, mut counts) = image.into_parts();
let (palette, mut indices) = image.into_parts();
let i = index_of(&palette, duplicate);
counts[i] += 1;
#[allow(clippy::cast_possible_truncation)]
indices.push(i as u32);
IndexedImageCounts::new_unchecked(
width.checked_add(1).unwrap(),
height,
palette,
counts,
indices,
)
};
let actual = dedup_image_u8_3(with_duplicate.as_ref()).into_indexed_image_counts();
assert_valid_indexed_counts(&actual, with_duplicate.as_slice());
assert_eq!(actual, expected);
let expected = {
let palette_counts = dedup_colors_u8_3_counts(without_duplicate.as_slice()).unwrap();
let total_count = palette_counts.total_count();
let (palette, mut counts) = palette_counts.into_parts();
let i = index_of(&palette, duplicate);
counts[i] += 1;
PaletteCounts::new_unchecked(palette, counts, total_count.checked_add(1).unwrap())
};
let actual = dedup_colors_u8_3_counts(with_duplicate.as_slice()).unwrap();
assert_valid_palette_counts(&actual, with_duplicate.as_slice());
assert_eq!(actual, expected);
let expected = {
let image = dedup_image_u8_3_counts(without_duplicate.as_ref());
let (width, height) = image.dimensions();
let (image, mut counts) = image.into_parts();
let (palette, mut indices) = image.into_parts();
let i = index_of(&palette, duplicate);
counts[i] += 1;
#[allow(clippy::cast_possible_truncation)]
indices.push(i as u32);
IndexedImageCounts::new_unchecked(
width.checked_add(1).unwrap(),
height,
palette,
counts,
indices,
)
};
let actual = dedup_image_u8_3_counts(with_duplicate.as_ref());
assert_valid_indexed_counts(&actual, with_duplicate.as_slice());
assert_eq!(actual, expected);
#[cfg(feature = "threads")]
{
let expected = dedup_colors_u8_3_par(without_duplicate.as_slice()).unwrap();
let actual = dedup_colors_u8_3_par(with_duplicate.as_slice()).unwrap();
assert_eq!(actual, expected);
let expected = {
let image =
dedup_image_u8_3_par(without_duplicate.as_ref()).into_indexed_image_counts();
let (width, height) = image.dimensions();
let (image, mut counts) = image.into_parts();
let (palette, mut indices) = image.into_parts();
let i = index_of(&palette, duplicate);
counts[i] += 1;
#[allow(clippy::cast_possible_truncation)]
indices.push(i as u32);
IndexedImageCounts::new_unchecked(
width.checked_add(1).unwrap(),
height,
palette,
counts,
indices,
)
};
let actual = dedup_image_u8_3_par(with_duplicate.as_ref()).into_indexed_image_counts();
assert_valid_indexed_counts(&actual, with_duplicate.as_slice());
assert_eq!(actual, expected);
let expected = {
let palette_counts =
dedup_colors_u8_3_counts_par(without_duplicate.as_slice()).unwrap();
let total_count = palette_counts.total_count();
let (palette, mut counts) = palette_counts.into_parts();
let i = index_of(&palette, duplicate);
counts[i] += 1;
PaletteCounts::new_unchecked(palette, counts, total_count.checked_add(1).unwrap())
};
let actual = dedup_colors_u8_3_counts_par(with_duplicate.as_slice()).unwrap();
assert_valid_palette_counts(&actual, with_duplicate.as_slice());
assert_eq!(actual, expected);
let expected = {
let image = dedup_image_u8_3_counts_par(without_duplicate.as_ref());
let (width, height) = image.dimensions();
let (image, mut counts) = image.into_parts();
let (palette, mut indices) = image.into_parts();
let i = index_of(&palette, duplicate);
counts[i] += 1;
#[allow(clippy::cast_possible_truncation)]
indices.push(i as u32);
IndexedImageCounts::new_unchecked(
width.checked_add(1).unwrap(),
height,
palette,
counts,
indices,
)
};
let actual = dedup_image_u8_3_counts_par(with_duplicate.as_ref());
assert_valid_indexed_counts(&actual, with_duplicate.as_slice());
assert_eq!(actual, expected);
}
}
#[test]
fn add_duplicate_color() {
let colors = test_data_1024();
add_duplicate_color_with_data(colors.clone());
let colors = [colors.as_slice(); 256].concat();
add_duplicate_color_with_data(colors);
}
fn reordered_input_with_data(colors: Vec<Srgb<u8>>) {
let image = image_1d(colors);
let reordered = {
let mut reordered = image.clone();
reordered.as_mut_slice().rotate_right(21);
reordered
};
let expected = dedup_colors_u8_3(image.as_slice()).unwrap();
let actual = dedup_colors_u8_3(reordered.as_slice()).unwrap();
assert_eq!(actual, expected);
let expected = dedup_image_u8_3(image.as_ref()).into_indexed_image_counts();
let actual = dedup_image_u8_3(reordered.as_ref()).into_indexed_image_counts();
assert_valid_indexed_counts(&actual, reordered.as_slice());
assert_eq!(actual.palette(), expected.palette());
assert_eq!(actual.counts(), expected.counts());
assert_eq!(actual.indices().len(), expected.indices().len());
let expected = dedup_colors_u8_3_counts(image.as_slice()).unwrap();
let actual = dedup_colors_u8_3_counts(reordered.as_slice()).unwrap();
assert_valid_palette_counts(&actual, reordered.as_slice());
assert_eq!(actual, expected);
let expected = dedup_image_u8_3_counts(image.as_ref());
let actual = dedup_image_u8_3_counts(reordered.as_ref());
assert_valid_indexed_counts(&actual, reordered.as_slice());
assert_eq!(actual.palette(), expected.palette());
assert_eq!(actual.counts(), expected.counts());
assert_eq!(actual.indices().len(), expected.indices().len());
#[cfg(feature = "threads")]
{
let expected = dedup_colors_u8_3_par(image.as_slice()).unwrap();
let actual = dedup_colors_u8_3_par(reordered.as_slice()).unwrap();
assert_eq!(actual, expected);
let expected = dedup_image_u8_3_par(image.as_ref()).into_indexed_image_counts();
let actual = dedup_image_u8_3_par(reordered.as_ref()).into_indexed_image_counts();
assert_valid_indexed_counts(&actual, reordered.as_slice());
assert_eq!(actual.palette(), expected.palette());
assert_eq!(actual.counts(), expected.counts());
assert_eq!(actual.indices().len(), expected.indices().len());
let expected = dedup_colors_u8_3_counts_par(image.as_slice()).unwrap();
let actual = dedup_colors_u8_3_counts_par(reordered.as_slice()).unwrap();
assert_valid_palette_counts(&actual, reordered.as_slice());
assert_eq!(actual, expected);
let expected = dedup_image_u8_3_counts_par(image.as_ref());
let actual = dedup_image_u8_3_counts_par(reordered.as_ref());
assert_valid_indexed_counts(&actual, reordered.as_slice());
assert_eq!(actual.palette(), expected.palette());
assert_eq!(actual.counts(), expected.counts());
assert_eq!(actual.indices().len(), expected.indices().len());
}
}
#[test]
fn reordered_input() {
let colors = test_data_1024();
reordered_input_with_data(colors.clone());
let colors = [colors.as_slice(); 256].concat();
reordered_input_with_data(colors);
}
#[cfg(feature = "threads")]
fn single_and_multi_threaded_match_with_data(colors: Vec<Srgb<u8>>) {
let image = image_1d(colors);
let single = dedup_colors_u8_3(image.as_slice()).unwrap();
let par = dedup_colors_u8_3_par(image.as_slice()).unwrap();
assert_eq!(single, par);
let single = dedup_image_u8_3(image.as_ref()).into_indexed_image_counts();
let par = dedup_image_u8_3_par(image.as_ref()).into_indexed_image_counts();
assert_valid_indexed_counts(&single, image.as_slice());
assert_eq!(single, par);
let single = dedup_colors_u8_3_counts(image.as_slice()).unwrap();
let par = dedup_colors_u8_3_counts_par(image.as_slice()).unwrap();
assert_valid_palette_counts(&single, image.as_slice());
assert_eq!(single, par);
let single = dedup_image_u8_3_counts(image.as_ref());
let par = dedup_image_u8_3_counts_par(image.as_ref());
assert_valid_indexed_counts(&single, image.as_slice());
assert_eq!(single, par);
}
#[test]
#[cfg(feature = "threads")]
fn single_and_multi_threaded_match() {
let colors = test_data_1024();
single_and_multi_threaded_match_with_data(colors.clone());
let colors = [colors.as_slice(); 256].concat();
single_and_multi_threaded_match_with_data(colors);
}
}