UNPKG

opentype.js

Version:
2,125 lines (1,709 loc) 74.4 kB
/* 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)