/ assets / thumbhash.js
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  }