Skip to content

Instantly share code, notes, and snippets.

@mattiaz9
Last active November 16, 2024 13:04
Show Gist options
  • Save mattiaz9/53cb67040fa135cb395b1d015a200aff to your computer and use it in GitHub Desktop.
Save mattiaz9/53cb67040fa135cb395b1d015a200aff 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
}
@chrisspiegl
Copy link

Thank you. I used the fork by @oleggrishechkin in a Nuxt.js app which now supports SSR and Blurhash thanks to this gist.

I tried thinking of a way to make this so that the base64 string that comes out if it would be webp (for even smaller delivery from SSR to client) but didn't have any luck and don't understand some parts of this gist enough πŸ™ˆ, so I decided for now png is good enough. Even having SSR blurhash is incredible.

One thing I did change is that I am trying to use fast-blurhash for the decode. It's supposedly faster, even though I did not really benchmark that πŸ™ˆ.

@johannschopplich
Copy link

johannschopplich commented Apr 21, 2023

Thanks for the code! I have adapted and incorporated it into unlazy – a universal lazy loading library for placeholder images:

  • πŸŽ€ Native: Utilizes the loading="lazy" attribute
  • πŸŽ›οΈ Framework-agnostic: Works with any framework or no framework at all
  • 🌊 BlurHash support: SSR & Client-Side BlurHash Decoding
  • πŸͺ„ Sizing: Automatically calculates the sizes attribute
  • πŸ” SEO-friendly: Detects search engine bots and preloads all images
  • 🎟 <picture>: Supports multiple image tags
  • 🏎 Auto-initialize: Usable without a build step

@tilmanmoser
Copy link

Thanks a lot!

@Cyberistic
Copy link

Heroooooo!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment