Spaces:
Sleeping
Sleeping
breaks away from single file
Browse files- app.py +97 -153
- pdf_processor.py +204 -0
- ui_components.py +166 -0
app.py
CHANGED
@@ -1,167 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import streamlit as st
|
2 |
-
import fitz # PyMuPDF
|
3 |
-
import numpy as np
|
4 |
-
from PIL import Image
|
5 |
-
import io
|
6 |
import tempfile
|
7 |
import os
|
8 |
import time
|
|
|
9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
st.set_page_config(
|
11 |
page_title="PDF to Single Image Converter",
|
12 |
page_icon="π",
|
13 |
-
layout="centered"
|
|
|
14 |
)
|
15 |
|
|
|
16 |
st.title("π PDF to Single Image Converter")
|
17 |
-
st.
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
# Open the PDF
|
22 |
-
pdf_document = fitz.open(pdf_path)
|
23 |
-
num_pages = len(pdf_document)
|
24 |
-
|
25 |
-
# Calculate total height and get width
|
26 |
-
total_height = 0
|
27 |
-
width = 0
|
28 |
-
|
29 |
-
# First pass to calculate dimensions
|
30 |
-
zooms = []
|
31 |
-
for page_num in range(num_pages):
|
32 |
-
page = pdf_document[page_num]
|
33 |
-
zoom = dpi / 72 # 72 is the default DPI for PDFs
|
34 |
-
zooms.append(zoom)
|
35 |
-
rect = page.rect
|
36 |
-
width = max(width, int(rect.width * zoom))
|
37 |
-
total_height += int(rect.height * zoom)
|
38 |
-
|
39 |
-
# Create a new image with the calculated dimensions
|
40 |
-
result_image = Image.new("RGB", (width, total_height), (255, 255, 255))
|
41 |
-
|
42 |
-
# Second pass to render pages
|
43 |
-
current_height = 0
|
44 |
-
progress_bar = st.progress(0)
|
45 |
-
status_text = st.empty()
|
46 |
-
|
47 |
-
for page_num in range(num_pages):
|
48 |
-
status_text.text(f"Processing page {page_num + 1}/{num_pages}")
|
49 |
-
page = pdf_document[page_num]
|
50 |
-
zoom = zooms[page_num]
|
51 |
-
|
52 |
-
# Get the page as a pixmap
|
53 |
-
pix = page.get_pixmap(matrix=fitz.Matrix(zoom, zoom))
|
54 |
-
|
55 |
-
# Convert pixmap to PIL Image
|
56 |
-
page_image = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
|
57 |
-
|
58 |
-
# Paste this page into the result image
|
59 |
-
result_image.paste(page_image, (0, current_height))
|
60 |
-
current_height += pix.height
|
61 |
-
|
62 |
-
# Update progress
|
63 |
-
progress_bar.progress((page_num + 1) / num_pages)
|
64 |
-
|
65 |
-
# Create a byte buffer for the image
|
66 |
-
buf = io.BytesIO()
|
67 |
-
if output_format.upper() == "PNG":
|
68 |
-
result_image.save(buf, format="PNG")
|
69 |
-
else:
|
70 |
-
result_image.save(buf, format="JPEG", quality=95)
|
71 |
-
|
72 |
-
buf.seek(0)
|
73 |
-
pdf_document.close()
|
74 |
-
status_text.text("Processing complete!")
|
75 |
-
return buf
|
76 |
-
|
77 |
-
# UI Components
|
78 |
-
with st.sidebar:
|
79 |
-
st.header("Settings")
|
80 |
-
dpi = st.slider("Resolution (DPI)", min_value=72, max_value=600, value=300, step=1,
|
81 |
-
help="Higher DPI means better quality but larger file size")
|
82 |
-
output_format = st.radio("Output Format", ["PNG", "JPG"],
|
83 |
-
help="PNG provides better quality but larger file size")
|
84 |
-
st.write("---")
|
85 |
-
st.write("### About")
|
86 |
-
st.write("This app converts multi-page PDFs into a single image file.")
|
87 |
-
st.write("Made with β€οΈ using Streamlit and PyMuPDF")
|
88 |
-
|
89 |
-
# File uploader
|
90 |
-
uploaded_file = st.file_uploader("Choose a PDF file", type="pdf")
|
91 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
92 |
if uploaded_file is not None:
|
93 |
-
# Display file
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
st.
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
os.unlink(pdf_path)
|
153 |
else:
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
caption="Example of converted PDF")
|
160 |
-
|
161 |
-
# Add requirements info at the bottom
|
162 |
-
st.write("---")
|
163 |
-
with st.expander("Installation Requirements"):
|
164 |
-
st.code("""
|
165 |
-
pip install streamlit PyMuPDF Pillow
|
166 |
-
""")
|
167 |
-
st.write("Run the app with: `streamlit run app.py`")
|
|
|
1 |
+
# app.py
|
2 |
+
"""
|
3 |
+
Main Streamlit application file for the PDF to Single Image Converter.
|
4 |
+
|
5 |
+
Coordinates the UI, file handling, and calls the processing logic.
|
6 |
+
"""
|
7 |
+
|
8 |
import streamlit as st
|
|
|
|
|
|
|
|
|
9 |
import tempfile
|
10 |
import os
|
11 |
import time
|
12 |
+
from typing import Optional
|
13 |
|
14 |
+
# Import functions from our modules
|
15 |
+
from pdf_processor import pdf_to_single_image
|
16 |
+
from ui_components import (
|
17 |
+
render_sidebar,
|
18 |
+
display_file_details,
|
19 |
+
display_results,
|
20 |
+
render_initial_info,
|
21 |
+
display_installation_info
|
22 |
+
)
|
23 |
+
|
24 |
+
# --- Page Configuration ---
|
25 |
st.set_page_config(
|
26 |
page_title="PDF to Single Image Converter",
|
27 |
page_icon="π",
|
28 |
+
layout="centered", # Can be "wide" or "centered"
|
29 |
+
initial_sidebar_state="expanded" # Keep sidebar open initially
|
30 |
)
|
31 |
|
32 |
+
# --- Main Application ---
|
33 |
st.title("π PDF to Single Image Converter")
|
34 |
+
st.markdown("Upload a multi-page PDF and convert it into a single, tall image file (PNG or JPG).")
|
35 |
+
|
36 |
+
# --- Sidebar ---
|
37 |
+
dpi_setting, format_setting = render_sidebar()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
38 |
|
39 |
+
# --- File Upload ---
|
40 |
+
# Use a key for the file uploader to potentially reset it later if needed
|
41 |
+
uploaded_file: Optional[st.runtime.uploaded_file_manager.UploadedFile] = st.file_uploader(
|
42 |
+
"Choose a PDF file", type="pdf", key="pdf_uploader"
|
43 |
+
)
|
44 |
+
|
45 |
+
# --- Processing Logic ---
|
46 |
if uploaded_file is not None:
|
47 |
+
# Display details of the uploaded file
|
48 |
+
display_file_details(uploaded_file)
|
49 |
+
|
50 |
+
# Use a temporary file for robust handling by PyMuPDF
|
51 |
+
temp_pdf_path: Optional[str] = None
|
52 |
+
try:
|
53 |
+
# Create a temporary file to store the uploaded PDF content
|
54 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp_file:
|
55 |
+
tmp_file.write(uploaded_file.getvalue())
|
56 |
+
temp_pdf_path = tmp_file.name # Store the path
|
57 |
+
|
58 |
+
# Add a button to trigger the conversion
|
59 |
+
if st.button(f"π Convert to {format_setting}", key="convert_button"):
|
60 |
+
if temp_pdf_path: # Ensure temp path is valid
|
61 |
+
try:
|
62 |
+
# Show a spinner during processing
|
63 |
+
with st.spinner(f"Converting PDF to {format_setting} at {dpi_setting} DPI... Please wait."):
|
64 |
+
start_time = time.time()
|
65 |
+
|
66 |
+
# Call the core conversion function from pdf_processor
|
67 |
+
img_buffer = pdf_to_single_image(
|
68 |
+
pdf_path=temp_pdf_path,
|
69 |
+
output_format=format_setting,
|
70 |
+
dpi=dpi_setting
|
71 |
+
)
|
72 |
+
|
73 |
+
processing_time = time.time() - start_time
|
74 |
+
|
75 |
+
# Prepare output filename
|
76 |
+
base_filename = os.path.splitext(uploaded_file.name)[0]
|
77 |
+
output_filename = f"{base_filename}_converted.{format_setting.lower()}"
|
78 |
+
|
79 |
+
# Display the results (download button, preview)
|
80 |
+
if img_buffer.getbuffer().nbytes > 0: # Check if buffer has content
|
81 |
+
display_results(
|
82 |
+
img_buffer=img_buffer,
|
83 |
+
output_filename=output_filename,
|
84 |
+
output_format=format_setting,
|
85 |
+
processing_time=processing_time
|
86 |
+
)
|
87 |
+
else:
|
88 |
+
st.error("Conversion resulted in an empty image. Please check the PDF file.")
|
89 |
+
|
90 |
+
|
91 |
+
except Exception as e:
|
92 |
+
st.error(f"β An error occurred during conversion:")
|
93 |
+
st.exception(e) # Displays the full traceback for debugging
|
94 |
+
|
95 |
+
finally:
|
96 |
+
# --- Cleanup ---
|
97 |
+
# Ensure the temporary file is deleted after processing or if an error occurs
|
98 |
+
if temp_pdf_path and os.path.exists(temp_pdf_path):
|
99 |
+
try:
|
100 |
+
os.unlink(temp_pdf_path)
|
101 |
+
# st.write(f"Temporary file {temp_pdf_path} deleted.") # Optional debug message
|
102 |
+
except OSError as e:
|
103 |
+
st.warning(f"Could not delete temporary file {temp_pdf_path}: {e}")
|
104 |
+
|
105 |
+
|
|
|
106 |
else:
|
107 |
+
# Show initial instructions if no file is uploaded
|
108 |
+
render_initial_info()
|
109 |
+
|
110 |
+
# --- Footer / Installation Info ---
|
111 |
+
display_installation_info()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pdf_processor.py
ADDED
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# pdf_processor.py
|
2 |
+
"""
|
3 |
+
Handles the core logic of converting a PDF document into a single image.
|
4 |
+
"""
|
5 |
+
|
6 |
+
import fitz # PyMuPDF
|
7 |
+
from PIL import Image
|
8 |
+
import io
|
9 |
+
import streamlit as st # Imported for progress bar updates
|
10 |
+
from typing import Tuple, List, Union
|
11 |
+
|
12 |
+
# Constants
|
13 |
+
DEFAULT_PDF_DPI = 72 # Standard PDF DPI used for scaling calculations
|
14 |
+
JPEG_QUALITY = 95 # Quality setting for JPEG output
|
15 |
+
|
16 |
+
def calculate_image_dimensions(pdf_document: fitz.Document, dpi: int) -> Tuple[int, int, List[float]]:
|
17 |
+
"""
|
18 |
+
Calculates the total dimensions required for the final image canvas.
|
19 |
+
|
20 |
+
Iterates through PDF pages to determine the maximum width and total height
|
21 |
+
needed when rendered at the specified DPI.
|
22 |
+
|
23 |
+
Parameters
|
24 |
+
----------
|
25 |
+
pdf_document : fitz.Document
|
26 |
+
The opened PyMuPDF document object.
|
27 |
+
dpi : int
|
28 |
+
The target resolution in dots per inch.
|
29 |
+
|
30 |
+
Returns
|
31 |
+
-------
|
32 |
+
Tuple[int, int, List[float]]
|
33 |
+
A tuple containing:
|
34 |
+
- max_width (int): The maximum width required among all pages.
|
35 |
+
- total_height (int): The sum of heights of all pages.
|
36 |
+
- zooms (List[float]): A list of zoom factors for each page.
|
37 |
+
"""
|
38 |
+
total_height = 0
|
39 |
+
max_width = 0
|
40 |
+
zooms = []
|
41 |
+
num_pages = len(pdf_document)
|
42 |
+
|
43 |
+
# First pass: Calculate dimensions and zoom factors
|
44 |
+
for page_num in range(num_pages):
|
45 |
+
page = pdf_document[page_num]
|
46 |
+
# Calculate the zoom factor needed to achieve the target DPI
|
47 |
+
zoom = dpi / DEFAULT_PDF_DPI
|
48 |
+
zooms.append(zoom)
|
49 |
+
# Get page dimensions in pixels at the calculated zoom
|
50 |
+
rect = page.rect
|
51 |
+
page_width = int(rect.width * zoom)
|
52 |
+
page_height = int(rect.height * zoom)
|
53 |
+
# Update maximum width and total height
|
54 |
+
max_width = max(max_width, page_width)
|
55 |
+
total_height += page_height
|
56 |
+
|
57 |
+
return max_width, total_height, zooms
|
58 |
+
|
59 |
+
def render_pages_to_image(
|
60 |
+
pdf_document: fitz.Document,
|
61 |
+
zooms: List[float],
|
62 |
+
canvas_width: int,
|
63 |
+
canvas_height: int
|
64 |
+
) -> Image.Image:
|
65 |
+
"""
|
66 |
+
Renders each page of the PDF onto a single PIL Image canvas.
|
67 |
+
|
68 |
+
Parameters
|
69 |
+
----------
|
70 |
+
pdf_document : fitz.Document
|
71 |
+
The opened PyMuPDF document object.
|
72 |
+
zooms : List[float]
|
73 |
+
A list of zoom factors, one for each page.
|
74 |
+
canvas_width : int
|
75 |
+
The width of the final image canvas.
|
76 |
+
canvas_height : int
|
77 |
+
The height of the final image canvas.
|
78 |
+
|
79 |
+
Returns
|
80 |
+
-------
|
81 |
+
Image.Image
|
82 |
+
A PIL Image object containing all rendered PDF pages.
|
83 |
+
"""
|
84 |
+
num_pages = len(pdf_document)
|
85 |
+
# Create a new blank image canvas (RGB white background)
|
86 |
+
result_image = Image.new("RGB", (canvas_width, canvas_height), (255, 255, 255))
|
87 |
+
current_height = 0
|
88 |
+
|
89 |
+
# Initialize Streamlit progress reporting
|
90 |
+
progress_bar = st.progress(0)
|
91 |
+
status_text = st.empty()
|
92 |
+
|
93 |
+
# Second pass: Render each page and paste it onto the canvas
|
94 |
+
for page_num in range(num_pages):
|
95 |
+
status_text.text(f"Processing page {page_num + 1}/{num_pages}...")
|
96 |
+
page = pdf_document[page_num]
|
97 |
+
zoom = zooms[page_num]
|
98 |
+
|
99 |
+
# Generate a pixmap (raster image) of the page
|
100 |
+
# Use fitz.Matrix for transformation with the calculated zoom
|
101 |
+
pix = page.get_pixmap(matrix=fitz.Matrix(zoom, zoom))
|
102 |
+
|
103 |
+
# Convert the pixmap to a PIL Image
|
104 |
+
# Ensure the mode ("RGB" or "RGBA") matches pix.samples structure if issues arise
|
105 |
+
try:
|
106 |
+
page_image = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
|
107 |
+
except ValueError as e:
|
108 |
+
st.error(f"Error converting page {page_num+1} to Image: {e}")
|
109 |
+
st.warning(f"Pixmap details: width={pix.width}, height={pix.height}, alpha={pix.alpha}, samples length={len(pix.samples)}")
|
110 |
+
# Attempt RGBA conversion as a fallback if alpha channel is present
|
111 |
+
if pix.alpha:
|
112 |
+
page_image = Image.frombytes("RGBA", [pix.width, pix.height], pix.samples).convert("RGB")
|
113 |
+
st.info("Retrying page conversion with RGBA mode.")
|
114 |
+
else:
|
115 |
+
raise # Re-raise the original error if not an alpha channel issue
|
116 |
+
|
117 |
+
# Paste the page image onto the main canvas
|
118 |
+
# The paste position is (0, current_height)
|
119 |
+
result_image.paste(page_image, (0, current_height))
|
120 |
+
current_height += pix.height # Move down for the next page
|
121 |
+
|
122 |
+
# Update Streamlit progress bar
|
123 |
+
progress_bar.progress((page_num + 1) / num_pages)
|
124 |
+
|
125 |
+
status_text.text("Rendering complete!")
|
126 |
+
return result_image
|
127 |
+
|
128 |
+
def pdf_to_single_image(pdf_path: str, output_format: str = "PNG", dpi: int = 300) -> io.BytesIO:
|
129 |
+
"""
|
130 |
+
Converts all pages of a PDF file into a single vertical image.
|
131 |
+
|
132 |
+
Opens the PDF, calculates the required dimensions, renders each page
|
133 |
+
at the specified DPI, stitches them together vertically, and returns
|
134 |
+
the result as an image in a BytesIO buffer.
|
135 |
+
|
136 |
+
Parameters
|
137 |
+
----------
|
138 |
+
pdf_path : str
|
139 |
+
The file path to the input PDF document.
|
140 |
+
output_format : str, optional
|
141 |
+
The desired output image format ("PNG" or "JPG"), by default "PNG".
|
142 |
+
dpi : int, optional
|
143 |
+
The resolution (dots per inch) for rendering the PDF pages, by default 300.
|
144 |
+
Higher DPI results in better quality but larger file size.
|
145 |
+
|
146 |
+
Returns
|
147 |
+
-------
|
148 |
+
io.BytesIO
|
149 |
+
A BytesIO buffer containing the generated image data in the specified format.
|
150 |
+
|
151 |
+
Raises
|
152 |
+
------
|
153 |
+
fitz.FitzError
|
154 |
+
If there is an error opening or processing the PDF file.
|
155 |
+
Exception
|
156 |
+
For other potential errors during image processing or saving.
|
157 |
+
"""
|
158 |
+
pdf_document = None # Initialize to ensure it's defined in finally block
|
159 |
+
try:
|
160 |
+
# Open the PDF document
|
161 |
+
pdf_document = fitz.open(pdf_path)
|
162 |
+
|
163 |
+
# Calculate the necessary dimensions for the final image
|
164 |
+
canvas_width, canvas_height, zooms = calculate_image_dimensions(pdf_document, dpi)
|
165 |
+
|
166 |
+
if canvas_width == 0 or canvas_height == 0:
|
167 |
+
st.warning("Could not determine valid dimensions for the PDF. It might be empty or corrupted.")
|
168 |
+
return io.BytesIO() # Return empty buffer
|
169 |
+
|
170 |
+
# Render pages onto the canvas
|
171 |
+
result_image = render_pages_to_image(pdf_document, zooms, canvas_width, canvas_height)
|
172 |
+
|
173 |
+
# Create an in-memory buffer to save the image
|
174 |
+
img_buffer = io.BytesIO()
|
175 |
+
|
176 |
+
# Save the final image to the buffer in the specified format
|
177 |
+
if output_format.upper() == "PNG":
|
178 |
+
result_image.save(img_buffer, format="PNG")
|
179 |
+
elif output_format.upper() == "JPG" or output_format.upper() == "JPEG":
|
180 |
+
# Save as JPEG with specified quality, converting RGBA to RGB if necessary
|
181 |
+
if result_image.mode == 'RGBA':
|
182 |
+
result_image = result_image.convert('RGB')
|
183 |
+
result_image.save(img_buffer, format="JPEG", quality=JPEG_QUALITY)
|
184 |
+
else:
|
185 |
+
# Default to PNG if format is unknown
|
186 |
+
st.warning(f"Unsupported format '{output_format}'. Defaulting to PNG.")
|
187 |
+
result_image.save(img_buffer, format="PNG")
|
188 |
+
|
189 |
+
# Reset buffer position to the beginning for reading
|
190 |
+
img_buffer.seek(0)
|
191 |
+
|
192 |
+
return img_buffer
|
193 |
+
|
194 |
+
except fitz.FitzError as e:
|
195 |
+
st.error(f"Error processing PDF: {e}")
|
196 |
+
raise # Re-raise the specific exception
|
197 |
+
except Exception as e:
|
198 |
+
st.error(f"An unexpected error occurred during conversion: {e}")
|
199 |
+
raise # Re-raise general exceptions
|
200 |
+
finally:
|
201 |
+
# Ensure the PDF document is closed even if errors occur
|
202 |
+
if pdf_document:
|
203 |
+
pdf_document.close()
|
204 |
+
# st.write("PDF document closed.") # Optional debug message
|
ui_components.py
ADDED
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# ui_components.py
|
2 |
+
"""
|
3 |
+
Defines functions for creating distinct UI sections of the Streamlit application.
|
4 |
+
"""
|
5 |
+
|
6 |
+
import streamlit as st
|
7 |
+
from typing import Tuple, Dict, Any, Optional
|
8 |
+
from PIL import Image
|
9 |
+
import io
|
10 |
+
|
11 |
+
# Constants
|
12 |
+
MAX_PREVIEW_HEIGHT = 10000 # Maximum height in pixels for full-size preview
|
13 |
+
|
14 |
+
def render_sidebar() -> Tuple[int, str]:
|
15 |
+
"""
|
16 |
+
Renders the sidebar UI elements for settings.
|
17 |
+
|
18 |
+
Returns
|
19 |
+
-------
|
20 |
+
Tuple[int, str]
|
21 |
+
A tuple containing:
|
22 |
+
- dpi (int): The selected resolution in DPI.
|
23 |
+
- output_format (str): The selected output format ('PNG' or 'JPG').
|
24 |
+
"""
|
25 |
+
with st.sidebar:
|
26 |
+
st.header("βοΈ Settings")
|
27 |
+
# DPI Slider
|
28 |
+
dpi = st.slider(
|
29 |
+
"Resolution (DPI)",
|
30 |
+
min_value=72,
|
31 |
+
max_value=600,
|
32 |
+
value=300,
|
33 |
+
step=1,
|
34 |
+
help="Dots Per Inch. Higher DPI means better quality but larger file size and longer processing time."
|
35 |
+
)
|
36 |
+
# Output Format Radio Buttons
|
37 |
+
output_format = st.radio(
|
38 |
+
"Output Format",
|
39 |
+
["PNG", "JPG"],
|
40 |
+
index=0, # Default to PNG
|
41 |
+
help="PNG offers lossless quality (larger file). JPG uses lossy compression (smaller file)."
|
42 |
+
)
|
43 |
+
|
44 |
+
st.write("---")
|
45 |
+
st.write("### About")
|
46 |
+
st.info(
|
47 |
+
"This app converts multi-page PDFs into a single, vertically stitched image file. "
|
48 |
+
"Useful for sharing or archiving documents as images."
|
49 |
+
)
|
50 |
+
st.write("Made with β€οΈ using [Streamlit](https://streamlit.io) & [PyMuPDF](https://pymupdf.readthedocs.io/en/latest/)")
|
51 |
+
st.write("Tim might be a π§")
|
52 |
+
# A little fun :)
|
53 |
+
# st.write("Tim might be a π§") # Uncomment if desired
|
54 |
+
|
55 |
+
return dpi, output_format
|
56 |
+
|
57 |
+
def display_file_details(uploaded_file: st.runtime.uploaded_file_manager.UploadedFile) -> None:
|
58 |
+
"""
|
59 |
+
Displays details of the uploaded file.
|
60 |
+
|
61 |
+
Parameters
|
62 |
+
----------
|
63 |
+
uploaded_file : st.runtime.uploaded_file_manager.UploadedFile
|
64 |
+
The file uploaded by the user via st.file_uploader.
|
65 |
+
"""
|
66 |
+
file_details = {
|
67 |
+
"Filename": uploaded_file.name,
|
68 |
+
"Type": uploaded_file.type,
|
69 |
+
"Size": f"{uploaded_file.size / (1024*1024):.2f} MB" # Show size in MB
|
70 |
+
}
|
71 |
+
st.write("### File Details")
|
72 |
+
# Use columns for better layout
|
73 |
+
col1, col2 = st.columns(2)
|
74 |
+
with col1:
|
75 |
+
st.write(f"**Filename:**")
|
76 |
+
st.write(f"**Type:**")
|
77 |
+
st.write(f"**Size:**")
|
78 |
+
with col2:
|
79 |
+
st.write(f"{file_details['Filename']}")
|
80 |
+
st.write(f"{file_details['Type']}")
|
81 |
+
st.write(f"{file_details['Size']}")
|
82 |
+
|
83 |
+
|
84 |
+
def display_results(
|
85 |
+
img_buffer: io.BytesIO,
|
86 |
+
output_filename: str,
|
87 |
+
output_format: str,
|
88 |
+
processing_time: float
|
89 |
+
) -> None:
|
90 |
+
"""
|
91 |
+
Displays the conversion results: success message, download button, and image preview.
|
92 |
+
|
93 |
+
Parameters
|
94 |
+
----------
|
95 |
+
img_buffer : io.BytesIO
|
96 |
+
The buffer containing the generated image data.
|
97 |
+
output_filename : str
|
98 |
+
The suggested filename for the downloaded image.
|
99 |
+
output_format : str
|
100 |
+
The format of the output image ('PNG' or 'JPG').
|
101 |
+
processing_time : float
|
102 |
+
The time taken for the conversion process in seconds.
|
103 |
+
"""
|
104 |
+
st.success(f"β
Conversion completed in {processing_time:.2f} seconds!")
|
105 |
+
|
106 |
+
# Determine MIME type based on format
|
107 |
+
mime_type = f"image/{output_format.lower()}"
|
108 |
+
|
109 |
+
# Provide download button
|
110 |
+
st.download_button(
|
111 |
+
label=f"β¬οΈ Download {output_format} Image",
|
112 |
+
data=img_buffer,
|
113 |
+
file_name=output_filename,
|
114 |
+
mime=mime_type
|
115 |
+
)
|
116 |
+
|
117 |
+
# Image preview section
|
118 |
+
st.write("---")
|
119 |
+
st.write("### πΌοΈ Image Preview")
|
120 |
+
try:
|
121 |
+
# Open image from buffer for preview
|
122 |
+
img = Image.open(img_buffer)
|
123 |
+
width, height = img.size
|
124 |
+
st.write(f"**Image dimensions:** {width}x{height} pixels")
|
125 |
+
|
126 |
+
# Warn and scale down preview if the image is excessively tall
|
127 |
+
if height > MAX_PREVIEW_HEIGHT:
|
128 |
+
st.warning(f"β οΈ Image is very tall ({height}px). Preview is scaled down.")
|
129 |
+
# Calculate width based on a max preview width (e.g., 800px) to maintain aspect ratio
|
130 |
+
preview_width = min(width, 800)
|
131 |
+
st.image(img, caption=f"Scaled Preview of {output_filename}", width=preview_width)
|
132 |
+
else:
|
133 |
+
# Show image using Streamlit's default width handling or a fixed width
|
134 |
+
st.image(img, caption=f"Preview of {output_filename}", use_column_width='auto')
|
135 |
+
|
136 |
+
except Exception as e:
|
137 |
+
st.error(f"Could not display image preview: {e}")
|
138 |
+
st.warning("The image file might be corrupted or too large for preview.")
|
139 |
+
|
140 |
+
|
141 |
+
def render_initial_info() -> None:
|
142 |
+
"""
|
143 |
+
Displays the initial instructions and placeholder content when no file is uploaded.
|
144 |
+
"""
|
145 |
+
st.info("π Upload a PDF file using the sidebar to get started.")
|
146 |
+
st.write("---")
|
147 |
+
# Placeholder or example section (optional)
|
148 |
+
# st.write("### Example Output Structure")
|
149 |
+
# st.image("https://via.placeholder.com/600x800/ccc/888?text=Page+1", caption="Page 1")
|
150 |
+
# st.image("https://via.placeholder.com/600x800/eee/777?text=Page+2", caption="Page 2")
|
151 |
+
# st.caption("...(Pages are stitched vertically)")
|
152 |
+
|
153 |
+
def display_installation_info() -> None:
|
154 |
+
"""Displays the installation requirements and run command."""
|
155 |
+
st.write("---")
|
156 |
+
with st.expander("π οΈ Installation & Usage"):
|
157 |
+
st.code("""
|
158 |
+
# 1. Install required libraries
|
159 |
+
pip install streamlit Pillow PyMuPDF
|
160 |
+
|
161 |
+
# 2. Save the code files (app.py, pdf_processor.py, ui_components.py)
|
162 |
+
# in the same directory.
|
163 |
+
|
164 |
+
# 3. Run the Streamlit application
|
165 |
+
streamlit run app.py
|
166 |
+
""", language="bash")
|