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)
|