//! Blurhash generation utilities for images and videos //! //! Blurhash is a compact representation of an image that can be decoded into a //! low-resolution placeholder. Perfect for showing while full images/thumbnails load. use image::{DynamicImage, GenericImageView}; use thiserror::Error; #[derive(Error, Debug)] pub enum BlurhashError { #[error("Image is too small for blurhash generation")] ImageTooSmall, #[error("Blurhash encoding failed: {0}")] EncodingFailed(String), #[error("Invalid image dimensions")] InvalidDimensions, } /// Generate a blurhash from a DynamicImage /// /// The blurhash will be generated using 4x3 components for good quality /// while keeping the hash compact (~20-30 chars). /// /// # Arguments /// /// * `image` - The image to generate a blurhash from /// /// # Returns /// /// A blurhash string that can be decoded into a placeholder image pub fn generate_blurhash(image: &DynamicImage) -> Result { let (width, height) = image.dimensions(); if width == 0 || height == 0 { return Err(BlurhashError::InvalidDimensions); } // Blurhash works best with reasonable dimensions // If image is too large, resize to max 256px for blurhash calculation let working_image = if width > 256 || height > 256 { let scale = 256.0 / width.max(height) as f64; let new_width = (width as f64 * scale) as u32; let new_height = (height as f64 * scale) as u32; image.resize_exact( new_width.max(1), new_height.max(1), image::imageops::FilterType::Lanczos3, ) } else { image.clone() }; let (w, h) = working_image.dimensions(); // Convert to RGB8 for blurhash encoding let rgb_image = working_image.to_rgb8(); let pixels = rgb_image.as_raw(); // Generate blurhash with 4x3 components (good balance of quality and size) // Results in ~20-30 character hash let hash = blurhash::encode(4, 3, w, h, pixels) .map_err(|e| BlurhashError::EncodingFailed(e.to_string()))?; Ok(hash) } /// Generate a blurhash from a video frame /// /// This is a convenience wrapper around `generate_blurhash` for video frames. pub fn generate_blurhash_from_video_frame(frame: &DynamicImage) -> Result { generate_blurhash(frame) } #[cfg(test)] mod tests { use super::*; use image::RgbImage; #[test] fn test_generate_blurhash() { // Create a simple gradient image for testing let width = 100; let height = 100; let mut img = RgbImage::new(width, height); for y in 0..height { for x in 0..width { let r = (x as f32 / width as f32 * 255.0) as u8; let g = (y as f32 / height as f32 * 255.0) as u8; let b = 128; img.put_pixel(x, y, image::Rgb([r, g, b])); } } let dynamic_img = DynamicImage::ImageRgb8(img); let hash = generate_blurhash(&dynamic_img).unwrap(); // Blurhash should be a non-empty string assert!(!hash.is_empty()); // Should be around 20-30 characters for 4x3 components assert!(hash.len() > 10 && hash.len() < 50); } #[test] fn test_zero_dimensions() { let img = DynamicImage::new_rgb8(0, 0); let result = generate_blurhash(&img); assert!(result.is_err()); } #[test] fn test_large_image_resize() { // Create a large image to test automatic resizing let img = DynamicImage::new_rgb8(2000, 2000); let hash = generate_blurhash(&img).unwrap(); // Should still generate a hash even with large dimensions assert!(!hash.is_empty()); } }