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"" 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'\1\2', 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 '') # 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('