# modules/report_generator.py from reportlab.lib.pagesizes import A4 from reportlab.lib.units import inch from reportlab.pdfgen import canvas from reportlab.lib.colors import HexColor from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt from datetime import datetime import logging import os from typing import Dict, Any import arabic_reshaper from bidi.algorithm import get_display logger = logging.getLogger(__name__) class PDFReportGenerator: def __init__(self): self.primary_color = HexColor('#1E40AF') self.success_color = HexColor('#059669') self.warning_color = HexColor('#F59E0B') self.danger_color = HexColor('#DC2626') self.gray_color = HexColor('#6B7280') try: font_path = self._download_urdu_font() if font_path: pdfmetrics.registerFont(TTFont('UrduFont', font_path)) self.urdu_font_available = True logger.info("Urdu font registered successfully") else: self.urdu_font_available = False logger.warning("Urdu font not available") except Exception as e: logger.error(f"Font registration failed: {e}") self.urdu_font_available = False logger.info("PDFReportGenerator initialized") def _download_urdu_font(self): """Download Urdu font if not exists""" try: import requests font_dir = 'assets/fonts' os.makedirs(font_dir, exist_ok=True) font_path = os.path.join(font_dir, 'NotoNastaliqUrdu-Regular.ttf') if not os.path.exists(font_path): logger.info("Downloading Urdu font...") url = "https://github.com/googlefonts/noto-fonts/raw/main/hinted/ttf/NotoNastaliqUrdu/NotoNastaliqUrdu-Regular.ttf" response = requests.get(url, timeout=30) if response.status_code == 200: with open(font_path, 'wb') as f: f.write(response.content) logger.info("Urdu font downloaded successfully") return font_path else: return font_path except Exception as e: logger.error(f"Font download failed: {e}") return None def _format_urdu_text(self, text): """Format Urdu text for proper display""" if not self.urdu_font_available: return text try: reshaped_text = arabic_reshaper.reshape(text) bidi_text = get_display(reshaped_text) return bidi_text except Exception as e: logger.error(f"Urdu text formatting failed: {e}") return text def create_report(self, student_name: str, grade_level: int, essay_type: str, grading_result: Dict[str, Any], output_path: str) -> str: try: logger.info(f"Creating PDF report: {output_path}") c = canvas.Canvas(output_path, pagesize=A4) width, height = A4 self._create_cover_page(c, width, height, student_name, grade_level, essay_type, grading_result) c.showPage() self._create_scores_page(c, width, height, grading_result) c.showPage() self._create_feedback_pages(c, width, height, grading_result) c.save() logger.info("PDF report created successfully") return output_path except Exception as e: logger.exception("Failed to create PDF") raise def _create_cover_page(self, c, width, height, student_name, grade_level, essay_type, result): c.setFillColor(self.primary_color) c.rect(0, height - 80, width, 80, fill=1, stroke=0) c.setFillColorRGB(1, 1, 1) c.setFont('Helvetica-Bold', 28) c.drawCentredString(width/2, height - 50, "Essay Grading Report") c.setFont('Helvetica', 12) today = datetime.now().strftime("%B %d, %Y") c.drawCentredString(width/2, height - 70, today) c.setFillColorRGB(0, 0, 0) y = height - 150 c.setFont('Helvetica-Bold', 16) c.drawString(100, y, "Student Details:") c.setFont('Helvetica', 14) c.drawString(100, y - 30, f"Name: {student_name}") c.drawString(100, y - 55, f"Grade: {grade_level}") c.drawString(100, y - 80, f"Essay Type: {essay_type}") score = result['total_score'] grade = result['grade'] if score >= 80: color = self.success_color elif score >= 60: color = self.warning_color else: color = self.danger_color c.setFillColor(color) c.setFont('Helvetica-Bold', 72) c.drawCentredString(width/2, height/2 + 20, f"{int(score)}") c.setFont('Helvetica', 24) c.setFillColor(self.gray_color) c.drawCentredString(width/2, height/2 - 25, "/ 100") c.setFont('Helvetica-Bold', 48) c.setFillColor(color) c.drawCentredString(width/2, height/2 - 90, f"Grade: {grade}") c.setFillColor(self.gray_color) c.setFont('Helvetica-Oblique', 10) c.drawCentredString(width/2, 50, "Generated by Essay Grader AI | Powered by Groq") c.setFont('Helvetica-Oblique', 9) c.drawCentredString(width/2, 35, "Developed by: Najaf Ali Sharqi") def _create_scores_page(self, c, width, height, result): c.setFillColor(self.primary_color) c.setFont('Helvetica-Bold', 24) c.drawString(50, height - 60, "Detailed Score Breakdown") try: chart_path = self._create_bar_chart(result['scores']) if os.path.exists(chart_path): c.drawImage(chart_path, 50, height - 400, width=500, height=300, preserveAspectRatio=True) os.remove(chart_path) except Exception as e: logger.error(f"Chart creation failed: {e}") y = height - 450 c.setFont('Helvetica-Bold', 14) c.setFillColorRGB(0, 0, 0) c.drawString(50, y, "Category") c.drawString(250, y, "Score") c.drawString(350, y, "Max") c.drawString(450, y, "Percentage") c.setStrokeColor(self.primary_color) c.setLineWidth(2) c.line(50, y - 5, width - 50, y - 5) categories = { 'content': ('Content & Arguments', 40), 'organization': ('Organization', 20), 'language': ('Language & Style', 20), 'grammar': ('Grammar & Spelling', 20) } c.setFont('Helvetica', 12) y -= 30 for key, (name, max_score) in categories.items(): score = result['scores'].get(key, 0) percentage = (score / max_score) * 100 if max_score > 0 else 0 if percentage >= 80: color = self.success_color elif percentage >= 60: color = self.warning_color else: color = self.danger_color c.setFillColorRGB(0, 0, 0) c.drawString(50, y, name) c.setFillColor(color) c.setFont('Helvetica-Bold', 12) c.drawString(250, y, str(int(score))) c.setFillColorRGB(0, 0, 0) c.setFont('Helvetica', 12) c.drawString(350, y, str(max_score)) c.drawString(450, y, f"{percentage:.1f}%") y -= 30 y -= 20 c.setFillColor(HexColor('#F3F4F6')) c.roundRect(50, y - 80, width - 100, 70, 10, fill=1) c.setFillColor(self.primary_color) c.setFont('Helvetica-Bold', 16) c.drawString(70, y - 30, "Overall Performance:") c.setFont('Helvetica', 14) total = result['total_score'] if total >= 80: performance = "Excellent Work!" elif total >= 60: performance = "Good Job!" elif total >= 40: performance = "Fair - Keep Improving" else: performance = "Needs Significant Improvement" c.drawString(70, y - 55, f"• {performance}") def _create_feedback_pages(self, c, width, height, result): y = height - 60 c.setFillColor(self.primary_color) c.setFont('Helvetica-Bold', 24) c.drawString(50, y, "Detailed Feedback") y -= 40 # Strengths c.setFillColor(self.success_color) c.setFont('Helvetica-Bold', 16) c.drawString(50, y, "Strengths") y -= 25 c.setFillColorRGB(0, 0, 0) strengths = result.get('strengths', []) if strengths: for i, strength in enumerate(strengths[:5], 1): if y < 100: c.showPage() y = height - 60 c.setFillColor(self.success_color) c.circle(60, y + 3, 4, fill=1) c.setFillColorRGB(0, 0, 0) if self.urdu_font_available and self._contains_urdu(strength): c.setFont('UrduFont', 11) formatted_text = self._format_urdu_text(f"{i}. {strength}") self._draw_wrapped_text_rtl(c, formatted_text, 75, y, width - 140, 'UrduFont', 11) else: c.setFont('Helvetica', 11) text = f"{i}. {strength}" self._draw_wrapped_text(c, text, 75, y, width - 140, 'Helvetica', 11) y -= 25 y -= 20 # Areas for Improvement if y < 150: c.showPage() y = height - 60 c.setFillColor(self.warning_color) c.setFont('Helvetica-Bold', 16) c.drawString(50, y, "Areas for Improvement") y -= 25 weaknesses = result.get('weaknesses', []) if weaknesses: for weakness in weaknesses[:5]: if y < 150: c.showPage() y = height - 60 c.setFillColor(self.warning_color) c.circle(60, y + 3, 4, fill=1) issue = weakness.get('issue', '') c.setFillColorRGB(0, 0, 0) if self.urdu_font_available and self._contains_urdu(issue): c.setFont('UrduFont', 11) formatted_issue = self._format_urdu_text(issue) self._draw_wrapped_text_rtl(c, formatted_issue, 75, y, width - 140, 'UrduFont', 11) else: c.setFont('Helvetica-Bold', 11) self._draw_wrapped_text(c, issue, 75, y, width - 140, 'Helvetica-Bold', 11) y -= 20 if 'suggestion' in weakness and weakness['suggestion']: suggestion = weakness['suggestion'] c.setFillColor(self.success_color) if self.urdu_font_available and self._contains_urdu(suggestion): c.setFont('UrduFont', 10) formatted_sugg = self._format_urdu_text(f"→ {suggestion}") self._draw_wrapped_text_rtl(c, formatted_sugg, 75, y, width - 140, 'UrduFont', 10) else: c.setFont('Helvetica', 10) self._draw_wrapped_text(c, f"→ {suggestion}", 75, y, width - 140, 'Helvetica', 10) y -= 20 y -= 10 # Overall Feedback if y < 200: c.showPage() y = height - 60 y -= 20 c.setFillColor(self.primary_color) c.setFont('Helvetica-Bold', 16) c.drawString(50, y, "Overall Feedback") y -= 25 c.setFillColorRGB(0, 0, 0) feedback = result.get('overall_feedback', 'Great effort!') if self.urdu_font_available and self._contains_urdu(feedback): c.setFont('UrduFont', 11) formatted_feedback = self._format_urdu_text(feedback) self._draw_wrapped_text_rtl(c, formatted_feedback, 60, y, width - 120, 'UrduFont', 11) else: c.setFont('Helvetica', 11) self._draw_wrapped_text(c, feedback, 60, y, width - 120, 'Helvetica', 11) def _contains_urdu(self, text): """Check if text contains Urdu characters""" if not text: return False return any('\u0600' <= char <= '\u06FF' for char in text) def _draw_wrapped_text(self, c, text, x, y, max_width, font, size): """Draw text with word wrapping (LTR)""" words = text.split() line = "" for word in words: test_line = line + word + " " if c.stringWidth(test_line, font, size) < max_width: line = test_line else: c.drawString(x, y, line.strip()) y -= size + 3 line = word + " " if line.strip(): c.drawString(x, y, line.strip()) def _draw_wrapped_text_rtl(self, c, text, x, y, max_width, font, size): """Draw text with word wrapping (RTL for Urdu)""" words = text.split() line = "" for word in words: test_line = word + " " + line if c.stringWidth(test_line, font, size) < max_width: line = test_line else: c.drawRightString(x + max_width, y, line.strip()) y -= size + 3 line = word + " " if line.strip(): c.drawRightString(x + max_width, y, line.strip()) def _create_bar_chart(self, scores: Dict[str, int]) -> str: fig, ax = plt.subplots(figsize=(10, 6)) categories = { 'content': ('Content', 40), 'organization': ('Organization', 20), 'language': ('Language', 20), 'grammar': ('Grammar', 20) } names = [v[0] for v in categories.values()] obtained = [scores.get(k, 0) for k in categories.keys()] maximum = [v[1] for v in categories.values()] x = range(len(names)) width_bar = 0.35 bars1 = ax.bar([i - width_bar/2 for i in x], obtained, width_bar, label='Obtained', color='#1E40AF') bars2 = ax.bar([i + width_bar/2 for i in x], maximum, width_bar, label='Maximum', color='#E5E7EB', alpha=0.5) ax.set_xlabel('Category', fontsize=12, fontweight='bold') ax.set_ylabel('Score', fontsize=12, fontweight='bold') ax.set_title('Score Breakdown', fontsize=14, fontweight='bold') ax.set_xticks(x) ax.set_xticklabels(names) ax.legend() ax.grid(axis='y', alpha=0.3) for bar in bars1: height = bar.get_height() ax.text(bar.get_x() + bar.get_width()/2., height, f'{int(height)}', ha='center', va='bottom', fontweight='bold') plt.tight_layout() chart_path = 'outputs/temp_chart.png' plt.savefig(chart_path, format='png', dpi=150, bbox_inches='tight') plt.close() return chart_path