Causes of blindness and symptoms of eye disorders
leading causes of blindness worldwide 2024 WHO

| Type | Symptoms |
|---|---|
| Open-angle glaucoma | Often silent early; gradual peripheral (tunnel) vision loss; discovered incidentally on tonometry |
| Angle-closure glaucoma (acute) | Sudden severe eye pain, headache, nausea/vomiting, halos around lights, blurred vision, red eye |
| Neovascular glaucoma | Secondary to diabetes/vascular disease; severe vision loss |
| Steroid-induced glaucoma | Follows prolonged corticosteroid eye drops or systemic steroids |
| Congenital glaucoma | Enlarged eyes (buphthalmos), tearing, light sensitivity |
| Cause | Key Features |
|---|---|
| Corneal disease/scarring | Trauma, infection, chemical burns; corneal transplant may restore vision |
| Temporal arteritis (GCA) | Older adults >50; headache, jaw claudication, elevated ESR/CRP; sudden vision loss |
| Cortical blindness | Bilateral occipital lobe infarcts; can cause complete loss of conscious vision |
| Amaurosis fugax | Transient monocular blindness from carotid emboli; TIA equivalent - 50% risk of stroke within 1 month |
| Amblyopia | "Lazy eye" - reduced vision from abnormal visual development in childhood |
| Vitamin A deficiency | Causes night blindness and xerophthalmia; major cause in developing countries |
| Hereditary retinal dystrophies | Retinitis pigmentosa: progressive peripheral then central loss; night blindness early |
| Hypertensive retinopathy | Chronic hypertension causes arteriolar changes; severe cases cause vision loss |
| Presentation | Most Likely Cause |
|---|---|
| Painful, with halos, nausea | Acute angle-closure glaucoma |
| Painless, sudden, monocular (cherry-red spot) | Central retinal artery occlusion |
| Painless, flashes and curtain | Retinal detachment |
| Painless, older adult with headache/jaw pain | Temporal arteritis / ischemic optic neuropathy |
Create a clinical decision summary PDF for diagnosing sudden vision loss
ls -la /home/daytona/workspace/ 2>/dev/null || mkdir -p /home/daytona/workspace/
mkdir -p /home/daytona/workspace/sudden-vision-loss && python3 -c "import reportlab; print(reportlab.Version)"
~/sudden-vision-loss/build_pdf.py
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import mm, cm
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, KeepTogether, PageBreak
)
from reportlab.graphics.shapes import Drawing, Rect, String, Line, Polygon, Circle
from reportlab.graphics import renderPDF
from reportlab.graphics.charts.flowcharts import FlowChart
import os
OUTPUT = "/home/daytona/workspace/sudden-vision-loss/Sudden_Vision_Loss_Clinical_Decision_Summary.pdf"
# ── Colour palette ────────────────────────────────────────────────────────────
NAVY = colors.HexColor("#1A3A5C")
MED_BLUE = colors.HexColor("#2563A8")
LIGHT_BLUE= colors.HexColor("#EAF2FB")
RED = colors.HexColor("#C0392B")
RED_BG = colors.HexColor("#FDEDEC")
ORANGE = colors.HexColor("#E67E22")
ORANGE_BG = colors.HexColor("#FEF5EC")
GREEN = colors.HexColor("#1A7A4A")
GREEN_BG = colors.HexColor("#E9F7EF")
YELLOW_BG = colors.HexColor("#FEFDE7")
GREY = colors.HexColor("#7F8C8D")
LIGHT_GREY= colors.HexColor("#F4F6F7")
DARK_GREY = colors.HexColor("#2C3E50")
WHITE = colors.white
BLACK = colors.HexColor("#1A1A1A")
DIVIDER = colors.HexColor("#D0D9E4")
# ── Styles ────────────────────────────────────────────────────────────────────
styles = getSampleStyleSheet()
def S(name, **kw):
return ParagraphStyle(name, **kw)
TITLE_STYLE = S("Title",
fontName="Helvetica-Bold", fontSize=22, textColor=WHITE,
leading=28, alignment=TA_CENTER, spaceAfter=4)
SUBTITLE_STYLE = S("Subtitle",
fontName="Helvetica", fontSize=11, textColor=colors.HexColor("#BDD7EE"),
leading=16, alignment=TA_CENTER, spaceAfter=2)
SECTION_HDR = S("SectionHdr",
fontName="Helvetica-Bold", fontSize=13, textColor=NAVY,
leading=18, spaceBefore=10, spaceAfter=4,
borderPad=4)
SUBSEC_HDR = S("SubsecHdr",
fontName="Helvetica-Bold", fontSize=10, textColor=MED_BLUE,
leading=14, spaceBefore=6, spaceAfter=2)
BODY = S("Body",
fontName="Helvetica", fontSize=9, textColor=BLACK,
leading=13, spaceAfter=2)
BODY_BOLD = S("BodyBold",
fontName="Helvetica-Bold", fontSize=9, textColor=BLACK,
leading=13, spaceAfter=2)
SMALL = S("Small",
fontName="Helvetica", fontSize=8, textColor=GREY,
leading=11, spaceAfter=1)
SMALL_BOLD = S("SmallBold",
fontName="Helvetica-Bold", fontSize=8, textColor=DARK_GREY,
leading=11)
TABLE_HDR = S("TableHdr",
fontName="Helvetica-Bold", fontSize=9, textColor=WHITE,
leading=12, alignment=TA_CENTER)
TABLE_CELL = S("TableCell",
fontName="Helvetica", fontSize=8.5, textColor=BLACK,
leading=12)
TABLE_CELL_BOLD = S("TableCellBold",
fontName="Helvetica-Bold", fontSize=8.5, textColor=BLACK,
leading=12)
WARNING = S("Warning",
fontName="Helvetica-Bold", fontSize=9, textColor=RED,
leading=13)
ALERT_HDR = S("AlertHdr",
fontName="Helvetica-Bold", fontSize=10, textColor=RED,
leading=14, spaceBefore=4, spaceAfter=2)
GREEN_HDR = S("GreenHdr",
fontName="Helvetica-Bold", fontSize=10, textColor=GREEN,
leading=14, spaceBefore=4, spaceAfter=2)
CAPTION = S("Caption",
fontName="Helvetica-Oblique", fontSize=7.5, textColor=GREY,
leading=10, alignment=TA_CENTER)
FOOTER_STYLE = S("Footer",
fontName="Helvetica", fontSize=7, textColor=GREY,
leading=10, alignment=TA_CENTER)
# ── Page setup ────────────────────────────────────────────────────────────────
PAGE_W, PAGE_H = A4
L_MARGIN = R_MARGIN = 18*mm
T_MARGIN = 15*mm
B_MARGIN = 18*mm
def header_footer(canvas, doc):
canvas.saveState()
# top bar
canvas.setFillColor(NAVY)
canvas.rect(0, PAGE_H - 12*mm, PAGE_W, 12*mm, stroke=0, fill=1)
canvas.setFillColor(WHITE)
canvas.setFont("Helvetica-Bold", 8)
canvas.drawString(L_MARGIN, PAGE_H - 8*mm, "CLINICAL DECISION SUMMARY | SUDDEN VISION LOSS")
canvas.setFont("Helvetica", 7)
canvas.drawRightString(PAGE_W - R_MARGIN, PAGE_H - 8*mm, "For clinical reference only - not a substitute for clinical judgment")
# bottom bar
canvas.setFillColor(NAVY)
canvas.rect(0, 0, PAGE_W, 10*mm, stroke=0, fill=1)
canvas.setFillColor(WHITE)
canvas.setFont("Helvetica", 7)
canvas.drawString(L_MARGIN, 3.5*mm, "Sources: Textbook of Family Medicine 9e | Goldman-Cecil Medicine | Robbins & Kumar Basic Pathology | Adams & Victor Neurology")
canvas.drawRightString(PAGE_W - R_MARGIN, 3.5*mm, f"Page {doc.page}")
canvas.restoreState()
def first_page(canvas, doc):
canvas.saveState()
# full-width header banner
canvas.setFillColor(NAVY)
canvas.rect(0, PAGE_H - 52*mm, PAGE_W, 52*mm, stroke=0, fill=1)
# accent stripe
canvas.setFillColor(MED_BLUE)
canvas.rect(0, PAGE_H - 53.5*mm, PAGE_W, 1.5*mm, stroke=0, fill=1)
# footer
canvas.setFillColor(NAVY)
canvas.rect(0, 0, PAGE_W, 10*mm, stroke=0, fill=1)
canvas.setFillColor(WHITE)
canvas.setFont("Helvetica", 7)
canvas.drawString(L_MARGIN, 3.5*mm, "Sources: Textbook of Family Medicine 9e | Goldman-Cecil Medicine | Robbins & Kumar Basic Pathology | Adams & Victor Neurology")
canvas.drawRightString(PAGE_W - R_MARGIN, 3.5*mm, "Page 1")
canvas.restoreState()
def later_pages(canvas, doc):
header_footer(canvas, doc)
# ── Helper: coloured box ──────────────────────────────────────────────────────
def colour_box(content_rows, bg=LIGHT_BLUE, border=MED_BLUE,
col_widths=None, hdr_row=False):
"""Wrap content in a styled single-column (or multi-column) table."""
data = [[Paragraph(c, BODY) if isinstance(c, str) else c
for c in (row if isinstance(row, (list, tuple)) else [row])]
for row in content_rows]
col_w = col_widths or [PAGE_W - L_MARGIN - R_MARGIN - 4*mm]
t = Table(data, colWidths=col_w, hAlign="LEFT")
style_cmds = [
("BACKGROUND", (0,0), (-1,-1), bg),
("BOX", (0,0), (-1,-1), 0.8, border),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING", (0,0), (-1,-1), 8),
("TOPPADDING", (0,0), (-1,-1), 6),
("BOTTOMPADDING", (0,0), (-1,-1), 6),
("ROWBACKGROUNDS", (0,0), (-1,-1), [bg]),
]
if hdr_row:
style_cmds += [
("BACKGROUND", (0,0), (-1,0), border),
("TEXTCOLOR", (0,0), (-1,0), WHITE),
]
t.setStyle(TableStyle(style_cmds))
return t
def section_label(text, colour=NAVY, bg=LIGHT_BLUE):
d = Drawing(PAGE_W - L_MARGIN - R_MARGIN, 10*mm)
d.add(Rect(0, 0, PAGE_W - L_MARGIN - R_MARGIN, 10*mm,
fillColor=bg, strokeColor=colour, strokeWidth=0.5))
d.add(Rect(0, 0, 4, 10*mm, fillColor=colour, strokeWidth=0))
d.add(String(10, 3, text,
fontName="Helvetica-Bold", fontSize=11, fillColor=colour))
return d
# ── Build story ───────────────────────────────────────────────────────────────
story = []
FULL_W = PAGE_W - L_MARGIN - R_MARGIN
# ════════════════════════════════════════════════════════════
# PAGE 1 HEADER (drawn by first_page callback, put text on top)
# ════════════════════════════════════════════════════════════
story.append(Spacer(1, 38*mm)) # space below banner
story.append(Paragraph("SUDDEN VISION LOSS", TITLE_STYLE))
story.append(Paragraph("Clinical Decision Summary | Rapid Differential Diagnosis & Management", SUBTITLE_STYLE))
story.append(Paragraph("Emergency & Primary Care Reference | June 2026", SUBTITLE_STYLE))
story.append(Spacer(1, 8*mm))
# ── Red flag box ─────────────────────────────────────────────
rf_data = [
[Paragraph("<b>⚠ IMMEDIATE OPHTHALMOLOGY REFERRAL REQUIRED for all causes listed below</b>", WARNING)],
[Paragraph("Sudden vision loss is a medical emergency until proven otherwise. Act within minutes to hours.", BODY)],
]
rf_tbl = Table(rf_data, colWidths=[FULL_W])
rf_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), RED_BG),
("BOX", (0,0), (-1,-1), 1.2, RED),
("LEFTPADDING", (0,0), (-1,-1), 10),
("RIGHTPADDING", (0,0), (-1,-1), 10),
("TOPPADDING", (0,0), (-1,-1), 7),
("BOTTOMPADDING", (0,0), (-1,-1), 7),
]))
story.append(rf_tbl)
story.append(Spacer(1, 6*mm))
# ── Section 1: Initial Assessment ────────────────────────────
story.append(section_label("1. INITIAL RAPID ASSESSMENT"))
story.append(Spacer(1, 3*mm))
assess_cols = [FULL_W * 0.48, FULL_W * 0.04, FULL_W * 0.48]
left_assess = [
Paragraph("<b>History - Key Questions</b>", SUBSEC_HDR),
Paragraph("• Onset: seconds / minutes / hours / days?", BODY),
Paragraph("• Monocular (one eye) vs. binocular (both eyes)?", BODY),
Paragraph("• Painless or painful?", BODY),
Paragraph("• Complete loss or partial (quadrant, central, peripheral)?", BODY),
Paragraph("• Preceding symptoms: flashes, floaters, curtain, halos?", BODY),
Paragraph("• Associated: headache, jaw claudication, diplopia, weakness?", BODY),
Paragraph("• PMH: diabetes, hypertension, AF, carotid disease, SLE, HIV?", BODY),
Paragraph("• Medications: corticosteroids, hydroxychloroquine, ethambutol?", BODY),
Paragraph("• Recent trauma or eye surgery?", BODY),
]
right_assess = [
Paragraph("<b>Examination - Must-Do Steps</b>", SUBSEC_HDR),
Paragraph("• Visual acuity (each eye separately)", BODY),
Paragraph("• Visual field confrontation testing", BODY),
Paragraph("• Pupillary reactions - check for RAPD (relative afferent pupillary defect)", BODY),
Paragraph("• Intraocular pressure (tonometry) - rule out acute glaucoma", BODY),
Paragraph("• Slit-lamp exam: anterior chamber, cornea, lens", BODY),
Paragraph("• Dilated fundoscopy: disc, vessels, macula, retina", BODY),
Paragraph("• Cardiovascular exam: BP, pulse, carotid bruits", BODY),
Paragraph("• Temporal artery palpation if age >50", BODY),
]
assess_tbl = Table(
[[left_assess, Spacer(1,1), right_assess]],
colWidths=assess_cols
)
assess_tbl.setStyle(TableStyle([
("VALIGN", (0,0), (-1,-1), "TOP"),
("LEFTPADDING", (0,0), (-1,-1), 0),
("RIGHTPADDING", (0,0), (-1,-1), 0),
("TOPPADDING", (0,0), (-1,-1), 0),
("BOTTOMPADDING", (0,0), (-1,-1), 0),
]))
story.append(assess_tbl)
story.append(Spacer(1, 5*mm))
# RAPD note
rapd_box = Table([[Paragraph(
"<b>RAPD (Marcus Gunn pupil):</b> Swinging flashlight test. "
"Affected pupil dilates when light swings to it. "
"Indicates unilateral optic nerve or severe retinal disease (optic neuritis, CRAO, ION).",
BODY)]], colWidths=[FULL_W])
rapd_box.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), YELLOW_BG),
("BOX", (0,0), (-1,-1), 0.8, ORANGE),
("LEFTPADDING", (0,0), (-1,-1), 9),
("RIGHTPADDING", (0,0), (-1,-1), 9),
("TOPPADDING", (0,0), (-1,-1), 6),
("BOTTOMPADDING", (0,0), (-1,-1), 6),
]))
story.append(rapd_box)
story.append(Spacer(1, 5*mm))
# ── Section 2: Differential Diagnosis Table ───────────────────
story.append(section_label("2. DIFFERENTIAL DIAGNOSIS BY PAIN AND LATERALITY"))
story.append(Spacer(1, 3*mm))
# header row
dd_hdr = [
Paragraph("DIAGNOSIS", TABLE_HDR),
Paragraph("ONSET", TABLE_HDR),
Paragraph("PAIN?", TABLE_HDR),
Paragraph("LATERALITY", TABLE_HDR),
Paragraph("KEY SYMPTOMS & SIGNS", TABLE_HDR),
Paragraph("URGENCY", TABLE_HDR),
]
def urg(text, colour):
return Paragraph(f'<font color="{colour.hexval()}"><b>{text}</b></font>', TABLE_CELL_BOLD)
dd_rows = [
# Painful causes
[Paragraph("<b>Acute Angle-Closure Glaucoma</b>", TABLE_CELL_BOLD),
Paragraph("Minutes", TABLE_CELL),
Paragraph("YES - severe", TABLE_CELL),
Paragraph("Unilateral", TABLE_CELL),
Paragraph("Severe eye/brow pain; N&V; halos; fixed mid-dilated pupil; rock-hard eye; corneal haze; IOP often >50 mmHg", TABLE_CELL),
urg("EMERGENCY", RED)],
[Paragraph("<b>Optic Neuritis</b>", TABLE_CELL_BOLD),
Paragraph("Hours-days", TABLE_CELL),
Paragraph("YES - retro-orbital, with eye movement", TABLE_CELL),
Paragraph("Usually unilateral", TABLE_CELL),
Paragraph("Central scotoma; RAPD present; disc swelling (papillitis) or normal disc (retrobulbar); often young woman; risk of MS", TABLE_CELL),
urg("URGENT", ORANGE)],
[Paragraph("<b>Uveitis / Endophthalmitis</b>", TABLE_CELL_BOLD),
Paragraph("Hours-days", TABLE_CELL),
Paragraph("YES", TABLE_CELL),
Paragraph("Unilateral", TABLE_CELL),
Paragraph("Photophobia; red eye; cells & flare in anterior chamber; hypopyon if severe; vision blur", TABLE_CELL),
urg("URGENT", ORANGE)],
# Separator row
[Paragraph("<b><i>--- Painless causes ---</i></b>", S("Sep", fontName="Helvetica-Oblique", fontSize=8, textColor=GREY, leading=11)),
Paragraph("", TABLE_CELL), Paragraph("", TABLE_CELL),
Paragraph("", TABLE_CELL), Paragraph("", TABLE_CELL), Paragraph("", TABLE_CELL)],
[Paragraph("<b>Central Retinal Artery Occlusion (CRAO)</b>", TABLE_CELL_BOLD),
Paragraph("Seconds", TABLE_CELL),
Paragraph("No", TABLE_CELL),
Paragraph("Unilateral", TABLE_CELL),
Paragraph("Complete, sudden monocular loss; pale retina; cherry-red spot at macula; RAPD; afferent pupil defect; may follow amaurosis fugax", TABLE_CELL),
urg("EMERGENCY", RED)],
[Paragraph("<b>Central Retinal Vein Occlusion (CRVO)</b>", TABLE_CELL_BOLD),
Paragraph("Hours", TABLE_CELL),
Paragraph("No", TABLE_CELL),
Paragraph("Unilateral", TABLE_CELL),
Paragraph("'Blood and thunder' fundus - flame hemorrhages in all 4 quadrants; disc swelling; associated with HTN, hyperviscosity", TABLE_CELL),
urg("URGENT", ORANGE)],
[Paragraph("<b>Retinal Detachment</b>", TABLE_CELL_BOLD),
Paragraph("Minutes-hours", TABLE_CELL),
Paragraph("No", TABLE_CELL),
Paragraph("Unilateral", TABLE_CELL),
Paragraph("Flashing lights (photopsia); shower of floaters; curtain/shadow over vision; painless visual field loss", TABLE_CELL),
urg("EMERGENCY", RED)],
[Paragraph("<b>Ischemic Optic Neuropathy (ION)</b>", TABLE_CELL_BOLD),
Paragraph("Hours (overnight)", TABLE_CELL),
Paragraph("No", TABLE_CELL),
Paragraph("Unilateral", TABLE_CELL),
Paragraph("Altitudinal field defect (inferior loss); pale swollen disc; RAPD; age >50; check ESR/CRP for GCA; arteritic ION can bilateralize", TABLE_CELL),
urg("EMERGENCY*", RED)],
[Paragraph("<b>Temporal (Giant Cell) Arteritis</b>", TABLE_CELL_BOLD),
Paragraph("Hours-days", TABLE_CELL),
Paragraph("Headache, jaw claudication, scalp tenderness", TABLE_CELL),
Paragraph("Unilateral (can bilateralize)", TABLE_CELL),
Paragraph("Age >50; ESR >50; CRP elevated; tender non-pulsatile temporal artery; vision can progress to bilateral if untreated", TABLE_CELL),
urg("EMERGENCY", RED)],
[Paragraph("<b>Vitreous Hemorrhage</b>", TABLE_CELL_BOLD),
Paragraph("Sudden", TABLE_CELL),
Paragraph("No", TABLE_CELL),
Paragraph("Unilateral", TABLE_CELL),
Paragraph("Dense floaters, cobwebs; vision may be grossly impaired; common in DR or posterior vitreous detachment; fundus view obscured", TABLE_CELL),
urg("URGENT", ORANGE)],
[Paragraph("<b>Wet AMD (Choroidal Neovascularization)</b>", TABLE_CELL_BOLD),
Paragraph("Days", TABLE_CELL),
Paragraph("No", TABLE_CELL),
Paragraph("Usually unilateral (bilateral risk)", TABLE_CELL),
Paragraph("Central distortion (metamorphopsia); scotoma; Amsler grid distortion; subretinal hemorrhage; usually age >60", TABLE_CELL),
urg("URGENT", ORANGE)],
[Paragraph("<b>Cortical (Cerebral) Blindness</b>", TABLE_CELL_BOLD),
Paragraph("Sudden", TABLE_CELL),
Paragraph("No (headache if stroke)", TABLE_CELL),
Paragraph("Bilateral", TABLE_CELL),
Paragraph("Bilateral occipital lobe infarct; normal pupils & fundus; Anton syndrome (denial of blindness); associated neurological deficits", TABLE_CELL),
urg("EMERGENCY", RED)],
[Paragraph("<b>Transient Monocular Blindness / Amaurosis Fugax</b>", TABLE_CELL_BOLD),
Paragraph("Seconds-minutes (resolves)", TABLE_CELL),
Paragraph("No", TABLE_CELL),
Paragraph("Unilateral, transient", TABLE_CELL),
Paragraph("'Shade pulled down'; resolves fully; carotid TIA equivalent; 50% risk major stroke within 1 month without treatment", TABLE_CELL),
urg("URGENT", ORANGE)],
]
col_w2 = [FULL_W*0.165, FULL_W*0.095, FULL_W*0.095, FULL_W*0.10, FULL_W*0.40, FULL_W*0.095]
dd_data = [dd_hdr] + dd_rows
dd_table = Table(dd_data, colWidths=col_w2, repeatRows=1)
dd_style = TableStyle([
# Header
("BACKGROUND", (0,0), (-1,0), NAVY),
("TEXTCOLOR", (0,0), (-1,0), WHITE),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("FONTSIZE", (0,0), (-1,0), 8.5),
("ALIGN", (0,0), (-1,0), "CENTER"),
("VALIGN", (0,0), (-1,-1), "TOP"),
# Grid
("GRID", (0,0), (-1,-1), 0.4, DIVIDER),
("ROWBACKGROUNDS", (0,1), (-1,-1), [WHITE, LIGHT_GREY]),
# Separator row (row 4 = index 4)
("BACKGROUND", (0,4), (-1,4), colors.HexColor("#EAE8E8")),
# Padding
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 5),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("FONTSIZE", (0,1), (-1,-1), 8.5),
])
dd_table.setStyle(dd_style)
story.append(dd_table)
story.append(Spacer(1, 4*mm))
# ── Urgency key ──────────────────────────────────────────────
urg_data = [[
Paragraph('<b><font color="#C0392B">EMERGENCY</font></b> Act within minutes. Call ophthalmology NOW. Do not delay investigation.', BODY),
Paragraph('<b><font color="#E67E22">URGENT</font></b> Same-day ophthalmology. Do not wait for next clinic.', BODY),
]]
urg_tbl = Table(urg_data, colWidths=[FULL_W*0.52, FULL_W*0.48])
urg_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), LIGHT_GREY),
("BOX", (0,0), (-1,-1), 0.6, DIVIDER),
("LINEAFTER", (0,0), (0,-1), 0.5, DIVIDER),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING", (0,0), (-1,-1), 8),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
]))
story.append(urg_tbl)
# ── PAGE BREAK ───────────────────────────────────────────────
story.append(PageBreak())
# ════════════════════════════════════════════════════════════
# PAGE 2
# ════════════════════════════════════════════════════════════
# ── Section 3: Diagnostic Pathway Flowchart ───────────────────
story.append(section_label("3. DIAGNOSTIC PATHWAY"))
story.append(Spacer(1, 3*mm))
# Draw a flowchart as a table-based diagram
def flow_box(text, bg, border, w, style=None):
s = style or BODY
t = Table([[Paragraph(text, s)]], colWidths=[w])
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), bg),
("BOX", (0,0), (-1,-1), 1, border),
("LEFTPADDING", (0,0), (-1,-1), 6),
("RIGHTPADDING", (0,0), (-1,-1), 6),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("ALIGN", (0,0), (-1,-1), "CENTER"),
]))
return t
ARROW = Paragraph("<b>▼</b>", S("A", fontName="Helvetica-Bold", fontSize=12,
textColor=MED_BLUE, leading=14, alignment=TA_CENTER))
ARROW_R = Paragraph("<b>►</b>", S("AR", fontName="Helvetica-Bold", fontSize=10,
textColor=MED_BLUE, leading=14, alignment=TA_CENTER))
FW = FULL_W
BW = FW * 0.55 # central box width
SW = FW * 0.42 # side box width
story.append(flow_box("<b>SUDDEN VISION LOSS - Patient presents</b>", NAVY, NAVY, FW,
S("FH", fontName="Helvetica-Bold", fontSize=10, textColor=WHITE, leading=14, alignment=TA_CENTER)))
story.append(Spacer(1, 1*mm))
story.append(ARROW)
story.append(Spacer(1, 1*mm))
story.append(flow_box(
"<b>Step 1:</b> Is the vision loss <b>transient</b> (now recovered) or <b>persistent</b>?",
LIGHT_BLUE, MED_BLUE, FW))
story.append(Spacer(1, 1*mm))
branch1_data = [[
flow_box("<b>TRANSIENT</b> (fully resolved)<br/>"
"• Amaurosis fugax (monocular TIA)<br/>"
"• Migraine with aura (bilateral)<br/>"
"• Vertebrobasilar TIA<br/>"
"<i>-> Urgent carotid Doppler, echo, ECG, neurology</i>",
ORANGE_BG, ORANGE, SW),
Paragraph("", BODY),
flow_box("<b>PERSISTENT</b> (continues now)<br/>"
"Continue to Step 2<br/>"
"<i>Assume structural cause until proven otherwise</i>",
RED_BG, RED, SW),
]]
b1 = Table(branch1_data, colWidths=[SW, FW-(2*SW), SW])
b1.setStyle(TableStyle([
("VALIGN", (0,0), (-1,-1), "TOP"),
("LEFTPADDING", (0,0), (-1,-1), 0),
("RIGHTPADDING", (0,0), (-1,-1), 0),
("TOPPADDING", (0,0), (-1,-1), 0),
("BOTTOMPADDING", (0,0), (-1,-1), 0),
]))
story.append(b1)
story.append(Spacer(1, 1*mm))
story.append(ARROW)
story.append(Spacer(1, 1*mm))
story.append(flow_box(
"<b>Step 2:</b> Is vision loss <b>monocular</b> (cover each eye) or <b>binocular</b>?",
LIGHT_BLUE, MED_BLUE, FW))
story.append(Spacer(1, 1*mm))
branch2_data = [[
flow_box("<b>MONOCULAR</b><br/>Lesion anterior to optic chiasm<br/>"
"(eye, optic nerve, or retina)<br/>"
"-> Check: IOP, fundus, RAPD<br/>"
"-> Consider: CRAO, CRVO, RD,<br/>"
" Glaucoma, ION, vitreous hemorrhage",
GREEN_BG, GREEN, SW),
Paragraph("", BODY),
flow_box("<b>BINOCULAR</b><br/>Lesion at or posterior to chiasm<br/>"
"(chiasm, optic tract, cortex)<br/>"
"-> Urgent MRI brain<br/>"
"-> Consider: stroke, cortical blindness,<br/>"
" pituitary apoplexy, MS",
ORANGE_BG, ORANGE, SW),
]]
b2 = Table(branch2_data, colWidths=[SW, FW-(2*SW), SW])
b2.setStyle(TableStyle([
("VALIGN", (0,0), (-1,-1), "TOP"),
("LEFTPADDING", (0,0), (-1,-1), 0),
("RIGHTPADDING", (0,0), (-1,-1), 0),
("TOPPADDING", (0,0), (-1,-1), 0),
("BOTTOMPADDING", (0,0), (-1,-1), 0),
]))
story.append(b2)
story.append(Spacer(1, 1*mm))
story.append(ARROW)
story.append(Spacer(1, 1*mm))
story.append(flow_box(
"<b>Step 3:</b> Is the vision loss <b>painful</b> or <b>painless</b>? (for monocular loss)",
LIGHT_BLUE, MED_BLUE, FW))
story.append(Spacer(1, 1*mm))
branch3_data = [[
flow_box("<b>PAINFUL (monocular)</b><br/>"
"• Acute angle-closure glaucoma<br/>"
" IOP >50, mid-dilated pupil, corneal haze<br/>"
" -> Immediate: pilocarpine, acetazolamide<br/>"
"• Optic neuritis<br/>"
" Pain on eye movement, RAPD, young woman<br/>"
" -> IV methylprednisolone, MRI brain<br/>"
"• Uveitis / endophthalmitis<br/>"
" Photophobia, cells in AC, hypopyon<br/>"
" -> Urgent slit-lamp, vitreous tap if endo",
RED_BG, RED, SW),
Paragraph("", BODY),
flow_box("<b>PAINLESS (monocular)</b><br/>"
"• Cherry-red spot + pale retina -> CRAO<br/>"
" -> O2, ocular massage, call stroke team<br/>"
"• Curtain + floaters + flashes -> RD<br/>"
" -> Urgent surgical referral<br/>"
"• Age >50 + altitudinal defect -> ION<br/>"
" -> Check ESR/CRP; if arteritic, steroids NOW<br/>"
"• 'Blood-thunder' fundus -> CRVO<br/>"
" -> Treat BP, anti-VEGF referral<br/>"
"• Dense floaters, poor view -> Vit. hemorrhage<br/>"
" -> Urgent USS eye, DM/anticoag review",
GREEN_BG, GREEN, SW),
]]
b3 = Table(branch3_data, colWidths=[SW, FW-(2*SW), SW])
b3.setStyle(TableStyle([
("VALIGN", (0,0), (-1,-1), "TOP"),
("LEFTPADDING", (0,0), (-1,-1), 0),
("RIGHTPADDING", (0,0), (-1,-1), 0),
("TOPPADDING", (0,0), (-1,-1), 0),
("BOTTOMPADDING", (0,0), (-1,-1), 0),
]))
story.append(b3)
story.append(Spacer(1, 5*mm))
# ── Section 4: Investigations ─────────────────────────────────
story.append(section_label("4. INVESTIGATIONS"))
story.append(Spacer(1, 3*mm))
inv_hdr = [Paragraph("INVESTIGATION", TABLE_HDR),
Paragraph("INDICATED FOR", TABLE_HDR),
Paragraph("KEY FINDING", TABLE_HDR)]
inv_rows = [
[Paragraph("Intraocular pressure (tonometry)", TABLE_CELL_BOLD),
Paragraph("All acute presentations; mandatory", TABLE_CELL),
Paragraph(">21 mmHg abnormal; >50 mmHg = acute glaucoma", TABLE_CELL)],
[Paragraph("Dilated fundoscopy", TABLE_CELL_BOLD),
Paragraph("All monocular; macula, disc, vessels, periphery", TABLE_CELL),
Paragraph("Cherry-red spot (CRAO); flame hemorrhages (CRVO); pale disc (ION); detached retina", TABLE_CELL)],
[Paragraph("Slit-lamp examination", TABLE_CELL_BOLD),
Paragraph("Anterior segment disease, uveitis, cornea", TABLE_CELL),
Paragraph("Cells/flare (uveitis); corneal haze (glaucoma); keratic precipitates", TABLE_CELL)],
[Paragraph("Visual field testing (confrontation/perimetry)", TABLE_CELL_BOLD),
Paragraph("All presentations to localise lesion", TABLE_CELL),
Paragraph("Central scotoma (optic neuritis, AMD); altitudinal (ION); bitemporal (chiasm); hemianopia (cortical)", TABLE_CELL)],
[Paragraph("ESR + CRP (URGENT)", TABLE_CELL_BOLD),
Paragraph("Age >50 with any monocular vision loss", TABLE_CELL),
Paragraph("ESR >50 mm/hr or CRP elevated -> GCA until proven otherwise; start prednisolone immediately", TABLE_CELL)],
[Paragraph("MRI/CT brain with diffusion", TABLE_CELL_BOLD),
Paragraph("Binocular loss; suspected stroke; cortical blindness", TABLE_CELL),
Paragraph("Occipital infarct; chiasmal lesion; pituitary apoplexy; demyelination (MS)", TABLE_CELL)],
[Paragraph("Carotid Doppler ultrasound", TABLE_CELL_BOLD),
Paragraph("Amaurosis fugax; CRAO; asymmetric retinal vascular disease", TABLE_CELL),
Paragraph(">70% stenosis -> urgent endarterectomy/stenting referral", TABLE_CELL)],
[Paragraph("12-lead ECG + cardiac echo", TABLE_CELL_BOLD),
Paragraph("Embolic causes: CRAO, amaurosis fugax", TABLE_CELL),
Paragraph("AF, valvular disease, patent foramen ovale -> anticoagulation", TABLE_CELL)],
[Paragraph("Blood glucose + HbA1c", TABLE_CELL_BOLD),
Paragraph("Diabetic retinopathy; vitreous hemorrhage", TABLE_CELL),
Paragraph("Uncontrolled DM -> aggressive glycaemic control; ophthalmology follow-up", TABLE_CELL)],
[Paragraph("FBC, platelets, coagulation screen", TABLE_CELL_BOLD),
Paragraph("Vitreous/retinal hemorrhage; hyperviscosity (CRVO)", TABLE_CELL),
Paragraph("Polycythaemia, thrombocytopenia, coagulopathy; consider haematology review", TABLE_CELL)],
[Paragraph("Ocular ultrasound (B-scan)", TABLE_CELL_BOLD),
Paragraph("Vitreous hemorrhage (fundus not visible)", TABLE_CELL),
Paragraph("Retinal detachment or vitreous hemorrhage visible behind opaque media", TABLE_CELL)],
[Paragraph("Fluorescein angiography", TABLE_CELL_BOLD),
Paragraph("CRVO, CRAO, AMD, diabetic retinopathy", TABLE_CELL),
Paragraph("Vascular filling defects; choroidal neovascularisation; leakage patterns", TABLE_CELL)],
[Paragraph("Temporal artery biopsy", TABLE_CELL_BOLD),
Paragraph("Suspected GCA after ESR/CRP elevated", TABLE_CELL),
Paragraph("Granulomatous inflammation; giant cells. Do NOT delay steroids pending biopsy", TABLE_CELL)],
[Paragraph("Optical coherence tomography (OCT)", TABLE_CELL_BOLD),
Paragraph("Macular pathology; AMD; macular oedema; RD", TABLE_CELL),
Paragraph("Drusen (dry AMD); subretinal fluid (wet AMD); macular oedema; inner retinal thinning (CRAO)", TABLE_CELL)],
]
inv_col_w = [FULL_W*0.25, FULL_W*0.32, FULL_W*0.43]
inv_table = Table([inv_hdr] + inv_rows, colWidths=inv_col_w, repeatRows=1)
inv_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), NAVY),
("ROWBACKGROUNDS", (0,1), (-1,-1), [WHITE, LIGHT_GREY]),
("GRID", (0,0), (-1,-1), 0.4, DIVIDER),
("VALIGN", (0,0), (-1,-1), "TOP"),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 5),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
]))
story.append(inv_table)
story.append(PageBreak())
# ════════════════════════════════════════════════════════════
# PAGE 3
# ════════════════════════════════════════════════════════════
# ── Section 5: Condition-specific management ───────────────────
story.append(section_label("5. CONDITION-SPECIFIC EMERGENCY MANAGEMENT"))
story.append(Spacer(1, 3*mm))
def mgmt_block(title, colour, bg, items):
rows = [[Paragraph(f"<b>{title}</b>",
S("MH", fontName="Helvetica-Bold", fontSize=9.5,
textColor=WHITE, leading=13))]] + \
[[Paragraph(f"• {i}", TABLE_CELL)] for i in items]
t = Table(rows, colWidths=[(FULL_W - 6*mm)/2])
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), colour),
("BACKGROUND", (0,1), (-1,-1), bg),
("BOX", (0,0), (-1,-1), 0.8, colour),
("INNERGRID", (0,0), (-1,-1), 0.3, colors.HexColor("#D0D0D0")),
("LEFTPADDING", (0,0), (-1,-1), 7),
("RIGHTPADDING", (0,0), (-1,-1), 7),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("VALIGN", (0,0), (-1,-1), "TOP"),
]))
return t
col_half = (FULL_W - 4*mm) / 2
m1 = mgmt_block("Acute Angle-Closure Glaucoma", RED, RED_BG, [
"Immediate ophthalmology - this is a sight emergency",
"Reduce IOP: oral glycerin 1 mL/kg + IM acetazolamide 500 mg",
"Topical pilocarpine 2-4% every 15 min (miotic to open angle)",
"Topical beta-blocker (timolol 0.5%) + alpha agonist (apraclonidine)",
"IV mannitol 1-2 g/kg if medical Rx fails",
"Definitive: laser peripheral iridotomy",
"Treat fellow eye prophylactically",
])
m2 = mgmt_block("Central Retinal Artery Occlusion (CRAO)", RED, RED_BG, [
"Act within 90 minutes for any chance of visual recovery",
"Ocular massage: digital massage 10-15s on/5s off x 15 min",
"Reduce IOP: acetazolamide 500 mg IV or anterior chamber paracentesis",
"Re-breathe into paper bag or carbogen (95% O2/5% CO2) to dilate arteries",
"Urgent stroke protocol: CT/MRI brain, carotid Doppler, ECG, echo",
"Antiplatelet therapy; consider thrombolysis if within 4.5 hrs",
"Do NOT use thrombolytics for ocular CRAO alone (no evidence)",
])
m3 = mgmt_block("Temporal Arteritis / Arteritic ION", RED, RED_BG, [
"DO NOT wait for biopsy - start steroids NOW",
"IV methylprednisolone 1 g/day x 3 days (if vision already lost)",
"Oral prednisolone 1 mg/kg/day (max 80 mg) if vision not yet affected",
"Aspirin 75 mg daily to reduce stroke/vision loss risk",
"Urgent temporal artery biopsy within 1-2 weeks (steroids do not affect for 2 wks)",
"Monitor ESR/CRP response; taper steroids very slowly over months",
"GI protection: PPI + bisphosphonate for long-term steroid use",
])
m4 = mgmt_block("Retinal Detachment", ORANGE, ORANGE_BG, [
"NPO (nil by mouth) - surgical emergency",
"Urgent ophthalmology surgical referral same day",
"Avoid head movement that may extend detachment",
"Macula-on RD: surgery within hours for best prognosis",
"Macula-off RD: surgery within 24 hrs; visual prognosis worse",
"Options: pneumatic retinopexy, scleral buckle, pars plana vitrectomy",
"Follow-up fellow eye for prophylactic treatment",
])
m5 = mgmt_block("Optic Neuritis", ORANGE, ORANGE_BG, [
"IV methylprednisolone 1 g/day x 3 days (speeds recovery, not final acuity)",
"MRI brain: high T2 lesions suggest MS (McDonald criteria)",
"Neurology referral for MS risk assessment and DMT discussion",
"75% of women / 34% men develop MS within 15 years",
"Visual acuity usually recovers 4-12 weeks with or without treatment",
"Avoid overheating (Uhthoff's phenomenon may worsen acuity transiently)",
])
m6 = mgmt_block("Wet AMD - Choroidal Neovascularization", ORANGE, ORANGE_BG, [
"Urgent ophthalmology - anti-VEGF injection within days",
"Anti-VEGF: ranibizumab, bevacizumab, or aflibercept intravitreal",
"Amsler grid self-monitoring for fellow eye",
"AREDS2 supplements for intermediate AMD: vitamins C, E, beta-carotene, zinc",
"Laser photocoagulation for extrafoveal lesions only",
"No smoking - major modifiable risk factor for AMD progression",
])
mgmt_grid = Table([
[m1, Spacer(4,1), m2],
[Spacer(1,4*mm), Spacer(1,4*mm), Spacer(1,4*mm)],
[m3, Spacer(4,1), m4],
[Spacer(1,4*mm), Spacer(1,4*mm), Spacer(1,4*mm)],
[m5, Spacer(4,1), m6],
], colWidths=[col_half, 4*mm, col_half])
mgmt_grid.setStyle(TableStyle([
("VALIGN", (0,0), (-1,-1), "TOP"),
("LEFTPADDING", (0,0), (-1,-1), 0),
("RIGHTPADDING", (0,0), (-1,-1), 0),
("TOPPADDING", (0,0), (-1,-1), 0),
("BOTTOMPADDING", (0,0), (-1,-1), 0),
]))
story.append(mgmt_grid)
story.append(Spacer(1, 5*mm))
# ── Section 6: Visual Field Patterns ─────────────────────────
story.append(section_label("6. VISUAL FIELD DEFECT LOCALISATION"))
story.append(Spacer(1, 3*mm))
vf_hdr = [Paragraph("FIELD DEFECT", TABLE_HDR),
Paragraph("LOCATION OF LESION", TABLE_HDR),
Paragraph("LIKELY DIAGNOSIS", TABLE_HDR)]
vf_rows = [
[Paragraph("Central scotoma (one eye)", TABLE_CELL_BOLD),
Paragraph("Macula or optic nerve (pre-chiasm)", TABLE_CELL),
Paragraph("Optic neuritis, CRAO (macular), AMD, toxic optic neuropathy", TABLE_CELL)],
[Paragraph("Monocular total visual loss", TABLE_CELL_BOLD),
Paragraph("Optic nerve or globe (unilateral, pre-chiasm)", TABLE_CELL),
Paragraph("CRAO, severe optic neuritis, globe trauma, arteritic ION", TABLE_CELL)],
[Paragraph("Altitudinal defect (upper or lower half)", TABLE_CELL_BOLD),
Paragraph("Optic nerve (usually inferior fibres)", TABLE_CELL),
Paragraph("Ischemic optic neuropathy (ION) - inferior altitudinal most common", TABLE_CELL)],
[Paragraph("Bitemporal hemianopia", TABLE_CELL_BOLD),
Paragraph("Optic chiasm (crossing nasal fibres)", TABLE_CELL),
Paragraph("Pituitary tumour / apoplexy, craniopharyngioma, chiasmal compression", TABLE_CELL)],
[Paragraph("Homonymous hemianopia", TABLE_CELL_BOLD),
Paragraph("Optic tract, lateral geniculate, or optic radiation", TABLE_CELL),
Paragraph("Stroke, tumour, abscess; side of defect opposite to lesion", TABLE_CELL)],
[Paragraph("Homonymous quadrantanopia", TABLE_CELL_BOLD),
Paragraph("Optic radiation (parietal = inferior; temporal = superior)", TABLE_CELL),
Paragraph("'Pie in the sky' (temporal lobe); 'Pie on the floor' (parietal lobe)", TABLE_CELL)],
[Paragraph("Peripheral (tunnel) vision", TABLE_CELL_BOLD),
Paragraph("Retinal periphery bilaterally", TABLE_CELL),
Paragraph("Retinitis pigmentosa (hereditary); advanced glaucoma; panretinal photocoagulation", TABLE_CELL)],
[Paragraph("Complete bilateral visual loss (normal pupils)", TABLE_CELL_BOLD),
Paragraph("Bilateral occipital cortex", TABLE_CELL),
Paragraph("Cortical blindness (stroke, hypoxia); Anton syndrome; may have pupil-sparing", TABLE_CELL)],
]
vf_col_w = [FULL_W*0.27, FULL_W*0.33, FULL_W*0.40]
vf_table = Table([vf_hdr] + vf_rows, colWidths=vf_col_w, repeatRows=1)
vf_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), NAVY),
("ROWBACKGROUNDS", (0,1), (-1,-1), [WHITE, LIGHT_GREY]),
("GRID", (0,0), (-1,-1), 0.4, DIVIDER),
("VALIGN", (0,0), (-1,-1), "TOP"),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 5),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
]))
story.append(vf_table)
story.append(Spacer(1, 5*mm))
# ── Section 7: Key Fundoscopic Signs ─────────────────────────
story.append(section_label("7. KEY FUNDOSCOPIC FINDINGS"))
story.append(Spacer(1, 3*mm))
fund_hdr = [Paragraph("FUNDOSCOPIC SIGN", TABLE_HDR),
Paragraph("APPEARANCE", TABLE_HDR),
Paragraph("DIAGNOSIS", TABLE_HDR)]
fund_rows = [
[Paragraph("Cherry-red spot", TABLE_CELL_BOLD),
Paragraph("Pale (ischaemic) retina with bright red spot at fovea (choroidal supply preserved)", TABLE_CELL),
Paragraph("CRAO; rarely Niemann-Pick, Tay-Sachs (bilateral)", TABLE_CELL)],
[Paragraph("Blood and thunder fundus", TABLE_CELL_BOLD),
Paragraph("Flame-shaped haemorrhages in all 4 quadrants; dilated tortuous veins; disc oedema", TABLE_CELL),
Paragraph("Central Retinal Vein Occlusion (CRVO)", TABLE_CELL)],
[Paragraph("Pale/swollen optic disc", TABLE_CELL_BOLD),
Paragraph("Disc swelling + pallor; may have haemorrhages at disc margin", TABLE_CELL),
Paragraph("Arteritic or non-arteritic ION; papilloedema (if bilateral)", TABLE_CELL)],
[Paragraph("Drusen", TABLE_CELL_BOLD),
Paragraph("Yellow-white spots at macula; hard (small) or soft (large, confluent)", TABLE_CELL),
Paragraph("Age-related macular degeneration (AMD) - dry type early sign", TABLE_CELL)],
[Paragraph("Subretinal haemorrhage / grey-green membrane", TABLE_CELL_BOLD),
Paragraph("Dark subretinal blood; grey-green choroidal neovascular membrane at macula", TABLE_CELL),
Paragraph("Wet AMD with choroidal neovascularisation", TABLE_CELL)],
[Paragraph("Microaneurysms + haemorrhages (dot-blot)", TABLE_CELL_BOLD),
Paragraph("Dot and blot haemorrhages; microaneurysms; hard exudates; cotton-wool spots", TABLE_CELL),
Paragraph("Diabetic retinopathy (non-proliferative)", TABLE_CELL)],
[Paragraph("New vessels (NVD/NVE)", TABLE_CELL_BOLD),
Paragraph("Abnormal frond-like vessels at disc (NVD) or elsewhere (NVE); may bleed into vitreous", TABLE_CELL),
Paragraph("Proliferative diabetic retinopathy; retinal vein occlusion", TABLE_CELL)],
[Paragraph("Bone-spicule pigmentation", TABLE_CELL_BOLD),
Paragraph("Peripheral retinal pigment clumping in bone-spicule pattern; attenuated vessels", TABLE_CELL),
Paragraph("Retinitis pigmentosa (hereditary; starts with night blindness)", TABLE_CELL)],
[Paragraph("Optic cup enlargement (C:D ratio >0.6)", TABLE_CELL_BOLD),
Paragraph("Enlarged cup-to-disc ratio; notching of neuroretinal rim; bayonetting of vessels", TABLE_CELL),
Paragraph("Glaucoma (chronic open-angle; often bilateral)", TABLE_CELL)],
[Paragraph("Silver/copper wiring + AV nipping", TABLE_CELL_BOLD),
Paragraph("Increased arteriolar light reflex; venous compression at arteriovenous crossings", TABLE_CELL),
Paragraph("Hypertensive retinopathy; chronic hypertension", TABLE_CELL)],
]
fund_col_w = [FULL_W*0.25, FULL_W*0.42, FULL_W*0.33]
fund_table = Table([fund_hdr] + fund_rows, colWidths=fund_col_w, repeatRows=1)
fund_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), NAVY),
("ROWBACKGROUNDS", (0,1), (-1,-1), [WHITE, LIGHT_GREY]),
("GRID", (0,0), (-1,-1), 0.4, DIVIDER),
("VALIGN", (0,0), (-1,-1), "TOP"),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 5),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
]))
story.append(fund_table)
story.append(PageBreak())
# ════════════════════════════════════════════════════════════
# PAGE 4
# ════════════════════════════════════════════════════════════
# ── Section 8: Drugs implicated ──────────────────────────────
story.append(section_label("8. DRUG-INDUCED VISUAL LOSS"))
story.append(Spacer(1, 3*mm))
drug_hdr = [Paragraph("DRUG", TABLE_HDR),
Paragraph("MECHANISM", TABLE_HDR),
Paragraph("VISUAL EFFECT", TABLE_HDR),
Paragraph("ACTION", TABLE_HDR)]
drug_rows = [
[Paragraph("Ethambutol", TABLE_CELL_BOLD),
Paragraph("Optic nerve toxicity", TABLE_CELL),
Paragraph("Central / centrocaecal scotoma; colour vision loss (red-green)", TABLE_CELL),
Paragraph("Stop drug; monitor monthly if TB treatment required", TABLE_CELL)],
[Paragraph("Hydroxychloroquine (HCQ)", TABLE_CELL_BOLD),
Paragraph("Retinal toxicity - 'bull's eye' maculopathy", TABLE_CELL),
Paragraph("Peripheral visual field loss; irreversible; dose-related", TABLE_CELL),
Paragraph("Annual OCT screening; stop if maculopathy detected", TABLE_CELL)],
[Paragraph("Amiodarone", TABLE_CELL_BOLD),
Paragraph("Corneal microdeposits; optic neuropathy", TABLE_CELL),
Paragraph("Halos; blurred vision; rarely optic neuritis", TABLE_CELL),
Paragraph("Often reversible on dose reduction; ophthalmology review", TABLE_CELL)],
[Paragraph("Corticosteroids (topical/systemic)", TABLE_CELL_BOLD),
Paragraph("Raises IOP; lens opacity", TABLE_CELL),
Paragraph("Steroid glaucoma; posterior subcapsular cataract", TABLE_CELL),
Paragraph("Monitor IOP; taper dose; switch to steroid-sparing agent", TABLE_CELL)],
[Paragraph("Vigabatrin", TABLE_CELL_BOLD),
Paragraph("Retinal GABAergic toxicity", TABLE_CELL),
Paragraph("Irreversible bilateral peripheral visual field constriction", TABLE_CELL),
Paragraph("Annual visual field testing; consider drug withdrawal", TABLE_CELL)],
[Paragraph("Sildenafil / PDE5 inhibitors", TABLE_CELL_BOLD),
Paragraph("Non-arteritic ION risk (controversial); colour vision disturbance", TABLE_CELL),
Paragraph("Blue-tinged vision; acute ION in rare cases", TABLE_CELL),
Paragraph("Avoid in high ION risk patients; stop if vision change occurs", TABLE_CELL)],
[Paragraph("Digoxin toxicity", TABLE_CELL_BOLD),
Paragraph("Retinal photoreceptor toxicity", TABLE_CELL),
Paragraph("Yellow/green halos; central scotoma; colour vision changes", TABLE_CELL),
Paragraph("Check digoxin levels; dose reduction", TABLE_CELL)],
[Paragraph("Isotretinoin", TABLE_CELL_BOLD),
Paragraph("Raised intracranial pressure (pseudotumour cerebri)", TABLE_CELL),
Paragraph("Papilloedema; visual field defects", TABLE_CELL),
Paragraph("Stop drug; ophthalmology + neurology referral", TABLE_CELL)],
[Paragraph("Topiramate", TABLE_CELL_BOLD),
Paragraph("Idiosyncratic uveal effusion causing angle-closure", TABLE_CELL),
Paragraph("Acute angle-closure glaucoma", TABLE_CELL),
Paragraph("Stop topiramate; treat acute glaucoma; do NOT use pilocarpine", TABLE_CELL)],
]
drug_col_w = [FULL_W*0.18, FULL_W*0.24, FULL_W*0.32, FULL_W*0.26]
drug_table = Table([drug_hdr] + drug_rows, colWidths=drug_col_w, repeatRows=1)
drug_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), NAVY),
("ROWBACKGROUNDS", (0,1), (-1,-1), [WHITE, LIGHT_GREY]),
("GRID", (0,0), (-1,-1), 0.4, DIVIDER),
("VALIGN", (0,0), (-1,-1), "TOP"),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 5),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
]))
story.append(drug_table)
story.append(Spacer(1, 5*mm))
# ── Section 9: Special Populations ───────────────────────────
story.append(section_label("9. SPECIAL POPULATIONS"))
story.append(Spacer(1, 3*mm))
sp_left = [
Paragraph("<b>Paediatric / Young Adult</b>", SUBSEC_HDR),
Paragraph("• <b>Optic neuritis</b>: often bilateral in children; high MS risk in adults", BODY),
Paragraph("• <b>Retinoblastoma</b>: leukocoria (white pupil reflex), strabismus", BODY),
Paragraph("• <b>Juvenile glaucoma</b>: buphthalmos, photophobia, tearing", BODY),
Paragraph("• <b>Non-accidental injury</b>: bilateral retinal haemorrhages in infants - mandatory safeguarding referral", BODY),
Paragraph("• <b>Myopia progression</b>: rapid change, risk of retinal detachment", BODY),
Spacer(1, 3*mm),
Paragraph("<b>Pregnancy</b>", SUBSEC_HDR),
Paragraph("• Visual disturbance + HTN -> pre-eclampsia / HELLP; emergency obstetric review", BODY),
Paragraph("• Hypertensive retinopathy may worsen in pre-eclampsia", BODY),
Paragraph("• Central serous retinopathy: more common in 3rd trimester", BODY),
Paragraph("• Pituitary adenoma may expand -> bitemporal hemianopia", BODY),
]
sp_right = [
Paragraph("<b>Elderly (>60 years)</b>", SUBSEC_HDR),
Paragraph("• AMD is leading cause of vision loss in this group", BODY),
Paragraph("• Always check ESR/CRP in >50 with monocular loss (rule out GCA)", BODY),
Paragraph("• Falls risk from visual impairment -> safety review", BODY),
Paragraph("• Glaucoma more prevalent; screen at least every 3 years from age 50", BODY),
Spacer(1, 3*mm),
Paragraph("<b>Diabetic Patients</b>", SUBSEC_HDR),
Paragraph("• Diabetic retinopathy leading cause of blindness age 20-74 in USA", BODY),
Paragraph("• Annual dilated fundoscopy mandatory for all diabetics", BODY),
Paragraph("• HbA1c < 53 mmol/mol (7%) target to slow progression", BODY),
Paragraph("• Macular oedema: anti-VEGF injections (first line)", BODY),
Paragraph("• Proliferative DR: panretinal photocoagulation; vitrectomy if vitreous haemorrhage", BODY),
Spacer(1, 3*mm),
Paragraph("<b>HIV / Immunocompromised</b>", SUBSEC_HDR),
Paragraph("• CMV retinitis: CD4 < 50; painless progressive loss; 'tomato sauce' fundus", BODY),
Paragraph("• Toxoplasma chorioretinitis: headlights in fog appearance", BODY),
Paragraph("• Treatment: IV ganciclovir; antiretroviral therapy for IRIS management", BODY),
]
sp_tbl = Table([[sp_left, Spacer(4,1), sp_right]],
colWidths=[col_half, 4*mm, col_half])
sp_tbl.setStyle(TableStyle([
("VALIGN", (0,0), (-1,-1), "TOP"),
("LEFTPADDING", (0,0), (-1,-1), 0),
("RIGHTPADDING", (0,0), (-1,-1), 0),
("TOPPADDING", (0,0), (-1,-1), 0),
("BOTTOMPADDING", (0,0), (-1,-1), 0),
]))
story.append(sp_tbl)
story.append(Spacer(1, 5*mm))
# ── Section 10: Disposition Summary ──────────────────────────
story.append(section_label("10. DISPOSITION & FOLLOW-UP SUMMARY"))
story.append(Spacer(1, 3*mm))
disp_data = [
[Paragraph("<b>ADMIT / CALL OPHTHALMOLOGY NOW</b>", S("DH",
fontName="Helvetica-Bold", fontSize=9, textColor=WHITE, leading=13)),
Paragraph("<b>SAME-DAY URGENT REFERRAL</b>", S("DH2",
fontName="Helvetica-Bold", fontSize=9, textColor=WHITE, leading=13)),
Paragraph("<b>ROUTINE / SCHEDULED FOLLOW-UP</b>", S("DH3",
fontName="Helvetica-Bold", fontSize=9, textColor=WHITE, leading=13))],
[Paragraph(
"• Acute angle-closure glaucoma<br/>"
"• CRAO (within 90 minutes!)<br/>"
"• Retinal detachment (macula-on)<br/>"
"• GCA / arteritic ION (give steroids first)<br/>"
"• Cortical blindness (stroke team)<br/>"
"• Endophthalmitis<br/>"
"• Bilateral vision loss",
TABLE_CELL),
Paragraph(
"• CRVO<br/>"
"• Wet AMD (new onset)<br/>"
"• Vitreous haemorrhage<br/>"
"• Optic neuritis<br/>"
"• Non-arteritic ION<br/>"
"• Amaurosis fugax (neurovascular workup)<br/>"
"• Retinal detachment (macula-off, stable)",
TABLE_CELL),
Paragraph(
"• Stable dry AMD<br/>"
"• Controlled glaucoma (review every 6-12 months)<br/>"
"• Refractive error correction<br/>"
"• Diabetic retinopathy screening (annual)<br/>"
"• Post-optic neuritis MRI follow-up<br/>"
"• Cataract (if vision not severely impaired)<br/>"
"• Hydroxychloroquine monitoring (annual OCT)",
TABLE_CELL)],
]
disp_col_w = [FULL_W/3, FULL_W/3, FULL_W/3]
disp_tbl = Table(disp_data, colWidths=disp_col_w)
disp_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (0,0), RED),
("BACKGROUND", (1,0), (1,0), ORANGE),
("BACKGROUND", (2,0), (2,0), GREEN),
("BACKGROUND", (0,1), (0,1), RED_BG),
("BACKGROUND", (1,1), (1,1), ORANGE_BG),
("BACKGROUND", (2,1), (2,1), GREEN_BG),
("BOX", (0,0), (-1,-1), 0.8, DIVIDER),
("INNERGRID", (0,0), (-1,-1), 0.5, DIVIDER),
("VALIGN", (0,0), (-1,-1), "TOP"),
("LEFTPADDING", (0,0), (-1,-1), 7),
("RIGHTPADDING", (0,0), (-1,-1), 7),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
]))
story.append(disp_tbl)
story.append(Spacer(1, 5*mm))
# ── Disclaimer ───────────────────────────────────────────────
disclaimer_data = [[Paragraph(
"<b>DISCLAIMER:</b> This clinical decision summary is intended as a quick-reference educational aid for healthcare professionals. "
"It does not replace clinical judgment, institutional protocols, or specialist consultation. "
"Drug doses and management steps should be verified against current formularies and guidelines before use. "
"Always consult ophthalmology for definitive diagnosis and management of acute vision loss.",
SMALL)]]
disc_tbl = Table(disclaimer_data, colWidths=[FULL_W])
disc_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), LIGHT_GREY),
("BOX", (0,0), (-1,-1), 0.5, GREY),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING", (0,0), (-1,-1), 8),
("TOPPADDING", (0,0), (-1,-1), 6),
("BOTTOMPADDING", (0,0), (-1,-1), 6),
]))
story.append(disc_tbl)
# ── Build ────────────────────────────────────────────────────
doc = SimpleDocTemplate(
OUTPUT,
pagesize=A4,
leftMargin=L_MARGIN,
rightMargin=R_MARGIN,
topMargin=55*mm, # space for header banner on p1
bottomMargin=B_MARGIN,
)
# First page has taller top margin for the big banner
from reportlab.platypus import BaseDocTemplate, Frame, PageTemplate
class MyDoc(BaseDocTemplate):
def __init__(self, filename, **kw):
super().__init__(filename, **kw)
frame_p1 = Frame(L_MARGIN, B_MARGIN,
PAGE_W - L_MARGIN - R_MARGIN,
PAGE_H - 55*mm - B_MARGIN,
id="p1")
frame_rest = Frame(L_MARGIN, B_MARGIN,
PAGE_W - L_MARGIN - R_MARGIN,
PAGE_H - 15*mm - B_MARGIN,
id="rest")
self.addPageTemplates([
PageTemplate(id="First", frames=[frame_p1], onPage=first_page),
PageTemplate(id="Later", frames=[frame_rest], onPage=later_pages),
])
def handle_pageBegin(self):
if self.page == 1:
self._handle_pageBegin()
else:
self._handle_pageBegin()
doc2 = MyDoc(OUTPUT)
# Force page template switch after page 1
from reportlab.platypus import NextPageTemplate
story.insert(0, NextPageTemplate("First"))
# Insert template switch before first page break
# Find position of first PageBreak
for i, elem in enumerate(story):
if isinstance(elem, PageBreak):
story.insert(i, NextPageTemplate("Later"))
break
doc2.build(story)
print(f"PDF created: {OUTPUT}")
cd /home/daytona/workspace/sudden-vision-loss && python3 build_pdf.py 2>&1
~/sudden-vision-loss/build_pdf.py
Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/sudden-vision-loss/build_pdf.py'
cd /home/daytona/workspace/sudden-vision-loss && python3 build_pdf.py 2>&1
ls -lh /home/daytona/workspace/sudden-vision-loss/Sudden_Vision_Loss_Clinical_Decision_Summary.pdf
Sudden Vision Loss Clinical Decision Summary
PDF Document · PDF