diff --git a/packages/core-service/package-lock.json b/packages/core-service/package-lock.json index cf1532b..b01241a 100644 --- a/packages/core-service/package-lock.json +++ b/packages/core-service/package-lock.json @@ -30,6 +30,7 @@ "passport": "^0.7.0", "passport-jwt": "^4.0.1", "pdfkit": "^0.17.2", + "pngjs": "^7.0.0", "qrcode": "^1.5.4", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", @@ -46,6 +47,7 @@ "@types/node": "^22.0.0", "@types/passport-jwt": "^4.0.1", "@types/pdfkit": "^0.17.5", + "@types/pngjs": "^6.0.5", "@types/qrcode": "^1.5.5", "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0", @@ -2712,6 +2714,16 @@ "@types/node": "*" } }, + "node_modules/@types/pngjs": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@types/pngjs/-/pngjs-6.0.5.tgz", + "integrity": "sha512-0k5eKfrA83JOZPppLtS2C7OUtyNAl2wKNxfyYl9Q5g9lPkgBl/9hNyAu6HuEH2J4XmIv2znEpkDd0SaZVxW6iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/qrcode": { "version": "1.5.6", "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", @@ -8836,12 +8848,12 @@ "integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==" }, "node_modules/pngjs": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", - "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", "license": "MIT", "engines": { - "node": ">=10.13.0" + "node": ">=14.19.0" } }, "node_modules/prelude-ls": { @@ -9079,6 +9091,15 @@ "node": ">=8" } }, + "node_modules/qrcode/node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/qrcode/node_modules/y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", diff --git a/packages/core-service/package.json b/packages/core-service/package.json index 499e636..e54dfeb 100644 --- a/packages/core-service/package.json +++ b/packages/core-service/package.json @@ -47,6 +47,7 @@ "passport": "^0.7.0", "passport-jwt": "^4.0.1", "pdfkit": "^0.17.2", + "pngjs": "^7.0.0", "qrcode": "^1.5.4", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", @@ -63,6 +64,7 @@ "@types/node": "^22.0.0", "@types/passport-jwt": "^4.0.1", "@types/pdfkit": "^0.17.5", + "@types/pngjs": "^6.0.5", "@types/qrcode": "^1.5.5", "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0", 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 a245302..af91087 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 @@ -3,6 +3,7 @@ import { ExpertProfileService } from './expert-profile.service'; import PDFDocument from 'pdfkit'; import * as fs from 'fs'; import * as path from 'path'; +import { PNG } from 'pngjs'; import { Document, Packer, @@ -102,11 +103,15 @@ export class ProfileExportService { constructor(private readonly expertProfileService: ExpertProfileService) {} - private loadIcon(name: string): Buffer | null { + private loadIcon(name: string, color?: string): Buffer | null { try { const iconPath = path.join(this.iconsDir, name); if (fs.existsSync(iconPath)) { - return fs.readFileSync(iconPath); + const raw = fs.readFileSync(iconPath); + if (color) { + return this.recolorPng(raw, color); + } + return raw; } this.logger.warn(`Icon nicht gefunden: ${iconPath}`); } catch (err) { @@ -115,6 +120,24 @@ export class ProfileExportService { return null; } + /** Ersetzt alle sichtbaren Pixel eines PNG durch die angegebene Farbe (Alpha bleibt erhalten) */ + private recolorPng(pngBuffer: Buffer, hexColor: string): Buffer { + const { r, g, b } = hexToRgb(hexColor); + const png = PNG.sync.read(pngBuffer); + + for (let i = 0; i < png.data.length; i += 4) { + const alpha = png.data[i + 3]; + if (alpha > 0) { + png.data[i] = r; + png.data[i + 1] = g; + png.data[i + 2] = b; + // Alpha bleibt unverändert + } + } + + return PNG.sync.write(png); + } + // ============================================================ // PDF Export // ============================================================ @@ -191,11 +214,11 @@ export class ProfileExportService { // --- KONTAKT --- yLeft = this.pdfSectionTitle(doc, 'KONTAKT', leftColX, yLeft, leftColWidth, accentColor); - // 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'); + // Icons laden (eingefärbt in Akzentfarbe) + const phoneIcon = this.loadIcon('Phone.png', accentColor); + const mobileIcon = this.loadIcon('Mobile.png', accentColor); + const mailIcon = this.loadIcon('Mail.png', accentColor); + const addressIcon = this.loadIcon('Address.png', accentColor); const iconSize = 12; const iconTextOffset = 20; // Abstand Icon → Text