Created
June 23, 2020 20:09
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const BASE83_ALPHABET = `0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~`; | |
const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; | |
const BMP_HEADER = `data:image/bmp;base64,Qk3mAQAAAAAAADYAAAAoAAAADAAAAPT///8BABgAAAAAALABAAAAAAAAAAAAAAAAAAAAAAAA`; | |
const sRGBToLinear = (value: number) => { | |
const v = value / 255; | |
if (v <= 0.04045) { | |
return v / 12.92; | |
} else { | |
return Math.pow((v + 0.055) / 1.055, 2.4); | |
} | |
}; | |
const linearTosRGB = (value: number) => { | |
const v = Math.max(0, Math.min(1, value)); | |
if (v <= 0.0031308) { | |
return Math.round(v * 12.92 * 255 + 0.5); | |
} else { | |
return Math.round((1.055 * Math.pow(v, 1 / 2.4) - 0.055) * 255 + 0.5); | |
} | |
}; | |
const sign = (n: number) => (n < 0 ? -1 : 1); | |
const signPow = (val: number, exp: number) => sign(val) * Math.pow(Math.abs(val), exp); | |
const decodeDC = (value: number) => [ | |
sRGBToLinear(value >> 16), | |
sRGBToLinear((value >> 8) & 255), | |
sRGBToLinear(value & 255), | |
]; | |
const decodeAC = (value: number, maximumValue: number) => [ | |
signPow((Math.floor(value / (19 * 19)) - 9) / 9, 2.0) * maximumValue, | |
signPow(((Math.floor(value / 19) % 19) - 9) / 9, 2.0) * maximumValue, | |
signPow(((value % 19) - 9) / 9, 2.0) * maximumValue, | |
]; | |
const decode83 = (str: string) => Array.from(str).reduce((value, c) => value * 83 + BASE83_ALPHABET.indexOf(c), 0); | |
export const blurhashToBmp = (blurhash?: string): string | null => { | |
if (!blurhash || blurhash.length < 6) { | |
if (process.env.NODE_ENV === "development") { | |
console.error("The blurhash string must be at least 6 characters"); | |
} | |
return null; | |
} | |
const sizeFlag = decode83(blurhash[0]); | |
const numY = Math.floor(sizeFlag / 9) + 1; | |
const numX = (sizeFlag % 9) + 1; | |
if (blurhash.length !== 4 + 2 * numX * numY) { | |
if (process.env.NODE_ENV === "development") { | |
console.error( | |
`blurhash length mismatch: length is ${blurhash.length} but it should be ${4 + 2 * numX * numY}` | |
); | |
} | |
return null; | |
} | |
const quantisedMaximumValue = decode83(blurhash[1]); | |
const maximumValue = (quantisedMaximumValue + 1) / 166; | |
const colors = new Array(numX * numY); | |
colors[0] = decodeDC(decode83(blurhash.substring(2, 6))); | |
for (let i = 1; i < colors.length; i++) { | |
colors[i] = decodeAC(decode83(blurhash.substring(4 + i * 2, 6 + i * 2)), maximumValue); | |
} | |
var result = BMP_HEADER; | |
for (let y = 0; y < 12; y++) { | |
for (let x = 0; x < 12; x++) { | |
let r = 0; | |
let g = 0; | |
let b = 0; | |
for (let j = 0; j < numY; j++) { | |
for (let i = 0; i < numX; i++) { | |
const basis = Math.cos((Math.PI * x * i) / 12) * Math.cos((Math.PI * y * j) / 12); | |
const color = colors[i + j * numX]; | |
r += color[0] * basis; | |
g += color[1] * basis; | |
b += color[2] * basis; | |
} | |
} | |
const intR = linearTosRGB(r); | |
const intG = linearTosRGB(g); | |
const intB = linearTosRGB(b); | |
result += BASE64_ALPHABET[(intB & 0b11111100) >> 2]; | |
result += BASE64_ALPHABET[((intB & 0b00000011) << 4) | ((intG & 0b11110000) >> 4)]; | |
result += BASE64_ALPHABET[((intG & 0b00001111) << 2) | ((intR & 0b11000000) >> 6)]; | |
result += BASE64_ALPHABET[intR & 0b00111111]; | |
} | |
} | |
return result; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment