export-kerning
Version:
Export kernings of a Opentype font
154 lines (133 loc) • 5.16 kB
JavaScript
import fs from 'fs';
import opentype from 'opentype.js';
import { Command } from 'commander';
const program = new Command();
program
.name('export-kerning')
.description('Export kerning information from OpenType fonts')
.argument('<font-file>', 'Path to the font file')
.option('-o, --output <file>', 'Output file path', 'output/kerning.json')
.option('-t, --text <text>', 'Text to analyze for kerning pairs', null)
.option('-r, --ranges <ranges>', 'Unicode ranges to analyze', null)
.action(async (fontPath, options) => {
try {
const font = await opentype.load(fontPath);
// Get all glyphs that map to characters
const supportedChars = getSupportedChars(font);
// Export all kerning pairs
const allKerningPairs = extractKerningPairs(font, options.ranges || supportedChars);
// Save optimized format only
const optimizedOutput = options.output//.replace(/\.[^.]+$/, '-optimized$&');
const optimizedData = {
unitsPerEm: font.unitsPerEm,
kerningPairs: convertToSVGStyleOptimizedFormat(allKerningPairs)
};
const optimizedJson = JSON.stringify(optimizedData);
fs.writeFileSync(optimizedOutput, optimizedJson);
console.log(`✔️ Optimized kerning pairs saved to ${optimizedOutput}`);
// If text is provided, also analyze it
if (options.text) {
const usedKerningPairs = extractUsedKerningPairs(options.text, font);
const subsetOutput = options.output.replace(/\.[^.]+$/, '-subset$&');
const optimizedSubsetJson = JSON.stringify({
unitsPerEm: font.unitsPerEm,
kerningPairs: convertToSVGStyleOptimizedFormat(usedKerningPairs)
});
fs.writeFileSync(subsetOutput, optimizedSubsetJson);
console.log(`✔️ Optimized used kerning subset saved to ${subsetOutput}`);
}
} catch (err) {
console.error('Error:', err.message);
process.exit(1);
}
});
program.parse();
function parseUnicodeRanges(ranges) {
const chars = new Set();
ranges.split(',').forEach(range => {
range = range.trim();
if (range.includes('-')) {
// Handle range like U+0000-007F
const [start, end] = range.replace('U+', '').split('-').map(hex => parseInt(hex, 16));
for (let i = start; i <= end; i++) {
chars.add(String.fromCharCode(i));
}
} else {
// Handle single character like U+0153
const code = parseInt(range.replace('U+', ''), 16);
chars.add(String.fromCharCode(code));
}
});
return Array.from(chars);
}
function extractKerningPairs(font, charsOrRanges) {
const pairs = {};
const chars = typeof charsOrRanges === 'string' ? parseUnicodeRanges(charsOrRanges) : charsOrRanges;
for (let i = 0; i < chars.length; i++) {
for (let j = 0; j < chars.length; j++) {
const left = font.charToGlyph(chars[i]);
const right = font.charToGlyph(chars[j]);
const kern = font.getKerningValue(left, right);
if (kern !== 0) {
pairs[chars[i] + chars[j]] = kern;
}
}
}
return pairs;
}
function extractUsedKerningPairs(text, font) {
const pairs = {};
const words = text.split(/\s+/); // split by whitespace
for (const word of words) {
for (let i = 0; i < word.length - 1; i++) {
const left = word[i];
const right = word[i + 1];
const leftGlyph = font.charToGlyph(left);
const rightGlyph = font.charToGlyph(right);
const kern = font.getKerningValue(leftGlyph, rightGlyph);
if (kern !== 0) {
pairs[left + right] = kern;
}
}
}
return pairs;
}
function getSupportedChars(font) {
const cmap = font.tables.cmap.glyphIndexMap;
return Object.keys(cmap).map(code => String.fromCharCode(code));
}
function convertToSVGStyleOptimizedFormat(kerningPairs) {
// Step 1: Group by value
const valueGroups = {};
for (const [pair, value] of Object.entries(kerningPairs)) {
if (!valueGroups[value]) valueGroups[value] = [];
valueGroups[value].push(pair);
}
// Step 2: For each value, group by left and right glyphs
const result = [];
for (const [value, pairs] of Object.entries(valueGroups)) {
// Map: left glyph -> set of right glyphs
const leftToRights = {};
for (const pair of pairs) {
const left = pair[0];
const right = pair[1];
if (!leftToRights[left]) leftToRights[left] = new Set();
leftToRights[left].add(right);
}
// Now, try to group lefts that share the same set of rights
// Map: stringified rights set -> set of lefts
const rightsToLefts = {};
for (const [left, rightsSet] of Object.entries(leftToRights)) {
const rightsArr = Array.from(rightsSet).sort();
const rightsKey = rightsArr.join(',');
if (!rightsToLefts[rightsKey]) rightsToLefts[rightsKey] = { lefts: [], rights: rightsArr };
rightsToLefts[rightsKey].lefts.push(left);
}
// Add to result
for (const group of Object.values(rightsToLefts)) {
result.push({ left: group.lefts.sort(), right: group.rights, value: Number(value) });
}
}
return result;
}