feat: implement backend label print agent system for remote label rendering and job management
Build and Push Multi-Platform Images / build-and-push (push) Successful in 33s

This commit is contained in:
2026-05-07 22:46:29 +02:00
parent 0c94e7b999
commit 80f862a0c0
15 changed files with 995 additions and 15 deletions
@@ -0,0 +1,81 @@
import { Injectable, Logger } from '@nestjs/common';
import * as QRCode from 'qrcode';
import sharp from 'sharp';
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function applyVars(template: string, vars: Record<string, string>): string {
return template.replace(/\{([^}]+)\}/g, (_, key) => vars[key] ?? `{${key}}`);
}
@Injectable()
export class LabelRendererService {
private readonly logger = new Logger(LabelRendererService.name);
async render(
layout: LabelElement[],
widthMm: number,
heightMm: number,
variables: Record<string, string>,
): Promise<Buffer> {
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"` : '';
parts.push(
`<text x="${x}" y="${y}" font-family="Arial,Helvetica,sans-serif" font-size="${fontSize}" font-weight="${fontWeight}" text-anchor="${textAnchor}" dominant-baseline="hanging"${maxWidthAttr}>${content}</text>`,
);
} 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(`<image href="data:image/png;base64,${b64}" x="${x}" y="${y}" width="${size}" height="${size}"/>`);
} 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(`<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="black" stroke-width="${strokeWidth}"/>`);
}
}
const svg = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
<rect width="${W}" height="${H}" fill="white"/>
${parts.join('\n ')}
</svg>`;
return sharp(Buffer.from(svg)).png().toBuffer();
}
}