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
115 lines (114 loc) • 4.72 kB
JavaScript
import { gaussianBlur } from "./blur.js";
import { overlap } from "./overlap.js";
import config from "./config.js";
import { logger } from "./log.js";
const MAX_KERN = config.MAX_KERN; // positive number, we use negative range for overlap
const KERN_STEP = Math.max(1, config.KERN_STEP);
/** Estimate kerning between two glyphs based on overlap
* @param left - Left glyph
* @param right - Right glyph
* @param minOverlap - Minimum overlap threshold (for calibrated mode)
* @param maxOverlap - Maximum overlap threshold (for calibrated mode)
* @param kernelWidth - Optional: kernel width for adaptive blur (used in calibrated mode)
*/
export function kernPair(left, right, minOverlap, maxOverlap, kernelWidth) {
const blurredLeft = {
...left,
bitmap: gaussianBlur(left.bitmap, undefined, kernelWidth),
};
const blurredRight = {
...right,
bitmap: gaussianBlur(right.bitmap, undefined, kernelWidth),
};
// For calibration
if (minOverlap === 0 && maxOverlap === 1e10) {
let bestKern = 0;
let bestOverlap = 0;
// Search for maximum overlap within configured max kern range
for (let k = -MAX_KERN; k <= 0; k += KERN_STEP) {
const s = overlap(blurredLeft, blurredRight, k);
if (s > bestOverlap) {
bestOverlap = s;
bestKern = k;
}
}
return bestKern;
}
// Normal kerning: multiple strategies supported
const strategy = config.SELECTION_STRATEGY || "conservative";
const EPS = config.NO_OVERLAP_EPS || 1;
logger.debug(`KernPair strategy: ${strategy}, EPS=${EPS}`);
// Precompute overlap values across the search range for deterministic selection
const samples = [];
for (let k = -MAX_KERN; k <= MAX_KERN; k += KERN_STEP) {
samples.push({
kernel: k,
sumPixel: overlap(blurredLeft, blurredRight, k),
});
}
if (strategy === "midpoint") {
const target = (minOverlap + maxOverlap) / 2;
let best = samples[0];
let bestDist = Math.abs(best.sumPixel - target);
for (const p of samples) {
const d = Math.abs(p.sumPixel - target);
if (d < bestDist) {
bestDist = d;
best = p;
}
}
return best.kernel;
}
if (strategy === "conservative") {
// Iterate kernels starting from the largest (most positive) towards the
// smallest (most negative). We want to find the first kernel where the
// overlap exceeds EPS and return the previous kernel (which will have
// sumPixel <= EPS). This handles the case where samples start with
// zeros on the positive side and begin to grow when moving negative.
//
// Sort samples by kernel descending (positive -> negative)
const sorted = samples.slice().sort((a, b) => b.kernel - a.kernel);
// Track the last kernel seen that was <= EPS. Initialize to null.
let lastGoodKernel = null;
for (const p of sorted) {
if (p.sumPixel <= EPS) {
lastGoodKernel = p.kernel;
continue;
}
// p.sumPixel > EPS: return the previously-seen kernel (closest to
// positive side) that was <= EPS. If none exists, fall back to 0.
return lastGoodKernel !== null ? lastGoodKernel : 0;
}
// If we finished the loop, all samples had sumPixel <= EPS. Choose the
// most negative kernel among them (conservative: most negative allowed).
if (lastGoodKernel !== null) {
// sorted is descending; the last element is the most negative kernel
return Math.min(...samples.map((p) => p.kernel));
}
return 0;
}
// default: 'calibrated' (original behaviour) using samples
// if s(0) within [minOverlap,maxOverlap] => 0
const s0 = samples.find((p) => p.kernel === 0).sumPixel;
if (s0 >= minOverlap && s0 <= maxOverlap)
return 0;
if (s0 < minOverlap) {
// return first negative k where s >= minOverlap
for (let kern = -KERN_STEP; kern >= -MAX_KERN; kern -= KERN_STEP) {
const p = samples.find((x) => x.kernel === kern);
if (p.sumPixel >= minOverlap)
return kern;
}
return 0;
}
if (s0 > maxOverlap) {
// return first positive k where s <= maxOverlap
for (let k = KERN_STEP; k <= MAX_KERN; k += KERN_STEP) {
const p = samples.find((x) => x.kernel === k);
if (p.sumPixel <= maxOverlap)
return k;
}
return 0;
}
return 0;
}