You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
407 lines
9.1 KiB
407 lines
9.1 KiB
'use strict'; |
|
|
|
// See documentation here: http://www.microsoft.com/typography/otspec/GSUB.htm |
|
|
|
var _ = require('lodash'); |
|
var identifier = require('../utils.js').identifier; |
|
var ByteBuffer = require('microbuffer'); |
|
|
|
function createScript() { |
|
var scriptRecord = (0 |
|
+ 2 // Script DefaultLangSys Offset |
|
+ 2 // Script[0] LangSysCount (0) |
|
); |
|
|
|
var langSys = (0 |
|
+ 2 // Script DefaultLangSys LookupOrder |
|
+ 2 // Script DefaultLangSys ReqFeatureIndex |
|
+ 2 // Script DefaultLangSys FeatureCount (0?) |
|
+ 2 // Script Optional Feature Index[0] |
|
); |
|
|
|
var length = (0 |
|
+ scriptRecord |
|
+ langSys |
|
); |
|
|
|
var buffer = new ByteBuffer(length); |
|
|
|
// Script Record |
|
// Offset to the start of langSys from the start of scriptRecord |
|
buffer.writeUint16(scriptRecord); // DefaultLangSys |
|
|
|
// Number of LangSys entries other than the default (none) |
|
buffer.writeUint16(0); |
|
|
|
// LangSys record (DefaultLangSys) |
|
// LookupOrder |
|
buffer.writeUint16(0); |
|
// ReqFeatureIndex -> only one required feature: all ligatures |
|
buffer.writeUint16(0); |
|
// Number of FeatureIndex values for this language system (excludes the required feature) |
|
buffer.writeUint16(1); |
|
// FeatureIndex for the first optional feature |
|
// Note: Adding the same feature to both the optional |
|
// and the required features is a clear violation of the spec |
|
// but it fixes IE not displaying the ligatures. |
|
// See http://partners.adobe.com/public/developer/opentype/index_table_formats.html, Section “Language System Table” |
|
// “FeatureCount: Number of FeatureIndex values for this language system-*excludes the required feature*” (emphasis added) |
|
buffer.writeUint16(0); |
|
|
|
return buffer; |
|
} |
|
|
|
function createScriptList() { |
|
var scriptSize = (0 |
|
+ 4 // Tag |
|
+ 2 // Offset |
|
); |
|
|
|
// tags should be arranged alphabetically |
|
var scripts = [ |
|
[ 'DFLT', createScript() ], |
|
[ 'latn', createScript() ] |
|
]; |
|
|
|
var header = (0 |
|
+ 2 // Script count |
|
+ scripts.length * scriptSize |
|
); |
|
|
|
var tableLengths = _.reduce(_.map(scripts, function (script) { return script[1].length; }), function (result, count) { return result + count; }, 0); |
|
|
|
var length = (0 |
|
+ header |
|
+ tableLengths |
|
); |
|
|
|
var buffer = new ByteBuffer(length); |
|
|
|
// Script count |
|
buffer.writeUint16(scripts.length); |
|
|
|
// Write all ScriptRecords |
|
var offset = header; |
|
|
|
_.forEach(scripts, function (script) { |
|
var name = script[0], table = script[1]; |
|
|
|
// Script identifier (DFLT/latn) |
|
buffer.writeUint32(identifier(name)); |
|
// Offset to the ScriptRecord from start of the script list |
|
buffer.writeUint16(offset); |
|
// Increment offset by script table length |
|
offset += table.length; |
|
}); |
|
|
|
// Write all ScriptTables |
|
_.forEach(scripts, function (script) { |
|
var table = script[1]; |
|
|
|
buffer.writeBytes(table.buffer); |
|
}); |
|
|
|
return buffer; |
|
} |
|
|
|
// Write one feature containing all ligatures |
|
function createFeatureList() { |
|
var header = (0 |
|
+ 2 // FeatureCount |
|
+ 4 // FeatureTag[0] |
|
+ 2 // Feature Offset[0] |
|
); |
|
|
|
var length = (0 |
|
+ header |
|
+ 2 // FeatureParams[0] |
|
+ 2 // LookupCount[0] |
|
+ 2 // Lookup[0] LookupListIndex[0] |
|
); |
|
|
|
var buffer = new ByteBuffer(length); |
|
|
|
// FeatureCount |
|
buffer.writeUint16(1); |
|
// FeatureTag[0] |
|
buffer.writeUint32(identifier('liga')); |
|
// Feature Offset[0] |
|
buffer.writeUint16(header); |
|
// FeatureParams[0] |
|
buffer.writeUint16(0); |
|
// LookupCount[0] |
|
buffer.writeUint16(1); |
|
// Index into lookup table. Since we only have ligatures, the index is always 0 |
|
buffer.writeUint16(0); |
|
|
|
return buffer; |
|
} |
|
|
|
function createLigatureCoverage(font, ligatureGroups) { |
|
var glyphCount = ligatureGroups.length; |
|
|
|
var length = (0 |
|
+ 2 // CoverageFormat |
|
+ 2 // GlyphCount |
|
+ 2 * glyphCount // GlyphID[i] |
|
); |
|
|
|
var buffer = new ByteBuffer(length); |
|
|
|
// CoverageFormat |
|
buffer.writeUint16(1); |
|
|
|
// Length |
|
buffer.writeUint16(glyphCount); |
|
|
|
|
|
_.forEach(ligatureGroups, function (group) { |
|
buffer.writeUint16(group.startGlyph.id); |
|
}); |
|
|
|
return buffer; |
|
} |
|
|
|
function createLigatureTable(font, ligature) { |
|
var allCodePoints = font.codePoints; |
|
|
|
var unicode = ligature.unicode; |
|
|
|
var length = (0 |
|
+ 2 // LigGlyph |
|
+ 2 // CompCount |
|
+ 2 * (unicode.length - 1) |
|
); |
|
|
|
var buffer = new ByteBuffer(length); |
|
|
|
// LigGlyph |
|
var glyph = ligature.glyph; |
|
|
|
buffer.writeUint16(glyph.id); |
|
|
|
// CompCount |
|
buffer.writeUint16(unicode.length); |
|
|
|
// Compound glyphs (excluding first as it’s already in the coverage table) |
|
for (var i = 1; i < unicode.length; i++) { |
|
glyph = allCodePoints[unicode[i]]; |
|
buffer.writeUint16(glyph.id); |
|
} |
|
|
|
return buffer; |
|
} |
|
|
|
function createLigatureSet(font, codePoint, ligatures) { |
|
var ligatureTables = []; |
|
|
|
_.forEach(ligatures, function (ligature) { |
|
ligatureTables.push(createLigatureTable(font, ligature)); |
|
}); |
|
|
|
var tableLengths = _.reduce(_.map(ligatureTables, 'length'), function (result, count) { return result + count; }, 0); |
|
|
|
var offset = (0 |
|
+ 2 // LigatureCount |
|
+ 2 * ligatures.length |
|
); |
|
|
|
var length = (0 |
|
+ offset |
|
+ tableLengths |
|
); |
|
|
|
var buffer = new ByteBuffer(length); |
|
|
|
// LigatureCount |
|
buffer.writeUint16(ligatures.length); |
|
|
|
// Ligature offsets |
|
_.forEach(ligatureTables, function (table) { |
|
// The offset to the current set, from SubstFormat |
|
buffer.writeUint16(offset); |
|
offset += table.length; |
|
}); |
|
|
|
// Ligatures |
|
_.forEach(ligatureTables, function (table) { |
|
buffer.writeBytes(table.buffer); |
|
}); |
|
|
|
return buffer; |
|
} |
|
|
|
function createLigatureList(font, ligatureGroups) { |
|
var sets = []; |
|
|
|
_.forEach(ligatureGroups, function (group) { |
|
var set = createLigatureSet(font, group.codePoint, group.ligatures); |
|
|
|
sets.push(set); |
|
}); |
|
|
|
var setLengths = _.reduce(_.map(sets, 'length'), function (result, count) { return result + count; }, 0); |
|
|
|
var coverage = createLigatureCoverage(font, ligatureGroups); |
|
|
|
var tableOffset = (0 |
|
+ 2 // Lookup type |
|
+ 2 // Lokup flag |
|
+ 2 // SubTableCount |
|
+ 2 // SubTable[0] Offset |
|
); |
|
|
|
var setOffset = (0 |
|
+ 2 // SubstFormat |
|
+ 2 // Coverage offset |
|
+ 2 // LigSetCount |
|
+ 2 * sets.length // LigSet Offsets |
|
); |
|
|
|
var coverageOffset = setOffset + setLengths; |
|
|
|
var length = (0 |
|
+ tableOffset |
|
+ coverageOffset |
|
+ coverage.length |
|
); |
|
|
|
var buffer = new ByteBuffer(length); |
|
|
|
// Lookup type 4 – ligatures |
|
buffer.writeUint16(4); |
|
|
|
// Lookup flag – empty |
|
buffer.writeUint16(0); |
|
|
|
// Subtable count |
|
buffer.writeUint16(1); |
|
|
|
// Subtable[0] offset |
|
buffer.writeUint16(tableOffset); |
|
|
|
// SubstFormat |
|
buffer.writeUint16(1); |
|
|
|
// Coverage |
|
buffer.writeUint16(coverageOffset); |
|
|
|
// LigSetCount |
|
buffer.writeUint16(sets.length); |
|
|
|
_.forEach(sets, function (set) { |
|
// The offset to the current set, from SubstFormat |
|
buffer.writeUint16(setOffset); |
|
setOffset += set.length; |
|
}); |
|
|
|
_.forEach(sets, function (set) { |
|
buffer.writeBytes(set.buffer); |
|
}); |
|
|
|
buffer.writeBytes(coverage.buffer); |
|
|
|
return buffer; |
|
} |
|
|
|
// Add a lookup for each ligature |
|
function createLookupList(font) { |
|
var ligatures = font.ligatures; |
|
|
|
var groupedLigatures = {}; |
|
|
|
// Group ligatures by first code point |
|
_.forEach(ligatures, function (ligature) { |
|
var first = ligature.unicode[0]; |
|
|
|
if (!_.has(groupedLigatures, first)) { |
|
groupedLigatures[first] = []; |
|
} |
|
groupedLigatures[first].push(ligature); |
|
}); |
|
|
|
var ligatureGroups = []; |
|
|
|
_.forEach(groupedLigatures, function (ligatures, codePoint) { |
|
codePoint = parseInt(codePoint, 10); |
|
// Order ligatures by length, descending |
|
// “Ligatures with more components must be stored ahead of those with fewer components in order to be found” |
|
// From: http://partners.adobe.com/public/developer/opentype/index_tag7.html#liga |
|
ligatures.sort(function (ligA, ligB) { |
|
return ligB.unicode.length - ligA.unicode.length; |
|
}); |
|
ligatureGroups.push({ |
|
codePoint: codePoint, |
|
ligatures: ligatures, |
|
startGlyph: font.codePoints[codePoint] |
|
}); |
|
}); |
|
|
|
ligatureGroups.sort(function (a, b) { |
|
return a.startGlyph.id - b.startGlyph.id; |
|
}); |
|
|
|
var offset = (0 |
|
+ 2 // Lookup count |
|
+ 2 // Lookup[0] offset |
|
); |
|
|
|
var set = createLigatureList(font, ligatureGroups); |
|
|
|
var length = (0 |
|
+ offset |
|
+ set.length |
|
); |
|
|
|
var buffer = new ByteBuffer(length); |
|
|
|
// Lookup count |
|
buffer.writeUint16(1); |
|
|
|
// Lookup[0] offset |
|
buffer.writeUint16(offset); |
|
|
|
// Lookup[0] |
|
buffer.writeBytes(set.buffer); |
|
|
|
return buffer; |
|
} |
|
|
|
function createGSUB(font) { |
|
var scriptList = createScriptList(); |
|
var featureList = createFeatureList(); |
|
var lookupList = createLookupList(font); |
|
|
|
var lists = [ scriptList, featureList, lookupList ]; |
|
|
|
var offset = (0 |
|
+ 4 // Version |
|
+ 2 * lists.length // List offsets |
|
); |
|
|
|
// Calculate offsets |
|
_.forEach(lists, function (list) { |
|
list._listOffset = offset; |
|
offset += list.length; |
|
}); |
|
|
|
var length = offset; |
|
var buffer = new ByteBuffer(length); |
|
|
|
// Version |
|
buffer.writeUint32(0x00010000); |
|
|
|
// Offsets |
|
_.forEach(lists, function (list) { |
|
buffer.writeUint16(list._listOffset); |
|
}); |
|
|
|
// List contents |
|
_.forEach(lists, function (list) { |
|
buffer.writeBytes(list.buffer); |
|
}); |
|
|
|
return buffer; |
|
} |
|
|
|
module.exports = createGSUB;
|
|
|