#!/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 [--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"Motion: {data['motion_id']}", f"Analysis Date: {data['timestamp']}", f"Model: {data['model']}", f"Countries Analyzed: {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"{analysis['country']}", styles['CustomSubHeading'] )) # Vote and confidence info_text = f"Vote: {analysis['vote'].upper()} | Confidence: {analysis['impact_analysis']['confidence']}" elements.append(Paragraph(info_text, styles['Normal'])) elements.append(Spacer(1, 0.1*inch)) # Analysis reasoning elements.append(Paragraph("Analysis:", 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("Key Factors:", 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("Country Statement:", 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()