use smix_error::ExpectationFailure;
pub(crate) fn compute_dhash(png_bytes: &[u8]) -> Result<u64, ExpectationFailure> {
let frame = crate::png_gray::decode_gray(png_bytes)?;
let (w, h) = (frame.w, frame.h);
let mut grid = [[0u8; 9]; 8];
for (dy, row) in grid.iter_mut().enumerate() {
for (dx, cell) in row.iter_mut().enumerate() {
let sx = (dx * w) / 9;
let sy = (dy * h) / 8;
*cell = frame.gray(sx, sy);
}
}
let mut hash: u64 = 0;
for row in &grid {
for dx in 0..8 {
hash <<= 1;
if row[dx] > row[dx + 1] {
hash |= 1;
}
}
}
Ok(hash)
}
pub(crate) fn hamming_distance(a: u64, b: u64) -> u32 {
(a ^ b).count_ones()
}
#[cfg(test)]
mod tests {
use super::*;
use smix_error::FailureCode;
fn encode_gray(width: u32, height: u32, mut pixel: impl FnMut(u32, u32) -> u8) -> Vec<u8> {
let mut out = Vec::new();
{
let mut encoder = png::Encoder::new(&mut out, width, height);
encoder.set_color(png::ColorType::Grayscale);
encoder.set_depth(png::BitDepth::Eight);
let mut writer = encoder.write_header().unwrap();
let mut buf = Vec::with_capacity((width * height) as usize);
for y in 0..height {
for x in 0..width {
buf.push(pixel(x, y));
}
}
writer.write_image_data(&buf).unwrap();
}
out
}
#[test]
fn dhash_of_uniform_image_is_stable() {
let png = encode_gray(8, 8, |_, _| 0);
let h = compute_dhash(&png).unwrap();
assert_eq!(h, 0, "uniform image → no left/right diff bits");
}
#[test]
fn dhash_of_left_right_split_is_nonzero() {
let png = encode_gray(16, 8, |x, _| if x < 8 { 255 } else { 0 });
let h = compute_dhash(&png).unwrap();
assert_ne!(
h, 0,
"left-half white / right-half black must trigger diff bits"
);
}
#[test]
fn hamming_distance_identical_is_zero() {
assert_eq!(
hamming_distance(0xDEAD_BEEF_DEAD_BEEF, 0xDEAD_BEEF_DEAD_BEEF),
0
);
}
#[test]
fn hamming_distance_flipped_bits_count_is_64() {
assert_eq!(hamming_distance(0, u64::MAX), 64);
}
#[test]
fn dhash_of_non_png_bytes_errors() {
let err = compute_dhash(b"definitely not a png").unwrap_err();
assert_eq!(err.code, FailureCode::DriverError);
assert!(
err.message.contains("PNG decode"),
"message must mention PNG decode, got: {}",
err.message
);
}
#[test]
fn dhash_rgb_and_rgba_match_grayscale() {
let gray = encode_gray(8, 8, |x, _| if x < 4 { 0 } else { 255 });
let mut rgb = Vec::new();
{
let mut enc = png::Encoder::new(&mut rgb, 8, 8);
enc.set_color(png::ColorType::Rgb);
enc.set_depth(png::BitDepth::Eight);
let mut w = enc.write_header().unwrap();
let mut buf = Vec::with_capacity(8 * 8 * 3);
for _ in 0..8 {
for x in 0..8 {
let v: u8 = if x < 4 { 0 } else { 255 };
buf.extend_from_slice(&[v, v, v]);
}
}
w.write_image_data(&buf).unwrap();
}
let h_gray = compute_dhash(&gray).unwrap();
let h_rgb = compute_dhash(&rgb).unwrap();
assert_eq!(h_gray, h_rgb);
}
}