awacke1 commited on
Commit
1de8c8e
·
verified ·
1 Parent(s): 6532be7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +211 -382
app.py CHANGED
@@ -1,401 +1,230 @@
1
  import io
2
  import re
3
- import streamlit as st
4
- import glob
5
  import os
 
 
 
 
 
6
  from PIL import Image
7
  import fitz
 
8
  from reportlab.lib.pagesizes import A4
9
  from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
10
  from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
11
  from reportlab.lib import colors
12
  from reportlab.pdfbase import pdfmetrics
13
  from reportlab.pdfbase.ttfonts import TTFont
14
- import unicodedata
15
 
16
  st.set_page_config(layout="wide", initial_sidebar_state="collapsed")
17
 
18
- def create_pdf_tab(default_markdown):
19
- font_files = glob.glob("*.ttf")
20
- if not font_files:
21
- st.error("No .ttf font files found in the current directory. Please add some, e.g., NotoColorEmoji-Regular.ttf and DejaVuSans.ttf.")
22
- return
23
- available_fonts = {os.path.splitext(os.path.basename(f))[0]: f for f in font_files}
24
-
25
- md_files = [f for f in glob.glob("*.md") if os.path.basename(f) != "README.md"]
26
- md_options = [os.path.splitext(os.path.basename(f))[0] for f in md_files]
27
-
28
- with st.sidebar:
29
- selected_md = st.selectbox("Select Markdown File", options=md_options, index=0 if md_options else -1)
30
- selected_font_name = st.selectbox("Select Emoji Font", options=list(available_fonts.keys()), index=0 if "NotoColorEmoji-Regular" in available_fonts else 0)
31
- selected_font_path = available_fonts[selected_font_name]
32
- base_font_size = st.slider("Font Size (points)", min_value=6, max_value=16, value=9, step=1)
33
- plain_text_mode = st.checkbox("Render as Plain Text (Preserve Bold Only)", value=False)
34
- auto_bold_numbers = st.checkbox("Auto-Bold Numbered Lines", value=False)
35
- num_columns = st.selectbox("Number of Columns", options=[1, 2, 3, 4, 5, 6], index=3)
36
-
37
- if 'markdown_content' not in st.session_state or not md_options:
38
- st.session_state.markdown_content = default_markdown
39
-
40
- if md_options and selected_md:
41
- with open(f"{selected_md}.md", "r", encoding="utf-8") as f:
42
- st.session_state.markdown_content = f.read()
43
-
44
- edited_markdown = st.text_area("Modify the markdown content below:", value=st.session_state.markdown_content, height=300, key=f"markdown_{selected_md}_{selected_font_name}_{num_columns}")
45
- if st.button("Update PDF"):
46
- st.session_state.markdown_content = edited_markdown
47
- with open(f"{selected_md}.md", "w", encoding="utf-8") as f:
48
- f.write(edited_markdown)
49
- st.rerun()
50
-
51
- st.download_button(label="Save Markdown", data=st.session_state.markdown_content, file_name=f"{selected_md}.md", mime="text/markdown")
52
-
53
- if not md_options:
54
- st.warning("No .md files found in the directory (excluding README.md). Using default content.")
55
- return
56
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  try:
58
- pdfmetrics.registerFont(TTFont(selected_font_name, selected_font_path))
 
 
 
 
 
 
 
 
 
 
59
  pdfmetrics.registerFont(TTFont("DejaVuSans", "DejaVuSans.ttf"))
60
  except Exception as e:
61
- st.error(f"Failed to register fonts: {e}. Ensure both {selected_font_name}.ttf and DejaVuSans.ttf are in the directory.")
62
  return
