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, ImageDraw, ImageFont | |
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() | |
TEMP_DIR = CWD / "temp_icons" | |
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) | |
TEMP_DIR.mkdir(exist_ok=True) | |
# --- Font & Emoji Handling --- | |
EMOJI_IMAGE_CACHE, EMOJI_SVG_CACHE = {}, {} | |
EMOJI_FONT_PATH, UI_FONT_PATH = None, None | |
def register_local_fonts(): | |
"""Finds and registers all .ttf files from the application's base directory.""" | |
global EMOJI_FONT_PATH, UI_FONT_PATH | |
print("--- Font Registration Process Starting ---") | |
text_font_names = [] | |
noto_emoji_path = FONT_DIR / "NotoColorEmoji-Regular.ttf" | |
if noto_emoji_path.exists(): | |
EMOJI_FONT_PATH = str(noto_emoji_path) | |
else: | |
print("CRITICAL: 'NotoColorEmoji-Regular.ttf' not found. Color emojis will not work.") | |
print(f"Scanning for fonts in: {FONT_DIR.absolute()}") | |
font_files = list(FONT_DIR.glob("*.ttf")) | |
for font_path in font_files: | |
try: | |
font_name = font_path.stem | |
if not UI_FONT_PATH: UI_FONT_PATH = str(font_path) # Grab first available font for UI icons | |
pdfmetrics.registerFont(TTFont(font_name, str(font_path))) | |
pdfmetrics.registerFontFamily(font_name, normal=font_name, bold=font_name, italic=font_name, boldItalic=font_name) | |
if "notocoloremoji-regular" not in font_name.lower() and "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: | |
text_font_names.append('Helvetica') | |
print(f"Successfully registered user-selectable fonts: {text_font_names}") | |
return sorted(text_font_names) | |
def render_emoji_as_image(emoji_char, size_pt): | |
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, doc = fitz.Rect(0, 0, size_pt*1.5, size_pt*1.5), 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, img_buffer = page.get_pixmap(alpha=True, dpi=300), io.BytesIO(pix.tobytes("png")) | |
doc.close(); 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): | |
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) | |
page.insert_font(fontname="emoji", fontfile=EMOJI_FONT_PATH) | |
svg_image = page.get_svg_image(page.rect, text=emoji_char, fontname="emoji") | |
doc.close() | |
drawing = svg2rlg(io.BytesIO(svg_image.encode('utf-8'))) | |
scale_factor = (size_pt * 1.2) / drawing.height | |
drawing.width, 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. | |
""" | |
return f"# Golem's Vision for: '{prompt}'\n\n{sample_story}" | |
# --- UI & File Handling --- | |
FILE_ICONS = defaultdict(lambda: 'โ', { | |
'.md': '๏ฟฝ', '.txt': '๐', '.py': '๐', '.js': 'โก', '.html': '๐', '.css': '๐จ', '.json': '๐ฎ', | |
'.png': '๐ผ๏ธ', '.jpg': '๐ผ๏ธ', '.jpeg': '๐ผ๏ธ', '.gif': '๐ผ๏ธ', '.bmp': '๐ผ๏ธ', '.tiff': '๐ผ๏ธ', | |
'.pdf': '๐' | |
}) | |
def create_file_icon_image(file_path: Path): | |
"""Creates a preview image for a file with an icon and its name.""" | |
icon = FILE_ICONS[file_path.suffix.lower()] | |
img = Image.new('RGB', (150, 100), color='#1f2937') | |
draw = ImageDraw.Draw(img) | |
try: | |
icon_font = ImageFont.truetype(EMOJI_FONT_PATH, 40) if EMOJI_FONT_PATH else ImageFont.load_default() | |
text_font = ImageFont.truetype(UI_FONT_PATH, 10) if UI_FONT_PATH else ImageFont.load_default() | |
except: | |
icon_font, text_font = ImageFont.load_default(), ImageFont.load_default() | |
draw.text((75, 40), icon, font=icon_font, anchor="mm", fill="#a855f7") | |
draw.text((75, 80), file_path.name, font=text_font, anchor="mm", fill="#d1d5db") | |
out_path = TEMP_DIR / f"{file_path.stem}_{file_path.suffix[1:]}.png" | |
img.save(out_path) | |
return out_path | |
def update_staging_and_manuscript(files, current_manuscript): | |
"""Processes uploaded files, updates staging galleries, and consolidates text content.""" | |
shutil.rmtree(TEMP_DIR, ignore_errors=True); TEMP_DIR.mkdir(exist_ok=True) | |
text_gallery, image_gallery, pdf_gallery = [], [], [] | |
text_content_to_add = [] | |
if not files: | |
return text_gallery, image_gallery, pdf_gallery, current_manuscript | |
sorted_files = sorted(files, key=lambda x: Path(x.name).suffix) | |
for file_obj in sorted_files: | |
file_path = Path(file_obj.name) | |
ext = file_path.suffix.lower() | |
if ext in ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff']: | |
image_gallery.append((file_path, file_path.name)) | |
elif ext == '.pdf': | |
pdf_gallery.append((create_file_icon_image(file_path), file_path.name)) | |
else: # Treat everything else as a text file | |
text_gallery.append((create_file_icon_image(file_path), file_path.name)) | |
try: | |
with open(file_path, 'r', encoding='utf-8') as f: | |
text_content_to_add.append(f.read()) | |
except Exception as e: | |
print(f"Could not read {file_path.name}: {e}") | |
# Update manuscript | |
updated_manuscript = current_manuscript | |
if "Golem awaits" in updated_manuscript: | |
updated_manuscript = "" # Clear placeholder if it exists | |
if text_content_to_add: | |
separator = "\n\n---\n\n" | |
if not updated_manuscript.strip(): separator = "" # Don't add separator if manuscript is empty | |
updated_manuscript += separator + "\n\n".join(text_content_to_add) | |
return text_gallery, image_gallery, pdf_gallery, updated_manuscript | |
# --- Main PDF Generation Logic (heavily adapted from before) --- | |
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, font_name, font_sizes, use_svg_engine): | |
styles = getSampleStyleSheet() | |
font_size_body, font_size_h1, font_size_h2, font_size_h3 = font_sizes | |
style_normal = ParagraphStyle('BodyText', fontName=font_name, fontSize=font_size_body, leading=font_size_body*1.4, 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=font_name) # Bold is handled in text | |
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): | |
parts, flowables = emoji_pattern.split(text), [] | |
for part in parts: | |
if not part: continue | |
if emoji_pattern.match(part): | |
for emoji_char in part: | |
emoji_flowable = render_emoji_as_svg(emoji_char, style.fontSize) if use_svg_engine else (lambda b: ReportLabImage(b, height=style.fontSize*1.2, width=style.fontSize*1.2) if b else None)(render_emoji_as_image(emoji_char, style.fontSize)) | |
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, first_heading, document_title = False, False, True, "Untitled Document" | |
code_block_text, table_data = "", [] | |
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.extend([Paragraph(escaped_code.replace('\n', '<br/>'), style_code), 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 | |
table_data.append([cell.strip() for cell in stripped_line.strip('|').split('|')]) | |
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[1:]] | |
header_row = [create_flowables_for_line(f"<b>{cell}</b>", style_table_header)[0] for cell in table_data[0]] | |
table = Table([header_row] + processed_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.extend([table, 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, first_heading = stripped_line.lstrip('# '), style_h1, False | |
if document_title == "Untitled Document": document_title = content | |
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, bullet_text = stripped_line[2:], 'โข ' | |
line_flowables = create_flowables_for_line(content, style) | |
if bullet_text: | |
list_item_table = Table([[Paragraph(bullet_text, style)] + 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 generate_pdfs_api(files, ai_content, layouts, fonts, num_columns, header_text, footer_text, font_sizes, margins, 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.") | |
shutil.rmtree(OUTPUT_DIR, ignore_errors=True); OUTPUT_DIR.mkdir() | |
shutil.rmtree(PREVIEW_DIR, ignore_errors=True); PREVIEW_DIR.mkdir() | |
image_files, pdf_files, txt_files = [], [], [] | |
if files: | |
for f in files: | |
p = Path(f.name); ext = p.suffix.lower() | |
if ext in ['.png','.jpg','.jpeg','.gif','.bmp','.tiff']: image_files.append(p) | |
elif ext == '.pdf': pdf_files.append(p) | |
else: txt_files.append(p) | |
log, all_text = "", [] | |
if ai_content and "Golem awaits" not in ai_content: all_text.append(ai_content) | |
for p in txt_files: | |
try: all_text.append(p.read_text(encoding='utf-8')) | |
except Exception as e: log += f"โ ๏ธ Failed to read {p.name}: {e}\n" | |
md_content = "\n\n---\n\n".join(all_text) | |
generated_pdfs = [] | |
EMOJI_IMAGE_CACHE.clear(); EMOJI_SVG_CACHE.clear() | |
for layout in progress.tqdm(layouts, desc="brewing potions..."): | |
for font in progress.tqdm(fonts, desc=f"enchanting scrolls with {layout}..."): | |
merger = PdfWriter() | |
if md_content: | |
buffer, (story, title) = io.BytesIO(), markdown_to_story(md_content, font, font_sizes, use_svg_engine) | |
doc = BaseDocTemplate(buffer, pagesize=LAYOUTS[layout]["size"], leftMargin=margins[2]*inch, rightMargin=margins[3]*inch, topMargin=margins[0]*inch, bottomMargin=margins[1]*inch) | |
frame_w = (doc.width / num_columns) - (num_columns - 1)*0.1*inch | |
frames = [Frame(doc.leftMargin + i*(frame_w+0.2*inch), doc.bottomMargin, frame_w, doc.height, id=f'col_{i}') for i in range(num_columns)] | |
doc.addPageTemplates([PageTemplate(id='main', frames=frames, onPage=partial(_draw_header_footer, header_text=header_text, footer_text=footer_text, title=title))]) | |
doc.build(story); buffer.seek(0); merger.append(fileobj=buffer) | |
for p in image_files: | |
try: | |
with Image.open(p) as img: w, h = img.size | |
w_pt, h_pt = w * (inch/72), h * (inch/72) | |
buffer = io.BytesIO() | |
SimpleDocTemplate(buffer, pagesize=(w_pt, h_pt), leftMargin=0,rightMargin=0,topMargin=0,bottomMargin=0).build([ReportLabImage(p, width=w_pt, height=h_pt)]) | |
buffer.seek(0); merger.append(fileobj=buffer) | |
except Exception as e: log += f"โ ๏ธ Failed to process image {p.name}: {e}\n" | |
for p in pdf_files: | |
try: merger.append(str(p)) | |
except Exception as e: log += f"โ ๏ธ Failed to merge PDF {p.name}: {e}\n" | |
if len(merger.pages) > 0: | |
filename = f"Scroll_{layout.replace(' ','')}_{font}_x{num_columns}_{datetime.datetime.now().strftime('%H%M%S')}.pdf" | |
out_path = OUTPUT_DIR / filename | |
with open(out_path, "wb") as f: merger.write(f) | |
generated_pdfs.append(out_path); log += f"โ Alchemized: {filename}\n" | |
previews = [p for p in [create_pdf_preview(pdf) for pdf in generated_pdfs] if p] | |
return previews, log if log else "โจ All scrolls alchemized successfully! โจ", [str(p) for p in generated_pdfs] | |
# --- Gradio UI Definition --- | |
AVAILABLE_FONTS = register_local_fonts() | |
def get_theme(): | |
"""Dynamically selects a font for the theme to avoid warnings for missing fonts.""" | |
desired = "MedievalSharp" | |
fallback = ("ui-sans-serif", "system-ui", "sans-serif") | |
font_family = fallback | |
# Check if the desired font is in the list of registered fonts | |
if any(desired in s for s in AVAILABLE_FONTS): | |
font_family = (gr.themes.GoogleFont(desired),) + fallback | |
elif AVAILABLE_FONTS: | |
# If desired font is not found, use the first available font as a better fallback | |
first_font = AVAILABLE_FONTS[0] | |
print(f"WARNING: '{desired}' font not found. Using '{first_font}' for UI theme instead.") | |
font_family = (gr.themes.GoogleFont(first_font),) + fallback | |
else: | |
# Absolute fallback if no custom fonts are found | |
print(f"WARNING: No custom fonts found. Using system default for UI.") | |
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): | |
# --- LEFT COLUMN: INPUTS & CONTROLS --- | |
with gr.Column(scale=2): | |
with gr.Group(): | |
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 (TXT, MD, Code, Images, PDFs)", file_count="multiple", file_types=['text', '.md', 'image', '.pdf', '.py', '.js', '.html', '.css', '.json']) | |
with gr.Accordion("๐ Arcane Blueprints (Layout & Structure)", open=True): | |
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) | |
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): | |
use_svg_engine_toggle = gr.Checkbox(label="Use SVG Emoji Engine (Vector Quality)", value=True, info="Toggle for higher quality, resolution-independent emojis. May be slower.") | |
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) | |
with gr.Row(): | |
margin_top_slider = gr.Slider(label="Margin Top (in)", minimum=0.25, maximum=1.5, step=0.05, value=0.75) | |
margin_bottom_slider = gr.Slider(label="Margin Bottom (in)", minimum=0.25, maximum=1.5, step=0.05, value=0.75) | |
with gr.Row(): | |
margin_left_slider = gr.Slider(label="Margin Left (in)", minimum=0.25, maximum=1.5, step=0.05, value=0.75) | |
margin_right_slider = gr.Slider(label="Margin Right (in)", minimum=0.25, maximum=1.5, step=0.05, value=0.75) | |
generate_pdfs_btn = gr.Button("๐ฎ Alchemize PDF!", variant="primary", size="lg") | |
# --- RIGHT COLUMN: STAGING & OUTPUTS --- | |
with gr.Column(scale=3): | |
with gr.Group(): | |
with gr.Accordion("๐ Staging Area (Your Uploaded Treasures)", open=True): | |
text_gallery = gr.Gallery(label="๐ Manuscripts & Code", show_label=True, columns=4, height=120, object_fit="contain") | |
image_gallery = gr.Gallery(label="๐ผ๏ธ Images & Glyphs", show_label=True, columns=4, height=120, object_fit="contain") | |
pdf_gallery = gr.Gallery(label="๐ Imported Scrolls", show_label=True, columns=4, height=120, object_fit="contain") | |
with gr.Group(): | |
gr.Markdown("### โ๏ธ Transmuted Scrolls (Final PDFs)") | |
final_gallery_output = gr.Gallery(label="PDF Previews", show_label=False, elem_id="gallery", columns=2, height=400, 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 --- | |
font_size_inputs = [font_size_body_slider, font_size_h1_slider, font_size_h2_slider, font_size_h3_slider] | |
margin_inputs = [margin_top_slider, margin_bottom_slider, margin_left_slider, margin_right_slider] | |
uploaded_files.upload(update_staging_and_manuscript, inputs=[uploaded_files, ai_content_output], outputs=[text_gallery, image_gallery, pdf_gallery, ai_content_output]) | |
generate_ai_btn.click(generate_ai_content_api, inputs=[ai_prompt], outputs=[ai_content_output]) | |
generate_pdfs_btn.click(generate_pdfs_api, | |
inputs=[uploaded_files, ai_content_output, selected_layouts, selected_fonts, num_columns_slider, header_input, footer_input] + font_size_inputs + margin_inputs + [use_svg_engine_toggle], | |
outputs=[final_gallery_output, log_output, downloadable_files_output]) | |
gr.Examples(examples=[["A technical summary of how alchemy works"],["A short poem about a grumpy gnome"]], inputs=[ai_prompt], outputs=[ai_content_output], fn=generate_ai_content_api, cache_examples=False) | |
if __name__ == "__main__": | |
if not EMOJI_FONT_PATH: | |
print("\n" + "="*80 + "\nCRITICAL WARNING: 'NotoColorEmoji-Regular.ttf' not found.\nThe application will fail to generate PDFs with color emojis.\n" + "="*80 + "\n") | |
demo.launch(debug=True) |