From 727d7d2976f73342f96b4d90db9e10d46bcccf97 Mon Sep 17 00:00:00 2001 From: Dotty Dotter Date: Wed, 6 May 2026 23:46:01 +0200 Subject: [PATCH] fix(#179): Scorecard via WeasyPrint-PDF (Container hat kein Chromium) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Container hat keine Playwright/Chromium-Installation — PNG-Render deferred. WeasyPrint-PDF mit exaktem Seiten-Setup (1200x630pt fuer og, 1080x1080pt fuer square) liefert jetzt /api/assessment/scorecard.pdf. Folge-Issue fuer PNG: Chromium ins Image bauen oder pypdfium2/pdf2image fuer pdf→png-Konvertierung ergaenzen. --- app/main.py | 73 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 45 insertions(+), 28 deletions(-) diff --git a/app/main.py b/app/main.py index 6b1a1b3..7648a3d 100644 --- a/app/main.py +++ b/app/main.py @@ -3413,50 +3413,67 @@ async def scorecard_template( }) -@app.get("/api/assessment/scorecard.png") -async def api_scorecard_png( +@app.get("/api/assessment/scorecard.pdf") +async def api_scorecard_pdf( drucksache: str, bundesland: str = "NRW", format: str = "og", ): - """Liefert die Scorecard als PNG via Playwright-Render (#179). + """Liefert die Scorecard als PDF via WeasyPrint (#179). - `format=og` → 1200×630, `format=square` → 1080×1080. + `format=og` → 1200×630pt, `format=square` → 1080×1080pt. + + PNG-Render via Playwright wird in einem Folge-Issue ergänzt + (Container hat heute kein Chromium installiert). """ + from fastapi.responses import Response as _Response + from weasyprint import HTML, CSS drucksache = validate_drucksache(drucksache) row = await get_assessment(drucksache) if not row: raise HTTPException(status_code=404, detail="Antrag nicht gefunden") - if format not in ("og", "square"): raise HTTPException(status_code=400, detail="format muss 'og' oder 'square' sein") + width, height = ((1200, 630) if format == "og" else (1080, 1080)) - try: - from playwright.sync_api import sync_playwright - import urllib.parse as _up - url = ( - f"http://127.0.0.1:{settings.port}/v2/scorecard" - f"?drucksache={_up.quote(drucksache, safe='')}" - f"&bundesland={_up.quote(bundesland)}" - f"&format={format}" - ) - with sync_playwright() as pw: - browser = pw.chromium.launch(args=["--no-sandbox"]) - page = browser.new_page(viewport={"width": width, "height": height}) - page.goto(url, wait_until="networkidle", timeout=15000) - png = page.screenshot( - clip={"x": 0, "y": 0, "width": width, "height": height}, type="png", - ) - browser.close() - except Exception as e: - logger.exception("Scorecard-Render fehlgeschlagen") - raise HTTPException(status_code=500, detail=f"Render-Fehler: {e}") + # HTML aus dem internen Template ziehen (gleiche Logik wie /v2/scorecard). + from .models import Assessment + assessment = Assessment.model_validate(row) + matrix_lookup = {e.field: {"rating": e.rating} for e in assessment.gwoe_matrix} + fraktionen = list(row.get("fraktionen", []) or []) + if isinstance(fraktionen, str): + try: + import json as _json + fraktionen = _json.loads(fraktionen) or [] + except Exception: + fraktionen = [] + score = assessment.gwoe_score + if score >= 8: score_color = "#1a7f37" + elif score >= 5: score_color = "#bf6c10" + else: score_color = "#9a2a2a" + + template = templates.env.get_template("v2/screens/scorecard.html") + html_content = template.render( + request=None, + assessment=assessment, + bundesland=bundesland, + matrix_lookup=matrix_lookup, + fraktionen=fraktionen[:4], + datum=(row.get("datum") or "")[:10], + score_color=score_color, + width=width, + height=height, + ) + + # PDF mit exakt einer Seite in der Zielgrösse + page_size_css = f"@page {{ size: {width}px {height}px; margin: 0; }}" + pdf = HTML(string=html_content).write_pdf(stylesheets=[CSS(string=page_size_css)]) safe_name = drucksache.replace("/", "-") - return Response( - content=png, media_type="image/png", + return _Response( + content=pdf, media_type="application/pdf", headers={ "Content-Disposition": ( - f'inline; filename="scorecard-{safe_name}-{format}.png"' + f'inline; filename="scorecard-{safe_name}-{format}.pdf"' ), "Cache-Control": "public, max-age=600", },