tsphan commited on
Commit
576a588
Β·
1 Parent(s): 7d83622

breaks away from single file

Browse files
Files changed (3) hide show
  1. app.py +97 -153
  2. pdf_processor.py +204 -0
  3. 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.write("Upload a PDF and convert it into a single image containing all pages.")
18
-
19
- def pdf_to_single_image(pdf_path, output_format="PNG", dpi=300):
20
- """Convert all pages of a PDF to a single image file"""
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 info
94
- file_details = {
95
- "Filename": uploaded_file.name,
96
- "File size": f"{uploaded_file.size / 1024:.2f} KB"
97
- }
98
- st.write("### File Details")
99
- for k, v in file_details.items():
100
- st.write(f"**{k}:** {v}")
101
-
102
- # Save uploaded file to temp file
103
- with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as tmp_file:
104
- tmp_file.write(uploaded_file.getvalue())
105
- pdf_path = tmp_file.name
106
-
107
- # Process on button click
108
- if st.button("Convert to Image"):
109
- try:
110
- with st.spinner("Converting PDF to image..."):
111
- start_time = time.time()
112
-
113
- # Process the PDF
114
- img_buffer = pdf_to_single_image(pdf_path, output_format, dpi)
115
-
116
- # Calculate processing time
117
- processing_time = time.time() - start_time
118
- st.success(f"Conversion completed in {processing_time:.2f} seconds!")
119
-
120
- # Get file extension
121
- ext = "png" if output_format == "PNG" else "jpg"
122
-
123
- # Create download button
124
- output_filename = f"{os.path.splitext(uploaded_file.name)[0]}.{ext}"
125
- st.download_button(
126
- label=f"Download {output_format} Image",
127
- data=img_buffer,
128
- file_name=output_filename,
129
- mime=f"image/{ext.lower()}"
130
- )
131
-
132
- # Preview (with warning for large files)
133
- img = Image.open(img_buffer)
134
- width, height = img.size
135
- aspect_ratio = width / height
136
-
137
- st.write("### Image Preview")
138
- if height > 10000:
139
- st.warning("This is a very tall image. Preview is scaled down.")
140
- st.image(img, caption=f"Output Image ({width}x{height} pixels)", width=min(width, 800))
141
- else:
142
- st.image(img, caption=f"Output Image ({width}x{height} pixels)")
143
-
144
- st.write(f"**Image dimensions:** {width}x{height} pixels")
145
-
146
- except Exception as e:
147
- st.error(f"An error occurred: {e}")
148
-
149
- finally:
150
- # Clean up temp file
151
- if os.path.exists(pdf_path):
152
- os.unlink(pdf_path)
153
  else:
154
- st.info("πŸ‘† Please upload a PDF file to get started.")
155
-
156
- # Example image
157
- st.write("### Example Output")
158
- st.image("https://via.placeholder.com/800x600?text=PDF+to+Single+Image+Example",
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")