Files
sber-med-server/export.py
T

292 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
Генерация PDF и DOCX отчётов из сохранённого результата диагностики.
"""
import io
import json
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
from reportlab.lib import colors
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, HRFlowable
)
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER
from docx import Document
from docx.shared import Pt, Cm, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
# --- Шрифты для кириллицы в PDF ---
FONT_PATHS = {
"DejaVuSans": "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"DejaVuSans-Bold": "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
"DejaVuSans-Oblique": "/usr/share/fonts/truetype/dejavu/DejaVuSans-Oblique.ttf",
}
for name, path in FONT_PATHS.items():
try:
pdfmetrics.registerFont(TTFont(name, path))
except Exception:
pass
# Регистрируем семейство, чтобы reportlab понимал, что DejaVuSans-Bold —
# это жирный вариант DejaVuSans (иначе Paragraph падает с "Can't map family/bold/italic").
try:
pdfmetrics.registerFontFamily(
'DejaVuSans',
normal='DejaVuSans',
bold='DejaVuSans-Bold',
italic='DejaVuSans',
boldItalic='DejaVuSans-Bold',
)
except Exception:
pass
STATUS_TEXT = {
"ok": "В норме",
"warning": "Обратить внимание",
"danger": "Отклонение",
"info": "Информация",
}
STATUS_COLORS = {
"ok": colors.HexColor("#155724"),
"warning": colors.HexColor("#856404"),
"danger": colors.HexColor("#721c24"),
"info": colors.HexColor("#0c5460"),
}
STATUS_BG = {
"ok": colors.HexColor("#d4edda"),
"warning": colors.HexColor("#fff3cd"),
"danger": colors.HexColor("#f8d7da"),
"info": colors.HexColor("#d1ecf1"),
}
def _load_result(share_id):
"""Загрузить результат из БД (импорт на месте, чтобы не было циклических зависимостей)."""
import storage
return storage.get_result(share_id)
def _parse_json_list(raw):
"""Вернуть список из БД (get_result уже возвращает распарсенные списки)."""
if isinstance(raw, list):
return raw
try:
return json.loads(raw) if raw else []
except Exception:
return []
# ============================ PDF ============================
def generate_pdf(share_id) -> bytes:
"""Сгенерировать PDF-отчёт. Возвращает байты."""
result = _load_result(share_id)
if result is None:
raise ValueError("Результат не найден")
buf = io.BytesIO()
doc = SimpleDocTemplate(
buf, pagesize=A4,
leftMargin=20 * mm, rightMargin=20 * mm,
topMargin=18 * mm, bottomMargin=18 * mm,
title=f"Результат диагностики #{share_id}",
author="med.my-dpr.ru",
)
styles = getSampleStyleSheet()
styles.add(ParagraphStyle(
name="TitleRu", fontName="DejaVuSans-Bold", fontSize=18,
leading=24, alignment=TA_CENTER, spaceAfter=4,
))
styles.add(ParagraphStyle(
name="SubtitleRu", fontName="DejaVuSans", fontSize=10,
leading=14, alignment=TA_CENTER, textColor=colors.grey, spaceAfter=14,
))
styles.add(ParagraphStyle(
name="H2Ru", fontName="DejaVuSans-Bold", fontSize=13,
leading=18, spaceBefore=12, spaceAfter=6,
))
styles.add(ParagraphStyle(
name="BodyRu", fontName="DejaVuSans", fontSize=10.5,
leading=16, spaceAfter=6,
))
styles.add(ParagraphStyle(
name="SmallRu", fontName="DejaVuSans", fontSize=8.5,
leading=12, textColor=colors.grey,
))
# Стили ячеек таблицы (Paragraph, чтобы текст переносился и не перекрывался)
styles.add(ParagraphStyle(
name="CellRu", fontName="DejaVuSans", fontSize=9.5,
leading=13, spaceAfter=0,
))
styles.add(ParagraphStyle(
name="CellRuBold", fontName="DejaVuSans-Bold", fontSize=9.5,
leading=13, spaceAfter=0, textColor=colors.white,
))
story = []
# Заголовок
story.append(Paragraph("🩺 Диагностика по видео", styles["TitleRu"]))
story.append(Paragraph(
f"Результат #{result['share_id']} • {result['created_at'][:10]} {result['created_at'][11:16]}",
styles["SubtitleRu"],
))
story.append(HRFlowable(width="100%", thickness=1, color=colors.HexColor("#667eea"), spaceAfter=12))
# Сводка
story.append(Paragraph("Сводка", styles["H2Ru"]))
story.append(Paragraph(result["summary"], styles["BodyRu"]))
# Предупреждения
warnings = _parse_json_list(result["warnings"])
if warnings:
story.append(Paragraph("⚠️ Требует внимания", styles["H2Ru"]))
for w in warnings:
story.append(Paragraph(f"• {w}", styles["BodyRu"]))
# Показатели
metrics = _parse_json_list(result["metrics"])
if metrics:
story.append(Paragraph("Показатели", styles["H2Ru"]))
data = [[Paragraph("Показатель", styles["CellRuBold"]),
Paragraph("Значение", styles["CellRuBold"]),
Paragraph("Оценка", styles["CellRuBold"])]]
for m in metrics:
value = m.get("value", "")
comment = m.get("comment", "")
if comment:
value = f"{value} ({comment})"
status = m.get("status", "info")
data.append([
Paragraph(m.get("name", ""), styles["CellRu"]),
Paragraph(value, styles["CellRu"]),
Paragraph(STATUS_TEXT.get(status, status), styles["CellRu"]),
])
table = Table(data, colWidths=[55 * mm, 75 * mm, 50 * mm])
table.setStyle(TableStyle([
("FONTNAME", (0, 0), (-1, 0), "DejaVuSans-Bold"),
("FONTNAME", (0, 1), (-1, -1), "DejaVuSans"),
("FONTSIZE", (0, 0), (-1, -1), 9.5),
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#667eea")),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#f5f6ff")]),
("GRID", (0, 0), (-1, -1), 0.4, colors.HexColor("#cccccc")),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("LEFTPADDING", (0, 0), (-1, -1), 6),
("RIGHTPADDING", (0, 0), (-1, -1), 6),
("TOPPADDING", (0, 0), (-1, -1), 5),
("BOTTOMPADDING", (0, 0), (-1, -1), 5),
]))
story.append(table)
# Рекомендации
advice = _parse_json_list(result["advice"])
if advice:
story.append(Paragraph("💡 Рекомендации", styles["H2Ru"]))
for a in advice:
story.append(Paragraph(f"• {a}", styles["BodyRu"]))
story.append(Spacer(1, 10))
story.append(HRFlowable(width="100%", thickness=0.5, color=colors.HexColor("#dddddd")))
story.append(Spacer(1, 6))
story.append(Paragraph(
"⚠️ Результат получен автоматически по анализу видео и не является медицинским диагнозом. "
"Для точной оценки обратитесь к врачу.",
styles["SmallRu"],
))
doc.build(story)
return buf.getvalue()
# ============================ DOCX ============================
def generate_docx(share_id) -> bytes:
"""Сгенерировать DOCX-отчёт. Возвращает байты."""
result = _load_result(share_id)
if result is None:
raise ValueError("Результат не найден")
doc = Document()
# Стили
style = doc.styles["Normal"]
style.font.name = "Calibri"
style.font.size = Pt(11)
# Заголовок
title = doc.add_heading("🩺 Диагностика по видео", level=0)
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
for run in title.runs:
run.font.color.rgb = RGBColor(0x66, 0x7E, 0xEA)
sub = doc.add_paragraph()
sub.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = sub.add_run(
f"Результат #{result['share_id']} • {result['created_at'][:10]} {result['created_at'][11:16]}"
)
run.font.size = Pt(10)
run.font.color.rgb = RGBColor(0x88, 0x88, 0x88)
# Сводка
doc.add_heading("Сводка", level=1)
doc.add_paragraph(result["summary"])
# Предупреждения
warnings = _parse_json_list(result["warnings"])
if warnings:
doc.add_heading("⚠️ Требует внимания", level=1)
for w in warnings:
doc.add_paragraph(w, style="List Bullet")
# Показатели
metrics = _parse_json_list(result["metrics"])
if metrics:
doc.add_heading("Показатели", level=1)
table = doc.add_table(rows=1, cols=3)
table.style = "Light Grid Accent 1"
hdr = table.rows[0].cells
for i, h in enumerate(["Показатель", "Значение", "Оценка"]):
hdr[i].text = h
for p in hdr[i].paragraphs:
for r in p.runs:
r.font.bold = True
for m in metrics:
row = table.add_row().cells
value = m.get("value", "")
comment = m.get("comment", "")
if comment:
value = f"{value} ({comment})"
row[0].text = m.get("name", "")
row[1].text = value
row[2].text = STATUS_TEXT.get(m.get("status", "info"), m.get("status", ""))
# Рекомендации
advice = _parse_json_list(result["advice"])
if advice:
doc.add_heading("💡 Рекомендации", level=1)
for a in advice:
doc.add_paragraph(a, style="List Bullet")
# Дисклеймер
doc.add_paragraph()
disc = doc.add_paragraph()
run = disc.add_run(
"⚠️ Результат получен автоматически по анализу видео и не является медицинским диагнозом. "
"Для точной оценки обратитесь к врачу."
)
run.font.size = Pt(9)
run.font.color.rgb = RGBColor(0x88, 0x88, 0x88)
buf = io.BytesIO()
doc.save(buf)
return buf.getvalue()