Heart disease and ecg changes with them , how to know the diagnosis by looking at ecg - explain me how to read a normal ecg and then move on to ecg changes in common heart diseases and also make a pfd of it
normal ECG tracing P wave QRS complex T wave labeled
STEMI ST elevation myocardial infarction ECG leads
atrial fibrillation ECG irregularly irregular no P waves
left bundle branch block LBBB ECG wide QRS
heart block AV block first degree second degree third degree ECG PR interval
ventricular tachycardia VT ECG wide complex tachycardia
hypertrophic cardiomyopathy ECG LVH left ventricular hypertrophy voltage criteria
mkdir -p /home/daytona/workspace/ecg-guide && ls /home/daytona/workspace/ecg-gui…
~/ecg-guide/build_ecg_pdf.py
#!/usr/bin/env python3
"""
Comprehensive ECG Guide PDF Builder
Uses reportlab for PDF generation and downloads ECG images from URLs
"""
import os
import requests
from io import BytesIO
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.colors import HexColor, black, white, Color
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,
Image, PageBreak, HRFlowable, KeepTogether
)
from reportlab.platypus.flowables import Flowable
from reportlab.pdfgen import canvas
from reportlab.lib import colors
# ── Colour palette ──────────────────────────────────────────────────────────
DEEP_NAVY = HexColor("#0A1931")
CARD_BLUE = HexColor("#1A3A5C")
ACCENT_RED = HexColor("#C0392B")
ACCENT_TEAL = HexColor("#1ABC9C")
ACCENT_GOLD = HexColor("#F39C12")
LIGHT_BG = HexColor("#F0F4F8")
MID_GREY = HexColor("#7F8C8D")
DARK_TEXT = HexColor("#1C2833")
TABLE_HEADER = HexColor("#1A3A5C")
TABLE_ROW1 = HexColor("#EAF2FF")
TABLE_ROW2 = HexColor("#FFFFFF")
BOX_BLUE = HexColor("#D6EAF8")
BOX_GREEN = HexColor("#D5F5E3")
BOX_RED = HexColor("#FADBD8")
BOX_ORANGE = HexColor("#FDEBD0")
W, H = A4 # 595 × 842 pt
# ── Image download helper ────────────────────────────────────────────────────
def fetch_image(url, max_w=13*cm, max_h=7*cm):
"""Download image from URL and return a ReportLab Image flowable."""
try:
r = requests.get(url, timeout=15)
r.raise_for_status()
img_data = BytesIO(r.content)
img = Image(img_data)
# Scale proportionally
iw, ih = img.imageWidth, img.imageHeight
scale = min(max_w / iw, max_h / ih)
img.drawWidth = iw * scale
img.drawHeight = ih * scale
return img
except Exception as e:
print(f" [WARN] Could not fetch {url[:60]}: {e}")
return None
# ── Page numbering canvas ────────────────────────────────────────────────────
def add_page_number(canvas, doc):
canvas.saveState()
# Footer bar
canvas.setFillColor(DEEP_NAVY)
canvas.rect(0, 0, W, 22, fill=1, stroke=0)
canvas.setFillColor(white)
canvas.setFont("Helvetica", 8)
canvas.drawCentredString(W/2, 7, f"ECG Interpretation Guide • Page {doc.page}")
canvas.drawString(30, 7, "© Orris Medical Education")
canvas.restoreState()
# ── Styles ───────────────────────────────────────────────────────────────────
styles = getSampleStyleSheet()
def make_style(name, parent="Normal", **kw):
return ParagraphStyle(name, parent=styles[parent], **kw)
S_TITLE = make_style("S_TITLE", fontSize=30, leading=36, textColor=white,
fontName="Helvetica-Bold", alignment=TA_CENTER)
S_SUBTITLE = make_style("S_SUBTITLE", fontSize=14, leading=18, textColor=ACCENT_TEAL,
fontName="Helvetica-Bold", alignment=TA_CENTER)
S_H1 = make_style("S_H1", fontSize=18, leading=22, textColor=white,
fontName="Helvetica-Bold", alignment=TA_LEFT,
spaceBefore=6, spaceAfter=4)
S_H2 = make_style("S_H2", fontSize=13, leading=17, textColor=CARD_BLUE,
fontName="Helvetica-Bold", spaceBefore=8, spaceAfter=3)
S_H3 = make_style("S_H3", fontSize=11, leading=14, textColor=ACCENT_RED,
fontName="Helvetica-Bold", spaceBefore=4, spaceAfter=2)
S_BODY = make_style("S_BODY", fontSize=9.5, leading=14, textColor=DARK_TEXT,
alignment=TA_JUSTIFY, spaceAfter=4)
S_BULLET = make_style("S_BULLET", fontSize=9.5, leading=14, textColor=DARK_TEXT,
leftIndent=14, bulletIndent=4, spaceAfter=2)
S_CAPTION = make_style("S_CAPTION", fontSize=8, leading=11, textColor=MID_GREY,
alignment=TA_CENTER, fontName="Helvetica-Oblique")
S_TABLE_H = make_style("S_TABLE_H", fontSize=9, leading=12, textColor=white,
fontName="Helvetica-Bold", alignment=TA_CENTER)
S_TABLE_C = make_style("S_TABLE_C", fontSize=8.5, leading=12, textColor=DARK_TEXT,
alignment=TA_LEFT)
S_TABLE_CC = make_style("S_TABLE_CC", fontSize=8.5, leading=12, textColor=DARK_TEXT,
alignment=TA_CENTER)
S_WARN = make_style("S_WARN", fontSize=9, leading=13, textColor=HexColor("#922B21"),
fontName="Helvetica-Bold")
S_TIP = make_style("S_TIP", fontSize=9, leading=13, textColor=HexColor("#1A5276"),
fontName="Helvetica-Bold")
# ── Section header flowable ──────────────────────────────────────────────────
class SectionHeader(Flowable):
def __init__(self, text, color=CARD_BLUE, height=28):
super().__init__()
self.text = text
self.color = color
self.height = height
self.width = W - 4*cm
def draw(self):
c = self.canv
c.setFillColor(self.color)
c.roundRect(0, 0, self.width, self.height, 6, fill=1, stroke=0)
c.setFillColor(white)
c.setFont("Helvetica-Bold", 13)
c.drawString(12, 8, self.text)
def wrap(self, aw, ah):
return self.width, self.height
# ── Coloured box helper ──────────────────────────────────────────────────────
def coloured_box(content_paras, bg_color, left_color=None, padding=8):
"""Wrap paragraphs in a coloured background table."""
cell = [p for p in content_paras]
ts = TableStyle([
("BACKGROUND", (0,0), (-1,-1), bg_color),
("LEFTPADDING", (0,0), (-1,-1), padding),
("RIGHTPADDING", (0,0), (-1,-1), padding),
("TOPPADDING", (0,0), (-1,-1), padding),
("BOTTOMPADDING",(0,0), (-1,-1), padding),
("ROUNDEDCORNERS", [6]),
])
if left_color:
ts.add("LINEBEFORE", (0,0), (0,-1), 4, left_color)
t = Table([[cell]], colWidths=[W - 4*cm])
t.setStyle(ts)
return t
# ── Build content ────────────────────────────────────────────────────────────
def build_story():
story = []
# ═══════════════════════════════════════════════════════════════════════
# COVER PAGE
# ═══════════════════════════════════════════════════════════════════════
story.append(Spacer(1, 3*cm))
# Title block
title_data = [[Paragraph("ECG Interpretation", S_TITLE)],
[Paragraph("Complete Clinical Guide", S_TITLE)],
[Spacer(1, 0.3*cm)],
[Paragraph("Normal ECG · Ischemia · Arrhythmias · Conduction Defects · Hypertrophy", S_SUBTITLE)]]
title_table = Table(title_data, colWidths=[W - 3*cm])
title_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), DEEP_NAVY),
("LEFTPADDING", (0,0), (-1,-1), 20),
("RIGHTPADDING", (0,0), (-1,-1), 20),
("TOPPADDING", (0,0), (-1,-1), 14),
("BOTTOMPADDING",(0,0), (-1,-1), 14),
("ROUNDEDCORNERS", [10]),
]))
story.append(title_table)
story.append(Spacer(1, 1*cm))
# Subtitle card
intro_box = Table([[Paragraph(
"This guide teaches you to read and interpret the ECG systematically — from basic waveforms "
"to advanced disease patterns. Each section builds on the last, taking you from a normal "
"tracing to the key ECG findings in common heart diseases.",
make_style("intro", fontSize=11, leading=16, textColor=DARK_TEXT,
alignment=TA_CENTER, fontName="Helvetica")
)]], colWidths=[W - 4*cm])
intro_box.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), LIGHT_BG),
("TOPPADDING", (0,0), (-1,-1), 16),
("BOTTOMPADDING",(0,0), (-1,-1), 16),
("LEFTPADDING", (0,0), (-1,-1), 20),
("RIGHTPADDING", (0,0), (-1,-1), 20),
("ROUNDEDCORNERS", [8]),
]))
story.append(intro_box)
story.append(Spacer(1, 0.8*cm))
# Contents list
toc_data = [
[Paragraph("CONTENTS", make_style("toc_h", fontSize=12, fontName="Helvetica-Bold",
textColor=CARD_BLUE))],
[Paragraph("1. ECG Basics — Paper, Leads, Calibration", S_BODY)],
[Paragraph("2. How to Read a Normal ECG — Step by Step", S_BODY)],
[Paragraph("3. Normal Waveforms: P, QRS, T, U, PR, QT intervals", S_BODY)],
[Paragraph("4. Acute MI (STEMI) — Anterior, Inferior, Lateral, Posterior", S_BODY)],
[Paragraph("5. NSTEMI / Unstable Angina — Ischaemic Changes", S_BODY)],
[Paragraph("6. Atrial Fibrillation & Atrial Flutter", S_BODY)],
[Paragraph("7. Heart Blocks — 1°, 2° (Mobitz I/II), 3°", S_BODY)],
[Paragraph("8. Bundle Branch Blocks — LBBB & RBBB", S_BODY)],
[Paragraph("9. Ventricular Tachycardia & Ventricular Fibrillation", S_BODY)],
[Paragraph("10. Left Ventricular Hypertrophy & Hypertrophic Cardiomyopathy", S_BODY)],
[Paragraph("11. Pericarditis", S_BODY)],
[Paragraph("12. Quick Reference Summary Table", S_BODY)],
]
toc_table = Table(toc_data, colWidths=[W - 4*cm])
toc_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (0,0), CARD_BLUE),
("BACKGROUND", (0,1), (-1,-1), LIGHT_BG),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING",(0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 12),
("RIGHTPADDING", (0,0), (-1,-1), 12),
("TEXTCOLOR", (0,0), (0,0), white),
("ROWBACKGROUNDS", (0,1), (-1,-1), [TABLE_ROW1, TABLE_ROW2]),
("ROUNDEDCORNERS", [6]),
]))
story.append(toc_table)
story.append(PageBreak())
# ═══════════════════════════════════════════════════════════════════════
# SECTION 1 — ECG BASICS
# ═══════════════════════════════════════════════════════════════════════
story.append(SectionHeader("SECTION 1 — ECG BASICS: PAPER, LEADS & CALIBRATION"))
story.append(Spacer(1, 0.4*cm))
story.append(Paragraph("What is an ECG?", S_H2))
story.append(Paragraph(
"An electrocardiogram (ECG) records the electrical activity of the heart over time. "
"Electrodes placed on the skin detect tiny voltage changes produced by ion fluxes across "
"cardiac cell membranes during depolarisation and repolarisation. The resulting waveform "
"is plotted on calibrated graph paper.", S_BODY))
story.append(Paragraph("ECG Paper & Speed", S_H2))
paper_data = [
[Paragraph("Property", S_TABLE_H), Paragraph("Value", S_TABLE_H), Paragraph("Clinical Meaning", S_TABLE_H)],
[Paragraph("Paper speed", S_TABLE_C), Paragraph("25 mm/s (standard)", S_TABLE_CC), Paragraph("1 small square = 0.04 s; 1 large square = 0.2 s", S_TABLE_C)],
[Paragraph("Voltage calibration", S_TABLE_C), Paragraph("10 mm/mV", S_TABLE_CC), Paragraph("1 small square = 0.1 mV; 1 large square = 0.5 mV", S_TABLE_C)],
[Paragraph("Small square", S_TABLE_C), Paragraph("1 mm × 1 mm", S_TABLE_CC), Paragraph("0.04 s horizontally; 0.1 mV vertically", S_TABLE_C)],
[Paragraph("Large square", S_TABLE_C), Paragraph("5 mm × 5 mm", S_TABLE_CC), Paragraph("0.2 s horizontally; 0.5 mV vertically", S_TABLE_C)],
]
paper_table = Table(paper_data, colWidths=[4*cm, 4.5*cm, 8*cm])
paper_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), TABLE_HEADER),
("ROWBACKGROUNDS",(0,1), (-1,-1), [TABLE_ROW1, TABLE_ROW2]),
("GRID", (0,0), (-1,-1), 0.5, MID_GREY),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 6),
]))
story.append(paper_table)
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("The 12 Leads", S_H2))
story.append(Paragraph(
"A standard ECG uses 12 leads — each a different 'view' of the heart's electrical activity:", S_BODY))
leads_data = [
[Paragraph("Lead Group", S_TABLE_H), Paragraph("Leads", S_TABLE_H), Paragraph("Area of Heart Viewed", S_TABLE_H)],
[Paragraph("Inferior limb leads", S_TABLE_C), Paragraph("II, III, aVF", S_TABLE_CC), Paragraph("Inferior wall (RCA territory)", S_TABLE_C)],
[Paragraph("Lateral limb leads", S_TABLE_C), Paragraph("I, aVL", S_TABLE_CC), Paragraph("High lateral wall (LCx territory)", S_TABLE_C)],
[Paragraph("Right-sided", S_TABLE_C), Paragraph("aVR", S_TABLE_CC), Paragraph("Cavity / right side of heart", S_TABLE_C)],
[Paragraph("Septal precordial", S_TABLE_C), Paragraph("V1, V2", S_TABLE_CC), Paragraph("Interventricular septum / RV", S_TABLE_C)],
[Paragraph("Anterior precordial", S_TABLE_C), Paragraph("V3, V4", S_TABLE_CC), Paragraph("Anterior LV wall (LAD territory)", S_TABLE_C)],
[Paragraph("Lateral precordial", S_TABLE_C), Paragraph("V5, V6", S_TABLE_CC), Paragraph("Lateral LV wall (LCx territory)", S_TABLE_C)],
]
leads_table = Table(leads_data, colWidths=[5*cm, 3.5*cm, 8*cm])
leads_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), TABLE_HEADER),
("ROWBACKGROUNDS",(0,1), (-1,-1), [TABLE_ROW1, TABLE_ROW2]),
("GRID", (0,0), (-1,-1), 0.5, MID_GREY),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 6),
]))
story.append(leads_table)
story.append(Paragraph(
"<b>Memory Aid:</b> Reciprocal changes (opposite deflection) appear in leads facing away "
"from an infarcted zone — very useful for diagnosing infarct territory.", S_BODY))
story.append(PageBreak())
# ═══════════════════════════════════════════════════════════════════════
# SECTION 2 — NORMAL ECG STEP BY STEP
# ═══════════════════════════════════════════════════════════════════════
story.append(SectionHeader("SECTION 2 — HOW TO READ A NORMAL ECG: SYSTEMATIC APPROACH"))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph(
"Always use the same 6-step system. Never jump straight to abnormalities.", S_H2))
steps = [
("STEP 1 — Rate",
"Count R-R intervals. Divide 300 by the number of large squares between two R peaks. "
"Normal: 60–100 bpm. <b>Bradycardia</b> <60 bpm; <b>Tachycardia</b> >100 bpm."),
("STEP 2 — Rhythm",
"Is it regular (equal R-R intervals) or irregular? If irregular, is it regularly irregular "
"(predictable pattern, e.g. Wenckebach) or irregularly irregular (e.g. AF)? Look for P waves before every QRS."),
("STEP 3 — Axis",
"Normal axis: −30° to +90°. Lead I and aVF positive = normal. "
"Left axis deviation (LAD): Lead I positive, aVF negative → causes: LBBB, inferior MI, LVH. "
"Right axis deviation (RAD): Lead I negative, aVF positive → causes: RBBB, RVH, PE."),
("STEP 4 — P Wave",
"Should be upright in I, II, aVF; inverted in aVR. Duration ≤ 0.12 s; amplitude ≤ 2.5 mm. "
"Broad notched P (P mitrale) = left atrial enlargement. "
"Tall peaked P (P pulmonale, >2.5 mm in II) = right atrial enlargement."),
("STEP 5 — PR Interval",
"Measured from start of P wave to start of QRS. Normal: 0.12–0.20 s (3–5 small squares). "
"Prolonged PR = 1st degree AV block. Short PR (<0.12 s) = WPW or AV nodal re-entry."),
("STEP 6 — QRS, ST, T, QT",
"QRS: normal ≤ 0.12 s (3 small squares). Wide QRS = BBB or VT. "
"ST segment: should be isoelectric. Elevation = injury/infarction or pericarditis. Depression = ischaemia or NSTEMI. "
"T wave: usually concordant with QRS. Inversion = ischaemia, PE, LVH, BBB. "
"QT interval: rate-corrected QTc ≤ 440 ms (male), ≤ 460 ms (female). Prolonged QTc → risk of torsades."),
]
for title, body in steps:
step_para = [
Paragraph(f"<b>{title}</b>", S_H3),
Paragraph(body, S_BODY),
]
box = coloured_box(step_para, LIGHT_BG, left_color=ACCENT_TEAL)
story.append(box)
story.append(Spacer(1, 0.25*cm))
# Normal ECG image
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("Normal 12-Lead ECG Example", S_H2))
img_normal = fetch_image(
"https://cdn.orris.care/cdss_images/pmc_clinical_VQA_1ffd3dfc7a991397d378cc543bdbfdc4d0596be66b6fbd7c53c0a792148ab1e7.jpg",
max_w=14.5*cm, max_h=8*cm)
if img_normal:
story.append(img_normal)
story.append(Paragraph(
"Normal 12-lead ECG: sinus rhythm, narrow QRS, normal R-wave progression V1→V6, "
"isoelectric ST segments, upright T waves in lateral leads.", S_CAPTION))
story.append(PageBreak())
# ═══════════════════════════════════════════════════════════════════════
# SECTION 3 — NORMAL WAVEFORMS
# ═══════════════════════════════════════════════════════════════════════
story.append(SectionHeader("SECTION 3 — NORMAL WAVEFORMS & INTERVALS"))
story.append(Spacer(1, 0.3*cm))
wave_data = [
[Paragraph("Wave / Interval", S_TABLE_H), Paragraph("Represents", S_TABLE_H),
Paragraph("Normal Duration", S_TABLE_H), Paragraph("Normal Amplitude", S_TABLE_H)],
[Paragraph("P wave", S_TABLE_C), Paragraph("Atrial depolarisation", S_TABLE_C),
Paragraph("≤ 0.12 s", S_TABLE_CC), Paragraph("≤ 2.5 mm in lead II", S_TABLE_CC)],
[Paragraph("PR interval", S_TABLE_C), Paragraph("AV node conduction time", S_TABLE_C),
Paragraph("0.12–0.20 s", S_TABLE_CC), Paragraph("N/A", S_TABLE_CC)],
[Paragraph("QRS complex", S_TABLE_C), Paragraph("Ventricular depolarisation", S_TABLE_C),
Paragraph("0.06–0.10 s", S_TABLE_CC), Paragraph("Variable (see LVH)", S_TABLE_CC)],
[Paragraph("Q wave", S_TABLE_C), Paragraph("Septal depolarisation (normal if small)", S_TABLE_C),
Paragraph("< 0.04 s / < 1 mm", S_TABLE_CC), Paragraph("< 25% of R wave", S_TABLE_CC)],
[Paragraph("ST segment", S_TABLE_C), Paragraph("Plateau phase of action potential", S_TABLE_C),
Paragraph("0.05–0.15 s", S_TABLE_CC), Paragraph("Isoelectric (±1 mm)", S_TABLE_CC)],
[Paragraph("T wave", S_TABLE_C), Paragraph("Ventricular repolarisation", S_TABLE_C),
Paragraph("0.1–0.25 s", S_TABLE_CC), Paragraph("< 5 mm limb / < 10 mm precordial", S_TABLE_CC)],
[Paragraph("QT interval", S_TABLE_C), Paragraph("Total ventricular depolarisation + repolarisation", S_TABLE_C),
Paragraph("QTc ≤ 440 ms (M) / 460 ms (F)", S_TABLE_CC), Paragraph("N/A", S_TABLE_CC)],
[Paragraph("U wave", S_TABLE_C), Paragraph("Papillary muscle / Purkinje repolarisation", S_TABLE_C),
Paragraph("After T wave", S_TABLE_CC), Paragraph("< T wave amplitude", S_TABLE_CC)],
]
wave_table = Table(wave_data, colWidths=[3.5*cm, 5*cm, 4.5*cm, 4.5*cm])
wave_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), TABLE_HEADER),
("ROWBACKGROUNDS",(0,1), (-1,-1), [TABLE_ROW1, TABLE_ROW2]),
("GRID", (0,0), (-1,-1), 0.5, MID_GREY),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 5),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
]))
story.append(wave_table)
story.append(Spacer(1, 0.5*cm))
# Pathological Q wave box
qbox = coloured_box([
Paragraph("<b>Pathological Q Wave</b>", S_WARN),
Paragraph(
"A Q wave is pathological if: width > 0.04 s (1 small square) OR depth > 25% of the "
"following R wave. Pathological Q waves indicate transmural (full-thickness) myocardial "
"infarction — they persist after the acute phase ('old' MI marker).", S_BODY),
], BOX_RED, left_color=ACCENT_RED)
story.append(qbox)
story.append(PageBreak())
# ═══════════════════════════════════════════════════════════════════════
# SECTION 4 — STEMI
# ═══════════════════════════════════════════════════════════════════════
story.append(SectionHeader("SECTION 4 — ACUTE MI (STEMI): RECOGNISING & LOCALISING", color=ACCENT_RED))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph(
"STEMI (ST-Elevation Myocardial Infarction) is a medical emergency requiring immediate "
"reperfusion. Recognising the culprit artery from ECG lead distribution is essential.", S_BODY))
story.append(Paragraph("Diagnostic Criteria for STEMI", S_H2))
stemi_crit = [
Paragraph("• ST elevation ≥ 1 mm in ≥ 2 contiguous limb leads (II, III, aVF or I, aVL)", S_BULLET),
Paragraph("• ST elevation ≥ 2 mm in ≥ 2 contiguous precordial leads (V1–V6)", S_BULLET),
Paragraph("• New LBBB in context of chest pain (Sgarbossa criteria apply)", S_BULLET),
Paragraph("• Posterior MI: ST depression V1–V3 + tall R wave V1 (mirror of posterior STEMI)", S_BULLET),
]
for p in stemi_crit:
story.append(p)
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("ECG Stages of STEMI", S_H2))
stages_data = [
[Paragraph("Time", S_TABLE_H), Paragraph("ECG Finding", S_TABLE_H), Paragraph("Significance", S_TABLE_H)],
[Paragraph("Minutes (hyperacute)", S_TABLE_C), Paragraph("Tall, peaked 'hyperacute' T waves", S_TABLE_C),
Paragraph("Very early sign — easy to miss. Compare with old ECG.", S_TABLE_C)],
[Paragraph("Hours (acute)", S_TABLE_C), Paragraph("ST elevation, tombstoning pattern", S_TABLE_C),
Paragraph("Full infarction developing. Activate cath lab.", S_TABLE_C)],
[Paragraph("Hours–days (evolving)", S_TABLE_C), Paragraph("Pathological Q waves develop; ST partially normalises", S_TABLE_C),
Paragraph("Myocardial necrosis established.", S_TABLE_C)],
[Paragraph("Days–weeks (resolving)", S_TABLE_C), Paragraph("T wave inversion; ST returns to baseline", S_TABLE_C),
Paragraph("Repolarisation abnormality phase.", S_TABLE_C)],
[Paragraph("Weeks–months (old)", S_TABLE_C), Paragraph("Persistent Q waves; T waves may normalise", S_TABLE_C),
Paragraph("Permanent scar. Q waves may persist for life.", S_TABLE_C)],
]
stages_table = Table(stages_data, colWidths=[3.5*cm, 6*cm, 8*cm])
stages_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), TABLE_HEADER),
("ROWBACKGROUNDS",(0,1), (-1,-1), [TABLE_ROW1, TABLE_ROW2]),
("GRID", (0,0), (-1,-1), 0.5, MID_GREY),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 5),
("VALIGN", (0,0), (-1,-1), "TOP"),
]))
story.append(stages_table)
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("Localising the Infarct — Which Artery?", S_H2))
terr_data = [
[Paragraph("Infarct Territory", S_TABLE_H), Paragraph("Leads with ST Elevation", S_TABLE_H),
Paragraph("Reciprocal Changes", S_TABLE_H), Paragraph("Culprit Vessel", S_TABLE_H)],
[Paragraph("Anterior", S_TABLE_C), Paragraph("V1–V4", S_TABLE_CC),
Paragraph("None or II, III, aVF", S_TABLE_CC), Paragraph("LAD (proximal)", S_TABLE_CC)],
[Paragraph("Anterolateral", S_TABLE_C), Paragraph("V1–V6, I, aVL", S_TABLE_CC),
Paragraph("II, III, aVF", S_TABLE_CC), Paragraph("LAD (large, dominant)", S_TABLE_CC)],
[Paragraph("Inferior", S_TABLE_C), Paragraph("II, III, aVF", S_TABLE_CC),
Paragraph("I, aVL (± V1–V3)", S_TABLE_CC), Paragraph("RCA (80%) or LCx", S_TABLE_CC)],
[Paragraph("Lateral", S_TABLE_C), Paragraph("I, aVL, V5, V6", S_TABLE_CC),
Paragraph("II, III, aVF", S_TABLE_CC), Paragraph("LCx or diagonal LAD", S_TABLE_CC)],
[Paragraph("Posterior", S_TABLE_C), Paragraph("No STE — ST depression + tall R in V1–V2", S_TABLE_CC),
Paragraph("Mirror image in V1–V3", S_TABLE_CC), Paragraph("RCA or LCx", S_TABLE_CC)],
[Paragraph("Right ventricular", S_TABLE_C), Paragraph("V1 + right-sided leads (V3R–V4R)", S_TABLE_CC),
Paragraph("With inferior MI pattern", S_TABLE_CC), Paragraph("Proximal RCA", S_TABLE_CC)],
]
terr_table = Table(terr_data, colWidths=[3.5*cm, 4.5*cm, 4*cm, 5.5*cm])
terr_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), TABLE_HEADER),
("ROWBACKGROUNDS",(0,1), (-1,-1), [TABLE_ROW1, TABLE_ROW2]),
("GRID", (0,0), (-1,-1), 0.5, MID_GREY),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 5),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
]))
story.append(terr_table)
story.append(Spacer(1, 0.5*cm))
# STEMI ECG image
img_stemi = fetch_image(
"https://cdn.orris.care/cdss_images/pmc_clinical_VQA_16738d4b125195130b5e53a811645d4064182fc2bbd0de16b0e00a57af7ff6ef.jpg",
max_w=14.5*cm, max_h=8.5*cm)
if img_stemi:
story.append(img_stemi)
story.append(Paragraph(
"Anterior STEMI: massive ST elevation V1–V6 with reciprocal changes in inferior leads. "
"This pattern indicates proximal LAD occlusion — a 'widow-maker'.", S_CAPTION))
story.append(PageBreak())
# ═══════════════════════════════════════════════════════════════════════
# SECTION 5 — NSTEMI
# ═══════════════════════════════════════════════════════════════════════
story.append(SectionHeader("SECTION 5 — NSTEMI & UNSTABLE ANGINA", color=HexColor("#7D3C98")))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph(
"NSTEMI and unstable angina (collectively NSTE-ACS) do NOT show ST elevation. "
"The ECG may be normal, subtly abnormal, or show the following:", S_BODY))
nstemi_findings = [
("ST Depression", "Horizontal or downsloping ST depression ≥ 0.5 mm in ≥ 2 contiguous leads. "
"Downsloping depression is more specific for ischaemia than upsloping."),
("T-Wave Inversion", "Deep symmetrical T-wave inversion (Wellens' pattern: Types A and B) in V2–V3 "
"indicates critical proximal LAD stenosis. <b>Do not exercise test these patients!</b>"),
("Transient ST Elevation", "Resolves within 20 minutes (variant/Prinzmetal angina). "
"Due to coronary vasospasm rather than thrombus."),
("Flat/Biphasic T Waves", "Subtle but important in context of chest pain. "
"Indicates subendocardial ischaemia."),
("Normal ECG", "~30% of NSTEMI patients have a normal or near-normal ECG. "
"Serial ECGs and troponins are essential."),
]
for title, body in nstemi_findings:
b = coloured_box([
Paragraph(f"<b>{title}</b>", S_H3),
Paragraph(body, S_BODY),
], BOX_ORANGE, left_color=ACCENT_GOLD)
story.append(b)
story.append(Spacer(1, 0.2*cm))
story.append(Spacer(1, 0.2*cm))
wbox = coloured_box([
Paragraph("<b>Wellens' Syndrome — Do Not Miss!</b>", S_WARN),
Paragraph("Type A: Biphasic T waves in V2–V3. Type B: Deep symmetric T-wave inversion in V2–V3. "
"ECG may be taken when patient is pain-free and looks 'almost normal'. "
"Represents reperfusion after near-complete LAD occlusion — high risk of sudden "
"anterior STEMI if patient goes home. Requires urgent cardiology review.", S_BODY),
], BOX_RED, left_color=ACCENT_RED)
story.append(wbox)
story.append(PageBreak())
# ═══════════════════════════════════════════════════════════════════════
# SECTION 6 — AF & FLUTTER
# ═══════════════════════════════════════════════════════════════════════
story.append(SectionHeader("SECTION 6 — ATRIAL FIBRILLATION & ATRIAL FLUTTER", color=HexColor("#1A5276")))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("Atrial Fibrillation (AF)", S_H2))
af_features = [
Paragraph("• <b>No P waves</b> — baseline is chaotic fibrillatory (f) waves", S_BULLET),
Paragraph("• <b>Irregularly irregular</b> R-R intervals — hallmark finding", S_BULLET),
Paragraph("• <b>Narrow QRS</b> (unless pre-existing BBB or aberrant conduction)", S_BULLET),
Paragraph("• Ventricular rate varies: controlled AF 60–100 bpm; uncontrolled >100 bpm", S_BULLET),
Paragraph("• Coarse AF: large fibrillatory waves (>1 mm) — may mimic flutter", S_BULLET),
Paragraph("• Fine AF: tiny low-amplitude fibrillatory waves — easy to miss; baseline looks flat", S_BULLET),
]
for p in af_features:
story.append(p)
story.append(Spacer(1, 0.3*cm))
img_af = fetch_image(
"https://cdn.orris.care/cdss_images/pmc_clinical_VQA_a8e7a35a18d282cc6de8c169a0213a6712f9fe9e4a7253f7928e22c867033097.jpg",
max_w=14.5*cm, max_h=7.5*cm)
if not img_af:
img_af = fetch_image(
"https://cdn.orris.care/cdss_images/pmc_clinical_VQA_8fef6879aef9f463cbd16b8e1160b99e781b069a9a85b042df9b2a43cb8c50c0.jpg",
max_w=14.5*cm, max_h=7.5*cm)
if img_af:
story.append(img_af)
story.append(Paragraph(
"Atrial fibrillation: absence of P waves, fine fibrillatory baseline, irregularly irregular "
"narrow QRS complexes. Most visible in V1.", S_CAPTION))
story.append(Spacer(1, 0.4*cm))
story.append(Paragraph("Atrial Flutter", S_H2))
flutter_features = [
Paragraph("• Regular 'sawtooth' flutter waves at ~300 bpm (F waves) — best seen in II, III, aVF, V1", S_BULLET),
Paragraph("• Usually 2:1 AV block → ventricular rate ~150 bpm (classic give-away!)", S_BULLET),
Paragraph("• Can also have 3:1 or 4:1 block → rates 100 or 75 bpm", S_BULLET),
Paragraph("• QRS is narrow (unless BBB)", S_BULLET),
Paragraph("• Carotid sinus massage or adenosine slows rate and unmasks flutter waves", S_BULLET),
]
for p in flutter_features:
story.append(p)
story.append(PageBreak())
# ═══════════════════════════════════════════════════════════════════════
# SECTION 7 — HEART BLOCKS
# ═══════════════════════════════════════════════════════════════════════
story.append(SectionHeader("SECTION 7 — ATRIOVENTRICULAR (AV) HEART BLOCKS", color=HexColor("#117A65")))
story.append(Spacer(1, 0.3*cm))
blocks = [
("1st Degree AV Block", BOX_GREEN, ACCENT_TEAL,
"PR interval > 0.20 s (> 5 small squares) in EVERY beat.\n"
"Every P wave is followed by a QRS — just with a delay.\n"
"Usually benign. Common causes: vagal tone, digoxin, inferior MI, myocarditis.\n"
"No treatment needed; monitor for progression."),
("2nd Degree AV Block — Mobitz Type I (Wenckebach)", BOX_GREEN, ACCENT_TEAL,
"PR interval progressively lengthens beat by beat until a QRS is DROPPED.\n"
"After the dropped beat, the cycle resets — the next PR is shortest.\n"
"Usually at level of AV node. Often benign (especially inferior MI).\n"
"Pattern: 'longer, longer, longer, drop — then you have a Wenckebach.'"),
("2nd Degree AV Block — Mobitz Type II", BOX_ORANGE, ACCENT_GOLD,
"PR interval is FIXED but QRS complexes are intermittently dropped — no warning.\n"
"May be 2:1, 3:1 etc. (2 P waves for every 1 QRS in 2:1 block).\n"
"More serious than Mobitz I — lesion is below AV node in His-Purkinje system.\n"
"Risk of sudden progression to complete (3rd degree) block → pacemaker indicated."),
("3rd Degree (Complete) AV Block", BOX_RED, ACCENT_RED,
"No relationship between P waves and QRS complexes — complete AV dissociation.\n"
"P waves and QRS complexes march at their own independent rates.\n"
"Ventricular escape rhythm: 20–40 bpm (wide QRS if low junctional/ventricular origin).\n"
"Causes: inferior MI (usually transient), anterior MI (usually permanent), digoxin toxicity.\n"
"Emergency: requires temporary/permanent pacemaker depending on cause."),
]
for title, bg, lc, body_text in blocks:
paras = [Paragraph(f"<b>{title}</b>", S_H2)]
for line in body_text.strip().split("\n"):
paras.append(Paragraph(f"• {line.strip()}", S_BULLET))
box = coloured_box(paras, bg, left_color=lc)
story.append(box)
story.append(Spacer(1, 0.3*cm))
img_blocks = fetch_image(
"https://cdn.orris.care/cdss_images/pmc_clinical_VQA_11ac037ff7836678ddd7fad143600ed71c2a3910d226a34ea257a3618beab0a5.jpg",
max_w=14.5*cm, max_h=9*cm)
if img_blocks:
story.append(img_blocks)
story.append(Paragraph(
"AV block comparison chart: 1st degree (prolonged PR, 1:1 conduction), "
"2nd degree (dropped beats), 3rd degree (complete AV dissociation).", S_CAPTION))
story.append(PageBreak())
# ═══════════════════════════════════════════════════════════════════════
# SECTION 8 — BUNDLE BRANCH BLOCKS
# ═══════════════════════════════════════════════════════════════════════
story.append(SectionHeader("SECTION 8 — BUNDLE BRANCH BLOCKS: LBBB & RBBB"))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph(
"A bundle branch block occurs when conduction through one of the main ventricular branches "
"is delayed or absent. The affected ventricle depolarises late via slower myocardial "
"cell-to-cell conduction → wide QRS ≥ 0.12 s (≥ 3 small squares).", S_BODY))
bbb_data = [
[Paragraph("Feature", S_TABLE_H), Paragraph("LBBB", S_TABLE_H), Paragraph("RBBB", S_TABLE_H)],
[Paragraph("QRS width", S_TABLE_C), Paragraph("≥ 0.12 s", S_TABLE_CC), Paragraph("≥ 0.12 s", S_TABLE_CC)],
[Paragraph("V1 morphology", S_TABLE_C), Paragraph("QS or rS (deep negative)", S_TABLE_CC),
Paragraph("rSR' ('M' shape / bunny ears)", S_TABLE_CC)],
[Paragraph("V6 / lateral morphology", S_TABLE_C), Paragraph("Broad monophasic R ('William' W in V1)", S_TABLE_CC),
Paragraph("Slurred S wave", S_TABLE_CC)],
[Paragraph("ST / T wave", S_TABLE_C),
Paragraph("Discordant: T wave opposite to QRS direction", S_TABLE_CC),
Paragraph("Discordant ST-T in V1–V3", S_TABLE_CC)],
[Paragraph("Septal Q waves", S_TABLE_C), Paragraph("Absent in I, V5–V6", S_TABLE_CC),
Paragraph("Present (normal)", S_TABLE_CC)],
[Paragraph("Memory aid", S_TABLE_C), Paragraph("WiLLiaM: W in V1, M in V6", S_TABLE_CC),
Paragraph("MaRRoW: M in V1, W in V6", S_TABLE_CC)],
[Paragraph("Common causes", S_TABLE_C),
Paragraph("IHD, cardiomyopathy, hypertension, aortic stenosis", S_TABLE_C),
Paragraph("RV strain (PE), ASD, ischaemia, idiopathic", S_TABLE_C)],
[Paragraph("Clinical importance", S_TABLE_C),
Paragraph("New LBBB = treat as STEMI until proven otherwise", S_TABLE_C),
Paragraph("New RBBB in PE context = right heart strain marker", S_TABLE_C)],
]
bbb_table = Table(bbb_data, colWidths=[4*cm, 6.5*cm, 7*cm])
bbb_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), TABLE_HEADER),
("ROWBACKGROUNDS",(0,1), (-1,-1), [TABLE_ROW1, TABLE_ROW2]),
("GRID", (0,0), (-1,-1), 0.5, MID_GREY),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 5),
("VALIGN", (0,0), (-1,-1), "TOP"),
]))
story.append(bbb_table)
story.append(Spacer(1, 0.4*cm))
img_lbbb = fetch_image(
"https://cdn.orris.care/cdss_images/pmc_clinical_VQA_c67bf446bd5b1f60fa1776c6dcf0b1056f942a3375d030842a7783555919effe.jpg",
max_w=14.5*cm, max_h=8*cm)
if img_lbbb:
story.append(img_lbbb)
story.append(Paragraph(
"LBBB: wide QRS, deep S waves V1–V3, broad monophasic R in lateral leads (I, aVL, V5–V6). "
"Discordant ST-T changes are expected and do NOT indicate primary ischaemia.", S_CAPTION))
story.append(Spacer(1, 0.3*cm))
sbox = coloured_box([
Paragraph("<b>Sgarbossa Criteria — Diagnosing MI in LBBB</b>", S_TIP),
Paragraph(
"When LBBB is present, use Sgarbossa criteria to detect superimposed MI: "
"(1) Concordant ST elevation ≥ 1 mm in leads with positive QRS — score 5. "
"(2) Concordant ST depression ≥ 1 mm in V1–V3 — score 3. "
"(3) Excessively discordant ST elevation ≥ 5 mm — score 2. "
"Score ≥ 3 is highly specific for MI. Modified criteria use STE/S ratio > 0.25.", S_BODY),
], BOX_BLUE, left_color=CARD_BLUE)
story.append(sbox)
story.append(PageBreak())
# ═══════════════════════════════════════════════════════════════════════
# SECTION 9 — VT & VF
# ═══════════════════════════════════════════════════════════════════════
story.append(SectionHeader("SECTION 9 — VENTRICULAR TACHYCARDIA & VENTRICULAR FIBRILLATION", color=ACCENT_RED))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("Ventricular Tachycardia (VT)", S_H2))
vt_features = [
Paragraph("• Rate 100–250 bpm; wide QRS ≥ 0.12 s (usually ≥ 0.14 s)", S_BULLET),
Paragraph("• Regular (monomorphic) or irregular (polymorphic / torsades)", S_BULLET),
Paragraph("• <b>AV dissociation</b>: P waves march at own rate, independent of QRS — diagnostic if seen", S_BULLET),
Paragraph("• <b>Fusion beats</b>: P wave conducts partially before VT complex takes over → hybrid beat", S_BULLET),
Paragraph("• <b>Capture beats</b>: occasional narrow QRS = sinus beat 'capturing' ventricle during VT", S_BULLET),
Paragraph("• Concordance: all precordial leads positive or all negative → strongly suggests VT", S_BULLET),
Paragraph("• Northwest axis (negative in I AND aVF) = VT until proven otherwise", S_BULLET),
]
for p in vt_features:
story.append(p)
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("Brugada Algorithm: Is a wide-complex tachycardia VT or SVT with aberrancy?", S_H3))
brugada_data = [
[Paragraph("Question", S_TABLE_H), Paragraph("Yes → Diagnosis", S_TABLE_H)],
[Paragraph("1. Absence of RS complex in ALL precordial leads?", S_TABLE_C),
Paragraph("VT (100% specificity)", S_TABLE_C)],
[Paragraph("2. RS interval > 100 ms in any precordial lead?", S_TABLE_C),
Paragraph("VT", S_TABLE_C)],
[Paragraph("3. AV dissociation present?", S_TABLE_C),
Paragraph("VT", S_TABLE_C)],
[Paragraph("4. BBB morphology criteria for VT met (V1, V6)?", S_TABLE_C),
Paragraph("VT; if no → SVT with aberrancy", S_TABLE_C)],
]
brugada_table = Table(brugada_data, colWidths=[10*cm, 7.5*cm])
brugada_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), TABLE_HEADER),
("ROWBACKGROUNDS",(0,1), (-1,-1), [TABLE_ROW1, TABLE_ROW2]),
("GRID", (0,0), (-1,-1), 0.5, MID_GREY),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 5),
]))
story.append(brugada_table)
story.append(Spacer(1, 0.3*cm))
img_vt = fetch_image(
"https://cdn.orris.care/cdss_images/pmc_clinical_VQA_11382947fbc169103a52cb7cc19887b12822207890d698f803abd8e853381693.jpg",
max_w=14.5*cm, max_h=7.5*cm)
if img_vt:
story.append(img_vt)
story.append(Paragraph(
"Monomorphic ventricular tachycardia: rapid, regular, very wide QRS complexes with no "
"visible P waves. QRS duration >200 ms indicates sodium channel blockade or severe VT.", S_CAPTION))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("Ventricular Fibrillation (VF)", S_H2))
vf_text = [
Paragraph("• Chaotic, irregular, high-frequency electrical activity — no recognisable QRS complexes", S_BULLET),
Paragraph("• ECG shows irregular undulating waveform of varying amplitude and morphology", S_BULLET),
Paragraph("• <b>No cardiac output — cardiac arrest</b>. Immediate CPR + defibrillation required", S_BULLET),
Paragraph("• Coarse VF (large amplitude): more amenable to shock. Fine VF: may look like asystole", S_BULLET),
Paragraph("• Torsades de Pointes: polymorphic VT where QRS axis 'twists' — associated with long QT", S_BULLET),
]
for p in vf_text:
story.append(p)
story.append(PageBreak())
# ═══════════════════════════════════════════════════════════════════════
# SECTION 10 — LVH & HCM
# ═══════════════════════════════════════════════════════════════════════
story.append(SectionHeader("SECTION 10 — LEFT VENTRICULAR HYPERTROPHY & HYPERTROPHIC CARDIOMYOPATHY"))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("Left Ventricular Hypertrophy (LVH)", S_H2))
lvh_criteria = [
("Sokolow-Lyon (most used)", "S in V1 + R in V5 or V6 > 35 mm"),
("Cornell", "R in aVL + S in V3 > 28 mm (male) / > 20 mm (female)"),
("Limb lead", "R in aVL > 11 mm OR R in I + S in III > 25 mm"),
("Strain pattern", "ST depression + T-wave inversion in lateral leads (I, aVL, V5–V6) — indicates pressure overload"),
]
lvh_data = [[Paragraph("Criterion", S_TABLE_H), Paragraph("Value / Finding", S_TABLE_H)]]
for name, val in lvh_criteria:
lvh_data.append([Paragraph(name, S_TABLE_C), Paragraph(val, S_TABLE_C)])
lvh_table = Table(lvh_data, colWidths=[6*cm, 11.5*cm])
lvh_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), TABLE_HEADER),
("ROWBACKGROUNDS",(0,1), (-1,-1), [TABLE_ROW1, TABLE_ROW2]),
("GRID", (0,0), (-1,-1), 0.5, MID_GREY),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 6),
]))
story.append(lvh_table)
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph(
"Causes of LVH on ECG: systemic hypertension (most common), aortic stenosis, "
"hypertrophic cardiomyopathy, coarctation of aorta.", S_BODY))
story.append(Paragraph("Hypertrophic Cardiomyopathy (HCM)", S_H2))
hcm_text = [
Paragraph("• High-voltage QRS (Sokolow-Lyon LVH criteria)", S_BULLET),
Paragraph("• <b>Giant T-wave inversions</b> V2–V6 — especially in apical HCM (Yamaguchi syndrome)", S_BULLET),
Paragraph("• Deep narrow Q waves in inferior and lateral leads (septal hypertrophy)", S_BULLET),
Paragraph("• LVH strain pattern: ST depression + T inversion in lateral leads", S_BULLET),
Paragraph("• Short PR interval if HOCM with accessory pathway overlap", S_BULLET),
Paragraph("• Arrhythmias: VT, VF — leading cause of sudden cardiac death in young athletes", S_BULLET),
]
for p in hcm_text:
story.append(p)
story.append(Spacer(1, 0.3*cm))
img_hcm = fetch_image(
"https://cdn.orris.care/cdss_images/pmc_clinical_VQA_30fc66415251f58bcaaf56a0cd84a79e73d23d06b4fcf95d599a2ef520246c53.jpg",
max_w=14.5*cm, max_h=8*cm)
if img_hcm:
story.append(img_hcm)
story.append(Paragraph(
"Apical HCM (Yamaguchi): giant deep symmetric T-wave inversions V2–V6 with high-voltage QRS. "
"Classic for apical variant of hypertrophic cardiomyopathy.", S_CAPTION))
story.append(PageBreak())
# ═══════════════════════════════════════════════════════════════════════
# SECTION 11 — PERICARDITIS
# ═══════════════════════════════════════════════════════════════════════
story.append(SectionHeader("SECTION 11 — PERICARDITIS", color=HexColor("#7D3C98")))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph(
"Acute pericarditis produces a characteristic diffuse ST elevation — distinguishable "
"from STEMI by its distribution and associated PR depression.", S_BODY))
peri_stages = [
("Stage 1 (hours–days)", "Diffuse saddle-shaped (concave-up) ST elevation in MOST leads "
"(I, II, aVF, V2–V6). PR segment depression in same leads. PR elevation in aVR (reciprocal). "
"This is the most diagnostic stage."),
("Stage 2 (days–week)", "ST returns to baseline. T waves flatten."),
("Stage 3 (1–3 weeks)", "T-wave inversion develops — can mimic ischaemia."),
("Stage 4 (weeks–months)", "T waves normalise. ECG returns to normal."),
]
peri_data = [[Paragraph("Stage", S_TABLE_H), Paragraph("ECG Changes", S_TABLE_H)]]
for stage, body in peri_stages:
peri_data.append([Paragraph(stage, S_TABLE_C), Paragraph(body, S_TABLE_C)])
peri_table = Table(peri_data, colWidths=[4.5*cm, 13*cm])
peri_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), TABLE_HEADER),
("ROWBACKGROUNDS",(0,1), (-1,-1), [TABLE_ROW1, TABLE_ROW2]),
("GRID", (0,0), (-1,-1), 0.5, MID_GREY),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 6),
("VALIGN", (0,0), (-1,-1), "TOP"),
]))
story.append(peri_table)
story.append(Spacer(1, 0.3*cm))
diff_data = [
[Paragraph("Feature", S_TABLE_H), Paragraph("Pericarditis", S_TABLE_H), Paragraph("STEMI", S_TABLE_H)],
[Paragraph("ST shape", S_TABLE_C), Paragraph("Concave up (saddle)", S_TABLE_CC), Paragraph("Convex up (dome/tombstone)", S_TABLE_CC)],
[Paragraph("Distribution", S_TABLE_C), Paragraph("Diffuse (most leads)", S_TABLE_CC), Paragraph("Regional (coronary territory)", S_TABLE_CC)],
[Paragraph("Reciprocal ST↓", S_TABLE_C), Paragraph("Absent (except aVR)", S_TABLE_CC), Paragraph("Present in opposite leads", S_TABLE_CC)],
[Paragraph("PR depression", S_TABLE_C), Paragraph("Present (pathognomonic)", S_TABLE_CC), Paragraph("Absent", S_TABLE_CC)],
[Paragraph("Q waves", S_TABLE_C), Paragraph("Absent", S_TABLE_CC), Paragraph("May develop", S_TABLE_CC)],
[Paragraph("Pain character", S_TABLE_C), Paragraph("Pleuritic, sharp, positional", S_TABLE_CC), Paragraph("Pressure, crushing, radiates to arm/jaw", S_TABLE_CC)],
]
diff_table = Table(diff_data, colWidths=[4.5*cm, 5.5*cm, 7.5*cm])
diff_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), TABLE_HEADER),
("ROWBACKGROUNDS",(0,1), (-1,-1), [TABLE_ROW1, TABLE_ROW2]),
("GRID", (0,0), (-1,-1), 0.5, MID_GREY),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 6),
]))
story.append(Paragraph("Pericarditis vs STEMI", S_H2))
story.append(diff_table)
story.append(PageBreak())
# ═══════════════════════════════════════════════════════════════════════
# SECTION 12 — QUICK REFERENCE SUMMARY
# ═══════════════════════════════════════════════════════════════════════
story.append(SectionHeader("SECTION 12 — QUICK REFERENCE SUMMARY TABLE", color=DEEP_NAVY))
story.append(Spacer(1, 0.4*cm))
summary_data = [
[Paragraph("Condition", S_TABLE_H), Paragraph("Key ECG Finding", S_TABLE_H),
Paragraph("Distinguishing Feature", S_TABLE_H)],
[Paragraph("Normal sinus rhythm", S_TABLE_C),
Paragraph("P→QRS→T, rate 60–100, PR 0.12–0.20 s, QRS <0.12 s", S_TABLE_C),
Paragraph("Every P followed by QRS; regular R-R", S_TABLE_C)],
[Paragraph("STEMI (anterior)", S_TABLE_C),
Paragraph("ST elevation V1–V4; reciprocal depression II, III, aVF", S_TABLE_C),
Paragraph("Convex ST elevation; evolves to Q waves", S_TABLE_C)],
[Paragraph("STEMI (inferior)", S_TABLE_C),
Paragraph("ST elevation II, III, aVF; reciprocal I, aVL", S_TABLE_C),
Paragraph("Check V3R–V4R for RV involvement", S_TABLE_C)],
[Paragraph("NSTEMI/UA", S_TABLE_C),
Paragraph("ST depression ≥ 0.5 mm; T-wave inversion; normal ECG possible", S_TABLE_C),
Paragraph("Troponin elevated; no STE", S_TABLE_C)],
[Paragraph("Atrial fibrillation", S_TABLE_C),
Paragraph("Absent P waves; irregularly irregular QRS; f waves on baseline", S_TABLE_C),
Paragraph("'Irregularly irregular' is the hallmark", S_TABLE_C)],
[Paragraph("Atrial flutter", S_TABLE_C),
Paragraph("Sawtooth F waves ~300/min; usually 2:1 → rate ~150", S_TABLE_C),
Paragraph("Ventricular rate 150 = flutter until proven otherwise", S_TABLE_C)],
[Paragraph("1st degree AV block", S_TABLE_C),
Paragraph("PR > 0.20 s; all P waves conduct", S_TABLE_C),
Paragraph("Usually benign", S_TABLE_C)],
[Paragraph("2nd degree (Wenckebach)", S_TABLE_C),
Paragraph("Progressive PR lengthening → dropped QRS; cycle repeats", S_TABLE_C),
Paragraph("PR gets longer, longer, then drops", S_TABLE_C)],
[Paragraph("2nd degree (Mobitz II)", S_TABLE_C),
Paragraph("Fixed PR; QRS randomly dropped without warning", S_TABLE_C),
Paragraph("Risk of sudden complete block → pacemaker", S_TABLE_C)],
[Paragraph("3rd degree block", S_TABLE_C),
Paragraph("P waves and QRS completely dissociated; escape rhythm", S_TABLE_C),
Paragraph("More P waves than QRS; P-R relationships vary randomly", S_TABLE_C)],
[Paragraph("LBBB", S_TABLE_C),
Paragraph("QRS ≥ 0.12 s; WiLLiaM pattern; discordant ST-T", S_TABLE_C),
Paragraph("New LBBB with chest pain = STEMI equivalent", S_TABLE_C)],
[Paragraph("RBBB", S_TABLE_C),
Paragraph("QRS ≥ 0.12 s; MaRRoW pattern; rSR' in V1", S_TABLE_C),
Paragraph("New RBBB in PE = right heart strain", S_TABLE_C)],
[Paragraph("VT", S_TABLE_C),
Paragraph("Wide QRS tachycardia; AV dissociation; fusion/capture beats", S_TABLE_C),
Paragraph("Treat as VT unless proven SVT with aberrancy", S_TABLE_C)],
[Paragraph("VF", S_TABLE_C),
Paragraph("Chaotic irregular undulating waveform; no QRS", S_TABLE_C),
Paragraph("Cardiac arrest — defibrillate immediately", S_TABLE_C)],
[Paragraph("LVH", S_TABLE_C),
Paragraph("High voltage QRS; Sokolow ≥ 35 mm; strain pattern laterally", S_TABLE_C),
Paragraph("Hypertension, AS, HCM", S_TABLE_C)],
[Paragraph("HCM", S_TABLE_C),
Paragraph("Giant T-wave inversions V2–V6; LVH voltage; deep Q waves inferior/lateral", S_TABLE_C),
Paragraph("Young athlete with syncope — rule out HCM", S_TABLE_C)],
[Paragraph("Pericarditis", S_TABLE_C),
Paragraph("Diffuse saddle-shaped ST elevation; PR depression; no reciprocal changes", S_TABLE_C),
Paragraph("PR depression in most leads; pleuritic chest pain", S_TABLE_C)],
]
summary_table = Table(summary_data, colWidths=[3.5*cm, 7*cm, 7*cm])
summary_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), TABLE_HEADER),
("ROWBACKGROUNDS",(0,1), (-1,-1), [TABLE_ROW1, TABLE_ROW2]),
("GRID", (0,0), (-1,-1), 0.5, MID_GREY),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 5),
("VALIGN", (0,0), (-1,-1), "TOP"),
("FONTSIZE", (0,0), (-1,-1), 8),
]))
story.append(summary_table)
story.append(Spacer(1, 0.5*cm))
# Final tip box
final_box = coloured_box([
Paragraph("<b>Clinical Pearl — Approach Every ECG the Same Way</b>", S_TIP),
Paragraph(
"Rate → Rhythm → Axis → P wave → PR → QRS → ST → T → QT. "
"Never skip steps. Context matters: a new LBBB in a 60-year-old with chest pain is a STEMI "
"until proven otherwise; the same LBBB in a stable 80-year-old on a routine ECG is not. "
"Always compare with the patient's old ECG when available.", S_BODY),
], BOX_BLUE, left_color=ACCENT_TEAL)
story.append(final_box)
return story
# ── Build the PDF ────────────────────────────────────────────────────────────
output_path = "/home/daytona/workspace/ecg-guide/ECG_Interpretation_Guide.pdf"
doc = SimpleDocTemplate(
output_path,
pagesize=A4,
leftMargin=2*cm, rightMargin=2*cm,
topMargin=2*cm, bottomMargin=1.5*cm,
title="ECG Interpretation Guide",
author="Orris Medical Education",
)
print("Building ECG Interpretation Guide PDF...")
story = build_story()
print(f"Story built with {len(story)} elements. Assembling PDF...")
doc.build(story, onFirstPage=add_page_number, onLaterPages=add_page_number)
print(f"PDF saved to: {output_path}")
import os
size_kb = os.path.getsize(output_path) / 1024
print(f"File size: {size_kb:.1f} KB")