""" pdf_export.py Generates formatted PDF exports for study notes and quiz results using reportlab. """ import io from datetime import date from reportlab.lib.pagesizes import A4 from reportlab.lib import colors from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.lib.units import mm from reportlab.platypus import ( SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, HRFlowable, KeepTogether ) from reportlab.lib.enums import TA_CENTER, TA_LEFT # ── Brand colours ───────────────────────────────────────────────────────────── PURPLE = colors.HexColor("#6c47ff") ORANGE = colors.HexColor("#f97316") LIGHT = colors.HexColor("#f7f4ef") MUTED = colors.HexColor("#6b6880") DARK = colors.HexColor("#1a1523") WHITE = colors.white GREEN = colors.HexColor("#10b981") RED = colors.HexColor("#ef4444") def _base_styles(): base = getSampleStyleSheet() styles = { "title": ParagraphStyle( "Title2", parent=base["Title"], fontSize=26, textColor=PURPLE, spaceAfter=4, fontName="Helvetica-Bold", alignment=TA_CENTER, ), "subtitle": ParagraphStyle( "Subtitle", parent=base["Normal"], fontSize=11, textColor=MUTED, spaceAfter=16, fontName="Helvetica", alignment=TA_CENTER, ), "section_head": ParagraphStyle( "SectionHead", parent=base["Heading2"], fontSize=13, textColor=PURPLE, spaceBefore=14, spaceAfter=6, fontName="Helvetica-Bold", ), "body": ParagraphStyle( "Body2", parent=base["Normal"], fontSize=10, textColor=DARK, spaceAfter=6, fontName="Helvetica", leading=15, ), "term": ParagraphStyle( "Term", parent=base["Normal"], fontSize=10, textColor=PURPLE, spaceAfter=2, fontName="Helvetica-Bold", ), "definition": ParagraphStyle( "Def", parent=base["Normal"], fontSize=9.5, textColor=MUTED, spaceAfter=8, fontName="Helvetica", leading=14, ), "summary_box": ParagraphStyle( "SummaryBox", parent=base["Normal"], fontSize=10, textColor=DARK, spaceAfter=6, fontName="Helvetica-Oblique", leading=15, ), "label": ParagraphStyle( "Label", parent=base["Normal"], fontSize=8, textColor=MUTED, spaceAfter=2, fontName="Helvetica", alignment=TA_CENTER, ), "score_big": ParagraphStyle( "ScoreBig", parent=base["Normal"], fontSize=40, textColor=PURPLE, spaceAfter=4, fontName="Helvetica-Bold", alignment=TA_CENTER, ), "feedback": ParagraphStyle( "Feedback", parent=base["Normal"], fontSize=12, textColor=DARK, spaceAfter=12, fontName="Helvetica-Bold", alignment=TA_CENTER, ), "q_text": ParagraphStyle( "QText", parent=base["Normal"], fontSize=10, textColor=DARK, spaceAfter=3, fontName="Helvetica-Bold", ), "q_detail": ParagraphStyle( "QDetail", parent=base["Normal"], fontSize=9.5, textColor=MUTED, spaceAfter=2, fontName="Helvetica", ), } return styles def export_study_notes_pdf(content: dict) -> bytes: """Generate a PDF for study notes. Returns bytes.""" buffer = io.BytesIO() doc = SimpleDocTemplate( buffer, pagesize=A4, leftMargin=20*mm, rightMargin=20*mm, topMargin=18*mm, bottomMargin=18*mm, ) s = _base_styles() story = [] # Header story.append(Paragraph("LearnCraft", s["title"])) story.append(Paragraph("Personalized Study Notes", s["subtitle"])) story.append(HRFlowable(width="100%", thickness=2, color=PURPLE, spaceAfter=12)) # Meta table meta_data = [ ["Topic", content.get("topic", "—"), "Level", content.get("level", "—")], ["Style", content.get("style", "—"), "Read Time", content.get("read_time", "—")], ["Generated", str(date.today()), "", ""], ] meta_table = Table(meta_data, colWidths=[30*mm, 65*mm, 30*mm, 50*mm]) meta_table.setStyle(TableStyle([ ("FONTNAME", (0,0), (-1,-1), "Helvetica"), ("FONTSIZE", (0,0), (-1,-1), 9), ("FONTNAME", (0,0), (0,-1), "Helvetica-Bold"), ("FONTNAME", (2,0), (2,-1), "Helvetica-Bold"), ("TEXTCOLOR", (0,0), (0,-1), PURPLE), ("TEXTCOLOR", (2,0), (2,-1), PURPLE), ("TEXTCOLOR", (1,0), (1,-1), DARK), ("TEXTCOLOR", (3,0), (3,-1), DARK), ("BACKGROUND", (0,0), (-1,-1), LIGHT), ("ROWBACKGROUNDS", (0,0), (-1,-1), [LIGHT, WHITE]), ("GRID", (0,0), (-1,-1), 0.5, colors.HexColor("#e2ddf5")), ("ROUNDEDCORNERS", [4]), ("TOPPADDING", (0,0), (-1,-1), 5), ("BOTTOMPADDING",(0,0), (-1,-1), 5), ("LEFTPADDING", (0,0), (-1,-1), 8), ])) story.append(meta_table) story.append(Spacer(1, 10)) # Sections for section in content.get("sections", []): story.append(Paragraph(f"▸ {section['title']}", s["section_head"])) story.append(HRFlowable(width="100%", thickness=0.5, color=colors.HexColor("#e2ddf5"), spaceAfter=6)) story.append(Paragraph(section["content"], s["body"])) # Key terms key_terms = content.get("key_terms", []) if key_terms: story.append(Spacer(1, 6)) story.append(Paragraph("Key Terms", s["section_head"])) story.append(HRFlowable(width="100%", thickness=0.5, color=colors.HexColor("#e2ddf5"), spaceAfter=8)) for term in key_terms: story.append(Paragraph(term["term"], s["term"])) story.append(Paragraph(term["definition"], s["definition"])) # Summary summary = content.get("summary", "") if summary: story.append(Spacer(1, 4)) story.append(Paragraph("Quick Summary", s["section_head"])) summary_table = Table([[Paragraph(f'"{summary}"', s["summary_box"])]], colWidths=[170*mm]) summary_table.setStyle(TableStyle([ ("BACKGROUND", (0,0), (-1,-1), colors.HexColor("#ede9fe")), ("LEFTPADDING", (0,0), (-1,-1), 12), ("RIGHTPADDING", (0,0), (-1,-1), 12), ("TOPPADDING", (0,0), (-1,-1), 10), ("BOTTOMPADDING",(0,0), (-1,-1), 10), ("ROUNDEDCORNERS", [8]), ])) story.append(summary_table) # Footer story.append(Spacer(1, 16)) story.append(HRFlowable(width="100%", thickness=1, color=colors.HexColor("#e2ddf5"), spaceAfter=6)) story.append(Paragraph(f"Generated by LearnCraft · {date.today()}", s["label"])) doc.build(story) return buffer.getvalue() def export_quiz_results_pdf(quiz: dict, results: dict, answers: dict) -> bytes: """Generate a PDF for quiz results. Returns bytes.""" buffer = io.BytesIO() doc = SimpleDocTemplate( buffer, pagesize=A4, leftMargin=20*mm, rightMargin=20*mm, topMargin=18*mm, bottomMargin=18*mm, ) s = _base_styles() story = [] # Header story.append(Paragraph("LearnCraft", s["title"])) story.append(Paragraph("Quiz Results Report", s["subtitle"])) story.append(HRFlowable(width="100%", thickness=2, color=PURPLE, spaceAfter=14)) # Score hero score = results["score_percent"] score_col = GREEN if score >= 60 else RED story.append(Paragraph(f"{score}%", ParagraphStyle( "BigScore", fontSize=48, textColor=score_col, fontName="Helvetica-Bold", alignment=TA_CENTER, spaceAfter=4, ))) story.append(Paragraph( f"{results['correct']} / {results['total']} correct · {results['feedback']}", s["feedback"] )) story.append(Spacer(1, 6)) # Meta meta_data = [ ["Topic", quiz.get("topic", "—"), "Difficulty", quiz.get("difficulty", "—")], ["Questions", str(results["total"]), "Date", str(date.today())], ] meta_table = Table(meta_data, colWidths=[30*mm, 65*mm, 30*mm, 50*mm]) meta_table.setStyle(TableStyle([ ("FONTNAME", (0,0), (-1,-1), "Helvetica"), ("FONTSIZE", (0,0), (-1,-1), 9), ("FONTNAME", (0,0), (0,-1), "Helvetica-Bold"), ("FONTNAME", (2,0), (2,-1), "Helvetica-Bold"), ("TEXTCOLOR", (0,0), (0,-1), PURPLE), ("TEXTCOLOR", (2,0), (2,-1), PURPLE), ("BACKGROUND", (0,0), (-1,-1), LIGHT), ("ROWBACKGROUNDS", (0,0), (-1,-1), [LIGHT, WHITE]), ("GRID", (0,0), (-1,-1), 0.5, colors.HexColor("#e2ddf5")), ("TOPPADDING", (0,0), (-1,-1), 5), ("BOTTOMPADDING",(0,0), (-1,-1), 5), ("LEFTPADDING", (0,0), (-1,-1), 8), ])) story.append(meta_table) story.append(Spacer(1, 14)) # Answer review story.append(Paragraph("Answer Review", s["section_head"])) story.append(HRFlowable(width="100%", thickness=0.5, color=colors.HexColor("#e2ddf5"), spaceAfter=8)) for i, q in enumerate(quiz.get("questions", [])): correct = results["details"][i]["correct"] user_ans = str(answers.get(i, "No answer")) right_ans = results["details"][i]["correct_answer"] explanation= results["details"][i].get("explanation", "") icon = "✓" if correct else "✗" bg_color = colors.HexColor("#d1fae5") if correct else colors.HexColor("#fee2e2") icon_color = GREEN if correct else RED block = [ [ Paragraph(f"{icon} Q{i+1}: {q['question']}", s["q_text"]), ], [ Paragraph( f"Your answer: {user_ans} | " f"Correct: {right_ans}" + (f"
{explanation}" if explanation else ""), s["q_detail"] ), ], ] t = Table(block, colWidths=[170*mm]) t.setStyle(TableStyle([ ("BACKGROUND", (0,0), (-1,-1), bg_color), ("LEFTPADDING", (0,0), (-1,-1), 10), ("RIGHTPADDING", (0,0), (-1,-1), 10), ("TOPPADDING", (0,0), (-1,-1), 8), ("BOTTOMPADDING", (0,0), (-1,-1), 8), ("ROUNDEDCORNERS", [6]), ])) story.append(KeepTogether([t, Spacer(1, 5)])) # Footer story.append(Spacer(1, 12)) story.append(HRFlowable(width="100%", thickness=1, color=colors.HexColor("#e2ddf5"), spaceAfter=6)) story.append(Paragraph(f"Generated by LearnCraft · {date.today()}", s["label"])) doc.build(story) return buffer.getvalue()