Created
October 10, 2022 20:05
-
-
Save LingDong-/9455d75c5134de3cfaacebf094881fa5 to your computer and use it in GitHub Desktop.
a baseline somewhat-compressing PNG writer in 150 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
// baseline somewhat-compressing PNG writer in 150 lines | |
// | |
// - supports gray, gray+A, RGB, RGBA, bit depth : 8 | |
// - uses "fixed" huffman codes of DEFLATE spec | |
// - compresses well for graphics with large "flat" areas, | |
// basically pixel-wise run-length encoding translated to LZ77 style <len,dist>, | |
// always filtered with SUB filter | |
// - writes IHDR, IDAT, IEND -- optional chunks can be | |
// passed as function argument to be encoded (see example) | |
// | |
// see also: | |
// - png_write.js, a non-compressing writer in 80 lines: | |
// https://gist.github.com/LingDong-/a405ab0220fc9df3747ba3d4937bc625 | |
// - png_read.js | |
// https://gist.github.com/LingDong-/fe21246d1b6c69d6418e6e9b13122344 | |
// | |
// 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_png1(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 zs = [0]; | |
let cur = 0; | |
function pbit(u,w,rev=false){ | |
for (let i = 0; i < w; i++){ | |
let q; | |
if (!rev){ | |
q = (u>>(w-i-1)) & 1; | |
}else{ | |
q = (u >> i) & 1; | |
} | |
let b = ~~(cur / 8); | |
if (b >= zs.length){ | |
zs.push(0); | |
} | |
let c = cur % 8; | |
zs[b] |= (q << c); | |
cur++; | |
} | |
} | |
let lenlvls = [0,11,19,35,67,131]; | |
let lenmins = [257,265,269,273,277,281]; | |
pbit(0b110,3); | |
for (let i = 0; i < h; i++){ | |
let diff = []; | |
let p0 = new Array(num_chan).fill(0); | |
for (let j = 0; j < w; j++){ | |
let p1 = new Array(num_chan).fill(0); | |
let p2 = new Array(num_chan).fill(0); | |
for (let k = 0; k < num_chan; k++){ | |
let v = data[(i*w+j)*num_chan+k]; | |
p1[k] = v; | |
p2[k] = (p1[k]-p0[k]+256)%256; | |
} | |
p0 = p1; | |
diff.push(p2); | |
} | |
let row = []; | |
for (let j = 0; j < w; j++){ | |
let pix = []; | |
let eq = true; | |
for (let k = 0; k < num_chan; k++){ | |
let v = diff[j][k]; | |
if (row.length){ | |
eq &= (row[row.length-1].v[k] == v); | |
} | |
pix.push(v); | |
} | |
if (eq && row.length && row[row.length-1].n < ~~(256/num_chan) ){ | |
row[row.length-1].n++; | |
}else{ | |
row.push({v:pix,n:0}); | |
} | |
} | |
pbit(0b00110001,8); | |
for (let j = 0; j < row.length; j++){ | |
for (let k = 0; k < num_chan; k++){ | |
let v = row[j].v[k]; | |
if (v < 144) pbit(v+0b00110000,8); | |
else pbit((v-144)+0b110010000,9); | |
} | |
if (row[j].n >= 3){ | |
let l = num_chan * row[j].n; | |
for (let m = 5; m >=0; m--){ | |
let d = l - lenlvls[m]; | |
if (d >= 0){ | |
let c = lenmins[m] + ~~(d/(1<<m)); | |
let r = d % (1<<m); | |
if (c < 280) pbit((c-256),7); | |
else pbit((c-280)+0b11000000,8); | |
pbit(r,m,true); | |
pbit(num_chan-1,5); | |
break; | |
} | |
} | |
}else if (row[j].n > 0){ | |
for (let jj = 0; jj < row[j].n; jj++){ | |
for (let k = 0; k < num_chan; k++){ | |
let v = row[j].v[k]; | |
if (v < 144) pbit(v+0b00110000,8); | |
else pbit((v-144)+0b110010000,9); | |
} | |
} | |
} | |
} | |
} | |
pbit(0,7); | |
for (let i = 0; i < zs.length; i++) idat.push(zs[i]); | |
idat.push(...uint32be(calc_adler32(zs))); | |
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*(i/w)); | |
data[(i*w+j)*4+1] = 0; | |
data[(i*w+j)*4+2] = 255; | |
data[(i*w+j)*4+3] = 255; | |
} | |
} | |
let buf = write_png1(data,w,h); | |
// optional: add more chunks, e.g. to make resolution 1000x1000 | |
// let buf = write_png1(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