feat(agenten): Porträts in 512×512 aus den Originalen der Präsentation
Die Porträts lagen bisher nur als kleine, liegende Zuschnitte aus einem HTML-Konzept vor — Ferdi etwa 215×160 Bildpunkte bei 6 KB. Im 3D-Meetingraum waren sie damit sichtbar unscharf, und Hochskalieren hilft dagegen nicht. Die Originale liegen in `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, und die Präsentation nennt Name und Funktion direkt neben jedem Bild. Jede Zuordnung wurde einzeln gegen den alten 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. `scripts/build-agent-portraits.mjs` beschneidet den Folienrand, skaliert auf 512×512 und schreibt die Dateien. Das reicht für jede Darstellung im Programm — im Raum sind die Kreise höchstens rund 100 Bildpunkte gross — und hält das Bündel klein: rund 30 KB je Porträt statt 250 KB im Original. Die Verarbeitung läuft ohne Bildbibliothek auf einem Canvas im vorhandenen Chrome, wie schon beim Basel-Panorama. Keine neue Abhängigkeit. Geprüft: 422 Tests, Typecheck, Build, ESLint, Token-Schwelle (884/925), Szene im Browser ohne Konsolenfehler. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
4349641ee9
commit
9e825c04bf
6 files changed
+131
No files matched your search
@@ -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()
|
||||
Reference in new issue
Block a user