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
63 lines (62 loc) • 2.66 kB
JavaScript
import { createCanvas } from "canvas";
import ndarray from "ndarray";
const FONT_SIZE = 100;
/** Render a single glyph from the font into a grayscale bitmap with proper bounds */
export function renderGlyph(font, char) {
const path = font.getPath(char, 0, FONT_SIZE, FONT_SIZE);
const bbox = path.getBoundingBox();
// Get glyph width and height from bounding box
let glyphWidth = Math.ceil(bbox.x2 - bbox.x1);
let glyphHeight = Math.ceil(bbox.y2 - bbox.y1);
if (glyphWidth <= 0)
glyphWidth = 1;
if (glyphHeight <= 0)
glyphHeight = 1;
// Create canvas with some padding for blur/antialiasing
const PADDING = 10;
// Use font metrics (ascender/descender) to produce a consistent baseline
const scale = FONT_SIZE / (font.unitsPerEm || 1000);
const ascenderPx = (font.ascender ?? 800) * scale;
const descenderPx = (font.descender ?? -200) * scale;
const emHeight = Math.ceil(ascenderPx - descenderPx);
const canvasWidth = glyphWidth + 2 * PADDING;
// allocate canvas to cover full em box so baseline is same for all glyphs
const canvasHeight = emHeight + 2 * PADDING;
const canvas = createCanvas(canvasWidth, canvasHeight);
const ctx = canvas.getContext("2d");
// White background
ctx.fillStyle = "white";
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
// Black glyph
ctx.fillStyle = "black";
// Translate so that the font baseline maps to a consistent y in the canvas
// Path was generated at y=FONT_SIZE; we want that baseline at PADDING + ascenderPx
const baselineCanvasY = PADDING + ascenderPx;
const translateY = baselineCanvasY - FONT_SIZE;
ctx.translate(PADDING - bbox.x1, translateY);
// @ts-ignore
path.draw(ctx);
ctx.fill();
// Extract bitmap: invert so black (glyph) = high values
const img = ctx.getImageData(0, 0, canvasWidth, canvasHeight);
const buf = new Float32Array(canvasWidth * canvasHeight);
for (let y = 0; y < canvasHeight; y++) {
const rowOff = y * canvasWidth;
for (let x = 0; x < canvasWidth; x++) {
const idx = (y * canvasWidth + x) * 4;
const red = img.data[idx];
// Invert: white (255) -> 0, black (0) -> 1
buf[rowOff + x] = (255 - red) / 255.0;
}
}
const gray = ndarray(buf, [canvasHeight, canvasWidth]);
const advance = font.getAdvanceWidth(char, FONT_SIZE);
return {
char,
width: canvasWidth,
height: canvasHeight,
advance: advance,
bboxOffsetX: -bbox.x1 + PADDING, // Store the x-bearing offset for overlap calc
bitmap: gray,
};
}