"""PDF-Export für Wochen-/Monatsberichte (ReportLab). Unterstützt Umlaute über die DejaVu-Schriftarten, die im Docker-Image (fonts-dejavu-core) enthalten sind. Falls keine TTF gefunden wird, fällt die Generierung auf Helvetica zurück (dann evtl. ohne Umlaute). """ import io import os from reportlab.lib import colors from reportlab.lib.enums import TA_LEFT from reportlab.lib.pagesizes import A4 from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet from reportlab.lib.units import mm from reportlab.platypus import ( SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, ) _DEJAVU_PATHS = [ "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", "/usr/share/fonts/dejavu/DejaVuSans.ttf", "/usr/share/fonts/truetype/DejaVuSans.ttf", ] _DEJAVU_BOLD_PATHS = [ "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", "/usr/share/fonts/dejavu/DejaVuSans-Bold.ttf", ] def _register_fonts(): from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont regular = next((p for p in _DEJAVU_PATHS if os.path.exists(p)), None) bold = next((p for p in _DEJAVU_BOLD_PATHS if os.path.exists(p)), None) if regular: pdfmetrics.registerFont(TTFont("DejaVu", regular)) if bold: pdfmetrics.registerFont(TTFont("DejaVu-Bold", bold)) return "DejaVu", "DejaVu-Bold" return "Helvetica", "Helvetica-Bold" def _build_styles(font_name: str, font_bold: str): styles = getSampleStyleSheet() base = ParagraphStyle( "ReportBase", fontName=font_name, fontSize=9, leading=12, textColor=colors.HexColor("#2b2b2b"), ) return { "title": ParagraphStyle( "ReportTitle", parent=styles["Title"], fontName=font_bold, fontSize=18, leading=22, spaceAfter=4, textColor=colors.HexColor("#2b2b2b"), ), "subtitle": ParagraphStyle( "ReportSub", parent=styles["Normal"], fontName=font_name, fontSize=10, leading=14, textColor=colors.HexColor("#8a867e"), ), "h2": ParagraphStyle( "ReportH2", parent=styles["Heading2"], fontName=font_bold, fontSize=12, leading=16, spaceBefore=10, spaceAfter=4, textColor=colors.HexColor("#a8871d"), ), "body": base, "cell": ParagraphStyle( "Cell", parent=base, fontSize=8.5, leading=11, alignment=TA_LEFT, ), "cellheader": ParagraphStyle( "CellHeader", parent=base, fontName=font_bold, fontSize=8.5, leading=11, textColor=colors.white, ), } def build_pdf(title: str, subtitle: str, rows: list[dict], summary: dict | None = None) -> bytes: """Erzeugt ein PDF mit einer Übersichtstabelle der Einträge. rows: Liste von dicts mit Schluesseln date_str, weekday, bullet, bullet_label, note_text, priority, status, refs summary: optionales dict mit open/done/total/Zeitraum. """ font_name, font_bold = _register_fonts() st = _build_styles(font_name, font_bold) buffer = io.BytesIO() doc = SimpleDocTemplate( buffer, pagesize=A4, leftMargin=15 * mm, rightMargin=15 * mm, topMargin=18 * mm, bottomMargin=18 * mm, title=title, ) story = [] story.append(Paragraph(title, st["title"])) story.append(Paragraph(subtitle, st["subtitle"])) story.append(Spacer(1, 4)) # Zusammenfassung if summary: story.append(Paragraph("Zusammenfassung", st["h2"])) sum_rows = [ ["Offen", "Erledigt", "Gesamt"], [str(summary.get("open", 0)), str(summary.get("done", 0)), str(summary.get("total", 0))], ] sum_table = Table(sum_rows, colWidths=[40 * mm, 40 * mm, 40 * mm]) sum_table.setStyle(TableStyle([ ("FONTNAME", (0, 0), (-1, 0), font_bold), ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#a8871d")), ("TEXTCOLOR", (0, 0), (-1, 0), colors.white), ("FONTNAME", (0, 1), (-1, -1), font_name), ("GRID", (0, 0), (-1, -1), 0.4, colors.HexColor("#e5ded2")), ("ALIGN", (0, 0), (-1, -1), "CENTER"), ("TOPPADDING", (0, 0), (-1, -1), 4), ("BOTTOMPADDING", (0, 0), (-1, -1), 4), ])) story.append(sum_table) story.append(Spacer(1, 6)) # Einträge story.append(Paragraph("Einträge", st["h2"])) header = ["Datum", "Wochentag", "Bullet", "Notiz", "Priorität", "Status", "Verweise"] table_data = [[Paragraph(h, st["cellheader"]) for h in header]] for r in rows: table_data.append([ Paragraph(r.get("date_str", ""), st["cell"]), Paragraph(r.get("weekday", ""), st["cell"]), Paragraph(f"{r.get('bullet', '')} {r.get('bullet_label', '')}", st["cell"]), Paragraph(r.get("note_text", ""), st["cell"]), Paragraph(r.get("priority", ""), st["cell"]), Paragraph(r.get("status", ""), st["cell"]), Paragraph(r.get("refs", ""), st["cell"]), ]) col_widths = [18 * mm, 18 * mm, 20 * mm, 60 * mm, 16 * mm, 16 * mm, 30 * mm] table = Table(table_data, colWidths=col_widths, repeatRows=1) table.setStyle(TableStyle([ ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#a8871d")), ("GRID", (0, 0), (-1, -1), 0.3, colors.HexColor("#e5ded2")), ("VALIGN", (0, 0), (-1, -1), "TOP"), ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#faf7ef")]), ("TOPPADDING", (0, 0), (-1, -1), 3), ("BOTTOMPADDING", (0, 0), (-1, -1), 3), ])) story.append(table) doc.build(story) buffer.seek(0) return buffer.getvalue()