Skip to content

Instantly share code, notes, and snippets.

@sdcooke
Created June 23, 2020 20:09
Show Gist options
  • Save sdcooke/975c9522335bede3fcb550d73dabb0fb to your computer and use it in GitHub Desktop.
Save sdcooke/975c9522335bede3fcb550d73dabb0fb to your computer and use it in GitHub Desktop.
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