"""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