Skip to content

Instantly share code, notes, and snippets.

@LingDong-
Created October 10, 2022 20:05
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-/9455d75c5134de3cfaacebf094881fa5 to your computer and use it in GitHub Desktop.
Save LingDong-/9455d75c5134de3cfaacebf094881fa5 to your computer and use it in GitHub Desktop.
a baseline somewhat-compressing PNG writer in 150 lines
// 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