import { Injectable, Logger } from '@nestjs/common'; import * as QRCode from 'qrcode'; import { Resvg } from '@resvg/resvg-js'; import type { LabelElement } from '../database/entities/barcode-template.entity'; const MM_TO_PX = 300 / 25.4; // 300 DPI function mm(v: number): number { return Math.round(v * MM_TO_PX); } function escape(s: string): string { return s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } function applyVars(template: string, vars: Record): string { return template.replace(/\{([^}]+)\}/g, (_, key: string) => { const colonIdx = key.indexOf(':'); if (colonIdx !== -1) { const varName = key.slice(0, colonIdx); const width = parseInt(key.slice(colonIdx + 1), 10); if (!isNaN(width)) { return (vars[varName] ?? '').padStart(width, '0'); } } return vars[key] ?? `{${key}}`; }); } @Injectable() export class LabelRendererService { private readonly logger = new Logger(LabelRendererService.name); async render( layout: LabelElement[], widthMm: number, heightMm: number, variables: Record, ): Promise { const W = mm(widthMm); const H = mm(heightMm); const parts: string[] = []; for (const el of layout) { if (el.type === 'text') { const x = mm(el.x); const y = mm(el.y); const fontSize = mm(el.fontSize); const content = escape(applyVars(el.content, variables)); const fontWeight = el.bold ? 'bold' : 'normal'; const textAnchor = el.align === 'center' ? 'middle' : el.align === 'right' ? 'end' : 'start'; const maxWidthAttr = el.maxWidth ? ` textLength="${mm(el.maxWidth)}" lengthAdjust="spacingAndGlyphs"` : ''; // librsvg (used by sharp) ignores dominant-baseline; add fontSize to y so that // the stored coordinate is the top of the text, not the baseline. const yBaseline = y + fontSize; parts.push( `${content}`, ); } else if (el.type === 'qr') { const x = mm(el.x); const y = mm(el.y); const size = mm(el.sizeMm); const content = applyVars(el.content, variables); try { const qrBuffer = await QRCode.toBuffer(content, { type: 'png', margin: 0, width: size, errorCorrectionLevel: 'M', }); const b64 = qrBuffer.toString('base64'); parts.push(``); } catch (err: any) { this.logger.warn(`QR-Code-Rendering fehlgeschlagen für "${content}": ${err.message}`); } } else if (el.type === 'line') { const x1 = mm(el.x1); const y1 = mm(el.y1); const x2 = mm(el.x2); const y2 = mm(el.y2); const strokeWidth = el.lineWidth ? mm(el.lineWidth) : 1; parts.push(``); } } const svg = ` ${parts.join('\n ')} `; const resvg = new Resvg(svg, { font: { loadSystemFonts: true, fontDirs: ['/usr/share/fonts', '/usr/local/share/fonts'], defaultFontFamily: 'Liberation Sans', sansSerifFamily: 'Liberation Sans', }, }); return Buffer.from(resvg.render().asPng()); } }