Skip to content

Instantly share code, notes, and snippets.

@dgca
Last active June 23, 2024 09:18
Show Gist options
  • Save dgca/adf5b2c2016f1ca4632531d7521fc08f to your computer and use it in GitHub Desktop.
Save dgca/adf5b2c2016f1ca4632531d7521fc08f to your computer and use it in GitHub Desktop.
Zero dependency function to generate an image in PNG format and return it as a base64-encoded string
/**
* Generates an image in PNG format and returns it as a base64-encoded string.
*
* @param colors - An array of color values
* @param pixels - An array of pixel values
* @param width - The width of the image
* @param height - The height of the image
* @param scale - The scale factor to apply to the image (default: 1, uses nearest-neighbor interpolation)
* @returns The base64-encoded string representation of the generated image
*/
function generateBase64EncodedPng(
colors: string[],
pixels: number[],
width: number,
height: number,
scale: number = 1,
): string {
const pngData = createPngData(colors, pixels, width, height, scale);
// Chunk out the base64 conversion to avoid call stack size exceeded error
const CHUNK_SIZE = 0x8000; // 32 KB
let imageString = "";
for (let i = 0; i < pngData.length; i += CHUNK_SIZE) {
imageString += String.fromCharCode.apply(
null,
pngData.slice(i, i + CHUNK_SIZE),
);
}
const image = btoa(imageString);
return `data:image/png;base64,${image}`;
}
/**
* Generates an image in PNG format and returns it as a Uint8Array.
* @param colors - An array of color values
* @param pixels - An array of pixel values
* @param width - The width of the image
* @param height - The height of the image
* @param scale - The scale factor to apply to the image (default: 1, uses nearest-neighbor interpolation)
* @returns The Uint8Array representation of the generated image
*/
function generatePng(
colors: string[],
pixels: number[],
width: number,
height: number,
scale: number = 1,
): Uint8Array {
const pngData = createPngData(colors, pixels, width, height, scale);
return new Uint8Array(pngData);
}
function createPngData(
colors: string[],
pixels: number[],
width: number,
height: number,
scale: number = 1,
): number[] {
const scaledWidth = width * scale;
const scaledHeight = height * scale;
const pngSignature = [137, 80, 78, 71, 13, 10, 26, 10];
const ihdr = createIHDRChunk(scaledWidth, scaledHeight);
const idat = createIDATChunk(colors, pixels, width, height, scale);
const iend = createIENDChunk();
return [...pngSignature, ...ihdr, ...idat, ...iend];
}
function createIHDRChunk(width: number, height: number): number[] {
const chunkData = [
...intToBytes(width, 4),
...intToBytes(height, 4),
8, // Bit depth
2, // Color type (2 = Truecolor)
0, // Compression method
0, // Filter method
0, // Interlace method
];
return createChunk("IHDR", chunkData);
}
function createIDATChunk(
colors: string[],
pixels: number[],
width: number,
height: number,
scale: number,
): number[] {
const scaledWidth = width * scale;
const scaledHeight = height * scale;
const scaledPixelData: number[] = [];
for (let y = 0; y < scaledHeight; y++) {
scaledPixelData.push(0); // Filter type byte (0 = None)
for (let x = 0; x < scaledWidth; x++) {
const sourceX = Math.floor(x / scale);
const sourceY = Math.floor(y / scale);
const sourceIndex = sourceY * width + sourceX;
const color = hexToRGB(colors[pixels[sourceIndex]]);
scaledPixelData.push(...color);
}
}
const deflateData = [
0x78,
0x01, // DEFLATE header
...createUncompressedBlock(scaledPixelData),
];
return createChunk("IDAT", deflateData);
}
function createIENDChunk(): number[] {
return createChunk("IEND", []);
}
function createChunk(type: string, data: number[]): number[] {
const typeBytes = type.split("").map((char) => char.charCodeAt(0));
const length = intToBytes(data.length, 4);
const crc = calculateCRC([...typeBytes, ...data]);
return [...length, ...typeBytes, ...data, ...crc];
}
function intToBytes(num: number, bytes: number): number[] {
const result: number[] = [];
for (let i = bytes - 1; i >= 0; i--) {
result.push((num >> (i * 8)) & 0xff);
}
return result;
}
function hexToRGB(hex: string): number[] {
return [
parseInt(hex.slice(1, 3), 16),
parseInt(hex.slice(3, 5), 16),
parseInt(hex.slice(5, 7), 16),
];
}
function calculateCRC(data: number[]): number[] {
let crc = 0xffffffff;
const crcTable = generateCRCTable();
for (const byte of data) {
crc = (crc >>> 8) ^ crcTable[(crc ^ byte) & 0xff];
}
crc = crc ^ 0xffffffff;
return [
(crc >>> 24) & 0xff,
(crc >>> 16) & 0xff,
(crc >>> 8) & 0xff,
crc & 0xff,
];
}
function generateCRCTable(): number[] {
const table: number[] = new Array(256);
for (let n = 0; n < 256; n++) {
let c = n;
for (let k = 0; k < 8; k++) {
if (c & 1) {
c = 0xedb88320 ^ (c >>> 1);
} else {
c = c >>> 1;
}
}
table[n] = c;
}
return table;
}
function createUncompressedBlock(data: number[]): number[] {
const result: number[] = [];
for (let i = 0; i < data.length; i += 65535) {
const chunk = data.slice(i, Math.min(i + 65535, data.length));
const len = chunk.length;
const nlen = ~len & 0xffff;
result.push(
i + 65535 >= data.length ? 0x01 : 0x00, // BFINAL bit
len & 0xff,
(len >> 8) & 0xff,
nlen & 0xff,
(nlen >> 8) & 0xff
);
for (let j = 0; j < chunk.length; j++) {
result.push(chunk[j]);
}
}
return result;
}
// Example
const colors = [
"#000000",
"#FFFFFF",
"#1D2B53",
"#7E2553",
"#008751",
"#AB5236",
"#5F574F",
"#C2C3C7",
"#FF004D",
"#FFA300",
"#FFEC27",
"#00E436",
"#29ADFF",
"#83769C",
"#FF77A8",
"#FFCCAA",
];
const pixels = [
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 0, 12, 12, 0, 12, 12, 12, 12, 12, 12, 12, 0, 12, 12, 12, 12, 0,
12, 12, 12, 12, 12, 12, 12, 0, 12, 12, 12, 12, 0, 0, 12, 12, 0, 12, 12, 12,
12, 0, 0, 12, 0, 12, 0, 12, 0, 12, 12, 12, 12, 12, 0, 0, 12, 0, 12, 0, 12, 0,
12, 0, 0, 12, 0, 12, 0, 0, 12, 0, 12, 12, 0, 0, 12, 12, 12, 0, 12, 0, 0, 12,
0, 12, 12, 0, 0, 12, 12, 12, 0, 0, 0, 12, 0, 12, 0, 0, 12, 0, 0, 12, 0, 12, 0,
12, 0, 12, 12, 0, 0, 12, 0, 0, 12, 0, 12, 0, 12, 0, 12, 8, 8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 9, 14, 9, 1, 1, 1, 14, 9, 14, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 14, 10, 14, 1, 1, 1, 9, 10, 9, 1, 1, 1, 1, 0, 0,
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 9, 14, 9, 1, 1, 1, 14, 9, 14, 1,
1, 1, 1, 9, 0, 0, 1, 1, 1, 1, 1, 1, 1, 8, 8, 1, 8, 8, 1, 1, 1, 1, 1, 11, 1,
11, 1, 1, 1, 1, 1, 1, 9, 9, 0, 0, 1, 1, 1, 1, 1, 1, 8, 8, 8, 8, 8, 8, 8, 1, 1,
1, 1, 1, 4, 11, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 8, 8, 8, 8, 8,
8, 8, 1, 14, 9, 14, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1,
1, 8, 8, 8, 8, 8, 1, 1, 9, 10, 9, 1, 1, 11, 4, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1,
1, 1, 1, 1, 1, 1, 1, 8, 8, 8, 1, 1, 1, 14, 9, 14, 11, 11, 1, 1, 11, 1, 1, 1,
1, 1, 6, 7, 7, 6, 6, 1, 1, 1, 1, 1, 1, 1, 8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
11, 1, 1, 1, 1, 6, 6, 7, 7, 6, 6, 6, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 6, 7, 7, 7, 7, 6, 6, 6, 0, 0, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 7, 7, 7, 7, 7, 7, 5, 5, 0, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 7, 7, 7, 7, 5, 5, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 7, 7, 5,
5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 0, 1, 0, 1, 1, 1, 1, 14, 12, 14, 1, 1, 12, 14, 12, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 14, 10, 14, 1, 1, 14, 10, 14, 1, 1,
1, 1, 8, 8, 1, 8, 8, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 12, 14, 12, 1, 1,
14, 12, 14, 1, 1, 1, 8, 8, 8, 8, 8, 8, 8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 4, 1, 11, 1, 1, 1, 1, 1, 1, 8, 8, 8, 8, 8, 8, 8, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 11, 11, 11, 1, 1, 1, 1, 1, 1, 1, 1, 8, 8, 8, 8, 8, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 11, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 8, 8, 8, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 11, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 12, 14, 14, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 14,
10, 12, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 12, 14, 14, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1,
1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1,
1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0,
1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1,
0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1,
1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1,
0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0,
1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1,
1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0,
1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1,
1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0,
0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1,
1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0,
0, 1, 1, 1, 1, 1, 1, 8, 8, 8, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1,
1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1,
];
const width = 30;
const height = 50;
const scale = 10;
console.time("base64 executed in...");
const base64result = generateBase64EncodedPng(
colors,
pixels,
width,
height,
scale,
);
console.timeEnd("base64 executed in...");
console.log(base64result);
console.time("raw png executed in...");
generatePng(colors, pixels, width, height, scale);
console.timeEnd("raw png executed in...");
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment