feat(flight-finder): implement milestone M1 - domain model and skill contract

This commit is contained in:
2026-03-30 16:45:40 -05:00
parent 57f6b132b2
commit 9c7103770a
1237 changed files with 901934 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
/**
* Represents a glyph bounding box
*/
export default class BBox {
constructor(minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity) {
/**
* The minimum X position in the bounding box
* @type {number}
*/
this.minX = minX;
/**
* The minimum Y position in the bounding box
* @type {number}
*/
this.minY = minY;
/**
* The maxmimum X position in the bounding box
* @type {number}
*/
this.maxX = maxX;
/**
* The maxmimum Y position in the bounding box
* @type {number}
*/
this.maxY = maxY;
}
/**
* The width of the bounding box
* @type {number}
*/
get width() {
return this.maxX - this.minX;
}
/**
* The height of the bounding box
* @type {number}
*/
get height() {
return this.maxY - this.minY;
}
addPoint(x, y) {
if (Math.abs(x) !== Infinity) {
if (x < this.minX) {
this.minX = x;
}
if (x > this.maxX) {
this.maxX = x;
}
}
if (Math.abs(y) !== Infinity) {
if (y < this.minY) {
this.minY = y;
}
if (y > this.maxY) {
this.maxY = y;
}
}
}
copy() {
return new BBox(this.minX, this.minY, this.maxX, this.maxY);
}
}

View File

@@ -0,0 +1,604 @@
import Glyph from './Glyph';
import Path from './Path';
/**
* Represents an OpenType PostScript glyph, in the Compact Font Format.
*/
export default class CFFGlyph extends Glyph {
type = 'CFF';
_getName() {
if (this._font.CFF2) {
return super._getName();
}
return this._font['CFF '].getGlyphName(this.id);
}
bias(s) {
if (s.length < 1240) {
return 107;
} else if (s.length < 33900) {
return 1131;
} else {
return 32768;
}
}
_getPath() {
let cff = this._font.CFF2 || this._font['CFF '];
let { stream } = cff;
let str = cff.topDict.CharStrings[this.id];
let end = str.offset + str.length;
stream.pos = str.offset;
let path = new Path;
let stack = [];
let trans = [];
let width = null;
let nStems = 0;
let x = 0, y = 0;
let usedGsubrs;
let usedSubrs;
let open = false;
this._usedGsubrs = usedGsubrs = {};
this._usedSubrs = usedSubrs = {};
let gsubrs = cff.globalSubrIndex || [];
let gsubrsBias = this.bias(gsubrs);
let privateDict = cff.privateDictForGlyph(this.id) || {};
let subrs = privateDict.Subrs || [];
let subrsBias = this.bias(subrs);
let vstore = cff.topDict.vstore && cff.topDict.vstore.itemVariationStore;
let vsindex = privateDict.vsindex;
let variationProcessor = this._font._variationProcessor;
function checkWidth() {
if (width == null) {
width = stack.shift() + privateDict.nominalWidthX;
}
}
function parseStems() {
if (stack.length % 2 !== 0) {
checkWidth();
}
nStems += stack.length >> 1;
return stack.length = 0;
}
function moveTo(x, y) {
if (open) {
path.closePath();
}
path.moveTo(x, y);
open = true;
}
let parse = function () {
while (stream.pos < end) {
let op = stream.readUInt8();
if (op < 32) {
let index, subr, phase;
let c1x, c1y, c2x, c2y, c3x, c3y;
let c4x, c4y, c5x, c5y, c6x, c6y;
let pts;
switch (op) {
case 1: // hstem
case 3: // vstem
case 18: // hstemhm
case 23: // vstemhm
parseStems();
break;
case 4: // vmoveto
if (stack.length > 1) {
checkWidth();
}
y += stack.shift();
moveTo(x, y);
break;
case 5: // rlineto
while (stack.length >= 2) {
x += stack.shift();
y += stack.shift();
path.lineTo(x, y);
}
break;
case 6: // hlineto
case 7: // vlineto
phase = op === 6;
while (stack.length >= 1) {
if (phase) {
x += stack.shift();
} else {
y += stack.shift();
}
path.lineTo(x, y);
phase = !phase;
}
break;
case 8: // rrcurveto
while (stack.length > 0) {
c1x = x + stack.shift();
c1y = y + stack.shift();
c2x = c1x + stack.shift();
c2y = c1y + stack.shift();
x = c2x + stack.shift();
y = c2y + stack.shift();
path.bezierCurveTo(c1x, c1y, c2x, c2y, x, y);
}
break;
case 10: // callsubr
index = stack.pop() + subrsBias;
subr = subrs[index];
if (subr) {
usedSubrs[index] = true;
let p = stream.pos;
let e = end;
stream.pos = subr.offset;
end = subr.offset + subr.length;
parse();
stream.pos = p;
end = e;
}
break;
case 11: // return
if (cff.version >= 2) {
break;
}
return;
case 14: // endchar
if (cff.version >= 2) {
break;
}
if (stack.length > 0) {
checkWidth();
}
if (open) {
path.closePath();
open = false;
}
break;
case 15: { // vsindex
if (cff.version < 2) {
throw new Error('vsindex operator not supported in CFF v1');
}
vsindex = stack.pop();
break;
}
case 16: { // blend
if (cff.version < 2) {
throw new Error('blend operator not supported in CFF v1');
}
if (!variationProcessor) {
throw new Error('blend operator in non-variation font');
}
let blendVector = variationProcessor.getBlendVector(vstore, vsindex);
let numBlends = stack.pop();
let numOperands = numBlends * blendVector.length;
let delta = stack.length - numOperands;
let base = delta - numBlends;
for (let i = 0; i < numBlends; i++) {
let sum = stack[base + i];
for (let j = 0; j < blendVector.length; j++) {
sum += blendVector[j] * stack[delta++];
}
stack[base + i] = sum;
}
while (numOperands--) {
stack.pop();
}
break;
}
case 19: // hintmask
case 20: // cntrmask
parseStems();
stream.pos += (nStems + 7) >> 3;
break;
case 21: // rmoveto
if (stack.length > 2) {
checkWidth();
}
x += stack.shift();
y += stack.shift();
moveTo(x, y);
break;
case 22: // hmoveto
if (stack.length > 1) {
checkWidth();
}
x += stack.shift();
moveTo(x, y);
break;
case 24: // rcurveline
while (stack.length >= 8) {
c1x = x + stack.shift();
c1y = y + stack.shift();
c2x = c1x + stack.shift();
c2y = c1y + stack.shift();
x = c2x + stack.shift();
y = c2y + stack.shift();
path.bezierCurveTo(c1x, c1y, c2x, c2y, x, y);
}
x += stack.shift();
y += stack.shift();
path.lineTo(x, y);
break;
case 25: // rlinecurve
while (stack.length >= 8) {
x += stack.shift();
y += stack.shift();
path.lineTo(x, y);
}
c1x = x + stack.shift();
c1y = y + stack.shift();
c2x = c1x + stack.shift();
c2y = c1y + stack.shift();
x = c2x + stack.shift();
y = c2y + stack.shift();
path.bezierCurveTo(c1x, c1y, c2x, c2y, x, y);
break;
case 26: // vvcurveto
if (stack.length % 2) {
x += stack.shift();
}
while (stack.length >= 4) {
c1x = x;
c1y = y + stack.shift();
c2x = c1x + stack.shift();
c2y = c1y + stack.shift();
x = c2x;
y = c2y + stack.shift();
path.bezierCurveTo(c1x, c1y, c2x, c2y, x, y);
}
break;
case 27: // hhcurveto
if (stack.length % 2) {
y += stack.shift();
}
while (stack.length >= 4) {
c1x = x + stack.shift();
c1y = y;
c2x = c1x + stack.shift();
c2y = c1y + stack.shift();
x = c2x + stack.shift();
y = c2y;
path.bezierCurveTo(c1x, c1y, c2x, c2y, x, y);
}
break;
case 28: // shortint
stack.push(stream.readInt16BE());
break;
case 29: // callgsubr
index = stack.pop() + gsubrsBias;
subr = gsubrs[index];
if (subr) {
usedGsubrs[index] = true;
let p = stream.pos;
let e = end;
stream.pos = subr.offset;
end = subr.offset + subr.length;
parse();
stream.pos = p;
end = e;
}
break;
case 30: // vhcurveto
case 31: // hvcurveto
phase = op === 31;
while (stack.length >= 4) {
if (phase) {
c1x = x + stack.shift();
c1y = y;
c2x = c1x + stack.shift();
c2y = c1y + stack.shift();
y = c2y + stack.shift();
x = c2x + (stack.length === 1 ? stack.shift() : 0);
} else {
c1x = x;
c1y = y + stack.shift();
c2x = c1x + stack.shift();
c2y = c1y + stack.shift();
x = c2x + stack.shift();
y = c2y + (stack.length === 1 ? stack.shift() : 0);
}
path.bezierCurveTo(c1x, c1y, c2x, c2y, x, y);
phase = !phase;
}
break;
case 12:
op = stream.readUInt8();
switch (op) {
case 3: // and
let a = stack.pop();
let b = stack.pop();
stack.push(a && b ? 1 : 0);
break;
case 4: // or
a = stack.pop();
b = stack.pop();
stack.push(a || b ? 1 : 0);
break;
case 5: // not
a = stack.pop();
stack.push(a ? 0 : 1);
break;
case 9: // abs
a = stack.pop();
stack.push(Math.abs(a));
break;
case 10: // add
a = stack.pop();
b = stack.pop();
stack.push(a + b);
break;
case 11: // sub
a = stack.pop();
b = stack.pop();
stack.push(a - b);
break;
case 12: // div
a = stack.pop();
b = stack.pop();
stack.push(a / b);
break;
case 14: // neg
a = stack.pop();
stack.push(-a);
break;
case 15: // eq
a = stack.pop();
b = stack.pop();
stack.push(a === b ? 1 : 0);
break;
case 18: // drop
stack.pop();
break;
case 20: // put
let val = stack.pop();
let idx = stack.pop();
trans[idx] = val;
break;
case 21: // get
idx = stack.pop();
stack.push(trans[idx] || 0);
break;
case 22: // ifelse
let s1 = stack.pop();
let s2 = stack.pop();
let v1 = stack.pop();
let v2 = stack.pop();
stack.push(v1 <= v2 ? s1 : s2);
break;
case 23: // random
stack.push(Math.random());
break;
case 24: // mul
a = stack.pop();
b = stack.pop();
stack.push(a * b);
break;
case 26: // sqrt
a = stack.pop();
stack.push(Math.sqrt(a));
break;
case 27: // dup
a = stack.pop();
stack.push(a, a);
break;
case 28: // exch
a = stack.pop();
b = stack.pop();
stack.push(b, a);
break;
case 29: // index
idx = stack.pop();
if (idx < 0) {
idx = 0;
} else if (idx > stack.length - 1) {
idx = stack.length - 1;
}
stack.push(stack[idx]);
break;
case 30: // roll
let n = stack.pop();
let j = stack.pop();
if (j >= 0) {
while (j > 0) {
var t = stack[n - 1];
for (let i = n - 2; i >= 0; i--) {
stack[i + 1] = stack[i];
}
stack[0] = t;
j--;
}
} else {
while (j < 0) {
var t = stack[0];
for (let i = 0; i <= n; i++) {
stack[i] = stack[i + 1];
}
stack[n - 1] = t;
j++;
}
}
break;
case 34: // hflex
c1x = x + stack.shift();
c1y = y;
c2x = c1x + stack.shift();
c2y = c1y + stack.shift();
c3x = c2x + stack.shift();
c3y = c2y;
c4x = c3x + stack.shift();
c4y = c3y;
c5x = c4x + stack.shift();
c5y = c4y;
c6x = c5x + stack.shift();
c6y = c5y;
x = c6x;
y = c6y;
path.bezierCurveTo(c1x, c1y, c2x, c2y, c3x, c3y);
path.bezierCurveTo(c4x, c4y, c5x, c5y, c6x, c6y);
break;
case 35: // flex
pts = [];
for (let i = 0; i <= 5; i++) {
x += stack.shift();
y += stack.shift();
pts.push(x, y);
}
path.bezierCurveTo(...pts.slice(0, 6));
path.bezierCurveTo(...pts.slice(6));
stack.shift(); // fd
break;
case 36: // hflex1
c1x = x + stack.shift();
c1y = y + stack.shift();
c2x = c1x + stack.shift();
c2y = c1y + stack.shift();
c3x = c2x + stack.shift();
c3y = c2y;
c4x = c3x + stack.shift();
c4y = c3y;
c5x = c4x + stack.shift();
c5y = c4y + stack.shift();
c6x = c5x + stack.shift();
c6y = c5y;
x = c6x;
y = c6y;
path.bezierCurveTo(c1x, c1y, c2x, c2y, c3x, c3y);
path.bezierCurveTo(c4x, c4y, c5x, c5y, c6x, c6y);
break;
case 37: // flex1
let startx = x;
let starty = y;
pts = [];
for (let i = 0; i <= 4; i++) {
x += stack.shift();
y += stack.shift();
pts.push(x, y);
}
if (Math.abs(x - startx) > Math.abs(y - starty)) { // horizontal
x += stack.shift();
y = starty;
} else {
x = startx;
y += stack.shift();
}
pts.push(x, y);
path.bezierCurveTo(...pts.slice(0, 6));
path.bezierCurveTo(...pts.slice(6));
break;
default:
throw new Error(`Unknown op: 12 ${op}`);
}
break;
default:
throw new Error(`Unknown op: ${op}`);
}
} else if (op < 247) {
stack.push(op - 139);
} else if (op < 251) {
var b1 = stream.readUInt8();
stack.push((op - 247) * 256 + b1 + 108);
} else if (op < 255) {
var b1 = stream.readUInt8();
stack.push(-(op - 251) * 256 - b1 - 108);
} else {
stack.push(stream.readInt32BE() / 65536);
}
}
};
parse();
if (open) {
path.closePath();
}
return path;
}
}

View File

@@ -0,0 +1,90 @@
import Glyph from './Glyph';
import BBox from './BBox';
class COLRLayer {
constructor(glyph, color) {
this.glyph = glyph;
this.color = color;
}
}
/**
* Represents a color (e.g. emoji) glyph in Microsoft's COLR format.
* Each glyph in this format contain a list of colored layers, each
* of which is another vector glyph.
*/
export default class COLRGlyph extends Glyph {
type = 'COLR';
_getBBox() {
let bbox = new BBox;
for (let i = 0; i < this.layers.length; i++) {
let layer = this.layers[i];
let b = layer.glyph.bbox;
bbox.addPoint(b.minX, b.minY);
bbox.addPoint(b.maxX, b.maxY);
}
return bbox;
}
/**
* Returns an array of objects containing the glyph and color for
* each layer in the composite color glyph.
* @type {object[]}
*/
get layers() {
let cpal = this._font.CPAL;
let colr = this._font.COLR;
let low = 0;
let high = colr.baseGlyphRecord.length - 1;
while (low <= high) {
let mid = (low + high) >> 1;
var rec = colr.baseGlyphRecord[mid];
if (this.id < rec.gid) {
high = mid - 1;
} else if (this.id > rec.gid) {
low = mid + 1;
} else {
var baseLayer = rec;
break;
}
}
// if base glyph not found in COLR table,
// default to normal glyph from glyf or CFF
if (baseLayer == null) {
var g = this._font._getBaseGlyph(this.id);
var color = {
red: 0,
green: 0,
blue: 0,
alpha: 255
};
return [new COLRLayer(g, color)];
}
// otherwise, return an array of all the layers
let layers = [];
for (let i = baseLayer.firstLayerIndex; i < baseLayer.firstLayerIndex + baseLayer.numLayers; i++) {
var rec = colr.layerRecords[i];
var color = cpal.colorRecords[rec.paletteIndex];
var g = this._font._getBaseGlyph(rec.gid);
layers.push(new COLRLayer(g, color));
}
return layers;
}
render(ctx, size) {
for (let {glyph, color} of this.layers) {
ctx.fillColor([color.red, color.green, color.blue], color.alpha / 255 * 100);
glyph.render(ctx, size);
}
return;
}
}

View File

@@ -0,0 +1,212 @@
import { cache } from '../decorators';
import Path from './Path';
import {isMark} from 'unicode-properties';
import StandardNames from './StandardNames';
/**
* Glyph objects represent a glyph in the font. They have various properties for accessing metrics and
* the actual vector path the glyph represents, and methods for rendering the glyph to a graphics context.
*
* You do not create glyph objects directly. They are created by various methods on the font object.
* There are several subclasses of the base Glyph class internally that may be returned depending
* on the font format, but they all inherit from this class.
*/
export default class Glyph {
constructor(id, codePoints, font) {
/**
* The glyph id in the font
* @type {number}
*/
this.id = id;
/**
* An array of unicode code points that are represented by this glyph.
* There can be multiple code points in the case of ligatures and other glyphs
* that represent multiple visual characters.
* @type {number[]}
*/
this.codePoints = codePoints;
this._font = font;
// TODO: get this info from GDEF if available
this.isMark = this.codePoints.length > 0 && this.codePoints.every(isMark);
this.isLigature = this.codePoints.length > 1;
}
_getPath() {
return new Path();
}
_getCBox() {
return this.path.cbox;
}
_getBBox() {
return this.path.bbox;
}
_getTableMetrics(table) {
if (this.id < table.metrics.length) {
return table.metrics.get(this.id);
}
let metric = table.metrics.get(table.metrics.length - 1);
let res = {
advance: metric ? metric.advance : 0,
bearing: table.bearings.get(this.id - table.metrics.length) || 0
};
return res;
}
_getMetrics(cbox) {
if (this._metrics) { return this._metrics; }
let {advance:advanceWidth, bearing:leftBearing} = this._getTableMetrics(this._font.hmtx);
// For vertical metrics, use vmtx if available, or fall back to global data from OS/2 or hhea
if (this._font.vmtx) {
var {advance:advanceHeight, bearing:topBearing} = this._getTableMetrics(this._font.vmtx);
} else {
let os2;
if (typeof cbox === 'undefined' || cbox === null) { ({ cbox } = this); }
if ((os2 = this._font['OS/2']) && os2.version > 0) {
var advanceHeight = Math.abs(os2.typoAscender - os2.typoDescender);
var topBearing = os2.typoAscender - cbox.maxY;
} else {
let { hhea } = this._font;
var advanceHeight = Math.abs(hhea.ascent - hhea.descent);
var topBearing = hhea.ascent - cbox.maxY;
}
}
if (this._font._variationProcessor && this._font.HVAR) {
advanceWidth += this._font._variationProcessor.getAdvanceAdjustment(this.id, this._font.HVAR);
}
return this._metrics = { advanceWidth, advanceHeight, leftBearing, topBearing };
}
/**
* The glyphs control box.
* This is often the same as the bounding box, but is faster to compute.
* Because of the way bezier curves are defined, some of the control points
* can be outside of the bounding box. Where `bbox` takes this into account,
* `cbox` does not. Thus, cbox is less accurate, but faster to compute.
* See [here](http://www.freetype.org/freetype2/docs/glyphs/glyphs-6.html#section-2)
* for a more detailed description.
*
* @type {BBox}
*/
@cache
get cbox() {
return this._getCBox();
}
/**
* The glyphs bounding box, i.e. the rectangle that encloses the
* glyph outline as tightly as possible.
* @type {BBox}
*/
@cache
get bbox() {
return this._getBBox();
}
/**
* A vector Path object representing the glyph outline.
* @type {Path}
*/
@cache
get path() {
// Cache the path so we only decode it once
// Decoding is actually performed by subclasses
return this._getPath();
}
/**
* Returns a path scaled to the given font size.
* @param {number} size
* @return {Path}
*/
getScaledPath(size) {
let scale = 1 / this._font.unitsPerEm * size;
return this.path.scale(scale);
}
/**
* The glyph's advance width.
* @type {number}
*/
@cache
get advanceWidth() {
return this._getMetrics().advanceWidth;
}
/**
* The glyph's advance height.
* @type {number}
*/
@cache
get advanceHeight() {
return this._getMetrics().advanceHeight;
}
get ligatureCaretPositions() {}
_getName() {
let { post } = this._font;
if (!post) {
return null;
}
switch (post.version) {
case 1:
return StandardNames[this.id];
case 2:
let id = post.glyphNameIndex[this.id];
if (id < StandardNames.length) {
return StandardNames[id];
}
return post.names[id - StandardNames.length];
case 2.5:
return StandardNames[this.id + post.offsets[this.id]];
case 4:
return String.fromCharCode(post.map[this.id]);
}
}
/**
* The glyph's name
* @type {string}
*/
@cache
get name() {
return this._getName();
}
/**
* Renders the glyph to the given graphics context, at the specified font size.
* @param {CanvasRenderingContext2d} ctx
* @param {number} size
*/
render(ctx, size) {
ctx.save();
let scale = 1 / this._font.head.unitsPerEm * size;
ctx.scale(scale, scale);
let fn = this.path.toFunction();
fn(ctx);
ctx.fill();
ctx.restore();
}
}

View File

@@ -0,0 +1,486 @@
const TUPLES_SHARE_POINT_NUMBERS = 0x8000;
const TUPLE_COUNT_MASK = 0x0fff;
const EMBEDDED_TUPLE_COORD = 0x8000;
const INTERMEDIATE_TUPLE = 0x4000;
const PRIVATE_POINT_NUMBERS = 0x2000;
const TUPLE_INDEX_MASK = 0x0fff;
const POINTS_ARE_WORDS = 0x80;
const POINT_RUN_COUNT_MASK = 0x7f;
const DELTAS_ARE_ZERO = 0x80;
const DELTAS_ARE_WORDS = 0x40;
const DELTA_RUN_COUNT_MASK = 0x3f;
/**
* This class is transforms TrueType glyphs according to the data from
* the Apple Advanced Typography variation tables (fvar, gvar, and avar).
* These tables allow infinite adjustments to glyph weight, width, slant,
* and optical size without the designer needing to specify every exact style.
*
* Apple's documentation for these tables is not great, so thanks to the
* Freetype project for figuring much of this out.
*
* @private
*/
export default class GlyphVariationProcessor {
constructor(font, coords) {
this.font = font;
this.normalizedCoords = this.normalizeCoords(coords);
this.blendVectors = new Map;
}
normalizeCoords(coords) {
// the default mapping is linear along each axis, in two segments:
// from the minValue to defaultValue, and from defaultValue to maxValue.
let normalized = [];
for (var i = 0; i < this.font.fvar.axis.length; i++) {
let axis = this.font.fvar.axis[i];
if (coords[i] < axis.defaultValue) {
normalized.push((coords[i] - axis.defaultValue + Number.EPSILON) / (axis.defaultValue - axis.minValue + Number.EPSILON));
} else {
normalized.push((coords[i] - axis.defaultValue + Number.EPSILON) / (axis.maxValue - axis.defaultValue + Number.EPSILON));
}
}
// if there is an avar table, the normalized value is calculated
// by interpolating between the two nearest mapped values.
if (this.font.avar) {
for (var i = 0; i < this.font.avar.segment.length; i++) {
let segment = this.font.avar.segment[i];
for (let j = 0; j < segment.correspondence.length; j++) {
let pair = segment.correspondence[j];
if (j >= 1 && normalized[i] < pair.fromCoord) {
let prev = segment.correspondence[j - 1];
normalized[i] = ((normalized[i] - prev.fromCoord) * (pair.toCoord - prev.toCoord) + Number.EPSILON) /
(pair.fromCoord - prev.fromCoord + Number.EPSILON) +
prev.toCoord;
break;
}
}
}
}
return normalized;
}
transformPoints(gid, glyphPoints) {
if (!this.font.fvar || !this.font.gvar) { return; }
let { gvar } = this.font;
if (gid >= gvar.glyphCount) { return; }
let offset = gvar.offsets[gid];
if (offset === gvar.offsets[gid + 1]) { return; }
// Read the gvar data for this glyph
let { stream } = this.font;
stream.pos = offset;
if (stream.pos >= stream.length) {
return;
}
let tupleCount = stream.readUInt16BE();
let offsetToData = offset + stream.readUInt16BE();
if (tupleCount & TUPLES_SHARE_POINT_NUMBERS) {
var here = stream.pos;
stream.pos = offsetToData;
var sharedPoints = this.decodePoints();
offsetToData = stream.pos;
stream.pos = here;
}
let origPoints = glyphPoints.map(pt => pt.copy());
tupleCount &= TUPLE_COUNT_MASK;
for (let i = 0; i < tupleCount; i++) {
let tupleDataSize = stream.readUInt16BE();
let tupleIndex = stream.readUInt16BE();
if (tupleIndex & EMBEDDED_TUPLE_COORD) {
var tupleCoords = [];
for (let a = 0; a < gvar.axisCount; a++) {
tupleCoords.push(stream.readInt16BE() / 16384);
}
} else {
if ((tupleIndex & TUPLE_INDEX_MASK) >= gvar.globalCoordCount) {
throw new Error('Invalid gvar table');
}
var tupleCoords = gvar.globalCoords[tupleIndex & TUPLE_INDEX_MASK];
}
if (tupleIndex & INTERMEDIATE_TUPLE) {
var startCoords = [];
for (let a = 0; a < gvar.axisCount; a++) {
startCoords.push(stream.readInt16BE() / 16384);
}
var endCoords = [];
for (let a = 0; a < gvar.axisCount; a++) {
endCoords.push(stream.readInt16BE() / 16384);
}
}
// Get the factor at which to apply this tuple
let factor = this.tupleFactor(tupleIndex, tupleCoords, startCoords, endCoords);
if (factor === 0) {
offsetToData += tupleDataSize;
continue;
}
var here = stream.pos;
stream.pos = offsetToData;
if (tupleIndex & PRIVATE_POINT_NUMBERS) {
var points = this.decodePoints();
} else {
var points = sharedPoints;
}
// points.length = 0 means there are deltas for all points
let nPoints = points.length === 0 ? glyphPoints.length : points.length;
let xDeltas = this.decodeDeltas(nPoints);
let yDeltas = this.decodeDeltas(nPoints);
if (points.length === 0) { // all points
for (let i = 0; i < glyphPoints.length; i++) {
var point = glyphPoints[i];
point.x += Math.round(xDeltas[i] * factor);
point.y += Math.round(yDeltas[i] * factor);
}
} else {
let outPoints = origPoints.map(pt => pt.copy());
let hasDelta = glyphPoints.map(() => false);
for (let i = 0; i < points.length; i++) {
let idx = points[i];
if (idx < glyphPoints.length) {
let point = outPoints[idx];
hasDelta[idx] = true;
point.x += xDeltas[i] * factor;
point.y += yDeltas[i] * factor;
}
}
this.interpolateMissingDeltas(outPoints, origPoints, hasDelta);
for (let i = 0; i < glyphPoints.length; i++) {
let deltaX = outPoints[i].x - origPoints[i].x;
let deltaY = outPoints[i].y - origPoints[i].y;
glyphPoints[i].x = Math.round(glyphPoints[i].x + deltaX);
glyphPoints[i].y = Math.round(glyphPoints[i].y + deltaY);
}
}
offsetToData += tupleDataSize;
stream.pos = here;
}
}
decodePoints() {
let stream = this.font.stream;
let count = stream.readUInt8();
if (count & POINTS_ARE_WORDS) {
count = (count & POINT_RUN_COUNT_MASK) << 8 | stream.readUInt8();
}
let points = new Uint16Array(count);
let i = 0;
let point = 0;
while (i < count) {
let run = stream.readUInt8();
let runCount = (run & POINT_RUN_COUNT_MASK) + 1;
let fn = run & POINTS_ARE_WORDS ? stream.readUInt16 : stream.readUInt8;
for (let j = 0; j < runCount && i < count; j++) {
point += fn.call(stream);
points[i++] = point;
}
}
return points;
}
decodeDeltas(count) {
let stream = this.font.stream;
let i = 0;
let deltas = new Int16Array(count);
while (i < count) {
let run = stream.readUInt8();
let runCount = (run & DELTA_RUN_COUNT_MASK) + 1;
if (run & DELTAS_ARE_ZERO) {
i += runCount;
} else {
let fn = run & DELTAS_ARE_WORDS ? stream.readInt16BE : stream.readInt8;
for (let j = 0; j < runCount && i < count; j++) {
deltas[i++] = fn.call(stream);
}
}
}
return deltas;
}
tupleFactor(tupleIndex, tupleCoords, startCoords, endCoords) {
let normalized = this.normalizedCoords;
let { gvar } = this.font;
let factor = 1;
for (let i = 0; i < gvar.axisCount; i++) {
if (tupleCoords[i] === 0) {
continue;
}
if (normalized[i] === 0) {
return 0;
}
if ((tupleIndex & INTERMEDIATE_TUPLE) === 0) {
if ((normalized[i] < Math.min(0, tupleCoords[i])) ||
(normalized[i] > Math.max(0, tupleCoords[i]))) {
return 0;
}
factor = (factor * normalized[i] + Number.EPSILON) / (tupleCoords[i] + Number.EPSILON);
} else {
if ((normalized[i] < startCoords[i]) ||
(normalized[i] > endCoords[i])) {
return 0;
} else if (normalized[i] < tupleCoords[i]) {
factor = factor * (normalized[i] - startCoords[i] + Number.EPSILON) / (tupleCoords[i] - startCoords[i] + Number.EPSILON);
} else {
factor = factor * (endCoords[i] - normalized[i] + Number.EPSILON) / (endCoords[i] - tupleCoords[i] + Number.EPSILON);
}
}
}
return factor;
}
// Interpolates points without delta values.
// Needed for the Ø and Q glyphs in Skia.
// Algorithm from Freetype.
interpolateMissingDeltas(points, inPoints, hasDelta) {
if (points.length === 0) {
return;
}
let point = 0;
while (point < points.length) {
let firstPoint = point;
// find the end point of the contour
let endPoint = point;
let pt = points[endPoint];
while (!pt.endContour) {
pt = points[++endPoint];
}
// find the first point that has a delta
while (point <= endPoint && !hasDelta[point]) {
point++;
}
if (point > endPoint) {
continue;
}
let firstDelta = point;
let curDelta = point;
point++;
while (point <= endPoint) {
// find the next point with a delta, and interpolate intermediate points
if (hasDelta[point]) {
this.deltaInterpolate(curDelta + 1, point - 1, curDelta, point, inPoints, points);
curDelta = point;
}
point++;
}
// shift contour if we only have a single delta
if (curDelta === firstDelta) {
this.deltaShift(firstPoint, endPoint, curDelta, inPoints, points);
} else {
// otherwise, handle the remaining points at the end and beginning of the contour
this.deltaInterpolate(curDelta + 1, endPoint, curDelta, firstDelta, inPoints, points);
if (firstDelta > 0) {
this.deltaInterpolate(firstPoint, firstDelta - 1, curDelta, firstDelta, inPoints, points);
}
}
point = endPoint + 1;
}
}
deltaInterpolate(p1, p2, ref1, ref2, inPoints, outPoints) {
if (p1 > p2) {
return;
}
let iterable = ['x', 'y'];
for (let i = 0; i < iterable.length; i++) {
let k = iterable[i];
if (inPoints[ref1][k] > inPoints[ref2][k]) {
var p = ref1;
ref1 = ref2;
ref2 = p;
}
let in1 = inPoints[ref1][k];
let in2 = inPoints[ref2][k];
let out1 = outPoints[ref1][k];
let out2 = outPoints[ref2][k];
// If the reference points have the same coordinate but different
// delta, inferred delta is zero. Otherwise interpolate.
if (in1 !== in2 || out1 === out2) {
let scale = in1 === in2 ? 0 : (out2 - out1) / (in2 - in1);
for (let p = p1; p <= p2; p++) {
let out = inPoints[p][k];
if (out <= in1) {
out += out1 - in1;
} else if (out >= in2) {
out += out2 - in2;
} else {
out = out1 + (out - in1) * scale;
}
outPoints[p][k] = out;
}
}
}
}
deltaShift(p1, p2, ref, inPoints, outPoints) {
let deltaX = outPoints[ref].x - inPoints[ref].x;
let deltaY = outPoints[ref].y - inPoints[ref].y;
if (deltaX === 0 && deltaY === 0) {
return;
}
for (let p = p1; p <= p2; p++) {
if (p !== ref) {
outPoints[p].x += deltaX;
outPoints[p].y += deltaY;
}
}
}
getAdvanceAdjustment(gid, table) {
let outerIndex, innerIndex;
if (table.advanceWidthMapping) {
let idx = gid;
if (idx >= table.advanceWidthMapping.mapCount) {
idx = table.advanceWidthMapping.mapCount - 1;
}
let entryFormat = table.advanceWidthMapping.entryFormat;
({outerIndex, innerIndex} = table.advanceWidthMapping.mapData[idx]);
} else {
outerIndex = 0;
innerIndex = gid;
}
return this.getDelta(table.itemVariationStore, outerIndex, innerIndex);
}
// See pseudo code from `Font Variations Overview'
// in the OpenType specification.
getDelta(itemStore, outerIndex, innerIndex) {
if (outerIndex >= itemStore.itemVariationData.length) {
return 0;
}
let varData = itemStore.itemVariationData[outerIndex];
if (innerIndex >= varData.deltaSets.length) {
return 0;
}
let deltaSet = varData.deltaSets[innerIndex];
let blendVector = this.getBlendVector(itemStore, outerIndex);
let netAdjustment = 0;
for (let master = 0; master < varData.regionIndexCount; master++) {
netAdjustment += deltaSet.deltas[master] * blendVector[master];
}
return netAdjustment;
}
getBlendVector(itemStore, outerIndex) {
let varData = itemStore.itemVariationData[outerIndex];
if (this.blendVectors.has(varData)) {
return this.blendVectors.get(varData);
}
let normalizedCoords = this.normalizedCoords;
let blendVector = [];
// outer loop steps through master designs to be blended
for (let master = 0; master < varData.regionIndexCount; master++) {
let scalar = 1;
let regionIndex = varData.regionIndexes[master];
let axes = itemStore.variationRegionList.variationRegions[regionIndex];
// inner loop steps through axes in this region
for (let j = 0; j < axes.length; j++) {
let axis = axes[j];
let axisScalar;
// compute the scalar contribution of this axis
// ignore invalid ranges
if (axis.startCoord > axis.peakCoord || axis.peakCoord > axis.endCoord) {
axisScalar = 1;
} else if (axis.startCoord < 0 && axis.endCoord > 0 && axis.peakCoord !== 0) {
axisScalar = 1;
// peak of 0 means ignore this axis
} else if (axis.peakCoord === 0) {
axisScalar = 1;
// ignore this region if coords are out of range
} else if (normalizedCoords[j] < axis.startCoord || normalizedCoords[j] > axis.endCoord) {
axisScalar = 0;
// calculate a proportional factor
} else {
if (normalizedCoords[j] === axis.peakCoord) {
axisScalar = 1;
} else if (normalizedCoords[j] < axis.peakCoord) {
axisScalar = (normalizedCoords[j] - axis.startCoord + Number.EPSILON) /
(axis.peakCoord - axis.startCoord + Number.EPSILON);
} else {
axisScalar = (axis.endCoord - normalizedCoords[j] + Number.EPSILON) /
(axis.endCoord - axis.peakCoord + Number.EPSILON);
}
}
// take product of all the axis scalars
scalar *= axisScalar;
}
blendVector[master] = scalar;
}
this.blendVectors.set(varData, blendVector);
return blendVector;
}
}

