import { Injectable, Logger } from '@nestjs/common'; import sharp from 'sharp'; import jsQR from 'jsqr'; export interface QrCodeResult { data: string; location: { x: number; y: number; width: number; height: number; }; } @Injectable() export class QrCodeService { private readonly logger = new Logger(QrCodeService.name); /** * Extrahiert ALLE QR-Codes aus einem Bild-Buffer (PNG/JPEG). * jsQR findet nur einen Code pro Aufruf — daher iteratives Vorgehen: * Code finden → Bereich weiß überdecken → erneut scannen, bis nichts mehr gefunden wird. */ async extractFromImage(imageBuffer: Buffer): Promise { const results: QrCodeResult[] = []; const seen = new Set(); let currentBuffer = imageBuffer; const MAX_PASSES = 10; for (let pass = 0; pass < MAX_PASSES; pass++) { const { data, info } = await sharp(currentBuffer) .ensureAlpha() .raw() .toBuffer({ resolveWithObject: true }); const imageData = new Uint8ClampedArray(data.buffer); const code = jsQR(imageData, info.width, info.height, { inversionAttempts: 'attemptBoth', }); if (!code) break; const corners = [ code.location.topLeftCorner, code.location.topRightCorner, code.location.bottomLeftCorner, code.location.bottomRightCorner, ]; const xs = corners.map((c) => c.x); const ys = corners.map((c) => c.y); const minX = Math.floor(Math.min(...xs)); const minY = Math.floor(Math.min(...ys)); const maxX = Math.ceil(Math.max(...xs)); const maxY = Math.ceil(Math.max(...ys)); const width = Math.max(1, maxX - minX); const height = Math.max(1, maxY - minY); if (!seen.has(code.data)) { seen.add(code.data); results.push({ data: code.data, location: { x: minX, y: minY, width, height }, }); this.logger.debug(`QR-Code erkannt (Pass ${pass + 1}): ${code.data}`); } // Erkannten Bereich mit weißem Rechteck (inkl. Padding) überdecken, // damit jsQR im nächsten Pass den nächsten QR findet. const pad = 12; const maskX = Math.max(0, minX - pad); const maskY = Math.max(0, minY - pad); const maskW = Math.min(info.width - maskX, width + 2 * pad); const maskH = Math.min(info.height - maskY, height + 2 * pad); const svg = ``; currentBuffer = await sharp(currentBuffer) .composite([{ input: Buffer.from(svg), top: 0, left: 0 }]) .png() .toBuffer(); } return results; } /** * Validiert ob der QR-Code-Inhalt dem erwarteten Schema entspricht. * Schema: JSON mit X, Y, Jahr, Nummer, Eingangsdatum */ parseBarcode(qrData: string): Record | null { try { const parsed = JSON.parse(qrData); if (parsed.Jahr !== undefined && parsed.Nummer !== undefined) { return parsed; } this.logger.warn(`QR-Code-Daten passen nicht zum Schema: ${qrData}`); return null; } catch { this.logger.debug(`QR-Code ist kein JSON: ${qrData}`); return null; } } }