350 lines
12 KiB
Plaintext
350 lines
12 KiB
Plaintext
// The `glyf` table describes the glyphs in TrueType outline format.
|
|
// http://www.microsoft.com/typography/otspec/glyf.htm
|
|
|
|
import check from '../check';
|
|
import glyphset from '../glyphset';
|
|
import parse from '../parse';
|
|
import Path from '../path';
|
|
|
|
// Parse the coordinate data for a glyph.
|
|
function parseGlyphCoordinate(p, flag, previousValue, shortVectorBitMask, sameBitMask) {
|
|
let v;
|
|
if ((flag & shortVectorBitMask) > 0) {
|
|
// The coordinate is 1 byte long.
|
|
v = p.parseByte();
|
|
// The `same` bit is re-used for short values to signify the sign of the value.
|
|
if ((flag & sameBitMask) === 0) {
|
|
v = -v;
|
|
}
|
|
|
|
v = previousValue + v;
|
|
} else {
|
|
// The coordinate is 2 bytes long.
|
|
// If the `same` bit is set, the coordinate is the same as the previous coordinate.
|
|
if ((flag & sameBitMask) > 0) {
|
|
v = previousValue;
|
|
} else {
|
|
// Parse the coordinate as a signed 16-bit delta value.
|
|
v = previousValue + p.parseShort();
|
|
}
|
|
}
|
|
|
|
return v;
|
|
}
|
|
|
|
// Parse a TrueType glyph.
|
|
function parseGlyph(glyph, data, start) {
|
|
const p = new parse.Parser(data, start);
|
|
glyph.numberOfContours = p.parseShort();
|
|
glyph._xMin = p.parseShort();
|
|
glyph._yMin = p.parseShort();
|
|
glyph._xMax = p.parseShort();
|
|
glyph._yMax = p.parseShort();
|
|
let flags;
|
|
let flag;
|
|
|
|
if (glyph.numberOfContours > 0) {
|
|
// This glyph is not a composite.
|
|
const endPointIndices = glyph.endPointIndices = [];
|
|
for (let i = 0; i < glyph.numberOfContours; i += 1) {
|
|
endPointIndices.push(p.parseUShort());
|
|
}
|
|
|
|
glyph.instructionLength = p.parseUShort();
|
|
glyph.instructions = [];
|
|
for (let i = 0; i < glyph.instructionLength; i += 1) {
|
|
glyph.instructions.push(p.parseByte());
|
|
}
|
|
|
|
const numberOfCoordinates = endPointIndices[endPointIndices.length - 1] + 1;
|
|
flags = [];
|
|
for (let i = 0; i < numberOfCoordinates; i += 1) {
|
|
flag = p.parseByte();
|
|
flags.push(flag);
|
|
// If bit 3 is set, we repeat this flag n times, where n is the next byte.
|
|
if ((flag & 8) > 0) {
|
|
const repeatCount = p.parseByte();
|
|
for (let j = 0; j < repeatCount; j += 1) {
|
|
flags.push(flag);
|
|
i += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
check.argument(flags.length === numberOfCoordinates, 'Bad flags.');
|
|
|
|
if (endPointIndices.length > 0) {
|
|
const points = [];
|
|
let point;
|
|
// X/Y coordinates are relative to the previous point, except for the first point which is relative to 0,0.
|
|
if (numberOfCoordinates > 0) {
|
|
for (let i = 0; i < numberOfCoordinates; i += 1) {
|
|
flag = flags[i];
|
|
point = {};
|
|
point.onCurve = !!(flag & 1);
|
|
point.lastPointOfContour = endPointIndices.indexOf(i) >= 0;
|
|
points.push(point);
|
|
}
|
|
|
|
let px = 0;
|
|
for (let i = 0; i < numberOfCoordinates; i += 1) {
|
|
flag = flags[i];
|
|
point = points[i];
|
|
point.x = parseGlyphCoordinate(p, flag, px, 2, 16);
|
|
px = point.x;
|
|
}
|
|
|
|
let py = 0;
|
|
for (let i = 0; i < numberOfCoordinates; i += 1) {
|
|
flag = flags[i];
|
|
point = points[i];
|
|
point.y = parseGlyphCoordinate(p, flag, py, 4, 32);
|
|
py = point.y;
|
|
}
|
|
}
|
|
|
|
glyph.points = points;
|
|
} else {
|
|
glyph.points = [];
|
|
}
|
|
} else if (glyph.numberOfContours === 0) {
|
|
glyph.points = [];
|
|
} else {
|
|
glyph.isComposite = true;
|
|
glyph.points = [];
|
|
glyph.components = [];
|
|
let moreComponents = true;
|
|
while (moreComponents) {
|
|
flags = p.parseUShort();
|
|
const component = {
|
|
glyphIndex: p.parseUShort(),
|
|
xScale: 1,
|
|
scale01: 0,
|
|
scale10: 0,
|
|
yScale: 1,
|
|
dx: 0,
|
|
dy: 0
|
|
};
|
|
if ((flags & 1) > 0) {
|
|
// The arguments are words
|
|
if ((flags & 2) > 0) {
|
|
// values are offset
|
|
component.dx = p.parseShort();
|
|
component.dy = p.parseShort();
|
|
} else {
|
|
// values are matched points
|
|
component.matchedPoints = [p.parseUShort(), p.parseUShort()];
|
|
}
|
|
|
|
} else {
|
|
// The arguments are bytes
|
|
if ((flags & 2) > 0) {
|
|
// values are offset
|
|
component.dx = p.parseChar();
|
|
component.dy = p.parseChar();
|
|
} else {
|
|
// values are matched points
|
|
component.matchedPoints = [p.parseByte(), p.parseByte()];
|
|
}
|
|
}
|
|
|
|
if ((flags & 8) > 0) {
|
|
// We have a scale
|
|
component.xScale = component.yScale = p.parseF2Dot14();
|
|
} else if ((flags & 64) > 0) {
|
|
// We have an X / Y scale
|
|
component.xScale = p.parseF2Dot14();
|
|
component.yScale = p.parseF2Dot14();
|
|
} else if ((flags & 128) > 0) {
|
|
// We have a 2x2 transformation
|
|
component.xScale = p.parseF2Dot14();
|
|
component.scale01 = p.parseF2Dot14();
|
|
component.scale10 = p.parseF2Dot14();
|
|
component.yScale = p.parseF2Dot14();
|
|
}
|
|
|
|
glyph.components.push(component);
|
|
moreComponents = !!(flags & 32);
|
|
}
|
|
if (flags & 0x100) {
|
|
// We have instructions
|
|
glyph.instructionLength = p.parseUShort();
|
|
glyph.instructions = [];
|
|
for (let i = 0; i < glyph.instructionLength; i += 1) {
|
|
glyph.instructions.push(p.parseByte());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Transform an array of points and return a new array.
|
|
function transformPoints(points, transform) {
|
|
const newPoints = [];
|
|
for (let i = 0; i < points.length; i += 1) {
|
|
const pt = points[i];
|
|
const newPt = {
|
|
x: transform.xScale * pt.x + transform.scale01 * pt.y + transform.dx,
|
|
y: transform.scale10 * pt.x + transform.yScale * pt.y + transform.dy,
|
|
onCurve: pt.onCurve,
|
|
lastPointOfContour: pt.lastPointOfContour
|
|
};
|
|
newPoints.push(newPt);
|
|
}
|
|
|
|
return newPoints;
|
|
}
|
|
|
|
function getContours(points) {
|
|
const contours = [];
|
|
let currentContour = [];
|
|
for (let i = 0; i < points.length; i += 1) {
|
|
const pt = points[i];
|
|
currentContour.push(pt);
|
|
if (pt.lastPointOfContour) {
|
|
contours.push(currentContour);
|
|
currentContour = [];
|
|
}
|
|
}
|
|
|
|
check.argument(currentContour.length === 0, 'There are still points left in the current contour.');
|
|
return contours;
|
|
}
|
|
|
|
// Convert the TrueType glyph outline to a Path.
|
|
function getPath(points) {
|
|
const p = new Path();
|
|
if (!points) {
|
|
return p;
|
|
}
|
|
|
|
const contours = getContours(points);
|
|
|
|
for (let contourIndex = 0; contourIndex < contours.length; ++contourIndex) {
|
|
const contour = contours[contourIndex];
|
|
|
|
let prev = null;
|
|
let curr = contour[contour.length - 1];
|
|
let next = contour[0];
|
|
|
|
if (curr.onCurve) {
|
|
p.moveTo(curr.x, curr.y);
|
|
} else {
|
|
if (next.onCurve) {
|
|
p.moveTo(next.x, next.y);
|
|
} else {
|
|
// If both first and last points are off-curve, start at their middle.
|
|
const start = {x: (curr.x + next.x) * 0.5, y: (curr.y + next.y) * 0.5};
|
|
p.moveTo(start.x, start.y);
|
|
}
|
|
}
|
|
|
|
for (let i = 0; i < contour.length; ++i) {
|
|
prev = curr;
|
|
curr = next;
|
|
next = contour[(i + 1) % contour.length];
|
|
|
|
if (curr.onCurve) {
|
|
// This is a straight line.
|
|
p.lineTo(curr.x, curr.y);
|
|
} else {
|
|
let prev2 = prev;
|
|
let next2 = next;
|
|
|
|
if (!prev.onCurve) {
|
|
prev2 = { x: (curr.x + prev.x) * 0.5, y: (curr.y + prev.y) * 0.5 };
|
|
}
|
|
|
|
if (!next.onCurve) {
|
|
next2 = { x: (curr.x + next.x) * 0.5, y: (curr.y + next.y) * 0.5 };
|
|
}
|
|
|
|
p.quadraticCurveTo(curr.x, curr.y, next2.x, next2.y);
|
|
}
|
|
}
|
|
|
|
p.closePath();
|
|
}
|
|
return p;
|
|
}
|
|
|
|
function buildPath(glyphs, glyph) {
|
|
if (glyph.isComposite) {
|
|
for (let j = 0; j < glyph.components.length; j += 1) {
|
|
const component = glyph.components[j];
|
|
const componentGlyph = glyphs.get(component.glyphIndex);
|
|
// Force the ttfGlyphLoader to parse the glyph.
|
|
componentGlyph.getPath();
|
|
if (componentGlyph.points) {
|
|
let transformedPoints;
|
|
if (component.matchedPoints === undefined) {
|
|
// component positioned by offset
|
|
transformedPoints = transformPoints(componentGlyph.points, component);
|
|
} else {
|
|
// component positioned by matched points
|
|
if ((component.matchedPoints[0] > glyph.points.length - 1) ||
|
|
(component.matchedPoints[1] > componentGlyph.points.length - 1)) {
|
|
throw Error('Matched points out of range in ' + glyph.name);
|
|
}
|
|
const firstPt = glyph.points[component.matchedPoints[0]];
|
|
let secondPt = componentGlyph.points[component.matchedPoints[1]];
|
|
const transform = {
|
|
xScale: component.xScale, scale01: component.scale01,
|
|
scale10: component.scale10, yScale: component.yScale,
|
|
dx: 0, dy: 0
|
|
};
|
|
secondPt = transformPoints([secondPt], transform)[0];
|
|
transform.dx = firstPt.x - secondPt.x;
|
|
transform.dy = firstPt.y - secondPt.y;
|
|
transformedPoints = transformPoints(componentGlyph.points, transform);
|
|
}
|
|
glyph.points = glyph.points.concat(transformedPoints);
|
|
}
|
|
}
|
|
}
|
|
|
|
return getPath(glyph.points);
|
|
}
|
|
|
|
function parseGlyfTableAll(data, start, loca, font) {
|
|
const glyphs = new glyphset.GlyphSet(font);
|
|
|
|
// The last element of the loca table is invalid.
|
|
for (let i = 0; i < loca.length - 1; i += 1) {
|
|
const offset = loca[i];
|
|
const nextOffset = loca[i + 1];
|
|
if (offset !== nextOffset) {
|
|
glyphs.push(i, glyphset.ttfGlyphLoader(font, i, parseGlyph, data, start + offset, buildPath));
|
|
} else {
|
|
glyphs.push(i, glyphset.glyphLoader(font, i));
|
|
}
|
|
}
|
|
|
|
return glyphs;
|
|
}
|
|
|
|
function parseGlyfTableOnLowMemory(data, start, loca, font) {
|
|
const glyphs = new glyphset.GlyphSet(font);
|
|
|
|
font._push = function(i) {
|
|
const offset = loca[i];
|
|
const nextOffset = loca[i + 1];
|
|
if (offset !== nextOffset) {
|
|
glyphs.push(i, glyphset.ttfGlyphLoader(font, i, parseGlyph, data, start + offset, buildPath));
|
|
} else {
|
|
glyphs.push(i, glyphset.glyphLoader(font, i));
|
|
}
|
|
};
|
|
|
|
return glyphs;
|
|
}
|
|
|
|
// Parse all the glyphs according to the offsets from the `loca` table.
|
|
function parseGlyfTable(data, start, loca, font, opt) {
|
|
if (opt.lowMemory)
|
|
return parseGlyfTableOnLowMemory(data, start, loca, font);
|
|
else
|
|
return parseGlyfTableAll(data, start, loca, font);
|
|
}
|
|
|
|
export default { getPath, parse: parseGlyfTable};
|