View File

@@ -0,0 +1,244 @@
import BBox from './BBox';
const SVG_COMMANDS = {
moveTo: 'M',
lineTo: 'L',
quadraticCurveTo: 'Q',
bezierCurveTo: 'C',
closePath: 'Z'
};
/**
* Path objects are returned by glyphs and represent the actual
* vector outlines for each glyph in the font. Paths can be converted
* to SVG path data strings, or to functions that can be applied to
* render the path to a graphics context.
*/
export default class Path {
constructor() {
this.commands = [];
this._bbox = null;
this._cbox = null;
}
/**
* Compiles the path to a JavaScript function that can be applied with
* a graphics context in order to render the path.
* @return {string}
*/
toFunction() {
return ctx => {
this.commands.forEach(c => {
return ctx[c.command].apply(ctx, c.args)
})
};
}
/**
* Converts the path to an SVG path data string
* @return {string}
*/
toSVG() {
let cmds = this.commands.map(c => {
let args = c.args.map(arg => Math.round(arg * 100) / 100);
return `${SVG_COMMANDS[c.command]}${args.join(' ')}`;
});
return cmds.join('');
}
/**
* Gets the "control box" of a path.
* This is like the bounding box, but it includes all points including
* control points of bezier segments and is much faster to compute than
* the real bounding box.
* @type {BBox}
*/
get cbox() {
if (!this._cbox) {
let cbox = new BBox;
for (let command of this.commands) {
for (let i = 0; i < command.args.length; i += 2) {
cbox.addPoint(command.args[i], command.args[i + 1]);
}
}
this._cbox = Object.freeze(cbox);
}
return this._cbox;
}
/**
* Gets the exact bounding box of the path by evaluating curve segments.
* Slower to compute than the control box, but more accurate.
* @type {BBox}
*/
get bbox() {
if (this._bbox) {
return this._bbox;
}
let bbox = new BBox;
let cx = 0, cy = 0;
let f = t => (
Math.pow(1 - t, 3) * p0[i]
+ 3 * Math.pow(1 - t, 2) * t * p1[i]
+ 3 * (1 - t) * Math.pow(t, 2) * p2[i]
+ Math.pow(t, 3) * p3[i]
);
for (let c of this.commands) {
switch (c.command) {
case 'moveTo':
case 'lineTo':
let [x, y] = c.args;
bbox.addPoint(x, y);
cx = x;
cy = y;
break;
case 'quadraticCurveTo':
case 'bezierCurveTo':
if (c.command === 'quadraticCurveTo') {
// http://fontforge.org/bezier.html
var [qp1x, qp1y, p3x, p3y] = c.args;
var cp1x = cx + 2 / 3 * (qp1x - cx); // CP1 = QP0 + 2/3 * (QP1-QP0)
var cp1y = cy + 2 / 3 * (qp1y - cy);
var cp2x = p3x + 2 / 3 * (qp1x - p3x); // CP2 = QP2 + 2/3 * (QP1-QP2)
var cp2y = p3y + 2 / 3 * (qp1y - p3y);
} else {
var [cp1x, cp1y, cp2x, cp2y, p3x, p3y] = c.args;
}
// http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html
bbox.addPoint(p3x, p3y);
var p0 = [cx, cy];
var p1 = [cp1x, cp1y];
var p2 = [cp2x, cp2y];
var p3 = [p3x, p3y];
for (var i = 0; i <= 1; i++) {
let b = 6 * p0[i] - 12 * p1[i] + 6 * p2[i];
let a = -3 * p0[i] + 9 * p1[i] - 9 * p2[i] + 3 * p3[i];
c = 3 * p1[i] - 3 * p0[i];
if (a === 0) {
if (b === 0) {
continue;
}
let t = -c / b;
if (0 < t && t < 1) {
if (i === 0) {
bbox.addPoint(f(t), bbox.maxY);
} else if (i === 1) {
bbox.addPoint(bbox.maxX, f(t));
}
}
continue;
}
let b2ac = Math.pow(b, 2) - 4 * c * a;
if (b2ac < 0) {
continue;
}
let t1 = (-b + Math.sqrt(b2ac)) / (2 * a);
if (0 < t1 && t1 < 1) {
if (i === 0) {
bbox.addPoint(f(t1), bbox.maxY);
} else if (i === 1) {
bbox.addPoint(bbox.maxX, f(t1));
}
}
let t2 = (-b - Math.sqrt(b2ac)) / (2 * a);
if (0 < t2 && t2 < 1) {
if (i === 0) {
bbox.addPoint(f(t2), bbox.maxY);
} else if (i === 1) {
bbox.addPoint(bbox.maxX, f(t2));
}
}
}
cx = p3x;
cy = p3y;
break;
}
}
return this._bbox = Object.freeze(bbox);
}
/**
* Applies a mapping function to each point in the path.
* @param {function} fn
* @return {Path}
*/
mapPoints(fn) {
let path = new Path;
for (let c of this.commands) {
let args = [];
for (let i = 0; i < c.args.length; i += 2) {
let [x, y] = fn(c.args[i], c.args[i + 1]);
args.push(x, y);
}
path[c.command](...args);
}
return path;
}
/**
* Transforms the path by the given matrix.
*/
transform(m0, m1, m2, m3, m4, m5) {
return this.mapPoints((x, y) => {
const tx = m0 * x + m2 * y + m4;
const ty = m1 * x + m3 * y + m5;
return [tx, ty];
});
}
/**
* Translates the path by the given offset.
*/
translate(x, y) {
return this.transform(1, 0, 0, 1, x, y);
}
/**
* Rotates the path by the given angle (in radians).
*/
rotate(angle) {
let cos = Math.cos(angle);
let sin = Math.sin(angle);
return this.transform(cos, sin, -sin, cos, 0, 0);
}
/**
* Scales the path.
*/
scale(scaleX, scaleY = scaleX) {
return this.transform(scaleX, 0, 0, scaleY, 0, 0);
}
}
for (let command of ['moveTo', 'lineTo', 'quadraticCurveTo', 'bezierCurveTo', 'closePath']) {
Path.prototype[command] = function(...args) {
this._bbox = this._cbox = null;
this.commands.push({
command,
args
});
return this;
};
}

