thumbhash.js
1 // ThumbHash decoder — https://github.com/evanw/thumbhash (MIT) 2 // Adapted from the ESM source: export keywords removed for bundle inclusion. 3 4 function thumbHashToRGBA(hash) { 5 var PI = Math.PI, min = Math.min, max = Math.max, cos = Math.cos, round = Math.round; 6 7 var header24 = hash[0] | (hash[1] << 8) | (hash[2] << 16); 8 var header16 = hash[3] | (hash[4] << 8); 9 var l_dc = (header24 & 63) / 63; 10 var p_dc = ((header24 >> 6) & 63) / 31.5 - 1; 11 var q_dc = ((header24 >> 12) & 63) / 31.5 - 1; 12 var l_scale = ((header24 >> 18) & 31) / 31; 13 var hasAlpha = header24 >> 23; 14 var p_scale = ((header16 >> 3) & 63) / 63; 15 var q_scale = ((header16 >> 9) & 63) / 63; 16 var isLandscape = header16 >> 15; 17 var lx = max(3, isLandscape ? (hasAlpha ? 5 : 7) : (header16 & 7)); 18 var ly = max(3, isLandscape ? (header16 & 7) : (hasAlpha ? 5 : 7)); 19 var a_dc = hasAlpha ? (hash[5] & 15) / 15 : 1; 20 var a_scale = (hash[5] >> 4) / 15; 21 22 var ac_start = hasAlpha ? 6 : 5; 23 var ac_index = 0; 24 function decodeChannel(nx, ny, scale) { 25 var ac = []; 26 for (var cy = 0; cy < ny; cy++) 27 for (var cx = cy ? 0 : 1; cx * ny < nx * (ny - cy); cx++) 28 ac.push((((hash[ac_start + (ac_index >> 1)] >> ((ac_index++ & 1) << 2)) & 15) / 7.5 - 1) * scale); 29 return ac; 30 } 31 var l_ac = decodeChannel(lx, ly, l_scale); 32 var p_ac = decodeChannel(3, 3, p_scale * 1.25); 33 var q_ac = decodeChannel(3, 3, q_scale * 1.25); 34 var a_ac = hasAlpha ? decodeChannel(5, 5, a_scale) : null; 35 36 var ratio = thumbHashToApproximateAspectRatio(hash); 37 var w = round(ratio > 1 ? 32 : 32 * ratio); 38 var h = round(ratio > 1 ? 32 / ratio : 32); 39 var rgba = new Uint8Array(w * h * 4); 40 var fx = [], fy = []; 41 for (var y = 0, i = 0; y < h; y++) { 42 for (var x = 0; x < w; x++, i += 4) { 43 var l = l_dc, p = p_dc, q = q_dc, a = a_dc; 44 var n1 = max(lx, hasAlpha ? 5 : 3); 45 for (var cx = 0; cx < n1; cx++) fx[cx] = cos(PI / w * (x + 0.5) * cx); 46 var n2 = max(ly, hasAlpha ? 5 : 3); 47 for (var cy = 0; cy < n2; cy++) fy[cy] = cos(PI / h * (y + 0.5) * cy); 48 for (var cy = 0, j = 0; cy < ly; cy++) 49 for (var cx = cy ? 0 : 1, fy2 = fy[cy] * 2; cx * ly < lx * (ly - cy); cx++, j++) 50 l += l_ac[j] * fx[cx] * fy2; 51 for (var cy = 0, j = 0; cy < 3; cy++) { 52 for (var cx = cy ? 0 : 1, fy2 = fy[cy] * 2; cx < 3 - cy; cx++, j++) { 53 var f = fx[cx] * fy2; 54 p += p_ac[j] * f; 55 q += q_ac[j] * f; 56 } 57 } 58 if (hasAlpha) 59 for (var cy = 0, j = 0; cy < 5; cy++) 60 for (var cx = cy ? 0 : 1, fy2 = fy[cy] * 2; cx < 5 - cy; cx++, j++) 61 a += a_ac[j] * fx[cx] * fy2; 62 var b = l - 2 / 3 * p; 63 var r = (3 * l - b + q) / 2; 64 var g = r - q; 65 rgba[i] = max(0, 255 * min(1, r)); 66 rgba[i + 1] = max(0, 255 * min(1, g)); 67 rgba[i + 2] = max(0, 255 * min(1, b)); 68 rgba[i + 3] = max(0, 255 * min(1, a)); 69 } 70 } 71 return { w: w, h: h, rgba: rgba }; 72 } 73 74 function thumbHashToApproximateAspectRatio(hash) { 75 var header = hash[3]; 76 var hasAlpha = hash[2] & 0x80; 77 var isLandscape = hash[4] & 0x80; 78 var lx = isLandscape ? (hasAlpha ? 5 : 7) : (header & 7); 79 var ly = isLandscape ? (header & 7) : (hasAlpha ? 5 : 7); 80 return lx / ly; 81 } 82 83 function rgbaToDataURL(w, h, rgba) { 84 var row = w * 4 + 1; 85 var idat = 6 + h * (5 + row); 86 var bytes = [ 87 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 88 w >> 8, w & 255, 0, 0, h >> 8, h & 255, 8, 6, 0, 0, 0, 0, 0, 0, 0, 89 idat >>> 24, (idat >> 16) & 255, (idat >> 8) & 255, idat & 255, 90 73, 68, 65, 84, 120, 1 91 ]; 92 var table = [ 93 0, 498536548, 997073096, 651767980, 1994146192, 1802195444, 1303535960, 94 1342533948, -306674912, -267414716, -690576408, -882789492, -1687895376, 95 -2032938284, -1609899400, -1111625188 96 ]; 97 var a = 1, b = 0; 98 for (var y = 0, i = 0, end = row - 1; y < h; y++, end += row - 1) { 99 bytes.push(y + 1 < h ? 0 : 1, row & 255, row >> 8, ~row & 255, (row >> 8) ^ 255, 0); 100 for (b = (b + a) % 65521; i < end; i++) { 101 var u = rgba[i] & 255; 102 bytes.push(u); 103 a = (a + u) % 65521; 104 b = (b + a) % 65521; 105 } 106 } 107 bytes.push( 108 b >> 8, b & 255, a >> 8, a & 255, 0, 0, 0, 0, 109 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130 110 ); 111 for (var range of [[12, 29], [37, 41 + idat]]) { 112 var start = range[0], end = range[1]; 113 var c = ~0; 114 for (var i = start; i < end; i++) { 115 c ^= bytes[i]; 116 c = (c >>> 4) ^ table[c & 15]; 117 c = (c >>> 4) ^ table[c & 15]; 118 } 119 c = ~c; 120 bytes[end++] = c >>> 24; 121 bytes[end++] = (c >> 16) & 255; 122 bytes[end++] = (c >> 8) & 255; 123 bytes[end++] = c & 255; 124 } 125 return 'data:image/png;base64,' + btoa(String.fromCharCode.apply(null, bytes)); 126 } 127 128 function thumbHashToDataURL(hash) { 129 var image = thumbHashToRGBA(hash); 130 return rgbaToDataURL(image.w, image.h, image.rgba); 131 }