UNPKG

export-kerning

Version:
154 lines (133 loc) 5.16 kB
#!/usr/bin/env node 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; }