63
-
64
- def apply_emoji_font(text, emoji_font):
65
- emoji_pattern = re.compile(
66
- r"([\U0001F300-\U0001F5FF"
67
- r"\U0001F600-\U0001F64F"
68
- r"\U0001F680-\U0001F6FF"
69
- r"\U0001F700-\U0001F77F"
70
- r"\U0001F780-\U0001F7FF"
71
- r"\U0001F800-\U0001F8FF"
72
- r"\U0001F900-\U0001F9FF"
73
- r"\U0001FA00-\U0001FA6F"
74
- r"\U0001FA70-\U0001FAFF"
75
- r"\u2600-\u26FF"
76
- r"\u2700-\u27BF]+)"
77
- )
78
-
79
- def replace_emoji(match):
80
- emoji = match.group(1)
81
- emoji = unicodedata.normalize('NFC', emoji)
82
- return f'<font face="{emoji_font}">{emoji}</font>'
83
-
84
- segments = []
85
- last_pos = 0
86
- for match in emoji_pattern.finditer(text):
87
- start, end = match.span()
88
- if last_pos < start:
89
- segments.append(f'<font face="DejaVuSans">{text[last_pos:start]}</font>')
90
- segments.append(replace_emoji(match))
91
- last_pos = end
92
- if last_pos < len(text):
93
- segments.append(f'<font face="DejaVuSans">{text[last_pos:]}</font>')
94
- return ''.join(segments)
95
-
96
- def markdown_to_pdf_content(markdown_text, plain_text_mode, auto_bold_numbers):
97
- lines = markdown_text.strip().split('\n')
98
- pdf_content = []
99
- number_pattern = re.compile(r'^\d+\.\s')
100
-
101
- if plain_text_mode:
102
- for line in lines:
103
- line = line.strip()
104
- if not line or line.startswith('# '):
105
- continue
106
- bold_pattern = re.compile(r'\*\*(.*?)\*\*')
107
- line = bold_pattern.sub(r'<b>\1</b>', line)
108
- pdf_content.append(line)
109
- else:
110
- for line in lines:
111
- line = line.strip()
112
- if not line or line.startswith('# '):
113
- continue
114
- if line.startswith('## ') or line.startswith('### '):
115
- text = line.replace('## ', '').replace('### ', '').strip()
116
- pdf_content.append(f"<b>{text}</b>")
117
- elif auto_bold_numbers and number_pattern.match(line):
118
- pdf_content.append(f"<b>{line}</b>")
119
- else:
120
- pdf_content.append(line.strip())
121
-
122
- total_lines = len(pdf_content)
123
- return pdf_content, total_lines
124
-
125
- def create_pdf(markdown_text, base_font_size, plain_text_mode, num_columns, auto_bold_numbers):
126
- buffer = io.BytesIO()
127
- page_width = A4[0] * 2
128
- page_height = A4[1]
129
- doc = SimpleDocTemplate(buffer, pagesize=(page_width, page_height), leftMargin=36, rightMargin=36, topMargin=36, bottomMargin=36)
130
- styles = getSampleStyleSheet()
131
- story = []
132
- spacer_height = 10
133
- section_spacer_height = 15
134
- pdf_content, total_lines = markdown_to_pdf_content(markdown_text, plain_text_mode, auto_bold_numbers)
135
-
136
- item_font_size = base_font_size
137
- section_font_size = base_font_size * 1.1
138
-
139
- section_style = ParagraphStyle(
140
- 'SectionStyle', parent=styles['Heading2'], fontName="DejaVuSans",
141
- textColor=colors.darkblue, fontSize=section_font_size, leading=section_font_size * 1.2, spaceAfter=2
142
- )
143
- item_style = ParagraphStyle(
144
- 'ItemStyle', parent=styles['Normal'], fontName="DejaVuSans",
145
- fontSize=item_font_size, leading=item_font_size * 1.15, spaceAfter=1
146
- )
147
-
148
- story.append(Spacer(1, spacer_height))
149
- columns = [[] for _ in range(num_columns)]
150
- lines_per_column = total_lines / num_columns if num_columns > 0 else total_lines
151
- current_line_count = 0
152
- current_column = 0
153
-
154
- number_pattern = re.compile(r'^\d+\.\s')
155
- for i, item in enumerate(pdf_content):
156
- if i > 0 and number_pattern.match(item.replace('<b>', '').replace('</b>', '')):
157
- columns[current_column].append(Spacer(1, section_spacer_height))
158
-
159
- if current_line_count >= lines_per_column and current_column < num_columns - 1:
160
- current_column += 1
161
- current_line_count = 0
162
- columns[current_column].append(item)
163
- current_line_count += 1
164
-
165
- column_cells = [[] for _ in range(num_columns)]
166
- for col_idx, column in enumerate(columns):
167
- for item in column:
168
- if isinstance(item, Spacer):
169
- column_cells[col_idx].append(item)
170
- elif isinstance(item, str) and item.startswith('<b>'):
171
- text = item.replace('<b>', '').replace('</b>', '')
172
- column_cells[col_idx].append(Paragraph(apply_emoji_font(text, selected_font_name), section_style))
173
  else:
