mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-25 00:16:41 +02:00
feat: recolor PNG contact icons to match accent color at runtime
Uses pngjs to replace all visible pixels in PNG icons with the configured accent color while preserving alpha transparency. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9a9800e17e
commit
b948027dab
3 changed files with 57 additions and 11 deletions
29
packages/core-service/package-lock.json
generated
29
packages/core-service/package-lock.json
generated
|
|
@ -30,6 +30,7 @@
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"pdfkit": "^0.17.2",
|
"pdfkit": "^0.17.2",
|
||||||
|
"pngjs": "^7.0.0",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
|
|
@ -46,6 +47,7 @@
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
"@types/passport-jwt": "^4.0.1",
|
"@types/passport-jwt": "^4.0.1",
|
||||||
"@types/pdfkit": "^0.17.5",
|
"@types/pdfkit": "^0.17.5",
|
||||||
|
"@types/pngjs": "^6.0.5",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||||
|
|
@ -2712,6 +2714,16 @@
|
||||||
"@types/node": "*"
|
"@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": {
|
"node_modules/@types/qrcode": {
|
||||||
"version": "1.5.6",
|
"version": "1.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz",
|
||||||
|
|
@ -8836,12 +8848,12 @@
|
||||||
"integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g=="
|
"integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g=="
|
||||||
},
|
},
|
||||||
"node_modules/pngjs": {
|
"node_modules/pngjs": {
|
||||||
"version": "5.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
|
||||||
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
|
"integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.13.0"
|
"node": ">=14.19.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prelude-ls": {
|
"node_modules/prelude-ls": {
|
||||||
|
|
@ -9079,6 +9091,15 @@
|
||||||
"node": ">=8"
|
"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": {
|
"node_modules/qrcode/node_modules/y18n": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"pdfkit": "^0.17.2",
|
"pdfkit": "^0.17.2",
|
||||||
|
"pngjs": "^7.0.0",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
|
|
@ -63,6 +64,7 @@
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
"@types/passport-jwt": "^4.0.1",
|
"@types/passport-jwt": "^4.0.1",
|
||||||
"@types/pdfkit": "^0.17.5",
|
"@types/pdfkit": "^0.17.5",
|
||||||
|
"@types/pngjs": "^6.0.5",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { ExpertProfileService } from './expert-profile.service';
|
||||||
import PDFDocument from 'pdfkit';
|
import PDFDocument from 'pdfkit';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import { PNG } from 'pngjs';
|
||||||
import {
|
import {
|
||||||
Document,
|
Document,
|
||||||
Packer,
|
Packer,
|
||||||
|
|
@ -102,11 +103,15 @@ export class ProfileExportService {
|
||||||
|
|
||||||
constructor(private readonly expertProfileService: ExpertProfileService) {}
|
constructor(private readonly expertProfileService: ExpertProfileService) {}
|
||||||
|
|
||||||
private loadIcon(name: string): Buffer | null {
|
private loadIcon(name: string, color?: string): Buffer | null {
|
||||||
try {
|
try {
|
||||||
const iconPath = path.join(this.iconsDir, name);
|
const iconPath = path.join(this.iconsDir, name);
|
||||||
if (fs.existsSync(iconPath)) {
|
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}`);
|
this.logger.warn(`Icon nicht gefunden: ${iconPath}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -115,6 +120,24 @@ export class ProfileExportService {
|
||||||
return null;
|
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
|
// PDF Export
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -191,11 +214,11 @@ export class ProfileExportService {
|
||||||
// --- KONTAKT ---
|
// --- KONTAKT ---
|
||||||
yLeft = this.pdfSectionTitle(doc, 'KONTAKT', leftColX, yLeft, leftColWidth, accentColor);
|
yLeft = this.pdfSectionTitle(doc, 'KONTAKT', leftColX, yLeft, leftColWidth, accentColor);
|
||||||
|
|
||||||
// Icons laden
|
// Icons laden (eingefärbt in Akzentfarbe)
|
||||||
const phoneIcon = this.loadIcon('Phone.png');
|
const phoneIcon = this.loadIcon('Phone.png', accentColor);
|
||||||
const mobileIcon = this.loadIcon('Mobile.png');
|
const mobileIcon = this.loadIcon('Mobile.png', accentColor);
|
||||||
const mailIcon = this.loadIcon('Mail.png');
|
const mailIcon = this.loadIcon('Mail.png', accentColor);
|
||||||
const addressIcon = this.loadIcon('Address.png');
|
const addressIcon = this.loadIcon('Address.png', accentColor);
|
||||||
const iconSize = 12;
|
const iconSize = 12;
|
||||||
const iconTextOffset = 20; // Abstand Icon → Text
|
const iconTextOffset = 20; // Abstand Icon → Text
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue