Spaces:
Paused
Paused
| """ | |
| PDF export for the Health Passport. | |
| Generates a clean, printable one-page PDF with all passport info, surgeries, | |
| vaccinations, and the emergency QR code embedded -- so a physical printed | |
| copy works exactly like the digital one for a doctor scanning it. | |
| Uses fpdf2 (pure Python, no system dependencies like wkhtmltopdf/Chrome | |
| needed -- important for easy deployment). | |
| """ | |
| import io | |
| import os | |
| from fpdf import FPDF | |
| PRIMARY_RGB = (15, 111, 255) # #0F6FFF | |
| SECONDARY_RGB = (16, 185, 129) # #10B981 | |
| TEXT_RGB = (30, 41, 59) | |
| MUTED_RGB = (100, 116, 139) | |
| def _safe_text(value) -> str: | |
| text = "" if value is None else str(value) | |
| return ( | |
| text.replace("\u2011", "-") | |
| .replace("\u2013", "-") | |
| .replace("\u2014", "-") | |
| .replace("\u2212", "-") | |
| .encode("latin-1", "replace") | |
| .decode("latin-1") | |
| ) | |
| def _configure_pdf_fonts(pdf: FPDF) -> tuple[str, callable]: | |
| """Prefer an embedded Unicode-capable font, fall back to Helvetica.""" | |
| font_dir = r"C:\Windows\Fonts" | |
| font_files = { | |
| "": os.path.join(font_dir, "arial.ttf"), | |
| "B": os.path.join(font_dir, "arialbd.ttf"), | |
| "I": os.path.join(font_dir, "ariali.ttf"), | |
| "BI": os.path.join(font_dir, "arialbi.ttf"), | |
| } | |
| if all(os.path.exists(path) for path in font_files.values()): | |
| pdf.add_font("Arial", "", font_files[""], uni=True) | |
| pdf.add_font("Arial", "B", font_files["B"], uni=True) | |
| pdf.add_font("Arial", "I", font_files["I"], uni=True) | |
| pdf.add_font("Arial", "BI", font_files["BI"], uni=True) | |
| return "Arial", lambda value: "" if value is None else str(value) | |
| return "Helvetica", _safe_text | |
| class PassportPDF(FPDF): | |
| def header(self): | |
| self.set_fill_color(*PRIMARY_RGB) | |
| self.rect(0, 0, 210, 22, style="F") | |
| self.set_text_color(255, 255, 255) | |
| self.set_font("Helvetica", "B", 16) | |
| self.set_xy(10, 6) | |
| self.cell(0, 10, _safe_text("VeriMed AI -- Health Passport"), new_x="LMARGIN", new_y="NEXT") | |
| self.set_font("Helvetica", "", 9) | |
| self.set_xy(10, 15) | |
| self.cell(0, 6, _safe_text("Emergency Medical Information Card"), new_x="LMARGIN", new_y="NEXT") | |
| self.ln(8) | |
| def footer(self): | |
| self.set_y(-15) | |
| self.set_font("Helvetica", "I", 7) | |
| self.set_text_color(*MUTED_RGB) | |
| self.cell(0, 10, _safe_text("Generated by VeriMed AI. Educational project -- not a substitute for official medical records."), align="C") | |
| def section_title(self, title: str): | |
| self.ln(3) | |
| self.set_font("Helvetica", "B", 12) | |
| self.set_text_color(*PRIMARY_RGB) | |
| self.cell(0, 8, _safe_text(title), new_x="LMARGIN", new_y="NEXT") | |
| self.set_draw_color(*PRIMARY_RGB) | |
| self.line(10, self.get_y(), 200, self.get_y()) | |
| self.ln(2) | |
| def field_row(self, label: str, value: str): | |
| self.set_font("Helvetica", "B", 10) | |
| self.set_text_color(*MUTED_RGB) | |
| self.cell(55, 7, _safe_text(label), new_x="RIGHT", new_y="TOP") | |
| self.set_font("Helvetica", "", 10) | |
| self.set_text_color(*TEXT_RGB) | |
| self.multi_cell(135, 7, _safe_text(value or "Not provided"), new_x="LMARGIN", new_y="NEXT") | |
| def generate_passport_pdf(passport: dict, surgeries: list[dict], vaccinations: list[dict], qr_png_bytes: bytes) -> bytes: | |
| pdf = PassportPDF(format="A4") | |
| font_family, normalize = _configure_pdf_fonts(pdf) | |
| pdf.add_page() | |
| pdf.set_auto_page_break(auto=True, margin=20) | |
| # ---- Personal details ---- | |
| pdf.section_title(normalize("Personal Details")) | |
| pdf.field_row(normalize("Full Name:"), normalize(passport.get("full_name", ""))) | |
| pdf.field_row(normalize("Date of Birth:"), normalize(passport.get("date_of_birth", ""))) | |
| pdf.field_row(normalize("Blood Group:"), normalize(passport.get("blood_group", ""))) | |
| # ---- Medical info ---- | |
| pdf.section_title(normalize("Medical Information")) | |
| pdf.field_row(normalize("Allergies:"), normalize(passport.get("allergies") or "None listed")) | |
| pdf.field_row(normalize("Chronic Conditions:"), normalize(passport.get("chronic_conditions") or "None listed")) | |
| pdf.field_row(normalize("Current Medicines:"), normalize(passport.get("current_medicines") or "None listed")) | |
| # ---- Surgeries ---- | |
| pdf.section_title(normalize("Surgical History")) | |
| if surgeries: | |
| for s in surgeries: | |
| pdf.field_row(normalize(f"{s['year']}:") , normalize(s["description"])) | |
| else: | |
| pdf.set_font(font_family, "I", 10) | |
| pdf.set_text_color(*MUTED_RGB) | |
| pdf.cell(0, 7, normalize("None recorded."), new_x="LMARGIN", new_y="NEXT") | |
| # ---- Vaccinations ---- | |
| pdf.section_title(normalize("Vaccination Record")) | |
| if vaccinations: | |
| for v in vaccinations: | |
| pdf.field_row(normalize(f"{v['month']} {v['year']}:") , normalize(v["vaccine_name"])) | |
| else: | |
| pdf.set_font(font_family, "I", 10) | |
| pdf.set_text_color(*MUTED_RGB) | |
| pdf.cell(0, 7, normalize("None recorded."), new_x="LMARGIN", new_y="NEXT") | |
| # ---- Emergency contact ---- | |
| pdf.section_title(normalize("Emergency Contact")) | |
| pdf.field_row(normalize("Name:"), normalize(passport.get("emergency_contact_name", ""))) | |
| pdf.field_row(normalize("Phone:"), normalize(passport.get("emergency_contact_phone", ""))) | |
| # ---- QR code ---- | |
| pdf.section_title(normalize("Emergency QR Code")) | |
| pdf.set_font(font_family, "", 9) | |
| pdf.set_text_color(*MUTED_RGB) | |
| pdf.multi_cell(0, 6, normalize("Scan this code to view this person's emergency medical summary instantly -- no app or login required.")) | |
| pdf.ln(2) | |
| qr_stream = io.BytesIO(qr_png_bytes) | |
| pdf.image(qr_stream, x=10, y=pdf.get_y(), w=35, h=35) | |
| pdf.ln(40) | |
| output = pdf.output() | |
| return bytes(output) | |
| def generate_verification_report_pdf( | |
| claim_text: str, | |
| verdict: str, | |
| confidence: int, | |
| explanation: str, | |
| entities: list[dict], | |
| sources: list[str], | |
| method_comparison: dict | None, | |
| ) -> bytes: | |
| """ | |
| Generates a printable one-page report for a single claim verification -- | |
| used by the 'Download Report' button on the Verify Claim results page. | |
| """ | |
| pdf = FPDF(format="A4") | |
| font_family, normalize = _configure_pdf_fonts(pdf) | |
| pdf.add_page() | |
| pdf.set_auto_page_break(auto=True, margin=20) | |
| # ---- Header band ---- | |
| pdf.set_fill_color(*PRIMARY_RGB) | |
| pdf.rect(0, 0, 210, 22, style="F") | |
| pdf.set_text_color(255, 255, 255) | |
| pdf.set_font(font_family, "B", 16) | |
| pdf.set_xy(10, 6) | |
| pdf.cell(0, 10, normalize("VeriMed AI -- Claim Verification Report"), new_x="LMARGIN", new_y="NEXT") | |
| pdf.set_font(font_family, "", 9) | |
| pdf.set_xy(10, 15) | |
| pdf.cell(0, 6, normalize("AI-Generated, Evidence-Grounded Fact-Check"), new_x="LMARGIN", new_y="NEXT") | |
| pdf.ln(14) | |
| # ---- Claim text ---- | |
| pdf.set_font(font_family, "B", 10) | |
| pdf.set_text_color(*MUTED_RGB) | |
| pdf.cell(0, 6, normalize("CLAIM SUBMITTED"), new_x="LMARGIN", new_y="NEXT") | |
| pdf.set_font(font_family, "I", 12) | |
| pdf.set_text_color(*TEXT_RGB) | |
| pdf.multi_cell(0, 7, normalize(f'"{claim_text}"')) | |
| pdf.ln(3) | |
| # ---- Verdict block ---- | |
| verdict_colors = { | |
| "True": (16, 185, 129), "False": (239, 68, 68), | |
| "Misleading": (245, 158, 11), "Unverified": (100, 116, 139), | |
| } | |
| v_color = verdict_colors.get(verdict, (100, 116, 139)) | |
| pdf.set_fill_color(*v_color) | |
| pdf.set_text_color(255, 255, 255) | |
| pdf.set_font(font_family, "B", 14) | |
| display_verdict = "Not Reliable" if verdict == "Unverified" else verdict | |
| pdf.cell(90, 12, normalize(f" VERDICT: {display_verdict.upper()}"), fill=True, new_x="LMARGIN", new_y="NEXT") | |
| pdf.ln(2) | |
| pdf.set_font(font_family, "B", 10) | |
| pdf.set_text_color(*MUTED_RGB) | |
| pdf.cell(0, 6, normalize(f"Trust Score: {confidence}%"), new_x="LMARGIN", new_y="NEXT") | |
| pdf.ln(2) | |
| pdf.set_font(font_family, "", 10) | |
| pdf.set_text_color(*TEXT_RGB) | |
| pdf.multi_cell(0, 6, normalize(explanation or "No explanation available.")) | |
| pdf.ln(3) | |
| # ---- Entities ---- | |
| if entities: | |
| pdf.set_font(font_family, "B", 10) | |
| pdf.set_text_color(*PRIMARY_RGB) | |
| pdf.cell(0, 7, normalize("BioBERT NER Entities Detected"), new_x="LMARGIN", new_y="NEXT") | |
| pdf.set_font(font_family, "", 9) | |
| pdf.set_text_color(*TEXT_RGB) | |
| entity_line = ", ".join(normalize(e.get("text", "")) for e in entities) | |
| pdf.multi_cell(0, 6, normalize(entity_line)) | |
| pdf.ln(2) | |
| # ---- Sources ---- | |
| pdf.set_font(font_family, "B", 10) | |
| pdf.set_text_color(*SECONDARY_RGB) | |
| pdf.cell(0, 7, normalize("Cited Sources"), new_x="LMARGIN", new_y="NEXT") | |
| pdf.set_font(font_family, "", 9) | |
| pdf.set_text_color(*TEXT_RGB) | |
| pdf.multi_cell(0, 6, normalize(", ".join(sources) if sources else "No sources returned.")) | |
| pdf.ln(4) | |
| # ---- Method comparison table ---- | |
| if method_comparison and method_comparison.get("methods"): | |
| pdf.set_font(font_family, "B", 12) | |
| pdf.set_text_color(*PRIMARY_RGB) | |
| pdf.cell(0, 8, normalize("Detection Method Comparison"), new_x="LMARGIN", new_y="NEXT") | |
| pdf.set_draw_color(*PRIMARY_RGB) | |
| pdf.line(10, pdf.get_y(), 200, pdf.get_y()) | |
| pdf.ln(3) | |
| for m in method_comparison["methods"]: | |
| pdf.set_font(font_family, "B", 10) | |
| pdf.set_text_color(*TEXT_RGB) | |
| winner_tag = " [MOST RELIABLE]" if m.get("is_winner") else "" | |
| pdf.cell(0, 6, normalize(f"{m['name']}: {m['score']}/100 -- {m['label']}{winner_tag}"), new_x="LMARGIN", new_y="NEXT") | |
| pdf.set_font(font_family, "", 9) | |
| pdf.set_text_color(*MUTED_RGB) | |
| pdf.multi_cell(0, 5, normalize(m.get("description", ""))) | |
| pdf.ln(1) | |
| pdf.ln(2) | |
| pdf.set_font(font_family, "B", 10) | |
| pdf.set_text_color(*PRIMARY_RGB) | |
| pdf.cell(0, 6, normalize("Conclusion"), new_x="LMARGIN", new_y="NEXT") | |
| pdf.set_font(font_family, "", 9) | |
| pdf.set_text_color(*TEXT_RGB) | |
| pdf.multi_cell(0, 6, normalize(method_comparison.get("conclusion", ""))) | |
| # ---- Footer ---- | |
| pdf.set_y(-15) | |
| pdf.set_font(font_family, "I", 7) | |
| pdf.set_text_color(*MUTED_RGB) | |
| pdf.cell(0, 10, normalize("Generated by VeriMed AI. Educational project -- always verify critical health decisions with a professional."), align="C") | |
| output = pdf.output() | |
| return bytes(output) | |
| if __name__ == "__main__": | |
| # Quick manual test -- run: python pdf_export.py | |
| import health_passport as hp | |
| import os | |
| if os.path.exists(hp.DB_FILE): | |
| os.remove(hp.DB_FILE) | |
| hp.init_passport_table() | |
| hp.save_passport(1, { | |
| "full_name": "Jane Doe", "blood_group": "O+", "date_of_birth": "01-01-1995", | |
| "allergies": "Penicillin", "chronic_conditions": "Asthma", | |
| "current_medicines": "Ventolin inhaler", "emergency_contact_name": "John Doe", | |
| "emergency_contact_phone": "+91-9999999999", | |
| }) | |
| hp.add_surgery(1, "2019", "Appendectomy") | |
| hp.add_vaccination(1, "COVID-19 Booster", "March", "2024") | |
| passport = hp.get_passport(1) | |
| surgeries = hp.get_surgeries(1) | |
| vaccinations = hp.get_vaccinations(1) | |
| qr_bytes = hp.generate_qr_code(passport["share_token"], "http://localhost:5000") | |
| pdf_bytes = generate_passport_pdf(passport, surgeries, vaccinations, qr_bytes) | |
| with open("test_passport_output.pdf", "wb") as f: | |
| f.write(pdf_bytes) | |
| print(f"✅ PDF generated: {len(pdf_bytes)} bytes, saved to test_passport_output.pdf") |