Skip to content

Instantly share code, notes, and snippets.

@LingDong-
Last active October 10, 2022 20:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save LingDong-/a405ab0220fc9df3747ba3d4937bc625 to your computer and use it in GitHub Desktop.
Save LingDong-/a405ab0220fc9df3747ba3d4937bc625 to your computer and use it in GitHub Desktop.
a bare minimum, non-compressing PNG writer in 80 lines
// a bare minimum, non-compressing PNG writer in 80 lines
//
// - supports gray, gray+A, RGB, RGBA, bit depth: 8 (0-255)
// - uses "non-compressed blocks" of DEFLATE spec
// - writes IHDR, IDAT, IEND -- optional chunks can be
// passed as function argument to be encoded (see example)
//
// reference:
// - https://en.wikipedia.org/wiki/Portable_Network_Graphics
// - https://datatracker.ietf.org/doc/html/rfc1951
// - https://datatracker.ietf.org/doc/html/rfc1950
// - https://www.rfc-editor.org/rfc/rfc2083
// - https://www.w3.org/TR/PNG/#D-CRCAppendix
//
// lingdong 2022, MIT license
function write_png(data,w,h,num_chan=4,add_chunks=[]){
function crc_8b(c){
for (let k = 0; k < 8; k++) {
if (c & 1) c = 0xedb88320 ^ (c >>> 1);
else c = c >>> 1;
}
return c>>>0;
}
function calc_crc(buf){
let c = 0xffffffff>>>0;
for (let n = 0; n < buf.length; n++){
c = (crc_8b((c^buf[n]) & 0xff) ^ (c>>>8))>>>0;
}
return (c ^ 0xffffffff)>>>0;
}
function calc_adler32(buf){
let adler = 1;
let s1 = adler & 0xffff;
let s2 = (adler >>> 16) & 0xffff;
let n;
for (n = 0; n < buf.length; n++) {
s1 = (s1 + buf[n]) % 65521;
s2 = (s2 + s1) % 65521;
}
return (s2 << 16) + s1;
}
let buf = [0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,0x0A];
function uint32be(x){
return [(x>>24)&0xff,(x>>16)&0xff,(x>>8)&0xff,x&0xff]
}
function write_chunk(name,data){
buf.push(...uint32be(data.length));
let bytes = [name.charCodeAt(0),name.charCodeAt(1),name.charCodeAt(2),name.charCodeAt(3)].concat(data);
let crc = calc_crc(bytes);
for (let i = 0; i < bytes.length; i++) buf.push(bytes[i]);
buf.push(...uint32be(crc));
}
write_chunk("IHDR",[...uint32be(w),...uint32be(h),8,[null,0,4,2,6][num_chan],0,0,0]);
for (let i = 0; i < add_chunks.length; i++){
write_chunk(add_chunks[i][0],add_chunks[i].slice(1));
}
let cmf = 8;
let flg = Math.ceil(cmf*256/31)*31-cmf*256;
let idat = [cmf,flg];
let raw = [];
for (let i = 0; i < h; i++){
let imgdata = [0];
for (let j = 0; j < w; j++){
for (let k = 0; k < num_chan; k++){
imgdata.push(~~data[(i*w+j)*num_chan+k]);
}
}
let len = imgdata.length;
let nlen = ((~imgdata.length)>>>0)&0xffff;
idat.push(Number(i==h-1));
idat.push(len&0xff,(len>>8)&0xff, nlen&0xff,(nlen>>8)&0xff);
imgdata.forEach(x=>{
raw.push(x);
idat.push(x);
})
}
idat.push(...uint32be(calc_adler32(raw)));
write_chunk("IDAT",idat);
write_chunk("IEND",[])
return buf;
}
/////////////////////////////////////////////
// node.js example
const fs = require('fs');
let w = 640;
let h = 480;
let data = new Array(w*h*4);
for (let i = 0; i < h; i++){
for (let j = 0; j < w; j++){
data[(i*w+j)*4+0] = 255;
data[(i*w+j)*4+1] = ~~(255*(j/w));
data[(i*w+j)*4+2] = ~~(255*(i/h));
data[(i*w+j)*4+3] = ~~(Math.sin(i*0.2)*127+128);
}
}
let buf = write_png(data,w,h);
// optional: add more chunks, e.g. to make resolution 1000x1000
// let buf = write_png(data,w,h,4,[["pHYs",0,0,0x99,0xca,0,0,0x99,0xca,1]]);
fs.writeFileSync("test.png",new Uint8Array(buf));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment