From 1608f4e936a99bd10702fefd8cf896ad11829885 Mon Sep 17 00:00:00 2001 From: Thomas Reitz Date: Sat, 14 Mar 2026 13:56:57 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20PDF=20export=20=E2=80=94=20tab=20chars?= =?UTF-8?q?=20garbled,=20unconditional=20bullets,=20encrypted=20PDF=20atta?= =?UTF-8?q?chments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root causes identified via DB hex-dump and server logs: 1. Tab character in budget lines: DB stores e.g. `**Budget Verantwortung:**\t750.000 EUR` (byte 0x09 between `:**` and the amount). PDFKit can't render \t in WinAnsiEncoding, producing garbage output like `"sSãUU`. Fix: `.replace(/\t/g, ' ')` in cleaned text. 2. Unconditional bullet: `\u2022 ${sanitize(hasBullet ? cleaned : cleaned)}` always prepended `•` — the ternary was a no-op. Fix: only add `•` when hasBullet is true; `**...**` header lines now render as Helvetica-Bold without a bullet. 3. ITIL4 Foundation Cert.pdf is owner-password-encrypted. pdf-lib threw "Input document is encrypted" → cert was silently skipped. Fix: `PdfLib.load(attBuffer, { ignoreEncryption: true })`. Applies to both PDF and DOCX export paths. Co-Authored-By: Claude Sonnet 4.6 --- .../expert-profile/profile-export.service.ts | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/packages/core-service/src/core/expert-profile/profile-export.service.ts b/packages/core-service/src/core/expert-profile/profile-export.service.ts index 6ce362c..9a8162b 100644 --- a/packages/core-service/src/core/expert-profile/profile-export.service.ts +++ b/packages/core-service/src/core/expert-profile/profile-export.service.ts @@ -528,10 +528,9 @@ export class ProfileExportService { yRight += doc.heightOfString(companyLine, { width: contentWidth }) + 2; } - // Aufgaben (Bullet nur wenn Original-Zeile ein Aufzählungszeichen hat) + // Aufgaben: Bold für **...**-Zeilen, Bullet nur wenn Original-Zeile ein Aufzählungszeichen hat if (proj.tasks) { const taskLines = proj.tasks.split('\n').filter((l: string) => l.trim()); - doc.font('Helvetica').fontSize(8).fillColor('#444444'); for (const task of taskLines) { if (yRight > pageBottom) { doc.addPage(); @@ -539,12 +538,21 @@ export class ProfileExportService { pageBreakOccurred = true; } const raw = task.trim(); + const isBold = /^\*\*/.test(raw); const hasBullet = /^[•\u2022]\s/.test(raw) || /^\d+\.\s/.test(raw); - const cleaned = this.normalizeTaskLine(raw); + const cleaned = this.normalizeTaskLine(raw).replace(/\t/g, ' '); if (!cleaned) continue; - const displayText = `\u2022 ${this.sanitizePdfText(hasBullet ? cleaned : cleaned)}`; - doc.text(displayText, contentX + 4, yRight, { width: contentWidth - 8 }); - yRight += doc.heightOfString(displayText, { width: contentWidth - 8 }) + 1; + const sanitized = this.sanitizePdfText(cleaned); + if (isBold) { + doc.font('Helvetica-Bold').fontSize(8).fillColor('#333333'); + doc.text(sanitized, contentX + 4, yRight, { width: contentWidth - 8 }); + yRight += doc.heightOfString(sanitized, { width: contentWidth - 8 }) + 1; + } else { + const displayText = hasBullet ? `\u2022 ${sanitized}` : sanitized; + doc.font('Helvetica').fontSize(8).fillColor('#444444'); + doc.text(displayText, contentX + 4, yRight, { width: contentWidth - 8 }); + yRight += doc.heightOfString(displayText, { width: contentWidth - 8 }) + 1; + } } } @@ -609,7 +617,8 @@ export class ProfileExportService { for (const att of pdfAttachments) { try { const attBuffer = this.base64ToBuffer(att.data); - const attPdf = await PdfLib.load(attBuffer); + // ignoreEncryption: true erlaubt das Einbetten von PDFs mit Owner-Password-Schutz + const attPdf = await PdfLib.load(attBuffer, { ignoreEncryption: true }); const pageIndices = attPdf.getPageIndices(); const copiedPages = await merged.copyPages(attPdf, pageIndices); for (const page of copiedPages) { @@ -894,15 +903,15 @@ export class ProfileExportService { const taskLines = proj.tasks.split('\n').filter((l: string) => l.trim()); for (const task of taskLines) { const raw = task.trim(); + const isBold = /^\*\*/.test(raw); const hasBullet = /^[•\u2022]\s/.test(raw) || /^\d+\.\s/.test(raw); - const cleaned = this.normalizeTaskLine(raw); + const cleaned = this.normalizeTaskLine(raw).replace(/\t/g, ' '); if (!cleaned) continue; - const displayText = hasBullet ? `\u2022 ${cleaned}` : cleaned; rightParagraphs.push( new Paragraph({ - children: [ - new TextRun({ text: displayText, size: 16, color: '444444' }), - ], + children: isBold + ? [new TextRun({ text: cleaned, size: 16, color: '333333', bold: true })] + : [new TextRun({ text: hasBullet ? `\u2022 ${cleaned}` : cleaned, size: 16, color: '444444' })], spacing: { after: 20 }, }), );