View File

@@ -0,0 +1,54 @@
import TTFGlyph from './TTFGlyph';
import * as r from 'restructure';
let SBIXImage = new r.Struct({
originX: r.uint16,
originY: r.uint16,
type: new r.String(4),
data: new r.Buffer(t => t.parent.buflen - t._currentOffset)
});
/**
* Represents a color (e.g. emoji) glyph in Apple's SBIX format.
*/
export default class SBIXGlyph extends TTFGlyph {
type = 'SBIX';
/**
* Returns an object representing a glyph image at the given point size.
* The object has a data property with a Buffer containing the actual image data,
* along with the image type, and origin.
*
* @param {number} size
* @return {object}
*/
getImageForSize(size) {
for (let i = 0; i < this._font.sbix.imageTables.length; i++) {
var table = this._font.sbix.imageTables[i];
if (table.ppem >= size) { break; }
}
let offsets = table.imageOffsets;
let start = offsets[this.id];
let end = offsets[this.id + 1];
if (start === end) {
return null;
}
this._font.stream.pos = start;
return SBIXImage.decode(this._font.stream, {buflen: end - start});
}
render(ctx, size) {
let img = this.getImageForSize(size);
if (img != null) {
let scale = size / this._font.unitsPerEm;
ctx.image(img.data, {height: size, x: img.originX, y: (this.bbox.minY - img.originY) * scale});
}
if (this._font.sbix.flags.renderOutlines) {
super.render(ctx, size);
}
}
}

View File

@@ -0,0 +1,27 @@
export default [
'.notdef', '.null', 'nonmarkingreturn', 'space', 'exclam', 'quotedbl', 'numbersign', 'dollar', 'percent',
'ampersand', 'quotesingle', 'parenleft', 'parenright', 'asterisk', 'plus', 'comma', 'hyphen', 'period', 'slash',
'zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'colon', 'semicolon', 'less',
'equal', 'greater', 'question', 'at', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'bracketleft', 'backslash', 'bracketright',
'asciicircum', 'underscore', 'grave', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'braceleft', 'bar', 'braceright', 'asciitilde',
'Adieresis', 'Aring', 'Ccedilla', 'Eacute', 'Ntilde', 'Odieresis', 'Udieresis', 'aacute', 'agrave',
'acircumflex', 'adieresis', 'atilde', 'aring', 'ccedilla', 'eacute', 'egrave', 'ecircumflex', 'edieresis',
'iacute', 'igrave', 'icircumflex', 'idieresis', 'ntilde', 'oacute', 'ograve', 'ocircumflex', 'odieresis',
'otilde', 'uacute', 'ugrave', 'ucircumflex', 'udieresis', 'dagger', 'degree', 'cent', 'sterling', 'section',
'bullet', 'paragraph', 'germandbls', 'registered', 'copyright', 'trademark', 'acute', 'dieresis', 'notequal',
'AE', 'Oslash', 'infinity', 'plusminus', 'lessequal', 'greaterequal', 'yen', 'mu', 'partialdiff', 'summation',
'product', 'pi', 'integral', 'ordfeminine', 'ordmasculine', 'Omega', 'ae', 'oslash', 'questiondown',
'exclamdown', 'logicalnot', 'radical', 'florin', 'approxequal', 'Delta', 'guillemotleft', 'guillemotright',
'ellipsis', 'nonbreakingspace', 'Agrave', 'Atilde', 'Otilde', 'OE', 'oe', 'endash', 'emdash', 'quotedblleft',
'quotedblright', 'quoteleft', 'quoteright', 'divide', 'lozenge', 'ydieresis', 'Ydieresis', 'fraction',
'currency', 'guilsinglleft', 'guilsinglright', 'fi', 'fl', 'daggerdbl', 'periodcentered', 'quotesinglbase',
'quotedblbase', 'perthousand', 'Acircumflex', 'Ecircumflex', 'Aacute', 'Edieresis', 'Egrave', 'Iacute',
'Icircumflex', 'Idieresis', 'Igrave', 'Oacute', 'Ocircumflex', 'apple', 'Ograve', 'Uacute', 'Ucircumflex',
'Ugrave', 'dotlessi', 'circumflex', 'tilde', 'macron', 'breve', 'dotaccent', 'ring', 'cedilla', 'hungarumlaut',
'ogonek', 'caron', 'Lslash', 'lslash', 'Scaron', 'scaron', 'Zcaron', 'zcaron', 'brokenbar', 'Eth', 'eth',
'Yacute', 'yacute', 'Thorn', 'thorn', 'minus', 'multiply', 'onesuperior', 'twosuperior', 'threesuperior',
'onehalf', 'onequarter', 'threequarters', 'franc', 'Gbreve', 'gbreve', 'Idotaccent', 'Scedilla', 'scedilla',
'Cacute', 'cacute', 'Ccaron', 'ccaron', 'dcroat'
];

View File

