diff --git a/benches/benchy.rs b/benches/benchy.rs index 7604393..8919982 100644 --- a/benches/benchy.rs +++ b/benches/benchy.rs @@ -59,6 +59,33 @@ pub fn criterion_benchmark(c: &mut Criterion) { group.finish(); + let mut group = c.benchmark_group("blurhash-update-auto"); + + let inputs = [ + "data/19dd1c444d1c7939.png", + "data/f73d2ee39133d871.jpg", + "data/shenzi.png", + ]; + + for input in inputs { + group.bench_with_input(BenchmarkId::from_parameter(input), input, |b, i| { + let img = image::open(i).unwrap(); + let width = img.width(); + let height = img.height(); + let rgba = img.to_rgba8(); + let bytes = rgba.as_bytes(); + + b.iter(|| { + let _bhash = black_box(blurhash_update::auto_encode( + ImageBounds { width, height }, + bytes, + )); + }); + }); + } + + group.finish(); + let mut group = c.benchmark_group("blurhash-update-skip"); let inputs = [ diff --git a/examples/stdin.rs b/examples/stdin.rs index 2ae255f..ab4d1ce 100644 --- a/examples/stdin.rs +++ b/examples/stdin.rs @@ -12,6 +12,9 @@ struct Args { /// Height of the provided image #[clap(long)] height: u32, + + #[clap(long, default_value = "8")] + skip: u32, } // Example usage: @@ -20,8 +23,16 @@ struct Args { // cargo r --example --release -- --width blah --height blah // ``` fn main() -> Result<(), Box> { - let Args { width, height } = Args::parse(); - let mut encoder = Encoder::new(Components { x: 4, y: 3 }, ImageBounds { width, height }, 8)?; + let Args { + width, + height, + skip, + } = Args::parse(); + let mut encoder = Encoder::new( + Components { x: 4, y: 3 }, + ImageBounds { width, height }, + skip, + )?; let mut stdin = std::io::stdin().lock(); let mut buf = [0u8; 1024]; diff --git a/src/lib.rs b/src/lib.rs index 1f13793..a8ac783 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,7 +42,13 @@ struct ComponentState { /// Error raised when too many components are requested #[derive(Debug)] -pub struct ComponentError; +pub enum ConfigurationError { + /// Component values are not within the required range. + InvalidComponentCount, + + /// Skip value must not be zero + ZeroSkip, +} /// Encoder type used to produce blurhashes pub struct Encoder { @@ -60,15 +66,95 @@ pub fn encode( components: Components, bounds: ImageBounds, rgba8_image: &[u8], -) -> Result { +) -> Result { let mut encoder = Encoder::new(components, bounds, 1)?; encoder.update(rgba8_image); Ok(encoder.finalize()) } +/// A simple "encode this image please" function that automatically selects component and skip +/// values +/// +/// The input image must be in the sRGB colorspace and be formatted as 8bit RGBA +pub fn auto_encode(bounds: ImageBounds, rgba8_image: &[u8]) -> String { + let mut encoder = Encoder::auto(bounds); + encoder.update(rgba8_image); + encoder.finalize() +} + +// determine closest component ratio to input bounds +fn calculate_components(ImageBounds { width, height }: ImageBounds) -> Components { + let mut out = Components { x: 0, y: 0 }; + + let (out_longer, out_shorter, in_longer, in_shorter) = if width > height { + (&mut out.x, &mut out.y, width as f32, height as f32) + } else { + (&mut out.y, &mut out.x, height as f32, width as f32) + }; + + struct State { + similarity: f32, + ratio: (u32, u32), + } + + let ratios = [(3, 3), (4, 3), (5, 3), (6, 3), (5, 2), (6, 2), (7, 2)]; + + let in_ratio = in_longer / in_shorter; + + let State { ratio, .. } = ratios.into_iter().fold( + State { + similarity: f32::MAX, + ratio: (0, 0), + }, + |state, (ratio_longer, ratio_shorter)| { + let ratio = ratio_longer as f32 / ratio_shorter as f32; + let diff = (ratio - in_ratio).abs(); + + if diff < state.similarity { + State { + similarity: diff, + ratio: (ratio_longer, ratio_shorter), + } + } else { + state + } + }, + ); + + *out_longer = ratio.0; + *out_shorter = ratio.1; + + out +} + +// target 256ish total pixels to process +fn calculate_skip(ImageBounds { width, height }: ImageBounds) -> u32 { + let target_1d = f32::sqrt((width * height / 256) as f32).floor() as u32; + + let mut base = 1; + + loop { + if base * 2 < target_1d { + base *= 2; + } else { + break base; + } + } +} + impl Encoder { + /// Create an encoder that automatically picks Compoent and Skip values + /// + /// This is a best-effort configuration + pub fn auto(bounds: ImageBounds) -> Self { + Self::new(calculate_components(bounds), bounds, calculate_skip(bounds)) + .expect("Generated bounds are always valid") + } + /// Create a new Encoder to produce a blurhash /// + /// The provided component x and y values must be between 1 and 9 inclusive. + /// /// The `skip` value indicates how many pixels can be skipped when proccessing the image. this /// value will be squared to produce the final skip value. When set to 1, no pixels will be /// skipped, when set to 2, one in four pixels will be processed. when set to 3, one in 9 @@ -79,9 +165,13 @@ impl Encoder { Components { x, y }: Components, bounds: ImageBounds, skip: u32, - ) -> Result { + ) -> Result { if !(1..=9).contains(&x) || !(1..=9).contains(&y) { - return Err(ComponentError); + return Err(ConfigurationError::InvalidComponentCount); + } + + if skip == 0 { + return Err(ConfigurationError::ZeroSkip); } Ok(Self { @@ -266,13 +356,16 @@ fn sign_pow(val: f32, exp: f32) -> f32 { sign(val) * val.abs().powf(exp) } -impl std::fmt::Display for ComponentError { +impl std::fmt::Display for ConfigurationError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Components out of bounds") + match self { + Self::InvalidComponentCount => write!(f, "Components out of bounds"), + Self::ZeroSkip => write!(f, "Skip value cannot be zero"), + } } } -impl std::error::Error for ComponentError {} +impl std::error::Error for ConfigurationError {} #[cfg(test)] mod tests { @@ -317,7 +410,31 @@ mod tests { ) .unwrap(); - assert_eq!(hash, output, "expected output wrong"); + assert_eq!(hash, output, "wrong output for {input}"); + } + } + + #[test] + fn auto() { + let inputs = [ + ("data/19dd1c444d1c7939.png", (5, 3), 64), + ("data/f73d2ee39133d871.jpg", (4, 3), 64), + ("data/shenzi.png", (3, 3), 16), + ]; + + for (input, expected_components, expected_skip) in inputs { + let img = image::open(input).unwrap(); + let (width, height) = img.dimensions(); + + let components = super::calculate_components(crate::ImageBounds { width, height }); + let skip = super::calculate_skip(crate::ImageBounds { width, height }); + + assert_eq!( + (components.x, components.y), + expected_components, + "wrong ratio for {input}" + ); + assert_eq!(skip, expected_skip, "wrong skip for {input}"); } } @@ -340,7 +457,7 @@ mod tests { ) .unwrap(); - assert_eq!(hash, output, "expected output wrong"); + assert_eq!(hash, output, "wrong output for {input}"); } } @@ -381,7 +498,7 @@ mod tests { let b2 = encoder.finalize(); - assert_eq!(b1, b2); + assert_eq!(b1, b2, "wrong hash for {input} with {chunk_count} chunks"); } } }