Update backup11.app.py
Browse files- backup11.app.py +81 -57
backup11.app.py
CHANGED
@@ -22,7 +22,7 @@ from pypdf import PdfReader, PdfWriter
|
|
22 |
from pypdf.annotations import Link
|
23 |
from pypdf.generic import Fit
|
24 |
|
25 |
-
st.set_page_config(layout="wide", initial_sidebar_state="
|
26 |
|
27 |
# Existing functions (unchanged)
|
28 |
def get_timestamp_prefix():
|
@@ -139,7 +139,7 @@ def apply_emoji_font(text, emoji_font):
|
|
139 |
|
140 |
return ''.join(result)
|
141 |
|
142 |
-
def markdown_to_pdf_content(markdown_text,
|
143 |
lines = markdown_text.strip().split('\n')
|
144 |
pdf_content = []
|
145 |
number_pattern = re.compile(r'^\d+(\.\d+)*\.\s')
|
@@ -174,16 +174,11 @@ def markdown_to_pdf_content(markdown_text, render_with_bold, auto_bold_numbers,
|
|
174 |
line = re.sub(r'\*\*(.+?)\*\*', r'<b>\1</b>', line)
|
175 |
line = re.sub(r'\*([^*]+?)\*', r'<b>\1</b>', line)
|
176 |
|
177 |
-
if auto_bold_numbers and is_numbered_line:
|
178 |
-
if not (line.startswith("<b>") and line.endswith("</b>")):
|
179 |
-
if "<b>" in line and "</b>" in line:
|
180 |
-
line = re.sub(r'</?b>', '', line)
|
181 |
-
line = f"<b>{line}</b>"
|
182 |
pdf_content.append(line)
|
183 |
total_lines = len(pdf_content)
|
184 |
return pdf_content, total_lines
|
185 |
|
186 |
-
def create_pdf(markdown_text, base_font_size,
|
187 |
buffer = io.BytesIO()
|
188 |
page_width = A4[0] * 2
|
189 |
page_height = A4[1]
|
@@ -198,7 +193,7 @@ def create_pdf(markdown_text, base_font_size, render_with_bold, auto_bold_number
|
|
198 |
)
|
199 |
styles = getSampleStyleSheet()
|
200 |
spacer_height = 10
|
201 |
-
pdf_content, total_lines = markdown_to_pdf_content(markdown_text,
|
202 |
try:
|
203 |
available_font_files = glob.glob("*.ttf")
|
204 |
if not available_font_files:
|
@@ -218,7 +213,7 @@ def create_pdf(markdown_text, base_font_size, render_with_bold, auto_bold_number
|
|
218 |
usable_width = page_width - 72
|
219 |
avg_line_chars = total_chars / total_lines if total_lines > 0 else 50
|
220 |
ideal_lines_per_col = 20
|
221 |
-
suggested_columns = max(
|
222 |
num_columns = num_columns if num_columns != 0 else suggested_columns
|
223 |
col_width = usable_width / num_columns
|
224 |
min_font_size = 6
|
@@ -230,6 +225,12 @@ def create_pdf(markdown_text, base_font_size, render_with_bold, auto_bold_number
|
|
230 |
if avg_line_chars > col_width / adjusted_font_size * 10:
|
231 |
adjusted_font_size = int(col_width / (avg_line_chars / 10))
|
232 |
adjusted_font_size = max(min_font_size, adjusted_font_size)
|
|
|
|
|
|
|
|
|
|
|
|
|
233 |
item_style = ParagraphStyle(
|
234 |
'ItemStyle', parent=styles['Normal'], fontName="DejaVuSans",
|
235 |
fontSize=adjusted_font_size, leading=adjusted_font_size * 1.15, spaceAfter=1,
|
@@ -237,8 +238,7 @@ def create_pdf(markdown_text, base_font_size, render_with_bold, auto_bold_number
|
|
237 |
)
|
238 |
numbered_bold_style = ParagraphStyle(
|
239 |
'NumberedBoldStyle', parent=styles['Normal'], fontName="NotoEmoji-Bold",
|
240 |
-
fontSize=adjusted_font_size
|
241 |
-
leading=(adjusted_font_size + 1) * 1.15 if enlarge_numbered else adjusted_font_size * 1.15, spaceAfter=1,
|
242 |
linkUnderline=True
|
243 |
)
|
244 |
section_style = ParagraphStyle(
|
@@ -323,9 +323,9 @@ def pdf_to_image(pdf_bytes):
|
|
323 |
return None
|
324 |
|
325 |
# PDF creation and linking functions
|
326 |
-
|
327 |
-
|
328 |
-
"eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen", "twenty"]
|
329 |
|
330 |
def create_crossfile_pdfs(source_pdf="TestSource.pdf", target_pdf="TestTarget.pdf"):
|
331 |
"""Create two PDFs with cross-file linking."""
|
@@ -333,7 +333,7 @@ def create_crossfile_pdfs(source_pdf="TestSource.pdf", target_pdf="TestTarget.pd
|
|
333 |
buffer = io.BytesIO()
|
334 |
c = canvas.Canvas(buffer)
|
335 |
c.setFont("Helvetica", 12)
|
336 |
-
for i, word in enumerate(
|
337 |
y = 800 - (i * 20)
|
338 |
c.drawString(50, y, f"{i}. {word}")
|
339 |
c.showPage()
|
@@ -414,7 +414,7 @@ def create_selflinking_pdf(pdf_file="SelfLinking.pdf"):
|
|
414 |
c.drawString(50, 800, "Table of Contents")
|
415 |
c.setFont("Helvetica", 12)
|
416 |
toc_y_positions = []
|
417 |
-
for i, word in enumerate(
|
418 |
y = 760 - (i * 20)
|
419 |
c.drawString(50, y, f"{word}")
|
420 |
toc_y_positions.append(y)
|
@@ -423,7 +423,7 @@ def create_selflinking_pdf(pdf_file="SelfLinking.pdf"):
|
|
423 |
# Page 2: Numbered list 1-20
|
424 |
c.setFont("Helvetica", 12)
|
425 |
list_y_positions = []
|
426 |
-
for i, word in enumerate(
|
427 |
y = 800 - (i * 20)
|
428 |
c.drawString(50, y, f"{i}. {word}")
|
429 |
list_y_positions.append(y)
|
@@ -446,12 +446,12 @@ def create_selflinking_pdf(pdf_file="SelfLinking.pdf"):
|
|
446 |
toc_page = writer.pages[0]
|
447 |
list_page = writer.pages[1]
|
448 |
writer.add_outline_item("Table of Contents", 0, fit=Fit(fit_type="/Fit"))
|
449 |
-
for i, word in enumerate(
|
450 |
y = list_y_positions[i-1]
|
451 |
writer.add_outline_item(word, 1, fit=Fit(fit_type="/XYZ", fit_args=[50, y, 0]))
|
452 |
|
453 |
# Add TOC links from page 1 to page 2
|
454 |
-
for i, word in enumerate(
|
455 |
toc_y = toc_y_positions[i]
|
456 |
list_y = list_y_positions[i]
|
457 |
link = Link(
|
@@ -472,7 +472,7 @@ md_files = [f for f in glob.glob("*.md") if os.path.basename(f) != "README.md"]
|
|
472 |
md_options = [os.path.splitext(os.path.basename(f))[0] for f in md_files]
|
473 |
|
474 |
with st.sidebar:
|
475 |
-
st.markdown("### PDF Options")
|
476 |
if md_options:
|
477 |
selected_md = st.selectbox("Select Markdown File", options=md_options, index=0)
|
478 |
with open(f"{selected_md}.md", "r", encoding="utf-8") as f:
|
@@ -481,51 +481,74 @@ with st.sidebar:
|
|
481 |
st.warning("No markdown file found. Please add one to your folder.")
|
482 |
selected_md = None
|
483 |
st.session_state.markdown_content = ""
|
|
|
484 |
available_font_files = {os.path.splitext(os.path.basename(f))[0]: f for f in glob.glob("*.ttf")}
|
485 |
-
selected_font_name = st.selectbox(
|
486 |
-
|
|
|
|
|
|
|
487 |
base_font_size = st.slider("Font Size (points)", min_value=6, max_value=16, value=8, step=1)
|
488 |
-
render_with_bold = st.checkbox("Render with Bold Formatting (remove ** markers)", value=True, key="render_with_bold")
|
489 |
-
auto_bold_numbers = st.checkbox("Auto Bold Numbered Lines", value=True, key="auto_bold_numbers")
|
490 |
-
enlarge_numbered = st.checkbox("Enlarge Font Size for Numbered Lines", value=True, key="enlarge_numbered")
|
491 |
-
add_space_before_numbered = st.checkbox("Add Space Ahead of Numbered Lines", value=False, key="add_space_before_numbered")
|
492 |
-
|
493 |
-
headings_to_fonts = st.checkbox("Headings to Fonts", value=False, key="headings_to_fonts",
|
494 |
-
help="Convert Markdown headings (# Heading) and emphasis (*word*) to appropriate font styles")
|
495 |
|
496 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
497 |
|
498 |
-
|
|
|
|
|
|
|
|
|
499 |
current_markdown = st.session_state.markdown_content
|
500 |
lines = current_markdown.strip().split('\n')
|
501 |
-
|
502 |
for line in lines:
|
503 |
if line.strip():
|
504 |
word_count = len(line.split())
|
505 |
longest_line_words = max(longest_line_words, word_count)
|
506 |
-
if
|
507 |
-
|
508 |
-
|
509 |
-
|
510 |
-
|
511 |
-
|
|
|
512 |
else:
|
513 |
-
recommended_columns =
|
514 |
-
|
515 |
-
|
516 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
517 |
|
518 |
-
column_options = [
|
519 |
-
num_columns = st.selectbox(
|
520 |
-
|
521 |
-
|
|
|
|
|
522 |
st.info("Font size and columns adjust to fit one page.")
|
523 |
|
524 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
525 |
|
|
|
526 |
col1, col2 = st.columns(2)
|
527 |
with col1:
|
528 |
-
if st.button("
|
529 |
st.session_state.markdown_content = edited_markdown
|
530 |
if selected_md:
|
531 |
with open(f"{selected_md}.md", "w", encoding="utf-8") as f:
|
@@ -543,12 +566,13 @@ with st.sidebar:
|
|
543 |
|
544 |
prefix = get_timestamp_prefix()
|
545 |
st.download_button(
|
546 |
-
label="
|
547 |
data=st.session_state.markdown_content,
|
548 |
file_name=f"{prefix} {selected_md}.md" if selected_md else f"{prefix} default.md",
|
549 |
mime="text/markdown"
|
550 |
)
|
551 |
-
|
|
|
552 |
VOICES = ["en-US-AriaNeural", "en-US-JennyNeural", "en-GB-SoniaNeural", "en-US-GuyNeural", "en-US-AnaNeural"]
|
553 |
selected_voice = st.selectbox("Select Voice for TTS", options=VOICES, index=0)
|
554 |
if st.button("Generate Audio"):
|
@@ -559,7 +583,7 @@ with st.sidebar:
|
|
559 |
with open(audio_file, "rb") as f:
|
560 |
audio_bytes = f.read()
|
561 |
st.download_button(
|
562 |
-
label="
|
563 |
data=audio_bytes,
|
564 |
file_name=audio_filename,
|
565 |
mime="audio/mpeg"
|
@@ -601,16 +625,16 @@ with st.spinner("Generating PDF..."):
|
|
601 |
pdf_bytes = create_pdf(
|
602 |
st.session_state.markdown_content,
|
603 |
base_font_size,
|
604 |
-
render_with_bold,
|
605 |
-
auto_bold_numbers,
|
606 |
-
enlarge_numbered,
|
607 |
num_columns,
|
608 |
add_space_before_numbered,
|
609 |
headings_to_fonts,
|
610 |
-
doc_title=selected_md if selected_md else "Untitled"
|
|
|
|
|
611 |
)
|
612 |
|
613 |
with st.container():
|
|
|
614 |
pdf_images = pdf_to_image(pdf_bytes)
|
615 |
if pdf_images:
|
616 |
for img in pdf_images:
|
@@ -620,7 +644,7 @@ with st.container():
|
|
620 |
|
621 |
with st.sidebar:
|
622 |
st.download_button(
|
623 |
-
label="
|
624 |
data=pdf_bytes,
|
625 |
file_name=f"{prefix} {selected_md}.pdf" if selected_md else f"{prefix} output.pdf",
|
626 |
mime="application/pdf"
|
|
|
22 |
from pypdf.annotations import Link
|
23 |
from pypdf.generic import Fit
|
24 |
|
25 |
+
st.set_page_config(layout="wide", initial_sidebar_state="expanded")
|
26 |
|
27 |
# Existing functions (unchanged)
|
28 |
def get_timestamp_prefix():
|
|
|
139 |
|
140 |
return ''.join(result)
|
141 |
|
142 |
+
def markdown_to_pdf_content(markdown_text, add_space_before_numbered, headings_to_fonts):
|
143 |
lines = markdown_text.strip().split('\n')
|
144 |
pdf_content = []
|
145 |
number_pattern = re.compile(r'^\d+(\.\d+)*\.\s')
|
|
|
174 |
line = re.sub(r'\*\*(.+?)\*\*', r'<b>\1</b>', line)
|
175 |
line = re.sub(r'\*([^*]+?)\*', r'<b>\1</b>', line)
|
176 |
|
|
|
|
|
|
|
|
|
|
|
177 |
pdf_content.append(line)
|
178 |
total_lines = len(pdf_content)
|
179 |
return pdf_content, total_lines
|
180 |
|
181 |
+
def create_pdf(markdown_text, base_font_size, num_columns, add_space_before_numbered, headings_to_fonts, doc_title, longest_line_words, total_lines):
|
182 |
buffer = io.BytesIO()
|
183 |
page_width = A4[0] * 2
|
184 |
page_height = A4[1]
|
|
|
193 |
)
|
194 |
styles = getSampleStyleSheet()
|
195 |
spacer_height = 10
|
196 |
+
pdf_content, total_lines = markdown_to_pdf_content(markdown_text, add_space_before_numbered, headings_to_fonts)
|
197 |
try:
|
198 |
available_font_files = glob.glob("*.ttf")
|
199 |
if not available_font_files:
|
|
|
213 |
usable_width = page_width - 72
|
214 |
avg_line_chars = total_chars / total_lines if total_lines > 0 else 50
|
215 |
ideal_lines_per_col = 20
|
216 |
+
suggested_columns = max(2, min(4, int(total_lines / ideal_lines_per_col) + 1))
|
217 |
num_columns = num_columns if num_columns != 0 else suggested_columns
|
218 |
col_width = usable_width / num_columns
|
219 |
min_font_size = 6
|
|
|
225 |
if avg_line_chars > col_width / adjusted_font_size * 10:
|
226 |
adjusted_font_size = int(col_width / (avg_line_chars / 10))
|
227 |
adjusted_font_size = max(min_font_size, adjusted_font_size)
|
228 |
+
|
229 |
+
# Adjust font size to fit one page based on longest line
|
230 |
+
if longest_line_words > 17:
|
231 |
+
font_scale = 17 / longest_line_words # Scale down from reference (17 words)
|
232 |
+
adjusted_font_size = max(min_font_size, int(adjusted_font_size * font_scale))
|
233 |
+
|
234 |
item_style = ParagraphStyle(
|
235 |
'ItemStyle', parent=styles['Normal'], fontName="DejaVuSans",
|
236 |
fontSize=adjusted_font_size, leading=adjusted_font_size * 1.15, spaceAfter=1,
|
|
|
238 |
)
|
239 |
numbered_bold_style = ParagraphStyle(
|
240 |
'NumberedBoldStyle', parent=styles['Normal'], fontName="NotoEmoji-Bold",
|
241 |
+
fontSize=adjusted_font_size, leading=adjusted_font_size * 1.15, spaceAfter=1,
|
|
|
242 |
linkUnderline=True
|
243 |
)
|
244 |
section_style = ParagraphStyle(
|
|
|
323 |
return None
|
324 |
|
325 |
# PDF creation and linking functions
|
326 |
+
WORDS_12 = ["one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve"]
|
327 |
+
WORDS_24 = ["one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten",
|
328 |
+
"eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen", "twenty", "twenty one", "twenty two", "twenty three", "twenty four"]
|
329 |
|
330 |
def create_crossfile_pdfs(source_pdf="TestSource.pdf", target_pdf="TestTarget.pdf"):
|
331 |
"""Create two PDFs with cross-file linking."""
|
|
|
333 |
buffer = io.BytesIO()
|
334 |
c = canvas.Canvas(buffer)
|
335 |
c.setFont("Helvetica", 12)
|
336 |
+
for i, word in enumerate(WORDS_12, 1):
|
337 |
y = 800 - (i * 20)
|
338 |
c.drawString(50, y, f"{i}. {word}")
|
339 |
c.showPage()
|
|
|
414 |
c.drawString(50, 800, "Table of Contents")
|
415 |
c.setFont("Helvetica", 12)
|
416 |
toc_y_positions = []
|
417 |
+
for i, word in enumerate(WORDS_12, 1):
|
418 |
y = 760 - (i * 20)
|
419 |
c.drawString(50, y, f"{word}")
|
420 |
toc_y_positions.append(y)
|
|
|
423 |
# Page 2: Numbered list 1-20
|
424 |
c.setFont("Helvetica", 12)
|
425 |
list_y_positions = []
|
426 |
+
for i, word in enumerate(WORDS_24, 1):
|
427 |
y = 800 - (i * 20)
|
428 |
c.drawString(50, y, f"{i}. {word}")
|
429 |
list_y_positions.append(y)
|
|
|
446 |
toc_page = writer.pages[0]
|
447 |
list_page = writer.pages[1]
|
448 |
writer.add_outline_item("Table of Contents", 0, fit=Fit(fit_type="/Fit"))
|
449 |
+
for i, word in enumerate(WORDS_12, 1):
|
450 |
y = list_y_positions[i-1]
|
451 |
writer.add_outline_item(word, 1, fit=Fit(fit_type="/XYZ", fit_args=[50, y, 0]))
|
452 |
|
453 |
# Add TOC links from page 1 to page 2
|
454 |
+
for i, word in enumerate(WORDS_12):
|
455 |
toc_y = toc_y_positions[i]
|
456 |
list_y = list_y_positions[i]
|
457 |
link = Link(
|
|
|
472 |
md_options = [os.path.splitext(os.path.basename(f))[0] for f in md_files]
|
473 |
|
474 |
with st.sidebar:
|
475 |
+
st.markdown("### π PDF Options")
|
476 |
if md_options:
|
477 |
selected_md = st.selectbox("Select Markdown File", options=md_options, index=0)
|
478 |
with open(f"{selected_md}.md", "r", encoding="utf-8") as f:
|
|
|
481 |
st.warning("No markdown file found. Please add one to your folder.")
|
482 |
selected_md = None
|
483 |
st.session_state.markdown_content = ""
|
484 |
+
|
485 |
available_font_files = {os.path.splitext(os.path.basename(f))[0]: f for f in glob.glob("*.ttf")}
|
486 |
+
selected_font_name = st.selectbox(
|
487 |
+
"Select Emoji Font",
|
488 |
+
options=list(available_font_files.keys()),
|
489 |
+
index=list(available_font_files.keys()).index("NotoEmoji-Bold") if "NotoEmoji-Bold" in available_font_files else 0
|
490 |
+
)
|
491 |
base_font_size = st.slider("Font Size (points)", min_value=6, max_value=16, value=8, step=1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
492 |
|
493 |
+
add_space_before_numbered = st.checkbox("Add Space Ahead of Numbered Lines", value=False)
|
494 |
+
headings_to_fonts = st.checkbox(
|
495 |
+
"Headings to Fonts",
|
496 |
+
value=False,
|
497 |
+
help="Convert Markdown headings (# Heading) to styled fonts"
|
498 |
+
)
|
499 |
+
auto_columns = st.checkbox("AutoColumns", value=True)
|
500 |
|
501 |
+
# Calculate document stats
|
502 |
+
longest_line_words = 0
|
503 |
+
total_lines = 0
|
504 |
+
adjusted_font_size_display = base_font_size
|
505 |
+
if 'markdown_content' in st.session_state:
|
506 |
current_markdown = st.session_state.markdown_content
|
507 |
lines = current_markdown.strip().split('\n')
|
508 |
+
total_lines = len([line for line in lines if line.strip()])
|
509 |
for line in lines:
|
510 |
if line.strip():
|
511 |
word_count = len(line.split())
|
512 |
longest_line_words = max(longest_line_words, word_count)
|
513 |
+
if auto_columns:
|
514 |
+
if longest_line_words > 38:
|
515 |
+
recommended_columns = 2
|
516 |
+
elif longest_line_words < 18 and total_lines < 20:
|
517 |
+
recommended_columns = 4
|
518 |
+
else:
|
519 |
+
recommended_columns = 3
|
520 |
else:
|
521 |
+
recommended_columns = 3
|
522 |
+
# Adjust font size for one-page fit
|
523 |
+
if longest_line_words > 17:
|
524 |
+
font_scale = 17 / longest_line_words
|
525 |
+
adjusted_font_size_display = max(6, int(base_font_size * font_scale))
|
526 |
+
st.markdown("**Document Stats**")
|
527 |
+
st.write(f"- Longest Line: {longest_line_words} words")
|
528 |
+
st.write(f"- Total Lines: {total_lines}")
|
529 |
+
st.write(f"- Recommended Columns: {recommended_columns}")
|
530 |
+
st.write(f"- Adjusted Font Size: {adjusted_font_size_display} points")
|
531 |
|
532 |
+
column_options = [2, 3, 4]
|
533 |
+
num_columns = st.selectbox(
|
534 |
+
"Number of Columns",
|
535 |
+
options=column_options,
|
536 |
+
index=column_options.index(recommended_columns)
|
537 |
+
)
|
538 |
st.info("Font size and columns adjust to fit one page.")
|
539 |
|
540 |
+
st.markdown("### βοΈ Edit Markdown")
|
541 |
+
edited_markdown = st.text_area(
|
542 |
+
"Input Markdown",
|
543 |
+
value=st.session_state.markdown_content,
|
544 |
+
height=200,
|
545 |
+
key=f"markdown_{selected_md}_{selected_font_name}_{num_columns}"
|
546 |
+
)
|
547 |
|
548 |
+
st.markdown("### πΎ Actions")
|
549 |
col1, col2 = st.columns(2)
|
550 |
with col1:
|
551 |
+
if st.button("π Update PDF"):
|
552 |
st.session_state.markdown_content = edited_markdown
|
553 |
if selected_md:
|
554 |
with open(f"{selected_md}.md", "w", encoding="utf-8") as f:
|
|
|
566 |
|
567 |
prefix = get_timestamp_prefix()
|
568 |
st.download_button(
|
569 |
+
label="πΎ Save Markdown",
|
570 |
data=st.session_state.markdown_content,
|
571 |
file_name=f"{prefix} {selected_md}.md" if selected_md else f"{prefix} default.md",
|
572 |
mime="text/markdown"
|
573 |
)
|
574 |
+
|
575 |
+
st.markdown("### π Text-to-Speech")
|
576 |
VOICES = ["en-US-AriaNeural", "en-US-JennyNeural", "en-GB-SoniaNeural", "en-US-GuyNeural", "en-US-AnaNeural"]
|
577 |
selected_voice = st.selectbox("Select Voice for TTS", options=VOICES, index=0)
|
578 |
if st.button("Generate Audio"):
|
|
|
583 |
with open(audio_file, "rb") as f:
|
584 |
audio_bytes = f.read()
|
585 |
st.download_button(
|
586 |
+
label="πΎ Save Audio",
|
587 |
data=audio_bytes,
|
588 |
file_name=audio_filename,
|
589 |
mime="audio/mpeg"
|
|
|
625 |
pdf_bytes = create_pdf(
|
626 |
st.session_state.markdown_content,
|
627 |
base_font_size,
|
|
|
|
|
|
|
628 |
num_columns,
|
629 |
add_space_before_numbered,
|
630 |
headings_to_fonts,
|
631 |
+
doc_title=selected_md if selected_md else "Untitled",
|
632 |
+
longest_line_words=longest_line_words,
|
633 |
+
total_lines=total_lines
|
634 |
)
|
635 |
|
636 |
with st.container():
|
637 |
+
st.markdown("### π PDF Preview")
|
638 |
pdf_images = pdf_to_image(pdf_bytes)
|
639 |
if pdf_images:
|
640 |
for img in pdf_images:
|
|
|
644 |
|
645 |
with st.sidebar:
|
646 |
st.download_button(
|
647 |
+
label="πΎ Save PDF",
|
648 |
data=pdf_bytes,
|
649 |
file_name=f"{prefix} {selected_md}.pdf" if selected_md else f"{prefix} output.pdf",
|
650 |
mime="application/pdf"
|