dithr 0.3.0

Buffer-first rust dithering and halftoning library.
Documentation
use super::{ordered_dither_in_place, DEFAULT_STRENGTH};
use crate::{
    core::{layout::validate_layout_invariants, PixelLayout, Sample},
    Buffer, Error, QuantizeMode, Result,
};
use std::sync::OnceLock;

const POLYOMINO_SIDE: usize = 48;
const POLYOMINO_LEN: usize = POLYOMINO_SIDE * POLYOMINO_SIDE;
const POLYOMINO_TILE_SIZE: usize = 3;
const POLYOMINO_BLOCKS: usize = POLYOMINO_SIDE / POLYOMINO_TILE_SIZE;

static POLYOMINO_48X48_FLAT: OnceLock<[u16; POLYOMINO_LEN]> = OnceLock::new();

pub fn polyomino_ordered_dither_in_place<S: Sample, L: PixelLayout>(
    buffer: &mut Buffer<'_, S, L>,
    mode: QuantizeMode<'_, S>,
) -> Result<()> {
    buffer.validate()?;
    validate_layout_invariants::<L>()?;

    if !L::IS_GRAY {
        return Err(Error::UnsupportedFormat(
            "polyomino ordered dithering supports Gray layouts only",
        ));
    }

    ordered_dither_in_place(
        buffer,
        mode,
        polyomino_48x48_flat(),
        POLYOMINO_SIDE,
        POLYOMINO_SIDE,
        DEFAULT_STRENGTH,
    )
}

fn polyomino_48x48_flat() -> &'static [u16; POLYOMINO_LEN] {
    POLYOMINO_48X48_FLAT.get_or_init(generate_polyomino_48x48_flat)
}

fn generate_polyomino_48x48_flat() -> [u16; POLYOMINO_LEN] {
    let mut tiles = Vec::with_capacity(POLYOMINO_LEN / POLYOMINO_TILE_SIZE);

    for block_y in 0..POLYOMINO_BLOCKS {
        for block_x in 0..POLYOMINO_BLOCKS {
            let x0 = block_x * POLYOMINO_TILE_SIZE;
            let y0 = block_y * POLYOMINO_TILE_SIZE;
            let orientation_horizontal = block_orientation(block_x, block_y);

            if orientation_horizontal {
                for row in 0..POLYOMINO_TILE_SIZE {
                    let y = y0 + row;
                    let cells = [
                        y * POLYOMINO_SIDE + x0,
                        y * POLYOMINO_SIDE + x0 + 1,
                        y * POLYOMINO_SIDE + x0 + 2,
                    ];
                    let tile_key = tile_key(x0, y, true, row);
                    tiles.push(Tile {
                        cells,
                        key: tile_key,
                    });
                }
            } else {
                for column in 0..POLYOMINO_TILE_SIZE {
                    let x = x0 + column;
                    let cells = [
                        y0 * POLYOMINO_SIDE + x,
                        (y0 + 1) * POLYOMINO_SIDE + x,
                        (y0 + 2) * POLYOMINO_SIDE + x,
                    ];
                    let tile_key = tile_key(x, y0, false, column);
                    tiles.push(Tile {
                        cells,
                        key: tile_key,
                    });
                }
            }
        }
    }

    tiles.sort_by_key(|tile| (tile.key, tile.cells[0]));

    let mut ranked = [0_u16; POLYOMINO_LEN];
    for (tile_rank, tile) in tiles.iter().enumerate() {
        let local_order = local_cell_order(tile.key);
        let base = tile_rank * POLYOMINO_TILE_SIZE;
        ranked[tile.cells[local_order[0]]] = base as u16;
        ranked[tile.cells[local_order[1]]] = (base + 1) as u16;
        ranked[tile.cells[local_order[2]]] = (base + 2) as u16;
    }

    ranked
}

struct Tile {
    cells: [usize; 3],
    key: u64,
}

fn block_orientation(block_x: usize, block_y: usize) -> bool {
    let level0 = mix_u64(hash2d(block_x, block_y, 0));
    let level1 = mix_u64(hash2d(block_x >> 1, block_y >> 1, 1));
    let level2 = mix_u64(hash2d(block_x >> 2, block_y >> 2, 2));
    (level0 ^ level1.rotate_left(17) ^ level2.rotate_left(29)) & 1 == 0
}

fn tile_key(x: usize, y: usize, horizontal: bool, lane: usize) -> u64 {
    let orientation = u64::from(horizontal as u8);
    mix_u64(
        (x as u64).wrapping_mul(0x9e37_79b1_85eb_ca87_u64)
            ^ (y as u64).wrapping_mul(0xc2b2_ae3d_27d4_eb4f_u64)
            ^ orientation.wrapping_mul(0x1656_67b1_9e37_79f9_u64)
            ^ (lane as u64).wrapping_mul(0x94d0_49bb_1331_11eb_u64),
    )
}

fn local_cell_order(key: u64) -> [usize; 3] {
    const PERMUTATIONS: [[usize; 3]; 6] = [
        [0, 1, 2],
        [0, 2, 1],
        [1, 0, 2],
        [1, 2, 0],
        [2, 0, 1],
        [2, 1, 0],
    ];

    let index = (key % (PERMUTATIONS.len() as u64)) as usize;
    PERMUTATIONS[index]
}

fn hash2d(x: usize, y: usize, salt: u64) -> u64 {
    mix_u64(
        (x as u64)
            .wrapping_mul(0x632b_e59b_d9b4_e019_u64)
            .wrapping_add((y as u64).wrapping_mul(0x8cb9_2baa_33a5_049f_u64))
            .wrapping_add(salt.wrapping_mul(0x9e37_79b1_85eb_ca87_u64)),
    )
}

fn mix_u64(mut value: u64) -> u64 {
    value ^= value >> 33;
    value = value.wrapping_mul(0xff51_afd7_ed55_8ccd_u64);
    value ^= value >> 33;
    value = value.wrapping_mul(0xc4ce_b9fe_1a85_ec53_u64);
    value ^ (value >> 33)
}

#[cfg(test)]
mod tests {
    use super::generate_polyomino_48x48_flat;

    #[test]
    fn generated_polyomino_map_has_full_rank_coverage() {
        let map = generate_polyomino_48x48_flat();
        let mut seen = vec![false; map.len()];

        for &rank in &map {
            let idx = usize::from(rank);
            assert!(idx < map.len());
            assert!(!seen[idx]);
            seen[idx] = true;
        }

        assert!(seen.into_iter().all(|entry| entry));
    }

    #[test]
    fn generated_polyomino_map_is_deterministic() {
        assert_eq!(
            generate_polyomino_48x48_flat(),
            generate_polyomino_48x48_flat()
        );
    }
}