/** * Tightening the letter-spacing of a wordmark, by image rather than by text. * * The logo arrives as artwork: its spacing is drawn in, not typeset, so no * property can adjust it. But between two letters every pixel column is blank, * and a blank column carries no shape — dropping some of them moves the * letters closer without altering a single letterform. * * Working on the full-resolution source matters: doing this after the * downscale would compress columns that are already an average of ink and * paper, and smear the hairlines this logo can least afford to lose. */ export type Greyscale = { data: Uint8Array | Buffer; width: number; height: number; }; /** Below this a pixel counts as ink; above it, as paper. */ const INK = 128; export function columnHasInk({ data, width, height }: Greyscale, x: number): boolean { for (let y = 0; y < height; y += 1) { if ((data[y * width + x] ?? 255) < INK) { return true; } } return false; } /** * The columns to keep so that no run of blank ones is longer than `maxGap`. * * Blank columns at the very edges are dropped entirely: they are margin, not * spacing, and the caller sets its own. */ export function keptColumns(image: Greyscale, maxGap: number): number[] { const ink: boolean[] = []; for (let x = 0; x < image.width; x += 1) { ink.push(columnHasInk(image, x)); } const first = ink.indexOf(true); const last = ink.lastIndexOf(true); if (first === -1) { return []; } const kept: number[] = []; let gap = 0; for (let x = first; x <= last; x += 1) { if (ink[x]) { gap = 0; kept.push(x); continue; } gap += 1; if (gap <= maxGap) { kept.push(x); } } return kept; } /** Rebuilds the image from the kept columns. */ export function tighten(image: Greyscale, maxGap: number): Greyscale { const columns = keptColumns(image, maxGap); const width = columns.length; const data = Buffer.alloc(width * image.height, 255); for (let y = 0; y < image.height; y += 1) { for (let index = 0; index < width; index += 1) { data[y * width + index] = image.data[y * image.width + (columns[index] ?? 0)] ?? 255; } } return { data, width, height: image.height }; }