Skip to content

Instantly share code, notes, and snippets.

@wout
Forked from mattiaz9/blurhashDataURL.ts
Created April 11, 2023 16:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wout/604d79a594a91e998400b11cbc78031a to your computer and use it in GitHub Desktop.
Save wout/604d79a594a91e998400b11cbc78031a 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"
export function blurHashToDataURL(hash: string | undefined): string | undefined {
if (!hash) return undefined
const pixels = decode(hash, 32, 32)
const dataURL = parsePixels(pixels, 32, 32)
return dataURL
}
// thanks to https://github.com/wheany/js-png-encoder
function 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
}
function getPngArray(pngString: string) {
const pngArray = new Uint8Array(pngString.length)
for (let i = 0; i < pngString.length; i++) {
pngArray[i] = pngString.charCodeAt(i)
}
return pngArray
}
function 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
function 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
}
function 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
}
function 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
}
function crc(buf: string) {
return updateCrc(0xffffffff, buf) ^ 0xffffffff
}
function dwordAsString(dword: number) {
return String.fromCharCode(
(dword & 0xFF000000) >>> 24, (dword & 0x00FF0000) >>> 16, (dword & 0x0000FF00) >>> 8, (dword & 0x000000FF)
)
}
function createChunk(length: number, type: string, data: string) {
const CRC = crc(type + data)
return dwordAsString(length) +
type +
data +
dwordAsString(CRC)
}
function 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