"""Server-side PDF export for posture reports."""
from __future__ import annotations
from io import BytesIO
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import mm
from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle
from app.db.models import Child, Report
from app.services.reports import METRIC_LEVEL_LABELS, RISK_LABELS, report_to_dict
LEVEL_COLORS = {
"normal": colors.HexColor("#2bb673"),
"low": colors.HexColor("#84cc16"),
"medium": colors.HexColor("#f59e0b"),
"high": colors.HexColor("#ef4444"),
}
def build_report_pdf(report: Report, child: Child | None = None) -> bytes:
payload = report_to_dict(report)
buffer = BytesIO()
doc = SimpleDocTemplate(
buffer,
pagesize=A4,
leftMargin=18 * mm,
rightMargin=18 * mm,
topMargin=16 * mm,
bottomMargin=16 * mm,
title=f"Happy Up Report #{report.id}",
)
styles = getSampleStyleSheet()
title_style = ParagraphStyle(
"ReportTitle",
parent=styles["Heading1"],
fontName="Helvetica-Bold",
fontSize=18,
textColor=colors.HexColor("#1a6fb5"),
spaceAfter=8,
)
body = styles["BodyText"]
muted = ParagraphStyle("Muted", parent=body, textColor=colors.HexColor("#6b7280"), fontSize=9)
child_name = child.name if child else f"儿童 #{report.child_id}"
story = [
Paragraph("儿童 AI 体态管理 · 筛查报告", title_style),
Paragraph(f"儿童:{child_name} · 报告编号 #{report.id}", muted),
Spacer(1, 8),
]
if report.report_type == "movement_scoring":
story.extend(_movement_sections(payload, body, muted))
else:
story.extend(_screening_sections(payload, body, muted))
story.append(Spacer(1, 10))
story.append(Paragraph(payload.get("disclaimer", ""), muted))
doc.build(story)
return buffer.getvalue()
def _screening_sections(payload: dict, body, muted) -> list:
risk = payload.get("riskLevel", "medium")
risk_label = RISK_LABELS.get(risk, risk)
sections = [
Paragraph(f"风险等级:{risk_label}", body),
Paragraph(f"摘要:{payload.get('summary', '')}", body),
Spacer(1, 8),
Paragraph("指标明细", body),
]
rows = [["指标", "数值", "等级", "置信度"]]
for metric in payload.get("metrics", []):
rows.append(
[
metric.get("name", ""),
f"{metric.get('value', 0)}%",
METRIC_LEVEL_LABELS.get(metric.get("level", ""), metric.get("level", "")),
f"{metric.get('confidence', 0):.0%}",
]
)
table = Table(rows, colWidths=[80, 60, 60, 60])
table.setStyle(
TableStyle(
[
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#eef2f7")),
("TEXTCOLOR", (0, 0), (-1, 0), colors.HexColor("#1f2937")),
("GRID", (0, 0), (-1, -1), 0.5, colors.HexColor("#e5e7eb")),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#fafafa")]),
]
)
)
sections.append(table)
sections.append(Spacer(1, 8))
sections.append(Paragraph("建议", body))
for idx, item in enumerate(payload.get("recommendations", []), start=1):
sections.append(Paragraph(f"{idx}. {item}", body))
return sections
def _movement_sections(payload: dict, body, muted) -> list:
score = payload.get("score", 0)
sections = [
Paragraph(f"跟练得分:{score}", body),
Paragraph(
f"完成 {payload.get('repsCompleted', 0)} 组 · "
f"时长 {payload.get('durationSeconds', 0)} 秒",
muted,
),
Spacer(1, 8),
Paragraph("维度评分", body),
]
dims = payload.get("dimensions", {})
rows = [["维度", "得分"]]
for key, label in (
("trajectory", "轨迹"),
("angle", "角度"),
("rhythm", "节奏"),
("stability", "稳定性"),
("completion", "完成度"),
):
rows.append([label, str(dims.get(key, "-"))])
table = Table(rows, colWidths=[120, 80])
table.setStyle(
TableStyle(
[
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#eef2f7")),
("GRID", (0, 0), (-1, -1), 0.5, colors.HexColor("#e5e7eb")),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
]
)
)
sections.append(table)
return sections