|
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 |
|
|
|
|
|
|
|
st.set_page_config(page_title="Ultimate PDF & Code Interpreter", layout="wide", page_icon="π") |
|
|
|
|
|
|
|
|
|
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()) |
|
|
|
|
|
|
|
|
|
|
|
|
|
class PdfGenerator: |
|
"""The engine room. This class takes your content and forges it into a PDF.""" |
|
|
|
|
|
|
|
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() |
|
|
|
|
|
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}") |
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
min_font, max_font = 5, 20 |
|
|
|
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)) |
|
|
|
|
|
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) |
|
if avg_chars_per_line > col_width_chars * 0.9: |
|
font_size = max(min_font, font_size * 0.85) |
|
|
|
return int(font_size) |
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
if self.settings['force_one_page']: |
|
page_width *= self.settings['num_columns'] |
|
page_height *= 1.5 |
|
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) |
|
|
|
|
|
styles = getSampleStyleSheet() |
|
item_style = ParagraphStyle('ItemStyle', parent=styles['Normal'], fontName="DejaVuSans", fontSize=font_size, leading=font_size * 1.2) |
|
|
|
|
|
column_data = [[] for _ in range(num_columns)] |
|
for i, item in enumerate(full_text): |
|
|
|
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 |
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
custom_page = (img_width + 72, img_height + 72) |
|
doc = SimpleDocTemplate(self.buffer, pagesize=custom_page) |
|
|
|
|
|
frame = doc.getFrame('normal') |
|
doc.addPageTemplates(st.PageTemplate(id='ImagePage', frames=[frame], pagesize=custom_page)) |
|
|
|
self.story.append(ReportLabImage(img_file, width=img_width, height=img_height)) |
|
|
|
|
|
|
|
|
|
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: |
|
for img_file in self.image_files: |
|
self.story.append(ReportLabImage(img_file)) |
|
|
|
doc.build(self.story) |
|
self.buffer.seek(0) |
|
return self.buffer |
|
|
|
|
|
|
|
|
|
def markdown_to_pdf_content(markdown_text): |
|
lines = markdown_text.strip().split('\n') |
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
if 'layout' in st.query_params: |
|
st.session_state.page_layout = st.query_params.get('layout') |
|
st.experimental_set_query_params() |
|
st.rerun() |
|
|
|
|
|
st.html(open('styles.html').read() if os.path.exists('styles.html') else '<style></style>') |
|
|
|
|
|
with st.sidebar: |
|
st.title("π Ultimate PDF Creator") |
|
|
|
|
|
|
|
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') |
|
} |
|
|
|
|
|
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')})") |
|
|
|
|
|
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}") |