1#![doc = include_str!("../README.md")]
2#![deny(missing_docs)]
3
4mod base83;
5mod srgb_lookup;
6
7use std::f32::consts::PI;
8
9use srgb_lookup::srgb_to_linear;
10
11const BYTES_PER_PIXEL: usize = 4;
12
13pub struct Components {
18 pub x: u32,
20
21 pub y: u32,
23}
24
25#[derive(Clone, Copy, Debug)]
29pub struct ImageBounds {
30 pub width: u32,
32
33 pub height: u32,
35}
36
37struct ComponentState {
38 x: u32,
39 y: u32,
40 basis: f32,
41}
42
43#[derive(Debug)]
45pub enum ConfigurationError {
46 InvalidComponentCount,
48
49 ZeroSkip,
51}
52
53pub struct Encoder {
55 index: usize,
56 skip: u32,
57 components: Components,
58 factors: Box<[(ComponentState, [f32; BYTES_PER_PIXEL])]>,
59 bounds: ImageBounds,
60}
61
62pub fn encode(
66 components: Components,
67 bounds: ImageBounds,
68 rgba8_image: &[u8],
69) -> Result<String, ConfigurationError> {
70 let mut encoder = Encoder::new(components, bounds, 1)?;
71 encoder.update(rgba8_image);
72 Ok(encoder.finalize())
73}
74
75pub fn auto_encode(bounds: ImageBounds, rgba8_image: &[u8]) -> String {
80 let mut encoder = Encoder::auto(bounds);
81 encoder.update(rgba8_image);
82 encoder.finalize()
83}
84
85fn calculate_components(ImageBounds { width, height }: ImageBounds) -> Components {
87 let mut out = Components { x: 0, y: 0 };
88
89 let (out_longer, out_shorter, in_longer, in_shorter) = if width > height {
90 (&mut out.x, &mut out.y, width as f32, height as f32)
91 } else {
92 (&mut out.y, &mut out.x, height as f32, width as f32)
93 };
94
95 struct State {
96 similarity: f32,
97 ratio: (u32, u32),
98 }
99
100 let ratios = [(3, 3), (4, 3), (5, 3), (6, 3), (5, 2), (6, 2), (7, 2)];
101
102 let in_ratio = in_longer / in_shorter;
103
104 let State { ratio, .. } = ratios.into_iter().fold(
105 State {
106 similarity: f32::MAX,
107 ratio: (0, 0),
108 },
109 |state, (ratio_longer, ratio_shorter)| {
110 let ratio = ratio_longer as f32 / ratio_shorter as f32;
111 let diff = (ratio - in_ratio).abs();
112
113 if diff < state.similarity {
114 State {
115 similarity: diff,
116 ratio: (ratio_longer, ratio_shorter),
117 }
118 } else {
119 state
120 }
121 },
122 );
123
124 *out_longer = ratio.0;
125 *out_shorter = ratio.1;
126
127 out
128}
129
130fn calculate_skip(ImageBounds { width, height }: ImageBounds) -> u32 {
132 let target_1d = f32::sqrt((width * height / 512) as f32).floor() as u32;
133
134 let mut base = 1;
135
136 loop {
137 if base * 2 < target_1d {
138 base *= 2;
139 } else {
140 break base;
141 }
142 }
143}
144
145impl Encoder {
146 pub fn auto(bounds: ImageBounds) -> Self {
150 Self::new(calculate_components(bounds), bounds, calculate_skip(bounds))
151 .expect("Generated bounds are always valid")
152 }
153
154 pub fn new(
165 Components { x, y }: Components,
166 bounds: ImageBounds,
167 skip: u32,
168 ) -> Result<Self, ConfigurationError> {
169 if !(1..=9).contains(&x) || !(1..=9).contains(&y) {
170 return Err(ConfigurationError::InvalidComponentCount);
171 }
172
173 if skip == 0 {
174 return Err(ConfigurationError::ZeroSkip);
175 }
176
177 Ok(Self {
178 index: 0,
179 skip,
180 components: Components { x, y },
181 factors: Box::from(
182 (0..y)
183 .flat_map(|y| {
184 (0..x).map(move |x| (ComponentState { x, y, basis: 0. }, [0., 0., 0., 0.]))
185 })
186 .collect::<Vec<_>>(),
187 ),
188 bounds,
189 })
190 }
191
192 pub fn update(&mut self, rgba8_image: &[u8]) {
198 if self.skip == 1 {
199 self.update_noskip(rgba8_image)
200 } else {
201 self.update_skip(rgba8_image)
202 }
203 }
204
205 fn update_skip(&mut self, rgba8_image: &[u8]) {
206 let basis_scale_x = PI / self.bounds.width as f32;
207 let basis_scale_y = PI / self.bounds.height as f32;
208
209 let mut current_index = self.index;
210
211 loop {
212 let (px_x, px_y) = self.next_px(current_index);
213
214 let scale_x = px_x as f32 * basis_scale_x;
215 let scale_y = px_y as f32 * basis_scale_y;
216
217 let next_index = (px_y * self.bounds.width + px_x) as usize * BYTES_PER_PIXEL;
218
219 let skip_rgb = current_index.saturating_sub(next_index);
220 let index_into = next_index.saturating_sub(self.index);
221
222 if index_into >= rgba8_image.len() {
223 break;
224 }
225
226 assert!(skip_rgb < BYTES_PER_PIXEL, "{skip_rgb}");
227
228 for (ComponentState { x, y, basis }, rgb) in self.factors.iter_mut() {
229 *basis = f32::cos(*x as f32 * scale_x) * f32::cos(*y as f32 * scale_y);
230
231 let slot_iter = rgb.iter_mut().skip(skip_rgb);
232 let value_iter = rgba8_image[index_into..]
233 .iter()
234 .take(BYTES_PER_PIXEL)
235 .map(|byte| *basis * srgb_to_linear(*byte));
236
237 for (val, slot) in value_iter.zip(slot_iter) {
238 *slot += val;
239 }
240 }
241
242 current_index = next_index + BYTES_PER_PIXEL;
243 }
244
245 self.index += rgba8_image.len();
246 }
247
248 fn next_px(&self, index: usize) -> (u32, u32) {
249 let pixel = (index / BYTES_PER_PIXEL) as u32;
250 let pixel_x = pixel % self.bounds.width;
251 let pixel_y = pixel / self.bounds.width;
252
253 let y_offset = pixel_y % self.skip;
254
255 if y_offset == 0 {
256 let x_offset = pixel_x % self.skip;
257
258 if x_offset == 0 {
259 (pixel_x, pixel_y)
260 } else {
261 let next_px_x = pixel_x + self.skip - x_offset;
262
263 if next_px_x >= self.bounds.width {
264 (0, pixel_y + self.skip)
265 } else {
266 (next_px_x, pixel_y)
267 }
268 }
269 } else {
270 (0, pixel_y + self.skip - y_offset)
271 }
272 }
273
274 fn update_noskip(&mut self, rgba8_image: &[u8]) {
275 let offset = self.index % BYTES_PER_PIXEL;
277 let offset = (BYTES_PER_PIXEL - offset) % BYTES_PER_PIXEL;
279
280 let basis_scale_x = PI / self.bounds.width as f32;
281 let basis_scale_y = PI / self.bounds.height as f32;
282
283 for (ComponentState { basis, .. }, [_, g, b, _]) in self.factors.iter_mut() {
284 for (val, slot) in rgba8_image[..offset]
285 .iter()
286 .map(|byte| *basis * srgb_to_linear(*byte))
287 .zip(
288 [b, g][..offset.saturating_sub(BYTES_PER_PIXEL - 2)]
289 .iter_mut()
290 .rev(),
291 )
292 {
293 **slot += val;
294 }
295 }
296
297 let pixels = ((self.index + offset) / BYTES_PER_PIXEL) as u32;
298
299 let mut chunks = rgba8_image[offset..].chunks_exact(BYTES_PER_PIXEL);
300
301 for (i, chunk) in (&mut chunks).enumerate() {
302 let px = pixels + i as u32;
303 let px_x = px % self.bounds.width;
304 let px_y = px / self.bounds.width;
305
306 let scale_x = px_x as f32 * basis_scale_x;
307 let scale_y = px_y as f32 * basis_scale_y;
308
309 for (ComponentState { x, y, .. }, rgb) in self.factors.iter_mut() {
310 let basis = f32::cos(*x as f32 * scale_x) * f32::cos(*y as f32 * scale_y);
311
312 assert_eq!(chunk.len(), rgb.len());
313 for (val, slot) in chunk
314 .iter()
315 .map(|byte| basis * srgb_to_linear(*byte))
316 .zip(rgb)
317 {
318 *slot += val;
319 }
320 }
321 }
322
323 if !chunks.remainder().is_empty() {
324 let px = pixels + (rgba8_image[offset..].len() / BYTES_PER_PIXEL) as u32;
325 let px_x = px % self.bounds.width;
326 let px_y = px / self.bounds.width;
327
328 let scale_x = px_x as f32 * basis_scale_x;
329 let scale_y = px_y as f32 * basis_scale_y;
330
331 for (ComponentState { x, y, basis }, rgb) in self.factors.iter_mut() {
332 *basis = f32::cos(*x as f32 * scale_x) * f32::cos(*y as f32 * scale_y);
333
334 for (val, slot) in chunks
335 .remainder()
336 .iter()
337 .map(|byte| *basis * srgb_to_linear(*byte))
338 .zip(rgb)
339 {
340 *slot += val;
341 }
342 }
343 }
344
345 self.index += rgba8_image.len();
346 }
347
348 pub fn finalize(mut self) -> String {
350 for (ComponentState { x, y, .. }, rgb) in self.factors.iter_mut() {
351 let normalisation = if *x == 0 && *y == 0 { 1. } else { 2. };
352
353 let scale = self.skip.pow(2) as f32 * normalisation
354 / (self.bounds.width * self.bounds.height) as f32;
355
356 for slot in rgb {
357 *slot *= scale;
358 }
359 }
360
361 let mut blurhash = String::with_capacity(30);
362
363 let (_, dc) = self.factors[0];
364 let ac = &self.factors[1..];
365
366 let size_flag = self.components.x - 1 + (self.components.y - 1) * 9;
367 base83::encode(size_flag, 1, &mut blurhash);
368
369 let maximum = ac.iter().fold(0.0_f32, |maximum, (_, [r, g, b, _])| {
370 maximum.max(r.abs()).max(g.abs()).max(b.abs())
371 });
372
373 let quantized_maximum = (maximum * 166. - 0.5).floor().max(0.) as u32;
374
375 base83::encode(quantized_maximum, 1, &mut blurhash);
376
377 let maximum_value = (quantized_maximum + 1) as f32 / 166.;
378
379 base83::encode(encode_dc(dc), 4, &mut blurhash);
380
381 for (_, rgb) in ac {
382 base83::encode(encode_ac(*rgb, maximum_value), 2, &mut blurhash);
383 }
384
385 blurhash
386 }
387}
388
389fn encode_dc(rgb: [f32; BYTES_PER_PIXEL]) -> u32 {
390 let [r, g, b, _] = rgb.map(linear_to_srgb);
391
392 (r << 16) + (g << 8) + b
393}
394
395fn encode_ac(rgb: [f32; BYTES_PER_PIXEL], maximum_value: f32) -> u32 {
396 let [r, g, b, _] = rgb.map(|c| encode_ac_digit(c, maximum_value));
397
398 r * 19 * 19 + g * 19 + b
399}
400
401fn encode_ac_digit(d: f32, maximum_value: f32) -> u32 {
402 ((sign_pow(d / maximum_value, 0.5) * 9. + 9.5) as i32).clamp(0, 18) as u32
403}
404
405fn linear_to_srgb(value: f32) -> u32 {
406 let v = f32::max(0., f32::min(1., value));
407 if v <= 0.003_130_8 {
408 (v * 12.92 * 255. + 0.5).round() as u32
409 } else {
410 ((1.055 * f32::powf(v, 1. / 2.4) - 0.055) * 255. + 0.5).round() as u32
411 }
412}
413
414fn sign(n: f32) -> f32 {
415 if n < 0. {
416 -1.
417 } else {
418 1.
419 }
420}
421
422fn sign_pow(val: f32, exp: f32) -> f32 {
423 sign(val) * val.abs().powf(exp)
424}
425
426impl std::fmt::Display for ConfigurationError {
427 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
428 match self {
429 Self::InvalidComponentCount => write!(f, "Components out of bounds"),
430 Self::ZeroSkip => write!(f, "Skip value cannot be zero"),
431 }
432 }
433}
434
435impl std::error::Error for ConfigurationError {}
436
437#[cfg(test)]
438mod tests {
439 use image::{EncodableLayout, GenericImageView};
440
441 #[test]
442 fn contrived() {
443 let input = [
444 0, 60, 120, 0, 0, 60, 120, 0, 0, 60, 120, 0, 0, 60, 120, 0, 0, 60, 120, 0, 0, 60, 120,
445 0, 0, 60, 120, 0, 0, 60, 120, 0, 120, 60, 0, 0, 120, 60, 0, 0, 120, 60, 0, 0, 120, 60,
446 0, 0, 120, 60, 0, 0, 120, 60, 0, 0, 120, 60, 0, 0, 120, 60, 0, 0,
447 ];
448 let width = 4;
449 let height = 4;
450
451 let hash = super::encode(
452 crate::Components { x: 4, y: 3 },
453 crate::ImageBounds { width, height },
454 &input,
455 )
456 .unwrap();
457
458 assert_eq!(hash, "LQ9~?d$,fQ$,G1S%fQS%A{SPfQSP");
459 }
460
461 #[test]
462 fn one_component() {
463 let inputs = [
464 ("data/19dd1c444d1c7939.png", "00AQtR"),
465 ("data/f73d2ee39133d871.jpg", "00E{R{"),
466 ("data/shenzi.png", "0039[D"),
467 ];
468
469 for (input, output) in inputs {
470 let img = image::open(input).unwrap();
471 let (width, height) = img.dimensions();
472
473 let hash = super::encode(
474 crate::Components { x: 1, y: 1 },
475 crate::ImageBounds { width, height },
476 img.to_rgba8().as_bytes(),
477 )
478 .unwrap();
479
480 assert_eq!(hash, output, "wrong output for {input}");
481 }
482 }
483
484 #[test]
485 fn auto() {
486 let inputs = [
487 ("data/19dd1c444d1c7939.png", (5, 3), 32),
488 ("data/f73d2ee39133d871.jpg", (4, 3), 32),
489 ("data/shenzi.png", (3, 3), 16),
490 ];
491
492 for (input, expected_components, expected_skip) in inputs {
493 let img = image::open(input).unwrap();
494 let (width, height) = img.dimensions();
495
496 let components = super::calculate_components(crate::ImageBounds { width, height });
497 let skip = super::calculate_skip(crate::ImageBounds { width, height });
498
499 assert_eq!(
500 (components.x, components.y),
501 expected_components,
502 "wrong ratio for {input}"
503 );
504 assert_eq!(skip, expected_skip, "wrong skip for {input}");
505 }
506 }
507
508 #[test]
509 fn matches_blurhash() {
510 let inputs = [
511 ("data/19dd1c444d1c7939.png", "L3AQtR2FSz6NrsOCW:ODR*,EE};h"),
512 ("data/f73d2ee39133d871.jpg", "LJE{R{Z}V?N#0JR*Rit7^htTfkaI"),
513 ("data/shenzi.png", "L239[DQ.91t,rJX9Qns+8zt5.PR6"),
514 ];
515
516 for (input, output) in inputs {
517 let img = image::open(input).unwrap();
518 let (width, height) = img.dimensions();
519
520 let hash = super::encode(
521 crate::Components { x: 4, y: 3 },
522 crate::ImageBounds { width, height },
523 img.to_rgba8().as_bytes(),
524 )
525 .unwrap();
526
527 assert_eq!(hash, output, "wrong output for {input}");
528 }
529 }
530
531 #[test]
532 fn matches_self_when_split() {
533 let inputs = [
534 "data/19dd1c444d1c7939.png",
535 "data/f73d2ee39133d871.jpg",
536 "data/shenzi.png",
537 ];
538
539 for input in inputs {
540 let img = image::open(input).unwrap();
541 let (width, height) = img.dimensions();
542 let rgba8_img = img.to_rgba8();
543 let bytes = rgba8_img.as_bytes();
544
545 let b1 = super::encode(
546 crate::Components { x: 4, y: 3 },
547 crate::ImageBounds { width, height },
548 bytes,
549 )
550 .unwrap();
551
552 for chunk_count in 2..20 {
553 let mut encoder = super::Encoder::new(
554 crate::Components { x: 4, y: 3 },
555 crate::ImageBounds { width, height },
556 1,
557 )
558 .unwrap();
559
560 let chunk_size = bytes.len() / chunk_count;
561
562 for chunk in bytes.chunks(chunk_size) {
563 encoder.update(chunk);
564 }
565
566 let b2 = encoder.finalize();
567
568 assert_eq!(b1, b2, "wrong hash for {input} with {chunk_count} chunks");
569 }
570 }
571 }
572}