awacke1 commited on
Commit
029ee1c
ยท
verified ยท
1 Parent(s): aa6da1a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +134 -167
app.py CHANGED
@@ -11,6 +11,10 @@ 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, Flowable
15
  from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
16
  from reportlab.lib.pagesizes import letter, A4, legal, landscape
@@ -43,6 +47,7 @@ PREVIEW_DIR.mkdir(exist_ok=True)
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():
@@ -92,41 +97,60 @@ def register_local_fonts():
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):
@@ -192,8 +216,8 @@ def _draw_header_footer(canvas, doc, header_text, footer_text, title):
192
 
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,7 +228,6 @@ def markdown_to_story(markdown_text: str, font_name: str, emoji_font: str, font_
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))))}"
@@ -212,34 +235,30 @@ def markdown_to_story(markdown_text: str, font_name: str, emoji_font: str, font_
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 = []
@@ -273,31 +292,21 @@ def markdown_to_story(markdown_text: str, font_name: str, emoji_font: str, font_
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),
294
- ('BOTTOMPADDING', (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
@@ -311,14 +320,13 @@ def markdown_to_story(markdown_text: str, font_name: str, emoji_font: str, font_
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)
@@ -346,7 +354,7 @@ def create_pdf_preview(pdf_path: Path):
346
  except: return None
347
 
348
  # --- Main API Function ---
349
- 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, progress=gr.Progress(track_tqdm=True)):
350
  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!")
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.")
@@ -355,24 +363,18 @@ def generate_pdfs_api(files, ai_content, layouts, fonts, num_columns, header_tex
355
  shutil.rmtree(OUTPUT_DIR, ignore_errors=True); shutil.rmtree(PREVIEW_DIR, ignore_errors=True)
356
  OUTPUT_DIR.mkdir(); PREVIEW_DIR.mkdir()
357
 
358
- # Separate uploaded files by type
359
  image_files, pdf_files, txt_files = [], [], []
360
  if files:
361
  for f in files:
362
  file_path = Path(f.name)
363
  ext = file_path.suffix.lower()
364
- if ext in ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff']:
365
- image_files.append(file_path)
366
- elif ext == '.pdf':
367
- pdf_files.append(file_path)
368
- elif ext == '.txt':
369
- txt_files.append(file_path)
370
 
371
  log_updates = ""
372
- # Consolidate all text content
373
  all_text_content = []
374
- if ai_content and "Golem awaits" not in ai_content:
375
- all_text_content.append(ai_content)
376
 
377
  for txt_path in txt_files:
378
  try:
@@ -384,22 +386,18 @@ def generate_pdfs_api(files, ai_content, layouts, fonts, num_columns, header_tex
384
 
385
  generated_pdf_paths = []
386
 
387
- # Clear emoji cache for each run to save memory
388
- EMOJI_IMAGE_CACHE.clear()
389
 
390
  for layout_name in progress.tqdm(layouts, desc=" brewing potions..."):
391
  for font_name in progress.tqdm(fonts, desc=f" enchanting scrolls with {layout_name}..."):
392
  merger = PdfWriter()
393
 
394
- # 1. Process main text content (AI + TXT)
395
  if md_content:
396
  md_buffer = io.BytesIO()
397
- story, title = markdown_to_story(md_content, font_name, EMOJI_FONT_NAME, font_size_body, font_size_h1, font_size_h2, font_size_h3)
398
  pagesize = LAYOUTS[layout_name]["size"]
399
 
400
- doc = BaseDocTemplate(md_buffer, pagesize=pagesize,
401
- leftMargin=margin_left*inch, rightMargin=margin_right*inch,
402
- topMargin=margin_top*inch, bottomMargin=margin_bottom*inch)
403
 
404
  frame_width = (doc.width / num_columns) - (num_columns - 1) * 0.1*inch
405
  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)]
@@ -412,7 +410,6 @@ def generate_pdfs_api(files, ai_content, layouts, fonts, num_columns, header_tex
412
  md_buffer.seek(0)
413
  merger.append(fileobj=md_buffer)
414
 
415
- # 2. Append image files
416
  for img_path in image_files:
417
  try:
418
  with Image.open(img_path) as img: img_width_px, img_height_px = img.size
@@ -424,14 +421,11 @@ def generate_pdfs_api(files, ai_content, layouts, fonts, num_columns, header_tex
424
  merger.append(fileobj=img_buffer)
425
  except Exception as e: log_updates += f"โš ๏ธ Failed to process image {img_path.name}: {e}\n"
426
 
427
- # 3. Append PDF files
428
  for pdf_path in pdf_files:
429
  try:
430
  merger.append(str(pdf_path))
431
- except Exception as e:
432
- log_updates += f"โš ๏ธ Failed to merge PDF {pdf_path.name}: {e}\n"
433
 
434
- # 4. Write the final merged PDF
435
  if len(merger.pages) > 0:
436
  time_str = datetime.datetime.now().strftime('%H%M%S')
437
  clean_layout = layout_name.replace(' ', '')
@@ -449,92 +443,75 @@ def generate_pdfs_api(files, ai_content, layouts, fonts, num_columns, header_tex
449
  # --- Gradio UI Definition ---
450
  AVAILABLE_FONTS, EMOJI_FONT_NAME = register_local_fonts()
451
 
452
- # A custom, magical theme
453
- theme = gr.themes.Base(
454
- primary_hue=gr.themes.colors.purple,
455
- secondary_hue=gr.themes.colors.indigo,
456
- neutral_hue=gr.themes.colors.gray,
457
- font=(gr.themes.GoogleFont("MedievalSharp"), "ui-sans-serif", "system-ui", "sans-serif"),
458
- ).set(
459
- body_background_fill="#111827",
460
- body_background_fill_dark="#111827",
461
- body_text_color="#d1d5db",
462
- body_text_color_dark="#d1d5db",
463
- button_primary_background_fill="#a855f7",
464
- button_primary_background_fill_dark="#a855f7",
465
- button_primary_text_color="#ffffff",
466
- button_primary_text_color_dark="#ffffff",
467
- button_secondary_background_fill="#6366f1",
468
- button_secondary_background_fill_dark="#6366f1",
469
- button_secondary_text_color="#ffffff",
470
- button_secondary_text_color_dark="#ffffff",
471
- block_background_fill="#1f2937",
472
- block_background_fill_dark="#1f2937",
473
- block_label_background_fill="#1f2937",
474
- block_label_background_fill_dark="#1f2937",
475
- block_title_text_color="#a855f7",
476
- block_title_text_color_dark="#a855f7",
477
- input_background_fill="#374151",
478
- input_background_fill_dark="#374151",
479
- )
480
-
481
- with gr.Blocks(theme=theme, title="The PDF Alchemist") as demo:
482
  gr.Markdown("# โœจ The PDF Alchemist โœจ")
483
  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.")
484
 
485
  with gr.Row(equal_height=False):
486
- # --- LEFT COLUMN: INPUTS & CONTROLS ---
487
  with gr.Column(scale=2):
488
- with gr.Group():
489
- with gr.Accordion("๐Ÿ“œ Content Crucible (Your Ingredients)", open=True):
490
- gr.Markdown("### ๐Ÿค– Command Your Idea Golem")
491
- ai_prompt = gr.Textbox(label="Incantation (Prompt)", placeholder="e.g., 'A recipe for a dragon's breath chili...'")
492
- generate_ai_btn = gr.Button("๐Ÿง  Animate Golem!")
493
- ai_content_output = gr.Textbox(label="Golem's Manuscript (Editable)", lines=10, interactive=True, value="# The Golem awaits your command!\n\n")
494
-
495
- gr.Markdown("<hr style='border-color: #374151; margin-top: 20px; margin-bottom: 20px;'>")
496
-
497
- gr.Markdown("### ๐Ÿ“ค Add Your Physical Treasures")
498
- uploaded_files = gr.File(label="Upload Files (Images, PDFs, TXT)", file_count="multiple", file_types=['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff', '.pdf', '.txt'])
499
- gr.Markdown("*<p style='font-size:0.8rem; color: #9ca3af;'>The Golem's manuscript and .txt files are combined. Images and PDF pages are appended after the text.</p>*")
500
-
501
- with gr.Group():
502
- with gr.Accordion("๐Ÿ“ Arcane Blueprints (Layout & Structure)", open=True):
503
- gr.Markdown("### Page & Column Spells")
504
- selected_layouts = gr.CheckboxGroup(choices=list(LAYOUTS.keys()), label="Page Layouts", value=["A4 Portrait"])
505
- num_columns_slider = gr.Slider(label="Number of Text Columns", minimum=1, maximum=4, step=1, value=1)
506
-
507
- gr.Markdown("<hr style='border-color: #374151; margin-top: 20px; margin-bottom: 20px;'>")
508
-
509
- gr.Markdown("### Header & Footer Runes")
510
- gr.Markdown("<p style='font-size:0.8rem; color: #9ca3af;'>Use `[Page #]` and `[Title]`. `[Total Pages]` works best in the footer.</p>")
511
- header_input = gr.Textbox(label="Header Inscription", value="[Title]", placeholder="e.g., Arcane Folio - [Page #]")
512
- footer_input = gr.Textbox(label="Footer Inscription", value="Page [Page #] of [Total Pages]", placeholder="e.g., Top Secret - Page [Page #]")
513
-
514
- with gr.Group():
515
- with gr.Accordion("๐Ÿ’… Stylist's Sanctum (Fonts & Margins)", open=True):
516
- gr.Markdown("### ๐Ÿ”ค Master the Glyphs")
517
- selected_fonts = gr.CheckboxGroup(choices=AVAILABLE_FONTS, label="Fonts", value=[AVAILABLE_FONTS[0]] if AVAILABLE_FONTS else [])
518
- font_size_body_slider = gr.Slider(label="Body Text Size (pt)", minimum=8, maximum=16, step=1, value=10)
519
- with gr.Row():
520
- font_size_h1_slider = gr.Slider(label="H1 Size", minimum=16, maximum=32, step=1, value=24)
521
- font_size_h2_slider = gr.Slider(label="H2 Size", minimum=14, maximum=28, step=1, value=18)
522
- font_size_h3_slider = gr.Slider(label="H3 Size", minimum=12, maximum=24, step=1, value=14)
523
-
524
- gr.Markdown("<hr style='border-color: #374151; margin-top: 20px; margin-bottom: 20px;'>")
525
-
526
- gr.Markdown("### ๐Ÿ“ Set Your Boundaries (inches)")
527
- with gr.Row():
528
- margin_top_slider = gr.Slider(label="Top", minimum=0.25, maximum=1.5, step=0.05, value=0.75)
529
- margin_bottom_slider = gr.Slider(label="Bottom", minimum=0.25, maximum=1.5, step=0.05, value=0.75)
530
- with gr.Row():
531
- margin_left_slider = gr.Slider(label="Left", minimum=0.25, maximum=1.5, step=0.05, value=0.75)
532
- margin_right_slider = gr.Slider(label="Right", minimum=0.25, maximum=1.5, step=0.05, value=0.75)
533
 
534
- # --- Main Generate Button ---
535
  generate_pdfs_btn = gr.Button("๐Ÿ”ฎ Alchemize PDF!", variant="primary", size="lg")
536
 
537
- # --- RIGHT COLUMN: OUTPUTS ---
538
  with gr.Column(scale=3):
539
  gr.Markdown("### ๐Ÿ–ผ๏ธ The Scrying Pool (Previews)")
540
  gallery_output = gr.Gallery(label="Generated PDF Previews", show_label=False, elem_id="gallery", columns=2, height=700, object_fit="contain")
@@ -544,27 +521,17 @@ with gr.Blocks(theme=theme, title="The PDF Alchemist") as demo:
544
  # --- API Calls & Examples ---
545
  generate_ai_btn.click(fn=generate_ai_content_api, inputs=[ai_prompt], outputs=[ai_content_output])
546
 
547
- 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]
548
  outputs_list = [gallery_output, log_output, downloadable_files_output]
549
  generate_pdfs_btn.click(fn=generate_pdfs_api, inputs=inputs_list, outputs=outputs_list)
550
 
551
- gr.Examples(
552
- examples=[["A technical summary of how alchemy works"], ["A short poem about a grumpy gnome"], ["A sample agenda for a wizard's council meeting"]],
553
- inputs=[ai_prompt], outputs=[ai_content_output], fn=generate_ai_content_api, cache_examples=False
554
- )
555
 
556
  if __name__ == "__main__":
557
  if not (FONT_DIR / "NotoColorEmoji-Regular.ttf").exists():
558
  print("\n" + "="*80)
559
  print("CRITICAL WARNING: 'NotoColorEmoji-Regular.ttf' not found.")
560
- print("Please download it from Google Fonts and place it in the script's directory for emojis to render correctly.")
561
  print("The application will fail to generate PDFs without it.")
562
  print("="*80 + "\n")
563
- if not any("MedievalSharp" in s for s in AVAILABLE_FONTS):
564
- print("\n" + "="*80)
565
- print("WARNING: 'MedievalSharp' font not found for the default theme.")
566
- print("Please download it from Google Fonts to get the intended 'alchemist' look.")
567
- print("The app will fall back to another font.")
568
- print("="*80 + "\n")
569
 
570
  demo.launch(debug=True)
 
11
  from pypdf import PdfWriter
12
  import random
13
 
14
+ # New imports for the SVG Emoji Engine
15
+ from lxml import etree
16
+ from svglib.svglib import svg2rlg
17
+
18
  from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak, BaseDocTemplate, Frame, PageTemplate, Image as ReportLabImage, Flowable
19
  from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
20
  from reportlab.lib.pagesizes import letter, A4, legal, landscape
 
47
 
48
  # Global cache for rendered emoji images to avoid re-rendering the same emoji
49
  EMOJI_IMAGE_CACHE = {}
50
+ EMOJI_SVG_CACHE = {} # New cache for the SVG engine
51
  EMOJI_FONT_PATH = None # Will be set in register_local_fonts
52
 
53
  def register_local_fonts():
 
97
  def render_emoji_as_image(emoji_char, size_pt):
98
  """
99
  Renders a single emoji character as a transparent PNG image in memory using PyMuPDF.
100
+ This is the original raster image engine.
101
  """
102
+ if not EMOJI_FONT_PATH: return None
103
+ if (emoji_char, size_pt) in EMOJI_IMAGE_CACHE: return EMOJI_IMAGE_CACHE[(emoji_char, size_pt)]
 
 
 
 
104
 
105
  try:
 
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
  page.insert_font(fontname="emoji", fontfile=EMOJI_FONT_PATH)
110
  page.insert_text(fitz.Point(0, size_pt * 1.1), emoji_char, fontname="emoji", fontsize=size_pt)
111
+ pix = page.get_pixmap(alpha=True, dpi=300)
 
 
112
  doc.close()
 
 
113
  img_buffer = io.BytesIO(pix.tobytes("png"))
114
  img_buffer.seek(0)
 
 
115
  EMOJI_IMAGE_CACHE[(emoji_char, size_pt)] = img_buffer
116
  return img_buffer
117
+ except Exception as e:
118
+ print(f"Could not render emoji {emoji_char} as image: {e}")
119
+ return None
120
 
121
+ def render_emoji_as_svg(emoji_char, size_pt):
122
+ """
123
+ Renders a single emoji character as an SVG drawing object.
124
+ This is the new vector graphics engine.
125
+ """
126
+ if not EMOJI_FONT_PATH: return None
127
+ if (emoji_char, size_pt) in EMOJI_SVG_CACHE: return EMOJI_SVG_CACHE[(emoji_char, size_pt)]
128
+
129
+ try:
130
+ doc = fitz.open()
131
+ page = doc.new_page(width=1000, height=1000) # Large canvas
132
+ page.insert_font(fontname="emoji", fontfile=EMOJI_FONT_PATH)
133
+
134
+ # Get the SVG image for the character
135
+ svg_image = page.get_svg_image(page.rect, text=emoji_char, fontname="emoji")
136
+ doc.close()
137
+
138
+ # Convert the SVG XML string to a ReportLab Graphic
139
+ drawing = svg2rlg(io.BytesIO(svg_image.encode('utf-8')))
140
+
141
+ # Scale the drawing to match the font size
142
+ scale_factor = (size_pt * 1.2) / drawing.height
143
+ drawing.width *= scale_factor
144
+ drawing.height *= scale_factor
145
+ drawing.scale(scale_factor, scale_factor)
146
+
147
+ EMOJI_SVG_CACHE[(emoji_char, size_pt)] = drawing
148
+ return drawing
149
  except Exception as e:
150
+ print(f"Could not render emoji {emoji_char} as SVG: {e}")
151
  return None
152
 
153
+
154
  # --- AI Content Generation (Simulation) ---
155
 
156
  def generate_ai_content_api(prompt):
 
216
 
217
  canvas.restoreState()
218
 
219
+ 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):
220
+ """Converts markdown to a ReportLab story, with a switch for emoji rendering engines."""
221
  styles = getSampleStyleSheet()
222
 
223
  leading_body = font_size_body * 1.4
 
228
  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)
229
  style_table_header = ParagraphStyle('TableHeader', parent=style_normal, fontName=f"{font_name}-Bold" if font_name != 'Helvetica' else 'Helvetica-Bold')
230
 
 
231
  emoji_pattern = re.compile(f"([{re.escape(''.join(map(chr, range(0x1f600, 0x1f650))))}"
232
  f"{re.escape(''.join(map(chr, range(0x1f300, 0x1f5ff))))}"
233
  f"{re.escape(''.join(map(chr, range(0x1f900, 0x1f9ff))))}"
 
235
  f"{re.escape(''.join(map(chr, range(0x2700, 0x27bf))))}]+)")
236
 
237
  def create_flowables_for_line(text, style):
238
+ """Splits a line of text into text and emoji flowables, returning a list."""
239
  parts = emoji_pattern.split(text)
240
  flowables = []
241
  for part in parts:
242
  if not part: continue
243
  if emoji_pattern.match(part):
244
+ for emoji_char in part:
245
+ emoji_flowable = None
246
+ if use_svg_engine:
247
+ emoji_flowable = render_emoji_as_svg(emoji_char, style.fontSize)
248
+ else:
249
+ img_buffer = render_emoji_as_image(emoji_char, style.fontSize)
250
+ if img_buffer:
251
+ emoji_flowable = ReportLabImage(img_buffer, height=style.fontSize * 1.2, width=style.fontSize * 1.2)
252
+
253
+ if emoji_flowable:
254
+ flowables.append(emoji_flowable)
255
  else:
 
 
256
  formatted_part = re.sub(r'_(.*?)_', r'<i>\1</i>', re.sub(r'\*\*(.*?)\*\*', r'<b>\1</b>', part))
257
  flowables.append(Paragraph(formatted_part, style))
258
 
259
+ if not flowables: return [Spacer(0, 0)]
 
 
 
260
  table = Table([flowables], colWidths=[None]*len(flowables))
261
+ table.setStyle(TableStyle([('VALIGN', (0,0), (-1,-1), 'MIDDLE'), ('LEFTPADDING', (0,0), (-1,-1), 0), ('RIGHTPADDING', (0,0), (-1,-1), 0)]))
 
 
 
262
  return [table]
263
 
264
  story = []
 
292
  if in_table:
293
  in_table = False
294
  if table_data:
295
+ processed_table_data = [[create_flowables_for_line(cell, style_normal)[0] for cell in row] for row in table_data]
 
 
 
 
 
 
296
  header_row = [create_flowables_for_line(cell, style_table_header)[0] for cell in table_data[0]]
 
297
  final_table_data = [header_row] + processed_table_data[1:]
298
 
299
  table = Table(final_table_data, hAlign='LEFT', repeatRows=1)
300
  table.setStyle(TableStyle([
301
  ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor("#4a044e")),
 
302
  ('GRID', (0, 0), (-1, -1), 1, colors.HexColor("#6b21a8")),
303
  ('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
304
+ ('TOPPADDING', (0,0), (-1,-1), 6), ('BOTTOMPADDING', (0,0), (-1,-1), 6),
 
305
  ]))
306
  story.append(table); story.append(Spacer(1, 0.2 * inch))
307
  table_data = []
308
  continue
309
 
 
310
  if not stripped_line: continue
311
 
312
  content, style, bullet_text = stripped_line, style_normal, None
 
320
  elif stripped_line.startswith("### "): content, style = stripped_line.lstrip('### '), style_h3
321
  elif stripped_line.startswith(("- ", "* ")):
322
  content = stripped_line[2:]
323
+ bullet_text = 'โ€ข '
324
 
 
325
  line_flowables = create_flowables_for_line(content, style)
326
 
327
  if bullet_text:
 
328
  bullet_p = Paragraph(bullet_text, style)
329
+ # Adjust colWidths to give bullet a fixed space
330
  list_item_table = Table([[bullet_p] + line_flowables], colWidths=[style.fontSize*1.5] + [None]*len(line_flowables))
331
  list_item_table.setStyle(TableStyle([('VALIGN', (0,0), (-1,-1), 'TOP'), ('LEFTPADDING', (0,0), (-1,-1), 0), ('RIGHTPADDING', (0,0), (-1,-1), 0)]))
332
  story.append(list_item_table)
 
354
  except: return None
355
 
356
  # --- Main API Function ---
357
+ 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)):
358
  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!")
