import PDFDocument from 'pdfkit'; import { LabelFontFamily, LabelTextElement } from './label-element.types'; export const MM_TO_PT = 72 / 25.4; /** Glyph metrics in AFM units (1/1000 of the font size). */ export interface FontMetrics { xHeight: number; capHeight: number; } export interface TextMeasurer { /** Height in pt the element's text needs at the element's width and font. */ heightOfStringPt(text: string, element: LabelTextElement): number; } /** The pdfkit standard font a family/style combination prints with. */ export function pdfFontName( family: LabelFontFamily, bold: boolean, italic: boolean, ): string { const base = family === 'serif' ? 'Times' : family === 'mono' ? 'Courier' : 'Helvetica'; if (base === 'Times') { if (bold && italic) return 'Times-BoldItalic'; if (bold) return 'Times-Bold'; if (italic) return 'Times-Italic'; return 'Times-Roman'; } const suffix = bold && italic ? '-BoldOblique' : bold ? '-Bold' : italic ? '-Oblique' : ''; return `${base}${suffix}`; } /** Height in mm of a glyph class: size in pt x metric / 1000 x 25.4 / 72. */ export function heightMm(fontSizePt: number, metricPer1000: number): number { return ((fontSizePt * metricPer1000) / 1000) * (25.4 / 72); } // pdfkit keeps the loaded font in the private field `_font`; this is the one // place that reads it. font-metrics.spec.ts pins the values, so a pdfkit // update that moves or renames the field fails there and nowhere else. interface PdfDocumentInternals { _font?: { xHeight?: unknown; capHeight?: unknown }; } const metricsCache = new Map(); /** AFM x-height and cap height of a built-in PDF font. */ export function fontMetricsOf( family: LabelFontFamily, bold: boolean, italic: boolean, ): FontMetrics { const name = pdfFontName(family, bold, italic); const cached = metricsCache.get(name); if (cached) { return cached; } const doc = new PDFDocument({ size: 'A4', margin: 0 }); doc.font(name); const font = (doc as unknown as PdfDocumentInternals)._font; const { xHeight, capHeight } = font ?? {}; if ( typeof xHeight !== 'number' || typeof capHeight !== 'number' || !(xHeight > 0) || !(capHeight > 0) ) { throw new Error(`No AFM metrics for font ${name}`); } const metrics = { xHeight, capHeight }; metricsCache.set(name, metrics); return metrics; } /** Measures text with a throw-away PDF document; nothing is written out. */ export function pdfTextMeasurer(): TextMeasurer { const doc = new PDFDocument({ size: 'A4', margin: 0 }); return { heightOfStringPt(text, element) { if (text === '') { return 0; } return doc .font(pdfFontName(element.fontFamily, element.bold, element.italic)) .fontSize(element.fontSize) .heightOfString(text, { width: element.width * MM_TO_PT }); }, }; }