# mydocs.py - Document creation utilities extracted from app.py import os import json import re import pandas as pd import openpyxl from io import BytesIO from docx import Document from docx.shared import Inches, Pt, RGBColor from pptx import Presentation from pptx.util import Inches as PptxInches from pptx.enum.text import PP_ALIGN from PIL import Image, ImageDraw, ImageFont from reportlab.lib.pagesizes import letter from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.lib import colors from reportlab.lib.units import inch import textwrap from pathlib import Path class DocumentCreator: """Utility class for creating various types of documents""" def __init__(self, output_dir="generated_docs"): """Initialize with output directory""" self.output_dir = Path(output_dir) self.output_dir.mkdir(exist_ok=True) self.temp_dir = Path("temp_docs") self.temp_dir.mkdir(exist_ok=True) def create_word_document(self, content, filename): """Create a Word document from markdown-style content with proper formatting""" doc = Document() # Add document title style lines = content.split('\n') in_table = False table_rows = [] current_heading_level = 0 for line in lines: line = line.rstrip() if not line.strip(): if not in_table: doc.add_paragraph() continue # Handle markdown tables if '|' in line and line.count('|') >= 2: # Check if this is a table row cells = [cell.strip() for cell in line.split('|')] cells = [c for c in cells if c or (cells.index(c) > 0 and cells.index(c) < len(cells)-1)] if not in_table: # Start new table in_table = True table_rows = [cells] else: # Check if this is a separator row (contains ---) if all('-' in cell for cell in cells if cell): continue # Skip separator row table_rows.append(cells) continue elif in_table: # End of table, create the table in Word if table_rows: num_cols = max(len(row) for row in table_rows) table = doc.add_table(rows=len(table_rows), cols=num_cols) table.style = 'Table Grid' for i, row in enumerate(table_rows): for j, cell_text in enumerate(row): if j < num_cols: table.cell(i, j).text = cell_text doc.add_paragraph() in_table = False table_rows = [] # Process current line as regular content if line.strip(): doc.add_paragraph(line) continue # Handle headings if line.startswith('# '): doc.add_heading(line[2:].strip(), level=1) current_heading_level = 1 elif line.startswith('## '): doc.add_heading(line[3:].strip(), level=2) current_heading_level = 2 elif line.startswith('### '): doc.add_heading(line[4:].strip(), level=3) current_heading_level = 3 elif line.startswith('#### '): doc.add_heading(line[5:].strip(), level=4) current_heading_level = 4 # Handle bullet points elif line.startswith('- ') or line.startswith('* '): p = doc.add_paragraph(line[2:].strip(), style='List Bullet') # Handle numbered lists elif re.match(r'^\d+\.\s', line): p = doc.add_paragraph(line, style='List Number') # Handle horizontal rule elif line.strip() == '---' or line.strip() == '***': doc.add_paragraph('_' * 50) # Handle bold text within paragraph else: # Process inline formatting p = doc.add_paragraph() self._add_formatted_text(p, line) # Handle any remaining table if in_table and table_rows: num_cols = max(len(row) for row in table_rows) table = doc.add_table(rows=len(table_rows), cols=num_cols) table.style = 'Table Grid' for i, row in enumerate(table_rows): for j, cell_text in enumerate(row): if j < num_cols: table.cell(i, j).text = cell_text output_path = self.output_dir / filename doc.save(str(output_path)) return output_path def _add_formatted_text(self, paragraph, text): """Add text with markdown formatting to a paragraph""" from docx.shared import RGBColor # Process bold and italic parts = [] current_pos = 0 bold_pattern = r'\*\*([^*]+)\*\*' italic_pattern = r'\*([^*]+)\*' # Combine patterns all_matches = [] for match in re.finditer(bold_pattern, text): all_matches.append((match.start(), match.end(), 'bold', match.group(1))) for match in re.finditer(italic_pattern, text): all_matches.append((match.start(), match.end(), 'italic', match.group(1))) all_matches.sort(key=lambda x: x[0]) if not all_matches: paragraph.add_run(text) return last_end = 0 for start, end, style, content in all_matches: if start > last_end: paragraph.add_run(text[last_end:start]) run = paragraph.add_run(content) if style == 'bold': run.bold = True elif style == 'italic': run.italic = True last_end = end if last_end < len(text): paragraph.add_run(text[last_end:]) def create_text_file(self, content, filename): """Create a plain text file with proper formatting""" output_path = self.output_dir / filename with open(output_path, 'w', encoding='utf-8') as f: f.write(content) return output_path def create_excel_file(self, content, filename): """Create an Excel file from markdown table or CSV content""" import openpyxl from openpyxl.styles import Font, PatternFill, Alignment, Border, Side wb = openpyxl.Workbook() ws = wb.active ws.title = "Sheet1" # Parse content - look for markdown tables first lines = content.strip().split('\n') table_data = [] in_table = False for line in lines: if '|' in line and line.count('|') >= 2: cells = [cell.strip() for cell in line.split('|')] cells = [c for c in cells if c or (cells.index(c) > 0 and cells.index(c) < len(cells)-1)] if cells: if not in_table: in_table = True # Skip separator rows if not all('-' in cell for cell in cells if cell): table_data.append(cells) elif in_table and not line.strip(): in_table = False if not table_data: # Try CSV format for line in lines: if ',' in line: table_data.append([cell.strip() for cell in line.split(',')]) elif line.strip(): table_data.append([line.strip()]) # Write to Excel with styling if table_data: thin_border = Border( left=Side(style='thin'), right=Side(style='thin'), top=Side(style='thin'), bottom=Side(style='thin') ) header_fill = PatternFill(start_color='366092', end_color='366092', fill_type='solid') header_font = Font(color='FFFFFF', bold=True) for i, row in enumerate(table_data): for j, cell_value in enumerate(row): cell = ws.cell(row=i+1, column=j+1, value=cell_value) cell.border = thin_border cell.alignment = Alignment(horizontal='left', vertical='center') if i == 0: cell.fill = header_fill cell.font = header_font # Auto-adjust column widths for column in ws.columns: max_length = 0 column_letter = column[0].column_letter for cell in column: try: if len(str(cell.value)) > max_length: max_length = len(str(cell.value)) except: pass adjusted_width = min(max_length + 2, 50) ws.column_dimensions[column_letter].width = adjusted_width output_path = self.output_dir / filename wb.save(str(output_path)) return output_path def create_csv_file(self, content, filename): """Create a CSV file from markdown table or text content""" import csv # Parse content - look for markdown tables lines = content.strip().split('\n') table_data = [] in_table = False for line in lines: if '|' in line and line.count('|') >= 2: cells = [cell.strip() for cell in line.split('|')] cells = [c for c in cells if c or (cells.index(c) > 0 and cells.index(c) < len(cells)-1)] if cells: if not in_table: in_table = True if not all('-' in cell for cell in cells if cell): table_data.append(cells) elif in_table and not line.strip(): in_table = False if not table_data: # Try plain CSV or text for line in lines: if ',' in line: table_data.append([cell.strip() for cell in line.split(',')]) elif line.strip(): table_data.append([line.strip()]) output_path = self.output_dir / filename with open(output_path, 'w', newline='', encoding='utf-8') as f: writer = csv.writer(f) writer.writerows(table_data) return output_path def create_powerpoint(self, content, filename): """Create a PowerPoint presentation with proper slide separation and formatting""" from pptx.util import Inches as PptxInches from pptx.enum.text import PP_ALIGN, MSO_ANCHOR from pptx.dml.color import RGBColor as PptxRGBColor prs = Presentation() # Set slide dimensions (standard 16:9) prs.slide_width = PptxInches(13.333) prs.slide_height = PptxInches(7.5) # Parse slides - split by "---" or "## Slide" patterns slides_content = [] # Try to split by markdown slide separators if '---' in content: raw_slides = content.split('---') for slide in raw_slides: if slide.strip(): slides_content.append(slide.strip()) else: # Try to split by "## Slide" pattern import re slide_pattern = r'(?:##\s*Slide\s*\d+)\s*\n(.*?)(?=(?:##\s*Slide|$))' matches = re.findall(slide_pattern, content, re.DOTALL) if matches: slides_content = matches else: # Try to split by numbered slides numbered_slides = re.split(r'\n(?=\d+\.\s+[A-Z])', content) if len(numbered_slides) > 1: slides_content = numbered_slides else: # Single slide slides_content = [content] # If no slides found, create one slide with all content if not slides_content: slides_content = [content] for slide_idx, slide_content in enumerate(slides_content): if not slide_content.strip(): continue lines = slide_content.split('\n') # Clean up lines - remove empty lines at start/end while lines and not lines[0].strip(): lines.pop(0) while lines and not lines[-1].strip(): lines.pop() if not lines: continue # Extract title and body content slide_title = "" body_lines = [] # First line as potential title first_line = lines[0].strip() # Check if first line is a heading (starts with #) if first_line.startswith('#'): slide_title = first_line.lstrip('#').strip() body_lines = lines[1:] elif first_line.startswith('Slide') or first_line.startswith('**'): slide_title = first_line.replace('**', '').replace('Slide', '').strip() if slide_title and slide_title[0].isdigit(): # Extract just the title part parts = slide_title.split(' ', 1) if len(parts) > 1: slide_title = parts[1] else: slide_title = f"Slide {slide_idx + 1}" body_lines = lines[1:] if len(lines) > 1 else [] else: # No clear title, use generic title slide_title = f"Slide {slide_idx + 1}" body_lines = lines # Determine slide layout if slide_idx == 0 and (len(body_lines) == 0 or len(slide_title) < 30): # Title slide slide_layout = prs.slide_layouts[0] slide = prs.slides.add_slide(slide_layout) # Set title if slide.shapes.title: title_shape = slide.shapes.title title_shape.text = slide_title if slide_title else "Presentation Title" # Format title for paragraph in title_shape.text_frame.paragraphs: paragraph.font.size = PptxInches(0.44) paragraph.font.bold = True paragraph.alignment = PP_ALIGN.CENTER # Set subtitle if available if len(slide.placeholders) > 1 and body_lines: subtitle_placeholder = slide.placeholders[1] subtitle_text = '\n'.join(body_lines[:3]) subtitle_placeholder.text = subtitle_text # Format subtitle for paragraph in subtitle_placeholder.text_frame.paragraphs: paragraph.font.size = PptxInches(0.28) paragraph.alignment = PP_ALIGN.CENTER else: # Content slide slide_layout = prs.slide_layouts[1] slide = prs.slides.add_slide(slide_layout) # Set title if slide.shapes.title: title_shape = slide.shapes.title title_shape.text = slide_title if slide_title else f"Slide {slide_idx + 1}" # Format title for paragraph in title_shape.text_frame.paragraphs: paragraph.font.size = PptxInches(0.36) paragraph.font.bold = True # Set content if len(slide.placeholders) > 1: content_placeholder = slide.placeholders[1] # Clear existing content content_placeholder.text = "" text_frame = content_placeholder.text_frame text_frame.clear() # Process body lines and add to content for line in body_lines: line = line.strip() if not line: continue # Add a new paragraph p = text_frame.add_paragraph() # Handle bullet points if line.startswith('- ') or line.startswith('* '): p.text = line[2:] p.level = 0 p.bullet = True elif line.startswith(' - ') or line.startswith(' * '): p.text = line[4:] p.level = 1 p.bullet = True elif line.startswith(' - ') or line.startswith(' * '): p.text = line[6:] p.level = 2 p.bullet = True elif re.match(r'^\d+\.\s', line): # Numbered list p.text = line p.bullet = False elif line.startswith('**') and line.endswith('**'): # Bold heading within content p.text = line.strip('*') p.font.bold = True else: # Regular paragraph p.text = line p.bullet = False # Set font size p.font.size = PptxInches(0.24) # Add spacing between paragraphs p.space_after = PptxInches(0.06) # Remove any empty slides at the end slides_to_remove = [] for i, slide in enumerate(prs.slides): has_content = False for shape in slide.shapes: if hasattr(shape, "text") and shape.text and shape.text.strip(): has_content = True break if not has_content and i > 0: slides_to_remove.append(i) # Note: python-pptx doesn't support direct slide removal easily # We'll just save as is - empty slides are rare output_path = self.output_dir / filename prs.save(str(output_path)) return output_path def create_image_from_text(self, content, filename): """Create an image from text content with proper formatting""" from PIL import Image, ImageDraw, ImageFont # Clean content lines = [] for line in content.split('\n'): if line.strip(): # Remove markdown formatting for image clean_line = re.sub(r'\*\*([^*]+)\*\*', r'\1', line) clean_line = re.sub(r'\*([^*]+)\*', r'\1', clean_line) clean_line = re.sub(r'#+\s*', '', clean_line) lines.append(clean_line) if not lines: lines = [content[:100]] # Calculate image size max_line_length = max(len(line) for line in lines) if lines else 40 img_width = min(1200, max(400, max_line_length * 12)) line_height = 30 img_height = max(400, len(lines) * line_height + 100) img = Image.new('RGB', (img_width, img_height), color='white') draw = ImageDraw.Draw(img) # Try to load a better font try: font = ImageFont.truetype("/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", 16) except: try: font = ImageFont.truetype("arial.ttf", 16) except: font = ImageFont.load_default() y = 40 for line in lines: # Wrap long lines wrapped_lines = textwrap.wrap(line, width=img_width // 10) for wrapped in wrapped_lines: draw.text((20, y), wrapped, fill='black', font=font) y += line_height output_path = self.output_dir / filename img.save(str(output_path)) return output_path def create_pdf_from_content(self, content, filename): """Create a PDF from markdown content with proper formatting""" from reportlab.lib.pagesizes import letter from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.lib.units import inch from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle from reportlab.lib import colors output_path = self.output_dir / filename doc = SimpleDocTemplate(str(output_path), pagesize=letter, rightMargin=72, leftMargin=72, topMargin=72, bottomMargin=72) styles = getSampleStyleSheet() story = [] # Create custom styles heading1_style = ParagraphStyle('Heading1Custom', parent=styles['Heading1'], fontSize=16, spaceAfter=12, spaceBefore=12) heading2_style = ParagraphStyle('Heading2Custom', parent=styles['Heading2'], fontSize=14, spaceAfter=10, spaceBefore=10) normal_style = ParagraphStyle('NormalCustom', parent=styles['Normal'], fontSize=10, spaceAfter=6) lines = content.split('\n') in_table = False table_data = [] for line in lines: line = line.strip() if not line: if not in_table: story.append(Spacer(1, 6)) continue # Handle markdown tables if '|' in line and line.count('|') >= 2: cells = [cell.strip() for cell in line.split('|')] cells = [c for c in cells if c or (cells.index(c) > 0 and cells.index(c) < len(cells)-1)] if cells: if not in_table: in_table = True table_data = [cells] elif not all('-' in cell for cell in cells if cell): table_data.append(cells) continue elif in_table: # Create table if table_data: # Convert to ReportLab table rt_table = Table(table_data) rt_table.setStyle(TableStyle([ ('BACKGROUND', (0, 0), (-1, 0), colors.grey), ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), ('ALIGN', (0, 0), (-1, -1), 'CENTER'), ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), ('FONTSIZE', (0, 0), (-1, 0), 10), ('BOTTOMPADDING', (0, 0), (-1, 0), 12), ('BACKGROUND', (0, 1), (-1, -1), colors.beige), ('GRID', (0, 0), (-1, -1), 1, colors.black) ])) story.append(rt_table) story.append(Spacer(1, 12)) in_table = False table_data = [] continue # Handle headings if line.startswith('# '): story.append(Paragraph(line[2:], heading1_style)) elif line.startswith('## '): story.append(Paragraph(line[3:], heading2_style)) elif line.startswith('### '): story.append(Paragraph(line[4:], normal_style)) elif line.startswith('- ') or line.startswith('* '): story.append(Paragraph('• ' + line[2:], normal_style)) elif re.match(r'^\d+\.\s', line): story.append(Paragraph(line, normal_style)) else: story.append(Paragraph(line, normal_style)) doc.build(story) return output_path def create_document(self, content, doc_type, filename, template_id=None): """ Main method to create a document based on type Args: content: The content to put in the document doc_type: Type of document ('word', 'txt', 'excel', 'csv', 'ppt', 'image', 'pdf') filename: Desired filename (without extension) template_id: Optional template ID for formatting Returns: Path to created document """ # Ensure filename has proper extension ext_map = { 'word': '.docx', 'txt': '.txt', 'pdf': '.pdf', 'excel': '.xlsx', 'csv': '.csv', 'ppt': '.pptx', 'image': '.png' } ext = ext_map.get(doc_type, '.txt') full_filename = f"{filename}{ext}" # Enhance content with template-specific formatting if template_id: content = self._apply_template_formatting(content, doc_type, template_id) # Create document based on type if doc_type == 'word': return self.create_word_document(content, full_filename) elif doc_type == 'txt': return self.create_text_file(content, full_filename) elif doc_type == 'excel': return self.create_excel_file(content, full_filename) elif doc_type == 'csv': return self.create_csv_file(content, full_filename) elif doc_type == 'ppt': return self.create_powerpoint(content, full_filename) elif doc_type == 'image': return self.create_image_from_text(content, full_filename) elif doc_type == 'pdf': return self.create_pdf_from_content(content, full_filename) else: raise ValueError(f"Unsupported document type: {doc_type}") def _apply_template_formatting(self, content, doc_type, template_id): """Apply template-specific formatting instructions to content""" if template_id == 'professional': if doc_type == 'word': content += """ Format this as a professional document with: - Clear headings (use ### for main sections) - Bullet points where appropriate - Professional tone - Proper spacing between sections""" elif doc_type == 'ppt': content += """ Format this as a presentation with: - Title slide (presentation title) - 3-5 content slides with headings and bullet points - Closing slide with summary or call to action Separate slides with ---""" elif doc_type == 'excel': content += """ Format this as structured data with: - Column headers as first row - Each row as a data entry - Use consistent formatting""" return content def get_download_url(self, filename, base_url="/api/docs/download/"): """Get download URL for a file""" return f"{base_url}{filename}" # Convenience functions for quick document creation def create_word_document(content, filename, output_dir="generated_docs"): """Quickly create a Word document""" creator = DocumentCreator(output_dir) return creator.create_word_document(content, filename) def create_text_file(content, filename, output_dir="generated_docs"): """Quickly create a text file""" creator = DocumentCreator(output_dir) return creator.create_text_file(content, filename) def create_excel_file(content, filename, output_dir="generated_docs"): """Quickly create an Excel file""" creator = DocumentCreator(output_dir) return creator.create_excel_file(content, filename) def create_powerpoint(content, filename, output_dir="generated_docs"): """Quickly create a PowerPoint presentation""" creator = DocumentCreator(output_dir) return creator.create_powerpoint(content, filename) def create_image_from_text(content, filename, output_dir="generated_docs"): """Quickly create an image from text""" creator = DocumentCreator(output_dir) return creator.create_image_from_text(content, filename) def create_pdf_from_content(content, filename, output_dir="generated_docs"): """Quickly create a PDF from text""" creator = DocumentCreator(output_dir) return creator.create_pdf_from_content(content, filename) # Example usage if __name__ == "__main__": # Test the document creator creator = DocumentCreator() # Create a sample Word document sample_content = """# Welcome to My Document ## Introduction This is a sample document created with the DocumentCreator class. ## Features - Easy document creation - Multiple format support - Professional formatting ### Conclusion Thank you for using this utility!""" doc_path = creator.create_document(sample_content, 'word', 'sample_document') print(f"Created Word document: {doc_path}") # Create a sample text file text_path = creator.create_document("Hello, World!", 'txt', 'hello_world') print(f"Created text file: {text_path}")