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 = "
".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(f"{n['energy']['value']} kcal/day", 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) ], ] # ===== 解读(适中间距)===== interpretation = [] if n["energy"]["evaluation"] == "不足": interpretation.append("能量摄入不足,应增加碳水化合物摄入") if n["calcium"]["evaluation"] == "不足": interpretation.append("钙摄入不足,应增加奶制品摄入") text = "
".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 _get_image(self, category): base = 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)