Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update UPNG.js from upstream, document tabs option #2

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,12 @@ var pngImage = window.UPNG.decode(/* ... */)

UPNG.js supports APNG and the interface expects "frames". Regular PNG is just a single-frame animation (single-item array).

#### `UPNG.encode(imgs, w, h, cnum, [dels])`
#### `UPNG.encode(imgs, w, h, cnum, [dels], [tabs])`
* `imgs`: array of frames. A frame is an ArrayBuffer containing the pixel data (RGBA, 8 bits per channel)
* `w`, `h` : width and height of the image
* `cnum`: number of colors in the result; 0: all colors (lossless PNG)
* `dels`: array of millisecond delays for each frame (only when 2 or more frames)
* `tabs`: object of options. `{ loop: 1 }` will play the animation only once (default is `0`, will play indefinitely).
* returns an ArrayBuffer with binary data of a PNG file

UPNG.js can do a lossy minification of PNG files, similar to [TinyPNG](https://tinypng.com/) and other tools. It performed quantization with [k-means algorithm](https://en.wikipedia.org/wiki/K-means_clustering) in the past, but now we use [K-d trees](https://en.wikipedia.org/wiki/K-d_tree).
Expand Down
157 changes: 120 additions & 37 deletions UPNG.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ UPNG.toRGBA8.decodeImage = function(data, w, h, out)
else if(depth== 2) for(var x=0; x<w; x++) { var gr= 85*((data[off+(x>>>2)]>>>(6 -((x&3)<<1)))& 3), al=(gr==tr* 85)?0:255; bf32[to+x]=(al<<24)|(gr<<16)|(gr<<8)|gr; }
else if(depth== 4) for(var x=0; x<w; x++) { var gr= 17*((data[off+(x>>>1)]>>>(4 -((x&1)<<2)))&15), al=(gr==tr* 17)?0:255; bf32[to+x]=(al<<24)|(gr<<16)|(gr<<8)|gr; }
else if(depth== 8) for(var x=0; x<w; x++) { var gr=data[off+ x], al=(gr ==tr)?0:255; bf32[to+x]=(al<<24)|(gr<<16)|(gr<<8)|gr; }
else if(depth==16) for(var x=0; x<w; x++) { var gr=data[off+(x<<1)], al=(rs(data,off+(x<<i))==tr)?0:255; bf32[to+x]=(al<<24)|(gr<<16)|(gr<<8)|gr; }
else if(depth==16) for(var x=0; x<w; x++) { var gr=data[off+(x<<1)], al=(rs(data,off+(x<<1))==tr)?0:255; bf32[to+x]=(al<<24)|(gr<<16)|(gr<<8)|gr; }
}
}
//console.log(Date.now()-time);
Expand All @@ -114,6 +114,7 @@ UPNG.decode = function(buff)
//console.log(type,len);

