Files
operator-mono-lig/index.js
T
Emanuele Ballarin 660c719779 Fix font family names in PS and TTF metadata
Fix font family names in PS and TTF metadata, as already defined in original font and master branch
2019-07-24 09:57:20 -04:00

407 lines
12 KiB
JavaScript

const fs = require('fs');
const os = require('os');
if (!os.EOL) {
os.EOL = process.platform === 'win32' ? '\r\n' : '\n';
}
const gsub = require('./gsub');
const xpath = require('xpath');
const { DOMParser, XMLSerializer } = require('xmldom');
const format = require('xml-formatter');
let fontName;
let ligFontName;
const regExWhitespace = /^\s+$/;
const NodeType = {};
NodeType.TEXT_NODE = 3;
let dom;
function main() {
fontName = process.argv[2];
ligFontName = fontName.split('-').join('Lig-');
const srcFileName = `./original/${fontName}.ttx`;
console.log(`Reading original font file ${srcFileName}`);
const xml = fs.readFileSync(srcFileName, 'utf-8');
dom = new DOMParser().parseFromString(xml);
const profiles = getProfiles();
// process settings (there may be more than one font to build)
profiles.forEach(profile => buildFont(profile));
console.log('Done');
}
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 xml = fs.readFileSync(fileName, 'utf-8');
return new DOMParser().parseFromString(xml);
};
const processPatch = (name, patchFunc, dom, ligatures, profile) => {
console.log(`Patching ${name}`);
patchFunc(dom, ligatures, profile);
};
const PlatformId = {
mac: 1,
win: 3
};
const NameId = {
familyName: 1,
fontStyle: 2,
uniqueId: 3,
fullName: 4,
version: 5,
postscriptName: 6,
windowsFamilyName: 16,
fontStyleName: 17
};
const patchNames = (dom, ligatures, profile) => {
const names = JSON.parse(
fs.readFileSync(`./ligature/${ligFontName}/names.json`)
);
const [name, style] = names.fontName.split('-');
var [familyStyle, fullNameStyleTry] = names.fontStyle.split(' ');
let fullNameStyle
if (fullNameStyleTry) {
fullNameStyle = fullNameStyleTry;
} else {
fullNameStyle = "";
}
const fontName = `${name}${profile.suffixWithLeadingHyphen}-${style}`;
const familyNamePlat = `${ names.familyName }${ profile.suffixWithLeadingSpace }`;
const familyName = `${ familyNamePlat } ${ familyStyle }`;
const fullName = `${ familyName } ${ fullNameStyle }`;
const uniqueId = `${names.foundry}: ${fullName}: ${names.version}`;
// patch CFFFont
const cffFont = xpath.select('/ttFont/CFF/CFFFont', dom, true);
cffFont.setAttribute('name', ligFontName);
setAttribute(cffFont, 'FullName', 'value', fullName);
setAttribute(cffFont, 'FamilyName', 'value', familyNamePlat);
// update existing names with new names
updateName(PlatformId.mac, NameId.familyName, familyName);
updateName(PlatformId.mac, NameId.fontStyle, names.fontStyle);
updateName(PlatformId.mac, NameId.uniqueId, uniqueId);
updateName(PlatformId.mac, NameId.fullName, fullName);
updateName(PlatformId.mac, NameId.postscriptName, fontName);
updateName(PlatformId.mac, NameId.windowsFamilyName, familyNamePlat);
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, familyNamePlat);
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
const target = xpath.select(
`/ttFont/name/namerecord[@nameID="${nameId}" and @platformID="${platformId}"]`,
dom,
true
);
if (target) {
target.childNodes[0].textContent = value;
}
};
const patchGlyphs = (dom, ligatures) => {
// get number of glyphs in target dom
const targetGlyphs = xpath.select('/ttFont/GlyphOrder', dom, true);
const glyphsCount = xpath.select('count(GlyphID)', targetGlyphs, true);
// only import glyphs specified
let n = glyphsCount;
// get ligature glyphs
ligatures.forEach(ligature => {
targetGlyphs.appendChild(
createElementWithAttributes('GlyphID', { id: n++, name: ligature.glyph })
);
});
// update glyph count
setAttribute(dom, '/ttFont/maxp/numGlyphs', 'value', n);
};
const patchGsub = (dom, ligatures) => {
ligatures.forEach(ligature => {
// build gsub tables
gsub.buildGsubTables(dom, ligature);
});
gsub.finalizeGsubTables();
};
const patchHmtx = dom => {
const mtxCount = xpath.select('count(/ttFont/hmtx/mtx)', dom, true);
setAttribute(dom, '/ttFont/hhea/numberOfHMetrics', 'value', mtxCount);
const configDom = loadXml('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'
);
};
const patchCharStrings = (dom, ligatures) => {
// patch CFFFont
const cffFont = xpath.select('/ttFont/CFF/CFFFont', dom, true);
const targetHmtx = xpath.select('/ttFont/hmtx', dom, 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 = {
sourcePath: '/Glyph/Subrs',
target: targetSubrs
};
const gsubrs = {
sourcePath: '/Glyph/GlobalSubrs',
target: targetGsubrs
};
patchCharStringSubrs(glyphDom, node, fingerprints, 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
const lines = node.childNodes[0].textContent.split(/\r|\r\n|\n/g);
const newLines = [];
lines.forEach(line => {
if (line.trim().length === 0) return;
const matches = line.match(/(.*?)\{([0-9a-z]+)\} (callsubr|callgsubr)$/);
if (matches != null) {
const { sourcePath, target } = matches[3] === 'callsubr' ? subrs : gsubrs;
const fingerprint = matches[2];
let newIndex = fingerprints[fingerprint];
if (!newIndex) {
const srcSubr = xpath.select(
`/${sourcePath}/CharString[@fingerprint="${fingerprint}"]`,
glyphDom,
true
);
// append subr to target dom and get new index
newIndex = xpath.select('count(CharString)', target, true);
const clone = srcSubr.cloneNode(true);
clone.setAttribute('index', newIndex);
target.appendChild(clone);
// add new index to map and rewrite call
newIndex = newIndex - 107;
fingerprints[fingerprint] = newIndex;
// patch up source in case it also has any callsubrs
patchCharStringSubrs(glyphDom, clone, fingerprints, subrs, gsubrs);
}
// rewrite line with new subr index
line = `${matches[1]}${newIndex} ${matches[3]}`;
}
newLines.push(line);
});
node.childNodes[0].textContent = newLines.join(os.EOL);
};
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 createElementWithAttributes = (tagName, attributes) => {
const element = dom.createElement(tagName);
Object.entries(attributes).forEach(attribute => {
element.setAttribute(attribute[0], attribute[1]);
});
return element;
};
//const dump = dom => console.log(serialize(dom));
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|\r\n|\n/g)
.filter(s => /\S+/.test(s))
.map(s => s.replace(/^\s+/g, ''))
.join(os.EOL);
return node.ownerDocument.createTextNode(data);
}
return node;
});
main();