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 1512c7b..39b7c70 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 @@ -187,6 +187,19 @@ export class ProfileExportService { .trim(); } + /** + * Ersetzt Sonderzeichen, die PDFKit's eingebauter Helvetica-Font (WinAnsiEncoding) + * nicht darstellen kann (z.B. €-Zeichen → Zeichensalat). + */ + private sanitizePdfText(text: string): string { + 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, '?'); + } + // ============================================================ // PDF Export // ============================================================ @@ -248,7 +261,9 @@ export class ProfileExportService { const leftColX = 40; const rightColX = leftColX + leftColWidth + 20; const rightColWidth = pageWidth - rightColX - 40; - const pageBottom = 800; + // pageBottom deutlich unter der echten Seitengrenze (~841px) halten, + // damit PDFKit keine automatischen Seitenumbrüche mitten in Einträgen auslöst + const pageBottom = 740; let yLeft = 40; let yRight = 40; @@ -388,12 +403,12 @@ export class ProfileExportService { // Titel doc.font('Helvetica-Bold').fontSize(9).fillColor(accentColor); - doc.text(cert.title, certContentX, yLeft, { width: certContentWidth }); - yLeft += doc.heightOfString(cert.title, { width: certContentWidth }) + 2; + doc.text(this.sanitizePdfText(cert.title), certContentX, yLeft, { width: certContentWidth }); + yLeft += doc.heightOfString(this.sanitizePdfText(cert.title), { width: certContentWidth }) + 2; // Zertifizierungsstelle doc.font('Helvetica').fontSize(8).fillColor('#555555'); - doc.text(cert.issuingBody, certContentX, yLeft, { width: certContentWidth }); + doc.text(this.sanitizePdfText(cert.issuingBody), certContentX, yLeft, { width: certContentWidth }); yLeft += 11; yLeft += 4; @@ -445,8 +460,21 @@ export class ProfileExportService { for (let i = 0; i < profile.projects.length; i++) { const proj = profile.projects[i]; - // Seitenumbruch prüfen - if (yRight > pageBottom) { + // Vor jedem Eintrag: Höhe des Headers schätzen (Datum + Rolle + Firma) + // damit der komplette Header auf einer Seite bleibt + const dateRange = this.formatDateRange(proj.fromMonth, proj.fromYear, proj.toMonth, proj.toYear, proj.isCurrent); + const roleText = this.sanitizePdfText(proj.role); + const companyLine = proj.company + ? this.sanitizePdfText([proj.company, proj.industry].filter(Boolean).join(' \u00b7 ')) + : ''; + doc.font('Helvetica-Bold').fontSize(10); + const roleH = doc.heightOfString(roleText, { width: contentWidth }); + doc.fontSize(9); + const companyH = companyLine ? doc.heightOfString(companyLine, { width: contentWidth }) + 2 : 0; + const headerH = 14 + roleH + 2 + companyH + 8; // Datum + Rolle + Firma + Puffer + + // Seitenumbruch wenn Header nicht mehr passt + if (yRight + headerH > pageBottom) { doc.addPage(); yRight = 40; yRight = this.pdfSectionTitle(doc, 'BERUFSERFAHRUNG / PROJEKTE (Forts.)', rightColX, yRight, rightColWidth, accentColor); @@ -459,19 +487,17 @@ export class ProfileExportService { doc.circle(timelineX, entryStartY + 4, 3.5).fill(accentColor); // Zeitraum (fett, größer) - const dateRange = this.formatDateRange(proj.fromMonth, proj.fromYear, proj.toMonth, proj.toYear, proj.isCurrent); doc.font('Helvetica-Bold').fontSize(10).fillColor('#555555'); - doc.text(dateRange, contentX, yRight, { width: contentWidth }); + doc.text(this.sanitizePdfText(dateRange), contentX, yRight, { width: contentWidth }); yRight += 14; // Rolle doc.font('Helvetica-Bold').fontSize(10).fillColor(accentColor); - doc.text(proj.role, contentX, yRight, { width: contentWidth }); - yRight += doc.heightOfString(proj.role, { width: contentWidth }) + 2; + doc.text(roleText, contentX, yRight, { width: contentWidth }); + yRight += doc.heightOfString(roleText, { width: contentWidth }) + 2; // Firma + Branche (fett) - if (proj.company) { - const companyLine = [proj.company, proj.industry].filter(Boolean).join(' · '); + if (companyLine) { doc.font('Helvetica-Bold').fontSize(9).fillColor('#333333'); doc.text(companyLine, contentX, yRight, { width: contentWidth }); yRight += doc.heightOfString(companyLine, { width: contentWidth }) + 2; @@ -491,7 +517,7 @@ export class ProfileExportService { const hasBullet = /^[•\u2022]\s/.test(raw) || /^\d+\.\s/.test(raw); const cleaned = this.normalizeTaskLine(raw); if (!cleaned) continue; - const displayText = hasBullet ? `\u2022 ${cleaned}` : cleaned; + 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; }