- 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]>
93 lines
2.8 KiB
TypeScript
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 });
|
|
},
|
|
};
|
|
}
|