Created
October 10, 2022 15:05
-
-
Save LingDong-/fe21246d1b6c69d6418e6e9b13122344 to your computer and use it in GitHub Desktop.
a baseline PNG decoder in 400 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 PNG decoder in 400 lines | |
// | |
// - supports all color types: RGBA, RGB, gray+A, gray, paletted | |
// - supports all bit depths: 1, 2, 4, 8, 16 bits (color type dependant, per spec) | |
// - allways return RGBA(0-255) format: other color+bit combinations will be scaled | |
// - downscaled 16-bit values retain floating point, e.g. 1000/65535 -> 3.89/255 | |
// - support transparency palette / chroma-key | |
// - supports interlaced / non-interlaced | |
// - supports all zlib methods: non-compressed / fixed / dynamic huffman | |
// - supports all filters: none / sub / up / average / paeth | |
// - ignores checksums (CRC / adler32 / etc.) | |
// - ignores gAMA (gamma correction suggestion) | |
// - ignores most ancillary blocks | |
// - not optimized for speed, no error handling | |
// | |
// 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 | |
// - http://www.libpng.org/pub/png/pngsuite-all-good.html | |
// | |
// lingdong 2022, MIT license | |
function read_png(bytes){ | |
let w = (bytes[16]<<24)|(bytes[17]<<16)|(bytes[18]<<8)|bytes[19]; | |
let h = (bytes[20]<<24)|(bytes[21]<<16)|(bytes[22]<<8)|bytes[23]; | |
let chan = [1,null,3,-1,2,null,4][bytes[25]]; | |
let bitdep = bytes[24]; | |
let lace = bytes[28]; | |
let ci=33, ct; | |
let zs = []; | |
let plte = []; | |
let trns = []; | |
do{ | |
let l = (bytes[ci]<<24)|(bytes[ci+1]<<16)|(bytes[ci+2]<<8)|bytes[ci+3]; | |
ct = String.fromCharCode(bytes[ci+4],bytes[ci+5],bytes[ci+6],bytes[ci+7]); | |
if (ct == 'IDAT'){ | |
for (let i = ci+8; i < ci+8+l; i++) zs.push(bytes[i]); | |
} | |
if (ct == 'PLTE' && chan == -1){ | |
for (let i = ci+8; i < ci+8+l; i+=3){ | |
plte.push([bytes[i],bytes[i+1],bytes[i+2]]); | |
} | |
} | |
if (ct == 'tRNS'){ | |
for (let i = ci+8; i < ci+8+l; i++) trns.push(bytes[i]); | |
} | |
ci += l + 12; | |
}while(ct != 'IEND'); | |
zs = zs.slice(2,-4); | |
let cur = 0; | |
function gbit(n=1,rev=false){ | |
function g(i){ | |
let b = ~~(i/8); | |
return (zs[b] >> (i%8)) &1 ; | |
} | |
let o = 0; | |
for (let j = 0; j < n; j++){ | |
if (!rev) o = (o << 1) | g(cur++); | |
else o |= (g(cur++) << j) | |
} | |
return o; | |
} | |
let bfinal,btype; | |
let dat = []; | |
function glz(a,read_dst=null){ | |
let len, ex0, dst, ex1; | |
if (a<=264){ | |
len = a - 254; | |
}else if (a<=284){ | |
let n = ~~((a-265)/4) + 1; | |
let m = (a-265)%4; | |
ex0 = gbit(n,true); | |
len = ex0 + m * (1<<n) + [0,11,19,35,67,131][n]; | |
}else{ | |
len = 258; | |
} | |
let d; | |
if (read_dst){ | |
d = read_dst(); | |
}else{ | |
d = gbit(5); | |
} | |
if (d <= 3){ | |
dst = d+1; | |
}else{ | |
let n = ~~((d-4)/2) + 1; | |
let m = (d-4)%2; | |
ex1 = gbit(n,true); | |
dst = ex1 + m * (1<<n) + [0,5,9,17,33,65,129,257,513,1025,2049,4097,8193,16385][n]; | |
} | |
let ptr = dat.length-dst; | |
for (let i = 0; i < len; i++){ | |
dat.push(dat[ptr++]); | |
} | |
} | |
function mkhuff(lens,num){ | |
let bl_count = new Array(num).fill(0); | |
for (let i = 0; i < num; i++){ | |
bl_count[lens[i]]++; | |
} | |
bl_count[0] = 0; | |
let MAX_BITS = 15; | |
let code = 0; | |
let nextcode = new Array(16).fill(0); | |
for (let bits = 1; bits <= MAX_BITS; bits++) { | |
code = (code + bl_count[bits-1]) << 1; | |
nextcode[bits] = code; | |
} | |
let codes = {}; | |
for (let n = 0; n < num; n++) { | |
let len = lens[n]; | |
if (len != 0) { | |
codes[nextcode[len].toString(2).padStart(lens[n],'0')] = n; | |
nextcode[len]++; | |
} | |
} | |
return codes; | |
} | |
do{ | |
bfinal = gbit(1); | |
btype = gbit(2,true); | |
if (btype == 0){ | |
zs.reverse(); | |
zs.pop(); | |
let len = zs.pop() | (zs.pop() << 8); | |
let nle = zs.pop() | (zs.pop() << 8); | |
for (let i = 0; i < len; i++){ | |
dat.push(zs.pop()); | |
} | |
zs.reverse(); | |
cur = 0; | |
}else if (btype == 1){ | |
while (1){ | |
let a = gbit(7); | |
if (a == 0){ | |
break; | |
}else if (a <= 0b0010111){ | |
glz(a+256); | |
}else{ | |
a = (a << 1) | gbit(1); | |
if (a <= 0b10111111){ | |
dat.push(a-0b00110000+0); | |
}else if (a <= 0b11000111){ | |
glz(a-0b11000000+280); | |
}else{ | |
a = (a << 1) | gbit(1); | |
dat.push(a-0b110010000+144); | |
} | |
} | |
} | |
}else if (btype == 2){ | |
let hlit = gbit(5,true) + 257; | |
let hdist = gbit(5,true) + 1; | |
let hclen = gbit(4,true) + 4; | |
let lens = new Array(19).fill(0); | |
let ordr = [16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]; | |
for (let i = 0; i < hclen; i++){ | |
lens[ordr[i]] = gbit(3,true); | |
} | |
let codes = mkhuff(lens,19); | |
let n = 0; | |
let b = ""; | |
let lencodes = []; | |
while (n < hlit+hdist){ | |
let a = gbit(1); | |
b += ""+a; | |
let c = codes[b]; | |
if (c != undefined){ | |
b = ""; | |
let f=0,m; | |
if (c <= 15){ | |
lencodes[n++] = c; | |
}else if (c == 16){ | |
m = gbit(2,true)+3; | |
f = lencodes[n-1]; | |
}else if (c == 17){ | |
m = gbit(3,true)+3; | |
}else if (c == 18){ | |
m = gbit(7,true)+11; | |
} | |
for (let j = 0; j < m; j++) lencodes[n++] = f; | |
} | |
} | |
let zlen = mkhuff(lencodes.slice(0,hlit),hlit); | |
let zdst = mkhuff(lencodes.slice(hlit),hdist); | |
b = ""; | |
while (1){ | |
b += ""+gbit(1); | |
let c = zlen[b]; | |
if (c != undefined){ | |
b = ""; | |
if (c < 256){ | |
dat.push(c); | |
}else if (c == 256){ | |
break; | |
}else{ | |
glz(c,function(){ | |
let bb = ""; | |
while (1){ | |
bb += ""+gbit(1); | |
let cc = zdst[bb]; | |
if (cc != undefined) return cc; | |
} | |
}); | |
} | |
} | |
} | |
} | |
}while(!bfinal); | |
function paeth (a, b, c){ | |
let p = a + b - c; | |
let pa = Math.abs(p - a); | |
let pb = Math.abs(p - b); | |
let pc = Math.abs(p - c); | |
if (pa <= pb && pa <= pc) return a; | |
else if (pb <= pc) return b; | |
return c; | |
} | |
let bpp = (chan >= 2) ? (chan * bitdep/8) : Math.ceil(bitdep/8); // byte/pix | |
function mkdata(bpl,h){ | |
let data = []; | |
for (let i = 0; i < h; i++){ | |
let t = dat.pop(); | |
for (let j = 0; j < (bpl)/bpp; j++){ | |
for (let k = 0; k < bpp; k++){ | |
let v = dat.pop(); | |
let ua = j?data[i*(bpl)+(j-1)*bpp+k]:0; | |
let ub = i?data[(i-1)*(bpl)+(j)*bpp+k]:0; | |
let uc = (i&&j)?data[(i-1)*(bpl)+(j-1)*bpp+k]:0; | |
if (t == 0){ | |
data.push(v); | |
}else if (t == 1){ | |
data.push( (ua+v)%256 ); | |
}else if (t == 2){ | |
data.push( (ub+v)%256 ); | |
}else if (t == 3){ | |
data.push( ((~~((ua+ub)/2))+v)%256 ); | |
}else if (t == 4){ | |
let u = paeth(ua,ub,uc); | |
data.push( (u+v)%256 ); | |
} | |
} | |
} | |
} | |
return data; | |
} | |
function mkrgba(data,w,h,bpl){ | |
if (chan == 4 && bitdep == 8){ | |
return data; | |
} | |
let kr,kg,kb,fix_chroma; | |
if (fix_chroma = (trns.length && ((chan == 3) || (chan == 1 && bitdep>=8) ))){ | |
kr = kg = kb = (trns[0]<<8 | trns[1]); | |
if (chan > 1){ | |
kg = (trns[2]<<8 | trns[3]); | |
kb = (trns[4]<<8 | trns[5]); | |
} | |
} | |
let rgba = new Array(w*h*4); | |
function remap8(a,b,c,d){ | |
for (let i = 0; i < w*h; i++){ | |
rgba[i*4 ] = (a>=0)?data[i*chan+a]:255; | |
rgba[i*4+1] = (b>=0)?data[i*chan+b]:255; | |
rgba[i*4+2] = (c>=0)?data[i*chan+c]:255; | |
rgba[i*4+3] = (d>=0)?data[i*chan+d]:255; | |
} | |
} | |
function remap16(a,b,c,d,dont_scale=false){ | |
let s=1,q=65535; | |
if (!dont_scale){ | |
s = 255/65535; q = 255; | |
} | |
for (let i = 0; i < w*h; i++){ | |
rgba[i*4 ] = (a>=0)?( ((data[i*chan*2+a*2]<<8) | data[i*chan*2+a*2])*s ):q; | |
rgba[i*4+1] = (b>=0)?( ((data[i*chan*2+b*2]<<8) | data[i*chan*2+b*2])*s ):q; | |
rgba[i*4+2] = (c>=0)?( ((data[i*chan*2+c*2]<<8) | data[i*chan*2+c*2])*s ):q; | |
rgba[i*4+3] = (d>=0)?( ((data[i*chan*2+d*2]<<8) | data[i*chan*2+d*2])*s ):q; | |
} | |
} | |
function gpb(i){ | |
let b = ~~(i * bitdep / 8); | |
let c = (i*bitdep) % 8; | |
let s = ((8-bitdep)-c); | |
let d = (data[b] >>> s) & ((1<<bitdep)-1); | |
return d; | |
} | |
if (bitdep == 8){ | |
if (chan == 4) remap8(0,1,2,3); | |
else if (chan == 3) remap8(0,1,2,-1); | |
else if (chan == 2) remap8(0,0,0,1); | |
else if (chan == 1) remap8(0,0,0,-1); | |
else{ | |
for (let i = 0; i < w*h; i++){ | |
rgba[i*4 ] = plte[data[i]][0]; | |
rgba[i*4+1] = plte[data[i]][1]; | |
rgba[i*4+2] = plte[data[i]][2]; | |
rgba[i*4+3] = trns[data[i]]??255; | |
} | |
} | |
}else if (bitdep == 16){ | |
if (chan == 4) remap16(0,1,2,3); | |
else if (chan == 3) remap16(0,1,2,-1, fix_chroma); | |
else if (chan == 2) remap16(0,0,0,1); | |
else if (chan == 1) remap16(0,0,0,-1, fix_chroma); | |
else{ | |
for (let i = 0; i < w*h; i++){ | |
let idx = (data[i*2]<<8) | data[i*2+1]; | |
rgba[i*4 ] = plte[idx][0]; | |
rgba[i*4+1] = plte[idx][1]; | |
rgba[i*4+2] = plte[idx][2]; | |
rgba[i*4+3] = trns[idx]??255; | |
} | |
} | |
}else{ | |
if (chan == 1){ | |
let key = trns.length ? (trns[0]<<8 | trns[1]) : -1; | |
for (let i = 0; i < h; i++){ | |
for (let j = 0; j < w; j++){ | |
let idx = gpb((bpl)*i*8/bitdep+j); | |
if (idx !== key){ | |
let v = idx/((1<<bitdep)-1) * 255; | |
rgba[(i*w+j)*4 ] = v; | |
rgba[(i*w+j)*4+1] = v; | |
rgba[(i*w+j)*4+2] = v; | |
rgba[(i*w+j)*4+3] = 255; | |
} | |
} | |
} | |
}else{ | |
for (let i = 0; i < h; i++){ | |
for (let j = 0; j < w; j++){ | |
let idx = gpb((bpl)*i*8/bitdep+j); | |
let v = plte[idx]; | |
rgba[(i*w+j)*4 ] = v[0]; | |
rgba[(i*w+j)*4+1] = v[1]; | |
rgba[(i*w+j)*4+2] = v[2]; | |
rgba[(i*w+j)*4+3] = trns[idx]??255; | |
} | |
} | |
} | |
} | |
if (fix_chroma){ | |
for (let i = 0; i < rgba.length; i+=4){ | |
if (rgba[i] == kr && rgba[i+1] == kg && rgba[i+2] == kb) rgba[i+3] = 0; | |
} | |
if (bitdep == 16){ | |
for (let i = 0; i < rgba.length; i++){ | |
rgba[i] *= 255/65535; | |
} | |
} | |
} | |
return rgba; | |
} | |
let bpl = Math.ceil(w * bitdep * Math.abs(chan)/8); | |
let data,rgba; | |
dat.reverse(); | |
function cpy4(i,j,b,k){ | |
if (b && j<w && i<h){ | |
let idx = i*w*4+j*4; k*=4; | |
rgba[idx] = b[k]; rgba[idx+1] = b[k+1]; rgba[idx+2] = b[k+2]; rgba[idx+3] = b[k+3]; | |
} | |
} | |
if (lace){ | |
let sb, sh; | |
let w0 = Math.ceil(w/8), w1 = Math.ceil((w-4)/8), w2 = Math.ceil(w/4); | |
let w3 = Math.ceil((w-2)/4), w4 = Math.ceil(w/2), w5 = Math.ceil((w-1)/2); | |
let q = bitdep*Math.abs(chan)/8; | |
let im0 = mkrgba(mkdata(sb=Math.ceil(w0*q),sh=Math.ceil(h/8)), w0,sh,sb); | |
let im1 = (w>4)?mkrgba(mkdata(sb=Math.ceil(w1*q),sh=Math.ceil(h/8)), w1,sh,sb):null; | |
let im2 = (h>4)?mkrgba(mkdata(sb=Math.ceil(w2*q),sh=Math.ceil((h-4)/8)), w2,sh,sb):null; | |
let im3 = (w>2)?mkrgba(mkdata(sb=Math.ceil(w3*q),sh=Math.ceil(h/4)), w3,sh,sb):null; | |
let im4 = (h>2)?mkrgba(mkdata(sb=Math.ceil(w4*q),sh=Math.ceil((h-2)/4)), w4,sh,sb):null; | |
let im5 = (w>1)?mkrgba(mkdata(sb=Math.ceil(w5*q),sh=Math.ceil(h/2)), w5,sh,sb):null; | |
let im6 = (h>1)?mkrgba(mkdata(sb=bpl, sh=Math.ceil(h/2)), w ,sh,sb):null; | |
rgba = new Array(w*h*4); | |
for (let i = 0; i < h; i+=8){ | |
for (let j = 0; j < w; j+=8){ | |
cpy4( i,j, im0, (~~(i/8)*w0+~~(j/8)) ); | |
cpy4( i,j+4, im1, (~~(i/8)*w1+~~(j/8)) ); | |
cpy4( i+4,j, im2, (~~(i/8)*w2+~~(j/4)) );cpy4( i+4,j+4, im2, (~~(i/8)*w2+~~(j/4)+1) ); | |
cpy4( i,j+2, im3, (~~(i/4)*w3+~~(j/4)) );cpy4( i,j+6, im3, (~~(i/4)*w3+~~(j/4)+1) ); | |
cpy4( i+4,j+2, im3, (~~(i/4+1)*w3+~~(j/4)) );cpy4( i+4,j+6, im3, (~~(i/4+1)*w3+~~(j/4)+1)); | |
for (let k = 0; k < 8; k++) cpy4( i+~~(k/4)*4+2, j+(k%4)*2, im4, (~~(i/4+~~(k/4))*w4+~~(j/2)+~~(k%4))); | |
for (let k = 0; k <16; k++) cpy4( i+~~(k/4)*2, j+(k%4)*2+1, im5, (~~(i/2+~~(k/4))*w5+~~(j/2)+~~(k%4))); | |
} | |
} | |
for (let i = 0; (i < h) && im6; i+=2){ | |
for (let j = 0; j < w*4; j++) rgba[(i+1)*w*4+j] = im6[(i/2)*w*4+j]; | |
} | |
}else{ | |
data = mkdata(bpl,h); | |
rgba = mkrgba(data,w,h,bpl); | |
} | |
return [rgba,w,h]; | |
} | |
///////////////////////////////////////////// | |
// node.js example | |
const fs = require('fs'); | |
let bytes = new Uint8Array(fs.readFileSync("test.png")); | |
let [data,w,h] = read_png(bytes); | |
// https://gist.github.com/LingDong-/a405ab0220fc9df3747ba3d4937bc625 | |
// fs.writeFileSync("out.png",new Uint8Array(write_png(data,w,h))); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment