Skip to content

Instantly share code, notes, and snippets.

@hkamran80
Forked from gxvxc/blurhashDataURL.ts
Last active August 17, 2022 04:29
Show Gist options
  • Save hkamran80/9852d1284ad8db033e88a8055c1e1ea3 to your computer and use it in GitHub Desktop.
Save hkamran80/9852d1284ad8db033e88a8055c1e1ea3 to your computer and use it in GitHub Desktop.
Convert blurhash to a base64 DataURL string (no canvas or node-canvas)
import { decode } from "blurhash";
const cache: Record<string, string> = {};
export const blurHashToDataURL = (
hash: string | undefined,
): string | undefined => {
if (!hash) return undefined;
const cachedBlurDataURL = cache[hash];
if (cachedBlurDataURL) {
return cachedBlurDataURL;
}
const pixels = decode(hash, 32, 32);
const dataURL = parsePixels(pixels, 32, 32);
cache[hash] = dataURL;
return dataURL;
};
// thanks to https://github.com/wheany/js-png-encoder
const parsePixels = (
pixels: Uint8ClampedArray,
width: number,
height: number,
) => {
const pixelsString = [...pixels]
.map((byte) => String.fromCharCode(byte))
.join("");
const pngString = generatePng(width, height, pixelsString);
const dataURL =
typeof Buffer !== "undefined"
? Buffer.from(getPngArray(pngString)).toString("base64")
: btoa(pngString);
return "data:image/png;base64," + dataURL;
};
const getPngArray = (pngString: string) => {
const pngArray = new Uint8Array(pngString.length);
for (let i = 0; i < pngString.length; i++) {
pngArray[i] = pngString.charCodeAt(i);
}
return pngArray;
};
const generatePng = (width: number, height: number, rgbaString: string) => {
const DEFLATE_METHOD = String.fromCharCode(0x78, 0x01);
const CRC_TABLE: number[] = [];
const SIGNATURE = String.fromCharCode(137, 80, 78, 71, 13, 10, 26, 10);
const NO_FILTER = String.fromCharCode(0);
let n, c, k;
// make crc table
for (n = 0; n < 256; n++) {
c = n;
for (k = 0; k < 8; k++) {
if (c & 1) {
c = 0xedb88320 ^ (c >>> 1);
} else {
c = c >>> 1;
}
}
CRC_TABLE[n] = c;
}
// Functions
const inflateStore = (data: string) => {
const MAX_STORE_LENGTH = 65535;
let storeBuffer = "";
let remaining;
let blockType;
for (let i = 0; i < data.length; i += MAX_STORE_LENGTH) {
remaining = data.length - i;
blockType = "";
if (remaining <= MAX_STORE_LENGTH) {
blockType = String.fromCharCode(0x01);
} else {
remaining = MAX_STORE_LENGTH;
blockType = String.fromCharCode(0x00);
}
// little-endian
storeBuffer +=
blockType +
String.fromCharCode(
remaining & 0xff,
(remaining & 0xff00) >>> 8,
);
storeBuffer += String.fromCharCode(
~remaining & 0xff,
(~remaining & 0xff00) >>> 8,
);
storeBuffer += data.substring(i, i + remaining);
}
return storeBuffer;
};
const adler32 = (data: string) => {
let MOD_ADLER = 65521;
let a = 1;
let b = 0;
for (let i = 0; i < data.length; i++) {
a = (a + data.charCodeAt(i)) % MOD_ADLER;
b = (b + a) % MOD_ADLER;
}
return (b << 16) | a;
};
const updateCrc = (crc: number, buf: string) => {
let c = crc;
let b: number;
for (let n = 0; n < buf.length; n++) {
b = buf.charCodeAt(n);
c = CRC_TABLE[(c ^ b) & 0xff] ^ (c >>> 8);
}
return c;
};
const crc = (buf: string) => updateCrc(0xffffffff, buf) ^ 0xffffffff;
const dwordAsString = (dword: number) => String.fromCharCode(
(dword & 0xff000000) >>> 24,
(dword & 0x00ff0000) >>> 16,
(dword & 0x0000ff00) >>> 8,
dword & 0x000000ff,
);
const createChunk = (length: number, type: string, data: string) => {
const CRC = crc(type + data);
return dwordAsString(length) + type + data + dwordAsString(CRC);
};
const createIHDR = (width: number, height: number) => {
const IHDRdata =
dwordAsString(width) +
dwordAsString(height) +
// bit depth
String.fromCharCode(8) +
// color type: 6=truecolor with alpha
String.fromCharCode(6) +
// compression method: 0=deflate, only allowed value
String.fromCharCode(0) +
// filtering: 0=adaptive, only allowed value
String.fromCharCode(0) +
// interlacing: 0=none
String.fromCharCode(0);
return createChunk(13, "IHDR", IHDRdata);
};
// PNG creations
const IEND = createChunk(0, "IEND", "");
const IHDR = createIHDR(width, height);
let scanlines = "";
let scanline;
for (let y = 0; y < rgbaString.length; y += width * 4) {
scanline = NO_FILTER;
if (Array.isArray(rgbaString)) {
for (let x = 0; x < width * 4; x++) {
scanline += String.fromCharCode(rgbaString[y + x] & 0xff);
}
} else {
scanline += rgbaString.substr(y, width * 4);
}
scanlines += scanline;
}
const compressedScanlines =
DEFLATE_METHOD +
inflateStore(scanlines) +
dwordAsString(adler32(scanlines));
const IDAT = createChunk(
compressedScanlines.length,
"IDAT",
compressedScanlines,
);
const pngString = SIGNATURE + IHDR + IDAT + IEND;
return pngString;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment