opentype.js
Version:
OpenType font parser
2,125 lines (1,709 loc) • 74.4 kB
JavaScript
/* A TrueType font hinting interpreter.
*
* (c) 2017 Axel Kittenberger
*
* This interpreter has been implemented according to this documentation:
* https://developer.apple.com/fonts/TrueType-Reference-Manual/RM05/Chap5.html
*
* According to the documentation F24DOT6 values are used for pixels.
* That means calculation is 1/64 pixel accurate and uses integer operations.
* However, Javascript has floating point operations by default and only
* those are available. One could make a case to simulate the 1/64 accuracy
* exactly by truncating after every division operation
* (for example with << 0) to get pixel exactly results as other TrueType
* implementations. It may make sense since some fonts are pixel optimized
* by hand using DELTAP instructions. The current implementation doesn't
* and rather uses full floating point precision.
*
* xScale, yScale and rotation is currently ignored.
*
* A few non-trivial instructions are missing as I didn't encounter yet
* a font that used them to test a possible implementation.
*
* Some fonts seem to use undocumented features regarding the twilight zone.
* Only some of them are implemented as they were encountered.
*
* The exports.DEBUG statements are removed on the minified distribution file.
*/
'use strict';
import glyf from './tables/glyf';
let instructionTable;
let exec;
let execGlyph;
let execComponent;
/*
* Creates a hinting object.
*
* There ought to be exactly one
* for each truetype font that is used for hinting.
*/
function Hinting(font) {
// the font this hinting object is for
this.font = font;
this.getCommands = function (hPoints) {
return glyf.getPath(hPoints).commands;
};
// cached states
this._fpgmState =
this._prepState =
undefined;
// errorState
// 0 ... all okay
// 1 ... had an error in a glyf,
// continue working but stop spamming
// the console
// 2 ... error at prep, stop hinting at this ppem
// 3 ... error at fpeg, stop hinting for this font at all
this._errorState = 0;
}
/*
* Not rounding.
*/
function roundOff(v) {
return v;
}
/*
* Rounding to grid.
*/
function roundToGrid(v) {
//Rounding in TT is supposed to "symmetrical around zero"
return Math.sign(v) * Math.round(Math.abs(v));
}
/*
* Rounding to double grid.
*/
function roundToDoubleGrid(v) {
return Math.sign(v) * Math.round(Math.abs(v * 2)) / 2;
}
/*
* Rounding to half grid.
*/
function roundToHalfGrid(v) {
return Math.sign(v) * (Math.round(Math.abs(v) + 0.5) - 0.5);
}
/*
* Rounding to up to grid.
*/
function roundUpToGrid(v) {
return Math.sign(v) * Math.ceil(Math.abs(v));
}
/*
* Rounding to down to grid.
*/
function roundDownToGrid(v) {
return Math.sign(v) * Math.floor(Math.abs(v));
}
/*
* Super rounding.
*/
const roundSuper = function (v) {
const period = this.srPeriod;
let phase = this.srPhase;
const threshold = this.srThreshold;
let sign = 1;
if (v < 0) {
v = -v;
sign = -1;
}
v += threshold - phase;
v = Math.trunc(v / period) * period;
v += phase;
// according to http://xgridfit.sourceforge.net/round.html
if (v < 0) return phase * sign;
return v * sign;
};
/*
* Unit vector of x-axis.
*/
const xUnitVector = {
x: 1,
y: 0,
axis: 'x',
// Gets the projected distance between two points.
// o1/o2 ... if true, respective original position is used.
distance: function (p1, p2, o1, o2) {
return (o1 ? p1.xo : p1.x) - (o2 ? p2.xo : p2.x);
},
// Moves point p so the moved position has the same relative
// position to the moved positions of rp1 and rp2 than the
// original positions had.
//
// See APPENDIX on INTERPOLATE at the bottom of this file.
interpolate: function (p, rp1, rp2, pv) {
let do1;
let do2;
let doa1;
let doa2;
let dm1;
let dm2;
let dt;
if (!pv || pv === this) {
do1 = p.xo - rp1.xo;
do2 = p.xo - rp2.xo;
dm1 = rp1.x - rp1.xo;
dm2 = rp2.x - rp2.xo;
doa1 = Math.abs(do1);
doa2 = Math.abs(do2);
dt = doa1 + doa2;
if (dt === 0) {
p.x = p.xo + (dm1 + dm2) / 2;
return;
}
p.x = p.xo + (dm1 * doa2 + dm2 * doa1) / dt;
return;
}
do1 = pv.distance(p, rp1, true, true);
do2 = pv.distance(p, rp2, true, true);
dm1 = pv.distance(rp1, rp1, false, true);
dm2 = pv.distance(rp2, rp2, false, true);
doa1 = Math.abs(do1);
doa2 = Math.abs(do2);
dt = doa1 + doa2;
if (dt === 0) {
xUnitVector.setRelative(p, p, (dm1 + dm2) / 2, pv, true);
return;
}
xUnitVector.setRelative(p, p, (dm1 * doa2 + dm2 * doa1) / dt, pv, true);
},
// Slope of line normal to this
normalSlope: Number.NEGATIVE_INFINITY,
// Sets the point 'p' relative to point 'rp'
// by the distance 'd'.
//
// See APPENDIX on SETRELATIVE at the bottom of this file.
//
// p ... point to set
// rp ... reference point
// d ... distance on projection vector
// pv ... projection vector (undefined = this)
// org ... if true, uses the original position of rp as reference.
setRelative: function (p, rp, d, pv, org) {
if (!pv || pv === this) {
p.x = (org ? rp.xo : rp.x) + d;
return;
}
const rpx = org ? rp.xo : rp.x;
const rpy = org ? rp.yo : rp.y;
const rpdx = rpx + d * pv.x;
const rpdy = rpy + d * pv.y;
p.x = rpdx + (p.y - rpdy) / pv.normalSlope;
},
// Slope of vector line.
slope: 0,
// Touches the point p.
touch: function (p) {
p.xTouched = true;
},
// Tests if a point p is touched.
touched: function (p) {
return p.xTouched;
},
// Untouches the point p.
untouch: function (p) {
p.xTouched = false;
}
};
/*
* Unit vector of y-axis.
*/
const yUnitVector = {
x: 0,
y: 1,
axis: 'y',
// Gets the projected distance between two points.
// o1/o2 ... if true, respective original position is used.
distance: function (p1, p2, o1, o2) {
return (o1 ? p1.yo : p1.y) - (o2 ? p2.yo : p2.y);
},
// Moves point p so the moved position has the same relative
// position to the moved positions of rp1 and rp2 than the
// original positions had.
//
// See APPENDIX on INTERPOLATE at the bottom of this file.
interpolate: function (p, rp1, rp2, pv) {
let do1;
let do2;
let doa1;
let doa2;
let dm1;
let dm2;
let dt;
if (!pv || pv === this) {
do1 = p.yo - rp1.yo;
do2 = p.yo - rp2.yo;
dm1 = rp1.y - rp1.yo;
dm2 = rp2.y - rp2.yo;
doa1 = Math.abs(do1);
doa2 = Math.abs(do2);
dt = doa1 + doa2;
if (dt === 0) {
p.y = p.yo + (dm1 + dm2) / 2;
return;
}
p.y = p.yo + (dm1 * doa2 + dm2 * doa1) / dt;
return;
}
do1 = pv.distance(p, rp1, true, true);
do2 = pv.distance(p, rp2, true, true);
dm1 = pv.distance(rp1, rp1, false, true);
dm2 = pv.distance(rp2, rp2, false, true);
doa1 = Math.abs(do1);
doa2 = Math.abs(do2);
dt = doa1 + doa2;
if (dt === 0) {
yUnitVector.setRelative(p, p, (dm1 + dm2) / 2, pv, true);
return;
}
yUnitVector.setRelative(p, p, (dm1 * doa2 + dm2 * doa1) / dt, pv, true);
},
// Slope of line normal to this.
normalSlope: 0,
// Sets the point 'p' relative to point 'rp'
// by the distance 'd'
//
// See APPENDIX on SETRELATIVE at the bottom of this file.
//
// p ... point to set
// rp ... reference point
// d ... distance on projection vector
// pv ... projection vector (undefined = this)
// org ... if true, uses the original position of rp as reference.
setRelative: function (p, rp, d, pv, org) {
if (!pv || pv === this) {
p.y = (org ? rp.yo : rp.y) + d;
return;
}
const rpx = org ? rp.xo : rp.x;
const rpy = org ? rp.yo : rp.y;
const rpdx = rpx + d * pv.x;
const rpdy = rpy + d * pv.y;
p.y = rpdy + pv.normalSlope * (p.x - rpdx);
},
// Slope of vector line.
slope: Number.POSITIVE_INFINITY,
// Touches the point p.
touch: function (p) {
p.yTouched = true;
},
// Tests if a point p is touched.
touched: function (p) {
return p.yTouched;
},
// Untouches the point p.
untouch: function (p) {
p.yTouched = false;
}
};
Object.freeze(xUnitVector);
Object.freeze(yUnitVector);
/*
* Creates a unit vector that is not x- or y-axis.
*/
function UnitVector(x, y) {
this.x = x;
this.y = y;
this.axis = undefined;
this.slope = y / x;
this.normalSlope = -x / y;
Object.freeze(this);
}
/*
* Gets the projected distance between two points.
* o1/o2 ... if true, respective original position is used.
*/
UnitVector.prototype.distance = function(p1, p2, o1, o2) {
return (
this.x * xUnitVector.distance(p1, p2, o1, o2) +
this.y * yUnitVector.distance(p1, p2, o1, o2)
);
};
/*
* Moves point p so the moved position has the same relative
* position to the moved positions of rp1 and rp2 than the
* original positions had.
*
* See APPENDIX on INTERPOLATE at the bottom of this file.
*/
UnitVector.prototype.interpolate = function(p, rp1, rp2, pv) {
let dm1;
let dm2;
let do1;
let do2;
let doa1;
let doa2;
let dt;
do1 = pv.distance(p, rp1, true, true);
do2 = pv.distance(p, rp2, true, true);
dm1 = pv.distance(rp1, rp1, false, true);
dm2 = pv.distance(rp2, rp2, false, true);
doa1 = Math.abs(do1);
doa2 = Math.abs(do2);
dt = doa1 + doa2;
if (dt === 0) {
this.setRelative(p, p, (dm1 + dm2) / 2, pv, true);
return;
}
this.setRelative(p, p, (dm1 * doa2 + dm2 * doa1) / dt, pv, true);
};
/*
* Sets the point 'p' relative to point 'rp'
* by the distance 'd'
*
* See APPENDIX on SETRELATIVE at the bottom of this file.
*
* p ... point to set
* rp ... reference point
* d ... distance on projection vector
* pv ... projection vector (undefined = this)
* org ... if true, uses the original position of rp as reference.
*/
UnitVector.prototype.setRelative = function(p, rp, d, pv, org) {
pv = pv || this;
const rpx = org ? rp.xo : rp.x;
const rpy = org ? rp.yo : rp.y;
const rpdx = rpx + d * pv.x;
const rpdy = rpy + d * pv.y;
const pvns = pv.normalSlope;
const fvs = this.slope;
const px = p.x;
const py = p.y;
p.x = (fvs * px - pvns * rpdx + rpdy - py) / (fvs - pvns);
p.y = fvs * (p.x - px) + py;
};
/*
* Touches the point p.
*/
UnitVector.prototype.touch = function(p) {
p.xTouched = true;
p.yTouched = true;
};
/*
* Returns a unit vector with x/y coordinates.
*/
function getUnitVector(x, y) {
const d = Math.sqrt(x * x + y * y);
x /= d;
y /= d;
if (x === 1 && y === 0) return xUnitVector;
else if (x === 0 && y === 1) return yUnitVector;
else return new UnitVector(x, y);
}
/*
* Creates a point in the hinting engine.
*/
function HPoint(
x,
y,
lastPointOfContour,
onCurve
) {
this.x = this.xo = Math.round(x * 64) / 64; // hinted x value and original x-value
this.y = this.yo = Math.round(y * 64) / 64; // hinted y value and original y-value
this.lastPointOfContour = lastPointOfContour;
this.onCurve = onCurve;
this.prevPointOnContour = undefined;
this.nextPointOnContour = undefined;
this.xTouched = false;
this.yTouched = false;
Object.preventExtensions(this);
}
/*
* Returns the next touched point on the contour.
*
* v ... unit vector to test touch axis.
*/
HPoint.prototype.nextTouched = function(v) {
let p = this.nextPointOnContour;
while (!v.touched(p) && p !== this) p = p.nextPointOnContour;
return p;
};
/*
* Returns the previous touched point on the contour
*
* v ... unit vector to test touch axis.
*/
HPoint.prototype.prevTouched = function(v) {
let p = this.prevPointOnContour;
while (!v.touched(p) && p !== this) p = p.prevPointOnContour;
return p;
};
/*
* The zero point.
*/
const HPZero = Object.freeze(new HPoint(0, 0));
/*
* The default state of the interpreter.
*
* Note: Freezing the defaultState and then deriving from it
* makes the V8 Javascript engine going awkward,
* so this is avoided, albeit the defaultState shouldn't
* ever change.
*/
const defaultState = {
cvCutIn: 17 / 16, // control value cut in
deltaBase: 9,
deltaShift: 0.125,
loop: 1, // loops some instructions
minDis: 1, // minimum distance
autoFlip: true
};
/*
* The current state of the interpreter.
*
* env ... 'fpgm' or 'prep' or 'glyf'
* prog ... the program
*/
function State(env, prog) {
this.env = env;
this.stack = [];
this.prog = prog;
switch (env) {
case 'glyf' :
this.zp0 = this.zp1 = this.zp2 = 1;
this.rp0 = this.rp1 = this.rp2 = 0;
/* fall through */
case 'prep' :
this.fv = this.pv = this.dpv = xUnitVector;
this.round = roundToGrid;
}
}
/*
* Executes a glyph program.
*
* This does the hinting for each glyph.
*
* Returns an array of moved points.
*
* glyph: the glyph to hint
* ppem: the size the glyph is rendered for
*/
Hinting.prototype.exec = function(glyph, ppem) {
if (typeof ppem !== 'number') {
throw new Error('Point size is not a number!');
}
// Received a fatal error, don't do any hinting anymore.
if (this._errorState > 2) return;
const font = this.font;
let prepState = this._prepState;
if (!prepState || prepState.ppem !== ppem) {
let fpgmState = this._fpgmState;
if (!fpgmState) {
// Executes the fpgm state.
// This is used by fonts to define functions.
State.prototype = defaultState;
fpgmState =
this._fpgmState =
new State('fpgm', font.tables.fpgm);
fpgmState.funcs = [ ];
fpgmState.font = font;
if (exports.DEBUG) {
console.log('---EXEC FPGM---');
fpgmState.step = -1;
}
try {
exec(fpgmState);
} catch (e) {
console.log('Hinting error in FPGM:' + e);
this._errorState = 3;
return;
}
}
// Executes the prep program for this ppem setting.
// This is used by fonts to set cvt values
// depending on to be rendered font size.
State.prototype = fpgmState;
prepState =
this._prepState =
new State('prep', font.tables.prep);
prepState.ppem = ppem;
// Creates a copy of the cvt table
// and scales it to the current ppem setting.
const oCvt = font.tables.cvt;
if (oCvt) {
const cvt = prepState.cvt = new Array(oCvt.length);
const scale = ppem / font.unitsPerEm;
for (let c = 0; c < oCvt.length; c++) {
cvt[c] = oCvt[c] * scale;
}
} else {
prepState.cvt = [];
}
if (exports.DEBUG) {
console.log('---EXEC PREP---');
prepState.step = -1;
}
try {
exec(prepState);
} catch (e) {
if (this._errorState < 2) {
console.log('Hinting error in PREP:' + e);
}
this._errorState = 2;
}
}
if (this._errorState > 1) return;
try {
return execGlyph(glyph, prepState);
} catch (e) {
if (this._errorState < 1) {
console.log('Hinting error:' + e);
console.log('Note: further hinting errors are silenced');
}
this._errorState = 1;
return undefined;
}
};
/*
* Executes the hinting program for a glyph.
*/
execGlyph = function(glyph, prepState) {
// original point positions
const xScale = prepState.ppem / prepState.font.unitsPerEm;
const yScale = xScale;
let components = glyph.components;
let contours;
let gZone;
let state;
State.prototype = prepState;
if (!components) {
state = new State('glyf', glyph.instructions);
if (exports.DEBUG) {
console.log('---EXEC GLYPH---');
state.step = -1;
}
execComponent(glyph, state, xScale, yScale);
gZone = state.gZone;
} else {
const font = prepState.font;
gZone = [];
contours = [];
for (let i = 0; i < components.length; i++) {
const c = components[i];
const cg = font.glyphs.get(c.glyphIndex);
state = new State('glyf', cg.instructions);
if (exports.DEBUG) {
console.log('---EXEC COMP ' + i + '---');
state.step = -1;
}
execComponent(cg, state, xScale, yScale);
// appends the computed points to the result array
// post processes the component points
const dx = Math.round(c.dx * xScale);
const dy = Math.round(c.dy * yScale);
const gz = state.gZone;
const cc = state.contours;
for (let pi = 0; pi < gz.length; pi++) {
const p = gz[pi];
p.xTouched = p.yTouched = false;
p.xo = p.x = p.x + dx;
p.yo = p.y = p.y + dy;
}
const gLen = gZone.length;
gZone.push.apply(gZone, gz);
for (let j = 0; j < cc.length; j++) {
contours.push(cc[j] + gLen);
}
}
if (glyph.instructions && !state.inhibitGridFit) {
// the composite has instructions on its own
state = new State('glyf', glyph.instructions);
state.gZone = state.z0 = state.z1 = state.z2 = gZone;
state.contours = contours;
// note: HPZero cannot be used here, since
// the point might be modified
gZone.push(
new HPoint(0, 0),
new HPoint(Math.round(glyph.advanceWidth * xScale), 0)
);
if (exports.DEBUG) {
console.log('---EXEC COMPOSITE---');
state.step = -1;
}
exec(state);
gZone.length -= 2;
}
}
return gZone;
};
/*
* Executes the hinting program for a component of a multi-component glyph
* or of the glyph itself for a non-component glyph.
*/
execComponent = function(glyph, state, xScale, yScale)
{
const points = glyph.points || [];
const pLen = points.length;
const gZone = state.gZone = state.z0 = state.z1 = state.z2 = [];
const contours = state.contours = [];
// Scales the original points and
// makes copies for the hinted points.
let cp; // current point
for (let i = 0; i < pLen; i++) {
cp = points[i];
gZone[i] = new HPoint(
cp.x * xScale,
cp.y * yScale,
cp.lastPointOfContour,
cp.onCurve
);
}
// Chain links the contours.
let sp; // start point
let np; // next point
for (let i = 0; i < pLen; i++) {
cp = gZone[i];
if (!sp) {
sp = cp;
contours.push(i);
}
if (cp.lastPointOfContour) {
cp.nextPointOnContour = sp;
sp.prevPointOnContour = cp;
sp = undefined;
} else {
np = gZone[i + 1];
cp.nextPointOnContour = np;
np.prevPointOnContour = cp;
}
}
if (state.inhibitGridFit) return;
if (exports.DEBUG) {
console.log('PROCESSING GLYPH', state.stack);
for (let i = 0; i < pLen; i++) {
console.log(i, gZone[i].x, gZone[i].y);
}
}
gZone.push(
new HPoint(0, 0),
new HPoint(Math.round(glyph.advanceWidth * xScale), 0)
);
exec(state);
// Removes the extra points.
gZone.length -= 2;
if (exports.DEBUG) {
console.log('FINISHED GLYPH', state.stack);
for (let i = 0; i < pLen; i++) {
console.log(i, gZone[i].x, gZone[i].y);
}
}
};
/*
* Executes the program loaded in state.
*/
exec = function(state) {
let prog = state.prog;
if (!prog) return;
const pLen = prog.length;
let ins;
for (state.ip = 0; state.ip < pLen; state.ip++) {
if (exports.DEBUG) state.step++;
ins = instructionTable[prog[state.ip]];
if (!ins) {
throw new Error(
'unknown instruction: 0x' +
Number(prog[state.ip]).toString(16)
);
}
ins(state);
// very extensive debugging for each step
/*
if (exports.DEBUG) {
var da;
if (state.gZone) {
da = [];
for (let i = 0; i < state.gZone.length; i++)
{
da.push(i + ' ' +
state.gZone[i].x * 64 + ' ' +
state.gZone[i].y * 64 + ' ' +
(state.gZone[i].xTouched ? 'x' : '') +
(state.gZone[i].yTouched ? 'y' : '')
);
}
console.log('GZ', da);
}
if (state.tZone) {
da = [];
for (let i = 0; i < state.tZone.length; i++) {
da.push(i + ' ' +
state.tZone[i].x * 64 + ' ' +
state.tZone[i].y * 64 + ' ' +
(state.tZone[i].xTouched ? 'x' : '') +
(state.tZone[i].yTouched ? 'y' : '')
);
}
console.log('TZ', da);
}
if (state.stack.length > 10) {
console.log(
state.stack.length,
'...', state.stack.slice(state.stack.length - 10)
);
} else {
console.log(state.stack.length, state.stack);
}
}
*/
}
};
/*
* Initializes the twilight zone.
*
* This is only done if a SZPx instruction
* refers to the twilight zone.
*/
function initTZone(state)
{
const tZone = state.tZone = new Array(state.gZone.length);
// no idea if this is actually correct...
for (let i = 0; i < tZone.length; i++)
{
tZone[i] = new HPoint(0, 0);
}
}
/*
* Skips the instruction pointer ahead over an IF/ELSE block.
* handleElse .. if true breaks on matching ELSE
*/
function skip(state, handleElse)
{
const prog = state.prog;
let ip = state.ip;
let nesting = 1;
let ins;
do {
ins = prog[++ip];
if (ins === 0x58) // IF
nesting++;
else if (ins === 0x59) // EIF
nesting--;
else if (ins === 0x40) // NPUSHB
ip += prog[ip + 1] + 1;
else if (ins === 0x41) // NPUSHW
ip += 2 * prog[ip + 1] + 1;
else if (ins >= 0xB0 && ins <= 0xB7) // PUSHB
ip += ins - 0xB0 + 1;
else if (ins >= 0xB8 && ins <= 0xBF) // PUSHW
ip += (ins - 0xB8 + 1) * 2;
else if (handleElse && nesting === 1 && ins === 0x1B) // ELSE
break;
} while (nesting > 0);
state.ip = ip;
}
/*----------------------------------------------------------*
* And then a lot of instructions... *
*----------------------------------------------------------*/
// SVTCA[a] Set freedom and projection Vectors To Coordinate Axis
// 0x00-0x01
function SVTCA(v, state) {
if (exports.DEBUG) console.log(state.step, 'SVTCA[' + v.axis + ']');
state.fv = state.pv = state.dpv = v;
}
// SPVTCA[a] Set Projection Vector to Coordinate Axis
// 0x02-0x03
function SPVTCA(v, state) {
if (exports.DEBUG) console.log(state.step, 'SPVTCA[' + v.axis + ']');
state.pv = state.dpv = v;
}
// SFVTCA[a] Set Freedom Vector to Coordinate Axis
// 0x04-0x05
function SFVTCA(v, state) {
if (exports.DEBUG) console.log(state.step, 'SFVTCA[' + v.axis + ']');
state.fv = v;
}
// SPVTL[a] Set Projection Vector To Line
// 0x06-0x07
function SPVTL(a, state) {
const stack = state.stack;
const p2i = stack.pop();
const p1i = stack.pop();
const p2 = state.z2[p2i];
const p1 = state.z1[p1i];
if (exports.DEBUG) console.log('SPVTL[' + a + ']', p2i, p1i);
let dx;
let dy;
if (!a) {
dx = p1.x - p2.x;
dy = p1.y - p2.y;
} else {
dx = p2.y - p1.y;
dy = p1.x - p2.x;
}
state.pv = state.dpv = getUnitVector(dx, dy);
}
// SFVTL[a] Set Freedom Vector To Line
// 0x08-0x09
function SFVTL(a, state) {
const stack = state.stack;
const p2i = stack.pop();
const p1i = stack.pop();
const p2 = state.z2[p2i];
const p1 = state.z1[p1i];
if (exports.DEBUG) console.log('SFVTL[' + a + ']', p2i, p1i);
let dx;
let dy;
if (!a) {
dx = p1.x - p2.x;
dy = p1.y - p2.y;
} else {
dx = p2.y - p1.y;
dy = p1.x - p2.x;
}
state.fv = getUnitVector(dx, dy);
}
// SPVFS[] Set Projection Vector From Stack
// 0x0A
function SPVFS(state) {
const stack = state.stack;
const y = stack.pop();
const x = stack.pop();
if (exports.DEBUG) console.log(state.step, 'SPVFS[]', y, x);
state.pv = state.dpv = getUnitVector(x, y);
}
// SFVFS[] Set Freedom Vector From Stack
// 0x0B
function SFVFS(state) {
const stack = state.stack;
const y = stack.pop();
const x = stack.pop();
if (exports.DEBUG) console.log(state.step, 'SPVFS[]', y, x);
state.fv = getUnitVector(x, y);
}
// GPV[] Get Projection Vector
// 0x0C
function GPV(state) {
const stack = state.stack;
const pv = state.pv;
if (exports.DEBUG) console.log(state.step, 'GPV[]');
stack.push(pv.x * 0x4000);
stack.push(pv.y * 0x4000);
}
// GFV[] Get Freedom Vector
// 0x0C
function GFV(state) {
const stack = state.stack;
const fv = state.fv;
if (exports.DEBUG) console.log(state.step, 'GFV[]');
stack.push(fv.x * 0x4000);
stack.push(fv.y * 0x4000);
}
// SFVTPV[] Set Freedom Vector To Projection Vector
// 0x0E
function SFVTPV(state) {
state.fv = state.pv;
if (exports.DEBUG) console.log(state.step, 'SFVTPV[]');
}
// ISECT[] moves point p to the InterSECTion of two lines
// 0x0F
function ISECT(state)
{
const stack = state.stack;
const pa0i = stack.pop();
const pa1i = stack.pop();
const pb0i = stack.pop();
const pb1i = stack.pop();
const pi = stack.pop();
const z0 = state.z0;
const z1 = state.z1;
const pa0 = z0[pa0i];
const pa1 = z0[pa1i];
const pb0 = z1[pb0i];
const pb1 = z1[pb1i];
const p = state.z2[pi];
if (exports.DEBUG) console.log('ISECT[], ', pa0i, pa1i, pb0i, pb1i, pi);
// math from
// en.wikipedia.org/wiki/Line%E2%80%93line_intersection#Given_two_points_on_each_line
const x1 = pa0.x;
const y1 = pa0.y;
const x2 = pa1.x;
const y2 = pa1.y;
const x3 = pb0.x;
const y3 = pb0.y;
const x4 = pb1.x;
const y4 = pb1.y;
const div = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
const f1 = x1 * y2 - y1 * x2;
const f2 = x3 * y4 - y3 * x4;
p.x = (f1 * (x3 - x4) - f2 * (x1 - x2)) / div;
p.y = (f1 * (y3 - y4) - f2 * (y1 - y2)) / div;
}
// SRP0[] Set Reference Point 0
// 0x10
function SRP0(state) {
state.rp0 = state.stack.pop();
if (exports.DEBUG) console.log(state.step, 'SRP0[]', state.rp0);
}
// SRP1[] Set Reference Point 1
// 0x11
function SRP1(state) {
state.rp1 = state.stack.pop();
if (exports.DEBUG) console.log(state.step, 'SRP1[]', state.rp1);
}
// SRP1[] Set Reference Point 2
// 0x12
function SRP2(state) {
state.rp2 = state.stack.pop();
if (exports.DEBUG) console.log(state.step, 'SRP2[]', state.rp2);
}
// SZP0[] Set Zone Pointer 0
// 0x13
function SZP0(state) {
const n = state.stack.pop();
if (exports.DEBUG) console.log(state.step, 'SZP0[]', n);
state.zp0 = n;
switch (n) {
case 0:
if (!state.tZone) initTZone(state);
state.z0 = state.tZone;
break;
case 1 :
state.z0 = state.gZone;
break;
default :
throw new Error('Invalid zone pointer');
}
}
// SZP1[] Set Zone Pointer 1
// 0x14
function SZP1(state) {
const n = state.stack.pop();
if (exports.DEBUG) console.log(state.step, 'SZP1[]', n);
state.zp1 = n;
switch (n) {
case 0:
if (!state.tZone) initTZone(state);
state.z1 = state.tZone;
break;
case 1 :
state.z1 = state.gZone;
break;
default :
throw new Error('Invalid zone pointer');
}
}
// SZP2[] Set Zone Pointer 2
// 0x15
function SZP2(state) {
const n = state.stack.pop();
if (exports.DEBUG) console.log(state.step, 'SZP2[]', n);
state.zp2 = n;
switch (n) {
case 0:
if (!state.tZone) initTZone(state);
state.z2 = state.tZone;
break;
case 1 :
state.z2 = state.gZone;
break;
default :
throw new Error('Invalid zone pointer');
}
}
// SZPS[] Set Zone PointerS
// 0x16
function SZPS(state) {
const n = state.stack.pop();
if (exports.DEBUG) console.log(state.step, 'SZPS[]', n);
state.zp0 = state.zp1 = state.zp2 = n;
switch (n) {
case 0:
if (!state.tZone) initTZone(state);
state.z0 = state.z1 = state.z2 = state.tZone;
break;
case 1 :
state.z0 = state.z1 = state.z2 = state.gZone;
break;
default :
throw new Error('Invalid zone pointer');
}
}
// SLOOP[] Set LOOP variable
// 0x17
function SLOOP(state) {
state.loop = state.stack.pop();
if (exports.DEBUG) console.log(state.step, 'SLOOP[]', state.loop);
}
// RTG[] Round To Grid
// 0x18
function RTG(state) {
if (exports.DEBUG) console.log(state.step, 'RTG[]');
state.round = roundToGrid;
}
// RTHG[] Round To Half Grid
// 0x19
function RTHG(state) {
if (exports.DEBUG) console.log(state.step, 'RTHG[]');
state.round = roundToHalfGrid;
}
// SMD[] Set Minimum Distance
// 0x1A
function SMD(state) {
const d = state.stack.pop();
if (exports.DEBUG) console.log(state.step, 'SMD[]', d);
state.minDis = d / 0x40;
}
// ELSE[] ELSE clause
// 0x1B
function ELSE(state) {
// This instruction has been reached by executing a then branch
// so it just skips ahead until matching EIF.
//
// In case the IF was negative the IF[] instruction already
// skipped forward over the ELSE[]
if (exports.DEBUG) console.log(state.step, 'ELSE[]');
skip(state, false);
}
// JMPR[] JuMP Relative
// 0x1C
function JMPR(state) {
const o = state.stack.pop();
if (exports.DEBUG) console.log(state.step, 'JMPR[]', o);
// A jump by 1 would do nothing.
state.ip += o - 1;
}
// SCVTCI[] Set Control Value Table Cut-In
// 0x1D
function SCVTCI(state) {
const n = state.stack.pop();
if (exports.DEBUG) console.log(state.step, 'SCVTCI[]', n);
state.cvCutIn = n / 0x40;
}
// DUP[] DUPlicate top stack element
// 0x20
function DUP(state) {
const stack = state.stack;
if (exports.DEBUG) console.log(state.step, 'DUP[]');
stack.push(stack[stack.length - 1]);
}
// POP[] POP top stack element
// 0x21
function POP(state) {
if (exports.DEBUG) console.log(state.step, 'POP[]');
state.stack.pop();
}
// CLEAR[] CLEAR the stack
// 0x22
function CLEAR(state) {
if (exports.DEBUG) console.log(state.step, 'CLEAR[]');
state.stack.length = 0;
}
// SWAP[] SWAP the top two elements on the stack
// 0x23
function SWAP(state) {
const stack = state.stack;
const a = stack.pop();
const b = stack.pop();
if (exports.DEBUG) console.log(state.step, 'SWAP[]');
stack.push(a);
stack.push(b);
}
// DEPTH[] DEPTH of the stack
// 0x24
function DEPTH(state) {
const stack = state.stack;
if (exports.DEBUG) console.log(state.step, 'DEPTH[]');
stack.push(stack.length);
}
// LOOPCALL[] LOOPCALL function
// 0x2A
function LOOPCALL(state) {
const stack = state.stack;
const fn = stack.pop();
const c = stack.pop();
if (exports.DEBUG) console.log(state.step, 'LOOPCALL[]', fn, c);
// saves callers program
const cip = state.ip;
const cprog = state.prog;
state.prog = state.funcs[fn];
// executes the function
for (let i = 0; i < c; i++) {
exec(state);
if (exports.DEBUG) console.log(
++state.step,
i + 1 < c ? 'next loopcall' : 'done loopcall',
i
);
}
// restores the callers program
state.ip = cip;
state.prog = cprog;
}
// CALL[] CALL function
// 0x2B
function CALL(state) {
const fn = state.stack.pop();
if (exports.DEBUG) console.log(state.step, 'CALL[]', fn);
// saves callers program
const cip = state.ip;
const cprog = state.prog;
state.prog = state.funcs[fn];
// executes the function
exec(state);
// restores the callers program
state.ip = cip;
state.prog = cprog;
if (exports.DEBUG) console.log(++state.step, 'returning from', fn);
}
// CINDEX[] Copy the INDEXed element to the top of the stack
// 0x25
function CINDEX(state) {
const stack = state.stack;
const k = stack.pop();
if (exports.DEBUG) console.log(state.step, 'CINDEX[]', k);
// In case of k == 1, it copies the last element after popping
// thus stack.length - k.
stack.push(stack[stack.length - k]);
}
// MINDEX[] Move the INDEXed element to the top of the stack
// 0x26
function MINDEX(state) {
const stack = state.stack;
const k = stack.pop();
if (exports.DEBUG) console.log(state.step, 'MINDEX[]', k);
stack.push(stack.splice(stack.length - k, 1)[0]);
}
// FDEF[] Function DEFinition
// 0x2C
function FDEF(state) {
if (state.env !== 'fpgm') throw new Error('FDEF not allowed here');
const stack = state.stack;
const prog = state.prog;
let ip = state.ip;
const fn = stack.pop();
const ipBegin = ip;
if (exports.DEBUG) console.log(state.step, 'FDEF[]', fn);
while (prog[++ip] !== 0x2D);
state.ip = ip;
state.funcs[fn] = prog.slice(ipBegin + 1, ip);
}
// MDAP[a] Move Direct Absolute Point
// 0x2E-0x2F
function MDAP(round, state) {
const pi = state.stack.pop();
const p = state.z0[pi];
const fv = state.fv;
const pv = state.pv;
if (exports.DEBUG) console.log(state.step, 'MDAP[' + round + ']', pi);
let d = pv.distance(p, HPZero);
if (round) d = state.round(d);
fv.setRelative(p, HPZero, d, pv);
fv.touch(p);
state.rp0 = state.rp1 = pi;
}
// IUP[a] Interpolate Untouched Points through the outline
// 0x30
function IUP(v, state) {
const z2 = state.z2;
const pLen = z2.length - 2;
let cp;
let pp;
let np;
if (exports.DEBUG) console.log(state.step, 'IUP[' + v.axis + ']');
for (let i = 0; i < pLen; i++) {
cp = z2[i]; // current point
// if this point has been touched go on
if (v.touched(cp)) continue;
pp = cp.prevTouched(v);
// no point on the contour has been touched?
if (pp === cp) continue;
np = cp.nextTouched(v);
if (pp === np) {
// only one point on the contour has been touched
// so simply moves the point like that
v.setRelative(cp, cp, v.distance(pp, pp, false, true), v, true);
}
v.interpolate(cp, pp, np, v);
}
}
// SHP[] SHift Point using reference point
// 0x32-0x33
function SHP(a, state) {
const stack = state.stack;
const rpi = a ? state.rp1 : state.rp2;
const rp = (a ? state.z0 : state.z1)[rpi];
const fv = state.fv;
const pv = state.pv;
let loop = state.loop;
const z2 = state.z2;
while (loop--)
{
const pi = stack.pop();
const p = z2[pi];
const d = pv.distance(rp, rp, false, true);
fv.setRelative(p, p, d, pv);
fv.touch(p);
if (exports.DEBUG) {
console.log(
state.step,
(state.loop > 1 ?
'loop ' + (state.loop - loop) + ': ' :
''
) +
'SHP[' + (a ? 'rp1' : 'rp2') + ']', pi
);
}
}
state.loop = 1;
}
// SHC[] SHift Contour using reference point
// 0x36-0x37
function SHC(a, state) {
const stack = state.stack;
const rpi = a ? state.rp1 : state.rp2;
const rp = (a ? state.z0 : state.z1)[rpi];
const fv = state.fv;
const pv = state.pv;
const ci = stack.pop();
const sp = state.z2[state.contours[ci]];
let p = sp;
if (exports.DEBUG) console.log(state.step, 'SHC[' + a + ']', ci);
const d = pv.distance(rp, rp, false, true);
do {
if (p !== rp) fv.setRelative(p, p, d, pv);
p = p.nextPointOnContour;
} while (p !== sp);
}
// SHZ[] SHift Zone using reference point
// 0x36-0x37
function SHZ(a, state) {
const stack = state.stack;
const rpi = a ? state.rp1 : state.rp2;
const rp = (a ? state.z0 : state.z1)[rpi];
const fv = state.fv;
const pv = state.pv;
const e = stack.pop();
if (exports.DEBUG) console.log(state.step, 'SHZ[' + a + ']', e);
let z;
switch (e) {
case 0 : z = state.tZone; break;
case 1 : z = state.gZone; break;
default : throw new Error('Invalid zone');
}
let p;
const d = pv.distance(rp, rp, false, true);
const pLen = z.length - 2;
for (let i = 0; i < pLen; i++)
{
p = z[i];
fv.setRelative(p, p, d, pv);
//if (p !== rp) fv.setRelative(p, p, d, pv);
}
}
// SHPIX[] SHift point by a PIXel amount
// 0x38
function SHPIX(state) {
const stack = state.stack;
let loop = state.loop;
const fv = state.fv;
const d = stack.pop() / 0x40;
const z2 = state.z2;
while (loop--) {
const pi = stack.pop();
const p = z2[pi];
if (exports.DEBUG) {
console.log(
state.step,
(state.loop > 1 ? 'loop ' + (state.loop - loop) + ': ' : '') +
'SHPIX[]', pi, d
);
}
fv.setRelative(p, p, d);
fv.touch(p);
}
state.loop = 1;
}
// IP[] Interpolate Point
// 0x39
function IP(state) {
const stack = state.stack;
const rp1i = state.rp1;
const rp2i = state.rp2;
let loop = state.loop;
const rp1 = state.z0[rp1i];
const rp2 = state.z1[rp2i];
const fv = state.fv;
const pv = state.dpv;
const z2 = state.z2;
while (loop--) {
const pi = stack.pop();
const p = z2[pi];
if (exports.DEBUG) {
console.log(
state.step,
(state.loop > 1 ? 'loop ' + (state.loop - loop) + ': ' : '') +
'IP[]', pi, rp1i, '<->', rp2i
);
}
fv.interpolate(p, rp1, rp2, pv);
fv.touch(p);
}
state.loop = 1;
}
// MSIRP[a] Move Stack Indirect Relative Point
// 0x3A-0x3B
function MSIRP(a, state) {
const stack = state.stack;
const d = stack.pop() / 64;
const pi = stack.pop();
const p = state.z1[pi];
const rp0 = state.z0[state.rp0];
const fv = state.fv;
const pv = state.pv;
fv.setRelative(p, rp0, d, pv);
fv.touch(p);
if (exports.DEBUG) console.log(state.step, 'MSIRP[' + a + ']', d, pi);
state.rp1 = state.rp0;
state.rp2 = pi;
if (a) state.rp0 = pi;
}
// ALIGNRP[] Align to reference point.
// 0x3C
function ALIGNRP(state) {
const stack = state.stack;
const rp0i = state.rp0;
const rp0 = state.z0[rp0i];
let loop = state.loop;
const fv = state.fv;
const pv = state.pv;
const z1 = state.z1;
while (loop--) {
const pi = stack.pop();
const p = z1[pi];
if (exports.DEBUG) {
console.log(
state.step,
(state.loop > 1 ? 'loop ' + (state.loop - loop) + ': ' : '') +
'ALIGNRP[]', pi
);
}
fv.setRelative(p, rp0, 0, pv);
fv.touch(p);
}
state.loop = 1;
}
// RTG[] Round To Double Grid
// 0x3D
function RTDG(state) {
if (exports.DEBUG) console.log(state.step, 'RTDG[]');
state.round = roundToDoubleGrid;
}
// MIAP[a] Move Indirect Absolute Point
// 0x3E-0x3F
function MIAP(round, state) {
const stack = state.stack;
const n = stack.pop();
const pi = stack.pop();
const p = state.z0[pi];
const fv = state.fv;
const pv = state.pv;
let cv = state.cvt[n];
if (exports.DEBUG) {
console.log(
state.step,
'MIAP[' + round + ']',
n, '(', cv, ')', pi
);
}
let d = pv.distance(p, HPZero);
if (round) {
if (Math.abs(d - cv) < state.cvCutIn) d = cv;
d = state.round(d);
}
fv.setRelative(p, HPZero, d, pv);
if (state.zp0 === 0) {
p.xo = p.x;
p.yo = p.y;
}
fv.touch(p);
state.rp0 = state.rp1 = pi;
}
// NPUSB[] PUSH N Bytes
// 0x40
function NPUSHB(state) {
const prog = state.prog;
let ip = state.ip;
const stack = state.stack;
const n = prog[++ip];
if (exports.DEBUG) console.log(state.step, 'NPUSHB[]', n);
for (let i = 0; i < n; i++) stack.push(prog[++ip]);
state.ip = ip;
}
// NPUSHW[] PUSH N Words
// 0x41
function NPUSHW(state) {
let ip = state.ip;
const prog = state.prog;
const stack = state.stack;
const n = prog[++ip];
if (exports.DEBUG) console.log(state.step, 'NPUSHW[]', n);
for (let i = 0; i < n; i++) {
let w = (prog[++ip] << 8) | prog[++ip];
if (w & 0x8000) w = -((w ^ 0xffff) + 1);
stack.push(w);
}
state.ip = ip;
}
// WS[] Write Store
// 0x42
function WS(state) {
const stack = state.stack;
let store = state.store;
if (!store) store = state.store = [];
const v = stack.pop();
const l = stack.pop();
if (exports.DEBUG) console.log(state.step, 'WS', v, l);
store[l] = v;
}
// RS[] Read Store
// 0x43
function RS(state) {
const stack = state.stack;
const store = state.store;
const l = stack.pop();
if (exports.DEBUG) console.log(state.step, 'RS', l);
const v = (store && store[l]) || 0;
stack.push(v);
}
// WCVTP[] Write Control Value Table in Pixel units
// 0x44
function WCVTP(state) {
const stack = state.stack;
const v = stack.pop();
const l = stack.pop();
if (exports.DEBUG) console.log(state.step, 'WCVTP', v, l);
state.cvt[l] = v / 0x40;
}
// RCVT[] Read Control Value Table entry
// 0x45
function RCVT(state) {
const stack = state.stack;
const cvte = stack.pop();
if (exports.DEBUG) console.log(state.step, 'RCVT', cvte);
stack.push(state.cvt[cvte] * 0x40);
}
// GC[] Get Coordinate projected onto the projection vector
// 0x46-0x47
function GC(a, state) {
const stack = state.stack;
const pi = stack.pop();
const p = state.z2[pi];
if (exports.DEBUG) console.log(state.step, 'GC[' + a + ']', pi);
stack.push(state.dpv.distance(p, HPZero, a, false) * 0x40);
}
// MD[a] Measure Distance
// 0x49-0x4A
function MD(a, state) {
const stack = state.stack;
const pi2 = stack.pop();
const pi1 = stack.pop();
const p2 = state.z1[pi2];
const p1 = state.z0[pi1];
const d = state.dpv.distance(p1, p2, a, a);
if (exports.DEBUG) console.log(state.step, 'MD[' + a + ']', pi2, pi1, '->', d);
state.stack.push(Math.round(d * 64));
}
// MPPEM[] Measure Pixels Per EM
// 0x4B
function MPPEM(state) {
if (exports.DEBUG) console.log(state.step, 'MPPEM[]');
state.stack.push(state.ppem);
}
// FLIPON[] set the auto FLIP Boolean to ON
// 0x4D
function FLIPON(state) {
if (exports.DEBUG) console.log(state.step, 'FLIPON[]');
state.autoFlip = true;
}
// LT[] Less Than
// 0x50
function LT(state) {
const stack = state.stack;
const e2 = stack.pop();
const e1 = stack.pop();
if (exports.DEBUG) console.log(state.step, 'LT[]', e2, e1);
stack.push(e1 < e2 ? 1 : 0);
}
// LTEQ[] Less Than or EQual
// 0x53
function LTEQ(state) {
const stack = state.stack;
const e2 = stack.pop();
const e1 = stack.pop();
if (exports.DEBUG) console.log(state.step, 'LTEQ[]', e2, e1);
stack.push(e1 <= e2 ? 1 : 0);
}
// GTEQ[] Greater Than
// 0x52
function GT(state) {
const stack = state.stack;
const e2 = stack.pop();
const e1 = stack.pop();
if (exports.DEBUG) console.log(state.step, 'GT[]', e2, e1);
stack.push(e1 > e2 ? 1 : 0);
}
// GTEQ[] Greater Than or EQual
// 0x53
function GTEQ(state) {
const stack = state.stack;
const e2 = stack.pop();
const e1 = stack.pop();
if (exports.DEBUG) console.log(state.step, 'GTEQ[]', e2, e1);
stack.push(e1 >= e2 ? 1 : 0);
}
// EQ[] EQual
// 0x54
function EQ(state) {
const stack = state.stack;
const e2 = stack.pop();
const e1 = stack.pop();
if (exports.DEBUG) console.log(state.step, 'EQ[]', e2, e1);
stack.push(e2 === e1 ? 1 : 0);
}
// NEQ[] Not EQual
// 0x55
function NEQ(state) {
const stack = state.stack;
const e2 = stack.pop();
const e1 = stack.pop();
if (exports.DEBUG) console.log(state.step, 'NEQ[]', e2, e1);
stack.push(e2 !== e1 ? 1 : 0);
}
// ODD[] ODD
// 0x56
function ODD(state) {
const stack = state.stack;
const n = stack.pop();
if (exports.DEBUG) console.log(state.step, 'ODD[]', n);
stack.push(Math.trunc(n) % 2 ? 1 : 0);
}
// EVEN[] EVEN
// 0x57
function EVEN(state) {
const stack = state.stack;
const n = stack.pop();
if (exports.DEBUG) console.log(state.step, 'EVEN[]', n);
stack.push(Math.trunc(n) % 2 ? 0 : 1);
}
// IF[] IF test
// 0x58
function IF(state) {
let test = state.stack.pop();
let ins;
if (exports.DEBUG) console.log(state.step, 'IF[]', test);
// if test is true it just continues
// if not the ip is skipped until matching ELSE or EIF
if (!test) {
skip(state, true);
if (exports.DEBUG) console.log(state.step, ins === 0x1B ? 'ELSE[]' : 'EIF[]');
}
}
// EIF[] End IF
// 0x59
function EIF(state) {
// this can be reached normally when
// executing an else branch.
// -> just ignore it
if (exports.DEBUG) console.log(state.step, 'EIF[]');
}
// AND[] logical AND
// 0x5A
function AND(state) {
const stack = state.stack;
const e2 = stack.pop();
const e1 = stack.pop();
if (exports.DEBUG) console.log(state.step, 'AND[]', e2, e1);
stack.push(e2 && e1 ? 1 : 0);
}
// OR[] logical OR
// 0x5B
function OR(state) {
const stack = state.stack;
const e2 = stack.pop();
const e1 = stack.pop();
if (exports.DEBUG) console.log(state.step, 'OR[]', e2, e1);
stack.push(e2 || e1 ? 1 : 0);
}
// NOT[] logical NOT
// 0x5C
function NOT(state) {
const stack = state.stack;
const e = stack.pop();
if (exports.DEBUG) console.log(state.step, 'NOT[]', e);
stack.push(e ? 0 : 1);
}
// DELTAP1[] DELTA exception P1
// DELTAP2[] DELTA exception P2
// DELTAP3[] DELTA exception P3
// 0x5D, 0x71, 0x72
function DELTAP123(b, state) {
const stack = state.stack;
const n = stack.pop();
const fv = state.fv;
const pv = state.pv;
const ppem = state.ppem;
const base = state.deltaBase + (b - 1) * 16;
const ds = state.deltaShift;
const z0 = state.z0;
if (exports.DEBUG) console.log(state.step, 'DELTAP[' + b + ']', n, stack);
for (let i = 0; i < n; i++) {
const pi = stack.pop();
const arg = stack.pop();
const appem = base + ((arg & 0xF0) >> 4);
if (appem !== ppem) continue;
let mag = (arg & 0x0F) - 8;
if (mag >= 0) mag++;
if (exports.DEBUG) console.log(state.step, 'DELTAPFIX', pi, 'by', mag * ds);
const p = z0[pi];
fv.setRelative(p, p, mag * ds, pv);
}
}
// SDB[] Set Delta Base in the graphics state
// 0x5E
function SDB(state) {
const stack = state.stack;
const n = stack.pop();
if (exports.DEBUG) console.log(state.step, 'SDB[]', n);
state.deltaBase = n;
}
// SDS[] Set Delta Shift in the graphics state
// 0x5F
function SDS(state) {
const stack = state.stack;
const n = stack.pop();
if (exports.DEBUG) console.log(state.step, 'SDS[]', n);
state.deltaShift = Math.pow(0.5, n);
}
// ADD[] ADD
// 0x60
function ADD(state) {
const stack = state.stack;
const n2 = stack.pop();
const n1 = stack.pop();
if (exports.DEBUG) console.log(state.step, 'ADD[]', n2, n1);
stack.push(n1 + n2);
}
// SUB[] SUB
// 0x61
function SUB(state) {
const stack = state.stack;
const n2 = stack.pop();
const n1 = stack.pop();
if (exports.DEBUG) console.log(state.step, 'SUB[]', n2, n1);
stack.push(n1 - n2);
}
// DIV[] DIV
// 0x62
function DIV(state) {
const stack = state.stack;
const n2 = stack.pop();
const n1 = stack.pop();
if (exports.DEBUG) console.log(state.step, 'DIV[]', n2, n1);
stack.push(n1 * 64 / n2);
}
// MUL[] MUL
// 0x63
function MUL(state) {
const stack = state.stack;
const n2 = stack.pop();
const n1 = stack.pop();
if (exports.DEBUG)