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
68 lines (67 loc) • 2.67 kB
JavaScript
import ndarray from "ndarray";
import config from "./config.js";
import { logger } from "./log.js";
/** Gaussian blur using a separable kernel */
export function gaussianBlur(input,
// Use factor from config so it is centrally adjustable
sigma = config.BLUR_SIGMA_FACTOR * input.shape[1],
// Optional: override sigma with fixed kernel width (for adaptive calibration)
kernelWidth) {
// If kernelWidth is provided, use it to calculate sigma (like Python's adaptive calibration)
if (kernelWidth !== undefined) {
sigma = kernelWidth / 4; // Matches Python: c = KERNEL_WIDTH / 4
}
logger.debug(`[GAUSSIAN] enter: kernelWidth=${kernelWidth ?? "(auto)"}, sigma=${sigma.toFixed(2)}`);
const width = input.shape[1];
const height = input.shape[0];
const radius = Math.round(sigma);
// Cache kernel arrays by radius to avoid recomputing for same sigma
const kernelCache = gaussianBlur._kernelCache || {};
gaussianBlur._kernelCache = kernelCache;
let kernel = kernelCache[radius];
if (!kernel) {
const klen = radius * 2 + 1;
const tmp = new Float64Array(klen);
let s = 0;
for (let i = 0; i < klen; i++) {
const x = i - radius;
const v = Math.exp(-(x * x) / (2 * sigma * sigma));
tmp[i] = v;
s += v;
}
for (let i = 0; i < klen; i++)
tmp[i] /= s;
kernel = tmp;
kernelCache[radius] = kernel;
}
const inData = input.data;
const tempData = new Float32Array(width * height);
const outData = new Float32Array(width * height);
// horizontal pass (direct indexing)
for (let y = 0; y < height; y++) {
const rowOff = y * width;
for (let x = 0; x < width; x++) {
let acc = 0;
for (let i = -radius; i <= radius; i++) {
const xi = Math.min(width - 1, Math.max(0, x + i));
acc += inData[rowOff + xi] * kernel[i + radius];
}
tempData[rowOff + x] = acc;
}
}
// vertical pass
for (let y = 0; y < height; y++) {
const rowOff = y * width;
for (let x = 0; x < width; x++) {
let acc = 0;
for (let i = -radius; i <= radius; i++) {
const yi = Math.min(height - 1, Math.max(0, y + i));
acc += tempData[yi * width + x] * kernel[i + radius];
}
outData[rowOff + x] = acc;
}
}
const out = ndarray(outData, [height, width]);
logger.debug(`[GAUSSIAN] exit: kernelWidth=${kernelWidth ?? "(auto)"}, radius=${radius}, width=${width}, height=${height}`);
return out;
}