""" export.py — Report export utilities for MicroPlastiNet M5 GenAI Supports: - Markdown (.md) export - PDF export via ReportLab """ import os import re from datetime import datetime from pathlib import Path # ─── Markdown export ─────────────────────────────────────────────────────────── def export_markdown(report_text: str, output_path: str | Path) -> Path: """Write report text to a .md file.""" output_path = Path(output_path) output_path.parent.mkdir(parents=True, exist_ok=True) with open(output_path, "w", encoding="utf-8") as f: f.write(report_text) print(f"Markdown report saved: {output_path}") return output_path # ─── PDF export via ReportLab ────────────────────────────────────────────────── def export_pdf(report_text: str, output_path: str | Path, station_id: str = "", event_id: str = "") -> Path: """ Render report text to a polished PDF using ReportLab. The PDF uses a two-column masthead, section headers with rule lines, and body text in a readable serif-style font. """ from reportlab.lib.pagesizes import letter from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.lib.units import inch from reportlab.lib import colors from reportlab.platypus import ( SimpleDocTemplate, Paragraph, Spacer, HRFlowable, Table, TableStyle ) from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT, TA_JUSTIFY output_path = Path(output_path) output_path.parent.mkdir(parents=True, exist_ok=True) # ── Colors ───────────────────────────────────────────────────────────────── DEEP_NAVY = colors.HexColor("#0a0e1a") TEAL = colors.HexColor("#00b8a9") CYAN = colors.HexColor("#00d4ff") DARK_GRAY = colors.HexColor("#1a2235") MID_GRAY = colors.HexColor("#64748b") TEXT = colors.HexColor("#1e293b") WHITE = colors.white # ── Document ─────────────────────────────────────────────────────────────── doc = SimpleDocTemplate( str(output_path), pagesize=letter, rightMargin=0.85 * inch, leftMargin=0.85 * inch, topMargin=1.0 * inch, bottomMargin=0.85 * inch, title=f"MicroPlastiNet Report — {station_id}", author="MicroPlastiNet Automated Monitoring System", subject="Environmental Compliance Report", ) # ── Styles ───────────────────────────────────────────────────────────────── base = getSampleStyleSheet() doc_title_style = ParagraphStyle( "DocTitle", fontName="Helvetica-Bold", fontSize=18, textColor=WHITE, spaceAfter=4, leading=22, ) doc_subtitle_style = ParagraphStyle( "DocSubtitle", fontName="Helvetica", fontSize=9, textColor=colors.HexColor("#94a3b8"), spaceAfter=2, ) section_header_style = ParagraphStyle( "SectionHeader", fontName="Helvetica-Bold", fontSize=11, textColor=CYAN, spaceBefore=16, spaceAfter=4, leading=14, ) body_style = ParagraphStyle( "Body", fontName="Times-Roman", fontSize=10, textColor=TEXT, spaceAfter=8, leading=15, alignment=TA_JUSTIFY, ) bold_body_style = ParagraphStyle( "BoldBody", fontName="Times-Bold", fontSize=10, textColor=TEXT, spaceAfter=4, leading=14, ) bullet_style = ParagraphStyle( "Bullet", fontName="Times-Roman", fontSize=10, textColor=TEXT, spaceAfter=3, leading=14, leftIndent=14, bulletIndent=4, ) meta_style = ParagraphStyle( "Meta", fontName="Helvetica", fontSize=8, textColor=MID_GRAY, spaceAfter=2, ) cite_style = ParagraphStyle( "Cite", fontName="Helvetica", fontSize=8, textColor=MID_GRAY, spaceAfter=3, leading=11, ) footer_style = ParagraphStyle( "Footer", fontName="Helvetica", fontSize=7, textColor=MID_GRAY, alignment=TA_CENTER, ) # ── Build content ────────────────────────────────────────────────────────── story = [] # Masthead header band (simulated with a colored table) header_data = [[ Paragraph("MicroPlastiNet", ParagraphStyle( "hdr", fontName="Helvetica-Bold", fontSize=20, textColor=WHITE)), Paragraph( f"Environmental Compliance Report
" f"Georgia Coastal Watershed Monitoring Network", ParagraphStyle("hdr2", fontName="Helvetica", fontSize=11, textColor=CYAN, alignment=TA_RIGHT)), ]] header_table = Table(header_data, colWidths=["45%", "55%"]) header_table.setStyle(TableStyle([ ("BACKGROUND", (0, 0), (-1, -1), DEEP_NAVY), ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), ("LEFTPADDING", (0, 0), (0, -1), 14), ("RIGHTPADDING", (-1, 0), (-1, -1), 14), ("TOPPADDING", (0, 0), (-1, -1), 14), ("BOTTOMPADDING", (0, 0), (-1, -1), 14), ("ROUNDEDCORNERS", [6, 6, 6, 6]), ])) story.append(header_table) story.append(Spacer(1, 10)) # Meta row now = datetime.now().strftime("%Y-%m-%d %H:%M UTC") meta_data = [[ Paragraph(f"Station: {station_id}", meta_style), Paragraph(f"Event: {event_id}", meta_style), Paragraph(f"Generated: {now}", meta_style), ]] meta_table = Table(meta_data, colWidths=["33%", "33%", "34%"]) meta_table.setStyle(TableStyle([ ("BACKGROUND", (0, 0), (-1, -1), colors.HexColor("#f1f5f9")), ("TOPPADDING", (0, 0), (-1, -1), 6), ("BOTTOMPADDING", (0, 0), (-1, -1), 6), ("LEFTPADDING", (0, 0), (0, -1), 10), ])) story.append(meta_table) story.append(Spacer(1, 12)) # ── Parse Markdown-ish text → ReportLab flowables ───────────────────────── lines = report_text.split("\n") in_list = False def flush_list(): nonlocal in_list if in_list: story.append(Spacer(1, 4)) in_list = False for line in lines: line_stripped = line.strip() # Skip the first H1 (we have the header table) if line_stripped.startswith("# ") and not line_stripped.startswith("## "): continue # Section headers if line_stripped.startswith("## "): flush_list() story.append(HRFlowable(width="100%", thickness=0.5, color=CYAN, spaceAfter=4, spaceBefore=12)) story.append(Paragraph(line_stripped[3:], section_header_style)) continue # Bold metadata lines (key: value) if line_stripped.startswith("**") and line_stripped.endswith("**"): flush_list() story.append(Paragraph(line_stripped[2:-2], bold_body_style)) continue # Horizontal rule if line_stripped == "---": story.append(HRFlowable(width="100%", thickness=0.5, color=MID_GRAY, spaceAfter=6, spaceBefore=6)) continue # Bullet items if line_stripped.startswith("- "): in_list = True content = line_stripped[2:] # Inline bold content = re.sub(r"\*\*(.+?)\*\*", r"\1", content) story.append(Paragraph(f"• {content}", bullet_style)) continue # Numbered items (1. 2. etc.) num_match = re.match(r"^(\d+)\.\s+(.+)$", line_stripped) if num_match: in_list = True num_text = num_match.group(2) num_text = re.sub(r"\*\*(.+?)\*\*", r"\1", num_text) story.append(Paragraph(f"{num_match.group(1)}. {num_text}", bullet_style)) continue # Bold/italic inline if line_stripped: flush_list() content = line_stripped content = re.sub(r"\*\*(.+?)\*\*", r"\1", content) content = re.sub(r"\*(.+?)\*", r"\1", content) # Citation lines (start with URL-like content) if "http" in content or content.startswith("- "): story.append(Paragraph(content, cite_style)) else: story.append(Paragraph(content, body_style)) else: flush_list() story.append(Spacer(1, 4)) flush_list() # Footer story.append(Spacer(1, 20)) story.append(HRFlowable(width="100%", thickness=0.3, color=MID_GRAY)) story.append(Spacer(1, 6)) story.append(Paragraph( "Generated by MicroPlastiNet — a research prototype by Saikumar Reddy Naidu (CS Graduate, Florida Atlantic University). " "Ongoing research; not affiliated with any agency.", footer_style, )) doc.build(story) print(f"PDF report saved: {output_path}") return output_path # ─── CLI convenience ─────────────────────────────────────────────────────────── if __name__ == "__main__": import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from report_generator import generate_report event = { "station_id": "STN-003", "polymers": {"PE": 0.18, "PET": 0.45, "PP": 0.21, "PS": 0.08, "PVC": 0.05, "Other": 0.03}, "confidence": {"PE": 0.89, "PET": 0.94, "PP": 0.82, "PS": 0.77, "PVC": 0.71, "Other": 0.65}, "total_particles": 1847, } attr = { "station_id": "STN-003", "event_id": "EVT-7731", "event_date": "2025-06-15", "sources": [ {"rank": 1, "name": "Upstream Wastewater Outfall", "probability": 0.52, "confidence": 0.88, "distance_km": 6.3, "lat": 32.31, "lon": -81.52}, {"rank": 2, "name": "Urban Stormwater Runoff", "probability": 0.26, "confidence": 0.81, "distance_km": 11.7, "lat": 32.41, "lon": -81.48}, {"rank": 3, "name": "Agricultural Drainage", "probability": 0.13, "confidence": 0.74, "distance_km": 18.2, "lat": 32.28, "lon": -81.61}, {"rank": 4, "name": "Industrial Discharge", "probability": 0.07, "confidence": 0.69, "distance_km": 24.5, "lat": 32.35, "lon": -81.39}, {"rank": 5, "name": "Marine Vessel Traffic", "probability": 0.02, "confidence": 0.55, "distance_km": 3.1, "lat": 32.18, "lon": -81.45}, ], } report = generate_report("STN-003", event, attr, mode="template") assets_dir = Path(__file__).parent.parent.parent / "assets" export_markdown(report, assets_dir / "sample_report.md") export_pdf(report, assets_dir / "sample_report.pdf", station_id="STN-003", event_id="EVT-7731")