/** * 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()