awacke1 commited on
Commit
1dc6c54
Β·
verified Β·
1 Parent(s): 302510f

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +628 -0
app.py ADDED
@@ -0,0 +1,628 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ from reportlab.pdfgen import canvas
19
+ from datetime import datetime
20
+ import pytz
21
+ from pypdf import PdfReader, PdfWriter
22
+ from pypdf.annotations import Link
23
+ from pypdf.generic import Fit
24
+
25
+ st.set_page_config(layout="wide", initial_sidebar_state="collapsed")
26
+
27
+ # Existing functions (unchanged)
28
+ def get_timestamp_prefix():
29
+ central = pytz.timezone("US/Central")
30
+ now = datetime.now(central)
31
+ return now.strftime("%a %m%d %I%M%p").upper()
32
+
33
+ def clean_for_speech(text):
34
+ text = text.replace("#", "")
35
+ emoji_pattern = re.compile(
36
+ r"[\U0001F300-\U0001F5FF"
37
+ r"\U0001F600-\U0001F64F"
38
+ r"\U0001F680-\U0001F6FF"
39
+ r"\U0001F700-\U0001F77F"
40
+ r"\U0001F780-\U0001F7FF"
41
+ r"\U0001F800-\U0001F8FF"
42
+ r"\U0001F900-\U0001F9FF"
43
+ r"\U0001FA00-\U0001FA6F"
44
+ r"\U0001FA70-\U0001FAFF"
45
+ r"\u2600-\u26FF"
46
+ r"\u2700-\u27BF]+", flags=re.UNICODE)
47
+ text = emoji_pattern.sub('', text)
48
+ return text
49
+
50
+ def trim_emojis_except_numbered(markdown_text):
51
+ emoji_pattern = re.compile(
52
+ r"[\U0001F300-\U0001F5FF"
53
+ r"\U0001F600-\U0001F64F"
54
+ r"\U0001F680-\U0001F6FF"
55
+ r"\U0001F700-\U0001F77F"
56
+ r"\U0001F780-\U0001F7FF"
57
+ r"\U0001F800-\U0001F8FF"
58
+ r"\U0001F900-\U0001F9FF"
59
+ r"\U0001FAD0-\U0001FAD9"
60
+ r"\U0001FA00-\U0001FA6F"
61
+ r"\U0001FA70-\U0001FAFF"
62
+ r"\u2600-\u26FF"
63
+ r"\u2700-\u27BF]+"
64
+ )
65
+ number_pattern = re.compile(r'^\d+\.\s')
66
+ lines = markdown_text.strip().split('\n')
67
+ processed_lines = []
68
+
69
+ for line in lines:
70
+ if number_pattern.match(line):
71
+ processed_lines.append(line)
72
+ else:
73
+ processed_lines.append(emoji_pattern.sub('', line))
74
+
75
+ return '\n'.join(processed_lines)
76
+
77
+ async def generate_audio(text, voice, filename):
78
+ communicate = edge_tts.Communicate(text, voice)
79
+ await communicate.save(filename)
80
+ return filename
81
+
82
+ def detect_and_convert_links(text):
83
+ url_pattern = re.compile(
84
+ r'(https?://|www\.)[^\s\[\]()<>{}]+(\.[^\s\[\]()<>{}]+)+(/[^\s\[\]()<>{}]*)?',
85
+ re.IGNORECASE
86
+ )
87
+ md_link_pattern = re.compile(r'\[(.*?)\]\((https?://[^\s\[\]()<>{}]+)\)')
88
+ text = md_link_pattern.sub(r'<a href="\2">\1</a>', text)
89
+ start_idx = 0
90
+ result = []
91
+ while start_idx < len(text):
92
+ match = url_pattern.search(text, start_idx)
93
+ if not match:
94
+ result.append(text[start_idx:])
95
+ break
96
+ prev_text = text[start_idx:match.start()]
97
+ tag_balance = prev_text.count('<a') - prev_text.count('</a')
98
+ if tag_balance > 0:
99
+ result.append(text[start_idx:match.end()])
100
+ else:
101
+ result.append(text[start_idx:match.start()])
102
+ url = match.group(0)
103
+ if url.startswith('www.'):
104
+ url_with_prefix = 'http://' + url
105
+ else:
106
+ url_with_prefix = url
107
+ result.append(f'<a href="{url_with_prefix}">{url}</a>')
108
+ start_idx = match.end()
109
+ return ''.join(result)
110
+
111
+ def apply_emoji_font(text, emoji_font):
112
+ link_pattern = re.compile(r'<a\s+href="([^"]+)">(.*?)</a>')
113
+ links = []
114
+ def save_link(match):
115
+ link_idx = len(links)
116
+ links.append((match.group(1), match.group(2)))
117
+ return f"###LINK_{link_idx}###"
118
+ text = link_pattern.sub(save_link, text)
119
+ text = re.sub(r'<b>(.*?)</b>', lambda m: f'###BOLD_START###{m.group(1)}###BOLD_END###', text)
120
+ emoji_pattern = re.compile(
121
+ r"([\U0001F300-\U0001F5FF"
122
+ r"\U0001F600-\U0001F64F"
123
+ r"\U0001F680-\U0001F6FF"
124
+ r"\U0001F700-\U0001F77F"
125
+ r"\U0001F780-\U0001F7FF"
126
+ r"\U0001F800-\U0001F8FF"
127
+ r"\U0001F900-\U0001F9FF"
128
+ r"\U0001FAD0-\U0001FAD9"
129
+ r"\U0001FA00-\U0001FA6F"
130
+ r"\U0001FA70-\U0001FAFF"
131
+ r"\u2600-\u26FF"
132
+ r"\u2700-\u27BF]+)"
133
+ )
134
+ def replace_emoji(match):
135
+ emoji = match.group(1)
136
+ emoji = unicodedata.normalize('NFC', emoji)
137
+ return f'<font face="{emoji_font}">{emoji}</font>'
138
+ segments = []
139
+ last_pos = 0
140
+ for match in emoji_pattern.finditer(text):
141
+ start, end = match.span()
142
+ if last_pos < start:
143
+ segments.append(f'<font face="DejaVuSans">{text[last_pos:start]}</font>')
144
+ segments.append(replace_emoji(match))
145
+ last_pos = end
146
+ if last_pos < len(text):
147
+ segments.append(f'<font face="DejaVuSans">{text[last_pos:]}</font>')
148
+ combined_text = ''.join(segments)
149
+ combined_text = combined_text.replace('###BOLD_START###', '</font><b><font face="DejaVuSans">')
150
+ combined_text = combined_text.replace('###BOLD_END###', '</font></b><font face="DejaVuSans">')
151
+ for i, (url, label) in enumerate(links):
152
+ placeholder = f"###LINK_{i}###"
153
+ link_html = f'<a href="{url}"><font face="DejaVuSans">{label}</font></a>'
154
+ combined_text = combined_text.replace(placeholder, link_html)
155
+ return combined_text
156
+
157
+ def markdown_to_pdf_content(markdown_text, render_with_bold, auto_bold_numbers, add_space_before_numbered, headings_to_fonts):
158
+ lines = markdown_text.strip().split('\n')
159
+ pdf_content = []
160
+ number_pattern = re.compile(r'^\d+\.\s')
161
+ heading_pattern = re.compile(r'^(#{1,4})\s+(.+)$')
162
+ first_numbered_seen = False
163
+
164
+ for line in lines:
165
+ line = line.strip()
166
+ if not line:
167
+ continue
168
+
169
+ if headings_to_fonts and line.startswith('#'):
170
+ heading_match = heading_pattern.match(line)
171
+ if heading_match:
172
+ level = len(heading_match.group(1))
173
+ heading_text = heading_match.group(2).strip()
174
+ formatted_heading = f"<h{level}>{heading_text}</h{level}>"
175
+ pdf_content.append(formatted_heading)
176
+ continue
177
+
178
+ is_numbered_line = number_pattern.match(line) is not None
179
+
180
+ if add_space_before_numbered and is_numbered_line:
181
+ if first_numbered_seen and not line.startswith("1."):
182
+ pdf_content.append("")
183
+ if not first_numbered_seen:
184
+ first_numbered_seen = True
185
+
186
+ line = detect_and_convert_links(line)
187
+
188
+ if render_with_bold or headings_to_fonts:
189
+ line = re.sub(r'\*\*(.+?)\*\*', r'<b>\1</b>', line)
190
+ if headings_to_fonts:
191
+ line = re.sub(r'\*([^*]+?)\*', r'<b>\1</b>', line)
192
+
193
+ if auto_bold_numbers and is_numbered_line:
194
+ if not (line.startswith("<b>") and line.endswith("</b>")):
195
+ if "<b>" in line and "</b>" in line:
196
+ line = re.sub(r'</?b>', '', line)
197
+ line = f"<b>{line}</b>"
198
+ else:
199
+ line = f"<b>{line}</b>"
200
+ pdf_content.append(line)
201
+ total_lines = len(pdf_content)
202
+ return pdf_content, total_lines
203
+
204
+ def create_pdf(markdown_text, base_font_size, render_with_bold, auto_bold_numbers, enlarge_numbered, num_columns, add_space_before_numbered, headings_to_fonts):
205
+ buffer = io.BytesIO()
206
+ page_width = A4[0] * 2
207
+ page_height = A4[1]
208
+ doc = SimpleDocTemplate(buffer, pagesize=(page_width, page_height), leftMargin=36, rightMargin=36, topMargin=36, bottomMargin=36)
209
+ styles = getSampleStyleSheet()
210
+ spacer_height = 10
211
+ pdf_content, total_lines = markdown_to_pdf_content(markdown_text, render_with_bold, auto_bold_numbers, add_space_before_numbered, headings_to_fonts)
212
+ try:
213
+ available_font_files = glob.glob("*.ttf")
214
+ if not available_font_files:
215
+ st.error("No .ttf font files found.")
216
+ return
217
+ selected_font_path = next((f for f in available_font_files if "NotoEmoji-Bold" in f), None)
218
+ if selected_font_path:
219
+ pdfmetrics.registerFont(TTFont("NotoEmoji-Bold", selected_font_path))
220
+ pdfmetrics.registerFont(TTFont("DejaVuSans", "DejaVuSans.ttf"))
221
+ except Exception as e:
222
+ st.error(f"Font registration error: {e}")
223
+ return
224
+ total_chars = sum(len(line) for line in pdf_content)
225
+ hierarchy_weight = sum(1.5 if line.startswith("<b>") else 1 for line in pdf_content)
226
+ content_density = total_lines * hierarchy_weight + total_chars / 50
227
+ usable_height = page_height - 72 - spacer_height
228
+ usable_width = page_width - 72
229
+ avg_line_chars = total_chars / total_lines if total_lines > 0 else 50
230
+ ideal_lines_per_col = 20
231
+ suggested_columns = max(1, min(6, int(total_lines / ideal_lines_per_col) + 1))
232
+ num_columns = num_columns if num_columns != 0 else suggested_columns
233
+ col_width = usable_width / num_columns
234
+ min_font_size = 6
235
+ max_font_size = 16
236
+ lines_per_col = total_lines / num_columns if num_columns > 0 else total_lines
237
+ target_height_per_line = usable_height / lines_per_col if lines_per_col > 0 else usable_height
238
+ estimated_font_size = int(target_height_per_line / 1.5)
239
+ adjusted_font_size = max(min_font_size, min(max_font_size, estimated_font_size))
240
+ if avg_line_chars > col_width / adjusted_font_size * 10:
241
+ adjusted_font_size = int(col_width / (avg_line_chars / 10))
242
+ adjusted_font_size = max(min_font_size, adjusted_font_size)
243
+ item_style = ParagraphStyle(
244
+ 'ItemStyle', parent=styles['Normal'], fontName="DejaVuSans",
245
+ fontSize=adjusted_font_size, leading=adjusted_font_size * 1.15, spaceAfter=1,
246
+ linkUnderline=True
247
+ )
248
+ numbered_bold_style = ParagraphStyle(
249
+ 'NumberedBoldStyle', parent=styles['Normal'], fontName="NotoEmoji-Bold",
250
+ fontSize=adjusted_font_size + 1 if enlarge_numbered else adjusted_font_size,
251
+ leading=(adjusted_font_size + 1) * 1.15 if enlarge_numbered else adjusted_font_size * 1.15, spaceAfter=1,
252
+ linkUnderline=True
253
+ )
254
+ section_style = ParagraphStyle(
255
+ 'SectionStyle', parent=styles['Heading2'], fontName="DejaVuSans",
256
+ textColor=colors.darkblue, fontSize=adjusted_font_size * 1.1, leading=adjusted_font_size * 1.32, spaceAfter=2,
257
+ linkUnderline=True
258
+ )
259
+ columns = [[] for _ in range(num_columns)]
260
+ lines_per_column = total_lines / num_columns if num_columns > 0 else total_lines
261
+ current_line_count = 0
262
+ current_column = 0
263
+ number_pattern = re.compile(r'^\d+\.\s')
264
+ for item in pdf_content:
265
+ if current_line_count >= lines_per_column and current_column < num_columns - 1:
266
+ current_column += 1
267
+ current_line_count = 0
268
+ columns[current_column].append(item)
269
+ current_line_count += 1
270
+ column_cells = [[] for _ in range(num_columns)]
271
+ for col_idx, column in enumerate(columns):
272
+ for item in column:
273
+ if isinstance(item, str):
274
+ heading_match = re.match(r'<h(\d)>(.*?)</h\1>', item) if headings_to_fonts else None
275
+ if heading_match:
276
+ level = int(heading_match.group(1))
277
+ heading_text = heading_match.group(2)
278
+ heading_style = ParagraphStyle(
279
+ f'Heading{level}Style',
280
+ parent=styles['Heading1'],
281
+ fontName="DejaVuSans",
282
+ textColor=colors.darkblue if level == 1 else (colors.black if level > 2 else colors.blue),
283
+ fontSize=adjusted_font_size * (1.6 - (level-1)*0.15),
284
+ leading=adjusted_font_size * (1.8 - (level-1)*0.15),
285
+ spaceAfter=4 - (level-1),
286
+ spaceBefore=6 - (level-1),
287
+ linkUnderline=True
288
+ )
289
+ column_cells[col_idx].append(Paragraph(apply_emoji_font(heading_text, "NotoEmoji-Bold"), heading_style))
290
+ elif item.startswith("<b>") and item.endswith("</b>"):
291
+ content = item[3:-4].strip()
292
+ if number_pattern.match(content):
293
+ column_cells[col_idx].append(Paragraph(apply_emoji_font(content, "NotoEmoji-Bold"), numbered_bold_style))
294
+ else:
295
+ column_cells[col_idx].append(Paragraph(apply_emoji_font(content, "NotoEmoji-Bold"), section_style))
296
+ else:
297
+ column_cells[col_idx].append(Paragraph(apply_emoji_font(item, "DejaVuSans"), item_style))
298
+ else:
299
+ column_cells[col_idx].append(Paragraph(apply_emoji_font(str(item), "DejaVuSans"), item_style))
300
+ max_cells = max(len(cells) for cells in column_cells) if column_cells else 0
301
+ for cells in column_cells:
302
+ cells.extend([Paragraph("", item_style)] * (max_cells - len(cells)))
303
+ table_data = list(zip(*column_cells)) if column_cells else [[]]
304
+ table = Table(table_data, colWidths=[col_width] * num_columns, hAlign='CENTER')
305
+ table.setStyle(TableStyle([
306
+ ('VALIGN', (0, 0), (-1, -1), 'TOP'),
307
+ ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
308
+ ('BACKGROUND', (0, 0), (-1, -1), colors.white),
309
+ ('GRID', (0, 0), (-1, -1), 0, colors.white),
310
+ ('LINEAFTER', (0, 0), (num_columns-1, -1), 0.5, colors.grey),
311
+ ('LEFTPADDING', (0, 0), (-1, -1), 2),
312
+ ('RIGHTPADDING', (0, 0), (-1, -1), 2),
313
+ ('TOPPADDING', (0, 0), (-1, -1), 1),
314
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 1),
315
+ ]))
316
+ story = [Spacer(1, spacer_height), table]
317
+ doc.build(story)
318
+ buffer.seek(0)
319
+ return buffer.getvalue()
320
+
321
+ def pdf_to_image(pdf_bytes):
322
+ try:
323
+ doc = fitz.open(stream=pdf_bytes, filetype="pdf")
324
+ images = []
325
+ for page in doc:
326
+ pix = page.get_pixmap(matrix=fitz.Matrix(2.0, 2.0))
327
+ img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
328
+ images.append(img)
329
+ doc.close()
330
+ return images
331
+ except Exception as e:
332
+ st.error(f"Failed to render PDF preview: {e}")
333
+ return None
334
+
335
+ # PDF creation and linking functions
336
+ WORDS_10 = ["one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"]
337
+ WORDS_20 = ["one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten",
338
+ "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen", "twenty"]
339
+
340
+ def create_crossfile_pdfs(source_pdf="TestSource.pdf", target_pdf="TestTarget.pdf"):
341
+ """Create two PDFs with cross-file linking."""
342
+ def create_base_pdf(filename):
343
+ buffer = io.BytesIO()
344
+ c = canvas.Canvas(buffer)
345
+ c.setFont("Helvetica", 12)
346
+ for i, word in enumerate(WORDS_10, 1):
347
+ y = 800 - (i * 20)
348
+ c.drawString(50, y, f"{i}. {word}")
349
+ c.showPage()
350
+ c.save()
351
+ buffer.seek(0)
352
+ with open(filename, "wb") as f:
353
+ f.write(buffer.getvalue())
354
+ buffer.close()
355
+
356
+ def add_bookmark_to_seven(pdf_file):
357
+ reader = PdfReader(pdf_file)
358
+ writer = PdfWriter()
359
+ for page in reader.pages:
360
+ writer.add_page(page)
361
+ page = writer.pages[0]
362
+ y_position = 800 - (7 * 20)
363
+ fit = Fit(fit_type="/XYZ", fit_args=[50, y_position, 0])
364
+ writer.add_outline_item("Seven Bookmark", 0, fit=fit)
365
+ with open(pdf_file, "wb") as f:
366
+ writer.write(f)
367
+
368
+ def modify_source_pdf(source, target):
369
+ reader = PdfReader(source)
370
+ writer = PdfWriter()
371
+ for page in reader.pages:
372
+ writer.add_page(page)
373
+ buffer = io.BytesIO()
374
+ c = canvas.Canvas(buffer)
375
+ c.setFont("Helvetica", 8)
376
+ seven_y = 800 - (7 * 20)
377
+ c.drawString(90, seven_y - 5, "link")
378
+ c.showPage()
379
+ c.save()
380
+ buffer.seek(0)
381
+ text_pdf = PdfReader(buffer)
382
+ page = writer.pages[0]
383
+ page.merge_page(text_pdf.pages[0])
384
+ link = Link(
385
+ rect=(90, seven_y - 10, 150, seven_y + 10),
386
+ url=f"file://{os.path.abspath(target)}#page=1"
387
+ )
388
+ writer.add_annotation(page_number=0, annotation=link)
389
+ with open(source, "wb") as f:
390
+ writer.write(f)
391
+ buffer.close()
392
+
393
+ def add_internal_link(pdf_file):
394
+ reader = PdfReader(pdf_file)
395
+ writer = PdfWriter()
396
+ for page in reader.pages:
397
+ writer.add_page(page)
398
+ one_y = 800 - (1 * 20)
399
+ ten_y = 800 - (10 * 20)
400
+ link = Link(
401
+ rect=(50, one_y - 10, 100, one_y + 10),
402
+ target_page_index=0,
403
+ fit=Fit(fit_type="/XYZ", fit_args=[50, ten_y, 0])
404
+ )
405
+ writer.add_annotation(page_number=0, annotation=link)
406
+ with open(pdf_file, "wb") as f:
407
+ writer.write(f)
408
+
409
+ create_base_pdf(source_pdf)
410
+ create_base_pdf(target_pdf)
411
+ add_bookmark_to_seven(target_pdf)
412
+ modify_source_pdf(source_pdf, target_pdf)
413
+ add_internal_link(source_pdf)
414
+ add_internal_link(target_pdf)
415
+ return source_pdf, target_pdf
416
+
417
+ def create_selflinking_pdf(pdf_file="SelfLinking.pdf"):
418
+ """Create a PDF with a TOC on page 1 linking to a 1-20 list starting on page 2."""
419
+ buffer = io.BytesIO()
420
+ c = canvas.Canvas(buffer)
421
+
422
+ # Page 1: Table of Contents
423
+ c.setFont("Helvetica", 14)
424
+ c.drawString(50, 800, "Table of Contents")
425
+ c.setFont("Helvetica", 12)
426
+ toc_y_positions = []
427
+ for i, word in enumerate(WORDS_10, 1):
428
+ y = 760 - (i * 20)
429
+ c.drawString(50, y, f"{word}")
430
+ toc_y_positions.append(y)
431
+ c.showPage()
432
+
433
+ # Page 2: Numbered list 1-20
434
+ c.setFont("Helvetica", 12)
435
+ list_y_positions = []
436
+ for i, word in enumerate(WORDS_20, 1):
437
+ y = 800 - (i * 20)
438
+ c.drawString(50, y, f"{i}. {word}")
439
+ list_y_positions.append(y)
440
+ c.showPage()
441
+
442
+ # Save the initial PDF
443
+ c.save()
444
+ buffer.seek(0)
445
+ with open(pdf_file, "wb") as f:
446
+ f.write(buffer.getvalue())
447
+ buffer.close()
448
+
449
+ # Add outlines and links
450
+ reader = PdfReader(pdf_file)
451
+ writer = PdfWriter()
452
+ for page in reader.pages:
453
+ writer.add_page(page)
454
+
455
+ # Add outline entries
456
+ toc_page = writer.pages[0]
457
+ list_page = writer.pages[1]
458
+ writer.add_outline_item("Table of Contents", 0, fit=Fit(fit_type="/Fit"))
459
+ for i, word in enumerate(WORDS_10, 1):
460
+ y = list_y_positions[i-1]
461
+ writer.add_outline_item(word, 1, fit=Fit(fit_type="/XYZ", fit_args=[50, y, 0]))
462
+
463
+ # Add TOC links from page 1 to page 2
464
+ for i, word in enumerate(WORDS_10):
465
+ toc_y = toc_y_positions[i]
466
+ list_y = list_y_positions[i]
467
+ link = Link(
468
+ rect=(50, toc_y - 10, 150, toc_y + 10),
469
+ target_page_index=1,
470
+ fit=Fit(fit_type="/XYZ", fit_args=[50, list_y, 0])
471
+ )
472
+ writer.add_annotation(page_number=0, annotation=link)
473
+
474
+ # Save the modified PDF
475
+ with open(pdf_file, "wb") as f:
476
+ writer.write(f)
477
+
478
+ return pdf_file
479
+
480
+ # Streamlit UI
481
+ md_files = [f for f in glob.glob("*.md") if os.path.basename(f) != "README.md"]
482
+ md_options = [os.path.splitext(os.path.basename(f))[0] for f in md_files]
483
+
484
+ with st.sidebar:
485
+ st.markdown("### PDF Options")
486
+ if md_options:
487
+ selected_md = st.selectbox("Select Markdown File", options=md_options, index=0)
488
+ with open(f"{selected_md}.md", "r", encoding="utf-8") as f:
489
+ st.session_state.markdown_content = f.read()
490
+ else:
491
+ st.warning("No markdown file found. Please add one to your folder.")
492
+ selected_md = None
493
+ st.session_state.markdown_content = ""
494
+ available_font_files = {os.path.splitext(os.path.basename(f))[0]: f for f in glob.glob("*.ttf")}
495
+ selected_font_name = st.selectbox("Select Emoji Font", options=list(available_font_files.keys()),
496
+ index=list(available_font_files.keys()).index("NotoEmoji-Bold") if "NotoEmoji-Bold" in available_font_files else 0)
497
+ base_font_size = st.slider("Font Size (points)", min_value=6, max_value=16, value=8, step=1)
498
+ render_with_bold = st.checkbox("Render with Bold Formatting (remove ** markers)", value=True, key="render_with_bold")
499
+ auto_bold_numbers = st.checkbox("Auto Bold Numbered Lines", value=True, key="auto_bold_numbers")
500
+ enlarge_numbered = st.checkbox("Enlarge Font Size for Numbered Lines", value=True, key="enlarge_numbered")
501
+ add_space_before_numbered = st.checkbox("Add Space Ahead of Numbered Lines", value=False, key="add_space_before_numbered")
502
+
503
+ # Here we use a font that has more emojis
504
+ headings_to_fonts = st.checkbox("Headings to Fonts", value=False, key="headings_to_fonts",
505
+ help="Convert Markdown headings (# Heading) and emphasis (*word*) to appropriate font styles")
506
+
507
+ auto_columns = st.checkbox("AutoColumns", value=False, key="auto_columns")
508
+
509
+ if auto_columns and 'markdown_content' in st.session_state:
510
+ current_markdown = st.session_state.markdown_content
511
+ lines = current_markdown.strip().split('\n')
512
+ longest_line_words = 0
513
+ for line in lines:
514
+ if line.strip():
515
+ word_count = len(line.split())
516
+ longest_line_words = max(longest_line_words, word_count)
517
+ if longest_line_words > 25:
518
+ recommended_columns = 1
519
+ elif longest_line_words >= 18:
520
+ recommended_columns = 2
521
+ elif longest_line_words >= 11:
522
+ recommended_columns = 3
523
+ else:
524
+ recommended_columns = "Auto"
525
+ st.info(f"Longest line has {longest_line_words} words. Recommending {recommended_columns} columns.")
526
+ else:
527
+ recommended_columns = "Auto"
528
+
529
+ column_options = ["Auto"] + list(range(1, 7))
530
+ num_columns = st.selectbox("Number of Columns", options=column_options,
531
+ index=0 if recommended_columns == "Auto" else column_options.index(recommended_columns))
532
+ num_columns = 0 if num_columns == "Auto" else int(num_columns)
533
+ st.info("Font size and columns adjust to fit one page.")
534
+
535
+ edited_markdown = st.text_area("Input Markdown", value=st.session_state.markdown_content, height=300, key=f"markdown_{selected_md}_{selected_font_name}_{num_columns}")
536
+
537
+ col1, col2 = st.columns(2)
538
+ with col1:
539
+ if st.button("πŸ”„πŸ“„ Update PDF"):
540
+ st.session_state.markdown_content = edited_markdown
541
+ if selected_md:
542
+ with open(f"{selected_md}.md", "w", encoding="utf-8") as f:
543
+ f.write(edited_markdown)
544
+ st.rerun()
545
+
546
+ with col2:
547
+ if st.button("βœ‚οΈ Trim Emojis"):
548
+ trimmed_content = trim_emojis_except_numbered(edited_markdown)
549
+ st.session_state.markdown_content = trimmed_content
550
+ if selected_md:
551
+ with open(f"{selected_md}.md", "w", encoding="utf-8") as f:
552
+ f.write(trimmed_content)
553
+ st.rerun()
554
+
555
+ prefix = get_timestamp_prefix()
556
+ st.download_button(
557
+ label="πŸ’ΎπŸ“ Save Markdown",
558
+ data=st.session_state.markdown_content,
559
+ file_name=f"{prefix} {selected_md}.md" if selected_md else f"{prefix} default.md",
560
+ mime="text/markdown"
561
+ )
562
+ st.markdown("### Text-to-Speech")
563
+ VOICES = ["en-US-AriaNeural", "en-US-JennyNeural", "en-GB-SoniaNeural", "en-US-GuyNeural", "en-US-AnaNeural"]
564
+ selected_voice = st.selectbox("Select Voice for TTS", options=VOICES, index=0)
565
+ if st.button("Generate Audio"):
566
+ cleaned_text = clean_for_speech(st.session_state.markdown_content)
567
+ audio_filename = f"{prefix} {selected_md} {selected_voice}.mp3" if selected_md else f"{prefix} default {selected_voice}.mp3"
568
+ audio_file = asyncio.run(generate_audio(cleaned_text, selected_voice, audio_filename))
569
+ st.audio(audio_file)
570
+ with open(audio_file, "rb") as f:
571
+ audio_bytes = f.read()
572
+ st.download_button(
573
+ label="πŸ’ΎπŸ”Š Save Audio",
574
+ data=audio_bytes,
575
+ file_name=audio_filename,
576
+ mime="audio/mpeg"
577
+ )
578
+
579
+ if st.button("πŸ“‘ Create CrossFile PDFs"):
580
+ with st.spinner("Creating cross-file linked PDFs..."):
581
+ source_pdf, target_pdf = create_crossfile_pdfs()
582
+ st.success(f"Created {source_pdf} and {target_pdf}")
583
+ for pdf_file in [source_pdf, target_pdf]:
584
+ with open(pdf_file, "rb") as f:
585
+ st.download_button(
586
+ label=f"πŸ’Ύ Download {pdf_file}",
587
+ data=f.read(),
588
+ file_name=pdf_file,
589
+ mime="application/pdf"
590
+ )
591
+
592
+ if st.button("πŸ§ͺ Create SelfLinking PDF"):
593
+ with st.spinner("Generating self-linking PDF with TOC..."):
594
+ pdf_file = create_selflinking_pdf()
595
+ st.success(f"Generated {pdf_file}")
596
+ with open(pdf_file, "rb") as f:
597
+ pdf_bytes = f.read()
598
+ images = pdf_to_image(pdf_bytes)
599
+ if images:
600
+ st.subheader(f"Preview of {pdf_file}")
601
+ for i, img in enumerate(images):
602
+ st.image(img, caption=f"{pdf_file} Page {i+1}", use_container_width=True)
603
+ with open(pdf_file, "rb") as f:
604
+ st.download_button(
605
+ label=f"πŸ’Ύ Download {pdf_file}",
606
+ data=f.read(),
607
+ file_name=pdf_file,
608
+ mime="application/pdf"
609
+ )
610
+
611
+ with st.spinner("Generating PDF..."):
612
+ pdf_bytes = create_pdf(st.session_state.markdown_content, base_font_size, render_with_bold, auto_bold_numbers, enlarge_numbered, num_columns, add_space_before_numbered, headings_to_fonts)
613
+
614
+ with st.container():
615
+ pdf_images = pdf_to_image(pdf_bytes)
616
+ if pdf_images:
617
+ for img in pdf_images:
618
+ st.image(img, use_container_width=True)
619
+ else:
620
+ st.info("Download the PDF to view it locally.")
621
+
622
+ with st.sidebar:
623
+ st.download_button(
624
+ label="πŸ’ΎπŸ“„ Save PDF",
625
+ data=pdf_bytes,
626
+ file_name=f"{prefix} {selected_md}.pdf" if selected_md else f"{prefix} output.pdf",
627
+ mime="application/pdf"
628
+ )