Spaces:
Running on Zero
Running on Zero
| import io | |
| import tempfile | |
| from datetime import datetime | |
| from fpdf import FPDF | |
| from PIL import Image as PILImage | |
| PAGE_W = 210 | |
| PAGE_H = 297 | |
| MARGIN = 15 | |
| USABLE_W = PAGE_W - 2 * MARGIN # 180 mm | |
| N_COLS = 3 | |
| COL_GAP = 3 | |
| IMG_W = (USABLE_W - (N_COLS - 1) * COL_GAP) / N_COLS # ~58 mm | |
| IMG_H = IMG_W | |
| CAP_H = 5 | |
| INPUT_W = 80 | |
| FS_TITLE = 16 | |
| FS_HEAD = 11 | |
| FS_BODY = 9 | |
| FS_SMALL = 7 | |
| FS_MONO = 8 | |
| DARK_GRAY = (50, 50, 50) | |
| MID_GRAY = (110, 110, 110) | |
| LIGHT_GRAY = (200, 200, 200) | |
| BLACK = (0, 0, 0) | |
| RED_TEXT = (180, 30, 30) | |
| GREEN_TEXT = (30, 120, 30) | |
| TBL_HDR = (230, 238, 248) | |
| TBL_ALT = (245, 249, 253) | |
| TBL_BOR = (185, 200, 218) | |
| LABELS = { | |
| "YOLO conf": "YOLO Confidence", | |
| "Seg failed": "Segmentation Failed", | |
| "vCDR": "Vertical CDR", | |
| "hCDR": "Horizontal CDR", | |
| "Inferior rim": "Inferior Rim Ratio", | |
| "Superior rim": "Superior Rim Ratio", | |
| "SI compliant": "Inferior > Superior Rim Rule", | |
| "NRR area": "NRR Area Ratio", | |
| "p holistic": "EfficientNet Probability", | |
| "p fused": "Fused Glaucoma Score", | |
| "Threshold": "Decision Threshold", | |
| } | |
| def _safe(text: str) -> str: | |
| text = (text | |
| .replace('β', '-').replace('β', '-') | |
| .replace('β', "'").replace('β', "'") | |
| .replace('β', '"').replace('β', '"') | |
| .replace('β¦', '...').replace('β’', '*') | |
| .replace('Β°', 'deg').replace('Β±', '+/-') | |
| ) | |
| return text.encode('latin-1', errors='replace').decode('latin-1') | |
| def _png(img: PILImage.Image) -> io.BytesIO: | |
| buf = io.BytesIO() | |
| img.save(buf, format='PNG') | |
| buf.seek(0) | |
| return buf | |
| def _fmt(v) -> str: | |
| if v is True: return "Yes" | |
| if v is False: return "No" | |
| if isinstance(v, float): return f"{v:.4f}" | |
| return str(v) | |
| def _hline(pdf: FPDF): | |
| y = pdf.get_y() | |
| pdf.set_draw_color(*LIGHT_GRAY) | |
| pdf.set_line_width(0.25) | |
| pdf.line(MARGIN, y, PAGE_W - MARGIN, y) | |
| pdf.set_draw_color(0, 0, 0) | |
| pdf.set_line_width(0.2) | |
| def _section(pdf: FPDF, title: str, gap_before: float = 7): | |
| pdf.ln(gap_before) | |
| pdf.set_font("Helvetica", style='B', size=FS_HEAD) | |
| pdf.set_text_color(*DARK_GRAY) | |
| pdf.cell(0, 6, title, new_x='LMARGIN', new_y='NEXT') | |
| pdf.ln(2) | |
| pdf.set_text_color(*BLACK) | |
| def _image_grid(pdf: FPDF, items: list): | |
| """ | |
| 3-column grid of (caption, PIL Image). | |
| Uses set_xy + image(w, h) [no explicit y] per image. | |
| Caption placed with pdf.text() β zero cursor impact. | |
| row_y is a local variable, never derived from get_y() after image placement. | |
| """ | |
| row_y = None | |
| for i, (caption, img) in enumerate(items): | |
| col = i % N_COLS | |
| if col == 0: | |
| row_needed = IMG_H + CAP_H + 4 | |
| if pdf.get_y() + row_needed > PAGE_H - MARGIN: | |
| pdf.add_page() | |
| row_y = pdf.get_y() | |
| x = MARGIN + col * (IMG_W + COL_GAP) | |
| # Place image at (x, row_y) without specifying y explicitly | |
| pdf.set_xy(x, row_y) | |
| pdf.image(_png(img), w=IMG_W, h=IMG_H) | |
| # fpdf2 advances cursor to (x + IMG_W, row_y + IMG_H) β we ignore it | |
| # Caption: pdf.text() places text at absolute coords, does NOT move cursor | |
| pdf.set_font("Helvetica", style='', size=FS_SMALL) | |
| pdf.set_text_color(*MID_GRAY) | |
| cap = _safe(caption) | |
| tw = pdf.get_string_width(cap) | |
| pdf.text(x + (IMG_W - tw) / 2, row_y + IMG_H + 4, cap) | |
| pdf.set_text_color(*BLACK) | |
| # After last column (or last item), advance cursor past the row | |
| if col == N_COLS - 1 or i == len(items) - 1: | |
| pdf.set_xy(MARGIN, row_y + IMG_H + CAP_H + 4) | |
| def _table(pdf: FPDF, metrics: dict): | |
| """ | |
| 2-column table. Uses new_y='TOP' for first cell (y stays), | |
| new_y='NEXT' for last cell (y advances one row). | |
| No TMARGIN β that resets y to page top and breaks layout. | |
| """ | |
| lw = USABLE_W * 0.68 | |
| vw = USABLE_W * 0.32 | |
| rh = 7 | |
| pdf.set_draw_color(*BLACK) | |
| # Header row | |
| pdf.set_font("Helvetica", style='B', size=FS_SMALL) | |
| pdf.set_text_color(*BLACK) | |
| pdf.cell(lw, rh, "BIOMARKER / SCORE", border=1, | |
| new_x='RIGHT', new_y='TOP') | |
| pdf.cell(vw, rh, "VALUE", border=1, | |
| new_x='LMARGIN', new_y='NEXT') | |
| # Data rows | |
| for k, v in metrics.items(): | |
| pdf.set_font("Helvetica", style='', size=FS_BODY) | |
| pdf.set_text_color(*BLACK) | |
| pdf.cell(lw, rh, LABELS.get(k, k), border=1, | |
| new_x='RIGHT', new_y='TOP') | |
| pdf.set_font("Courier", style='', size=FS_MONO) | |
| pdf.cell(vw, rh, _fmt(v), border=1, | |
| new_x='LMARGIN', new_y='NEXT') | |
| def generate_pdf_report(data: dict) -> str: | |
| pdf = FPDF(orientation='P', unit='mm', format='A4') | |
| pdf.set_margins(MARGIN, MARGIN, MARGIN) | |
| pdf.set_auto_page_break(auto=True, margin=MARGIN) | |
| pdf.add_page() | |
| # ββ TITLE ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| pdf.set_font("Helvetica", style='B', size=FS_TITLE) | |
| pdf.set_text_color(*DARK_GRAY) | |
| pdf.cell(0, 9, "GlauNET - Glaucoma Screening Report", | |
| align='C', new_x='LMARGIN', new_y='NEXT') | |
| pdf.set_font("Helvetica", style='', size=FS_SMALL) | |
| pdf.set_text_color(*MID_GRAY) | |
| pdf.cell(0, 5, f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", | |
| align='C', new_x='LMARGIN', new_y='NEXT') | |
| pdf.set_font("Helvetica", style='I', size=FS_SMALL - 1) | |
| pdf.cell(0, 4, | |
| "Research prototype - not a substitute for diagnosis by a qualified ophthalmologist.", | |
| align='C', new_x='LMARGIN', new_y='NEXT') | |
| pdf.ln(3) | |
| _hline(pdf) | |
| pdf.set_text_color(*BLACK) | |
| # ββ DIAGNOSIS ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| _section(pdf, "Automated Diagnosis", gap_before=5) | |
| decision = data["decision"] | |
| is_positive = "SUSPECTED" in decision | |
| pdf.set_font("Helvetica", style='B', size=14) | |
| pdf.set_text_color(*BLACK) | |
| pdf.cell(0, 9, _safe(decision), align='C', new_x='LMARGIN', new_y='NEXT') | |
| # ββ INPUT FUNDUS IMAGE βββββββββββββββββββββββββββββββββββββββ | |
| _section(pdf, "Input Fundus Image") | |
| if pdf.get_y() + INPUT_W + 8 > PAGE_H - MARGIN: | |
| pdf.add_page() | |
| x_img = MARGIN + (USABLE_W - INPUT_W) / 2 | |
| img_top = pdf.get_y() # save y BEFORE image | |
| pdf.set_xy(x_img, img_top) | |
| pdf.image(_png(data["input_image"]), w=INPUT_W, h=INPUT_W) | |
| # fpdf2 cursor is now at img_top + INPUT_W β we IGNORE it and use img_top | |
| pdf.set_font("Helvetica", style='I', size=FS_SMALL) | |
| pdf.set_text_color(*MID_GRAY) | |
| cap = "Original fundus photograph" | |
| pdf.text(MARGIN + (USABLE_W - pdf.get_string_width(cap)) / 2, | |
| img_top + INPUT_W + 4, cap) | |
| pdf.set_text_color(*BLACK) | |
| # Advance cursor manually: img_top + image height + caption gap | |
| pdf.set_xy(MARGIN, img_top + INPUT_W + 8) | |
| # ββ SEGMENTATION PIPELINE (first 5 images) βββββββββββββββββββ | |
| all_images = list(data["images"].items()) | |
| _section(pdf, "Segmentation Pipeline") | |
| _image_grid(pdf, all_images[:5]) | |
| # ββ GRADCAM ANALYSIS (last 4 images) βββββββββββββββββββββββββ | |
| _section(pdf, "GradCAM Analysis") | |
| _image_grid(pdf, all_images[5:]) | |
| # ββ CLINICAL BIOMARKERS ββββββββββββββββββββββββββββββββββββββ | |
| _section(pdf, "Clinical Biomarkers") | |
| _table(pdf, data["metrics"]) | |
| pdf.ln(3) | |
| # ββ MEDGEMMA CLINICAL REPORT βββββββββββββββββββββββββββββββββ | |
| _section(pdf, "MedGemma Clinical Report") | |
| pdf.set_font("Courier", style='', size=FS_MONO) | |
| pdf.set_text_color(*DARK_GRAY) | |
| report = _safe(data.get("report", "")) | |
| if report: | |
| pdf.multi_cell(USABLE_W, 5, report) | |
| else: | |
| pdf.set_text_color(*MID_GRAY) | |
| pdf.cell(0, 5, "(No report generated)", new_x='LMARGIN', new_y='NEXT') | |
| pdf.set_text_color(*BLACK) | |
| tmp = tempfile.NamedTemporaryFile( | |
| suffix=".pdf", prefix="glaunet_report_", delete=False | |
| ) | |
| tmp.close() | |
| pdf.output(tmp.name) | |
| return tmp.name | |