dithr 0.3.0

Buffer-first rust dithering and halftoning library.
Documentation
use crate::{
    core::{alpha_index, layout::validate_layout_invariants, read_unit_pixel, PixelLayout, Sample},
    quantize_pixel, Buffer, BufferError, Error, QuantizeMode, Result,
};
#[cfg(feature = "rayon")]
use rayon::prelude::*;

pub(crate) fn ordered_dither_in_place<S: Sample, L: PixelLayout>(
    buffer: &mut Buffer<'_, S, L>,
    mode: QuantizeMode<'_, S>,
    map: &[u16],
    map_w: usize,
    map_h: usize,
    strength: f32,
) -> Result<()> {
    buffer.validate()?;
    validate_layout_invariants::<L>()?;
    let (map_min, threshold_den) = validate_map(map, map_w, map_h)?;

    let width = buffer.width;
    let height = buffer.height;
    let ctx = OrderedDitherCtx {
        map,
        map_w,
        map_h,
        map_min,
        threshold_den,
        strength,
    };

    for y in 0..height {
        let row = buffer.try_row_mut(y)?;
        ordered_dither_row::<S, L>(row, y, width, &ctx, mode)?;
    }

    Ok(())
}

#[cfg(feature = "rayon")]
pub(crate) fn ordered_dither_in_place_par<S: Sample, L: PixelLayout>(
    buffer: &mut Buffer<'_, S, L>,
    mode: QuantizeMode<'_, S>,
    map: &[u16],
    map_w: usize,
    map_h: usize,
    strength: f32,
) -> Result<()> {
    buffer.validate()?;
    validate_layout_invariants::<L>()?;
    let (map_min, threshold_den) = validate_map(map, map_w, map_h)?;

    let width = buffer.width;
    let height = buffer.height;
    let stride = buffer.stride;
    let ctx = OrderedDitherCtx {
        map,
        map_w,
        map_h,
        map_min,
        threshold_den,
        strength,
    };

    buffer
        .data
        .par_chunks_mut(stride)
        .take(height)
        .enumerate()
        .try_for_each(|(y, row)| -> Result<()> {
            ordered_dither_row::<S, L>(row, y, width, &ctx, mode)
        })?;

    Ok(())
}

struct OrderedDitherCtx<'a> {
    map: &'a [u16],
    map_w: usize,
    map_h: usize,
    map_min: u16,
    threshold_den: u16,
    strength: f32,
}

fn ordered_dither_row<S: Sample, L: PixelLayout>(
    row: &mut [S],
    y: usize,
    width: usize,
    ctx: &OrderedDitherCtx<'_>,
    mode: QuantizeMode<'_, S>,
) -> Result<()> {
    for x in 0..width {
        let threshold = ordered_threshold_for_xy(x, y, ctx.map, ctx.map_w, ctx.map_h)?;
        let threshold_rank = threshold.saturating_sub(ctx.map_min);
        let offset = x.checked_mul(L::CHANNELS).ok_or(BufferError::OutOfBounds)?;
        let end = offset
            .checked_add(L::CHANNELS)
            .ok_or(BufferError::OutOfBounds)?;
        let pixel = row.get_mut(offset..end).ok_or(BufferError::OutOfBounds)?;
        if ctx.strength == 1.0 {
            ordered_apply_pixel::<S, L>(pixel, threshold_rank, ctx.threshold_den, mode)?;
        } else {
            ordered_apply_pixel_with_strength::<S, L>(
                pixel,
                threshold_rank,
                ctx.threshold_den,
                ctx.strength,
                mode,
            )?;
        }
    }

    Ok(())
}

pub(crate) fn ordered_apply_pixel<S: Sample, L: PixelLayout>(
    pixel: &mut [S],
    threshold_rank: u16,
    threshold_den: u16,
    mode: QuantizeMode<'_, S>,
) -> Result<()> {
    ordered_apply_pixel_with_strength::<S, L>(pixel, threshold_rank, threshold_den, 1.0, mode)
}

fn ordered_apply_pixel_with_strength<S: Sample, L: PixelLayout>(
    pixel: &mut [S],
    threshold_rank: u16,
    threshold_den: u16,
    strength: f32,
    mode: QuantizeMode<'_, S>,
) -> Result<()> {
    if !ordered_layout_supported::<L>() {
        return Err(Error::UnsupportedFormat(
            "ordered dithering supports Gray, Rgb, and Rgba formats only",
        ));
    }

    if pixel.len() != L::CHANNELS {
        return Err(Error::InvalidArgument(
            "pixel slice length does not match layout",
        ));
    }

    let preserved_alpha = alpha_index::<L>().and_then(|idx| pixel.get(idx).copied());
    let mut rgba = read_unit_pixel::<S, L>(pixel)?;
    let threshold = ordered_threshold_unit(threshold_rank, threshold_den, strength);
    for channel in rgba.iter_mut().take(3) {
        *channel = (*channel + threshold).clamp(0.0, 1.0);
    }

    let biased = [
        S::from_unit_f32(rgba[0]),
        S::from_unit_f32(rgba[1]),
        S::from_unit_f32(rgba[2]),
        S::from_unit_f32(rgba[3]),
    ];
    let quantized = quantize_pixel::<S, L>(&biased[..L::CHANNELS], mode)?;
    pixel[..L::COLOR_CHANNELS].copy_from_slice(&quantized[..L::COLOR_CHANNELS]);
    if let Some(alpha_lane) = alpha_index::<L>() {
        pixel[alpha_lane] = preserved_alpha.ok_or(BufferError::OutOfBounds)?;
    }

    Ok(())
}

pub(crate) fn ordered_threshold_unit(rank: u16, denom: u16, strength: f32) -> f32 {
    if denom <= 1 || strength == 0.0 {
        return 0.0;
    }

    let range = f32::from(denom - 1);
    let centered = f32::from(rank) * 2.0 - range;
    let scaled_steps = (centered * (strength * 255.0) / range).trunc();
    scaled_steps / 255.0
}

pub(crate) fn ordered_threshold_for_xy(
    x: usize,
    y: usize,
    map: &[u16],
    map_w: usize,
    map_h: usize,
) -> Result<u16> {
    if map_w == 0 || map_h == 0 {
        return Err(Error::InvalidArgument(
            "ordered map dimensions must be positive",
        ));
    }
    let expected_len = map_w
        .checked_mul(map_h)
        .ok_or(Error::InvalidArgument("ordered map dimensions overflow"))?;
    if map.len() != expected_len {
        return Err(Error::InvalidArgument(
            "ordered map length must match dimensions",
        ));
    }
    let map_x = x % map_w;
    let map_y = y % map_h;
    let idx = map_y
        .checked_mul(map_w)
        .and_then(|base| base.checked_add(map_x))
        .ok_or(Error::InvalidArgument("ordered map indexing overflow"))?;
    map.get(idx)
        .copied()
        .ok_or(Error::InvalidArgument("ordered map index out of bounds"))
}

fn validate_map(map: &[u16], map_w: usize, map_h: usize) -> Result<(u16, u16)> {
    if map_w == 0 || map_h == 0 {
        return Err(Error::InvalidArgument(
            "ordered map dimensions must be positive",
        ));
    }

    let expected_len = map_w
        .checked_mul(map_h)
        .ok_or(Error::InvalidArgument("ordered map dimensions overflow"))?;
    if map.len() != expected_len {
        return Err(Error::InvalidArgument(
            "ordered map length must match dimensions",
        ));
    }

    let map_min = *map
        .iter()
        .min()
        .ok_or(Error::InvalidArgument("ordered map must not be empty"))?;
    let map_max = *map
        .iter()
        .max()
        .ok_or(Error::InvalidArgument("ordered map must not be empty"))?;
    let threshold_den = map_max.saturating_sub(map_min).saturating_add(1);

    Ok((map_min, threshold_den))
}

const fn ordered_layout_supported<L: PixelLayout>() -> bool {
    (L::HAS_ALPHA && L::CHANNELS == 4) || (!L::HAS_ALPHA && (L::CHANNELS == 1 || L::CHANNELS == 3))
}

#[cfg(test)]
mod tests {
    use super::ordered_threshold_for_xy;
    use crate::Error;

    #[test]
    fn ordered_threshold_rejects_zero_dimensions() {
        let map = [0_u16, 1, 2, 3];
        assert_eq!(
            ordered_threshold_for_xy(0, 0, &map, 0, 2),
            Err(Error::InvalidArgument(
                "ordered map dimensions must be positive"
            ))
        );
    }

    #[test]
    fn ordered_threshold_rejects_mismatched_map_length() {
        let map = [0_u16, 1, 2];
        assert_eq!(
            ordered_threshold_for_xy(0, 0, &map, 2, 2),
            Err(Error::InvalidArgument(
                "ordered map length must match dimensions"
            ))
        );
    }
}