Spaces:
Sleeping
Sleeping
| # PDF Generator Module | |
| # Contains create_pdf_document() and all PDF-related functions | |
| import re | |
| import os | |
| import tempfile | |
| import time | |
| import sys | |
| from typing import List, Dict, Any | |
| # Add the parent directory to sys.path to import from models.py | |
| sys.path.append(os.path.dirname(os.path.dirname(__file__))) | |
| from models import ExportRequest | |
| # Import from the same directory (Document Generation) | |
| from document_helpers import ( | |
| filter_empty_sections, | |
| extract_business_name_from_content, | |
| remove_duplicate_headings | |
| ) | |
| from table_of_content import generate_table_of_contents_after_content, add_table_of_contents_page | |
| from chart_generator import parse_embedded_chart_data, create_chart_from_data | |
| from markdown_renderer import ( | |
| clean_text_for_pdf, | |
| render_markdown_to_pdf_grouped | |
| ) | |
| # Add Currency Mapping path | |
| currency_mapping_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'CurrencyMapping') | |
| if currency_mapping_path not in sys.path: | |
| sys.path.append(currency_mapping_path) | |
| from CurrencyMapping.currency_mapping import convert_currency_symbols_to_iso | |
| # Import sort_sections_by_order from utils module | |
| from utils import sort_sections_by_order | |
| def calculate_real_page_numbers(request: ExportRequest, filtered_sections): | |
| """Calculate real page numbers by creating a temporary PDF with all content INCLUDING charts""" | |
| try: | |
| from fpdf2 import FPDF | |
| except ImportError: | |
| try: | |
| from fpdf import FPDF | |
| except ImportError: | |
| raise Exception("Neither fpdf2 nor fpdf package found. Please install fpdf2.") | |
| # Create temporary PDF to calculate page numbers | |
| temp_pdf = FPDF() | |
| temp_pdf.set_auto_page_break(auto=True, margin=15) | |
| # Add cover page | |
| temp_pdf.add_page() | |
| # Add TOC page (placeholder) | |
| temp_pdf.add_page() | |
| # Parse charts from embedded chart data in section content | |
| embedded_charts = parse_embedded_chart_data(filtered_sections) | |
| # Create charts and map them to sections | |
| chart_info_map = {} | |
| if embedded_charts: | |
| for chart_info in embedded_charts: | |
| chart_bytes = create_chart_from_data(chart_info, request.businessIdea or "Business") | |
| if chart_bytes: | |
| source_section = chart_info["source_section"] | |
| if source_section not in chart_info_map: | |
| chart_info_map[source_section] = [] | |
| chart_info_map[source_section].append({ | |
| "chart_bytes": chart_bytes, | |
| "title": chart_info["title"], | |
| "type": chart_info["type"] | |
| }) | |
| # Add all sections to calculate real page numbers INCLUDING charts | |
| section_page_map = {} | |
| sorted_sections = sort_sections_by_order(filtered_sections) | |
| for section in sorted_sections: | |
| temp_pdf.add_page() | |
| current_page = temp_pdf.page_no() | |
| section_page_map[section.title] = current_page | |
| # Don't add section title manually - let the markdown content handle headings | |
| # Add section content | |
| temp_pdf.set_font("Arial", "", 12) | |
| temp_pdf.set_text_color(51, 51, 51) | |
| temp_pdf.set_xy(25, temp_pdf.get_y()) | |
| # Process content | |
| try: | |
| content_text = clean_text_for_pdf(section.content) | |
| except Exception as e: | |
| content_text = section.content or "" | |
| # Remove chart data markers | |
| content_text = re.sub(r'<!--CHARTDATASTART-->.*?<!--CHARTDATAEND-->', '', content_text, flags=re.DOTALL) | |
| content_text = re.sub(r'<!--CHARTDATASTART-->.*$', '', content_text, flags=re.DOTALL) | |
| # No need to filter section titles since we're not adding them manually | |
| # Add content with proper markdown rendering | |
| if content_text and content_text.strip(): | |
| render_markdown_to_pdf_grouped(temp_pdf, content_text, (0,0,0), (75,0,130), (51,51,51)) | |
| # Add charts for this section (same logic as in actual PDF generation) | |
| section_charts = chart_info_map.get(section.title, []) | |
| # Try different matching strategies for charts | |
| if not section_charts: | |
| # Case-insensitive match | |
| for chart_source, charts in chart_info_map.items(): | |
| if chart_source.lower() == section.title.lower(): | |
| section_charts = charts | |
| break | |
| # Partial match | |
| if not section_charts: | |
| for chart_source, charts in chart_info_map.items(): | |
| if (section.title.lower() in chart_source.lower() or | |
| chart_source.lower() in section.title.lower()): | |
| section_charts = charts | |
| break | |
| # Key-based match | |
| if not section_charts and hasattr(section, 'key'): | |
| for chart_source, charts in chart_info_map.items(): | |
| if section.key and section.key.replace('prompt_', '') in chart_source.lower(): | |
| section_charts = charts | |
| break | |
| # Add charts with same page break logic as actual PDF | |
| if section_charts: | |
| charts_per_page = 0 | |
| max_charts_per_page = 5 | |
| for chart_info in section_charts: | |
| try: | |
| chart_height_needed = 140 # Chart height (120) + title (20) + minimal spacing | |
| current_y = temp_pdf.get_y() | |
| charts_per_page += 1 | |
| # Only force page break if absolutely necessary | |
| if (current_y + chart_height_needed > 290) or (charts_per_page > 5): | |
| temp_pdf.add_page() | |
| y_title = 25 | |
| charts_per_page = 1 | |
| else: | |
| y_title = current_y | |
| # Add minimal spacing before chart if not at top of page | |
| if y_title > 25: | |
| temp_pdf.ln(3) | |
| # Chart title is already included in the chart itself, no need for separate title | |
| # Add chart image | |
| img_width = 160 # Much larger chart width | |
| x = (210 - img_width) / 2 | |
| y_img = y_title | |
| # Save image to temp file | |
| with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmp_img: | |
| tmp_img.write(chart_info["chart_bytes"]) | |
| temp_img_path = tmp_img.name | |
| # Add chart | |
| chart_height = 120 # Much larger chart height | |
| temp_pdf.image(temp_img_path, x=x, y=y_img, w=img_width) | |
| temp_pdf.ln(chart_height + 5) | |
| # Clean up temp file | |
| try: | |
| os.remove(temp_img_path) | |
| except Exception: | |
| pass | |
| except Exception: | |
| # Skip faulty chart and continue | |
| continue | |
| return section_page_map | |
| def create_pdf_document(request: ExportRequest): | |
| """Create a PROFESSIONAL PDF document from the business plan data using FPDF2 | |
| NOTE: Now uses matplotlib charts exclusively - old base64 chart logic commented out""" | |
| try: | |
| from fpdf2 import FPDF | |
| except ImportError: | |
| try: | |
| from fpdf import FPDF | |
| except ImportError: | |
| raise Exception("Neither fpdf2 nor fpdf package found. Please install fpdf2.") | |
| # Filter out empty sections to prevent blank pages | |
| try: | |
| filtered_sections = filter_empty_sections(request.sections) | |
| except Exception as e: | |
| raise Exception(f"Failed to filter sections: {str(e)}") | |
| if not filtered_sections: | |
| raise Exception("No valid sections found after filtering. All sections appear to be empty.") | |
| # First pass: Calculate real page numbers by creating a temporary PDF | |
| section_page_map = calculate_real_page_numbers(request, filtered_sections) | |
| # Generate TOC with real page numbers | |
| toc_items = generate_table_of_contents_after_content(filtered_sections, section_page_map, request.businessIdea) | |
| # Second pass: Create final PDF with TOC on page 2 | |
| pdf = FPDF() | |
| pdf.set_auto_page_break(auto=True, margin=15) # Reduced margin to allow more charts per page | |
| # Add confidentiality footer to every page | |
| def add_confidentiality_footer(pdf): | |
| pdf.set_font("Arial", "", 8) | |
| pdf.set_text_color(100, 100, 100) # Gray text | |
| pdf.set_y(-15) # 15mm from bottom | |
| pdf.cell(0, 5, "CONFIDENTIAL - This document contains proprietary information and may not be shared without consent", 0, 0, 'C') | |
| # Override the footer method | |
| pdf.footer = lambda: add_confidentiality_footer(pdf) | |
| pdf.add_page() | |
| # Professional color scheme - Refined for bank/investor presentation | |
| primary_color = (0, 0, 0) # Black - for headings | |
| accent_color = (75, 0, 130) # Navy purple - for accent lines and highlights | |
| text_color = (51, 51, 51) # Dark gray - for body text | |
| light_gray = (221, 221, 221) # Lighter gray - for subtle lines (#DDDDDD) | |
| navy_color = (25, 25, 112) # Navy blue - for TOC and emphasis | |
| # Professional fonts - Arial for body, optional serif for main titles | |
| title_font = 'Arial' # Keep Arial for consistency | |
| body_font = 'Arial' # Modern business feel | |
| # Helper functions for consistent styling | |
| def add_header(pdf, text, size=24, y_offset=25, primary_color=(0,0,0), accent_color=(75,0,130)): | |
| pdf.set_font("Arial", "B", size) | |
| pdf.set_text_color(*primary_color) | |
| pdf.set_xy(25, y_offset) | |
| pdf.cell(0, 18, text, ln=True) | |
| # Calculate text width and make line autofit | |
| text_width = pdf.get_string_width(text) | |
| line_width = min(text_width + 10, 160) # Add 10mm padding, max 160mm (page width - margins) | |
| # Removed purple accent line | |
| return y_offset + 25 | |
| def add_subheader(pdf, text, size=16, y_offset=None, primary_color=(0,0,0)): | |
| if y_offset is None: | |
| y_offset = pdf.get_y() + 10 | |
| pdf.set_font("Arial", "B", size) | |
| pdf.set_text_color(*primary_color) | |
| pdf.set_xy(25, y_offset) | |
| pdf.cell(0, 14, text, ln=True) | |
| # Calculate text width and make line autofit | |
| text_width = pdf.get_string_width(text) | |
| line_width = min(text_width + 8, 160) # Add 8mm padding, max 160mm (page width - margins) | |
| pdf.set_draw_color(221, 221, 221) # Lighter gray (#DDDDDD) for subtle lines | |
| pdf.set_line_width(0.3) # Thinner line | |
| pdf.line(25, y_offset + 14, 25 + line_width, y_offset + 14) | |
| return y_offset + 20 | |
| def add_body_text(pdf, text, y_offset=None, text_color=(51,51,51)): | |
| if y_offset is None: | |
| y_offset = pdf.get_y() + 5 | |
| pdf.set_font("Arial", "", 12) | |
| pdf.set_text_color(*text_color) | |
| pdf.set_xy(25, y_offset) | |
| pdf.multi_cell(160, 8, text, align='L') # Left align to prevent text justification | |
| return pdf.get_y() | |
| def check_page_break(pdf, required_height=120, bottom_margin=300): | |
| """Check if we need a page break and return the Y position to use""" | |
| y = pdf.get_y() | |
| if y + required_height > bottom_margin: | |
| pdf.add_page() | |
| return 25 | |
| return y | |
| # ===== COVER PAGE ===== | |
| def add_cover_page(): | |
| # Clean white background - no purple background | |
| # Add company logo at the top | |
| try: | |
| # Use the MBD logo PNG file - now in app folder for HF deployment | |
| app_root = os.path.dirname(os.path.dirname(__file__)) | |
| logo_path = os.path.join(app_root, 'assets', 'logos', 'MBD logo.png') | |
| if os.path.exists(logo_path): | |
| # Add logo to PDF (centered, top of page) - LOGO FIRST, THEN TITLE BELOW | |
| # Center the logo: (210 - 70) / 2 = 70, so logo starts at x=70 | |
| pdf.image(logo_path, x=70, y=20, w=70) # Even larger size to show full logo | |
| print(f"Logo added successfully from: {logo_path}") | |
| else: | |
| print(f"Logo file not found at: {logo_path}") | |
| except Exception as e: | |
| # Log any errors with logo | |
| print(f"Logo error: {e}") | |
| pass | |
| # Extract business name from content, fallback to businessIdea | |
| extracted_business_name = extract_business_name_from_content(filtered_sections) | |
| business_name = extracted_business_name or request.businessIdea or "Business Plan" | |
| # Company/Project name - DARK TEXT (moved down below logo) | |
| pdf.set_font(title_font, "B", 32) | |
| pdf.set_text_color(0, 0, 0) # Black text for visibility on white background | |
| pdf.ln(60) # More space to move title below logo | |
| pdf.cell(0, 25, business_name, ln=True, align='C') | |
| # Subtitle - DARK GRAY, BOLD, NO ITALICS | |
| pdf.set_font(title_font, "B", 16) # Reduced from 18pt, removed italics | |
| pdf.set_text_color(51, 51, 51) # Dark gray for visibility on white background | |
| pdf.cell(0, 12, "Business Plan", ln=True, align='C') | |
| # Add some space | |
| pdf.ln(40) | |
| pdf.set_font(body_font, "", 12) | |
| pdf.set_text_color(51, 51, 51) # Dark text for content | |
| pdf.set_xy(35, 145) | |
| pdf.cell(0, 8, f"Created: {time.strftime('%B %d, %Y')}", ln=True) | |
| pdf.set_xy(35, 153) | |
| pdf.cell(0, 8, f"Format: {request.format.upper()}", ln=True) | |
| pdf.set_xy(35, 161) | |
| pdf.cell(0, 8, f"Sections: {len(request.sections)}", ln=True) | |
| # Footer - simple footer since confidentiality is now in page footer | |
| pdf.set_font(body_font, "", 10) | |
| pdf.set_text_color(51, 51, 51) # Dark gray text for visibility on white background | |
| pdf.set_xy(0, 280) | |
| pdf.cell(0, 8, "Business Plan Document", ln=True, align='C') | |
| # ===== SECTION PAGES ===== | |
| def add_sections_with_charts(): | |
| # Parse charts from embedded chart data in section content | |
| embedded_charts = parse_embedded_chart_data(filtered_sections) | |
| if embedded_charts: | |
| # Create actual charts from the parsed data | |
| matplotlib_charts = [] | |
| chart_info_map = {} # Map section names to their charts | |
| # logging.info(f"Chart info map keys: {list(chart_info_map.keys())}") | |
| for chart_info in embedded_charts: | |
| chart_bytes = create_chart_from_data(chart_info, request.businessIdea or "Business") | |
| if chart_bytes: | |
| matplotlib_charts.append(chart_bytes) | |
| # Map this chart to its source section | |
| source_section = chart_info["source_section"] | |
| # logging.info(f"Chart '{chart_info['title']}' has source_section: '{source_section}'") | |
| if source_section not in chart_info_map: | |
| chart_info_map[source_section] = [] | |
| chart_info_map[source_section].append({ | |
| "chart_bytes": chart_bytes, | |
| "title": chart_info["title"], | |
| "type": chart_info["type"] | |
| }) | |
| # logging.info(f"Mapping chart '{chart_info['title']}' to source section: '{source_section}'") | |
| # CRITICAL DEBUG: Show the exact mapping being created | |
| # logging.info(f" -> Chart '{chart_info['title']}' mapped to key '{source_section}' in chart_info_map") | |
| else: | |
| pass | |
| # logging.info(f"Total charts created: {len(matplotlib_charts)}") | |
| # logging.info(f"Final chart_info_map: {chart_info_map}") | |
| # Debug: Show all chart mappings | |
| for source_section, charts in chart_info_map.items(): | |
| # logging.info(f"Source section '{source_section}' has {len(charts)} charts:") | |
| for chart in charts: | |
| # logging.info(f" - {chart['title']} ({chart['type']})") | |
| pass | |
| else: | |
| matplotlib_charts = [] | |
| chart_info_map = {} | |
| total_charts_placed = 0 | |
| # Sort sections according to predefined order | |
| sorted_sections = sort_sections_by_order(filtered_sections) | |
| # logging.info(f"Section titles in sorted order: {[s.title for s in sorted_sections]}") | |
| # CRITICAL DEBUG: Show exact section titles and their keys | |
| for i, section in enumerate(sorted_sections): | |
| # logging.info(f"Section {i+1}: '{section.title}' (key: {getattr(section, 'key', 'NO_KEY')})") | |
| pass | |
| for i, section in enumerate(sorted_sections): | |
| # Always start each section on a new page | |
| pdf.add_page() | |
| # Don't add section title manually - let the markdown content handle headings | |
| # Section content (properly positioned) | |
| pdf.set_font(body_font, "", 12) | |
| pdf.set_text_color(*text_color) | |
| pdf.set_xy(25, pdf.get_y()) # Use current Y position for consistent spacing | |
| # Process content | |
| try: | |
| content_text = clean_text_for_pdf(section.content) | |
| except Exception as e: | |
| content_text = section.content or "" | |
| try: | |
| content_text = convert_currency_symbols_to_iso(content_text) | |
| except Exception: | |
| # If currency conversion fails, keep original text | |
| pass | |
| # Remove chart data markers | |
| content_text = re.sub(r'<!--CHARTDATASTART-->.*?<!--CHARTDATAEND-->', '', content_text, flags=re.DOTALL) | |
| content_text = re.sub(r'<!--CHARTDATASTART-->.*$', '', content_text, flags=re.DOTALL) | |
| # Remove chart titles from content to prevent duplicate headings | |
| # Get chart titles for this section | |
| section_charts = chart_info_map.get(section.title, []) | |
| if not section_charts: | |
| # Try case-insensitive match | |
| for chart_source, charts in chart_info_map.items(): | |
| if chart_source.lower() == section.title.lower(): | |
| section_charts = charts | |
| break | |
| # Remove chart titles from content | |
| for chart_info in section_charts: | |
| chart_title = chart_info["title"] | |
| # Remove various markdown heading formats for the chart title | |
| patterns_to_remove = [ | |
| rf'^#+\s*{re.escape(chart_title)}\s*$', # # Title, ## Title, etc. | |
| rf'^\*\*{re.escape(chart_title)}\*\*\s*$', # **Title** | |
| rf'^{re.escape(chart_title)}\s*$', # Just the title | |
| ] | |
| for pattern in patterns_to_remove: | |
| content_text = re.sub(pattern, '', content_text, flags=re.MULTILINE | re.IGNORECASE) | |
| # No need to filter section titles since we're not adding them manually | |
| # Skip enhance_subheadings_with_bold for grouped rendering to preserve ** markers | |
| # content_text = enhance_subheadings_with_bold(content_text) | |
| content_text = remove_duplicate_headings(content_text) | |
| # Add content with proper markdown rendering using grouped approach | |
| if content_text and content_text.strip(): | |
| render_markdown_to_pdf_grouped(pdf, content_text, primary_color, accent_color, text_color) | |
| else: | |
| # Add placeholder text to prevent blank page | |
| pdf.multi_cell(160, 8, f"Content for {section.title} section is being processed...", align='L') | |
| # Add visual separation between content and charts | |
| if content_text and content_text.strip(): | |
| pdf.ln(15) # More generous spacing for better visual separation | |
| # ---- Charts for this section ---- | |
| charts_added = 0 # IMPORTANT: initialize to avoid NameError | |
| # CRITICAL DEBUG: Show exactly what charts exist and their source_section values | |
| if section.title == "Financial Plan & Funding": | |
| # logging.info(f"=== LAST SECTION DEBUG ===") | |
| # logging.info(f"Section title: '{section.title}'") | |
| # logging.info(f"All chart_info_map keys: {list(chart_info_map.keys())}") | |
| for key, charts in chart_info_map.items(): | |
| # logging.info(f" Key '{key}' has {len(charts)} charts") | |
| for chart in charts: | |
| # logging.info(f" Chart: {chart['title']} ({chart['type']})") | |
| pass | |
| # logging.info(f"=== END LAST SECTION DEBUG ===") | |
| pass | |
| section_charts = chart_info_map.get(section.title, []) | |
| # logging.info(f"Section '{section.title}': Direct lookup found {len(section_charts)} charts") | |
| # Strategy 1: Exact title match | |
| if not section_charts: | |
| # logging.info(f"No exact match found for '{section.title}'") | |
| pass | |
| # Strategy 2: Case-insensitive match | |
| for chart_source, charts in chart_info_map.items(): | |
| if chart_source.lower() == section.title.lower(): | |
| section_charts = charts | |
| # logging.info(f"Found charts via case-insensitive match: '{chart_source}' -> {len(charts)} charts") | |
| break | |
| # Strategy 3: Partial match | |
| if not section_charts: | |
| for chart_source, charts in chart_info_map.items(): | |
| if (section.title.lower() in chart_source.lower() or | |
| chart_source.lower() in section.title.lower()): | |
| section_charts = charts | |
| # logging.info(f"Found charts via partial match: '{chart_source}' -> {len(charts)} charts") | |
| break | |
| # Strategy 4: Key-based match (for prompt_ prefixed sections) | |
| if not section_charts and hasattr(section, 'key'): | |
| for chart_source, charts in chart_info_map.items(): | |
| if section.key and section.key.replace('prompt_', '') in chart_source.lower(): | |
| section_charts = charts | |
| # logging.info(f"Found charts via key match: '{section.key}' -> '{chart_source}' -> {len(charts)} charts") | |
| break | |
| # logging.info(f"Section '{section.title}': Final chart count: {len(section_charts)}") | |
| if section_charts: | |
| # Group charts that can fit on the same page | |
| charts_per_page = 0 | |
| max_charts_per_page = 5 # Allow up to 5 charts per page | |
| for chart_info in section_charts: | |
| try: | |
| # Check if we need a new page for this chart | |
| chart_height_needed = 140 # Chart height (120) + title (20) + minimal spacing | |
| # Smart page break logic - allow multiple charts per page | |
| current_y = pdf.get_y() | |
| charts_per_page += 1 | |
| # Only force page break if absolutely necessary - allow up to 5 charts per page | |
| if (current_y + chart_height_needed > 290) or (charts_per_page > 5): | |
| pdf.add_page() | |
| # Header/footer removed for now | |
| y_title = 25 # Start at normal position | |
| charts_per_page = 1 # Reset counter for new page | |
| else: | |
| y_title = current_y | |
| # Add minimal spacing before chart if not at top of page | |
| if y_title > 25: # If not at top of page, add minimal space | |
| pdf.ln(3) # Minimal spacing to fit more charts per page | |
| # Chart title is already included in the chart itself, no need for separate subheader | |
| # Centered image with larger dimensions for better visibility | |
| img_width = 160 # Much larger chart width for better visibility | |
| x = (210 - img_width) / 2 | |
| y_img = y_title | |
| # Save image to temp file | |
| with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmp_img: | |
| tmp_img.write(chart_info["chart_bytes"]) | |
| temp_img_path = tmp_img.name | |
| # Chart dimensions (no border needed) | |
| chart_height = 120 # Much larger height for better visibility | |
| chart_width = img_width | |
| # Image | |
| pdf.image(temp_img_path, x=x, y=y_img, w=chart_width) | |
| # Move below image and clean up (spacing based on actual chart height) | |
| pdf.ln(chart_height + 5) # Chart height (120) + minimal spacing between charts | |
| try: | |
| os.remove(temp_img_path) | |
| except Exception: | |
| pass | |
| charts_added += 1 | |
| total_charts_placed += 1 | |
| except Exception: | |
| # Skip faulty chart and continue rendering others | |
| continue | |
| else: | |
| # Debug: Check if there are any charts that might match this section | |
| potential_matches = [] | |
| for chart_source, charts in chart_info_map.items(): | |
| if (section.title.lower() in chart_source.lower() or | |
| chart_source.lower() in section.title.lower()): | |
| potential_matches.append(f"'{chart_source}' -> {len(charts)} charts") | |
| if potential_matches: | |
| # logging.info(f"Section '{section.title}': Potential chart matches: {potential_matches}") | |
| pass | |
| else: | |
| # logging.info(f"Section '{section.title}': No charts found and no potential matches") | |
| pass | |
| # Each section is already on its own page, no need for complex page break logic | |
| # ===== BUILD THE PDF ===== | |
| try: | |
| # Add all sections | |
| try: | |
| add_cover_page() | |
| except Exception as e: | |
| raise Exception(f"Cover page failed: {str(e)}") | |
| # Add TOC on page 2 with REAL page numbers (already calculated above) | |
| try: | |
| if toc_items: | |
| add_table_of_contents_page(pdf, toc_items, request.businessIdea) | |
| else: | |
| # Add a basic TOC page if no items were generated | |
| pdf.add_page() | |
| pdf.set_font("Arial", "B", 20) | |
| pdf.set_text_color(0, 0, 0) | |
| pdf.set_xy(25, 25) | |
| pdf.cell(0, 18, "Table of Contents", ln=True) | |
| pdf.set_font("Arial", "", 12) | |
| pdf.set_text_color(51, 51, 51) | |
| pdf.set_xy(25, 50) | |
| pdf.multi_cell(160, 8, "No sections available for table of contents.", align='L') | |
| except Exception as e: | |
| # logging.error(f"Table of contents failed: {str(e)}") | |
| pass | |
| # Add a basic TOC page as fallback | |
| pdf.add_page() | |
| pdf.set_font("Arial", "B", 20) | |
| pdf.set_text_color(0, 0, 0) | |
| pdf.set_xy(25, 25) | |
| pdf.cell(0, 18, "Table of Contents", ln=True) | |
| pdf.set_font("Arial", "", 12) | |
| pdf.set_text_color(51, 51, 51) | |
| pdf.set_xy(25, 50) | |
| pdf.multi_cell(160, 8, "Table of contents could not be generated.", align='L') | |
| # Executive summary is now handled as part of the sections | |
| # No separate page needed - the first section contains the executive summary | |
| try: | |
| add_sections_with_charts() | |
| except Exception as e: | |
| raise Exception(f"Sections with charts failed: {str(e)}") | |
| # CRITICAL CHECK: Ensure we have content in the PDF | |
| total_pages = pdf.page_no() | |
| if total_pages < 2: # Should have at least cover + 1 content page | |
| # Add a content page to prevent blank PDF | |
| pdf.add_page() | |
| pdf.set_font("Arial", "B", 16) | |
| pdf.set_text_color(255, 0, 0) | |
| pdf.cell(0, 20, "Content Processing Issue", ln=True, align='C') | |
| pdf.set_font("Arial", "", 12) | |
| pdf.set_text_color(0, 0, 0) | |
| pdf.cell(0, 10, "The PDF generation encountered an issue with content processing.", ln=True, align='C') | |
| pdf.cell(0, 10, "Please check the server logs for detailed information.", ln=True, align='C') | |
| # Get PDF output and convert to bytes | |
| try: | |
| pdf_output = pdf.output(dest='S') | |
| if isinstance(pdf_output, bytearray): | |
| pdf_bytes = bytes(pdf_output) | |
| else: | |
| pdf_bytes = pdf_output.encode('latin-1') if isinstance(pdf_output, str) else pdf_output | |
| # CRITICAL SAFETY CHECK: Ensure PDF is not empty | |
| if len(pdf_bytes) < 1000: # PDF should be at least 1KB | |
| # Try to add some content to prevent blank PDF | |
| pdf.add_page() | |
| pdf.set_font("Arial", "B", 16) | |
| pdf.set_text_color(255, 0, 0) # Red text | |
| pdf.cell(0, 20, "PDF Generation Issue Detected", ln=True, align='C') | |
| pdf.set_font("Arial", "", 12) | |
| pdf.set_text_color(0, 0, 0) | |
| pdf.cell(0, 10, "If you see this message, there was an issue with content processing.", ln=True, align='C') | |
| pdf.cell(0, 10, "Please check the server logs for details.", ln=True, align='C') | |
| # Regenerate PDF with error message | |
| pdf_output = pdf.output(dest='S') | |
| if isinstance(pdf_output, bytearray): | |
| pdf_bytes = bytes(pdf_output) | |
| else: | |
| pdf_bytes = pdf_output.encode('latin-1') if isinstance(pdf_output, str) else pdf_output | |
| return pdf_bytes, f"business_plan_{request.businessIdea or 'export'}.pdf" | |
| except Exception as e: | |
| raise Exception(f"PDF conversion failed: {str(e)}") | |
| except Exception as e: | |
| # logging.error(f"PDF generation failed: {e}") | |
| raise Exception(f"Failed to generate professional PDF: {str(e)}") | |