awacke1 commited on
Commit
067c431
·
verified ·
1 Parent(s): 6f19ed2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +139 -742
app.py CHANGED
@@ -1,768 +1,165 @@
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
- # 🌟 1. Set the stage for a wide, welcoming app - wisdom: A broad canvas invites creativity, like a galaxy awaiting stars!
26
- st.set_page_config(layout="wide", initial_sidebar_state="expanded")
27
-
28
- # Functions
29
- # 2. Timestamp generator - wisdom: Time stamps your work like a cosmic signature, grounding chaos in order!
30
- def get_timestamp_prefix():
31
- central = pytz.timezone("US/Central")
32
- now = datetime.now(central)
33
- return now.strftime("%a %m%d %I%M%p").upper()
34
-
35
- # 🧹 3. Text cleaner for speech - wisdom: Strip away emojis to let words sing clear, like a bard sans distractions!
36
- def clean_for_speech(text):
37
- text = text.replace("#", "")
38
- emoji_pattern = re.compile(
39
- r"[\U0001F300-\U0001F5FF"
40
- r"\U0001F600-\U0001F64F"
41
- r"\U0001F680-\U0001F6FF"
42
- r"\U0001F700-\U0001F77F"
43
- r"\U0001F780-\U0001F7FF"
44
- r"\U0001F800-\U0001F8FF"
45
- r"\U0001F900-\U0001F9FF"
46
- r"\U0001FA00-\U0001FA6F"
47
- r"\U0001FA70-\U0001FAFF"
48
- r"\u2600-\u26FF"
49
- r"\u2700-\u27BF]+", flags=re.UNICODE)
50
- text = emoji_pattern.sub('', text)
51
- return text
52
-
53
- # ✂️ 4. Emoji trimmer - wisdom: Keep numbered lines sacred; prune emojis elsewhere to focus the tale!
54
- def trim_emojis_except_numbered(markdown_text):
55
- emoji_pattern = re.compile(
56
- r"[\U0001F300-\U0001F5FF"
57
- r"\U0001F600-\U0001F64F"
58
- r"\U0001F680-\U0001F6FF"
59
- r"\U0001F700-\U0001F77F"
60
- r"\U0001F780-\U0001F7FF"
61
- r"\U0001F800-\U0001F8FF"
62
- r"\U0001F900-\U0001F9FF"
63
- r"\U0001FAD0-\U0001FAD9"
64
- r"\U0001FA00-\U0001FA6F"
65
- r"\U0001FA70-\U0001FAFF"
66
- r"\u2600-\u26FF"
67
- r"\u2700-\u27BF]+"
 
 
 
 
 
 
 
68
  )
69
- number_pattern = re.compile(r'^\d+\.\s')
70
- lines = markdown_text.strip().split('\n')
71
- processed_lines = []
72
-
73
- for line in lines:
74
- if number_pattern.match(line):
75
- processed_lines.append(line)
76
- else:
77
- processed_lines.append(emoji_pattern.sub('', line))
78
-
79
- return '\n'.join(processed_lines)
80
 
81
- # 🎙️ 5. Audio generator - wisdom: Give voice to text, like a storyteller breathing life into ancient scrolls!
82
- async def generate_audio(text, voice, filename):
83
- communicate = edge_tts.Communicate(text, voice)
84
- await communicate.save(filename)
85
- return filename
86
 
87
- # 🔗 6. Link converter - wisdom: Transform raw URLs into clickable paths, guiding readers like stars in the night!
88
- def detect_and_convert_links(text):
89
- md_link_pattern = re.compile(r'\[(.*?)\]\((https?://[^\s\[\]()<>{}]+)\)')
90
- text = md_link_pattern.sub(r'<a href="\2" color="blue">\1</a>', text)
91
- url_pattern = re.compile(
92
- r'(?<!href=")(https?://[^\s<>{}]+)',
93
- re.IGNORECASE
94
- )
95
- text = url_pattern.sub(r'<a href="\1" color="blue">\1</a>', text)
96
- return text
97
 
98
- # 😊 7. Emoji font applicator - wisdom: Dress emojis in bold fonts, letting them shine like jewels in a crown!
99
- def apply_emoji_font(text, emoji_font):
100
- tag_pattern = re.compile(r'(<[^>]+>)')
101
- segments = tag_pattern.split(text)
102
- result = []
103
- emoji_pattern = re.compile(
104
- r"([\U0001F300-\U0001F5FF"
105
- r"\U0001F600-\U0001F64F"
106
- r"\U0001F680-\U0001F6FF"
107
- r"\U0001F700-\U0001F77F"
108
- r"\U0001F780-\U0001F7FF"
109
- r"\U0001F800-\U0001F8FF"
110
- r"\U0001F900-\U0001F9FF"
111
- r"\U0001FAD0-\U0001FAD9"
112
- r"\U0001FA00-\U0001FA6F"
113
- r"\U0001FA70-\U0001FAFF"
114
- r"\u2600-\u26FF"
115
- r"\u2700-\u27BF]+)"
116
- )
117
-
118
- def replace_emoji(match):
119
- emoji = match.group(1)
120
- emoji = unicodedata.normalize('NFC', emoji)
121
- return f'<font face="{emoji_font}">{emoji}</font>'
122
-
123
- for segment in segments:
124
- if tag_pattern.match(segment):
125
- result.append(segment)
126
- else:
127
- parts = []
128
- last_pos = 0
129
- for match in emoji_pattern.finditer(segment):
130
- start, end = match.span()
131
- if last_pos < start:
132
- parts.append(f'<font face="DejaVuSans">{segment[last_pos:start]}</font>')
133
- parts.append(replace_emoji(match))
134
- last_pos = end
135
- if last_pos < len(segment):
136
- parts.append(f'<font face="DejaVuSans">{segment[last_pos:]}</font>')
137
- result.append(''.join(parts))
138
-
139
- return ''.join(result)
140
 
141
- # 📝 8. Markdown to PDF content - wisdom: Parse markdown like a sage, crafting content that flows like a river!
142
- def markdown_to_pdf_content(markdown_text, add_space_before_numbered, headings_to_fonts):
143
- lines = markdown_text.strip().split('\n')
144
- pdf_content = []
145
- number_pattern = re.compile(r'^\d+(\.\d+)*\.\s')
146
- heading_pattern = re.compile(r'^(#{1,4})\s+(.+)$')
147
- first_numbered_seen = False
148
-
149
- for line in lines:
150
- line = line.strip()
151
- if not line:
152
- continue
153
-
154
- if headings_to_fonts and line.startswith('#'):
155
- heading_match = heading_pattern.match(line)
156
- if heading_match:
157
- level = len(heading_match.group(1))
158
- heading_text = heading_match.group(2).strip()
159
- formatted_heading = f"<h{level}>{heading_text}</h{level}>"
160
- pdf_content.append(formatted_heading)
161
- continue
162
-
163
- is_numbered_line = number_pattern.match(line) is not None
164
-
165
- if add_space_before_numbered and is_numbered_line:
166
- if first_numbered_seen and not line.startswith("1."):
167
- pdf_content.append("")
168
- if not first_numbered_seen:
169
- first_numbered_seen = True
170
-
171
- line = detect_and_convert_links(line)
172
- line = re.sub(r'\*\*(.+?)\*\*', r'<b>\1</b>', line)
173
- line = re.sub(r'\*([^*]+?)\*', r'<b>\1</b>', line)
174
-
175
- pdf_content.append(line)
176
- total_lines = len(pdf_content)
177
- return pdf_content, total_lines
178
 
179
- # 📄 9. PDF creator - wisdom: Weave text into PDFs, like an alchemist turning words into eternal pages!
180
- def create_pdf(markdown_text, base_font_size, num_columns, add_space_before_numbered, headings_to_fonts, doc_title, longest_line_words, total_lines):
181
- if not markdown_text.strip():
182
- return None
183
  buffer = io.BytesIO()
184
- page_width = A4[0] * 2
185
- page_height = A4[1]
186
- doc = SimpleDocTemplate(
187
- buffer,
188
- pagesize=(page_width, page_height),
189
- leftMargin=36,
190
- rightMargin=36,
191
- topMargin=36,
192
- bottomMargin=36,
193
- title=doc_title
194
- )
195
- styles = getSampleStyleSheet()
196
- spacer_height = 10
197
- pdf_content, total_lines = markdown_to_pdf_content(markdown_text, add_space_before_numbered, headings_to_fonts)
198
- try:
199
- available_font_files = glob.glob("*.ttf")
200
- if not available_font_files:
201
- st.error("No .ttf font files found.")
202
- return None
203
- selected_font_path = next((f for f in available_font_files if "NotoEmoji-Bold" in f), None)
204
- if selected_font_path:
205
- pdfmetrics.registerFont(TTFont("NotoEmoji-Bold", selected_font_path))
206
- pdfmetrics.registerFont(TTFont("DejaVuSans", "DejaVuSans.ttf"))
207
- except Exception as e:
208
- st.error(f"Font registration error: {e}")
209
- return None
210
- total_chars = sum(len(line) for line in pdf_content)
211
- hierarchy_weight = sum(1.5 if line.startswith("<b>") else 1 for line in pdf_content)
212
- content_density = total_lines * hierarchy_weight + total_chars / 50
213
- usable_height = page_height - 72 - spacer_height
214
- usable_width = page_width - 72
215
- avg_line_chars = total_chars / total_lines if total_lines > 0 else 50
216
- ideal_lines_per_col = 20
217
- suggested_columns = max(2, min(4, int(total_lines / ideal_lines_per_col) + 1))
218
- num_columns = num_columns if num_columns != 0 else suggested_columns
219
- col_width = usable_width / num_columns
220
- min_font_size = 5
221
- max_font_size = 16
222
- lines_per_col = total_lines / num_columns if num_columns > 0 else total_lines
223
- target_height_per_line = usable_height / lines_per_col if lines_per_col > 0 else usable_height
224
- estimated_font_size = int(target_height_per_line / 1.5)
225
- adjusted_font_size = max(min_font_size, min(max_font_size, estimated_font_size))
226
- if avg_line_chars > col_width / adjusted_font_size * 10:
227
- adjusted_font_size = int(col_width / (avg_line_chars / 10))
228
- adjusted_font_size = max(min_font_size, adjusted_font_size)
229
-
230
- if longest_line_words > 17 or lines_per_col > 20:
231
- font_scale = min(17 / max(longest_line_words, 17), 60 / max(lines_per_col, 20))
232
- adjusted_font_size = max(min_font_size, int(base_font_size * font_scale))
233
-
234
- item_style = ParagraphStyle(
235
- 'ItemStyle', parent=styles['Normal'], fontName="DejaVuSans",
236
- fontSize=adjusted_font_size, leading=adjusted_font_size * 1.15, spaceAfter=1,
237
- linkUnderline=True
238
- )
239
- numbered_bold_style = ParagraphStyle(
240
- 'NumberedBoldStyle', parent=styles['Normal'], fontName="NotoEmoji-Bold",
241
- fontSize=adjusted_font_size, leading=adjusted_font_size * 1.15, spaceAfter=1,
242
- linkUnderline=True
243
- )
244
- section_style = ParagraphStyle(
245
- 'SectionStyle', parent=styles['Heading2'], fontName="DejaVuSans",
246
- textColor=colors.darkblue, fontSize=adjusted_font_size * 1.1, leading=adjusted_font_size * 1.32, spaceAfter=2,
247
- linkUnderline=True
248
- )
249
- columns = [[] for _ in range(num_columns)]
250
- lines_per_column = total_lines / num_columns if num_columns > 0 else total_lines
251
- current_line_count = 0
252
- current_column = 0
253
- number_pattern = re.compile(r'^\d+(\.\d+)*\.\s')
254
- for item in pdf_content:
255
- if current_line_count >= lines_per_column and current_column < num_columns - 1:
256
- current_column += 1
257
- current_line_count = 0
258
- columns[current_column].append(item)
259
- current_line_count += 1
260
- column_cells = [[] for _ in range(num_columns)]
261
- for col_idx, column in enumerate(columns):
262
- for item in column:
263
- if isinstance(item, str):
264
- heading_match = re.match(r'<h(\d)>(.*?)</h\1>', item) if headings_to_fonts else None
265
- if heading_match:
266
- level = int(heading_match.group(1))
267
- heading_text = heading_match.group(2)
268
- heading_style = ParagraphStyle(
269
- f'Heading{level}Style',
270
- parent=styles['Heading1'],
271
- fontName="DejaVuSans",
272
- textColor=colors.darkblue if level == 1 else (colors.black if level > 2 else colors.blue),
273
- fontSize=adjusted_font_size * (1.6 - (level-1)*0.15),
274
- leading=adjusted_font_size * (1.8 - (level-1)*0.15),
275
- spaceAfter=4 - (level-1),
276
- spaceBefore=6 - (level-1),
277
- linkUnderline=True
278
- )
279
- column_cells[col_idx].append(Paragraph(apply_emoji_font(heading_text, "NotoEmoji-Bold"), heading_style))
280
- elif item.startswith("<b>") and item.endswith("</b>"):
281
- content = item[3:-4].strip()
282
- if number_pattern.match(content):
283
- column_cells[col_idx].append(Paragraph(apply_emoji_font(content, "NotoEmoji-Bold"), numbered_bold_style))
284
- else:
285
- column_cells[col_idx].append(Paragraph(apply_emoji_font(content, "NotoEmoji-Bold"), section_style))
286
- else:
287
- column_cells[col_idx].append(Paragraph(apply_emoji_font(item, "NotoEmoji-Bold"), item_style))
288
- else:
289
- column_cells[col_idx].append(Paragraph(apply_emoji_font(str(item), "NotoEmoji-Bold"), item_style))
290
- max_cells = max(len(cells) for cells in column_cells) if column_cells else 0
291
- for cells in column_cells:
292
- cells.extend([Paragraph("", item_style)] * (max_cells - len(cells)))
293
- table_data = list(zip(*column_cells)) if column_cells else [[]]
294
- table = Table(table_data, colWidths=[col_width] * num_columns, hAlign='CENTER')
295
- table.setStyle(TableStyle([
296
- ('VALIGN', (0, 0), (-1, -1), 'TOP'),
297
- ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
298
- ('BACKGROUND', (0, 0), (-1, -1), colors.white),
299
- ('GRID', (0, 0), (-1, -1), 0, colors.white),
300
- ('LINEAFTER', (0, 0), (num_columns-1, -1), 0.5, colors.grey),
301
- ('LEFTPADDING', (0, 0), (-1, -1), 2),
302
- ('RIGHTPADDING', (0, 0), (-1, -1), 2),
303
- ('TOPPADDING', (0, 0), (-1, -1), 1),
304
- ('BOTTOMPADDING', (0, 0), (-1, -1), 1),
305
- ]))
306
- story = [Spacer(1, spacer_height), table]
307
- doc.build(story)
308
- buffer.seek(0)
309
- return buffer.getvalue()
310
-
311
- # 🖼️ 10. PDF to image converter - wisdom: Turn PDFs into images, like painting a story for eager eyes!
312
- def pdf_to_image(pdf_bytes):
313
- if pdf_bytes is None:
314
- return None
315
- try:
316
- doc = fitz.open(stream=pdf_bytes, filetype="pdf")
317
- images = []
318
- for page in doc:
319
- pix = page.get_pixmap(matrix=fitz.Matrix(2.0, 2.0))
320
- img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
321
- images.append(img)
322
- doc.close()
323
- return images
324
- except Exception as e:
325
- st.error(f"Failed to render PDF preview: {e}")
326
- return None
327
-
328
- # PDF creation and linking functions
329
- WORDS_12 = ["one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve"]
330
- WORDS_24 = ["one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten",
331
- "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen", "twenty",
332
- "twenty-one", "twenty-two", "twenty-three", "twenty-four"]
333
 
