Forked from dgca/typescript-png-pixel-art-generator.ts
Created
June 21, 2024 23:16
-
-
Save NetOpWibby/d72964791485afd4a2bda8fa450ddcc5 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
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
/** | |
* 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