use anyhow::Context as _;
#[cfg(any(feature = "ahash", feature = "dhash"))]
pub(crate) struct PerceptualHash(u64);
#[cfg(feature = "blockhash")]
pub(crate) struct PerceptualHash(blockhash::Blockhash64);
impl PerceptualHash {
#[cfg(feature = "ahash")]
pub(crate) fn from_image_buffer(buf: &[u8]) -> anyhow::Result<Self> {
const PERCEPTUAL_HASH_IMG_SIZE: u32 = 8;
let img = image::load_from_memory(buf)
.context("Failed to decode thumbnail")?
.resize_exact(
PERCEPTUAL_HASH_IMG_SIZE,
PERCEPTUAL_HASH_IMG_SIZE,
image::imageops::FilterType::Lanczos3,
)
.to_luma8();
let pixels = img.as_raw();
#[expect(clippy::cast_possible_truncation)]
let mean = (pixels.iter().map(|v| u64::from(*v)).sum::<u64>() / pixels.len() as u64) as u8;
let hash = pixels
.iter()
.enumerate()
.fold(0_u64, |mut hash, (i, pixel)| {
if *pixel > mean {
hash |= 1 << i;
}
hash
});
Ok(Self(hash))
}
#[cfg(feature = "dhash")]
pub(crate) fn from_image_buffer(buf: &[u8]) -> anyhow::Result<Self> {
const PERCEPTUAL_HASH_IMG_SIZE: (u32, u32) = (9, 8);
let img = image::load_from_memory(buf)
.context("Failed to decode thumbnail")?
.resize_exact(
PERCEPTUAL_HASH_IMG_SIZE.0,
PERCEPTUAL_HASH_IMG_SIZE.1,
image::imageops::FilterType::Lanczos3,
)
.to_luma8();
let pixels = img.as_raw();
let mut hash = 0;
for row in 0..PERCEPTUAL_HASH_IMG_SIZE.1 {
let start = (row * PERCEPTUAL_HASH_IMG_SIZE.0) as usize;
let end = ((row + 1) * PERCEPTUAL_HASH_IMG_SIZE.0) as usize;
#[expect(clippy::indexing_slicing)]
for (i, ps) in pixels[start..end].windows(2).enumerate() {
debug_assert!(i < 8);
if ps[0] < ps[1] {
hash |= 1 << (u64::from(row) * 8 + i as u64);
}
}
}
Ok(Self(hash))
}
#[cfg(feature = "blockhash")]
pub(crate) fn from_image_buffer(buf: &[u8]) -> anyhow::Result<Self> {
let img = image::load_from_memory(buf).context("Failed to decode thumbnail")?;
let hash = blockhash::blockhash64(&img);
Ok(Self(hash))
}
#[cfg(any(feature = "ahash", feature = "dhash"))]
pub(crate) fn is_similar(&self, other: &Self) -> bool {
const MAX_HAMMING_DELTA: u32 = if cfg!(feature = "ahash") { 5 } else { 8 };
(self.0 ^ other.0).count_ones() < MAX_HAMMING_DELTA
}
#[cfg(feature = "blockhash")]
pub(crate) fn is_similar(&self, other: &Self) -> bool {
const MAX_HAMMING_DELTA: u32 = 2;
self.0.distance(&other.0).count_ones() < MAX_HAMMING_DELTA
}
}