export-kerning
Version:
Exports the kerning of an OpenType font
130 lines (112 loc) • 4.06 kB
JavaScript
// Pure functions for extracting and formatting font kerning pairs
// Works in both Node.js and browser environments
export 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);
}
export 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;
}
export function extractUsedKerningPairs(text, font) {
const pairs = {};
const words = text.split(/\s+/);
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;
}
export function getSupportedChars(font) {
const cmap = font.tables.cmap.glyphIndexMap;
return Object.keys(cmap).map(code => String.fromCharCode(code));
}
export 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;
}
// High-level export function that combines all the functionality
export function exportKerning(font, options = {}) {
const { ranges, text } = options;
// Get supported chars or use ranges
const supportedChars = getSupportedChars(font);
const charsToUse = ranges || supportedChars;
// Extract all kerning pairs
const allKerningPairs = extractKerningPairs(font, charsToUse);
const result = {
unitsPerEm: font.unitsPerEm,
kerningPairs: convertToSVGStyleOptimizedFormat(allKerningPairs)
};
// If text is provided, also generate subset
if (text) {
const usedKerningPairs = extractUsedKerningPairs(text, font);
result.subset = {
unitsPerEm: font.unitsPerEm,
kerningPairs: convertToSVGStyleOptimizedFormat(usedKerningPairs)
};
}
return result;
}