359
  if not layouts: raise gr.Error("You must select a scroll (page layout)!")
360
  if not fonts: raise gr.Error("A scribe needs a font! Please choose one.")
 
363
  shutil.rmtree(OUTPUT_DIR, ignore_errors=True); shutil.rmtree(PREVIEW_DIR, ignore_errors=True)
364
  OUTPUT_DIR.mkdir(); PREVIEW_DIR.mkdir()
365
 
 
366
  image_files, pdf_files, txt_files = [], [], []
367
  if files:
368
  for f in files:
369
  file_path = Path(f.name)
370
  ext = file_path.suffix.lower()
371
+ if ext in ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff']: image_files.append(file_path)
372
+ elif ext == '.pdf': pdf_files.append(file_path)
373
+ elif ext == '.txt': txt_files.append(file_path)
 
 
 
374
 
375
  log_updates = ""
 
376
  all_text_content = []
377
+ if ai_content and "Golem awaits" not in ai_content: all_text_content.append(ai_content)
 
378
 
379
  for txt_path in txt_files:
380
  try:
 
386
 
387
  generated_pdf_paths = []
388
 
389
+ EMOJI_IMAGE_CACHE.clear(); EMOJI_SVG_CACHE.clear()
 
390
 
391
  for layout_name in progress.tqdm(layouts, desc=" brewing potions..."):
392
  for font_name in progress.tqdm(fonts, desc=f" enchanting scrolls with {layout_name}..."):
393
  merger = PdfWriter()
394
 
 
395
  if md_content:
396
  md_buffer = io.BytesIO()
397
+ 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)
398
  pagesize = LAYOUTS[layout_name]["size"]
399
 
400
+ doc = BaseDocTemplate(md_buffer, pagesize=pagesize, leftMargin=margin_left*inch, rightMargin=margin_right*inch, topMargin=margin_top*inch, bottomMargin=margin_bottom*inch)
 
 
401
 
402
  frame_width = (doc.width / num_columns) - (num_columns - 1) * 0.1*inch
403
  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)]
 
410
  md_buffer.seek(0)
411
  merger.append(fileobj=md_buffer)
412
 
 
413
  for img_path in image_files:
414
  try:
415
  with Image.open(img_path) as img: img_width_px, img_height_px = img.size
 
421
  merger.append(fileobj=img_buffer)
422
  except Exception as e: log_updates += f"โš ๏ธ Failed to process image {img_path.name}: {e}\n"
423
 
 
424
  for pdf_path in pdf_files:
425
  try:
426
  merger.append(str(pdf_path))
427
+ except Exception as e: log_updates += f"โš ๏ธ Failed to merge PDF {pdf_path.name}: {e}\n"
 
