feat(core): PDF-Export — Akzentfarbe dynamisch aus Branding-Logo extrahiert

- extractDominantColor(): 20x20 Resize via sharp, Alpha gegen Weiss flatten,
  alle gesaettigten Pixel (nicht weiss/schwarz/grau, range > 35) mitteln
- Ergebnis wird als accentColor fuer Timeline-Linien, Ueberschriften,
  Skill-Chips usw. verwendet
- Fallback auf #009688 wenn kein Logo hinterlegt oder keine Farbe extrahierbar

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Thomas Reitz 2026-03-14 09:27:41 +01:00
parent f5d83dc1c3
commit 382beab9c3

View file

@ -218,6 +218,13 @@ export class ProfileExportService {
} }
} }
} catch { /* Einstellungen nicht verfügbar */ } } catch { /* Einstellungen nicht verfügbar */ }
// Akzentfarbe dynamisch aus Logo extrahieren
if (platformLogo) {
const extracted = await this.extractDominantColor(platformLogo);
if (extracted) accentColor = extracted;
}
const fullName = `${data.firstName} ${data.lastName}`; const fullName = `${data.firstName} ${data.lastName}`;
const { firstName, lastName } = data; const { firstName, lastName } = data;
@ -953,4 +960,45 @@ export class ProfileExportService {
const base64Data = dataUrl.includes(',') ? dataUrl.split(',')[1] : dataUrl; const base64Data = dataUrl.includes(',') ? dataUrl.split(',')[1] : dataUrl;
return Buffer.from(base64Data, 'base64'); return Buffer.from(base64Data, 'base64');
} }
/**
* Extrahiert die dominante Akzentfarbe aus einem Logo-Bild.
* Strategie: 20x20 Pixel resizen, transparenten Alpha gegen Weiss flatten,
* dann alle "bunten" Pixel (nicht weiss/schwarz/grau) mitteln.
* Gibt null zurück wenn keine ausreichend gesättigte Farbe gefunden wird.
*/
private async extractDominantColor(imageBuffer: Buffer): Promise<string | null> {
try {
const { data, info } = await sharp(imageBuffer)
.resize(20, 20, { fit: 'fill' })
.flatten({ background: { r: 255, g: 255, b: 255 } })
.raw()
.toBuffer({ resolveWithObject: true });
let rSum = 0, gSum = 0, bSum = 0, count = 0;
for (let i = 0; i < info.width * info.height; i++) {
const r = data[i * 3]!;
const g = data[i * 3 + 1]!;
const b = data[i * 3 + 2]!;
const avg = (r + g + b) / 3;
const range = Math.max(r, g, b) - Math.min(r, g, b);
// Nur gesättigte Pixel: nicht zu hell, nicht zu dunkel, genug Farbabstand
if (avg > 25 && avg < 225 && range > 35) {
rSum += r;
gSum += g;
bSum += b;
count++;
}
}
if (count === 0) return null;
const r = Math.round(rSum / count);
const g = Math.round(gSum / count);
const b = Math.round(bSum / count);
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
} catch {
return null;
}
}
} }