Spaces:
Build error
Build error
Update app.py
Browse files
app.py
CHANGED
@@ -11,7 +11,7 @@ import io
|
|
11 |
from pypdf import PdfWriter
|
12 |
import random
|
13 |
|
14 |
-
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak, BaseDocTemplate, Frame, PageTemplate, Image as ReportLabImage
|
15 |
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
16 |
from reportlab.lib.pagesizes import letter, A4, legal, landscape
|
17 |
from reportlab.lib.units import inch
|
@@ -41,16 +41,24 @@ PREVIEW_DIR.mkdir(exist_ok=True)
|
|
41 |
|
42 |
# --- Font & Emoji Handling ---
|
43 |
|
|
|
|
|
|
|
|
|
44 |
def register_local_fonts():
|
45 |
"""Finds and registers all .ttf files from the application's base directory."""
|
|
|
46 |
print("--- Font Registration Process Starting ---")
|
47 |
text_font_names = []
|
48 |
emoji_font_name = None
|
49 |
|
50 |
noto_emoji_path = FONT_DIR / "NotoColorEmoji-Regular.ttf"
|
51 |
if not noto_emoji_path.exists():
|
52 |
-
print(f"
|
53 |
-
print("Please download 'NotoColorEmoji-Regular.ttf' and place it in the application directory.")
|
|
|
|
|
|
|
54 |
|
55 |
print(f"Scanning for fonts in: {FONT_DIR.absolute()}")
|
56 |
font_files = list(FONT_DIR.glob("*.ttf"))
|
@@ -81,24 +89,49 @@ def register_local_fonts():
|
|
81 |
print("--- Font Registration Process Finished ---")
|
82 |
return sorted(text_font_names), emoji_font_name
|
83 |
|
84 |
-
def
|
85 |
-
"""
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
|
|
94 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
95 |
|
96 |
# --- AI Content Generation (Simulation) ---
|
97 |
|
98 |
def generate_ai_content_api(prompt):
|
99 |
"""
|
100 |
Simulates a call to an LLM to generate markdown content.
|
101 |
-
In a real application, this would contain `fetch` calls to a generative AI API.
|
102 |
"""
|
103 |
if not prompt:
|
104 |
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! ✨"
|
@@ -144,17 +177,14 @@ def _draw_header_footer(canvas, doc, header_text, footer_text, title):
|
|
144 |
canvas.saveState()
|
145 |
page_num = canvas.getPageNumber()
|
146 |
|
147 |
-
# Replace variables for header and footer. Note: [Total Pages] is only accurate at the end of the build.
|
148 |
final_footer_text = footer_text.replace("[Page #]", str(page_num)).replace("[Total Pages]", str(doc.page))
|
149 |
final_header_text = header_text.replace("[Page #]", str(page_num)).replace("[Title]", title)
|
150 |
|
151 |
-
# Header
|
152 |
if final_header_text:
|
153 |
canvas.setFont('Helvetica', 9)
|
154 |
canvas.setFillColor(colors.grey)
|
155 |
canvas.drawRightString(doc.width + doc.leftMargin, doc.height + doc.topMargin + 0.25*inch, final_header_text)
|
156 |
|
157 |
-
# Footer
|
158 |
if final_footer_text:
|
159 |
canvas.setFont('Helvetica', 9)
|
160 |
canvas.setFillColor(colors.grey)
|
@@ -163,7 +193,7 @@ def _draw_header_footer(canvas, doc, header_text, footer_text, title):
|
|
163 |
canvas.restoreState()
|
164 |
|
165 |
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):
|
166 |
-
"""Converts markdown to a ReportLab story
|
167 |
styles = getSampleStyleSheet()
|
168 |
|
169 |
leading_body = font_size_body * 1.4
|
@@ -174,6 +204,44 @@ def markdown_to_story(markdown_text: str, font_name: str, emoji_font: str, font_
|
|
174 |
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)
|
175 |
style_table_header = ParagraphStyle('TableHeader', parent=style_normal, fontName=f"{font_name}-Bold" if font_name != 'Helvetica' else 'Helvetica-Bold')
|
176 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
177 |
story = []
|
178 |
lines = markdown_text.split('\n')
|
179 |
|
@@ -205,18 +273,21 @@ def markdown_to_story(markdown_text: str, font_name: str, emoji_font: str, font_
|
|
205 |
if in_table:
|
206 |
in_table = False
|
207 |
if table_data:
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
|
|
|
|
|
|
217 |
table.setStyle(TableStyle([
|
218 |
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor("#4a044e")),
|
219 |
-
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
|
220 |
('GRID', (0, 0), (-1, -1), 1, colors.HexColor("#6b21a8")),
|
221 |
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
|
222 |
('TOPPADDING', (0,0), (-1,-1), 6),
|
@@ -224,10 +295,12 @@ def markdown_to_story(markdown_text: str, font_name: str, emoji_font: str, font_
|
|
224 |
]))
|
225 |
story.append(table); story.append(Spacer(1, 0.2 * inch))
|
226 |
table_data = []
|
|
|
|
|
227 |
|
228 |
if not stripped_line: continue
|
229 |
|
230 |
-
content, style,
|
231 |
|
232 |
if stripped_line.startswith("# "):
|
233 |
if not first_heading: story.append(PageBreak())
|
@@ -236,11 +309,21 @@ def markdown_to_story(markdown_text: str, font_name: str, emoji_font: str, font_
|
|
236 |
first_heading = False
|
237 |
elif stripped_line.startswith("## "): content, style = stripped_line.lstrip('## '), style_h2
|
238 |
elif stripped_line.startswith("### "): content, style = stripped_line.lstrip('### '), style_h3
|
239 |
-
elif stripped_line.startswith(("- ", "* ")):
|
|
|
|
|
240 |
|
241 |
-
|
242 |
-
|
243 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
244 |
|
245 |
return story, document_title
|
246 |
|
@@ -267,6 +350,7 @@ def generate_pdfs_api(files, ai_content, layouts, fonts, num_columns, header_tex
|
|
267 |
if not files and not ai_content.strip(): raise gr.Error("Please conjure some content or upload an image before alchemizing!")
|
268 |
if not layouts: raise gr.Error("You must select a scroll (page layout)!")
|
269 |
if not fonts: raise gr.Error("A scribe needs a font! Please choose one.")
|
|
|
270 |
|
271 |
shutil.rmtree(OUTPUT_DIR, ignore_errors=True); shutil.rmtree(PREVIEW_DIR, ignore_errors=True)
|
272 |
OUTPUT_DIR.mkdir(); PREVIEW_DIR.mkdir()
|
@@ -276,6 +360,9 @@ def generate_pdfs_api(files, ai_content, layouts, fonts, num_columns, header_tex
|
|
276 |
|
277 |
log_updates, generated_pdf_paths = "", []
|
278 |
|
|
|
|
|
|
|
279 |
for layout_name in progress.tqdm(layouts, desc=" brewing potions..."):
|
280 |
for font_name in progress.tqdm(fonts, desc=f" enchanting scrolls with {layout_name}..."):
|
281 |
merger = PdfWriter()
|
@@ -424,8 +511,9 @@ with gr.Blocks(theme=theme, title="The PDF Alchemist") as demo:
|
|
424 |
if __name__ == "__main__":
|
425 |
if not (FONT_DIR / "NotoColorEmoji-Regular.ttf").exists():
|
426 |
print("\n" + "="*80)
|
427 |
-
print("WARNING: 'NotoColorEmoji-Regular.ttf' not found.")
|
428 |
print("Please download it from Google Fonts and place it in the script's directory for emojis to render correctly.")
|
|
|
429 |
print("="*80 + "\n")
|
430 |
if not any("MedievalSharp" in s for s in AVAILABLE_FONTS):
|
431 |
print("\n" + "="*80)
|
|
|
11 |
from pypdf import PdfWriter
|
12 |
import random
|
13 |
|
14 |
+
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak, BaseDocTemplate, Frame, PageTemplate, Image as ReportLabImage, Flowable
|
15 |
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
16 |
from reportlab.lib.pagesizes import letter, A4, legal, landscape
|
17 |
from reportlab.lib.units import inch
|
|
|
41 |
|
42 |
# --- Font & Emoji Handling ---
|
43 |
|
44 |
+
# Global cache for rendered emoji images to avoid re-rendering the same emoji
|
45 |
+
EMOJI_IMAGE_CACHE = {}
|
46 |
+
EMOJI_FONT_PATH = None # Will be set in register_local_fonts
|
47 |
+
|
48 |
def register_local_fonts():
|
49 |
"""Finds and registers all .ttf files from the application's base directory."""
|
50 |
+
global EMOJI_FONT_PATH
|
51 |
print("--- Font Registration Process Starting ---")
|
52 |
text_font_names = []
|
53 |
emoji_font_name = None
|
54 |
|
55 |
noto_emoji_path = FONT_DIR / "NotoColorEmoji-Regular.ttf"
|
56 |
if not noto_emoji_path.exists():
|
57 |
+
print(f"CRITICAL: Color Emoji font not found at {noto_emoji_path}.")
|
58 |
+
print("Please download 'NotoColorEmoji-Regular.ttf' and place it in the application directory for color emojis to work.")
|
59 |
+
else:
|
60 |
+
EMOJI_FONT_PATH = str(noto_emoji_path)
|
61 |
+
|
62 |
|
63 |
print(f"Scanning for fonts in: {FONT_DIR.absolute()}")
|
64 |
font_files = list(FONT_DIR.glob("*.ttf"))
|
|
|
89 |
print("--- Font Registration Process Finished ---")
|
90 |
return sorted(text_font_names), emoji_font_name
|
91 |
|
92 |
+
def render_emoji_as_image(emoji_char, size_pt):
|
93 |
+
"""
|
94 |
+
Renders a single emoji character as a transparent PNG image in memory using PyMuPDF.
|
95 |
+
This is the magic to get color emojis into the PDF.
|
96 |
+
"""
|
97 |
+
if not EMOJI_FONT_PATH:
|
98 |
+
return None # Cannot render without the font file
|
99 |
+
|
100 |
+
# Check cache first
|
101 |
+
if (emoji_char, size_pt) in EMOJI_IMAGE_CACHE:
|
102 |
+
return EMOJI_IMAGE_CACHE[(emoji_char, size_pt)]
|
103 |
|
104 |
+
try:
|
105 |
+
# Use fitz to draw the emoji with its color font onto a temporary surface
|
106 |
+
rect = fitz.Rect(0, 0, size_pt * 1.5, size_pt * 1.5)
|
107 |
+
doc = fitz.open()
|
108 |
+
page = doc.new_page(width=rect.width, height=rect.height)
|
109 |
+
|
110 |
+
# Insert the text with the color emoji font
|
111 |
+
page.insert_font(fontname="emoji", fontfile=EMOJI_FONT_PATH)
|
112 |
+
page.insert_text(fitz.Point(0, size_pt * 1.1), emoji_char, fontname="emoji", fontsize=size_pt)
|
113 |
+
|
114 |
+
# Render to a pixmap (PNG)
|
115 |
+
pix = page.get_pixmap(alpha=True, dpi=300) # High DPI for quality
|
116 |
+
doc.close()
|
117 |
+
|
118 |
+
# Convert to a PIL Image and save to a byte buffer
|
119 |
+
img_buffer = io.BytesIO(pix.tobytes("png"))
|
120 |
+
img_buffer.seek(0)
|
121 |
+
|
122 |
+
# Cache the result
|
123 |
+
EMOJI_IMAGE_CACHE[(emoji_char, size_pt)] = img_buffer
|
124 |
+
return img_buffer
|
125 |
+
|
126 |
+
except Exception as e:
|
127 |
+
print(f"Could not render emoji {emoji_char}: {e}")
|
128 |
+
return None
|
129 |
|
130 |
# --- AI Content Generation (Simulation) ---
|
131 |
|
132 |
def generate_ai_content_api(prompt):
|
133 |
"""
|
134 |
Simulates a call to an LLM to generate markdown content.
|
|
|
135 |
"""
|
136 |
if not prompt:
|
137 |
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! ✨"
|
|
|
177 |
canvas.saveState()
|
178 |
page_num = canvas.getPageNumber()
|
179 |
|
|
|
180 |
final_footer_text = footer_text.replace("[Page #]", str(page_num)).replace("[Total Pages]", str(doc.page))
|
181 |
final_header_text = header_text.replace("[Page #]", str(page_num)).replace("[Title]", title)
|
182 |
|
|
|
183 |
if final_header_text:
|
184 |
canvas.setFont('Helvetica', 9)
|
185 |
canvas.setFillColor(colors.grey)
|
186 |
canvas.drawRightString(doc.width + doc.leftMargin, doc.height + doc.topMargin + 0.25*inch, final_header_text)
|
187 |
|
|
|
188 |
if final_footer_text:
|
189 |
canvas.setFont('Helvetica', 9)
|
190 |
canvas.setFillColor(colors.grey)
|
|
|
193 |
canvas.restoreState()
|
194 |
|
195 |
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):
|
196 |
+
"""Converts markdown to a ReportLab story, turning emojis into inline images for full color support."""
|
197 |
styles = getSampleStyleSheet()
|
198 |
|
199 |
leading_body = font_size_body * 1.4
|
|
|
204 |
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)
|
205 |
style_table_header = ParagraphStyle('TableHeader', parent=style_normal, fontName=f"{font_name}-Bold" if font_name != 'Helvetica' else 'Helvetica-Bold')
|
206 |
|
207 |
+
# This regex is the key: it finds and captures all emoji sequences.
|
208 |
+
emoji_pattern = re.compile(f"([{re.escape(''.join(map(chr, range(0x1f600, 0x1f650))))}"
|
209 |
+
f"{re.escape(''.join(map(chr, range(0x1f300, 0x1f5ff))))}"
|
210 |
+
f"{re.escape(''.join(map(chr, range(0x1f900, 0x1f9ff))))}"
|
211 |
+
f"{re.escape(''.join(map(chr, range(0x2600, 0x26ff))))}"
|
212 |
+
f"{re.escape(''.join(map(chr, range(0x2700, 0x27bf))))}]+)")
|
213 |
+
|
214 |
+
def create_flowables_for_line(text, style):
|
215 |
+
"""Splits a line of text into text and emoji images, returning a list of flowables."""
|
216 |
+
parts = emoji_pattern.split(text)
|
217 |
+
flowables = []
|
218 |
+
for part in parts:
|
219 |
+
if not part: continue
|
220 |
+
if emoji_pattern.match(part):
|
221 |
+
# It's an emoji, render it as an image
|
222 |
+
for emoji_char in part: # Render one by one in case they are clumped
|
223 |
+
img_buffer = render_emoji_as_image(emoji_char, style.fontSize)
|
224 |
+
if img_buffer:
|
225 |
+
# Create an image that is sized relative to the font
|
226 |
+
img = ReportLabImage(img_buffer, height=style.fontSize * 1.2, width=style.fontSize * 1.2)
|
227 |
+
flowables.append(img)
|
228 |
+
else:
|
229 |
+
# It's regular text, create a Paragraph
|
230 |
+
# Apply markdown for bold/italic within this text segment
|
231 |
+
formatted_part = re.sub(r'_(.*?)_', r'<i>\1</i>', re.sub(r'\*\*(.*?)\*\*', r'<b>\1</b>', part))
|
232 |
+
flowables.append(Paragraph(formatted_part, style))
|
233 |
+
|
234 |
+
if not flowables:
|
235 |
+
return [Spacer(0, 0)]
|
236 |
+
|
237 |
+
# Use a table to keep all flowables on the same line(s)
|
238 |
+
table = Table([flowables], colWidths=[None]*len(flowables))
|
239 |
+
table.setStyle(TableStyle([('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
|
240 |
+
('LEFTPADDING', (0,0), (-1,-1), 0),
|
241 |
+
('RIGHTPADDING', (0,0), (-1,-1), 0),
|
242 |
+
]))
|
243 |
+
return [table]
|
244 |
+
|
245 |
story = []
|
246 |
lines = markdown_text.split('\n')
|
247 |
|
|
|
273 |
if in_table:
|
274 |
in_table = False
|
275 |
if table_data:
|
276 |
+
# Table processing needs to create flowables for each cell
|
277 |
+
processed_table_data = []
|
278 |
+
for row in table_data:
|
279 |
+
processed_row = [create_flowables_for_line(cell, style_normal)[0] for cell in row]
|
280 |
+
processed_table_data.append(processed_row)
|
281 |
+
|
282 |
+
# Style header separately
|
283 |
+
header_row = [create_flowables_for_line(cell, style_table_header)[0] for cell in table_data[0]]
|
284 |
+
|
285 |
+
final_table_data = [header_row] + processed_table_data[1:]
|
286 |
+
|
287 |
+
table = Table(final_table_data, hAlign='LEFT', repeatRows=1)
|
288 |
table.setStyle(TableStyle([
|
289 |
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor("#4a044e")),
|
290 |
+
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), # Note: this may not affect paragraphs
|
291 |
('GRID', (0, 0), (-1, -1), 1, colors.HexColor("#6b21a8")),
|
292 |
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
|
293 |
('TOPPADDING', (0,0), (-1,-1), 6),
|
|
|
295 |
]))
|
296 |
story.append(table); story.append(Spacer(1, 0.2 * inch))
|
297 |
table_data = []
|
298 |
+
continue
|
299 |
+
|
300 |
|
301 |
if not stripped_line: continue
|
302 |
|
303 |
+
content, style, bullet_text = stripped_line, style_normal, None
|
304 |
|
305 |
if stripped_line.startswith("# "):
|
306 |
if not first_heading: story.append(PageBreak())
|
|
|
309 |
first_heading = False
|
310 |
elif stripped_line.startswith("## "): content, style = stripped_line.lstrip('## '), style_h2
|
311 |
elif stripped_line.startswith("### "): content, style = stripped_line.lstrip('### '), style_h3
|
312 |
+
elif stripped_line.startswith(("- ", "* ")):
|
313 |
+
content = stripped_line[2:]
|
314 |
+
bullet_text = '• ' # Using spaces for indentation
|
315 |
|
316 |
+
# Now, process the content into flowables (text + emoji images)
|
317 |
+
line_flowables = create_flowables_for_line(content, style)
|
318 |
+
|
319 |
+
if bullet_text:
|
320 |
+
# For lists, put the bullet and the content table in another table
|
321 |
+
bullet_p = Paragraph(bullet_text, style)
|
322 |
+
list_item_table = Table([[bullet_p] + line_flowables], colWidths=[style.fontSize*1.5] + [None]*len(line_flowables))
|
323 |
+
list_item_table.setStyle(TableStyle([('VALIGN', (0,0), (-1,-1), 'TOP'), ('LEFTPADDING', (0,0), (-1,-1), 0), ('RIGHTPADDING', (0,0), (-1,-1), 0)]))
|
324 |
+
story.append(list_item_table)
|
325 |
+
else:
|
326 |
+
story.extend(line_flowables)
|
327 |
|
328 |
return story, document_title
|
329 |
|
|
|
350 |
if not files and not ai_content.strip(): raise gr.Error("Please conjure some content or upload an image before alchemizing!")
|
351 |
if not layouts: raise gr.Error("You must select a scroll (page layout)!")
|
352 |
if not fonts: raise gr.Error("A scribe needs a font! Please choose one.")
|
353 |
+
if not EMOJI_FONT_PATH: raise gr.Error("CRITICAL: Cannot generate PDFs. 'NotoColorEmoji-Regular.ttf' not found. Please add it to the app directory.")
|
354 |
|
355 |
shutil.rmtree(OUTPUT_DIR, ignore_errors=True); shutil.rmtree(PREVIEW_DIR, ignore_errors=True)
|
356 |
OUTPUT_DIR.mkdir(); PREVIEW_DIR.mkdir()
|
|
|
360 |
|
361 |
log_updates, generated_pdf_paths = "", []
|
362 |
|
363 |
+
# Clear emoji cache for each run to save memory
|
364 |
+
EMOJI_IMAGE_CACHE.clear()
|
365 |
+
|
366 |
for layout_name in progress.tqdm(layouts, desc=" brewing potions..."):
|
367 |
for font_name in progress.tqdm(fonts, desc=f" enchanting scrolls with {layout_name}..."):
|
368 |
merger = PdfWriter()
|
|
|
511 |
if __name__ == "__main__":
|
512 |
if not (FONT_DIR / "NotoColorEmoji-Regular.ttf").exists():
|
513 |
print("\n" + "="*80)
|
514 |
+
print("CRITICAL WARNING: 'NotoColorEmoji-Regular.ttf' not found.")
|
515 |
print("Please download it from Google Fonts and place it in the script's directory for emojis to render correctly.")
|
516 |
+
print("The application will fail to generate PDFs without it.")
|
517 |
print("="*80 + "\n")
|
518 |
if not any("MedievalSharp" in s for s in AVAILABLE_FONTS):
|
519 |
print("\n" + "="*80)
|