428
 
 
429
  if len(merger.pages) > 0:
430
  time_str = datetime.datetime.now().strftime('%H%M%S')
431
  clean_layout = layout_name.replace(' ', '')
 
443
  # --- Gradio UI Definition ---
444
  AVAILABLE_FONTS, EMOJI_FONT_NAME = register_local_fonts()
445
 
446
+ def get_theme():
447
+ """Dynamically selects a font for the theme to avoid warnings for missing fonts."""
448
+ desired_font = "MedievalSharp"
449
+ font_family = None
450
+ if any(desired_font in s for s in AVAILABLE_FONTS):
451
+ font_family = (gr.themes.GoogleFont(desired_font), "ui-sans-serif", "system-ui", "sans-serif")
452
+ elif AVAILABLE_FONTS:
453
+ first_font = AVAILABLE_FONTS[0]
454
+ print(f"WARNING: '{desired_font}' font not found. Using '{first_font}' for UI theme instead.")
455
+ font_family = (gr.themes.GoogleFont(first_font), "ui-sans-serif", "system-ui", "sans-serif")
456
+ else:
457
+ print(f"WARNING: '{desired_font}' font not found and no other fonts available. Using system default.")
458
+ font_family = ("ui-sans-serif", "system-ui", "sans-serif")
459
+
460
+ 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(
461
+ body_background_fill="#111827", body_text_color="#d1d5db", button_primary_background_fill="#a855f7", button_primary_text_color="#ffffff",
462
+ button_secondary_background_fill="#6366f1", button_secondary_text_color="#ffffff", block_background_fill="#1f2937",
463
+ block_label_background_fill="#1f2937", block_title_text_color="#a855f7", input_background_fill="#374151"
464
+ )
465
+
466
+ with gr.Blocks(theme=get_theme(), title="The PDF Alchemist") as demo:
 
 
 
 
 
 
 
 
 