if (type=="IHDR") { UPNG.decode._IHDR(data, offset, out); }
else if(type=="CgBI") { out.tabs[type] = data.slice(offset,offset+4); }
else if(type=="IDAT") {
for(var i=0; i<len; i++) dd[doff+i] = data[offset+i];
doff += len;
Expand Down Expand Up @@ -143,11 +144,16 @@ UPNG.decode = function(buff)
out.tabs[type] = [];
for(var i=0; i<8; i++) out.tabs[type].push(bin.readUint(data, offset+i*4));
}
else if(type=="tEXt") {
else if(type=="tEXt" || type=="zTXt") {
if(out.tabs[type]==null) out.tabs[type] = {};
var nz = bin.nextZero(data, offset);
var keyw = bin.readASCII(data, offset, nz-offset);
var text = bin.readASCII(data, nz+1, offset+len-nz-1);
var text, tl=offset+len-nz-1;
if(type=="tEXt") text = bin.readASCII(data, nz+1, tl);
else {
var bfr = UPNG.decode._inflate(data.slice(nz+2,nz+2+tl));
text = bin.readUTF8(bfr,0,bfr.length);
}
out.tabs[type][keyw] = text;
}
else if(type=="iTXt") {
Expand All @@ -160,7 +166,12 @@ UPNG.decode = function(buff)
var ltag = bin.readASCII(data, off, nz-off); off = nz + 1;
nz = bin.nextZero(data, off);
var tkeyw = bin.readUTF8(data, off, nz-off); off = nz + 1;
var text = bin.readUTF8(data, off, len-(off-offset));
var text, tl=len-(off-offset);
if(cflag==0) text = bin.readUTF8(data, off, tl);
else {
var bfr = UPNG.decode._inflate(data.slice(off,off+tl));
text = bin.readUTF8(bfr,0,bfr.length);
}
out.tabs[type][keyw] = text;
}
else if(type=="PLTE") {
Expand All @@ -187,12 +198,12 @@ UPNG.decode = function(buff)
else if(type=="IEND") {
break;
}
//else { log("unknown chunk type", type, len); }
//else { console.log("unknown chunk type", type, len); out.tabs[type]=data.slice(offset,offset+len); }
offset += len;
var crc = bin.readUint(data, offset); offset += 4;
}
if(foff!=0) { var fr = out.frames[out.frames.length-1];
fr.data = UPNG.decode._decompress(out, fd.slice(0,foff), fr.rect.width, fr.rect.height); foff=0;
fr.data = UPNG.decode._decompress(out, fd.slice(0,foff), fr.rect.width, fr.rect.height);
}
out.data = UPNG.decode._decompress(out, dd, out.width, out.height);

Expand All @@ -203,7 +214,8 @@ UPNG.decode = function(buff)
UPNG.decode._decompress = function(out, dd, w, h) {
var time = Date.now();
var bpp = UPNG.decode._getBPP(out), bpl = Math.ceil(w*bpp/8), buff = new Uint8Array((bpl+1+out.interlace)*h);
dd = UPNG.decode._inflate(dd,buff);
if(out.tabs["CgBI"]) dd = UPNG.inflateRaw(dd,buff);
else dd = UPNG.decode._inflate(dd,buff);
//console.log(dd.length, buff.length);
//console.log(Date.now()-time);

Expand All @@ -217,7 +229,7 @@ UPNG.decode._decompress = function(out, dd, w, h) {
UPNG.decode._inflate = function(data, buff) { var out=UPNG["inflateRaw"](new Uint8Array(data.buffer, 2,data.length-6),buff); return out; }
UPNG.inflateRaw=function(){var H={};H.H={};H.H.N=function(N,W){var R=Uint8Array,i=0,m=0,J=0,h=0,Q=0,X=0,u=0,w=0,d=0,v,C;
if(N[0]==3&&N[1]==0)return W?W:new R(0);var V=H.H,n=V.b,A=V.e,l=V.R,M=V.n,I=V.A,e=V.Z,b=V.m,Z=W==null;
if(Z)W=new R(N.length>>>2<<3);while(i==0){i=n(N,d,1);m=n(N,d+1,2);d+=3;if(m==0){if((d&7)!=0)d+=8-(d&7);
if(Z)W=new R(N.length>>>2<<5);while(i==0){i=n(N,d,1);m=n(N,d+1,2);d+=3;if(m==0){if((d&7)!=0)d+=8-(d&7);
var D=(d>>>3)+4,q=N[D-4]|N[D-3]<<8;if(Z)W=H.H.W(W,w+q);W.set(new R(N.buffer,N.byteOffset+D,q),w);d=D+q<<3;
w+=q;continue}if(Z)W=H.H.W(W,w+(1<<17));if(m==1){v=b.J;C=b.h;X=(1<<9)-1;u=(1<<5)-1}if(m==2){J=A(N,d,5)+257;
h=A(N,d+5,5)+1;Q=A(N,d+10,4)+4;d+=14;var E=d,j=1;for(var c=0;c<38;c+=2){b.Q[c]=0;b.Q[c+1]=0}for(var c=0;
Expand Down Expand Up @@ -314,7 +326,7 @@ UPNG.decode._filterZero = function(data, out, off, w, h)
var bpp = UPNG.decode._getBPP(out), bpl = Math.ceil(w*bpp/8), paeth = UPNG.decode._paeth;
bpp = Math.ceil(bpp/8);

var i=0, di=1, type=data[off], x=0;
var i,di, type=data[off], x=0;

if(type>1) data[off]=[0,0,1][type-2];
if(type==3) for(x=bpp; x<bpl; x++) data[x+1] = (data[x+1] + (data[x+1-bpp]>>>1) )&255;
Expand Down Expand Up @@ -414,12 +426,13 @@ UPNG._copyTile = function(sb, sw, sh, tb, tw, th, xoff, yoff, mode)




UPNG.encode = function(bufs, w, h, ps, dels, tabs, forbidPlte)
{
if(ps==null) ps=0;
if(forbidPlte==null) forbidPlte = false;

var nimg = UPNG.encode.compress(bufs, w, h, ps, [false, false, false, 0, forbidPlte]);
var nimg = UPNG.encode.compress(bufs, w, h, ps, [false, false, false, 0, forbidPlte,false]);
UPNG.encode.compressPNG(nimg, -1);

return UPNG.encode._main(nimg, w, h, dels, tabs);
Expand Down Expand Up @@ -569,7 +582,7 @@ UPNG.encode.compressPNG = function(out, filter, levelZero) {
UPNG.encode.compress = function(bufs, w, h, ps, prms) // prms: onlyBlend, minBits, forbidPlte
{
//var time = Date.now();
var onlyBlend = prms[0], evenCrd = prms[1], forbidPrev = prms[2], minBits = prms[3], forbidPlte = prms[4];
var onlyBlend = prms[0], evenCrd = prms[1], forbidPrev = prms[2], minBits = prms[3], forbidPlte = prms[4], dither=prms[5];

var ctype = 6, depth = 8, alphaAnd=255

Expand All @@ -585,17 +598,27 @@ UPNG.encode.compress = function(bufs, w, h, ps, prms) // prms: onlyBlend, minBi
var frms = UPNG.encode.framize(bufs, w, h, onlyBlend, evenCrd, forbidPrev);
//console.log("framize", Date.now()-time); time = Date.now();

var cmap={}, plte=[], inds=[];
var cmap={}, plte=[], inds=[];

if(ps!=0) {
var nbufs = []; for(var i=0; i<frms.length; i++) nbufs.push(frms[i].img.buffer);

var abuf = UPNG.encode.concatRGBA(nbufs), qres = UPNG.quantize(abuf, ps);
var cof = 0, bb = new Uint8Array(qres.abuf);
for(var i=0; i<frms.length; i++) { var ti=frms[i].img, bln=ti.length; inds.push(new Uint8Array(qres.inds.buffer, cof>>2, bln>>2));
for(var j=0; j<bln; j+=4) { ti[j]=bb[cof+j]; ti[j+1]=bb[cof+j+1]; ti[j+2]=bb[cof+j+2]; ti[j+3]=bb[cof+j+3]; } cof+=bln; }
var abuf = UPNG.encode.concatRGBA(nbufs), qres = UPNG.quantize(abuf, ps);

for(var i=0; i<qres.plte.length; i++) plte.push(qres.plte[i].est.rgba);

var cof = 0;
for(var i=0; i<frms.length; i++) {
var frm=frms[i], bln=frm.img.length, ind = new Uint8Array(qres.inds.buffer, cof>>2, bln>>2); inds.push(ind);
var bb = new Uint8Array(qres.abuf,cof,bln);

//console.log(frm.img, frm.width, frm.height);
//var time = Date.now();
if(dither) UPNG.encode.dither(frm.img, frm.rect.width, frm.rect.height, plte, bb, ind);
//console.log(Date.now()-time);
frm.img.set(bb); cof+=bln;
}

//console.log("quantize", Date.now()-time); time = Date.now();
}
else {
Expand Down Expand Up @@ -787,8 +810,10 @@ UPNG.encode._filterZero = function(img,h,bpp,bpl,data, filter, levelZero)
else if(h*bpl>500000 || bpp==1) ftry=[0];
var opts; if(levelZero) opts={level:0};

var CMPR = (levelZero && UZIP!=null) ? UZIP : pako;

var CMPR = (data.length>10e6 && UZIP!=null) ? UZIP : pako;

var time = Date.now();
for(var i=0; i<ftry.length; i++) {
for(var y=0; y<h; y++) UPNG.encode._filterLine(data, img, y, bpl, bpp, ftry[i]);
//var nimg = new Uint8Array(data.length);
Expand All @@ -798,6 +823,7 @@ UPNG.encode._filterZero = function(img,h,bpp,bpl,data, filter, levelZero)
//console.log(crc, UZIP.adler(data,2,data.length-6));
fls.push(CMPR["deflate"](data,opts));
}

var ti, tsize=1e9;
for(var i=0; i<fls.length; i++) if(fls[i].length<tsize) { ti=i; tsize=fls[i].length; }
return fls[ti];
Expand Down Expand Up @@ -852,27 +878,30 @@ UPNG.crc = {

UPNG.quantize = function(abuf, ps)
{
var oimg = new Uint8Array(abuf), nimg = oimg.slice(0), nimg32 = new Uint32Array(nimg.buffer);
var sb = new Uint8Array(abuf), tb = sb.slice(0), tb32 = new Uint32Array(tb.buffer);

var KD = UPNG.quantize.getKDtree(nimg, ps);
var KD = UPNG.quantize.getKDtree(tb, ps);
var root = KD[0], leafs = KD[1];

var planeDst = UPNG.quantize.planeDst;
var sb = oimg, tb = nimg32, len=sb.length;
var len=sb.length;

var inds = new Uint8Array(oimg.length>>2);
for(var i=0; i<len; i+=4) {
var r=sb[i]*(1/255), g=sb[i+1]*(1/255), b=sb[i+2]*(1/255), a=sb[i+3]*(1/255);

// exact, but too slow :(
var nd = UPNG.quantize.getNearest(root, r, g, b, a);
//var nd = root;
//while(nd.left) nd = (planeDst(nd.est,r,g,b,a)<=0) ? nd.left : nd.right;

inds[i>>2] = nd.ind;
tb[i>>2] = nd.est.rgba;
}
return { abuf:nimg.buffer, inds:inds, plte:leafs };
var inds = new Uint8Array(len>>2), nd;
if(sb.length<20e6) // precise, but slow :(
for(var i=0; i<len; i+=4) {
var r=sb[i]*(1/255), g=sb[i+1]*(1/255), b=sb[i+2]*(1/255), a=sb[i+3]*(1/255);

nd = UPNG.quantize.getNearest(root, r, g, b, a);
inds[i>>2] = nd.ind; tb32[i>>2] = nd.est.rgba;
}
else
for(var i=0; i<len; i+=4) {
var r=sb[i]*(1/255), g=sb[i+1]*(1/255), b=sb[i+2]*(1/255), a=sb[i+3]*(1/255);

nd = root; while(nd.left) nd = (planeDst(nd.est,r,g,b,a)<=0) ? nd.left : nd.right;
inds[i>>2] = nd.ind; tb32[i>>2] = nd.est.rgba;
}
return { abuf:tb.buffer, inds:inds, plte:leafs };
}

UPNG.quantize.getKDtree = function(nimg, ps, err) {
Expand Down Expand Up @@ -981,12 +1010,12 @@ UPNG.quantize.estats = function(stats){
];

var A = Rj, M = UPNG.M4;
var b = [0.5,0.5,0.5,0.5], mi = 0, tmi = 0;
var b = [Math.random(),Math.random(),Math.random(),Math.random()], mi = 0, tmi = 0;

if(N!=0)
for(var i=0; i<10; i++) {
for(var i=0; i<16; i++) {
b = M.multVec(A, b); tmi = Math.sqrt(M.dot(b,b)); b = M.sml(1/tmi, b);
if(Math.abs(tmi-mi)<1e-9) break; mi = tmi;
if(i!=0 && Math.abs(tmi-mi)<1e-9) break; mi = tmi;
}
//b = [0,0,1,0]; mi=N;
var q = [m0*iN, m1*iN, m2*iN, m3*iN];
Expand Down Expand Up @@ -1023,5 +1052,59 @@ UPNG.encode.concatRGBA = function(bufs) {
return nimg.buffer;
}

export default UPNG;
UPNG.encode.dither = function(sb, w, h, plte, tb, oind) {

function addErr(er, tg, ti, f) {
tg[ti]+=(er[0]*f)>>4; tg[ti+1]+=(er[1]*f)>>4; tg[ti+2]+=(er[2]*f)>>4; tg[ti+3]+=(er[3]*f)>>4;
}
function N(x) { return Math.max(0, Math.min(255, x)); }
function D(a,b) { var dr=a[0]-b[0], dg=a[1]-b[1], db=a[2]-b[2], da=a[3]-b[3]; return (dr*dr + dg*dg + db*db + da*da); }


var pc=plte.length, nplt = [], rads=[];
for(var i=0; i<pc; i++) {
var c = plte[i];
nplt.push([((c>>>0)&255), ((c>>>8)&255), ((c>>>16)&255), ((c>>>24)&255)]);
}
for(var i=0; i<pc; i++) {
var ne=0xffffffff, ni=0;
for(var j=0; j<pc; j++) { var ce=D(nplt[i],nplt[j]); if(j!=i && ce<ne) { ne=ce; ni=j; } }
var hd = Math.sqrt(ne)/2;
rads[i] = ~~(hd*hd);
}

var tb32 = new Uint32Array(tb.buffer);
var err = new Int16Array(w*h*4);

for(var y=0; y<h; y++) {
for(var x=0; x<w; x++) {
var i = (y*w+x)*4;

var cc = [N(sb[i]+err[i]), N(sb[i+1]+err[i+1]), N(sb[i+2]+err[i+2]), N(sb[i+3]+err[i+3])];

var ni=0, nd = 0xffffff;
for(var j=0; j<pc; j++) {
var cd = D(cc,nplt[j]);
if(cd<nd) { nd=cd; ni=j; }
}

//ni = oind[i>>2];
var nc = nplt[ni];
var er = [cc[0]-nc[0], cc[1]-nc[1], cc[2]-nc[2], cc[3]-nc[3]];

//addErr(er, err, i+4, 16);

//*
if(x!=w-1) addErr(er, err, i+4 , 7);
if(y!=h-1) {
if(x!= 0) addErr(er, err, i+4*w-4, 3);
addErr(er, err, i+4*w , 5);
if(x!=w-1) addErr(er, err, i+4*w+4, 1); //*/
}

oind[i>>2] = ni; tb32[i>>2] = plte[ni];
}
}
}

export default UPNG;