Spaces:
Sleeping
Sleeping
import logging | |
logger = logging.getLogger(__name__) | |
logger.info("Importing PDFFeedbackGenerator.py...") | |
from reportlab.lib.pagesizes import A4 | |
from reportlab.pdfgen import canvas | |
from reportlab.lib.utils import ImageReader | |
from reportlab.lib.colors import black, yellow, Color | |
from reportlab.pdfbase import pdfmetrics | |
from reportlab.pdfbase.ttfonts import TTFont | |
from reportlab.lib.styles import ParagraphStyle | |
from reportlab.platypus import Paragraph | |
from reportlab.lib.enums import TA_LEFT | |
import json | |
import re | |
import os | |
def wrap_text(text, max_width, canvas_obj, font_name, font_size): | |
"""Wrap text for PDF output.""" | |
words = text.split() | |
lines = [] | |
current_line = "" | |
for word in words: | |
test_line = f"{current_line} {word}".strip() | |
if canvas_obj.stringWidth(test_line, font_name, font_size) <= max_width: | |
current_line = test_line | |
else: | |
lines.append(current_line) | |
current_line = word | |
if current_line: | |
lines.append(current_line) | |
return lines | |
def highlight_examples(text): | |
"""Extract and format examples from text using quotes or parentheses.""" | |
examples = [] | |
# Match text within single quotes | |
quote_pattern = r"'([^']+)'" | |
examples.extend([f"'{match}'" for match in re.findall(quote_pattern, text)]) | |
# Match text within parentheses | |
paren_pattern = r'\(([^)]+)\)' | |
examples.extend([f"({match})" for match in re.findall(paren_pattern, text)]) | |
return examples | |
class PDFFeedbackGenerator: | |
def __init__(self, output_path, logo_path): | |
logger.info("Initializing PDFFeedbackGenerator...") | |
self.output_path = output_path | |
self.logo_path = logo_path | |
# Define colors for text and highlighting | |
self.colors = { | |
'normal': black, | |
'highlight': Color(1, 1, 0, alpha=0.3) # Semi-transparent yellow for highlighting | |
} | |
# Register and define standard fonts | |
self.register_fonts() | |
self.fonts = { | |
'title': 'Helvetica-Bold', | |
'heading': 'Helvetica-Bold', | |
'normal': 'Helvetica', | |
'quote': 'Helvetica' | |
} | |
def register_fonts(self): | |
"""Register the required fonts for the PDF.""" | |
try: | |
# Register the basic Helvetica fonts that come with ReportLab | |
pdfmetrics.registerFont(pdfmetrics.Font('Helvetica', 'Helvetica', 'WinAnsiEncoding')) | |
pdfmetrics.registerFont(pdfmetrics.Font('Helvetica-Bold', 'Helvetica-Bold', 'WinAnsiEncoding')) | |
except Exception as e: | |
print(f"Warning: Could not register fonts: {e}") | |
# Fallback to basic fonts if registration fails | |
self.fonts = { | |
'title': 'Helvetica', | |
'heading': 'Helvetica', | |
'normal': 'Helvetica', | |
'quote': 'Helvetica' | |
} | |
def draw_highlighted_text(self, canvas_obj, text, x, y, font_name, font_size, max_width, page_height=None, margin=None, header_func=None): | |
"""Draw text with selective highlighting for quotes and parentheses, with page break support.""" | |
words = text.split() | |
current_line = "" | |
current_x = x | |
y_pos = y | |
line_height = font_size * 1.2 | |
# Find all highlighted segments (text in quotes or parentheses) | |
segments = [] | |
last_end = 0 | |
pattern = r"'[^']+'|\([^)]+\)" | |
for match in re.finditer(pattern, text): | |
if last_end < match.start(): | |
segments.append(('normal', text[last_end:match.start()])) | |
segments.append(('highlight', match.group())) | |
last_end = match.end() | |
if last_end < len(text): | |
segments.append(('normal', text[last_end:])) | |
for segment_type, segment_text in segments: | |
words = segment_text.split() | |
for word in words: | |
test_line = f"{current_line} {word}".strip() | |
test_width = canvas_obj.stringWidth(test_line, font_name, font_size) | |
if test_width <= max_width: | |
current_line = test_line | |
else: | |
# PAGE BREAK LOGIC | |
if page_height and margin and y_pos - line_height < margin: | |
canvas_obj.showPage() | |
if header_func: | |
y_pos = header_func(canvas_obj) | |
else: | |
y_pos = page_height - margin | |
current_x = x | |
# Draw the current line with proper highlighting | |
if current_line: | |
if segment_type == 'highlight': | |
text_width = canvas_obj.stringWidth(current_line, font_name, font_size) | |
canvas_obj.setFillColor(self.colors['highlight']) | |
canvas_obj.rect(current_x, y_pos - font_size + 2, text_width, font_size, fill=1, stroke=0) | |
canvas_obj.setFillColor(self.colors['normal']) | |
canvas_obj.drawString(current_x, y_pos, current_line) | |
y_pos -= line_height | |
current_x = x | |
current_line = word | |
else: | |
current_line = word | |
# Draw the last line if any | |
if current_line: | |
if page_height and margin and y_pos - line_height < margin: | |
canvas_obj.showPage() | |
if header_func: | |
y_pos = header_func(canvas_obj) | |
else: | |
y_pos = page_height - margin | |
current_x = x | |
if segment_type == 'highlight': | |
text_width = canvas_obj.stringWidth(current_line, font_name, font_size) | |
canvas_obj.setFillColor(self.colors['highlight']) | |
canvas_obj.rect(current_x, y_pos - font_size + 2, text_width, font_size, fill=1, stroke=0) | |
canvas_obj.setFillColor(self.colors['normal']) | |
canvas_obj.drawString(current_x, y_pos, current_line) | |
y_pos -= line_height | |
return y_pos | |
def draw_color_scheme_legend(self, canvas_obj, x, y): | |
"""Removed color scheme legend as per new requirements""" | |
pass | |
def draw_rephrased_section(self, canvas_obj, text_analysis, x_pos, y_pos, max_width): | |
"""Draw the rephrased text section with highlights for mistakes and synonyms.""" | |
original_y = y_pos | |
# Section Title | |
canvas_obj.setFont(self.fonts['heading'], 14) | |
canvas_obj.setFillColor(self.colors['normal']) | |
canvas_obj.drawString(x_pos, y_pos, "Rephrased Text") | |
y_pos -= 25 | |
# Draw rephrased text | |
canvas_obj.setFont(self.fonts['normal'], 12) | |
y_pos = self.draw_highlighted_text( | |
canvas_obj, | |
text_analysis['rephrased_text'], | |
x_pos, | |
y_pos, | |
self.fonts['normal'], | |
12, | |
max_width | |
) | |
y_pos -= 20 | |
# Grammar Mistakes Section | |
canvas_obj.setFont(self.fonts['heading'], 12) | |
canvas_obj.setFillColor(self.colors['normal']) | |
canvas_obj.drawString(x_pos, y_pos, "Grammar Corrections:") | |
y_pos -= 20 | |
canvas_obj.setFont(self.fonts['normal'], 11) | |
for mistake in text_analysis['grammar_mistakes']: | |
text = f"• '{mistake['original']}' → '{mistake['correction']}'" | |
y_pos = self.draw_highlighted_text( | |
canvas_obj, | |
text, | |
x_pos + 15, | |
y_pos, | |
self.fonts['normal'], | |
11, | |
max_width - 20 | |
) | |
y_pos -= 15 | |
# Synonyms Section | |
canvas_obj.setFont(self.fonts['heading'], 12) | |
canvas_obj.setFillColor(self.colors['normal']) | |
canvas_obj.drawString(x_pos, y_pos, "Suggested Synonyms:") | |
y_pos -= 20 | |
canvas_obj.setFont(self.fonts['normal'], 11) | |
for syn in text_analysis['synonyms']: | |
text = f"• '{syn['original']}' → {', '.join(syn['suggestions'])}" | |
y_pos = self.draw_highlighted_text( | |
canvas_obj, | |
text, | |
x_pos + 15, | |
y_pos, | |
self.fonts['normal'], | |
11, | |
max_width - 20 | |
) | |
return y_pos | |
def draw_section_header(self, canvas_obj, text, x, y, font_name, font_size): | |
"""Draw a section header with improved styling.""" | |
canvas_obj.setFont(font_name, font_size) | |
canvas_obj.setFillColor(self.colors['normal']) | |
# Draw the header text | |
canvas_obj.drawString(x, y, text) | |
# Draw a decorative line under the header | |
text_width = canvas_obj.stringWidth(text, font_name, font_size) | |
canvas_obj.setLineWidth(1.5) | |
canvas_obj.line(x, y - 3, x + text_width, y - 3) | |
canvas_obj.setLineWidth(1) | |
return y - font_size - 20 # Increased spacing after header | |
def draw_quote(self, canvas_obj, text, x, y, max_width, font_name, font_size): | |
"""Draw a quote with professional styling.""" | |
# Draw quote background | |
canvas_obj.setFillColor(Color(0.95, 0.97, 1.0)) | |
quote_height = font_size * 3 # Approximate height for quote | |
canvas_obj.rect(x - 5, y - quote_height, max_width + 10, quote_height, fill=1, stroke=0) | |
# Draw quote text | |
canvas_obj.setFont(font_name, font_size) | |
canvas_obj.setFillColor(self.colors['normal']) | |
y_pos = self.draw_highlighted_text(canvas_obj, f'"{text}"', x, y, font_name, font_size, max_width) | |
# Draw quote decoration | |
canvas_obj.setStrokeColor(Color(0.4, 0.6, 0.8)) | |
# Save current line width | |
current_line_width = canvas_obj._lineWidth | |
# Set new line width for quote decoration | |
canvas_obj.setLineWidth(2) | |
canvas_obj.line(x - 5, y - quote_height/2, x - 5, y - 5) | |
# Restore original line width | |
canvas_obj.setLineWidth(current_line_width) | |
return y_pos - 10 | |
def draw_feedback_item(self, canvas_obj, text, x, y, max_width, font_name, font_size, indent=0): | |
"""Draw a feedback item with improved formatting and alignment.""" | |
bullet = "•" | |
if indent > 0: | |
x += indent | |
bullet = "◦" | |
# Draw the bullet point with proper spacing | |
canvas_obj.setFont(font_name, font_size) | |
canvas_obj.drawString(x, y, bullet) | |
# Draw the text with improved indentation and alignment | |
text_x = x + canvas_obj.stringWidth(bullet + " ", font_name, font_size) | |
y_pos = self.draw_highlighted_text( | |
canvas_obj, | |
text, | |
text_x, | |
y, | |
font_name, | |
font_size, | |
max_width - (text_x - x) | |
) | |
return y_pos - 5 # Add small spacing between items | |
def draw_boxed_section(self, canvas_obj, x, y, width, height, fill_color=Color(0.97, 0.97, 0.97), stroke_color=Color(0.8, 0.8, 0.8)): | |
"""Draw a box (card) for a section.""" | |
canvas_obj.saveState() | |
# Draw the box | |
canvas_obj.setFillColor(fill_color) | |
canvas_obj.setStrokeColor(stroke_color) | |
canvas_obj.setLineWidth(1) | |
canvas_obj.roundRect(x, y - height, width, height, 8, fill=1, stroke=1) | |
# Set up clipping using a path | |
p = canvas_obj.beginPath() | |
p.roundRect(x, y - height, width, height, 8) | |
canvas_obj.clipPath(p, stroke=0, fill=0) | |
canvas_obj.restoreState() | |
def create_rephrased_pdf(self, user_name, question, rephrased_analysis): | |
"""Generate PDF report for rephrased text with professional formatting and layout (matching feedback PDF style).""" | |
try: | |
c = canvas.Canvas(self.output_path, pagesize=A4) | |
width, height = A4 | |
margin = 50 # Match feedback PDF margin | |
line_height = 14 | |
section_spacing = 20 | |
max_width = width - 2 * margin | |
page_number = 1 | |
def draw_header(canvas_obj, is_first_page=False): | |
nonlocal y_position | |
canvas_obj.setFillColor(self.colors['normal']) | |
# Page number | |
canvas_obj.setFont(self.fonts['normal'], 9) | |
page_text = f"Page {page_number}" | |
page_width = canvas_obj.stringWidth(page_text, self.fonts['normal'], 9) | |
canvas_obj.drawString(width - margin - page_width, height - 25, page_text) | |
if is_first_page: | |
# Logo | |
if os.path.exists(self.logo_path): | |
try: | |
logo = ImageReader(self.logo_path) | |
logo_width = 60 | |
logo_height = 60 | |
canvas_obj.drawImage(logo, margin, height - 80, width=logo_width, height=logo_height, mask='auto') | |
except Exception as e: | |
print(f"Logo loading error: {e}") | |
# Title | |
canvas_obj.setFont(self.fonts['title'], 20) | |
title_text = "CSS Essay Rephrased Text Report" | |
title_width = canvas_obj.stringWidth(title_text, self.fonts['title'], 20) | |
canvas_obj.drawString(margin + 80, height - 80, title_text) | |
canvas_obj.setStrokeColor(self.colors['normal']) | |
canvas_obj.setLineWidth(1.5) | |
canvas_obj.line(margin + 80, height - 85, margin + 80 + title_width, height - 85) | |
canvas_obj.setLineWidth(1) | |
# Student name | |
y_position = height - 120 | |
canvas_obj.setFont(self.fonts['heading'], 14) | |
canvas_obj.drawString(margin, y_position, f"Student Name: {user_name}") | |
else: | |
y_position = height - margin | |
return y_position | |
def check_new_page(y_pos, required_space): | |
nonlocal y_position, page_number | |
if y_pos - required_space < margin + 40: | |
c.showPage() | |
page_number += 1 | |
y_position = draw_header(c, False) | |
return True | |
return False | |
# Draw first page header | |
y_position = draw_header(c, True) | |
# Rephrased Text Section | |
y_position -= section_spacing | |
y_position = self.draw_section_header(c, "Rephrased Text", margin, y_position, self.fonts['heading'], 16) | |
y_position -= 5 | |
c.setFont(self.fonts['normal'], 12) | |
y_position = self.draw_highlighted_text( | |
c, | |
rephrased_analysis['rephrased_text'], | |
margin, | |
y_position, | |
self.fonts['normal'], | |
12, | |
max_width - margin, | |
page_height=height, | |
margin=margin, | |
header_func=lambda canv: draw_header(canv, False) | |
) | |
y_position -= section_spacing | |
# Grammar Corrections Section | |
c.setFont(self.fonts['heading'], 12) | |
c.drawString(margin, y_position, "Grammar Corrections:") | |
y_position -= line_height | |
c.setFont(self.fonts['normal'], 10) | |
for mistake in rephrased_analysis['grammar_mistakes']: | |
text = f"• '{mistake['original']}' → '{mistake['correction']}'" | |
y_position = self.draw_highlighted_text( | |
c, text, margin + 15, y_position, self.fonts['normal'], 10, max_width - 20, | |
page_height=height, margin=margin, header_func=lambda canv: draw_header(canv, False) | |
) | |
y_position -= section_spacing | |
# Synonyms Section | |
c.setFont(self.fonts['heading'], 12) | |
c.drawString(margin, y_position, "Suggested Synonyms:") | |
y_position -= line_height | |
c.setFont(self.fonts['normal'], 10) | |
for syn in rephrased_analysis['synonyms']: | |
text = f"• '{syn['original']}' → {', '.join(syn['suggestions'])}" | |
y_position = self.draw_highlighted_text( | |
c, text, margin + 15, y_position, self.fonts['normal'], 10, max_width - 20, | |
page_height=height, margin=margin, header_func=lambda canv: draw_header(canv, False) | |
) | |
c.save() | |
return True | |
except Exception as e: | |
print(f"Error generating Rephrased PDF: {str(e)}") | |
raise | |
def create_feedback_pdf(self, user_name, question, feedback): | |
"""Generate PDF report with the new AI Evaluation & Score structure.""" | |
try: | |
c = canvas.Canvas(self.output_path, pagesize=A4) | |
width, height = A4 | |
margin = 50 | |
line_height = 14 | |
section_spacing = 20 | |
max_width = width - 2 * margin | |
page_number = 1 | |
def draw_header(canvas_obj, is_first_page=False): | |
nonlocal y_position | |
canvas_obj.setFillColor(self.colors['normal']) | |
# Page number | |
canvas_obj.setFont(self.fonts['normal'], 9) | |
page_text = f"Page {page_number}" | |
page_width = canvas_obj.stringWidth(page_text, self.fonts['normal'], 9) | |
canvas_obj.drawString(width - margin - page_width, height - 25, page_text) | |
if is_first_page: | |
# Logo | |
if os.path.exists(self.logo_path): | |
try: | |
logo = ImageReader(self.logo_path) | |
logo_width = 60 | |
logo_height = 60 | |
canvas_obj.drawImage(logo, margin, height - 80, width=logo_width, height=logo_height, mask='auto') | |
except Exception as e: | |
print(f"Logo loading error: {e}") | |
# Title | |
canvas_obj.setFont(self.fonts['title'], 20) | |
title_text = "AI Evaluation & Score Report" | |
title_width = canvas_obj.stringWidth(title_text, self.fonts['title'], 20) | |
canvas_obj.drawString(margin + 80, height - 80, title_text) | |
canvas_obj.setStrokeColor(self.colors['normal']) | |
canvas_obj.setLineWidth(1.5) | |
canvas_obj.line(margin + 80, height - 85, margin + 80 + title_width, height - 85) | |
canvas_obj.setLineWidth(1) | |
# Overall Score | |
overall_score = feedback.get('overall_score', 40) | |
canvas_obj.setFont(self.fonts['title'], 22) | |
canvas_obj.setFillColor(Color(0.16, 0.52, 0.96)) | |
score_text = f"Overall Essay Evaluation: {overall_score}%" | |
canvas_obj.drawString(margin, height - 110, score_text) | |
canvas_obj.setFillColor(self.colors['normal']) | |
# Student name | |
y_position = height - 140 | |
canvas_obj.setFont(self.fonts['heading'], 14) | |
canvas_obj.drawString(margin, y_position, f"Student Name: {user_name}") | |
else: | |
y_position = height - margin | |
return y_position | |
def check_new_page(y_pos, required_space): | |
nonlocal y_position, page_number | |
if y_pos - required_space < margin + 40: | |
c.showPage() | |
page_number += 1 | |
y_position = draw_header(c, False) | |
return True | |
return False | |
# Draw first page header | |
y_position = draw_header(c, True) | |
# Draw Essay Structure section after overall score | |
essay_structure = feedback.get('essay_structure', {}) | |
y_position -= 30 | |
c.setFont(self.fonts['heading'], 15) | |
c.drawString(margin, y_position, 'Essay Structure') | |
y_position -= 20 | |
for section, criteria in essay_structure.items(): | |
c.setFont(self.fonts['heading'], 13) | |
c.drawString(margin, y_position, section) | |
y_position -= 16 | |
for crit, result in criteria.items(): | |
passed = result.get('value', False) | |
explanation = result.get('explanation', '') | |
icon = '✔' if passed else '✘' | |
color = Color(0.16, 0.7, 0.3) if passed else Color(0.9, 0.2, 0.2) | |
c.setFont(self.fonts['normal'], 11) | |
c.setFillColor(color) | |
c.drawString(margin + 15, y_position, f'{icon} {crit}') | |
y_position -= 14 | |
if not passed and explanation: | |
c.setFont(self.fonts['normal'], 10) | |
c.setFillColor(Color(0.9, 0.2, 0.2)) | |
c.drawString(margin + 35, y_position, explanation) | |
y_position -= 13 | |
c.setFillColor(self.colors['normal']) | |
y_position -= 8 | |
# Process each feedback section (AI Evaluation & Score) | |
for section in feedback["sections"]: | |
if check_new_page(y_position, section_spacing + 80): | |
pass | |
else: | |
y_position -= section_spacing | |
# Draw section header | |
c.setFont(self.fonts['heading'], 14) | |
c.setFillColor(self.colors['normal']) | |
heading_text = section['name'] | |
c.drawString(margin, y_position, heading_text) | |
text_width = c.stringWidth(heading_text, self.fonts['heading'], 14) | |
c.setLineWidth(1.5) | |
c.line(margin, y_position - 3, margin + text_width, y_position - 3) | |
c.setLineWidth(1) | |
# Draw score as percentage | |
c.setFont(self.fonts['normal'], 12) | |
c.drawString(margin + text_width + 20, y_position, f"Score: {section.get('score', 0)}%") | |
y_position -= line_height + 8 | |
# Draw number of issues | |
num_issues = len(section.get('issues', [])) | |
c.setFont(self.fonts['normal'], 11) | |
c.drawString(margin, y_position, f"{num_issues} Issue{'s' if num_issues != 1 else ''}") | |
y_position -= line_height | |
# Draw issues (before/after) | |
for issue in section.get('issues', []): | |
before = issue.get('before', '') | |
after = issue.get('after', '') | |
c.setFont(self.fonts['normal'], 11) | |
c.setFillColor(Color(0.9, 0.4, 0.4)) | |
c.drawString(margin + 15, y_position, f"Before: {before}") | |
y_position -= line_height | |
c.setFillColor(Color(0.3, 0.6, 0.3)) | |
c.drawString(margin + 15, y_position, f"After: {after}") | |
y_position -= line_height + 2 | |
c.setFillColor(self.colors['normal']) | |
y_position -= 8 | |
c.save() | |
return True | |
except Exception as e: | |
print(f"Error generating Feedback PDF: {str(e)}") | |
raise | |
def wrap_text(self, text, max_width, canvas_obj, font_name, font_size): | |
"""Wrap text with error handling""" | |
try: | |
words = text.split() | |
lines = [] | |
current_line = "" | |
for word in words: | |
test_line = f"{current_line} {word}".strip() | |
if canvas_obj.stringWidth(test_line, font_name, font_size) <= max_width: | |
current_line = test_line | |
else: | |
lines.append(current_line) | |
current_line = word | |
if current_line: | |
lines.append(current_line) | |
return lines | |
except Exception as e: | |
print(f"Warning: Error in text wrapping. Using basic wrap. Error: {e}") | |
# Fallback to basic wrapping | |
return [text[i:i+100] for i in range(0, len(text), 100)] | |
def draw_justified_text(self, canvas_obj, text, x, y, font_name, font_size, max_width): | |
"""Draw justified text (left and right aligned) for PDF feedback items.""" | |
words = text.split() | |
lines = [] | |
current_line = [] | |
current_width = 0 | |
space_width = canvas_obj.stringWidth(' ', font_name, font_size) | |
for word in words: | |
word_width = canvas_obj.stringWidth(word, font_name, font_size) | |
if current_width + word_width + (len(current_line) * space_width) <= max_width: | |
current_line.append(word) | |
current_width += word_width | |
else: | |
lines.append(current_line) | |
current_line = [word] | |
current_width = word_width | |
if current_line: | |
lines.append(current_line) | |
line_height = font_size * 1.2 | |
y_pos = y | |
for i, line_words in enumerate(lines): | |
if len(line_words) == 1 or i == len(lines) - 1: | |
# Last line or single word: left align | |
canvas_obj.drawString(x, y_pos, ' '.join(line_words)) | |
else: | |
total_words_width = sum(canvas_obj.stringWidth(w, font_name, font_size) for w in line_words) | |
total_spaces = len(line_words) - 1 | |
if total_spaces > 0: | |
space = (max_width - total_words_width) / total_spaces | |
else: | |
space = space_width | |
x_pos = x | |
for j, word in enumerate(line_words): | |
canvas_obj.drawString(x_pos, y_pos, word) | |
word_width = canvas_obj.stringWidth(word, font_name, font_size) | |
x_pos += word_width | |
if j < len(line_words) - 1: | |
x_pos += space | |
y_pos -= line_height | |
return y_pos | |
def create_unlimited_feedback_pdf(self, user_name, question, unlimited_analysis): | |
""" | |
Create a comprehensive PDF report for unlimited text analysis with line-by-line feedback. | |
""" | |
try: | |
# Create the PDF file | |
canvas_obj = canvas.Canvas(self.output_path, pagesize=A4) | |
page_width, page_height = A4 | |
margin = 50 | |
max_width = page_width - 2 * margin | |
def draw_header(canvas_obj, is_first_page=False): | |
"""Draw the header for each page.""" | |
y_pos = page_height - margin | |
# Logo (only on first page) | |
if is_first_page and os.path.exists(self.logo_path): | |
try: | |
logo = ImageReader(self.logo_path) | |
logo_width, logo_height = logo.getSize() | |
# Scale logo to fit | |
scale = min(100 / logo_width, 50 / logo_height) | |
canvas_obj.drawImage(self.logo_path, margin, y_pos - 50, | |
logo_width * scale, logo_height * scale) | |
except Exception as e: | |
logger.warning(f"Could not add logo: {e}") | |
# Title | |
canvas_obj.setFont(self.fonts['title'], 16) | |
canvas_obj.setFillColor(self.colors['normal']) | |
title = "Unlimited Essay Analysis Report" | |
canvas_obj.drawString(margin, y_pos - 30, title) | |
# Subtitle | |
canvas_obj.setFont(self.fonts['normal'], 12) | |
canvas_obj.drawString(margin, y_pos - 50, f"Student: {user_name}") | |
canvas_obj.drawString(margin, y_pos - 65, f"Question: {question}") | |
# Processing info | |
processing_meta = unlimited_analysis.get('processing_metadata', {}) | |
info_text = f"Lines: {processing_meta.get('total_lines', 0)} | Words: {processing_meta.get('total_tokens', 0) * 4} | Processing: {processing_meta.get('processing_mode', 'unknown')}" | |
canvas_obj.setFont(self.fonts['normal'], 10) | |
canvas_obj.drawString(margin, y_pos - 80, info_text) | |
return y_pos - 100 # Return starting y position for content | |
def check_new_page(y_pos, required_space): | |
"""Check if we need a new page and create one if necessary.""" | |
if y_pos - required_space < margin: | |
canvas_obj.showPage() | |
return draw_header(canvas_obj, is_first_page=False) | |
return y_pos | |
# Start with header | |
y_pos = draw_header(canvas_obj, is_first_page=True) | |
# Overall Analysis Section | |
overall_analysis = unlimited_analysis.get('overall_analysis', {}) | |
if overall_analysis and 'error' not in overall_analysis: | |
y_pos = check_new_page(y_pos, 200) | |
# Overall Score | |
canvas_obj.setFont(self.fonts['heading'], 14) | |
canvas_obj.setFillColor(self.colors['normal']) | |
canvas_obj.drawString(margin, y_pos, "Overall Analysis") | |
y_pos -= 25 | |
canvas_obj.setFont(self.fonts['normal'], 12) | |
overall_score = overall_analysis.get('overall_score', 0) | |
canvas_obj.drawString(margin, y_pos, f"Overall Score: {overall_score}/100") | |
y_pos -= 20 | |
# Category Scores | |
category_scores = overall_analysis.get('category_scores', {}) | |
if category_scores: | |
canvas_obj.setFont(self.fonts['heading'], 12) | |
canvas_obj.drawString(margin, y_pos, "Category Scores:") | |
y_pos -= 20 | |
canvas_obj.setFont(self.fonts['normal'], 10) | |
for category, score in category_scores.items(): | |
category_name = category.replace('_', ' ').title() | |
canvas_obj.drawString(margin + 20, y_pos, f"{category_name}: {score}/100") | |
y_pos -= 15 | |
y_pos -= 20 | |
# Summary Statistics Section | |
summary_stats = unlimited_analysis.get('summary_statistics', {}) | |
if summary_stats and 'error' not in summary_stats: | |
y_pos = check_new_page(y_pos, 150) | |
canvas_obj.setFont(self.fonts['heading'], 14) | |
canvas_obj.setFillColor(self.colors['normal']) | |
canvas_obj.drawString(margin, y_pos, "Summary Statistics") | |
y_pos -= 25 | |
canvas_obj.setFont(self.fonts['normal'], 10) | |
stats = [ | |
f"Total Lines: {summary_stats.get('total_lines', 0)}", | |
f"Non-empty Lines: {summary_stats.get('non_empty_lines', 0)}", | |
f"Lines with Issues: {summary_stats.get('lines_with_issues', 0)}", | |
f"Average Score: {summary_stats.get('average_score', 0)}" | |
] | |
for stat in stats: | |
canvas_obj.drawString(margin, y_pos, stat) | |
y_pos -= 15 | |
y_pos -= 20 | |
# Line-by-Line Analysis Section | |
line_analyses = unlimited_analysis.get('line_by_line_analysis', []) | |
if line_analyses: | |
y_pos = check_new_page(y_pos, 100) | |
canvas_obj.setFont(self.fonts['heading'], 14) | |
canvas_obj.setFillColor(self.colors['normal']) | |
canvas_obj.drawString(margin, y_pos, "Line-by-Line Analysis") | |
y_pos -= 25 | |
# Process each line analysis | |
for line_analysis in line_analyses: | |
line_number = line_analysis.get('line_number', 0) | |
line_content = line_analysis.get('line_content', '') | |
line_type = line_analysis.get('line_type', 'unknown') | |
score = line_analysis.get('score', 0) | |
analysis = line_analysis.get('analysis', '') | |
# Check if we need a new page | |
required_space = 100 # Estimate space needed | |
y_pos = check_new_page(y_pos, required_space) | |
# Line header | |
canvas_obj.setFont(self.fonts['heading'], 11) | |
canvas_obj.setFillColor(self.colors['normal']) | |
header_text = f"Line {line_number} ({line_type}) - Score: {score}/100" | |
canvas_obj.drawString(margin, y_pos, header_text) | |
y_pos -= 20 | |
# Line content | |
canvas_obj.setFont(self.fonts['normal'], 10) | |
if line_content.strip(): | |
# Wrap and draw line content | |
wrapped_content = self.wrap_text(line_content, max_width - 20, canvas_obj, self.fonts['normal'], 10) | |
for line in wrapped_content: | |
canvas_obj.drawString(margin + 10, y_pos, line) | |
y_pos -= 15 | |
y_pos -= 5 | |
# Analysis | |
if analysis and analysis.strip(): | |
canvas_obj.setFont(self.fonts['normal'], 9) | |
wrapped_analysis = self.wrap_text(analysis, max_width - 20, canvas_obj, self.fonts['normal'], 9) | |
for line in wrapped_analysis: | |
canvas_obj.drawString(margin + 10, y_pos, line) | |
y_pos -= 12 | |
# Issues | |
issues = line_analysis.get('issues', []) | |
if issues: | |
y_pos -= 5 | |
canvas_obj.setFont(self.fonts['heading'], 9) | |
canvas_obj.drawString(margin + 10, y_pos, "Issues:") | |
y_pos -= 15 | |
canvas_obj.setFont(self.fonts['normal'], 8) | |
for issue in issues[:3]: # Limit to first 3 issues per line | |
issue_text = f"• {issue.get('description', 'Issue')}" | |
wrapped_issue = self.wrap_text(issue_text, max_width - 30, canvas_obj, self.fonts['normal'], 8) | |
for line in wrapped_issue: | |
canvas_obj.drawString(margin + 20, y_pos, line) | |
y_pos -= 10 | |
y_pos -= 10 # Space between lines | |
# Recommendations Section | |
recommendations = unlimited_analysis.get('recommendations', []) | |
if recommendations: | |
y_pos = check_new_page(y_pos, 100) | |
canvas_obj.setFont(self.fonts['heading'], 14) | |
canvas_obj.setFillColor(self.colors['normal']) | |
canvas_obj.drawString(margin, y_pos, "Recommendations") | |
y_pos -= 25 | |
canvas_obj.setFont(self.fonts['normal'], 10) | |
for i, recommendation in enumerate(recommendations[:10], 1): # Limit to first 10 recommendations | |
wrapped_rec = self.wrap_text(f"{i}. {recommendation}", max_width - 20, canvas_obj, self.fonts['normal'], 10) | |
for line in wrapped_rec: | |
canvas_obj.drawString(margin, y_pos, line) | |
y_pos -= 15 | |
y_pos -= 5 | |
# Save the PDF | |
canvas_obj.save() | |
logger.info(f"Unlimited feedback PDF created: {self.output_path}") | |
return self.output_path | |
except Exception as e: | |
logger.error(f"Error creating unlimited feedback PDF: {str(e)}") | |
raise Exception(f"PDF generation failed: {str(e)}") | |
# Example Usage | |
if __name__ == "__main__": | |
# Example feedback structure with the new format | |
feedback_data = { | |
"sections": [ | |
{ | |
"name": "Relevance & Argumentation", | |
"strengths": [ | |
"Clear thesis statement addressing the main topic", | |
"Well-supported arguments with relevant examples", | |
"Logical flow of ideas throughout the essay" | |
], | |
"improvements": [ | |
"Could include more counterarguments", | |
"Some points need stronger evidence", | |
"Consider addressing opposing viewpoints" | |
], | |
"suggestions": [ | |
"Research and include at least two counterarguments", | |
"Add more specific examples to support key points", | |
"Strengthen the conclusion with a call to action" | |
], | |
"example_quote": "The essay demonstrates a clear understanding of the topic with the statement: 'Climate change is primarily driven by human activities...'" | |
}, | |
# ... similar structure for other sections ... | |
] | |
} | |
generator = PDFFeedbackGenerator( | |
output_path="feedback_report.pdf", | |
logo_path="logo.png" | |
) | |
generator.create_feedback_pdf( | |
user_name="Jane Smith", | |
question="Discuss the main anthropogenic factors contributing to climate change and propose feasible solutions.", | |
feedback=feedback_data | |
) |