Add the Luximmo site build
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f2c04b311f
commit
9add795d6b
14 files changed
+1407
No files matched your search
@@ -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);
|
||||
}
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
Reference in new issue
Block a user