""" 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")