|
|
import Glyph from './Glyph';
|
|
|
import Path from './Path';
|
|
|
import BBox from './BBox';
|
|
|
import 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 {
|
|
|
// 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;
|
|
|
}
|
|
|
}
|
|
|
|