|
|
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 += Math.round(xDeltas[i] * factor);
|
|
|
point.y += Math.round(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 += deltaX;
|
|
|
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;
|
|
|
}
|
|
|
}
|
|
|
|