174
- column_cells[col_idx].append(Paragraph(apply_emoji_font(item, selected_font_name), item_style))
175
-
176
- max_cells = max(len(cells) for cells in column_cells) if column_cells else 0
177
- for cells in column_cells:
178
- cells.extend([Paragraph("", item_style)] * (max_cells - len(cells)))
179
-
180
- col_width = (page_width - 72) / num_columns if num_columns > 0 else page_width - 72
181
- table_data = list(zip(*column_cells)) if column_cells else [[]]
182
- table = Table(table_data, colWidths=[col_width] * num_columns, hAlign='CENTER')
183
- table.setStyle(TableStyle([
184
- ('VALIGN', (0, 0), (-1, -1), 'TOP'), ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
185
- ('BACKGROUND', (0, 0), (-1, -1), colors.white), ('GRID', (0, 0), (-1, -1), 0, colors.white),
186
- ('LINEAFTER', (0, 0), (num_columns-1, -1), 0.5, colors.grey),
187
- ('LEFTPADDING', (0, 0), (-1, -1), 2), ('RIGHTPADDING', (0, 0), (-1, -1), 2),
188
- ('TOPPADDING', (0, 0), (-1, -1), 1), ('BOTTOMPADDING', (0, 0), (-1, -1), 1),
189
- ]))
190
-
191
- story.append(table)
192
- doc.build(story)
193
- buffer.seek(0)
194
- return buffer.getvalue()
195
-
196
- def pdf_to_image(pdf_bytes):
197
- try:
198
- doc = fitz.open(stream=pdf_bytes, filetype="pdf")
199
- images = []
200
- for page in doc:
201
- pix = page.get_pixmap(matrix=fitz.Matrix(2.0, 2.0))
202
- img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
203
- images.append(img)
204
- doc.close()
205
- return images
206
- except Exception as e:
207
- st.error(f"Failed to render PDF preview: {e}")
208
- return None
209
-
210
- with st.spinner("Generating PDF..."):
211
- pdf_bytes = create_pdf(st.session_state.markdown_content, base_font_size, plain_text_mode, num_columns, auto_bold_numbers)
212
-
213
- with st.container():
214
- pdf_images = pdf_to_image(pdf_bytes)
215
- if pdf_images:
216
- for img in pdf_images:
217
- st.image(img, use_container_width=True)
218
- else:
219
- st.info("Download the PDF to view it locally.")
220
-
221
- with st.sidebar:
222
- st.download_button(label="Download PDF", data=pdf_bytes, file_name="deities_guide.pdf", mime="application/pdf")
223
-
224
- default_markdown = """# Deities Guide: Mythology and Moral Lessons 🌟✨
225
-
226
- 1. 📜 **Introduction**
227
- - **Purpose**: Explore deities, spirits, saints, and beings with their epic stories and morals! 🌍📖
228
- - **Usage**: A guide for learning and storytelling across traditions. 🎭✍️
229
- - **Themes**: Justice ⚖️, faith 🙏, hubris 😤, redemption 🌈, cosmic order 🌌.
230
-
231
- 2. 🛠️ **Core Concepts of Divinity**
232
- - **Powers**: Creation 🌍, omniscience 👁️‍🗨️, shapeshifting 🦋 across entities.
233
- - **Life Cycle**: Mortality 💀, immortality ✨, transitions like saints and avatars 🌟.
234
- - **Communication**: Omens 🌩️, visions 👁️, miracles ✨ from gods and spirits.
235
-
236
- 3. ⚡ **Standard Abilities**
237
- - **Creation**: Gods and spirits shape worlds, e.g., Allah 🌍 and Vishnu 🌀.
238
- - **Influence**: Saints and prophets intercede, like Muhammad 🕌 and Paul ✝️.
239
- - **Transformation**: Angels and avatars shift forms, e.g., Gabriel 😇 and Krishna 🦚.
240
- - **Knowledge**: Foresight 🔮 or revelation 📜, as with the Holy Spirit 🕊️ and Brahma 🧠.
241
- - **Judgment**: Divine authority 👑, e.g., Yahweh ⚖️ and Yama 💀.
242
-
243
- 4. ⏳ **Mortality and Immortality**
244
- - **Gods**: Eternal ⏰, like Allah 🌟 and Shiva 🕉️.
245
- - **Spirits**: Realm-bound 🌠, e.g., jinn 🔥 and devas ✨.
246
- - **Saints/Prophets**: Mortal to divine 🌍➡️🌌, e.g., Moses 📜 and Rama 🏹.
247
- - **Beings**: Limbo states ❓, like cherubim 😇 and rakshasas 👹.
248
- - **Lessons**: Faith 🙏 and duty ⚙️ define transitions.
249
-
250
- 5. 🌠 **Ascension and Signs**
251
- - **Paths**: Birth 👶, deeds 🛡️, revelation 📖, as with Jesus ✝️ and Arjuna 🏹.
252
- - **Signs**: Miracles ✨ and prophecies 🔮, like those in the Quran 📘 and Gita 📚.
253
- - **Morals**: Obedience 🧎 and devotion ❤️ shape destiny 🌟.
254
-
255
- 6. 🎲 **Storytelling and Games**
256
- - **Portrayal**: Gods, spirits, and saints in narratives or RPGs 🎮📜.
257
- - **Dynamics**: Clerics ⛪, imams 🕌, and sadhus 🧘 serve higher powers.
258
- - **Balance**: Power 💪 vs. personality 😊 for depth.
259
-
260
- 7. 🎮 **Dungeon Mastering Beings**
261
- - **Gods**: Epic scope 🌌, e.g., Allah ✨ and Vishnu 🌀.
262
- - **Spirits**: Local influence 🏞️, like jinn 🔥 and apsaras 💃.
263
- - **Saints**: Moral anchors ⚓, e.g., St. Francis 🐾 and Ali ⚔️.
264
-
265
- 8. 🙏 **Devotee Relationships**
266
- - **Clerics**: Serve gods, e.g., Krishna’s priests 🦚.
267
- - **Mediums**: Channel spirits, like jinn whisperers 🔥👁️.
268
- - **Faithful**: Venerate saints and prophets, e.g., Fatima’s followers 🌹.
269
-
270
- 9. 🦅 **American Indian Traditions**
271
- - **Coyote, Raven, White Buffalo Woman**: Trickster kin 🦊🐦 and wise mother 🐃.
272
- - **Relation**: Siblings and guide teach balance ⚖️.
273
- - **Lesson**: Chaos 🌪️ breeds wisdom 🧠.
274
-
275
- 10. ⚔️ **Arthurian Legends**
276
- - **Merlin, Morgan le Fay, Arthur**: Mentor 🧙, rival 🧙‍♀️, son 👑.
277
- - **Relation**: Family tests loyalty 🤝.
278
- - **Lesson**: Honor 🛡️ vs. betrayal 🗡️.
279
-
280
- 11. 🏛️ **Babylonian Mythology**
281
- - **Marduk, Tiamat, Ishtar**: Son ⚔️, mother 🌊, lover ❤️.
282
- - **Relation**: Kinship drives order 🏰.
283
- - **Lesson**: Power 💪 reshapes chaos 🌪️.
284
-
285
- 12. ✝️ **Christian Trinity**
286
- - **God (Yahweh), Jesus, Holy Spirit**: Father 👑, Son ✝️, Spirit 🕊️.
287
- - **Relation**: Divine family redeems 🌈.
288
- - **Lesson**: Faith 🙏 restores grace ✨.
289
-
290
- 13. 😇 **Christian Saints & Angels**
291
- - **St. Michael, Gabriel, Mary**: Warrior ⚔️, messenger 📜, mother 🌹.
292
- - **Relation**: Heavenly kin serve God 👑.
293
- - **Lesson**: Duty ⚙️ upholds divine will 🌟.
294
-
295
- 14. 🍀 **Celtic Mythology**
296
- - **Lugh, Morrigan, Cernunnos**: Son ☀️, mother 🦇, father 🦌.
297
- - **Relation**: Family governs cycles 🌍.
298
- - **Lesson**: Courage 💪 in fate 🎲.
299
-
300
- 15. 🌄 **Central American Traditions**
301
- - **Quetzalcoatl, Tezcatlipoca, Huitzilopochtli**: Brothers 🐍🐆 and war son ⚔️.
302
- - **Relation**: Sibling rivalry creates 🌍.
303
- - **Lesson**: Sacrifice 🩸 builds worlds 🏰.
304
-
305
- 16. 🐉 **Chinese Mythology**
306
- - **Jade Emperor, Nuwa, Sun Wukong**: Father 👑, mother 🐍, rebel son 🐒.
307
- - **Relation**: Family enforces harmony 🎶.
308
- - **Lesson**: Duty ⚙️ curbs chaos 🌪️.
309
-
310
- 17. 🐙 **Cthulhu Mythos**
311
- - **Cthulhu, Nyarlathotep, Yog-Sothoth**: Elder kin 🐙👁️‍🗨️🌌.
312
- - **Relation**: Cosmic trio overwhelms 😱.
313
- - **Lesson**: Insignificance 🌌 humbles 🙇.
314
-
315
- 18. ☥ **Egyptian Mythology**
316
- - **Ra, Osiris, Isis**: Father ☀️, son ⚰️, mother 🌟.
317
- - **Relation**: Family ensures renewal 🔄.
318
- - **Lesson**: Justice ⚖️ prevails.
319
-
320
- 19. ❄️ **Finnish Mythology**
321
- - **Väinämöinen, Louhi, Ukko**: Son 🎶, mother ❄️, father ⚡.
322
- - **Relation**: Kinship tests wisdom 🧠.
323
- - **Lesson**: Perseverance 🏋️ wins.
324
-
325
- 20. 🏛️ **Greek Mythology**
326
- - **Zeus, Hera, Athena**: Father ⚡, mother 👑, daughter 🦇.
327
- - **Relation**: Family rules with tension ⚔️.
328
- - **Lesson**: Hubris 😤 meets wisdom 🧠.
329
-
330
- 21. 🕉️ **Hindu Trimurti**
331
- - **Brahma, Vishnu, Shiva**: Creator 🌀, preserver 🛡️, destroyer 🔥.
332
- - **Relation**: Divine trio cycles existence 🔄.
333
- - **Lesson**: Balance ⚖️ sustains life 🌍.
334
-
335
- 22. 🌺 **Hindu Avatars & Devis**
336
- - **Krishna, Rama, Durga**: Sons 🦚🏹 and fierce mother 🗡️.
337
- - **Relation**: Avatars and goddess protect dharma ⚖️.
338
- - **Lesson**: Duty ⚙️ defeats evil 👹.
339
-
340
- 23. 🌸 **Japanese Mythology**
341
- - **Amaterasu, Susanoo, Tsukuyomi**: Sister ☀️, brothers 🌊🌙.
342
- - **Relation**: Siblings balance cosmos 🌌.
343
- - **Lesson**: Harmony 🎶 vs. chaos 🌪️.
344
-
345
- 24. 🗡️ **Melnibonean Legends**
346
- - **Arioch, Xiombarg, Elric**: Lords 👑 and mortal son ⚔️.
347
- - **Relation**: Pact binds chaos 🌪️.
348
- - **Lesson**: Power 💪 corrupts 😈.
349
-
350
- 25. ☪️ **Muslim Divine & Messengers**
351
- - **Allah, Muhammad, Gabriel**: God 🌟, prophet 🕌, angel 😇.
352
- - **Relation**: Messenger reveals divine will 📜.
353
- - **Lesson**: Submission 🙇 brings peace ☮️.
354
-
355
- 26. 👻 **Muslim Spirits & Kin**
356
- - **Jinn, Iblis, Khidr**: Spirits 🔥😈 and guide 🌿 defy or aid.
357
- - **Relation**: Supernatural kin test faith 🙏.
358
- - **Lesson**: Obedience 🧎 vs. rebellion 😡.
359
-
360
- 27. 🏰 **Nehwon Legends**
361
- - **Death, Ningauble, Sheelba**: Fateful trio 💀👁️‍🗨️🌿.
362
- - **Relation**: Guides shape destiny 🎲.
363
- - **Lesson**: Cunning 🧠 defies fate ⚰️.
364
-
365
- 28. 🧝 **Nonhuman Traditions**
366
- - **Corellon, Moradin, Gruumsh**: Elf 🧝, dwarf ⛏️, orc 🗡️ fathers.
367
- - **Relation**: Rivals define purpose ⚔️.
368
- - **Lesson**: Community 🤝 endures.
369
-
370
- 29. ᚱ **Norse Mythology**
371
- - **Odin, Frigg, Loki**: Father 👁️, mother 👑, trickster son 🦊.
372
- - **Relation**: Family faces doom ⚡.
373
- - **Lesson**: Sacrifice 🩸 costs.
374
-
375
- 30. 🗿 **Sumerian Mythology**
376
- - **Enki, Inanna, Anu**: Son 🌊, daughter ❤️, father 🌌.
377
- - **Relation**: Kin wield knowledge 🧠.
378
- - **Lesson**: Ambition 🌟 shapes.
379
-
380
- 31. 📚 **Appendices**
381
- - **Planes**: Realms of gods, spirits, saints, e.g., Paradise 🌈 and Svarga ✨.
382
- - **Symbols**: Rituals 🕉️ and artifacts 🗿 of faith.
383
- - **Charts**: Domains and duties for devotees 📊.
384
-
385
- 32. 🌌 **Planes of Existence**
386
- - **Heaven/Paradise**: Christian/Muslim abode 🌟.
387
- - **Svarga**: Hindu divine realm ✨.
388
- - **Underworld**: Spirits linger, e.g., Sheol ⚰️ and Naraka 🔥.
389
-
390
- 33. 🕍 **Temple Trappings**
391
- - **Cross/Crescent**: Christian/Muslim faith ✝️☪️.
392
- - **Mandalas**: Hindu devotion 🌀.
393
- - **Relics**: Saints’ and prophets’ legacy 🗝️.
394
-
395
- 34. 📊 **Clerical Chart**
396
- - **Gods**: Domains, e.g., creation 🌍 and mercy ❤️.
397
- - **Spirits**: Influence, like guidance 🌿 and mischief 😈.
398
- - **Saints/Prophets**: Virtues, e.g., justice ⚖️ and prophecy 🔮.
399
- """
400
-
401
- create_pdf_tab(default_markdown)
 
