● Zion Boggan
repos/Sightline/backend/app/services/report_pdf.py
zionboggan.com ↗
64 lines · python
History for this file →
1
"""Render an assembled report dict (from reporting.build_report) to a real PDF using reportlab.
2
Kept separate from reporting.py so the core assembly stays dependency-free and testable."""
3
from __future__ import annotations
4
 
5
import io
6
 
7
 
8
def render_pdf(report: dict) -> bytes:
9
    from reportlab.lib import colors
10
    from reportlab.lib.pagesizes import letter
11
    from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
12
    from reportlab.lib.units import inch
13
    from reportlab.platypus import (
14
        SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
15
    )
16
 
17
    buf = io.BytesIO()
18
    doc = SimpleDocTemplate(buf, pagesize=letter, title=report.get("title", "Report"),
19
                            topMargin=0.7 * inch, bottomMargin=0.7 * inch)
20
    styles = getSampleStyleSheet()
21
    h1 = styles["Title"]
22
    h2 = ParagraphStyle("H2", parent=styles["Heading2"], spaceBefore=14, textColor=colors.HexColor("#1a3a5a"))
23
    body = styles["BodyText"]
24
    story = [Paragraph(report.get("title", "Investigation Report"), h1), Spacer(1, 10)]
25
 
26
    for key, content in report.get("sections", {}).items():
27
        story.append(Paragraph(key.replace("_", " ").title(), h2))
28
        if isinstance(content, str):
29
            story.append(Paragraph(content, body))
30
        elif isinstance(content, list):
31
            if content and isinstance(content[0], dict):
32
                cols = list(content[0].keys())
33
                data = [cols] + [[_cell(row.get(c)) for c in cols] for row in content[:60]]
34
                story.append(_table(data, Table, TableStyle, colors))
35
            else:
36
                for item in content:
37
                    story.append(Paragraph(f"• {_cell(item)}", body))
38
        elif isinstance(content, dict):
39
            data = [["Field", "Value"]] + [[k, _cell(v)] for k, v in content.items()]
40
            story.append(_table(data, Table, TableStyle, colors))
41
        story.append(Spacer(1, 6))
42
 
43
    doc.build(story)
44
    return buf.getvalue()
45
 
46
 
47
def _cell(v) -> str:
48
    import json
49
    if isinstance(v, (dict, list)):
50
        return json.dumps(v, default=str)[:120]
51
    return str(v) if v is not None else "-"
52
 
53
 
54
def _table(data, Table, TableStyle, colors):
55
    t = Table(data, repeatRows=1, hAlign="LEFT")
56
    t.setStyle(TableStyle([
57
        ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1a3a5a")),
58
        ("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
59
        ("FONTSIZE", (0, 0), (-1, -1), 7),
60
        ("GRID", (0, 0), (-1, -1), 0.4, colors.HexColor("#cccccc")),
61
        ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#f2f6fa")]),
62
        ("VALIGN", (0, 0), (-1, -1), "TOP"),
63
    ]))
64
    return t