Files
ApiCore/ApiCore-Backend/src/label-templates/font-metrics.ts
T
Nick SzaunigandClaude Opus 5.5 c18860c75c
Backend Image / build-and-push (push) Successful in 6m34s
Frontend Image / build-and-push (push) Successful in 10m17s
feat: T6 wave 2 – medicine book, document rights, label particulars, herd events
- Veterinary medicine catalogue, lots and append-only movement ledger
  (rest always computed; corrections as counter-bookings; inactive
  medicines take no new stock; unit must match on lot moves).
- Documents of medicine lots, authority notices and restrictions need the
  domain right on top of documents:* (403 before any lookup).
- Label particulars: pure checkLabel with six mandatory gaps and four
  warnings, AFM font metrics, Berlin-day dates, geometric reading order
  for split addresses, new master-data fields with row-locked updates.
- Herd events ColonyCreated, ColonyMoved, ApiaryChanged and
  TskReferenceDayReached (ids only, never forwarded to webhooks); CSV
  import emits ColonyCreated with audit.
- ARCHITECTURE.md §12 Compliance (T6), Annahmen 59–70.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-15 13:43:44 +02:00

93 lines
2.8 KiB
TypeScript

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<string, FontMetrics>();
/** 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 });
},
};
}