use crate::clip::ClipRect;
use crate::framebuffer::{pack_rgba, unpack, Framebuffer};
#[derive(Clone, Debug)]
pub struct BayerMatrix {
pub matrix: [[u8; 8]; 8],
}
impl BayerMatrix {
pub fn standard_8x8() -> Self {
Self {
matrix: [
[0, 32, 8, 40, 2, 34, 10, 42],
[48, 16, 56, 24, 50, 18, 58, 26],
[12, 44, 4, 36, 14, 46, 6, 38],
[60, 28, 52, 20, 62, 30, 54, 22],
[3, 35, 11, 43, 1, 33, 9, 41],
[51, 19, 59, 27, 49, 17, 57, 25],
[15, 47, 7, 39, 13, 45, 5, 37],
[63, 31, 55, 23, 61, 29, 53, 21],
],
}
}
pub fn threshold(&self, x: u32, y: u32) -> f32 {
let tx = (x % 8) as usize;
let ty = (y % 8) as usize;
self.matrix[ty][tx] as f32 / 64.0
}
}
impl Default for BayerMatrix {
fn default() -> Self {
Self::standard_8x8()
}
}
pub fn ordered_dither_rgba(fb: &mut Framebuffer, rect: ClipRect, bits_to_drop: u32) {
if bits_to_drop == 0 {
return;
}
let bits = bits_to_drop.min(7);
let step = (1u32 << bits) as f32; let matrix = BayerMatrix::standard_8x8();
let fb_w = fb.width();
let fb_h = fb.height();
let x0 = rect.x0.max(0) as u32;
let y0 = rect.y0.max(0) as u32;
let x1 = (rect.x1 as u32).min(fb_w);
let y1 = (rect.y1 as u32).min(fb_h);
for y in y0..y1 {
for x in x0..x1 {
let Some(px) = fb.get(x, y) else { continue };
let (r, g, b, a) = unpack(px);
let thresh = matrix.threshold(x, y); let dither_channel = |c: u8| -> u8 {
let v = c as f32 + thresh * step;
let q = (v / step).floor() * step;
q.clamp(0.0, 255.0) as u8
};
let nr = dither_channel(r);
let ng = dither_channel(g);
let nb = dither_channel(b);
fb.set(x, y, pack_rgba(nr, ng, nb, a));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::framebuffer::Framebuffer;
fn grey_fb(w: u32, h: u32, level: u8) -> Framebuffer {
use oxiui_core::Color;
Framebuffer::with_fill(w, h, Color(level, level, level, 255))
}
#[test]
fn bayer_matrix_range() {
let m = BayerMatrix::standard_8x8();
let mut all_values = std::collections::HashSet::new();
for row in &m.matrix {
for &v in row {
all_values.insert(v);
}
}
assert_eq!(
all_values.len(),
64,
"8x8 Bayer matrix must have 64 unique values"
);
assert!(!all_values.contains(&64), "max value must be 63");
}
#[test]
fn bayer_threshold_range() {
let m = BayerMatrix::standard_8x8();
for y in 0..8 {
for x in 0..8 {
let t = m.threshold(x, y);
assert!(
(0.0..1.0).contains(&t),
"threshold {t} out of range at ({x},{y})"
);
}
}
}
#[test]
fn bayer_determinism() {
use crate::clip::ClipRect;
let mut fb1 = grey_fb(8, 8, 100);
let mut fb2 = grey_fb(8, 8, 100);
let rect = ClipRect::full(8, 8);
ordered_dither_rgba(&mut fb1, rect, 2);
ordered_dither_rgba(&mut fb2, rect, 2);
for y in 0..8 {
for x in 0..8 {
assert_eq!(fb1.get(x, y), fb2.get(x, y), "mismatch at ({x},{y})");
}
}
}
#[test]
fn zero_bits_noop() {
use crate::clip::ClipRect;
use oxiui_core::Color;
let mut fb = Framebuffer::with_fill(4, 4, Color(123, 45, 67, 200));
let original = fb.get(0, 0);
ordered_dither_rgba(&mut fb, ClipRect::full(4, 4), 0);
assert_eq!(fb.get(0, 0), original, "0 bits_to_drop must be a no-op");
}
#[test]
fn dither_changes_some_pixels() {
use crate::clip::ClipRect;
let original = grey_fb(8, 8, 10);
let mut dithered = grey_fb(8, 8, 10);
ordered_dither_rgba(&mut dithered, ClipRect::full(8, 8), 4);
let mut changed = 0u32;
for y in 0..8 {
for x in 0..8 {
if original.get(x, y) != dithered.get(x, y) {
changed += 1;
}
}
}
assert!(
changed > 0,
"dithering should change at least some pixels (level=10, bits=4)"
);
}
}