UNPKG

autokerning

Version:

autokerning computes suggested kerning values for glyph pairs from TrueType/OpenType fonts by rendering glyph bitmaps, applying a small Gaussian blur, and measuring pixel overlap across horizontal offsets. It can be used programmatically (as an imported E

107 lines (106 loc) 4.12 kB
import * as opentype from "opentype.js"; import { renderGlyph } from "./glyph.js"; import { kernPair } from "./kernPair.js"; import { gaussianBlur } from "./blur.js"; import { overlap } from "./overlap.js"; import config from "./config.js"; import { logger } from "./log.js"; import { COMMON_PAIRS } from "./commonPairs.js"; /** * Find overlap calibration thresholds with adaptive kernel width adjustment. * Matches Python algorithm: find kernel size where min_s > max_s / 2 * @returns [minS, maxS, usedKernelWidth] */ export function findS(font) { const TUNING_CHARS = "lno"; const FONT_SIZE = config.FONT_SIZE; let kernelWidth = Math.round(0.2 * FONT_SIZE); if (kernelWidth % 2 === 0) kernelWidth += 1; // Make it odd let iteration = 0; const MAX_ITERATIONS = 100; // Safety limit while (iteration < MAX_ITERATIONS) { iteration++; const ss = []; // Compute overlap for each tuning character for (const char of TUNING_CHARS) { const glyph = renderGlyph(font, char); // Blur with current kernel width const blurred = { ...glyph, bitmap: gaussianBlur(glyph.bitmap, undefined, kernelWidth), }; // Glyph with itself (kern=0) const s = overlap(blurred, blurred, 0); ss.push(s); } const minS = Math.min(...ss); const maxS = Math.max(...ss); const ratio = minS / maxS; logger.debug(`findS iteration ${iteration}: kernelWidth=${kernelWidth}, minS=${minS.toFixed(2)}, maxS=${maxS.toFixed(2)}, ratio=${ratio.toFixed(3)}`); // Check calibration condition (matching Python algorithm) if (minS > maxS / 2) { logger.info(`✓ Calibration converged: kernelWidth=${kernelWidth}, minS=${minS.toFixed(2)}, maxS=${maxS.toFixed(2)}`); return [minS, maxS, kernelWidth]; } // Kernel too small, increase it kernelWidth += 2; // Safety check if (kernelWidth > 2 * FONT_SIZE) { logger.warn(`⚠️ Failed to find reasonable kernel size (exceeded ${2 * FONT_SIZE}). Using kernelWidth=${kernelWidth - 2}`); return [minS, maxS, kernelWidth - 2]; } } throw new Error("findS: Max iterations exceeded"); } export async function generateKerningTable(fontfile, opts = undefined) { let outputfile; let pairs; let writeFile = true; if (typeof opts === "object" && opts !== null) { outputfile = opts.outputfile; pairs = opts.pairs; writeFile = opts.writeFile ?? true; } const font = await opentype.load(fontfile); const [minS, maxS, kernelWidth] = findS(font); // Get font name for output file const fontName = outputfile || (fontfile .split("/") .pop() ?.replace(/\.(ttf|otf)$/i, "") || "font") + ".json"; // Determine pairs to analyze let pairList; if (pairs && pairs.length > 0) { pairList = pairs.filter((p) => p.length === 2); } else { pairList = COMMON_PAIRS.filter((p) => p.length === 2); } const kerningTable = {}; for (const pair of pairList) { const [lch, rch] = pair; if (!font.hasChar(lch) || !font.hasChar(rch)) { continue; } process.stdout.write(`Calculating pair ${pair}\r`); const left = renderGlyph(font, lch); const right = renderGlyph(font, rch); // Pass kernelWidth to kernPair for calibrated blur const kernPx = kernPair(left, right, minS, maxS, kernelWidth); const kernPercent = (kernPx / left.advance) * 100; kerningTable[pair] = Math.round(kernPercent * 100) / 100; } if (writeFile) { const output = { font: fontName.replace(".json", ""), fontSize: 100, kerning: kerningTable, }; const fs = await import("fs"); fs.writeFileSync(fontName, JSON.stringify(output, null, 2)); return { outputPath: fontName, kerningTable }; } return { kerningTable }; }