blurhash_ng/
lib.rs

1//! A pure Rust implementation of [woltapp/blurhash][1].
2//!
3//! ### Encoding
4//!
5//! ```
6//! use blurhash::encode;
7//! use image::GenericImageView;
8//!
9//! let img = image::open("octocat.png").unwrap();
10//! let (width, height) = img.dimensions();
11//! let blurhash = encode(4, 3, width, height, &img.to_rgba().into_vec());
12//!
13//! assert_eq!(blurhash, "LBAdAqof00WCqZj[PDay0.WB}pof");
14//! ```
15//!
16//! ### Decoding
17//!
18//! ```no_run
19//! use blurhash::decode;
20//!
21//! let pixels = decode("LBAdAqof00WCqZj[PDay0.WB}pof", 50, 50, 1.0);
22//! ```
23//! [1]: https://github.com/woltapp/blurhash
24mod ac;
25mod base83;
26mod dc;
27mod util;
28
29use std::f32::consts::PI;
30pub use util::{linear_to_srgb, srgb_to_linear};
31
32/// Calculates the blurhash for an image using the given x and y component counts.
33pub fn encode(components_x: u32, components_y: u32, width: u32, height: u32, rgb: &[u8]) -> String {
34    if components_x < 1 || components_x > 9 || components_y < 1 || components_y > 9 {
35        panic!("BlurHash must have between 1 and 9 components");
36    }
37
38    let mut factors: Vec<[f32; 3]> = Vec::new();
39
40    for y in 0..components_y {
41        for x in 0..components_x {
42            let factor = multiply_basis_function(x, y, width, height, rgb);
43            factors.push(factor);
44        }
45    }
46
47    let dc = factors[0];
48    let ac = &factors[1..];
49
50    let mut blurhash = String::new();
51
52    let size_flag = (components_x - 1) + (components_y - 1) * 9;
53    blurhash.push_str(&base83::encode(size_flag, 1));
54
55    let maximum_value: f32;
56    if !ac.is_empty() {
57        let mut actualmaximum_value = 0.0;
58        for i in 0..components_y * components_x - 1 {
59            actualmaximum_value = f32::max(f32::abs(ac[i as usize][0]), actualmaximum_value);
60            actualmaximum_value = f32::max(f32::abs(ac[i as usize][1]), actualmaximum_value);
61            actualmaximum_value = f32::max(f32::abs(ac[i as usize][2]), actualmaximum_value);
62        }
63
64        let quantised_maximum_value = f32::max(
65            0.,
66            f32::min(82., f32::floor(actualmaximum_value * 166. - 0.5)),
67        ) as u32;
68
69        maximum_value = (quantised_maximum_value + 1) as f32 / 166.;
70        blurhash.push_str(&base83::encode(quantised_maximum_value, 1));
71    } else {
72        maximum_value = 1.;
73        blurhash.push_str(&base83::encode(0, 1));
74    }
75
76    blurhash.push_str(&base83::encode(dc::encode(dc), 4));
77
78    for i in 0..components_y * components_x - 1 {
79        blurhash.push_str(&base83::encode(
80            ac::encode(ac[i as usize], maximum_value),
81            2,
82        ));
83    }
84
85    blurhash
86}
87
88fn multiply_basis_function(
89    component_x: u32,
90    component_y: u32,
91    width: u32,
92    height: u32,
93    rgb: &[u8],
94) -> [f32; 3] {
95    let mut r = 0.;
96    let mut g = 0.;
97    let mut b = 0.;
98    let normalisation = match (component_x, component_y) {
99        (0, 0) => 1.,
100        _ => 2.,
101    };
102
103    let bytes_per_row = width * 4;
104
105    for y in 0..height {
106        for x in 0..width {
107            let basis = f32::cos(PI * component_x as f32 * x as f32 / width as f32)
108                * f32::cos(PI * component_y as f32 * y as f32 / height as f32);
109            r += basis * srgb_to_linear(u32::from(rgb[(4 * x + y * bytes_per_row) as usize]));
110            g += basis * srgb_to_linear(u32::from(rgb[(4 * x + 1 + y * bytes_per_row) as usize]));
111            b += basis * srgb_to_linear(u32::from(rgb[(4 * x + 2 + y * bytes_per_row) as usize]));
112        }
113    }
114
115    let scale = normalisation / (width * height) as f32;
116
117    [r * scale, g * scale, b * scale]
118}
119
120/// Decodes the given blurhash to an image of the specified size.
121///
122/// The punch parameter can be used to de- or increase the contrast of the
123/// resulting image.
124pub fn decode(blurhash: &str, width: u32, height: u32, punch: f32) -> Vec<u8> {
125    let (num_x, num_y) = components(blurhash);
126
127    let quantised_maximum_value = base83::decode(&blurhash.chars().nth(1).unwrap().to_string());
128    let maximum_value = (quantised_maximum_value + 1) as f32 / 166.;
129
130    let mut colors = vec![[0.; 3]; num_x * num_y];
131
132    for i in 0..colors.len() {
133        if i == 0 {
134            let value = base83::decode(&blurhash[2..6]);
135            colors[i as usize] = dc::decode(value as u32);
136        } else {
137            let value = base83::decode(&blurhash[4 + i * 2..6 + i * 2]);
138            colors[i as usize] = ac::decode(value as u32, maximum_value * punch);
139        }
140    }
141
142    let bytes_per_row = width * 4;
143    let mut pixels = vec![0; (bytes_per_row * height) as usize];
144
145    for y in 0..height {
146        for x in 0..width {
147            let mut pixel = [0.; 3];
148
149            for j in 0..num_y {
150                for i in 0..num_x {
151                    let basis = f32::cos((PI * x as f32 * i as f32) / width as f32)
152                        * f32::cos((PI * y as f32 * j as f32) / height as f32);
153                    let color = &colors[i + j * num_x as usize];
154
155                    pixel[0] += color[0] * basis;
156                    pixel[1] += color[1] * basis;
157                    pixel[2] += color[2] * basis;
158                }
159            }
160
161            let int_r = linear_to_srgb(pixel[0]);
162            let int_g = linear_to_srgb(pixel[1]);
163            let int_b = linear_to_srgb(pixel[2]);
164
165            pixels[(4 * x + y * bytes_per_row) as usize] = int_r as u8;
166            pixels[(4 * x + 1 + y * bytes_per_row) as usize] = int_g as u8;
167            pixels[(4 * x + 2 + y * bytes_per_row) as usize] = int_b as u8;
168            pixels[(4 * x + 3 + y * bytes_per_row) as usize] = 255 as u8;
169        }
170    }
171    pixels
172}
173
174fn components(blurhash: &str) -> (usize, usize) {
175    if blurhash.len() < 6 {
176        panic!("The blurhash string must be at least 6 characters");
177    }
178
179    let size_flag = base83::decode(&blurhash.chars().nth(0).unwrap().to_string());
180    let num_y = (f32::floor(size_flag as f32 / 9.) + 1.) as usize;
181    let num_x = (size_flag % 9) + 1;
182
183    if blurhash.len() != 4 + 2 * num_x * num_y {
184        panic!(
185            "blurhash length mismatch: length is {} but it should be {}",
186            blurhash.len(),
187            (4 + 2 * num_x * num_y)
188        );
189    }
190
191    (num_x, num_y)
192}
193
194#[cfg(test)]
195mod tests {
196    use super::{decode, encode};
197    use image::GenericImageView;
198    use image::{save_buffer, RGBA};
199
200    #[test]
201    fn decode_blurhash() {
202        let img = image::open("octocat.png").unwrap();
203        let (width, height) = img.dimensions();
204
205        let blurhash = encode(4, 3, width, height, &img.to_rgba().into_vec());
206        let img = decode(&blurhash, width, height, 1.0);
207        save_buffer("out.png", &img, width, height, RGBA(8)).unwrap();
208
209        assert_eq!(img[0..5], [45, 1, 56, 255, 45]);
210    }
211}