Update app.py
Browse files
app.py
CHANGED
@@ -3,8 +3,11 @@ import os
|
|
3 |
import re
|
4 |
import glob
|
5 |
import textwrap
|
|
|
|
|
6 |
from datetime import datetime
|
7 |
from pathlib import Path
|
|
|
8 |
|
9 |
import streamlit as st
|
10 |
import pandas as pd
|
@@ -12,6 +15,8 @@ from PIL import Image
|
|
12 |
from reportlab.pdfgen import canvas
|
13 |
from reportlab.lib.pagesizes import letter
|
14 |
from reportlab.lib.utils import ImageReader
|
|
|
|
|
15 |
import mistune
|
16 |
from gtts import gTTS
|
17 |
|
@@ -40,7 +45,7 @@ def get_text_input(file_uploader_label, accepted_types, text_area_label):
|
|
40 |
md_text = uploaded_file.getvalue().decode("utf-8")
|
41 |
stem = Path(uploaded_file.name).stem
|
42 |
else:
|
43 |
-
md_text = st.text_area(text_area_label, height=200)
|
44 |
|
45 |
# Convert markdown to plain text for processing
|
46 |
renderer = mistune.HTMLRenderer()
|
@@ -75,15 +80,30 @@ def generate_pdf(text_content, images, pdf_params):
|
|
75 |
Generates a PDF buffer from text and a list of images based on specified parameters.
|
76 |
"""
|
77 |
buf = io.BytesIO()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
78 |
c = canvas.Canvas(buf)
|
79 |
page_w, page_h = letter
|
80 |
margin = 40
|
81 |
gutter = 20
|
82 |
col_w = (page_w - 2 * margin - (pdf_params['columns'] - 1) * gutter) / pdf_params['columns']
|
83 |
-
|
84 |
-
|
|
|
|
|
|
|
85 |
line_height = pdf_params['font_size'] * 1.2
|
86 |
-
|
|
|
|
|
|
|
87 |
|
88 |
y = page_h - margin
|
89 |
col_idx = 0
|
@@ -96,7 +116,7 @@ def generate_pdf(text_content, images, pdf_params):
|
|
96 |
col_idx += 1
|
97 |
if col_idx >= pdf_params['columns']:
|
98 |
c.showPage()
|
99 |
-
c.setFont(
|
100 |
col_idx = 0
|
101 |
y = page_h - margin
|
102 |
|
@@ -108,13 +128,15 @@ def generate_pdf(text_content, images, pdf_params):
|
|
108 |
# --- Render Images ---
|
109 |
for img_file in images:
|
110 |
try:
|
|
|
111 |
img = Image.open(img_file)
|
112 |
w, h = img.size
|
113 |
c.showPage()
|
114 |
c.setPageSize((w, h))
|
115 |
c.drawImage(ImageReader(img), 0, 0, w, h, preserveAspectRatio=True, mask='auto')
|
116 |
except Exception as e:
|
117 |
-
|
|
|
118 |
continue
|
119 |
|
120 |
c.save()
|
@@ -132,24 +154,73 @@ def show_asset_manager():
|
|
132 |
return
|
133 |
|
134 |
for asset_path in assets:
|
135 |
-
|
|
|
|
|
|
|
|
|
136 |
cols = st.columns([3, 1, 1])
|
137 |
-
cols[0].write(asset_path)
|
138 |
|
139 |
try:
|
140 |
with open(asset_path, 'rb') as fp:
|
141 |
file_bytes = fp.read()
|
142 |
|
143 |
-
if ext == 'pdf':
|
144 |
cols[1].download_button("๐ฅ", data=file_bytes, file_name=asset_path, mime="application/pdf", key=f"dl_{asset_path}")
|
145 |
-
elif ext == 'mp3':
|
146 |
cols[1].audio(file_bytes)
|
|
|
|
|
147 |
except Exception as e:
|
148 |
-
cols[1].error("Error
|
149 |
|
150 |
cols[2].button("๐๏ธ", key=f"del_{asset_path}", on_click=delete_asset, args=(asset_path,))
|
151 |
|
152 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
153 |
def render_code_interpreter():
|
154 |
"""Sets up the UI and execution logic for the code interpreter tab."""
|
155 |
st.header("๐งช Python Code Executor & Demo")
|
@@ -161,18 +232,70 @@ def render_code_interpreter():
|
|
161 |
def execute_code(code_str):
|
162 |
output_buffer = io.StringIO()
|
163 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
164 |
with redirect_stdout(output_buffer):
|
165 |
-
exec(code_str,
|
166 |
return output_buffer.getvalue(), None
|
167 |
except Exception as e:
|
168 |
return None, str(e)
|
169 |
|
170 |
# --- Main Logic for the Tab ---
|
171 |
-
DEFAULT_CODE = "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
172 |
if 'code' not in st.session_state:
|
173 |
st.session_state.code = DEFAULT_CODE
|
174 |
|
175 |
-
uploaded_file = st.file_uploader("Upload .py or .md", type=['py', 'md'])
|
176 |
if uploaded_file:
|
177 |
file_content = uploaded_file.getvalue().decode()
|
178 |
if uploaded_file.type == 'text/markdown':
|
@@ -180,19 +303,20 @@ def render_code_interpreter():
|
|
180 |
st.session_state.code = codes[0] if codes else ''
|
181 |
else:
|
182 |
st.session_state.code = file_content
|
183 |
-
|
184 |
-
|
185 |
-
st.session_state.code = st.text_area("๐ป Code Editor", value=st.session_state.code, height=300)
|
186 |
|
187 |
c1, c2 = st.columns(2)
|
188 |
if c1.button("โถ๏ธ Run Code", use_container_width=True):
|
189 |
output, err = execute_code(st.session_state.code)
|
|
|
190 |
if err:
|
191 |
st.error(err)
|
192 |
-
|
193 |
st.code(output, language='text')
|
194 |
-
|
195 |
-
st.success("Executed successfully
|
|
|
196 |
if c2.button("๐๏ธ Clear Code", use_container_width=True):
|
197 |
st.session_state.code = ''
|
198 |
st.rerun()
|
@@ -209,10 +333,18 @@ def main():
|
|
209 |
|
210 |
# --- Sidebar Controls ---
|
211 |
st.sidebar.title("PDF Settings")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
212 |
pdf_params = {
|
213 |
'columns': st.sidebar.slider("Text columns", 1, 3, 1),
|
214 |
-
'font_family': st.sidebar.selectbox("Font",
|
215 |
-
'font_size': st.sidebar.slider("Font size", 6,
|
|
|
216 |
}
|
217 |
|
218 |
# --- Main UI ---
|
@@ -238,28 +370,30 @@ def main():
|
|
238 |
df_imgs = pd.DataFrame([{"name": f.name, "order": i} for i, f in enumerate(uploaded_images)])
|
239 |
edited_df = st.data_editor(df_imgs, use_container_width=True, key="img_order_editor")
|
240 |
|
241 |
-
# Create a map for quick lookup
|
242 |
image_map = {f.name: f for f in uploaded_images}
|
243 |
|
244 |
-
# Sort and append images based on the edited order
|
245 |
for _, row in edited_df.sort_values("order").iterrows():
|
246 |
if row['name'] in image_map:
|
247 |
ordered_images.append(image_map[row['name']])
|
248 |
|
249 |
st.subheader("๐๏ธ PDF Generation")
|
250 |
-
if st.button("Generate PDF
|
251 |
-
|
252 |
-
|
253 |
-
|
254 |
-
|
255 |
-
|
256 |
-
|
257 |
-
|
|
|
|
|
|
|
258 |
|
259 |
show_asset_manager()
|
|
|
260 |
|
261 |
with tab2:
|
262 |
render_code_interpreter()
|
263 |
|
264 |
if __name__ == "__main__":
|
265 |
-
main()
|
|
|
3 |
import re
|
4 |
import glob
|
5 |
import textwrap
|
6 |
+
import base64
|
7 |
+
import sys
|
8 |
from datetime import datetime
|
9 |
from pathlib import Path
|
10 |
+
from contextlib import redirect_stdout
|
11 |
|
12 |
import streamlit as st
|
13 |
import pandas as pd
|
|
|
15 |
from reportlab.pdfgen import canvas
|
16 |
from reportlab.lib.pagesizes import letter
|
17 |
from reportlab.lib.utils import ImageReader
|
18 |
+
from reportlab.pdfbase import pdfmetrics
|
19 |
+
from reportlab.pdfbase.ttfonts import TTFont
|
20 |
import mistune
|
21 |
from gtts import gTTS
|
22 |
|
|
|
45 |
md_text = uploaded_file.getvalue().decode("utf-8")
|
46 |
stem = Path(uploaded_file.name).stem
|
47 |
else:
|
48 |
+
md_text = st.text_area(text_area_label, height=200, value="## Your Markdown Here\n\nEnter your markdown text, or upload a file.")
|
49 |
|
50 |
# Convert markdown to plain text for processing
|
51 |
renderer = mistune.HTMLRenderer()
|
|
|
80 |
Generates a PDF buffer from text and a list of images based on specified parameters.
|
81 |
"""
|
82 |
buf = io.BytesIO()
|
83 |
+
|
84 |
+
# --- Register Custom Fonts ---
|
85 |
+
for font_path in pdf_params.get('ttf_files', []):
|
86 |
+
try:
|
87 |
+
font_name = Path(font_path).stem
|
88 |
+
pdfmetrics.registerFont(TTFont(font_name, font_path))
|
89 |
+
except Exception as e:
|
90 |
+
st.warning(f"Could not register font {font_path}: {e}")
|
91 |
+
|
92 |
c = canvas.Canvas(buf)
|
93 |
page_w, page_h = letter
|
94 |
margin = 40
|
95 |
gutter = 20
|
96 |
col_w = (page_w - 2 * margin - (pdf_params['columns'] - 1) * gutter) / pdf_params['columns']
|
97 |
+
|
98 |
+
# Use registered font name, which is the stem of the file path
|
99 |
+
font_name_to_use = Path(pdf_params['font_family']).stem if ".ttf" in pdf_params['font_family'] else pdf_params['font_family']
|
100 |
+
c.setFont(font_name_to_use, pdf_params['font_size'])
|
101 |
+
|
102 |
line_height = pdf_params['font_size'] * 1.2
|
103 |
+
# Estimate characters per line for wrapping
|
104 |
+
# 0.6 is a common factor for average character width vs font size
|
105 |
+
wrap_width = int(col_w / (pdf_params['font_size'] * 0.6)) if pdf_params['font_size'] > 0 else 50
|
106 |
+
|
107 |
|
108 |
y = page_h - margin
|
109 |
col_idx = 0
|
|
|
116 |
col_idx += 1
|
117 |
if col_idx >= pdf_params['columns']:
|
118 |
c.showPage()
|
119 |
+
c.setFont(font_name_to_use, pdf_params['font_size'])
|
120 |
col_idx = 0
|
121 |
y = page_h - margin
|
122 |
|
|
|
128 |
# --- Render Images ---
|
129 |
for img_file in images:
|
130 |
try:
|
131 |
+
# Handle both file paths and uploaded file objects
|
132 |
img = Image.open(img_file)
|
133 |
w, h = img.size
|
134 |
c.showPage()
|
135 |
c.setPageSize((w, h))
|
136 |
c.drawImage(ImageReader(img), 0, 0, w, h, preserveAspectRatio=True, mask='auto')
|
137 |
except Exception as e:
|
138 |
+
img_name = img_file.name if hasattr(img_file, 'name') else img_file
|
139 |
+
st.warning(f"Could not process image {img_name}: {e}")
|
140 |
continue
|
141 |
|
142 |
c.save()
|
|
|
154 |
return
|
155 |
|
156 |
for asset_path in assets:
|
157 |
+
# Avoid showing the script itself
|
158 |
+
if asset_path.endswith('.py'):
|
159 |
+
continue
|
160 |
+
|
161 |
+
ext = Path(asset_path).suffix.lower()
|
162 |
cols = st.columns([3, 1, 1])
|
163 |
+
cols[0].write(f"`{asset_path}`")
|
164 |
|
165 |
try:
|
166 |
with open(asset_path, 'rb') as fp:
|
167 |
file_bytes = fp.read()
|
168 |
|
169 |
+
if ext == '.pdf':
|
170 |
cols[1].download_button("๐ฅ", data=file_bytes, file_name=asset_path, mime="application/pdf", key=f"dl_{asset_path}")
|
171 |
+
elif ext == '.mp3':
|
172 |
cols[1].audio(file_bytes)
|
173 |
+
elif ext in ['.png', '.jpg', '.jpeg']:
|
174 |
+
cols[1].image(file_bytes, width=60)
|
175 |
except Exception as e:
|
176 |
+
cols[1].error("Error")
|
177 |
|
178 |
cols[2].button("๐๏ธ", key=f"del_{asset_path}", on_click=delete_asset, args=(asset_path,))
|
179 |
|
180 |
+
# ๐งฉ Shows a demo of how to use the functions with file lists
|
181 |
+
def show_batch_processing_demo(pdf_params):
|
182 |
+
"""Finds local files and shows how to process them."""
|
183 |
+
st.markdown("---")
|
184 |
+
st.subheader("๐งฉ Batch Processing Demo")
|
185 |
+
st.info("This section demonstrates how you could call the PDF generation function programmatically with lists of files.")
|
186 |
+
|
187 |
+
md_files = glob.glob("*.md")
|
188 |
+
img_files = glob.glob("*.png") + glob.glob("*.jpg")
|
189 |
+
|
190 |
+
if not md_files or not img_files:
|
191 |
+
st.warning("To run the demo, please ensure there is at least one `.md` file and one image (`.png`/`.jpg`) in the directory.")
|
192 |
+
return
|
193 |
+
|
194 |
+
st.write("Found the following files to use for the demo:")
|
195 |
+
st.write(f"**Markdown file:** `{md_files[0]}`")
|
196 |
+
st.write(f"**Image files:** `{', '.join(img_files)}`")
|
197 |
+
|
198 |
+
if st.button("๐งช Run Demo with Above Files"):
|
199 |
+
md_file_str = md_files[0]
|
200 |
+
img_files_str = ",".join(img_files)
|
201 |
+
|
202 |
+
# --- Example of programmatic execution ---
|
203 |
+
# 1. Read the markdown file
|
204 |
+
with open(md_file_str, 'r') as f:
|
205 |
+
text_content = f.read()
|
206 |
+
|
207 |
+
# 2. Open the image files (generate_pdf expects file-like objects or paths)
|
208 |
+
image_objects = img_files # Pass paths directly
|
209 |
+
|
210 |
+
# 3. Call the generator function
|
211 |
+
pdf_buffer = generate_pdf(text_content, image_objects, pdf_params)
|
212 |
+
|
213 |
+
# 4. Provide for download
|
214 |
+
st.download_button(
|
215 |
+
"โฌ๏ธ Download Batch Demo PDF",
|
216 |
+
data=pdf_buffer,
|
217 |
+
file_name="batch_demo_output.pdf",
|
218 |
+
mime="application/pdf"
|
219 |
+
)
|
220 |
+
st.success("Batch demo PDF generated!")
|
221 |
+
|
222 |
+
|
223 |
+
# ๐ Renders the entire UI and logic for the Python code interpreter.
|
224 |
def render_code_interpreter():
|
225 |
"""Sets up the UI and execution logic for the code interpreter tab."""
|
226 |
st.header("๐งช Python Code Executor & Demo")
|
|
|
232 |
def execute_code(code_str):
|
233 |
output_buffer = io.StringIO()
|
234 |
try:
|
235 |
+
# The exec function will have access to globally imported libraries
|
236 |
+
exec_globals = {
|
237 |
+
"st": st,
|
238 |
+
"glob": glob,
|
239 |
+
"base64": base64,
|
240 |
+
"io": io,
|
241 |
+
"canvas": canvas,
|
242 |
+
"letter": letter
|
243 |
+
}
|
244 |
with redirect_stdout(output_buffer):
|
245 |
+
exec(code_str, exec_globals)
|
246 |
return output_buffer.getvalue(), None
|
247 |
except Exception as e:
|
248 |
return None, str(e)
|
249 |
|
250 |
# --- Main Logic for the Tab ---
|
251 |
+
DEFAULT_CODE = """
|
252 |
+
import streamlit as st
|
253 |
+
import glob
|
254 |
+
import base64
|
255 |
+
import io
|
256 |
+
from reportlab.pdfgen import canvas
|
257 |
+
from reportlab.lib.pagesizes import letter
|
258 |
+
|
259 |
+
st.title("๐ Enhanced Demo App")
|
260 |
+
st.markdown("This demo shows file galleries and base64 PDF downloads.")
|
261 |
+
|
262 |
+
# --- Image Gallery ---
|
263 |
+
with st.expander("๐ผ๏ธ Show Image Files in Directory"):
|
264 |
+
image_files = glob.glob("*.png") + glob.glob("*.jpg")
|
265 |
+
if not image_files:
|
266 |
+
st.write("No image files found.")
|
267 |
+
else:
|
268 |
+
st.image(image_files)
|
269 |
+
|
270 |
+
# --- PDF Gallery ---
|
271 |
+
with st.expander("๐ Show PDF Files in Directory"):
|
272 |
+
pdf_files = glob.glob("*.pdf")
|
273 |
+
if not pdf_files:
|
274 |
+
st.write("No PDF files found.")
|
275 |
+
else:
|
276 |
+
st.write(pdf_files)
|
277 |
+
|
278 |
+
# --- PDF Generation and Download ---
|
279 |
+
if st.button("Generate Demo PDF & Download Link"):
|
280 |
+
# 1. Create PDF in memory
|
281 |
+
buffer = io.BytesIO()
|
282 |
+
p = canvas.Canvas(buffer, pagesize=letter)
|
283 |
+
p.drawString(100, 750, "This is a demo PDF generated from the code interpreter.")
|
284 |
+
p.showPage()
|
285 |
+
p.save()
|
286 |
+
|
287 |
+
# 2. Encode PDF to Base64
|
288 |
+
b64 = base64.b64encode(buffer.getvalue()).decode()
|
289 |
+
|
290 |
+
# 3. Create download link
|
291 |
+
href = f'<a href="data:application/pdf;base64,{b64}" download="demo_from_code.pdf">Download Generated PDF</a>'
|
292 |
+
st.markdown(href, unsafe_allow_html=True)
|
293 |
+
st.success("PDF generated! Click the link above to download.")
|
294 |
+
"""
|
295 |
if 'code' not in st.session_state:
|
296 |
st.session_state.code = DEFAULT_CODE
|
297 |
|
298 |
+
uploaded_file = st.file_uploader("Upload .py or .md", type=['py', 'md'], key="code_uploader")
|
299 |
if uploaded_file:
|
300 |
file_content = uploaded_file.getvalue().decode()
|
301 |
if uploaded_file.type == 'text/markdown':
|
|
|
303 |
st.session_state.code = codes[0] if codes else ''
|
304 |
else:
|
305 |
st.session_state.code = file_content
|
306 |
+
|
307 |
+
st.session_state.code = st.text_area("๐ป Code Editor", value=st.session_state.code, height=400)
|
|
|
308 |
|
309 |
c1, c2 = st.columns(2)
|
310 |
if c1.button("โถ๏ธ Run Code", use_container_width=True):
|
311 |
output, err = execute_code(st.session_state.code)
|
312 |
+
st.subheader("Output")
|
313 |
if err:
|
314 |
st.error(err)
|
315 |
+
if output: # Show output even if empty, to confirm it ran
|
316 |
st.code(output, language='text')
|
317 |
+
if not err:
|
318 |
+
st.success("Executed successfully.")
|
319 |
+
|
320 |
if c2.button("๐๏ธ Clear Code", use_container_width=True):
|
321 |
st.session_state.code = ''
|
322 |
st.rerun()
|
|
|
333 |
|
334 |
# --- Sidebar Controls ---
|
335 |
st.sidebar.title("PDF Settings")
|
336 |
+
|
337 |
+
# --- Dynamic Font Loading ---
|
338 |
+
ttf_files = glob.glob("*.ttf")
|
339 |
+
standard_fonts = ["Helvetica", "Times-Roman", "Courier"]
|
340 |
+
available_fonts = ttf_files + standard_fonts
|
341 |
+
default_font_index = 0 if ttf_files else 0 # Default to first ttf or Helvetica
|
342 |
+
|
343 |
pdf_params = {
|
344 |
'columns': st.sidebar.slider("Text columns", 1, 3, 1),
|
345 |
+
'font_family': st.sidebar.selectbox("Font", available_fonts, index=default_font_index),
|
346 |
+
'font_size': st.sidebar.slider("Font size", 6, 48, 12),
|
347 |
+
'ttf_files': ttf_files
|
348 |
}
|
349 |
|
350 |
# --- Main UI ---
|
|
|
370 |
df_imgs = pd.DataFrame([{"name": f.name, "order": i} for i, f in enumerate(uploaded_images)])
|
371 |
edited_df = st.data_editor(df_imgs, use_container_width=True, key="img_order_editor")
|
372 |
|
|
|
373 |
image_map = {f.name: f for f in uploaded_images}
|
374 |
|
|
|
375 |
for _, row in edited_df.sort_values("order").iterrows():
|
376 |
if row['name'] in image_map:
|
377 |
ordered_images.append(image_map[row['name']])
|
378 |
|
379 |
st.subheader("๐๏ธ PDF Generation")
|
380 |
+
if st.button("Generate PDF from UI"):
|
381 |
+
if not plain_text.strip() and not ordered_images:
|
382 |
+
st.warning("Please provide some text or images to generate a PDF.")
|
383 |
+
else:
|
384 |
+
pdf_buffer = generate_pdf(plain_text, ordered_images, pdf_params)
|
385 |
+
st.download_button(
|
386 |
+
"โฌ๏ธ Download PDF",
|
387 |
+
data=pdf_buffer,
|
388 |
+
file_name=f"{filename_stem}.pdf",
|
389 |
+
mime="application/pdf"
|
390 |
+
)
|
391 |
|
392 |
show_asset_manager()
|
393 |
+
show_batch_processing_demo(pdf_params)
|
394 |
|
395 |
with tab2:
|
396 |
render_code_interpreter()
|
397 |
|
398 |
if __name__ == "__main__":
|
399 |
+
main()
|