diff --git a/Cargo.toml b/Cargo.toml index e1f63f0..cddcbfc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,3 +6,12 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] + +[dev-dependencies] +blurhash = { version = "0.2.0", path = "../blurhash-rs" } +criterion = "0.5.1" +image = "0.24.8" + +[[bench]] +name = "benchy" +harness = false diff --git a/benches/benchy.rs b/benches/benchy.rs new file mode 100644 index 0000000..43df7ba --- /dev/null +++ b/benches/benchy.rs @@ -0,0 +1,63 @@ +use blurhash_update::{Components, ImageBounds}; +use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; +use image::EncodableLayout; + +pub fn criterion_benchmark(c: &mut Criterion) { + let mut group = c.benchmark_group("blurhash"); + + 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::encode(4, 3, width, height, black_box(bytes)).unwrap()); + }); + }); + } + + group.finish(); + + let mut group = c.benchmark_group("blurhash-update"); + + 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 mut encoder = blurhash_update::encoder( + Components { x: 4, y: 3 }, + ImageBounds { width, height }, + ) + .unwrap(); + encoder.update(bytes); + let _bhash = black_box(encoder.finalize()); + }); + }); + } + + group.finish(); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/data/19dd1c444d1c7939.png b/data/19dd1c444d1c7939.png new file mode 100644 index 0000000..efbe62a Binary files /dev/null and b/data/19dd1c444d1c7939.png differ diff --git a/data/f73d2ee39133d871.jpg b/data/f73d2ee39133d871.jpg new file mode 100644 index 0000000..671b307 Binary files /dev/null and b/data/f73d2ee39133d871.jpg differ diff --git a/data/shenzi.png b/data/shenzi.png new file mode 100644 index 0000000..c565ff4 Binary files /dev/null and b/data/shenzi.png differ diff --git a/src/base83.rs b/src/base83.rs new file mode 100644 index 0000000..50eea88 --- /dev/null +++ b/src/base83.rs @@ -0,0 +1,15 @@ +static CHARACTERS: [u8; 83] = [ + b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', b'A', b'B', b'C', b'D', b'E', b'F', + b'G', b'H', b'I', b'J', b'K', b'L', b'M', b'N', b'O', b'P', b'Q', b'R', b'S', b'T', b'U', b'V', + b'W', b'X', b'Y', b'Z', b'a', b'b', b'c', b'd', b'e', b'f', b'g', b'h', b'i', b'j', b'k', b'l', + b'm', b'n', b'o', b'p', b'q', b'r', b's', b't', b'u', b'v', b'w', b'x', b'y', b'z', b'#', b'$', + b'%', b'*', b'+', b',', b'-', b'.', b':', b';', b'=', b'?', b'@', b'[', b']', b'^', b'_', b'{', + b'|', b'}', b'~', +]; + +pub(crate) fn encode(value: u32, length: u32, s: &mut String) { + for i in 1..=length { + let digit: u32 = (value / u32::pow(83, length - i)) % 83; + s.push(CHARACTERS[digit as usize] as char); + } +} diff --git a/src/lib.rs b/src/lib.rs index 7d12d9a..1723890 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,321 @@ -pub fn add(left: usize, right: usize) -> usize { - left + right +mod base83; +mod srgb_lookup; + +use std::f32::consts::PI; + +use srgb_lookup::SRGB_LOOKUP; + +pub struct Components { + pub x: u32, + pub y: u32, +} + +#[derive(Clone, Copy, Debug)] +pub struct ImageBounds { + pub width: u32, + pub height: u32, +} + +struct ComponentState { + x: u32, + y: u32, + basis: f32, +} + +#[derive(Debug)] +pub struct ComponentError; + +impl std::fmt::Display for ComponentError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Components out of bounds") + } +} + +pub struct Encoder { + index: usize, + components: Components, + factors: Vec<(ComponentState, [f32; 3])>, + bounds: ImageBounds, +} + +pub fn encoder(components: Components, bounds: ImageBounds) -> Result { + Encoder::new(components, bounds) +} + +impl Encoder { + fn new(Components { x, y }: Components, bounds: ImageBounds) -> Result { + if !(1..=9).contains(&x) || !(1..=9).contains(&y) { + return Err(ComponentError); + } + + Ok(Self { + index: 0, + components: Components { x, y }, + factors: (0..y) + .flat_map(|y| { + (0..x).map(move |x| (ComponentState { x, y, basis: 0. }, [0., 0., 0.])) + }) + .collect(), + bounds, + }) + } + + pub fn update(&mut self, buf: &[u8]) { + const BYTES_PER_PIXEL: usize = 4; + + // get offset in terms of already-processed bytes + let offset = self.index % BYTES_PER_PIXEL; + // get offset in terms of remaining bytes on head of buf + let offset = (BYTES_PER_PIXEL - offset) % BYTES_PER_PIXEL; + + for (ComponentState { basis, .. }, [_, g, b]) in self.factors.iter_mut() { + for (byte, value) in buf[..offset].iter().zip( + [&mut *b, &mut *g][0..offset.saturating_sub(2)] + .iter_mut() + .rev(), + ) { + **value += *basis * SRGB_LOOKUP[*byte as usize] + } + } + + let pixels = ((self.index + offset) / BYTES_PER_PIXEL) as u32; + + let mut chunks = buf[offset..].chunks_exact(BYTES_PER_PIXEL); + + for (i, chunk) in (&mut chunks).enumerate() { + let px = pixels + i as u32; + let px_x = px % self.bounds.width; + let px_y = px / self.bounds.width; + + for (ComponentState { x, y, .. }, [r, g, b]) in self.factors.iter_mut() { + let basis = compute_basis( + *x as _, + *y as _, + px_x as _, + px_y as _, + self.bounds.width as _, + self.bounds.height as _, + ); + + *r += basis * SRGB_LOOKUP[chunk[0] as usize]; + *g += basis * SRGB_LOOKUP[chunk[1] as usize]; + *b += basis * SRGB_LOOKUP[chunk[2] as usize]; + } + } + + if !chunks.remainder().is_empty() { + let px = pixels + (buf[offset..].len() / BYTES_PER_PIXEL) as u32; + let px_x = px % self.bounds.width; + let px_y = px / self.bounds.width; + + for (ComponentState { x, y, basis }, [r, g, b]) in self.factors.iter_mut() { + *basis = compute_basis( + *x as _, + *y as _, + px_x as _, + px_y as _, + self.bounds.width as _, + self.bounds.height as _, + ); + + for (byte, value) in chunks.remainder().iter().zip([&mut *r, &mut *g, &mut *b]) { + *value += *basis * SRGB_LOOKUP[*byte as usize] + } + } + } + + self.index += buf.len(); + } + + pub fn finalize(mut self) -> String { + for (ComponentState { x, y, .. }, [r, g, b]) in &mut self.factors { + let normalisation = if *x == 0 && *y == 0 { 1. } else { 2. }; + + let scale = normalisation / (self.bounds.width * self.bounds.height) as f32; + + *r *= scale; + *g *= scale; + *b *= scale; + } + + let mut blurhash = String::new(); + + let (_, dc) = self.factors[0]; + let ac = &self.factors[1..]; + + let size_flag = self.components.x - 1 + (self.components.y - 1) * 9; + base83::encode(size_flag, 1, &mut blurhash); + + let maximum_value = if !ac.is_empty() { + let maximum = ac.iter().fold(0.0_f32, |maximum, (_, [r, g, b])| { + maximum.max(r.abs()).max(g.abs()).max(b.abs()) + }); + + let quantized_maximum = (maximum * 166. - 0.5).floor().max(0.) as u32; + + base83::encode(quantized_maximum, 1, &mut blurhash); + + (quantized_maximum + 1) as f32 / 166. + } else { + base83::encode(0, 1, &mut blurhash); + + 1. + }; + + base83::encode(encode_dc(dc), 4, &mut blurhash); + + for (_, rgb) in ac { + base83::encode(encode_ac(*rgb, maximum_value), 2, &mut blurhash); + } + + blurhash + } +} + +fn compute_basis( + component_x: f32, + component_y: f32, + px_x: f32, + px_y: f32, + width: f32, + height: f32, +) -> f32 { + f32::cos(PI * component_x * px_x / width) * f32::cos(PI * component_y * px_y / height) +} + +fn encode_dc([r, g, b]: [f32; 3]) -> u32 { + let r = linear_to_srgb(r); + let g = linear_to_srgb(g); + let b = linear_to_srgb(b); + + (r << 16) + (g << 8) + b +} + +fn encode_ac([r, g, b]: [f32; 3], maximum_value: f32) -> u32 { + let r = encode_ac_digit(r, maximum_value); + let g = encode_ac_digit(g, maximum_value); + let b = encode_ac_digit(b, maximum_value); + + r * 19 * 19 + g * 19 + b +} + +fn encode_ac_digit(d: f32, maximum_value: f32) -> u32 { + ((sign_pow(d / maximum_value, 0.5) * 9. + 9.5) as i32).clamp(0, 18) as u32 +} + +pub fn linear_to_srgb(value: f32) -> u32 { + let v = f32::max(0., f32::min(1., value)); + if v <= 0.003_130_8 { + (v * 12.92 * 255. + 0.5).round() as u32 + } else { + ((1.055 * f32::powf(v, 1. / 2.4) - 0.055) * 255. + 0.5).round() as u32 + } +} + +fn sign(n: f32) -> f32 { + if n < 0. { + -1. + } else { + 1. + } +} + +pub fn sign_pow(val: f32, exp: f32) -> f32 { + sign(val) * val.abs().powf(exp) } #[cfg(test)] mod tests { - use super::*; + use image::{EncodableLayout, GenericImageView}; #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); + fn contrived() { + let input = [ + 0, 60, 120, 0, 0, 60, 120, 0, 0, 60, 120, 0, 0, 60, 120, 0, 0, 60, 120, 0, 0, 60, 120, + 0, 0, 60, 120, 0, 0, 60, 120, 0, 120, 60, 0, 0, 120, 60, 0, 0, 120, 60, 0, 0, 120, 60, + 0, 0, 120, 60, 0, 0, 120, 60, 0, 0, 120, 60, 0, 0, 120, 60, 0, 0, + ]; + let width = 4; + let height = 4; + + let mut encoder = super::encoder( + crate::Components { x: 4, y: 3 }, + crate::ImageBounds { width, height }, + ) + .unwrap(); + encoder.update(&input); + let b1 = encoder.finalize(); + + let b2 = blurhash::encode(4, 3, width, height, &input).unwrap(); + + assert_eq!(b1, b2); + } + + #[test] + fn matches_blurhash() { + let inputs = [ + "data/19dd1c444d1c7939.png", + "data/f73d2ee39133d871.jpg", + "data/shenzi.png", + ]; + + for input in inputs { + let img = image::open(input).unwrap(); + let (width, height) = img.dimensions(); + + let mut encoder = super::encoder( + crate::Components { x: 4, y: 3 }, + crate::ImageBounds { width, height }, + ) + .unwrap(); + encoder.update(img.to_rgba8().as_bytes()); + let b1 = encoder.finalize(); + + let b2 = blurhash::encode(4, 3, width, height, img.to_rgba8().as_bytes()).unwrap(); + + assert_eq!(b1, b2); + } + } + + #[test] + fn matches_self_when_split() { + let inputs = [ + "data/19dd1c444d1c7939.png", + "data/f73d2ee39133d871.jpg", + "data/shenzi.png", + ]; + + for input in inputs { + let img = image::open(input).unwrap(); + let (width, height) = img.dimensions(); + let rgba8_img = img.to_rgba8(); + let bytes = rgba8_img.as_bytes(); + + let mut encoder = super::encoder( + crate::Components { x: 4, y: 3 }, + crate::ImageBounds { width, height }, + ) + .unwrap(); + encoder.update(bytes); + let b1 = encoder.finalize(); + + for chunk_count in 2..20 { + encoder = super::encoder( + crate::Components { x: 4, y: 3 }, + crate::ImageBounds { width, height }, + ) + .unwrap(); + + let chunk_size = bytes.len() / chunk_count; + + for chunk in bytes.chunks(chunk_size) { + encoder.update(chunk); + } + + let b2 = encoder.finalize(); + + assert_eq!(b1, b2); + } + } } } diff --git a/src/srgb_lookup.rs b/src/srgb_lookup.rs new file mode 100644 index 0000000..5102d03 --- /dev/null +++ b/src/srgb_lookup.rs @@ -0,0 +1,258 @@ +pub(crate) static SRGB_LOOKUP: [f32; 256] = [ + 0.0, + 0.000303527, + 0.000607054, + 0.000910581, + 0.001214108, + 0.001517635, + 0.001821162, + 0.0021246888, + 0.002428216, + 0.002731743, + 0.00303527, + 0.0033465356, + 0.003676507, + 0.004024717, + 0.004391442, + 0.0047769533, + 0.005181517, + 0.0056053917, + 0.0060488326, + 0.006512091, + 0.00699541, + 0.0074990317, + 0.008023192, + 0.008568125, + 0.009134057, + 0.009721218, + 0.010329823, + 0.010960094, + 0.011612245, + 0.012286487, + 0.012983031, + 0.013702081, + 0.014443844, + 0.015208514, + 0.015996292, + 0.016807375, + 0.017641952, + 0.018500218, + 0.019382361, + 0.020288562, + 0.02121901, + 0.022173883, + 0.023153365, + 0.02415763, + 0.025186857, + 0.026241222, + 0.027320892, + 0.028426038, + 0.029556833, + 0.03071344, + 0.03189603, + 0.033104762, + 0.034339808, + 0.035601314, + 0.036889445, + 0.038204364, + 0.039546236, + 0.0409152, + 0.04231141, + 0.043735027, + 0.045186203, + 0.046665084, + 0.048171822, + 0.049706563, + 0.051269468, + 0.052860655, + 0.05448028, + 0.056128494, + 0.057805434, + 0.05951124, + 0.06124607, + 0.06301003, + 0.06480328, + 0.06662595, + 0.06847818, + 0.07036011, + 0.07227186, + 0.07421358, + 0.07618539, + 0.07818743, + 0.08021983, + 0.082282715, + 0.084376216, + 0.086500466, + 0.088655606, + 0.09084173, + 0.09305898, + 0.095307484, + 0.09758736, + 0.09989874, + 0.10224175, + 0.10461649, + 0.10702311, + 0.10946172, + 0.111932434, + 0.11443538, + 0.11697067, + 0.119538434, + 0.1221388, + 0.12477184, + 0.1274377, + 0.13013649, + 0.13286833, + 0.13563335, + 0.13843162, + 0.1412633, + 0.14412849, + 0.14702728, + 0.1499598, + 0.15292616, + 0.15592647, + 0.15896086, + 0.1620294, + 0.16513222, + 0.1682694, + 0.1714411, + 0.17464739, + 0.17788841, + 0.18116423, + 0.18447499, + 0.18782076, + 0.19120167, + 0.19461781, + 0.1980693, + 0.20155624, + 0.2050787, + 0.20863685, + 0.21223073, + 0.21586053, + 0.21952623, + 0.22322798, + 0.22696589, + 0.23074007, + 0.23455065, + 0.23839766, + 0.2422812, + 0.2462014, + 0.25015837, + 0.25415218, + 0.2581829, + 0.26225072, + 0.26635566, + 0.27049786, + 0.27467737, + 0.27889434, + 0.2831488, + 0.2874409, + 0.2917707, + 0.29613832, + 0.30054384, + 0.30498737, + 0.30946895, + 0.31398875, + 0.31854683, + 0.32314324, + 0.32777813, + 0.33245158, + 0.33716366, + 0.34191445, + 0.3467041, + 0.3515327, + 0.35640025, + 0.36130688, + 0.3662527, + 0.37123778, + 0.37626222, + 0.3813261, + 0.38642952, + 0.39157256, + 0.3967553, + 0.40197787, + 0.4072403, + 0.4125427, + 0.41788515, + 0.42326775, + 0.42869055, + 0.4341537, + 0.43965724, + 0.44520125, + 0.45078585, + 0.45641106, + 0.46207705, + 0.46778384, + 0.47353154, + 0.47932023, + 0.48514998, + 0.4910209, + 0.49693304, + 0.5028866, + 0.50888145, + 0.5149178, + 0.5209957, + 0.5271152, + 0.5332765, + 0.5394796, + 0.5457246, + 0.5520115, + 0.5583405, + 0.56471163, + 0.5711249, + 0.5775805, + 0.5840785, + 0.5906189, + 0.5972019, + 0.6038274, + 0.6104956, + 0.61720663, + 0.62396044, + 0.6307572, + 0.63759696, + 0.64447975, + 0.6514057, + 0.65837485, + 0.66538733, + 0.6724432, + 0.67954254, + 0.68668544, + 0.6938719, + 0.701102, + 0.70837593, + 0.71569365, + 0.72305524, + 0.7304609, + 0.73791057, + 0.74540436, + 0.7529423, + 0.76052463, + 0.7681513, + 0.77582234, + 0.7835379, + 0.79129803, + 0.79910284, + 0.80695236, + 0.8148467, + 0.82278585, + 0.83076996, + 0.8387991, + 0.8468733, + 0.8549927, + 0.8631573, + 0.8713672, + 0.87962234, + 0.8879232, + 0.8962694, + 0.90466136, + 0.9130987, + 0.92158204, + 0.9301109, + 0.9386859, + 0.9473066, + 0.9559735, + 0.9646863, + 0.9734455, + 0.9822506, + 0.9911022, + 1.0, +];