/** * Regenerates src/features/invoice-designer/Pdf/fontWidths.ts from the TTFs * the PDFs embed, so text can be wrapped identically on server and client * without loading font files at runtime. Run: node scripts/generate-font-widths.mjs */ import { createRequire } from 'node:module' import { readFileSync, writeFileSync } from 'node:fs' import path from 'node:path' const require = createRequire(import.meta.url) const fontkit = require('fontkit') const FONTS = [ ['Helvetica', 'Roboto-Regular.ttf', 'Roboto-Bold.ttf'], ['Times-Roman', 'NotoSerif-Regular.ttf', 'NotoSerif-Bold.ttf'], ['Courier', 'NotoSansMono-Regular.ttf', 'NotoSansMono-Bold.ttf'], ['Open Sans', 'OpenSans-Regular.ttf', 'OpenSans-Bold.ttf'], ['Lato', 'Lato-Regular.ttf', 'Lato-Bold.ttf'], ['Montserrat', 'Montserrat-Regular.ttf', 'Montserrat-Bold.ttf'], ['PT Sans', 'PTSans-Regular.ttf', 'PTSans-Bold.ttf'], ] // ASCII plus the Latin-1/Latin Extended and Cyrillic ranges the app's locales use. const CHARS = [] for (let c = 0x20; c <= 0x7e; c++) CHARS.push(c) for (let c = 0xa0; c <= 0x17f; c++) CHARS.push(c) for (const c of [0x2013, 0x2014, 0x2018, 0x2019, 0x201c, 0x201d, 0x2022, 0x20ac, 0xb7]) CHARS.push(c) for (let c = 0x400; c <= 0x45f; c++) CHARS.push(c) const dir = path.resolve('src/assets/fonts') const table = {} for (const [family, regular, bold] of FONTS) { for (const [weight, file] of [['regular', regular], ['bold', bold]]) { const font = fontkit.create(readFileSync(path.join(dir, file))) const upm = font.unitsPerEm const widths = {} for (const code of CHARS) { if (!font.hasGlyphForCodePoint(code)) continue const glyph = font.glyphForCodePoint(code) widths[code] = Math.round((glyph.advanceWidth / upm) * 1000) } // A representative width for anything outside the table. const fallback = widths[0x6e] ?? 500 // 'n' table[`${family}|${weight}`] = { fallback, widths } } } const out = `/** * Character advance widths, in thousandths of the font size, for the exact * font files the PDFs embed. Generated by scripts/generate-font-widths.mjs — * do not edit by hand. This is what lets the server wrap text the same way * the printed page will, without opening a font file at runtime. */ export interface WidthTable { fallback: number widths: Record } export const FONT_WIDTHS: Record = ${JSON.stringify(table)} ` writeFileSync('src/features/invoice-designer/Pdf/fontWidths.ts', out) console.log('written', Object.keys(table).join(', '))