@@ -0,0 +1,393 @@
import Glyph from './Glyph';
import Path from './Path';
import BBox from './BBox';
import * as r from 'restructure';
// The header for both simple and composite glyphs
let GlyfHeader = new r.Struct({
numberOfContours: r.int16, // if negative, this is a composite glyph
xMin: r.int16,
yMin: r.int16,
xMax: r.int16,
yMax: r.int16
});
// Flags for simple glyphs
const ON_CURVE = 1 << 0;
const X_SHORT_VECTOR = 1 << 1;
const Y_SHORT_VECTOR = 1 << 2;
const REPEAT = 1 << 3;
const SAME_X = 1 << 4;
const SAME_Y = 1 << 5;
// Flags for composite glyphs
const ARG_1_AND_2_ARE_WORDS = 1 << 0;
const ARGS_ARE_XY_VALUES = 1 << 1;
const ROUND_XY_TO_GRID = 1 << 2;
const WE_HAVE_A_SCALE = 1 << 3;
const MORE_COMPONENTS = 1 << 5;
const WE_HAVE_AN_X_AND_Y_SCALE = 1 << 6;
const WE_HAVE_A_TWO_BY_TWO = 1 << 7;
const WE_HAVE_INSTRUCTIONS = 1 << 8;
const USE_MY_METRICS = 1 << 9;
const OVERLAP_COMPOUND = 1 << 10;
const SCALED_COMPONENT_OFFSET = 1 << 11;
const UNSCALED_COMPONENT_OFFSET = 1 << 12;
// Represents a point in a simple glyph
export class Point {
constructor(onCurve, endContour, x = 0, y = 0) {
this.onCurve = onCurve;
this.endContour = endContour;
this.x = x;
this.y = y;
}
copy() {
return new Point(this.onCurve, this.endContour, this.x, this.y);
}
}
// Represents a component in a composite glyph
class Component {
constructor(glyphID, dx, dy) {
this.glyphID = glyphID;
this.dx = dx;
this.dy = dy;
this.pos = 0;
this.scaleX = this.scaleY = 1;
this.scale01 = this.scale10 = 0;
}
}
/**
* Represents a TrueType glyph.
*/
export default class TTFGlyph extends Glyph {
type = 'TTF';
// Parses just the glyph header and returns the bounding box
_getCBox(internal) {
// We need to decode the glyph if variation processing is requested,
// so it's easier just to recompute the path's cbox after decoding.
if (this._font._variationProcessor && !internal) {
return this.path.cbox;
}
let stream = this._font._getTableStream('glyf');
stream.pos += this._font.loca.offsets[this.id];
let glyph = GlyfHeader.decode(stream);
let cbox = new BBox(glyph.xMin, glyph.yMin, glyph.xMax, glyph.yMax);
return Object.freeze(cbox);
}
// Parses a single glyph coordinate
_parseGlyphCoord(stream, prev, short, same) {
if (short) {
var val = stream.readUInt8();
if (!same) {
val = -val;
}
val += prev;
} else {
if (same) {
var val = prev;
} else {
var val = prev + stream.readInt16BE();
}
}
return val;
}
// Decodes the glyph data into points for simple glyphs,
// or components for composite glyphs
_decode() {
let glyfPos = this._font.loca.offsets[this.id];
let nextPos = this._font.loca.offsets[this.id + 1];
// Nothing to do if there is no data for this glyph
if (glyfPos === nextPos) { return null; }
let stream = this._font._getTableStream('glyf');
stream.pos += glyfPos;
let startPos = stream.pos;
let glyph = GlyfHeader.decode(stream);
if (glyph.numberOfContours > 0) {
this._decodeSimple(glyph, stream);
} else if (glyph.numberOfContours < 0) {
this._decodeComposite(glyph, stream, startPos);
}
return glyph;
}
_decodeSimple(glyph, stream) {
// this is a simple glyph
glyph.points = [];
let endPtsOfContours = new r.Array(r.uint16, glyph.numberOfContours).decode(stream);
glyph.instructions = new r.Array(r.uint8, r.uint16).decode(stream);
let flags = [];
let numCoords = endPtsOfContours[endPtsOfContours.length - 1] + 1;
while (flags.length < numCoords) {
var flag = stream.readUInt8();
flags.push(flag);
// check for repeat flag
if (flag & REPEAT) {
let count = stream.readUInt8();
for (let j = 0; j < count; j++) {
flags.push(flag);
}
}
}
for (var i = 0; i < flags.length; i++) {
var flag = flags[i];
let point = new Point(!!(flag & ON_CURVE), endPtsOfContours.indexOf(i) >= 0, 0, 0);
glyph.points.push(point);
}
let px = 0;
for (var i = 0; i < flags.length; i++) {
var flag = flags[i];
glyph.points[i].x = px = this._parseGlyphCoord(stream, px, flag & X_SHORT_VECTOR, flag & SAME_X);
}
let py = 0;
for (var i = 0; i < flags.length; i++) {
var flag = flags[i];
glyph.points[i].y = py = this._parseGlyphCoord(stream, py, flag & Y_SHORT_VECTOR, flag & SAME_Y);
}
if (this._font._variationProcessor) {
let points = glyph.points.slice();
points.push(...this._getPhantomPoints(glyph));
this._font._variationProcessor.transformPoints(this.id, points);
glyph.phantomPoints = points.slice(-4);
}
return;
}
_decodeComposite(glyph, stream, offset = 0) {
// this is a composite glyph
glyph.components = [];
let haveInstructions = false;
let flags = MORE_COMPONENTS;
while (flags & MORE_COMPONENTS) {
flags = stream.readUInt16BE();
let gPos = stream.pos - offset;
let glyphID = stream.readUInt16BE();
if (!haveInstructions) {
haveInstructions = (flags & WE_HAVE_INSTRUCTIONS) !== 0;
}
if (flags & ARG_1_AND_2_ARE_WORDS) {
var dx = stream.readInt16BE();
var dy = stream.readInt16BE();
} else {
var dx = stream.readInt8();
var dy = stream.readInt8();
}
var component = new Component(glyphID, dx, dy);
component.pos = gPos;
if (flags & WE_HAVE_A_SCALE) {
// fixed number with 14 bits of fraction
component.scaleX =
component.scaleY = ((stream.readUInt8() << 24) | (stream.readUInt8() << 16)) / 1073741824;
} else if (flags & WE_HAVE_AN_X_AND_Y_SCALE) {
component.scaleX = ((stream.readUInt8() << 24) | (stream.readUInt8() << 16)) / 1073741824;
component.scaleY = ((stream.readUInt8() << 24) | (stream.readUInt8() << 16)) / 1073741824;
} else if (flags & WE_HAVE_A_TWO_BY_TWO) {
component.scaleX = ((stream.readUInt8() << 24) | (stream.readUInt8() << 16)) / 1073741824;
component.scale01 = ((stream.readUInt8() << 24) | (stream.readUInt8() << 16)) / 1073741824;
component.scale10 = ((stream.readUInt8() << 24) | (stream.readUInt8() << 16)) / 1073741824;
component.scaleY = ((stream.readUInt8() << 24) | (stream.readUInt8() << 16)) / 1073741824;
}
glyph.components.push(component);
}
if (this._font._variationProcessor) {
let points = [];
for (let j = 0; j < glyph.components.length; j++) {
var component = glyph.components[j];
points.push(new Point(true, true, component.dx, component.dy));
}
points.push(...this._getPhantomPoints(glyph));
this._font._variationProcessor.transformPoints(this.id, points);
glyph.phantomPoints = points.splice(-4, 4);
for (let i = 0; i < points.length; i++) {
let point = points[i];
glyph.components[i].dx = point.x;
glyph.components[i].dy = point.y;
}
}
return haveInstructions;
}
_getPhantomPoints(glyph) {
let cbox = this._getCBox(true);
if (this._metrics == null) {
this._metrics = Glyph.prototype._getMetrics.call(this, cbox);
}
let { advanceWidth, advanceHeight, leftBearing, topBearing } = this._metrics;
return [
new Point(false, true, glyph.xMin - leftBearing, 0),
new Point(false, true, glyph.xMin - leftBearing + advanceWidth, 0),
new Point(false, true, 0, glyph.yMax + topBearing),
new Point(false, true, 0, glyph.yMax + topBearing + advanceHeight)
];
}
// Decodes font data, resolves composite glyphs, and returns an array of contours
_getContours() {
let glyph = this._decode();
if (!glyph) {
return [];
}
let points = [];
if (glyph.numberOfContours < 0) {
// resolve composite glyphs
for (let component of glyph.components) {
let contours = this._font.getGlyph(component.glyphID)._getContours();
for (let i = 0; i < contours.length; i++) {
let contour = contours[i];
for (let j = 0; j < contour.length; j++) {
let point = contour[j];
let x = point.x * component.scaleX + point.y * component.scale01 + component.dx;
let y = point.y * component.scaleY + point.x * component.scale10 + component.dy;
points.push(new Point(point.onCurve, point.endContour, x, y));
}
}
}
} else {
points = glyph.points || [];
}
// Recompute and cache metrics if we performed variation processing, and don't have an HVAR table
if (glyph.phantomPoints && !this._font.directory.tables.HVAR) {
this._metrics.advanceWidth = glyph.phantomPoints[1].x - glyph.phantomPoints[0].x;
this._metrics.advanceHeight = glyph.phantomPoints[3].y - glyph.phantomPoints[2].y;
this._metrics.leftBearing = glyph.xMin - glyph.phantomPoints[0].x;
this._metrics.topBearing = glyph.phantomPoints[2].y - glyph.yMax;
}
let contours = [];
let cur = [];
for (let k = 0; k < points.length; k++) {
var point = points[k];
cur.push(point);
if (point.endContour) {
contours.push(cur);
cur = [];
}
}
return contours;
}
_getMetrics() {
if (this._metrics) {
return this._metrics;
}
let cbox = this._getCBox(true);
super._getMetrics(cbox);
if (this._font._variationProcessor && !this._font.HVAR) {
// No HVAR table, decode the glyph. This triggers recomputation of metrics.
this.path;
}
return this._metrics;
}
// Converts contours to a Path object that can be rendered
_getPath() {
let contours = this._getContours();
let path = new Path;
for (let i = 0; i < contours.length; i++) {
let contour = contours[i];
let firstPt = contour[0];
let lastPt = contour[contour.length - 1];
let start = 0;
if (firstPt.onCurve) {
// The first point will be consumed by the moveTo command, so skip in the loop
var curvePt = null;
start = 1;
} else {
if (lastPt.onCurve) {
// Start at the last point if the first point is off curve and the last point is on curve
firstPt = lastPt;
} else {
// Start at the middle if both the first and last points are off curve
firstPt = new Point(false, false, (firstPt.x + lastPt.x) / 2, (firstPt.y + lastPt.y) / 2);
}
var curvePt = firstPt;
}
path.moveTo(firstPt.x, firstPt.y);
for (let j = start; j < contour.length; j++) {
let pt = contour[j];
let prevPt = j === 0 ? firstPt : contour[j - 1];
if (prevPt.onCurve && pt.onCurve) {
path.lineTo(pt.x, pt.y);
} else if (prevPt.onCurve && !pt.onCurve) {
var curvePt = pt;
} else if (!prevPt.onCurve && !pt.onCurve) {
let midX = (prevPt.x + pt.x) / 2;
let midY = (prevPt.y + pt.y) / 2;
path.quadraticCurveTo(prevPt.x, prevPt.y, midX, midY);
var curvePt = pt;
} else if (!prevPt.onCurve && pt.onCurve) {
path.quadraticCurveTo(curvePt.x, curvePt.y, pt.x, pt.y);
var curvePt = null;
} else {
throw new Error("Unknown TTF path state");
}
}
// Connect the first and last points
if (curvePt) {
path.quadraticCurveTo(curvePt.x, curvePt.y, firstPt.x, firstPt.y);
}
path.closePath();
}
return path;
}
}

