Skip to content

Commit

Permalink
Rewrite GPOS parsing (#323)
Browse files Browse the repository at this point in the history
* Rewrite GPOS parsing
* let is the new var
  • Loading branch information
fpirsch authored and Jolg42 committed Mar 5, 2018
1 parent d186ba9 commit f570fe9
Show file tree
Hide file tree
Showing 9 changed files with 535 additions and 231 deletions.
17 changes: 13 additions & 4 deletions src/font.js
Expand Up @@ -4,6 +4,7 @@ import Path from './path';
import sfnt from './tables/sfnt';
import { DefaultEncoding } from './encoding';
import glyphset from './glyphset';
import Position from './position';
import Substitution from './substitution';
import { isBrowser, checkArgument, arrayBufferToNodeBuffer } from './util';
import HintingTrueType from './hintingtt';
Expand Down Expand Up @@ -87,6 +88,7 @@ function Font(options) {
this.supported = true; // Deprecated: parseBuffer will throw an error if font is not supported.
this.glyphs = new glyphset.GlyphSet(this, options.glyphs || []);
this.encoding = new DefaultEncoding(this);
this.position = new Position(this);
this.substitution = new Substitution(this);
this.tables = this.tables || {};

Expand Down Expand Up @@ -234,9 +236,7 @@ Font.prototype.glyphIndexToName = function(gid) {
Font.prototype.getKerningValue = function(leftGlyph, rightGlyph) {
leftGlyph = leftGlyph.index || leftGlyph;
rightGlyph = rightGlyph.index || rightGlyph;
const gposKerning = this.getGposKerningValue;
return gposKerning ? gposKerning(leftGlyph, rightGlyph) :
(this.kerningPairs[leftGlyph + ',' + rightGlyph] || 0);
return this.kerningPairs[leftGlyph + ',' + rightGlyph] || 0;
};

/**
Expand Down Expand Up @@ -275,6 +275,11 @@ Font.prototype.forEachGlyph = function(text, x, y, fontSize, options, callback)
options = options || this.defaultRenderOptions;
const fontScale = 1 / this.unitsPerEm * fontSize;
const glyphs = this.stringToGlyphs(text, options);
let kerningLookups;
if (options.kerning) {
const script = options.script || this.position.getDefaultScriptName();
kerningLookups = this.position.getKerningTables(script, options.language);
}
for (let i = 0; i < glyphs.length; i += 1) {
const glyph = glyphs[i];
callback.call(this, glyph, x, y, fontSize, options);
Expand All @@ -283,7 +288,11 @@ Font.prototype.forEachGlyph = function(text, x, y, fontSize, options, callback)
}

if (options.kerning && i < glyphs.length - 1) {
const kerningValue = this.getKerningValue(glyph, glyphs[i + 1]);
// We should apply position adjustment lookups in a more generic way.
// Here we only use the xAdvance value.
const kerningValue = kerningLookups ?
this.position.getKerningValue(kerningLookups, glyph.index, glyphs[i + 1].index) :
this.getKerningValue(glyph, glyphs[i + 1]);
x += kerningValue * fontScale;
}

Expand Down
63 changes: 62 additions & 1 deletion src/layout.js
Expand Up @@ -37,6 +37,29 @@ function binSearch(arr, value) {
return -imin - 1;
}

// binary search in a list of ranges (coverage, class definition)
function searchRange(ranges, value) {
// jshint bitwise: false
let range;
let imin = 0;
let imax = ranges.length - 1;
while (imin <= imax) {
const imid = (imin + imax) >>> 1;
range = ranges[imid];
const start = range.start;
if (start === value) {
return range;
} else if (start < value) {
imin = imid + 1;
} else { imax = imid - 1; }
}
if (imin > 0) {
range = ranges[imin - 1];
if (value > range.end) return 0;
return range;
}
}

/**
* @exports opentype.Layout
* @class
Expand Down Expand Up @@ -215,7 +238,7 @@ Layout.prototype = {
* @param {string} [script='DFLT']
* @param {string} [language='dlft']
* @param {string} feature - 4-letter feature code
* @param {number} lookupType - 1 to 8
* @param {number} lookupType - 1 to 9
* @param {boolean} create - forces the creation of the lookup table if it doesn't exist, with no subtables.
* @return {Object[]}
*/
Expand Down Expand Up @@ -249,6 +272,44 @@ Layout.prototype = {
return tables;
},

/**
* Find a glyph in a class definition table
* https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#class-definition-table
* @param {object} classDefTable - an OpenType Layout class definition table
* @param {number} glyphIndex - the index of the glyph to find
* @returns {number} -1 if not found
*/
getGlyphClass: function(classDefTable, glyphIndex) {
switch (classDefTable.format) {
case 1:
if (classDefTable.startGlyph <= glyphIndex && glyphIndex < classDefTable.startGlyph + classDefTable.classes.length) {
return classDefTable.classes[glyphIndex - classDefTable.startGlyph];
}
return 0;
case 2:
const range = searchRange(classDefTable.ranges, glyphIndex);
return range ? range.classId : 0;
}
},

/**
* Find a glyph in a coverage table
* https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#coverage-table
* @param {object} coverageTable - an OpenType Layout coverage table
* @param {number} glyphIndex - the index of the glyph to find
* @returns {number} -1 if not found
*/
getCoverageIndex: function(coverageTable, glyphIndex) {
switch (coverageTable.format) {
case 1:
const index = binSearch(coverageTable.glyphs, glyphIndex);
return index >= 0 ? index : -1;
case 2:
const range = searchRange(coverageTable.ranges, glyphIndex);
return range ? range.index + glyphIndex - range.start : -1;
}
},

/**
* Returns the list of glyph indexes of a coverage table.
* Format 1: the list is stored raw
Expand Down
2 changes: 1 addition & 1 deletion src/opentype.js
Expand Up @@ -334,7 +334,7 @@ function parseBuffer(buffer) {

if (gposTableEntry) {
const gposTable = uncompressTable(data, gposTableEntry);
gpos.parse(gposTable.data, gposTable.offset, font);
font.tables.gpos = gpos.parse(gposTable.data, gposTable.offset);
}

if (gsubTableEntry) {
Expand Down
48 changes: 46 additions & 2 deletions src/parse.js
Expand Up @@ -345,6 +345,50 @@ Parser.prototype.parseStruct = function(description) {
}
};

/**
* Parse a GPOS valueRecord
* https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#value-record
* valueFormat is optional, if omitted it is read from the stream.
*/
Parser.prototype.parseValueRecord = function(valueFormat) {
if (valueFormat === undefined) {
valueFormat = this.parseUShort();
}
if (valueFormat === 0) { // valueFormat2 in kerning pairs is most often 0
return; // in this case return undefined instead of an empty object, to save space
}
const valueRecord = {};

if (valueFormat & 0x0001) { valueRecord.xPlacement = this.parseShort(); }
if (valueFormat & 0x0002) { valueRecord.yPlacement = this.parseShort(); }
if (valueFormat & 0x0004) { valueRecord.xAdvance = this.parseShort(); }
if (valueFormat & 0x0008) { valueRecord.yAdvance = this.parseShort(); }

// Device table (non-variable font) / VariationIndex table (variable font) not supported
// https://docs.microsoft.com/fr-fr/typography/opentype/spec/chapter2#devVarIdxTbls
if (valueFormat & 0x0010) { valueRecord.xPlaDevice = undefined; this.parseShort(); }
if (valueFormat & 0x0020) { valueRecord.yPlaDevice = undefined; this.parseShort(); }
if (valueFormat & 0x0040) { valueRecord.xAdvDevice = undefined; this.parseShort(); }
if (valueFormat & 0x0080) { valueRecord.yAdvDevice = undefined; this.parseShort(); }

return valueRecord;
};

/**
* Parse a list of GPOS valueRecords
* https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#value-record
* valueFormat and valueCount are read from the stream.
*/
Parser.prototype.parseValueRecordList = function() {
const valueFormat = this.parseUShort();
const valueCount = this.parseUShort();
const values = new Array(valueCount);
for (let i = 0; i < valueCount; i++) {
values[i] = this.parseValueRecord(valueFormat);
}
return values;
};

Parser.prototype.parsePointer = function(description) {
const structOffset = this.parseOffset16();
if (structOffset > 0) { // NULL offset => return undefined
Expand Down Expand Up @@ -535,7 +579,7 @@ Parser.prototype.parseFeatureList = function() {
Parser.prototype.parseLookupList = function(lookupTableParsers) {
return this.parsePointer(Parser.list(Parser.pointer(function() {
const lookupType = this.parseUShort();
check.argument(1 <= lookupType && lookupType <= 8, 'GSUB lookup type ' + lookupType + ' unknown.');
check.argument(1 <= lookupType && lookupType <= 9, 'GPOS/GSUB lookup type ' + lookupType + ' unknown.');
const lookupFlag = this.parseUShort();
const useMarkFilteringSet = lookupFlag & 0x10;
return {
Expand All @@ -551,7 +595,7 @@ Parser.prototype.parseFeatureVariationsList = function() {
return this.parsePointer32(function() {
const majorVersion = this.parseUShort();
const minorVersion = this.parseUShort();
check.argument(majorVersion === 1 && minorVersion < 1, 'GSUB feature variations table unknown.');
check.argument(majorVersion === 1 && minorVersion < 1, 'GPOS/GSUB feature variations table unknown.');
const featureVariations = this.parseRecordList32({
conditionSetOffset: Parser.offset32,
featureTableSubstitutionOffset: Parser.offset32
Expand Down
69 changes: 69 additions & 0 deletions src/position.js
@@ -0,0 +1,69 @@
// The Position object provides utility methods to manipulate
// the GPOS position table.

import Layout from './layout';

/**
* @exports opentype.Position
* @class
* @extends opentype.Layout
* @param {opentype.Font}
* @constructor
*/
function Position(font) {
Layout.call(this, font, 'gpos');
}

Position.prototype = Layout.prototype;

/**
* Find a glyph pair in a list of lookup tables of type 2 and retrieve the xAdvance kerning value.
*
* @param {integer} leftIndex - left glyph index
* @param {integer} rightIndex - right glyph index
* @returns {integer}
*/
Position.prototype.getKerningValue = function(kerningLookups, leftIndex, rightIndex) {
for (let i = 0; i < kerningLookups.length; i++) {
const subtables = kerningLookups[i].subtables;
for (let j = 0; j < subtables.length; j++) {
const subtable = subtables[j];
const covIndex = this.getCoverageIndex(subtable.coverage, leftIndex);
if (covIndex < 0) continue;
switch (subtable.posFormat) {
case 1:
// Search Pair Adjustment Positioning Format 1
let pairSet = subtable.pairSets[covIndex];
for (let k = 0; k < pairSet.length; k++) {
let pair = pairSet[k];
if (pair.secondGlyph === rightIndex) {
return pair.value1 && pair.value1.xAdvance || 0;
}
}
break; // left glyph found, not right glyph - try next subtable
case 2:
// Search Pair Adjustment Positioning Format 2
const class1 = this.getGlyphClass(subtable.classDef1, leftIndex);
const class2 = this.getGlyphClass(subtable.classDef2, rightIndex);
const pair = subtable.classRecords[class1][class2];
return pair.value1 && pair.value1.xAdvance || 0;
}
}
}
return 0;
};

/**
* List all kerning lookup tables.
*
* @param {string} [script='DFLT'] - use font.position.getDefaultScriptName() for a better default value
* @param {string} [language='dflt']
* @return {object[]} The list of kerning lookup tables (may be empty), or undefined if there is no GPOS table (and we should use the kern table)
*/
Position.prototype.getKerningTables = function(script, language) {
if (this.font.tables.gpos) {
return this.getLookupTables(script, language, 'kern', 2);
}
};

export default Position;

0 comments on commit f570fe9

Please sign in to comment.