467
  gr.Markdown("# โœจ The PDF Alchemist โœจ")
468
  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.")
469
 
470
  with gr.Row(equal_height=False):
 
471
  with gr.Column(scale=2):
472
+ # --- Accordion Groups for Inputs ---
473
+ with gr.Accordion("๐Ÿ“œ Content Crucible (Your Ingredients)", open=True):
474
+ gr.Markdown("### ๐Ÿค– Command Your Idea Golem")
475
+ ai_prompt = gr.Textbox(label="Incantation (Prompt)", placeholder="e.g., 'A recipe for a dragon's breath chili...'")
476
+ generate_ai_btn = gr.Button("๐Ÿง  Animate Golem!")
477
+ ai_content_output = gr.Textbox(label="Golem's Manuscript (Editable)", lines=10, interactive=True, value="# The Golem awaits your command!\n\n")
478
+ gr.Markdown("<hr style='border-color: #374151; margin-top: 20px; margin-bottom: 20px;'>")
479
+ gr.Markdown("### ๐Ÿ“ค Add Your Physical Treasures")
480
+ uploaded_files = gr.File(label="Upload Files (Images, PDFs, TXT)", file_count="multiple", file_types=['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff', '.pdf', '.txt'])
481
+
482
+ with gr.Accordion("๐Ÿ“ Arcane Blueprints (Layout & Structure)", open=True):
483
+ gr.Markdown("### Page & Column Spells")
484
+ selected_layouts = gr.CheckboxGroup(choices=list(LAYOUTS.keys()), label="Page Layouts", value=["A4 Portrait"])
485
+ num_columns_slider = gr.Slider(label="Number of Text Columns", minimum=1, maximum=4, step=1, value=1)
486
+ gr.Markdown("<hr style='border-color: #374151; margin-top: 20px; margin-bottom: 20px;'>")
487
+ gr.Markdown("### Header & Footer Runes")
488
+ header_input = gr.Textbox(label="Header Inscription", value="[Title]", placeholder="e.g., Arcane Folio - [Page #]")
489
+ footer_input = gr.Textbox(label="Footer Inscription", value="Page [Page #] of [Total Pages]", placeholder="e.g., Top Secret - Page [Page #]")
490
+
491
+ with gr.Accordion("๐Ÿ’… Stylist's Sanctum (Fonts & Margins)", open=True):
492
+ gr.Markdown("### โš™๏ธ Alternative Emoji Engine")
493
+ use_svg_engine_toggle = gr.Checkbox(label="Use SVG Emoji Engine (Vector Quality)", value=True)
494
+ gr.Markdown("*<p style='font-size:0.8rem; color: #9ca3af;'>Toggle for higher quality, resolution-independent emojis. May be slower.</p>*")
495
+ gr.Markdown("<hr style='border-color: #374151; margin-top: 20px; margin-bottom: 20px;'>")
496
+ gr.Markdown("### ๐Ÿ”ค Master the Glyphs")
497
+ selected_fonts = gr.CheckboxGroup(choices=AVAILABLE_FONTS, label="Fonts", value=[AVAILABLE_FONTS[0]] if AVAILABLE_FONTS else [])
498
+ with gr.Row():
499
+ font_size_body_slider = gr.Slider(label="Body (pt)", minimum=8, maximum=16, step=1, value=10)
500
+ font_size_h1_slider = gr.Slider(label="H1 (pt)", minimum=16, maximum=32, step=1, value=24)
501
+ with gr.Row():
502
+ font_size_h2_slider = gr.Slider(label="H2 (pt)", minimum=14, maximum=28, step=1, value=18)
503
+ font_size_h3_slider = gr.Slider(label="H3 (pt)", minimum=12, maximum=24, step=1, value=14)
504
+ gr.Markdown("<hr style='border-color: #374151; margin-top: 20px; margin-bottom: 20px;'>")
505
+ gr.Markdown("### ๐Ÿ“ Set Your Boundaries (inches)")
506
+ with gr.Row():
507
+ margin_top_slider = gr.Slider(label="Top", minimum=0.25, maximum=1.5, step=0.05, value=0.75)
508
+ margin_bottom_slider = gr.Slider(label="Bottom", minimum=0.25, maximum=1.5, step=0.05, value=0.75)
509
+ with gr.Row():
510
+ margin_left_slider = gr.Slider(label="Left", minimum=0.25, maximum=1.5, step=0.05, value=0.75)
511
+ margin_right_slider = gr.Slider(label="Right", minimum=0.25, maximum=1.5, step=0.05, value=0.75)
 
 
 
 
 
