thumbhash/
lib.rs

1use std::f32::consts::PI;
2use std::io::Read;
3
4/// Encodes an RGBA image to a ThumbHash. RGB should not be premultiplied by A.
5///
6/// * `w`: The width of the input image. Must be ≤100px.
7/// * `h`: The height of the input image. Must be ≤100px.
8/// * `rgba`: The pixels in the input image, row-by-row. Must have `w*h*4` elements.
9pub fn rgba_to_thumb_hash(w: usize, h: usize, rgba: &[u8]) -> Vec<u8> {
10    // Encoding an image larger than 100x100 is slow with no benefit
11    assert!(w <= 100 && h <= 100);
12    assert_eq!(rgba.len(), w * h * 4);
13
14    // Determine the average color
15    let mut avg_r = 0.0;
16    let mut avg_g = 0.0;
17    let mut avg_b = 0.0;
18    let mut avg_a = 0.0;
19    for rgba in rgba.chunks_exact(4) {
20        let alpha = rgba[3] as f32 / 255.0;
21        avg_r += alpha / 255.0 * rgba[0] as f32;
22        avg_g += alpha / 255.0 * rgba[1] as f32;
23        avg_b += alpha / 255.0 * rgba[2] as f32;
24        avg_a += alpha;
25    }
26    if avg_a > 0.0 {
27        avg_r /= avg_a;
28        avg_g /= avg_a;
29        avg_b /= avg_a;
30    }
31
32    let has_alpha = avg_a < (w * h) as f32;
33    let l_limit = if has_alpha { 5 } else { 7 }; // Use fewer luminance bits if there's alpha
34    let lx = (((l_limit * w) as f32 / w.max(h) as f32).round() as usize).max(1);
35    let ly = (((l_limit * h) as f32 / w.max(h) as f32).round() as usize).max(1);
36    let mut l = Vec::with_capacity(w * h); // luminance
37    let mut p = Vec::with_capacity(w * h); // yellow - blue
38    let mut q = Vec::with_capacity(w * h); // red - green
39    let mut a = Vec::with_capacity(w * h); // alpha
40
41    // Convert the image from RGBA to LPQA (composite atop the average color)
42    for rgba in rgba.chunks_exact(4) {
43        let alpha = rgba[3] as f32 / 255.0;
44        let r = avg_r * (1.0 - alpha) + alpha / 255.0 * rgba[0] as f32;
45        let g = avg_g * (1.0 - alpha) + alpha / 255.0 * rgba[1] as f32;
46        let b = avg_b * (1.0 - alpha) + alpha / 255.0 * rgba[2] as f32;
47        l.push((r + g + b) / 3.0);
48        p.push((r + g) / 2.0 - b);
49        q.push(r - g);
50        a.push(alpha);
51    }
52
53    // Encode using the DCT into DC (constant) and normalized AC (varying) terms
54    let encode_channel = |channel: &[f32], nx: usize, ny: usize| -> (f32, Vec<f32>, f32) {
55        let mut dc = 0.0;
56        let mut ac = Vec::with_capacity(nx * ny / 2);
57        let mut scale = 0.0;
58        let mut fx = [0.0].repeat(w);
59        for cy in 0..ny {
60            let mut cx = 0;
61            while cx * ny < nx * (ny - cy) {
62                let mut f = 0.0;
63                for x in 0..w {
64                    fx[x] = (PI / w as f32 * cx as f32 * (x as f32 + 0.5)).cos();
65                }
66                for y in 0..h {
67                    let fy = (PI / h as f32 * cy as f32 * (y as f32 + 0.5)).cos();
68                    for x in 0..w {
69                        f += channel[x + y * w] * fx[x] * fy;
70                    }
71                }
72                f /= (w * h) as f32;
73                if cx > 0 || cy > 0 {
74                    ac.push(f);
75                    scale = f.abs().max(scale);
76                } else {
77                    dc = f;
78                }
79                cx += 1;
80            }
81        }
82        if scale > 0.0 {
83            for ac in &mut ac {
84                *ac = 0.5 + 0.5 / scale * *ac;
85            }
86        }
87        (dc, ac, scale)
88    };
89    let (l_dc, l_ac, l_scale) = encode_channel(&l, lx.max(3), ly.max(3));
90    let (p_dc, p_ac, p_scale) = encode_channel(&p, 3, 3);
91    let (q_dc, q_ac, q_scale) = encode_channel(&q, 3, 3);
92    let (a_dc, a_ac, a_scale) = if has_alpha {
93        encode_channel(&a, 5, 5)
94    } else {
95        (1.0, Vec::new(), 1.0)
96    };
97
98    // Write the constants
99    let is_landscape = w > h;
100    let header24 = (63.0 * l_dc).round() as u32
101        | (((31.5 + 31.5 * p_dc).round() as u32) << 6)
102        | (((31.5 + 31.5 * q_dc).round() as u32) << 12)
103        | (((31.0 * l_scale).round() as u32) << 18)
104        | if has_alpha { 1 << 23 } else { 0 };
105    let header16 = (if is_landscape { ly } else { lx }) as u16
106        | (((63.0 * p_scale).round() as u16) << 3)
107        | (((63.0 * q_scale).round() as u16) << 9)
108        | if is_landscape { 1 << 15 } else { 0 };
109    let mut hash = Vec::with_capacity(25);
110    hash.extend_from_slice(&[
111        (header24 & 255) as u8,
112        ((header24 >> 8) & 255) as u8,
113        (header24 >> 16) as u8,
114        (header16 & 255) as u8,
115        (header16 >> 8) as u8,
116    ]);
117    let mut is_odd = false;
118    if has_alpha {
119        hash.push((15.0 * a_dc).round() as u8 | (((15.0 * a_scale).round() as u8) << 4));
120    }
121
122    // Write the varying factors
123    for ac in [l_ac, p_ac, q_ac] {
124        for f in ac {
125            let u = (15.0 * f).round() as u8;
126            if is_odd {
127                *hash.last_mut().unwrap() |= u << 4;
128            } else {
129                hash.push(u);
130            }
131            is_odd = !is_odd;
132        }
133    }
134    if has_alpha {
135        for f in a_ac {
136            let u = (15.0 * f).round() as u8;
137            if is_odd {
138                *hash.last_mut().unwrap() |= u << 4;
139            } else {
140                hash.push(u);
141            }
142            is_odd = !is_odd;
143        }
144    }
145    hash
146}
147
148fn read_byte(bytes: &mut &[u8]) -> Result<u8, ()> {
149    let mut byte = [0; 1];
150    bytes.read_exact(&mut byte).map_err(|_| ())?;
151    Ok(byte[0])
152}
153
154/// Decodes a ThumbHash to an RGBA image.
155///
156/// RGB is not be premultiplied by A. Returns the width, height, and pixels of
157/// the rendered placeholder image. An error will be returned if the input is
158/// too short.
159pub fn thumb_hash_to_rgba(mut hash: &[u8]) -> Result<(usize, usize, Vec<u8>), ()> {
160    let ratio = thumb_hash_to_approximate_aspect_ratio(hash)?;
161
162    // Read the constants
163    let header24 = read_byte(&mut hash)? as u32
164        | ((read_byte(&mut hash)? as u32) << 8)
165        | ((read_byte(&mut hash)? as u32) << 16);
166    let header16 = read_byte(&mut hash)? as u16 | ((read_byte(&mut hash)? as u16) << 8);
167    let l_dc = (header24 & 63) as f32 / 63.0;
168    let p_dc = ((header24 >> 6) & 63) as f32 / 31.5 - 1.0;
169    let q_dc = ((header24 >> 12) & 63) as f32 / 31.5 - 1.0;
170    let l_scale = ((header24 >> 18) & 31) as f32 / 31.0;
171    let has_alpha = (header24 >> 23) != 0;
172    let p_scale = ((header16 >> 3) & 63) as f32 / 63.0;
173    let q_scale = ((header16 >> 9) & 63) as f32 / 63.0;
174    let is_landscape = (header16 >> 15) != 0;
175    let l_max = if has_alpha { 5 } else { 7 };
176    let lx = 3.max(if is_landscape { l_max } else { header16 & 7 }) as usize;
177    let ly = 3.max(if is_landscape { header16 & 7 } else { l_max }) as usize;
178    let (a_dc, a_scale) = if has_alpha {
179        let header8 = read_byte(&mut hash)?;
180        ((header8 & 15) as f32 / 15.0, (header8 >> 4) as f32 / 15.0)
181    } else {
182        (1.0, 1.0)
183    };
184
185    // Read the varying factors (boost saturation by 1.25x to compensate for quantization)
186    let mut prev_bits = None;
187    let mut decode_channel = |nx: usize, ny: usize, scale: f32| -> Result<Vec<f32>, ()> {
188        let mut ac = Vec::with_capacity(nx * ny);
189        for cy in 0..ny {
190            let mut cx = if cy > 0 { 0 } else { 1 };
191            while cx * ny < nx * (ny - cy) {
192                let bits = if let Some(bits) = prev_bits {
193                    prev_bits = None;
194                    bits
195                } else {
196                    let bits = read_byte(&mut hash)?;
197                    prev_bits = Some(bits >> 4);
198                    bits & 15
199                };
200                ac.push((bits as f32 / 7.5 - 1.0) * scale);
201                cx += 1;
202            }
203        }
204        Ok(ac)
205    };
206    let l_ac = decode_channel(lx, ly, l_scale)?;
207    let p_ac = decode_channel(3, 3, p_scale * 1.25)?;
208    let q_ac = decode_channel(3, 3, q_scale * 1.25)?;
209    let a_ac = if has_alpha {
210        decode_channel(5, 5, a_scale)?
211    } else {
212        Vec::new()
213    };
214
215    // Decode using the DCT into RGB
216    let (w, h) = if ratio > 1.0 {
217        (32, (32.0 / ratio).round() as usize)
218    } else {
219        ((32.0 * ratio).round() as usize, 32)
220    };
221    let mut rgba = Vec::with_capacity(w * h * 4);
222    let mut fx = [0.0].repeat(7);
223    let mut fy = [0.0].repeat(7);
224    for y in 0..h {
225        for x in 0..w {
226            let mut l = l_dc;
227            let mut p = p_dc;
228            let mut q = q_dc;
229            let mut a = a_dc;
230
231            // Precompute the coefficients
232            for cx in 0..lx.max(if has_alpha { 5 } else { 3 }) {
233                fx[cx] = (PI / w as f32 * (x as f32 + 0.5) * cx as f32).cos();
234            }
235            for cy in 0..ly.max(if has_alpha { 5 } else { 3 }) {
236                fy[cy] = (PI / h as f32 * (y as f32 + 0.5) * cy as f32).cos();
237            }
238
239            // Decode L
240            let mut j = 0;
241            for cy in 0..ly {
242                let mut cx = if cy > 0 { 0 } else { 1 };
243                let fy2 = fy[cy] * 2.0;
244                while cx * ly < lx * (ly - cy) {
245                    l += l_ac[j] * fx[cx] * fy2;
246                    j += 1;
247                    cx += 1;
248                }
249            }
250
251            // Decode P and Q
252            let mut j = 0;
253            for cy in 0..3 {
254                let mut cx = if cy > 0 { 0 } else { 1 };
255                let fy2 = fy[cy] * 2.0;
256                while cx < 3 - cy {
257                    let f = fx[cx] * fy2;
258                    p += p_ac[j] * f;
259                    q += q_ac[j] * f;
260                    j += 1;
261                    cx += 1;
262                }
263            }
264
265            // Decode A
266            if has_alpha {
267                let mut j = 0;
268                for cy in 0..5 {
269                    let mut cx = if cy > 0 { 0 } else { 1 };
270                    let fy2 = fy[cy] * 2.0;
271                    while cx < 5 - cy {
272                        a += a_ac[j] * fx[cx] * fy2;
273                        j += 1;
274                        cx += 1;
275                    }
276                }
277            }
278
279            // Convert to RGB
280            let b = l - 2.0 / 3.0 * p;
281            let r = (3.0 * l - b + q) / 2.0;
282            let g = r - q;
283            rgba.extend_from_slice(&[
284                (r.clamp(0.0, 1.0) * 255.0) as u8,
285                (g.clamp(0.0, 1.0) * 255.0) as u8,
286                (b.clamp(0.0, 1.0) * 255.0) as u8,
287                (a.clamp(0.0, 1.0) * 255.0) as u8,
288            ]);
289        }
290    }
291    Ok((w, h, rgba))
292}
293
294/// Extracts the average color from a ThumbHash.
295///
296/// Returns the RGBA values where each value ranges from 0 to 1. RGB is not be
297/// premultiplied by A. An error will be returned if the input is too short.
298pub fn thumb_hash_to_average_rgba(hash: &[u8]) -> Result<(f32, f32, f32, f32), ()> {
299    if hash.len() < 5 {
300        return Err(());
301    }
302    let header = hash[0] as u32 | ((hash[1] as u32) << 8) | ((hash[2] as u32) << 16);
303    let l = (header & 63) as f32 / 63.0;
304    let p = ((header >> 6) & 63) as f32 / 31.5 - 1.0;
305    let q = ((header >> 12) & 63) as f32 / 31.5 - 1.0;
306    let has_alpha = (header >> 23) != 0;
307    let a = if has_alpha {
308        (hash[5] & 15) as f32 / 15.0
309    } else {
310        1.0
311    };
312    let b = l - 2.0 / 3.0 * p;
313    let r = (3.0 * l - b + q) / 2.0;
314    let g = r - q;
315    Ok((r.clamp(0.0, 1.0), g.clamp(0.0, 1.0), b.clamp(0.0, 1.0), a))
316}
317
318/// Extracts the approximate aspect ratio of the original image.
319///
320/// An error will be returned if the input is too short.
321pub fn thumb_hash_to_approximate_aspect_ratio(hash: &[u8]) -> Result<f32, ()> {
322    if hash.len() < 5 {
323        return Err(());
324    }
325    let has_alpha = (hash[2] & 0x80) != 0;
326    let l_max = if has_alpha { 5 } else { 7 };
327    let l_min = hash[3] & 7;
328    let is_landscape = (hash[4] & 0x80) != 0;
329    let lx = if is_landscape { l_max } else { l_min };
330    let ly = if is_landscape { l_min } else { l_max };
331    Ok(lx as f32 / ly as f32)
332}