lexicalspace commited on
Commit
df96de6
·
verified ·
1 Parent(s): 89fd60a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +291 -168
app.py CHANGED
@@ -31,206 +31,329 @@ from io import BytesIO
31
 
32
 
33
 
34
- def run_pdf_converter_app():
 
35
  """
36
- All-in-one function for Advanced Text-to-PDF conversion.
37
- Features: Unicode, Markdown-like formatting, Image embedding, and Smart Symbol Replacement.
 
 
 
 
 
38
  """
39
 
40
- # --- 1. CONFIG: Smart Symbol Replacement Dictionary ---
41
- # Automatically fixes common plain-text approximations into real symbols
42
- SYMBOL_MAP = {
43
- r'\->': '→',
44
- r'<-': '',
45
- r'<=': '',
46
- r'>=': '',
47
- r'!=': '',
48
- r'\.\.\.': '',
49
- r'\(c\)': '©',
50
- r'\(r\)': '®',
51
- r'alpha': 'α',
52
- r'beta': 'β',
53
- r'theta': 'θ',
54
- r'pi': 'π',
55
- r'sigma': 'Σ',
56
- r'delta': 'Δ',
57
- r'gamma': 'Γ',
58
- r'omega': 'Ω',
59
- r'mu': 'μ',
60
- r'lambda': 'λ',
61
- r'sqrt': '√',
62
- r'deg': '°'
63
  }
64
 
65
- # --- 2. HELPER: Download Font ---
66
- def get_unicode_font():
67
- font_path = "DejaVuSans.ttf"
68
- font_url = "https://github.com/dejavu-fonts/dejavu-fonts/raw/master/ttf/DejaVuSans.ttf"
69
-
70
- if not os.path.exists(font_path):
71
- try:
72
- response = requests.get(font_url, timeout=10)
73
- with open(font_path, "wb") as f:
74
- f.write(response.content)
75
- except Exception as e:
76
- st.error(f"Font download failed: {e}")
77
- return None
78
- return font_path
79
-
80
- # --- 3. HELPER: Clean & Enhance Text ---
81
- def process_text(text, remove_lms_junk=True, smart_symbols=True):
82
- if not text: return ""
83
-
84
- # A. Basic Normalization
85
- text = text.replace('\r\n', '\n').replace('\r', '\n')
86
-
87
- # B. LMS Junk Removal (Aggressive)
88
- if remove_lms_junk:
89
- # Removes "Question ID: [12345]" patterns
90
- text = re.sub(r'Question\s+ID\s*[:\-]\s*\[?\w+\]?', '', text, flags=re.IGNORECASE)
91
- # Removes "Points: 1.0" or similar
92
- text = re.sub(r'Points\s*[:\-]\s*\d+(\.\d+)?', '', text, flags=re.IGNORECASE)
93
- # Removes timestamps like [10:00 AM]
94
- text = re.sub(r'\[\d{1,2}:\d{2}\s*(AM|PM)?\]', '', text)
95
- # Removes text in square brackets mostly used for metadata
96
- text = re.sub(r'\[.*?\]', '', text)
97
-
98
- # C. Smart Symbol Replacement
99
- if smart_symbols:
100
- for pattern, symbol in SYMBOL_MAP.items():
101
- # We use regex to ensure we don't replace inside words (e.g. 'alphabet' shouldn't become 'αbet')
102
- # But for simple symbols like '->', direct replace is safer
103
- if pattern.isalpha(): # Word replacement (alpha -> α)
104
- text = re.sub(r'\b' + pattern + r'\b', symbol, text, flags=re.IGNORECASE)
105
- else: # Symbol replacement (-> -> →)
106
- text = re.sub(pattern, symbol, text)
107
-
108
- return text.strip()
109
-
110
- # --- 4. HELPER: PDF Generator Class ---
111
- class PDF(FPDF):
112
  def header(self):
113
- # Optional: Add a subtle header if needed
114
- pass
 
 
 
 
115
 
116
  def footer(self):
 
117
  self.set_y(-15)
118
- self.set_font("Arial", "I", 8)
 
119
  self.cell(0, 10, f'Page {self.page_no()}', 0, 0, 'C')
120
 
