Spaces:
Build error
Build error
| import gradio as gr | |
| from pathlib import Path | |
| import datetime | |
| import re | |
| import os | |
| import shutil | |
| import fitz # PyMuPDF | |
| from PIL import Image | |
| from collections import defaultdict | |
| import io | |
| from pypdf import PdfWriter | |
| import random | |
| # New imports for the SVG Emoji Engine | |
| from lxml import etree | |
| from svglib.svglib import svg2rlg | |
| from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak, BaseDocTemplate, Frame, PageTemplate, Image as ReportLabImage, Flowable | |
| from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle | |
| from reportlab.lib.pagesizes import letter, A4, legal, landscape | |
| from reportlab.lib.units import inch | |
| from reportlab.lib import colors | |
| from reportlab.pdfbase import pdfmetrics | |
| from reportlab.pdfbase.ttfonts import TTFont | |
| from functools import partial | |
| # --- Configuration & Setup --- | |
| CWD = Path.cwd() | |
| LAYOUTS = { | |
| "A4 Portrait": {"size": A4}, | |
| "A4 Landscape": {"size": landscape(A4)}, | |
| "Letter Portrait": {"size": letter}, | |
| "Letter Landscape": {"size": landscape(letter)}, | |
| "Legal Portrait": {"size": legal}, | |
| "Legal Landscape": {"size": landscape(legal)}, | |
| } | |
| OUTPUT_DIR = CWD / "generated_pdfs" | |
| PREVIEW_DIR = CWD / "previews" | |
| FONT_DIR = CWD # Assumes fonts are in the same directory as the script | |
| # Create necessary directories | |
| OUTPUT_DIR.mkdir(exist_ok=True) | |
| PREVIEW_DIR.mkdir(exist_ok=True) | |
| # --- Font & Emoji Handling --- | |
| # Global cache for rendered emoji images to avoid re-rendering the same emoji | |
| EMOJI_IMAGE_CACHE = {} | |
| EMOJI_SVG_CACHE = {} # New cache for the SVG engine | |
| EMOJI_FONT_PATH = None # Will be set in register_local_fonts | |
| def register_local_fonts(): | |
| """Finds and registers all .ttf files from the application's base directory.""" | |
| global EMOJI_FONT_PATH | |
| print("--- Font Registration Process Starting ---") | |
| text_font_names = [] | |
| emoji_font_name = None | |
| noto_emoji_path = FONT_DIR / "NotoColorEmoji-Regular.ttf" | |
| if not noto_emoji_path.exists(): | |
| print(f"CRITICAL: Color Emoji font not found at {noto_emoji_path}.") | |
| print("Please download 'NotoColorEmoji-Regular.ttf' and place it in the application directory for color emojis to work.") | |
| else: | |
| EMOJI_FONT_PATH = str(noto_emoji_path) | |
| print(f"Scanning for fonts in: {FONT_DIR.absolute()}") | |
| font_files = list(FONT_DIR.glob("*.ttf")) | |
| print(f"Found {len(font_files)} .ttf files: {[f.name for f in font_files]}") | |
| for font_path in font_files: | |
| try: | |
| font_name = font_path.stem | |
| pdfmetrics.registerFont(TTFont(font_name, str(font_path))) | |
| pdfmetrics.registerFont(TTFont(f"{font_name}-Bold", str(font_path))) | |
| pdfmetrics.registerFont(TTFont(f"{font_name}-Italic", str(font_path))) | |
| pdfmetrics.registerFont(TTFont(f"{font_name}-BoldItalic", str(font_path))) | |
| pdfmetrics.registerFontFamily(font_name, normal=font_name, bold=f"{font_name}-Bold", italic=f"{font_name}-Italic", boldItalic=f"{font_name}-BoldItalic") | |
| if "notocoloremoji-regular" in font_name.lower(): | |
| emoji_font_name = font_name | |
| elif "notoemoji" not in font_name.lower(): | |
| text_font_names.append(font_name) | |
| except Exception as e: | |
| print(f"Could not register font {font_path.name}: {e}") | |
| if not text_font_names: | |
| print("WARNING: No text fonts found. Adding 'Helvetica' as a default.") | |
| text_font_names.append('Helvetica') | |
| print(f"Successfully registered user-selectable fonts: {text_font_names}") | |
| print(f"Emoji font set to: {emoji_font_name}") | |
| print("--- Font Registration Process Finished ---") | |
| return sorted(text_font_names), emoji_font_name | |
| def render_emoji_as_image(emoji_char, size_pt): | |
| """ | |
| Renders a single emoji character as a transparent PNG image in memory using PyMuPDF. | |
| This is the original raster image engine. | |
| """ | |
| if not EMOJI_FONT_PATH: return None | |
| if (emoji_char, size_pt) in EMOJI_IMAGE_CACHE: return EMOJI_IMAGE_CACHE[(emoji_char, size_pt)] | |
| try: | |
| rect = fitz.Rect(0, 0, size_pt * 1.5, size_pt * 1.5) | |
| doc = fitz.open() | |
| page = doc.new_page(width=rect.width, height=rect.height) | |
| page.insert_font(fontname="emoji", fontfile=EMOJI_FONT_PATH) | |
| page.insert_text(fitz.Point(0, size_pt * 1.1), emoji_char, fontname="emoji", fontsize=size_pt) | |
| pix = page.get_pixmap(alpha=True, dpi=300) | |
| doc.close() | |
| img_buffer = io.BytesIO(pix.tobytes("png")) | |
| img_buffer.seek(0) | |
| EMOJI_IMAGE_CACHE[(emoji_char, size_pt)] = img_buffer | |
| return img_buffer | |
| except Exception as e: | |
| print(f"Could not render emoji {emoji_char} as image: {e}") | |
| return None | |
| def render_emoji_as_svg(emoji_char, size_pt): | |
| """ | |
| Renders a single emoji character as an SVG drawing object. | |
| This is the new vector graphics engine. | |
| """ | |
| if not EMOJI_FONT_PATH: return None | |
| if (emoji_char, size_pt) in EMOJI_SVG_CACHE: return EMOJI_SVG_CACHE[(emoji_char, size_pt)] | |
| try: | |
| doc = fitz.open() | |
| page = doc.new_page(width=1000, height=1000) # Large canvas | |
| page.insert_font(fontname="emoji", fontfile=EMOJI_FONT_PATH) | |
| # Get the SVG image for the character | |
| svg_image = page.get_svg_image(page.rect, text=emoji_char, fontname="emoji") | |
| doc.close() | |
| # Convert the SVG XML string to a ReportLab Graphic | |
| drawing = svg2rlg(io.BytesIO(svg_image.encode('utf-8'))) | |
| # Scale the drawing to match the font size | |
| scale_factor = (size_pt * 1.2) / drawing.height | |
| drawing.width *= scale_factor | |
| drawing.height *= scale_factor | |
| drawing.scale(scale_factor, scale_factor) | |
| EMOJI_SVG_CACHE[(emoji_char, size_pt)] = drawing | |
| return drawing | |
| except Exception as e: | |
| print(f"Could not render emoji {emoji_char} as SVG: {e}") | |
| return None | |
| # --- AI Content Generation (Simulation) --- | |
| def generate_ai_content_api(prompt): | |
| """ | |
| Simulates a call to an LLM to generate markdown content. | |
| """ | |
| if not prompt: | |
| return "# The Golem awaits your command!\n\nPlease enter a prompt in the box above and click '๐ง Animate Golem!' to get started. I can help you write reports, stories, poems, and more! โจ" | |
| import time | |
| time.sleep(1.5) | |
| sample_story = f""" | |
| # The Quest for the Sunstone โ๏ธ | |
| Long ago, in the mystical land of Aerthos, a creeping shadow began to fall across the valleys. The once-vibrant flowers ๐ธ drooped, and the rivers ran slow. The elders knew the cause: the light of the Sunstone, hidden deep within Mount Cinder, was fading. | |
| ## The Prophecy ๐ | |
| An ancient prophecy spoke of a hero who would rekindle the stone. It read: | |
| > When darkness drapes the verdant ground, | |
| > A soul of courage shall be found. | |
| > Through trials of fire ๐ฅ, wit, and might, | |
| > They'll bring once more the sacred light. | |
| ## The Chosen One ๐ฆธโโ๏ธ | |
| A young woman named Elara, known for her kindness and bravery, was chosen. She accepted the quest, her only companions a loyal wolf ๐บ and a map gifted by the village shaman. | |
| ### The Journey Begins | |
| Elara's journey was fraught with peril. She navigated enchanted forests and crossed treacherous chasms. | |
| | Trial | Location | Challenge | | |
| |------------------|-------------------|----------------------------------------| | |
| | The Whispering | The Gloomwood | Resist maddening whispers of despair | | |
| | The Riddle of | Sphinx Gate | Answer three impossible questions | | |
| | The Fiery Path | The Magma Caverns | Walk barefoot across burning embers | | |
| Finally, she reached the heart of the mountain. There, resting on a pedestal, was the dim Sunstone. Pouring her own hope and courage into it, the stone blazed with renewed life, banishing the shadows from Aerthos forever. The people rejoiced, and Elara became a legend. | |
| """ | |
| return f"# Golem's Vision for: '{prompt}'\n\n{sample_story}" | |
| # --- PDF Generation & Handling --- | |
| def _draw_header_footer(canvas, doc, header_text, footer_text, title): | |
| """Draws the header and footer on each page.""" | |
| canvas.saveState() | |
| page_num = canvas.getPageNumber() | |
| final_footer_text = footer_text.replace("[Page #]", str(page_num)).replace("[Total Pages]", str(doc.page)) | |
| final_header_text = header_text.replace("[Page #]", str(page_num)).replace("[Title]", title) | |
| if final_header_text: | |
| canvas.setFont('Helvetica', 9) | |
| canvas.setFillColor(colors.grey) | |
| canvas.drawRightString(doc.width + doc.leftMargin, doc.height + doc.topMargin + 0.25*inch, final_header_text) | |
| if final_footer_text: | |
| canvas.setFont('Helvetica', 9) | |
| canvas.setFillColor(colors.grey) | |
| canvas.drawString(doc.leftMargin, doc.bottomMargin - 0.25*inch, final_footer_text) | |
| canvas.restoreState() | |
| def markdown_to_story(markdown_text: str, font_name: str, emoji_font: str, font_size_body: int, font_size_h1: int, font_size_h2: int, font_size_h3: int, use_svg_engine: bool): | |
| """Converts markdown to a ReportLab story, with a switch for emoji rendering engines.""" | |
| styles = getSampleStyleSheet() | |
| leading_body = font_size_body * 1.4 | |
| style_normal = ParagraphStyle('BodyText', fontName=font_name, fontSize=font_size_body, leading=leading_body, spaceAfter=6) | |
| style_h1 = ParagraphStyle('h1', parent=styles['h1'], fontName=font_name, fontSize=font_size_h1, leading=font_size_h1*1.2, spaceBefore=12, textColor=colors.HexColor("#a855f7")) | |
| style_h2 = ParagraphStyle('h2', parent=styles['h2'], fontName=font_name, fontSize=font_size_h2, leading=font_size_h2*1.2, spaceBefore=10, textColor=colors.HexColor("#6366f1")) | |
| style_h3 = ParagraphStyle('h3', parent=styles['h3'], fontName=font_name, fontSize=font_size_h3, leading=font_size_h3*1.2, spaceBefore=8, textColor=colors.HexColor("#3b82f6")) | |
| style_code = ParagraphStyle('Code', fontName='Courier', backColor=colors.HexColor("#333333"), textColor=colors.HexColor("#f472b6"), borderWidth=1, borderColor=colors.HexColor("#444444"), padding=8, leading=12, fontSize=9) | |
| style_table_header = ParagraphStyle('TableHeader', parent=style_normal, fontName=f"{font_name}-Bold" if font_name != 'Helvetica' else 'Helvetica-Bold') | |
| emoji_pattern = re.compile(f"([{re.escape(''.join(map(chr, range(0x1f600, 0x1f650))))}" | |
| f"{re.escape(''.join(map(chr, range(0x1f300, 0x1f5ff))))}" | |
| f"{re.escape(''.join(map(chr, range(0x1f900, 0x1f9ff))))}" | |
| f"{re.escape(''.join(map(chr, range(0x2600, 0x26ff))))}" | |
| f"{re.escape(''.join(map(chr, range(0x2700, 0x27bf))))}]+)") | |
| def create_flowables_for_line(text, style): | |
| """Splits a line of text into text and emoji flowables, returning a list.""" | |
| parts = emoji_pattern.split(text) | |
| flowables = [] | |
| for part in parts: | |
| if not part: continue | |
| if emoji_pattern.match(part): | |
| for emoji_char in part: | |
| emoji_flowable = None | |
| if use_svg_engine: | |
| emoji_flowable = render_emoji_as_svg(emoji_char, style.fontSize) | |
| else: | |
| img_buffer = render_emoji_as_image(emoji_char, style.fontSize) | |
| if img_buffer: | |
| emoji_flowable = ReportLabImage(img_buffer, height=style.fontSize * 1.2, width=style.fontSize * 1.2) | |
| if emoji_flowable: | |
| flowables.append(emoji_flowable) | |
| else: | |
| formatted_part = re.sub(r'_(.*?)_', r'<i>\1</i>', re.sub(r'\*\*(.*?)\*\*', r'<b>\1</b>', part)) | |
| flowables.append(Paragraph(formatted_part, style)) | |
| if not flowables: return [Spacer(0, 0)] | |
| table = Table([flowables], colWidths=[None]*len(flowables)) | |
| table.setStyle(TableStyle([('VALIGN', (0,0), (-1,-1), 'MIDDLE'), ('LEFTPADDING', (0,0), (-1,-1), 0), ('RIGHTPADDING', (0,0), (-1,-1), 0)])) | |
| return [table] | |
| story = [] | |
| lines = markdown_text.split('\n') | |
| in_code_block, in_table = False, False | |
| code_block_text, table_data = "", [] | |
| first_heading = True | |
| document_title = "Untitled Document" | |
| for line in lines: | |
| stripped_line = line.strip() | |
| if stripped_line.startswith("```"): | |
| if in_code_block: | |
| escaped_code = code_block_text.replace('&', '&').replace('<', '<').replace('>', '>') | |
| story.append(Paragraph(escaped_code.replace('\n', '<br/>'), style_code)) | |
| story.append(Spacer(1, 0.1 * inch)) | |
| in_code_block, code_block_text = False, "" | |
| else: in_code_block = True | |
| continue | |
| if in_code_block: | |
| code_block_text += line + '\n'; continue | |
| if stripped_line.startswith('|'): | |
| if not in_table: in_table = True | |
| if all(c in '-|: ' for c in stripped_line): continue | |
| cells = [cell.strip() for cell in stripped_line.strip('|').split('|')] | |
| table_data.append(cells) | |
| continue | |
| if in_table: | |
| in_table = False | |
| if table_data: | |
| processed_table_data = [[create_flowables_for_line(cell, style_normal)[0] for cell in row] for row in table_data] | |
| header_row = [create_flowables_for_line(cell, style_table_header)[0] for cell in table_data[0]] | |
| final_table_data = [header_row] + processed_table_data[1:] | |
| table = Table(final_table_data, hAlign='LEFT', repeatRows=1) | |
| table.setStyle(TableStyle([ | |
| ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor("#4a044e")), | |
| ('GRID', (0, 0), (-1, -1), 1, colors.HexColor("#6b21a8")), | |
| ('VALIGN', (0,0), (-1,-1), 'MIDDLE'), | |
| ('TOPPADDING', (0,0), (-1,-1), 6), ('BOTTOMPADDING', (0,0), (-1,-1), 6), | |
| ])) | |
| story.append(table); story.append(Spacer(1, 0.2 * inch)) | |
| table_data = [] | |
| continue | |
| if not stripped_line: continue | |
| content, style, bullet_text = stripped_line, style_normal, None | |
| if stripped_line.startswith("# "): | |
| if not first_heading: story.append(PageBreak()) | |
| content, style = stripped_line.lstrip('# '), style_h1 | |
| if first_heading: document_title = content | |
| first_heading = False | |
| elif stripped_line.startswith("## "): content, style = stripped_line.lstrip('## '), style_h2 | |
| elif stripped_line.startswith("### "): content, style = stripped_line.lstrip('### '), style_h3 | |
| elif stripped_line.startswith(("- ", "* ")): | |
| content = stripped_line[2:] | |
| bullet_text = 'โข ' | |
| line_flowables = create_flowables_for_line(content, style) | |
| if bullet_text: | |
| bullet_p = Paragraph(bullet_text, style) | |
| # Adjust colWidths to give bullet a fixed space | |
| list_item_table = Table([[bullet_p] + line_flowables], colWidths=[style.fontSize*1.5] + [None]*len(line_flowables)) | |
| list_item_table.setStyle(TableStyle([('VALIGN', (0,0), (-1,-1), 'TOP'), ('LEFTPADDING', (0,0), (-1,-1), 0), ('RIGHTPADDING', (0,0), (-1,-1), 0)])) | |
| story.append(list_item_table) | |
| else: | |
| story.extend(line_flowables) | |
| return story, document_title | |
| def create_pdf_preview(pdf_path: Path): | |
| """Generates a PNG preview of the first page of a PDF.""" | |
| preview_path = PREVIEW_DIR / f"{pdf_path.stem}.png" | |
| try: | |
| doc = fitz.open(pdf_path) | |
| page = doc.load_page(0) | |
| pix = page.get_pixmap(dpi=150) | |
| pix.save(str(preview_path)) | |
| doc.close() | |
| return str(preview_path) | |
| except Exception as e: | |
| print(f"Could not create preview for {pdf_path.name}: {e}") | |
| try: | |
| img = Image.new('RGB', (400, 500), color = '#111827') | |
| img.save(str(preview_path)) | |
| return str(preview_path) | |
| except: return None | |
| # --- Main API Function --- | |
| def generate_pdfs_api(files, ai_content, layouts, fonts, num_columns, header_text, footer_text, font_size_body, font_size_h1, font_size_h2, font_size_h3, margin_top, margin_bottom, margin_left, margin_right, use_svg_engine, progress=gr.Progress(track_tqdm=True)): | |
| if not files and (not ai_content or "Golem awaits" in ai_content): raise gr.Error("Please conjure some content or upload a file before alchemizing!") | |
| if not layouts: raise gr.Error("You must select a scroll (page layout)!") | |
| if not fonts: raise gr.Error("A scribe needs a font! Please choose one.") | |
| if not EMOJI_FONT_PATH: raise gr.Error("CRITICAL: Cannot generate PDFs. 'NotoColorEmoji-Regular.ttf' not found. Please add it to the app directory.") | |
| shutil.rmtree(OUTPUT_DIR, ignore_errors=True); shutil.rmtree(PREVIEW_DIR, ignore_errors=True) | |
| OUTPUT_DIR.mkdir(); PREVIEW_DIR.mkdir() | |
| image_files, pdf_files, txt_files = [], [], [] | |
| if files: | |
| for f in files: | |
| file_path = Path(f.name) | |
| ext = file_path.suffix.lower() | |
| if ext in ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff']: image_files.append(file_path) | |
| elif ext == '.pdf': pdf_files.append(file_path) | |
| elif ext == '.txt': txt_files.append(file_path) | |
| log_updates = "" | |
| all_text_content = [] | |
| if ai_content and "Golem awaits" not in ai_content: all_text_content.append(ai_content) | |
| for txt_path in txt_files: | |
| try: | |
| all_text_content.append(txt_path.read_text(encoding='utf-8')) | |
| except Exception as e: | |
| log_updates += f"โ ๏ธ Failed to read text file {txt_path.name}: {e}\n" | |
| md_content = "\n\n---\n\n".join(all_text_content) | |
| generated_pdf_paths = [] | |
| EMOJI_IMAGE_CACHE.clear(); EMOJI_SVG_CACHE.clear() | |
| for layout_name in progress.tqdm(layouts, desc=" brewing potions..."): | |
| for font_name in progress.tqdm(fonts, desc=f" enchanting scrolls with {layout_name}..."): | |
| merger = PdfWriter() | |
| if md_content: | |
| md_buffer = io.BytesIO() | |
| story, title = markdown_to_story(md_content, font_name, EMOJI_FONT_NAME, font_size_body, font_size_h1, font_size_h2, font_size_h3, use_svg_engine) | |
| pagesize = LAYOUTS[layout_name]["size"] | |
| doc = BaseDocTemplate(md_buffer, pagesize=pagesize, leftMargin=margin_left*inch, rightMargin=margin_right*inch, topMargin=margin_top*inch, bottomMargin=margin_bottom*inch) | |
| frame_width = (doc.width / num_columns) - (num_columns - 1) * 0.1*inch | |
| frames = [Frame(doc.leftMargin + i * (frame_width + 0.2*inch), doc.bottomMargin, frame_width, doc.height, id=f'col_{i}') for i in range(num_columns)] | |
| header_footer_callback = partial(_draw_header_footer, header_text=header_text, footer_text=footer_text, title=title) | |
| page_template = PageTemplate(id='main_template', frames=frames, onPage=header_footer_callback) | |
| doc.addPageTemplates([page_template]) | |
| doc.build(story) | |
| md_buffer.seek(0) | |
| merger.append(fileobj=md_buffer) | |
| for img_path in image_files: | |
| try: | |
| with Image.open(img_path) as img: img_width_px, img_height_px = img.size | |
| img_width_pt, img_height_pt = img_width_px * (inch / 72), img_height_px * (inch / 72) | |
| img_buffer = io.BytesIO() | |
| img_doc = SimpleDocTemplate(img_buffer, pagesize=(img_width_pt, img_height_pt), leftMargin=0, rightMargin=0, topMargin=0, bottomMargin=0) | |
| img_doc.build([ReportLabImage(img_path, width=img_width_pt, height=img_height_pt)]) | |
| img_buffer.seek(0) | |
| merger.append(fileobj=img_buffer) | |
| except Exception as e: log_updates += f"โ ๏ธ Failed to process image {img_path.name}: {e}\n" | |
| for pdf_path in pdf_files: | |
| try: | |
| merger.append(str(pdf_path)) | |
| except Exception as e: log_updates += f"โ ๏ธ Failed to merge PDF {pdf_path.name}: {e}\n" | |
| if len(merger.pages) > 0: | |
| time_str = datetime.datetime.now().strftime('%H%M%S') | |
| clean_layout = layout_name.replace(' ', '') | |
| filename = f"Scroll_{clean_layout}_{font_name}_x{num_columns}_{time_str}.pdf" | |
| output_path = OUTPUT_DIR / filename | |
| with open(output_path, "wb") as f: merger.write(f) | |
| generated_pdf_paths.append(output_path) | |
| log_updates += f"โ Successfully alchemized: {filename}\n" | |
| gallery_previews = [create_pdf_preview(p) for p in generated_pdf_paths] | |
| final_gallery = [g for g in gallery_previews if g is not None] | |
| return final_gallery, log_updates if log_updates else "โจ All scrolls alchemized successfully! โจ", [str(p) for p in generated_pdf_paths] | |
| # --- Gradio UI Definition --- | |
| AVAILABLE_FONTS, EMOJI_FONT_NAME = register_local_fonts() | |
| def get_theme(): | |
| """Dynamically selects a font for the theme to avoid warnings for missing fonts.""" | |
| desired_font = "MedievalSharp" | |
| font_family = None | |
| if any(desired_font in s for s in AVAILABLE_FONTS): | |
| font_family = (gr.themes.GoogleFont(desired_font), "ui-sans-serif", "system-ui", "sans-serif") | |
| elif AVAILABLE_FONTS: | |
| first_font = AVAILABLE_FONTS[0] | |
| print(f"WARNING: '{desired_font}' font not found. Using '{first_font}' for UI theme instead.") | |
| font_family = (gr.themes.GoogleFont(first_font), "ui-sans-serif", "system-ui", "sans-serif") | |
| else: | |
| print(f"WARNING: '{desired_font}' font not found and no other fonts available. Using system default.") | |
| font_family = ("ui-sans-serif", "system-ui", "sans-serif") | |
| return gr.themes.Base(primary_hue=gr.themes.colors.purple, secondary_hue=gr.themes.colors.indigo, neutral_hue=gr.themes.colors.gray, font=font_family).set( | |
| body_background_fill="#111827", body_text_color="#d1d5db", button_primary_background_fill="#a855f7", button_primary_text_color="#ffffff", | |
| button_secondary_background_fill="#6366f1", button_secondary_text_color="#ffffff", block_background_fill="#1f2937", | |
| block_label_background_fill="#1f2937", block_title_text_color="#a855f7", input_background_fill="#374151" | |
| ) | |
| with gr.Blocks(theme=get_theme(), title="The PDF Alchemist") as demo: | |
| gr.Markdown("# โจ The PDF Alchemist โจ") | |
| gr.Markdown("A single-page grimoire to turn your ideas into beautifully crafted PDF scrolls. Use the power of AI or upload your own treasures.") | |
| with gr.Row(equal_height=False): | |
| with gr.Column(scale=2): | |
| # --- Accordion Groups for Inputs --- | |
| with gr.Accordion("๐ Content Crucible (Your Ingredients)", open=True): | |
| gr.Markdown("### ๐ค Command Your Idea Golem") | |
| ai_prompt = gr.Textbox(label="Incantation (Prompt)", placeholder="e.g., 'A recipe for a dragon's breath chili...'") | |
| generate_ai_btn = gr.Button("๐ง Animate Golem!") | |
| ai_content_output = gr.Textbox(label="Golem's Manuscript (Editable)", lines=10, interactive=True, value="# The Golem awaits your command!\n\n") | |
| gr.Markdown("<hr style='border-color: #374151; margin-top: 20px; margin-bottom: 20px;'>") | |
| gr.Markdown("### ๐ค Add Your Physical Treasures") | |
| uploaded_files = gr.File(label="Upload Files (Images, PDFs, TXT)", file_count="multiple", file_types=['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff', '.pdf', '.txt']) | |
| with gr.Accordion("๐ Arcane Blueprints (Layout & Structure)", open=True): | |
| gr.Markdown("### Page & Column Spells") | |
| selected_layouts = gr.CheckboxGroup(choices=list(LAYOUTS.keys()), label="Page Layouts", value=["A4 Portrait"]) | |
| num_columns_slider = gr.Slider(label="Number of Text Columns", minimum=1, maximum=4, step=1, value=1) | |
| gr.Markdown("<hr style='border-color: #374151; margin-top: 20px; margin-bottom: 20px;'>") | |
| gr.Markdown("### Header & Footer Runes") | |
| header_input = gr.Textbox(label="Header Inscription", value="[Title]", placeholder="e.g., Arcane Folio - [Page #]") | |
| footer_input = gr.Textbox(label="Footer Inscription", value="Page [Page #] of [Total Pages]", placeholder="e.g., Top Secret - Page [Page #]") | |
| with gr.Accordion("๐ Stylist's Sanctum (Fonts & Margins)", open=True): | |
| gr.Markdown("### โ๏ธ Alternative Emoji Engine") | |
| use_svg_engine_toggle = gr.Checkbox(label="Use SVG Emoji Engine (Vector Quality)", value=True) | |
| gr.Markdown("*<p style='font-size:0.8rem; color: #9ca3af;'>Toggle for higher quality, resolution-independent emojis. May be slower.</p>*") | |
| gr.Markdown("<hr style='border-color: #374151; margin-top: 20px; margin-bottom: 20px;'>") | |
| gr.Markdown("### ๐ค Master the Glyphs") | |
| selected_fonts = gr.CheckboxGroup(choices=AVAILABLE_FONTS, label="Fonts", value=[AVAILABLE_FONTS[0]] if AVAILABLE_FONTS else []) | |
| with gr.Row(): | |
| font_size_body_slider = gr.Slider(label="Body (pt)", minimum=8, maximum=16, step=1, value=10) | |
| font_size_h1_slider = gr.Slider(label="H1 (pt)", minimum=16, maximum=32, step=1, value=24) | |
| with gr.Row(): | |
| font_size_h2_slider = gr.Slider(label="H2 (pt)", minimum=14, maximum=28, step=1, value=18) | |
| font_size_h3_slider = gr.Slider(label="H3 (pt)", minimum=12, maximum=24, step=1, value=14) | |
| gr.Markdown("<hr style='border-color: #374151; margin-top: 20px; margin-bottom: 20px;'>") | |
| gr.Markdown("### ๐ Set Your Boundaries (inches)") | |
| with gr.Row(): | |
| margin_top_slider = gr.Slider(label="Top", minimum=0.25, maximum=1.5, step=0.05, value=0.75) | |
| margin_bottom_slider = gr.Slider(label="Bottom", minimum=0.25, maximum=1.5, step=0.05, value=0.75) | |
| with gr.Row(): | |
| margin_left_slider = gr.Slider(label="Left", minimum=0.25, maximum=1.5, step=0.05, value=0.75) | |
| margin_right_slider = gr.Slider(label="Right", minimum=0.25, maximum=1.5, step=0.05, value=0.75) | |
| generate_pdfs_btn = gr.Button("๐ฎ Alchemize PDF!", variant="primary", size="lg") | |
| with gr.Column(scale=3): | |
| gr.Markdown("### ๐ผ๏ธ The Scrying Pool (Previews)") | |
| gallery_output = gr.Gallery(label="Generated PDF Previews", show_label=False, elem_id="gallery", columns=2, height=700, object_fit="contain") | |
| log_output = gr.Markdown(label="Alchemist's Log", value="Your log of successful transmutations will appear here...") | |
| downloadable_files_output = gr.Files(label="Collect Your Scrolls") | |
| # --- API Calls & Examples --- | |
| generate_ai_btn.click(fn=generate_ai_content_api, inputs=[ai_prompt], outputs=[ai_content_output]) | |
| inputs_list = [uploaded_files, ai_content_output, selected_layouts, selected_fonts, num_columns_slider, header_input, footer_input, font_size_body_slider, font_size_h1_slider, font_size_h2_slider, font_size_h3_slider, margin_top_slider, margin_bottom_slider, margin_left_slider, margin_right_slider, use_svg_engine_toggle] | |
| outputs_list = [gallery_output, log_output, downloadable_files_output] | |
| generate_pdfs_btn.click(fn=generate_pdfs_api, inputs=inputs_list, outputs=outputs_list) | |
| gr.Examples(examples=[["A technical summary of how alchemy works"], ["A short poem about a grumpy gnome"],["A sample agenda for a wizard's council meeting"]], inputs=[ai_prompt], outputs=[ai_content_output], fn=generate_ai_content_api, cache_examples=False) | |
| if __name__ == "__main__": | |
| if not (FONT_DIR / "NotoColorEmoji-Regular.ttf").exists(): | |
| print("\n" + "="*80) | |
| print("CRITICAL WARNING: 'NotoColorEmoji-Regular.ttf' not found.") | |
| print("The application will fail to generate PDFs without it.") | |
| print("="*80 + "\n") | |
| demo.launch(debug=True) | |