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
Build and Push Multi-Platform Images / build-and-push (push) Successful in 33s
This commit is contained in:
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user