mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-24 22:36:38 +02:00
fix(core): PDF-Anhänge korrekt einbetten via pdf-lib + Zeichenbereinigung verbessert
- pdf-lib installiert und importiert - PDF-Anhänge werden nicht mehr als Platzhalter-Seite angezeigt, sondern alle Seiten des Anhang-PDFs direkt in das Export-PDF eingebettet (pdf-lib merge) - Bild-Anhänge bleiben weiterhin per PDFKit eingebettet - sanitizePdfText() erweitert: vollständige Windows-1252 U+0080-U+009F Mapping-Tabelle für mis-enkodierte Zeichen (€, Anführungszeichen, Gedankenstriche, TM usw.) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d08bedecf7
commit
79ad5e4be3
3 changed files with 109 additions and 25 deletions
37
packages/core-service/package-lock.json
generated
37
packages/core-service/package-lock.json
generated
|
|
@ -30,6 +30,7 @@
|
|||
"otplib": "^12.0.1",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pdfkit": "^0.17.2",
|
||||
"pngjs": "^7.0.0",
|
||||
"qrcode": "^1.5.4",
|
||||
|
|
@ -2748,6 +2749,24 @@
|
|||
"@otplib/plugin-thirty-two": "^12.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@pdf-lib/standard-fonts": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz",
|
||||
"integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pako": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@pdf-lib/upng": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz",
|
||||
"integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pako": "^1.0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@pkgjs/parseargs": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
|
|
@ -9210,6 +9229,24 @@
|
|||
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
|
||||
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
|
||||
},
|
||||
"node_modules/pdf-lib": {
|
||||
"version": "1.17.1",
|
||||
"resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz",
|
||||
"integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pdf-lib/standard-fonts": "^1.0.0",
|
||||
"@pdf-lib/upng": "^1.0.1",
|
||||
"pako": "^1.0.11",
|
||||
"tslib": "^1.11.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pdf-lib/node_modules/tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/pdfkit": {
|
||||
"version": "0.17.2",
|
||||
"resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.17.2.tgz",
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@
|
|||
"otplib": "^12.0.1",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pdfkit": "^0.17.2",
|
||||
"pngjs": "^7.0.0",
|
||||
"qrcode": "^1.5.4",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
|
|||
import { ExpertProfileService } from './expert-profile.service';
|
||||
import { RedisService } from '../../redis/redis.service';
|
||||
import PDFDocument from 'pdfkit';
|
||||
import { PDFDocument as PdfLib } from 'pdf-lib';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { PNG } from 'pngjs';
|
||||
|
|
@ -188,16 +189,34 @@ export class ProfileExportService {
|
|||
}
|
||||
|
||||
/**
|
||||
* Ersetzt Sonderzeichen, die PDFKit's eingebauter Helvetica-Font (WinAnsiEncoding)
|
||||
* nicht darstellen kann (z.B. €-Zeichen → Zeichensalat).
|
||||
* Bereinigt Text für PDFKit's eingebauten Helvetica-Font (WinAnsiEncoding).
|
||||
* PDFKit kann Zeichen außerhalb des Latin-1-Druckbereichs nicht darstellen.
|
||||
* Behandelt auch Windows-1252-Zeichen die als U+0080–U+009F mis-enkodiert sind
|
||||
* (passiert wenn Text ursprünglich als Windows-1252 gespeichert wurde).
|
||||
*/
|
||||
private sanitizePdfText(text: string): string {
|
||||
// Windows-1252 Bereich U+0080–U+009F → lesbare ASCII-Äquivalente
|
||||
const w1252: Record<string, string> = {
|
||||
'\u0080': 'EUR', '\u0082': ',', '\u0083': 'f', '\u0084': '"',
|
||||
'\u0085': '...', '\u0086': '+', '\u0087': '++', '\u0089': '%',
|
||||
'\u008a': 'S', '\u008b': '<', '\u008c': 'OE', '\u008e': 'Z',
|
||||
'\u0091': "'", '\u0092': "'", '\u0093': '"', '\u0094': '"',
|
||||
'\u0095': '-', '\u0096': '-', '\u0097': '--', '\u0099': 'TM',
|
||||
'\u009a': 's', '\u009b': '>', '\u009c': 'oe', '\u009e': 'z',
|
||||
'\u009f': 'Y',
|
||||
};
|
||||
return text
|
||||
.replace(/€/g, 'EUR')
|
||||
.replace(/£/g, 'GBP')
|
||||
.replace(/¥/g, 'JPY')
|
||||
// Weiterer Fallback: alle Zeichen außerhalb Latin-1 (U+0100+) durch '?' ersetzen
|
||||
.replace(/[\u0100-\uFFFF]/g, '?');
|
||||
.replace(/€/g, 'EUR') // U+20AC korrektes Euro-Zeichen
|
||||
.replace(/\u00a3/g, 'GBP') // £
|
||||
.replace(/\u00a5/g, 'JPY') // ¥
|
||||
// Windows-1252 Sonderzeichen ersetzen
|
||||
.replace(/[\u0080-\u009f]/g, (c) => w1252[c] ?? '')
|
||||
// Alles ab U+0100 ersetzen (Zeichen außerhalb Latin-1)
|
||||
.replace(/[\u0100-\uffff]/g, (c) => {
|
||||
// Versuch NFD-Normalisierung für Umlaute/Akzente
|
||||
const nfd = c.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
||||
return nfd.length > 0 && nfd.charCodeAt(0) < 0x100 ? nfd : '';
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
|
@ -243,7 +262,13 @@ export class ProfileExportService {
|
|||
const fullName = `${data.firstName} ${data.lastName}`;
|
||||
const { firstName, lastName } = data;
|
||||
|
||||
return new Promise<{ buffer: Buffer; firstName: string; lastName: string }>((resolve, reject) => {
|
||||
// Speichere PDF-Anhänge für späteres Merging (pdf-lib)
|
||||
const pdfAttachments = (profile?.attachments ?? []).filter(
|
||||
(a) => a.mimetype === 'application/pdf',
|
||||
);
|
||||
|
||||
// Schritt 1: CV-PDF mit PDFKit generieren
|
||||
const cvResult = await new Promise<{ buffer: Buffer; firstName: string; lastName: string }>((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
const doc = new PDFDocument({
|
||||
size: 'A4',
|
||||
|
|
@ -533,29 +558,22 @@ export class ProfileExportService {
|
|||
}
|
||||
}
|
||||
|
||||
// --- ANHÄNGE als zusätzliche Seiten ---
|
||||
// --- BILD-ANHÄNGE als zusätzliche Seiten (nur Bilder — PDFs werden per pdf-lib gemerged) ---
|
||||
const attachments = profile?.attachments ?? [];
|
||||
for (const att of attachments) {
|
||||
if (!att.mimetype.startsWith('image/')) continue;
|
||||
doc.addPage();
|
||||
let yAtt = 40;
|
||||
yAtt = this.pdfSectionTitle(doc, `ANLAGE: ${att.filename}`, 40, yAtt, pageWidth - 80, accentColor);
|
||||
if (att.mimetype.startsWith('image/')) {
|
||||
try {
|
||||
const imgBuffer = this.base64ToBuffer(att.data);
|
||||
const maxW = pageWidth - 80;
|
||||
const maxH = 680;
|
||||
doc.image(imgBuffer, 40, yAtt, { fit: [maxW, maxH], align: 'center' });
|
||||
} catch (err) {
|
||||
this.logger.warn(`Anhang-Bild konnte nicht gerendert werden: ${att.filename}`, err);
|
||||
doc.font('Helvetica').fontSize(9).fillColor('#777777');
|
||||
doc.text(`(Bild konnte nicht dargestellt werden: ${att.filename})`, 40, yAtt, { width: pageWidth - 80 });
|
||||
}
|
||||
} else {
|
||||
// Nicht-Bild-Anhänge: Dateiname und Hinweis
|
||||
try {
|
||||
const imgBuffer = this.base64ToBuffer(att.data);
|
||||
const maxW = pageWidth - 80;
|
||||
const maxH = 680;
|
||||
doc.image(imgBuffer, 40, yAtt, { fit: [maxW, maxH], align: 'center' });
|
||||
} catch (err) {
|
||||
this.logger.warn(`Anhang-Bild konnte nicht gerendert werden: ${att.filename}`, err);
|
||||
doc.font('Helvetica').fontSize(9).fillColor('#777777');
|
||||
doc.text(`Typ: ${att.mimetype}`, 40, yAtt, { width: pageWidth - 80 });
|
||||
doc.moveDown(0.4);
|
||||
doc.text(`(Datei-Anhang – nicht als Vorschau darstellbar)`, 40, doc.y, { width: pageWidth - 80 });
|
||||
doc.text(`(Bild konnte nicht dargestellt werden: ${att.filename})`, 40, yAtt, { width: pageWidth - 80 });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -580,6 +598,34 @@ export class ProfileExportService {
|
|||
|
||||
doc.end();
|
||||
});
|
||||
|
||||
// Schritt 2: PDF-Anhänge mit pdf-lib einbetten
|
||||
if (pdfAttachments.length === 0) {
|
||||
return cvResult;
|
||||
}
|
||||
|
||||
try {
|
||||
const merged = await PdfLib.load(cvResult.buffer);
|
||||
for (const att of pdfAttachments) {
|
||||
try {
|
||||
const attBuffer = this.base64ToBuffer(att.data);
|
||||
const attPdf = await PdfLib.load(attBuffer);
|
||||
const pageIndices = attPdf.getPageIndices();
|
||||
const copiedPages = await merged.copyPages(attPdf, pageIndices);
|
||||
for (const page of copiedPages) {
|
||||
merged.addPage(page);
|
||||
}
|
||||
this.logger.log(`PDF-Anhang eingebettet: ${att.filename} (${pageIndices.length} Seite(n))`);
|
||||
} catch (err) {
|
||||
this.logger.warn(`PDF-Anhang konnte nicht eingebettet werden: ${att.filename}`, err);
|
||||
}
|
||||
}
|
||||
const mergedBuffer = Buffer.from(await merged.save());
|
||||
return { buffer: mergedBuffer, firstName: cvResult.firstName, lastName: cvResult.lastName };
|
||||
} catch (err) {
|
||||
this.logger.warn('PDF-Merge fehlgeschlagen, CV ohne Anhänge zurückgegeben', err);
|
||||
return cvResult;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue