Initial commit with Email Import Wizard and Task Processor updates

This commit is contained in:
2026-05-04 08:02:11 +02:00
commit effdc5d59f
170 changed files with 67739 additions and 0 deletions
@@ -0,0 +1,102 @@
import { Injectable, Logger } from '@nestjs/common';
import sharp = require('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<QrCodeResult[]> {
const results: QrCodeResult[] = [];
const seen = new Set<string>();
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 = `<svg width="${info.width}" height="${info.height}" xmlns="http://www.w3.org/2000/svg"><rect x="${maskX}" y="${maskY}" width="${maskW}" height="${maskH}" fill="white"/></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<string, any> | 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;
}
}
}