mirror of
https://github.com/kiliman/operator-mono-lig.git
synced 2026-08-12 02:10:52 +02:00
660c719779
Fix font family names in PS and TTF metadata, as already defined in original font and master branch
407 lines
12 KiB
JavaScript
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();
|