~/sci-study-guide/generate_sci_pdf.py
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import cm, mm
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_JUSTIFY
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, PageBreak, KeepTogether
)
from reportlab.platypus.flowables import BalancedColumns
from reportlab.graphics.shapes import Drawing, Rect, String, Circle, Line
from reportlab.graphics import renderPDF
OUTPUT = "/home/daytona/workspace/sci-study-guide/SCI_Study_Guide.pdf"
# ── Colour palette ──────────────────────────────────────────────────────────
NAVY = colors.HexColor("#1A2C5B")
BLUE = colors.HexColor("#2155A3")
LBLUE = colors.HexColor("#D6E4F7")
TEAL = colors.HexColor("#1B7F8E")
LTEAL = colors.HexColor("#D0EEF2")
RED = colors.HexColor("#C0392B")
LRED = colors.HexColor("#FAD7D2")
AMBER = colors.HexColor("#D97706")
LAMBER = colors.HexColor("#FEF3C7")
GREEN = colors.HexColor("#1A7A3F")
LGREEN = colors.HexColor("#D4EDDA")
LGRAY = colors.HexColor("#F2F4F8")
DGRAY = colors.HexColor("#4A4A4A")
WHITE = colors.white
BLACK = colors.black
# ── Styles ───────────────────────────────────────────────────────────────────
styles = getSampleStyleSheet()
def make_style(name, parent="Normal", **kw):
return ParagraphStyle(name, parent=styles[parent], **kw)
H1 = make_style("H1", fontSize=22, textColor=WHITE, fontName="Helvetica-Bold",
spaceAfter=0, spaceBefore=0, alignment=TA_CENTER)
H2 = make_style("H2", fontSize=13, textColor=WHITE, fontName="Helvetica-Bold",
spaceAfter=2, spaceBefore=0, leftIndent=0)
H3 = make_style("H3", fontSize=11, textColor=NAVY, fontName="Helvetica-Bold",
spaceAfter=3, spaceBefore=6)
BODY = make_style("BODY", fontSize=9, textColor=DGRAY, fontName="Helvetica",
spaceAfter=4, spaceBefore=2, leading=13)
SMALL = make_style("SMALL", fontSize=8, textColor=DGRAY, fontName="Helvetica",
spaceAfter=2, spaceBefore=1, leading=11)
BOLD_BODY = make_style("BOLD_BODY", fontSize=9, textColor=BLACK, fontName="Helvetica-Bold",
spaceAfter=2, spaceBefore=2)
TH = make_style("TH", fontSize=8.5, textColor=WHITE, fontName="Helvetica-Bold",
alignment=TA_CENTER, leading=10)
TD = make_style("TD", fontSize=8, textColor=DGRAY, fontName="Helvetica",
alignment=TA_CENTER, leading=10)
TD_L = make_style("TD_L", fontSize=8, textColor=DGRAY, fontName="Helvetica",
alignment=TA_LEFT, leading=10)
BULLET = make_style("BULLET", fontSize=8.5, textColor=DGRAY, fontName="Helvetica",
spaceAfter=3, spaceBefore=1, leftIndent=12, firstLineIndent=-8, leading=12)
CAPTION = make_style("CAPTION", fontSize=7.5, textColor=colors.grey, fontName="Helvetica-Oblique",
alignment=TA_CENTER, spaceAfter=4)
WARN = make_style("WARN", fontSize=9, textColor=RED, fontName="Helvetica-Bold",
alignment=TA_CENTER, spaceAfter=2)
NOTE = make_style("NOTE", fontSize=8.5, textColor=NAVY, fontName="Helvetica",
leftIndent=8, leading=12, spaceAfter=3)
# ── Helper builders ──────────────────────────────────────────────────────────
def section_header(text, color=NAVY):
"""Coloured section banner."""
tbl = Table([[Paragraph(text, H2)]], colWidths=[17*cm])
tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), color),
("TOPPADDING", (0,0), (-1,-1), 6),
("BOTTOMPADDING", (0,0), (-1,-1), 6),
("LEFTPADDING", (0,0), (-1,-1), 10),
("RIGHTPADDING", (0,0), (-1,-1), 6),
("ROWBACKGROUNDS",(0,0), (-1,-1), [color]),
]))
return tbl
def info_box(title, paragraphs_list, bg=LBLUE, border=BLUE):
"""Shaded info box."""
content = [Paragraph(f"<b>{title}</b>", make_style("IBH", fontSize=9.5,
textColor=border, fontName="Helvetica-Bold", spaceAfter=4))]
for p in paragraphs_list:
content.append(Paragraph(p, BULLET))
tbl = Table([[content]], colWidths=[16.6*cm])
tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), bg),
("BOX", (0,0), (-1,-1), 1, border),
("TOPPADDING", (0,0), (-1,-1), 8),
("BOTTOMPADDING",(0,0), (-1,-1), 8),
("LEFTPADDING", (0,0), (-1,-1), 10),
("RIGHTPADDING", (0,0), (-1,-1), 10),
]))
return tbl
def warning_box(text):
tbl = Table([[Paragraph(f"⚠ {text}", WARN)]], colWidths=[16.6*cm])
tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), LRED),
("BOX", (0,0), (-1,-1), 1.5, RED),
("TOPPADDING", (0,0), (-1,-1), 7),
("BOTTOMPADDING",(0,0), (-1,-1), 7),
("LEFTPADDING", (0,0), (-1,-1), 10),
("RIGHTPADDING", (0,0), (-1,-1), 10),
]))
return tbl
def green_box(text):
tbl = Table([[Paragraph(text, make_style("GBT", fontSize=9, textColor=GREEN,
fontName="Helvetica-Bold", alignment=TA_CENTER))]], colWidths=[16.6*cm])
tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), LGREEN),
("BOX", (0,0), (-1,-1), 1, GREEN),
("TOPPADDING", (0,0), (-1,-1), 7),
("BOTTOMPADDING",(0,0), (-1,-1), 7),
]))
return tbl
def amber_box(lines):
content = [Paragraph(l, make_style("ABT", fontSize=8.5, textColor=AMBER,
fontName="Helvetica", spaceAfter=2, leading=12)) for l in lines]
tbl = Table([content], colWidths=[16.6*cm / len(lines)] * len(lines))
tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), LAMBER),
("BOX", (0,0), (-1,-1), 1, AMBER),
("TOPPADDING", (0,0), (-1,-1), 6),
("BOTTOMPADDING",(0,0), (-1,-1), 6),
("LEFTPADDING", (0,0), (-1,-1), 8),
]))
return tbl
def make_table(headers, rows, col_widths, header_color=NAVY, alt_color=LGRAY):
data = [[Paragraph(h, TH) for h in headers]]
for i, row in enumerate(rows):
styles_row = []
for j, cell in enumerate(row):
st = TD if j > 0 else TD_L
styles_row.append(Paragraph(str(cell), st))
data.append(styles_row)
tbl = Table(data, colWidths=col_widths, repeatRows=1)
style_cmds = [
("BACKGROUND", (0,0), (-1,0), header_color),
("ROWBACKGROUNDS",(0,1), (-1,-1), [WHITE, alt_color]),
("GRID", (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING",(0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 5),
]
tbl.setStyle(TableStyle(style_cmds))
return tbl
# ── Spine tract diagram (SVG-like via Drawing) ────────────────────────────────
def tract_diagram():
"""Simple schematic of spinal cord cross-section tracts."""
d = Drawing(200, 160)
# outer cord oval
from reportlab.graphics.shapes import Ellipse, String, Polygon, Rect as R
d.add(Ellipse(100, 80, 90, 65, fillColor=colors.HexColor("#F5E6D3"),
strokeColor=DGRAY, strokeWidth=1.5))
# gray matter butterfly
d.add(Ellipse(100, 80, 28, 40, fillColor=colors.HexColor("#D4B8A0"),
strokeColor=DGRAY, strokeWidth=1))
# dorsal columns - top
d.add(Ellipse(100, 130, 22, 12, fillColor=colors.HexColor("#4CAF50"),
strokeColor=GREEN, strokeWidth=1))
d.add(String(100, 127, "Dorsal Col.", fontSize=6, fillColor=WHITE,
textAnchor="middle", fontName="Helvetica-Bold"))
# lateral CST - sides
d.add(Ellipse(58, 88, 15, 22, fillColor=BLUE, strokeColor=NAVY, strokeWidth=1))
d.add(String(58, 84, "Lat CST", fontSize=5.5, fillColor=WHITE,
textAnchor="middle", fontName="Helvetica-Bold"))
d.add(Ellipse(142, 88, 15, 22, fillColor=BLUE, strokeColor=NAVY, strokeWidth=1))
d.add(String(142, 84, "Lat CST", fontSize=5.5, fillColor=WHITE,
textAnchor="middle", fontName="Helvetica-Bold"))
# spinothalamic - anterolateral
d.add(Ellipse(65, 52, 16, 14, fillColor=RED, strokeColor=colors.darkred, strokeWidth=1))
d.add(String(65, 49, "SpThal", fontSize=5.5, fillColor=WHITE,
textAnchor="middle", fontName="Helvetica-Bold"))
d.add(Ellipse(135, 52, 16, 14, fillColor=RED, strokeColor=colors.darkred, strokeWidth=1))
d.add(String(135, 49, "SpThal", fontSize=5.5, fillColor=WHITE,
textAnchor="middle", fontName="Helvetica-Bold"))
# labels
d.add(String(100, 8, "Schematic: Major Spinal Cord Tracts", fontSize=7,
fillColor=DGRAY, textAnchor="middle", fontName="Helvetica-Oblique"))
d.add(String(100, 148, "Posterior", fontSize=7, fillColor=DGRAY,
textAnchor="middle", fontName="Helvetica"))
d.add(String(100, 18, "Anterior", fontSize=7, fillColor=DGRAY,
textAnchor="middle", fontName="Helvetica"))
return d
# ── Document ─────────────────────────────────────────────────────────────────
doc = SimpleDocTemplate(
OUTPUT,
pagesize=A4,
leftMargin=2*cm, rightMargin=2*cm,
topMargin=2*cm, bottomMargin=2*cm,
title="Spinal Cord Injury - Classification & Syndromes Study Guide",
author="Orris Medical Education",
)
story = []
# ════════════════════════════════════════════════════════════════════════════
# COVER BANNER
# ════════════════════════════════════════════════════════════════════════════
cover = Table(
[[Paragraph("SPINAL CORD INJURIES", H1)],
[Paragraph("Classification & Syndromes - Medical Student Study Guide",
make_style("SUB", fontSize=13, textColor=colors.HexColor("#B8D4F5"),
fontName="Helvetica", alignment=TA_CENTER))],
[Paragraph("Bradley & Daroff's Neurology | Tintinalli's EM | Campbell's Orthopaedics | Rockwood & Green's Fractures",
make_style("SRC", fontSize=8, textColor=colors.HexColor("#8AAFD4"),
fontName="Helvetica-Oblique", alignment=TA_CENTER))]],
colWidths=[17*cm]
)
cover.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), NAVY),
("TOPPADDING", (0,0), (0,0), 18),
("BOTTOMPADDING",(0,2), (0,2), 14),
("TOPPADDING", (0,1), (0,2), 4),
("LEFTPADDING", (0,0), (-1,-1), 10),
("RIGHTPADDING", (0,0), (-1,-1), 10),
("LINEBELOW", (0,0), (-1,-1), 2, colors.HexColor("#2E86C1")),
]))
story.append(cover)
story.append(Spacer(1, 10))
# ════════════════════════════════════════════════════════════════════════════
# SECTION 1 — EPIDEMIOLOGY & BASICS
# ════════════════════════════════════════════════════════════════════════════
story.append(section_header("1. EPIDEMIOLOGY & BASICS", NAVY))
story.append(Spacer(1, 6))
epi_rows = [
["Prevalence (USA)", "~259,000 patients"],
["Peak age", "Young adults (15-35 yrs) — bimodal with elderly >65 yrs"],
["Male:Female", "~4:1"],
["Most common level", "Cervical (55%) > Thoracic (30%) > Lumbar/Sacral (15%)"],
]
epi_tbl = make_table(["Parameter", "Detail"], epi_rows,
[5*cm, 11.6*cm], header_color=TEAL)
story.append(epi_tbl)
story.append(Spacer(1, 6))
story.append(Paragraph("<b>Common Causes</b>", H3))
causes = [
"Motor vehicle collisions — most common cause overall",
"Falls — leading cause in elderly; increasingly common",
"Violence / penetrating trauma (gunshots, stab wounds)",
"Sports / recreational injuries (diving, contact sports)",
]
for c in causes:
story.append(Paragraph(f"• {c}", BULLET))
story.append(Spacer(1, 4))
# ════════════════════════════════════════════════════════════════════════════
# SECTION 2 — ASIA IMPAIRMENT SCALE
# ════════════════════════════════════════════════════════════════════════════
story.append(Spacer(1, 6))
story.append(section_header("2. ASIA IMPAIRMENT SCALE (AIS)", BLUE))
story.append(Spacer(1, 6))
story.append(Paragraph(
"The <b>American Spinal Injury Association (ASIA) Impairment Scale</b> is the international standard for "
"classifying SCI severity. It is based on the neurological level of injury (NLI) and the presence of "
"<b>sacral sparing</b> (S4-S5 sensory/motor function).",
BODY))
story.append(Spacer(1, 4))
asia_rows = [
["A", "Complete", "No sensory or motor function preserved in sacral segments S4-5."],
["B", "Sensory incomplete", "Sensory but NOT motor preserved below NLI, including S4-5.\nNo motor >3 levels below motor level on either side."],
["C", "Motor incomplete", "Motor preserved; LESS than half of key muscles below NLI have grade ≥3.\n(Sacral sparing present: VAC or S4-5 sensation)"],
["D", "Motor incomplete", "Motor preserved; AT LEAST HALF of key muscles below NLI have grade ≥3."],
["E", "Normal", "Sensation and motor normal in all segments (only assigned if prior deficits existed)."],
]
asia_tbl = make_table(["Grade", "Type", "Description"], asia_rows,
[1.5*cm, 3.5*cm, 12.5*cm], header_color=BLUE)
story.append(asia_tbl)
story.append(Spacer(1, 6))
story.append(warning_box(
"AIS CANNOT be determined during spinal shock! "
"Wait for return of the bulbocavernosus reflex before classifying."
))
story.append(Spacer(1, 6))
story.append(info_box("Key Concepts: Sacral Sparing", [
"<b>Why sacral segments?</b> The S4-S5 segments are the most caudal — if ANY function persists there, the injury is incomplete.",
"<b>Tests for sacral sparing:</b> Light touch at S4-5 dermatome (perianal), pinprick at S4-5, deep anal pressure (DAP), or voluntary anal contraction (VAC).",
"<b>NLI (Neurological Level of Injury):</b> The most caudal level with normal sensory AND motor function bilaterally.",
"<b>Zone of Partial Preservation (ZPP):</b> Dermatomes/myotomes caudal to NLI with partial function — only relevant in complete (AIS A) injuries.",
], LBLUE, BLUE))
story.append(Spacer(1, 6))
# Prognosis factors
story.append(Paragraph("<b>Prognostic Factors</b>", H3))
prog_data = [
["Greater sparing of motor/sensory function caudal to injury", "Better recovery expected"],
["Earlier onset + faster progression of recovery", "Better prognosis"],
["Age < 50 years", "Better prognosis vs. same deficit in older patient"],
["Recovery window", "Up to 12-15 months; once progress halts, no further recovery"],
["Complete injury (AIS A)", "Poor functional recovery; only zone of partial preservation may improve"],
]
prog_tbl = make_table(["Factor", "Implication"], prog_data,
[9*cm, 8.5*cm], header_color=TEAL)
story.append(prog_tbl)
# ════════════════════════════════════════════════════════════════════════════
# SECTION 3 — SPINAL CORD SYNDROMES
# ════════════════════════════════════════════════════════════════════════════
story.append(PageBreak())
story.append(section_header("3. INCOMPLETE SPINAL CORD SYNDROMES", NAVY))
story.append(Spacer(1, 6))
story.append(Paragraph(
"In incomplete injuries, recognising the <b>syndrome pattern</b> guides prognosis and treatment decisions. "
"Complete injuries (AIS A) may show recovery only within the zone of partial preservation.",
BODY))
story.append(Spacer(1, 8))
# 3.1 CENTRAL CORD
story.append(KeepTogether([
section_header("3.1 Central Cord Syndrome (Most Common — ~70% of incomplete SCIs)", TEAL),
Spacer(1, 5),
make_table(
["Feature", "Detail"],
[
["Frequency", "Most common incomplete SCI syndrome"],
["Mechanism", "Hyperextension injury + pre-existing cervical spondylosis (older patients)\nCord pinched between disc/osteophyte anteriorly & buckled ligamentum flavum posteriorly"],
["Typical patient", "Elderly with cervical stenosis; may have no fracture"],
["Motor deficit", "UE weakness > LE weakness (arm fibers are most MEDIAL in corticospinal tracts);\nGreater dysfunction distally than proximally"],
["Sensory deficit", "Variable; sacral pinprick sensation usually PRESERVED"],
["Bladder", "Dysfunction common (urinary retention)"],
["Prognosis", "GOOD — >50% regain bowel/bladder control, become ambulatory, improve hand function"],
],
[4*cm, 12.5*cm], header_color=TEAL
),
Spacer(1, 4),
info_box("Why UE > LE?", [
"The corticospinal tracts are somatotopically arranged: <b>arm fibers lie most medially</b> (closest to central gray).",
"Central cord injury preferentially damages these medial arm fibers while relatively sparing the lateral leg fibers.",
], LTEAL, TEAL),
Spacer(1, 8),
]))
# 3.2 ANTERIOR CORD
story.append(KeepTogether([
section_header("3.2 Anterior Cord Syndrome", RED),
Spacer(1, 5),
make_table(
["Feature", "Detail"],
[
["Frequency", "~2.7% of all traumatic SCIs"],
["Mechanism", "Flexion injury; retropulsed disc/bone; anterior spinal artery thrombosis (ischemic)"],
["Tracts involved", "Corticospinal tracts + spinothalamic tracts (anterior 2/3 of cord)"],
["Motor deficit", "LOST below lesion (bilateral)"],
["Pain / Temperature", "LOST below lesion (spinothalamic)"],
["Proprioception / Vibration", "PRESERVED — posterior columns spared"],
["Prognosis", "WORST of incomplete syndromes — poor functional recovery"],
],
[4*cm, 12.5*cm], header_color=RED
),
Spacer(1, 8),
]))
# 3.3 BROWN-SEQUARD
story.append(KeepTogether([
section_header("3.3 Brown-Sequard Syndrome", colors.HexColor("#6A0572")),
Spacer(1, 5),
make_table(
["Feature", "Detail"],
[
["Mechanism", "Hemisection of spinal cord; most common cause = PENETRATING trauma"],
["Ipsilateral (same side)", "Spastic paresis (corticospinal tract)\nLoss of proprioception & vibration (dorsal columns)"],
["Contralateral (opposite)", "Loss of pain & temperature (spinothalamic — crosses within 1-2 levels)"],
["Why crossed?", "Spinothalamic tract crosses at the level of entry; dorsal columns ascend ipsilaterally to medulla"],
["Prognosis", "GOOD"],
],
[4*cm, 12.5*cm], header_color=colors.HexColor("#6A0572")
),
Spacer(1, 5),
info_box("Memory Trick: Brown-Sequard", [
"<b>IPSI</b>lateral = <b>I</b> <b>P</b>aralysed my <b>I</b>psilateral side and lost <b>P</b>roprioception",
"<b>CONTRA</b>lateral = <b>P</b>ain & Temperature cross to the <b>C</b>ontralateral side",
], colors.HexColor("#F3E5F5"), colors.HexColor("#6A0572")),
Spacer(1, 8),
]))
# 3.4 POSTERIOR COLUMN
story.append(KeepTogether([
section_header("3.4 Posterior Column Syndrome (Rare <1%)", colors.HexColor("#795548")),
Spacer(1, 5),
make_table(
["Feature", "Detail"],
[
["Mechanism", "Hyperextension injuries"],
["Deficit", "Loss of vibration and proprioception; pain/temperature RETAINED"],
["Motor", "Intact"],
["Prognosis", "Good"],
],
[4*cm, 12.5*cm], header_color=colors.HexColor("#795548")
),
Spacer(1, 8),
]))
# 3.5 CAUDA EQUINA
story.append(KeepTogether([
section_header("3.5 Cauda Equina Syndrome", RED),
Spacer(1, 5),
make_table(
["Feature", "Detail"],
[
["Anatomy", "Below L1 — compression of lumbar & sacral NERVE ROOTS (not spinal cord)"],
["Motor", "Asymmetric FLACCID leg weakness (LMN — decreased reflexes)"],
["Sensory", "SADDLE ANESTHESIA (S3-S5 dermatomes — perianal, perineum, inner thighs)"],
["Bowel/Bladder", "Urinary/bowel incontinence; areflexic bladder; loss of rectal tone"],
["Signs", "Pure LMN signs — hyporeflexia/areflexia; NO Babinski"],
["Emergency?", "YES — surgical decompression urgently needed"],
],
[4*cm, 12.5*cm], header_color=RED
),
Spacer(1, 5),
warning_box("Cauda Equina Syndrome is a SURGICAL EMERGENCY — decompress urgently!"),
Spacer(1, 8),
]))
# 3.6 CONUS MEDULLARIS
story.append(KeepTogether([
section_header("3.6 Conus Medullaris Syndrome", AMBER),
Spacer(1, 5),
make_table(
["Feature", "Detail"],
[
["Level", "L1-L2 (tip of spinal cord)"],
["Key difference from Cauda Equina", "MIXED UMN + LMN signs — distinguishes it"],
["UMN signs", "Spasticity, hyperreflexia, Babinski — from cord involvement"],
["LMN signs", "Flaccidity, areflexia — from anterior horn / root involvement"],
["Bowel/Bladder", "Dysfunction present (similar to cauda equina)"],
],
[5*cm, 11.5*cm], header_color=AMBER
),
Spacer(1, 8),
]))
# ════════════════════════════════════════════════════════════════════════════
# SECTION 4 — MASTER COMPARISON TABLE
# ════════════════════════════════════════════════════════════════════════════
story.append(PageBreak())
story.append(section_header("4. MASTER COMPARISON TABLE", NAVY))
story.append(Spacer(1, 6))
comp_rows = [
["Central Cord", "Hyperextension + spondylosis", "Arms > Legs (UMN)", "Variable", "Preserved (sacral)", "GOOD"],
["Anterior Cord", "Flexion / ASA thrombosis", "Bilateral below", "Bilateral below", "PRESERVED", "POOR"],
["Brown-Sequard", "Penetrating / hemisection", "IPSILATERAL", "CONTRALATERAL", "IPSILATERAL lost", "GOOD"],
["Post. Column", "Hyperextension", "Intact", "Intact", "LOST (both sides)", "GOOD"],
["Cauda Equina", "Disc/tumor at L1+", "Flaccid (LMN)", "Saddle area", "Lost", "Variable"],
["Conus Medullaris", "L1-L2 injury", "Mixed UMN+LMN", "Similar to CE", "Lost", "Variable"],
]
comp_tbl = make_table(
["Syndrome", "Mechanism", "Motor Loss", "Pain/Temp Loss", "Proprioception/Vib", "Prognosis"],
comp_rows,
[3*cm, 3.5*cm, 3*cm, 3*cm, 3*cm, 2*cm],
header_color=NAVY
)
story.append(comp_tbl)
story.append(Spacer(1, 8))
story.append(Paragraph("<b>Tract Anatomy Reference</b>", H3))
story.append(Paragraph(
"Understanding which tracts are damaged in each syndrome requires knowing their locations in the cord cross-section:",
BODY))
story.append(Spacer(1, 4))
tract_rows = [
["Dorsal (Posterior) Columns", "Posterior cord", "Ipsilateral proprioception, vibration, fine touch", "Ascends ipsilateral to medulla then crosses"],
["Lateral Corticospinal Tract", "Lateral white matter", "Ipsilateral voluntary motor", "Crosses at pyramids in medulla"],
["Spinothalamic Tract", "Anterolateral white matter", "Contralateral pain and temperature", "Crosses within 1-2 spinal levels of entry"],
]
tract_tbl = make_table(
["Tract", "Location", "Function", "Crossing"],
tract_rows,
[4*cm, 3.5*cm, 5*cm, 5*cm],
header_color=TEAL
)
story.append(tract_tbl)
story.append(Spacer(1, 8))
# ════════════════════════════════════════════════════════════════════════════
# SECTION 5 — SPINAL SHOCK vs NEUROGENIC SHOCK
# ════════════════════════════════════════════════════════════════════════════
story.append(section_header("5. SPINAL SHOCK vs NEUROGENIC SHOCK", RED))
story.append(Spacer(1, 6))
story.append(warning_box("These are NOT the same thing! Confusing them is a classic exam error."))
story.append(Spacer(1, 6))
shock_rows = [
["Nature", "Neurological phenomenon", "Hemodynamic (cardiovascular) phenomenon"],
["Definition", "Temporary loss of ALL spinal cord\nreflexes below injury level",
"Hypotension + bradycardia from loss\nof sympathetic tone"],
["Reflexes", "Areflexia (bulbocavernosus,\ncremasteric, DTRs absent)",
"Normal or depressed reflexes"],
["Blood pressure","Normal", "LOW (hypotension)"],
["Heart rate", "Normal", "LOW (bradycardia) — esp. C5 and above;\n may see tachycardia in lower injuries"],
["Skin", "Variable", "Warm, dry (vasodilation)"],
["Resolution", "Return of bulbocavernosus reflex\n(BCR marks end of spinal shock)",
"With vasopressor support"],
["Treatment", "Supportive; defer AIS classification",
"IV fluids + vasopressors\n(norepinephrine preferred if bradycardic)"],
]
shock_tbl = make_table(
["Feature", "SPINAL SHOCK", "NEUROGENIC SHOCK"],
shock_rows,
[3.5*cm, 6.5*cm, 7.5*cm],
header_color=RED
)
story.append(shock_tbl)
story.append(Spacer(1, 6))
story.append(info_box("Target MAP in Acute SCI", [
"Maintain MAP ≥ 85-90 mmHg for 7 days after injury to optimise spinal cord perfusion.",
"Phenylephrine was historically used; <b>norepinephrine is now preferred</b>, especially with bradycardia.",
"High cervical injuries (C5+): bradycardia + hypotension — atropine may be needed.",
], LRED, RED))
# ════════════════════════════════════════════════════════════════════════════
# SECTION 6 — ACUTE MANAGEMENT
# ════════════════════════════════════════════════════════════════════════════
story.append(PageBreak())
story.append(section_header("6. ACUTE MANAGEMENT", NAVY))
story.append(Spacer(1, 6))
mgmt_rows = [
["Airway", "HIGH priority — C3/C4/C5 supply diaphragm (phrenic nerve)\nIntubate without neck manipulation in cervical injuries"],
["Immobilisation", "Cervical collar + spinal precautions; rigid backboard\nEssential until injury excluded/stabilised"],
["Neurogenic shock", "IV fluids first; vasopressors (norepinephrine) if MAP <85-90 mmHg\nMaintain MAP 85-90 for 7 days"],
["Bladder", "Urinary catheterisation (avoid retention-induced AD)"],
["Steroids", "High-dose methylprednisolone is NO LONGER recommended\n(evidence of harm > benefit per current guidelines)"],
["Imaging: MRI", "Gold standard — confirms level/extent of injury; full cord scan\nEmergent in all acute SCIs"],
["Imaging: CT", "NOT sufficient to rule out spinal cord compression alone\nUseful for bony injury assessment"],
["SCIWORA", "SCI Without Radiological Abnormality — no finding on X-ray or CT\nOccurs especially in children; MRI may show cord signal change"],
]
mgmt_tbl = make_table(
["Step / Issue", "Action / Detail"],
mgmt_rows,
[4.5*cm, 12.5*cm],
header_color=NAVY
)
story.append(mgmt_tbl)
story.append(Spacer(1, 8))
# ════════════════════════════════════════════════════════════════════════════
# SECTION 7 — COMPLICATIONS
# ════════════════════════════════════════════════════════════════════════════
story.append(section_header("7. KEY COMPLICATIONS", TEAL))
story.append(Spacer(1, 6))
# Autonomic dysreflexia
story.append(Paragraph("<b>7.1 Autonomic Dysreflexia (AD)</b>", H3))
ad_rows = [
["Level at risk", "Injuries at T6 and ABOVE (above major splanchnic outflow)"],
["Trigger", "Any noxious stimulus below level of injury"],
["Common triggers", "Bladder distension (most common), constipation, rectal fissure, pressure sores, uterine contractions, UTI"],
["Presentation", "Severe paroxysmal HYPERTENSION, pounding headache, bradycardia, diaphoresis above lesion, piloerection, flushing, nasal congestion"],
["Treatment", "1. Sit patient UPRIGHT (reduces BP)\n2. Find and remove the trigger — catheterise bladder, disimpact bowel\n3. If persists: nifedipine (oral/sublingual), nitrates (transdermal/spray)"],
["In pregnancy", "Difficult to distinguish from preeclampsia\nAD: symptoms resolve once stimulus removed\nPreeclampsia: symptoms + labs persist"],
["In labour", "Lesions above T10: labour may be imperceptible; monitor uterine contractions\nUse regional anaesthesia (spinal/epidural) to prevent AD during labour"],
]
ad_tbl = make_table(["Feature", "Detail"], ad_rows, [3.5*cm, 14*cm], header_color=TEAL)
story.append(ad_tbl)
story.append(Spacer(1, 8))
# Other complications 2-column
story.append(Paragraph("<b>7.2 Other Important Complications</b>", H3))
comp_data = [
["RESPIRATORY", [
"C3-C5: phrenic nerve → diaphragm paralysis → ventilatory failure",
"All cervical SCIs: impaired cough reflex → pneumonia risk",
"Requires ICU-level respiratory monitoring",
]],
["BLADDER DYSFUNCTION", [
"Suprasacral (UMN): detrusor overactivity, vesicoureteral reflux",
"Sacral/infrasacral (LMN): flaccid areflexic bladder, retention",
"CIC (clean intermittent catheterisation) is mainstay",
"Historically: renal failure was leading cause of death in SCI",
]],
["VTE (DVT/PE)", [
"Chronic immobilisation = high VTE risk",
"Prophylaxis: LMWH + compression stockings mandatory",
"Pregnant SCI patients: 9× higher VTE risk vs controls",
]],
["SPASTICITY", [
"UMN injuries → spastic paraplegia/tetraplegia",
"Rx: baclofen (oral or intrathecal), tizanidine, botulinum toxin",
]],
["NEUROPATHIC PAIN", [
"Below-level pain common in chronic SCI",
"Rx: gabapentin, pregabalin, tricyclics (amitriptyline)",
]],
["POSTTRAUMATIC SYRINGOMYELIA", [
"Delayed cystic cavity formation in spinal cord",
"Presents with ascending deficit, dissociated sensory loss, hyperhidrosis",
"Treat: surgical syrinx drainage/shunting",
]],
]
for item in comp_data:
story.append(KeepTogether([
info_box(item[0], item[1], LGRAY, TEAL),
Spacer(1, 4),
]))
# ════════════════════════════════════════════════════════════════════════════
# SECTION 8 — HIGH-YIELD EXAM PEARLS
# ════════════════════════════════════════════════════════════════════════════
story.append(PageBreak())
story.append(section_header("8. HIGH-YIELD EXAM PEARLS", colors.HexColor("#B7410E")))
story.append(Spacer(1, 8))
pearls = [
("<b>Central Cord</b>", "Most common incomplete SCI. UE > LE weakness. Hyperextension + spondylosis. Good prognosis."),
("<b>Anterior Cord</b>", "Motor + pain/temp lost; PROPRIOCEPTION PRESERVED. Worst prognosis of incomplete syndromes."),
("<b>Brown-Sequard</b>", "IPSI: paresis + proprioception loss. CONTRA: pain/temp loss. Most common cause = penetrating trauma."),
("<b>Cauda Equina</b>", "Below L1 — LMN signs only. Saddle anesthesia. Areflexic bladder. SURGICAL EMERGENCY."),
("<b>Conus vs Cauda</b>", "Conus = MIXED UMN+LMN. Cauda = pure LMN. Both at L1 level but conus involves cord itself."),
("<b>Spinal vs Neurogenic Shock</b>", "Spinal = reflex loss (neuro). Neurogenic = hypotension+bradycardia (haemodynamic). NOT synonymous."),
("<b>AIS Classification</b>", "AIS A = complete (no S4-S5 function). AIS B-D = incomplete. Cannot classify during spinal shock."),
("<b>Autonomic Dysreflexia</b>", "T6 and above. Triggered by visceral stimuli. Severe HTN + headache. Sit up + find/remove trigger."),
("<b>Steroids</b>", "High-dose methylprednisolone is NO LONGER recommended in acute traumatic SCI."),
("<b>Phrenic nerve</b>", "C3, C4, C5 keep the diaphragm alive. Injuries at/above C5 risk respiratory failure."),
("<b>Bulbocavernosus reflex</b>", "Its RETURN marks the end of spinal shock. Only then can reliable AIS grading be performed."),
("<b>SCIWORA</b>", "SCI without X-ray/CT abnormality. Especially in children. MRI may show signal change."),
]
# Two-column pearl layout
pearl_cells = []
for label, text in pearls:
cell = [
Paragraph(label, make_style("PL", fontSize=8.5, textColor=NAVY,
fontName="Helvetica-Bold", spaceAfter=2)),
Paragraph(text, make_style("PT", fontSize=8, textColor=DGRAY,
fontName="Helvetica", leading=11, spaceAfter=0)),
]
pearl_cells.append(cell)
# Arrange as 2-column grid
rows_2col = []
for i in range(0, len(pearl_cells), 2):
left = pearl_cells[i]
right = pearl_cells[i+1] if i+1 < len(pearl_cells) else [[Spacer(1,1)]]
rows_2col.append([left, right])
pearl_tbl = Table(rows_2col, colWidths=[8.3*cm, 8.3*cm])
pearl_tbl.setStyle(TableStyle([
("VALIGN", (0,0), (-1,-1), "TOP"),
("BACKGROUND", (0,0), (-1,-1), LGRAY),
("ROWBACKGROUNDS",(0,0), (-1,-1), [LGRAY, WHITE]),
("BOX", (0,0), (-1,-1), 0.5, colors.HexColor("#CCCCCC")),
("INNERGRID", (0,0), (-1,-1), 0.3, colors.HexColor("#DDDDDD")),
("TOPPADDING", (0,0), (-1,-1), 7),
("BOTTOMPADDING",(0,0), (-1,-1), 7),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING", (0,0), (-1,-1), 8),
]))
story.append(pearl_tbl)
# ════════════════════════════════════════════════════════════════════════════
# FOOTER — Sources
# ════════════════════════════════════════════════════════════════════════════
story.append(Spacer(1, 12))
story.append(HRFlowable(width="100%", thickness=0.5, color=colors.HexColor("#CCCCCC")))
story.append(Spacer(1, 4))
story.append(Paragraph(
"<b>Sources:</b> Bradley & Daroff's Neurology in Clinical Practice | "
"Campbell's Operative Orthopaedics 15th Ed. 2026 | "
"Tintinalli's Emergency Medicine | "
"Rosen's Emergency Medicine | "
"Sabiston Textbook of Surgery | "
"The Washington Manual of Medical Therapeutics | "
"Rockwood & Green's Fractures in Adults 10th Ed. 2025",
CAPTION
))
# ════════════════════════════════════════════════════════════════════════════
# BUILD
# ════════════════════════════════════════════════════════════════════════════
doc.build(story)
print(f"PDF generated: {OUTPUT}")