import matplotlib matplotlib.use('Agg') # Required for server-side plotting import matplotlib.pyplot as plt import matplotlib.patheffects as pe import numpy as np import io import base64 import requests from xhtml2pdf import pisa from django.template import Context, Template def get_base64_image(url): """Fetches image from URL and converts to base64 for PDF embedding.""" if not url: return None try: response = requests.get(url, timeout=5) if response.status_code == 200: return f"data:image/jpeg;base64,{base64.b64encode(response.content).decode('utf-8')}" except Exception as e: print(f"Error fetching image: {e}") return None return None def generate_scatter_plot(model_conf, symptom_score): """ Generates a Linear Regression style plot with a SCIENTIFIC GRID. """ # 1. Setup Figure fig, ax = plt.subplots(figsize=(6, 4), dpi=300) # 2. Formal Gridding (Scientific Look) ax.minorticks_on() ax.grid(which='major', linestyle='-', linewidth=0.6, color='#cbd5e1', alpha=0.8, zorder=0) ax.grid(which='minor', linestyle=':', linewidth=0.4, color='#e2e8f0', alpha=0.5, zorder=0) # Clean Spines ax.spines['top'].set_visible(False) ax.spines['right'].set_visible(False) ax.spines['left'].set_color('#64748b') ax.spines['bottom'].set_color('#64748b') # 3. Generate Population Trend (Green Line) np.random.seed(42) pop_x = np.linspace(5, 95, 150) pop_y = (pop_x * 0.3) + np.random.normal(0, 5, 150) pop_y = np.clip(pop_y, 0, 100) # Plot Population (Green Dots) ax.scatter( pop_x, pop_y, c='#2ecc71', # Green s=50, alpha=0.5, label='Normal Population', zorder=5, edgecolors='none' ) # Visual "Regression Line" ax.plot(pop_x, pop_x * 0.3, color='#27ae60', linestyle='--', alpha=0.6, linewidth=1.5, zorder=4) # 4. Patient Dot (Red, Off the Line based on Risk) ax.scatter( [symptom_score], [model_conf], c='#ef4444', # Red s=50, zorder=10, label='Patient Result', edgecolors='black', linewidth=1 ) # 5. Visual Guide for Distance normal_y_at_x = symptom_score * 0.3 ax.plot( [symptom_score, symptom_score], [model_conf, normal_y_at_x], color='#ef4444', linestyle=':', alpha=0.5, zorder=9 ) # 6. Labels & Regions ax.set_xlabel('Symptom Severity', fontsize=9, fontweight='bold', color='#475569') ax.set_ylabel('AI Risk Score', fontsize=9, fontweight='bold', color='#475569') ax.legend(loc='upper left', fontsize=8, framealpha=0.9, edgecolor='#cbd5e1') # Scaling ax.set_xlim(0, 105) ax.set_ylim(0, 105) # 7. Save buffer = io.BytesIO() plt.tight_layout() plt.savefig(buffer, format='png', facecolor='white', edgecolor='none') buffer.seek(0) plt.close(fig) return f"data:image/png;base64,{base64.b64encode(buffer.getvalue()).decode('utf-8')}" def generate_medical_pdf(test_result): # --- 1. Data Calculation --- model_conf = float(test_result.confidence_score) symptoms = test_result.symptoms_data or {} yes_count = sum(1 for ans in symptoms.values() if isinstance(ans, str) and ans.lower() == 'yes') symptom_score = (yes_count / 8) * 100 mean_score = (model_conf + symptom_score) / 2 is_positive = test_result.result == 'Positive' # --- DYNAMIC RISK RECALCULATION (Fix for PDF) --- # This ensures old "Low" records show as "Medium" if score is 50-80% if is_positive: if model_conf > 80: current_risk_level = "High" elif model_conf >= 50: current_risk_level = "Medium" else: current_risk_level = "Low" else: current_risk_level = "Low" # ------------------------------------------------ # --- 2. Theme Configuration --- theme_color = '#3498db' accent_color = '#60a5fa' light_bg = '#eff6ff' status_text = 'POSITIVE FOR ABNORMALITIES' if is_positive else 'NO ABNORMALITIES DETECTED' # --- 3. Fetch Assets --- xray_img_b64 = get_base64_image(test_result.xray_image_url) scatter_plot_b64 = generate_scatter_plot(model_conf, symptom_score) # --- 4. Medication Logic --- if is_positive: meds_title = "Suggested Clinical Protocol" meds_note = "Standard First-Line Regimen (Requires Prescription)" meds_list = [ {'name': 'Isoniazid (H)', 'dose': '5 mg/kg', 'desc': 'Primary antibiotic for treatment'}, {'name': 'Rifampicin (R)', 'dose': '10 mg/kg', 'desc': 'Broad-spectrum antibiotic'}, {'name': 'Pyrazinamide (Z)', 'dose': '25 mg/kg', 'desc': 'Sterilizing agent'}, {'name': 'Ethambutol (E)', 'dose': '15 mg/kg', 'desc': 'Bacteriostatic agent'} ] else: meds_title = "Preventive Recommendations" meds_note = "Nutritional Support for Respiratory Health" meds_list = [ {'name': 'Vitamin D3', 'dose': '1000 IU', 'desc': 'Immune modulation support'}, {'name': 'Vitamin C', 'dose': '500 mg', 'desc': 'Antioxidant cellular protection'}, {'name': 'Zinc Gluconate', 'dose': '50 mg', 'desc': 'Immune defense enhancement'} ] # --- 5. HTML Template --- html_string = """
RespireX Medical AI
Report ID: #{{ result_id }} | Date: {{ date }}
Patient Demographics
Patient Name
{{ patient.full_name|default:patient.user.email }}
Age / Gender
{{ patient.age|default:"--" }} / {{ patient.gender|default:"--" }}
Patient ID
{{ patient.id }}
Location
{{ patient.city|default:"--" }}
Diagnostic Assessment
{{ status_text }}
Risk Classification: {{ risk_level }} | Confidence: {{ mean_score|floatformat:1 }}%
Radiographic & Comparative Analysis
ANALYZED RADIOGRAPH
{% if xray_img %} {% else %}
Image Not Available
{% endif %}
POPULATION RISK ANALYSIS
{% if scatter_plot %} {% else %}
Chart Generation Failed
{% endif %}
Detailed Metrics
Analysis Metric Score Clinical Significance
AI Model Prediction {{ model_conf|floatformat:1 }}% Computer-Aided Detection (CAD) Score
Symptom Correlation {{ symptom_score|floatformat:1 }}% Self-reported symptom severity index
Composite Score {{ mean_score|floatformat:1 }}% Weighted diagnostic probability
{{ meds_title }}
Note: {{ meds_note }}
{% for med in meds_list %} {% endfor %}
Medication Dosage Indication
{{ med.name }} {{ med.dose }} {{ med.desc }}
DISCLAIMER: This report is generated by the RespireX Artificial Intelligence system.
It is intended for screening purposes only and DOES NOT constitute a final medical diagnosis.
© 2025 RespireX. All rights reserved.
By Team BitBash
""" # --- 6. Render --- template = Template(html_string) context = Context({ 'theme_color': theme_color, 'accent_color': accent_color, 'light_bg': light_bg, 'status_text': status_text, 'patient': test_result.patient, 'result_id': test_result.id, 'date': test_result.date_tested.strftime('%B %d, %Y'), 'risk_level': current_risk_level, # <--- Using the Recalculated Variable 'model_conf': model_conf, 'symptom_score': symptom_score, 'mean_score': mean_score, 'meds_title': meds_title, 'meds_note': meds_note, 'meds_list': meds_list, 'xray_img': xray_img_b64, 'scatter_plot': scatter_plot_b64 }) html = template.render(context) result = io.BytesIO() pisa_status = pisa.CreatePDF(io.BytesIO(html.encode("UTF-8")), dest=result) if pisa_status.err: raise Exception("PDF Generation Error") result.seek(0) return result