"""PDF Export Module Converts markdown stage outputs into beautifully formatted PDF documents. Uses markdown2 for markdown parsing and WeasyPrint for PDF generation. """ import logging from pathlib import Path from typing import Optional from datetime import datetime import tempfile # Import PDF generation libraries try: import markdown2 from weasyprint import HTML, CSS PDF_AVAILABLE = True except ImportError: PDF_AVAILABLE = False logging.warning("PDF export libraries not available. Install with: pip install markdown2 weasyprint") logger = logging.getLogger(__name__) # CSS styling for beautiful PDFs PDF_STYLES = """ @page { size: A4; margin: 2cm; } body { font-family: 'Helvetica', 'Arial', sans-serif; font-size: 11pt; line-height: 1.6; color: #333; } h1 { color: #2c3e50; font-size: 24pt; border-bottom: 3px solid #3498db; padding-bottom: 10px; margin-top: 20px; } h2 { color: #34495e; font-size: 18pt; margin-top: 18px; border-bottom: 2px solid #ecf0f1; padding-bottom: 6px; } h3 { color: #555; font-size: 14pt; margin-top: 14px; } h4 { color: #666; font-size: 12pt; margin-top: 12px; } code { background-color: #f4f4f4; padding: 2px 6px; border-radius: 3px; font-family: 'Courier New', monospace; font-size: 10pt; } pre { background-color: #f8f8f8; padding: 15px; border-left: 4px solid #3498db; overflow-x: auto; border-radius: 4px; } pre code { background-color: transparent; padding: 0; } blockquote { border-left: 4px solid #95a5a6; padding-left: 15px; margin-left: 0; font-style: italic; color: #555; } ul, ol { margin-left: 20px; } li { margin-bottom: 6px; } strong { color: #2c3e50; } em { color: #7f8c8d; } hr { border: none; border-top: 2px solid #ecf0f1; margin: 20px 0; } /* Footer */ @page { @bottom-center { content: "Page " counter(page) " of " counter(pages); font-size: 9pt; color: #7f8c8d; } } """ def generate_stage_pdf( stage_num: int, markdown_content: str, output_path: Optional[Path] = None, brand_name: str = "Unknown Brand" ) -> Path: """Generate PDF from markdown content for a specific stage. Args: stage_num: Stage number (0-6) markdown_content: Markdown-formatted stage output output_path: Optional output path (defaults to temp file) brand_name: Brand name for PDF header Returns: Path to generated PDF file Raises: ImportError: If PDF generation libraries not installed Exception: If PDF generation fails """ if not PDF_AVAILABLE: raise ImportError( "PDF export libraries not available. " "Install with: pip install markdown2 weasyprint" ) logger.info(f"Generating PDF for Stage {stage_num}") try: # CRITICAL: Ensure markdown_content is a string, not dict/JSON if not isinstance(markdown_content, str): logger.error(f"markdown_content is not a string (type={type(markdown_content)})") # Convert to JSON string as fallback import json markdown_content = f"```json\n{json.dumps(markdown_content, indent=2)}\n```" # Convert markdown to HTML html_content = markdown2.markdown( markdown_content, extras=["fenced-code-blocks", "tables", "break-on-newline"] ) # Add header and metadata timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") stage_names = { 0: "Brand Context", 1: "Extraction", 2: "Signals", 3: "Insights", 4: "Ideation", 5: "Opportunity Cards", 6: "Executive Summary" } stage_name = stage_names.get(stage_num, f"Stage {stage_num}") full_html = f""" {brand_name} - {stage_name}

{brand_name}

Stage {stage_num}: {stage_name}

Generated: {timestamp}

{html_content} """ # Determine output path if output_path is None: # Create temp file temp_dir = Path(tempfile.gettempdir()) / "innovation-pdfs" temp_dir.mkdir(exist_ok=True) output_path = temp_dir / f"stage_{stage_num}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf" else: output_path = Path(output_path) output_path.parent.mkdir(parents=True, exist_ok=True) # Generate PDF html = HTML(string=full_html) css = CSS(string=PDF_STYLES) html.write_pdf(output_path, stylesheets=[css]) logger.info(f"PDF generated successfully: {output_path}") return output_path except Exception as e: logger.error(f"PDF generation failed for Stage {stage_num}: {e}", exc_info=True) raise def generate_all_stages_pdf( stage_markdowns: dict, output_path: Optional[Path] = None, brand_name: str = "Unknown Brand" ) -> Path: """Generate combined PDF with all pipeline stages. Args: stage_markdowns: Dict mapping stage numbers to markdown content output_path: Optional output path (defaults to temp file) brand_name: Brand name for PDF header Returns: Path to generated PDF file Raises: ImportError: If PDF generation libraries not installed Exception: If PDF generation fails """ if not PDF_AVAILABLE: raise ImportError( "PDF export libraries not available. " "Install with: pip install markdown2 weasyprint" ) logger.info("Generating combined PDF for all stages") try: # Combine all stages into single markdown combined_markdown = [] for stage_num in sorted(stage_markdowns.keys()): markdown_content = stage_markdowns[stage_num] if markdown_content: # CRITICAL: Ensure markdown_content is a string, not dict/JSON if not isinstance(markdown_content, str): logger.error(f"Stage {stage_num} markdown_content is not a string (type={type(markdown_content)})") # Convert to JSON string as fallback import json markdown_content = f"```json\n{json.dumps(markdown_content, indent=2)}\n```" combined_markdown.append(markdown_content) combined_markdown.append("\n\n---\n\n") # Page break separator full_markdown = "\n".join(combined_markdown) # Convert to HTML html_content = markdown2.markdown( full_markdown, extras=["fenced-code-blocks", "tables", "break-on-newline"] ) # Add cover page timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") full_html = f""" {brand_name} - Innovation Intelligence Report

{brand_name}

Innovation Intelligence Report

Complete Pipeline Analysis

Generated: {timestamp}

{html_content} """ # Determine output path if output_path is None: temp_dir = Path(tempfile.gettempdir()) / "innovation-pdfs" temp_dir.mkdir(exist_ok=True) output_path = temp_dir / f"full_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf" else: output_path = Path(output_path) output_path.parent.mkdir(parents=True, exist_ok=True) # Generate PDF html = HTML(string=full_html) css = CSS(string=PDF_STYLES) html.write_pdf(output_path, stylesheets=[css]) logger.info(f"Combined PDF generated successfully: {output_path}") return output_path except Exception as e: logger.error(f"Combined PDF generation failed: {e}", exc_info=True) raise