512
 
 
513
  generate_pdfs_btn = gr.Button("๐Ÿ”ฎ Alchemize PDF!", variant="primary", size="lg")
514
 
 
515
  with gr.Column(scale=3):
516
  gr.Markdown("### ๐Ÿ–ผ๏ธ The Scrying Pool (Previews)")
517
  gallery_output = gr.Gallery(label="Generated PDF Previews", show_label=False, elem_id="gallery", columns=2, height=700, object_fit="contain")
 
521
  # --- API Calls & Examples ---
522
  generate_ai_btn.click(fn=generate_ai_content_api, inputs=[ai_prompt], outputs=[ai_content_output])
523
 
524
+ 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]
525
  outputs_list = [gallery_output, log_output, downloadable_files_output]
526
  generate_pdfs_btn.click(fn=generate_pdfs_api, inputs=inputs_list, outputs=outputs_list)
527
 
528
+ 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)
 
 
 
529
 
530
  if __name__ == "__main__":
531
  if not (FONT_DIR / "NotoColorEmoji-Regular.ttf").exists():
532
  print("\n" + "="*80)
533
  print("CRITICAL WARNING: 'NotoColorEmoji-Regular.ttf' not found.")
 
534
  print("The application will fail to generate PDFs without it.")
535
  print("="*80 + "\n")
 
 
 
 
 
 
536
 
537
  demo.launch(debug=True)