Last active
October 10, 2022 20:06
-
-
Save LingDong-/a405ab0220fc9df3747ba3d4937bc625 to your computer and use it in GitHub Desktop.
a bare minimum, non-compressing PNG writer in 80 lines
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
// 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