Verimed-AI / pdf_export.py
Arnavdarange's picture
Fix PDF report download
49245cb
Raw
History Blame Contribute Delete
11.6 kB
"""
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")