feat: use PNG contact icons in PDF export instead of vector drawing

- Add Phone.png, Mobile.png, Mail.png, Address.png icon assets
- Replace hand-drawn vector icons with professional PNG icons
- Icons stored in packages/core-service/assets/icons/ (included in Docker build)
- Also stored in templates/cv/default/ and Icons/ for reference

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Thomas Reitz 2026-03-09 20:11:40 +01:00
parent ea5dfda913
commit 9a9800e17e
13 changed files with 30 additions and 75 deletions

BIN
Icons/Address.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
Icons/Mail.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
Icons/Mobile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 750 B

BIN
Icons/Phone.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 750 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -1,6 +1,8 @@
import { Injectable, Logger } from '@nestjs/common';
import { ExpertProfileService } from './expert-profile.service';
import PDFDocument from 'pdfkit';
import * as fs from 'fs';
import * as path from 'path';
import {
Document,
Packer,
@ -95,8 +97,24 @@ function lightenColor(hex: string, factor: number): string {
export class ProfileExportService {
private readonly logger = new Logger(ProfileExportService.name);
// Pfad zu den Icon-Assets (relativ zum Working Directory /app im Container)
private readonly iconsDir = path.resolve(process.cwd(), 'assets', 'icons');
constructor(private readonly expertProfileService: ExpertProfileService) {}
private loadIcon(name: string): Buffer | null {
try {
const iconPath = path.join(this.iconsDir, name);
if (fs.existsSync(iconPath)) {
return fs.readFileSync(iconPath);
}
this.logger.warn(`Icon nicht gefunden: ${iconPath}`);
} catch (err) {
this.logger.warn(`Icon konnte nicht geladen werden: ${name}`, err);
}
return null;
}
// ============================================================
// PDF Export
// ============================================================
@ -173,21 +191,28 @@ export class ProfileExportService {
// --- KONTAKT ---
yLeft = this.pdfSectionTitle(doc, 'KONTAKT', leftColX, yLeft, leftColWidth, accentColor);
const iconTextOffset = 18; // Abstand Icon → Text
// Icons laden
const phoneIcon = this.loadIcon('Phone.png');
const mobileIcon = this.loadIcon('Mobile.png');
const mailIcon = this.loadIcon('Mail.png');
const addressIcon = this.loadIcon('Address.png');
const iconSize = 12;
const iconTextOffset = 20; // Abstand Icon → Text
if (data.phone) {
this.drawHandsetIcon(doc, leftColX + 1, yLeft, accentColor);
if (phoneIcon) doc.image(phoneIcon, leftColX, yLeft - 1, { width: iconSize, height: iconSize });
yLeft = this.pdfContactText(doc, data.phone, leftColX + iconTextOffset, yLeft, leftColWidth - iconTextOffset);
}
if (data.mobile) {
this.drawMobileIcon(doc, leftColX + 1, yLeft, accentColor);
if (mobileIcon) doc.image(mobileIcon, leftColX + 1, yLeft - 1, { width: iconSize, height: iconSize });
yLeft = this.pdfContactText(doc, data.mobile, leftColX + iconTextOffset, yLeft, leftColWidth - iconTextOffset);
}
if (data.email) {
this.drawEmailIcon(doc, leftColX, yLeft + 1, accentColor);
if (mailIcon) doc.image(mailIcon, leftColX, yLeft - 1, { width: iconSize, height: iconSize });
yLeft = this.pdfContactText(doc, data.email, leftColX + iconTextOffset, yLeft, leftColWidth - iconTextOffset);
}
if (data.street || data.city) {
this.drawLocationIcon(doc, leftColX, yLeft, accentColor);
if (addressIcon) doc.image(addressIcon, leftColX + 1, yLeft - 2, { width: iconSize, height: iconSize + 2 });
const line1 = data.street || '';
const line2 = [data.postalCode, data.city].filter(Boolean).join(' ');
const addressText = [line1, line2].filter(Boolean).join('\n');
@ -729,76 +754,6 @@ export class ProfileExportService {
return y + Math.max(textHeight, 10) + 3;
}
// --- Vektor-Icons für Kontakt ---
private drawHandsetIcon(doc: PDFKit.PDFDocument, x: number, y: number, color: string): void {
doc.save();
// Klassischer Telefonhörer als zusammenhängende Silhouette
// Ohrmuschel + Sprechmuschel als Blöcke, Griff als gefüllter Bogen
// Ohrmuschel (oben)
doc.roundedRect(x, y, 10, 3.5, 1.5).fill(color);
// Sprechmuschel (unten)
doc.roundedRect(x, y + 7.5, 10, 3.5, 1.5).fill(color);
// Griff: gefüllte Bogenform (Crescent) — verbindet nahtlos
// Äußerer Bogen (weit nach links)
doc.moveTo(x + 4, y + 3)
.bezierCurveTo(x - 2, y + 4, x - 2, y + 7, x + 4, y + 8)
// Innerer Bogen (zurück, näher zur Mitte)
.bezierCurveTo(x + 1.5, y + 6.5, x + 1.5, y + 4.5, x + 4, y + 3)
.fill(color);
doc.restore();
}
private drawMobileIcon(doc: PDFKit.PDFDocument, x: number, y: number, color: string): void {
doc.save();
// Smartphone-Icon: abgerundetes Rechteck mit Display und Home-Button
const w = 7;
const h = 10;
doc.roundedRect(x, y - 1, w, h, 1.5).strokeColor(color).lineWidth(1).stroke();
// Display-Bereich (inneres Rechteck)
doc.rect(x + 1.2, y + 1, w - 2.4, h - 4.5).fill(color);
// Home-Button (kleiner Kreis unten)
doc.circle(x + w / 2, y + h - 2, 0.8).fill(color);
doc.restore();
}
private drawEmailIcon(doc: PDFKit.PDFDocument, x: number, y: number, color: string): void {
doc.save();
const w = 11;
const h = 8;
// Briefumschlag-Körper
doc.rect(x, y, w, h).strokeColor(color).lineWidth(0.8).stroke();
// Klappen-Linien (V-Form von oben + Ecken unten)
doc.moveTo(x, y).lineTo(x + w / 2, y + h * 0.55).lineTo(x + w, y)
.strokeColor(color).lineWidth(0.8).stroke();
doc.restore();
}
private drawLocationIcon(doc: PDFKit.PDFDocument, x: number, y: number, color: string): void {
doc.save();
// Map-Pin als Tropfenform mit Bezier-Kurven
const cx = x + 5; // Mittelpunkt X
const top = y - 1; // Oberkante
const r = 4; // Radius des Kopfes
const tip = y + 11; // Spitze des Pins
// Tropfenform: Start oben, links herum, Spitze unten, rechts hoch
doc.moveTo(cx, top)
.bezierCurveTo(cx - r * 1.1, top, cx - r, top + r * 0.6, cx - r, top + r)
.bezierCurveTo(cx - r, top + r * 1.8, cx - r * 0.3, tip - 2, cx, tip)
.bezierCurveTo(cx + r * 0.3, tip - 2, cx + r, top + r * 1.8, cx + r, top + r)
.bezierCurveTo(cx + r, top + r * 0.6, cx + r * 1.1, top, cx, top)
.fill(color);
// Innerer Kreis (weiß) für Pin-Look
doc.circle(cx, top + r, 1.5).fill('#ffffff');
doc.restore();
}
// ============================================================
// DOCX-Hilfsfunktionen
// ============================================================

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 750 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB