AUDIT (Auftrag: vollstaendiger Durchgang, Fehler direkt beheben)
Ausgangslage waren 40 Pruefungen mit 1616 Einzelpunkten, alle gruen.
Acht neue Pruefungen kamen dazu; sie haben gefunden, was die alten nicht
sehen konnten.
Der schwerste Fund: Die Zugangsschranke verglich req.path EXAKT gegen
eine Liste. Express raeumt Punkt-Segmente selbst weg, mehrfache
Schraegstriche aber nicht. Damit kam //workspace/start.html OHNE
Anmeldung mit HTTP 200, und ein Creator bekam ueber
/workspace//personen.html die Verwaltungsseite. Die DATEN waren nie
betroffen (nachgemessen: 404 bzw. 401). Behoben durch Normalisierung
UND eine Umkehr der Logik -- jetzt ist jede .html geschuetzt ausser der
Anmeldeseite, statt nur die in der Liste. Eine vergessene neue Seite
steht damit nicht mehr versehentlich offen.
Weiter behoben:
* Kaputter/abgebrochener Rumpf ergab 500 in HTML statt 400 in JSON --
die Oberflaeche ruft ueberall a.json() und lief in einen zweiten
Fehler; der Knopf hing ohne Meldung.
* POST /zustand/sichern war der einzige von 60 schreibenden Wegen
ohne Herkunftspruefung.
* workspace-sicherung.js gab interne Pfade in Fehlermeldungen nach
aussen; alle 23 anderen Module antworten neutral.
* admin_notiz war als einziges von 14 Feldern ohne <label>.
* Der aktive Filter hatte keinen sichtbaren Fokus (CSS-Spezifitaet
0,3,0 schlug 0,2,0) -- genau der Knopf, auf dem man steht.
* h1 -> h3 ohne Zwischenstufe auf zwei Seiten.
* HSTS ging auch ueber http mit (RFC 6797, 7.2 verbietet das).
* upgrade-insecure-requests galt auch auf 127.0.0.1 -- dadurch war
WebKit/Safari ueberhaupt nicht pruefbar, also der Browser, den
jedes iPhone benutzt.
* pruef-grosscheck las readdirSync(".") und pruefte aus server/
gestartet NULL oeffentliche Seiten -- meldete aber "ok".
Neue Pruefungen: struktur, schranke, haerte, alle-wege,
barrierefrei-workspace, breiten, tempo-workspace, browser.
Jede mit Gegenprobe und mit der geprueften Anzahl in der Bedingung.
Vier davon sind beim Bauen durch die eigene Gegenprobe aufgeflogen und
haetten sonst dauerhaft gruen gemeldet, ohne etwas zu messen.
APP-SYMBOLE (Wunsch: alle Apps der Domain, jede anders, ausser
safeaddress)
Zehn Apps, zehn Stile, zehn in OKLCH gerechnete Farben. Zusammen haelt
sie dasselbe Logo, dieselbe Eckenrundung und eine gemeinsame gedeckte
Farbreihe. Beim Bauen wird gemessen, ob sich das Logo vom Grund abhebt
(19 bis 58 Helligkeitsstufen).
Dabei aufgefallen: Das Kundenportal hatte kein eigenes Manifest und
trug Namen und Symbol der Webdesign-Seite. Der Workspace hatte gar
keins und war als App nicht installierbar. Beide haben jetzt eins.
Geaendert wurde AUSSCHLIESSLICH das Symbol. Ein Zwischenstand hatte
auch die Themenfarben gesetzt; das war mehr als bestellt und wurde
zurueckgenommen.
Werkzeuge: tools/logo-freistellen.mjs, tools/app-symbole.mjs,
tools/app-symbole-einbinden.mjs -- alles im Browser gerechnet, kein
Bildprogramm, keine neue Abhaengigkeit.
Gitea und Nextcloud sind bereits live und nachgeprueft.
Co-Authored-By: Claude Opus 5 <[email protected]>
234 lines
9.7 KiB
JavaScript
234 lines
9.7 KiB
JavaScript
/* ===================================================================
|
|
Stellt den DogFather-Logokopf frei — einmal, als Vorlage für alle
|
|
App-Symbole.
|
|
|
|
WOZU. Jede App an dogfather-universe.com soll dasselbe Logo tragen,
|
|
aber in einer eigenen Farbe (Wunsch Filipe, 04.09.2026: "alle sollen
|
|
das Logo haben, aber durch Farben anders aussehen"). Dafür braucht es
|
|
den Kopf OHNE Hintergrund — sonst klebt an jedem Symbol das alte
|
|
Hellblau.
|
|
|
|
DAS VORHANDENE MATERIAL taugt so nicht:
|
|
assets/img/icon-512.png Kopf, aber auf hellblauem Grund
|
|
assets/img/logo-dogfather-*.png Kopf MIT Schriftzug, auf Weiss
|
|
assets/img/marke/dogfather-husky.png die Maskottchen-Figur im
|
|
Hoodie, kein Logo
|
|
Gebraucht wird der Kopf aus dem ersten, ohne dessen Hintergrund.
|
|
|
|
WARUM FLUTFUELLUNG UND KEIN FARBFILTER. Der naheliegende Weg — "alle
|
|
hellblauen Bildpunkte durchsichtig machen" — zerstört das Logo: Im
|
|
Gesicht liegen dieselben hellen Töne (Wange, Schnauze, Ohrinnenseite).
|
|
Die Füllung startet stattdessen an den vier Ecken und frisst sich nur
|
|
so weit nach innen, wie die Farbe zusammenhängt. Was vom Rand nicht
|
|
erreichbar ist, bleibt stehen — auch wenn es dieselbe Farbe hat.
|
|
|
|
WEICHE KANTE. Ein harter Schnitt ergibt Treppenstufen, die man bei
|
|
192 px deutlich sieht. Deshalb wird der Rand über die Farbnähe
|
|
abgestuft: fast gleich = ganz durchsichtig, nah dran = teilweise.
|
|
|
|
Gerechnet im Browser, der ohnehin für die Prüfungen da ist — kein
|
|
Bildprogramm, keine neue Abhängigkeit, 0 EUR. Derselbe Weg wie bei
|
|
tools/buehne-bauen.mjs.
|
|
|
|
Aufrufen mit: node tools/logo-freistellen.mjs
|
|
=================================================================== */
|
|
|
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
import { join, dirname } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const WURZEL = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
const QUELLE = join(WURZEL, "assets", "img", "icon-512.png");
|
|
const ZIEL_ORDNER = join(WURZEL, "assets", "img", "marke");
|
|
const ZIEL = join(ZIEL_ORDNER, "logo-kopf-frei.png");
|
|
|
|
if (!existsSync(QUELLE)) {
|
|
console.error(`Quelle fehlt: ${QUELLE}`);
|
|
process.exit(1);
|
|
}
|
|
if (!existsSync(ZIEL_ORDNER)) mkdirSync(ZIEL_ORDNER, { recursive: true });
|
|
|
|
const { chromium } = await import(
|
|
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await chromium.launch();
|
|
const seite = await (await browser.newContext()).newPage();
|
|
|
|
const alsDatenAdresse = (pfad) =>
|
|
"data:image/png;base64," + readFileSync(pfad).toString("base64");
|
|
|
|
const ergebnis = await seite.evaluate(async (quelle) => {
|
|
const bild = new Image();
|
|
bild.src = quelle;
|
|
await bild.decode();
|
|
|
|
const n = bild.naturalWidth;
|
|
const leinwand = document.createElement("canvas");
|
|
leinwand.width = n; leinwand.height = n;
|
|
const stift = leinwand.getContext("2d", { willReadFrequently: true });
|
|
stift.drawImage(bild, 0, 0);
|
|
const daten = stift.getImageData(0, 0, n, n);
|
|
const p = daten.data;
|
|
|
|
/* DEN GRUNDTON AUS DEM HAEUFIGSTEN RANDTON LESEN, nicht aus den Ecken.
|
|
|
|
Erster Versuch: die vier Ecken mitteln. Ergebnis war rgb(0,0,0) und
|
|
ein Bild, aus dem nichts entfernt wurde. Der Grund ist die Form des
|
|
Symbols: `icon-512.png` hat ABGERUNDETE Ecken, dort ist es
|
|
durchsichtig -- und durchsichtige Punkte tragen die Farbwerte
|
|
0,0,0. Gemittelt wurde also viermal Schwarz, und gesucht wurde
|
|
danach nach schwarzem Hintergrund, den es nicht gibt.
|
|
|
|
Jetzt wird der ganze Rand abgelaufen, undurchsichtige Punkte werden
|
|
grob nach Farbe gebuendelt, und der groesste Haufen gewinnt. Das
|
|
ueberlebt runde Ecken, einen Rahmen und einzelne Ausreisser. */
|
|
const haufen = new Map();
|
|
const merke = (x, y) => {
|
|
const i = (y * n + x) * 4;
|
|
if (p[i + 3] < 200) return; // durchsichtig zaehlt nicht mit
|
|
const schluessel = `${p[i] >> 3},${p[i + 1] >> 3},${p[i + 2] >> 3}`;
|
|
const e = haufen.get(schluessel) || { n: 0, r: 0, g: 0, b: 0 };
|
|
e.n++; e.r += p[i]; e.g += p[i + 1]; e.b += p[i + 2];
|
|
haufen.set(schluessel, e);
|
|
};
|
|
for (let x = 0; x < n; x++) { merke(x, 0); merke(x, n - 1); }
|
|
for (let y = 0; y < n; y++) { merke(0, y); merke(n - 1, y); }
|
|
|
|
if (!haufen.size) return { fehler: "Der ganze Rand ist durchsichtig — nichts abzulesen." };
|
|
const groesster = [...haufen.values()].sort((a, b) => b.n - a.n)[0];
|
|
const randPunkte = [...haufen.values()].reduce((s, e) => s + e.n, 0);
|
|
if (groesster.n / randPunkte < 0.5) {
|
|
return { fehler: `Der Rand ist nicht einfarbig genug `
|
|
+ `(haeufigster Ton nur ${Math.round(groesster.n / randPunkte * 100)} % des Randes).` };
|
|
}
|
|
const grund = [
|
|
Math.round(groesster.r / groesster.n),
|
|
Math.round(groesster.g / groesster.n),
|
|
Math.round(groesster.b / groesster.n),
|
|
];
|
|
|
|
const abstand = (i) => Math.sqrt(
|
|
(p[i] - grund[0]) ** 2 + (p[i + 1] - grund[1]) ** 2 + (p[i + 2] - grund[2]) ** 2);
|
|
|
|
/* NAH bis hierher gilt ein Punkt als Hintergrund
|
|
WEIT ab hier gilt er als Logo; dazwischen wird weich abgestuft */
|
|
const NAH = 42, WEIT = 96;
|
|
|
|
/* Flutfuellung von den Raendern. Eine eigene Liste statt Rekursion --
|
|
bei 512x512 liefe der Aufrufstapel sonst über. */
|
|
const besucht = new Uint8Array(n * n);
|
|
const liste = [];
|
|
for (let x = 0; x < n; x++) { liste.push(x, x + (n - 1) * n); }
|
|
for (let y = 0; y < n; y++) { liste.push(y * n, n - 1 + y * n); }
|
|
|
|
let entfernt = 0, weich = 0;
|
|
while (liste.length) {
|
|
const feld = liste.pop();
|
|
if (besucht[feld]) continue;
|
|
besucht[feld] = 1;
|
|
const i = feld * 4;
|
|
|
|
/* Schon durchsichtige Punkte (die runden Ecken) sind bereits
|
|
Hintergrund -- durch sie hindurch muss die Fuellung weiterlaufen,
|
|
sonst kommt sie gar nicht erst an den hellblauen Grund heran. */
|
|
if (p[i + 3] < 40) {
|
|
const x0 = feld % n, y0 = (feld - x0) / n;
|
|
if (x0 > 0) liste.push(feld - 1);
|
|
if (x0 < n - 1) liste.push(feld + 1);
|
|
if (y0 > 0) liste.push(feld - n);
|
|
if (y0 < n - 1) liste.push(feld + n);
|
|
continue;
|
|
}
|
|
|
|
const a = abstand(i);
|
|
if (a > WEIT) continue; // gehört zum Logo, hier ist Schluss
|
|
|
|
if (a <= NAH) { p[i + 3] = 0; entfernt++; }
|
|
else {
|
|
/* Zwischenzone: je näher am Grundton, desto durchsichtiger. */
|
|
p[i + 3] = Math.round(255 * ((a - NAH) / (WEIT - NAH)));
|
|
weich++;
|
|
}
|
|
|
|
const x = feld % n, y = (feld - x) / n;
|
|
if (x > 0) liste.push(feld - 1);
|
|
if (x < n - 1) liste.push(feld + 1);
|
|
if (y > 0) liste.push(feld - n);
|
|
if (y < n - 1) liste.push(feld + n);
|
|
}
|
|
|
|
/* ---- Zweiter Durchgang: die EINGESCHLOSSENEN Reste --------------------
|
|
|
|
Die Flutfuellung kommt nur an das heran, was mit dem Rand
|
|
zusammenhaengt. In den Ohren, am Auge und an der Schnauze liegen
|
|
aber Flaechen desselben Hellblau, die ringsum von schwarzer Kontur
|
|
umschlossen sind -- die Fuellung erreicht sie nie. Im ersten
|
|
Ergebnis blieben sie als blaue Flecken stehen und haetten auf jedem
|
|
eingefaerbten Symbol als Fremdkoerper geleuchtet.
|
|
|
|
Im Original-Logo (assets/img/marke/logo.png, auf Weiss) sind genau
|
|
diese Stellen WEISS -- sie waren also nie blau, dort schien beim
|
|
Bau des Symbols der Hintergrund durch. Sie gehoeren damit ebenfalls
|
|
weg.
|
|
|
|
Hier ist ein reiner Farbvergleich gefahrlos, und das laesst sich
|
|
nachrechnen statt hoffen: Der Abstand von Weiss (255,255,255) zum
|
|
Grundton betraegt rund 120 -- deutlich mehr als die Schwelle NAH
|
|
(42). Das Fell kann also nicht versehentlich mitgeloescht werden.
|
|
Genau diese Zahl steht unten in der Ausgabe, damit sie beim
|
|
naechsten Logo nachgeprueft wird, statt geglaubt zu werden. */
|
|
const abstandWeiss = Math.sqrt(
|
|
(255 - grund[0]) ** 2 + (255 - grund[1]) ** 2 + (255 - grund[2]) ** 2);
|
|
let eingeschlossen = 0;
|
|
if (abstandWeiss > NAH * 1.8) {
|
|
for (let i = 0; i < p.length; i += 4) {
|
|
if (p[i + 3] === 0) continue;
|
|
if (abstand(i) <= NAH) { p[i + 3] = 0; eingeschlossen++; }
|
|
}
|
|
}
|
|
|
|
stift.putImageData(daten, 0, 0);
|
|
|
|
/* Wie viel Bild ist übrig? Bleibt fast nichts stehen, hat die Füllung
|
|
das Logo mitgefressen -- dann ist das Ergebnis wertlos und darf
|
|
nicht gespeichert werden. */
|
|
let sichtbar = 0;
|
|
for (let i = 3; i < p.length; i += 4) if (p[i] > 40) sichtbar++;
|
|
|
|
return {
|
|
png: leinwand.toDataURL("image/png"),
|
|
grund, entfernt, weich, sichtbar, gesamt: n * n, groesse: n,
|
|
eingeschlossen, abstandWeiss: Math.round(abstandWeiss),
|
|
};
|
|
}, alsDatenAdresse(QUELLE));
|
|
|
|
await browser.close();
|
|
|
|
if (ergebnis.fehler) {
|
|
console.error("Abgebrochen:", ergebnis.fehler);
|
|
process.exit(1);
|
|
}
|
|
|
|
const anteil = ergebnis.sichtbar / ergebnis.gesamt;
|
|
console.log(`Grundton erkannt: rgb(${ergebnis.grund.join(", ")})`);
|
|
console.log(`ganz entfernt: ${ergebnis.entfernt} Punkte`);
|
|
console.log(`weiche Kante: ${ergebnis.weich} Punkte`);
|
|
console.log(`eingeschlossen: ${ergebnis.eingeschlossen} Punkte `
|
|
+ `(Abstand Weiss-zu-Grundton: ${ergebnis.abstandWeiss}, Schwelle 42 — Fell bleibt sicher)`);
|
|
console.log(`Logo übrig: ${ergebnis.sichtbar} Punkte (${(anteil * 100).toFixed(1)} %)`);
|
|
|
|
/* Die Anzahl gehört in die Bedingung: Ein leeres Bild wäre sonst das
|
|
sauberste Ergebnis von allen. */
|
|
if (anteil < 0.12) {
|
|
console.error(`\nFEHLER: Nur ${(anteil * 100).toFixed(1)} % übrig — die Füllung hat das Logo`
|
|
+ ` mitgenommen. Nichts gespeichert.`);
|
|
process.exit(1);
|
|
}
|
|
if (anteil > 0.92) {
|
|
console.error(`\nFEHLER: ${(anteil * 100).toFixed(1)} % übrig — es wurde praktisch nichts`
|
|
+ ` entfernt. Nichts gespeichert.`);
|
|
process.exit(1);
|
|
}
|
|
|
|
writeFileSync(ZIEL, Buffer.from(ergebnis.png.split(",")[1], "base64"));
|
|
console.log(`\nGespeichert: ${ZIEL.replace(WURZEL, ".")} (${ergebnis.groesse}x${ergebnis.groesse})`);
|