# PDF生成服务 - 支持中文 import os import re from reportlab.lib.pagesizes import A4 from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.lib.units import mm from reportlab.lib import colors from reportlab.platypus import ( SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak, ) from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT def register_chinese_font(): """注册中文字体""" # 尝试注册系统自带的中文字体 font_paths = [ # Windows常用字体路径 r"C:\Windows\Fonts\simsun.ttc", # 宋体 r"C:\Windows\Fonts\msyh.ttc", # 微软雅黑 r"C:\Windows\Fonts\STSONG.TTF", # 华文宋体 r"C:\Windows\Fonts\simhei.ttf", # 黑体 # Linux容器字体路径 "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc", "/usr/share/fonts/wqy-microhei/wqy-microhei.ttc", "/usr/share/fonts/noto/NotoSansCJK-Regular.ttc", "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", ] # 尝试注册一个可用的字体 for font_path in font_paths: if os.path.exists(font_path): try: pdfmetrics.registerFont(TTFont("Chinese", font_path)) return True except Exception as e: continue return False # 尝试注册中文字体 _has_chinese_font = register_chinese_font() def clean_text(text): """清理文本,移除不支持的字符""" if not text: return "" # 移除多余空白 text = re.sub(r"\s+", " ", text) return text.strip() class PianoPDF: def __init__(self): self.elements = [] self.styles = getSampleStyleSheet() # 创建中文字体样式 if _has_chinese_font: self.base_font = "Chinese" else: self.base_font = "Helvetica" # 标题样式 self.title_style = ParagraphStyle( "CustomTitle", parent=self.styles["Heading1"], fontName=self.base_font, fontSize=18, spaceAfter=10 * mm, alignment=TA_CENTER, ) # 副标题样式 self.heading_style = ParagraphStyle( "CustomHeading", parent=self.styles["Heading2"], fontName=self.base_font, fontSize=14, spaceAfter=6 * mm, spaceBefore=6 * mm, textColor=colors.HexColor("#2c3e50"), ) # 正文样式 self.body_style = ParagraphStyle( "CustomBody", parent=self.styles["Normal"], fontName=self.base_font, fontSize=10, spaceAfter=4 * mm, leading=14, ) # 表格样式 self.table_style = TableStyle( [ ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#3498db")), ("TEXTCOLOR", (0, 0), (-1, 0), colors.whitesmoke), ("ALIGN", (0, 0), (-1, -1), "LEFT"), ("FONTNAME", (0, 0), (-1, 0), self.base_font), ("FONTSIZE", (0, 0), (-1, 0), 10), ("BOTTOMPADDING", (0, 0), (-1, 0), 8), ("BACKGROUND", (0, 1), (-1, -1), colors.white), ("FONTNAME", (0, 1), (-1, -1), self.base_font), ("FONTSIZE", (0, 1), (-1, -1), 9), ("GRID", (0, 0), (-1, -1), 0.5, colors.grey), ( "ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#f8f9fa")], ), ] ) def add_title(self, text): """添加标题""" self.elements.append(Paragraph(clean_text(text), self.title_style)) self.elements.append(Spacer(1, 5 * mm)) def add_heading(self, text): """添加副标题""" self.elements.append(Paragraph(clean_text(text), self.heading_style)) def add_paragraph(self, text): """添加段落""" if text: self.elements.append(Paragraph(clean_text(text), self.body_style)) self.elements.append(Spacer(1, 3 * mm)) def add_list(self, items): """添加列表""" for item in items: if item: self.elements.append( Paragraph(f"• {clean_text(item)}", self.body_style) ) self.elements.append(Spacer(1, 3 * mm)) def add_table(self, data): """添加表格""" if not data or len(data) < 2: return # 确保所有数据都是字符串 table_data = [] for row in data: table_data.append([clean_text(str(cell)) for cell in row]) if table_data: table = Table(table_data) table.setStyle(self.table_style) self.elements.append(table) self.elements.append(Spacer(1, 5 * mm)) def add_spacer(self, height=5 * mm): """添加间距""" self.elements.append(Spacer(1, height)) def generate_pdf(plan_id, student_name, content, output_dir, rendered_report=None): """ 使用reportlab生成PDF文件 Args: plan_id: 方案ID student_name: 学员姓名 content: 方案内容字典 output_dir: 输出目录 rendered_report: 可选的预渲染报告(Markdown格式),如果提供则使用模板渲染整个报告 Returns: str: PDF文件路径 """ os.makedirs(output_dir, exist_ok=True) output_path = os.path.join(output_dir, f"plan_{plan_id}.pdf") # 创建PDF文档 doc = SimpleDocTemplate( output_path, pagesize=A4, rightMargin=20 * mm, leftMargin=20 * mm, topMargin=20 * mm, bottomMargin=20 * mm, ) pdf = PianoPDF() # 如果提供了预渲染的报告,使用模板渲染整个报告 if rendered_report: # 解析Markdown并添加到PDF lines = rendered_report.split('\n') in_table = False table_data = [] for line in lines: line = line.strip() if not line or line.startswith('---'): if in_table and table_data: pdf.add_table(table_data) table_data = [] in_table = False continue if line.startswith('# ') and not line.startswith('## '): # 一级标题作为主标题 title = line.replace('# ', '').replace('*', '') pdf.add_title(title) elif line.startswith('## '): # 二级标题 if in_table and table_data: pdf.add_table(table_data) table_data = [] in_table = False heading = line.replace('## ', '').replace('**', '') pdf.add_heading(heading) elif line.startswith('|') and '|' in line[1:]: # 表格行 in_table = True cells = [c.strip() for c in line.split('|')[1:-1]] if cells and any(cells): # 跳过表头分隔行 if not all(c.strip().startswith('-') for c in cells if c.strip()): table_data.append(cells) elif line.startswith('- '): # 列表项 pdf.add_paragraph(line) elif line and not in_table: # 普通段落 pdf.add_paragraph(line.replace('**', '').replace('*', '')) # 表格收尾 if in_table and table_data: pdf.add_table(table_data) else: # 使用原有结构化方式生成PDF # 标题 pdf.add_title(f"钢琴练习方案 - {student_name}") # 学员信息 pdf.add_heading("📋 学员信息") pdf.add_paragraph(f"学员姓名:{student_name}") pdf.add_paragraph(f"每日练习时间:{content.get('practice_time', 'N/A')}") pdf.add_paragraph(f"生成时间:{content.get('generated_at', '')}") # 问题诊断 pdf.add_heading("🔍 问题诊断") for problem in content.get("problems", []): name = problem.get("name", "") severity = problem.get("severity", "") focus = problem.get("focus", {}) pdf.add_paragraph( f"• {name} ({severity}) - 基础练习: {focus.get('basic', 0)}分钟, 技术练习: {focus.get('tech', 0)}分钟" ) # AI报告(如果有) if content.get("ai_report"): pdf.add_heading("📝 AI个性化练习报告") # 将AI报告分段添加 for line in content["ai_report"].split("\n"): line = line.strip() if line.startswith("## "): pdf.add_heading(line.replace("## ", "")) elif line.startswith("# "): pdf.add_title(line.replace("# ", "")) elif line.startswith("- "): pdf.add_paragraph(line) elif line: pdf.add_paragraph(line) # 每日练习计划 pdf.add_heading("📅 每日练习计划") total_minutes = content.get("total_daily_minutes", 0) pdf.add_paragraph(f"总时长:{total_minutes} 分钟") # 表格 table_data = [["阶段", "时长", "内容", "目的"]] for item in content.get("daily_schedule", []): table_data.append( [ item.get("phase", ""), item.get("duration", ""), item.get("content", ""), item.get("purpose", ""), ] ) pdf.add_table(table_data) # 练习建议 pdf.add_heading("💡 练习建议") tips = [ "每天在固定时间练习,养成良好习惯", "练习前先热身,放松手指", "先难后易,先练习重点部分", "录音回听,发现问题", "保持耐心,进步需要时间", ] pdf.add_list(tips) # 生成PDF doc.build(pdf.elements) return output_path