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 _render_scorecard_pdf(
|
||||||
async def api_scorecard_pdf(
|
drucksache: str, bundesland: str, format: str,
|
||||||
drucksache: str, bundesland: str = "NRW", format: str = "og",
|
) -> tuple[bytes, int, int, str]:
|
||||||
):
|
"""Render Scorecard-HTML zu PDF (gemeinsamer Helper für .pdf und .png).
|
||||||
"""Liefert die Scorecard als PDF via WeasyPrint (#179).
|
|
||||||
|
|
||||||
`format=og` → 1200×630pt, `format=square` → 1080×1080pt.
|
Returns (pdf_bytes, width, height, safe_filename).
|
||||||
|
|
||||||
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
|
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)
|
drucksache = validate_drucksache(drucksache)
|
||||||
row = await get_assessment(drucksache)
|
row = await get_assessment(drucksache)
|
||||||
if not row:
|
if not row:
|
||||||
raise HTTPException(status_code=404, detail="Antrag nicht gefunden")
|
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)
|
assessment = Assessment.model_validate(row)
|
||||||
matrix_lookup = {e.field: {"rating": e.rating} for e in assessment.gwoe_matrix}
|
matrix_lookup = {e.field: {"rating": e.rating} for e in assessment.gwoe_matrix}
|
||||||
fraktionen = list(row.get("fraktionen", []) or [])
|
fraktionen = list(row.get("fraktionen", []) or [])
|
||||||
@ -3461,20 +3456,63 @@ async def api_scorecard_pdf(
|
|||||||
fraktionen=fraktionen[:4],
|
fraktionen=fraktionen[:4],
|
||||||
datum=(row.get("datum") or "")[:10],
|
datum=(row.get("datum") or "")[:10],
|
||||||
score_color=score_color,
|
score_color=score_color,
|
||||||
width=width,
|
width=width, height=height,
|
||||||
height=height,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# PDF mit exakt einer Seite in der Zielgrösse
|
|
||||||
page_size_css = f"@page {{ size: {width}px {height}px; margin: 0; }}"
|
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)])
|
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(
|
return _Response(
|
||||||
content=pdf, media_type="application/pdf",
|
content=pdf, media_type="application/pdf",
|
||||||
headers={
|
headers={
|
||||||
"Content-Disposition": (
|
"Content-Disposition": f'inline; filename="{safe_name}.pdf"',
|
||||||
f'inline; filename="scorecard-{safe_name}-{format}.pdf"'
|
|
||||||
),
|
|
||||||
"Cache-Control": "public, max-age=600",
|
"Cache-Control": "public, max-age=600",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user