mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-25 00:16:41 +02:00
fix: PDF export — tab chars garbled, unconditional bullets, encrypted PDF attachments
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 <noreply@anthropic.com>
This commit is contained in:
parent
98e7f48ce2
commit
1608f4e936
1 changed files with 21 additions and 12 deletions
|
|
@ -528,10 +528,9 @@ export class ProfileExportService {
|
||||||
yRight += doc.heightOfString(companyLine, { width: contentWidth }) + 2;
|
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) {
|
if (proj.tasks) {
|
||||||
const taskLines = proj.tasks.split('\n').filter((l: string) => l.trim());
|
const taskLines = proj.tasks.split('\n').filter((l: string) => l.trim());
|
||||||
doc.font('Helvetica').fontSize(8).fillColor('#444444');
|
|
||||||
for (const task of taskLines) {
|
for (const task of taskLines) {
|
||||||
if (yRight > pageBottom) {
|
if (yRight > pageBottom) {
|
||||||
doc.addPage();
|
doc.addPage();
|
||||||
|
|
@ -539,12 +538,21 @@ export class ProfileExportService {
|
||||||
pageBreakOccurred = true;
|
pageBreakOccurred = true;
|
||||||
}
|
}
|
||||||
const raw = task.trim();
|
const raw = task.trim();
|
||||||
|
const isBold = /^\*\*/.test(raw);
|
||||||
const hasBullet = /^[•\u2022]\s/.test(raw) || /^\d+\.\s/.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;
|
if (!cleaned) continue;
|
||||||
const displayText = `\u2022 ${this.sanitizePdfText(hasBullet ? cleaned : cleaned)}`;
|
const sanitized = this.sanitizePdfText(cleaned);
|
||||||
doc.text(displayText, contentX + 4, yRight, { width: contentWidth - 8 });
|
if (isBold) {
|
||||||
yRight += doc.heightOfString(displayText, { width: contentWidth - 8 }) + 1;
|
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) {
|
for (const att of pdfAttachments) {
|
||||||
try {
|
try {
|
||||||
const attBuffer = this.base64ToBuffer(att.data);
|
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 pageIndices = attPdf.getPageIndices();
|
||||||
const copiedPages = await merged.copyPages(attPdf, pageIndices);
|
const copiedPages = await merged.copyPages(attPdf, pageIndices);
|
||||||
for (const page of copiedPages) {
|
for (const page of copiedPages) {
|
||||||
|
|
@ -894,15 +903,15 @@ export class ProfileExportService {
|
||||||
const taskLines = proj.tasks.split('\n').filter((l: string) => l.trim());
|
const taskLines = proj.tasks.split('\n').filter((l: string) => l.trim());
|
||||||
for (const task of taskLines) {
|
for (const task of taskLines) {
|
||||||
const raw = task.trim();
|
const raw = task.trim();
|
||||||
|
const isBold = /^\*\*/.test(raw);
|
||||||
const hasBullet = /^[•\u2022]\s/.test(raw) || /^\d+\.\s/.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;
|
if (!cleaned) continue;
|
||||||
const displayText = hasBullet ? `\u2022 ${cleaned}` : cleaned;
|
|
||||||
rightParagraphs.push(
|
rightParagraphs.push(
|
||||||
new Paragraph({
|
new Paragraph({
|
||||||
children: [
|
children: isBold
|
||||||
new TextRun({ text: displayText, size: 16, color: '444444' }),
|
? [new TextRun({ text: cleaned, size: 16, color: '333333', bold: true })]
|
||||||
],
|
: [new TextRun({ text: hasBullet ? `\u2022 ${cleaned}` : cleaned, size: 16, color: '444444' })],
|
||||||
spacing: { after: 20 },
|
spacing: { after: 20 },
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue