Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| """ | |
| Simple PDF Report Generator using reportlab | |
| Generates professional PDF reports from UN motion analysis results without | |
| requiring heavy dependencies like weasyprint. | |
| Usage: | |
| python scripts/generate_simple_pdf.py <input_file> [--output output.pdf] | |
| Example: | |
| python scripts/generate_simple_pdf.py analysis/01_gaza_ceasefire_resolution_israel_bilateral_impact_latest.json | |
| """ | |
| import argparse | |
| import json | |
| import sys | |
| from datetime import datetime | |
| from pathlib import Path | |
| from typing import Optional, List, Dict | |
| # Add project root to path | |
| PROJECT_ROOT = Path(__file__).parent.parent | |
| sys.path.insert(0, str(PROJECT_ROOT)) | |
| def check_dependencies(): | |
| """Check if required dependencies are installed""" | |
| try: | |
| from reportlab.lib.pagesizes import letter, A4 | |
| from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak | |
| from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle | |
| from reportlab.lib.units import inch | |
| from reportlab.lib import colors | |
| return True | |
| except ImportError as e: | |
| print(f"Missing dependency: reportlab") | |
| print("\nInstall with:") | |
| print(" pip install reportlab") | |
| return False | |
| def generate_bilateral_impact_pdf(json_file: Path, output_pdf: Optional[Path] = None): | |
| """ | |
| Generate PDF report from bilateral impact analysis JSON | |
| Args: | |
| json_file: Path to JSON analysis results | |
| output_pdf: Optional output PDF path | |
| """ | |
| from reportlab.lib.pagesizes import letter, A4 | |
| from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak | |
| from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle | |
| from reportlab.lib.units import inch | |
| from reportlab.lib import colors | |
| from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY | |
| # Load JSON data | |
| with open(json_file, 'r', encoding='utf-8') as f: | |
| data = json.load(f) | |
| # Determine output path | |
| if output_pdf is None: | |
| output_pdf = json_file.parent / "pdf" / f"{json_file.stem}.pdf" | |
| # Create output directory | |
| output_pdf.parent.mkdir(parents=True, exist_ok=True) | |
| # Create PDF | |
| doc = SimpleDocTemplate( | |
| str(output_pdf), | |
| pagesize=A4, | |
| rightMargin=72, | |
| leftMargin=72, | |
| topMargin=72, | |
| bottomMargin=18, | |
| ) | |
| # Container for the 'Flowable' objects | |
| elements = [] | |
| # Define styles | |
| styles = getSampleStyleSheet() | |
| styles.add(ParagraphStyle( | |
| name='CustomTitle', | |
| parent=styles['Heading1'], | |
| fontSize=24, | |
| textColor=colors.HexColor('#1a5490'), | |
| spaceAfter=30, | |
| alignment=TA_CENTER, | |
| fontName='Helvetica-Bold' | |
| )) | |
| styles.add(ParagraphStyle( | |
| name='CustomHeading', | |
| parent=styles['Heading2'], | |
| fontSize=16, | |
| textColor=colors.HexColor('#2c5f8d'), | |
| spaceAfter=12, | |
| spaceBefore=12, | |
| fontName='Helvetica-Bold' | |
| )) | |
| styles.add(ParagraphStyle( | |
| name='CustomSubHeading', | |
| parent=styles['Heading3'], | |
| fontSize=12, | |
| textColor=colors.HexColor('#34495e'), | |
| spaceAfter=6, | |
| spaceBefore=6, | |
| fontName='Helvetica-Bold' | |
| )) | |
| styles.add(ParagraphStyle( | |
| name='Justified', | |
| parent=styles['Normal'], | |
| alignment=TA_JUSTIFY, | |
| fontSize=10, | |
| leading=14 | |
| )) | |
| # Title | |
| title = Paragraph("Israel Bilateral Relationship Impact Analysis", styles['CustomTitle']) | |
| elements.append(title) | |
| elements.append(Spacer(1, 0.2*inch)) | |
| # Metadata | |
| metadata = [ | |
| f"<b>Motion:</b> {data['motion_id']}", | |
| f"<b>Analysis Date:</b> {data['timestamp']}", | |
| f"<b>Model:</b> {data['model']}", | |
| f"<b>Countries Analyzed:</b> {data['total_analyzed']}" | |
| ] | |
| for line in metadata: | |
| elements.append(Paragraph(line, styles['Normal'])) | |
| elements.append(Spacer(1, 0.3*inch)) | |
| # Executive Summary | |
| elements.append(Paragraph("Executive Summary", styles['CustomHeading'])) | |
| summary_text = f"This report analyzes how the Gaza ceasefire resolution vote affects Israel's bilateral relationships with {data['total_analyzed']} UN member states." | |
| elements.append(Paragraph(summary_text, styles['Justified'])) | |
| elements.append(Spacer(1, 0.2*inch)) | |
| # Impact Distribution Table | |
| elements.append(Paragraph("Impact Distribution", styles['CustomSubHeading'])) | |
| impact_data = [['Impact Category', 'Count', 'Percentage']] | |
| total = data['total_analyzed'] | |
| for category, count in data['impact_summary'].items(): | |
| pct = (count / total * 100) if total > 0 else 0 | |
| impact_data.append([ | |
| category.replace('_', ' ').title(), | |
| str(count), | |
| f"{pct:.1f}%" | |
| ]) | |
| impact_table = Table(impact_data, colWidths=[3.5*inch, 1*inch, 1*inch]) | |
| impact_table.setStyle(TableStyle([ | |
| ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#1a5490')), | |
| ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), | |
| ('ALIGN', (0, 0), (-1, -1), 'LEFT'), | |
| ('ALIGN', (1, 0), (-1, -1), 'CENTER'), | |
| ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), | |
| ('FONTSIZE', (0, 0), (-1, 0), 11), | |
| ('BOTTOMPADDING', (0, 0), (-1, 0), 12), | |
| ('BACKGROUND', (0, 1), (-1, -1), colors.white), | |
| ('GRID', (0, 0), (-1, -1), 1, colors.grey), | |
| ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#f9f9f9')]), | |
| ])) | |
| elements.append(impact_table) | |
| elements.append(Spacer(1, 0.3*inch)) | |
| # Detailed Analyses by Category | |
| elements.append(PageBreak()) | |
| elements.append(Paragraph("Detailed Country Analyses", styles['CustomHeading'])) | |
| elements.append(Spacer(1, 0.2*inch)) | |
| # Group analyses by impact category | |
| by_category = {} | |
| for analysis in data['analyses']: | |
| category = analysis['impact_analysis']['impact_category'] | |
| if category not in by_category: | |
| by_category[category] = [] | |
| by_category[category].append(analysis) | |
| # Category order | |
| category_order = [ | |
| 'strengthened_significantly', | |
| 'strengthened_moderately', | |
| 'strengthened_slightly', | |
| 'neutral', | |
| 'strained_slightly', | |
| 'strained_moderately', | |
| 'strained_significantly' | |
| ] | |
| for category in category_order: | |
| if category not in by_category or not by_category[category]: | |
| continue | |
| # Category heading | |
| elements.append(Paragraph( | |
| category.replace('_', ' ').title(), | |
| styles['CustomHeading'] | |
| )) | |
| elements.append(Spacer(1, 0.1*inch)) | |
| for i, analysis in enumerate(by_category[category]): | |
| # Country name | |
| elements.append(Paragraph( | |
| f"<b>{analysis['country']}</b>", | |
| styles['CustomSubHeading'] | |
| )) | |
| # Vote and confidence | |
| info_text = f"<b>Vote:</b> {analysis['vote'].upper()} | <b>Confidence:</b> {analysis['impact_analysis']['confidence']}" | |
| elements.append(Paragraph(info_text, styles['Normal'])) | |
| elements.append(Spacer(1, 0.1*inch)) | |
| # Analysis reasoning | |
| elements.append(Paragraph("<b>Analysis:</b>", styles['Normal'])) | |
| reasoning = analysis['impact_analysis']['reasoning'] | |
| elements.append(Paragraph(reasoning, styles['Justified'])) | |
| elements.append(Spacer(1, 0.1*inch)) | |
| # Key factors | |
| elements.append(Paragraph("<b>Key Factors:</b>", styles['Normal'])) | |
| for factor in analysis['impact_analysis']['key_factors']: | |
| elements.append(Paragraph(f"• {factor}", styles['Normal'])) | |
| elements.append(Spacer(1, 0.1*inch)) | |
| # Country statement | |
| elements.append(Paragraph("<b>Country Statement:</b>", styles['Normal'])) | |
| statement = analysis['statement'] | |
| if len(statement) > 500: | |
| statement = statement[:500] + "..." | |
| elements.append(Paragraph(statement, styles['Justified'])) | |
| # Separator between countries | |
| if i < len(by_category[category]) - 1: | |
| elements.append(Spacer(1, 0.2*inch)) | |
| # Space between categories | |
| elements.append(Spacer(1, 0.3*inch)) | |
| # Footer | |
| footer_text = f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" | |
| elements.append(Spacer(1, 0.3*inch)) | |
| elements.append(Paragraph(footer_text, styles['Normal'])) | |
| # Build PDF | |
| print(f"Generating PDF: {output_pdf}") | |
| doc.build(elements) | |
| print(f"✓ PDF generated successfully") | |
| return output_pdf | |
| def generate_markdown_pdf(md_file: Path, output_pdf: Optional[Path] = None): | |
| """ | |
| Generate PDF from markdown file | |
| Args: | |
| md_file: Path to markdown file | |
| output_pdf: Optional output PDF path | |
| """ | |
| from reportlab.lib.pagesizes import A4 | |
| from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak | |
| from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle | |
| from reportlab.lib.units import inch | |
| from reportlab.lib import colors | |
| from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY | |
| # Read markdown | |
| with open(md_file, 'r', encoding='utf-8') as f: | |
| md_content = f.read() | |
| # Determine output path | |
| if output_pdf is None: | |
| output_pdf = md_file.parent / "pdf" / f"{md_file.stem}.pdf" | |
| # Create output directory | |
| output_pdf.parent.mkdir(parents=True, exist_ok=True) | |
| # Create PDF | |
| doc = SimpleDocTemplate( | |
| str(output_pdf), | |
| pagesize=A4, | |
| rightMargin=72, | |
| leftMargin=72, | |
| topMargin=72, | |
| bottomMargin=18, | |
| ) | |
| elements = [] | |
| styles = getSampleStyleSheet() | |
| # Add custom styles | |
| styles.add(ParagraphStyle( | |
| name='CustomTitle', | |
| parent=styles['Heading1'], | |
| fontSize=20, | |
| textColor=colors.HexColor('#1a5490'), | |
| spaceAfter=20, | |
| fontName='Helvetica-Bold' | |
| )) | |
| styles.add(ParagraphStyle( | |
| name='Justified', | |
| parent=styles['Normal'], | |
| alignment=TA_JUSTIFY, | |
| fontSize=10, | |
| leading=14 | |
| )) | |
| # Simple markdown parsing (basic headers and paragraphs) | |
| lines = md_content.split('\n') | |
| for line in lines: | |
| line = line.strip() | |
| if not line: | |
| elements.append(Spacer(1, 0.1*inch)) | |
| continue | |
| if line.startswith('# '): | |
| elements.append(Paragraph(line[2:], styles['CustomTitle'])) | |
| elif line.startswith('## '): | |
| elements.append(Paragraph(line[3:], styles['Heading2'])) | |
| elif line.startswith('### '): | |
| elements.append(Paragraph(line[4:], styles['Heading3'])) | |
| elif line.startswith('**') or line.startswith('*'): | |
| elements.append(Paragraph(line, styles['Normal'])) | |
| elif line.startswith('---'): | |
| elements.append(Spacer(1, 0.2*inch)) | |
| else: | |
| elements.append(Paragraph(line, styles['Justified'])) | |
| # Build PDF | |
| print(f"Generating PDF: {output_pdf}") | |
| doc.build(elements) | |
| print(f"✓ PDF generated successfully") | |
| return output_pdf | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description="Generate PDF reports from analysis results (lightweight version)", | |
| formatter_class=argparse.RawDescriptionHelpFormatter, | |
| epilog=""" | |
| Examples: | |
| # Convert JSON bilateral impact to PDF | |
| python scripts/generate_simple_pdf.py tasks/analysis/01_gaza_ceasefire_resolution_israel_bilateral_impact_latest.json | |
| # Convert markdown to PDF | |
| python scripts/generate_simple_pdf.py analysis/report.md | |
| # Specify output path | |
| python scripts/generate_simple_pdf.py analysis/report.md --output custom.pdf | |
| """ | |
| ) | |
| parser.add_argument( | |
| "input_file", | |
| type=Path, | |
| help="Input file (.md or .json)" | |
| ) | |
| parser.add_argument( | |
| "--output", | |
| type=Path, | |
| help="Output PDF file (optional)" | |
| ) | |
| args = parser.parse_args() | |
| # Check dependencies | |
| if not check_dependencies(): | |
| sys.exit(1) | |
| # Validate input file | |
| if not args.input_file.exists(): | |
| print(f"Error: Input file not found: {args.input_file}") | |
| sys.exit(1) | |
| try: | |
| # Determine file type and process | |
| if args.input_file.suffix == '.json': | |
| print("Processing bilateral impact JSON...") | |
| pdf_path = generate_bilateral_impact_pdf(args.input_file, args.output) | |
| elif args.input_file.suffix == '.md': | |
| print("Processing markdown file...") | |
| pdf_path = generate_markdown_pdf(args.input_file, args.output) | |
| else: | |
| print(f"Error: Unsupported file type: {args.input_file.suffix}") | |
| print("Supported types: .md, .json") | |
| sys.exit(1) | |
| print(f"\n✓ PDF report generated: {pdf_path}") | |
| print(f" Size: {pdf_path.stat().st_size / 1024:.1f} KB") | |
| except Exception as e: | |
| print(f"\n❌ Error generating PDF: {e}") | |
| import traceback | |
| traceback.print_exc() | |
| sys.exit(1) | |
| if __name__ == "__main__": | |
| main() | |