diff --git a/scripts/build-agent-portraits.mjs b/scripts/build-agent-portraits.mjs new file mode 100644 index 0000000..71ff1cd --- /dev/null +++ b/scripts/build-agent-portraits.mjs @@ -0,0 +1,131 @@ +/** + * Erzeugt die Agentenporträts aus den hochauflösenden Originalen. + * + * Die Porträts im Repository stammten bis Runde 6 aus einem HTML-Konzept und + * lagen dort nur als kleine, liegende Zuschnitte vor — Ferdi etwa 215 × 160 + * Bildpunkte bei 6 KB. Im 3D-Meetingraum sind sie damit sichtbar unscharf, und + * hochskalieren hilft nicht. + * + * Die Originale liegen in der Präsentation `PropertyOn_Engeli_v5.pptx` als + * quadratische Bilder mit 1200 × 1200 Bildpunkten. Sie zeigen dieselben fünf + * Personen — Kleidung, Frisur und Ausschnitt stimmen mit den bisherigen + * Zuschnitten überein, es werden also keine Gesichter getauscht. + * + * Das Skript beschneidet den Rand (die Bilder tragen oben und an den Seiten + * Reste des Folienrahmens), skaliert auf eine für den Browser sinnvolle Grösse + * und schreibt sie nach `src/assets/team/`. + * + * Einmalig auszuführen; die Ergebnisse liegen als Assets im Repository: + * + * node scripts/build-agent-portraits.mjs + * + * Es braucht keine Bildbibliothek — die Verarbeitung läuft auf einem Canvas im + * bereits vorhandenen Chrome, den auch die visuellen Prüfungen nutzen. + */ + +import { chromium } from 'playwright-core' +import { existsSync, readFileSync, writeFileSync } from 'node:fs' +import { inflateRawSync } from 'node:zlib' +import { fileURLToPath } from 'node:url' +import { dirname, resolve } from 'node:path' + +const here = dirname(fileURLToPath(import.meta.url)) +const ziel = resolve(here, '../src/assets/team') + +/** Quellpräsentation. Nur zum Erzeugen nötig, nicht Teil des Repositorys. */ +const QUELLE = process.env.PORTRAIT_SOURCE + ?? 'C:/Users/beni_/Downloads/PropertyOn_Engeli_v5.pptx' + +/** + * Zuordnung Bild → Agent. + * + * Die Reihenfolge entspricht der Titelfolie der Präsentation: Ferdi, Bruno, + * Livia, Nora, Sina. Jede Zuordnung wurde einzeln gegen den bisherigen + * Zuschnitt geprüft — eine frühere Quelle trug dieselben Vornamen für eine + * andere Belegschaft, und ein vertauschtes Gesicht wäre schlimmer als ein + * unscharfes. + */ +const ZUORDNUNG = { + 'image1.jpg': 'ferdi', + 'image2.jpg': 'bruno', + 'image3.jpg': 'livia', + 'image4.jpg': 'nora', + 'image5.jpg': 'sina', +} + +/** Kantenbeschnitt je Seite, als Anteil der Bildbreite. */ +const BESCHNITT = 0.03 +/** Zielgrösse. Reicht für jede Darstellung im Programm, bleibt klein im Bündel. */ +const GROESSE = 512 + +const CHROME = [ + process.env.CHROME_PATH, + 'C:/Program Files/Google/Chrome/Application/chrome.exe', + 'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe', +].filter(Boolean).find(p => existsSync(p)) + +if (!CHROME) throw new Error('Kein Chrome gefunden — CHROME_PATH setzen.') +if (!existsSync(QUELLE)) throw new Error(`Quelldatei fehlt: ${QUELLE}`) + +/** Bilder aus dem ZIP-Container der Präsentation lesen, ohne Fremdbibliothek. */ +function medienAusPptx(pfad, gesucht) { + const b = readFileSync(pfad) + const gefunden = {} + let i = 0 + while (i < b.length - 4) { + if (b.readUInt32LE(i) !== 0x04034b50) { i++; continue } + const method = b.readUInt16LE(i + 8) + const csize = b.readUInt32LE(i + 18) + const nameLen = b.readUInt16LE(i + 26) + const extraLen = b.readUInt16LE(i + 28) + const name = b.slice(i + 30, i + 30 + nameLen).toString() + const start = i + 30 + nameLen + extraLen + const datei = name.split('/').pop() + if (name.startsWith('ppt/media/') && gesucht.includes(datei)) { + try { + gefunden[datei] = method === 8 + ? inflateRawSync(b.slice(start, start + csize)) + : b.slice(start, start + csize) + } catch { /* beschädigter Eintrag — überspringen */ } + } + i = start + csize + } + return gefunden +} + +const roh = medienAusPptx(QUELLE, Object.keys(ZUORDNUNG)) +const fehlend = Object.keys(ZUORDNUNG).filter(f => !roh[f]) +if (fehlend.length) throw new Error(`Nicht gefunden: ${fehlend.join(', ')}`) + +const browser = await chromium.launch({ executablePath: CHROME }) +const page = await browser.newPage() + +for (const [datei, agent] of Object.entries(ZUORDNUNG)) { + const dataUri = `data:image/jpeg;base64,${roh[datei].toString('base64')}` + const ergebnis = await page.evaluate(async ({ src, beschnitt, groesse }) => { + const img = await new Promise((ok, err) => { + const el = new Image() + el.onload = () => ok(el) + el.onerror = err + el.src = src + }) + const rand = Math.round(Math.min(img.width, img.height) * beschnitt) + const sw = img.width - 2 * rand + const sh = img.height - 2 * rand + const c = document.createElement('canvas') + c.width = groesse + c.height = groesse + const g = c.getContext('2d') + g.imageSmoothingEnabled = true + g.imageSmoothingQuality = 'high' + g.drawImage(img, rand, rand, sw, sh, 0, 0, groesse, groesse) + return { dataUri: c.toDataURL('image/jpeg', 0.86), quelle: `${img.width}x${img.height}` } + }, { src: dataUri, beschnitt: BESCHNITT, groesse: GROESSE }) + + const aus = resolve(ziel, `${agent}.jpg`) + writeFileSync(aus, Buffer.from(ergebnis.dataUri.split(',')[1], 'base64')) + const kb = (readFileSync(aus).length / 1024).toFixed(0) + console.log(`${agent.padEnd(7)} ${ergebnis.quelle} → ${GROESSE}x${GROESSE}, ${kb} KB`) +} + +await browser.close() diff --git a/src/assets/team/bruno.jpg b/src/assets/team/bruno.jpg index 36e878c..e2bf53a 100644 Binary files a/src/assets/team/bruno.jpg and b/src/assets/team/bruno.jpg differ diff --git a/src/assets/team/ferdi.jpg b/src/assets/team/ferdi.jpg index f1a0566..6ee14d2 100644 Binary files a/src/assets/team/ferdi.jpg and b/src/assets/team/ferdi.jpg differ diff --git a/src/assets/team/livia.jpg b/src/assets/team/livia.jpg index 130ffa7..316cd45 100644 Binary files a/src/assets/team/livia.jpg and b/src/assets/team/livia.jpg differ diff --git a/src/assets/team/nora.jpg b/src/assets/team/nora.jpg index 5036413..ce1b186 100644 Binary files a/src/assets/team/nora.jpg and b/src/assets/team/nora.jpg differ diff --git a/src/assets/team/sina.jpg b/src/assets/team/sina.jpg index 0e9a2bf..830c505 100644 Binary files a/src/assets/team/sina.jpg and b/src/assets/team/sina.jpg differ