Update app.py
Browse files
app.py
CHANGED
@@ -10,7 +10,7 @@ import streamlit as st
|
|
10 |
import pandas as pd
|
11 |
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
|
@@ -19,50 +19,224 @@ from gtts import gTTS
|
|
19 |
st.set_page_config(page_title="PDF & Code Interpreter", layout="wide", page_icon="🚀")
|
20 |
|
21 |
def delete_asset(path):
|
|
|
22 |
try:
|
23 |
os.remove(path)
|
|
|
|
|
|
|
24 |
except Exception as e:
|
25 |
st.error(f"Error deleting file: {e}")
|
26 |
st.rerun()
|
27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
28 |
# Tabs setup
|
29 |
tab1, tab2 = st.tabs(["📄 PDF Composer", "🧪 Code Interpreter"])
|
30 |
|
31 |
with tab1:
|
32 |
st.header("📄 PDF Composer & Voice Generator 🚀")
|
33 |
|
34 |
-
# Sidebar PDF
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
|
|
|
|
|
|
|
|
41 |
if md_file:
|
42 |
md_text = md_file.getvalue().decode("utf-8")
|
43 |
-
stem
|
|
|
44 |
else:
|
45 |
-
md_text = st.text_area("Or enter markdown text directly", height=200)
|
46 |
-
|
47 |
|
48 |
-
# Convert Markdown to plain text
|
49 |
-
# Using mistune to parse markdown to HTML, then stripping HTML tags
|
50 |
renderer = mistune.HTMLRenderer()
|
51 |
markdown = mistune.create_markdown(renderer=renderer)
|
52 |
html = markdown(md_text or "")
|
53 |
-
|
54 |
|
55 |
-
# Voice settings
|
|
|
56 |
languages = {"English (US)": "en", "English (UK)": "en-uk", "Spanish": "es"}
|
57 |
voice_choice = st.selectbox("Voice Language", list(languages.keys()))
|
58 |
voice_lang = languages[voice_choice]
|
59 |
slow = st.checkbox("Slow Speech")
|
60 |
|
61 |
if st.button("🔊 Generate & Download Voice MP3 from Text"):
|
62 |
-
if
|
63 |
-
voice_file = f"{
|
64 |
try:
|
65 |
-
|
|
|
66 |
tts.save(voice_file)
|
67 |
st.audio(voice_file)
|
68 |
with open(voice_file, 'rb') as mp3:
|
@@ -72,29 +246,33 @@ with tab1:
|
|
72 |
else:
|
73 |
st.warning("No text to generate voice from.")
|
74 |
|
75 |
-
# Image uploads and ordering
|
76 |
-
|
77 |
-
|
|
|
78 |
if imgs:
|
79 |
# Create a DataFrame for editing image order
|
80 |
df_imgs = pd.DataFrame([{"name": f.name, "order": i} for i, f in enumerate(imgs)])
|
81 |
-
|
82 |
-
|
|
|
83 |
for _, row in edited.sort_values("order").iterrows():
|
84 |
for f in imgs:
|
85 |
if f.name == row['name']:
|
86 |
-
|
87 |
break # Found the file object, move to the next row
|
88 |
|
89 |
-
|
90 |
-
|
91 |
-
|
|
|
|
|
92 |
else:
|
93 |
buf = io.BytesIO()
|
94 |
c = canvas.Canvas(buf)
|
95 |
|
96 |
-
# Render text
|
97 |
-
if
|
98 |
page_w, page_h = letter
|
99 |
margin = 40
|
100 |
gutter = 20
|
@@ -104,101 +282,205 @@ with tab1:
|
|
104 |
col = 0
|
105 |
x = margin
|
106 |
y = page_h - margin
|
107 |
-
# Estimate wrap width
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
117 |
col += 1
|
118 |
if col >= columns:
|
119 |
c.showPage()
|
120 |
c.setFont(font_family, font_size)
|
121 |
col = 0
|
122 |
-
|
123 |
-
|
124 |
-
|
|
|
|
|
125 |
|
126 |
-
for line in textwrap.wrap(paragraph, wrap_width):
|
127 |
-
if y < margin:
|
128 |
-
col += 1
|
129 |
-
if col >= columns:
|
130 |
-
c.showPage()
|
131 |
-
c.setFont(font_family, font_size)
|
132 |
-
col = 0
|
133 |
-
x = margin + col*(col_w+gutter)
|
134 |
-
y = page_h - margin
|
135 |
c.drawString(x, y, line)
|
136 |
y -= line_height
|
137 |
-
y -= line_height # Add space between paragraphs
|
138 |
|
139 |
-
|
140 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
141 |
try:
|
142 |
-
img = Image.open(img_f)
|
143 |
w, h = img.size
|
144 |
-
|
145 |
-
#
|
146 |
-
|
147 |
-
|
148 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
149 |
except Exception as e:
|
150 |
-
st.warning(f"Could not process image {img_f.name}: {e}")
|
151 |
continue
|
152 |
|
|
|
|
|
|
|
|
|
|
|
|
|
153 |
c.save()
|
154 |
buf.seek(0)
|
155 |
-
pdf_name = f"{
|
156 |
-
st.download_button("⬇️ Download PDF", data=buf, file_name=pdf_name, mime="application/pdf")
|
157 |
|
158 |
st.markdown("---")
|
159 |
st.subheader("📂 Available Assets")
|
|
|
|
|
160 |
# Get all files and filter out unwanted ones
|
161 |
all_assets = glob.glob("*.*")
|
162 |
excluded_extensions = ['.py', '.ttf']
|
163 |
-
excluded_files = ['README.md', 'index.html']
|
164 |
|
165 |
assets = sorted([
|
166 |
a for a in all_assets
|
167 |
-
if not (a.lower().endswith(tuple(excluded_extensions)) or a in excluded_files)
|
168 |
])
|
169 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
170 |
if not assets:
|
171 |
st.info("No available assets found.")
|
172 |
else:
|
|
|
|
|
|
|
|
|
|
|
|
|
173 |
for a in assets:
|
174 |
ext = a.split('.')[-1].lower()
|
175 |
-
cols = st.columns([3, 1, 1])
|
176 |
-
|
|
|
|
|
|
|
|
|
177 |
|
178 |
-
#
|
|
|
|
|
|
|
179 |
try:
|
180 |
if ext == 'pdf':
|
181 |
with open(a, 'rb') as fp:
|
182 |
-
cols[
|
183 |
elif ext == 'mp3':
|
184 |
-
#
|
185 |
-
cols[1].audio(a)
|
186 |
with open(a, 'rb') as mp3:
|
187 |
-
cols[
|
188 |
# Add more file types here if needed (e.g., images)
|
189 |
elif ext in ['png', 'jpg', 'jpeg', 'gif']:
|
190 |
# Can't preview image directly in this column, offer download
|
191 |
with open(a, 'rb') as img_file:
|
192 |
-
cols[
|
|
|
|
|
|
|
|
|
193 |
# Handle other file types - maybe just offer download
|
194 |
else:
|
195 |
with open(a, 'rb') as other_file:
|
196 |
-
cols[
|
197 |
|
198 |
-
# Delete button
|
199 |
-
cols[
|
200 |
except Exception as e:
|
201 |
-
cols[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
202 |
|
203 |
|
204 |
with tab2:
|
|
|
10 |
import pandas as pd
|
11 |
from PIL import Image
|
12 |
from reportlab.pdfgen import canvas
|
13 |
+
from reportlab.lib.pagesizes import letter # Using letter size for consistency
|
14 |
from reportlab.lib.utils import ImageReader
|
15 |
import mistune
|
16 |
from gtts import gTTS
|
|
|
19 |
st.set_page_config(page_title="PDF & Code Interpreter", layout="wide", page_icon="🚀")
|
20 |
|
21 |
def delete_asset(path):
|
22 |
+
"""Deletes a file asset and reruns the app."""
|
23 |
try:
|
24 |
os.remove(path)
|
25 |
+
# Also remove from session state selection if it exists
|
26 |
+
if 'selected_assets' in st.session_state and path in st.session_state.selected_assets:
|
27 |
+
del st.session_state.selected_assets[path]
|
28 |
except Exception as e:
|
29 |
st.error(f"Error deleting file: {e}")
|
30 |
st.rerun()
|
31 |
|
32 |
+
# --- New Function to Generate Combined PDF ---
|
33 |
+
def generate_combined_pdf(selected_asset_paths):
|
34 |
+
"""Generates a single PDF from selected markdown and image file paths."""
|
35 |
+
buf = io.BytesIO()
|
36 |
+
c = canvas.Canvas(buf)
|
37 |
+
|
38 |
+
# --- Process Markdown Files ---
|
39 |
+
all_plain_text = ""
|
40 |
+
md_count = 0
|
41 |
+
for path in selected_asset_paths:
|
42 |
+
# Process only markdown files first
|
43 |
+
if path.lower().endswith('.md'):
|
44 |
+
md_count += 1
|
45 |
+
try:
|
46 |
+
with open(path, 'r', encoding='utf-8') as f:
|
47 |
+
md_text = f.read()
|
48 |
+
# Convert Markdown to plain text using mistune (removes formatting but keeps content)
|
49 |
+
renderer = mistune.HTMLRenderer()
|
50 |
+
markdown = mistune.create_markdown(renderer=renderer)
|
51 |
+
html = markdown(md_text or "")
|
52 |
+
plain_text = re.sub(r'<[^>]+>', '', html) # Strip HTML tags
|
53 |
+
|
54 |
+
if all_plain_text:
|
55 |
+
all_plain_text += "\n\n---\n\n" # Add a separator between combined MD files
|
56 |
+
all_plain_text += plain_text
|
57 |
+
|
58 |
+
except Exception as e:
|
59 |
+
st.warning(f"Could not read or process markdown file {path}: {e}")
|
60 |
+
# Decide if you want to continue or stop if an MD fails
|
61 |
+
|
62 |
+
# Render combined markdown content if any was found
|
63 |
+
if all_plain_text.strip():
|
64 |
+
# --- Canvas Text Rendering (2 columns, 14pt font) ---
|
65 |
+
page_w, page_h = letter # Use standard letter size (8.5 x 11 inches, approx 612 x 792 points)
|
66 |
+
margin = 40 # Margin around the content area (points)
|
67 |
+
gutter = 15 # Space between columns (points)
|
68 |
+
num_columns = 2 # Fixed number of columns as requested
|
69 |
+
|
70 |
+
# Calculate available width for text and column width
|
71 |
+
available_text_width = page_w - 2 * margin
|
72 |
+
col_w = (available_text_width - (num_columns - 1) * gutter) / num_columns
|
73 |
+
|
74 |
+
font_family = "Helvetica" # A standard font available in ReportLab canvas
|
75 |
+
font_size = 14 # Font size as requested
|
76 |
+
c.setFont(font_family, font_size)
|
77 |
+
|
78 |
+
# Estimate line height and character width for text wrapping
|
79 |
+
# ReportLab units are points. Approximating char width for wrapping.
|
80 |
+
# A common approximation for average character width is font_size * 0.6
|
81 |
+
avg_char_width_points = font_size * 0.6
|
82 |
+
# wrap_width is the number of characters that fit in one line of a column
|
83 |
+
wrap_width = int(col_w / avg_char_width_points) if avg_char_width_points > 0 else 100 # Prevent division by zero
|
84 |
+
|
85 |
+
line_height = font_size * 1.3 # Line spacing (e.g., 1.3 times font size)
|
86 |
+
|
87 |
+
# Initialize column and vertical position
|
88 |
+
col = 0
|
89 |
+
x = margin + col * (col_w + gutter) # Starting x for the first column
|
90 |
+
y = page_h - margin # Starting y from the top margin
|
91 |
+
|
92 |
+
paragraphs = all_plain_text.split("\n")
|
93 |
+
|
94 |
+
for paragraph in paragraphs:
|
95 |
+
# Handle empty lines (add vertical space)
|
96 |
+
if not paragraph.strip():
|
97 |
+
y -= line_height / 2 # Add less space for blank lines compared to paragraphs
|
98 |
+
# Check for page/column break after adding vertical space
|
99 |
+
if y < margin:
|
100 |
+
col += 1
|
101 |
+
if col >= num_columns:
|
102 |
+
c.showPage() # Move to a new page
|
103 |
+
c.setFont(font_family, font_size) # Re-set font after new page
|
104 |
+
col = 0 # Reset to the first column
|
105 |
+
x = margin + col * (col_w + gutter) # Reset x position
|
106 |
+
y = page_h - margin # Reset y position to top margin
|
107 |
+
else:
|
108 |
+
# Move to the next column on the same page
|
109 |
+
x = margin + col * (col_w + gutter)
|
110 |
+
y = page_h - margin # Reset y position to top margin
|
111 |
+
continue # Move to the next paragraph
|
112 |
+
|
113 |
+
# Wrap the paragraph text into lines that fit the column width
|
114 |
+
lines = textwrap.wrap(paragraph, wrap_width)
|
115 |
+
|
116 |
+
for line in lines:
|
117 |
+
# Check for page/column break before drawing the line
|
118 |
+
if y < margin:
|
119 |
+
col += 1
|
120 |
+
if col >= num_columns:
|
121 |
+
c.showPage() # Move to a new page
|
122 |
+
c.setFont(font_family, font_size) # Re-set font after new page
|
123 |
+
col = 0 # Reset to the first column
|
124 |
+
x = margin + col * (col_w + gutter) # Reset x position
|
125 |
+
y = page_h - margin # Reset y position to top margin
|
126 |
+
else:
|
127 |
+
# Move to the next column on the same page
|
128 |
+
x = margin + col * (col_w + gutter)
|
129 |
+
y = page_h - margin # Reset y position to top margin
|
130 |
+
|
131 |
+
# Draw the line
|
132 |
+
c.drawString(x, y, line)
|
133 |
+
# Move y position down for the next line
|
134 |
+
y -= line_height
|
135 |
+
|
136 |
+
# Add extra space after a paragraph (except the last one)
|
137 |
+
if paragraph != paragraphs[-1] or lines: # Add space if it's not the very last line of the last paragraph
|
138 |
+
y -= line_height / 2
|
139 |
+
|
140 |
+
# After all markdown text, ensure subsequent images start on a new page
|
141 |
+
if all_plain_text.strip():
|
142 |
+
c.showPage() # Start images on a fresh page
|
143 |
+
|
144 |
+
# --- Process Image Files ---
|
145 |
+
image_count = 0
|
146 |
+
for path in selected_asset_paths:
|
147 |
+
# Process image files after markdown
|
148 |
+
if path.lower().endswith(('.png', '.jpg', '.jpeg', '.gif')): # Add other image types if needed
|
149 |
+
image_count += 1
|
150 |
+
try:
|
151 |
+
img = Image.open(path)
|
152 |
+
img_w, img_h = img.size
|
153 |
+
|
154 |
+
# Get current page size (should be letter if no text was added or after showPage)
|
155 |
+
page_w, page_h = letter
|
156 |
+
margin_img = 40 # Margin around the image on the page
|
157 |
+
|
158 |
+
# Calculate available space within margins on the page
|
159 |
+
available_w = page_w - 2 * margin_img
|
160 |
+
available_h = page_h - 2 * margin_img
|
161 |
+
|
162 |
+
# Calculate scaling factor to fit the image within the available space while preserving aspect ratio
|
163 |
+
scale = min(available_w / img_w, available_h / img_h)
|
164 |
+
draw_w = img_w * scale
|
165 |
+
draw_h = img_h * scale
|
166 |
+
|
167 |
+
# Calculate position to center the scaled image on the page
|
168 |
+
pos_x = margin_img + (available_w - draw_w) / 2
|
169 |
+
# Position from the bottom left corner
|
170 |
+
pos_y = margin_img + (available_h - draw_h) / 2
|
171 |
+
|
172 |
+
# Draw the image. Ensure it's on a new page.
|
173 |
+
# If this is the first image and no text was added, it will use the initial page.
|
174 |
+
# Otherwise, showPage() is called before drawing.
|
175 |
+
if image_count > 1 or all_plain_text.strip():
|
176 |
+
c.showPage() # Start a new page for this image
|
177 |
+
|
178 |
+
# Draw the image onto the current page
|
179 |
+
c.drawImage(path, pos_x, pos_y, width=draw_w, height=draw_h, preserveAspectRatio=True)
|
180 |
+
|
181 |
+
except Exception as e:
|
182 |
+
st.warning(f"Could not process image file {path}: {e}")
|
183 |
+
continue # Continue with other selected assets
|
184 |
+
|
185 |
+
# If no markdown or images were selected/processed
|
186 |
+
if not all_plain_text.strip() and image_count == 0:
|
187 |
+
page_w, page_h = letter
|
188 |
+
c.drawString(40, page_h - 40, "No selected markdown or image files to generate PDF.")
|
189 |
+
|
190 |
+
c.save() # Finalize the PDF
|
191 |
+
buf.seek(0) # Rewind the buffer to the beginning
|
192 |
+
return buf.getvalue() # Return the PDF bytes
|
193 |
+
# --- End of New Function ---
|
194 |
+
|
195 |
+
|
196 |
# Tabs setup
|
197 |
tab1, tab2 = st.tabs(["📄 PDF Composer", "🧪 Code Interpreter"])
|
198 |
|
199 |
with tab1:
|
200 |
st.header("📄 PDF Composer & Voice Generator 🚀")
|
201 |
|
202 |
+
# Sidebar settings for the original PDF composer
|
203 |
+
# These settings (columns, font size for the *first* PDF button) are separate
|
204 |
+
# from the settings for the combined PDF generation below.
|
205 |
+
st.sidebar.markdown("### Original PDF Composer Settings")
|
206 |
+
columns = st.sidebar.slider("Text columns (Original PDF)", 1, 3, 1)
|
207 |
+
font_family = st.sidebar.selectbox("Font (Original PDF)", ["Helvetica","Times-Roman","Courier"])
|
208 |
+
font_size = st.sidebar.slider("Font size (Original PDF)", 6, 24, 12)
|
209 |
+
|
210 |
+
# Markdown input for the original PDF composer
|
211 |
+
st.markdown("#### Original PDF Composer Input")
|
212 |
+
md_file = st.file_uploader("Upload Markdown (.md) for Original PDF", type=["md"])
|
213 |
if md_file:
|
214 |
md_text = md_file.getvalue().decode("utf-8")
|
215 |
+
# Use stem from uploaded file or timestamp if text area is used
|
216 |
+
original_pdf_stem = Path(md_file.name).stem
|
217 |
else:
|
218 |
+
md_text = st.text_area("Or enter markdown text directly for Original PDF", height=200)
|
219 |
+
original_pdf_stem = datetime.now().strftime('%Y%m%d_%H%M%S')
|
220 |
|
221 |
+
# Convert Markdown to plain text for original PDF
|
|
|
222 |
renderer = mistune.HTMLRenderer()
|
223 |
markdown = mistune.create_markdown(renderer=renderer)
|
224 |
html = markdown(md_text or "")
|
225 |
+
original_pdf_plain_text = re.sub(r'<[^>]+>', '', html) # Strip HTML tags
|
226 |
|
227 |
+
# Voice settings (Applies to the text entered above)
|
228 |
+
st.markdown("#### Voice Generation from Text Input")
|
229 |
languages = {"English (US)": "en", "English (UK)": "en-uk", "Spanish": "es"}
|
230 |
voice_choice = st.selectbox("Voice Language", list(languages.keys()))
|
231 |
voice_lang = languages[voice_choice]
|
232 |
slow = st.checkbox("Slow Speech")
|
233 |
|
234 |
if st.button("🔊 Generate & Download Voice MP3 from Text"):
|
235 |
+
if original_pdf_plain_text.strip():
|
236 |
+
voice_file = f"{original_pdf_stem}.mp3"
|
237 |
try:
|
238 |
+
# Using the plain text from the text area/uploaded MD for voice
|
239 |
+
tts = gTTS(text=original_pdf_plain_text, lang=voice_lang, slow=slow)
|
240 |
tts.save(voice_file)
|
241 |
st.audio(voice_file)
|
242 |
with open(voice_file, 'rb') as mp3:
|
|
|
246 |
else:
|
247 |
st.warning("No text to generate voice from.")
|
248 |
|
249 |
+
# Image uploads and ordering for the original PDF composer
|
250 |
+
st.markdown("#### Images for Original PDF")
|
251 |
+
imgs = st.file_uploader("Upload Images for Original PDF", type=["png", "jpg", "jpeg"], accept_multiple_files=True)
|
252 |
+
ordered_images_original_pdf = []
|
253 |
if imgs:
|
254 |
# Create a DataFrame for editing image order
|
255 |
df_imgs = pd.DataFrame([{"name": f.name, "order": i} for i, f in enumerate(imgs)])
|
256 |
+
# Use num_rows="dynamic" for better UI, though less relevant if not adding/deleting rows
|
257 |
+
edited = st.data_editor(df_imgs, use_container_width=True)
|
258 |
+
# Reconstruct the ordered list of file objects based on edited order
|
259 |
for _, row in edited.sort_values("order").iterrows():
|
260 |
for f in imgs:
|
261 |
if f.name == row['name']:
|
262 |
+
ordered_images_original_pdf.append(f)
|
263 |
break # Found the file object, move to the next row
|
264 |
|
265 |
+
|
266 |
+
# --- Original PDF Generation Button ---
|
267 |
+
if st.button("🖋️ Generate Original PDF with Markdown & Images"):
|
268 |
+
if not original_pdf_plain_text.strip() and not ordered_images_original_pdf:
|
269 |
+
st.warning("Please provide some text or upload images to generate the Original PDF.")
|
270 |
else:
|
271 |
buf = io.BytesIO()
|
272 |
c = canvas.Canvas(buf)
|
273 |
|
274 |
+
# Render text using original settings and logic if text is provided
|
275 |
+
if original_pdf_plain_text.strip():
|
276 |
page_w, page_h = letter
|
277 |
margin = 40
|
278 |
gutter = 20
|
|
|
282 |
col = 0
|
283 |
x = margin
|
284 |
y = page_h - margin
|
285 |
+
# Estimate wrap width
|
286 |
+
avg_char_width = font_size * 0.6
|
287 |
+
wrap_width = int(col_w / avg_char_width) if avg_char_width > 0 else 100
|
288 |
+
|
289 |
+
for paragraph in original_pdf_plain_text.split("\n"):
|
290 |
+
if not paragraph.strip(): # Handle empty lines
|
291 |
+
y -= line_height / 2
|
292 |
+
if y < margin: # Check for column/page break
|
293 |
+
col += 1
|
294 |
+
if col >= columns:
|
295 |
+
c.showPage()
|
296 |
+
c.setFont(font_family, font_size)
|
297 |
+
col = 0
|
298 |
+
x = margin + col*(col_w+gutter)
|
299 |
+
y = page_h - margin
|
300 |
+
else:
|
301 |
+
x = margin + col*(col_w+gutter)
|
302 |
+
y = page_h - margin
|
303 |
+
continue
|
304 |
+
|
305 |
+
lines = textwrap.wrap(paragraph, wrap_width) if paragraph.strip() else [""]
|
306 |
+
|
307 |
+
for line in lines:
|
308 |
+
if y < margin: # Check for column/page break
|
309 |
col += 1
|
310 |
if col >= columns:
|
311 |
c.showPage()
|
312 |
c.setFont(font_family, font_size)
|
313 |
col = 0
|
314 |
+
x = margin + col*(col_w+gutter)
|
315 |
+
y = page_h - margin
|
316 |
+
else:
|
317 |
+
x = margin + col*(col_w+gutter)
|
318 |
+
y = page_h - margin
|
319 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
320 |
c.drawString(x, y, line)
|
321 |
y -= line_height
|
|
|
322 |
|
323 |
+
y -= line_height / 2 # Space after paragraph
|
324 |
+
|
325 |
+
# Ensure images start on a new page if text was added
|
326 |
+
if original_pdf_plain_text.strip():
|
327 |
+
c.showPage()
|
328 |
+
|
329 |
+
# Autosize pages to each uploaded image
|
330 |
+
image_count = 0
|
331 |
+
for img_f in ordered_images_original_pdf:
|
332 |
+
image_count += 1
|
333 |
try:
|
334 |
+
img = Image.open(img_f) # img_f is a file-like object from st.file_uploader
|
335 |
w, h = img.size
|
336 |
+
|
337 |
+
# Start a new page for each image
|
338 |
+
if image_count > 1 or original_pdf_plain_text.strip():
|
339 |
+
c.showPage()
|
340 |
+
|
341 |
+
# Draw image scaled to fit a letter page within margins, centered
|
342 |
+
page_w, page_h = letter
|
343 |
+
margin_img = 40
|
344 |
+
available_w = page_w - 2 * margin_img
|
345 |
+
available_h = page_h - 2 * margin_img
|
346 |
+
|
347 |
+
scale = min(available_w / w, available_h / h)
|
348 |
+
draw_w = w * scale
|
349 |
+
draw_h = h * scale
|
350 |
+
|
351 |
+
pos_x = margin_img + (available_w - draw_w) / 2
|
352 |
+
pos_y = margin_img + (available_h - draw_h) / 2
|
353 |
+
|
354 |
+
# Use ImageReader for file-like objects
|
355 |
+
c.drawImage(ImageReader(img_f), pos_x, pos_y, width=draw_w, height=draw_h, preserveAspectRatio=True)
|
356 |
+
|
357 |
except Exception as e:
|
358 |
+
st.warning(f"Could not process uploaded image {img_f.name}: {e}")
|
359 |
continue
|
360 |
|
361 |
+
# If nothing was generated
|
362 |
+
if not original_pdf_plain_text.strip() and not ordered_images_original_pdf:
|
363 |
+
page_w, page_h = letter
|
364 |
+
c.drawString(40, page_h - 40, "No content to generate Original PDF.")
|
365 |
+
|
366 |
+
|
367 |
c.save()
|
368 |
buf.seek(0)
|
369 |
+
pdf_name = f"{original_pdf_stem}.pdf"
|
370 |
+
st.download_button("⬇️ Download Original PDF", data=buf, file_name=pdf_name, mime="application/pdf")
|
371 |
|
372 |
st.markdown("---")
|
373 |
st.subheader("📂 Available Assets")
|
374 |
+
st.markdown("Select assets below and click 'Generate Combined PDF'.")
|
375 |
+
|
376 |
# Get all files and filter out unwanted ones
|
377 |
all_assets = glob.glob("*.*")
|
378 |
excluded_extensions = ['.py', '.ttf']
|
379 |
+
excluded_files = ['README.md', 'index.html'] # Added index.html here
|
380 |
|
381 |
assets = sorted([
|
382 |
a for a in all_assets
|
383 |
+
if not (a.lower().endswith(tuple(excluded_extensions)) or os.path.basename(a) in excluded_files)
|
384 |
])
|
385 |
|
386 |
+
# Initialize session state for selected assets if not already done
|
387 |
+
if 'selected_assets' not in st.session_state:
|
388 |
+
st.session_state.selected_assets = {}
|
389 |
+
|
390 |
+
# Ensure all current assets have an entry in session state, initialize to False if new
|
391 |
+
# Clean up session state from assets that no longer exist
|
392 |
+
current_asset_paths = [os.path.abspath(a) for a in assets]
|
393 |
+
st.session_state.selected_assets = {
|
394 |
+
k: v for k, v in st.session_state.selected_assets.items()
|
395 |
+
if os.path.abspath(k) in current_asset_paths # Keep only existing assets
|
396 |
+
}
|
397 |
+
for asset_path in assets:
|
398 |
+
if asset_path not in st.session_state.selected_assets:
|
399 |
+
st.session_state.selected_assets[asset_path] = False
|
400 |
+
|
401 |
+
|
402 |
+
# --- Display Assets with Checkboxes ---
|
403 |
if not assets:
|
404 |
st.info("No available assets found.")
|
405 |
else:
|
406 |
+
# Header row for clarity
|
407 |
+
header_cols = st.columns([0.5, 3, 1, 1])
|
408 |
+
header_cols[1].write("**File**")
|
409 |
+
# header_cols[2].write("**Action**") # Optional header
|
410 |
+
|
411 |
+
|
412 |
for a in assets:
|
413 |
ext = a.split('.')[-1].lower()
|
414 |
+
cols = st.columns([0.5, 3, 1, 1])
|
415 |
+
|
416 |
+
# Checkbox in the first column, updating session state
|
417 |
+
# Use absolute path for robust keying in case of directory changes (less likely in Streamlit sharing, but good practice)
|
418 |
+
asset_key = os.path.abspath(a)
|
419 |
+
st.session_state.selected_assets[a] = cols[0].checkbox("", value=st.session_state.selected_assets.get(a, False), key=f"select_asset_{asset_key}")
|
420 |
|
421 |
+
# File name in the second column
|
422 |
+
cols[1].write(a)
|
423 |
+
|
424 |
+
# Provide download/preview based on file type in the third column
|
425 |
try:
|
426 |
if ext == 'pdf':
|
427 |
with open(a, 'rb') as fp:
|
428 |
+
cols[2].download_button("📥", data=fp, file_name=a, mime="application/pdf", key=f"download_{a}")
|
429 |
elif ext == 'mp3':
|
430 |
+
# Audio player takes up too much space here, just offer download
|
|
|
431 |
with open(a, 'rb') as mp3:
|
432 |
+
cols[2].download_button("📥", data=mp3, file_name=a, mime="audio/mpeg", key=f"download_{a}")
|
433 |
# Add more file types here if needed (e.g., images)
|
434 |
elif ext in ['png', 'jpg', 'jpeg', 'gif']:
|
435 |
# Can't preview image directly in this column, offer download
|
436 |
with open(a, 'rb') as img_file:
|
437 |
+
cols[2].download_button("⬇️", data=img_file.read(), file_name=a, mime=f"image/{ext}", key=f"download_{a}")
|
438 |
+
elif ext == 'md':
|
439 |
+
# Offer download for markdown files
|
440 |
+
with open(a, 'r', encoding='utf-8') as md_file:
|
441 |
+
cols[2].download_button("⬇️", data=md_file.read(), file_name=a, mime="text/markdown", key=f"download_{a}")
|
442 |
# Handle other file types - maybe just offer download
|
443 |
else:
|
444 |
with open(a, 'rb') as other_file:
|
445 |
+
cols[2].download_button("⬇️", data=other_file.read(), file_name=a, key=f"download_{a}") # Mime type is guessed by streamlit
|
446 |
|
447 |
+
# Delete button in the fourth column
|
448 |
+
cols[3].button("🗑️", key=f"del_{a}", on_click=delete_asset, args=(a,))
|
449 |
except Exception as e:
|
450 |
+
cols[3].error(f"Error handling file {a}: {e}") # Place error in the delete column or add a separate status
|
451 |
+
|
452 |
+
|
453 |
+
# --- Combined PDF Generation Button ---
|
454 |
+
# Only show button if there are any assets listed
|
455 |
+
if assets:
|
456 |
+
if st.button("Generate Combined PDF from Selected Assets"):
|
457 |
+
# Get the list of selected asset paths
|
458 |
+
selected_asset_paths = [path for path, selected in st.session_state.selected_assets.items() if selected]
|
459 |
+
|
460 |
+
if not selected_asset_paths:
|
461 |
+
st.warning("Please select at least one asset.")
|
462 |
+
else:
|
463 |
+
with st.spinner("Generating combined PDF..."):
|
464 |
+
try:
|
465 |
+
# Call the new function to generate the combined PDF
|
466 |
+
combined_pdf_bytes = generate_combined_pdf(selected_asset_paths)
|
467 |
+
|
468 |
+
if combined_pdf_bytes: # Check if the function returned bytes (meaning content was added)
|
469 |
+
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
470 |
+
pdf_name = f"Combined_Assets_{timestamp}.pdf"
|
471 |
+
# Provide the generated PDF for download
|
472 |
+
st.download_button(
|
473 |
+
"⬇️ Download Combined PDF",
|
474 |
+
data=combined_pdf_bytes,
|
475 |
+
file_name=pdf_name,
|
476 |
+
mime="application/pdf"
|
477 |
+
)
|
478 |
+
st.success("Combined PDF generated!")
|
479 |
+
else:
|
480 |
+
st.warning("Generated PDF is empty. No valid markdown or image files were selected.")
|
481 |
+
|
482 |
+
except Exception as e:
|
483 |
+
st.error(f"An unexpected error occurred during PDF generation: {e}")
|
484 |
|
485 |
|
486 |
with tab2:
|