fast_blurhash/
lib.rs

1//! # Fast(er) BlurHash
2//!
3//! Provides a faster implementation of the BlurHash algorithm for encoding
4//! and decoding BlurHashes. It minimizes the number of allocated arrays to reduce
5//! the memory footprint. The base83 encode and decode are also both very fast!
6//!
7//! #### Example
8//!
9//! Generating a blurhash from an image:
10//! ```no_run
11//! use fast_blurhash::compute_dct;
12//!
13//! let (width, height) = todo!("Get image width and height");
14//! let image: Vec<u32> = todo!("Load the image");
15//! let blurhash = compute_dct(&image, width, height, 3, 4).into_blurhash();
16//! ```
17//!
18//! Generating an image from a blurhash:
19//! ```
20//! use fast_blurhash::decode;
21//!
22//! let blurhash = "LlMF%n00%#MwS|WCWEM{R*bbWBbH";
23//! let image: Vec<u32> = decode(&blurhash, 1.).unwrap().to_rgba(32, 32);
24//! ```
25//!
26//! ## Custom color types
27//!
28//! **fast-blurhash** provides an easy way to convert custom types for pixel values
29//! into the linear space to be used by the algorithm. Simply implements the trait
30//! `AsLinear` on your type!
31//!
32//! #### Example
33//!
34//! ```no_run
35//! use fast_blurhash::{convert::{AsLinear, Linear, srgb_to_linear}, compute_dct};
36//!
37//! struct MyColor {
38//!     r: u8,
39//!     g: u8,
40//!     b: u8
41//! }
42//!
43//! impl AsLinear for MyColor {
44//!     fn as_linear(&self) -> Linear {
45//!         [srgb_to_linear(self.r), srgb_to_linear(self.g), srgb_to_linear(self.b)]
46//!     }
47//! }
48//!
49//! // And then compute the blurhash!
50//! let (width, height) = todo!("Get image width and height");
51//! let image: Vec<MyColor> = todo!("Load the image");
52//! let blurhash = compute_dct(&image, width, height, 3, 4).into_blurhash();
53//! ```
54//!
55//! Several conversion function are available such as sRGB to Linear, check out the
56//! [`convert`] module.
57//!
58//! [`convert`]: convert/index.html
59//!
60//! You can also generate an image using your custom type:
61//! ```
62//! use fast_blurhash::{decode, convert::linear_to_srgb};
63//!
64//! struct MyColor {
65//!     r: u8,
66//!     g: u8,
67//!     b: u8
68//! }
69//!
70//! let blurhash = "LlMF%n00%#MwS|WCWEM{R*bbWBbH";
71//! let image: Vec<MyColor> = decode(&blurhash, 1.).unwrap().to_image(32, 32, |l| MyColor {
72//!     r: linear_to_srgb(l[0]),
73//!     g: linear_to_srgb(l[1]),
74//!     b: linear_to_srgb(l[2])
75//! });
76//! ```
77//!
78//! ## Using iterators
79//!
80//! You can also use the iterator version of the *compute_dct* function to
81//! prevent allocating more memory for the type conversion. This is espacially
82//! useful with nested types. Plus it has no performance overhead.
83//! However, make sure the iterator is long enough or the result of the DCT will
84//! be incorrect.
85//!
86//! #### Example
87//! ```no_run
88//! use fast_blurhash::{convert::{AsLinear, Linear, srgb_to_linear}, compute_dct_iter};
89//!
90//! struct Color(u8, u8, u8);
91//!
92//! impl AsLinear for &Color {
93//!     fn as_linear(&self) -> Linear {
94//!         [srgb_to_linear(self.0), srgb_to_linear(self.1), srgb_to_linear(self.2)]
95//!     }
96//! }
97//!
98//! // And then compute the blurhash!
99//! let (width, height) = todo!("Get image width and height");
100//! let image: Vec<Vec<Color>> = todo!("Load the image");
101//! let blurhash = compute_dct_iter(image.iter().flatten(), width, height, 3, 4).into_blurhash();
102//! ```
103
104pub mod base83;
105pub mod convert;
106
107use std::f32::consts::PI;
108use convert::*;
109use base83::encode_fixed_to;
110
111#[derive(Debug, Clone, Copy, PartialEq)]
112pub enum BlurhashError {
113    /// Occurs when the provided blurhash's lenght is not the same as the expected
114    /// length that was extracted from the 'header' (first char) of the blurhash
115    InvalidLength,
116    /// Occurs when the provided punch is not a non-zero positive float.
117    InvalidPunch,
118    /// The blurhash contains invalid base83 codes.
119    BadFormat(base83::Base83ConversionError),
120    /// The blurhash's number of X or Y components was greater than 9.
121    UnsupportedMode,
122}
123
124impl std::fmt::Display for BlurhashError {
125    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
126        use BlurhashError::*;
127        match self {
128            InvalidLength => write!(fmt, "The extracted length of the blurhash does not match the actual length"),
129            InvalidPunch => write!(fmt, "The punch parameter must be positive and non-zero"),
130            BadFormat(kind) => write!(fmt, "The blurhash contains invalid base83 codes ({kind:?})"),
131            UnsupportedMode => write!(fmt, "The blurhash's number of X or Y components was greater than 9")
132        }
133    }
134}
135
136impl From<base83::Base83ConversionError> for BlurhashError {
137    fn from(err: base83::Base83ConversionError) -> Self {
138        Self::BadFormat(err)
139    }
140}
141
142/// DCTResult is the result of a Discrete Cosine Transform performed on a image
143/// with a specific number of X and Y components. It stores the frequency and
144/// location of colors within the image.
145#[derive(Default, Clone, Debug)]
146pub struct DCTResult {
147    /// The absolute maximum value of each channel in the alternative currents
148    ac_max: f32,
149    /// 2D-array represented in row-major column (x_components columns and
150    /// y_components rows) that stores information about the average color in
151    /// the cosine distribution (kernel).
152    /// The totak number of currents is (x_components * y_components)
153    currents: Vec<Factor>,
154    /// Number of X components
155    x_components: usize,
156    /// Number of Y components
157    y_components: usize
158}
159
160impl DCTResult {
161    /// Store the result of a DCT
162    pub fn new(ac_max: f32, currents: Vec<Factor>, x_components: usize, y_components: usize) -> DCTResult {
163        assert!(currents.len() == x_components * y_components);
164        assert!(ac_max != 0.);
165
166        DCTResult { ac_max, currents, x_components, y_components }
167    }
168
169    /// Convert the computed color frequencies into a base83 string using
170    /// the wolt/blurhash algorithm.
171    pub fn into_blurhash(self) -> String {
172        encode(&self)
173    }
174
175    /// Generate an image from this DCT Result to recreate (sort of) the original
176    /// image. This function allocates a vector of (width * height) pixels in
177    /// the linear space.
178    pub fn to_image<T>(&self, width: usize, height: usize, convert: fn(Linear) -> T) -> Vec<T> {
179        let mut pixels = Vec::with_capacity(width * height);
180
181        for y in 0..height {
182            let percent_y = y as f32 / height as f32;
183            for x in 0..width {
184                let percent_x = x as f32 / width as f32;
185
186                let mut col = inv_multiply_basis(self.x_components, self.y_components,
187                    percent_x, percent_y, &self.currents);
188
189                col[0] = col[0].max(0.).min(1.);
190                col[1] = col[1].max(0.).min(1.);
191                col[2] = col[2].max(0.).min(1.);
192
193                pixels.push(convert(col));
194            }
195        }
196
197        pixels
198    }
199
200    /// Generate an image from this DCT Result to recreate (sort of) the original
201    /// image. This function allocates a vector of (width * height) pixels in
202    /// the sRGB space as in [RR, GG, BB].
203    pub fn to_rgb8(&self, width: usize, height: usize) -> Vec<[u8; 3]> {
204        self.to_image(width, height, |col| [
205            linear_to_srgb(col[0]),
206            linear_to_srgb(col[1]),
207            linear_to_srgb(col[2]),
208        ])
209    }
210
211    /// Generate an image from this DCT Result to recreate (sort of) the original
212    /// image. This function allocates a vector of (width * height) pixels in
213    /// the sRGB space as in [RR, GG, BB, AA]. (alpha will always be 255).
214    pub fn to_rgba8(&self, width: usize, height: usize) -> Vec<[u8; 4]> {
215        self.to_image(width, height, |col| [
216            linear_to_srgb(col[0]),
217            linear_to_srgb(col[1]),
218            linear_to_srgb(col[2]),
219            255
220        ])
221    }
222
223    /// Generate an image from this DCT Result to recreate (sort of) the original
224    /// image. This function allocates a vector of (width * height) u32 in
225    /// the sRGB space as in AARRGGBB in hex (alpha will always be 255).
226    pub fn to_rgba(&self, width: usize, height: usize) -> Vec<u32> {
227        self.to_image(width, height, |col|
228            ((linear_to_srgb(col[2]) as u32) <<  0) |
229            ((linear_to_srgb(col[1]) as u32) <<  8) |
230            ((linear_to_srgb(col[0]) as u32) << 16) |
231            ((255                    as u32) << 24)
232        )
233    }
234
235    /// Retrieve the currents of the DCT. The returned array is
236    /// a 2D-array represented in row-major column with
237    /// (x_components * y_components) items. Note that the first current is the
238    /// DC or Direct Current.
239    pub fn currents(&self) -> &[Factor] {
240        &self.currents
241    }
242
243    /// Retrieve the ACs or Alternative Currents of the DCT. The returned array is
244    /// a 2D-array represented in row-major column with (x_components * y_components) - 1 items.
245    /// Note that the first current, which is the DC or Direct Current, is not included.
246    pub fn acs(&self) -> &[Factor] {
247        &self.currents[1..]
248    }
249
250    /// Retrieve the Direct Current or DC of the DCT. It corresponds to the
251    /// average value of the components. For example, in an image it would be
252    /// the average color of the image.
253    pub fn dc(&self) -> &Factor {
254        &self.currents[0]
255    }
256
257    /// Retrive the number of X components
258    pub fn x_components(&self) -> usize {
259        self.x_components
260    }
261
262    /// Retrive the number of Y components
263    pub fn y_components(&self) -> usize {
264        self.y_components
265    }
266
267    // Retrive the dimension (x_components, y_components) of the computed DCT
268    pub fn dim(&self) -> (usize, usize) {
269        (self.x_components, self.y_components)
270    }
271}
272
273/// Compute the blurhash string from the DCT result using the wolt/blurhash format.
274/// This function allocates a string of length (1 + 1 + 4 + 2 * components) where
275/// components is the total number of components (components_x * components_y).
276pub fn encode(dct: &DCTResult) -> String {
277    let DCTResult { mut ac_max, currents, x_components, y_components } = dct;
278    assert!((1..=9).contains(x_components), "The number of X components must be between 1 and 9");
279    assert!((1..=9).contains(y_components), "The number of Y components must be between 1 and 9");
280
281    let mut blurhash = String::with_capacity(1 + 1 + 4 + 2 * (currents.len() - 1));
282
283    encode_fixed_to(((x_components - 1) + (y_components - 1) * 9) as u32, 1, &mut blurhash);
284
285    if currents.len() > 0 {
286        let quantised_max = (ac_max * 166. - 0.5).floor().min(82.).max(0.);
287        encode_fixed_to(quantised_max as u32, 1, &mut blurhash);
288        ac_max = (quantised_max + 1.) / 166.;
289    } else {
290        encode_fixed_to(0, 1, &mut blurhash);
291    }
292
293    encode_fixed_to(to_rgb(currents[0]), 4, &mut blurhash);
294
295    for &ac in currents.iter().skip(1) {
296        encode_fixed_to(encode_ac(ac, ac_max), 2, &mut blurhash);
297    }
298
299    blurhash
300}
301
302/// Decode a blurhash to retrive the DCT results (containing the color frequencies
303/// disposition) using the wolt/blurhash format. This function may allocate up to a
304/// vector of length 81 contained in the DCTResult struct.
305pub fn decode(blurhash: &str, punch: f32) -> Result<DCTResult, BlurhashError> {
306    if punch <= 0. {
307        return Err(BlurhashError::InvalidPunch)
308    }
309
310    if blurhash.is_empty() {
311        return Err(BlurhashError::InvalidLength)
312    }
313    let total = base83::decode(&blurhash[..1])? as usize;
314    let (x_components, y_components) = ((total % 9) + 1, (total / 9) + 1);
315
316    if x_components > 9 || y_components > 9 {
317        return Err(BlurhashError::UnsupportedMode)
318    }
319
320    let current_count = x_components * y_components;
321    if blurhash.len() != 1 + 1 + 4 + 2 * (current_count - 1) {
322        return Err(BlurhashError::InvalidLength)
323    }
324
325    let ac_max = base83::decode(&blurhash[1..2])? + 1;
326    let ac_max = ((ac_max as f32) / 166.) * punch;
327
328    let mut currents = Vec::with_capacity(current_count);
329    currents.push(decode_dc(base83::decode(&blurhash[2..6])?));
330
331    for i in 1..current_count {
332        let idx = (i - 1) * 2 + 6;
333        let ac = base83::decode(&blurhash[idx..(idx + 2)])?;
334        currents.push(decode_ac(ac, ac_max));
335    }
336
337    Ok(DCTResult { ac_max, currents, x_components, y_components })
338}
339
340/// Compute the Discrete Cosine Transform on an image in linear space. The iterator
341/// must be long enough (it must have at least width * height items).
342///
343/// Altought the function traverses only once the input image and allocates a
344/// vector of (x_components * y_components * 3) floats, the process may be a
345/// little slow depending on the size of the input image.
346///
347/// Note: To generate a valid blurhash, the number of X or/and Y components
348/// must be between 1 and 9. This is a limitation of the encoding scheme.
349pub fn compute_dct_iter<T: AsLinear>(image: impl Iterator<Item = T>, width: usize, height: usize, x_components: usize, y_components: usize) -> DCTResult {
350    let mut currents: Vec<Factor> = vec![[0., 0., 0.]; x_components * y_components];
351
352    let total = width * height;
353    for (i, pixel) in image.take(total).enumerate() {
354        let col = pixel.as_linear();
355
356        let p = i as f32 / width as f32;
357        let percent_y = p / height as f32;
358        let percent_x = p.fract();
359
360        multiply_basis(x_components, y_components, percent_x, percent_y, &col, &mut currents);
361    }
362
363    let ac_max = normalize_and_max(&mut currents, total);
364
365    DCTResult { ac_max, currents, x_components, y_components }
366}
367
368/// Compute the Discrete Cosine Transform on an image in linear space. The slice
369/// must be long enough (it must have at least width * height items).
370///
371/// Altought the function traverses only once the input image and allocates a
372/// vector of (x_components * y_components * 3) floats, the process may be a
373/// little slow depending on the size of the input image.
374///
375/// Note: To generate a valid blurhash, the number of X or/and Y components
376/// must be between 1 and 9. This is a limitation of the encoding scheme.
377pub fn compute_dct<T: AsLinear>(image: &[T], width: usize, height: usize, x_components: usize, y_components: usize) -> DCTResult {
378    assert!(image.len() >= width * height);
379    let mut currents: Vec<Factor> = vec![[0., 0., 0.]; x_components * y_components];
380
381    for y in 0..height {
382        let percent_y = y as f32 / height as f32;
383        for x in 0..width {
384            let percent_x = x as f32 / width as f32;
385
386            let col = image[y * width + x].as_linear();
387            multiply_basis(x_components, y_components, percent_x, percent_y, &col, &mut currents);
388        }
389    }
390
391    let ac_max = normalize_and_max(&mut currents, width * height);
392
393    DCTResult { ac_max, currents, x_components, y_components }
394}
395
396/// Compute an iteration of the DCT for every component on the pixel (x, y)
397/// that have the color `col` in linear space. Note that the currents slice must
398/// be long enough (x_comps * y_comps) and the pixel coordinates (x, y) are between
399/// 0 and 1.
400#[inline]
401pub fn multiply_basis(x_comps: usize, y_comps: usize, x: f32, y: f32, col: &[f32; 3], currents: &mut [Factor]) {
402    for comp_y in 0..y_comps {
403        let base_y = (PI * comp_y as f32 * y).cos();
404
405        for comp_x in 0..x_comps {
406            let f = &mut currents[comp_y * x_comps + comp_x];
407
408            let base_x = (PI * comp_x as f32 * x).cos();
409            let basis = base_y * base_x;
410
411            f[0] += basis * col[0];
412            f[1] += basis * col[1];
413            f[2] += basis * col[2];
414        }
415    }
416}
417
418/// Compute an iteration of the inverse DCT for every component on the pixel (x, y)
419/// and stores the color of that pixel into `col`. Note that the currents slice must
420/// be long enough (x_comps * y_comps).
421#[inline]
422pub fn inv_multiply_basis(x_comps: usize, y_comps: usize, x: f32, y: f32, currents: &[Factor]) -> [f32; 3] {
423    let mut col = [0.; 3];
424    for comp_y in 0..y_comps {
425        let base_y = (PI * comp_y as f32 * y).cos();
426
427        for comp_x in 0..x_comps {
428            let f = currents[comp_y * x_comps + comp_x];
429
430            let base_x = (PI * comp_x as f32 * x).cos();
431            let basis = base_y * base_x;
432
433            col[0] += basis * f[0];
434            col[1] += basis * f[1];
435            col[2] += basis * f[2];
436        }
437    }
438
439    col
440}
441
442/// Normalizes in-plae the currents by a predefined quantization table for the
443/// wolt/blurhash encoding algorithm (1 for DC and 2 for ACs) and returns the
444/// absolute maximum value within every channel of every currents. Note that
445/// currents must have one or more items and len is the total number of pixels
446/// of the image (width * height).
447pub fn normalize_and_max(currents: &mut [Factor], len: usize) -> f32 {
448    let len = len as f32;
449    let norm = 1. / len; // Normalisation for DC is 1
450    let f = &mut currents[0];
451    f[0] *= norm;
452    f[1] *= norm;
453    f[2] *= norm;
454
455    if currents.len() == 1 {
456        return 1.
457    }
458
459    let mut ac_max = 0f32;
460    let norm = 2. / len; // Normalisation for ACs is 2
461    for f in currents.iter_mut().skip(1).flatten() {
462        *f *= norm;
463        ac_max = ac_max.max(f.abs());
464    }
465
466    ac_max
467}
468
469#[cfg(test)]
470mod tests {
471    use super::*;
472
473    #[test]
474    fn test_multiply_basis() {
475        let width: usize = 4;
476        let height: usize = 4;
477        let x_comps: usize = 5;
478        let y_comps: usize = 5;
479        let image: [Linear; 16] = [
480            [1., 1., 1.], [0., 0., 0.], [1., 1., 1.], [0., 0., 0.],
481            [0., 0., 0.], [0., 0., 0.], [1., 1., 1.], [0., 0., 0.],
482            [1., 1., 1.], [1., 1., 1.], [1., 1., 1.], [1., 1., 1.],
483            [0., 0., 0.], [0., 0., 0.], [1., 1., 1.], [0., 0., 0.],
484        ];
485        let mut currents: Vec<Factor> = vec![[0., 0., 0.]; x_comps * y_comps];
486
487        for y in 0..height {
488            let percent_y = y as f32 / height as f32;
489            for x in 0..width {
490                let percent_x = x as f32 / width as f32;
491                multiply_basis(x_comps, y_comps, percent_x, percent_y,
492                    &image[y * width + x], &mut currents);
493            }
494        }
495
496        let average_color = [8., 8., 8.]; // 8/16 of the colors are black
497        assert_eq!(currents[0 * x_comps + 0], average_color);
498
499        // the (0, 2) kernel looks like this:
500        // [   1,   1,   1,   1,
501        //    ~0,  ~0,  ~0,  ~0,
502        //    -1,  -1,  -1,  -1,
503        //    ~0,  ~0,  ~0,  ~0  ]
504        // Image * kernel looks like this:
505        // [  1,   .,   1,   .,
506        //    .,   .,   .,   .,
507        //   -1,  -1,  -1,  -1,
508        //    .,   .,   .,   .  ] => adding up to -2
509        assert_eq!(currents[2 * x_comps + 0], [-2., -2., -2.]);
510
511        // the (2, 0) kernel looks like this:
512        // [  1,  ~0, -1,  ~0,
513        //    1,  ~0, -1,  ~0,
514        //    1,  ~0, -1,  ~0,
515        //    1,  ~0, -1,  ~0  ]
516        // Image * kernel looks like this:
517        // [  1,   .,  -1,  .,
518        //    .,   .,  -1,  .,
519        //    1,   .,  -1,  .,
520        //    .,   .,  -1,  .  ] => adding up to -2
521        assert_eq!(currents[0 * x_comps + 2], [-2., -2., -2.]);
522
523        // the (3, 3) kernel looks like this:
524        // [     1,  -0.7,  ~0,   0.7,
525        //    -0.7,   0.x_comps,  ~0,  -0.x_comps,
526        //      ~0,    ~0,  ~0,    ~0,
527        //     0.7,  -0.x_comps,  ~0,  -0.x_comps  ]
528        // Image * kernel looks like this:
529        // [  1,   .,   .,   .,
530        //    .,   .,   .,   .,
531        //    .,   .,   .,   .,
532        //    .,   .,   .,   .  ] => adding up to 1
533        assert_eq!(currents[3 * x_comps + 3], [1., 1., 1.]);
534
535        // the (4, 2) kernel looks like this:
536        // [  1,  -1,   1,  -1,
537        //   ~0,  ~0,  ~0,  ~0,
538        //    1,  -1,   1,  -1,
539        //   ~0,  ~0,  ~0,  ~0  ]
540        // Image * kernel looks like this:
541        // [  1,   .,   1,   .,
542        //    .,   .,   .,   .,
543        //    1,  -1,   1,  -1,
544        //    .,   .,   .,   .  ] => adding up to 2
545        assert_eq!(currents[2 * x_comps + 4], [2., 2., 2.]);
546
547        // the (2, 4) kernel looks like this:
548        // [  1,  ~0,  -1,  ~0,
549        //   -1,  ~0,   1,  ~0,
550        //    1,  ~0,  -1,  ~0,
551        //   -1,  ~0,   1,  ~0  ]
552        // Image * kernel looks like this:
553        // [  1,   .,   1,   .,
554        //    .,   .,  -1,   .,
555        //    1,   .,   1,   .,
556        //    .,   .,  -1,   .  ] => adding up to 2
557        assert_eq!(currents[4 * x_comps + 2], [2., 2., 2.]);
558    }
559
560    #[test]
561    fn test_encode_33() {
562        let image: [Rgb; 16] = [
563            [255,   0,   0], [  0,   0,   0], [255, 255, 255], [  0,   0,   0],
564            [  0,   0,   0], [  0,   0,   0], [255, 255, 255], [  0,   0,   0],
565            [255, 255, 255], [255, 255, 255], [  0, 255,   0], [255, 255, 255],
566            [  0,   0,   0], [  0,   0,   0], [255, 255, 255], [  0,   0,   0],
567        ];
568        assert_eq!(compute_dct(&image, 4, 4, 3, 3).into_blurhash(), "KzKUZY=|HZ=|$5e9HZe9IS");
569    }
570
571    #[test]
572    fn test_encode_decode_no_comps() {
573        let image: [Rgb; 16] = [[255, 127, 55]; 16];
574        let dct = compute_dct(&image, 4, 4, 1, 1);
575        let blurhash = encode(&dct);
576        assert_eq!(blurhash, "0~TNl]");
577
578        let inv = decode(&blurhash, 1.).unwrap();
579        assert_eq!(inv.x_components, dct.x_components);
580        assert_eq!(inv.y_components, dct.y_components);
581
582        for (i, (a, b)) in inv.currents.iter().flatten().zip(dct.currents.iter().flatten()).enumerate() {
583            assert!((a - b).abs() < 0.05, "{a}, {b}: Error too big at index {i}");
584        }
585    }
586
587    #[test]
588    fn test_encode_decode_white() {
589        let image: [Rgb; 16] = [[255, 255, 255]; 16];
590        let dct = compute_dct(&image, 4, 4, 4, 4);
591        let blurhash = encode(&dct);
592        assert_eq!(blurhash, "U~TSUA~qfQ~q~q%MfQ%MfQfQfQfQ~q%MfQ%M");
593        let inv = decode(&blurhash, 1.).unwrap();
594        assert_eq!(inv.x_components, dct.x_components);
595        assert_eq!(inv.y_components, dct.y_components);
596
597        for (i, (a, b)) in inv.currents.iter().flatten().zip(dct.currents.iter().flatten()).enumerate() {
598            assert!((a - b).abs() < 0.05, "{a}, {b}: Error too big at index {i}");
599        }
600
601        let generated = inv.to_image(4, 4, |p| p);
602        println!("{generated:?}");
603        for (i, &pixel) in generated.iter().flatten().enumerate() {
604            assert!((pixel - 1.).abs() < 0.05, "Expected white pixel got {pixel} at index {i}");
605        }
606    }
607
608    #[test]
609    fn test_encode_decode_black() {
610        let image: [Rgb; 16] = [[0, 0, 0]; 16];
611        let dct = compute_dct(&image, 4, 4, 4, 4);
612        let blurhash = encode(&dct);
613        assert_eq!(blurhash, "U00000fQfQfQfQfQfQfQfQfQfQfQfQfQfQfQ");
614
615        let inv = decode(&blurhash, 1.).unwrap();
616        assert_eq!(inv.x_components, dct.x_components);
617        assert_eq!(inv.y_components, dct.y_components);
618        assert_eq!(inv.dc(), dct.dc());
619
620        for (i, (a, b)) in inv.currents.iter().flatten().zip(dct.currents.iter().flatten()).enumerate() {
621            assert!((a - b).abs() < 0.05, "{a}, {b} at index {i}");
622        }
623
624        let generated = inv.to_image(4, 4, |p| p);
625        for (i, &pixel) in generated.iter().flatten().enumerate() {
626            assert!((pixel - 0.).abs() < 0.05, "Expected black pixel got {pixel} at index {i}");
627        }
628    }
629
630    use ril::prelude::Image;
631
632    impl AsLinear for &ril::pixel::Rgb {
633        fn as_linear(&self) -> Linear {
634            [srgb_to_linear(self.r), srgb_to_linear(self.g), srgb_to_linear(self.b)]
635        }
636    }
637
638    #[test]
639    fn test_encode_image() {
640        let img = Image::<ril::pixel::Rgb>::open("test.webp").unwrap();
641        let w = img.width() as usize;
642        let h = img.height() as usize;
643        let s = compute_dct_iter(img.pixels().flatten(), w, h, 4, 7);
644        assert_eq!(s.into_blurhash(), "vbHCG?SgNGxD~pX9R+i_NfNIt7V@NL%Mt7Rj-;t7e:WCfPWXV[ofM{WXbHof");
645    }
646
647    #[test]
648    fn test_decode_image() {
649        let s = decode("vbHLxdSgNHxD~pX9R+i_NfNIt7V@NL%Mt7Rj-;t7e:WCj[WXV[ofM{WXbHof", 1.)
650            .unwrap().to_rgb8(32, 48);
651
652        let img = Image::<ril::pixel::Rgb>::from_fn(32, 48, |x, y| {
653            let [r, g, b] = s[y as usize * 32 + x as usize];
654            ril::pixel::Rgb::new(r, g, b)
655        });
656        img.save_inferred("out.webp").unwrap();
657    }
658}