Spaces:
Sleeping
Sleeping
File size: 6,533 Bytes
70d9e6c 669f7f8 70d9e6c bec7397 70d9e6c bec7397 70d9e6c bec7397 70d9e6c bec7397 70d9e6c bec7397 70d9e6c bec7397 70d9e6c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 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 140 141 | """PDF export for DDA detection reports (FR-05)."""
from __future__ import annotations
import json
from io import BytesIO
from pathlib import Path
from typing import Any, Dict, List, Optional
from ..database import DATA_DIR
from ..models import DetectionRun
from .detect_service import _isoformat_ist
def _safe_filename(title: str, run_id: int) -> str:
base = "".join(c if c.isalnum() or c in " -_" else "_" for c in (title or "report"))
base = base.strip().replace(" ", "_")[:60] or "report"
return f"DDA_Report_{run_id}_{base}.pdf"
def build_report_dict(run: DetectionRun, *, include_overlay_b64: bool = False, regions: Optional[List[dict]] = None) -> Dict[str, Any]:
if regions is None:
regions = json.loads(run.regions_json or "[]")
payload: Dict[str, Any] = {
"id": run.id,
"title": run.title,
"method": run.method,
"zone": run.zone or "",
"village": run.village or "",
"statistics": {
"totalPixels": run.total_pixels,
"changedPixels": run.changed_pixels,
"unchangedPixels": run.total_pixels - run.changed_pixels,
"changePercentage": run.change_percentage,
},
"regions": regions,
"regionsCount": run.regions_count,
"overlayUrl": f"/api/overlay/{run.overlay_path}" if run.overlay_path else None,
"beforeFullUrl": f"/api/overlay/{run.before_full_path}" if run.before_full_path else None,
"beforeThumbUrl": f"/api/overlay/{run.before_thumb_path}" if run.before_thumb_path else None,
"afterThumbUrl": f"/api/overlay/{run.after_thumb_path}" if run.after_thumb_path else None,
"afterFullUrl": f"/api/overlay/{run.after_full_path}" if getattr(run, "after_full_path", None) else None,
"createdAt": _isoformat_ist(run.created_at),
"pdfUrl": f"/api/dda/reports/{run.id}/pdf",
}
if include_overlay_b64 and run.overlay_path:
overlay_file = DATA_DIR / run.overlay_path
if overlay_file.exists():
import base64
payload["overlayBase64Png"] = base64.b64encode(overlay_file.read_bytes()).decode("utf-8")
return payload
def generate_report_pdf(run: DetectionRun, *, regions: Optional[List[dict]] = None) -> tuple[bytes, str]:
"""Build PDF bytes and suggested download filename."""
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import mm
from reportlab.platypus import Image as RLImage
from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle
from .geo_regions import region_lat_lng
if regions is None:
regions = json.loads(run.regions_json or "[]")
buf = BytesIO()
doc = SimpleDocTemplate(buf, pagesize=A4, leftMargin=18 * mm, rightMargin=18 * mm, topMargin=16 * mm, bottomMargin=16 * mm)
styles = getSampleStyleSheet()
title_style = ParagraphStyle("ReportTitle", parent=styles["Heading1"], fontSize=16, spaceAfter=8)
sub_style = ParagraphStyle("ReportSub", parent=styles["Normal"], fontSize=10, textColor=colors.grey)
body = styles["Normal"]
location = ", ".join(filter(None, [run.village, run.zone])) or "—"
story = [
Paragraph("DDA Change Detection Report", title_style),
Paragraph(run.title or f"Run #{run.id}", styles["Heading2"]),
Paragraph(f"Generated {_isoformat_ist(run.created_at)} IST", sub_style),
Spacer(1, 8),
Paragraph(f"<b>Method:</b> {run.method or '—'}", body),
Paragraph(f"<b>Location:</b> {location}", body),
Paragraph(f"<b>Change:</b> {run.change_percentage:.2f}% ({run.changed_pixels:,} / {run.total_pixels:,} px)", body),
Paragraph(f"<b>Regions detected:</b> {run.regions_count}", body),
Spacer(1, 10),
]
overlay_path = run.overlay_path
if overlay_path:
overlay_file = DATA_DIR / overlay_path
if overlay_file.exists():
try:
from PIL import Image as PILImage
with PILImage.open(overlay_file) as im:
im = im.convert("RGB")
max_w = 160 * mm
ratio = min(1.0, max_w / im.width)
w, h = int(im.width * ratio), int(im.height * ratio)
thumb = im.resize((w, h), PILImage.Resampling.LANCZOS)
img_buf = BytesIO()
thumb.save(img_buf, format="JPEG", quality=85)
img_buf.seek(0)
story.append(Paragraph("Change overlay", styles["Heading3"]))
story.append(RLImage(img_buf, width=w, height=h))
story.append(Spacer(1, 10))
except Exception:
pass
story.append(Paragraph("Detected regions", styles["Heading3"]))
if regions:
table_data = [["#", "DDA type", "Internal type", "Conf.", "Area (px)", "Lat", "Lng", "Review"]]
for r in regions[:50]:
lat, lng = region_lat_lng(r)
table_data.append([
str(r.get("id", "")),
r.get("ddaChangeType") or r.get("objectType") or "—",
r.get("internalObjectType") or r.get("objectType") or "—",
f'{(r.get("confidence", 0) or 0) * 100:.0f}%',
f'{r.get("area", 0):,}',
f"{lat:.5f}" if lat is not None else "—",
f"{lng:.5f}" if lng is not None else "—",
r.get("reviewStatus") or "pending",
])
tbl = Table(table_data, repeatRows=1, colWidths=[22, 72, 72, 36, 52, 48, 48])
tbl.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#2e33c5")),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("FONTSIZE", (0, 0), (-1, -1), 8),
("GRID", (0, 0), (-1, -1), 0.25, colors.lightgrey),
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#f9f9fb")]),
("VALIGN", (0, 0), (-1, -1), "TOP"),
]))
story.append(tbl)
if len(regions) > 50:
story.append(Spacer(1, 6))
story.append(Paragraph(f"Showing first 50 of {len(regions)} regions.", sub_style))
else:
story.append(Paragraph("No change regions detected.", body))
doc.build(story)
return buf.getvalue(), _safe_filename(run.title or "", run.id)
|