267 lines
7.7 KiB
Python
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) |