mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-25 00:16:41 +02:00
fix(core): PDF-Export Seitenumbruch und Sonderzeichen repariert
- pageBottom von 800 auf 740 reduziert: verhindert PDFKit-Auto-Seitenumbrüche mitten in Projekt-Einträgen (bisher: Datum/Rolle/Firma je auf eigener Seite) - Vor jedem Projekt-Eintrag Header-Höhe (Datum+Rolle+Firma) vorberechnen und Seitenumbruch proaktiv auslösen, bevor der Header gezeichnet wird - sanitizePdfText() Hilfsmethode: ersetzt €→EUR sowie Zeichen außerhalb Latin-1 die Helvetica (WinAnsiEncoding) nicht rendern kann (bisher: Zeichensalat) - sanitizePdfText auf Projekt-Texte und Zertifizierungs-Texte angewendet Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
90a0388b22
commit
9d7dcaaaea
1 changed files with 39 additions and 13 deletions
|
|
@ -187,6 +187,19 @@ export class ProfileExportService {
|
||||||
.trim();
|
.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
|
// PDF Export
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -248,7 +261,9 @@ export class ProfileExportService {
|
||||||
const leftColX = 40;
|
const leftColX = 40;
|
||||||
const rightColX = leftColX + leftColWidth + 20;
|
const rightColX = leftColX + leftColWidth + 20;
|
||||||
const rightColWidth = pageWidth - rightColX - 40;
|
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 yLeft = 40;
|
||||||
let yRight = 40;
|
let yRight = 40;
|
||||||
|
|
@ -388,12 +403,12 @@ export class ProfileExportService {
|
||||||
|
|
||||||
// Titel
|
// Titel
|
||||||
doc.font('Helvetica-Bold').fontSize(9).fillColor(accentColor);
|
doc.font('Helvetica-Bold').fontSize(9).fillColor(accentColor);
|
||||||
doc.text(cert.title, certContentX, yLeft, { width: certContentWidth });
|
doc.text(this.sanitizePdfText(cert.title), certContentX, yLeft, { width: certContentWidth });
|
||||||
yLeft += doc.heightOfString(cert.title, { width: certContentWidth }) + 2;
|
yLeft += doc.heightOfString(this.sanitizePdfText(cert.title), { width: certContentWidth }) + 2;
|
||||||
|
|
||||||
// Zertifizierungsstelle
|
// Zertifizierungsstelle
|
||||||
doc.font('Helvetica').fontSize(8).fillColor('#555555');
|
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 += 11;
|
||||||
|
|
||||||
yLeft += 4;
|
yLeft += 4;
|
||||||
|
|
@ -445,8 +460,21 @@ export class ProfileExportService {
|
||||||
for (let i = 0; i < profile.projects.length; i++) {
|
for (let i = 0; i < profile.projects.length; i++) {
|
||||||
const proj = profile.projects[i];
|
const proj = profile.projects[i];
|
||||||
|
|
||||||
// Seitenumbruch prüfen
|
// Vor jedem Eintrag: Höhe des Headers schätzen (Datum + Rolle + Firma)
|
||||||
if (yRight > pageBottom) {
|
// 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();
|
doc.addPage();
|
||||||
yRight = 40;
|
yRight = 40;
|
||||||
yRight = this.pdfSectionTitle(doc, 'BERUFSERFAHRUNG / PROJEKTE (Forts.)', rightColX, yRight, rightColWidth, accentColor);
|
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);
|
doc.circle(timelineX, entryStartY + 4, 3.5).fill(accentColor);
|
||||||
|
|
||||||
// Zeitraum (fett, größer)
|
// 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.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;
|
yRight += 14;
|
||||||
|
|
||||||
// Rolle
|
// Rolle
|
||||||
doc.font('Helvetica-Bold').fontSize(10).fillColor(accentColor);
|
doc.font('Helvetica-Bold').fontSize(10).fillColor(accentColor);
|
||||||
doc.text(proj.role, contentX, yRight, { width: contentWidth });
|
doc.text(roleText, contentX, yRight, { width: contentWidth });
|
||||||
yRight += doc.heightOfString(proj.role, { width: contentWidth }) + 2;
|
yRight += doc.heightOfString(roleText, { width: contentWidth }) + 2;
|
||||||
|
|
||||||
// Firma + Branche (fett)
|
// Firma + Branche (fett)
|
||||||
if (proj.company) {
|
if (companyLine) {
|
||||||
const companyLine = [proj.company, proj.industry].filter(Boolean).join(' · ');
|
|
||||||
doc.font('Helvetica-Bold').fontSize(9).fillColor('#333333');
|
doc.font('Helvetica-Bold').fontSize(9).fillColor('#333333');
|
||||||
doc.text(companyLine, contentX, yRight, { width: contentWidth });
|
doc.text(companyLine, contentX, yRight, { width: contentWidth });
|
||||||
yRight += doc.heightOfString(companyLine, { width: contentWidth }) + 2;
|
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 hasBullet = /^[•\u2022]\s/.test(raw) || /^\d+\.\s/.test(raw);
|
||||||
const cleaned = this.normalizeTaskLine(raw);
|
const cleaned = this.normalizeTaskLine(raw);
|
||||||
if (!cleaned) continue;
|
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 });
|
doc.text(displayText, contentX + 4, yRight, { width: contentWidth - 8 });
|
||||||
yRight += doc.heightOfString(displayText, { width: contentWidth - 8 }) + 1;
|
yRight += doc.heightOfString(displayText, { width: contentWidth - 8 }) + 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue