Add the Luximmo site build

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
kantarciandClaude Opus 5.5 committed 2026-11-09 08:02:38 +01:00
1 parent f2c04b311f
commit 9add795d6b
14 files changed
+1407

No files matched your search

+147
View File
@@ -0,0 +1,147 @@
<?php
/**
* Bereitet die Bilder des Luximmo-Entwurfs (luximmo-website.zip) für die Mediathek vor.
*
* Aus dem Ordner „assets“ des Pakets entstehen:
* - logo.png: das Logo ohne Rand, 640 px breit (Original 2596 × 2274, Zeichen und Schriftzug),
* - favicon.png: nur das goldene Zeichen (Kreis mit Häusern), quadratisch auf Weiß, 512 px,
* - mietverwaltung.webp, weg.webp, sonder.webp: die drei Leistungsfotos als WebP,
* - hero.webm: das Hero-Video unverändert,
* - hero-poster.webp: das Standbild aus dem Video (vorher mit build-luximmo-poster.mjs als PNG).
*
* Aufruf: php scripts/build-luximmo-assets.php <assets-Ordner des Pakets> <Zielordner> [poster.png]
*/
$quelle = rtrim($argv[1] ?? '', '/\\');
$ziel = rtrim($argv[2] ?? '', '/\\');
$poster = $argv[3] ?? '';
if ('' === $quelle || '' === $ziel || !is_file($quelle.'/luximmo-logo.png')) {
fwrite(STDERR, "Aufruf: php scripts/build-luximmo-assets.php <Ordner mit luximmo-logo.png usw.> <Zielordner> [poster.png]\n");
exit(1);
}
if (!is_dir($ziel) && !mkdir($ziel, 0775, true)) {
fwrite(STDERR, "Zielordner lässt sich nicht anlegen: $ziel\n");
exit(1);
}
/** Verkleinert ein Bild auf eine Zielbreite und speichert es als WebP. */
$webp = static function (GdImage $bild, string $name, int $breite) use ($ziel): void {
$breite = min($breite, imagesx($bild));
$hoehe = (int) round(imagesy($bild) * $breite / imagesx($bild));
$neu = imagecreatetruecolor($breite, $hoehe);
imagecopyresampled($neu, $bild, 0, 0, 0, 0, $breite, $hoehe, imagesx($bild), imagesy($bild));
imagewebp($neu, $ziel.'/'.$name, 80);
echo "$name: {$breite} × {$hoehe}\n";
};
foreach (['mietverwaltung', 'weg', 'sonder'] as $foto) {
$bild = imagecreatefromjpeg($quelle.'/'.$foto.'.jpg') ?: exit("Foto fehlt: $foto.jpg\n");
$webp($bild, $foto.'.webp', 1600);
}
if ('' !== $poster) {
$webp(imagecreatefrompng($poster) ?: exit("Standbild lässt sich nicht lesen: $poster\n"), 'hero-poster.webp', 1920);
}
copy($quelle.'/hero.webm', $ziel.'/hero.webm');
echo "hero.webm übernommen\n";
// Das Logo: Grenzen aller nicht weißen, nicht durchsichtigen Punkte, dazu die des Goldtons allein.
$logo = imagecreatefrompng($quelle.'/luximmo-logo.png');
imagepalettetotruecolor($logo);
$alles = [PHP_INT_MAX, PHP_INT_MAX, 0, 0];
$gold = [PHP_INT_MAX, PHP_INT_MAX, 0, 0];
for ($y = 0; $y < imagesy($logo); $y += 2) {
for ($x = 0; $x < imagesx($logo); $x += 2) {
$c = imagecolorat($logo, $x, $y);
[$a, $r, $g, $b] = [$c >> 24 & 0x7F, $c >> 16 & 0xFF, $c >> 8 & 0xFF, $c & 0xFF];
if ($a > 100 || ($r > 235 && $g > 235 && $b > 235)) {
continue;
}
$alles = [min($alles[0], $x), min($alles[1], $y), max($alles[2], $x), max($alles[3], $y)];
if ($r - $b > 40) {
$gold = [min($gold[0], $x), min($gold[1], $y), max($gold[2], $x), max($gold[3], $y)];
}
}
}
/** Ein Ausschnitt des Logos auf eine Fläche gesetzt: durchsichtig oder weiß. */
$ausschnitt = static function (array $kasten, int $breite, int $hoehe, int $rand, bool $weiss) use ($logo): GdImage {
$neu = imagecreatetruecolor($breite, $hoehe);
imagealphablending($neu, false);
imagesavealpha($neu, true);
imagefill($neu, 0, 0, $weiss ? imagecolorallocate($neu, 255, 255, 255) : imagecolorallocatealpha($neu, 255, 255, 255, 127));
imagealphablending($neu, true);
[$x1, $y1, $x2, $y2] = $kasten;
$b = $x2 - $x1 + 1;
$h = $y2 - $y1 + 1;
$faktor = min(($breite - 2 * $rand) / $b, ($hoehe - 2 * $rand) / $h);
$zb = (int) round($b * $faktor);
$zh = (int) round($h * $faktor);
imagecopyresampled($neu, $logo, (int) (($breite - $zb) / 2), (int) (($hoehe - $zh) / 2), $x1, $y1, $zb, $zh, $b, $h);
return $neu;
};
$breite = 640;
$hoehe = (int) round(($alles[3] - $alles[1] + 1) * $breite / ($alles[2] - $alles[0] + 1));
imagepng($ausschnitt($alles, $breite, $hoehe, 0, false), $ziel.'/logo.png', 9);
echo "logo.png: {$breite} × {$hoehe}\n";
imagepng($ausschnitt($gold, 512, 512, 36, true), $ziel.'/favicon.png', 9);
echo "favicon.png: 512 × 512 (Zeichen ohne Schriftzug)\n";
// Piktogramme für die Objektart der Immobilienbewertung: goldene Linien auf Creme, wie das Logo.
// Vierfach gezeichnet und verkleinert, damit die dicken Linien glatte Kanten bekommen.
$piktogramm = static function (string $name, callable $zeichnen) use ($ziel): void {
$s = 4;
$gross = imagecreatetruecolor(480 * $s, 360 * $s);
imagefill($gross, 0, 0, imagecolorallocate($gross, 0xF7, 0xF4, 0xEF));
$gold = imagecolorallocate($gross, 0xBD, 0xA0, 0x76);
imagesetthickness($gross, 5 * $s);
$p = static fn (array $punkte): array => array_map(static fn (int $v): int => $v * $s, $punkte);
$linie = static fn (int $x1, int $y1, int $x2, int $y2) => imageline($gross, $x1 * $s, $y1 * $s, $x2 * $s, $y2 * $s, $gold);
$rahmen = static fn (int $x1, int $y1, int $x2, int $y2) => imagepolygon($gross, $p([$x1, $y1, $x2, $y1, $x2, $y2, $x1, $y2]), $gold);
$flaeche = static fn (int $x1, int $y1, int $x2, int $y2) => imagefilledrectangle($gross, $x1 * $s, $y1 * $s, $x2 * $s, $y2 * $s, $gold);
$zeichnen($linie, $rahmen, $flaeche, static fn (array $punkte) => imagepolygon($gross, $p($punkte), $gold));
$klein = imagecreatetruecolor(480, 360);
imagecopyresampled($klein, $gross, 0, 0, 0, 0, 480, 360, 480 * $s, 360 * $s);
imagewebp($klein, $ziel.'/'.$name, 90);
echo "$name: 480 × 360\n";
};
$piktogramm('option-wohnung.webp', static function ($linie, $rahmen, $flaeche): void {
$linie(110, 300, 370, 300);
$rahmen(180, 80, 300, 300);
foreach ([110, 150, 190, 230] as $y) {
foreach ([200, 228, 256] as $x) {
150 === $y && 228 === $x ? $flaeche($x, $y, $x + 22, $y + 22) : $rahmen($x, $y, $x + 22, $y + 22);
}
}
$rahmen(225, 262, 255, 300);
});
$piktogramm('option-haus.webp', static function ($linie, $rahmen, $flaeche, $vieleck): void {
$linie(110, 300, 370, 300);
$vieleck([150, 175, 240, 98, 330, 175]);
$rahmen(170, 175, 310, 300);
$rahmen(226, 240, 256, 300);
$rahmen(186, 198, 214, 226);
$rahmen(268, 198, 296, 226);
});
$piktogramm('option-mehrfamilienhaus.webp', static function ($linie, $rahmen): void {
$linie(100, 300, 380, 300);
$rahmen(140, 100, 340, 300);
$linie(140, 122, 340, 122);
foreach ([140, 178, 216] as $y) {
foreach ([162, 202, 256, 296] as $x) {
$rahmen($x, $y, $x + 22, $y + 22);
}
}
$rahmen(224, 256, 256, 300);
});
$piktogramm('option-grundstueck.webp', static function ($linie, $rahmen, $flaeche, $vieleck): void {
$vieleck([100, 290, 380, 290, 325, 195, 155, 195]);
$linie(240, 245, 240, 120);
$vieleck([240, 120, 290, 138, 240, 156]);
foreach ([[100, 290], [380, 290], [325, 195], [155, 195]] as [$x, $y]) {
$flaeche($x - 7, $y - 7, $x + 7, $y + 7);
}
});
+37
View File
@@ -0,0 +1,37 @@
// Holt ein Standbild aus dem Hero-Video von Luximmo: Es steht als Hintergrundbild der Sektion,
// bis das Video läuft (und für immer bei „Bewegung reduzieren“ oder Datensparen). ffmpeg gibt es
// hier nicht, Chromium spielt WebM ab und zeichnet das Bild in ein Canvas.
//
// Aufruf: node scripts/build-luximmo-poster.mjs <hero.webm> <Ziel.png> [Sekunde]
import { readFileSync, writeFileSync } from 'node:fs';
import { chromium } from 'playwright';
const [quelle, ziel, sekunde = '0.4'] = process.argv.slice(2);
if (!quelle || !ziel) {
console.error('Aufruf: node scripts/build-luximmo-poster.mjs <hero.webm> <Ziel.png> [Sekunde]');
process.exit(1);
}
const browser = await chromium.launch();
try {
const page = await browser.newPage();
await page.setContent('<video muted playsinline></video>');
const daten = readFileSync(quelle).toString('base64');
const png = await page.evaluate(async ([b64, t]) => {
const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
const video = document.querySelector('video');
video.src = URL.createObjectURL(new Blob([bytes], { type: 'video/webm' }));
await new Promise((ok, fehler) => { video.onloadeddata = ok; video.onerror = () => fehler(new Error('Video lässt sich nicht lesen')); });
video.currentTime = Math.min(Number(t), Math.max(0, video.duration - 0.1));
await new Promise((ok) => { video.onseeked = ok; });
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.getContext('2d').drawImage(video, 0, 0);
return { url: canvas.toDataURL('image/png'), breite: video.videoWidth, hoehe: video.videoHeight, dauer: video.duration };
}, [daten, sekunde]);
writeFileSync(ziel, Buffer.from(png.url.split(',')[1], 'base64'));
console.log(`Standbild ${png.breite} × ${png.hoehe} (Video ${png.dauer.toFixed(1)} s) → ${ziel}`);
} finally {
await browser.close();
}
+34
View File
@@ -0,0 +1,34 @@
#!/bin/sh
# Wirft die Luximmo-Installation weg und baut sie neu auf. Nur fuer den Aufbau gedacht.
# Erstes Argument (optional): der Ordner „assets“ aus luximmo-website.zip (Logo, Fotos, hero.webm).
# Dann entstehen Logo, Seitensymbol, WebP-Fotos und das Standbild des Videos neu.
# Empfaenger der Formulare per LUXIMMO_MAIL, sonst eine Platzhalteradresse.
set -e
root="C:/Django Project/KantSites"
php="${PHP_BINARY:-C:/php84/php.exe}"
export DATABASE_URL="sqlite:///$root/var/luximmo.db"
export MEDIA_DIRECTORY="$root/var/luximmo-media"
assets="$root/var/luximmo-assets"
if [ -n "$1" ]; then
mkdir -p "$assets"
node scripts/build-luximmo-poster.mjs "$1/hero.webm" "$assets/hero-poster.png" 0.4
"$php" scripts/build-luximmo-assets.php "$1" "$assets" "$assets/hero-poster.png"
rm -f "$assets/hero-poster.png"
fi
if [ ! -f "$assets/hero-poster.webp" ] || [ ! -f "$assets/logo-original.svg" ] || [ ! -f "$assets/hero.webm" ]; then
echo "Bilder fehlen: erst mit dem assets-Ordner des Pakets als Argument aufrufen." >&2
exit 1
fi
rm -f "$root/var/luximmo.db"
rm -rf "$root/var/luximmo-media"
"$php" vendor/bin/doctrine-migrations migrate --no-interaction -q
"$php" -d memory_limit=512M scripts/seed-luximmo.php "$assets"
# Instrument Sans vom eigenen Server; der Abruf ergaenzt fonts.css nur.
if [ -f "$root/var/cacert.pem" ]; then
"$php" -d curl.cainfo="$root/var/cacert.pem" -d openssl.cafile="$root/var/cacert.pem" bin/console cms:fonts:fetch
else
"$php" bin/console cms:fonts:fetch
fi
# Das Inhaltspaket fuer eine Live-Seite (ADR 118): dort unter System -> Inhalte uebertragen importieren.
rm -f "$root/var/luximmo-paket.zip"
"$php" bin/console cms:paket:export --ziel="$root/var/luximmo-paket.zip"
File diff suppressed because it is too large. Load diff