feat(ci): jede Seite durchstempeln, Bestand nachgestempelt (1.2.0)
This commit is contained in:
1 parent
6858c92865
commit
ad5a3f0c6a
3 files changed
+98
-11
No files matched your search
@@ -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
|
sie verstreut (Marke in `brand/`, Dokument-CSS im Open-WebUI-Skill) und wurde nicht
|
||||||
versioniert.
|
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 `© <Jahr> 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
|
## 1.1.0 — 2026-10-23
|
||||||
|
|
||||||
- **Autor und Impressum verbindlich:** Jedes Dokument nennt **Kalina** als Urheber (auch Entwürfe),
|
- **Autor und Impressum verbindlich:** Jedes Dokument nennt **Kalina** als Urheber (auch Entwürfe),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
name: kalina-ci
|
name: kalina-ci
|
||||||
description: Use when a PDF, page or slide must carry the KALINA corporate identity. One CSS, one CLI, one source.
|
description: Use when a PDF, page or slide must carry the KALINA corporate identity. One CSS, one CLI, one source.
|
||||||
metadata:
|
metadata:
|
||||||
version: "1.0.0"
|
version: "1.2.0"
|
||||||
stand: "2026-10-23"
|
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
|
(Mathias, 23.10.2026): das Kalina-System ist der durchführende Part. Gilt ausdrücklich auch
|
||||||
für **Entwürfe**.
|
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`**.
|
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
|
Mathias, 23.10.2026: „Alle durchstempeln – es benötigt keine eigene Seite." Keine separate
|
||||||
füllen, nicht löschen.
|
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`
|
- **Decks:** Autor auf der Titelfolie, auf der Schlussfolie genügt Marke + `© «JJJJ» kalina.si`
|
||||||
in einer Zeile.
|
in einer Zeile.
|
||||||
- **HTML:** `<meta name="author" content="Kalina">` und dieselbe Fußzeile.
|
- **HTML:** `<meta name="author" content="Kalina">` und dieselbe Fußzeile.
|
||||||
- **Commit-Autor:** `Kalina <kalina@m-m.bio>`.
|
- **Commit-Autor:** `Kalina <kalina@m-m.bio>`.
|
||||||
- **Prüfung vor Auslieferung:** `kalina.si` per Textsuche im PDF nachweisen (muss auf der letzten
|
- **Prüfung vor Auslieferung:** `kalina.si` per Textsuche **auf jeder Seite** nachweisen und das
|
||||||
Seite stehen) und das Fußband als Raster ansehen — Marke sichtbar, Copyright vollständig.
|
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
|
Ein Impressum, das allein auf einer leeren Seite landet, ist ein Umbruchfehler: kompakt ins
|
||||||
Fußband, nicht als eigener Abschnitt mit `break-inside: avoid`.
|
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 <in.pdf> <out.pdf> --ohne-deckblatt` setzt Fußlinie, Marke und
|
||||||
|
`KALINA · © <Jahr> 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 <datei> "<pfad>" --overwrite` nutzt
|
||||||
|
`nc_webdav_write_file` über MCP (ohne `--overwrite` schlägt das Ersetzen fail-closed fehl).
|
||||||
|
|
||||||
## Vision lokal (verbindlich)
|
## Vision lokal (verbindlich)
|
||||||
|
|
||||||
Bildprüfung läuft **lokal auf dem ROG**, nie in der Cloud — gleiches Ergebnis, keine Token-Kosten
|
Bildprüfung läuft **lokal auf dem ROG**, nie in der Cloud — gleiches Ergebnis, keine Token-Kosten
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ Danach `display_file` auf die AUSGABE, nie auf das Original.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import datetime
|
||||||
import io
|
import io
|
||||||
import sys
|
import sys
|
||||||
import textwrap
|
import textwrap
|
||||||
@@ -47,6 +48,10 @@ ROT = "#c1121f"
|
|||||||
STEEL = "#3a4250"
|
STEEL = "#3a4250"
|
||||||
MUTED = "#8a94a2"
|
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 = FontProperties(family="DejaVu Sans")
|
||||||
FONT_BOLD = FontProperties(family="DejaVu Sans", weight="bold") # Black (900) fehlt in DejaVu
|
FONT_BOLD = FontProperties(family="DejaVu Sans", weight="bold") # Black (900) fehlt in DejaVu
|
||||||
TRACKING = 0.06 # Buchstabenabstand der Wortmarke, in Vielfachen der Schriftgröße
|
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)
|
return _als_reader(fig)
|
||||||
|
|
||||||
|
|
||||||
def fussbogen(breite: float, hoehe: float, fuss_text: str) -> PdfReader:
|
def fussbogen(breite: float, hoehe: float, fuss_text: str, hintergrund: str = BONE) -> PdfReader:
|
||||||
"""Transparente Seite mit Fußlinie + Fußtext (überdeckt nur den unteren Rand)."""
|
"""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)
|
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.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)
|
fontproperties=FONT, ha="left", va="baseline", zorder=4)
|
||||||
if fuss_text:
|
if fuss_text:
|
||||||
ax.text(breite / 2, FUSS_LINIE - 4.4 * MM, fuss_text, fontsize=6.5, color=MUTED,
|
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
|
y -= 7.5 * MM
|
||||||
|
|
||||||
ax.plot([RAND_L, breite - RAND_R], [FUSS_LINIE, FUSS_LINIE], color=STEEL, linewidth=0.6, zorder=3)
|
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)
|
fontproperties=FONT, ha="left", va="baseline", zorder=4)
|
||||||
if fuss_text:
|
if fuss_text:
|
||||||
ax.text(breite - RAND_R, FUSS_LINIE - 4.4 * MM, fuss_text, fontsize=6.5, color=MUTED,
|
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)
|
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,
|
def verarbeiten(quelle: Path, ziel: Path, *, titel: str, untertitel: str, fuss: str,
|
||||||
mit_deckblatt: bool, mit_fuss: bool, mit_kopf: bool) -> dict:
|
mit_deckblatt: bool, mit_fuss: bool, mit_kopf: bool) -> dict:
|
||||||
leser = PdfReader(str(quelle))
|
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:
|
if breite < 100 or hoehe < 100:
|
||||||
raise SystemExit(f"FEHLER: unerwartetes Seitenformat {breite:.0f}×{hoehe:.0f} pt")
|
raise SystemExit(f"FEHLER: unerwartetes Seitenformat {breite:.0f}×{hoehe:.0f} pt")
|
||||||
|
|
||||||
|
hintergruende = _bandfarben(quelle, len(leser.pages)) if mit_fuss else []
|
||||||
schreiber = PdfWriter()
|
schreiber = PdfWriter()
|
||||||
seiten = 0
|
seiten = 0
|
||||||
if mit_deckblatt:
|
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:
|
if abs(float(seite.mediabox.width) - breite) > 1 or abs(float(seite.mediabox.height) - hoehe) > 1:
|
||||||
seite.scale_to(breite, hoehe)
|
seite.scale_to(breite, hoehe)
|
||||||
if mit_fuss:
|
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:
|
if mit_kopf and i == 0:
|
||||||
seite.merge_page(kopfbogen(breite, hoehe, "KALINA" if not fuss else fuss.upper()).pages[0])
|
seite.merge_page(kopfbogen(breite, hoehe, "KALINA" if not fuss else fuss.upper()).pages[0])
|
||||||
schreiber.add_page(seite)
|
schreiber.add_page(seite)
|
||||||
|
|||||||
Reference in new issue
Block a user