Files

267 lines
7.7 KiB
Python

import os
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
from PIL import Image as PILImage
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, Image as RLImage, PageBreak
from reportlab.lib.styles import ParagraphStyle
from reportlab.lib.units import mm
from reportlab.lib import colors
from reportlab.platypus import TableStyle
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfbase import pdfmetrics
from reportlab.lib.pagesizes import A4
class NutritionPDFBuilder:
def __init__(self):
pdfmetrics.registerFont(TTFont('SimHei', 'C:/Windows/Fonts/simhei.ttf'))
def build(self, data, output_path):
doc = SimpleDocTemplate(output_path, pagesize=A4)
story = []
# ===== 统一字体 =====
normal = ParagraphStyle(
"normal",
fontName="SimHei",
fontSize=13,
leading=18,
alignment=1
)
header = ParagraphStyle(
"header",
fontName="SimHei",
fontSize=13,
leading=18,
alignment=1,
textColor=colors.white
)
story.append(Paragraph("营养健康评估报告", normal))
story.append(Spacer(1, 10 * mm))
# ================= 食物表 =================
food_table = []
food_table.append([Paragraph("食物摄入量评估报告", header), "", "", ""])
food_table.append([
Paragraph("种类", header),
Paragraph("图片", header),
Paragraph("摄入量", header),
Paragraph("评价", header)
])
for cat, info in data["category_result"].items():
food_table.append([
Paragraph(cat, normal),
self._get_image(cat),
Paragraph(f"{info['value']} g", normal),
Paragraph(info["evaluation"], normal)
])
# ===== 建议(适中行距)=====
suggestion_text = "<br/>".join(
[f"{i+1}. {s}" for i, s in enumerate(data["suggestions"])]
)
food_table.append([
Paragraph("整体评估及建议", normal),
Paragraph(suggestion_text, normal),
"",
""
])
t1 = Table(
food_table,
colWidths=[40*mm, 55*mm, 40*mm, 30*mm],
repeatRows=2
)
t1.setStyle(TableStyle([
("SPAN", (0,0), (-1,0)),
("SPAN", (1,-1), (-1,-1)),
("BACKGROUND", (0,0), (-1,1), colors.HexColor("#4F75B5")),
("TEXTCOLOR", (0,0), (-1,1), colors.white),
("GRID",(0,0),(-1,-1),0.5,colors.grey),
("FONTNAME",(0,0),(-1,-1),"SimHei"),
("ALIGN",(0,0),(-1,-1),"CENTER"),
("VALIGN",(0,0),(-1,-1),"MIDDLE"),
("TOPPADDING",(0,0),(-1,-1),14),
("BOTTOMPADDING",(0,0),(-1,-1),14),
("ROWHEIGHT",(0,2),(-1,-2),45)
]))
story.append(t1)
story.append(PageBreak())
# ================= 饼图 =================
pie_path = "temp_pie.png"
self._generate_pie(data, pie_path)
n = data["nutrition"]
energy_table = [
[Paragraph("能量及钙摄入量评估报告", header), "", ""],
[
Paragraph("项目", header),
Paragraph("摄入量", header),
Paragraph("评价", header)
],
[
Paragraph("总能量", normal),
Paragraph(self._format_energy_intake(n["energy"]), normal),
Paragraph(n["energy"]["evaluation"], normal)
],
[
Paragraph("三大营养素供能占比", normal),
self._get_pie(pie_path),
""
],
[
Paragraph("钙", normal),
Paragraph(f"{n['calcium']['value']} mg/day", normal),
Paragraph(n["calcium"]["evaluation"], normal)
],
]
# ===== 解读 =====
if data.get("interpretation"):
text = "<br/>".join(
line.strip()
for line in data["interpretation"].splitlines()
if line.strip()
)
else:
interpretation = []
if n["energy"]["evaluation"] == "不足":
interpretation.append("能量摄入不足,应增加碳水化合物摄入")
if n["energy"]["evaluation"] == "偏高":
interpretation.append("能量摄入偏高,应适当控制总能量摄入")
if n["calcium"]["evaluation"] == "不足":
interpretation.append("钙摄入不足,应增加奶制品摄入")
text = "<br/>".join(interpretation)
energy_table.append([
Paragraph("解读", normal),
Paragraph(text, normal),
""
])
t2 = Table(energy_table, colWidths=[50*mm, 70*mm, 40*mm])
t2.setStyle(TableStyle([
("SPAN", (0,0), (-1,0)),
("SPAN", (1,3), (2,3)),
("SPAN", (1,-1), (2,-1)),
("BACKGROUND",(0,0),(-1,1),colors.HexColor("#4F75B5")),
("TEXTCOLOR",(0,0),(-1,1),colors.white),
("GRID",(0,0),(-1,-1),0.5,colors.grey),
("FONTNAME",(0,0),(-1,-1),"SimHei"),
("ALIGN",(0,0),(-1,-1),"CENTER"),
("VALIGN",(0,0),(-1,-1),"MIDDLE"),
("TOPPADDING",(0,0),(-1,-1),14),
("BOTTOMPADDING",(0,0),(-1,-1),14),
]))
story.append(t2)
doc.build(story)
def _format_energy_intake(self, energy_info):
value = energy_info.get("value", "-")
standard = energy_info.get("standard")
if standard:
return f"{value} kcal/day<br/>(标准 {standard} kcal)"
return f"{value} kcal/day"
# ================= 图片 =================
def _get_image(self, category):
base = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
path = os.path.join(base, "static", "images", f"{category}.png")
if os.path.exists(path):
return RLImage(path, width=35*mm, height=35*mm)
return ""
# ================= 饼图 =================
def _generate_pie(self, data, path):
font_path = r"C:\Windows\Fonts\simhei.ttf"
font_prop = fm.FontProperties(fname=font_path)
protein = float(data["nutrition"].get("protein", 0))
fat = float(data["nutrition"].get("fat", 0))
carb = float(data["nutrition"].get("carbohydrate", 0))
protein_kcal = protein * 4
fat_kcal = fat * 9
carb_kcal = carb * 4
total = protein_kcal + fat_kcal + carb_kcal
values = [1,1,1] if total == 0 else [
protein_kcal / total,
fat_kcal / total,
carb_kcal / total
]
labels = [
f"蛋白质 {protein}g",
f"脂肪 {fat}g",
f"碳水 {carb}g"
]
plt.figure(figsize=(6,6))
plt.gca().set_aspect('equal')
plt.pie(
values,
labels=labels,
autopct="%1.1f%%",
startangle=90,
textprops={'fontproperties': font_prop, 'fontsize': 12}
)
plt.title("三大营养素供能占比", fontproperties=font_prop)
plt.savefig(path, bbox_inches='tight')
plt.close()
# ================= 饼图插入 =================
def _get_pie(self, path):
if not os.path.exists(path):
return ""
img = PILImage.open(path)
w, h = img.size
target_w = 90 * mm
target_h = target_w * (h / w)
return RLImage(path, width=target_w, height=target_h)