# 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 Dosage")) | |
# elements.extend(self._add_chart(student_metrics_fig, "Student Attendance and Engagement")) | |
# 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'): # Plotly figure | |
# fig.write_image(img_buffer, format="png", width=700, height=400) | |
# elif isinstance(fig, plt.Figure): # Matplotlib figure | |
# fig.set_size_inches(10, 6) # Set a consistent size | |
# 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) | |
# # Use PIL to get image dimensions | |
# with PILImage.open(img_buffer) as img: | |
# img_width, img_height = img.size | |
# # Calculate width and height to maintain aspect ratio | |
# max_width = 6.5 * inch # Maximum width (letter width is 8.5 inches, leaving margins) | |
# max_height = 4 * inch # Maximum height | |
# 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 | |
# # Reset buffer position | |
# img_buffer.seek(0) | |
# # Create ReportLab Image with calculated dimensions | |
# 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("MTSS.ai Analysis", self.styles['Heading1'])) | |
# # Convert markdown to HTML | |
# html = markdown.markdown(recommendations) | |
# # Wrap the HTML in a root element to ensure valid XML | |
# wrapped_html = f"<root>{html}</root>" | |
# try: | |
# root = ET.fromstring(wrapped_html) | |
# except ExpatError: | |
# # If parsing fails, fallback to treating the entire content as plain text | |
# 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: | |
# # For any other tags, just extract the text | |
# text = ''.join(elem.itertext()) | |
# if text.strip(): | |
# elements.append(Paragraph(text, self.styles['BodyText'])) | |
# return elements | |
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, TA_LEFT | |
from reportlab.lib.units import inch | |
import matplotlib.pyplot as plt | |
import markdown | |
from bs4 import BeautifulSoup | |
from PIL import Image as PILImage | |
from reportlab.lib.colors import black | |
class ReportGenerator: | |
def __init__(self): | |
self.styles = getSampleStyleSheet() | |
self.styles['BodyText'].alignment = TA_JUSTIFY | |
self.styles['Bullet'].leftIndent = 20 | |
self.styles['Bullet'].firstLineIndent = 0 | |
self.styles['Bullet'].alignment = TA_LEFT | |
self.styles['Heading1'].fontSize = 18 | |
self.styles['Heading2'].fontSize = 16 | |
self.styles['Heading3'].fontSize = 14 | |
self.styles['Heading4'].fontSize = 12 | |
# Add a new style for bold text | |
self.styles.add(ParagraphStyle(name='Bold', parent=self.styles['BodyText'], fontName='Helvetica-Bold')) | |
def create_combined_pdf(self, intervention_fig, student_metrics_fig, recommendations): | |
buffer = io.BytesIO() | |
doc = SimpleDocTemplate(buffer, pagesize=letter, topMargin=0.5*inch, bottomMargin=0.5*inch) | |
elements = [] | |
elements.extend(self._add_chart(intervention_fig, "Intervention Dosage")) | |
elements.extend(self._add_chart(student_metrics_fig, "Student Attendance and Engagement")) | |
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'): # Plotly figure | |
fig.write_image(img_buffer, format="png", width=700, height=400) | |
elif isinstance(fig, plt.Figure): # Matplotlib figure | |
fig.set_size_inches(10, 6) # Set a consistent size | |
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) | |
# Use PIL to get image dimensions | |
with PILImage.open(img_buffer) as img: | |
img_width, img_height = img.size | |
# Calculate width and height to maintain aspect ratio | |
max_width = 6.5 * inch # Maximum width (letter width is 8.5 inches, leaving margins) | |
max_height = 4 * inch # Maximum height | |
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 | |
# Reset buffer position | |
img_buffer.seek(0) | |
# Create ReportLab Image with calculated dimensions | |
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("MTSS.ai Analysis", self.styles['Heading1'])) | |
html = markdown.markdown(recommendations) | |
soup = BeautifulSoup(html, 'html.parser') | |
for element in soup.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'ul']): | |
if element.name.startswith('h'): | |
level = int(element.name[1]) | |
style = f'Heading{min(level, 4)}' | |
elements.append(Paragraph(element.text, self.styles[style])) | |
elif element.name == 'p': | |
elements.append(self._create_paragraph_with_inline_styles(element)) | |
elif element.name == 'ul': | |
for li in element.find_all('li'): | |
bullet_text = '• ' + self._get_text_with_inline_styles(li) | |
elements.append(Paragraph(bullet_text, self.styles['Bullet'])) | |
elements.append(Spacer(1, 6)) | |
return elements | |
def _create_paragraph_with_inline_styles(self, element): | |
text = self._get_text_with_inline_styles(element) | |
return Paragraph(text, self.styles['BodyText']) | |
def _get_text_with_inline_styles(self, element): | |
text = "" | |
for child in element.children: | |
if isinstance(child, str): | |
text += child | |
elif child.name in ['strong', 'b']: | |
text += f'<b>{child.text}</b>' | |
elif child.name in ['em', 'i']: | |
text += f'<i>{child.text}</i>' | |
elif child.name == 'u': | |
text += f'<u>{child.text}</u>' | |
return text |