mirror of
https://github.com/kiliman/operator-mono-lig.git
synced 2026-08-12 02:10:52 +02:00
Update extract and build process with support for context substitutions
This commit is contained in:
@@ -19,7 +19,7 @@ set lig=%1
|
|||||||
set otf=%lig:Lig=%
|
set otf=%lig:Lig=%
|
||||||
|
|
||||||
if not exist .\original\%otf%.otf exit /b
|
if not exist .\original\%otf%.otf exit /b
|
||||||
if not exist .\ligature\%lig%\charstrings.xml exit /b
|
if not exist .\ligature\%lig%\glyphs\* exit /b
|
||||||
|
|
||||||
@echo Building %lig%
|
@echo Building %lig%
|
||||||
ttx -f .\original\%otf%.otf
|
ttx -f .\original\%otf%.otf
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ build_font() {
|
|||||||
then
|
then
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
if [ ! -e "./ligature/$lig/charstrings.xml" ]
|
if [ ! -e "./ligature/$lig/glyphs" ]
|
||||||
then
|
then
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|||||||
+137
-56
@@ -1,38 +1,45 @@
|
|||||||
const Promise = require('bluebird');
|
const fs = require('fs');
|
||||||
const fs = Promise.promisifyAll(require('fs'));
|
const os = require('os');
|
||||||
|
if (!os.EOL) {
|
||||||
|
os.EOL = process.platform === 'win32' ? '\r\n' : '\n';
|
||||||
|
}
|
||||||
const xpath = require('xpath');
|
const xpath = require('xpath');
|
||||||
const { DOMParser, XMLSerializer } = require('xmldom');
|
const { DOMParser, XMLSerializer } = require('xmldom');
|
||||||
const format = require('xml-formatter');
|
const format = require('xml-formatter');
|
||||||
|
const hash = require('hash.js');
|
||||||
|
|
||||||
let fontName;
|
let fontName;
|
||||||
|
|
||||||
const regEx = /\.liga$/;
|
const regEx = /^LIG$|\.liga$/;
|
||||||
const regExBlankLines = /^(?=\n)$|^\s*/gm;
|
|
||||||
const regExWhitespace = /^\s+$/;
|
|
||||||
const NodeType = {};
|
const NodeType = {};
|
||||||
NodeType.TEXT_NODE = 3;
|
NodeType.TEXT_NODE = 3;
|
||||||
|
|
||||||
async function main() {
|
function main() {
|
||||||
fontName = process.argv[2];
|
fontName = process.argv[2];
|
||||||
|
|
||||||
const srcFileName = `./ligature_source/${fontName}.ttx`;
|
const srcFileName = `./ligature_source/${fontName}.ttx`;
|
||||||
|
|
||||||
|
const folder = `./ligature/${fontName}`;
|
||||||
|
const fileName = `./${folder}/names.json`;
|
||||||
|
if (!fs.existsSync(fileName)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`Reading file ${srcFileName}`);
|
console.log(`Reading file ${srcFileName}`);
|
||||||
const xml = await fs.readFileAsync(srcFileName, 'utf-8');
|
const xml = fs.readFileSync(srcFileName, 'utf-8');
|
||||||
const dom = new DOMParser().parseFromString(xml);
|
const dom = new DOMParser().parseFromString(xml);
|
||||||
|
|
||||||
await extractAndWrite('charstrings', extractCharStrings, dom);
|
extractAndWrite('config', extractConfig, dom);
|
||||||
await extractAndWrite('config', extractConfig, dom);
|
|
||||||
await extractAndWrite('gpos', extractGpos, dom);
|
extractCharStrings(dom);
|
||||||
await extractAndWrite('gsub', extractGsub, dom);
|
|
||||||
await extractAndWrite('hmtx', extractHmtx, dom);
|
extractAndWrite('subrs', extractSubrs, dom);
|
||||||
await extractAndWrite('subrs', extractSubrs, dom);
|
extractAndWrite('gsubrs', extractGlobalSubrs, dom);
|
||||||
await extractAndWrite('gsubrs', extractGlobalSubrs, dom);
|
|
||||||
|
|
||||||
console.log('Done');
|
console.log('Done');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function extractAndWrite(name, func, dom) {
|
function extractAndWrite(name, func, dom) {
|
||||||
const newDom = func(dom);
|
const newDom = func(dom);
|
||||||
console.log('Finished extracting ' + name);
|
console.log('Finished extracting ' + name);
|
||||||
|
|
||||||
@@ -41,24 +48,11 @@ async function extractAndWrite(name, func, dom) {
|
|||||||
if (!fs.existsSync(folder)) {
|
if (!fs.existsSync(folder)) {
|
||||||
fs.mkdirSync(folder);
|
fs.mkdirSync(folder);
|
||||||
}
|
}
|
||||||
await fs.writeFileAsync(fileName, format(serialize(newDom)));
|
fs.writeFileSync(fileName, format(serialize(newDom)));
|
||||||
console.log('Finished writing ' + name);
|
console.log('Finished writing ' + name);
|
||||||
}
|
}
|
||||||
|
//const dump = dom => console.log(serialize(dom));
|
||||||
const serialize = dom =>
|
const serialize = dom => new XMLSerializer().serializeToString(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;
|
|
||||||
});
|
|
||||||
|
|
||||||
const extractConfig = dom => {
|
const extractConfig = dom => {
|
||||||
const newDom = new DOMParser().parseFromString('<ttFont/>');
|
const newDom = new DOMParser().parseFromString('<ttFont/>');
|
||||||
@@ -92,29 +86,7 @@ const extractFromPath = (path, dom) => {
|
|||||||
return newDom.documentElement;
|
return newDom.documentElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
const extractGpos = dom => {
|
|
||||||
const gpos = xpath.select('/ttFont/GPOS', dom, true);
|
|
||||||
return gpos;
|
|
||||||
};
|
|
||||||
|
|
||||||
const extractGsub = dom => {
|
|
||||||
const gsub = xpath.select('/ttFont/GSUB', dom, true);
|
|
||||||
return gsub;
|
|
||||||
};
|
|
||||||
|
|
||||||
const extractHmtx = dom => {
|
|
||||||
const newDom = new DOMParser().parseFromString('<hmtx/>');
|
|
||||||
const mtx = xpath.select('/ttFont/hmtx/mtx', dom);
|
|
||||||
|
|
||||||
mtx
|
|
||||||
.filter(node => regEx.test(node.getAttribute('name')))
|
|
||||||
.forEach(node => newDom.documentElement.appendChild(node));
|
|
||||||
|
|
||||||
return newDom;
|
|
||||||
};
|
|
||||||
|
|
||||||
const extractCharStrings = dom => {
|
const extractCharStrings = dom => {
|
||||||
const newDom = new DOMParser().parseFromString('<CharStrings/>');
|
|
||||||
const charStrings = xpath.select(
|
const charStrings = xpath.select(
|
||||||
'/ttFont/CFF/CFFFont/CharStrings/CharString',
|
'/ttFont/CFF/CFFFont/CharStrings/CharString',
|
||||||
dom
|
dom
|
||||||
@@ -122,9 +94,118 @@ const extractCharStrings = dom => {
|
|||||||
|
|
||||||
charStrings
|
charStrings
|
||||||
.filter(node => regEx.test(node.getAttribute('name')))
|
.filter(node => regEx.test(node.getAttribute('name')))
|
||||||
.forEach(node => newDom.documentElement.appendChild(node));
|
.forEach(node => writeGlyphData(dom, node));
|
||||||
|
console.log('Finished writing charstrings');
|
||||||
|
};
|
||||||
|
|
||||||
return newDom;
|
const writeGlyphData = (dom, node) => {
|
||||||
|
const newDom = new DOMParser().parseFromString('<Glyph/>').documentElement;
|
||||||
|
const name = node.getAttribute('name');
|
||||||
|
newDom.setAttribute('name', name);
|
||||||
|
|
||||||
|
// get mtx
|
||||||
|
const mtx = xpath.select(`/ttFont/hmtx/mtx[@name="${name}"]`, dom, true);
|
||||||
|
newDom.setAttribute('lsb', mtx.getAttribute('lsb'));
|
||||||
|
newDom.setAttribute('width', mtx.getAttribute('width'));
|
||||||
|
|
||||||
|
const charStringDom = dom.createElement('CharString');
|
||||||
|
|
||||||
|
const subrsDom = dom.createElement('Subrs');
|
||||||
|
const gsubrsDom = dom.createElement('GlobalSubrs');
|
||||||
|
newDom.appendChild(charStringDom);
|
||||||
|
newDom.appendChild(subrsDom);
|
||||||
|
newDom.appendChild(gsubrsDom);
|
||||||
|
|
||||||
|
const subrs = {
|
||||||
|
map: [],
|
||||||
|
fingerprints: [],
|
||||||
|
sourcePath: '/ttFont/CFF/CFFFont/Private/Subrs',
|
||||||
|
target: subrsDom
|
||||||
|
};
|
||||||
|
const gsubrs = {
|
||||||
|
map: [],
|
||||||
|
fingerprints: [],
|
||||||
|
sourcePath: '/ttFont/CFF/GlobalSubrs',
|
||||||
|
target: gsubrsDom
|
||||||
|
};
|
||||||
|
|
||||||
|
extractCharStringSubrs(dom, node, subrs, gsubrs, 8);
|
||||||
|
|
||||||
|
const outline = indentTextContent(node.childNodes[0].textContent, 8);
|
||||||
|
charStringDom.appendChild(dom.createTextNode(outline));
|
||||||
|
|
||||||
|
const folder = `./ligature/${fontName}/glyphs`;
|
||||||
|
const fileName = `./${folder}/${name}.xml`;
|
||||||
|
if (!fs.existsSync(folder)) {
|
||||||
|
fs.mkdirSync(folder);
|
||||||
|
}
|
||||||
|
fs.writeFileSync(fileName, format(serialize(newDom)));
|
||||||
|
console.log('* ' + name);
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractCharStringSubrs = (dom, node, subrs, gsubrs, indent) => {
|
||||||
|
// check for callsubr/callgsubr
|
||||||
|
|
||||||
|
const lines = node.childNodes[0].textContent.split(os.EOL);
|
||||||
|
const newLines = [];
|
||||||
|
let fingerprint = '';
|
||||||
|
|
||||||
|
lines.forEach(line => {
|
||||||
|
if (line.trim().length === 0) return;
|
||||||
|
const matches = line.match(/(.*?)(-?\d+) (callsubr|callgsubr)$/);
|
||||||
|
if (matches != null) {
|
||||||
|
const { map, fingerprints, sourcePath, target } =
|
||||||
|
matches[3] === 'callsubr' ? subrs : gsubrs;
|
||||||
|
const index = matches[2];
|
||||||
|
const srcIndex = parseInt(index) + 107;
|
||||||
|
let newIndex = map[srcIndex];
|
||||||
|
|
||||||
|
if (!newIndex) {
|
||||||
|
// find subr in source dom and copy to target dom
|
||||||
|
const srcSubr = xpath.select(
|
||||||
|
`/${sourcePath}/CharString[@index="${srcIndex}"]`,
|
||||||
|
dom,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// append subr to target dom and get new index
|
||||||
|
newIndex = xpath.select('count(CharString)', target, true);
|
||||||
|
map[srcIndex] = newIndex;
|
||||||
|
|
||||||
|
const clone = srcSubr.cloneNode(true);
|
||||||
|
clone.setAttribute('index', newIndex);
|
||||||
|
target.appendChild(clone);
|
||||||
|
|
||||||
|
// patch up source in case it also has any callsubrs
|
||||||
|
fingerprint = extractCharStringSubrs(dom, clone, subrs, gsubrs, 12);
|
||||||
|
fingerprints[newIndex] = fingerprint;
|
||||||
|
} else {
|
||||||
|
fingerprint = fingerprints[newIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
// rewrite line with fingerprint
|
||||||
|
line = `${matches[1]}{${fingerprint}} ${matches[3]}`;
|
||||||
|
}
|
||||||
|
newLines.push(line.trim());
|
||||||
|
});
|
||||||
|
const content = newLines.join('\n');
|
||||||
|
fingerprint = hash
|
||||||
|
.sha256()
|
||||||
|
.update(content)
|
||||||
|
.digest('hex')
|
||||||
|
.substr(0, 8);
|
||||||
|
node.setAttribute('fingerprint', fingerprint);
|
||||||
|
|
||||||
|
node.childNodes[0].textContent = indentTextContent(content, indent);
|
||||||
|
return fingerprint;
|
||||||
|
};
|
||||||
|
|
||||||
|
const indentTextContent = (text, indent) => {
|
||||||
|
return text
|
||||||
|
.split('\n')
|
||||||
|
.map(line => ' '.repeat(indent) + line.trim())
|
||||||
|
.join('\n')
|
||||||
|
.trim();
|
||||||
};
|
};
|
||||||
|
|
||||||
const extractSubrs = dom => {
|
const extractSubrs = dom => {
|
||||||
|
|||||||
+1
-1
@@ -15,6 +15,6 @@ then
|
|||||||
else
|
else
|
||||||
# build all available fonts
|
# build all available fonts
|
||||||
for f in ./ligature_source/*.otf ; do
|
for f in ./ligature_source/*.otf ; do
|
||||||
extract_font $(basename $f)
|
extract_font `basename "${f%.*}"`
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -0,0 +1,264 @@
|
|||||||
|
const xpath = require('xpath');
|
||||||
|
//const { XMLSerializer } = require('xmldom');
|
||||||
|
//const format = require('xml-formatter');
|
||||||
|
const NodeType = {};
|
||||||
|
NodeType.TEXT_NODE = 3;
|
||||||
|
|
||||||
|
let dom;
|
||||||
|
let gsubDom;
|
||||||
|
let scriptListDom;
|
||||||
|
let featureListDom;
|
||||||
|
let lookupListDom;
|
||||||
|
let featureDom;
|
||||||
|
let lookupIndex = 0;
|
||||||
|
let chainIndex = 0;
|
||||||
|
const substLookupMap = {};
|
||||||
|
|
||||||
|
const buildGsubTables = (_dom, ligature) => {
|
||||||
|
const glyphs = ligature.name.split('_');
|
||||||
|
|
||||||
|
dom = _dom;
|
||||||
|
gsubDom = xpath.select('ttFont/GSUB', dom, true);
|
||||||
|
|
||||||
|
// look for 'calt' feature
|
||||||
|
featureListDom = xpath.select('FeatureList', gsubDom, true);
|
||||||
|
|
||||||
|
let featureRecord;
|
||||||
|
const featureTag = xpath.select(
|
||||||
|
'FeatureRecord/FeatureTag[@value="calt"]',
|
||||||
|
featureListDom,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
if (!featureTag) {
|
||||||
|
const featureListCount = xpath.select(
|
||||||
|
'count(FeatureRecord)',
|
||||||
|
featureListDom,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
featureRecord = createElementWithAttributes('FeatureRecord', {
|
||||||
|
index: featureListCount
|
||||||
|
});
|
||||||
|
featureRecord.appendChild(
|
||||||
|
createElementWithAttributes('FeatureTag', { value: 'calt' })
|
||||||
|
);
|
||||||
|
featureRecord.appendChild(dom.createElement('Feature'));
|
||||||
|
featureListDom.appendChild(featureRecord);
|
||||||
|
} else {
|
||||||
|
featureRecord = featureTag.parentNode;
|
||||||
|
}
|
||||||
|
featureDom = xpath.select('Feature', featureRecord, true);
|
||||||
|
|
||||||
|
const featureIndex = featureRecord.getAttribute('index');
|
||||||
|
|
||||||
|
// add feature to scriptlist for DFLT and latn if not present
|
||||||
|
addFeatureToScriptList('DFLT', featureIndex);
|
||||||
|
addFeatureToScriptList('latn', featureIndex);
|
||||||
|
|
||||||
|
lookupListDom = xpath.select('LookupList', gsubDom, true);
|
||||||
|
lookupIndex = xpath.select('count(Lookup)', lookupListDom, true);
|
||||||
|
|
||||||
|
const lookupDom = createElementWithAttributes('Lookup', {
|
||||||
|
index: lookupIndex++
|
||||||
|
});
|
||||||
|
appendChildren(
|
||||||
|
lookupDom,
|
||||||
|
createElementWithAttributes('LookupType', { value: 6 }),
|
||||||
|
createElementWithAttributes('LookupFlag', { value: 0 })
|
||||||
|
);
|
||||||
|
|
||||||
|
lookupListDom.appendChild(lookupDom);
|
||||||
|
|
||||||
|
// add contextual lookup to feature
|
||||||
|
const featureLookupCount = xpath.select(
|
||||||
|
'count(LookupListIndex)',
|
||||||
|
featureDom,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
featureDom.appendChild(
|
||||||
|
createElementWithAttributes('LookupListIndex', {
|
||||||
|
index: featureLookupCount,
|
||||||
|
value: lookupDom.getAttribute('index')
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
lookupDom.appendChild(buildBacktrackFirst(glyphs));
|
||||||
|
lookupDom.appendChild(buildLookAheadLast(glyphs));
|
||||||
|
lookupDom.appendChild(buildLigatureSubst(glyphs, ligature.glyph));
|
||||||
|
// remaining context
|
||||||
|
for (let n = glyphs.length - 2; n >= 0; n--) {
|
||||||
|
lookupDom.appendChild(
|
||||||
|
buildChainContext(getLIGs(n), [glyphs[n]], glyphs.slice(n + 1), 'LIG')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const finalizeGsubTables = () => {
|
||||||
|
// get current LookupList count
|
||||||
|
let newIndex = xpath.select('count(Lookup)', lookupListDom, true);
|
||||||
|
|
||||||
|
// append added lookups
|
||||||
|
Object.entries(substLookupMap).forEach(lookupEntry => {
|
||||||
|
// fixup existing lookups with new index
|
||||||
|
const lookup = lookupEntry[1];
|
||||||
|
const oldIndex = lookup.getAttribute('index');
|
||||||
|
const substLookups = xpath.select(
|
||||||
|
`//ChainContextSubst/SubstLookupRecord/LookupListIndex[@value="${oldIndex}"]`,
|
||||||
|
lookupListDom,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
substLookups.forEach(substLookup => {
|
||||||
|
substLookup.setAttribute('value', newIndex);
|
||||||
|
});
|
||||||
|
lookup.setAttribute('index', newIndex++);
|
||||||
|
lookupListDom.appendChild(lookup);
|
||||||
|
lookupListDom.appendChild(lookup);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//const dump = dom => console.log(format(serialize(dom)));
|
||||||
|
|
||||||
|
const addFeatureToScriptList = (tag, featureIndex) => {
|
||||||
|
scriptListDom = xpath.select('ScriptList', gsubDom, true);
|
||||||
|
const defaultLangSys = xpath.select(
|
||||||
|
`ScriptRecord/ScriptTag[@value="${tag}"]/../Script/DefaultLangSys`,
|
||||||
|
scriptListDom,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
const featureIndexNode = xpath.select(
|
||||||
|
`FeatureIndex[@value=${featureIndex}]`,
|
||||||
|
defaultLangSys,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
if (!featureIndexNode) {
|
||||||
|
const count = xpath.select('count(FeatureIndex)', defaultLangSys, true);
|
||||||
|
defaultLangSys.appendChild(
|
||||||
|
createElementWithAttributes('FeatureIndex', {
|
||||||
|
index: count,
|
||||||
|
value: featureIndex
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildBacktrackFirst = glyphs => {
|
||||||
|
// backtrack = first glyph
|
||||||
|
// input = first glphy
|
||||||
|
// lookAhead = [1..n]
|
||||||
|
return buildChainContext([glyphs[0]], [glyphs[0]], glyphs.slice(1));
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildLookAheadLast = glyphs => {
|
||||||
|
// backtrack = none
|
||||||
|
// input = first glyph
|
||||||
|
// lookAhead = [1..n] + [n]
|
||||||
|
return buildChainContext(
|
||||||
|
[],
|
||||||
|
[glyphs[0]],
|
||||||
|
[...glyphs.slice(1), glyphs.slice(-1)]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildLigatureSubst = (glyphs, lookup) => {
|
||||||
|
// backtrack = LIG * n-1
|
||||||
|
// input = last glyph
|
||||||
|
// lookAhead = none
|
||||||
|
return buildChainContext(
|
||||||
|
getLIGs(glyphs.length - 1),
|
||||||
|
glyphs.slice(-1),
|
||||||
|
[],
|
||||||
|
lookup
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLIGs = length => new Array(length).fill('LIG', 0, length);
|
||||||
|
|
||||||
|
const buildChainContext = (backtrack, input, lookAhead, substitute) => {
|
||||||
|
const chainDom = createElementWithAttributes('ChainContextSubst', {
|
||||||
|
index: chainIndex++,
|
||||||
|
Format: 3
|
||||||
|
});
|
||||||
|
|
||||||
|
buildCoverage(chainDom, 'BacktrackCoverage', backtrack);
|
||||||
|
buildCoverage(chainDom, 'InputCoverage', input);
|
||||||
|
buildCoverage(chainDom, 'LookAheadCoverage', lookAhead);
|
||||||
|
|
||||||
|
if (substitute) {
|
||||||
|
const lookup = getSubstLookup(input[0], substitute);
|
||||||
|
const listIndex = lookup.getAttribute('index');
|
||||||
|
const substLookupRecordDom = chainDom.appendChild(
|
||||||
|
createElementWithAttributes('SubstLookupRecord', { index: 0 })
|
||||||
|
);
|
||||||
|
appendChildren(
|
||||||
|
substLookupRecordDom,
|
||||||
|
createElementWithAttributes('SequenceIndex', { value: 0 }),
|
||||||
|
createElementWithAttributes('LookupListIndex', { value: listIndex })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return chainDom;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildCoverage = (chainDom, tagName, coverage) => {
|
||||||
|
if (coverage) {
|
||||||
|
coverage.forEach((glyph, i) => {
|
||||||
|
const coverageDom = createElementWithAttributes(tagName, {
|
||||||
|
index: i++,
|
||||||
|
Format: 1
|
||||||
|
});
|
||||||
|
coverageDom.appendChild(
|
||||||
|
createElementWithAttributes('Glyph', { value: glyph })
|
||||||
|
);
|
||||||
|
chainDom.appendChild(coverageDom);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSubstLookup = (input, output) => {
|
||||||
|
let lookupDom = substLookupMap[output];
|
||||||
|
|
||||||
|
if (!lookupDom) {
|
||||||
|
lookupDom = createElementWithAttributes('Lookup', {
|
||||||
|
index: Object.keys(substLookupMap).length + 1
|
||||||
|
});
|
||||||
|
lookupDom.appendChild(
|
||||||
|
createElementWithAttributes('LookupType', { value: 1 })
|
||||||
|
);
|
||||||
|
lookupDom.appendChild(
|
||||||
|
createElementWithAttributes('LookupFlag', { value: 0 })
|
||||||
|
);
|
||||||
|
lookupDom.appendChild(
|
||||||
|
createElementWithAttributes('SingleSubst', { index: 0, Format: 2 })
|
||||||
|
);
|
||||||
|
substLookupMap[output] = lookupDom;
|
||||||
|
}
|
||||||
|
const singleSubstDom = xpath.select('SingleSubst', lookupDom, true);
|
||||||
|
|
||||||
|
// look for input subst
|
||||||
|
const substitionDom = xpath.select(
|
||||||
|
`Substitution[@in="${input}"]`,
|
||||||
|
singleSubstDom,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
if (!substitionDom) {
|
||||||
|
singleSubstDom.appendChild(
|
||||||
|
createElementWithAttributes('Substitution', { in: input, out: output })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return lookupDom;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createElementWithAttributes = (tagName, attributes) => {
|
||||||
|
const element = dom.createElement(tagName);
|
||||||
|
Object.entries(attributes).forEach(attribute => {
|
||||||
|
element.setAttribute(attribute[0], attribute[1]);
|
||||||
|
});
|
||||||
|
return element;
|
||||||
|
};
|
||||||
|
|
||||||
|
const appendChildren = (node, ...children) => {
|
||||||
|
children.forEach(child => node.appendChild(child));
|
||||||
|
};
|
||||||
|
|
||||||
|
//const serialize = dom => new XMLSerializer().serializeToString(dom);
|
||||||
|
|
||||||
|
exports.buildGsubTables = buildGsubTables;
|
||||||
|
exports.finalizeGsubTables = finalizeGsubTables;
|
||||||
@@ -1,76 +1,193 @@
|
|||||||
const Promise = require('bluebird');
|
const fs = require('fs');
|
||||||
const fs = Promise.promisifyAll(require('fs'));
|
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
if (!os.EOL) {
|
if (!os.EOL) {
|
||||||
os.EOL = process.platform === 'win32' ? '\r\n' : '\n';
|
os.EOL = process.platform === 'win32' ? '\r\n' : '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const gsub = require('./gsub');
|
||||||
|
|
||||||
const xpath = require('xpath');
|
const xpath = require('xpath');
|
||||||
const { DOMParser, XMLSerializer } = require('xmldom');
|
const { DOMParser, XMLSerializer } = require('xmldom');
|
||||||
const format = require('xml-formatter');
|
const format = require('xml-formatter');
|
||||||
let fontName;
|
let fontName;
|
||||||
let ligFontName;
|
let ligFontName;
|
||||||
|
|
||||||
const regEx = /\.liga$/;
|
|
||||||
const regExBlankLines = /^(?=\n)$|^\s*/gm;
|
|
||||||
const regExWhitespace = /^\s+$/;
|
const regExWhitespace = /^\s+$/;
|
||||||
const NodeType = {};
|
const NodeType = {};
|
||||||
NodeType.TEXT_NODE = 3;
|
NodeType.TEXT_NODE = 3;
|
||||||
|
|
||||||
async function main() {
|
let dom;
|
||||||
|
function main() {
|
||||||
fontName = process.argv[2];
|
fontName = process.argv[2];
|
||||||
ligFontName = fontName.split('-').join('Lig-');
|
ligFontName = fontName.split('-').join('Lig-');
|
||||||
|
|
||||||
const srcFileName = `./original/${fontName}.ttx`;
|
const srcFileName = `./original/${fontName}.ttx`;
|
||||||
const dstFileName = `./build/${ligFontName}.ttx`;
|
|
||||||
console.log(`Reading original font file ${srcFileName}`);
|
console.log(`Reading original font file ${srcFileName}`);
|
||||||
const xml = await fs.readFileAsync(srcFileName, 'utf-8');
|
const xml = fs.readFileSync(srcFileName, 'utf-8');
|
||||||
const dom = new DOMParser().parseFromString(xml);
|
dom = new DOMParser().parseFromString(xml);
|
||||||
|
|
||||||
try {
|
const profiles = getProfiles();
|
||||||
await processPatch('names', patchNames, dom);
|
|
||||||
await processPatch('glyphs', patchGlyphs, dom);
|
// process settings (there may be more than one font to build)
|
||||||
await processPatch('gpos', patchGpos, dom);
|
profiles.forEach(profile => buildFont(profile));
|
||||||
await processPatch('gsub', patchGsub, dom);
|
|
||||||
await processPatch('hmtx', patchHmtx, dom);
|
|
||||||
//await processPatch('lookup', patchLookup, dom);
|
|
||||||
await processPatch('charstrings', patchCharStrings, dom);
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Writing ligature font file ${dstFileName}`);
|
|
||||||
await fs.writeFileAsync(dstFileName, format(serialize(dom)));
|
|
||||||
console.log('Done');
|
console.log('Done');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadConfigAsync(name) {
|
const buildFont = profile => {
|
||||||
|
// add suffix to dstFileName if present
|
||||||
|
const dstFileName = `./build/${ligFontName}${
|
||||||
|
profile.suffixWithLeadingHyphen
|
||||||
|
}.ttx`;
|
||||||
|
console.log(
|
||||||
|
`Building ligature font file ${dstFileName} name = ${profile.name}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const ligatures = sortLigatures(
|
||||||
|
fs
|
||||||
|
.readdirSync(`./ligature/${ligFontName}/glyphs`)
|
||||||
|
.filter(file => file != 'LIG.xml')
|
||||||
|
.map(file => file.replace('.liga.xml', ''))
|
||||||
|
)
|
||||||
|
.filter(name => !/\d+$/.test(name)) // skip alternates (ends with .#)
|
||||||
|
.filter(name => filterLigatures(name, profile.ligatures))
|
||||||
|
.map(name => mapLigatures(name, profile.ligatures));
|
||||||
|
|
||||||
|
const ligaturesWithLIG = [...ligatures, { name: 'LIG', glyph: 'LIG' }];
|
||||||
|
|
||||||
|
processPatch('names', patchNames, dom, ligatures, profile);
|
||||||
|
processPatch('glyphs', patchGlyphs, dom, ligaturesWithLIG);
|
||||||
|
processPatch('gsub', patchGsub, dom, ligatures);
|
||||||
|
processPatch('charstrings', patchCharStrings, dom, ligaturesWithLIG);
|
||||||
|
processPatch('hmtx', patchHmtx, dom);
|
||||||
|
|
||||||
|
console.log(`Writing ligature font file ${dstFileName}`);
|
||||||
|
fs.writeFileSync(dstFileName, format(serialize(dom)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProfiles = () => {
|
||||||
|
const profilePath = './original/profiles.ini';
|
||||||
|
// return default profiles if profile doesn't exist
|
||||||
|
if (!fs.existsSync(profilePath)) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'default',
|
||||||
|
suffix: '',
|
||||||
|
suffixWithLeadingSpace: '',
|
||||||
|
suffixWithLeadingHyphen: '',
|
||||||
|
ligatures: []
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const profiles = [];
|
||||||
|
let profile = null;
|
||||||
|
|
||||||
|
const content = fs.readFileSync(profilePath, 'utf-8');
|
||||||
|
content
|
||||||
|
.split(os.EOL)
|
||||||
|
.filter(line => /^#/.test(line) === false || line.length > 0)
|
||||||
|
.forEach(line => {
|
||||||
|
const ch = line.trim()[0];
|
||||||
|
if (ch === '[') {
|
||||||
|
let name = line.substr(1, line.indexOf(']') - 1);
|
||||||
|
profile = {
|
||||||
|
name,
|
||||||
|
suffix: name === 'default' ? '' : name,
|
||||||
|
suffixWithLeadingSpace: name === 'default' ? '' : ' ' + name,
|
||||||
|
suffixWithLeadingHyphen: name === 'default' ? '' : '-' + name,
|
||||||
|
ligatures: []
|
||||||
|
};
|
||||||
|
profiles.push(profile);
|
||||||
|
} else {
|
||||||
|
if (!profile) {
|
||||||
|
throw new Error('You must profile a profile name in []');
|
||||||
|
}
|
||||||
|
profile.ligatures.push(line);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return profiles;
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterLigatures = (name, ligatures) => {
|
||||||
|
// loop through ligatures and return if name applies or not
|
||||||
|
// skip if setting is !name
|
||||||
|
return ligatures.filter(ligature => ligature === '!' + name).length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapLigatures = (name, ligatures) => {
|
||||||
|
// return { ligature, glyph }
|
||||||
|
let entry = { name, glyph: name + '.liga' };
|
||||||
|
ligatures.forEach(ligature => {
|
||||||
|
const n = ligature.indexOf('=');
|
||||||
|
if (n > 0 && ligature.substr(0, n) === name) {
|
||||||
|
entry.glyph = ligature.substr(n + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return entry;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortLigatures = ligatures => {
|
||||||
|
// sort by most glyphs then alphabetically
|
||||||
|
const sorted = ligatures
|
||||||
|
.map(ligature => {
|
||||||
|
return { count: ligature.split('_').length + 1, ligature: ligature };
|
||||||
|
})
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
-compareProperty(a.count, b.count) || // sort by count descending
|
||||||
|
compareProperty(a.ligature, b.ligature) // then by ligature alphabetically
|
||||||
|
)
|
||||||
|
.map(entry => entry.ligature);
|
||||||
|
return sorted;
|
||||||
|
};
|
||||||
|
|
||||||
|
const compareProperty = (a, b) => {
|
||||||
|
if (typeof a === 'number') {
|
||||||
|
return a || b ? (!a ? -1 : !b ? 1 : a === b ? 0 : a < b ? -1 : 1) : 0;
|
||||||
|
} else {
|
||||||
|
return a || b ? (!a ? -1 : !b ? 1 : a.localeCompare(b)) : 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadXml = name => {
|
||||||
const fileName = `./ligature/${ligFontName}/${name}.xml`;
|
const fileName = `./ligature/${ligFontName}/${name}.xml`;
|
||||||
const xml = await fs.readFileAsync(fileName, 'utf-8');
|
const xml = fs.readFileSync(fileName, 'utf-8');
|
||||||
return new DOMParser().parseFromString(xml);
|
return new DOMParser().parseFromString(xml);
|
||||||
}
|
};
|
||||||
|
|
||||||
async function processPatch(name, patchFunc, dom) {
|
const processPatch = (name, patchFunc, dom, ligatures, profile) => {
|
||||||
console.log(`Patching ${name}`);
|
console.log(`Patching ${name}`);
|
||||||
await patchFunc(dom);
|
patchFunc(dom, ligatures, profile);
|
||||||
}
|
};
|
||||||
|
|
||||||
async function patchNames(dom) {
|
const PlatformId = {
|
||||||
const configDom = await loadConfigAsync('names');
|
mac: 1,
|
||||||
|
win: 3
|
||||||
|
};
|
||||||
|
|
||||||
const names = xpath.select('/name/namerecord', configDom);
|
const NameId = {
|
||||||
const targetName = xpath.select('/ttFont/name', dom, true);
|
familyName: 1,
|
||||||
|
fontStyle: 2,
|
||||||
|
uniqueId: 3,
|
||||||
|
fullName: 4,
|
||||||
|
version: 5,
|
||||||
|
postscriptName: 6,
|
||||||
|
windowsFamilyName: 16,
|
||||||
|
fontStyleName: 17
|
||||||
|
};
|
||||||
|
|
||||||
// get font and family name
|
const patchNames = (dom, ligatures, profile) => {
|
||||||
const familyName = getTextNode(
|
const names = JSON.parse(
|
||||||
configDom,
|
fs.readFileSync(`./ligature/${ligFontName}/names.json`)
|
||||||
'/name/namerecord[@nameID="1" and @platformID="1"]'
|
|
||||||
);
|
|
||||||
const fullName = getTextNode(
|
|
||||||
configDom,
|
|
||||||
'/name/namerecord[@nameID="4" and @platformID="1"]'
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [name, style] = names.fontName.split('-');
|
||||||
|
const fontName = `${name}${profile.suffixWithLeadingHyphen}-${style}`;
|
||||||
|
const familyName = `${names.familyName}${profile.suffixWithLeadingSpace}`;
|
||||||
|
const fullName = `${familyName} ${names.fontStyle}`;
|
||||||
|
const uniqueId = `${names.foundry}: ${fullName}: ${names.version}`;
|
||||||
// patch CFFFont
|
// patch CFFFont
|
||||||
const cffFont = xpath.select('/ttFont/CFF/CFFFont', dom, true);
|
const cffFont = xpath.select('/ttFont/CFF/CFFFont', dom, true);
|
||||||
|
|
||||||
@@ -79,10 +196,24 @@ async function patchNames(dom) {
|
|||||||
setAttribute(cffFont, 'FamilyName', 'value', familyName);
|
setAttribute(cffFont, 'FamilyName', 'value', familyName);
|
||||||
|
|
||||||
// update existing names with new names
|
// update existing names with new names
|
||||||
names.forEach(node => {
|
updateName(PlatformId.mac, NameId.familyName, familyName);
|
||||||
const nameId = node.getAttribute('nameID');
|
updateName(PlatformId.mac, NameId.fontStyle, names.fontStyle);
|
||||||
const platformId = node.getAttribute('platformID');
|
updateName(PlatformId.mac, NameId.uniqueId, uniqueId);
|
||||||
|
updateName(PlatformId.mac, NameId.fullName, fullName);
|
||||||
|
updateName(PlatformId.mac, NameId.postscriptName, fontName);
|
||||||
|
updateName(PlatformId.mac, NameId.windowsFamilyName, familyName);
|
||||||
|
updateName(PlatformId.mac, NameId.fontStyleName, names.fontStyle);
|
||||||
|
|
||||||
|
updateName(PlatformId.win, NameId.familyName, familyName);
|
||||||
|
updateName(PlatformId.win, NameId.fontStyle, names.windowsFontStyle);
|
||||||
|
updateName(PlatformId.win, NameId.uniqueId, uniqueId);
|
||||||
|
updateName(PlatformId.win, NameId.fullName, fullName);
|
||||||
|
updateName(PlatformId.win, NameId.postscriptName, fontName);
|
||||||
|
updateName(PlatformId.win, NameId.windowsFamilyName, familyName);
|
||||||
|
updateName(PlatformId.win, NameId.fontStyleName, names.fontStyle);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateName = (platformId, nameId, value) => {
|
||||||
// search for namerecord in target dom and replace with this one or append node
|
// search for namerecord in target dom and replace with this one or append node
|
||||||
const target = xpath.select(
|
const target = xpath.select(
|
||||||
`/ttFont/name/namerecord[@nameID="${nameId}" and @platformID="${platformId}"]`,
|
`/ttFont/name/namerecord[@nameID="${nameId}" and @platformID="${platformId}"]`,
|
||||||
@@ -90,82 +221,40 @@ async function patchNames(dom) {
|
|||||||
true
|
true
|
||||||
);
|
);
|
||||||
if (target) {
|
if (target) {
|
||||||
target.parentNode.replaceChild(node, target);
|
target.childNodes[0].textContent = value;
|
||||||
} else {
|
|
||||||
targetName.appendChild(node);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
async function patchGlyphs(dom) {
|
const patchGlyphs = (dom, ligatures) => {
|
||||||
const configDom = await loadConfigAsync('../glyphs');
|
|
||||||
// get number of glyphs in target dom
|
// get number of glyphs in target dom
|
||||||
const targetGlyphs = xpath.select('/ttFont/GlyphOrder', dom, true);
|
const targetGlyphs = xpath.select('/ttFont/GlyphOrder', dom, true);
|
||||||
const glyphsCount = xpath.select('count(GlyphID)', targetGlyphs, true);
|
const glyphsCount = xpath.select('count(GlyphID)', targetGlyphs, true);
|
||||||
|
|
||||||
|
// only import glyphs specified
|
||||||
let n = glyphsCount;
|
let n = glyphsCount;
|
||||||
// get ligature glyphs
|
// get ligature glyphs
|
||||||
const glyphs = xpath.select('/GlyphOrder/GlyphID', configDom);
|
ligatures.forEach(ligature => {
|
||||||
glyphs.forEach(node => {
|
targetGlyphs.appendChild(
|
||||||
node.setAttribute('id', n++);
|
createElementWithAttributes('GlyphID', { id: n++, name: ligature.glyph })
|
||||||
targetGlyphs.appendChild(node);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// update glyph count
|
// update glyph count
|
||||||
setAttribute(dom, '/ttFont/maxp/numGlyphs', 'value', n);
|
setAttribute(dom, '/ttFont/maxp/numGlyphs', 'value', n);
|
||||||
}
|
|
||||||
|
|
||||||
async function patchGpos(dom) {
|
|
||||||
const newGpos = await loadConfigAsync('gpos');
|
|
||||||
const oldGpos = xpath.select('/ttFont/GPOS', dom, true);
|
|
||||||
dom.documentElement.replaceChild(newGpos, oldGpos);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function patchGsub(dom) {
|
|
||||||
const newGsub = await loadConfigAsync('gsub');
|
|
||||||
|
|
||||||
// loop through Substituion/Ligature nodes and remap chars
|
|
||||||
remap(newGsub, dom, '//Substitution', 'out');
|
|
||||||
remap(newGsub, dom, '//Ligature', 'glyph');
|
|
||||||
|
|
||||||
const oldGsub = xpath.select('/ttFont/GSUB', dom, true);
|
|
||||||
dom.documentElement.replaceChild(newGsub, oldGsub);
|
|
||||||
}
|
|
||||||
|
|
||||||
const remap = (patchDom, cmapDom, path, attr) => {
|
|
||||||
const cmap = [];
|
|
||||||
let nodes = xpath.select(path, patchDom);
|
|
||||||
nodes.forEach(n => {
|
|
||||||
const out = n.getAttribute(attr);
|
|
||||||
if (out.startsWith('uni')) {
|
|
||||||
// get cmap entry
|
|
||||||
const code = '0x' + out.replace(/uni0*/g, '').toLowerCase();
|
|
||||||
let name = cmap[code];
|
|
||||||
if (!name) {
|
|
||||||
const map = xpath.select(
|
|
||||||
`/ttFont/cmap/cmap_format_4/map[@code="${code}"]`,
|
|
||||||
cmapDom,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
name = map == null ? out : map.getAttribute('name');
|
|
||||||
cmap[code] = name;
|
|
||||||
}
|
|
||||||
n.setAttribute(attr, name);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
async function patchHmtx(dom) {
|
const patchGsub = (dom, ligatures) => {
|
||||||
const hmtxDom = await loadConfigAsync('hmtx');
|
ligatures.forEach(ligature => {
|
||||||
const targetHmtx = xpath.select('/ttFont/hmtx', dom, true);
|
// build gsub tables
|
||||||
|
gsub.buildGsubTables(dom, ligature);
|
||||||
const mtx = xpath.select('/hmtx/mtx', hmtxDom);
|
});
|
||||||
mtx.forEach(node => targetHmtx.appendChild(node));
|
gsub.finalizeGsubTables();
|
||||||
|
};
|
||||||
|
|
||||||
|
const patchHmtx = dom => {
|
||||||
const mtxCount = xpath.select('count(/ttFont/hmtx/mtx)', dom, true);
|
const mtxCount = xpath.select('count(/ttFont/hmtx/mtx)', dom, true);
|
||||||
setAttribute(dom, '/ttFont/hhea/numberOfHMetrics', 'value', mtxCount);
|
setAttribute(dom, '/ttFont/hhea/numberOfHMetrics', 'value', mtxCount);
|
||||||
|
|
||||||
const configDom = await loadConfigAsync('config');
|
const configDom = loadXml('config');
|
||||||
copyConfigAttribute(dom, configDom, '/ttFont/head/xMin', 'value');
|
copyConfigAttribute(dom, configDom, '/ttFont/head/xMin', 'value');
|
||||||
copyConfigAttribute(dom, configDom, '/ttFont/head/yMin', 'value');
|
copyConfigAttribute(dom, configDom, '/ttFont/head/yMin', 'value');
|
||||||
copyConfigAttribute(dom, configDom, '/ttFont/head/xMax', 'value');
|
copyConfigAttribute(dom, configDom, '/ttFont/head/xMax', 'value');
|
||||||
@@ -179,159 +268,95 @@ async function patchHmtx(dom) {
|
|||||||
'/ttFont/CFF/CFFFont/Private/nominalWidthX',
|
'/ttFont/CFF/CFFFont/Private/nominalWidthX',
|
||||||
'value'
|
'value'
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
async function patchLookup(dom) {
|
|
||||||
const configDom = await loadConfigAsync('../lookup');
|
|
||||||
const featureList = xpath.select('/ttFont/GSUB/FeatureList', dom, true);
|
|
||||||
|
|
||||||
const featuresCount = xpath.select(
|
|
||||||
'count(/ttFont/GSUB/FeatureList/FeatureRecord)',
|
|
||||||
dom,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
const featureRecord = dom.createElement('FeatureRecord');
|
|
||||||
featureRecord.setAttribute('index', featuresCount);
|
|
||||||
const featureTag = dom.createElement('FeatureTag');
|
|
||||||
featureTag.setAttribute('value', 'liga');
|
|
||||||
const feature = dom.createElement('Feature');
|
|
||||||
featureRecord.appendChild(featureTag);
|
|
||||||
featureRecord.appendChild(feature);
|
|
||||||
featureList.appendChild(featureRecord);
|
|
||||||
|
|
||||||
const lookupCount = xpath.select(
|
|
||||||
'count(/ttFont/GSUB/LookupList/Lookup)',
|
|
||||||
dom,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
const lookupListIndex = dom.createElement('LookupListIndex');
|
|
||||||
lookupListIndex.setAttribute('index', '0');
|
|
||||||
lookupListIndex.setAttribute('value', lookupCount);
|
|
||||||
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 patchCharStrings = (dom, ligatures) => {
|
||||||
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
|
|
||||||
const lookupList = xpath.select('/ttFont/GSUB/LookupList', dom, true);
|
|
||||||
const newLookup = xpath.select('/LookupList/Lookup', configDom, true);
|
|
||||||
newLookup.setAttribute('index', lookupCount);
|
|
||||||
lookupList.appendChild(newLookup);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function patchCharStrings(dom) {
|
|
||||||
const configDom = await loadConfigAsync('charstrings');
|
|
||||||
const nameDom = await loadConfigAsync('names');
|
|
||||||
|
|
||||||
// get font and family name
|
|
||||||
const familyName = getTextNode(
|
|
||||||
nameDom,
|
|
||||||
'/name/namerecord[@nameID="1" and @platformID="1"]'
|
|
||||||
);
|
|
||||||
const fullName = getTextNode(
|
|
||||||
nameDom,
|
|
||||||
'/name/namerecord[@nameID="4" and @platformID="1"]'
|
|
||||||
);
|
|
||||||
|
|
||||||
// patch CFFFont
|
// patch CFFFont
|
||||||
const cffFont = xpath.select('/ttFont/CFF/CFFFont', dom, true);
|
const cffFont = xpath.select('/ttFont/CFF/CFFFont', dom, true);
|
||||||
|
const targetHmtx = xpath.select('/ttFont/hmtx', dom, true);
|
||||||
|
|
||||||
cffFont.setAttribute('name', ligFontName);
|
|
||||||
setAttribute(cffFont, 'FullName', 'value', fullName);
|
|
||||||
setAttribute(cffFont, 'FamilyName', 'value', familyName);
|
|
||||||
|
|
||||||
const charStrings = xpath.select('/CharStrings/CharString', configDom);
|
|
||||||
const targetCharStrings = xpath.select('CharStrings', cffFont, true);
|
const targetCharStrings = xpath.select('CharStrings', cffFont, true);
|
||||||
|
const targetSubrs = xpath.select(
|
||||||
|
'/ttFont/CFF/CFFFont/Private/Subrs',
|
||||||
|
dom,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
const targetGsubrs = xpath.select('/ttFont/CFF/GlobalSubrs', dom, true);
|
||||||
|
const fingerprints = {};
|
||||||
|
ligatures.forEach(ligature => {
|
||||||
|
console.log(
|
||||||
|
`* ${ligature.name}${
|
||||||
|
ligature.name + '.liga' === ligature.glyph || ligature.name === 'LIG'
|
||||||
|
? ''
|
||||||
|
: ' => ' + ligature.glyph
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
const glyphDom = loadXml(`glyphs/${ligature.glyph}`).documentElement;
|
||||||
|
const node = xpath.select('/Glyph/CharString', glyphDom, true);
|
||||||
|
node.setAttribute('name', ligature.glyph);
|
||||||
|
|
||||||
const subrs = {
|
const subrs = {
|
||||||
map: [],
|
sourcePath: '/Glyph/Subrs',
|
||||||
source: await loadConfigAsync('subrs'),
|
target: targetSubrs
|
||||||
target: xpath.select('/ttFont/CFF/CFFFont/Private/Subrs', dom, true)
|
|
||||||
};
|
};
|
||||||
const gsubrs = {
|
const gsubrs = {
|
||||||
map: [],
|
sourcePath: '/Glyph/GlobalSubrs',
|
||||||
source: await loadConfigAsync('gsubrs'),
|
target: targetGsubrs
|
||||||
target: xpath.select('/ttFont/CFF/GlobalSubrs', dom, true)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
charStrings.forEach(node => {
|
patchCharStringSubrs(glyphDom, node, fingerprints, subrs, gsubrs);
|
||||||
patchCharStringSubrs(node, subrs, gsubrs);
|
|
||||||
targetCharStrings.appendChild(node);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const patchCharStringSubrs = (node, subrs, gsubrs) => {
|
targetCharStrings.appendChild(node);
|
||||||
|
targetHmtx.appendChild(
|
||||||
|
createElementWithAttributes('mtx', {
|
||||||
|
name: ligature.glyph,
|
||||||
|
width: glyphDom.getAttribute('width'),
|
||||||
|
lsb: glyphDom.getAttribute('lsb')
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const patchCharStringSubrs = (glyphDom, node, fingerprints, subrs, gsubrs) => {
|
||||||
// check for callsubr/callgsubr
|
// check for callsubr/callgsubr
|
||||||
|
|
||||||
const lines = node.childNodes[0].textContent.split(/\r|\r\n|\n/g);
|
const lines = node.childNodes[0].textContent.split(/\r|\r\n|\n/g);
|
||||||
const newLines = [];
|
const newLines = [];
|
||||||
let patched = false;
|
|
||||||
lines.forEach(line => {
|
lines.forEach(line => {
|
||||||
const matches = line.match(/(.*?)(-?\d+) (callsubr|callgsubr)$/);
|
if (line.trim().length === 0) return;
|
||||||
|
const matches = line.match(/(.*?)\{([0-9a-z]+)\} (callsubr|callgsubr)$/);
|
||||||
if (matches != null) {
|
if (matches != null) {
|
||||||
const { map, source, target } =
|
const { sourcePath, target } = matches[3] === 'callsubr' ? subrs : gsubrs;
|
||||||
matches[3] === 'callsubr' ? subrs : gsubrs;
|
const fingerprint = matches[2];
|
||||||
const index = matches[2];
|
let newIndex = fingerprints[fingerprint];
|
||||||
let newIndex = 0;
|
if (!newIndex) {
|
||||||
if (!map[index]) {
|
|
||||||
// find subr in source dom and copy to target dom
|
|
||||||
const srcIndex = parseInt(index) + 107;
|
|
||||||
const srcSubr = xpath.select(
|
const srcSubr = xpath.select(
|
||||||
`//CharString[@index="${srcIndex}"]`,
|
`/${sourcePath}/CharString[@fingerprint="${fingerprint}"]`,
|
||||||
source,
|
glyphDom,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
// patch up source in case it also has any callsubrs
|
|
||||||
patchCharStringSubrs(srcSubr, subrs, gsubrs);
|
|
||||||
|
|
||||||
// append subr to target dom and get new index
|
// append subr to target dom and get new index
|
||||||
newIndex = xpath.select('count(CharString)', target, true);
|
newIndex = xpath.select('count(CharString)', target, true);
|
||||||
srcSubr.setAttribute('index', newIndex);
|
|
||||||
target.appendChild(srcSubr);
|
const clone = srcSubr.cloneNode(true);
|
||||||
|
clone.setAttribute('index', newIndex);
|
||||||
|
target.appendChild(clone);
|
||||||
|
|
||||||
// add new index to map and rewrite call
|
// add new index to map and rewrite call
|
||||||
newIndex = newIndex - 107;
|
newIndex = newIndex - 107;
|
||||||
map[index] = newIndex;
|
fingerprints[fingerprint] = newIndex;
|
||||||
} else {
|
|
||||||
newIndex = map[index];
|
// patch up source in case it also has any callsubrs
|
||||||
|
patchCharStringSubrs(glyphDom, clone, fingerprints, subrs, gsubrs);
|
||||||
}
|
}
|
||||||
|
|
||||||
// rewrite line with new subr index
|
// rewrite line with new subr index
|
||||||
line = `${matches[1]}${newIndex} ${matches[3]}`;
|
line = `${matches[1]}${newIndex} ${matches[3]}`;
|
||||||
patched = true;
|
|
||||||
}
|
}
|
||||||
newLines.push(line);
|
newLines.push(line);
|
||||||
});
|
});
|
||||||
if (patched) {
|
|
||||||
node.childNodes[0].textContent = newLines.join(os.EOL);
|
node.childNodes[0].textContent = newLines.join(os.EOL);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const setAttribute = (parent, path, name, value) => {
|
const setAttribute = (parent, path, name, value) => {
|
||||||
@@ -344,13 +369,15 @@ const copyConfigAttribute = (dom, configDom, path, name) => {
|
|||||||
setAttribute(dom, path, name, value);
|
setAttribute(dom, path, name, value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTextNode = (dom, path) => {
|
const createElementWithAttributes = (tagName, attributes) => {
|
||||||
var node = xpath.select(path, dom, true);
|
const element = dom.createElement(tagName);
|
||||||
return node && node.childNodes.length
|
Object.entries(attributes).forEach(attribute => {
|
||||||
? node.childNodes[0].nodeValue.trim()
|
element.setAttribute(attribute[0], attribute[1]);
|
||||||
: '';
|
});
|
||||||
|
return element;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//const dump = dom => console.log(serialize(dom));
|
||||||
const serialize = dom =>
|
const serialize = dom =>
|
||||||
new XMLSerializer().serializeToString(dom, false, node => {
|
new XMLSerializer().serializeToString(dom, false, node => {
|
||||||
if (node.nodeType === NodeType.TEXT_NODE) {
|
if (node.nodeType === NodeType.TEXT_NODE) {
|
||||||
|
|||||||
Reference in New Issue
Block a user