feat(#180): PNG-Scorecards via WeasyPrint→PyMuPDF
PyMuPDF (fitz) ist bereits in requirements.txt — also kein extra Dependency noetig. Render-Pipeline: HTML-Template → WeasyPrint-PDF → fitz Pixmap → PNG-Bytes. Endpoint: GET /api/assessment/scorecard.png?drucksache=&bundesland= &format=og|square&scale=2.0 - scale=2.0 (Default) liefert Retina-Aufloesung. - scale=1.0..4.0 erlaubt. Refactor: gemeinsamer Helper _render_scorecard_pdf() fuer .pdf und .png — vorher Code-Duplikat zwischen den Endpoints. Refs: #179, #180 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
eeedf85d7e
commit
52ff36a136
84
app/main.py
84
app/main.py
@ -3413,30 +3413,25 @@ async def scorecard_template(
|
||||
})
|
||||
|
||||
|
||||
@app.get("/api/assessment/scorecard.pdf")
|
||||
async def api_scorecard_pdf(
|
||||
drucksache: str, bundesland: str = "NRW", format: str = "og",
|
||||
):
|
||||
"""Liefert die Scorecard als PDF via WeasyPrint (#179).
|
||||
async def _render_scorecard_pdf(
|
||||
drucksache: str, bundesland: str, format: str,
|
||||
) -> tuple[bytes, int, int, str]:
|
||||
"""Render Scorecard-HTML zu PDF (gemeinsamer Helper für .pdf und .png).
|
||||
|
||||
`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).
|
||||
Returns (pdf_bytes, width, height, safe_filename).
|
||||
"""
|
||||
from fastapi.responses import Response as _Response
|
||||
from weasyprint import HTML, CSS
|
||||
from .models import Assessment
|
||||
|
||||
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))
|
||||
|
||||
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))
|
||||
|
||||
# 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 [])
|
||||
@ -3461,20 +3456,63 @@ async def api_scorecard_pdf(
|
||||
fraktionen=fraktionen[:4],
|
||||
datum=(row.get("datum") or "")[:10],
|
||||
score_color=score_color,
|
||||
width=width,
|
||||
height=height,
|
||||
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("/", "-")
|
||||
safe = drucksache.replace("/", "-")
|
||||
return pdf, width, height, f"scorecard-{safe}-{format}"
|
||||
|
||||
|
||||
@app.get("/api/assessment/scorecard.png")
|
||||
async def api_scorecard_png(
|
||||
drucksache: str, bundesland: str = "NRW", format: str = "og", scale: float = 2.0,
|
||||
):
|
||||
"""Liefert die Scorecard als PNG via WeasyPrint→PyMuPDF.
|
||||
|
||||
`scale=2.0` rendert in doppelter Auflösung (für Retina-Displays).
|
||||
"""
|
||||
from fastapi.responses import Response as _Response
|
||||
pdf_bytes, width, height, safe_name = await _render_scorecard_pdf(
|
||||
drucksache, bundesland, format,
|
||||
)
|
||||
try:
|
||||
import fitz # pymupdf
|
||||
doc = fitz.open(stream=pdf_bytes, filetype="pdf")
|
||||
page = doc[0]
|
||||
# zoom-Matrix für höhere Auflösung
|
||||
zoom = max(0.5, min(4.0, float(scale)))
|
||||
mat = fitz.Matrix(zoom, zoom)
|
||||
pix = page.get_pixmap(matrix=mat, alpha=False)
|
||||
png = pix.tobytes("png")
|
||||
doc.close()
|
||||
except Exception as e:
|
||||
logger.exception("PNG-Render fehlgeschlagen")
|
||||
raise HTTPException(status_code=500, detail=f"PNG-Render-Fehler: {e}")
|
||||
return _Response(
|
||||
content=png, media_type="image/png",
|
||||
headers={
|
||||
"Content-Disposition": f'inline; filename="{safe_name}.png"',
|
||||
"Cache-Control": "public, max-age=600",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/api/assessment/scorecard.pdf")
|
||||
async def api_scorecard_pdf(
|
||||
drucksache: str, bundesland: str = "NRW", format: str = "og",
|
||||
):
|
||||
"""Liefert die Scorecard als PDF via WeasyPrint (#179).
|
||||
|
||||
`format=og` → 1200×630, `format=square` → 1080×1080.
|
||||
"""
|
||||
from fastapi.responses import Response as _Response
|
||||
pdf, _w, _h, safe_name = await _render_scorecard_pdf(drucksache, bundesland, format)
|
||||
return _Response(
|
||||
content=pdf, media_type="application/pdf",
|
||||
headers={
|
||||
"Content-Disposition": (
|
||||
f'inline; filename="scorecard-{safe_name}-{format}.pdf"'
|
||||
),
|
||||
"Content-Disposition": f'inline; filename="{safe_name}.pdf"',
|
||||
"Cache-Control": "public, max-age=600",
|
||||
},
|
||||
)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user