syedkhalid076 commited on
Commit
fdc6283
·
verified ·
1 Parent(s): 8a64dc5

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +471 -38
src/streamlit_app.py CHANGED
@@ -1,40 +1,473 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
4
  import streamlit as st
 
 
 
 
 
 
 
5
 
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import streamlit as st
2
+ import markdown
3
+ from weasyprint import HTML, CSS
4
+ from datetime import datetime
5
+ import base64
6
+ import os
7
+ from io import BytesIO
8
+ import re
9
 
10
+ # Page config
11
+ st.set_page_config(page_title="Markdown to PDF", layout="wide")
12
+
13
+ # Font options
14
+ FONTS = {
15
+ "Open Sans": "Open+Sans:wght@400;600;700",
16
+ "Montserrat": "Montserrat:wght@400;600;700",
17
+ "DM Mono": "DM+Mono:wght@400;500",
18
+ "Anonymous Pro": "Anonymous+Pro:wght@400;700",
19
+ "Inconsolata": "Inconsolata:wght@400;700"
20
+ }
21
+
22
+ MONOSPACE_FONTS = ["DM Mono", "Anonymous Pro", "Inconsolata"]
23
+
24
+ def get_css_template(font_name, spacing="normal", font_size=11):
25
+ """Generate CSS template with selected font and spacing"""
26
+ is_mono = font_name in MONOSPACE_FONTS
27
+ code_font = font_name if is_mono else "DM Mono"
28
+
29
+ google_fonts_url = f"https://fonts.googleapis.com/css2?family={FONTS[font_name]}"
30
+ if not is_mono:
31
+ google_fonts_url += f"&family={FONTS[code_font]}"
32
+
33
+ line_height = 1.8 if spacing == "spacious" else 1.6
34
+ margin_multiplier = 1.2 if spacing == "spacious" else 1.0
35
+
36
+ return f"""
37
+ @import url('{google_fonts_url}');
38
+
39
+ @page {{
40
+ size: A4;
41
+ margin: {2.5 * margin_multiplier}cm {2 * margin_multiplier}cm;
42
+
43
+ @top-left {{
44
+ content: element(header-left);
45
+ vertical-align: middle;
46
+ }}
47
+
48
+ @top-right {{
49
+ content: element(header-right);
50
+ vertical-align: middle;
51
+ text-align: right;
52
+ }}
53
+
54
+ @bottom-center {{
55
+ content: "Page " counter(page) " of " counter(pages);
56
+ font-family: '{font_name}', sans-serif;
57
+ font-size: 9pt;
58
+ color: #666;
59
+ }}
60
+
61
+ @bottom-right {{
62
+ content: "{datetime.now().strftime('%B %d, %Y')}";
63
+ font-family: '{font_name}', sans-serif;
64
+ font-size: 9pt;
65
+ color: #666;
66
+ }}
67
+ }}
68
+
69
+ .header-left {{
70
+ position: running(header-left);
71
+ }}
72
+
73
+ .header-right {{
74
+ position: running(header-right);
75
+ }}
76
+
77
+ .logo-container {{
78
+ max-width: 120px;
79
+ max-height: 40px;
80
+ display: inline-block;
81
+ }}
82
+
83
+ .logo-container img {{
84
+ max-width: 120px;
85
+ max-height: 40px;
86
+ width: auto;
87
+ height: auto;
88
+ display: block;
89
+ }}
90
+
91
+ .title-header {{
92
+ font-family: '{font_name}', sans-serif;
93
+ font-size: 14pt;
94
+ font-weight: 600;
95
+ color: #2c3e50;
96
+ margin: 0;
97
+ padding: 0;
98
+ }}
99
+
100
+ body {{
101
+ font-family: '{font_name}', sans-serif;
102
+ font-size: {font_size}pt;
103
+ line-height: {line_height};
104
+ color: #333;
105
+ margin: 0;
106
+ padding: 0;
107
+ }}
108
+
109
+ h1, h2, h3, h4, h5, h6 {{
110
+ font-family: '{font_name}', sans-serif;
111
+ color: #2c3e50;
112
+ margin-top: {1.5 * margin_multiplier}em;
113
+ margin-bottom: {0.5 * margin_multiplier}em;
114
+ page-break-after: avoid;
115
+ font-weight: 600;
116
+ }}
117
+
118
+ h1 {{ font-size: {font_size * 2}pt; border-bottom: 2px solid #e0e0e0; padding-bottom: 0.3em; }}
119
+ h2 {{ font-size: {font_size * 1.6}pt; border-bottom: 1px solid #e0e0e0; padding-bottom: 0.2em; }}
120
+ h3 {{ font-size: {font_size * 1.3}pt; }}
121
+ h4 {{ font-size: {font_size * 1.1}pt; }}
122
+ h5 {{ font-size: {font_size}pt; }}
123
+ h6 {{ font-size: {font_size * 0.9}pt; color: #666; }}
124
+
125
+ p {{
126
+ margin: {0.8 * margin_multiplier}em 0;
127
+ }}
128
+
129
+ /* List item atomic page breaking */
130
+ ul, ol {{
131
+ margin: {1 * margin_multiplier}em 0;
132
+ padding-left: 2em;
133
+ }}
134
+
135
+ li {{
136
+ margin: {0.5 * margin_multiplier}em 0;
137
+ page-break-inside: avoid;
138
+ break-inside: avoid;
139
+ }}
140
+
141
+ /* Prevent orphaned list items */
142
+ ul, ol {{
143
+ orphans: 3;
144
+ widows: 3;
145
+ }}
146
+
147
+ blockquote {{
148
+ border-left: 4px solid #3498db;
149
+ margin: {1.2 * margin_multiplier}em 0;
150
+ padding: {0.5 * margin_multiplier}em 0 {0.5 * margin_multiplier}em 1em;
151
+ background: #f8f9fa;
152
+ font-style: italic;
153
+ color: #555;
154
+ page-break-inside: avoid;
155
+ }}
156
+
157
+ code {{
158
+ font-family: '{code_font}', monospace;
159
+ font-size: {font_size * 0.9}pt;
160
+ background: #f4f4f4;
161
+ padding: 0.1em 0.3em;
162
+ border-radius: 3px;
163
+ color: #c7254e;
164
+ }}
165
+
166
+ pre {{
167
+ font-family: '{code_font}', monospace;
168
+ font-size: {font_size * 0.85}pt;
169
+ background: #f8f8f8;
170
+ border: 1px solid #ddd;
171
+ border-radius: 4px;
172
+ padding: {1 * margin_multiplier}em;
173
+ overflow-x: auto;
174
+ line-height: 1.4;
175
+ page-break-inside: avoid;
176
+ margin: {1 * margin_multiplier}em 0;
177
+ }}
178
+
179
+ pre code {{
180
+ background: none;
181
+ padding: 0;
182
+ color: #333;
183
+ }}
184
+
185
+ table {{
186
+ border-collapse: collapse;
187
+ width: 100%;
188
+ margin: {1.2 * margin_multiplier}em 0;
189
+ page-break-inside: avoid;
190
+ }}
191
+
192
+ th, td {{
193
+ border: 1px solid #ddd;
194
+ padding: {0.6 * margin_multiplier}em {0.8 * margin_multiplier}em;
195
+ text-align: left;
196
+ }}
197
+
198
+ th {{
199
+ background: #f5f5f5;
200
+ font-weight: 600;
201
+ color: #2c3e50;
202
+ }}
203
+
204
+ tr:nth-child(even) {{
205
+ background: #fafafa;
206
+ }}
207
+
208
+ img {{
209
+ max-width: 100%;
210
+ height: auto;
211
+ display: block;
212
+ margin: {1.2 * margin_multiplier}em 0;
213
+ page-break-inside: avoid;
214
+ }}
215
+
216
+ hr {{
217
+ border: none;
218
+ border-top: 1px solid #ddd;
219
+ margin: {2 * margin_multiplier}em 0;
220
+ }}
221
+
222
+ a {{
223
+ color: #3498db;
224
+ text-decoration: none;
225
+ }}
226
+
227
+ a:hover {{
228
+ text-decoration: underline;
229
+ }}
230
+
231
+ /* Syntax highlighting for code blocks - Pygments style */
232
+ .codehilite .hll {{ background-color: #ffffcc }}
233
+ .codehilite .c {{ color: #008000; font-style: italic }} /* Comment */
234
+ .codehilite .err {{ border: 1px solid #FF0000 }} /* Error */
235
+ .codehilite .k {{ color: #0000ff; font-weight: bold }} /* Keyword */
236
+ .codehilite .o {{ color: #666666 }} /* Operator */
237
+ .codehilite .ch {{ color: #008000; font-style: italic }} /* Comment.Hashbang */
238
+ .codehilite .cm {{ color: #008000; font-style: italic }} /* Comment.Multiline */
239
+ .codehilite .cp {{ color: #0000ff }} /* Comment.Preproc */
240
+ .codehilite .cpf {{ color: #008000; font-style: italic }} /* Comment.PreprocFile */
241
+ .codehilite .c1 {{ color: #008000; font-style: italic }} /* Comment.Single */
242
+ .codehilite .cs {{ color: #008000; font-style: italic }} /* Comment.Special */
243
+ .codehilite .gd {{ color: #A00000 }} /* Generic.Deleted */
244
+ .codehilite .ge {{ font-style: italic }} /* Generic.Emph */
245
+ .codehilite .gr {{ color: #FF0000 }} /* Generic.Error */
246
+ .codehilite .gh {{ color: #000080; font-weight: bold }} /* Generic.Heading */
247
+ .codehilite .gi {{ color: #00A000 }} /* Generic.Inserted */
248
+ .codehilite .go {{ color: #888888 }} /* Generic.Output */
249
+ .codehilite .gp {{ color: #000080; font-weight: bold }} /* Generic.Prompt */
250
+ .codehilite .gs {{ font-weight: bold }} /* Generic.Strong */
251
+ .codehilite .gu {{ color: #800080; font-weight: bold }} /* Generic.Subheading */
252
+ .codehilite .gt {{ color: #0044DD }} /* Generic.Traceback */
253
+ .codehilite .kc {{ color: #0000ff; font-weight: bold }} /* Keyword.Constant */
254
+ .codehilite .kd {{ color: #0000ff; font-weight: bold }} /* Keyword.Declaration */
255
+ .codehilite .kn {{ color: #0000ff; font-weight: bold }} /* Keyword.Namespace */
256
+ .codehilite .kp {{ color: #0000ff }} /* Keyword.Pseudo */
257
+ .codehilite .kr {{ color: #0000ff; font-weight: bold }} /* Keyword.Reserved */
258
+ .codehilite .kt {{ color: #2b91af }} /* Keyword.Type */
259
+ .codehilite .m {{ color: #009999 }} /* Literal.Number */
260
+ .codehilite .s {{ color: #a31515 }} /* Literal.String */
261
+ .codehilite .na {{ color: #FF0000 }} /* Name.Attribute */
262
+ .codehilite .nb {{ color: #0086B3 }} /* Name.Builtin */
263
+ .codehilite .nc {{ color: #2b91af; font-weight: bold }} /* Name.Class */
264
+ .codehilite .no {{ color: #008080 }} /* Name.Constant */
265
+ .codehilite .nd {{ color: #AA22FF }} /* Name.Decorator */
266
+ .codehilite .ni {{ color: #999999; font-weight: bold }} /* Name.Entity */
267
+ .codehilite .ne {{ color: #D2413A; font-weight: bold }} /* Name.Exception */
268
+ .codehilite .nf {{ color: #000000; font-weight: bold }} /* Name.Function */
269
+ .codehilite .nl {{ color: #A0A000 }} /* Name.Label */
270
+ .codehilite .nn {{ color: #0000FF; font-weight: bold }} /* Name.Namespace */
271
+ .codehilite .nt {{ color: #0000ff }} /* Name.Tag */
272
+ .codehilite .nv {{ color: #008080 }} /* Name.Variable */
273
+ .codehilite .ow {{ color: #0000ff; font-weight: bold }} /* Operator.Word */
274
+ .codehilite .w {{ color: #bbbbbb }} /* Text.Whitespace */
275
+ .codehilite .mb {{ color: #009999 }} /* Literal.Number.Bin */
276
+ .codehilite .mf {{ color: #009999 }} /* Literal.Number.Float */
277
+ .codehilite .mh {{ color: #009999 }} /* Literal.Number.Hex */
278
+ .codehilite .mi {{ color: #009999 }} /* Literal.Number.Integer */
279
+ .codehilite .mo {{ color: #009999 }} /* Literal.Number.Oct */
280
+ .codehilite .sa {{ color: #a31515 }} /* Literal.String.Affix */
281
+ .codehilite .sb {{ color: #a31515 }} /* Literal.String.Backtick */
282
+ .codehilite .sc {{ color: #a31515 }} /* Literal.String.Char */
283
+ .codehilite .dl {{ color: #a31515 }} /* Literal.String.Delimiter */
284
+ .codehilite .sd {{ color: #a31515; font-style: italic }} /* Literal.String.Doc */
285
+ .codehilite .s2 {{ color: #a31515 }} /* Literal.String.Double */
286
+ .codehilite .se {{ color: #a31515; font-weight: bold }} /* Literal.String.Escape */
287
+ .codehilite .sh {{ color: #a31515 }} /* Literal.String.Heredoc */
288
+ .codehilite .si {{ color: #a31515 }} /* Literal.String.Interpol */
289
+ .codehilite .sx {{ color: #a31515 }} /* Literal.String.Other */
290
+ .codehilite .sr {{ color: #a31515 }} /* Literal.String.Regex */
291
+ .codehilite .s1 {{ color: #a31515 }} /* Literal.String.Single */
292
+ .codehilite .ss {{ color: #a31515 }} /* Literal.String.Symbol */
293
+ .codehilite .bp {{ color: #0086B3 }} /* Name.Builtin.Pseudo */
294
+ .codehilite .fm {{ color: #000000; font-weight: bold }} /* Name.Function.Magic */
295
+ .codehilite .vc {{ color: #008080 }} /* Name.Variable.Class */
296
+ .codehilite .vg {{ color: #008080 }} /* Name.Variable.Global */
297
+ .codehilite .vi {{ color: #008080 }} /* Name.Variable.Instance */
298
+ .codehilite .vm {{ color: #008080 }} /* Name.Variable.Magic */
299
+ .codehilite .il {{ color: #009999 }} /* Literal.Number.Integer.Long */
300
+ """
301
+
302
+ def generate_html(markdown_text, title, font_name, spacing="normal", font_size=11, logo_path="./logo.png"):
303
+ """Convert Markdown to styled HTML"""
304
+ # Convert Markdown to HTML with extensions
305
+ md = markdown.Markdown(
306
+ extensions=[
307
+ 'extra',
308
+ 'codehilite',
309
+ 'tables',
310
+ 'fenced_code',
311
+ 'nl2br',
312
+ 'sane_lists'
313
+ ],
314
+ extension_configs={
315
+ 'codehilite': {
316
+ 'linenums': False,
317
+ 'guess_lang': True,
318
+ 'css_class': 'codehilite',
319
+ 'use_pygments': True
320
+ }
321
+ }
322
+ )
323
+ body_html = md.convert(markdown_text)
324
+
325
+ # Encode logo as base64 if it exists
326
+ logo_html = ""
327
+ if os.path.exists(logo_path):
328
+ with open(logo_path, 'rb') as f:
329
+ logo_data = base64.b64encode(f.read()).decode()
330
+ ext = logo_path.split('.')[-1]
331
+ logo_html = f'<div class="logo-container"><img src="data:image/{ext};base64,{logo_data}" alt="Logo" /></div>'
332
+
333
+ css = get_css_template(font_name, spacing, font_size)
334
+
335
+ html = f"""
336
+ <!DOCTYPE html>
337
+ <html>
338
+ <head>
339
+ <meta charset="UTF-8">
340
+ <style>{css}</style>
341
+ </head>
342
+ <body>
343
+ <div class="header-left">
344
+ {logo_html}
345
+ </div>
346
+ <div class="header-right">
347
+ <div class="title-header">{title}</div>
348
+ </div>
349
+ {body_html}
350
+ </body>
351
+ </html>
352
+ """
353
+ return html
354
+
355
+ def generate_pdf(html_content):
356
+ """Generate PDF from HTML using WeasyPrint"""
357
+ pdf_buffer = BytesIO()
358
+ HTML(string=html_content).write_pdf(pdf_buffer)
359
+ pdf_buffer.seek(0)
360
+ return pdf_buffer
361
+
362
+ # Streamlit UI
363
+ st.title("📄 Markdown to PDF Converter")
364
+ st.markdown("Convert your Markdown documents into beautifully formatted PDFs")
365
+
366
+ # Sidebar for settings
367
+ with st.sidebar:
368
+ st.header("⚙️ Settings")
369
+
370
+ selected_font = st.selectbox(
371
+ "Font",
372
+ options=list(FONTS.keys()),
373
+ index=0
374
+ )
375
+
376
+ spacing = st.radio(
377
+ "Spacing",
378
+ options=["normal", "spacious"],
379
+ index=0
380
+ )
381
+
382
+ font_size = st.slider(
383
+ "Font Size (pt)",
384
+ min_value=9,
385
+ max_value=14,
386
+ value=11,
387
+ step=1
388
+ )
389
+
390
+ st.divider()
391
+ st.caption("Logo: Place `logo.png` in the app directory")
392
+
393
+ # Main content area
394
+ col1, col2 = st.columns([1, 1])
395
+
396
+ with col1:
397
+ st.subheader("Input")
398
+
399
+ title = st.text_input("Document Title", value="My Document", max_chars=100)
400
+
401
+ # Markdown input tabs
402
+ input_tab1, input_tab2 = st.tabs(["✍️ Write Markdown", "📁 Upload File"])
403
+
404
+ with input_tab1:
405
+ markdown_text = st.text_area(
406
+ "Markdown Content",
407
+ value="# Welcome\n\nThis is a **sample** document.\n\n## Features\n\n- Beautiful typography\n- Professional layout\n- Code highlighting\n\n```python\nprint('Hello, World!')\n```",
408
+ height=400
409
+ )
410
+
411
+ with input_tab2:
412
+ uploaded_file = st.file_uploader("Upload Markdown File", type=['md', 'markdown'])
413
+ if uploaded_file is not None:
414
+ markdown_text = uploaded_file.read().decode('utf-8')
415
+ st.success(f"Loaded {uploaded_file.name}")
416
+
417
+ with col2:
418
+ st.subheader("Preview")
419
+
420
+ # Generate HTML preview
421
+ try:
422
+ preview_html = generate_html(
423
+ markdown_text,
424
+ title,
425
+ selected_font,
426
+ spacing,
427
+ font_size
428
+ )
429
+
430
+ # Display preview
431
+ st.markdown(
432
+ f'<div style="border: 1px solid #ddd; padding: 20px; background: white; max-height: 500px; overflow-y: auto;">{markdown.markdown(markdown_text, extensions=["extra", "tables", "fenced_code"])}</div>',
433
+ unsafe_allow_html=True
434
+ )
435
+ except Exception as e:
436
+ st.error(f"Preview error: {str(e)}")
437
+
438
+ # Generate PDF button
439
+ st.divider()
440
+
441
+ col_btn1, col_btn2, col_btn3 = st.columns([1, 1, 1])
442
+
443
+ with col_btn2:
444
+ if st.button("🎨 Generate PDF", type="primary", use_container_width=True):
445
+ with st.spinner("Generating PDF..."):
446
+ try:
447
+ html_content = generate_html(
448
+ markdown_text,
449
+ title,
450
+ selected_font,
451
+ spacing,
452
+ font_size
453
+ )
454
+ pdf_buffer = generate_pdf(html_content)
455
+
456
+ st.success("✅ PDF generated successfully!")
457
+
458
+ # Download button
459
+ st.download_button(
460
+ label="⬇️ Download PDF",
461
+ data=pdf_buffer,
462
+ file_name=f"{title.replace(' ', '_').lower()}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf",
463
+ mime="application/pdf",
464
+ use_container_width=True
465
+ )
466
+
467
+ except Exception as e:
468
+ st.error(f"Error generating PDF: {str(e)}")
469
+ st.exception(e)
470
+
471
+ # Footer
472
+ # st.divider()
473
+ # st.caption("Built with Streamlit • Supports GitHub-Flavored Markdown • Professional PDF output")