Spaces:
Runtime error
Runtime error
Merge branch 'live-test-branch'
Browse files
app/DocumentGeneration/pdf_generator.py
ADDED
|
@@ -0,0 +1,682 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# PDF Generator Module
|
| 2 |
+
# Contains create_pdf_document() and all PDF-related functions
|
| 3 |
+
|
| 4 |
+
import re
|
| 5 |
+
import os
|
| 6 |
+
import tempfile
|
| 7 |
+
import time
|
| 8 |
+
import sys
|
| 9 |
+
from typing import List, Dict, Any
|
| 10 |
+
|
| 11 |
+
# Add the parent directory to sys.path to import from models.py
|
| 12 |
+
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
|
| 13 |
+
from models import ExportRequest
|
| 14 |
+
|
| 15 |
+
# Import from the same directory (Document Generation)
|
| 16 |
+
from document_helpers import (
|
| 17 |
+
filter_empty_sections,
|
| 18 |
+
extract_business_name_from_content,
|
| 19 |
+
remove_duplicate_headings
|
| 20 |
+
)
|
| 21 |
+
from table_of_content import generate_table_of_contents_after_content, add_table_of_contents_page
|
| 22 |
+
from chart_generator import parse_embedded_chart_data, create_chart_from_data
|
| 23 |
+
from markdown_renderer import (
|
| 24 |
+
clean_text_for_pdf,
|
| 25 |
+
render_markdown_to_pdf_grouped
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
# Add Currency Mapping path
|
| 29 |
+
currency_mapping_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'Currency Mapping')
|
| 30 |
+
if currency_mapping_path not in sys.path:
|
| 31 |
+
sys.path.append(currency_mapping_path)
|
| 32 |
+
from CurrencyMapping.currency_mapping import convert_currency_symbols_to_iso
|
| 33 |
+
|
| 34 |
+
# Import sort_sections_by_order from utils module
|
| 35 |
+
from utils import sort_sections_by_order
|
| 36 |
+
|
| 37 |
+
def calculate_real_page_numbers(request: ExportRequest, filtered_sections):
|
| 38 |
+
"""Calculate real page numbers by creating a temporary PDF with all content INCLUDING charts"""
|
| 39 |
+
try:
|
| 40 |
+
from fpdf import FPDF
|
| 41 |
+
except ImportError:
|
| 42 |
+
raise Exception("fpdf2 is not installed. Please install it with: pip install fpdf2")
|
| 43 |
+
|
| 44 |
+
# Create temporary PDF to calculate page numbers
|
| 45 |
+
temp_pdf = FPDF()
|
| 46 |
+
temp_pdf.set_auto_page_break(auto=True, margin=15)
|
| 47 |
+
|
| 48 |
+
# Add cover page
|
| 49 |
+
temp_pdf.add_page()
|
| 50 |
+
|
| 51 |
+
# Add TOC page (placeholder)
|
| 52 |
+
temp_pdf.add_page()
|
| 53 |
+
|
| 54 |
+
# Parse charts from embedded chart data in section content
|
| 55 |
+
embedded_charts = parse_embedded_chart_data(filtered_sections)
|
| 56 |
+
|
| 57 |
+
# Create charts and map them to sections
|
| 58 |
+
chart_info_map = {}
|
| 59 |
+
if embedded_charts:
|
| 60 |
+
for chart_info in embedded_charts:
|
| 61 |
+
chart_bytes = create_chart_from_data(chart_info, request.businessIdea or "Business")
|
| 62 |
+
if chart_bytes:
|
| 63 |
+
source_section = chart_info["source_section"]
|
| 64 |
+
if source_section not in chart_info_map:
|
| 65 |
+
chart_info_map[source_section] = []
|
| 66 |
+
chart_info_map[source_section].append({
|
| 67 |
+
"chart_bytes": chart_bytes,
|
| 68 |
+
"title": chart_info["title"],
|
| 69 |
+
"type": chart_info["type"]
|
| 70 |
+
})
|
| 71 |
+
|
| 72 |
+
# Add all sections to calculate real page numbers INCLUDING charts
|
| 73 |
+
section_page_map = {}
|
| 74 |
+
sorted_sections = sort_sections_by_order(filtered_sections)
|
| 75 |
+
|
| 76 |
+
for section in sorted_sections:
|
| 77 |
+
temp_pdf.add_page()
|
| 78 |
+
current_page = temp_pdf.page_no()
|
| 79 |
+
section_page_map[section.title] = current_page
|
| 80 |
+
|
| 81 |
+
# Don't add section title manually - let the markdown content handle headings
|
| 82 |
+
|
| 83 |
+
# Add section content
|
| 84 |
+
temp_pdf.set_font("Arial", "", 12)
|
| 85 |
+
temp_pdf.set_text_color(51, 51, 51)
|
| 86 |
+
temp_pdf.set_xy(25, temp_pdf.get_y())
|
| 87 |
+
|
| 88 |
+
# Process content
|
| 89 |
+
try:
|
| 90 |
+
content_text = clean_text_for_pdf(section.content)
|
| 91 |
+
except Exception as e:
|
| 92 |
+
content_text = section.content or ""
|
| 93 |
+
|
| 94 |
+
# Remove chart data markers
|
| 95 |
+
content_text = re.sub(r'<!--CHARTDATASTART-->.*?<!--CHARTDATAEND-->', '', content_text, flags=re.DOTALL)
|
| 96 |
+
content_text = re.sub(r'<!--CHARTDATASTART-->.*$', '', content_text, flags=re.DOTALL)
|
| 97 |
+
|
| 98 |
+
# No need to filter section titles since we're not adding them manually
|
| 99 |
+
|
| 100 |
+
# Add content with proper markdown rendering
|
| 101 |
+
if content_text and content_text.strip():
|
| 102 |
+
render_markdown_to_pdf_grouped(temp_pdf, content_text, (0,0,0), (75,0,130), (51,51,51))
|
| 103 |
+
|
| 104 |
+
# Add charts for this section (same logic as in actual PDF generation)
|
| 105 |
+
section_charts = chart_info_map.get(section.title, [])
|
| 106 |
+
|
| 107 |
+
# Try different matching strategies for charts
|
| 108 |
+
if not section_charts:
|
| 109 |
+
# Case-insensitive match
|
| 110 |
+
for chart_source, charts in chart_info_map.items():
|
| 111 |
+
if chart_source.lower() == section.title.lower():
|
| 112 |
+
section_charts = charts
|
| 113 |
+
break
|
| 114 |
+
|
| 115 |
+
# Partial match
|
| 116 |
+
if not section_charts:
|
| 117 |
+
for chart_source, charts in chart_info_map.items():
|
| 118 |
+
if (section.title.lower() in chart_source.lower() or
|
| 119 |
+
chart_source.lower() in section.title.lower()):
|
| 120 |
+
section_charts = charts
|
| 121 |
+
break
|
| 122 |
+
|
| 123 |
+
# Key-based match
|
| 124 |
+
if not section_charts and hasattr(section, 'key'):
|
| 125 |
+
for chart_source, charts in chart_info_map.items():
|
| 126 |
+
if section.key and section.key.replace('prompt_', '') in chart_source.lower():
|
| 127 |
+
section_charts = charts
|
| 128 |
+
break
|
| 129 |
+
|
| 130 |
+
# Add charts with same page break logic as actual PDF
|
| 131 |
+
if section_charts:
|
| 132 |
+
charts_per_page = 0
|
| 133 |
+
max_charts_per_page = 5
|
| 134 |
+
|
| 135 |
+
for chart_info in section_charts:
|
| 136 |
+
try:
|
| 137 |
+
chart_height_needed = 140 # Chart height (120) + title (20) + minimal spacing
|
| 138 |
+
current_y = temp_pdf.get_y()
|
| 139 |
+
charts_per_page += 1
|
| 140 |
+
|
| 141 |
+
# Only force page break if absolutely necessary
|
| 142 |
+
if (current_y + chart_height_needed > 290) or (charts_per_page > 5):
|
| 143 |
+
temp_pdf.add_page()
|
| 144 |
+
y_title = 25
|
| 145 |
+
charts_per_page = 1
|
| 146 |
+
else:
|
| 147 |
+
y_title = current_y
|
| 148 |
+
|
| 149 |
+
# Add minimal spacing before chart if not at top of page
|
| 150 |
+
if y_title > 25:
|
| 151 |
+
temp_pdf.ln(3)
|
| 152 |
+
|
| 153 |
+
# Chart title is already included in the chart itself, no need for separate title
|
| 154 |
+
|
| 155 |
+
# Add chart image
|
| 156 |
+
img_width = 160 # Much larger chart width
|
| 157 |
+
x = (210 - img_width) / 2
|
| 158 |
+
y_img = y_title
|
| 159 |
+
|
| 160 |
+
# Save image to temp file
|
| 161 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmp_img:
|
| 162 |
+
tmp_img.write(chart_info["chart_bytes"])
|
| 163 |
+
temp_img_path = tmp_img.name
|
| 164 |
+
|
| 165 |
+
# Add chart
|
| 166 |
+
chart_height = 120 # Much larger chart height
|
| 167 |
+
temp_pdf.image(temp_img_path, x=x, y=y_img, w=img_width)
|
| 168 |
+
temp_pdf.ln(chart_height + 5)
|
| 169 |
+
|
| 170 |
+
# Clean up temp file
|
| 171 |
+
try:
|
| 172 |
+
os.remove(temp_img_path)
|
| 173 |
+
except Exception:
|
| 174 |
+
pass
|
| 175 |
+
|
| 176 |
+
except Exception:
|
| 177 |
+
# Skip faulty chart and continue
|
| 178 |
+
continue
|
| 179 |
+
|
| 180 |
+
return section_page_map
|
| 181 |
+
|
| 182 |
+
def create_pdf_document(request: ExportRequest):
|
| 183 |
+
"""Create a PROFESSIONAL PDF document from the business plan data using FPDF2
|
| 184 |
+
NOTE: Now uses matplotlib charts exclusively - old base64 chart logic commented out"""
|
| 185 |
+
try:
|
| 186 |
+
from fpdf import FPDF
|
| 187 |
+
except ImportError:
|
| 188 |
+
raise Exception("fpdf2 is not installed. Please install it with: pip install fpdf2")
|
| 189 |
+
|
| 190 |
+
# Filter out empty sections to prevent blank pages
|
| 191 |
+
try:
|
| 192 |
+
filtered_sections = filter_empty_sections(request.sections)
|
| 193 |
+
except Exception as e:
|
| 194 |
+
raise Exception(f"Failed to filter sections: {str(e)}")
|
| 195 |
+
if not filtered_sections:
|
| 196 |
+
raise Exception("No valid sections found after filtering. All sections appear to be empty.")
|
| 197 |
+
|
| 198 |
+
# First pass: Calculate real page numbers by creating a temporary PDF
|
| 199 |
+
section_page_map = calculate_real_page_numbers(request, filtered_sections)
|
| 200 |
+
|
| 201 |
+
# Generate TOC with real page numbers
|
| 202 |
+
toc_items = generate_table_of_contents_after_content(filtered_sections, section_page_map, request.businessIdea)
|
| 203 |
+
|
| 204 |
+
# Second pass: Create final PDF with TOC on page 2
|
| 205 |
+
pdf = FPDF()
|
| 206 |
+
pdf.set_auto_page_break(auto=True, margin=15) # Reduced margin to allow more charts per page
|
| 207 |
+
|
| 208 |
+
# Add confidentiality footer to every page
|
| 209 |
+
def add_confidentiality_footer(pdf):
|
| 210 |
+
pdf.set_font("Arial", "", 8)
|
| 211 |
+
pdf.set_text_color(100, 100, 100) # Gray text
|
| 212 |
+
pdf.set_y(-15) # 15mm from bottom
|
| 213 |
+
pdf.cell(0, 5, "CONFIDENTIAL - This document contains proprietary information and may not be shared without consent", 0, 0, 'C')
|
| 214 |
+
|
| 215 |
+
# Override the footer method
|
| 216 |
+
pdf.footer = lambda: add_confidentiality_footer(pdf)
|
| 217 |
+
pdf.add_page()
|
| 218 |
+
|
| 219 |
+
# Professional color scheme - Refined for bank/investor presentation
|
| 220 |
+
primary_color = (0, 0, 0) # Black - for headings
|
| 221 |
+
accent_color = (75, 0, 130) # Navy purple - for accent lines and highlights
|
| 222 |
+
text_color = (51, 51, 51) # Dark gray - for body text
|
| 223 |
+
light_gray = (221, 221, 221) # Lighter gray - for subtle lines (#DDDDDD)
|
| 224 |
+
navy_color = (25, 25, 112) # Navy blue - for TOC and emphasis
|
| 225 |
+
|
| 226 |
+
# Professional fonts - Arial for body, optional serif for main titles
|
| 227 |
+
title_font = 'Arial' # Keep Arial for consistency
|
| 228 |
+
body_font = 'Arial' # Modern business feel
|
| 229 |
+
|
| 230 |
+
# Helper functions for consistent styling
|
| 231 |
+
def add_header(pdf, text, size=24, y_offset=25, primary_color=(0,0,0), accent_color=(75,0,130)):
|
| 232 |
+
pdf.set_font("Arial", "B", size)
|
| 233 |
+
pdf.set_text_color(*primary_color)
|
| 234 |
+
pdf.set_xy(25, y_offset)
|
| 235 |
+
pdf.cell(0, 18, text, ln=True)
|
| 236 |
+
|
| 237 |
+
# Calculate text width and make line autofit
|
| 238 |
+
text_width = pdf.get_string_width(text)
|
| 239 |
+
line_width = min(text_width + 10, 160) # Add 10mm padding, max 160mm (page width - margins)
|
| 240 |
+
|
| 241 |
+
# Removed purple accent line
|
| 242 |
+
return y_offset + 25
|
| 243 |
+
|
| 244 |
+
def add_subheader(pdf, text, size=16, y_offset=None, primary_color=(0,0,0)):
|
| 245 |
+
if y_offset is None:
|
| 246 |
+
y_offset = pdf.get_y() + 10
|
| 247 |
+
pdf.set_font("Arial", "B", size)
|
| 248 |
+
pdf.set_text_color(*primary_color)
|
| 249 |
+
pdf.set_xy(25, y_offset)
|
| 250 |
+
pdf.cell(0, 14, text, ln=True)
|
| 251 |
+
|
| 252 |
+
# Calculate text width and make line autofit
|
| 253 |
+
text_width = pdf.get_string_width(text)
|
| 254 |
+
line_width = min(text_width + 8, 160) # Add 8mm padding, max 160mm (page width - margins)
|
| 255 |
+
|
| 256 |
+
pdf.set_draw_color(221, 221, 221) # Lighter gray (#DDDDDD) for subtle lines
|
| 257 |
+
pdf.set_line_width(0.3) # Thinner line
|
| 258 |
+
pdf.line(25, y_offset + 14, 25 + line_width, y_offset + 14)
|
| 259 |
+
return y_offset + 20
|
| 260 |
+
|
| 261 |
+
def add_body_text(pdf, text, y_offset=None, text_color=(51,51,51)):
|
| 262 |
+
if y_offset is None:
|
| 263 |
+
y_offset = pdf.get_y() + 5
|
| 264 |
+
pdf.set_font("Arial", "", 12)
|
| 265 |
+
pdf.set_text_color(*text_color)
|
| 266 |
+
pdf.set_xy(25, y_offset)
|
| 267 |
+
pdf.multi_cell(160, 8, text, align='L') # Left align to prevent text justification
|
| 268 |
+
return pdf.get_y()
|
| 269 |
+
|
| 270 |
+
def check_page_break(pdf, required_height=120, bottom_margin=300):
|
| 271 |
+
"""Check if we need a page break and return the Y position to use"""
|
| 272 |
+
y = pdf.get_y()
|
| 273 |
+
if y + required_height > bottom_margin:
|
| 274 |
+
pdf.add_page()
|
| 275 |
+
return 25
|
| 276 |
+
return y
|
| 277 |
+
|
| 278 |
+
# ===== COVER PAGE =====
|
| 279 |
+
def add_cover_page():
|
| 280 |
+
# Clean white background - no purple background
|
| 281 |
+
|
| 282 |
+
# Add company logo at the top
|
| 283 |
+
try:
|
| 284 |
+
# Use the MBD logo PNG file - now in app folder for HF deployment
|
| 285 |
+
app_root = os.path.dirname(os.path.dirname(__file__))
|
| 286 |
+
logo_path = os.path.join(app_root, 'assets', 'logos', 'MBD logo.png')
|
| 287 |
+
|
| 288 |
+
if os.path.exists(logo_path):
|
| 289 |
+
# Add logo to PDF (centered, top of page) - LOGO FIRST, THEN TITLE BELOW
|
| 290 |
+
# Center the logo: (210 - 70) / 2 = 70, so logo starts at x=70
|
| 291 |
+
pdf.image(logo_path, x=70, y=20, w=70) # Even larger size to show full logo
|
| 292 |
+
print(f"Logo added successfully from: {logo_path}")
|
| 293 |
+
else:
|
| 294 |
+
print(f"Logo file not found at: {logo_path}")
|
| 295 |
+
except Exception as e:
|
| 296 |
+
# Log any errors with logo
|
| 297 |
+
print(f"Logo error: {e}")
|
| 298 |
+
pass
|
| 299 |
+
|
| 300 |
+
# Extract business name from content, fallback to businessIdea
|
| 301 |
+
extracted_business_name = extract_business_name_from_content(filtered_sections)
|
| 302 |
+
business_name = extracted_business_name or request.businessIdea or "Business Plan"
|
| 303 |
+
|
| 304 |
+
# Company/Project name - DARK TEXT (moved down below logo)
|
| 305 |
+
pdf.set_font(title_font, "B", 32)
|
| 306 |
+
pdf.set_text_color(0, 0, 0) # Black text for visibility on white background
|
| 307 |
+
pdf.ln(60) # More space to move title below logo
|
| 308 |
+
pdf.cell(0, 25, business_name, ln=True, align='C')
|
| 309 |
+
|
| 310 |
+
# Subtitle - DARK GRAY, BOLD, NO ITALICS
|
| 311 |
+
pdf.set_font(title_font, "B", 16) # Reduced from 18pt, removed italics
|
| 312 |
+
pdf.set_text_color(51, 51, 51) # Dark gray for visibility on white background
|
| 313 |
+
pdf.cell(0, 12, "Business Plan", ln=True, align='C')
|
| 314 |
+
|
| 315 |
+
# Add some space
|
| 316 |
+
pdf.ln(40)
|
| 317 |
+
|
| 318 |
+
|
| 319 |
+
pdf.set_font(body_font, "", 12)
|
| 320 |
+
pdf.set_text_color(51, 51, 51) # Dark text for content
|
| 321 |
+
pdf.set_xy(35, 145)
|
| 322 |
+
pdf.cell(0, 8, f"Created: {time.strftime('%B %d, %Y')}", ln=True)
|
| 323 |
+
pdf.set_xy(35, 153)
|
| 324 |
+
pdf.cell(0, 8, f"Format: {request.format.upper()}", ln=True)
|
| 325 |
+
pdf.set_xy(35, 161)
|
| 326 |
+
pdf.cell(0, 8, f"Sections: {len(request.sections)}", ln=True)
|
| 327 |
+
|
| 328 |
+
# Footer - simple footer since confidentiality is now in page footer
|
| 329 |
+
pdf.set_font(body_font, "", 10)
|
| 330 |
+
pdf.set_text_color(51, 51, 51) # Dark gray text for visibility on white background
|
| 331 |
+
pdf.set_xy(0, 280)
|
| 332 |
+
pdf.cell(0, 8, "Business Plan Document", ln=True, align='C')
|
| 333 |
+
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
# ===== SECTION PAGES =====
|
| 337 |
+
def add_sections_with_charts():
|
| 338 |
+
# Parse charts from embedded chart data in section content
|
| 339 |
+
embedded_charts = parse_embedded_chart_data(filtered_sections)
|
| 340 |
+
|
| 341 |
+
if embedded_charts:
|
| 342 |
+
# Create actual charts from the parsed data
|
| 343 |
+
matplotlib_charts = []
|
| 344 |
+
chart_info_map = {} # Map section names to their charts
|
| 345 |
+
|
| 346 |
+
# logging.info(f"Chart info map keys: {list(chart_info_map.keys())}")
|
| 347 |
+
|
| 348 |
+
for chart_info in embedded_charts:
|
| 349 |
+
chart_bytes = create_chart_from_data(chart_info, request.businessIdea or "Business")
|
| 350 |
+
if chart_bytes:
|
| 351 |
+
matplotlib_charts.append(chart_bytes)
|
| 352 |
+
# Map this chart to its source section
|
| 353 |
+
source_section = chart_info["source_section"]
|
| 354 |
+
# logging.info(f"Chart '{chart_info['title']}' has source_section: '{source_section}'")
|
| 355 |
+
|
| 356 |
+
if source_section not in chart_info_map:
|
| 357 |
+
chart_info_map[source_section] = []
|
| 358 |
+
chart_info_map[source_section].append({
|
| 359 |
+
"chart_bytes": chart_bytes,
|
| 360 |
+
"title": chart_info["title"],
|
| 361 |
+
"type": chart_info["type"]
|
| 362 |
+
})
|
| 363 |
+
# logging.info(f"Mapping chart '{chart_info['title']}' to source section: '{source_section}'")
|
| 364 |
+
|
| 365 |
+
# CRITICAL DEBUG: Show the exact mapping being created
|
| 366 |
+
# logging.info(f" -> Chart '{chart_info['title']}' mapped to key '{source_section}' in chart_info_map")
|
| 367 |
+
else:
|
| 368 |
+
pass
|
| 369 |
+
|
| 370 |
+
# logging.info(f"Total charts created: {len(matplotlib_charts)}")
|
| 371 |
+
# logging.info(f"Final chart_info_map: {chart_info_map}")
|
| 372 |
+
|
| 373 |
+
# Debug: Show all chart mappings
|
| 374 |
+
for source_section, charts in chart_info_map.items():
|
| 375 |
+
# logging.info(f"Source section '{source_section}' has {len(charts)} charts:")
|
| 376 |
+
for chart in charts:
|
| 377 |
+
# logging.info(f" - {chart['title']} ({chart['type']})")
|
| 378 |
+
pass
|
| 379 |
+
|
| 380 |
+
else:
|
| 381 |
+
matplotlib_charts = []
|
| 382 |
+
chart_info_map = {}
|
| 383 |
+
|
| 384 |
+
total_charts_placed = 0
|
| 385 |
+
|
| 386 |
+
# Sort sections according to predefined order
|
| 387 |
+
sorted_sections = sort_sections_by_order(filtered_sections)
|
| 388 |
+
# logging.info(f"Section titles in sorted order: {[s.title for s in sorted_sections]}")
|
| 389 |
+
|
| 390 |
+
# CRITICAL DEBUG: Show exact section titles and their keys
|
| 391 |
+
for i, section in enumerate(sorted_sections):
|
| 392 |
+
# logging.info(f"Section {i+1}: '{section.title}' (key: {getattr(section, 'key', 'NO_KEY')})")
|
| 393 |
+
pass
|
| 394 |
+
|
| 395 |
+
for i, section in enumerate(sorted_sections):
|
| 396 |
+
|
| 397 |
+
# Always start each section on a new page
|
| 398 |
+
pdf.add_page()
|
| 399 |
+
|
| 400 |
+
# Don't add section title manually - let the markdown content handle headings
|
| 401 |
+
|
| 402 |
+
# Section content (properly positioned)
|
| 403 |
+
pdf.set_font(body_font, "", 12)
|
| 404 |
+
pdf.set_text_color(*text_color)
|
| 405 |
+
pdf.set_xy(25, pdf.get_y()) # Use current Y position for consistent spacing
|
| 406 |
+
|
| 407 |
+
# Process content
|
| 408 |
+
try:
|
| 409 |
+
content_text = clean_text_for_pdf(section.content)
|
| 410 |
+
except Exception as e:
|
| 411 |
+
content_text = section.content or ""
|
| 412 |
+
try:
|
| 413 |
+
content_text = convert_currency_symbols_to_iso(content_text)
|
| 414 |
+
except Exception:
|
| 415 |
+
# If currency conversion fails, keep original text
|
| 416 |
+
pass
|
| 417 |
+
|
| 418 |
+
# Remove chart data markers
|
| 419 |
+
content_text = re.sub(r'<!--CHARTDATASTART-->.*?<!--CHARTDATAEND-->', '', content_text, flags=re.DOTALL)
|
| 420 |
+
content_text = re.sub(r'<!--CHARTDATASTART-->.*$', '', content_text, flags=re.DOTALL)
|
| 421 |
+
|
| 422 |
+
# Remove chart titles from content to prevent duplicate headings
|
| 423 |
+
# Get chart titles for this section
|
| 424 |
+
section_charts = chart_info_map.get(section.title, [])
|
| 425 |
+
if not section_charts:
|
| 426 |
+
# Try case-insensitive match
|
| 427 |
+
for chart_source, charts in chart_info_map.items():
|
| 428 |
+
if chart_source.lower() == section.title.lower():
|
| 429 |
+
section_charts = charts
|
| 430 |
+
break
|
| 431 |
+
|
| 432 |
+
# Remove chart titles from content
|
| 433 |
+
for chart_info in section_charts:
|
| 434 |
+
chart_title = chart_info["title"]
|
| 435 |
+
# Remove various markdown heading formats for the chart title
|
| 436 |
+
patterns_to_remove = [
|
| 437 |
+
rf'^#+\s*{re.escape(chart_title)}\s*$', # # Title, ## Title, etc.
|
| 438 |
+
rf'^\*\*{re.escape(chart_title)}\*\*\s*$', # **Title**
|
| 439 |
+
rf'^{re.escape(chart_title)}\s*$', # Just the title
|
| 440 |
+
]
|
| 441 |
+
for pattern in patterns_to_remove:
|
| 442 |
+
content_text = re.sub(pattern, '', content_text, flags=re.MULTILINE | re.IGNORECASE)
|
| 443 |
+
|
| 444 |
+
# No need to filter section titles since we're not adding them manually
|
| 445 |
+
|
| 446 |
+
# Skip enhance_subheadings_with_bold for grouped rendering to preserve ** markers
|
| 447 |
+
# content_text = enhance_subheadings_with_bold(content_text)
|
| 448 |
+
content_text = remove_duplicate_headings(content_text)
|
| 449 |
+
|
| 450 |
+
# Add content with proper markdown rendering using grouped approach
|
| 451 |
+
if content_text and content_text.strip():
|
| 452 |
+
render_markdown_to_pdf_grouped(pdf, content_text, primary_color, accent_color, text_color)
|
| 453 |
+
else:
|
| 454 |
+
# Add placeholder text to prevent blank page
|
| 455 |
+
pdf.multi_cell(160, 8, f"Content for {section.title} section is being processed...", align='L')
|
| 456 |
+
|
| 457 |
+
# Add visual separation between content and charts
|
| 458 |
+
if content_text and content_text.strip():
|
| 459 |
+
pdf.ln(15) # More generous spacing for better visual separation
|
| 460 |
+
|
| 461 |
+
# ---- Charts for this section ----
|
| 462 |
+
charts_added = 0 # IMPORTANT: initialize to avoid NameError
|
| 463 |
+
|
| 464 |
+
# CRITICAL DEBUG: Show exactly what charts exist and their source_section values
|
| 465 |
+
if section.title == "Financial Plan & Funding":
|
| 466 |
+
# logging.info(f"=== LAST SECTION DEBUG ===")
|
| 467 |
+
# logging.info(f"Section title: '{section.title}'")
|
| 468 |
+
# logging.info(f"All chart_info_map keys: {list(chart_info_map.keys())}")
|
| 469 |
+
for key, charts in chart_info_map.items():
|
| 470 |
+
# logging.info(f" Key '{key}' has {len(charts)} charts")
|
| 471 |
+
for chart in charts:
|
| 472 |
+
# logging.info(f" Chart: {chart['title']} ({chart['type']})")
|
| 473 |
+
pass
|
| 474 |
+
# logging.info(f"=== END LAST SECTION DEBUG ===")
|
| 475 |
+
pass
|
| 476 |
+
|
| 477 |
+
section_charts = chart_info_map.get(section.title, [])
|
| 478 |
+
# logging.info(f"Section '{section.title}': Direct lookup found {len(section_charts)} charts")
|
| 479 |
+
|
| 480 |
+
# Strategy 1: Exact title match
|
| 481 |
+
if not section_charts:
|
| 482 |
+
# logging.info(f"No exact match found for '{section.title}'")
|
| 483 |
+
pass
|
| 484 |
+
|
| 485 |
+
# Strategy 2: Case-insensitive match
|
| 486 |
+
for chart_source, charts in chart_info_map.items():
|
| 487 |
+
if chart_source.lower() == section.title.lower():
|
| 488 |
+
section_charts = charts
|
| 489 |
+
# logging.info(f"Found charts via case-insensitive match: '{chart_source}' -> {len(charts)} charts")
|
| 490 |
+
break
|
| 491 |
+
|
| 492 |
+
# Strategy 3: Partial match
|
| 493 |
+
if not section_charts:
|
| 494 |
+
for chart_source, charts in chart_info_map.items():
|
| 495 |
+
if (section.title.lower() in chart_source.lower() or
|
| 496 |
+
chart_source.lower() in section.title.lower()):
|
| 497 |
+
section_charts = charts
|
| 498 |
+
# logging.info(f"Found charts via partial match: '{chart_source}' -> {len(charts)} charts")
|
| 499 |
+
break
|
| 500 |
+
|
| 501 |
+
# Strategy 4: Key-based match (for prompt_ prefixed sections)
|
| 502 |
+
if not section_charts and hasattr(section, 'key'):
|
| 503 |
+
for chart_source, charts in chart_info_map.items():
|
| 504 |
+
if section.key and section.key.replace('prompt_', '') in chart_source.lower():
|
| 505 |
+
section_charts = charts
|
| 506 |
+
# logging.info(f"Found charts via key match: '{section.key}' -> '{chart_source}' -> {len(charts)} charts")
|
| 507 |
+
break
|
| 508 |
+
|
| 509 |
+
# logging.info(f"Section '{section.title}': Final chart count: {len(section_charts)}")
|
| 510 |
+
|
| 511 |
+
if section_charts:
|
| 512 |
+
|
| 513 |
+
# Group charts that can fit on the same page
|
| 514 |
+
charts_per_page = 0
|
| 515 |
+
max_charts_per_page = 5 # Allow up to 5 charts per page
|
| 516 |
+
|
| 517 |
+
for chart_info in section_charts:
|
| 518 |
+
try:
|
| 519 |
+
# Check if we need a new page for this chart
|
| 520 |
+
chart_height_needed = 140 # Chart height (120) + title (20) + minimal spacing
|
| 521 |
+
|
| 522 |
+
# Smart page break logic - allow multiple charts per page
|
| 523 |
+
current_y = pdf.get_y()
|
| 524 |
+
charts_per_page += 1
|
| 525 |
+
|
| 526 |
+
# Only force page break if absolutely necessary - allow up to 5 charts per page
|
| 527 |
+
if (current_y + chart_height_needed > 290) or (charts_per_page > 5):
|
| 528 |
+
pdf.add_page()
|
| 529 |
+
# Header/footer removed for now
|
| 530 |
+
y_title = 25 # Start at normal position
|
| 531 |
+
charts_per_page = 1 # Reset counter for new page
|
| 532 |
+
else:
|
| 533 |
+
y_title = current_y
|
| 534 |
+
|
| 535 |
+
# Add minimal spacing before chart if not at top of page
|
| 536 |
+
if y_title > 25: # If not at top of page, add minimal space
|
| 537 |
+
pdf.ln(3) # Minimal spacing to fit more charts per page
|
| 538 |
+
|
| 539 |
+
# Chart title is already included in the chart itself, no need for separate subheader
|
| 540 |
+
|
| 541 |
+
# Centered image with larger dimensions for better visibility
|
| 542 |
+
img_width = 160 # Much larger chart width for better visibility
|
| 543 |
+
x = (210 - img_width) / 2
|
| 544 |
+
y_img = y_title
|
| 545 |
+
|
| 546 |
+
# Save image to temp file
|
| 547 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmp_img:
|
| 548 |
+
tmp_img.write(chart_info["chart_bytes"])
|
| 549 |
+
temp_img_path = tmp_img.name
|
| 550 |
+
|
| 551 |
+
# Chart dimensions (no border needed)
|
| 552 |
+
chart_height = 120 # Much larger height for better visibility
|
| 553 |
+
chart_width = img_width
|
| 554 |
+
|
| 555 |
+
# Image
|
| 556 |
+
pdf.image(temp_img_path, x=x, y=y_img, w=chart_width)
|
| 557 |
+
|
| 558 |
+
# Move below image and clean up (spacing based on actual chart height)
|
| 559 |
+
pdf.ln(chart_height + 5) # Chart height (120) + minimal spacing between charts
|
| 560 |
+
try:
|
| 561 |
+
os.remove(temp_img_path)
|
| 562 |
+
except Exception:
|
| 563 |
+
pass
|
| 564 |
+
|
| 565 |
+
charts_added += 1
|
| 566 |
+
total_charts_placed += 1
|
| 567 |
+
except Exception:
|
| 568 |
+
# Skip faulty chart and continue rendering others
|
| 569 |
+
continue
|
| 570 |
+
else:
|
| 571 |
+
# Debug: Check if there are any charts that might match this section
|
| 572 |
+
potential_matches = []
|
| 573 |
+
for chart_source, charts in chart_info_map.items():
|
| 574 |
+
if (section.title.lower() in chart_source.lower() or
|
| 575 |
+
chart_source.lower() in section.title.lower()):
|
| 576 |
+
potential_matches.append(f"'{chart_source}' -> {len(charts)} charts")
|
| 577 |
+
|
| 578 |
+
if potential_matches:
|
| 579 |
+
# logging.info(f"Section '{section.title}': Potential chart matches: {potential_matches}")
|
| 580 |
+
pass
|
| 581 |
+
else:
|
| 582 |
+
# logging.info(f"Section '{section.title}': No charts found and no potential matches")
|
| 583 |
+
pass
|
| 584 |
+
|
| 585 |
+
# Each section is already on its own page, no need for complex page break logic
|
| 586 |
+
|
| 587 |
+
|
| 588 |
+
|
| 589 |
+
|
| 590 |
+
# ===== BUILD THE PDF =====
|
| 591 |
+
try:
|
| 592 |
+
# Add all sections
|
| 593 |
+
try:
|
| 594 |
+
add_cover_page()
|
| 595 |
+
except Exception as e:
|
| 596 |
+
raise Exception(f"Cover page failed: {str(e)}")
|
| 597 |
+
|
| 598 |
+
# Add TOC on page 2 with REAL page numbers (already calculated above)
|
| 599 |
+
try:
|
| 600 |
+
if toc_items:
|
| 601 |
+
add_table_of_contents_page(pdf, toc_items, request.businessIdea)
|
| 602 |
+
else:
|
| 603 |
+
# Add a basic TOC page if no items were generated
|
| 604 |
+
pdf.add_page()
|
| 605 |
+
pdf.set_font("Arial", "B", 20)
|
| 606 |
+
pdf.set_text_color(0, 0, 0)
|
| 607 |
+
pdf.set_xy(25, 25)
|
| 608 |
+
pdf.cell(0, 18, "Table of Contents", ln=True)
|
| 609 |
+
pdf.set_font("Arial", "", 12)
|
| 610 |
+
pdf.set_text_color(51, 51, 51)
|
| 611 |
+
pdf.set_xy(25, 50)
|
| 612 |
+
pdf.multi_cell(160, 8, "No sections available for table of contents.", align='L')
|
| 613 |
+
except Exception as e:
|
| 614 |
+
# logging.error(f"Table of contents failed: {str(e)}")
|
| 615 |
+
pass
|
| 616 |
+
# Add a basic TOC page as fallback
|
| 617 |
+
pdf.add_page()
|
| 618 |
+
pdf.set_font("Arial", "B", 20)
|
| 619 |
+
pdf.set_text_color(0, 0, 0)
|
| 620 |
+
pdf.set_xy(25, 25)
|
| 621 |
+
pdf.cell(0, 18, "Table of Contents", ln=True)
|
| 622 |
+
pdf.set_font("Arial", "", 12)
|
| 623 |
+
pdf.set_text_color(51, 51, 51)
|
| 624 |
+
pdf.set_xy(25, 50)
|
| 625 |
+
pdf.multi_cell(160, 8, "Table of contents could not be generated.", align='L')
|
| 626 |
+
|
| 627 |
+
# Executive summary is now handled as part of the sections
|
| 628 |
+
# No separate page needed - the first section contains the executive summary
|
| 629 |
+
|
| 630 |
+
try:
|
| 631 |
+
add_sections_with_charts()
|
| 632 |
+
except Exception as e:
|
| 633 |
+
raise Exception(f"Sections with charts failed: {str(e)}")
|
| 634 |
+
|
| 635 |
+
# CRITICAL CHECK: Ensure we have content in the PDF
|
| 636 |
+
total_pages = pdf.page_no()
|
| 637 |
+
|
| 638 |
+
if total_pages < 2: # Should have at least cover + 1 content page
|
| 639 |
+
# Add a content page to prevent blank PDF
|
| 640 |
+
pdf.add_page()
|
| 641 |
+
pdf.set_font("Arial", "B", 16)
|
| 642 |
+
pdf.set_text_color(255, 0, 0)
|
| 643 |
+
pdf.cell(0, 20, "Content Processing Issue", ln=True, align='C')
|
| 644 |
+
pdf.set_font("Arial", "", 12)
|
| 645 |
+
pdf.set_text_color(0, 0, 0)
|
| 646 |
+
pdf.cell(0, 10, "The PDF generation encountered an issue with content processing.", ln=True, align='C')
|
| 647 |
+
pdf.cell(0, 10, "Please check the server logs for detailed information.", ln=True, align='C')
|
| 648 |
+
|
| 649 |
+
# Get PDF output and convert to bytes
|
| 650 |
+
try:
|
| 651 |
+
pdf_output = pdf.output(dest='S')
|
| 652 |
+
if isinstance(pdf_output, bytearray):
|
| 653 |
+
pdf_bytes = bytes(pdf_output)
|
| 654 |
+
else:
|
| 655 |
+
pdf_bytes = pdf_output.encode('latin-1') if isinstance(pdf_output, str) else pdf_output
|
| 656 |
+
|
| 657 |
+
# CRITICAL SAFETY CHECK: Ensure PDF is not empty
|
| 658 |
+
if len(pdf_bytes) < 1000: # PDF should be at least 1KB
|
| 659 |
+
# Try to add some content to prevent blank PDF
|
| 660 |
+
pdf.add_page()
|
| 661 |
+
pdf.set_font("Arial", "B", 16)
|
| 662 |
+
pdf.set_text_color(255, 0, 0) # Red text
|
| 663 |
+
pdf.cell(0, 20, "PDF Generation Issue Detected", ln=True, align='C')
|
| 664 |
+
pdf.set_font("Arial", "", 12)
|
| 665 |
+
pdf.set_text_color(0, 0, 0)
|
| 666 |
+
pdf.cell(0, 10, "If you see this message, there was an issue with content processing.", ln=True, align='C')
|
| 667 |
+
pdf.cell(0, 10, "Please check the server logs for details.", ln=True, align='C')
|
| 668 |
+
|
| 669 |
+
# Regenerate PDF with error message
|
| 670 |
+
pdf_output = pdf.output(dest='S')
|
| 671 |
+
if isinstance(pdf_output, bytearray):
|
| 672 |
+
pdf_bytes = bytes(pdf_output)
|
| 673 |
+
else:
|
| 674 |
+
pdf_bytes = pdf_output.encode('latin-1') if isinstance(pdf_output, str) else pdf_output
|
| 675 |
+
|
| 676 |
+
return pdf_bytes, f"business_plan_{request.businessIdea or 'export'}.pdf"
|
| 677 |
+
except Exception as e:
|
| 678 |
+
raise Exception(f"PDF conversion failed: {str(e)}")
|
| 679 |
+
|
| 680 |
+
except Exception as e:
|
| 681 |
+
# logging.error(f"PDF generation failed: {e}")
|
| 682 |
+
raise Exception(f"Failed to generate professional PDF: {str(e)}")
|
app/DocumentGeneration/word_generator.py
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Word Generator Module
|
| 2 |
+
# Contains create_word_document() and all Word-related functions
|
| 3 |
+
|
| 4 |
+
import re
|
| 5 |
+
import os
|
| 6 |
+
import tempfile
|
| 7 |
+
import time
|
| 8 |
+
import io
|
| 9 |
+
import sys
|
| 10 |
+
from typing import List, Dict, Any
|
| 11 |
+
|
| 12 |
+
# Add the parent directory to sys.path to import from models.py
|
| 13 |
+
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
|
| 14 |
+
from models import ExportRequest
|
| 15 |
+
|
| 16 |
+
# Import from the same directory (Document Generation)
|
| 17 |
+
from document_helpers import (
|
| 18 |
+
filter_empty_sections,
|
| 19 |
+
extract_business_name_from_content
|
| 20 |
+
)
|
| 21 |
+
# from table_of_content import generate_table_of_contents # Not used in word generator
|
| 22 |
+
from chart_generator import parse_embedded_chart_data, create_chart_from_data
|
| 23 |
+
from markdown_renderer import (
|
| 24 |
+
render_markdown_to_docx_grouped,
|
| 25 |
+
process_content_with_inline_charts
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
# Add Currency Mapping path
|
| 29 |
+
sys.path.append(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'Currency Mapping'))
|
| 30 |
+
from CurrencyMapping.currency_mapping import convert_currency_symbols_to_iso
|
| 31 |
+
|
| 32 |
+
def create_word_document(request: ExportRequest):
|
| 33 |
+
"""Create a Word document from the business plan data with front page, TOC, and proper page breaks"""
|
| 34 |
+
try:
|
| 35 |
+
from docx import Document
|
| 36 |
+
from docx.shared import Inches, Pt, RGBColor
|
| 37 |
+
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_TAB_ALIGNMENT
|
| 38 |
+
from docx.oxml import OxmlElement
|
| 39 |
+
from docx.oxml.ns import qn
|
| 40 |
+
except ImportError:
|
| 41 |
+
raise Exception("python-docx is not installed. Please install it with: pip install python-docx")
|
| 42 |
+
|
| 43 |
+
# Filter out empty sections to prevent blank pages
|
| 44 |
+
try:
|
| 45 |
+
filtered_sections = filter_empty_sections(request.sections)
|
| 46 |
+
except Exception as e:
|
| 47 |
+
raise Exception(f"Failed to filter sections: {str(e)}")
|
| 48 |
+
if not filtered_sections:
|
| 49 |
+
raise Exception("No valid sections found after filtering. All sections appear to be empty.")
|
| 50 |
+
|
| 51 |
+
doc = Document()
|
| 52 |
+
|
| 53 |
+
# Add confidentiality footer to every page
|
| 54 |
+
section = doc.sections[0]
|
| 55 |
+
footer = section.footer
|
| 56 |
+
footer_para = footer.paragraphs[0]
|
| 57 |
+
footer_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 58 |
+
footer_run = footer_para.add_run("CONFIDENTIAL - This document contains proprietary information and may not be shared without consent")
|
| 59 |
+
footer_run.font.size = Pt(8)
|
| 60 |
+
footer_run.font.color.rgb = RGBColor(100, 100, 100) # Gray text
|
| 61 |
+
|
| 62 |
+
# ===== FRONT PAGE =====
|
| 63 |
+
# Add company logo at the top
|
| 64 |
+
try:
|
| 65 |
+
# Use the MBD logo PNG file - now in app folder for HF deployment
|
| 66 |
+
app_root = os.path.dirname(os.path.dirname(__file__))
|
| 67 |
+
logo_path = os.path.join(app_root, 'assets', 'logos', 'MBD logo.png')
|
| 68 |
+
|
| 69 |
+
if os.path.exists(logo_path):
|
| 70 |
+
# Add logo to Word document (centered, top of page)
|
| 71 |
+
logo_para = doc.add_paragraph()
|
| 72 |
+
logo_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 73 |
+
logo_run = logo_para.add_run()
|
| 74 |
+
logo_run.add_picture(logo_path, width=Inches(3.0)) # Even larger size to show full logo
|
| 75 |
+
print(f"Logo added successfully from: {logo_path}")
|
| 76 |
+
else:
|
| 77 |
+
print(f"Logo file not found at: {logo_path}")
|
| 78 |
+
except Exception as e:
|
| 79 |
+
# Log any errors with logo
|
| 80 |
+
print(f"Logo error: {e}")
|
| 81 |
+
pass
|
| 82 |
+
|
| 83 |
+
# Add top spacing
|
| 84 |
+
doc.add_paragraph()
|
| 85 |
+
doc.add_paragraph()
|
| 86 |
+
|
| 87 |
+
# Extract business name from content, fallback to businessIdea
|
| 88 |
+
extracted_business_name = extract_business_name_from_content(filtered_sections)
|
| 89 |
+
business_name = extracted_business_name or request.businessIdea or "Business Plan"
|
| 90 |
+
|
| 91 |
+
# Business name as main title with enhanced styling - PURPLE THEME
|
| 92 |
+
title = doc.add_heading(business_name, 0)
|
| 93 |
+
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 94 |
+
|
| 95 |
+
# Style the title with larger font and PURPLE color
|
| 96 |
+
for run in title.runs:
|
| 97 |
+
run.font.size = Pt(36)
|
| 98 |
+
run.font.color.rgb = RGBColor(139, 92, 246) # PURPLE
|
| 99 |
+
|
| 100 |
+
# Removed decorative line under title
|
| 101 |
+
|
| 102 |
+
# Add subtitle with enhanced styling - PURPLE
|
| 103 |
+
doc.add_paragraph()
|
| 104 |
+
subtitle = doc.add_paragraph("Professional Business Plan Document")
|
| 105 |
+
subtitle.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 106 |
+
subtitle_run = subtitle.runs[0]
|
| 107 |
+
subtitle_run.font.size = Pt(18)
|
| 108 |
+
subtitle_run.font.color.rgb = RGBColor(124, 58, 237) # DARKER PURPLE
|
| 109 |
+
subtitle_run.italic = True
|
| 110 |
+
|
| 111 |
+
# Add more spacing
|
| 112 |
+
doc.add_paragraph()
|
| 113 |
+
doc.add_paragraph()
|
| 114 |
+
doc.add_paragraph()
|
| 115 |
+
|
| 116 |
+
# Add document info in a styled box - PURPLE BORDER
|
| 117 |
+
info_para = doc.add_paragraph()
|
| 118 |
+
info_para.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 119 |
+
|
| 120 |
+
# Add background color and border styling
|
| 121 |
+
info_run = info_para.add_run("Prepared by: Business Plan Generator")
|
| 122 |
+
info_run.font.size = Pt(14)
|
| 123 |
+
info_run.font.color.rgb = RGBColor(139, 92, 246) # PURPLE
|
| 124 |
+
|
| 125 |
+
doc.add_paragraph()
|
| 126 |
+
date_para = doc.add_paragraph()
|
| 127 |
+
date_para.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 128 |
+
date_run = date_para.add_run(f"Date: {time.strftime('%B %Y')}")
|
| 129 |
+
date_run.font.size = Pt(14)
|
| 130 |
+
date_run.font.color.rgb = RGBColor(139, 92, 246) # PURPLE
|
| 131 |
+
|
| 132 |
+
# Confidentiality statement removed - now in footer of every page
|
| 133 |
+
|
| 134 |
+
# Add more spacing before footer
|
| 135 |
+
doc.add_paragraph()
|
| 136 |
+
doc.add_paragraph()
|
| 137 |
+
doc.add_paragraph()
|
| 138 |
+
doc.add_paragraph()
|
| 139 |
+
|
| 140 |
+
# Add footer with enhanced styling - PURPLE
|
| 141 |
+
footer = doc.add_paragraph("Confidential Business Information")
|
| 142 |
+
footer.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 143 |
+
footer_run = footer.runs[0]
|
| 144 |
+
footer_run.font.size = Pt(12)
|
| 145 |
+
footer_run.font.color.rgb = RGBColor(139, 92, 246) # PURPLE
|
| 146 |
+
footer_run.bold = True
|
| 147 |
+
|
| 148 |
+
# Removed decorative line above footer
|
| 149 |
+
|
| 150 |
+
# ===== TABLE OF CONTENTS =====
|
| 151 |
+
# Add page break before TOC
|
| 152 |
+
doc.add_page_break()
|
| 153 |
+
|
| 154 |
+
# Add TOC title
|
| 155 |
+
toc_title = doc.add_heading("Table of Contents", level=1)
|
| 156 |
+
toc_title.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 157 |
+
|
| 158 |
+
# Removed decorative line under TOC title
|
| 159 |
+
|
| 160 |
+
# Add spacing before TOC entries
|
| 161 |
+
doc.add_paragraph()
|
| 162 |
+
|
| 163 |
+
# Sort sections according to predefined order (do this before TOC generation)
|
| 164 |
+
try:
|
| 165 |
+
sorted_sections = sort_sections_by_order(filtered_sections)
|
| 166 |
+
except Exception as e:
|
| 167 |
+
# Fallback: use filtered order if sorting fails
|
| 168 |
+
sorted_sections = filtered_sections
|
| 169 |
+
|
| 170 |
+
# Generate TOC entries with actual page numbers
|
| 171 |
+
# Since each section starts on a new page, we can calculate page numbers
|
| 172 |
+
toc_items = []
|
| 173 |
+
current_page = 3 # Start after cover (1) and TOC (2)
|
| 174 |
+
|
| 175 |
+
for section in sorted_sections:
|
| 176 |
+
if section.content and section.content.strip():
|
| 177 |
+
# Add main section
|
| 178 |
+
toc_items.append({
|
| 179 |
+
"title": section.title,
|
| 180 |
+
"level": 1,
|
| 181 |
+
"page": current_page
|
| 182 |
+
})
|
| 183 |
+
|
| 184 |
+
# Skip subheadings and chart titles - only include main section headings
|
| 185 |
+
|
| 186 |
+
current_page += 1 # Each section starts on a new page
|
| 187 |
+
|
| 188 |
+
# Add the actual TOC entries
|
| 189 |
+
for item in toc_items:
|
| 190 |
+
# Create TOC entry paragraph
|
| 191 |
+
toc_para = doc.add_paragraph()
|
| 192 |
+
toc_para.paragraph_format.space_after = Pt(6)
|
| 193 |
+
|
| 194 |
+
# Add section title
|
| 195 |
+
title_run = toc_para.add_run(item["title"])
|
| 196 |
+
title_run.font.size = Pt(11)
|
| 197 |
+
|
| 198 |
+
# Add dots and page number
|
| 199 |
+
if item["level"] == 1:
|
| 200 |
+
# Main section - bold
|
| 201 |
+
title_run.bold = True
|
| 202 |
+
# Add dots
|
| 203 |
+
dots_run = toc_para.add_run(" " + "." * (60 - len(item["title"])) + " ")
|
| 204 |
+
dots_run.font.size = Pt(11)
|
| 205 |
+
dots_run.font.color.rgb = RGBColor(128, 128, 128) # Gray
|
| 206 |
+
# Add page number
|
| 207 |
+
page_run = toc_para.add_run(str(item["page"]))
|
| 208 |
+
page_run.font.size = Pt(11)
|
| 209 |
+
page_run.bold = True
|
| 210 |
+
else:
|
| 211 |
+
# Subsection - indented
|
| 212 |
+
toc_para.paragraph_format.left_indent = Inches(0.3)
|
| 213 |
+
# Add dots
|
| 214 |
+
dots_run = toc_para.add_run(" " + "." * (50 - len(item["title"])) + " ")
|
| 215 |
+
dots_run.font.size = Pt(11)
|
| 216 |
+
dots_run.font.color.rgb = RGBColor(128, 128, 128) # Gray
|
| 217 |
+
# Add page number
|
| 218 |
+
page_run = toc_para.add_run(str(item["page"]))
|
| 219 |
+
page_run.font.size = Pt(11)
|
| 220 |
+
|
| 221 |
+
# logging.info(f"Generated {len(toc_items)} TOC items with actual page numbers")
|
| 222 |
+
|
| 223 |
+
# Executive Summary is now handled as a regular section above
|
| 224 |
+
|
| 225 |
+
# ===== MAIN SECTIONS =====
|
| 226 |
+
for i, section in enumerate(sorted_sections):
|
| 227 |
+
if section.content.strip():
|
| 228 |
+
# Add page break before each section (including Executive Summary)
|
| 229 |
+
doc.add_page_break()
|
| 230 |
+
|
| 231 |
+
# Add section heading
|
| 232 |
+
doc.add_heading(section.title, level=1)
|
| 233 |
+
|
| 234 |
+
# Clean and process content WITH inline charts
|
| 235 |
+
try:
|
| 236 |
+
clean_content = clean_text_for_pdf(section.content)
|
| 237 |
+
except Exception:
|
| 238 |
+
clean_content = section.content or ""
|
| 239 |
+
try:
|
| 240 |
+
clean_content = convert_currency_symbols_to_iso(clean_content)
|
| 241 |
+
except Exception:
|
| 242 |
+
pass
|
| 243 |
+
|
| 244 |
+
# Remove the section title from content to prevent duplication
|
| 245 |
+
section_title_clean = section.title.strip()
|
| 246 |
+
content_lines = clean_content.split('\n')
|
| 247 |
+
filtered_lines = []
|
| 248 |
+
for line in content_lines:
|
| 249 |
+
line_clean = line.strip()
|
| 250 |
+
# Skip lines that match the section title (with or without markdown)
|
| 251 |
+
# Check for various markdown formats and variations
|
| 252 |
+
if (line_clean == section_title_clean or
|
| 253 |
+
line_clean == f"**{section_title_clean}**" or
|
| 254 |
+
line_clean == f"# {section_title_clean}" or
|
| 255 |
+
line_clean == f"## {section_title_clean}" or
|
| 256 |
+
line_clean == f"### {section_title_clean}" or
|
| 257 |
+
line_clean == f"#### {section_title_clean}" or
|
| 258 |
+
# Also check if the line starts with the section title (for numbered headings)
|
| 259 |
+
line_clean.startswith(f"{section_title_clean}") and len(line_clean) <= len(section_title_clean) + 10):
|
| 260 |
+
continue
|
| 261 |
+
filtered_lines.append(line)
|
| 262 |
+
clean_content = '\n'.join(filtered_lines)
|
| 263 |
+
|
| 264 |
+
# Process content with inline charts - THIS IS THE KEY CHANGE
|
| 265 |
+
process_content_with_inline_charts(doc, clean_content, request.businessIdea or "Business")
|
| 266 |
+
|
| 267 |
+
doc.add_paragraph()
|
| 268 |
+
|
| 269 |
+
# Charts are now handled inline with the sections above
|
| 270 |
+
|
| 271 |
+
# Save to bytes
|
| 272 |
+
buffer = io.BytesIO()
|
| 273 |
+
doc.save(buffer)
|
| 274 |
+
buffer.seek(0)
|
| 275 |
+
doc_bytes = buffer.getvalue()
|
| 276 |
+
buffer.close()
|
| 277 |
+
|
| 278 |
+
filename = f"business_plan_{request.businessIdea or 'export'}.docx"
|
| 279 |
+
return doc_bytes, filename
|
app/assets/logos/MBD logo.png
ADDED
|