Spaces:
Sleeping
Sleeping
Commit ·
4bed098
1
Parent(s): 1f0784e
Paginate photo-only PDF pages by capacity
Browse filesCompute the max number of images that can fit on a photo-only page based on available height and minimum cell size.
Chunk overflow photos across additional continued pages instead of shrinking frames too far.
Align spacing constants for photo grids so continued pages match the main template.
server/app/services/pdf_reportlab.py
CHANGED
|
@@ -224,9 +224,12 @@ def render_report_pdf(
|
|
| 224 |
footer_h = 8 * mm
|
| 225 |
gap = 4 * mm
|
| 226 |
photo_col_gap = 6 * mm
|
|
|
|
|
|
|
| 227 |
min_photo_cell_w = 80 * mm
|
|
|
|
| 228 |
two_col_cell_w = (width - 2 * margin - photo_col_gap) / 2
|
| 229 |
-
|
| 230 |
|
| 231 |
gray_50 = colors.HexColor("#f9fafb")
|
| 232 |
gray_200 = colors.HexColor("#e5e7eb")
|
|
@@ -260,6 +263,15 @@ def render_report_pdf(
|
|
| 260 |
uploads = (session.get("uploads") or {}).get("photos") or []
|
| 261 |
by_id = {item.get("id"): item for item in uploads if item.get("id")}
|
| 262 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
sections: List[dict]
|
| 264 |
if sections_or_pages and isinstance(sections_or_pages[0], dict) and "pages" in sections_or_pages[0]:
|
| 265 |
sections = sections_or_pages
|
|
@@ -286,21 +298,35 @@ def render_report_pdf(
|
|
| 286 |
if path and path.exists():
|
| 287 |
label = _safe_text(item.get("name") or path.name)
|
| 288 |
photo_entries.append({"path": path, "label": label})
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 294 |
variant = "full" if chunk_index == 0 else "photos"
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
|
| 305 |
if not print_pages:
|
| 306 |
print_pages = [
|
|
@@ -378,7 +404,7 @@ def render_report_pdf(
|
|
| 378 |
pdf.drawString(margin, y, "Observations and Findings")
|
| 379 |
pdf.setStrokeColor(gray_200)
|
| 380 |
pdf.line(margin, y - 2, width - margin, y - 2)
|
| 381 |
-
y -=
|
| 382 |
|
| 383 |
ref = _safe_text(template.get("reference"))
|
| 384 |
area = _safe_text(template.get("area"))
|
|
@@ -578,7 +604,7 @@ def render_report_pdf(
|
|
| 578 |
pdf.drawString(margin, y, "Photo Documentation (continued)")
|
| 579 |
pdf.setStrokeColor(gray_200)
|
| 580 |
pdf.line(margin, y - 2, width - margin, y - 2)
|
| 581 |
-
y -=
|
| 582 |
|
| 583 |
if variant == "full":
|
| 584 |
y -= 2 * mm
|
|
@@ -595,16 +621,16 @@ def render_report_pdf(
|
|
| 595 |
if photos:
|
| 596 |
columns = 1 if len(photos) == 1 else 2
|
| 597 |
rows = math.ceil(len(photos) / columns)
|
| 598 |
-
cell_w = (width - 2 * margin - (columns - 1) *
|
| 599 |
-
cell_h = (photo_area_height - (rows - 1) *
|
| 600 |
|
| 601 |
for idx, photo in enumerate(photos):
|
| 602 |
photo_path = photo["path"]
|
| 603 |
label = photo.get("label") or photo_path.name
|
| 604 |
row = idx // columns
|
| 605 |
col = idx % columns
|
| 606 |
-
x = margin + col * (cell_w +
|
| 607 |
-
y = photo_area_top - (row + 1) * cell_h - row *
|
| 608 |
pdf.setStrokeColor(gray_200)
|
| 609 |
pdf.setFillColor(gray_50)
|
| 610 |
pdf.roundRect(x, y, cell_w, cell_h, 3 * mm, stroke=1, fill=1)
|
|
|
|
| 224 |
footer_h = 8 * mm
|
| 225 |
gap = 4 * mm
|
| 226 |
photo_col_gap = 6 * mm
|
| 227 |
+
photo_row_gap = 6 * mm
|
| 228 |
+
photos_header_gap = 8 * mm
|
| 229 |
min_photo_cell_w = 80 * mm
|
| 230 |
+
min_photo_cell_h = 60 * mm
|
| 231 |
two_col_cell_w = (width - 2 * margin - photo_col_gap) / 2
|
| 232 |
+
columns_for_photos = 2 if two_col_cell_w >= min_photo_cell_w else 1
|
| 233 |
|
| 234 |
gray_50 = colors.HexColor("#f9fafb")
|
| 235 |
gray_200 = colors.HexColor("#e5e7eb")
|
|
|
|
| 263 |
uploads = (session.get("uploads") or {}).get("photos") or []
|
| 264 |
by_id = {item.get("id"): item for item in uploads if item.get("id")}
|
| 265 |
|
| 266 |
+
content_top = height - margin - header_h - gap
|
| 267 |
+
content_bottom = margin + footer_h + gap
|
| 268 |
+
photo_area_height_photos = max(0, content_top - photos_header_gap - content_bottom)
|
| 269 |
+
max_rows_photos = max(
|
| 270 |
+
1,
|
| 271 |
+
int((photo_area_height_photos + photo_row_gap) // (min_photo_cell_h + photo_row_gap)),
|
| 272 |
+
)
|
| 273 |
+
max_photos_photos = max(1, max_rows_photos * columns_for_photos)
|
| 274 |
+
|
| 275 |
sections: List[dict]
|
| 276 |
if sections_or_pages and isinstance(sections_or_pages[0], dict) and "pages" in sections_or_pages[0]:
|
| 277 |
sections = sections_or_pages
|
|
|
|
| 298 |
if path and path.exists():
|
| 299 |
label = _safe_text(item.get("name") or path.name)
|
| 300 |
photo_entries.append({"path": path, "label": label})
|
| 301 |
+
if base_variant == "photos":
|
| 302 |
+
chunks = _chunk(photo_entries, max_photos_photos) or [[]]
|
| 303 |
+
for chunk in chunks:
|
| 304 |
+
print_pages.append(
|
| 305 |
+
{
|
| 306 |
+
"page_index": page_index,
|
| 307 |
+
"template": template,
|
| 308 |
+
"photos": chunk,
|
| 309 |
+
"variant": "photos",
|
| 310 |
+
"section_index": section_index,
|
| 311 |
+
}
|
| 312 |
+
)
|
| 313 |
+
else:
|
| 314 |
+
first_chunk = photo_entries[:2]
|
| 315 |
+
remainder = photo_entries[2:]
|
| 316 |
+
chunks = [first_chunk]
|
| 317 |
+
if remainder:
|
| 318 |
+
chunks.extend(_chunk(remainder, max_photos_photos))
|
| 319 |
+
for chunk_index, chunk in enumerate(chunks):
|
| 320 |
variant = "full" if chunk_index == 0 else "photos"
|
| 321 |
+
print_pages.append(
|
| 322 |
+
{
|
| 323 |
+
"page_index": page_index,
|
| 324 |
+
"template": template,
|
| 325 |
+
"photos": chunk,
|
| 326 |
+
"variant": variant,
|
| 327 |
+
"section_index": section_index,
|
| 328 |
+
}
|
| 329 |
+
)
|
| 330 |
|
| 331 |
if not print_pages:
|
| 332 |
print_pages = [
|
|
|
|
| 404 |
pdf.drawString(margin, y, "Observations and Findings")
|
| 405 |
pdf.setStrokeColor(gray_200)
|
| 406 |
pdf.line(margin, y - 2, width - margin, y - 2)
|
| 407 |
+
y -= photos_header_gap
|
| 408 |
|
| 409 |
ref = _safe_text(template.get("reference"))
|
| 410 |
area = _safe_text(template.get("area"))
|
|
|
|
| 604 |
pdf.drawString(margin, y, "Photo Documentation (continued)")
|
| 605 |
pdf.setStrokeColor(gray_200)
|
| 606 |
pdf.line(margin, y - 2, width - margin, y - 2)
|
| 607 |
+
y -= photos_header_gap
|
| 608 |
|
| 609 |
if variant == "full":
|
| 610 |
y -= 2 * mm
|
|
|
|
| 621 |
if photos:
|
| 622 |
columns = 1 if len(photos) == 1 else 2
|
| 623 |
rows = math.ceil(len(photos) / columns)
|
| 624 |
+
cell_w = (width - 2 * margin - (columns - 1) * photo_col_gap) / columns
|
| 625 |
+
cell_h = (photo_area_height - (rows - 1) * photo_row_gap) / rows
|
| 626 |
|
| 627 |
for idx, photo in enumerate(photos):
|
| 628 |
photo_path = photo["path"]
|
| 629 |
label = photo.get("label") or photo_path.name
|
| 630 |
row = idx // columns
|
| 631 |
col = idx % columns
|
| 632 |
+
x = margin + col * (cell_w + photo_col_gap)
|
| 633 |
+
y = photo_area_top - (row + 1) * cell_h - row * photo_row_gap
|
| 634 |
pdf.setStrokeColor(gray_200)
|
| 635 |
pdf.setFillColor(gray_50)
|
| 636 |
pdf.roundRect(x, y, cell_w, cell_h, 3 * mm, stroke=1, fill=1)
|