1
  import io
2
  import re
 
 
3
  import os
4
+ import glob
5
+ import asyncio
6
+ import hashlib
7
+ import unicodedata
8
+ import streamlit as st
9
  from PIL import Image
10
  import fitz
11
+ import edge_tts
12
  from reportlab.lib.pagesizes import A4
13
  from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
14
  from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
15
  from reportlab.lib import colors
16
  from reportlab.pdfbase import pdfmetrics
17
  from reportlab.pdfbase.ttfonts import TTFont
 
18
 
19
  st.set_page_config(layout="wide", initial_sidebar_state="collapsed")
20
 
21
+ async def generate_audio(text, voice):
22
+ filename = f"{hashlib.md5(text.encode()).hexdigest()}_{voice}.mp3"
23
+ communicate = edge_tts.Communicate(text, voice)
24
+ await communicate.save(filename)
25
+ return filename
26
+
27
+ def apply_emoji_font(text, emoji_font):
28
+ emoji_pattern = re.compile(
29
+ r"([\U0001F300-\U0001F5FF"
30
+ r"\U0001F600-\U0001F64F"
31
+ r"\U0001F680-\U0001F6FF"
32
+ r"\U0001F700-\U0001F77F"
33
+ r"\U0001F780-\U0001F7FF"
34
+ r"\U0001F800-\U0001F8FF"
35
+ r"\U0001F900-\U0001F9FF"
36
+ r"\U0001FA00-\U0001FA6F"
37
+ r"\U0001FA70-\U0001FAFF"
38
+ r"\u2600-\u26FF"
39
+ r"\u2700-\u27BF]+)"
40
+ )
41
+ def replace_emoji(match):
42
+ emoji = match.group(1)
43
+ emoji = unicodedata.normalize('NFC', emoji)
44
+ return f'<font face="{emoji_font}">{emoji}</font>'
45
+ segments = []
46
+ last_pos = 0
47
+ for match in emoji_pattern.finditer(text):
48
+ start, end = match.span()
49
+ if last_pos < start:
50
+ segments.append(f'<font face="DejaVuSans">{text[last_pos:start]}</font>')
51
+ segments.append(replace_emoji(match))
52
+ last_pos = end
53
+ if last_pos < len(text):
54
+ segments.append(f'<font face="DejaVuSans">{text[last_pos:]}</font>')
55
+ return ''.join(segments)
56
+
57
+ def markdown_to_pdf_content(markdown_text, render_with_bold, auto_bold_numbers):
58
+ lines = markdown_text.strip().split('\n')
59
+ pdf_content = []
60
+ number_pattern = re.compile(r'^\d+\.\s')
61
+ for line in lines:
62
+ line = line.strip()
63
+ if not line or line.startswith('# '):
64
+ continue
65
+ if render_with_bold:
66
+ line = re.sub(r'\*\*(.*?)\*\*', r'<b>\1</b>', line)
67
+ if auto_bold_numbers and number_pattern.match(line):
68
+ if not (line.startswith("<b>") and line.endswith("</b>")):
69
+ line = f"<b>{line}</b>"
70
+ pdf_content.append(line)
71
+ total_lines = len(pdf_content)
72
+ return pdf_content, total_lines
73
+
74
+ def create_pdf(markdown_text, base_font_size, render_with_bold, auto_bold_numbers, enlarge_numbered, num_columns):
75
+ buffer = io.BytesIO()
76
+ page_width = A4[0] * 2
77
+ page_height = A4[1]
78
+ doc = SimpleDocTemplate(buffer, pagesize=(page_width, page_height), leftMargin=36, rightMargin=36, topMargin=36, bottomMargin=36)
79
+ styles = getSampleStyleSheet()
80
+ spacer_height = 10
81
+ section_spacer_height = 15
82
+ pdf_content, total_lines = markdown_to_pdf_content(markdown_text, render_with_bold, auto_bold_numbers)
83
+ item_style = ParagraphStyle(
84
+ 'ItemStyle', parent=styles['Normal'], fontName="DejaVuSans",
85
+ fontSize=base_font_size, leading=base_font_size * 1.15, spaceAfter=1
86
+ )
87
+ bold_style = ParagraphStyle(
88
+ 'BoldStyle', parent=styles['Normal'], fontName="NotoEmoji-Bold",
89
+ fontSize=base_font_size, leading=base_font_size * 1.15, spaceAfter=1
90
+ )
91
+ numbered_bold_style = ParagraphStyle(
92
+ 'NumberedBoldStyle', parent=styles['Normal'], fontName="NotoEmoji-Bold",
93
+ fontSize=base_font_size + 1 if enlarge_numbered else base_font_size,
94
+ leading=(base_font_size + 1) * 1.15 if enlarge_numbered else base_font_size * 1.15, spaceAfter=1
95
+ )
96
+ section_style = ParagraphStyle(
97
+ 'SectionStyle', parent=styles['Heading2'], fontName="DejaVuSans",
98
+ textColor=colors.darkblue, fontSize=base_font_size * 1.1, leading=base_font_size * 1.32, spaceAfter=2
99
+ )
100
  try:
