Spaces:
Sleeping
Sleeping
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}") |