#[cfg(feature = "kmeans")]
use crate::kmeans::Kmeans;
use crate::{
BoundedSlice, ImageBuf, ImageRef, IndexedColorMap, IndexedImage, IndexedImageCounts,
LengthOutOfRange, PaletteBuf, PaletteSize, QuantizeMethod,
color_map::{NearestNeighborColorMap, PaletteSubstitution},
color_space::{oklab_to_srgb8, srgb8_to_oklab},
dedup,
dither::FloydSteinberg,
wu::{BinnerF32x3, WuF32x3},
};
#[cfg(feature = "threads")]
use crate::{color_map::NearestNeighborParallelColorMap, color_space::srgb8_to_oklab_par};
use alloc::vec;
use bytemuck::Zeroable;
use palette::{Oklab, Srgb};
#[must_use]
#[derive(Debug, Clone, PartialEq)]
pub struct Pipeline {
k: PaletteSize,
quantize_method: QuantizeMethod,
ditherer: Option<FloydSteinberg>,
dedup: Option<bool>,
#[cfg(feature = "threads")]
parallel: bool,
}
impl Pipeline {
pub fn new() -> Self {
Self {
k: PaletteSize::MAX,
quantize_method: QuantizeMethod::Wu,
ditherer: Some(FloydSteinberg::new()),
dedup: None,
#[cfg(feature = "threads")]
parallel: false,
}
}
#[inline]
pub fn palette_size(mut self, size: PaletteSize) -> Self {
self.k = size;
self
}
#[inline]
pub fn quantize_method(mut self, quantize_method: impl Into<QuantizeMethod>) -> Self {
self.quantize_method = quantize_method.into();
self
}
#[cfg(feature = "threads")]
#[inline]
pub fn parallel(mut self, parallel: bool) -> Self {
self.parallel = parallel;
self
}
#[inline]
pub fn ditherer(mut self, ditherer: impl Into<Option<FloydSteinberg>>) -> Self {
self.ditherer = ditherer.into();
self
}
#[inline]
pub fn dedup(mut self, dedup: impl Into<Option<bool>>) -> Self {
self.dedup = dedup.into();
self
}
#[inline]
pub fn input_slice(
self,
colors: &[Srgb<u8>],
) -> Result<PipelineWithSliceInput<'_>, LengthOutOfRange> {
let colors = BoundedSlice::new(colors)?;
Ok(PipelineWithSliceInput { options: self, colors })
}
#[inline]
pub fn input_image(self, image: ImageRef<'_, Srgb<u8>>) -> PipelineWithImageRefInput<'_> {
PipelineWithImageRefInput { options: self, image }
}
}
impl Default for Pipeline {
#[inline]
fn default() -> Self {
Self::new()
}
}
#[must_use]
#[derive(Debug, Clone, PartialEq)]
pub struct PipelineWithSliceInput<'a> {
options: Pipeline,
colors: &'a BoundedSlice<Srgb<u8>>,
}
impl PipelineWithSliceInput<'_> {
#[allow(clippy::missing_panics_doc)]
#[must_use]
pub fn output_oklab_palette(self) -> PaletteBuf<Oklab> {
let Self { options, colors } = self;
let Pipeline {
k,
quantize_method,
dedup,
#[cfg(feature = "threads")]
parallel,
..
} = options;
let binner = BinnerF32x3::oklab_from_srgb8();
#[cfg(feature = "threads")]
if parallel {
return match quantize_method {
QuantizeMethod::CustomPalette(palette) => palette.into_oklab(),
QuantizeMethod::Wu =>
{
#[allow(clippy::expect_used)]
if dedup.unwrap_or(colors.len() >= 2048 * 2048) {
let palette_counts = dedup::dedup_colors_u8_3_counts_bounded_par(colors)
.map(|palette| srgb8_to_oklab_par(&palette));
WuF32x3::run_palette_counts_par(&palette_counts, binner)
.expect("deduping a non-empty slice to not result in an empty slice")
.palette(k)
} else {
let colors = srgb8_to_oklab_par(colors);
WuF32x3::run_slice_bounded_par(&colors, binner).palette(k)
}
}
#[cfg(feature = "kmeans")]
QuantizeMethod::Kmeans(options) => {
if dedup.unwrap_or(colors.len() >= 2048 * 2048) {
let image = ImageRef::new_unchecked(colors.length(), 1, colors);
let image = dedup::dedup_image_u8_3_counts_par(image)
.map(|palette| srgb8_to_oklab_par(&palette));
#[allow(clippy::expect_used)]
let centroids = WuF32x3::run_indexed_image_counts_par(&image, binner)
.expect("deduping a non-empty image to not result in an empty image")
.palette(k);
Kmeans::run_indexed_image_par(image.as_ref(), centroids, options)
.into_palette()
} else {
let colors = srgb8_to_oklab_par(colors);
let centroids = WuF32x3::run_slice_bounded_par(&colors, binner).palette(k);
Kmeans::run_slice_par_unchecked(&colors, centroids, options).into_palette()
}
}
};
}
match quantize_method {
QuantizeMethod::CustomPalette(palette) => palette.into_oklab(),
QuantizeMethod::Wu =>
{
#[allow(clippy::expect_used)]
if dedup.unwrap_or(colors.len() >= 2048 * 2048) {
let palette_counts = dedup::dedup_colors_u8_3_counts_bounded(colors)
.map(|palette| srgb8_to_oklab(&palette));
WuF32x3::run_palette_counts(&palette_counts, binner)
.expect("deduping a non-empty slice to not result in an empty slice")
.palette(k)
} else {
let colors = srgb8_to_oklab(colors);
let colors = BoundedSlice::new_unchecked(&colors);
WuF32x3::run_slice_bounded(colors, binner).palette(k)
}
}
#[cfg(feature = "kmeans")]
QuantizeMethod::Kmeans(options) => {
if dedup.unwrap_or(colors.len() >= 2048 * 2048) {
let image = ImageRef::new_unchecked(colors.length(), 1, colors);
let image = dedup::dedup_image_u8_3_counts(image)
.map(|palette| srgb8_to_oklab(&palette));
#[allow(clippy::expect_used)]
let centroids = WuF32x3::run_indexed_image_counts(&image, binner)
.expect("deduping a non-empty image to not result in an empty image")
.palette(k);
Kmeans::run_indexed_image(image.as_ref(), centroids, options).into_palette()
} else {
let colors = srgb8_to_oklab(colors);
let colors = BoundedSlice::new_unchecked(&colors);
let centroids = WuF32x3::run_slice_bounded(colors, binner).palette(k);
Kmeans::run_slice_bounded(colors, centroids, options).into_palette()
}
}
}
}
#[must_use]
pub fn output_srgb8_palette(self) -> PaletteBuf<Srgb<u8>> {
PaletteBuf::from_mapping(&self.output_oklab_palette(), oklab_to_srgb8)
}
#[allow(clippy::missing_panics_doc)]
#[must_use]
pub fn output_oklab_palette_and_counts(self) -> (PaletteBuf<Oklab>, PaletteBuf<u32>) {
let Self { options, colors } = self;
let Pipeline {
k,
quantize_method,
dedup,
#[cfg(feature = "threads")]
parallel,
..
} = options;
let binner = BinnerF32x3::oklab_from_srgb8();
#[cfg(feature = "threads")]
if parallel {
return match quantize_method {
QuantizeMethod::CustomPalette(palette) => {
let palette = palette.into_oklab();
let counts = PaletteBuf::new_unchecked(vec![0; palette.len()]);
(palette, counts)
}
QuantizeMethod::Wu =>
{
#[allow(clippy::expect_used)]
if dedup.unwrap_or(colors.len() >= 2048 * 2048) {
let palette_counts = dedup::dedup_colors_u8_3_counts_bounded_par(colors)
.map(|palette| srgb8_to_oklab_par(&palette));
WuF32x3::run_palette_counts_par(&palette_counts, binner)
.expect("deduping a non-empty slice to not result in an empty slice")
.palette_and_counts(k)
} else {
let colors = srgb8_to_oklab_par(colors);
WuF32x3::run_slice_bounded_par(&colors, binner).palette_and_counts(k)
}
}
#[cfg(feature = "kmeans")]
QuantizeMethod::Kmeans(options) => {
if dedup.unwrap_or(colors.len() >= 2048 * 2048) {
let image = ImageRef::new_unchecked(colors.length(), 1, colors);
let image = dedup::dedup_image_u8_3_counts_par(image)
.map(|palette| srgb8_to_oklab_par(&palette));
#[allow(clippy::expect_used)]
let centroids = WuF32x3::run_indexed_image_counts_par(&image, binner)
.expect("deduping a non-empty image to not result in an empty image")
.palette(k);
Kmeans::run_indexed_image_par(image.as_ref(), centroids, options)
.into_palette_and_counts()
} else {
let colors = srgb8_to_oklab_par(colors);
let centroids = WuF32x3::run_slice_bounded_par(&colors, binner).palette(k);
Kmeans::run_slice_par_unchecked(&colors, centroids, options)
.into_palette_and_counts()
}
}
};
}
match quantize_method {
QuantizeMethod::CustomPalette(palette) => {
let palette = palette.into_oklab();
let counts = PaletteBuf::new_unchecked(vec![0; palette.len()]);
(palette, counts)
}
QuantizeMethod::Wu =>
{
#[allow(clippy::expect_used)]
if dedup.unwrap_or(colors.len() >= 2048 * 2048) {
let palette_counts = dedup::dedup_colors_u8_3_counts_bounded(colors)
.map(|palette| srgb8_to_oklab(&palette));
WuF32x3::run_palette_counts(&palette_counts, binner)
.expect("deduping a non-empty slice to not result in an empty slice")
.palette_and_counts(k)
} else {
let colors = srgb8_to_oklab(colors);
let colors = BoundedSlice::new_unchecked(&colors);
WuF32x3::run_slice_bounded(colors, binner).palette_and_counts(k)
}
}
#[cfg(feature = "kmeans")]
QuantizeMethod::Kmeans(options) => {
if dedup.unwrap_or(colors.len() >= 2048 * 2048) {
let image = ImageRef::new_unchecked(colors.length(), 1, colors);
let image = dedup::dedup_image_u8_3_counts(image)
.map(|palette| srgb8_to_oklab(&palette));
#[allow(clippy::expect_used)]
let centroids = WuF32x3::run_indexed_image_counts(&image, binner)
.expect("deduping a non-empty image to not result in an empty image")
.palette(k);
Kmeans::run_indexed_image(image.as_ref(), centroids, options)
.into_palette_and_counts()
} else {
let colors = srgb8_to_oklab(colors);
let colors = BoundedSlice::new_unchecked(&colors);
let centroids = WuF32x3::run_slice_bounded(colors, binner).palette(k);
Kmeans::run_slice_bounded(colors, centroids, options).into_palette_and_counts()
}
}
}
}
#[must_use]
pub fn output_srgb8_palette_and_counts(self) -> (PaletteBuf<Srgb<u8>>, PaletteBuf<u32>) {
let (palette, counts) = self.output_oklab_palette_and_counts();
let palette = PaletteBuf::from_mapping(&palette, oklab_to_srgb8);
(palette, counts)
}
}
#[must_use]
#[derive(Debug, Clone, PartialEq)]
pub struct PipelineWithImageRefInput<'a> {
options: Pipeline,
image: ImageRef<'a, Srgb<u8>>,
}
macro_rules! impl_pipeline {
($pipeline: expr, $remap_indexed: ident, $remap_image: ident, $output: ty) => {{
fn run_indexed<ColorMap: IndexedColorMap<Oklab, Output = Oklab>>(
image: ImageRef<'_, Srgb<u8>>,
ditherer: Option<FloydSteinberg>,
quantize: impl FnOnce(&IndexedImageCounts<Oklab, u32>) -> Option<ColorMap>,
) -> Option<$output> {
let image =
dedup::dedup_image_u8_3_counts(image).map(|palette| srgb8_to_oklab(&palette));
let color_map = quantize(&image)?;
let image = $remap_indexed(image.as_ref(), ditherer, color_map);
Some(image)
}
fn run_image<ColorMap: IndexedColorMap<Oklab, Output = Oklab>>(
image: ImageRef<'_, Srgb<u8>>,
ditherer: Option<FloydSteinberg>,
quantize: impl FnOnce(ImageRef<'_, Oklab>) -> Option<ColorMap>,
) -> Option<$output> {
let image = image.map_ref(srgb8_to_oklab);
let color_map = quantize(image.as_ref())?;
let image = $remap_image(image.as_ref(), ditherer, color_map);
Some(image)
}
let Self { options, image } = $pipeline;
let Pipeline { k, quantize_method, dedup, ditherer, .. } = options;
let binner = BinnerF32x3::oklab_from_srgb8();
match quantize_method {
QuantizeMethod::CustomPalette(palette) => {
#[cfg(not(target_feature = "avx2"))]
let dedup_threshold = (1024 * 1024 * 2) / u32::from(k.as_u16() / 8).max(1);
#[cfg(target_feature = "avx2")]
let dedup_threshold = (4096 * 4096) / u32::from(k.as_u16() / 8).max(1);
let color_map = NearestNeighborColorMap::new(palette.into_oklab());
if dedup.unwrap_or(ditherer.is_none() && image.num_pixels() >= dedup_threshold) {
run_indexed(image, ditherer, |_| Some(color_map))
} else {
run_image(image, ditherer, |_| Some(color_map))
}
}
QuantizeMethod::Wu => {
if dedup.unwrap_or(ditherer.is_none() && image.num_pixels() >= 2048 * 2048) {
run_indexed(image, ditherer, |image| {
WuF32x3::run_indexed_image_counts(image, binner).map(|wu| wu.color_map(k))
})
} else {
run_image(image, ditherer, |image| {
WuF32x3::run_image(image.as_ref(), binner).map(|wu| wu.color_map(k))
})
}
}
#[cfg(feature = "kmeans")]
QuantizeMethod::Kmeans(options) => {
#[cfg(not(target_feature = "avx2"))]
let dedup_threshold = (1024 * 1024 * 2) / u32::from(k.as_u16() / 8).max(1);
#[cfg(target_feature = "avx2")]
let dedup_threshold = (4096 * 4096) / u32::from(k.as_u16() / 8).max(1);
if dedup.unwrap_or(ditherer.is_none() && image.num_pixels() >= dedup_threshold) {
run_indexed(image, ditherer, |image| {
let centroids =
WuF32x3::run_indexed_image_counts(image, binner)?.palette(k);
let kmeans = Kmeans::run_indexed_image(image.as_ref(), centroids, options);
Some(kmeans.into_color_map())
})
} else {
run_image(image, ditherer, |image| {
let centroids = WuF32x3::run_image(image, binner)?.palette(k);
let kmeans = Kmeans::run_image(image, centroids, options);
Some(kmeans.into_color_map())
})
}
}
}
}};
}
#[cfg(feature = "threads")]
macro_rules! impl_pipeline_par {
($pipeline: expr, $remap_indexed: ident, $remap_image: ident, $output: ty) => {{
fn run_indexed<ColorMap: IndexedColorMap<Oklab, Output = Oklab> + Sync>(
image: ImageRef<'_, Srgb<u8>>,
ditherer: Option<FloydSteinberg>,
quantize: impl FnOnce(&IndexedImageCounts<Oklab, u32>) -> Option<ColorMap>,
) -> Option<$output> {
let image = dedup::dedup_image_u8_3_counts_par(image)
.map(|palette| srgb8_to_oklab_par(&palette));
let color_map = quantize(&image)?;
let image = $remap_indexed(image.as_ref(), ditherer, color_map);
Some(image)
}
fn run_image<ColorMap: IndexedColorMap<Oklab, Output = Oklab> + Sync>(
image: ImageRef<'_, Srgb<u8>>,
ditherer: Option<FloydSteinberg>,
quantize: impl FnOnce(ImageRef<'_, Oklab>) -> Option<ColorMap>,
) -> Option<$output> {
let image = image.map_ref(srgb8_to_oklab_par);
let color_map = quantize(image.as_ref())?;
let image = $remap_image(image.as_ref(), ditherer, color_map);
Some(image)
}
let Self { options, image } = $pipeline;
let Pipeline { k, quantize_method, dedup, ditherer, .. } = options;
let binner = BinnerF32x3::oklab_from_srgb8();
match quantize_method {
QuantizeMethod::CustomPalette(palette) => {
#[cfg(not(target_feature = "avx2"))]
let dedup_threshold = (4096 * 4096) / u32::from(k.as_u16() / 32).max(1);
#[cfg(target_feature = "avx2")]
let dedup_threshold = (4096 * 4096 * 2) / u32::from(k.as_u16() / 32).max(1);
let color_map = NearestNeighborParallelColorMap::new(palette.into_oklab());
if dedup.unwrap_or(ditherer.is_none() && image.num_pixels() >= dedup_threshold) {
run_indexed(image, ditherer, |_| Some(color_map))
} else {
run_image(image, ditherer, |_| Some(color_map))
}
}
QuantizeMethod::Wu => {
#[cfg(not(target_feature = "avx2"))]
let dedup_threshold = 1024 * 1024 * 2;
#[cfg(target_feature = "avx2")]
let dedup_threshold = 4096 * 4096;
if dedup.unwrap_or(ditherer.is_none() && image.num_pixels() >= dedup_threshold) {
run_indexed(image, ditherer, |image| {
WuF32x3::run_indexed_image_counts_par(image, binner)
.map(|wu| wu.parallel_color_map(k))
})
} else {
run_image(image, ditherer, |image| {
WuF32x3::run_image_par(image.as_ref(), binner)
.map(|wu| wu.parallel_color_map(k))
})
}
}
#[cfg(feature = "kmeans")]
QuantizeMethod::Kmeans(options) => {
#[cfg(not(target_feature = "avx2"))]
let dedup_threshold = (4096 * 4096) / u32::from(k.as_u16() / 32).max(1);
#[cfg(target_feature = "avx2")]
let dedup_threshold = (4096 * 4096 * 2) / u32::from(k.as_u16() / 32).max(1);
if dedup.unwrap_or(ditherer.is_none() && image.num_pixels() >= dedup_threshold) {
run_indexed(image, ditherer, |image| {
let centroids =
WuF32x3::run_indexed_image_counts_par(image, binner)?.palette(k);
let kmeans =
Kmeans::run_indexed_image_par(image.as_ref(), centroids, options);
Some(kmeans.into_parallel_color_map())
})
} else {
run_image(image, ditherer, |image| {
let centroids = WuF32x3::run_image_par(image, binner)?.palette(k);
let kmeans = Kmeans::run_image_par(image, centroids, options);
Some(kmeans.into_parallel_color_map())
})
}
}
}
}};
}
impl<'a> PipelineWithImageRefInput<'a> {
fn into_pipeline_with_slice_input(self) -> Option<PipelineWithSliceInput<'a>> {
let Self { options, image, .. } = self;
let colors = BoundedSlice::new(image.into_inner()).ok()?;
Some(PipelineWithSliceInput { options, colors })
}
#[must_use]
pub fn output_srgb8_palette(self) -> Option<PaletteBuf<Srgb<u8>>> {
self.into_pipeline_with_slice_input()
.map(PipelineWithSliceInput::output_srgb8_palette)
}
#[must_use]
pub fn output_oklab_palette(self) -> Option<PaletteBuf<Oklab>> {
self.into_pipeline_with_slice_input()
.map(PipelineWithSliceInput::output_oklab_palette)
}
#[must_use]
pub fn output_srgb8_palette_and_counts(
self,
) -> Option<(PaletteBuf<Srgb<u8>>, PaletteBuf<u32>)> {
self.into_pipeline_with_slice_input()
.map(PipelineWithSliceInput::output_srgb8_palette_and_counts)
}
#[must_use]
pub fn output_oklab_palette_and_counts(self) -> Option<(PaletteBuf<Oklab>, PaletteBuf<u32>)> {
self.into_pipeline_with_slice_input()
.map(PipelineWithSliceInput::output_oklab_palette_and_counts)
}
#[must_use]
pub fn output_srgb8_indexed_image(self) -> IndexedImage<Srgb<u8>> {
self.output_oklab_indexed_image()
.map(|palette| oklab_to_srgb8(&palette))
}
#[must_use]
pub fn output_oklab_indexed_image(self) -> IndexedImage<Oklab> {
#[cfg(feature = "threads")]
if self.options.parallel {
return impl_pipeline_par!(
self,
remap_indexed_to_indexed_par,
remap_image_to_indexed_par,
IndexedImage<Oklab>
)
.unwrap_or_default();
}
impl_pipeline!(
self,
remap_indexed_to_indexed,
remap_image_to_indexed,
IndexedImage<Oklab>
)
.unwrap_or_default()
}
#[must_use]
pub fn output_srgb8_image(self) -> ImageBuf<Srgb<u8>> {
fn remap_indexed(
image: &IndexedImage<Oklab, u32>,
ditherer: Option<FloydSteinberg>,
color_map: impl IndexedColorMap<Oklab, Output = Oklab>,
) -> ImageBuf<Srgb<u8>> {
let color_map = PaletteSubstitution::from_slice_mapping(color_map, oklab_to_srgb8);
remap_indexed_to_image(image, ditherer, color_map)
}
fn remap_image(
image: ImageRef<'_, Oklab>,
ditherer: Option<FloydSteinberg>,
color_map: impl IndexedColorMap<Oklab, Output = Oklab>,
) -> ImageBuf<Srgb<u8>> {
let color_map = PaletteSubstitution::from_slice_mapping(color_map, oklab_to_srgb8);
remap_image_to_image(image, ditherer, color_map)
}
#[cfg(feature = "threads")]
if self.options.parallel {
fn remap_indexed(
image: &IndexedImage<Oklab, u32>,
ditherer: Option<FloydSteinberg>,
color_map: impl IndexedColorMap<Oklab, Output = Oklab> + Sync,
) -> ImageBuf<Srgb<u8>> {
let color_map = PaletteSubstitution::from_slice_mapping(color_map, oklab_to_srgb8);
remap_indexed_to_image_par(image, ditherer, color_map)
}
fn remap_image(
image: ImageRef<'_, Oklab>,
ditherer: Option<FloydSteinberg>,
color_map: impl IndexedColorMap<Oklab, Output = Oklab> + Sync,
) -> ImageBuf<Srgb<u8>> {
let color_map = PaletteSubstitution::from_slice_mapping(color_map, oklab_to_srgb8);
remap_image_to_image_par(image, ditherer, color_map)
}
return impl_pipeline_par!(self, remap_indexed, remap_image, ImageBuf<Srgb<u8>>)
.unwrap_or_default();
}
impl_pipeline!(self, remap_indexed, remap_image, ImageBuf<Srgb<u8>>).unwrap_or_default()
}
#[must_use]
pub fn output_oklab_image(self) -> ImageBuf<Oklab> {
#[cfg(feature = "threads")]
if self.options.parallel {
return impl_pipeline_par!(
self,
remap_indexed_to_image_par,
remap_image_to_image_par,
ImageBuf<Oklab>
)
.unwrap_or_default();
}
impl_pipeline!(
self,
remap_indexed_to_image,
remap_image_to_image,
ImageBuf<Oklab>
)
.unwrap_or_default()
}
#[allow(clippy::type_complexity)]
#[must_use]
pub fn output_srgb8_palette_and_image(
self,
) -> Option<(PaletteBuf<Srgb<u8>>, ImageBuf<Srgb<u8>>)> {
fn remap_indexed(
image: &IndexedImage<Oklab, u32>,
ditherer: Option<FloydSteinberg>,
color_map: impl IndexedColorMap<Oklab, Output = Oklab>,
) -> (PaletteBuf<Srgb<u8>>, ImageBuf<Srgb<u8>>) {
let color_map = PaletteSubstitution::from_slice_mapping(color_map, oklab_to_srgb8);
let image = remap_indexed_to_image(image, ditherer, &color_map);
(color_map.into_palette(), image)
}
fn remap_image(
image: ImageRef<'_, Oklab>,
ditherer: Option<FloydSteinberg>,
color_map: impl IndexedColorMap<Oklab, Output = Oklab>,
) -> (PaletteBuf<Srgb<u8>>, ImageBuf<Srgb<u8>>) {
let color_map = PaletteSubstitution::from_slice_mapping(color_map, oklab_to_srgb8);
let image = remap_image_to_image(image, ditherer, &color_map);
(color_map.into_palette(), image)
}
#[cfg(feature = "threads")]
if self.options.parallel {
fn remap_indexed(
image: &IndexedImage<Oklab, u32>,
ditherer: Option<FloydSteinberg>,
color_map: impl IndexedColorMap<Oklab, Output = Oklab> + Sync,
) -> (PaletteBuf<Srgb<u8>>, ImageBuf<Srgb<u8>>) {
let color_map = PaletteSubstitution::from_slice_mapping(color_map, oklab_to_srgb8);
let image = remap_indexed_to_image_par(image, ditherer, &color_map);
(color_map.into_palette(), image)
}
fn remap_image(
image: ImageRef<'_, Oklab>,
ditherer: Option<FloydSteinberg>,
color_map: impl IndexedColorMap<Oklab, Output = Oklab> + Sync,
) -> (PaletteBuf<Srgb<u8>>, ImageBuf<Srgb<u8>>) {
let color_map = PaletteSubstitution::from_slice_mapping(color_map, oklab_to_srgb8);
let image = remap_image_to_image_par(image, ditherer, &color_map);
(color_map.into_palette(), image)
}
return impl_pipeline_par!(
self,
remap_indexed,
remap_image,
(PaletteBuf<Srgb<u8>>, ImageBuf<Srgb<u8>>)
);
}
impl_pipeline!(
self,
remap_indexed,
remap_image,
(PaletteBuf<Srgb<u8>>, ImageBuf<Srgb<u8>>)
)
}
#[must_use]
pub fn output_oklab_palette_and_image(self) -> Option<(PaletteBuf<Oklab>, ImageBuf<Oklab>)> {
fn remap_indexed(
image: &IndexedImage<Oklab, u32>,
ditherer: Option<FloydSteinberg>,
color_map: impl IndexedColorMap<Oklab, Output = Oklab>,
) -> (PaletteBuf<Oklab>, ImageBuf<Oklab>) {
let image = remap_indexed_to_image(image, ditherer, &color_map);
(color_map.into_palette(), image)
}
fn remap_image(
image: ImageRef<'_, Oklab>,
ditherer: Option<FloydSteinberg>,
color_map: impl IndexedColorMap<Oklab, Output = Oklab>,
) -> (PaletteBuf<Oklab>, ImageBuf<Oklab>) {
let image = remap_image_to_image(image, ditherer, &color_map);
(color_map.into_palette(), image)
}
#[cfg(feature = "threads")]
if self.options.parallel {
fn remap_indexed(
image: &IndexedImage<Oklab, u32>,
ditherer: Option<FloydSteinberg>,
color_map: impl IndexedColorMap<Oklab, Output = Oklab> + Sync,
) -> (PaletteBuf<Oklab>, ImageBuf<Oklab>) {
let image = remap_indexed_to_image_par(image, ditherer, &color_map);
(color_map.into_palette(), image)
}
fn remap_image(
image: ImageRef<'_, Oklab>,
ditherer: Option<FloydSteinberg>,
color_map: impl IndexedColorMap<Oklab, Output = Oklab> + Sync,
) -> (PaletteBuf<Oklab>, ImageBuf<Oklab>) {
let image = remap_image_to_image_par(image, ditherer, &color_map);
(color_map.into_palette(), image)
}
return impl_pipeline_par!(
self,
remap_indexed,
remap_image,
(PaletteBuf<Oklab>, ImageBuf<Oklab>)
);
}
impl_pipeline!(
self,
remap_indexed,
remap_image,
(PaletteBuf<Oklab>, ImageBuf<Oklab>)
)
}
}
fn remap_image_to_image<ColorMap>(
image: ImageRef<'_, Oklab>,
ditherer: Option<FloydSteinberg>,
color_map: ColorMap,
) -> ImageBuf<ColorMap::Output>
where
ColorMap: IndexedColorMap<Oklab>,
ColorMap::Output: Zeroable,
{
if let Some(ditherer) = ditherer {
ditherer.dither_image_to_image(image, color_map)
} else {
image.map_to_image(color_map)
}
}
fn remap_indexed_to_image<ColorMap>(
image: &IndexedImage<Oklab, u32>,
ditherer: Option<FloydSteinberg>,
color_map: ColorMap,
) -> ImageBuf<ColorMap::Output>
where
ColorMap: IndexedColorMap<Oklab>,
ColorMap::Output: Zeroable,
{
if let Some(ditherer) = ditherer {
ditherer.dither_indexed_to_image(image, color_map)
} else {
image.map_to_image(color_map)
}
}
fn remap_indexed_to_indexed(
image: &IndexedImage<Oklab, u32>,
ditherer: Option<FloydSteinberg>,
color_map: impl IndexedColorMap<Oklab, Output = Oklab>,
) -> IndexedImage<Oklab> {
if let Some(ditherer) = ditherer {
ditherer.dither_indexed_to_indexed(image, color_map)
} else {
image.map_to_indexed(color_map)
}
}
fn remap_image_to_indexed(
image: ImageRef<'_, Oklab>,
ditherer: Option<FloydSteinberg>,
color_map: impl IndexedColorMap<Oklab, Output = Oklab>,
) -> IndexedImage<Oklab> {
if let Some(ditherer) = ditherer {
ditherer.dither_image_to_indexed(image, color_map)
} else {
image.map_to_indexed(color_map)
}
}
#[cfg(feature = "threads")]
fn remap_image_to_image_par<ColorMap>(
image: ImageRef<'_, Oklab>,
ditherer: Option<FloydSteinberg>,
color_map: ColorMap,
) -> ImageBuf<ColorMap::Output>
where
ColorMap: IndexedColorMap<Oklab> + Sync,
ColorMap::Output: Zeroable,
{
if let Some(ditherer) = ditherer {
ditherer.dither_image_to_image_par(image, color_map)
} else {
image.map_to_image(color_map)
}
}
#[cfg(feature = "threads")]
fn remap_indexed_to_image_par<ColorMap>(
image: &IndexedImage<Oklab, u32>,
ditherer: Option<FloydSteinberg>,
color_map: ColorMap,
) -> ImageBuf<ColorMap::Output>
where
ColorMap: IndexedColorMap<Oklab> + Sync,
ColorMap::Output: Zeroable,
{
if let Some(ditherer) = ditherer {
ditherer.dither_indexed_to_image_par(image, color_map)
} else {
image.map_to_image_par(color_map)
}
}
#[cfg(feature = "threads")]
fn remap_indexed_to_indexed_par(
image: &IndexedImage<Oklab, u32>,
ditherer: Option<FloydSteinberg>,
color_map: impl IndexedColorMap<Oklab, Output = Oklab> + Sync,
) -> IndexedImage<Oklab> {
if let Some(ditherer) = ditherer {
ditherer.dither_indexed_to_indexed_par(image, color_map)
} else {
image.map_to_indexed_par(color_map)
}
}
#[cfg(feature = "threads")]
fn remap_image_to_indexed_par(
image: ImageRef<'_, Oklab>,
ditherer: Option<FloydSteinberg>,
color_map: impl IndexedColorMap<Oklab, Output = Oklab> + Sync,
) -> IndexedImage<Oklab> {
if let Some(ditherer) = ditherer {
ditherer.dither_image_to_indexed_par(image, color_map)
} else {
image.map_to_indexed(color_map)
}
}