.svg — die Master-Dateien. `aktuell.svg` ist * das heutige Logo (identisch zu public/assets/img/logo.svg, das bleibt das * Website-Asset und wird hier nicht angefasst). Alte Vereinslogos kommen als * weitere Dateien dazu und werden unten in $OUTPUTS eingetragen. * * Ziel: public/assets/downloads/logo/ — je Ausgabe SVG, PNG (transparent, drei * Breiten), JPG (weißer Grund, zwei Breiten) und PDF, dazu ein ZIP über alles. * Der Ordner wird bei jedem Lauf GELEERT und neu gefüllt; nur so lässt sich eine * Variante auch wieder entfernen. * * WARUM CHROME UND NICHT bin/img-resize.php: GD kennt kein SVG, und Inkscape, * rsvg-convert oder ImageMagick sind hier nicht vorhanden (und wären auf dem * Shared Hosting auch nicht zu haben). Headless Chrome kann beides, was wir * brauchen: transparent rastern und ein echtes VEKTOR-PDF drucken (geprüft: * das PDF enthält Kurven-Operatoren, kein eingebettetes Pixelbild). * * Das Skript läuft EINMALIG LOKAL, das Ergebnis wird committet. Nichts davon * läuft auf dem Server, es gibt keinen Cron und keine Laufzeit-Abhängigkeit. * * Die Dateinamen sind absichtlich ausgeschrieben: sie landen im Download-Ordner * fremder Leute, wo „logo-2.png“ niemandem hilft. */ if (PHP_SAPI !== 'cli') { exit(1); } require dirname(__DIR__) . '/app/bootstrap.php'; require_once __DIR__ . '/chrome-lib.php'; $verbose = in_array('-v', $argv, true); $log = static function (string $msg, string $level = 'INFO') use ($verbose): void { log_write('logo', $level, $msg); if ($verbose || $level === 'ERROR' || $level === 'WARN') { echo '[' . date('c') . "] {$level} {$msg}\n"; } }; $fail = static function (string $msg) use ($log): never { $log($msg, 'ERROR'); exit(1); }; // --- Was erzeugt wird --------------------------------------------------------- // Schlüssel = Dateiname-Suffix UND Schlüssel in data/logo.json (bin/preflight.php // prüft, dass beide Listen zusammenpassen). `ersetzen` färbt die Master-SVG um, // indem die Farbklassen im -Block ausgetauscht werden — die CorelDRAW-Datei // legt alle Farben genau dort ab (.fil0/.fil3 schwarz, .fil1 weiß, .fil2 rot). $OUTPUTS = [ 'farbig' => [ 'quelle' => 'aktuell.svg', 'ersetzen' => [], ], // Das genaue Gegenstück zu einfarbig-schwarz: dort trägt die weiße Fläche die // schwarzen Linien, hier tragen die weißen Linien den freien Grund. KEIN Rot — // eine einfarbige Fassung ist einfarbig, sonst wäre sie nur ein farbiges Logo // mit weißen Konturen (so war es bis zum 17.09.2026 und war schlicht falsch). 'einfarbig-weiss' => [ 'quelle' => 'aktuell.svg', // Weiße Konturen auf weißem JPG wären unsichtbar: diese Variante wird auf // den Anthrazit-Grund der Website gelegt, denn genau dafür ist sie gedacht. 'jpg_bg' => [0x22, 0x22, 0x22], 'ersetzen' => [ '.fil3 {fill:black}' => '.fil3 {fill:white}', '.fil0 {fill:black;' => '.fil0 {fill:white;', '.fil1 {fill:white}' => '.fil1 {fill:none}', '.fil2 {fill:red}' => '.fil2 {fill:none}', ], ], // Reine Strichzeichnung: Linien schwarz, ALLE Flächen frei. Vorher wurde das Rot // durch Weiß ersetzt, dann steckte im PNG eine deckend weiße Schildfläche — auf // farbigem Papier oder einem Briefkopf lag da ein weißer Klotz. Für Stempel, // Gravur und Stickerei will man die Kontur, nicht die Fläche. 'einfarbig-schwarz' => [ 'quelle' => 'aktuell.svg', 'ersetzen' => [ '.fil2 {fill:red}' => '.fil2 {fill:none}', '.fil1 {fill:white}' => '.fil1 {fill:none}', ], ], ]; const LOGO_PNG_WIDTHS = [500, 1000, 2000]; const LOGO_JPG_WIDTHS = [1000, 2000]; // 90 statt der 78 aus bin/img-resize.php: ein Logo ist Fläche mit harten Kanten, // bei 78 franst die Rundung des Wappens sichtbar aus. Fotos vertragen 78, Grafik nicht. const LOGO_JPG_QUALITY = 90; const LOGO_PREFIX = 'tsv08-kulmbach-logo'; $srcDir = STORAGE_PATH . '/logo-quellen'; $destDir = PUBLIC_PATH . '/assets/downloads/logo'; $tmpDir = sys_get_temp_dir() . '/tsv08-logo-' . getmypid(); // --- Chrome finden --- $chrome = chrome_binary(); if ($chrome === null) { $fail('Kein Chrome gefunden. Pfad per Umgebungsvariable setzen: CHROME="/Pfad/zu/Chrome" php bin/logo-build.php'); } if (!is_dir($srcDir)) { $fail("Quellordner fehlt: {$srcDir}"); } if (!is_dir($destDir) && !mkdir($destDir, 0775, true) && !is_dir($destDir)) { $fail("Zielordner nicht anlegbar: {$destDir}"); } if (!mkdir($tmpDir, 0775, true) && !is_dir($tmpDir)) { $fail("Arbeitsordner nicht anlegbar: {$tmpDir}"); } register_shutdown_function(static function () use ($tmpDir): void { foreach (glob($tmpDir . '/*') ?: [] as $file) { @unlink($file); } @rmdir($tmpDir); }); $runChrome = static function (array $args) use ($chrome): void { chrome_run($chrome, $args); }; /** Maße der SVG in mm aus dem Wurzelelement lesen (für die PDF-Seitengröße). */ $svgSize = static function (string $svg) use ($fail): array { if (!preg_match('/width="([\d.]+)mm"/', $svg, $w) || !preg_match('/height="([\d.]+)mm"/', $svg, $h)) { if (preg_match('/viewBox="[\d.\s-]*?([\d.]+)\s+([\d.]+)"/', $svg, $vb)) { return [(float) $vb[1], (float) $vb[2]]; // viewBox-Einheiten als mm deuten } $fail('SVG ohne verwertbare Maße (weder mm-Angaben noch viewBox).'); } return [(float) $w[1], (float) $h[1]]; }; /** * PNG mit Alphakanal auf eine deckende Fläche legen und als JPG sichern. * JPG kann keine Transparenz; ohne dieses Unterlegen rechnet GD den Alphakanal * gegen Schwarz und das Wappen bekäme einen schwarzen Kasten. */ $flattenToJpg = static function (string $pngPath, string $jpgPath, int $quality, array $bg): bool { $png = @imagecreatefrompng($pngPath); if (!$png) { return false; } $canvas = imagecreatetruecolor(imagesx($png), imagesy($png)); imagefill($canvas, 0, 0, imagecolorallocate($canvas, $bg[0], $bg[1], $bg[2])); imagecopy($canvas, $png, 0, 0, 0, 0, imagesx($png), imagesy($png)); return imagejpeg($canvas, $jpgPath, $quality); }; // --- Zielordner leeren -------------------------------------------------------- foreach (glob($destDir . '/*') ?: [] as $old) { if (is_file($old)) { unlink($old); } } // --- Erzeugen ----------------------------------------------------------------- $built = []; foreach ($OUTPUTS as $key => $spec) { $srcFile = $srcDir . '/' . $spec['quelle']; if (!is_file($srcFile)) { $fail("Master-Datei fehlt: {$srcFile}"); } $base = LOGO_PREFIX . '-' . $key; $isVector = strtolower(pathinfo($srcFile, PATHINFO_EXTENSION)) === 'svg'; if (!$isVector) { // Alte Logos liegen oft nur als Scan vor: dann gibt es kein SVG und kein PDF, // aber PNG und JPG lassen sich wie überall sonst im Projekt erzeugen. foreach (resize_image_variants($srcFile, $destDir . '/' . $base, LOGO_PNG_WIDTHS, LOGO_JPG_QUALITY, true) as $v) { $built[] = $v['file']; } foreach (resize_image_variants($srcFile, $destDir . '/' . $base, LOGO_JPG_WIDTHS, LOGO_JPG_QUALITY) as $v) { $built[] = $v['file']; } $log("{$key}: aus Rasterquelle erzeugt (kein SVG/PDF möglich).", 'WARN'); continue; } // 1. Varianten-SVG $svg = file_get_contents($srcFile); if ($svg === false) { $fail("Master-Datei nicht lesbar: {$srcFile}"); } foreach ($spec['ersetzen'] as $from => $to) { if (!str_contains($svg, $from)) { $fail("Farbklasse „{$from}“ steht nicht in {$spec['quelle']} — die Variante „{$key}“ wäre unverändert."); } $svg = str_replace($from, $to, $svg); } $svgFile = $destDir . '/' . $base . '.svg'; file_put_contents($svgFile, $svg); $built[] = $svgFile; [$mmW, $mmH] = $svgSize($svg); $ratio = $mmH / $mmW; // Arbeitskopie: Chrome lädt die SVG relativ zur Wrapper-Datei. $workSvg = $tmpDir . '/' . $base . '.svg'; file_put_contents($workSvg, $svg); // 2. PNG, transparent foreach (LOGO_PNG_WIDTHS as $width) { $height = (int) round($width * $ratio); $wrap = $tmpDir . "/wrap-{$key}-{$width}.html"; file_put_contents($wrap, << HTML); $pngFile = $destDir . '/' . $base . '-' . $width . '.png'; $runChrome([ '--default-background-color=00000000', '--window-size=' . $width . ',' . $height, '--screenshot=' . escapeshellarg($pngFile), escapeshellarg('file://' . $wrap), ]); if (!is_file($pngFile) || filesize($pngFile) < 1000) { $fail("PNG nicht erzeugt: {$pngFile} — läuft Chrome?"); } $built[] = $pngFile; // 3. JPG auf weiß, aus der passenden PNG-Breite if (in_array($width, LOGO_JPG_WIDTHS, true)) { $jpgFile = $destDir . '/' . $base . '-' . $width . '.jpg'; if (!$flattenToJpg($pngFile, $jpgFile, LOGO_JPG_QUALITY, $spec['jpg_bg'] ?? [255, 255, 255])) { $fail("JPG nicht erzeugt: {$jpgFile}"); } $built[] = $jpgFile; } } // 4. PDF, Vektor $pdfWrap = $tmpDir . "/pdf-{$key}.html"; file_put_contents($pdfWrap, << HTML); $pdfFile = $destDir . '/' . $base . '.pdf'; $runChrome([ '--no-pdf-header-footer', '--print-to-pdf=' . escapeshellarg($pdfFile), escapeshellarg('file://' . $pdfWrap), ]); if (!is_file($pdfFile) || filesize($pdfFile) < 500) { $fail("PDF nicht erzeugt: {$pdfFile}"); } $built[] = $pdfFile; $log("{$key}: SVG, " . count(LOGO_PNG_WIDTHS) . ' PNG, ' . count(LOGO_JPG_WIDTHS) . ' JPG und PDF erzeugt.'); } // --- LIESMICH + ZIP ----------------------------------------------------------- // Die Texte kommen aus data/logo.json, damit Heft-, Seiten- und ZIP-Fassung nicht // auseinanderlaufen. Fehlt die JSON noch (erster Lauf), bleibt es beim Grundgerüst. $page = json_load('logo'); $club = json_load('club'); $lines = ['Logo des ' . ($club['name'] ?? 'TSV 08 Kulmbach'), str_repeat('=', 40), '']; if (!empty($page['notice']['text'])) { $lines[] = trim($page['notice']['title'] ?? 'Hinweis'); $lines[] = trim($page['notice']['text']); $lines[] = ''; } $lines[] = 'Welche Datei wofür'; $lines[] = '-------------------'; $lines[] = 'SVG Vektor, beliebig skalierbar. Erste Wahl für die Druckerei.'; $lines[] = 'PDF Vektor, für Druckereien, die kein SVG annehmen.'; $lines[] = 'PNG Pixelbild mit freigestelltem Grund, für Word, PowerPoint und Aushänge.'; $lines[] = 'JPG Pixelbild mit gefülltem Grund (weiß, bei der Fassung für dunklen Grund anthrazit),'; $lines[] = ' für Portale und Programme, die keine Transparenz können.'; $lines[] = ''; $texte = array_column($page['varianten'] ?? [], null, 'key'); foreach (array_keys($OUTPUTS) as $key) { $lines[] = ($texte[$key]['title'] ?? $key) . ' (Dateien: ' . LOGO_PREFIX . '-' . $key . '.*)'; if (!empty($texte[$key]['text'])) { $lines[] = ' ' . $texte[$key]['text']; } } if (!empty($page['farben']['items'])) { $lines[] = ''; $lines[] = 'Farben'; $lines[] = '------'; foreach ($page['farben']['items'] as $farbe) { $lines[] = sprintf( '%s HEX %s RGB %s CMYK %s', $farbe['name'] ?? '', $farbe['hex'] ?? '', $farbe['rgb'] ?? '', $farbe['cmyk'] ?? '' ); } } $lines[] = ''; $lines[] = 'Alle Dateien und weitere Formate: ' . club_url('logo'); $lines[] = 'Fragen: ' . ($club['email'] ?? ''); $readme = $destDir . '/LIESMICH.txt'; file_put_contents($readme, implode("\n", $lines) . "\n"); $zipFile = $destDir . '/' . LOGO_PREFIX . '-paket.zip'; $zip = new ZipArchive(); if ($zip->open($zipFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) { $fail("ZIP nicht anlegbar: {$zipFile}"); } $zip->addFile($readme, 'LIESMICH.txt'); foreach ($built as $file) { $zip->addFile($file, LOGO_PREFIX . '/' . basename($file)); } $zip->close(); // --- Bericht ------------------------------------------------------------------ $total = 0; echo "\nErzeugt in public/assets/downloads/logo/:\n"; foreach (array_merge($built, [$readme, $zipFile]) as $file) { $size = (int) filesize($file); $total += $size; $dim = ''; if (in_array(strtolower(pathinfo($file, PATHINFO_EXTENSION)), ['png', 'jpg'], true)) { $info = @getimagesize($file); $dim = $info ? " {$info[0]}x{$info[1]}" : ''; } printf(" %-46s %8s KB%s\n", basename($file), number_format($size / 1024, 1, ',', '.'), $dim); } printf(" %-46s %8s KB\n", '= gesamt (ohne ZIP doppelt gezählt)', number_format($total / 1024, 1, ',', '.')); $log('Fertig: ' . (count($built) + 2) . ' Dateien.');