334
- # 🔗 11. Cross-file PDF linker - wisdom: Connect PDFs like bridges between islands, uniting stories!
335
- def create_crossfile_pdfs(source_pdf="TestSource.pdf", target_pdf="TestTarget.pdf"):
336
- """Create two PDFs with cross-file linking."""
337
- # 📜 11.1 Base PDF creator - wisdom: Lay the foundation with words, like carving runes on stone!
338
- def create_base_pdf(filename):
339
- buffer = io.BytesIO()
340
- c = canvas.Canvas(buffer)
341
- c.setFont("Helvetica", 12)
342
- for i, word in enumerate(WORDS_12, 1):
343
- y = 800 - (i * 20)
344
- c.drawString(50, y, f"{i}. {word}")
345
- c.showPage()
346
- c.save()
347
- buffer.seek(0)
348
- with open(filename, "wb") as f:
349
- f.write(buffer.getvalue())
350
- buffer.close()
351
 
352
- # 📑 11.2 Bookmark adder - wisdom: Mark the path to seven, like a beacon in the fog!
353
- def add_bookmark_to_seven(pdf_file):
354
- reader = PdfReader(pdf_file)
355
- writer = PdfWriter()
356
- for page in reader.pages:
357
- writer.add_page(page)
358
- page = writer.pages[0]
359
- y_position = 800 - (7 * 20)
360
- fit = Fit(fit_type="/XYZ", fit_args=[50, y_position, 0])
361
- writer.add_outline_item("Seven Bookmark", 0, fit=fit)
362
- with open(pdf_file, "wb") as f:
363
- writer.write(f)
364
 
365
- # 🌉 11.3 Source PDF modifier - wisdom: Forge links between files, like threads in a tapestry!
366
- def modify_source_pdf(source, target):
367
- reader = PdfReader(source)
368
- writer = PdfWriter()
369
- for page in reader.pages:
370
- writer.add_page(page)
371
- buffer = io.BytesIO()
372
- c = canvas.Canvas(buffer)
373
- c.setFont("Helvetica", 8)
374
- seven_y = 800 - (7 * 20)
375
- c.drawString(90, seven_y - 5, "link")
376
- c.showPage()
377
- c.save()
378
- buffer.seek(0)
379
- text_pdf = PdfReader(buffer)
380
- page = writer.pages[0]
381
- page.merge_page(text_pdf.pages[0])
382
- link = Link(
383
- rect=(90, seven_y - 10, 150, seven_y + 10),
384
- url=f"file://{os.path.abspath(target)}#page=1"
385
  )
386
- writer.add_annotation(page_number=0, annotation=link)
387
- with open(source, "wb") as f:
388
- writer.write(f)
389
- buffer.close()
390
 
391
- # 🛤️ 11.4 Internal link adder - wisdom: Guide readers within, like a map to hidden treasure!
392
- def add_internal_link(pdf_file):
393
- reader = PdfReader(pdf_file)
394
- writer = PdfWriter()
395
- for page in reader.pages:
396
- writer.add_page(page)
397
- one_y = 800 - (1 * 20)
398
- ten_y = 800 - (10 * 20)
399
- link = Link(
400
- rect=(50, one_y - 10, 100, one_y + 10),
401
- target_page_index=0,
402
- fit=Fit(fit_type="/XYZ", fit_args=[50, ten_y, 0])
403
- )
404
- writer.add_annotation(page_number=0, annotation=link)
405
- with open(pdf_file, "wb") as f:
406
- writer.write(f)
407
-
408
- create_base_pdf(source_pdf)
409
- create_base_pdf(target_pdf)
410
- add_bookmark_to_seven(target_pdf)
411
- modify_source_pdf(source, target)
412
- add_internal_link(source_pdf)
413
- add_internal_link(target_pdf)
414
- return source_pdf, target_pdf
415
-
416
- # 📘 12. Self-linking PDF creator - wisdom: Build a PDF that guides itself, like a book with its own compass!
417
- def create_selflinking_pdf(pdf_file="SelfLinking.pdf"):
418
- """Create a PDF with a TOC on page 1 linking to a 1-24 list starting on page 2."""
419
- buffer = io.BytesIO()
420
- c = canvas.Canvas(buffer)
421
- c.setFont("Helvetica", 14)
422
- c.drawString(50, 800, "Table of Contents")
423
- c.setFont("Helvetica", 12)
424
- toc_y_positions = []
425
- for i, word in enumerate(WORDS_12, 1):
426
- y = 760 - (i * 20)
427
- c.drawString(50, y, f"{word}")
428
- toc_y_positions.append(y)
429
- c.showPage()
430
- c.setFont("Helvetica", 12)
431
- list_y_positions = []
432
- for i, word in enumerate(WORDS_24, 1):
433
- y = 800 - (i * 20)
434
- c.drawString(50, y, f"{i}. {word}")
435
- list_y_positions.append(y)
436
  c.showPage()
437
  c.save()
438
  buffer.seek(0)
439
- with open(pdf_file, "wb") as f:
440
- f.write(buffer.getvalue())
441
- buffer.close()
442
- reader = PdfReader(pdf_file)
443
- writer = PdfWriter()
444
- for page in reader.pages:
445
- writer.add_page(page)
446
- toc_page = writer.pages[0]
447
- list_page = writer.pages[1]
448
- writer.add_outline_item("Table of Contents", 0, fit=Fit(fit_type="/Fit"))
449
- for i, word in enumerate(WORDS_12, 1):
450
- y = list_y_positions[i-1]
451
- writer.add_outline_item(word, 1, fit=Fit(fit_type="/XYZ", fit_args=[50, y, 0]))
452
- for i, word in enumerate(WORDS_12):
453
- toc_y = toc_y_positions[i]
454
- list_y = list_y_positions[i]
455
- link = Link(
456
- rect=(50, toc_y - 10, 150, toc_y + 10),
457
- target_page_index=1,
458
- fit=Fit(fit_type="/XYZ", fit_args=[50, list_y, 0])
459
- )
460
- writer.add_annotation(page_number=0, annotation=link)
461
- with open(pdf_file, "wb") as f:
462
- writer.write(f)
463
- return pdf_file
464
-
465
- # 🖼️ 13. Image-linked PDF creator - wisdom: Link images to text, like windows opening to new worlds!
466
- def create_pdf_with_images(source_pdf_bytes, output_pdf="ImageLinked.pdf"):
467
- """Create a PDF with links on numbered headings to new pages with images."""
468
- image_files = sorted(glob.glob("*.png"))
469
- if not source_pdf_bytes:
470
- st.error("No source PDF provided.")
471
- return None
472
- if not image_files:
473
- st.error("No PNG images found in the directory.")
474
- return source_pdf_bytes
475
-
476
- reader = PdfReader(io.BytesIO(source_pdf_bytes))
477
- writer = PdfWriter()
478
-
479
- # Copy all pages from source PDF
480
- original_page_count = len(reader.pages)
481
- for page in reader.pages:
482
- writer.add_page(page)
483
-
484
- # Add image pages
485
- image_page_indices = []
486
- for image_file in image_files[:12]: # Limit to 12 images
487
- buffer = io.BytesIO()
488
- c = canvas.Canvas(buffer, pagesize=A4)
489
- try:
490
- img = Image.open(image_file)
491
- img_width, img_height = img.size
492
- page_width, page_height = A4
493
- scale = min((page_width - 40) / img_width, (page_height - 40) / img_height)
494
- new_width = img_width * scale
495
- new_height = img_height * scale
496
- x = (page_width - new_width) / 2
497
- y = (page_height - new_height) / 2
498
- c.drawImage(image_file, x, y, new_width, new_height)
499
- c.showPage()
500
- c.save()
501
- buffer.seek(0)
502
- img_pdf = PdfReader(buffer)
503
- writer.add_page(img_pdf.pages[0])
504
- image_page_indices.append(original_page_count + len(image_page_indices))
505
- buffer.close()
506
- except Exception as e:
507
- st.error(f"Failed to process image {image_file}: {e}")
508
- buffer.close()
509
- continue
510
-
511
- # Add links to numbered headings on first page
512
- if image_page_indices:
513
- page = writer.pages[0]
514
- y_positions = []
515
- for i in range(1, 13):
516
- y = 800 - (i * 20) # Matches layout from create_pdf
517
- y_positions.append(y)
518
-
519
- for idx, (y, target_page_idx) in enumerate(zip(y_positions, image_page_indices)):
520
- # Add "link" text
521
- buffer = io.BytesIO()
522
- c = canvas.Canvas(buffer)
523
- c.setFont("Helvetica", 8)
524
- c.drawString(90, y - 5, "link")
525
- c.showPage()
526
- c.save()
527
- buffer.seek(0)
528
- text_pdf = PdfReader(buffer)
529
- page.merge_page(text_pdf.pages[0])
530
-
531
- # Add link annotation
532
- link = Link(
533
- rect=(90, y - 10, 150, y + 10),
534
- target_page_index=target_page_idx,
535
- fit=Fit(fit_type="/Fit")
536
- )
537
- writer.add_annotation(page_number=0, annotation=link)
538
- buffer.close()
539
-
540
- output_buffer = io.BytesIO()
541
- writer.write(output_buffer)
542
- output_buffer.seek(0)
543
- with open(output_pdf, "wb") as f:
544
- f.write(output_buffer.getvalue())
545
- return output_buffer.getvalue()
546
 
547
- # 🎨 14. Streamlit UI - wisdom: Craft an interface like a garden, where users bloom with possibilities!
548
- md_files = [f for f in glob.glob("*.md") if os.path.basename(f) != "README.md"]
549
- md_options = [os.path.splitext(os.path.basename(f))[0] for f in md_files]
550
 