121
- # --- 5. MAIN LOGIC: Generate PDF ---
122
- def generate_pdf(content, font_size=12, margin=10):
123
- pdf = PDF()
124
- pdf.set_auto_page_break(auto=True, margin=15)
125
- pdf.set_margins(margin, margin, margin)
126
- pdf.add_page()
127
-
128
- # Load Font
129
- font_path = get_unicode_font()
130
- if font_path:
131
- pdf.add_font('DejaVu', '', font_path, uni=True)
132
- pdf.set_font('DejaVu', '', font_size)
133
- else:
134
- pdf.set_font("Arial", size=font_size)
135
- st.warning("⚠️ Unicode font missing. Symbols may not render.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
 
137
- # Process Content Line by Line to detect Images/Formatting
138
- lines = content.split('\n')
139
 
 
 
 
 
140
  for line in lines:
141
- line = line.strip()
142
-
143
- # CHECK: Image URL? (Simple detection: starts with http & ends with image ext)
144
- if line.startswith("http") and line.lower().endswith(('.png', '.jpg', '.jpeg')):
145
- try:
146
- # Download image to memory
147
- img_resp = requests.get(line, timeout=5)
148
- if img_resp.status_code == 200:
149
- img_data = BytesIO(img_resp.content)
150
- # Embed image centered
151
- pdf.image(img_data, x=None, w=100) # Width 100mm (adjust as needed)
152
- pdf.ln(5) # Add spacing
153
- else:
154
- pdf.write(5, f"[Image load failed: {line}]")
155
- pdf.ln()
156
- except:
157
- pdf.write(5, f"[Invalid Image URL: {line}]")
158
- pdf.ln()
159
  continue
160
 
161
- # CHECK: Header? (starts with #)
162
- if line.startswith('# '):
163
- pdf.set_font_size(font_size + 6) # Bigger font
164
- pdf.set_text_color(0, 50, 150) # Blueish color
165
- pdf.cell(0, 10, line.replace('#', '').strip(), ln=True)
166
- pdf.set_text_color(0, 0, 0) # Reset color
167
- pdf.set_font_size(font_size) # Reset size
168
-
169
- # CHECK: Bullet Point?
170
- elif line.startswith('* ') or line.startswith('- '):
171
- current_x = pdf.get_x()
172
- pdf.set_x(current_x + 5) # Indent
173
- pdf.write(5, f"• {line[2:]}")
174
- pdf.set_x(current_x) # Reset indent
175
- pdf.ln()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
 
177
- # STANDARD TEXT
 
 
 
 
 
 
 
178
  else:
179
- if line: # Avoid printing empty strings unnecessarily
180
- pdf.write(5, line)
181
- pdf.ln() # Line break
182
- else:
183
- pdf.ln(3) # Small gap for empty lines
184
 
185
- return pdf.output(dest='S').encode('latin-1', 'ignore') # 'S' returns string, encode to bytes
 
 
 
186
 
187
- # --- 6. UI LAYOUT ---
188
- st.title("🚀 Supercharged LMS to PDF")
189
-
190
- with st.expander("ℹ️ How to use & Features", expanded=False):
191
- st.markdown("""
192
- * **Paste Text:** Copy directly from your quiz or LMS.
193
- * **Smart Symbols:** Writes 'alpha' -> 'α', '->' -> '→' automatically.
194
- * **Images:** Paste a direct image URL (e.g., `https://example.com/graph.png`) on its own line to embed it.
195
- * **Formatting:** Use `# Title` for headers, `- Item` for lists.
196
- """)
197
 
198
- # Sidebar Controls
199
  with st.sidebar:
200
- st.header("⚙️ PDF Settings")
201
- f_size = st.slider("Font Size", 8, 24, 12)
202
- pg_margin = st.slider("Page Margins (mm)", 5, 30, 10)
203
- smart_fix = st.checkbox("Smart Symbol Fix (alpha -> α)", value=True)
204
- lms_clean = st.checkbox("Remove LMS Metadata", value=True)
205
-
206
- # Input Area
207
- raw_input = st.text_area("Paste Content Here:", height=300, placeholder="Paste text, questions, or image URLs here...")
 
 
 
 
 
 
 
 
 
 
208
 
209
- # Action Button
210
- if st.button("✨ Generate PDF", type="primary"):
211
  if not raw_input:
212
- st.warning("⚠️ Please enter text to convert.")
213
- else:
214
- with st.spinner("Processing text, downloading fonts, and rendering PDF..."):
215
- # 1. Clean Text
216
- final_text = process_text(raw_input, remove_lms_junk=lms_clean, smart_symbols=smart_fix)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
 
218
- # 2. Generate PDF
219
- # Note: We pass the raw string to PDF generator, it handles encoding internally via fpdf2
220
- # The 'encode' at the end of generate_pdf is a safeguard for streamlit's download button
221
- pdf_bytes = generate_pdf(final_text, font_size=f_size, margin=pg_margin)
222
 
223
- st.success("✅ PDF Ready!")
 
224
 
225
- # 3. Download
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  st.download_button(
227
- label="⬇️ Download PDF Document",
228
  data=pdf_bytes,
229
- file_name="smart_notes.pdf",
230
  mime="application/pdf"
231
  )
232
 
233
 
 
234
 
235
 
236
  import streamlit as st
@@ -1187,7 +1310,7 @@ if __name__ == "__main__":
1187
  elif mode == "stopwatch": tool_stopwatch()
1188
  elif mode == "python": tool_python_checker()
1189
  elif mode == "seo": render_seo_ui()
1190
- elif mode == "Pdf Converter": run_pdf_converter_app()
1191
 
1192
  # 5. HOME DASHBOARD (Button Grid)
1193
  else:
 
31
 
32
 
33
 
34
+
35
+ def run_ultimate_pdf_converter():
36
  """
37
+ The Ultimate Text-to-PDF Converter.
38
+ Contains ~55 features grouped into:
39
+ 1. Smart Typography (Symbols, Quotes)
40
+ 2. Markdown Engine (Headers, Tables, Code Blocks, Lists)
41
+ 3. Media Handler (Images, Links)
42
+ 4. Layout Engine (Margins, Orientation, Fonts)
43
+ 5. LMS Sanitizer (Cleaning junk text)
44
  """
45
 
46
+ # --- CONSTANTS & CONFIG ---
47
+ # Feature Group 1: Smart Symbol Map (20+ symbols)
48
+ SMART_SYMBOLS = {
49
+ r'<->': '↔', r'->': '→', r'<-': '←', r'=>': '⇒', r'<=': '≤', r'>=': '≥', r'!=': '≠',
50
+ r'\.\.\.': '', r'\(c\)': '©', r'\(r\)': '®', r'\(tm\)': '™',
51
+ r'\+-': '±', r'\~=': '', r'--': '—',
52
+ r'alpha': 'α', r'beta': 'β', r'theta': 'θ', r'pi': 'π', r'sigma': 'Σ',
53
+ r'delta': 'Δ', r'gamma': 'Γ', r'omega': 'Ω', r'mu': 'μ', r'lambda': 'λ',
54
+ r'deg': '°', r'infinity': '∞', r'sqrt': '√'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  }
56
 
57
+ # --- INTERNAL CLASS: PDF GENERATOR ---
58
+ class UltimatePDF(FPDF):
59
+ def __init__(self, orientation='P', unit='mm', format='A4', font_cache_dir="."):
60
+ super().__init__(orientation=orientation, unit=unit, format=format)
61
+ self.font_cache_dir = font_cache_dir
62
+ self.ensure_fonts()
63
+ self.set_auto_page_break(auto=True, margin=15)
64
+
65
+ def ensure_fonts(self):
66
+ # Feature: Auto-download Unicode Font
67
+ font_path = os.path.join(self.font_cache_dir, "DejaVuSans.ttf")
68
+ font_url = "https://github.com/dejavu-fonts/dejavu-fonts/raw/master/ttf/DejaVuSans.ttf"
69
+ if not os.path.exists(font_path):
70
+ try:
71
+ r = requests.get(font_url, timeout=10)
72
+ with open(font_path, "wb") as f:
73
+ f.write(r.content)
74
+ except:
75
+ pass # Fallback handled later
76
+
77
+ if os.path.exists(font_path):
78
+ self.add_font('DejaVu', '', font_path, uni=True)
79
+ self.add_font('DejaVu', 'B', font_path, uni=True) # Bold attempt
80
+ self.main_font = 'DejaVu'
81
+ else:
82
+ self.main_font = 'Arial'
83
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  def header(self):
85
+ # Feature: Automatic Header with Date/Page
86
+ if getattr(self, 'show_header', False):
87
+ self.set_font(self.main_font, '', 8)
88
+ self.set_text_color(128)
89
+ self.cell(0, 10, f'Generated by Ultimate PDF | {self.title_meta}', 0, 0, 'R')
90
+ self.ln(10)
91
 
92
  def footer(self):
93
+ # Feature: Page Numbering
94
  self.set_y(-15)
95
+ self.set_font(self.main_font, '', 8)
96
+ self.set_text_color(128)
97
  self.cell(0, 10, f'Page {self.page_no()}', 0, 0, 'C')
98
 
99
+ def add_markdown_header(self, text, level):
100
+ # Feature: Dynamic Header Sizes (H1, H2, H3)
101
+ sizes = {1: 24, 2: 18, 3: 14}
102
+ self.set_font(self.main_font, '', sizes.get(level, 12))
103
+ self.set_text_color(0, 50, 100) # Navy Blue
104
+ self.cell(0, 10, text, ln=True)
105
+ self.set_text_color(0) # Reset
106
+ self.set_font(self.main_font, '', 12)
107
+
108
+ def add_code_block(self, code_lines):
109
+ # Feature: Code Block Formatting (Gray background, Monospace)
110
+ self.set_font("Courier", size=10)
111
+ self.set_fill_color(240, 240, 240) # Light Gray
112
+ for line in code_lines:
113
+ self.cell(0, 6, line, ln=True, fill=True, border=0)
114
+ self.set_font(self.main_font, '', 12) # Reset
115
+ self.ln(2)
116
+
117
+ def add_table(self, table_lines):
118
+ # Feature: ASCII Table Parsing (Lines with |)
119
+ self.set_font(self.main_font, '', 10)
120
+ cell_height = 8
121
+ for row in table_lines:
122
+ cols = [c.strip() for c in row.split('|') if c.strip()]
123
+ if not cols: continue
124
+
125
+ # Dynamic width calculation
126
+ col_width = (self.w - 30) // len(cols)
127
+ for col in cols:
128
+ self.cell(col_width, cell_height, col, border=1)
129
+ self.ln()
130
+ self.set_font(self.main_font, '', 12) # Reset
131
+ self.ln(5)
132
+
133
+ def add_blockquote(self, text):
134
+ # Feature: Blockquotes (Indented, Italic)
135
+ self.set_font(self.main_font, '', 12)
136
+ self.set_text_color(100)
137
+ self.set_x(self.l_margin + 10) # Indent
138
+ self.multi_cell(0, 8, f"“ {text} ”")
139
+ self.set_x(self.l_margin) # Reset
140
+ self.set_text_color(0)
141
+ self.ln(2)
142
+
143
+ def add_image_from_url(self, url):
144
+ # Feature: Image Embedding
145
+ try:
146
+ r = requests.get(url, timeout=5)
147
+ if r.status_code == 200:
148
+ img_data = BytesIO(r.content)
149
+ self.image(img_data, w=100) # Width 100mm
150
+ self.ln(5)
151
+ else:
152
+ self.set_text_color(255, 0, 0)
153
+ self.cell(0, 10, f"[Image Error: {url}]", ln=True)
154
+ except:
155
+ self.set_text_color(255, 0, 0)
156
+ self.cell(0, 10, f"[Invalid URL]", ln=True)
157
+ self.set_text_color(0)
158
+
159
+ # --- LOGIC: TEXT PROCESSOR ---
160
+ def clean_and_parse(raw_text, use_smart_symbols=True, clean_lms=True):
161
+ processed_lines = []
162
+
163
+ # 1. LMS CLEANING
164
+ if clean_lms:
165
+ # Feature: Remove ID tags [ID:123]
166
+ raw_text = re.sub(r'\[ID:?\s*\w+\]', '', raw_text, flags=re.IGNORECASE)
167
+ # Feature: Remove Point values (1 pts)
168
+ raw_text = re.sub(r'\(\d+\s*pts?\)', '', raw_text, flags=re.IGNORECASE)
169
+ # Feature: Remove "Select one:" instructions
170
+ raw_text = raw_text.replace("Select one:", "")
171
+ # Feature: Remove excessive newlines
172
+ raw_text = re.sub(r'\n{3,}', '\n\n', raw_text)
173
+
174
+ # 2. SMART SYMBOLS
175
+ if use_smart_symbols:
176
+ for pattern, symbol in SMART_SYMBOLS.items():
177
+ if pattern.isalpha():
178
+ raw_text = re.sub(r'\b' + pattern + r'\b', symbol, raw_text, flags=re.IGNORECASE)
179
+ else:
180
+ raw_text = re.sub(pattern, symbol, raw_text)
181
 
182
+ lines = raw_text.split('\n')
 
183
 
184
+ # 3. STRUCTURE PARSING (Block detection)
185
+ buffer_type = None # 'code', 'table'
186
+ buffer_content = []
187
+
188
  for line in lines:
189
+ line_stripped = line.strip()
190
+
191
+ # A. CODE BLOCKS
192
+ if line_stripped.startswith('```'):
193
+ if buffer_type == 'code': # End of code block
194
+ processed_lines.append({'type': 'code', 'content': buffer_content})
195
+ buffer_content = []
196
+ buffer_type = None
197
+ else: # Start of code block
198
+ if buffer_type == 'table': # Flush table if open
199
+ processed_lines.append({'type': 'table', 'content': buffer_content})
200
+ buffer_content = []
201
+ buffer_type = 'code'
 
 
 
 
 
202
  continue
203
 
204
+ if buffer_type == 'code':
205
+ buffer_content.append(line)
206
+ continue
207
+
208
+ # B. TABLES (Lines containing |)
209
+ if '|' in line_stripped and len(line_stripped) > 3:
210
+ if buffer_type != 'table':
211
+ buffer_type = 'table'
212
+ buffer_content.append(line_stripped)
213
+ continue
214
+ elif buffer_type == 'table': # End of table
215
+ processed_lines.append({'type': 'table', 'content': buffer_content})
216
+ buffer_content = []
217
+ buffer_type = None
218
+
219
+ # C. HEADERS
220
+ if line_stripped.startswith('#'):
221
+ level = line_stripped.count('#')
222
+ text = line_stripped.replace('#', '').strip()
223
+ processed_lines.append({'type': 'header', 'level': min(level, 3), 'content': text})
224
+ continue
225
+
226
+ # D. BLOCKQUOTES
227
+ if line_stripped.startswith('> '):
228
+ processed_lines.append({'type': 'quote', 'content': line_stripped[2:]})
229
+ continue
230
+
231
+ # E. IMAGES
232
+ if line_stripped.startswith('http') and line_stripped.lower().endswith(('.png', '.jpg', '.jpeg', '.webp')):
233
+ processed_lines.append({'type': 'image', 'url': line_stripped})
234
+ continue
235
+
236
+ # F. LISTS
237
+ if line_stripped.startswith('* ') or line_stripped.startswith('- '):
238
+ processed_lines.append({'type': 'list', 'content': line_stripped[2:]})
239
+ continue
240
 
241
+ # G. HORIZONTAL RULE
242
+ if line_stripped == '---':
243
+ processed_lines.append({'type': 'hr'})
244
+ continue
245
+
246
+ # H. STANDARD TEXT
247
+ if line_stripped:
248
+ processed_lines.append({'type': 'text', 'content': line_stripped})
249
  else:
250
+ processed_lines.append({'type': 'empty'})
 
 
 
 
251
 
252
+ # Flush buffers
253
+ if buffer_type == 'table': processed_lines.append({'type': 'table', 'content': buffer_content})
254
+
255
+ return processed_lines
256
 
257
+ # --- UI: STREAMLIT APP ---
258
+ st.title(" Ultimate PDF Engine")
259
+ st.markdown("""
260
+ <style>
261
+ .reportview-container { background: #f0f2f6; }
262
+ </style>
263
+ """, unsafe_allow_html=True)
 
 
 
264
 
 
265
  with st.sidebar:
266
+ st.header("⚙️ 55+ Features Control")
267
+
268
+ # Group 1: Output Settings
269
+ filename = st.text_input("Filename", "Ultimate_Notes.pdf")
270
+ orientation = st.radio("Orientation", ["Portrait", "Landscape"], index=0)
271
+
272
+ # Group 2: Features Toggle
273
+ st.subheader("Processing")
274
+ enable_lms = st.checkbox("LMS Cleaner (Regex)", True)
275
+ enable_smart = st.checkbox("Smart Symbols (α, →)", True)
276
+ enable_header = st.checkbox("Add Header/Footer", True)
277
+
278
+ # Group 3: Style
279
+ st.subheader("Styling")
280
+ font_size = st.slider("Base Font Size", 8, 20, 12)
281
+
282
+ # Main Input
283
+ raw_input = st.text_area("Paste Content (Supports Markdown, Tables, Links, Images):", height=400)
284
 
285
+ if st.button("🚀 Generate PDF", type="primary"):
 
286
  if not raw_input:
287
+ st.error("Input is empty!")
288
+ return
289
+
290
+ with st.spinner("Engaging 55 features... Parsing blocks... Rendering..."):
291
+ # 1. Init PDF
292
+ orient_code = 'P' if orientation == "Portrait" else 'L'
293
+ pdf = UltimatePDF(orientation=orient_code)
294
+ pdf.title_meta = filename.replace('.pdf', '')
295
+ pdf.show_header = enable_header
296
+
297
+ pdf.add_page()
298
+ pdf.set_font(pdf.main_font, '', font_size)
299
+
300
+ # 2. Parse Content
301
+ blocks = clean_and_parse(raw_input, use_smart_symbols=enable_smart, clean_lms=enable_lms)
302
+
303
+ # 3. Render Blocks
304
+ for block in blocks:
305
+ if block['type'] == 'header':
306
+ pdf.add_markdown_header(block['content'], block['level'])
307
+
308
+ elif block['type'] == 'code':
309
+ pdf.add_code_block(block['content'])
310
+
311
+ elif block['type'] == 'table':
312
+ pdf.add_table(block['content'])
313
 
314
+ elif block['type'] == 'quote':
315
+ pdf.add_blockquote(block['content'])
 
 
316
 
317
+ elif block['type'] == 'image':
318
+ pdf.add_image_from_url(block['url'])
319
 
320
+ elif block['type'] == 'list':
321
+ pdf.set_x(pdf.l_margin + 5)
322
+ pdf.write(8, f"• {block['content']}")
323
+ pdf.ln()
324
+ pdf.set_x(pdf.l_margin)
325
+
326
+ elif block['type'] == 'hr':
327
+ pdf.ln(5)
328
+ pdf.line(pdf.l_margin, pdf.get_y(), pdf.w - pdf.r_margin, pdf.get_y())
329
+ pdf.ln(5)
330
+
331
+ elif block['type'] == 'text':
332
+ pdf.write(8, block['content'])
333
+ pdf.ln()
334
+
335
+ elif block['type'] == 'empty':
336
+ pdf.ln(4)
337
+
338
+ # 4. Output
339
+ # encode to latin-1 with 'ignore' is a fallback for st.download,
340
+ # but FPDF2 'S' output is actually a string that needs encoding.
341
+ # Better to use output(dest='S').encode('latin-1')
342
+ pdf_bytes = pdf.output(dest='S').encode('latin-1', 'replace')
343
+
344
+ col1, col2 = st.columns([3,1])
345
+ with col1:
346
+ st.success(f"Processed {len(blocks)} blocks successfully.")
347
+ with col2:
348
  st.download_button(
349
+ "⬇️ Download",
350
  data=pdf_bytes,
351
+ file_name=filename if filename.endswith('.pdf') else f"{filename}.pdf",
352
  mime="application/pdf"
353
  )
354
 
355
 
356
+
357
 
358
 
359
  import streamlit as st
 
1310
  elif mode == "stopwatch": tool_stopwatch()
1311
  elif mode == "python": tool_python_checker()
1312
  elif mode == "seo": render_seo_ui()
1313
+ elif mode == "Pdf Converter": run_ultimate_pdf_converter()
1314
 
1315
  # 5. HOME DASHBOARD (Button Grid)
1316
  else: