Files
operator-mono-lig/index.js
T
2017-08-20 11:26:20 -04:00

285 lines
9.3 KiB
JavaScript

const Promise = require('bluebird');
const fs = Promise.promisifyAll(require('fs'));
const xpath = require('xpath');
const { DOMParser, XMLSerializer } = require('xmldom');
const format = require('xml-formatter');
let fontName;
let ligFontName;
const regEx = /\.liga$/;
const regExBlankLines = /^(?=\n)$|^\s*/gm;
const regExWhitespace = /^\s+$/;
const NodeType = {};
NodeType.TEXT_NODE = 3;
async function main() {
fontName = process.argv[2];
ligFontName = fontName.split('-').join('Lig-');
const srcFileName = `./original/${fontName}.ttx`;
const dstFileName = `./build/${ligFontName}.ttx`;
console.log(`Reading original font file ${srcFileName}`);
const xml = await fs.readFileAsync(srcFileName, 'utf-8');
const dom = new DOMParser().parseFromString(xml);
await processPatch('names', patchNames, dom);
await processPatch('glyphs', patchGlyphs, dom);
await processPatch('cmap', patchCmap, dom);
await processPatch('classdef', patchClassDef, dom);
await processPatch('hmtx', patchHmtx, dom);
await processPatch('lookup', patchLookup, dom);
await processPatch('charstrings', patchCharStrings, dom);
console.log(`Writing ligature font file ${dstFileName}`);
await fs.writeFileAsync(dstFileName, format(serialize(dom)));
console.log('Done');
}
async function loadConfigAsync(name) {
const fileName = `./ligature/${ligFontName}_${name}.xml`;
const xml = await fs.readFileAsync(fileName, 'utf-8');
return new DOMParser().parseFromString(xml);
}
async function processPatch(name, patchFunc, dom) {
console.log(`Patching ${name}`);
await patchFunc(dom);
}
async function patchNames(dom) {
const configDom = await loadConfigAsync('names');
const names = xpath.select('/name/namerecord', configDom);
const targetName = xpath.select('/ttFont/name', dom, true);
names.forEach(node => {
const nameId = node.getAttribute('nameID');
const platformId = node.getAttribute('platformID');
// search for namerecord in target dom and replace with this one or append node
const target = xpath.select(
`/ttFont/name/namerecord[@nameID="${nameId}" and @platformID="${platformId}"]`,
dom,
true
);
if (target) {
target.parentNode.replaceChild(node, target);
} else {
targetName.appendChild(node);
}
});
}
async function patchGlyphs(dom) {
const configDom = await loadConfigAsync('glyphs');
// get number of glyphs in target dom
const targetGlyphs = xpath.select('/ttFont/GlyphOrder', dom, true);
const glyphsCount = xpath.select('count(GlyphID)', targetGlyphs, true);
let n = glyphsCount;
// get ligature glyphs
const glyphs = xpath.select('/GlyphOrder/GlyphID', configDom);
glyphs.forEach(node => {
node.setAttribute('id', n++);
targetGlyphs.appendChild(node);
});
// update glyph count
setAttribute(dom, '/ttFont/maxp/numGlyphs', 'value', n);
}
async function patchCmap(dom) {
const configDom = await loadConfigAsync('cmap');
const cmaps = xpath.select('/cmap/cmap_format_4', configDom);
cmaps.forEach(node => {
const platformId = node.getAttribute('platformID');
const platEncId = node.getAttribute('platEncID');
// search for cmap_format_4 in target dom
const target = xpath.select(
`/ttFont/cmap/cmap_format_4[@platformID="${platformId}" and @platEncID="${platEncId}"]`,
dom,
true
);
if (target) {
// append all map elements to target cmap_format_4
const maps = xpath.select('map', node);
maps.forEach(child => target.appendChild(child));
}
});
}
async function patchClassDef(dom) {
const configDom = await loadConfigAsync('classdef');
const glyphClassDef = xpath.select('/ttFont/GDEF/GlyphClassDef', dom, true);
const classDefs = xpath.select('/GlyphClassDef/ClassDefs', configDom);
classDefs.forEach(node => glyphClassDef.appendChild(node));
}
async function patchHmtx(dom) {
const hmtxDom = await loadConfigAsync('hmtx');
const targetHmtx = xpath.select('/ttFont/hmtx', dom, true);
const mtx = xpath.select('/hmtx/mtx', hmtxDom);
mtx.forEach(node => targetHmtx.appendChild(node));
const mtxCount = xpath.select('count(/ttFont/hmtx/mtx)', dom, true);
setAttribute(dom, '/ttFont/hhea/numberOfHMetrics', 'value', mtxCount);
const configDom = await loadConfigAsync('config');
copyConfigAttribute(dom, configDom, '/ttFont/head/xMin', 'value');
copyConfigAttribute(dom, configDom, '/ttFont/head/yMin', 'value');
copyConfigAttribute(dom, configDom, '/ttFont/head/xMax', 'value');
copyConfigAttribute(dom, configDom, '/ttFont/head/yMax', 'value');
copyConfigAttribute(dom, configDom, '/ttFont/hhea/advanceWidthMax', 'value');
copyConfigAttribute(dom, configDom, '/ttFont/hhea/xMaxExtent', 'value');
copyConfigAttribute(dom, configDom, '/ttFont/CFF/CFFFont/FontBBox', 'value');
copyConfigAttribute(
dom,
configDom,
'/ttFont/CFF/CFFFont/Private/nominalWidthX',
'value'
);
}
async function patchLookup(dom) {
const configDom = await loadConfigAsync('lookup');
const featureList = xpath.select('/ttFont/GSUB/FeatureList', dom, true);
// look for feature with FeatureTag liga
let featureRecord;
let featureTag;
let feature;
let featureTags = xpath.select(
'/ttFont/GSUB/FeatureList/FeatureRecord/FeatureTag[@value="liga"]',
dom
);
if (featureTags.length) {
featureTag = featureTags[0];
featureRecord = featureTag.parentNode;
feature = xpath.select('Feature', featureRecord, true);
} else {
// need to add feature to FeatureList
const featuresCount = xpath.select(
'count(/ttFont/GSUB/FeatureList/FeatureRecord)',
dom,
true
);
featureRecord = dom.createElement('FeatureRecord');
featureRecord.setAttribute('index', featuresCount);
featureTag = dom.createElement('FeatureTag');
featureTag.setAttribute('value', 'liga');
feature = dom.createElement('Feature');
featureRecord.appendChild(featureTag);
featureRecord.appendChild(feature);
featureList.appendChild(featureRecord);
}
let lookupListIndex = xpath.select(
'LookupListIndex[@value="20"]',
feature,
true
);
if (!lookupListIndex) {
const count = xpath.select('count(LookupListIndex)', feature, true);
lookupListIndex = dom.createElement('LookupListIndex');
lookupListIndex.setAttribute('index', count);
lookupListIndex.setAttribute('value', '20');
feature.appendChild(lookupListIndex);
}
// helper function for adding feature to ScriptRecord
const addFeature = (scriptRecord, featureRecord, lang) => {
if (!lang) return;
const index = featureRecord.getAttribute('index');
if (xpath.select(`FeatureIndex[@value="${index}"]`, lang, true)) return;
const count = xpath.select('count(FeatureIndex)', lang, true);
const featureIndex = dom.createElement('FeatureIndex');
featureIndex.setAttribute('index', count);
featureIndex.setAttribute('value', index);
lang.appendChild(featureIndex);
};
// nead to add feature to ScriptList
const scriptList = xpath.select('/ttFont/GSUB/ScriptList', dom, true);
const scriptRecords = xpath.select('ScriptRecord', scriptList);
scriptRecords.forEach(node => {
// check for feature in DefaultLangSys
addFeature(
node,
featureRecord,
xpath.select('Script/DefaultLangSys', node, true)
);
// add features to any other languages
xpath
.select('Script/LangSysRecord/LangSys', node)
.forEach(lang => addFeature(node, featureRecord, lang));
});
// finally add LigatureSubst to Lookup index=20
const lookupList = xpath.select('/ttFont/GSUB/LookupList', dom, true);
const newLookup = xpath.select('/LookupList/Lookup', configDom, true);
let lookup = xpath.select(
'/ttFont/GSUB/LookupList/Lookup[@index="20"]',
dom,
true
);
if (!lookup) {
lookupList.appendChild(newLookup);
} else {
lookupList.replaceChild(newLookup, lookup);
}
}
async function patchCharStrings(dom) {
const configDom = await loadConfigAsync('charstrings');
const nameDom = await loadConfigAsync('names');
const cffFont = xpath.select('/ttFont/CFF/CFFFont', dom, true);
cffFont.setAttribute('name', ligFontName);
// get names from name config
const fullName = nameDom.documentElement.getAttribute('fullName');
const familyName = nameDom.documentElement.getAttribute('familyName');
setAttribute(cffFont, 'FullName', 'value', fullName);
setAttribute(cffFont, 'FamilyName', 'value', familyName);
const targetCharStrings = xpath.select('CharStrings', cffFont, true);
const charStrings = xpath.select('/CharStrings/CharString', configDom);
charStrings.forEach(node => targetCharStrings.appendChild(node));
}
const setAttribute = (parent, path, name, value) => {
var node = xpath.select(path, parent, true);
node.setAttribute(name, value);
};
const copyConfigAttribute = (dom, configDom, path, name) => {
const value = xpath.select(path, configDom, true).getAttribute(name);
setAttribute(dom, path, name, value);
};
const serialize = dom =>
new XMLSerializer().serializeToString(dom, false, node => {
if (node.nodeType === NodeType.TEXT_NODE) {
if (regExWhitespace.test(node.data)) return null;
const data = node.data
.split('\r\n')
.filter(s => /\S+/.test(s))
.map(s => s.replace(/^\s+/g, ''))
.join('\n');
return node.ownerDocument.createTextNode(data);
}
return node;
});
main();