551
- with st.sidebar:
552
- # 📚 14.1 Markdown selector - wisdom: Offer choices like a librarian, guiding users to their story!
553
- st.markdown("### 📄 PDF Options")
554
- if md_options:
555
- selected_md = st.selectbox("Select Markdown File", options=md_options, index=0, key="markdown_select")
556
- if selected_md != st.session_state.get('last_selected_md'):
557
- with open(f"{selected_md}.md", "r", encoding="utf-8") as f:
558
- st.session_state.markdown_content = f.read()
559
- st.session_state.last_selected_md = selected_md
560
- else:
561
- st.warning("No markdown file found. Please add one to your folder.")
562
- selected_md = None
563
- st.session_state.markdown_content = ""
564
-
565
- # 🔠 14.2 Font selector - wisdom: Choose fonts like a painter picks colors, shaping the mood!
566
- available_font_files = {os.path.splitext(os.path.basename(f))[0]: f for f in glob.glob("*.ttf")}
567
- selected_font_name = st.selectbox(
568
- "Select Emoji Font",
569
- options=list(available_font_files.keys()),
570
- index=list(available_font_files.keys()).index("NotoEmoji-Bold") if "NotoEmoji-Bold" in available_font_files else 0
571
- )
572
- base_font_size = st.slider("Font Size (points)", min_value=6, max_value=16, value=8, step=1)
573
-
574
- # 📏 14.3 Layout options - wisdom: Space and style are the frame of your masterpiece!
575
- add_space_before_numbered = st.checkbox("Add Space Ahead of Numbered Lines", value=True)
576
- headings_to_fonts = st.checkbox(
577
- "Headings to Fonts",
578
- value=True,
579
- help="Convert Markdown headings (# Heading) to styled fonts"
580
- )
581
- auto_columns = st.checkbox("AutoColumns", value=True)
582
-
583
- # 📊 14.4 Document stats - wisdom: Know your text’s pulse, like a doctor checking its heartbeat!
584
- column_options = [2, 3, 4]
585
- num_columns = 3
586
- recommended_columns = 3
587
- longest_line_words = 0
588
- total_lines = 0
589
- adjusted_font_size_display = base_font_size
590
- if 'markdown_content' in st.session_state and st.session_state.markdown_content.strip():
591
- current_markdown = st.session_state.markdown_content
592
- lines = current_markdown.strip().split('\n')
593
- total_lines = len([line for line in lines if line.strip()])
594
- for line in lines:
595
- if line.strip():
596
- word_count = len(line.split())
597
- longest_line_words = max(longest_line_words, word_count)
598
- if auto_columns:
599
- if longest_line_words > 38:
600
- recommended_columns = 2
601
- elif longest_line_words < 18 and total_lines < 20:
602
- recommended_columns = 4
603
- else:
604
- recommended_columns = 3
605
- if longest_line_words > 17 or total_lines / max(recommended_columns, 1) > 20:
606
- font_scale = min(17 / max(longest_line_words, 17), 60 / max(total_lines / max(recommended_columns, 1), 20))
607
- adjusted_font_size_display = max(5, int(base_font_size * font_scale))
608
- st.markdown("**Document Stats**")
609
- st.write(f"- Longest Line: {longest_line_words} words")
610
- st.write(f"- Total Lines: {total_lines}")
611
- st.write(f"- Recommended Columns: {recommended_columns}")
612
- st.write(f"- Adjusted Font Size: {adjusted_font_size_display} points")
613
  else:
614
- st.markdown("**Document Stats**")
615
- st.write("- Longest Line: 0 words")
616
- st.write("- Total Lines: 0")
617
- st.write("- Recommended Columns: 3")
618
- st.write(f"- Adjusted Font Size: {base_font_size} points")
619
-
620
- # 🔢 14.5 Column selector - wisdom: Columns organize chaos, like shelves in a wizard’s library!
621
- num_columns = st.selectbox(
622
- "Number of Columns",
623
- options=column_options,
624
- index=column_options.index(recommended_columns) if recommended_columns in column_options else 0
625
- )
626
- st.info("Font size and columns adjust to fit one page.")
627
-
628
- # ✍️ 14.6 Markdown editor - wisdom: Let users scribe their saga, shaping worlds with words!
629
- st.markdown("### ✍️ Edit Markdown")
630
- edited_markdown = st.text_area(
631
- "Input Markdown",
632
- value=st.session_state.markdown_content,
633
- height=200,
634
- key=f"markdown_{selected_md}_{selected_font_name}_{num_columns}"
635
- )
636
-
637
- # 💾 14.7 Action buttons - wisdom: Actions spark progress, like flint igniting a fire!
638
- st.markdown("### 💾 Actions")
639
- col1, col2 = st.columns(2)
640
- with col1:
641
- if st.button("🔄 Update PDF"):
642
- st.session_state.markdown_content = edited_markdown
643
- if selected_md:
644
- with open(f"{selected_md}.md", "w", encoding="utf-8") as f:
645
- f.write(edited_markdown)
646
- st.rerun()
647
-
648
- with col2:
649
- if st.button("✂️ Trim Emojis"):
650
- trimmed_content = trim_emojis_except_numbered(edited_markdown)
651
- st.session_state.markdown_content = trimmed_content
652
- if selected_md:
653
- with open(f"{selected_md}.md", "w", encoding="utf-8") as f:
654
- f.write(trimmed_content)
655
- st.rerun()
656
-
657
- # 📥 14.8 Markdown saver - wisdom: Save your work, like bottling a potion for later!
658
- prefix = get_timestamp_prefix()
659
- st.download_button(
660
- label="💾 Save Markdown",
661
- data=st.session_state.markdown_content,
662
- file_name=f"{prefix} {selected_md}.md" if selected_md else f"{prefix} default.md",
663
- mime="text/markdown"
664
- )
665
-
666
- # 🔊 14.9 Text-to-speech - wisdom: Let words echo aloud, like a bard’s tale in the hall!
667
- st.markdown("### 🔊 Text-to-Speech")
668
- VOICES = ["en-US-AriaNeural", "en-US-JennyNeural", "en-GB-SoniaNeural", "en-US-GuyNeural", "en-US-AnaNeural"]
669
- selected_voice = st.selectbox("Select Voice for TTS", options=VOICES, index=0)
670
- if st.button("Generate Audio"):
671
- cleaned_text = clean_for_speech(st.session_state.markdown_content)
672
- audio_filename = f"{prefix} {selected_md} {selected_voice}.mp3" if selected_md else f"{prefix} default {selected_voice}.mp3"
673
- audio_file = asyncio.run(generate_audio(cleaned_text, selected_voice, audio_filename))
674
- st.audio(audio_file)
675
- with open(audio_file, "rb") as f:
676
- audio_bytes = f.read()
677
- st.download_button(
678
- label="💾 Save Audio",
679
- data=audio_bytes,
680
- file_name=audio_filename,
681
- mime="audio/mpeg"
682
- )
683
-
684
- # 📑 14.10 PDF action buttons - wisdom: Create and link PDFs, like crafting portals to knowledge!
685
- col1, col2 = st.columns(2)
686
- with col1:
687
- if st.button("📑 Create CrossFile PDFs"):
688
- with st.spinner("Creating cross-file linked PDFs..."):
689
- source_pdf, target_pdf = create_crossfile_pdfs()
690
- st.success(f"Created {source_pdf} and {target_pdf}")
691
- for pdf_file in [source_pdf, target_pdf]:
692
- with open(pdf_file, "rb") as f:
693
- st.download_button(
694
- label=f"💾 Download {pdf_file}",
695
- data=f.read(),
696
- file_name=pdf_file,
697
- mime="application/pdf"
698
- )
699
-
700
- with col2:
701
- if st.button("🧪 Create SelfLinking PDF"):
702
- with st.spinner("Generating self-linking PDF with TOC..."):
703
- pdf_file = create_selflinking_pdf()
704
- st.success(f"Generated {pdf_file}")
705
- with open(pdf_file, "rb") as f:
706
- self_linked_pdf_bytes = f.read()
707
- images = pdf_to_image(self_linked_pdf_bytes)
708
- if images:
709
- st.subheader(f"Preview of {pdf_file}")
710
- for i, img in enumerate(images):
711
- st.image(img, caption=f"{pdf_file} Page {i+1}", use_container_width=True)
712
- with open(pdf_file, "rb") as f:
713
- st.download_button(
714
- label=f"💾 Download {pdf_file}",
715
- data=f.read(),
716
- file_name=pdf_file,
717
- mime="application/pdf"
718
- )
719
 
720
- # 🖥️ 15. Main PDF generation - wisdom: Spin up the PDF like a weaver at the loom, crafting beauty!
721
- with st.spinner("Generating PDF..."):
722
- pdf_bytes = create_pdf(
723
- st.session_state.get('markdown_content', ''),
724
- base_font_size,
725
- num_columns,
726
- add_space_before_numbered,
727
- headings_to_fonts,
728
- doc_title=selected_md if selected_md else "Untitled",
729
- longest_line_words=longest_line_words,
730
- total_lines=total_lines
731
- )
732
 
733
- # 🖼️ 16. PDF preview - wisdom: Show the masterpiece before it’s framed, delighting the creator!
734
- with st.container():
735
- st.markdown("### 📊 PDF Preview")
736
- pdf_images = pdf_to_image(pdf_bytes)
737
- if pdf_images:
738
- for img in pdf_images:
739
- st.image(img, use_container_width=True)
740
- else:
741
- st.info("Download the PDF to view it locally.")
742
 
743
- with st.sidebar:
744
- # 💾 17. PDF saver - wisdom: Offer the final scroll, ready to be shared like wisdom across ages!
745
- st.download_button(
746
- label="💾 Save PDF",
747
- data=pdf_bytes if pdf_bytes else "",
748
- file_name=f"{prefix} {selected_md}.pdf" if selected_md else f"{prefix} output.pdf",
749
- mime="application/pdf",
750
- disabled=pdf_bytes is None
751
- )
752
-
753
- if st.button("🖼️ Generate PDF With Images"):
754
- with st.spinner("Generating PDF with image links..."):
755
- linked_pdf_bytes = create_pdf_with_images(pdf_bytes)
756
- if linked_pdf_bytes and linked_pdf_bytes != pdf_bytes:
757
- st.success("Generated PDF with image links")
758
- images = pdf_to_image(linked_pdf_bytes)
759
- if images:
760
- st.subheader("Preview of Image-Linked PDF")
761
- for i, img in enumerate(images):
762
- st.image(img, caption=f"Image-Linked PDF Page {i+1}", use_container_width=True)
763
- st.download_button(
764
- label="💾 Download Image-Linked PDF",
765
- data=linked_pdf_bytes,
766
- file_name=f"{prefix} {selected_md}_image_linked.pdf" if selected_md else f"{prefix} image_linked.pdf",
767
- mime="application/pdf"
768
- )
 
1
  import io
 
2
  import os
3
+ import math
4
+ import re
5
+ from collections import Counter
6
+ from datetime import datetime
7
+
8
+ import pandas as pd
9
  import streamlit as st
10
  from PIL import Image
 
 
 
 
 
 
 
 
11
  from reportlab.pdfgen import canvas
12
+ from reportlab.lib.units import inch
13
+ from reportlab.lib.utils import ImageReader
14
+
15
+ # --- App Configuration ----------------------------------
16
+ st.set_page_config(
17
+ page_title="Image → PDF Comic Layout",
18
+ layout="wide",
19
+ initial_sidebar_state="expanded",
20
+ )
21
+
22
+ st.title("🖼️ Image PDF Comic-Book Layout Generator")
23
+ st.markdown(
24
+ "Upload images, choose a page aspect ratio, reorder panels, and generate a high‑definition PDF with smart naming."
25
+ )
26
+
27
+
28
+ # --- Sidebar: Page Settings -----------------------------
29
+ st.sidebar.header("1️⃣ Page Aspect Ratio & Size")
30
+ ratio_map = {
31
+ "4:3 (Landscape)": (4, 3),
32
+ "16:9 (Landscape)": (16, 9),
33
+ "1:1 (Square)": (1, 1),
34
+ "2:3 (Portrait)": (2, 3),
35
+ "9:16 (Portrait)": (9, 16),
36
+ }
37
+ ratio_choice = st.sidebar.selectbox(
38
+ "Preset Ratio", list(ratio_map.keys()) + ["Custom…"]
39
+ )
40
+ if ratio_choice != "Custom…":
41
+ rw, rh = ratio_map[ratio_choice]
42
+ else:
43
+ rw = st.sidebar.number_input("Custom Width Ratio", min_value=1, value=4)
44
+ rh = st.sidebar.number_input("Custom Height Ratio", min_value=1, value=3)
45
+
46
+ # Base page width in points (1pt = 1/72 inch)
47
+ BASE_WIDTH_PT = st.sidebar.slider(
48
+ "Base Page Width (pt)", min_value=400, max_value=1200, value=800, step=100
49
+ )
50
+ page_width = BASE_WIDTH_PT
51
+ page_height = int(BASE_WIDTH_PT * (rh / rw))
52
+
53
+ st.sidebar.markdown(f"**Page size:** {page_width}×{page_height} pt")
54
+
55
+
56
+ # --- Main: Upload & Reorder -----------------------------
57
+ st.header("2️⃣ Upload & Reorder Images")
58
+ uploaded_files = st.file_uploader(
59
+ "📂 Select PNG/JPG images", type=["png", "jpg", "jpeg"], accept_multiple_files=True
60
+ )
61
+
62
+ # Build ordering table
63
+ if uploaded_files:
64
+ df = pd.DataFrame({"filename": [f.name for f in uploaded_files]})
65
+ st.markdown("Drag to reorder panels below:")
66
+ ordered = st.experimental_data_editor(
67
+ df, num_rows="fixed", use_container_width=True
68
  )
