blurhash_update/
lib.rs

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
13/// How many components should be used in blurhash creation
14///
15/// More components will increase the definition of the blurhash, but also increase processing
16/// time.
17pub struct Components {
18    /// How many components to process in the x direction
19    pub x: u32,
20
21    /// How many components to process in the y direction
22    pub y: u32,
23}
24
25/// Bounds for the input image
26///
27/// These are required since they can not be inferred from the RGBA values
28#[derive(Clone, Copy, Debug)]
29pub struct ImageBounds {
30    /// The input image's width
31    pub width: u32,
32
33    /// The input image's height
34    pub height: u32,
35}
36
37struct ComponentState {
38    x: u32,
39    y: u32,
40    basis: f32,
41}
42
43/// Error raised when too many components are requested
44#[derive(Debug)]
45pub enum ConfigurationError {
46    /// Component values are not within the required range.
47    InvalidComponentCount,
48
49    /// Skip value must not be zero
50    ZeroSkip,
51}
52
53/// Encoder type used to produce blurhashes
54pub struct Encoder {
55    index: usize,
56    skip: u32,
57    components: Components,
58    factors: Box<[(ComponentState, [f32; BYTES_PER_PIXEL])]>,
59    bounds: ImageBounds,
60}
61
62/// A simple "encode this image please" function
63///
64/// The input image must be in the sRGB colorspace and be formatted as 8bit RGBA
65pub 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
75/// A simple "encode this image please" function that automatically selects component and skip
76/// values
77///
78/// The input image must be in the sRGB colorspace and be formatted as 8bit RGBA
79pub 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
85// determine closest component ratio to input bounds
86fn 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
130// target 256ish total pixels to process
131fn 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    /// Create an encoder that automatically picks Compoent and Skip values
147    ///
148    /// This is a best-effort configuration
149    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    /// Create a new Encoder to produce a blurhash
155    ///
156    /// The provided component x and y values must be between 1 and 9 inclusive.
157    ///
158    /// The `skip` value indicates how many pixels can be skipped when proccessing the image. this
159    /// value will be squared to produce the final skip value. When set to 1, no pixels will be
160    /// skipped, when set to 2, one in four pixels will be processed. when set to 3, one in 9
161    /// pixels will be processed, etc. This improves performance at the cost of losing accuracy.
162    ///
163    /// Errors if too many components are requested
164    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    /// Update the encoder with bytes from an image
193    ///
194    /// The input image must be in the sRGB colorspace and be formatted as 8bit RGBA
195    /// The input doesn't need to contain whole pixels, the encoder is capable of handling partial
196    /// pixels
197    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        // get offset in terms of already-processed bytes
276        let offset = self.index % BYTES_PER_PIXEL;
277        // get offset in terms of remaining bytes on head of rgba8_image
278        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    /// Produce a blurhash from the provided encoder
349    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}