const fs = require('fs'); const os = require('os'); if (!os.EOL) { os.EOL = process.platform === 'win32' ? '\r\n' : '\n'; } const xpath = require('xpath'); const { DOMParser, XMLSerializer } = require('xmldom'); const format = require('xml-formatter'); const hash = require('hash.js'); let fontName; const regEx = /^LIG$|\.liga$/; const NodeType = {}; NodeType.TEXT_NODE = 3; function main() { fontName = process.argv[2]; 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}`); const xml = fs.readFileSync(srcFileName, 'utf-8'); const dom = new DOMParser().parseFromString(xml); extractAndWrite('config', extractConfig, dom); extractCharStrings(dom); extractAndWrite('subrs', extractSubrs, dom); extractAndWrite('gsubrs', extractGlobalSubrs, dom); console.log('Done'); } function extractAndWrite(name, func, dom) { const newDom = func(dom); console.log('Finished extracting ' + name); const folder = `./ligature/${fontName}`; const fileName = `./${folder}/${name}.xml`; if (!fs.existsSync(folder)) { fs.mkdirSync(folder); } fs.writeFileSync(fileName, format(serialize(newDom))); console.log('Finished writing ' + name); } //const dump = dom => console.log(serialize(dom)); const serialize = dom => new XMLSerializer().serializeToString(dom); const extractConfig = dom => { const newDom = new DOMParser().parseFromString(''); const head = xpath.select('/ttFont/head', dom, true); const hhea = xpath.select('/ttFont/hhea', dom, true); const bbox = extractFromPath('/ttFont/CFF/CFFFont/FontBBox', dom); const widthX = extractFromPath( '/ttFont/CFF/CFFFont/Private/nominalWidthX', dom ); newDom.documentElement.appendChild(head); newDom.documentElement.appendChild(hhea); newDom.documentElement.appendChild(bbox); newDom.documentElement.appendChild(widthX); return newDom; }; const extractFromPath = (path, dom) => { const parentPath = path.substring(0, path.lastIndexOf('/')); const childPath = parentPath.substring(path.indexOf('/', 1)); const elements = parentPath.split('/').slice(2); let xml = ''; elements.forEach(e => (xml += `<${e}>`)); elements.reverse().forEach(e => (xml += ``)); const newDom = new DOMParser().parseFromString(xml); const node = xpath.select(path, dom, true); const parentNode = xpath.select(childPath, newDom, true); parentNode.appendChild(node); return newDom.documentElement; }; const extractCharStrings = dom => { const charStrings = xpath.select( '/ttFont/CFF/CFFFont/CharStrings/CharString', dom ); charStrings .filter(node => regEx.test(node.getAttribute('name'))) .forEach(node => writeGlyphData(dom, node)); console.log('Finished writing charstrings'); }; const writeGlyphData = (dom, node) => { const newDom = new DOMParser().parseFromString('').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 subrs = xpath.select('/ttFont/CFF/CFFFont/Private/Subrs', dom, true); return subrs; }; const extractGlobalSubrs = dom => { const subrs = xpath.select('/ttFont/CFF/GlobalSubrs', dom, true); return subrs; }; main();