69
+ # Map back to actual file objects in new order
70
+ name2file = {f.name: f for f in uploaded_files}
71
+ ordered_files = [name2file[n] for n in ordered["filename"] if n in name2file]
72
+ else:
73
+ ordered_files = []
 
 
 
 
 
 
74
 
 
 
 
 
 
75
 
76
+ # --- PDF Creation Logic ----------------------------------
 
 
 
 
 
 
 
 
 
77
 
78
+ def top_n_words(filenames, n=5):
79
+ words = []
80
+ for fn in filenames:
81
+ stem = os.path.splitext(fn)[0]
82
+ words += re.findall(r"\w+", stem.lower())
83
+ return [w for w, _ in Counter(words).most_common(n)]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
 
86
+ def make_comic_pdf(images, w_pt, h_pt):
 
 
 
87
  buffer = io.BytesIO()
88
+ c = canvas.Canvas(buffer, pagesize=(w_pt, h_pt))
89
+
90
+ N = len(images)
91
+ cols = int(math.ceil(math.sqrt(N)))
92
+ rows = int(math.ceil(N / cols))
93
+ panel_w = w_pt / cols
94
+ panel_h = h_pt / rows
95
+
96
+ for idx, img_file in enumerate(images):
97
+ im = Image.open(img_file)
98
+ iw, ih = im.size
99
+ target_ar = panel_w / panel_h
100
+ img_ar = iw / ih
101
+
102
+ # Center-crop to panel aspect
103
+ if img_ar > target_ar:
104
+ new_w = int(ih * target_ar)
105
+ left = (iw - new_w) // 2
106
+ im = im.crop((left, 0, left + new_w, ih))
107
+ else:
108
+ new_h = int(iw / target_ar)
109
+ top = (ih - new_h) // 2
110
+ im = im.crop((0, top, iw, top + new_h))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
+ im = im.resize((int(panel_w), int(panel_h)), Image.LANCZOS)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
 
114
+ col = idx % cols
115
+ row = idx // cols
116
+ x = col * panel_w
117
+ y = h_pt - (row + 1) * panel_h
 
 
 
 
 
 
 
 
118
 
119
+ c.drawImage(
120
+ ImageReader(im), x, y, panel_w, panel_h,
121
+ preserveAspectRatio=False, mask='auto'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  )
 
 
 
 
123
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  c.showPage()
125
  c.save()
126
  buffer.seek(0)
127
+ return buffer.getvalue()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
 
 
 
129
 
130
+ # --- Generate & Download -------------------------------
131
+ st.header("3️⃣ Generate & Download PDF")
132
+ if st.button("🎉 Generate PDF"):
133
+ if not ordered_files:
134
+ st.warning("Please upload and order at least one image.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  else:
136
+ # Build filename: YYYY-MMdd-top5words.pdf
137
+ date_str = datetime.now().strftime("%Y-%m%d")
138
+ words = top_n_words([f.name for f in ordered_files], n=5)
139
+ slug = "-".join(words)
140
+ out_name = f"{date_str}-{slug}.pdf"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
 
142
+ pdf_bytes = make_comic_pdf(ordered_files, page_width, page_height)
 
 
 
 
 
 
 
 
 
 
 
143
 
144
+ st.success(f"✅ PDF ready: **{out_name}**")
145
+ st.download_button(
146
+ "⬇️ Download PDF", data=pdf_bytes,
147
+ file_name=out_name, mime="application/pdf"
148
+ )
 
 
 
 
149
 
150
+ # Preview first page (requires pymupdf)
151
+ st.markdown("#### PDF Preview")
152
+ try:
153
+ import fitz # pymupdf
154
+ doc = fitz.open(stream=pdf_bytes, filetype="pdf")
155
+ pix = doc[0].get_pixmap(matrix=fitz.Matrix(1.5, 1.5))
156
+ st.image(pix.tobytes(), use_column_width=True)
157
+ except Exception:
158
+ st.info("Install `pymupdf` for live PDF preview.")
159
+
160
+
161
+ # --- Footer ------------------------------------------------
162
+ st.sidebar.markdown("---")
163
+ st.sidebar.markdown(
164
+ "Built by Aaron C. Wacker • Senior AI Engineer"
165
+ )