vox-core
Version:
Runtime de aplicaciones multiplataforma
344 lines (336 loc) • 12 kB
JavaScript
/** Particularly simple-minded implementation of PPM compression. */
if (typeof define !== 'function') { var define = require('amdefine')(module); }
define(['./RangeCoder','./Util'], function(RangeCoder,Util) {
var MAX_CONTEXT = 5;
var LOG_WINDOW_SIZE = 18;
var WINDOW_SIZE = 1 << LOG_WINDOW_SIZE;
var Window = function() {
this.buffer = Util.makeU8Buffer(WINDOW_SIZE);
this.pos = 0;
this.firstPass = true;
for (var i=0; i<MAX_CONTEXT; i++) {
this.put('cSaCsA'.charCodeAt(i%6));
}
};
Window.prototype.put = function(_byte) {
this.buffer[this.pos++] = _byte;
if (this.pos >= WINDOW_SIZE) { this.pos = 0; this.firstPass = false; }
return _byte;
};
Window.prototype.get = function(pos) {
return this.buffer[pos & (WINDOW_SIZE-1)];
};
// the context ending just before 'pos'
Window.prototype.context = function(pos, n) {
var c = [], i;
pos = (pos - n) & (WINDOW_SIZE-1);
for (i=0; i<n; i++) {
c.push(this.buffer[pos++]);
if (pos >= WINDOW_SIZE) { pos = 0; }
}
return String.fromCharCode.apply(String, c);
};
var DMM_INCREMENT = 0x100, DMM_MAX_PROB = 0xFF00;
var PPM = function(coder, size) {
this.window = new Window();
this.contexts = Object.create(null);
// brain-dead '-1' context, using full exclusion
var Cm1Context = function() { };
Cm1Context.prototype.encode = function(symbol, exclude) {
var i, lt_f = 0;
for (i=0; i<symbol; i++) {
if (!exclude[i]) {
lt_f++;
}
}
var tot_f = size - exclude.total;
coder.encodeFreq(1, lt_f, tot_f);
};
Cm1Context.prototype.decode = function(exclude) {
var i, symbol, lt_f;
var tot_f = size - exclude.total;
symbol = lt_f = coder.decodeCulFreq(tot_f);
for (i=0; i<=symbol; i++) {
if (exclude[i]) {
symbol++;
}
}
coder.decodeUpdate(1, lt_f, tot_f);
return symbol;
};
this.cm1coder = new Cm1Context();
var DenseMTFModel = function() {
this.sym = [size];
this.prob= [0, DMM_INCREMENT];
this.refcount = 0;
};
DenseMTFModel.prototype._rescale = function() {
var seenSyms = this.sym.length;
var i, j, total=0;
var noEscape = true;
for(i=0, j=0; i<seenSyms; i++) {
var sym = this.sym[i];
var sy_f = this.prob[i+1] - this.prob[i];
sy_f >>>= 1;
if (sy_f > 0) {
if (sym === size) {
noEscape = false;
}
this.sym[j] = sym;
this.prob[j++] = total;
total += sy_f;
}
}
this.prob[j] = total;
seenSyms = this.sym.length = j;
this.prob.length = seenSyms + 1;
// don't allow escape to go to zero prob if we still need it
if (noEscape && seenSyms < size) {
total = this._update(size/*escape*/, seenSyms/*at end*/, 0, 1);
}
return total;
};
DenseMTFModel.prototype.update = function(symbol, incr) {
// find symbol
var i=0;
for (i=0; i<this.sym.length; i++) {
if (this.sym[i] === symbol) {
return this._update(symbol, i, this.prob[i+1] - this.prob[i], incr);
}
}
// symbol escaped
return this._update(symbol, i, 0, incr);
};
DenseMTFModel.prototype._update = function(symbol, index, sy_f, incr) {
var seenSyms = this.sym.length;
var i, j, tot_f;
// move this symbol to the end
for (j=index; j<seenSyms-1; j++) {
this.sym[j] = this.sym[j+1];
this.prob[j] = this.prob[j+1] - sy_f;
}
// "method D" -- if we add a new escaped symbol, escape & the symbol
// both increase by 1/2.
if (index < seenSyms) {
this.sym[j] = symbol;
this.prob[j] = this.prob[j+1] - sy_f;
// increase frequency for this symbol, and total freq at same time
this.prob[seenSyms] = tot_f =
this.prob[seenSyms] + incr;
} else { // add to the end
tot_f = this.prob[seenSyms];
this.sym[index] = symbol;
this.prob[index] = tot_f;
tot_f += incr;
this.prob[++seenSyms] = tot_f;
// remove probability of escape if table just filled up
if (this.sym.length > size) {
for (i=0; i<seenSyms; i++) {
if (size === this.sym[i]) {
// found it.
this._update(size, i, this.prob[i+1] - this.prob[i], -1);
this.sym.length--;
this.prob.length--;
tot_f = this.prob[this.prob.length-1];
}
}
}
}
if (tot_f >= DMM_MAX_PROB) { tot_f = this._rescale(); }
return tot_f;
};
DenseMTFModel.prototype.encode = function(symbol, exclude) {
// look for symbol, from most-recent to oldest
var i, j, sy_f, lt_f, tot_f, seenSyms = this.sym.length;
var ex_seen = 0, ex_lt_f = 0, ex_tot_f = 0, ex_sy_f;
for (i=seenSyms-1; i>=0; i--) {
lt_f = this.prob[i];
sy_f = this.prob[i + 1] - lt_f;
if (symbol === this.sym[i]) {
// ok, found it.
// count up the rest of the probabilities
for (j=i-1; j>=0 && ex_seen < exclude.total; j--) {
if (exclude[this.sym[j]]) {
ex_seen += 1;
ex_sy_f = this.prob[j+1] - this.prob[j];
ex_lt_f += ex_sy_f;
ex_tot_f += ex_sy_f;
}
}
tot_f = this.prob[seenSyms];
// adjust by excluded symbols
lt_f -= ex_lt_f;
tot_f -= ex_tot_f;
coder.encodeFreq(sy_f, lt_f, tot_f);
if (symbol === size) { // only update table for escapes
this._update(symbol, i, sy_f, DMM_INCREMENT/2);
return false; // escape.
} // otherwise we'll do update later
return true; // encoded character!
} else if (exclude[this.sym[i]]) {
ex_seen += 1;
ex_tot_f += sy_f;
}
}
// couldn't find this symbol. encode as escape.
this.encode(size, exclude);
// add symbols to exclusion table
console.assert(this.sym[this.sym.length-1] === size);//escape
for (i=0; i<this.sym.length-1; i++) {
if (!exclude[this.sym[i]]) {
exclude[this.sym[i]] = true;
exclude.total++;
}
}
};
DenseMTFModel.prototype.decode = function(exclude) {
var seenSyms = this.sym.length;
var tot_f = this.prob[seenSyms];
var ex_seen = 0, ex_lt_f = 0, ex_tot_f = 0, ex_sy_f;
var i;
for (i=seenSyms-1; i>=0 && ex_seen < exclude.total; i--) {
if (exclude[this.sym[i]]) {
ex_seen += 1;
ex_tot_f += this.prob[i+1] - this.prob[i];
}
}
var prob = coder.decodeCulFreq(tot_f - ex_tot_f) + ex_tot_f;
// we're expecting to find the probability near the "most recent" side
// of our array
ex_lt_f = ex_tot_f;
for (i=seenSyms-1; i>=0; i--) {
if (exclude[this.sym[i]]) {
ex_sy_f = this.prob[i+1] - this.prob[i];
ex_lt_f -= ex_sy_f;
prob -= ex_sy_f;
} else if (this.prob[i] <= prob /*&& prob < this.prob[i+1]*/)
break;
}
console.assert(i>=0);
var symbol = this.sym[i];
var lt_f = this.prob[i];
var sy_f = this.prob[i + 1] - lt_f;
coder.decodeUpdate(sy_f, lt_f - ex_lt_f, tot_f - ex_tot_f);
// defer update
if (symbol < size) { return symbol; }
// an escape
this._update(symbol, i, sy_f, DMM_INCREMENT/2);
// add symbols to exclusion table
console.assert(this.sym[this.sym.length-1] === size);//escape
for (i=0; i<this.sym.length-1; i++) {
if (!exclude[this.sym[i]]) {
exclude[this.sym[i]] = true;
exclude.total++;
}
}
return -1;
};
this.newContext = function(initialSymbol) {
return new DenseMTFModel();
};
this.newExclude = function() {
var result = Object.create(null);
result.total = 0; // no excluded symbols (yet)
return result;
};
// set up some initial contexts
(function() {
var i, j;
for (i=0; i<MAX_CONTEXT; i++) {
for (j=0; j<=i; j++) {
var cc = this.window.context(j+((MAX_CONTEXT-1)-i), j);
if (!this.contexts[cc]) { this.contexts[cc] = this.newContext(); }
this.contexts[cc].refcount++;
}
}
}).call(this);
};
PPM.prototype.update = function(symbol, contextString, matchLevel) {
// slide up the contexts, updating them
var model, c, cc;
for (c=0; c <= MAX_CONTEXT; c++) {
cc = contextString.slice(MAX_CONTEXT - c);
model = this.contexts[cc];
if (!model) {
model = this.contexts[cc] = this.newContext();
}
if (c >= matchLevel) {
// only update useful contexts
model.update(symbol, DMM_INCREMENT / 2);
}
// refcount all contexts, whether used/updated or not
model.refcount++;
}
// now garbage-collect old contexts
contextString = this.window.context(this.window.pos + MAX_CONTEXT,
MAX_CONTEXT);
var firstPass = this.window.firstPass;
for (c=MAX_CONTEXT; c>=0 && !firstPass; c--) {
cc = contextString.slice(0, c);
model = this.contexts[cc];
console.assert(model);
if ((--model.refcount) <= 0) {
console.assert(cc !== ''); // don't allow context-0 to be gc'ed!
delete this.contexts[cc];
}
}
// ok, advance window.
this.window.put(symbol);
};
PPM.prototype.decode = function() {
var contextString = this.window.context(this.window.pos, MAX_CONTEXT);
var exclude = this.newExclude();
var model, c, cc, symbol;
for (c=MAX_CONTEXT; c>=0; c--) {
cc = contextString.slice(MAX_CONTEXT - c);
model = this.contexts[cc];
if (model) {
symbol = model.decode(exclude);
if (symbol >= 0) {
this.update(symbol, contextString, c);
return symbol;
}
}
}
// still no match, fall back to context -1
symbol = this.cm1coder.decode(exclude);
this.update(symbol, contextString, c);
return symbol;
};
PPM.prototype.encode = function(symbol) {
var contextString = this.window.context(this.window.pos, MAX_CONTEXT);
var exclude = this.newExclude();
var c;
for (c=MAX_CONTEXT; c>=0; c--) {
var cc = contextString.slice(MAX_CONTEXT - c);
var model = this.contexts[cc];
if (model) {
var success = model.encode(symbol, exclude);
if (success) {
this.update(symbol, contextString, c);
return;
}
}
}
// fall back to context -1 (but still use exclusion table)
this.cm1coder.encode(symbol, exclude);
this.update(symbol, contextString, c);
return;
};
PPM.MAGIC = 'ppm2';
PPM.compressFile = Util.compressFileHelper(PPM.MAGIC, function(inStream, outStream, fileSize, props, finalByte) {
var range = new RangeCoder(outStream);
range.encodeStart(finalByte, 1);
var model = new PPM(range, (fileSize<0) ? 257 : 256);
Util.compressWithModel(inStream, fileSize, model);
range.encodeFinish();
}, true);
PPM.decompressFile = Util.decompressFileHelper(PPM.MAGIC, function(inStream, outStream, fileSize) {
var range = new RangeCoder(inStream);
range.decodeStart(true/*we already read the 'free' byte*/);
var model = new PPM(range, (fileSize<0) ? 257 : 256);
Util.decompressWithModel(outStream, fileSize, model);
range.decodeFinish();
});
return PPM;
});