bin/instagram-sync.php lieferte seit dem 03.09.2026 bei jedem Lauf "Keine Posts ermittelbar". Von einem privaten Anschluss gegengeprueft: feed/user und web_profile_info antworten mit 401 require_login, das Profil-HTML traegt keine Beitragsdaten mehr, auch /embed/ liefert nur noch eine App-Huelle. Es lag also nicht an der Server-IP, und der Scraper ist geloescht, damit ihn niemand wiederbelebt. Graph API (Meta-Konto noetig) und kommerzielle Dienste wurden verworfen. Neu: bin/instagram-add.php. --profil liest die Beitragsliste ueber headless Chrome aus dem gerenderten Profil-DOM (im HTML stehen die Codes nicht) und holt zu jedem neuen Beitrag Bild, Caption und Datum ueber Instagrams oEmbed-Endpunkt; das Datum steckt in der media_id. Daneben einzelne URLs, --liste, --entfernen und --bild als Notfall. Der Shortcode ist Primaerschluessel und damit die komplette Dedupe-Logik. Die Struktur von data/instagram.json bleibt exakt gleich, deshalb war an Startseite, News-Feed und Stadionzeitung nichts zu aendern. Kein Cron mehr: der Job faellt aus public/cron.php, auf dem Webspace gibt es kein Chrome. Die Chrome-Suche liegt jetzt gemeinsam in bin/chrome-lib.php, genutzt von bin/logo-build.php und bin/instagram-add.php. data/instagram.json ist ab jetzt versioniert. Solange sie ignoriert war, lief sie gegen die versionierten Bilder auseinander: lokal lagen 10 Bilder zu 9 Eintraegen, drei Beitraege fehlten im Feed, zwei Eintraege hatten kein Bild. Repariert, jetzt 16 Eintraege zu 16 Bildern. bin/preflight.php prueft das in beide Richtungen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J9mubC8gtpjtJntbhSJBRS
343 lines
13 KiB
PHP
343 lines
13 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* Alle Download-Dateien des Vereinslogos aus den Master-Dateien erzeugen (nur CLI).
|
|
*
|
|
* php bin/logo-build.php [-v]
|
|
*
|
|
* Quelle: storage/logo-quellen/<datei>.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 <defs>-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
|
|
<!doctype html><meta charset="utf-8">
|
|
<style>html,body{margin:0;padding:0;background:transparent}
|
|
img{display:block;width:{$width}px;height:{$height}px}</style>
|
|
<img src="{$base}.svg" alt="">
|
|
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
|
|
<!doctype html><meta charset="utf-8">
|
|
<style>@page{size:{$mmW}mm {$mmH}mm;margin:0}html,body{margin:0;padding:0}
|
|
img{display:block;width:{$mmW}mm;height:{$mmH}mm}</style>
|
|
<img src="{$base}.svg" alt="">
|
|
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.');
|