This commit is contained in:
2026-06-24 14:40:43 +00:00
parent 22b05a7a1b
commit 7058970f98
80 changed files with 6383 additions and 0 deletions
+163
View File
@@ -0,0 +1,163 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import base64
import json
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "renderer"))
from app.renderer import render_documents
from app.theme_catalog import THEMES
from app.validation import validate_report
def command(name: str, override: str | None = None) -> str:
value = override or shutil.which(name)
if not value:
raise SystemExit(f"Benötigtes Programm nicht gefunden: {name}")
return value
def print_pdf(engine: str, executable: str, html_path: Path, pdf_path: Path) -> None:
if engine == "weasyprint":
subprocess.run(
[executable, str(html_path), str(pdf_path)],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
timeout=90,
)
return
subprocess.run(
[
executable,
"--headless=new",
"--no-sandbox",
"--disable-gpu",
"--disable-dev-shm-usage",
"--disable-background-networking",
"--disable-extensions",
"--no-first-run",
f"--user-data-dir={pdf_path.parent / ('.chrome-' + pdf_path.stem)}",
f"--print-to-pdf={pdf_path.resolve()}",
"--no-pdf-header-footer",
html_path.resolve().as_uri(),
],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
timeout=90,
)
def first_page_png(pdftoppm: str, pdf_path: Path, png_path: Path) -> None:
prefix = png_path.with_suffix("")
subprocess.run(
[pdftoppm, "-f", "1", "-singlefile", "-scale-to", "760", "-png", str(pdf_path), str(prefix)],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
def make_gallery(cards: list[dict[str, str]], output: Path) -> None:
blocks = []
for card in cards:
image_data = base64.b64encode(Path(card["image"]).read_bytes()).decode("ascii")
swatches = "".join(f'<span style="background:{color}"></span>' for color in card["colors"].split(","))
blocks.append(
f'''<article class="card">
<div class="preview"><img src="data:image/png;base64,{image_data}" alt="{card['name']}"></div>
<div class="meta"><div><div class="category">{card['category']} · {card['layout']}</div><h2>{card['name']}</h2><code>{card['id']}</code></div><div class="swatches">{swatches}</div></div>
<p>{card['description']}</p>
</article>'''
)
output.write_text(
'''<!doctype html><html lang="de"><head><meta charset="utf-8"><title>Monthly Report Kit - Theme-Katalog</title><style>
@page{size:A4 landscape;margin:10mm 11mm 13mm}*{box-sizing:border-box}html{-webkit-print-color-adjust:exact;print-color-adjust:exact}body{margin:0;font-family:Arial,sans-serif;color:#202a30;background:#fff}.title{display:flex;justify-content:space-between;align-items:flex-end;border-bottom:2px solid #172a42;padding-bottom:4mm;margin-bottom:5mm}.title h1{font-size:24pt;margin:0}.title p{margin:0;color:#667680}.grid{display:grid;grid-template-columns:1fr 1fr;gap:5mm}.card{display:grid;grid-template-columns:66mm 1fr;grid-template-rows:auto 1fr;column-gap:5mm;border:1px solid #d8dfe3;padding:3.5mm;min-height:83mm;break-inside:avoid}.preview{grid-row:1/3;background:#eef2f4;display:flex;align-items:flex-start;justify-content:center;overflow:hidden;height:76mm}.preview img{width:100%;height:auto}.meta{display:flex;justify-content:space-between;gap:3mm}.category{font-size:6.5pt;text-transform:uppercase;letter-spacing:.8px;color:#718087;font-weight:700}.meta h2{font-size:13pt;margin:1mm 0}.meta code{font-size:7pt;background:#f1f4f5;padding:1mm 1.5mm}.card p{font-size:8pt;line-height:1.4;color:#56656d;margin:3mm 0 0}.swatches{display:flex;gap:1mm}.swatches span{width:5mm;height:5mm;border:1px solid #00000015}.footer{margin-top:4mm;font-size:7pt;color:#718087;display:flex;justify-content:space-between}
</style></head><body><header class="title"><div><h1>Monthly Report Kit</h1><p>28 Designs in 10 Layoutfamilien</p></div><p>Auswahl über <code>design.theme</code></p></header><main class="grid">'''
+ "".join(blocks)
+ '''</main><footer class="footer"><span>Modern und Classic unterstützen standardmäßig eigene Kundenfarben.</span><span>Bei allen benannten Varianten: palette_mode = theme oder custom</span></footer></body></html>''',
encoding="utf-8",
)
def main() -> int:
parser = argparse.ArgumentParser(description="Rendert alle Themes und erzeugt einen PDF-Theme-Katalog.")
parser.add_argument("--input", type=Path, default=ROOT / "examples" / "sample-report.json")
parser.add_argument("--output-dir", type=Path, default=ROOT / "output" / "theme-catalog")
parser.add_argument("--engine", choices=["auto", "chromium", "weasyprint"], default="auto")
parser.add_argument("--chromium", default=None)
parser.add_argument("--keep-theme-pdfs", action="store_true")
parser.add_argument("--themes", nargs="*", default=None, help="Optional: nur diese Theme-IDs rendern")
args = parser.parse_args()
if args.engine == "weasyprint" or (args.engine == "auto" and shutil.which("weasyprint")):
engine = "weasyprint"
renderer = command("weasyprint")
else:
engine = "chromium"
renderer = command("chromium", args.chromium)
pdftoppm = command("pdftoppm")
payload = json.loads(args.input.read_text(encoding="utf-8"))
errors = validate_report(payload)
if errors:
print(json.dumps(errors, ensure_ascii=False, indent=2), file=sys.stderr)
return 2
args.output_dir.mkdir(parents=True, exist_ok=True)
preview_dir = args.output_dir / "previews"
preview_dir.mkdir(exist_ok=True)
pdf_dir = args.output_dir / "pdfs"
pdf_dir.mkdir(exist_ok=True)
cards: list[dict[str, str]] = []
selected = args.themes or list(THEMES)
unknown = sorted(set(selected) - set(THEMES))
if unknown:
raise SystemExit(f"Unbekannte Themes: {', '.join(unknown)}")
for theme_id in selected:
theme = THEMES[theme_id]
payload["design"]["theme"] = theme_id
payload["design"].pop("palette_mode", None)
html, _ = render_documents(payload)
html_path = args.output_dir / f"{theme_id}.html"
pdf_path = pdf_dir / f"{theme_id}.pdf"
png_path = preview_dir / f"{theme_id}.png"
html_path.write_text(html, encoding="utf-8")
print_pdf(engine, renderer, html_path, pdf_path)
first_page_png(pdftoppm, pdf_path, png_path)
palette = theme["palette"]
cards.append({
"id": theme_id,
"name": theme["name"],
"layout": theme["layout"],
"category": theme["category"],
"description": theme["description"],
"image": str(png_path),
"colors": ",".join([palette["primary_color"], palette["secondary_color"], palette["accent_color"], palette["surface_color"]]),
})
print(f"{theme_id}: OK")
if set(selected) == set(THEMES):
gallery_html = args.output_dir / "theme-gallery.html"
gallery_pdf = args.output_dir / "theme-gallery.pdf"
make_gallery(cards, gallery_html)
print_pdf(engine, renderer, gallery_html, gallery_pdf)
print(gallery_pdf)
if not args.keep_theme_pdfs and pdf_dir.exists():
shutil.rmtree(pdf_dir)
return 0
if __name__ == "__main__":
raise SystemExit(main())