upload
This commit is contained in:
Executable
+163
@@ -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())
|
||||
Reference in New Issue
Block a user