fix: PNG-Export-Canvas + Doppel-Borders bei field-chip/party-pill

User: 'immer noch doppelte Borders ... Inhalte zu klein skaliert
nach oben links gerutscht (800 px breit statt 1080)'

Ursachen:

1. Canvas-Content-Mismatch (Inhalt 75% der PNG-Breite):
   WeasyPrint rechnet 1 CSS-px = 0.75 PDF-pt (96dpi → 72dpi). @page
   war auf {width}pt × {height}pt (1080×1350) gesetzt, body aber auf
   1080×1350 CSS-px. Folge: Body fuellte nur 1080*0.75=810pt der
   1080pt-Page → Content top-left, 25% rechts/unten leer; PyMuPDF
   rasterisiert mit zoom=1 → 1080×1350 PNG, Content nur in den linken
   810×1012 px → 'Inhalte zu klein nach oben links gerutscht'.

   Fix: @page-Groesse auf (width * 0.75)pt × (height * 0.75)pt setzen.
   Body fuellt jetzt die volle Canvas-Breite. PyMuPDF kompensiert mit
   zoom = scale * 4/3, damit die PNG wieder die gewuenschten Pixel-
   Dimensionen hat (1080×1350 für scale=1).

2. Doppel-Borders auf field-chip + party-pill:
   WeasyPrint hat einen bekannten Render-Bug bei
   'border + border-radius' auf inline-flex-Elementen — der Border
   wird zweimal gezeichnet (innen + aussen). 1.5px → 2px hat das
   nicht behoben, weil's nicht am Subpixel-Wert lag.

   Fix: border ersetzt durch box-shadow: inset 0 0 0 2px var(--rule).
   Inset-Shadow rendert sauber, kein Doppel-Effekt. border-radius
   bleibt erhalten.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dotty Dotter 2026-05-07 14:37:55 +02:00
parent 4e122bceb0
commit f59286d15f
2 changed files with 19 additions and 8 deletions

View File

@ -3811,11 +3811,15 @@ async def _render_scorecard_pdf(
width=width, height=height, width=width, height=height,
) )
# `size: NNNpt` → PDF-Page hat exakt N×M Punkte. PyMuPDF rendert # CSS rechnet 1px = 0.75pt (96dpi vs. 72dpi). Wenn body width:1080px
# bei zoom=1 dann 1 PDF-Punkt = 1 PNG-Pixel. CSS-Pixel werden # gerendert wird und @page size:1080pt ist, fuellt der Body nur 75%
# aber auch in pt umgerechnet (96dpi → 72dpi → ×0.75) — daher # der Page-Breite (810pt) → Inhalt rutscht oben links und ist zu klein.
# konvertiere pt→css-px so, dass die Inhalts-Layouts passen. # Fix: @page-Groesse in pt = (CSS-px * 0.75) setzen, Body fuellt
page_size_css = f"@page {{ size: {width}pt {height}pt; margin: 0; }}" # damit die volle Canvas-Breite. PyMuPDF kompensiert beim Render mit
# zoom=4/3 zurueck auf die gewuenschten Pixel-Dimensionen.
pt_w = width * 0.75
pt_h = height * 0.75
page_size_css = f"@page {{ size: {pt_w}pt {pt_h}pt; 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 = drucksache.replace("/", "-") safe = drucksache.replace("/", "-")
return pdf, width, height, f"scorecard-{safe}-{format}" return pdf, width, height, f"scorecard-{safe}-{format}"
@ -3838,7 +3842,10 @@ async def api_scorecard_png(
doc = fitz.open(stream=pdf_bytes, filetype="pdf") doc = fitz.open(stream=pdf_bytes, filetype="pdf")
page = doc[0] page = doc[0]
# zoom-Matrix für höhere Auflösung # zoom-Matrix für höhere Auflösung
zoom = max(0.5, min(4.0, float(scale))) # PDF wird mit @page in pt = CSS-px * 0.75 erzeugt. Um die
# gewuenschte Pixel-Dimension (width × height) wieder zu
# erreichen, kompensiert PyMuPDF mit zoom = 4/3 * scale.
zoom = max(0.5, min(4.0, float(scale))) * 4.0 / 3.0
mat = fitz.Matrix(zoom, zoom) mat = fitz.Matrix(zoom, zoom)
pix = page.get_pixmap(matrix=mat, alpha=False) pix = page.get_pixmap(matrix=mat, alpha=False)
png = pix.tobytes("png") png = pix.tobytes("png")

View File

@ -53,7 +53,10 @@
.ids strong{color:var(--ink);font-weight:600} .ids strong{color:var(--ink);font-weight:600}
.party-pill{ .party-pill{
display:inline-flex;align-items:center;gap:10px; display:inline-flex;align-items:center;gap:10px;
border:2px solid var(--rule);border-radius:999px; /* box-shadow inset statt border: WeasyPrint rendert border+border-radius
auf inline-flex-Elementen doppelt (ein Render-Bug). inset ist sauber. */
box-shadow: inset 0 0 0 2px var(--rule);
border-radius:999px;
font-weight:700;letter-spacing:.04em; font-weight:700;letter-spacing:.04em;
} }
.party-pill::before{content:"";border-radius:50%;background:#d44} .party-pill::before{content:"";border-radius:50%;background:#d44}
@ -77,7 +80,8 @@
.verdict .fields{display:flex;flex-wrap:wrap} .verdict .fields{display:flex;flex-wrap:wrap}
.field-chip{ .field-chip{
font-family:'JetBrains Mono',monospace;font-weight:600; font-family:'JetBrains Mono',monospace;font-weight:600;
border:2px solid var(--rule);border-radius:4px;background:transparent; box-shadow: inset 0 0 0 2px var(--rule);
border-radius:4px;background:transparent;
} }
.fractions{display:grid;grid-template-columns:repeat({{ fraktionen_count }},1fr);border:2px solid var(--rule)} .fractions{display:grid;grid-template-columns:repeat({{ fraktionen_count }},1fr);border:2px solid var(--rule)}
.frac{ .frac{