Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -17,12 +17,7 @@ pillow_heif.register_heif_opener()
|
|
| 17 |
|
| 18 |
# --- CONSTANTS & LAYOUT ---
|
| 19 |
# Page: Letter 8.5" x 11", margins below
|
| 20 |
-
CONTENT_WIDTH_IN = 8.5 - (0.5 + 0.5) # 7.5"
|
| 21 |
-
DESC_COL_IN = CONTENT_WIDTH_IN * (1 / 5.0) # strict 1:4 ratio
|
| 22 |
-
PHOTO_COL_IN = CONTENT_WIDTH_IN * (4 / 5.0)
|
| 23 |
-
ROW_HEIGHT_IN = 4.5 # two rows per page (fits within 9.5" body height)
|
| 24 |
-
PHOTO_MAX_W_IN = PHOTO_COL_IN - 0.20 # small padding
|
| 25 |
-
PHOTO_MAX_H_IN = ROW_HEIGHT_IN - 0.20
|
| 26 |
|
| 27 |
JOB_MAP = {
|
| 28 |
"WM 3155 SSM Parking": "T250014 Walmart 3155 Sault Ste. Marie FY26 Parking Lot Refresh\n446 Great Northern Rd.\nSault Ste. Marie, Ontario, P6B 4Z9",
|
|
@@ -141,7 +136,6 @@ def generate_report(image_items, job_details, compress_images, quality, progress
|
|
| 141 |
|
| 142 |
# Header
|
| 143 |
header = section.header
|
| 144 |
-
# --- FIX 1: The header's add_table method REQUIRES the width argument directly ---
|
| 145 |
header_table = header.add_table(rows=1, cols=2, width=Inches(CONTENT_WIDTH_IN))
|
| 146 |
header_table.autofit = False
|
| 147 |
set_table_fixed_layout(header_table)
|
|
@@ -167,93 +161,101 @@ def generate_report(image_items, job_details, compress_images, quality, progress
|
|
| 167 |
footer = section.footer
|
| 168 |
add_page_number(footer.paragraphs[0])
|
| 169 |
|
| 170 |
-
#
|
| 171 |
num_images = len(image_items)
|
| 172 |
processed = 0
|
| 173 |
|
| 174 |
-
for start in range(0, num_images,
|
| 175 |
if start > 0:
|
| 176 |
doc.add_page_break()
|
| 177 |
|
| 178 |
-
page_items = image_items[start:start +
|
| 179 |
|
| 180 |
-
#
|
| 181 |
-
|
| 182 |
-
page_table = doc.add_table(rows=2, cols=2)
|
| 183 |
page_table.width = Inches(CONTENT_WIDTH_IN)
|
| 184 |
page_table.autofit = False
|
| 185 |
set_table_fixed_layout(page_table)
|
| 186 |
page_table.style = 'Table Grid'
|
| 187 |
page_table.alignment = WD_TABLE_ALIGNMENT.CENTER
|
| 188 |
|
| 189 |
-
#
|
| 190 |
-
|
| 191 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
for r in page_table.rows:
|
| 193 |
-
r.height = Inches(
|
| 194 |
r.height_rule = WD_ROW_HEIGHT_RULE.EXACTLY
|
| 195 |
-
r.cells[0].width = Inches(DESC_COL_IN)
|
| 196 |
-
r.cells[1].width = Inches(PHOTO_COL_IN)
|
| 197 |
|
| 198 |
-
#
|
| 199 |
-
for
|
| 200 |
-
|
| 201 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
|
| 203 |
-
|
| 204 |
-
|
|
|
|
|
|
|
|
|
|
| 205 |
|
| 206 |
desc_cell.vertical_alignment = WD_ALIGN_VERTICAL.TOP
|
| 207 |
photo_cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
|
| 208 |
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
|
|
|
|
|
|
|
|
|
| 241 |
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
p_photo.paragraph_format.space_after = Pt(0)
|
| 246 |
-
p_photo.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 247 |
-
if img_ratio >= box_ratio:
|
| 248 |
-
p_photo.add_run().add_picture(img_stream, width=Inches(PHOTO_MAX_W_IN))
|
| 249 |
-
else:
|
| 250 |
-
p_photo.add_run().add_picture(img_stream, height=Inches(PHOTO_MAX_H_IN))
|
| 251 |
-
|
| 252 |
-
processed += 1
|
| 253 |
-
if num_images:
|
| 254 |
-
progress_bar.progress(processed / num_images)
|
| 255 |
-
else:
|
| 256 |
-
pass
|
| 257 |
|
| 258 |
# Save DOCX
|
| 259 |
doc_io = io.BytesIO()
|
|
@@ -318,6 +320,7 @@ with st.sidebar:
|
|
| 318 |
st.rerun()
|
| 319 |
|
| 320 |
st.header("Upload, Arrange, Rotate, and Describe Images")
|
|
|
|
| 321 |
uploaded_files = st.file_uploader(
|
| 322 |
"Upload all your photos here",
|
| 323 |
accept_multiple_files=True,
|
|
@@ -338,7 +341,6 @@ if uploaded_files:
|
|
| 338 |
}
|
| 339 |
|
| 340 |
if st.session_state.image_dict:
|
| 341 |
-
st.info("Set the order, rotate if needed, then add descriptions. Exactly 2 photos per page (last page may have 1).")
|
| 342 |
items = list(st.session_state.image_dict.items())
|
| 343 |
items.sort(key=lambda kv: kv[1].get('order', 0))
|
| 344 |
|
|
@@ -391,4 +393,5 @@ if generate_button:
|
|
| 391 |
c2.download_button("⬇️ Download PDF (.pdf)", pdf_io, f"{file_base}.pdf", use_container_width=True)
|
| 392 |
except Exception as e:
|
| 393 |
st.error(f"A critical error occurred: {e}")
|
| 394 |
-
progress_bar.empty()
|
|
|
|
|
|
| 17 |
|
| 18 |
# --- CONSTANTS & LAYOUT ---
|
| 19 |
# Page: Letter 8.5" x 11", margins below
|
| 20 |
+
CONTENT_WIDTH_IN = 8.5 - (0.5 + 0.5) # 7.5" total content width
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
JOB_MAP = {
|
| 23 |
"WM 3155 SSM Parking": "T250014 Walmart 3155 Sault Ste. Marie FY26 Parking Lot Refresh\n446 Great Northern Rd.\nSault Ste. Marie, Ontario, P6B 4Z9",
|
|
|
|
| 136 |
|
| 137 |
# Header
|
| 138 |
header = section.header
|
|
|
|
| 139 |
header_table = header.add_table(rows=1, cols=2, width=Inches(CONTENT_WIDTH_IN))
|
| 140 |
header_table.autofit = False
|
| 141 |
set_table_fixed_layout(header_table)
|
|
|
|
| 161 |
footer = section.footer
|
| 162 |
add_page_number(footer.paragraphs[0])
|
| 163 |
|
| 164 |
+
# --- LAYOUT CHANGE: 2x2 GRID (4 PHOTOS PER PAGE) ---
|
| 165 |
num_images = len(image_items)
|
| 166 |
processed = 0
|
| 167 |
|
| 168 |
+
for start in range(0, num_images, 4): # Process 4 images at a time
|
| 169 |
if start > 0:
|
| 170 |
doc.add_page_break()
|
| 171 |
|
| 172 |
+
page_items = image_items[start:start + 4]
|
| 173 |
|
| 174 |
+
# A 2x4 table holds two (Desc | Photo) pairs per row
|
| 175 |
+
page_table = doc.add_table(rows=2, cols=4)
|
|
|
|
| 176 |
page_table.width = Inches(CONTENT_WIDTH_IN)
|
| 177 |
page_table.autofit = False
|
| 178 |
set_table_fixed_layout(page_table)
|
| 179 |
page_table.style = 'Table Grid'
|
| 180 |
page_table.alignment = WD_TABLE_ALIGNMENT.CENTER
|
| 181 |
|
| 182 |
+
# Define column widths for the new grid layout
|
| 183 |
+
PAIR_WIDTH_IN = CONTENT_WIDTH_IN / 2.0
|
| 184 |
+
DESC_COL_IN_GRID = PAIR_WIDTH_IN * (1 / 5.0)
|
| 185 |
+
PHOTO_COL_IN_GRID = PAIR_WIDTH_IN * (4 / 5.0)
|
| 186 |
+
|
| 187 |
+
# Lock column widths
|
| 188 |
+
page_table.columns[0].width = Inches(DESC_COL_IN_GRID)
|
| 189 |
+
page_table.columns[1].width = Inches(PHOTO_COL_IN_GRID)
|
| 190 |
+
page_table.columns[2].width = Inches(DESC_COL_IN_GRID)
|
| 191 |
+
page_table.columns[3].width = Inches(PHOTO_COL_IN_GRID)
|
| 192 |
+
|
| 193 |
+
# Define row heights to split the page
|
| 194 |
+
ROW_HEIGHT_GRID_IN = 4.4 # Height for each of the two main rows
|
| 195 |
+
|
| 196 |
+
# Lock row heights
|
| 197 |
for r in page_table.rows:
|
| 198 |
+
r.height = Inches(ROW_HEIGHT_GRID_IN)
|
| 199 |
r.height_rule = WD_ROW_HEIGHT_RULE.EXACTLY
|
|
|
|
|
|
|
| 200 |
|
| 201 |
+
# Clean all cells before populating
|
| 202 |
+
for row in page_table.rows:
|
| 203 |
+
for cell in row.cells:
|
| 204 |
+
ensure_cell_has_one_empty_paragraph(cell)
|
| 205 |
+
|
| 206 |
+
# Loop through the up-to-4 items for this page and place them in the grid
|
| 207 |
+
for item_idx, item in enumerate(page_items):
|
| 208 |
+
row_idx = item_idx // 2 # Item 0,1 -> row 0. Item 2,3 -> row 1.
|
| 209 |
+
pair_idx = item_idx % 2 # Item 0,2 -> pair 0. Item 1,3 -> pair 1.
|
| 210 |
|
| 211 |
+
desc_col_idx = pair_idx * 2
|
| 212 |
+
photo_col_idx = desc_col_idx + 1
|
| 213 |
+
|
| 214 |
+
desc_cell = page_table.cell(row_idx, desc_col_idx)
|
| 215 |
+
photo_cell = page_table.cell(row_idx, photo_col_idx)
|
| 216 |
|
| 217 |
desc_cell.vertical_alignment = WD_ALIGN_VERTICAL.TOP
|
| 218 |
photo_cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
|
| 219 |
|
| 220 |
+
rotation = item.get('rotation', 0) or 0
|
| 221 |
+
|
| 222 |
+
# Description
|
| 223 |
+
p_desc = desc_cell.paragraphs[0]
|
| 224 |
+
run_label = p_desc.add_run("Description:\n")
|
| 225 |
+
run_label.bold = True
|
| 226 |
+
p_desc.add_run(item.get('description', '') or '')
|
| 227 |
+
|
| 228 |
+
# Photo Sizing and Placement
|
| 229 |
+
PHOTO_MAX_W_IN_GRID = PHOTO_COL_IN_GRID - 0.15
|
| 230 |
+
PHOTO_MAX_H_IN_GRID = ROW_HEIGHT_GRID_IN - 0.15
|
| 231 |
+
|
| 232 |
+
try:
|
| 233 |
+
item['file'].seek(0)
|
| 234 |
+
im = Image.open(item['file'])
|
| 235 |
+
if rotation:
|
| 236 |
+
im = im.rotate(rotation, expand=True)
|
| 237 |
+
w_px, h_px = im.size
|
| 238 |
+
except Exception:
|
| 239 |
+
w_px, h_px = (1000, 1000)
|
| 240 |
+
|
| 241 |
+
img_ratio = (w_px / h_px) if h_px else 1.0
|
| 242 |
+
box_ratio = PHOTO_MAX_W_IN_GRID / PHOTO_MAX_H_IN_GRID
|
| 243 |
+
|
| 244 |
+
img_stream = process_image(
|
| 245 |
+
item['file'], compress=compress_images, quality=quality, rotation=rotation
|
| 246 |
+
)
|
| 247 |
+
|
| 248 |
+
if img_stream:
|
| 249 |
+
p_photo = photo_cell.paragraphs[0]
|
| 250 |
+
p_photo.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 251 |
+
if img_ratio >= box_ratio:
|
| 252 |
+
p_photo.add_run().add_picture(img_stream, width=Inches(PHOTO_MAX_W_IN_GRID))
|
| 253 |
+
else:
|
| 254 |
+
p_photo.add_run().add_picture(img_stream, height=Inches(PHOTO_MAX_H_IN_GRID))
|
| 255 |
|
| 256 |
+
processed += 1
|
| 257 |
+
if num_images:
|
| 258 |
+
progress_bar.progress(processed / num_images)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
|
| 260 |
# Save DOCX
|
| 261 |
doc_io = io.BytesIO()
|
|
|
|
| 320 |
st.rerun()
|
| 321 |
|
| 322 |
st.header("Upload, Arrange, Rotate, and Describe Images")
|
| 323 |
+
st.info("Arrange photos in the desired order. The report will be generated with a 2x2 grid (4 photos per page).")
|
| 324 |
uploaded_files = st.file_uploader(
|
| 325 |
"Upload all your photos here",
|
| 326 |
accept_multiple_files=True,
|
|
|
|
| 341 |
}
|
| 342 |
|
| 343 |
if st.session_state.image_dict:
|
|
|
|
| 344 |
items = list(st.session_state.image_dict.items())
|
| 345 |
items.sort(key=lambda kv: kv[1].get('order', 0))
|
| 346 |
|
|
|
|
| 393 |
c2.download_button("⬇️ Download PDF (.pdf)", pdf_io, f"{file_base}.pdf", use_container_width=True)
|
| 394 |
except Exception as e:
|
| 395 |
st.error(f"A critical error occurred: {e}")
|
| 396 |
+
progress_bar.empty()
|
| 397 |
+
|