View File

@@ -0,0 +1,158 @@
import * as r from 'restructure';
// Flags for simple glyphs
const ON_CURVE = 1 << 0;
const X_SHORT_VECTOR = 1 << 1;
const Y_SHORT_VECTOR = 1 << 2;
const REPEAT = 1 << 3;
const SAME_X = 1 << 4;
const SAME_Y = 1 << 5;
class Point {
static size(val) {
return val >= 0 && val <= 255 ? 1 : 2;
}
static encode(stream, value) {
if (value >= 0 && value <= 255) {
stream.writeUInt8(value);
} else {
stream.writeInt16BE(value);
}
}
}
let Glyf = new r.Struct({
numberOfContours: r.int16, // if negative, this is a composite glyph
xMin: r.int16,
yMin: r.int16,
xMax: r.int16,
yMax: r.int16,
endPtsOfContours: new r.Array(r.uint16, 'numberOfContours'),
instructions: new r.Array(r.uint8, r.uint16),
flags: new r.Array(r.uint8, 0),
xPoints: new r.Array(Point, 0),
yPoints: new r.Array(Point, 0)
});
/**
* Encodes TrueType glyph outlines
*/
export default class TTFGlyphEncoder {
encodeSimple(path, instructions = []) {
let endPtsOfContours = [];
let xPoints = [];
let yPoints = [];
let flags = [];
let same = 0;
let lastX = 0, lastY = 0, lastFlag = 0;
let pointCount = 0;
for (let i = 0; i < path.commands.length; i++) {
let c = path.commands[i];
for (let j = 0; j < c.args.length; j += 2) {
let x = c.args[j];
let y = c.args[j + 1];
let flag = 0;
// If the ending point of a quadratic curve is the midpoint
// between the control point and the control point of the next
// quadratic curve, we can omit the ending point.
if (c.command === 'quadraticCurveTo' && j === 2) {
let next = path.commands[i + 1];
if (next && next.command === 'quadraticCurveTo') {
let midX = (lastX + next.args[0]) / 2;
let midY = (lastY + next.args[1]) / 2;
if (x === midX && y === midY) {
continue;
}
}
}
// All points except control points are on curve.
if (!(c.command === 'quadraticCurveTo' && j === 0)) {
flag |= ON_CURVE;
}
flag = this._encodePoint(x, lastX, xPoints, flag, X_SHORT_VECTOR, SAME_X);
flag = this._encodePoint(y, lastY, yPoints, flag, Y_SHORT_VECTOR, SAME_Y);
if (flag === lastFlag && same < 255) {
flags[flags.length - 1] |= REPEAT;
same++;
} else {
if (same > 0) {
flags.push(same);
same = 0;
}
flags.push(flag);
lastFlag = flag;
}
lastX = x;
lastY = y;
pointCount++;
}
if (c.command === 'closePath') {
endPtsOfContours.push(pointCount - 1);
}
}
// Close the path if the last command didn't already
if (path.commands.length > 1 && path.commands[path.commands.length - 1].command !== 'closePath') {
endPtsOfContours.push(pointCount - 1);
}
let bbox = path.bbox;
let glyf = {
numberOfContours: endPtsOfContours.length,
xMin: bbox.minX,
yMin: bbox.minY,
xMax: bbox.maxX,
yMax: bbox.maxY,
endPtsOfContours: endPtsOfContours,
instructions: instructions,
flags: flags,
xPoints: xPoints,
yPoints: yPoints
};
let size = Glyf.size(glyf);
let tail = 4 - (size % 4);
let stream = new r.EncodeStream(size + tail);
Glyf.encode(stream, glyf);
// Align to 4-byte length
if (tail !== 0) {
stream.fill(0, tail);
}
return stream.buffer;
}
_encodePoint(value, last, points, flag, shortFlag, sameFlag) {
let diff = value - last;
if (value === last) {
flag |= sameFlag;
} else {
if (-255 <= diff && diff <= 255) {
flag |= shortFlag;
if (diff < 0) {
diff = -diff;
} else {
flag |= sameFlag;
}
}
points.push(diff);
}
return flag;
}
}

View File

@@ -0,0 +1,17 @@
import TTFGlyph from './TTFGlyph';
/**
* Represents a TrueType glyph in the WOFF2 format, which compresses glyphs differently.
*/
export default class WOFF2Glyph extends TTFGlyph {
type = 'WOFF2';
_decode() {
// We have to decode in advance (in WOFF2Font), so just return the pre-decoded data.
return this._font._transformedGlyphs[this.id];
}
_getCBox() {
return this.path.bbox;
}
}