#!/usr/bin/env python3 """ 生成 PDF 成绩报告 合并 REPORT.md、FRONTEND.md、成绩摘要,生成完整的 PDF 报告 使用 weasyprint 进行 HTML→PDF 转换 """ import argparse import json import os import re import sys from datetime import datetime from pathlib import Path try: import markdown from weasyprint import HTML, CSS HAS_PDF_SUPPORT = True except ImportError: HAS_PDF_SUPPORT = False def load_json(filepath, default=None): """安全加载 JSON 文件""" if not os.path.exists(filepath): return default or {} try: with open(filepath, "r", encoding="utf-8") as f: return json.load(f) except Exception as e: print(f"Error loading {filepath}: {e}", file=sys.stderr) return default or {} def read_file(filepath): """读取文件内容""" if os.path.exists(filepath): with open(filepath, "r", encoding="utf-8") as f: return f.read() return "" def generate_grade_summary(final_grade): """生成成绩摘要 Markdown""" total = final_grade.get("total_score", 0) max_score = final_grade.get("max_score", 100) breakdown = final_grade.get("breakdown", {}) lines = [ "# 📊 成绩报告", "", f"**生成时间**: {datetime.now().strftime('%Y-%m-%d %H:%M')}", "", f"## 总分: {total:.2f} / {max_score}", "", "### 分项成绩", "", "| 项目 | 得分 | 满分 |", "|------|------|------|", ] # 编程测试 prog = breakdown.get("programming", {}) if prog: lines.append(f"| 编程测试 | {prog.get('score', 0):.2f} | {prog.get('max_score', 80)} |") # REPORT.md report = breakdown.get("report", {}) if report: lines.append(f"| REPORT.md | {report.get('score', 0):.2f} | {report.get('max_score', 10)} |") # FRONTEND.md frontend = breakdown.get("frontend", {}) if frontend: lines.append(f"| FRONTEND.md | {frontend.get('score', 0):.2f} | {frontend.get('max_score', 10)} |") # 编程测试详情 if prog.get("groups"): lines.extend([ "", "### 编程测试详情", "", "| 分组 | 通过 | 总数 | 得分 | 满分 |", "|------|------|------|------|------|", ]) for group_name, group_info in prog["groups"].items(): lines.append( f"| {group_name} | {group_info.get('passed', 0)} | " f"{group_info.get('total', 0)} | {group_info.get('score', 0):.2f} | " f"{group_info.get('max_score', 0)} |" ) # LLM 评分详情 for section_key, section_title in [ ("report", "📝 REPORT.md 评分详情"), ("frontend", "🎨 FRONTEND.md 评分详情") ]: section = breakdown.get(section_key, {}) criteria = section.get("criteria", []) if criteria: lines.extend([ "", f"### {section_title}", "", "| 评分项 | 得分 | 评语 |", "|--------|------|------|", ]) for c in criteria: reason = c.get("reason", "").replace("|", "|").replace("\n", " ")[:80] lines.append(f"| {c.get('id', '')} | {c.get('score', 0)} | {reason} |") confidence = section.get("confidence") if confidence: lines.append(f"\n*置信度: {confidence:.2f}*") flags = section.get("flags", []) if flags: lines.append(f"\n*标记: {', '.join(flags)}*") lines.extend(["", "---", ""]) return "\n".join(lines) def fix_image_paths(content, images_dir): """修复图片路径为绝对路径(用于 PDF 生成)""" if not images_dir or not os.path.isdir(images_dir): return content abs_images_dir = os.path.abspath(images_dir) # 替换 Markdown 图片语法 def replace_img(match): alt = match.group(1) src = match.group(2) if not src.startswith(('http://', 'https://', '/')): # 相对路径,转为绝对路径 abs_src = os.path.join(abs_images_dir, os.path.basename(src)) if os.path.exists(abs_src): return f'' return match.group(0) content = re.sub(r'!\[([^\]]*)\]\(([^)]+)\)', replace_img, content) return content def create_combined_markdown(args, final_grade): """创建合并后的 Markdown 文件""" # 成绩摘要 grade_summary = generate_grade_summary(final_grade) # 读取学生报告 report_content = read_file(args.report) frontend_content = read_file(args.frontend) # 修复图片路径 frontend_content = fix_image_paths(frontend_content, args.images) # 合并内容 combined = [ grade_summary, "# 📝 后端开发反思报告", "", report_content.strip() if report_content.strip() else "*(未提交)*", "", "---", "", "# 🎨 前端开发反思报告", "", frontend_content.strip() if frontend_content.strip() else "*(未提交)*", ] return "\n".join(combined) def markdown_to_html(md_content): """将 Markdown 转换为 HTML""" extensions = ['tables', 'fenced_code', 'codehilite'] html_body = markdown.markdown(md_content, extensions=extensions) # 添加 CSS 样式 html = f'''
{html_body} ''' return html def convert_to_pdf(md_content, pdf_file, images_dir=None): """使用 weasyprint 将 Markdown 转换为 PDF""" if not HAS_PDF_SUPPORT: print("weasyprint not available, skipping PDF generation", file=sys.stderr) return False try: html_content = markdown_to_html(md_content) # 设置基础 URL 用于图片加载 base_url = os.path.abspath(images_dir) if images_dir else os.getcwd() HTML(string=html_content, base_url=base_url).write_pdf(pdf_file) return True except Exception as e: print(f"PDF generation error: {e}", file=sys.stderr) return False def main(): parser = argparse.ArgumentParser(description="Generate PDF grade report") parser.add_argument("--report", default="REPORT.md", help="REPORT.md file path") parser.add_argument("--frontend", default="FRONTEND.md", help="FRONTEND.md file path") parser.add_argument("--grade", default="final_grade.json", help="Final grade JSON file") parser.add_argument("--images", default="images", help="Images directory") parser.add_argument("--out", default="grade_report.pdf", help="Output PDF file") parser.add_argument("--student-id", default="", help="Student ID for filename") args = parser.parse_args() # 加载成绩 final_grade = load_json(args.grade, {"total_score": 0, "max_score": 100, "breakdown": {}}) # 创建合并的 Markdown combined_md = create_combined_markdown(args, final_grade) # 保存 Markdown 文件(始终保存,作为备份) md_out = args.out.replace(".pdf", ".md") with open(md_out, "w", encoding="utf-8") as f: f.write(combined_md) print(f"📝 Markdown report saved: {md_out}") # 尝试生成 PDF if HAS_PDF_SUPPORT: if convert_to_pdf(combined_md, args.out, args.images): print(f"✅ PDF report generated: {args.out}") return 0 else: print(f"⚠️ PDF generation failed, markdown saved as fallback", file=sys.stderr) return 0 else: print(f"ℹ️ PDF support not available (weasyprint not installed), markdown saved") return 0 if __name__ == "__main__": sys.exit(main())