henaiv2 / mydocs.py
joashsam's picture
Upload 5 files
f42611c verified
Raw
History Blame Contribute Delete
30.8 kB
# 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}")