Update src/streamlit_app.py
Browse files- src/streamlit_app.py +147 -38
src/streamlit_app.py
CHANGED
@@ -1,40 +1,149 @@
|
|
1 |
-
import altair as alt
|
2 |
-
import numpy as np
|
3 |
-
import pandas as pd
|
4 |
import streamlit as st
|
|
|
|
|
|
|
|
|
|
|
5 |
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
"""
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
"
|
28 |
-
"
|
29 |
-
|
30 |
-
"
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
.
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import streamlit as st
|
2 |
+
from pathlib import Path
|
3 |
+
import base64
|
4 |
+
import datetime
|
5 |
+
import markdown2
|
6 |
+
from weasyprint import HTML, CSS
|
7 |
|
8 |
+
# --- Configuration & Setup ---
|
9 |
+
|
10 |
+
# Define the layouts based on the specification.
|
11 |
+
# The 'size' key uses CSS-compatible dimensions.
|
12 |
+
LAYOUTS = {
|
13 |
+
"A4 Portrait": {"size": "210mm 297mm", "icon": "π"},
|
14 |
+
"A4 Landscape": {"size": "297mm 210mm", "icon": "π"},
|
15 |
+
"Letter Portrait": {"size": "8.5in 11in", "icon": "π"},
|
16 |
+
"Letter Landscape": {"size": "11in 8.5in", "icon": "π"},
|
17 |
+
"Wide 16:9": {"aspect_ratio": "16/9", "icon": "πΊ"},
|
18 |
+
"Vertical 9:16": {"aspect_ratio": "9/16", "icon": "π±"},
|
19 |
+
"Square 1:1": {"aspect_ratio": "1/1", "icon": "πΌοΈ"},
|
20 |
+
}
|
21 |
+
|
22 |
+
# Directory to save the generated PDFs
|
23 |
+
OUTPUT_DIR = Path("generated_pdfs")
|
24 |
+
OUTPUT_DIR.mkdir(exist_ok=True)
|
25 |
+
|
26 |
+
# --- Helper Functions ---
|
27 |
+
|
28 |
+
def get_file_download_link(file_path: Path) -> str:
|
29 |
+
"""Generates a base64-encoded download link for a file."""
|
30 |
+
with open(file_path, "rb") as f:
|
31 |
+
data = base64.b64encode(f.read()).decode()
|
32 |
+
return f'<a href="data:application/octet-stream;base64,{data}" download="{file_path.name}">Download</a>'
|
33 |
+
|
34 |
+
def display_file_explorer():
|
35 |
+
"""Renders a simple file explorer in the Streamlit app."""
|
36 |
+
st.header("π File Explorer")
|
37 |
+
|
38 |
+
# Display Source Markdown files
|
39 |
+
st.subheader("Source Markdown Files (.md)")
|
40 |
+
md_files = list(Path(".").glob("*.md"))
|
41 |
+
if not md_files:
|
42 |
+
st.info("No Markdown files found in the current directory. Create a `.md` file to begin.")
|
43 |
+
else:
|
44 |
+
for md_file in md_files:
|
45 |
+
col1, col2 = st.columns([0.8, 0.2])
|
46 |
+
with col1:
|
47 |
+
st.write(f"π `{md_file.name}`")
|
48 |
+
with col2:
|
49 |
+
st.markdown(get_file_download_link(md_file), unsafe_allow_html=True)
|
50 |
+
|
51 |
+
# Display Generated PDF files
|
52 |
+
st.subheader("Generated PDF Files")
|
53 |
+
pdf_files = sorted(list(OUTPUT_DIR.glob("*.pdf")), reverse=True)
|
54 |
+
if not pdf_files:
|
55 |
+
st.info("No PDFs generated yet. Click the button above.")
|
56 |
+
else:
|
57 |
+
for pdf_file in pdf_files:
|
58 |
+
col1, col2 = st.columns([0.8, 0.2])
|
59 |
+
with col1:
|
60 |
+
st.write(f"π `{pdf_file.name}`")
|
61 |
+
with col2:
|
62 |
+
st.markdown(get_file_download_link(pdf_file), unsafe_allow_html=True)
|
63 |
+
|
64 |
+
def generate_pdf_from_markdown(md_path: Path):
|
65 |
+
"""
|
66 |
+
Reads a markdown file and generates PDFs for all defined layouts.
|
67 |
+
"""
|
68 |
+
try:
|
69 |
+
md_content = md_path.read_text(encoding="utf-8")
|
70 |
+
html_content = markdown2.markdown(md_content, extras=["tables", "fenced-code-blocks", "cuddled-lists"])
|
71 |
+
|
72 |
+
# Basic styling for the PDF
|
73 |
+
base_css = """
|
74 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap');
|
75 |
+
body { font-family: 'Inter', sans-serif; line-height: 1.6; }
|
76 |
+
h1, h2, h3 { font-weight: 700; }
|
77 |
+
code {
|
78 |
+
background-color: #f0f0f0;
|
79 |
+
padding: 2px 4px;
|
80 |
+
border-radius: 3px;
|
81 |
+
font-family: monospace;
|
82 |
+
}
|
83 |
+
pre { background-color: #f0f0f0; padding: 1em; border-radius: 5px; overflow: auto; }
|
84 |
+
table { border-collapse: collapse; width: 100%; }
|
85 |
+
th, td { border: 1px solid #ddd; padding: 8px; }
|
86 |
+
th { background-color: #f2f2f2; }
|
87 |
+
"""
|
88 |
+
|
89 |
+
date_str = datetime.datetime.now().strftime("%Y-%m-%d")
|
90 |
+
|
91 |
+
for name, properties in LAYOUTS.items():
|
92 |
+
st.write(f" - Generating `{name}` format...")
|
93 |
+
|
94 |
+
page_css = f"@page {{ size: {properties.get('size', 'A4')}; margin: 2cm; }}"
|
95 |
+
if 'aspect_ratio' in properties:
|
96 |
+
# For aspect ratio, we fix width and calculate height. This is an approximation.
|
97 |
+
# A more robust solution might require more complex CSS.
|
98 |
+
page_css = f"@page {{ size: 210mm calc(210mm * {properties['aspect_ratio']}); margin: 1cm; }}"
|
99 |
+
|
100 |
+
final_css = CSS(string=base_css + page_css)
|
101 |
+
|
102 |
+
output_filename = f"{md_path.stem}_{name.replace(' ', '-')}_{date_str}.pdf"
|
103 |
+
output_path = OUTPUT_DIR / output_filename
|
104 |
+
|
105 |
+
HTML(string=html_content).write_pdf(output_path, stylesheets=[final_css])
|
106 |
+
|
107 |
+
except Exception as e:
|
108 |
+
st.error(f"Failed to process {md_path.name}: {e}")
|
109 |
+
|
110 |
+
|
111 |
+
# --- Streamlit App UI ---
|
112 |
+
|
113 |
+
st.set_page_config(layout="wide", page_title="PDF Generator")
|
114 |
+
|
115 |
+
st.title("π Markdown to PDF Generator")
|
116 |
+
st.markdown("This tool finds all `.md` files in this directory, converts them to PDF in various layouts, and provides download links.")
|
117 |
+
|
118 |
+
# Create a sample markdown file if none exists
|
119 |
+
if not list(Path(".").glob("*.md")):
|
120 |
+
with open("sample.md", "w", encoding="utf-8") as f:
|
121 |
+
f.write("# Sample Document\n\n")
|
122 |
+
f.write("This is a sample markdown file created for you. You can edit this file or add your own `.md` files to this directory.\n\n")
|
123 |
+
f.write("- Item 1\n- Item 2\n\n")
|
124 |
+
f.write("`code snippet`\n\n")
|
125 |
+
f.write("Click the button below to start the PDF generation process.")
|
126 |
+
st.rerun()
|
127 |
+
|
128 |
+
|
129 |
+
if st.button("π Generate PDFs from all Markdown Files", type="primary"):
|
130 |
+
markdown_files = list(Path(".").glob("*.md"))
|
131 |
+
|
132 |
+
if not markdown_files:
|
133 |
+
st.warning("No `.md` files found. Please add a markdown file to the directory.")
|
134 |
+
else:
|
135 |
+
with st.spinner("Generating PDFs... This may take a moment."):
|
136 |
+
progress_bar = st.progress(0)
|
137 |
+
total_steps = len(markdown_files)
|
138 |
+
|
139 |
+
for i, md_file in enumerate(markdown_files):
|
140 |
+
st.info(f"Processing: **{md_file.name}**")
|
141 |
+
generate_pdf_from_markdown(md_file)
|
142 |
+
progress_bar.progress((i + 1) / total_steps)
|
143 |
+
|
144 |
+
st.success("β
PDF generation complete!")
|
145 |
+
# Use st.rerun() to immediately refresh the file explorer
|
146 |
+
st.rerun()
|
147 |
+
|
148 |
+
# Display the file explorer section
|
149 |
+
display_file_explorer()
|