dithr 0.3.0

Buffer-first rust dithering and halftoning library.
Documentation
use super::core::{ordered_threshold_for_xy, ordered_threshold_unit};
use crate::{
    core::{alpha_index, layout::validate_layout_invariants, read_unit_pixel, PixelLayout, Sample},
    data::{
        generate_bayer_16x16_flat, BAYER_2X2_FLAT, BAYER_4X4_FLAT, BAYER_8X8_FLAT,
        CLUSTER_DOT_4X4_FLAT, CLUSTER_DOT_8X8_FLAT,
    },
    quantize_pixel, Buffer, BufferError, Error, QuantizeMode, Result,
};
use std::sync::OnceLock;

const ADAPTIVE_STRENGTH: f32 = 64.0 / 255.0;

static BAYER_16X16_FLAT: OnceLock<[u16; 256]> = OnceLock::new();

struct ThresholdMap<'a> {
    data: &'a [u16],
    width: usize,
    height: usize,
    denom: u16,
}

pub fn adaptive_ordered_dither_in_place<S: Sample, L: PixelLayout>(
    buffer: &mut Buffer<'_, S, L>,
    mode: QuantizeMode<'_, S>,
) -> Result<()> {
    buffer.validate()?;
    validate_layout_invariants::<L>()?;
    if !ordered_layout_supported::<L>() {
        return Err(Error::UnsupportedFormat(
            "adaptive ordered dithering supports Gray, Rgb, and Rgba formats only",
        ));
    }

    let width = buffer.width();
    let height = buffer.height();
    let stride = buffer.stride();
    let source = buffer.data().to_vec();

    for y in 0..height {
        let row = buffer.try_row_mut(y)?;
        for x in 0..width {
            let activity = local_activity::<S, L>(&source, width, height, stride, x, y)?;
            let map = select_threshold_map(activity);
            let threshold = ordered_threshold_for_xy(x, y, map.data, map.width, map.height)?;
            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)?;
            adaptive_apply_pixel::<S, L>(pixel, threshold, map.denom, mode)?;
        }
    }

    Ok(())
}

fn select_threshold_map(activity: f32) -> ThresholdMap<'static> {
    if activity < 0.08 {
        return ThresholdMap {
            data: &CLUSTER_DOT_8X8_FLAT,
            width: 8,
            height: 8,
            denom: 64,
        };
    }

    if activity < 0.16 {
        return ThresholdMap {
            data: &CLUSTER_DOT_4X4_FLAT,
            width: 4,
            height: 4,
            denom: 16,
        };
    }

    if activity < 0.26 {
        return ThresholdMap {
            data: bayer_16x16_flat(),
            width: 16,
            height: 16,
            denom: 256,
        };
    }

    if activity < 0.38 {
        return ThresholdMap {
            data: &BAYER_8X8_FLAT,
            width: 8,
            height: 8,
            denom: 64,
        };
    }

    if activity < 0.52 {
        return ThresholdMap {
            data: &BAYER_4X4_FLAT,
            width: 4,
            height: 4,
            denom: 16,
        };
    }

    ThresholdMap {
        data: &BAYER_2X2_FLAT,
        width: 2,
        height: 2,
        denom: 4,
    }
}

fn local_activity<S: Sample, L: PixelLayout>(
    source: &[S],
    width: usize,
    height: usize,
    stride: usize,
    x: usize,
    y: usize,
) -> Result<f32> {
    let x_prev = x.saturating_sub(1);
    let y_prev = y.saturating_sub(1);
    let x_next = (x + 1).min(width - 1);
    let y_next = (y + 1).min(height - 1);

    let center = luma_at::<S, L>(source, stride, x, y)?;
    let left = luma_at::<S, L>(source, stride, x_prev, y)?;
    let right = luma_at::<S, L>(source, stride, x_next, y)?;
    let up = luma_at::<S, L>(source, stride, x, y_prev)?;
    let down = luma_at::<S, L>(source, stride, x, y_next)?;

    let gx = (right - left).abs();
    let gy = (down - up).abs();
    let lap = (4.0 * center - left - right - up - down).abs();

    Ok((gx + gy + lap * 0.5).clamp(0.0, 1.0))
}

fn luma_at<S: Sample, L: PixelLayout>(
    source: &[S],
    stride: usize,
    x: usize,
    y: usize,
) -> Result<f32> {
    let row_start = y.checked_mul(stride).ok_or(BufferError::OutOfBounds)?;
    let offset = x
        .checked_mul(L::CHANNELS)
        .and_then(|in_row| row_start.checked_add(in_row))
        .ok_or(BufferError::OutOfBounds)?;
    let end = offset
        .checked_add(L::CHANNELS)
        .ok_or(BufferError::OutOfBounds)?;
    let pixel = source.get(offset..end).ok_or(BufferError::OutOfBounds)?;
    let rgba = read_unit_pixel::<S, L>(pixel)?;
    Ok((0.299 * rgba[0] + 0.587 * rgba[1] + 0.114 * rgba[2]).clamp(0.0, 1.0))
}

fn adaptive_apply_pixel<S: Sample, L: PixelLayout>(
    pixel: &mut [S],
    threshold_rank: u16,
    threshold_den: u16,
    mode: QuantizeMode<'_, S>,
) -> Result<()> {
    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, ADAPTIVE_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(())
}

fn bayer_16x16_flat() -> &'static [u16; 256] {
    BAYER_16X16_FLAT.get_or_init(generate_bayer_16x16_flat)
}

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