blurhash_wasm/
lib.rs

1mod utils;
2
3use std::f64::consts::PI;
4use wasm_bindgen::prelude::*;
5
6// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
7// allocator.
8#[cfg(feature = "wee_alloc")]
9#[global_allocator]
10static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
11
12// TODO: Use failure
13// TODO: Use format for the error in wasm_decode
14// TODO: Add adjustable 'punch'
15// TODO: Avoid panicing infrasturcture (checked division, .get, no unwrap)
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub enum Error {
19    LengthInvalid,
20    LengthMismatch,
21}
22
23/* Decode for WASM target
24 * It is similar to `decode`, but uses an option for the Error
25 * I could not figure out a good way to use the Error in the regular decode,
26 * and was getting a E0277 or not being able to convert automatically.
27
28 * There are two current options, afaict:
29 *  1) Result E be a JsValue with hardcoded (or formatted) strings.
30 *  2) Transform the Result to Option, which on failure would be undefined in JS.
31
32 * For convenience, I went with 2), until we can return Error.
33 * This seems to be an open topic atm, so a separate decode seems ok for now :)
34 * @see https://github.com/rustwasm/wasm-bindgen/issues/1017
35*/
36#[wasm_bindgen(js_name = "decode")]
37pub fn wasm_decode(blur_hash: &str, width: usize, height: usize) -> Option<Vec<u8>> {
38    match decode(blur_hash, width, height) {
39        Ok(img) => Some(img),
40        Err(_err) => None,
41    }
42}
43
44pub fn decode(blur_hash: &str, width: usize, height: usize) -> Result<Vec<u8>, Error> {
45    if blur_hash.len() < 6 {
46        return Err(Error::LengthInvalid);
47    }
48
49    // 1. Number of components
50    // For a BlurHash with nx components along the X axis and ny components
51    // along the Y axis, this is equal to (nx - 1) + (ny - 1) * 9.
52    let size_flag = decode_base83_string(blur_hash.get(0..1).unwrap());
53
54    let num_y = (size_flag / 9) + 1;
55    let num_x = (size_flag % 9) + 1;
56
57    // Validate that the number of digits is what we expect:
58    // 1 (size flag) + 1 (maximum value) + 4 (average colour) + (num_x - num_y - 1) components * 2 digits each
59    let expected_digits = 4 + 2 * num_x * num_y;
60
61    if blur_hash.len() != expected_digits {
62        return Err(Error::LengthMismatch);
63    }
64
65    // 2. Maximum AC component value, 1 digit.
66    // All AC components are scaled by this value.
67    // It represents a floating-point value of (max + 1) / 166.
68    let quantised_maximum_value = decode_base83_string(blur_hash.get(1..2).unwrap());
69    let maximum_value = ((quantised_maximum_value + 1) as f64) / 166f64;
70
71    let mut colours: Vec<[f64; 3]> = Vec::new();
72
73    for i in 0..(num_x * num_y) {
74        if i == 0 {
75            // 3. Average colour, 4 digits.
76            let value = decode_base83_string(blur_hash.get(2..6).unwrap());
77            colours.push(decode_dc(value));
78        } else {
79            // 4. AC components, 2 digits each, nx * ny - 1 components in total.
80            let value = decode_base83_string(blur_hash.get((4 + i * 2)..(4 + i * 2 + 2)).unwrap());
81            colours.push(decode_ac(value, maximum_value * 1.0));
82        }
83    }
84
85    // Now, construct the image
86    // NOTE: We include an alpha channel of 255 as well, because it is more convenient,
87    // for various representations (browser canvas, for example).
88    // This could probably be configured
89    let bytes_per_row = width * 4;
90
91    let mut pixels = vec![0; bytes_per_row * height];
92
93    for y in 0..height {
94        for x in 0..width {
95            let mut r = 0f64;
96            let mut g = 0f64;
97            let mut b = 0f64;
98
99            for j in 0..num_y {
100                for i in 0..num_x {
101                    let basis = f64::cos(PI * (x as f64) * (i as f64) / (width as f64))
102                        * f64::cos(PI * (y as f64) * (j as f64) / (height as f64));
103                    let colour = colours[i + j * num_x];
104                    r += colour[0] * basis;
105                    g += colour[1] * basis;
106                    b += colour[2] * basis;
107                }
108            }
109
110            let int_r = linear_to_srgb(r);
111            let int_g = linear_to_srgb(g);
112            let int_b = linear_to_srgb(b);
113
114            pixels[4 * x + 0 + y * bytes_per_row] = int_r;
115            pixels[4 * x + 1 + y * bytes_per_row] = int_g;
116            pixels[4 * x + 2 + y * bytes_per_row] = int_b;
117            pixels[4 * x + 3 + y * bytes_per_row] = 255;
118        }
119    }
120
121    Ok(pixels)
122}
123
124fn decode_dc(value: usize) -> [f64; 3] {
125    let int_r = value >> 16;
126    let int_g = (value >> 8) & 255;
127    let int_b = value & 255;
128    [
129        srgb_to_linear(int_r),
130        srgb_to_linear(int_g),
131        srgb_to_linear(int_b),
132    ]
133}
134
135fn decode_ac(value: usize, maximum_value: f64) -> [f64; 3] {
136    let quant_r = f64::floor((value / (19 * 19)) as f64);
137    let quant_g = f64::floor(((value / 19) as f64) % 19f64);
138    let quant_b = (value as f64) % 19f64;
139
140    let rgb = [
141        sign_pow((quant_r - 9f64) / 9f64, 2f64) * maximum_value,
142        sign_pow((quant_g - 9f64) / 9f64, 2f64) * maximum_value,
143        sign_pow((quant_b - 9f64) / 9f64, 2f64) * maximum_value,
144    ];
145    rgb
146}
147
148fn sign_pow(value: f64, exp: f64) -> f64 {
149    get_sign(value) * f64::powf(f64::abs(value), exp)
150}
151
152fn get_sign(n: f64) -> f64 {
153    if n < 0f64 {
154        -1f64
155    } else {
156        1f64
157    }
158}
159
160fn linear_to_srgb(value: f64) -> u8 {
161    let v = f64::max(0f64, f64::min(1f64, value));
162    if v <= 0.0031308 {
163        return (v * 12.92 * 255f64 + 0.5) as u8;
164    } else {
165        return ((1.055 * f64::powf(v, 1f64 / 2.4) - 0.055) * 255f64 + 0.5) as u8;
166    }
167}
168
169fn srgb_to_linear(value: usize) -> f64 {
170    let v = (value as f64) / 255f64;
171    if v <= 0.04045 {
172        return v / 12.92;
173    } else {
174        return ((v + 0.055) / 1.055).powf(2.4);
175    }
176}
177
178// TODO: Consider using lazy_static to expand this, or even write long-hand
179const ENCODE_CHARACTERS: &str =
180    "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~";
181
182fn decode_base83_string(string: &str) -> usize {
183    let mut value: usize = 0;
184
185    for character in string.chars() {
186        match ENCODE_CHARACTERS.find(character) {
187            Some(digit) => value = value * 83 + digit,
188
189            None => (),
190        }
191    }
192    value
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[test]
200    fn it_decodes_size_flag() {
201        assert_eq!(21, decode_base83_string("L"));
202        assert_eq!(0, decode_base83_string("0"));
203    }
204    #[test]
205    fn decodes_size_0_out_of_range() {
206        let res = decode_base83_string("/");
207        assert_eq!(
208            0, res,
209            "Did not expect to decode size for input out of range (expected 0), but got {}",
210            res
211        );
212    }
213}