diff --git a/CHANGELOG.md b/CHANGELOG.md index ccf1e26..b2c2edb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,23 @@ Format: Datum — was — warum. Die CI beginnt versioniert am 2026-10-23; davor sie verstreut (Marke in `brand/`, Dokument-CSS im Open-WebUI-Skill) und wurde nicht versioniert. +## 1.2.0 — 2026-10-23 + +- **Jede Seite wird durchgestempelt, keine eigene Impressumsseite** (Mathias: „Alle + durchstempeln – es benötigt keine eigene Seite"). Gilt auch für den Bestand: zehn + Gästehaus-PDFs und der Router-Bericht tragen jetzt `KALINA · © 2026 kalina.si` auf jeder Seite. +- **`scripts/stamp_existing_pdf.py` gestempelt-tauglich:** Das Fußband wird vorher in der + gemessenen Seitenfarbe gefüllt, sonst steht auf Ink-Seiten ein weißer Balken über dem alten + Fuß; Copyright-Vermerk ist `© kalina.si` statt `ai.m-m.bio` (Jahr zur Laufzeit). +- **Bestehende PDFs nicht aus HTML neu bauen.** WeasyPrint 69 lokal gegen 62.3 bei den + Originalen plus eigene CSS-Kopien je Dokumentordner: derselbe Quelltext verlor 155 Wörter und + verlagerte Tabellen (`buchungsportale-…-v1.pdf`, Seite 5: 157 → 55 Wörter). Stempeln erhält + das Layout; `--ohne-deckblatt` hält die Seitenzahl. +- **Agentennamen raus aus dem Verfasserfeld:** sieben Dokumente nannten „Seitenweise", „Textmarke" + oder „Webstuhl" als Verfasser. Jetzt `Kalina`; die Zuordnung bleibt im Ticket. +- **`nc-put.py --overwrite`** (nutzt `nc_webdav_write_file` über MCP) für das Ersetzen in + Nextcloud — ohne `--overwrite` schlägt es fail-closed fehl. Danach die Dateigröße vergleichen. + ## 1.1.0 — 2026-10-23 - **Autor und Impressum verbindlich:** Jedes Dokument nennt **Kalina** als Urheber (auch Entwürfe), diff --git a/SKILL.md b/SKILL.md index 3bd839f..18fc012 100644 --- a/SKILL.md +++ b/SKILL.md @@ -2,7 +2,7 @@ name: kalina-ci description: Use when a PDF, page or slide must carry the KALINA corporate identity. One CSS, one CLI, one source. metadata: - version: "1.0.0" + version: "1.2.0" stand: "2026-10-23" --- @@ -63,19 +63,40 @@ Fließtext selten. Seite 1 gilt als Deckblatt und wird berichtet, nicht bewertet (Mathias, 23.10.2026): das Kalina-System ist der durchführende Part. Gilt ausdrücklich auch für **Entwürfe**. -- Jedes Dokument endet mit dem **Impressum im Fußband**: Marke, `KALINA · «Dokumenttitel» · Stand …`, +- **Jede Seite** trägt das **Impressum im Fußband**: Marke, `KALINA · «Dokumenttitel» · Stand …`, Zeile „Erstellt durch das Kalina-System · «Auftrag/Projekt · Ticket»" und **`© «JJJJ» kalina.si`**. - Die Vorlagen `report.html`, `page.html`, `bild-mit-prompt.html` tragen das bereits — Platzhalter - füllen, nicht löschen. + Mathias, 23.10.2026: „Alle durchstempeln – es benötigt keine eigene Seite." Keine separate + Impressumsseite. Die Vorlagen `report.html`, `page.html`, `bild-mit-prompt.html` tragen das + bereits — Platzhalter füllen, nicht löschen. - **Decks:** Autor auf der Titelfolie, auf der Schlussfolie genügt Marke + `© «JJJJ» kalina.si` in einer Zeile. - **HTML:** `` und dieselbe Fußzeile. - **Commit-Autor:** `Kalina `. -- **Prüfung vor Auslieferung:** `kalina.si` per Textsuche im PDF nachweisen (muss auf der letzten - Seite stehen) und das Fußband als Raster ansehen — Marke sichtbar, Copyright vollständig. +- **Prüfung vor Auslieferung:** `kalina.si` per Textsuche **auf jeder Seite** nachweisen und das + Fußband mindestens einmal als Raster ansehen — Marke sichtbar, Copyright vollständig. Ein Impressum, das allein auf einer leeren Seite landet, ist ein Umbruchfehler: kompakt ins Fußband, nicht als eigener Abschnitt mit `break-inside: avoid`. +## Bestehende PDFs nachstempeln + +Alte Artefakte tragen oft `ai.m-m.bio` im Fuß und einen Agentennamen als Verfasser. **Nicht aus +dem HTML neu bauen:** lokal läuft WeasyPrint 69, die Originale entstanden mit 62.3, und die +Dokumentordner haben eigene CSS-Kopien — derselbe Quelltext ergab 155 Wörter weniger und +verlagerte Tabellen (gemessen an `buchungsportale-…-v1.pdf`, Seite 5: 157 → 55 Wörter). + +1. `scripts/stamp_existing_pdf.py --ohne-deckblatt` setzt Fußlinie, Marke und + `KALINA · © kalina.si` auf **jede** Seite, ohne das Layout anzufassen. +2. Das Fußband wird vorher in der **gemessenen** Seitenfarbe gefüllt (häufigste Farbe im + Streifen) — sonst steht auf Ink-Seiten ein weißer Balken über dem alten Fuß. +3. Verfasserzelle redigieren (pymupdf `add_redact_annot`), aber nur in Zeilen, die Agentenname + **und** „CORP 1" tragen; inhaltliche Nennungen im Fließtext bleiben stehen. +4. Prüfen: Seitenzahl alt = neu, `kalina.si` auf jeder Seite, kein Agentenname mehr, letzte Seite + als Raster ansehen. + +Das Stempel-PDF wächst (~30–40 kB je Seite durch den matplotlib-Bett). Beim Ersetzen in Nextcloud +die **Größe** vergleichen, nicht den HTTP-Status; `nc-put.py "" --overwrite` nutzt +`nc_webdav_write_file` über MCP (ohne `--overwrite` schlägt das Ersetzen fail-closed fehl). + ## Vision lokal (verbindlich) Bildprüfung läuft **lokal auf dem ROG**, nie in der Cloud — gleiches Ergebnis, keine Token-Kosten diff --git a/scripts/stamp_existing_pdf.py b/scripts/stamp_existing_pdf.py index df04695..339b4ad 100755 --- a/scripts/stamp_existing_pdf.py +++ b/scripts/stamp_existing_pdf.py @@ -24,6 +24,7 @@ Danach `display_file` auf die AUSGABE, nie auf das Original. from __future__ import annotations import argparse +import datetime import io import sys import textwrap @@ -47,6 +48,10 @@ ROT = "#c1121f" STEEL = "#3a4250" MUTED = "#8a94a2" +# Autor ist immer Kalina (Beschluss 23.10.2026): Copyright-Vermerk auf jeder Seite. +JAHR = datetime.date.today().year +COPYRIGHT = f"© {JAHR} kalina.si" + FONT = FontProperties(family="DejaVu Sans") FONT_BOLD = FontProperties(family="DejaVu Sans", weight="bold") # Black (900) fehlt in DejaVu TRACKING = 0.06 # Buchstabenabstand der Wortmarke, in Vielfachen der Schriftgröße @@ -131,11 +136,16 @@ def kopfbogen(breite: float, hoehe: float, rechts_text: str) -> PdfReader: return _als_reader(fig) -def fussbogen(breite: float, hoehe: float, fuss_text: str) -> PdfReader: - """Transparente Seite mit Fußlinie + Fußtext (überdeckt nur den unteren Rand).""" +def fussbogen(breite: float, hoehe: float, fuss_text: str, hintergrund: str = BONE) -> PdfReader: + """Seite mit Fußlinie + Fußtext. Das Band wird zuerst in der Seitenfarbe gefüllt, + damit ein alter Fuß (z. B. „ai.m-m.bio") nicht unter dem neuen durchscheint.""" fig, ax = _figur(breite, hoehe) + band = plt.Rectangle((RAND_L - 1.5 * MM, FUSS_LINIE - 7.0 * MM), + breite - RAND_L - RAND_R + 3.0 * MM, 8.4 * MM, + facecolor=hintergrund, edgecolor="none", zorder=2) + ax.add_patch(band) ax.plot([RAND_L, breite - RAND_R], [FUSS_LINIE, FUSS_LINIE], color=STEEL, linewidth=0.6, zorder=3) - ax.text(RAND_L, FUSS_LINIE - 4.4 * MM, "KALINA · ai.m-m.bio", fontsize=6.5, color=MUTED, + ax.text(RAND_L, FUSS_LINIE - 4.4 * MM, "KALINA · " + COPYRIGHT, fontsize=6.5, color=MUTED, fontproperties=FONT, ha="left", va="baseline", zorder=4) if fuss_text: ax.text(breite / 2, FUSS_LINIE - 4.4 * MM, fuss_text, fontsize=6.5, color=MUTED, @@ -170,7 +180,7 @@ def deckblatt(breite: float, hoehe: float, titel: str, untertitel: str, fuss_tex y -= 7.5 * MM ax.plot([RAND_L, breite - RAND_R], [FUSS_LINIE, FUSS_LINIE], color=STEEL, linewidth=0.6, zorder=3) - ax.text(RAND_L, FUSS_LINIE - 4.4 * MM, "KALINA · ai.m-m.bio", fontsize=6.5, color=MUTED, + ax.text(RAND_L, FUSS_LINIE - 4.4 * MM, "KALINA · " + COPYRIGHT, fontsize=6.5, color=MUTED, fontproperties=FONT, ha="left", va="baseline", zorder=4) if fuss_text: ax.text(breite - RAND_R, FUSS_LINIE - 4.4 * MM, fuss_text, fontsize=6.5, color=MUTED, @@ -178,6 +188,43 @@ def deckblatt(breite: float, hoehe: float, titel: str, untertitel: str, fuss_tex return _als_reader(fig) +def _bandfarben(quelle: Path, anzahl: int) -> list[str]: + """Haeufigste Farbe im Fussband je Seite - der Seitenhintergrund. + + Ohne Messung wuerde ein helles Band auf dunklen Dokumenten (z. B. Ink-Seiten der CI) + als weisser Streifen stehen. + """ + try: + import pymupdf # type: ignore + except ImportError: + try: + import fitz as pymupdf # type: ignore + except ImportError: + return [BONE] * anzahl + farben: list[str] = [] + dok = pymupdf.open(str(quelle)) + for i in range(anzahl): + if i >= dok.page_count: + farben.append(BONE) + continue + seite = dok[i] + h = seite.rect.height + clip = pymupdf.Rect(RAND_L, FUSS_LINIE - 5 * MM, seite.rect.width - RAND_R, FUSS_LINIE - 1 * MM) + try: + pix = seite.get_pixmap(clip=clip, dpi=72) + zaehler: dict[tuple, int] = {} + for y in range(pix.height): + for x in range(pix.width): + px = pix.pixel(x, y)[:3] + zaehler[px] = zaehler.get(px, 0) + 1 + farbe = max(zaehler.items(), key=lambda kv: kv[1])[0] + farben.append("#%02x%02x%02x" % farbe) + except Exception: + farben.append(BONE) + dok.close() + return farben + + def verarbeiten(quelle: Path, ziel: Path, *, titel: str, untertitel: str, fuss: str, mit_deckblatt: bool, mit_fuss: bool, mit_kopf: bool) -> dict: leser = PdfReader(str(quelle)) @@ -186,6 +233,7 @@ def verarbeiten(quelle: Path, ziel: Path, *, titel: str, untertitel: str, fuss: if breite < 100 or hoehe < 100: raise SystemExit(f"FEHLER: unerwartetes Seitenformat {breite:.0f}×{hoehe:.0f} pt") + hintergruende = _bandfarben(quelle, len(leser.pages)) if mit_fuss else [] schreiber = PdfWriter() seiten = 0 if mit_deckblatt: @@ -194,7 +242,8 @@ def verarbeiten(quelle: Path, ziel: Path, *, titel: str, untertitel: str, fuss: if abs(float(seite.mediabox.width) - breite) > 1 or abs(float(seite.mediabox.height) - hoehe) > 1: seite.scale_to(breite, hoehe) if mit_fuss: - seite.merge_page(fussbogen(breite, hoehe, fuss).pages[0]) + farbe = hintergruende[i] if i < len(hintergruende) else BONE + seite.merge_page(fussbogen(breite, hoehe, fuss, farbe).pages[0]) if mit_kopf and i == 0: seite.merge_page(kopfbogen(breite, hoehe, "KALINA" if not fuss else fuss.upper()).pages[0]) schreiber.add_page(seite)