Skip to content

Commit

Permalink
GSUB editing: add lookups 1 and 3 (single and alternates)
Browse files Browse the repository at this point in the history
  • Loading branch information
fpirsch committed Jul 31, 2016
1 parent 0aefac9 commit b173f08
Show file tree
Hide file tree
Showing 6 changed files with 258 additions and 31 deletions.
6 changes: 5 additions & 1 deletion src/check.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@

'use strict';

exports.fail = function(message) {
throw new Error(message);
};

// Precondition function that checks if the given predicate is true.
// If not, it will throw an error.
exports.argument = function(predicate, message) {
if (!predicate) {
throw new Error(message);
exports.fail(message);
}
};

Expand Down
6 changes: 5 additions & 1 deletion src/layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

'use strict';

var check = require('./check');

function searchTag(arr, tag) {
/* jshint bitwise: false */
var imin = 0;
Expand Down Expand Up @@ -128,11 +130,13 @@ var Layout = {
}
}
if (create) {
var index = allFeatures.length;
// Automatic ordering of features would require to shift feature indexes in the script list.
check.assert(index === 0 || feature >= allFeatures[index - 1].tag, 'Features must be added in alphabetical order.');
featureRecord = {
tag: feature,
feature: { params: 0, lookupListIndexes: [] }
};
var index = allFeatures.length;
allFeatures.push(featureRecord);
featIndexes.push(index);
return featureRecord.feature;
Expand Down
172 changes: 154 additions & 18 deletions src/substitution.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,21 @@ function arraysEqual(ar1, ar2) {
return true;
}

// Find the first subtable of a lookup table in a particular format.
function getSubstFormat(lookupTable, format, defaultSubtable) {
var subtables = lookupTable.subtables;
for (var i = 0; i < subtables.length; i++) {
var subtable = subtables[i];
if (subtable.substFormat === format) {
return subtable;
}
}
if (defaultSubtable) {
subtables.push(defaultSubtable);
return defaultSubtable;
}
}

Substitution.prototype = Layout;

// Get or create the GSUB table.
Expand All @@ -43,35 +58,139 @@ Substitution.prototype.getGsubTable = function(create) {
return gsub;
};

/**
* List all single substitutions (lookup type 1) for a given script, language, and feature.
* @param {string} script
* @param {string} language
* @param {string} feature - 4-character feature name ('aalt', 'salt', 'ss01'...)
*/
Substitution.prototype.getSingle = function(script, language, feature) {
var substitutions = [];
var lookupTable = this.getLookupTable(script, language, feature, 1);
if (!lookupTable) { return substitutions; }
var subtables = lookupTable.subtables;
for (var i = 0; i < subtables.length; i++) {
var subtable = subtables[i];
var glyphs = this.expandCoverage(subtable.coverage);
var j;
if (subtable.substFormat === 1) {
var delta = subtable.deltaGlyphId;
for (j = 0; j < glyphs.length; j++) {
var glyph = glyphs[j];
substitutions.push({ sub: glyph, by: glyph + delta });
}
} else {
var substitute = subtable.substitute;
for (j = 0; j < glyphs.length; j++) {
substitutions.push({ sub: glyphs[j], by: substitute[j] });
}
}
}
return substitutions;
};

/**
* List all alternates (lookup type 3) for a given script, language, and feature.
* @param {string} script
* @param {string} language
* @param {string} feature - 4-character feature name ('aalt', 'salt'...)
*/
Substitution.prototype.getAlternates = function(script, language, feature) {
var alternates = [];
var lookupTable = this.getLookupTable(script, language, feature, 3);
if (!lookupTable) { return alternates; }
var subtables = lookupTable.subtables;
for (var i = 0; i < subtables.length; i++) {
var subtable = subtables[i];
var glyphs = this.expandCoverage(subtable.coverage);
var alternateSets = subtable.alternateSets;
for (var j = 0; j < glyphs.length; j++) {
alternates.push({ sub: glyphs[j], by: alternateSets[j] });
}
}
return alternates;
};

/**
* List all ligatures (lookup type 4) for a given script, language, and feature.
* The result is an array of ligature objects like { sub: [ids], by: id }
* @param {string} script
* @param {string} language
* @param {string} feature - 4-letter feature name (liga, rlig, dlig...)
* @param {string} feature - 4-letter feature name ('liga', 'rlig', 'dlig'...)
*/
Substitution.prototype.getLigatures = function(script, language, feature) {
var ligatures = [];
var lookupTable = this.getLookupTable(script, language, feature, 4);
if (!lookupTable) { return []; }
var subtable = lookupTable.subtables[0];
if (!subtable) { return []; }
var glyphs = this.expandCoverage(subtable.coverage);
var ligatureSets = subtable.ligatureSets;
var ligatures = [];
for (var i = 0; i < glyphs.length; i++) {
var startGlyph = glyphs[i];
var ligSet = ligatureSets[i];
for (var j = 0; j < ligSet.length; j++) {
var lig = ligSet[j];
ligatures.push({
sub: [startGlyph].concat(lig.components),
by: lig.ligGlyph
});
var subtables = lookupTable.subtables;
for (var i = 0; i < subtables.length; i++) {
var subtable = subtables[i];
var glyphs = this.expandCoverage(subtable.coverage);
var ligatureSets = subtable.ligatureSets;
for (var j = 0; j < glyphs.length; j++) {
var startGlyph = glyphs[j];
var ligSet = ligatureSets[j];
for (var k = 0; k < ligSet.length; k++) {
var lig = ligSet[k];
ligatures.push({
sub: [startGlyph].concat(lig.components),
by: lig.ligGlyph
});
}
}
}
return ligatures;
};

/**
* Add or modify a single substitution (lookup type 1)
* Format 2, more flexible, is always used.
* @param {string} [script='DFLT']
* @param {string} [language='DFLT']
* @param {object} substitution - { sub: id, delta: number } for format 1 or { sub: id, by: id } for format 2.
*/
Substitution.prototype.addSingle = function(script, language, feature, substitution) {
var lookupTable = this.getLookupTable(script, language, feature, 1, true);
var subtable = getSubstFormat(lookupTable, 2, { // lookup type 1 subtable, format 2, coverage format 1
substFormat: 2,
coverage: { format: 1, glyphs: [] },
substitute: []
});
check.assert(subtable.coverage.format === 1, 'Ligature: unable to modify coverage table format ' + subtable.coverage.format);
var coverageGlyph = substitution.sub;
var pos = this.binSearch(subtable.coverage.glyphs, coverageGlyph);
if (pos < 0) {
pos = -1 - pos;
subtable.coverage.glyphs.splice(pos, 0, coverageGlyph);
subtable.substitute.splice(pos, 0, 0);
}
subtable.substitute[pos] = substitution.by;
};

/**
* Add or modify an alternate substitution (lookup type 1)
* @param {string} [script='DFLT']
* @param {string} [language='DFLT']
* @param {object} substitution - { sub: id, by: [ids] }
*/
Substitution.prototype.addAlternate = function(script, language, feature, substitution) {
var lookupTable = this.getLookupTable(script, language, feature, 3, true);
var subtable = getSubstFormat(lookupTable, 1, { // lookup type 3 subtable, format 1, coverage format 1
substFormat: 1,
coverage: { format: 1, glyphs: [] },
alternateSets: []
});
check.assert(subtable.coverage.format === 1, 'Ligature: unable to modify coverage table format ' + subtable.coverage.format);
var coverageGlyph = substitution.sub;
var pos = this.binSearch(subtable.coverage.glyphs, coverageGlyph);
if (pos < 0) {
pos = -1 - pos;
subtable.coverage.glyphs.splice(pos, 0, coverageGlyph);
subtable.alternateSets.splice(pos, 0, 0);
}
subtable.alternateSets[pos] = substitution.by;
};

/**
* Add a ligature (lookup type 4)
* Ligatures with more components must be stored ahead of those with fewer components in order to be found
Expand Down Expand Up @@ -128,7 +247,14 @@ Substitution.prototype.getFeature = function(script, language, feature) {
feature = arguments[0];
script = language = 'DFLT';
}
if (/ss\d\d/.test(feature)) { // ss01 - ss20
return this.getSingle(script, language, feature);
}
switch (feature) {
case 'aalt':
case 'salt':
return this.getSingle(script, language, feature)
.concat(this.getAlternates(script, language, feature));
case 'dlig':
case 'liga':
case 'rlig': return this.getLigatures(script, language, feature);
Expand All @@ -137,22 +263,32 @@ Substitution.prototype.getFeature = function(script, language, feature) {

/**
* Add a substitution to a feature for a given script and language.
* The result is an array of ligature objects like { sub: [ids], by: id }
*
* @param {string} [script='DFLT']
* @param {string} [language='DFLT']
* @param {string} feature - 4-letter feature name
* @param {object} sub - the substitution to add
* @param {object} sub - the substitution to add (an object like { sub: id or [ids], by: id or [ids] })
*/
Substitution.prototype.add = function(script, language, feature, sub) {
if (arguments.length === 2) {
feature = arguments[0];
sub = arguments[1];
script = language = 'DFLT';
}
if (/ss\d\d/.test(feature)) { // ss01 - ss20
return this.addSingle(script, language, feature, sub);
}
switch (feature) {
case 'aalt':
case 'salt':
if (typeof sub.by === 'number') {
return this.addSingle(script, language, feature, sub);
}
return this.addAlternate(script, language, feature, sub);
case 'dlig':
case 'liga':
case 'rlig': return this.addLigature(script, language, feature, sub);
case 'rlig':
return this.addLigature(script, language, feature, sub);
}
};

Expand Down
18 changes: 15 additions & 3 deletions src/tables/gsub.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@ subtableParsers[5] = function parseLookup5() {

// https://www.microsoft.com/typography/OTSPEC/GSUB.htm#CC
subtableParsers[6] = function parseLookup6() {
// TODO add automated tests for lookup 6 : no examples in the MS doc.
var start = this.offset + this.relativeOffset;
var substFormat = this.parseUShort();
if (substFormat === 1) {
Expand Down Expand Up @@ -213,9 +212,22 @@ subtableMakers[1] = function makeLookup1(subtable) {
{name: 'deltaGlyphID', type: 'USHORT', value: subtable.deltaGlyphId}
]);
} else {
check.assert(false, 'Can\'t write lookup type 1 subtable format 2.');
return new table.Table('substitutionTable', [
{name: 'substFormat', type: 'USHORT', value: 2},
{name: 'coverage', type: 'TABLE', value: new table.Coverage(subtable.coverage)}
].concat(table.ushortList('substitute', subtable.substitute)));
}
check.assert(false, 'Lookup type 1 substFormat must be 1 or 2.');
check.fail('Lookup type 1 substFormat must be 1 or 2.');
};

subtableMakers[3] = function makeLookup3(subtable) {
check.assert(subtable.substFormat === 1, 'Lookup type 3 substFormat must be 1.');
return new table.Table('substitutionTable', [
{name: 'substFormat', type: 'USHORT', value: 1},
{name: 'coverage', type: 'TABLE', value: new table.Coverage(subtable.coverage)}
].concat(table.tableList('altSet', subtable.alternateSets, function(alternateSet) {
return new table.Table('alternateSetTable', table.ushortList('alternate', alternateSet));
})));
};

subtableMakers[4] = function makeLookup4(subtable) {
Expand Down
56 changes: 49 additions & 7 deletions test/substitution.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ describe('substitution.js', function() {
});
}));

var defaultScriptList = [{
tag: 'DFLT',
script: {
defaultLangSys: { reserved: 0, reqFeatureIndex: 0xffff, featureIndexes: [0] },
langSysRecords: []
}
}];

beforeEach(function() {
font = new opentype.Font({
familyName: 'MyFont',
Expand Down Expand Up @@ -59,17 +67,51 @@ describe('substitution.js', function() {
});

describe('add', function() {
it('can add single substitutions (lookup type 1 format 2)', function() {
substitution.add('salt', { sub: 4, by: 10 });
substitution.add('salt', { sub: 5, by: 11 });
assert.deepEqual(font.tables.gsub.scripts, defaultScriptList);
assert.deepEqual(font.tables.gsub.features, [{
tag: 'salt',
feature: { params: 0, lookupListIndexes: [0] }
}]);
assert.deepEqual(font.tables.gsub.lookups, [{
lookupFlag: 0,
lookupType: 1,
markFilteringSet: undefined,
subtables: [{
substFormat: 2,
coverage: { format: 1, glyphs: [4, 5] },
substitute: [10, 11]
}]
}]);
});

it('can add alternate substitutions (lookup type 3)', function() {
substitution.add('aalt', { sub: 4, by: [5, 6, 7] });
substitution.add('aalt', { sub: 8, by: [9, 10] });
assert.deepEqual(font.tables.gsub.scripts, defaultScriptList);
assert.deepEqual(font.tables.gsub.features, [{
tag: 'aalt',
feature: { params: 0, lookupListIndexes: [0] }
}]);
assert.deepEqual(font.tables.gsub.lookups, [{
lookupFlag: 0,
lookupType: 3,
markFilteringSet: undefined,
subtables: [{
substFormat: 1,
coverage: { format: 1, glyphs: [4, 8] },
alternateSets: [[5, 6, 7], [9, 10]]
}]
}]);
});

it('can add ligatures (lookup type 4)', function() {
substitution.add('liga', { sub: [4, 5], by: 17 });
substitution.add('liga', { sub: [4, 6], by: 18 });
substitution.add('liga', { sub: [8, 1, 2], by: 19 });
assert.deepEqual(font.tables.gsub.scripts, [{
tag: 'DFLT',
script: {
defaultLangSys: { reserved: 0, reqFeatureIndex: 0xffff, featureIndexes: [0] },
langSysRecords: []
}
}]);
assert.deepEqual(font.tables.gsub.scripts, defaultScriptList);
assert.deepEqual(font.tables.gsub.features, [{
tag: 'liga',
feature: { params: 0, lookupListIndexes: [0] }
Expand Down
Loading

0 comments on commit b173f08

Please sign in to comment.