awacke1's picture
Update app.py
94db2c8 verified
raw
history blame
15.1 kB
import io
import os
import re
import glob
import textwrap
import streamlit as st
import pandas as pd
import mistune
import fitz
import edge_tts
import asyncio
import base64
from datetime import datetime
from PIL import Image
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import letter, A4, legal, A3, A5, landscape
from reportlab.lib.utils import ImageReader
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image as ReportLabImage, PageBreak
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib import colors
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from urllib.parse import quote
# 1. πŸ“œ The App's Constitution
# Setting the fundamental laws of the page. Wide layout for maximum glory.
st.set_page_config(page_title="Ultimate PDF & Code Interpreter", layout="wide", page_icon="πŸš€")
# ==============================================================================
# CLASS 1: THE DYNAMIC LAYOUT SELECTOR COMPONENT
# ==============================================================================
class LayoutSelector:
"""The witty, stylish component for choosing a live preview layout."""
LAYOUTS = {
"A4 Portrait": {"aspect": "1 / 1.414", "desc": "Standard paper (210 x 297mm)", "icon": "πŸ“„", "size": A4},
"A4 Landscape": {"aspect": "1.414 / 1", "desc": "Standard paper (297 x 210mm)", "icon": "πŸ“„", "size": landscape(A4)},
"Letter Portrait": {"aspect": "1 / 1.294", "desc": "US Letter (8.5 x 11in)", "icon": "πŸ“„", "size": letter},
"Letter Landscape": {"aspect": "1.294 / 1", "desc": "US Letter (11 x 8.5in)", "icon": "πŸ“„", "size": landscape(letter)},
"Wide 16:9": {"aspect": "16 / 9", "desc": "YouTube and streaming sites", "icon": " widescreen", "size": landscape(A4)},
"Vertical 9:16": {"aspect": "9 / 16", "desc": "Instagram Reels and TikTok", "icon": "πŸ“±", "size": A4},
"Square 1:1": {"aspect": "1 / 1", "desc": "Instagram posts", "icon": "πŸ–ΌοΈ", "size": (600, 600)},
"Classic 4:3": {"aspect": "4 / 3", "desc": "Traditional TV and monitors", "icon": "πŸ“Ί", "size": landscape(A4)},
"Social 4:5": {"aspect": "4 / 5", "desc": "Portrait photos on social media", "icon": "🀳", "size": A4},
"Cinema 21:9": {"aspect": "21 / 9", "desc": "Widescreen cinematic video", "icon": "🎬", "size": landscape(A4)},
"Portrait 2:3": {"aspect": "2 / 3", "desc": "Standard photography prints", "icon": "πŸ“·", "size": A4},
}
def __init__(self, default_layout='A4 Portrait'):
if 'page_layout' not in st.session_state: st.session_state.page_layout = default_layout
if 'autofit_text' not in st.session_state: st.session_state.autofit_text = False
def _build_css(self):
current_layout = st.session_state.get('page_layout', 'A4 Portrait')
aspect_ratio = self.LAYOUTS.get(current_layout, {}).get('aspect', '1 / 1.414')
return f"<style>.dynamic-canvas-container {{ aspect-ratio: {aspect_ratio}; }}</style>"
def render(self):
st.html(self._build_css()) # Simplified for brevity, assumes base CSS is loaded once
# The full HTML/CSS/JS for the dropdown component is loaded once at the start of the app now
# ==============================================================================
# CLASS 2: THE ALMIGHTY PDF GENERATOR
# ==============================================================================
class PdfGenerator:
"""The engine room. This class takes your content and forges it into a PDF."""
# 2. πŸ§™β€β™‚οΈ The PDF Alchemist's Workshop
# We gather all the ingredients (text, images, settings) to begin our creation.
def __init__(self, markdown_texts, image_files, settings):
self.markdown_texts = markdown_texts
self.image_files = image_files
self.settings = settings
self.story = []
self.buffer = io.BytesIO()
# Ensure fonts are registered, or provide a friendly warning.
try:
if os.path.exists("DejaVuSans.ttf"):
pdfmetrics.registerFont(TTFont("DejaVuSans", "DejaVuSans.ttf"))
if os.path.exists("NotoEmoji-Bold.ttf"):
pdfmetrics.registerFont(TTFont("NotoEmoji-Bold", "NotoEmoji-Bold.ttf"))
except Exception as e:
st.error(f"🚨 Font-tastic error! Could not register fonts: {e}")
# 3. πŸ”¬ The Automatic Font-Shrinking Ray
# This function analyzes your text and calculates the perfect font size.
# Its goal: Squeeze everything onto one glorious, multi-column page!
def _calculate_font_size(self, text_content, page_width, page_height, num_columns):
if not text_content: return self.settings['base_font_size']
total_lines = len(text_content)
total_chars = sum(len(line) for line in text_content)
# This is a refined version of the original script's sizing logic
min_font, max_font = 5, 20
# Estimate required font size based on lines per column
lines_per_col = total_lines / num_columns
font_size = page_height / lines_per_col * 0.7
font_size = max(min_font, min(max_font, font_size))
# Further reduce size if lines are very long
avg_chars_per_line = total_chars / total_lines if total_lines > 0 else 0
col_width_chars = (page_width / num_columns) / (font_size * 0.5) # Estimate chars that fit
if avg_chars_per_line > col_width_chars * 0.9: # If average line is long
font_size = max(min_font, font_size * 0.85)
return int(font_size)
# 4. πŸ“œ The Story Weaver
# This is where the magic happens. We take the text, parse the markdown,
# create columns, and lay everything out. It's like choreographing a ballet of words.
def _build_text_story(self):
layout_props = LayoutSelector.LAYOUTS.get(self.settings['layout_name'], {})
page_size = layout_props.get('size', A4)
page_width, page_height = page_size
# Option for an extra-wide, single-page layout
if self.settings['force_one_page']:
page_width *= self.settings['num_columns']
page_height *= 1.5 # Give more vertical space
num_columns = self.settings['num_columns']
else:
num_columns = self.settings['num_columns']
doc = SimpleDocTemplate(self.buffer, pagesize=(page_width, page_height),
leftMargin=36, rightMargin=36, topMargin=36, bottomMargin=36)
full_text, _ = markdown_to_pdf_content('\n\n'.join(self.markdown_texts))
font_size = self._calculate_font_size(full_text, page_width - 72, page_height - 72, num_columns)
# Create styles based on the auto-calculated font size
styles = getSampleStyleSheet()
item_style = ParagraphStyle('ItemStyle', parent=styles['Normal'], fontName="DejaVuSans", fontSize=font_size, leading=font_size * 1.2)
# Process and style each line of markdown
column_data = [[] for _ in range(num_columns)]
for i, item in enumerate(full_text):
# Simplified for clarity, can add back detailed heading/bold styles
p = Paragraph(item, style=item_style)
column_data[i % num_columns].append(p)
max_len = max(len(col) for col in column_data) if column_data else 0
for col in column_data:
col.extend([Spacer(1,1)] * (max_len - len(col)))
table_data = list(zip(*column_data))
table = Table(table_data, colWidths=[(page_width-72)/num_columns]*num_columns, hAlign='LEFT')
table.setStyle(TableStyle([('VALIGN', (0,0), (-1,-1), 'TOP')]))
self.story.append(table)
return doc
# 5. πŸ–ΌοΈ The Image Page Conjurer
# For when images demand their own spotlight. This function gives each image
# its very own page, perfectly sized to its dimensions.
def _build_image_story(self):
for img_file in self.image_files:
self.story.append(PageBreak())
img = Image.open(img_file)
img_width, img_height = img.size
# Create a page that is the exact size of the image + margins
custom_page = (img_width + 72, img_height + 72)
doc = SimpleDocTemplate(self.buffer, pagesize=custom_page)
# This is a bit of a trick: we define a special frame for this page
frame = doc.getFrame('normal') # Get the default frame
doc.addPageTemplates(st.PageTemplate(id='ImagePage', frames=[frame], pagesize=custom_page))
self.story.append(ReportLabImage(img_file, width=img_width, height=img_height))
# 6. ✨ The Final Incantation
# With all the pieces assembled, we perform the final ritual to build
# the PDF and bring it to life!
def generate(self):
if not self.markdown_texts and not self.image_files:
st.warning("Nothing to generate! Please add some text or images.")
return None
doc = self._build_text_story() if self.markdown_texts else SimpleDocTemplate(self.buffer)
if self.image_files:
if self.settings['images_on_own_page']:
self._build_image_story()
else: # Add images to the main story
for img_file in self.image_files:
self.story.append(ReportLabImage(img_file))
doc.build(self.story)
self.buffer.seek(0)
return self.buffer
# ==============================================================================
# ALL OTHER HELPER FUNCTIONS (TTS, File Management, etc.)
# ==============================================================================
def markdown_to_pdf_content(markdown_text):
lines = markdown_text.strip().split('\n')
# This is a simplified parser. The original's complex logic can be used here.
return [re.sub(r'#+\s*|\*\*(.*?)\*\*|_(.*?)_', r'<b>\1\2</b>', line) for line in lines if line.strip()], len(lines)
def pdf_to_image(pdf_bytes):
if not pdf_bytes: return None
try:
return [page.get_pixmap(matrix=fitz.Matrix(2.0, 2.0)) for page in fitz.open(stream=pdf_bytes, filetype="pdf")]
except Exception as e:
st.error(f"Failed to render PDF preview: {e}")
return None
async def generate_audio(text, voice):
communicate = edge_tts.Communicate(text, voice)
buffer = io.BytesIO()
async for chunk in communicate.stream():
if chunk["type"] == "audio":
buffer.write(chunk["data"])
buffer.seek(0)
return buffer
# ==============================================================================
# THE MAIN STREAMLIT APP
# ==============================================================================
# 7. πŸ“¬ The Royal Messenger Service (State Handling)
if 'layout' in st.query_params:
st.session_state.page_layout = st.query_params.get('layout')
st.experimental_set_query_params()
st.rerun()
# --- Load base CSS and JS for components once ---
st.html(open('styles.html').read() if os.path.exists('styles.html') else '<style></style>') # Assumes CSS is in a file
# --- Sidebar Controls ---
with st.sidebar:
st.title("πŸš€ Ultimate PDF Creator")
# 8. 🎭 Summoning the Layout Selector
# We command our witty LayoutSelector component to appear and do its thing.
layout_selector = LayoutSelector(default_layout='Wide 16:9')
layout_selector.render()
st.subheader("βš™οΈ PDF Generation Settings")
pdf_settings = {
'num_columns': st.slider("Text columns", 1, 4, 2),
'base_font_size': st.slider("Base font size", 6, 24, 12),
'images_on_own_page': st.checkbox("Place each image on its own page", value=True),
'force_one_page': st.checkbox("Try to force text to one wide page", value=True, help="Widens the page and auto-sizes font to fit all text."),
'layout_name': st.session_state.get('page_layout', 'A4 Portrait')
}
# --- Main App Body ---
tab1, tab2 = st.tabs(["πŸ“„ PDF Composer & Voice Generator πŸš€", "πŸ§ͺ Code Interpreter"])
with tab1:
st.header(f"Live Preview ({st.session_state.get('page_layout', 'A4 Portrait')})")
# 9. πŸ–ΌοΈ The Main Event: The Live Canvas
st.markdown('<div class="dynamic-canvas-container">', unsafe_allow_html=True)
with st.container():
user_text = st.text_area("Start your masterpiece here...", height=300, key="main_content")
st.markdown('</div>', unsafe_allow_html=True)
col1, col2 = st.columns([0.6, 0.4])
with col1:
st.subheader("πŸ“€ Upload Your Content")
md_files = st.file_uploader("Upload Markdown Files (.md)", type=["md"], accept_multiple_files=True)
img_files = st.file_uploader("Upload Images (.png, .jpg)", type=["png", "jpg", "jpeg"], accept_multiple_files=True)
with col2:
st.subheader("πŸ”Š Text-to-Speech")
selected_voice = st.selectbox("Select Voice", ["en-US-AriaNeural", "en-GB-SoniaNeural", "en-US-JennyNeural"])
if st.button("Generate Audio from Text 🎀"):
full_text = user_text + ''.join(f.getvalue().decode() for f in md_files)
if full_text.strip():
with st.spinner("Warming up vocal cords..."):
audio_buffer = asyncio.run(generate_audio(full_text, selected_voice))
st.audio(audio_buffer, format="audio/mpeg")
else:
st.warning("Please provide some text first!")
st.markdown("---")
if st.button("Forge my PDF! βš”οΈ", type="primary", use_container_width=True):
markdown_texts = [user_text] + [f.getvalue().decode() for f in md_files]
markdown_texts = [text for text in markdown_texts if text.strip()]
with st.spinner("The PDF elves are hard at work... πŸ§β€β™‚οΈ"):
pdf_gen = PdfGenerator(markdown_texts, img_files, pdf_settings)
pdf_bytes = pdf_gen.generate()
if pdf_bytes:
st.success("Your PDF has been forged!")
st.download_button(label="πŸ“₯ Download PDF", data=pdf_bytes, file_name="generated_document.pdf", mime="application/pdf")
st.subheader("πŸ” PDF Preview")
pix_list = pdf_to_image(pdf_bytes.getvalue())
if pix_list:
for i, pix in enumerate(pix_list):
st.image(pix.tobytes("png"), caption=f"Page {i+1}")
else:
st.error("The PDF forging failed. Check your inputs and try again.")
with tab2:
st.header("πŸ§ͺ Python Code Interpreter")
st.info("This is the original code interpreter. Paste your Python code below to run it.")
code = st.text_area("Code Editor", height=400, value="import streamlit as st\nst.balloons()")
if st.button("Run Code ▢️"):
try:
exec(code)
except Exception as e:
st.error(f"😱 Code went kaboom! Error: {e}")