|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import io |
|
from reportlab.lib.pagesizes import letter |
|
from reportlab.platypus import SimpleDocTemplate, Image, Paragraph, Spacer |
|
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle |
|
from reportlab.lib.enums import TA_JUSTIFY |
|
from reportlab.lib.units import inch |
|
import matplotlib.pyplot as plt |
|
import markdown |
|
from xml.etree import ElementTree as ET |
|
from PIL import Image as PILImage |
|
from xml.parsers.expat import ExpatError |
|
from html import escape |
|
|
|
class ReportGenerator: |
|
def __init__(self): |
|
self.styles = getSampleStyleSheet() |
|
self.styles.add(ParagraphStyle(name='Justify', alignment=TA_JUSTIFY)) |
|
|
|
def create_combined_pdf(self, intervention_fig, student_metrics_fig, recommendations): |
|
buffer = io.BytesIO() |
|
doc = SimpleDocTemplate(buffer, pagesize=letter) |
|
|
|
elements = [] |
|
|
|
elements.extend(self._add_chart(intervention_fig, "Intervention Statistics")) |
|
elements.extend(self._add_chart(student_metrics_fig, "Student Metrics")) |
|
elements.extend(self._add_recommendations(recommendations)) |
|
|
|
doc.build(elements) |
|
buffer.seek(0) |
|
return buffer |
|
|
|
def _add_chart(self, fig, title): |
|
elements = [] |
|
elements.append(Paragraph(title, self.styles['Heading2'])) |
|
img_buffer = io.BytesIO() |
|
|
|
if hasattr(fig, 'write_image'): |
|
fig.write_image(img_buffer, format="png", width=700, height=400) |
|
elif isinstance(fig, plt.Figure): |
|
fig.set_size_inches(10, 6) |
|
fig.savefig(img_buffer, format='png', dpi=100, bbox_inches='tight') |
|
plt.close(fig) |
|
else: |
|
raise ValueError(f"Unsupported figure type: {type(fig)}") |
|
|
|
img_buffer.seek(0) |
|
|
|
|
|
with PILImage.open(img_buffer) as img: |
|
img_width, img_height = img.size |
|
|
|
|
|
max_width = 6.5 * inch |
|
max_height = 4 * inch |
|
|
|
aspect = img_width / float(img_height) |
|
|
|
if img_width > max_width: |
|
img_width = max_width |
|
img_height = img_width / aspect |
|
|
|
if img_height > max_height: |
|
img_height = max_height |
|
img_width = img_height * aspect |
|
|
|
|
|
img_buffer.seek(0) |
|
|
|
|
|
img = Image(img_buffer, width=img_width, height=img_height) |
|
|
|
elements.append(img) |
|
elements.append(Spacer(1, 12)) |
|
return elements |
|
|
|
def _add_recommendations(self, recommendations): |
|
elements = [] |
|
elements.append(Paragraph("AI Recommendations", self.styles['Heading1'])) |
|
|
|
|
|
html = markdown.markdown(recommendations) |
|
|
|
|
|
wrapped_html = f"<root>{html}</root>" |
|
|
|
try: |
|
root = ET.fromstring(wrapped_html) |
|
except ExpatError: |
|
|
|
elements.append(Paragraph(escape(recommendations), self.styles['BodyText'])) |
|
return elements |
|
|
|
for elem in root: |
|
if elem.tag == 'h3': |
|
elements.append(Paragraph(elem.text or "", self.styles['Heading3'])) |
|
elif elem.tag == 'h4': |
|
elements.append(Paragraph(elem.text or "", self.styles['Heading4'])) |
|
elif elem.tag == 'p': |
|
text = ''.join(elem.itertext()) |
|
elements.append(Paragraph(text, self.styles['Justify'])) |
|
elif elem.tag == 'ul': |
|
for li in elem.findall('li'): |
|
bullet_text = '• ' + ''.join(li.itertext()).strip() |
|
elements.append(Paragraph(bullet_text, self.styles['BodyText'])) |
|
else: |
|
|
|
text = ''.join(elem.itertext()) |
|
if text.strip(): |
|
elements.append(Paragraph(text, self.styles['BodyText'])) |
|
|
|
return elements |