101
+ available_font_files = glob.glob("*.ttf")
102
+ if not available_font_files:
103
+ st.error("No .ttf font files found in the current directory.")
104
+ return
105
+ selected_font_path = None
106
+ for f in available_font_files:
107
+ if "NotoEmoji-Bold" in f:
108
+ selected_font_path = f
109
+ break
110
+ if selected_font_path:
111
+ pdfmetrics.registerFont(TTFont("NotoEmoji-Bold", selected_font_path))
112
  pdfmetrics.registerFont(TTFont("DejaVuSans", "DejaVuSans.ttf"))
113
  except Exception as e:
114
+ st.error(f"Font registration error: {e}")
115
  return
116
+ columns = [[] for _ in range(num_columns)]
117
+ lines_per_column = total_lines / num_columns if num_columns > 0 else total_lines
118
+ current_line_count = 0
119
+ current_column = 0
120
+ number_pattern = re.compile(r'^\d+\.\s')
121
+ for item in pdf_content:
122
+ if current_line_count >= lines_per_column and current_column < num_columns - 1:
123
+ current_column += 1
124
+ current_line_count = 0
125
+ columns[current_column].append(item)
126
+ current_line_count += 1
127
+ column_cells = [[] for _ in range(num_columns)]
128
+ for col_idx, column in enumerate(columns):
129
+ for item in column:
130
+ if isinstance(item, str) and item.startswith("<b>") and item.endswith("</b>"):
131
+ content = item[3:-4].strip()
132
+ if number_pattern.match(content):
133
+ column_cells[col_idx].append(Paragraph(apply_emoji_font(content, "NotoEmoji-Bold"), numbered_bold_style))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  else:
135
+ column_cells[col_idx].append(Paragraph(apply_emoji_font(content, "NotoEmoji-Bold"), section_style))
136
+ else:
137
+ column_cells[col_idx].append(Paragraph(apply_emoji_font(item, "DejaVuSans"), item_style))
138
+ max_cells = max(len(cells) for cells in column_cells) if column_cells else 0
139
+ for cells in column_cells:
140
+ cells.extend([Paragraph("", item_style)] * (max_cells - len(cells)))
141
+ col_width = (page_width - 72) / num_columns if num_columns > 0 else page_width - 72
142
+ table_data = list(zip(*column_cells)) if column_cells else [[]]
143
+ table = Table(table_data, colWidths=[col_width] * num_columns, hAlign='CENTER')
144
+ table.setStyle(TableStyle([
145
+ ('VALIGN', (0, 0), (-1, -1), 'TOP'),
146
+ ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
147
+ ('BACKGROUND', (0, 0), (-1, -1), colors.white),
148
+ ('GRID', (0, 0), (-1, -1), 0, colors.white),
149
+ ('LINEAFTER', (0, 0), (num_columns-1, -1), 0.5, colors.grey),
150
+ ('LEFTPADDING', (0, 0), (-1, -1), 2),
151
+ ('RIGHTPADDING', (0, 0), (-1, -1), 2),
152
+ ('TOPPADDING', (0, 0), (-1, -1), 1),
153
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 1),
154
+ ]))
155
+ story = [Spacer(1, spacer_height), table]
156
+ doc.build(story)
157
+ buffer.seek(0)
158
+ return buffer.getvalue()
159
+
160
+ def pdf_to_image(pdf_bytes):
161
+ try:
162
+ doc = fitz.open(stream=pdf_bytes, filetype="pdf")
163
+ images = []
164
+ for page in doc:
165
+ pix = page.get_pixmap(matrix=fitz.Matrix(2.0, 2.0))
166
+ img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
167
+ images.append(img)
168
+ doc.close()
169
+ return images
170
+ except Exception as e:
171
+ st.error(f"Failed to render PDF preview: {e}")
172
+ return None
173
+
174
+ md_files = [f for f in glob.glob("*.md") if os.path.basename(f) != "README.md"]
175
+ md_options = [os.path.splitext(os.path.basename(f))[0] for f in md_files]
176
+
177
+ with st.sidebar:
178
+ st.markdown("### PDF Options")
179
+ selected_md = st.selectbox("Select Markdown File", options=md_options, index=0 if md_options else -1)
180
+ available_font_files = {os.path.splitext(os.path.basename(f))[0]: f for f in glob.glob("*.ttf")}
181
+ selected_font_name = st.selectbox("Select Emoji Font", options=list(available_font_files.keys()), index=list(available_font_files.keys()).index("NotoEmoji-Bold") if "NotoEmoji-Bold" in available_font_files else 0)
182
+ base_font_size = st.slider("Font Size (points)", min_value=6, max_value=16, value=8, step=1)
183
+ render_with_bold = st.checkbox("Render with Bold Formatting (remove ** markers)", value=True, key="render_with_bold")
184
+ auto_bold_numbers = st.checkbox("Auto Bold Numbered Lines", value=True, key="auto_bold_numbers")
185
+ enlarge_numbered = st.checkbox("Enlarge Font Size for Numbered Lines", value=True, key="enlarge_numbered")
186
+ num_columns = st.selectbox("Number of Columns", options=[1, 2, 3, 4, 5, 6], index=3)
187
+ if 'markdown_content' not in st.session_state or not md_options:
188
+ st.session_state.markdown_content = ""
189
+ if md_options and selected_md:
190
+ with open(f"{selected_md}.md", "r", encoding="utf-8") as f:
191
+ st.session_state.markdown_content = f.read()
192
+ edited_markdown = st.text_area("Modify the markdown content below:", value=st.session_state.markdown_content, height=300, key=f"markdown_{selected_md}_{selected_font_name}_{num_columns}")
193
+ if st.button("Update PDF"):
194
+ st.session_state.markdown_content = edited_markdown
195
+ with open(f"{selected_md}.md", "w", encoding="utf-8") as f:
196
+ f.write(edited_markdown)
197
+ st.experimental_rerun()
198
+ st.download_button(label="Save Markdown", data=st.session_state.markdown_content, file_name=f"{selected_md}.md", mime="text/markdown")
199
+ st.markdown("### Text-to-Speech")
200
+ VOICES = ["en-US-AriaNeural", "en-US-JennyNeural", "en-GB-SoniaNeural", "en-US-GuyNeural", "en-US-AnaNeural"]
201
+ selected_voice = st.selectbox("Select Voice for TTS", options=VOICES, index=0)
202
+ if st.button("Generate Audio"):
203
+ audio_file = asyncio.run(generate_audio(st.session_state.markdown_content, selected_voice))
204
+ st.audio(audio_file)
205
+ with open(audio_file, "rb") as f:
206
+ audio_bytes = f.read()
207
+ st.download_button("Download Audio", data=audio_bytes, file_name=os.path.basename(audio_file), mime="audio/mpeg")
208
+
209
+ if not md_options:
210
+ st.warning("No .md files found in the directory (excluding README.md). Using default content.")
211
+ default_markdown = """# Default Markdown Content
212
+ This is a sample **bold text** example.
213
+ 1. **Introduction**
214
+ 2. **Core Concepts**
215
+ 3. **Summary**"""
216
+ st.session_state.markdown_content = default_markdown
217
+
218
+ with st.spinner("Generating PDF..."):
219
+ pdf_bytes = create_pdf(st.session_state.markdown_content, base_font_size, render_with_bold, auto_bold_numbers, enlarge_numbered, num_columns)
220
+
221
+ with st.container():
222
+ pdf_images = pdf_to_image(pdf_bytes)
223
+ if pdf_images:
224
+ for img in pdf_images:
225
+ st.image(img, use_container_width=True)
226
+ else:
227
+ st.info("Download the PDF to view it locally.")
228
+
229
+ with st.sidebar:
230
+ st.download_button(label="Download PDF", data=pdf_bytes, file_name="output.pdf", mime="application/pdf")