Make me pdf explaining all the basics things about ECG with diagram, also include how to identify different types of pathology in ecg( include ecg and identify steps). Use standard medical book as reference

Reading File
Reading File
Finding Sources
Finding Sources
Finding Sources
Finding Sources
Finding Sources
Reading File
Reading File
Reading File
Reading File
Reading File
Loading Image
Running Command

mkdir -p /home/daytona/workspace/ecg-pdf && ls /home/daytona/workspace/ecg-pdf/

Writing File

~/ecg-pdf/generate_ecg_pdf.py

"""
ECG Basics & Pathology PDF Generator
References: Harrison's Principles of Internal Medicine 22E (2025),
            Guyton and Hall Textbook of Medical Physiology
"""
import math, io, urllib.request
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import mm, cm
from reportlab.platypus import (
    SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
    HRFlowable, Image, PageBreak, KeepTogether
)
from reportlab.graphics.shapes import Drawing, Path, Line, Rect, String, Circle, Polygon
from reportlab.graphics import renderPDF
from reportlab.graphics.renderPDF import drawToString
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
from reportlab.platypus.flowables import Flowable
import reportlab.graphics.shapes as shapes

# ─── Color Palette ───────────────────────────────────────────────────────────
ECG_RED      = colors.HexColor("#C0392B")
ECG_BLUE     = colors.HexColor("#1A5276")
ECG_LIGHT    = colors.HexColor("#D6EAF8")
ECG_YELLOW   = colors.HexColor("#FEF9E7")
ECG_GREEN    = colors.HexColor("#1E8449")
ECG_ORANGE   = colors.HexColor("#E67E22")
ECG_PURPLE   = colors.HexColor("#7D3C98")
GRID_COLOR   = colors.HexColor("#FADBD8")
PAPER_BG     = colors.HexColor("#FDFEFE")
DARK_TEXT    = colors.HexColor("#1B2631")
BOX_BLUE     = colors.HexColor("#EBF5FB")
BOX_GREEN    = colors.HexColor("#EAFAF1")
BOX_ORANGE   = colors.HexColor("#FEF5E7")
BOX_RED      = colors.HexColor("#FDEDEC")

PAGE_W, PAGE_H = A4
MARGIN = 2 * cm

# ─── Styles ──────────────────────────────────────────────────────────────────
styles = getSampleStyleSheet()

title_style = ParagraphStyle("Title",
    fontName="Helvetica-Bold", fontSize=26, textColor=ECG_BLUE,
    alignment=TA_CENTER, spaceAfter=6, leading=32)

subtitle_style = ParagraphStyle("Subtitle",
    fontName="Helvetica", fontSize=13, textColor=colors.HexColor("#555555"),
    alignment=TA_CENTER, spaceAfter=4)

h1_style = ParagraphStyle("H1",
    fontName="Helvetica-Bold", fontSize=16, textColor=ECG_BLUE,
    spaceBefore=14, spaceAfter=4, leading=20,
    borderPad=(0,0,3,0))

h2_style = ParagraphStyle("H2",
    fontName="Helvetica-Bold", fontSize=13, textColor=ECG_GREEN,
    spaceBefore=10, spaceAfter=3, leading=17)

h3_style = ParagraphStyle("H3",
    fontName="Helvetica-Bold", fontSize=11, textColor=ECG_ORANGE,
    spaceBefore=6, spaceAfter=2, leading=14)

body_style = ParagraphStyle("Body",
    fontName="Helvetica", fontSize=10, textColor=DARK_TEXT,
    spaceBefore=2, spaceAfter=4, leading=15, alignment=TA_JUSTIFY)

bullet_style = ParagraphStyle("Bullet",
    fontName="Helvetica", fontSize=10, textColor=DARK_TEXT,
    spaceBefore=1, spaceAfter=2, leading=14,
    leftIndent=14, bulletIndent=4)

caption_style = ParagraphStyle("Caption",
    fontName="Helvetica-Oblique", fontSize=8.5, textColor=colors.HexColor("#555555"),
    alignment=TA_CENTER, spaceBefore=2, spaceAfter=6)

ref_style = ParagraphStyle("Ref",
    fontName="Helvetica-Oblique", fontSize=8, textColor=colors.HexColor("#888888"),
    spaceBefore=1, spaceAfter=1, leading=11)

label_style = ParagraphStyle("Label",
    fontName="Helvetica-Bold", fontSize=9, textColor=ECG_BLUE,
    spaceBefore=0, spaceAfter=0)

# ─── ECG Drawing Helpers ─────────────────────────────────────────────────────

def draw_ecg_grid(d, x0, y0, width, height, small=5, large=25):
    """Draw standard ECG graph paper grid."""
    d.add(Rect(x0, y0, width, height, fillColor=colors.HexColor("#FFF8F8"),
               strokeColor=colors.HexColor("#CCCCCC"), strokeWidth=0.5))
    # small squares
    for i in range(0, int(width)+1, small):
        d.add(Line(x0+i, y0, x0+i, y0+height,
                   strokeColor=GRID_COLOR, strokeWidth=0.3))
    for j in range(0, int(height)+1, small):
        d.add(Line(x0, y0+j, x0+width, y0+j,
                   strokeColor=GRID_COLOR, strokeWidth=0.3))
    # large squares
    for i in range(0, int(width)+1, large):
        d.add(Line(x0+i, y0, x0+i, y0+height,
                   strokeColor=colors.HexColor("#F1948A"), strokeWidth=0.7))
    for j in range(0, int(height)+1, large):
        d.add(Line(x0, y0+j, x0+width, y0+j,
                   strokeColor=colors.HexColor("#F1948A"), strokeWidth=0.7))

def ecg_path(d, pts, x0, y0, baseline, scale_x=1.0, scale_y=1.0,
             color=ECG_RED, width=1.5):
    """Draw ECG trace from list of (t, v) points."""
    if len(pts) < 2:
        return
    p = Path(strokeColor=color, strokeWidth=width, fillColor=None)
    px0 = x0 + pts[0][0]*scale_x
    py0 = y0 + baseline + pts[0][1]*scale_y
    p.moveTo(px0, py0)
    for t, v in pts[1:]:
        p.lineTo(x0 + t*scale_x, y0 + baseline + v*scale_y)
    d.add(p)

def normal_beat(t_offset=0, duration=125, amp=1.0):
    """Return (t,v) pts for a normal sinus beat. t in 'steps', v in mm."""
    pts = []
    # Isoelectric baseline
    for t in range(0, 20):
        pts.append((t_offset+t, 0))
    # P wave (upright, small)
    pw = 12; ph = 3*amp
    for t in range(pw):
        v = ph * math.sin(math.pi * t / pw)
        pts.append((t_offset+20+t, v))
    # PR segment
    for t in range(12):
        pts.append((t_offset+32+t, 0))
    # Q wave
    pts.append((t_offset+44, 0))
    pts.append((t_offset+46, -3*amp))
    pts.append((t_offset+48, 0))
    # R wave (tall)
    pts.append((t_offset+50, 20*amp))
    pts.append((t_offset+52, 0))
    # S wave
    pts.append((t_offset+54, -4*amp))
    pts.append((t_offset+56, 0))
    # ST segment (isoelectric)
    for t in range(14):
        pts.append((t_offset+57+t, 0))
    # T wave (upright)
    tw = 18; th = 6*amp
    for t in range(tw):
        v = th * math.sin(math.pi * t / tw)
        pts.append((t_offset+71+t, v))
    # Baseline after
    for t in range(duration-(71+tw)):
        pts.append((t_offset+71+tw+t, 0))
    return pts

# ─── Custom Flowables ─────────────────────────────────────────────────────────

class SectionHeader(Flowable):
    def __init__(self, text, color=ECG_BLUE, width=None):
        Flowable.__init__(self)
        self.text = text
        self.color = color
        self._width = width or (PAGE_W - 2*MARGIN)
    def wrap(self, aw, ah):
        self.width = self._width
        return self.width, 22
    def draw(self):
        c = self.canv
        c.setFillColor(self.color)
        c.roundRect(0, 0, self.width, 20, 4, fill=1, stroke=0)
        c.setFillColor(colors.white)
        c.setFont("Helvetica-Bold", 13)
        c.drawString(10, 5, self.text)

class InfoBox(Flowable):
    def __init__(self, title, lines, bg=BOX_BLUE, title_color=ECG_BLUE, width=None):
        Flowable.__init__(self)
        self.title = title
        self.lines = lines
        self.bg = bg
        self.tc = title_color
        self._width = width or (PAGE_W - 2*MARGIN)
        self._height = 18 + len(lines)*14 + 6
    def wrap(self, aw, ah):
        return self._width, self._height
    def draw(self):
        c = self.canv
        c.setFillColor(self.bg)
        c.roundRect(0, 0, self._width, self._height, 5, fill=1, stroke=0)
        c.setFillColor(self.tc)
        c.setFont("Helvetica-Bold", 9.5)
        c.drawString(8, self._height-13, self.title)
        c.setFillColor(DARK_TEXT)
        c.setFont("Helvetica", 9)
        for i, ln in enumerate(self.lines):
            c.drawString(14, self._height-26-(i*14), ln)

# ─── Normal Sinus ECG Drawing ─────────────────────────────────────────────────

def make_normal_ecg(w=480, h=100, n_beats=3, title="Normal Sinus Rhythm"):
    d = Drawing(w, h+30)
    # Title
    d.add(String(w/2, h+20, title, textAnchor='middle',
                 fontName='Helvetica-Bold', fontSize=9, fillColor=ECG_BLUE))
    draw_ecg_grid(d, 0, 0, w, h)
    # baseline
    bl = h//2
    all_pts = []
    beat_len = 125
    for b in range(n_beats):
        all_pts += normal_beat(t_offset=b*beat_len, duration=beat_len)
    # add trailing baseline
    for t in range(n_beats*beat_len, int(w)+5):
        all_pts.append((t, 0))
    scale_x = w / (n_beats * beat_len + 10)
    ecg_path(d, all_pts, 0, 0, bl, scale_x=scale_x, scale_y=1.8, color=ECG_RED)
    return d

# ─── Pathology ECG Drawings ───────────────────────────────────────────────────

def make_stemi_ecg(w=480, h=100):
    """Anterior STEMI: ST elevation in V1-V4, pathological Q waves."""
    d = Drawing(w, h+30)
    d.add(String(w/2, h+20, "STEMI - ST Elevation Myocardial Infarction",
                 textAnchor='middle', fontName='Helvetica-Bold',
                 fontSize=9, fillColor=ECG_RED))
    draw_ecg_grid(d, 0, 0, w, h)
    bl = h//2
    beat_len = 150
    pts = []
    for b in range(3):
        t0 = b * beat_len
        # Baseline
        for t in range(15): pts.append((t0+t, 0))
        # P wave
        for t in range(10):
            pts.append((t0+15+t, 2.5*math.sin(math.pi*t/10)))
        # PR
        for t in range(10): pts.append((t0+25+t, 0))
        # Pathological Q
        pts += [(t0+35, 0),(t0+37, -7),(t0+39, 0)]
        # R (smaller than normal — infarction)
        pts += [(t0+41, 8),(t0+43, 0)]
        # ST Elevation — tombstone pattern
        pts += [(t0+44, 6),(t0+50, 8),(t0+58, 6),(t0+64, 4)]
        # T wave inverted merged with elevated ST
        for t in range(14):
            pts.append((t0+64+t, 4 - 6*math.sin(math.pi*t/14)))
        # baseline
        for t in range(beat_len-(64+14)):
            pts.append((t0+78+t, 0))
    # extend
    for t in range(3*beat_len, int(w)+5): pts.append((t, 0))
    sx = w/(3*beat_len+5)
    ecg_path(d, pts, 0, 0, bl, scale_x=sx, scale_y=1.6, color=ECG_RED)
    # annotation
    d.add(String(w*0.5, bl+28, "ST Elevation", textAnchor='middle',
                 fontName='Helvetica-Bold', fontSize=7.5, fillColor=ECG_RED))
    d.add(String(w*0.15, bl-28, "Pathological Q", textAnchor='middle',
                 fontName='Helvetica-Bold', fontSize=7.5, fillColor=ECG_PURPLE))
    return d

def make_afib_ecg(w=480, h=100):
    """Atrial Fibrillation: no P waves, irregular RR, fibrillatory baseline."""
    d = Drawing(w, h+30)
    d.add(String(w/2, h+20, "Atrial Fibrillation (AF)",
                 textAnchor='middle', fontName='Helvetica-Bold',
                 fontSize=9, fillColor=ECG_BLUE))
    draw_ecg_grid(d, 0, 0, w, h)
    bl = h//2
    import random; random.seed(42)
    pts = []
    t = 0
    # fibrillatory baseline
    while t < w/1.0:
        pts.append((t, 1.2*math.sin(2*math.pi*t/4.5 + random.uniform(0,1))))
        t += 0.5
    # overlay irregular QRS complexes
    qrs_times = [20, 62, 97, 138, 167, 210, 255, 290, 330, 370, 410, 445]
    qrs_pts = []
    for qt in qrs_times:
        scale_x = 0.95
        qrs_pts += [(qt-2, 0),(qt, -3),(qt+2, 0),(qt+3, 18),(qt+5, -4),(qt+7, 0)]
    # merge
    all_pts = pts + qrs_pts
    all_pts.sort(key=lambda x: x[0])
    sx = w/(w/1.0)
    ecg_path(d, all_pts, 0, 0, bl, scale_x=sx, scale_y=1.5, color=ECG_BLUE)
    d.add(String(w*0.45, bl+30, "No P waves; irregular RR intervals",
                 textAnchor='middle', fontName='Helvetica-Bold', fontSize=7.5,
                 fillColor=ECG_BLUE))
    d.add(String(w*0.45, bl-30, "Fibrillatory baseline",
                 textAnchor='middle', fontName='Helvetica-Bold', fontSize=7.5,
                 fillColor=ECG_BLUE))
    return d

def make_heart_block_ecg(w=480, h=100, block_type="complete"):
    """Complete (3rd degree) heart block: P waves independent of QRS."""
    d = Drawing(w, h+30)
    if block_type == "complete":
        ttl = "Complete (3rd Degree) Heart Block"
        clr = ECG_PURPLE
    else:
        ttl = "2nd Degree Heart Block (Mobitz II)"
        clr = ECG_ORANGE
    d.add(String(w/2, h+20, ttl, textAnchor='middle',
                 fontName='Helvetica-Bold', fontSize=9, fillColor=clr))
    draw_ecg_grid(d, 0, 0, w, h)
    bl = h//2
    pts = []
    # P waves at ~70 bpm (atrial rate)
    p_times = list(range(10, int(w/0.9), 22))
    for pt in p_times:
        for t in range(10):
            pts.append((pt+t, 2.5*math.sin(math.pi*t/10)))
    pts.sort(key=lambda x: x[0])
    # Add baseline
    baseline = [(t, 0) for t in range(0, int(w/0.9)+5, 1)]
    merged = baseline + pts
    merged.sort(key=lambda x: x[0])
    ecg_path(d, merged, 0, 0, bl, scale_x=0.9, scale_y=1.4, color=ECG_ORANGE)
    # QRS at slow escape rate ~35 bpm (wide, ventricular)
    qrs_times = [40, 98, 156, 214, 272, 330, 388, 446]
    qrs_pts = []
    for qt in qrs_times:
        qrs_pts += [(qt, 0),(qt+2, -3),(qt+4, 0),(qt+6, 15),(qt+10, -5),(qt+14, 0)]
    qrs_pts.sort(key=lambda x: x[0])
    ecg_path(d, qrs_pts, 0, 0, bl, scale_x=0.9, scale_y=1.4, color=ECG_PURPLE)
    d.add(String(w*0.35, bl+28, "P waves (independent)",
                 textAnchor='middle', fontName='Helvetica-Bold', fontSize=7.5,
                 fillColor=ECG_ORANGE))
    d.add(String(w*0.65, bl-30, "Wide QRS (escape rhythm)",
                 textAnchor='middle', fontName='Helvetica-Bold', fontSize=7.5,
                 fillColor=ECG_PURPLE))
    return d

def make_lbbb_ecg(w=480, h=100):
    """Left Bundle Branch Block: wide QRS >120ms, broad notched R in V5/V6."""
    d = Drawing(w, h+30)
    d.add(String(w/2, h+20, "Left Bundle Branch Block (LBBB)",
                 textAnchor='middle', fontName='Helvetica-Bold',
                 fontSize=9, fillColor=ECG_GREEN))
    draw_ecg_grid(d, 0, 0, w, h)
    bl = h//2
    beat_len = 150
    pts = []
    for b in range(3):
        t0 = b * beat_len
        # Isoelectric
        for t in range(15): pts.append((t0+t, 0))
        # P wave
        for t in range(10):
            pts.append((t0+15+t, 2.5*math.sin(math.pi*t/10)))
        # PR
        for t in range(10): pts.append((t0+25+t, 0))
        # Wide QRS: broad notched R (no Q, dominant R, no S)
        pts += [(t0+35, 0),(t0+38, 5),(t0+41, 8),(t0+43, 12),(t0+46, 14),
                (t0+49, 12),(t0+52, 9),(t0+55, 12),(t0+58, 14),(t0+61, 12),
                (t0+64, 8),(t0+67, 4),(t0+70, 0)]
        # ST depression + T wave inversion
        pts += [(t0+71, -3),(t0+74, -4),(t0+78, -4)]
        for t in range(14):
            pts.append((t0+78+t, -3.5*math.sin(math.pi*t/14)))
        for t in range(beat_len-(78+14)):
            pts.append((t0+92+t, 0))
    for t in range(3*beat_len, int(w)+5): pts.append((t, 0))
    sx = w/(3*beat_len+5)
    ecg_path(d, pts, 0, 0, bl, scale_x=sx, scale_y=1.5, color=ECG_GREEN)
    d.add(String(w*0.5, bl+28, "Broad notched R (William's pattern)",
                 textAnchor='middle', fontName='Helvetica-Bold', fontSize=7.5,
                 fillColor=ECG_GREEN))
    d.add(String(w*0.5, bl-28, "T inversion; QRS >120 ms",
                 textAnchor='middle', fontName='Helvetica-Bold', fontSize=7.5,
                 fillColor=ECG_GREEN))
    return d

def make_vt_ecg(w=480, h=100):
    """Ventricular Tachycardia: fast, wide, regular QRS, no P waves."""
    d = Drawing(w, h+30)
    d.add(String(w/2, h+20, "Ventricular Tachycardia (VT)",
                 textAnchor='middle', fontName='Helvetica-Bold',
                 fontSize=9, fillColor=ECG_RED))
    draw_ecg_grid(d, 0, 0, w, h)
    bl = h//2
    beat_len = 55
    pts = []
    for b in range(8):
        t0 = b * beat_len
        pts += [(t0, 0),(t0+3, -4),(t0+5, 0),(t0+7, 16),(t0+11, -5),(t0+15, -6),
                (t0+20, -2),(t0+25, 1),(t0+30, 0)]
        for t in range(30, beat_len): pts.append((t0+t, 0))
    for t in range(8*beat_len, int(w)+5): pts.append((t, 0))
    sx = w/(8*beat_len+5)
    ecg_path(d, pts, 0, 0, bl, scale_x=sx, scale_y=1.5, color=ECG_RED)
    d.add(String(w*0.5, bl+28, "Fast rate (~150-200 bpm), wide QRS, no P waves",
                 textAnchor='middle', fontName='Helvetica-Bold', fontSize=7.5,
                 fillColor=ECG_RED))
    return d

def make_hyperkalemia_ecg(w=480, h=100):
    """Hyperkalemia: tall peaked T waves, wide QRS, loss of P."""
    d = Drawing(w, h+30)
    d.add(String(w/2, h+20, "Hyperkalemia ECG Changes",
                 textAnchor='middle', fontName='Helvetica-Bold',
                 fontSize=9, fillColor=ECG_ORANGE))
    draw_ecg_grid(d, 0, 0, w, h)
    bl = h//2
    beat_len = 140
    pts = []
    for b in range(3):
        t0 = b * beat_len
        for t in range(18): pts.append((t0+t, 0))
        # Tiny P wave (flat/absent in severe)
        for t in range(8):
            pts.append((t0+18+t, 1.0*math.sin(math.pi*t/8)))
        for t in range(10): pts.append((t0+26+t, 0))
        # Wide QRS
        pts += [(t0+36, 0),(t0+38,-4),(t0+40,0),(t0+42,16),(t0+46,-6),(t0+52,0)]
        # Tall peaked T wave (tented)
        for t in range(18):
            v = 12 * math.sin(math.pi*t/18)
            pts.append((t0+54+t, v))
        # baseline
        for t in range(beat_len-(54+18)):
            pts.append((t0+72+t, 0))
    for t in range(3*beat_len, int(w)+5): pts.append((t, 0))
    sx = w/(3*beat_len+5)
    ecg_path(d, pts, 0, 0, bl, scale_x=sx, scale_y=1.5, color=ECG_ORANGE)
    d.add(String(w*0.5, bl+32, "Tall peaked (tented) T waves",
                 textAnchor='middle', fontName='Helvetica-Bold', fontSize=7.5,
                 fillColor=ECG_ORANGE))
    return d

def make_wpw_ecg(w=480, h=100):
    """Wolff-Parkinson-White: short PR, delta wave, wide QRS."""
    d = Drawing(w, h+30)
    d.add(String(w/2, h+20, "Wolff-Parkinson-White (WPW) Syndrome",
                 textAnchor='middle', fontName='Helvetica-Bold',
                 fontSize=9, fillColor=ECG_PURPLE))
    draw_ecg_grid(d, 0, 0, w, h)
    bl = h//2
    beat_len = 130
    pts = []
    for b in range(3):
        t0 = b * beat_len
        for t in range(12): pts.append((t0+t, 0))
        # P wave
        for t in range(10):
            pts.append((t0+12+t, 2.5*math.sin(math.pi*t/10)))
        # SHORT PR (<120ms)
        for t in range(5): pts.append((t0+22+t, 0))
        # Delta wave (slurred upstroke)
        pts += [(t0+27, 0),(t0+30, 3),(t0+33, 7),(t0+36, 12),(t0+38,15),
                (t0+40,18),(t0+42, 16),(t0+44,12),(t0+47, 6),(t0+50, 0)]
        # S wave absent, broad QRS
        pts += [(t0+51, -3),(t0+54, 0)]
        # ST/T
        for t in range(15):
            pts.append((t0+55+t, -4*math.sin(math.pi*t/15)))
        for t in range(beat_len-(55+15)):
            pts.append((t0+70+t, 0))
    for t in range(3*beat_len, int(w)+5): pts.append((t, 0))
    sx = w/(3*beat_len+5)
    ecg_path(d, pts, 0, 0, bl, scale_x=sx, scale_y=1.4, color=ECG_PURPLE)
    d.add(String(w*0.5, bl+28, "Short PR + Delta wave (slurred QRS onset)",
                 textAnchor='middle', fontName='Helvetica-Bold', fontSize=7.5,
                 fillColor=ECG_PURPLE))
    return d

def make_long_qt_ecg(w=480, h=100):
    """Long QT syndrome: prolonged QT interval."""
    d = Drawing(w, h+30)
    d.add(String(w/2, h+20, "Long QT Syndrome",
                 textAnchor='middle', fontName='Helvetica-Bold',
                 fontSize=9, fillColor=ECG_PURPLE))
    draw_ecg_grid(d, 0, 0, w, h)
    bl = h//2
    beat_len = 160
    pts = []
    for b in range(3):
        t0 = b * beat_len
        for t in range(15): pts.append((t0+t, 0))
        for t in range(10):
            pts.append((t0+15+t, 2.5*math.sin(math.pi*t/10)))
        for t in range(10): pts.append((t0+25+t, 0))
        pts += [(t0+35, 0),(t0+37,-3),(t0+39,0),(t0+41,18),(t0+43,0)]
        pts += [(t0+44,-4),(t0+46,0)]
        # Prolonged ST segment before T
        for t in range(28): pts.append((t0+47+t, 0))
        # T wave (late, broad)
        for t in range(22):
            pts.append((t0+75+t, 5*math.sin(math.pi*t/22)))
        for t in range(beat_len-(75+22)):
            pts.append((t0+97+t, 0))
    for t in range(3*beat_len, int(w)+5): pts.append((t, 0))
    sx = w/(3*beat_len+5)
    ecg_path(d, pts, 0, 0, bl, scale_x=sx, scale_y=1.5, color=ECG_PURPLE)
    # Draw QT interval arrow
    # annotation
    d.add(String(w*0.5, bl-30, "Prolonged QT interval (> 450 ms men / 460 ms women)",
                 textAnchor='middle', fontName='Helvetica-Bold', fontSize=7.5,
                 fillColor=ECG_PURPLE))
    return d

# ─── Annotated Normal Beat Drawing ───────────────────────────────────────────

def make_labeled_beat(w=420, h=130):
    d = Drawing(w, h+40)
    d.add(String(w/2, h+30, "ECG Waveform Components (Labeled)",
                 textAnchor='middle', fontName='Helvetica-Bold',
                 fontSize=10, fillColor=ECG_BLUE))
    draw_ecg_grid(d, 0, 0, w, h)
    bl = h//2 - 10

    beat_len = 200
    pts = normal_beat(t_offset=20, duration=beat_len)
    pts = [(t, v) for t, v in pts if t < w]
    pts = [(t, 0) for t in range(20)] + pts
    sx = w / (beat_len + 40)
    ecg_path(d, pts, 0, 0, bl, scale_x=sx, scale_y=2.2, color=ECG_RED, width=2)

    # Scale factor for annotation positions
    def tx(t): return t * sx
    sy = 2.2

    # Label P wave
    p_peak_x = tx(26)
    p_peak_y = bl + 3 * sy
    d.add(Line(p_peak_x, p_peak_y+3, p_peak_x, p_peak_y+20,
               strokeColor=ECG_BLUE, strokeWidth=0.8))
    d.add(String(p_peak_x, p_peak_y+22, "P wave", textAnchor='middle',
                 fontName='Helvetica-Bold', fontSize=8, fillColor=ECG_BLUE))

    # Label QRS
    qrs_x = tx(72)
    qrs_y = bl + 20*sy
    d.add(Line(qrs_x, qrs_y+3, qrs_x, qrs_y+18,
               strokeColor=ECG_RED, strokeWidth=0.8))
    d.add(String(qrs_x, qrs_y+20, "QRS", textAnchor='middle',
                 fontName='Helvetica-Bold', fontSize=8, fillColor=ECG_RED))

    # Label T wave
    t_peak_x = tx(100)
    t_peak_y = bl + 6*sy
    d.add(Line(t_peak_x, t_peak_y+3, t_peak_x, t_peak_y+16,
               strokeColor=ECG_GREEN, strokeWidth=0.8))
    d.add(String(t_peak_x, t_peak_y+18, "T wave", textAnchor='middle',
                 fontName='Helvetica-Bold', fontSize=8, fillColor=ECG_GREEN))

    # Label intervals below baseline
    # PR interval
    pr_start = tx(20); pr_end = tx(69)
    d.add(Line(pr_start, bl-30, pr_end, bl-30,
               strokeColor=ECG_BLUE, strokeWidth=1))
    d.add(Line(pr_start, bl-30, pr_start, bl-26, strokeColor=ECG_BLUE, strokeWidth=1))
    d.add(Line(pr_end, bl-30, pr_end, bl-26, strokeColor=ECG_BLUE, strokeWidth=1))
    d.add(String((pr_start+pr_end)/2, bl-42, "PR interval (120-200 ms)",
                 textAnchor='middle', fontName='Helvetica', fontSize=7,
                 fillColor=ECG_BLUE))

    # QRS interval
    qrs_start = tx(66); qrs_end = tx(79)
    d.add(Line(qrs_start, bl-18, qrs_end, bl-18,
               strokeColor=ECG_RED, strokeWidth=1))
    d.add(Line(qrs_start, bl-18, qrs_start, bl-14, strokeColor=ECG_RED, strokeWidth=1))
    d.add(Line(qrs_end, bl-18, qrs_end, bl-14, strokeColor=ECG_RED, strokeWidth=1))
    d.add(String((qrs_start+qrs_end)/2, bl-14, "QRS (≤110 ms)",
                 textAnchor='middle', fontName='Helvetica', fontSize=7,
                 fillColor=ECG_RED))

    # QT interval
    qt_start = tx(66); qt_end = tx(125)
    d.add(Line(qt_start, bl-50, qt_end, bl-50,
               strokeColor=ECG_PURPLE, strokeWidth=1))
    d.add(Line(qt_start, bl-50, qt_start, bl-46, strokeColor=ECG_PURPLE, strokeWidth=1))
    d.add(Line(qt_end, bl-50, qt_end, bl-46, strokeColor=ECG_PURPLE, strokeWidth=1))
    d.add(String((qt_start+qt_end)/2, bl-62, "QT interval",
                 textAnchor='middle', fontName='Helvetica', fontSize=7,
                 fillColor=ECG_PURPLE))

    return d

# ─── Leads Diagram ───────────────────────────────────────────────────────────

def make_leads_diagram(w=420, h=140):
    d = Drawing(w, h)
    cx, cy = w//2, h//2 + 10

    # Body outline
    d.add(Rect(w*0.3, h*0.1, w*0.4, h*0.85,
               strokeColor=colors.HexColor("#AAAAAA"), strokeWidth=0.5,
               fillColor=colors.HexColor("#FFF0F0"), rx=20, ry=20))

    # Electrode positions
    electrodes = {
        "RA": (w*0.2, h*0.75), "LA": (w*0.8, h*0.75),
        "RL": (w*0.3, h*0.15), "LL": (w*0.7, h*0.15),
        "V1": (w*0.48, h*0.62), "V2": (w*0.52, h*0.58),
        "V3": (w*0.56, h*0.52), "V4": (w*0.60, h*0.47),
        "V5": (w*0.65, h*0.42), "V6": (w*0.70, h*0.38),
    }
    colors_map = {"RA":"#E74C3C","LA":"#2980B9","RL":"#27AE60","LL":"#F39C12",
                  "V1":"#8E44AD","V2":"#8E44AD","V3":"#8E44AD",
                  "V4":"#8E44AD","V5":"#8E44AD","V6":"#8E44AD"}

    for name, (ex, ey) in electrodes.items():
        c = colors.HexColor(colors_map[name])
        d.add(Circle(ex, ey, 7, fillColor=c, strokeColor=colors.white, strokeWidth=1))
        d.add(String(ex, ey-3, name, textAnchor='middle',
                     fontName='Helvetica-Bold', fontSize=6, fillColor=colors.white))

    # Lead I line
    d.add(Line(electrodes["RA"][0]+7, electrodes["RA"][1],
               electrodes["LA"][0]-7, electrodes["LA"][1],
               strokeColor=colors.HexColor("#E74C3C"), strokeWidth=0.8, strokeDashArray=[3,2]))
    d.add(String(w/2, h*0.85, "Lead I", textAnchor='middle',
                 fontName='Helvetica', fontSize=7, fillColor=ECG_BLUE))

    d.add(String(w/2, h+5, "Standard 12-Lead Electrode Placement",
                 textAnchor='middle', fontName='Helvetica-Bold',
                 fontSize=9, fillColor=ECG_BLUE))
    return d

# ─── Axis Diagram ─────────────────────────────────────────────────────────────

def make_axis_diagram(w=220, h=220):
    d = Drawing(w, h+20)
    d.add(String(w/2, h+10, "QRS Axis (Frontal Plane)",
                 textAnchor='middle', fontName='Helvetica-Bold',
                 fontSize=9, fillColor=ECG_BLUE))
    cx, cy = w//2, h//2

    # Background sectors
    sectors = [
        ((-30, 90), colors.HexColor("#EAFAF1"), "Normal"),
        ((-90, -30), colors.HexColor("#FEF9E7"), "LAD"),
        ((90, 180), colors.HexColor("#FEF5E7"), "RAD"),
        ((180, 270), colors.HexColor("#FDEDEC"), "Extreme"),
    ]
    r = 90
    for (a1, a2), col, label in sectors:
        pts = [cx, cy]
        steps = 20
        for i in range(steps+1):
            angle = math.radians(a1 + (a2-a1)*i/steps)
            pts.extend([cx + r*math.cos(angle), cy + r*math.sin(angle)])
        pts.extend([cx, cy])
        poly_pts = [(pts[i*2], pts[i*2+1]) for i in range(len(pts)//2)]
        d.add(Polygon(pts[2:], fillColor=col, strokeColor=None))

    # Circle
    d.add(Circle(cx, cy, r, fillColor=None, strokeColor=colors.HexColor("#BBBBBB"),
                 strokeWidth=1))

    # Axis lines (lead directions)
    leads = [
        (0, "I", ECG_BLUE), (60, "aVF", ECG_BLUE), (-60, "aVL", ECG_BLUE),
        (90, "aVF", None), (120, "III", ECG_BLUE), (-120, "aVR", ECG_BLUE),
    ]
    for angle_deg, lbl, col in leads:
        angle = math.radians(angle_deg)
        x1 = cx - r*math.cos(angle)
        y1 = cy - r*math.sin(angle)
        x2 = cx + r*math.cos(angle)
        y2 = cy + r*math.sin(angle)
        d.add(Line(x1, y1, x2, y2, strokeColor=colors.HexColor("#CCCCCC"),
                   strokeWidth=0.7))

    # Normal axis arrow
    mean_angle = math.radians(60)  # typical normal ~60°
    d.add(Line(cx, cy, cx + 70*math.cos(mean_angle), cy + 70*math.sin(mean_angle),
               strokeColor=ECG_GREEN, strokeWidth=2.5))

    # Labels
    d.add(String(cx+r+5, cy, "0°", fontName='Helvetica-Bold', fontSize=7,
                 fillColor=ECG_BLUE))
    d.add(String(cx-r-18, cy, "±180°", fontName='Helvetica-Bold', fontSize=7,
                 fillColor=ECG_BLUE))
    d.add(String(cx-5, cy+r+5, "+90°", fontName='Helvetica-Bold', fontSize=7,
                 fillColor=ECG_BLUE))
    d.add(String(cx-5, cy-r-12, "-90°", fontName='Helvetica-Bold', fontSize=7,
                 fillColor=ECG_BLUE))

    # Zone labels
    d.add(String(cx+15, cy+35, "NORMAL\n-30°to+90°",
                 fontName='Helvetica-Bold', fontSize=6.5, fillColor=ECG_GREEN))
    d.add(String(cx-75, cy+40, "LAD",
                 fontName='Helvetica-Bold', fontSize=6.5, fillColor=ECG_ORANGE))
    d.add(String(cx+35, cy-40, "RAD",
                 fontName='Helvetica-Bold', fontSize=6.5, fillColor=ECG_RED))
    return d

# ─── Build the PDF ────────────────────────────────────────────────────────────

def build_pdf(out_path):
    doc = SimpleDocTemplate(out_path, pagesize=A4,
                            leftMargin=MARGIN, rightMargin=MARGIN,
                            topMargin=MARGIN, bottomMargin=MARGIN,
                            title="ECG Basics & Pathology Reference",
                            author="Medical Reference - Harrison's & Guyton")
    story = []
    W = PAGE_W - 2*MARGIN

    # ── Cover ──────────────────────────────────────────────────────────────
    story.append(Spacer(1, 1.5*cm))

    # Title block
    title_drawing = Drawing(W, 80)
    title_drawing.add(Rect(0, 0, W, 80, fillColor=ECG_BLUE, strokeColor=None, rx=8, ry=8))
    title_drawing.add(String(W/2, 52, "ECG Basics & Clinical Pathology",
                             textAnchor='middle', fontName='Helvetica-Bold',
                             fontSize=22, fillColor=colors.white))
    title_drawing.add(String(W/2, 30, "A Comprehensive Reference Guide",
                             textAnchor='middle', fontName='Helvetica',
                             fontSize=13, fillColor=colors.HexColor("#AED6F1")))
    title_drawing.add(String(W/2, 12, "Based on Harrison's Principles of Internal Medicine 22E (2025) & Guyton and Hall Textbook of Medical Physiology",
                             textAnchor='middle', fontName='Helvetica-Oblique',
                             fontSize=7.5, fillColor=colors.HexColor("#85C1E9")))
    story.append(title_drawing)

    # Cover ECG trace
    story.append(Spacer(1, 0.4*cm))
    cover_ecg = make_normal_ecg(w=int(W), h=80, n_beats=5,
                                title="")
    story.append(cover_ecg)
    story.append(Spacer(1, 0.3*cm))

    # Table of contents summary
    toc_data = [
        ["Section", "Topic"],
        ["1", "Fundamentals of Electrocardiography"],
        ["2", "ECG Waveforms and Intervals"],
        ["3", "The 12-Lead System"],
        ["4", "Cardiac Axis"],
        ["5", "Systematic ECG Interpretation"],
        ["6", "Pathology: STEMI & Ischaemia"],
        ["7", "Pathology: Arrhythmias (AF, VT)"],
        ["8", "Pathology: Conduction Blocks"],
        ["9", "Pathology: WPW & Long QT"],
        ["10", "Pathology: Electrolyte Disturbances"],
    ]
    toc_style = TableStyle([
        ('BACKGROUND', (0,0), (-1,0), ECG_BLUE),
        ('TEXTCOLOR', (0,0), (-1,0), colors.white),
        ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
        ('FONTSIZE', (0,0), (-1,0), 9),
        ('FONTNAME', (0,1), (-1,-1), 'Helvetica'),
        ('FONTSIZE', (0,1), (-1,-1), 9),
        ('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, BOX_BLUE]),
        ('GRID', (0,0), (-1,-1), 0.5, colors.HexColor("#CCCCCC")),
        ('LEFTPADDING', (0,0), (-1,-1), 8),
        ('BOTTOMPADDING', (0,0), (-1,-1), 4),
    ])
    toc_table = Table(toc_data, colWidths=[1*cm, W-1*cm])
    toc_table.setStyle(toc_style)
    story.append(toc_table)
    story.append(PageBreak())

    # ── Section 1: Fundamentals ───────────────────────────────────────────
    story.append(SectionHeader("SECTION 1 — Fundamentals of Electrocardiography"))
    story.append(Spacer(1, 0.3*cm))

    story.append(Paragraph("What is an ECG?", h2_style))
    story.append(Paragraph(
        "When a cardiac impulse passes through the heart, electrical current spreads from the heart "
        "into adjacent tissues and to the body surface. If electrodes are placed on the skin on "
        "opposite sides of the heart, the electrical potentials generated can be recorded as an "
        "<b>electrocardiogram (ECG)</b>. The ECG provides a graphic representation of the heart's "
        "electrical activity over time.", body_style))

    story.append(Paragraph("Cardiac Conduction System", h2_style))
    story.append(Paragraph(
        "The stimulus initiating the normal heartbeat originates in the <b>sinoatrial (SA) node</b>, "
        "which possesses spontaneous automaticity. Depolarization spreads through the atria, then passes "
        "through the <b>AV node</b> (where it is delayed to allow ventricular filling), then into the "
        "<b>Bundle of His</b>, which splits into the <b>right and left bundle branches</b>, and finally "
        "to the ventricular myocardium via <b>Purkinje fibers</b>.", body_style))

    # Conduction table
    cond_data = [
        ["Structure", "Function", "Normal Velocity"],
        ["SA Node", "Pacemaker — initiates impulse (60-100 bpm)", "~1 m/s"],
        ["AV Node", "Gatekeeper — delays impulse (PR interval)", "~0.05 m/s"],
        ["Bundle of His", "Transmits impulse to ventricles", "~1 m/s"],
        ["Bundle Branches (R&L)", "Rapid spread to respective ventricles", "~2 m/s"],
        ["Purkinje Fibers", "Final fast delivery to myocardium", "~4 m/s"],
    ]
    ct_style = TableStyle([
        ('BACKGROUND', (0,0), (-1,0), ECG_BLUE),
        ('TEXTCOLOR', (0,0), (-1,0), colors.white),
        ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
        ('FONTSIZE', (0,0), (-1,-1), 9),
        ('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, BOX_BLUE]),
        ('GRID', (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
        ('LEFTPADDING', (0,0), (-1,-1), 6),
        ('BOTTOMPADDING', (0,0), (-1,-1), 4),
    ])
    ct = Table(cond_data, colWidths=[W*0.28, W*0.52, W*0.20])
    ct.setStyle(ct_style)
    story.append(ct)
    story.append(Spacer(1, 0.3*cm))
    story.append(Paragraph(
        "<i>Reference: Harrison's Principles of Internal Medicine 22E (2025), Chapter 247; "
        "Guyton and Hall Textbook of Medical Physiology, Chapter 11</i>", ref_style))

    # ── Section 2: Waveforms & Intervals ─────────────────────────────────
    story.append(PageBreak())
    story.append(SectionHeader("SECTION 2 — ECG Waveforms and Intervals", color=ECG_GREEN))
    story.append(Spacer(1, 0.3*cm))

    labeled_beat = make_labeled_beat(w=int(W), h=130)
    story.append(labeled_beat)
    story.append(Paragraph("Figure 1. Labeled ECG waveform components with key intervals. "
                           "(Adapted from Harrison's 22E, Figure 247-2; Guyton & Hall, Ch.11)", caption_style))
    story.append(Spacer(1, 0.3*cm))

    # Waveform table
    wave_data = [
        ["Wave/Segment", "Origin", "Normal Duration", "Normal Amplitude"],
        ["P wave", "Atrial depolarization", "80-100 ms", "<2.5 mm (height)\n<120 ms (width)"],
        ["PR interval", "Atrial depol. + AV node delay", "120-200 ms", "Isoelectric"],
        ["QRS complex", "Ventricular depolarization", "≤110 ms", "Variable"],
        ["ST segment", "Ventricular plateau (phase 2)", "Isoelectric", "±1 mm from baseline"],
        ["T wave", "Ventricular repolarization", "150-250 ms", "Positive in most leads"],
        ["QT interval", "Ventricular depol. + repol.", "QTc ≤450 ms (M)\n≤460 ms (F)", "N/A"],
        ["U wave", "Purkinje repolarization", "~200 ms", "<1 mm (small, positive)"],
    ]
    wt_style = TableStyle([
        ('BACKGROUND', (0,0), (-1,0), ECG_GREEN),
        ('TEXTCOLOR', (0,0), (-1,0), colors.white),
        ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
        ('FONTSIZE', (0,0), (-1,-1), 8.5),
        ('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, BOX_GREEN]),
        ('GRID', (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
        ('LEFTPADDING', (0,0), (-1,-1), 5),
        ('BOTTOMPADDING', (0,0), (-1,-1), 3),
        ('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
    ])
    wt = Table(wave_data, colWidths=[W*0.22, W*0.30, W*0.22, W*0.26])
    wt.setStyle(wt_style)
    story.append(wt)
    story.append(Spacer(1, 0.2*cm))

    # Info boxes for key facts
    story.append(InfoBox("Key Fact: QRS Formation",
        ["• First phase: Septal depolarization (left → right, anteriorly)",
         "• Second phase: Ventricular depolarization (left ventricle dominates, points left & posteriorly)",
         "• V1: Small r wave then deep S wave (rS pattern)",
         "• V6: Small q wave then tall R wave (qR pattern)",
         "• R-wave progression: R grows from V1→V6 (transition at V3 or V4)"],
        bg=BOX_BLUE, title_color=ECG_BLUE))
    story.append(Spacer(1, 0.2*cm))
    story.append(Paragraph("<i>Reference: Harrison's 22E, p.1912-1913</i>", ref_style))

    # ── Section 3: 12-Lead System ─────────────────────────────────────────
    story.append(PageBreak())
    story.append(SectionHeader("SECTION 3 — The 12-Lead ECG System", color=ECG_ORANGE))
    story.append(Spacer(1, 0.3*cm))

    story.append(Paragraph("Lead Groups", h2_style))
    story.append(Paragraph(
        "The 12 conventional ECG leads are divided into two groups: <b>six limb (extremity) leads</b> "
        "that record potentials in the frontal plane, and <b>six chest (precordial) leads</b> that record "
        "potentials in the horizontal plane. Each lead acts like a different 'camera angle' looking at the "
        "same cardiac electrical events from a different spatial orientation.", body_style))

    leads_data = [
        ["Lead Group", "Leads", "View / Territory"],
        ["Inferior", "II, III, aVF", "Inferior wall (right coronary artery territory)"],
        ["Lateral", "I, aVL, V5, V6", "Lateral wall (left circumflex territory)"],
        ["Anterior/Septal", "V1, V2", "Septal/anterior wall (LAD territory)"],
        ["Anterior", "V3, V4", "Anterior wall (LAD territory)"],
        ["Right heart", "aVR", "Right atrium / cavity; reciprocal of lateral leads"],
        ["Extended", "V3R, V4R", "Right ventricle (add-on leads for RV infarction)"],
    ]
    lt_style = TableStyle([
        ('BACKGROUND', (0,0), (-1,0), ECG_ORANGE),
        ('TEXTCOLOR', (0,0), (-1,0), colors.white),
        ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
        ('FONTSIZE', (0,0), (-1,-1), 9),
        ('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, BOX_ORANGE]),
        ('GRID', (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
        ('LEFTPADDING', (0,0), (-1,-1), 6),
        ('BOTTOMPADDING', (0,0), (-1,-1), 4),
    ])
    lt = Table(leads_data, colWidths=[W*0.20, W*0.25, W*0.55])
    lt.setStyle(lt_style)
    story.append(lt)
    story.append(Spacer(1, 0.4*cm))

    # Normal sinus ECG
    norm_ecg = make_normal_ecg(w=int(W), h=90, n_beats=4, title="Normal Sinus Rhythm (Rate 60-100 bpm)")
    story.append(norm_ecg)
    story.append(Paragraph("Figure 2. Normal sinus rhythm: upright P wave before each QRS, regular rhythm, "
                           "normal intervals.", caption_style))

    # ── Section 4: Cardiac Axis ────────────────────────────────────────────
    story.append(PageBreak())
    story.append(SectionHeader("SECTION 4 — Cardiac Axis", color=ECG_PURPLE))
    story.append(Spacer(1, 0.3*cm))

    story.append(Paragraph(
        "The <b>QRS axis</b> describes the mean direction of ventricular depolarization in the frontal plane. "
        "Normally the QRS axis ranges from <b>-30° to +100°</b>. Since the cardiac depolarization and "
        "repolarization wavefronts have direction and magnitude, they can be represented by vectors. "
        "The axis is determined by examining which limb leads have the tallest positive and negative deflections.",
        body_style))

    # Axis table + diagram side by side
    axis_diag = make_axis_diagram(w=200, h=200)
    axis_data = [
        ["Axis", "Degrees", "Common Causes"],
        ["Normal", "-30° to +90°", "Normal variant"],
        ["Left axis\ndeviation (LAD)", "-30° to -90°", "LBBB, inferior MI,\nleft anterior\nfascicular block"],
        ["Right axis\ndeviation (RAD)", "+90° to +180°", "RVH, RBBB,\nLateral MI,\npulmonary embolism"],
        ["Extreme\n(NW axis)", "±180° to -90°", "VT, emphysema,\nelectrode reversal"],
    ]
    at_style = TableStyle([
        ('BACKGROUND', (0,0), (-1,0), ECG_PURPLE),
        ('TEXTCOLOR', (0,0), (-1,0), colors.white),
        ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
        ('FONTSIZE', (0,0), (-1,-1), 8),
        ('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, colors.HexColor("#F5EEF8")]),
        ('GRID', (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
        ('LEFTPADDING', (0,0), (-1,-1), 5),
        ('BOTTOMPADDING', (0,0), (-1,-1), 3),
        ('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
    ])
    at = Table(axis_data, colWidths=[W*0.22, W*0.18, W*0.28])
    at.setStyle(at_style)

    combo_table = Table([[axis_diag, at]], colWidths=[W*0.40, W*0.60])
    combo_table.setStyle(TableStyle([
        ('VALIGN', (0,0), (-1,-1), 'TOP'),
        ('LEFTPADDING', (0,0), (-1,-1), 0),
        ('RIGHTPADDING', (0,0), (-1,-1), 4),
    ]))
    story.append(combo_table)

    story.append(Spacer(1, 0.2*cm))
    story.append(InfoBox("How to Determine Axis Quickly",
        ["1. Find the lead where QRS is most isoelectric (equal + and - deflections) — axis is perpendicular to it",
         "2. Lead I positive + aVF positive → Normal axis",
         "3. Lead I positive + aVF negative → Left axis deviation",
         "4. Lead I negative + aVF positive → Right axis deviation",
         "5. Lead I negative + aVF negative → Extreme (northwest) axis"],
        bg=colors.HexColor("#F5EEF8"), title_color=ECG_PURPLE))

    # ── Section 5: Systematic Interpretation ─────────────────────────────
    story.append(PageBreak())
    story.append(SectionHeader("SECTION 5 — Systematic ECG Interpretation", color=ECG_BLUE))
    story.append(Spacer(1, 0.3*cm))
    story.append(Paragraph(
        "Use a systematic approach every time to avoid missing findings. "
        "The following 8-step method ensures comprehensive analysis.", body_style))

    steps_data = [
        ["Step", "Parameter", "What to Check", "Normal Values"],
        ["1", "Rate", "Count R-R intervals; divide 300 by # large squares", "60-100 bpm"],
        ["2", "Rhythm", "Regular? P before every QRS? QRS after every P?", "Regular sinus"],
        ["3", "P wave", "Present? Upright in I, II? Same morphology?", "Present, ≤2.5mm, ≤110ms"],
        ["4", "PR interval", "Measure P onset → QRS onset", "120-200 ms"],
        ["5", "QRS complex", "Width? Morphology? Q waves? R progression?", "≤110 ms, narrow"],
        ["6", "Axis", "Lead I & aVF positivity (see Section 4)", "-30° to +90°"],
        ["7", "ST segment", "Elevation or depression vs. baseline?", "Isoelectric ±1mm"],
        ["8", "T wave & QT", "Upright or inverted? QTc prolonged?", "Upright; QTc <450ms"],
    ]
    st_style = TableStyle([
        ('BACKGROUND', (0,0), (-1,0), ECG_BLUE),
        ('TEXTCOLOR', (0,0), (-1,0), colors.white),
        ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
        ('FONTSIZE', (0,0), (-1,-1), 8.5),
        ('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, BOX_BLUE]),
        ('GRID', (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
        ('LEFTPADDING', (0,0), (-1,-1), 5),
        ('BOTTOMPADDING', (0,0), (-1,-1), 3),
        ('ALIGN', (0,0), (1,-1), 'CENTER'),
    ])
    st = Table(steps_data, colWidths=[W*0.07, W*0.16, W*0.47, W*0.30])
    st.setStyle(st_style)
    story.append(st)
    story.append(Spacer(1, 0.3*cm))

    story.append(Paragraph("Heart Rate Calculation Methods", h2_style))
    rate_data = [
        ["Method", "How", "Best Used For"],
        ["300 ÷ large squares", "Count large squares between R-R; divide 300", "Regular rhythms (quick)"],
        ["1500 ÷ small squares", "Count small squares between R-R; divide 1500", "Regular rhythms (precise)"],
        ["6-second strip", "Count QRS complexes in 6-sec strip × 10", "Irregular rhythms (AF)"],
    ]
    rr_style = TableStyle([
        ('BACKGROUND', (0,0), (-1,0), ECG_GREEN),
        ('TEXTCOLOR', (0,0), (-1,0), colors.white),
        ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
        ('FONTSIZE', (0,0), (-1,-1), 9),
        ('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, BOX_GREEN]),
        ('GRID', (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
        ('LEFTPADDING', (0,0), (-1,-1), 6),
        ('BOTTOMPADDING', (0,0), (-1,-1), 4),
    ])
    rr = Table(rate_data, colWidths=[W*0.30, W*0.40, W*0.30])
    rr.setStyle(rr_style)
    story.append(rr)

    # ── Section 6: STEMI ──────────────────────────────────────────────────
    story.append(PageBreak())
    story.append(SectionHeader("SECTION 6 — Pathology: ST-Elevation MI (STEMI)", color=ECG_RED))
    story.append(Spacer(1, 0.3*cm))

    story.append(Paragraph("STEMI: ECG Findings", h2_style))
    story.append(Paragraph(
        "Acute STEMI produces a characteristic sequence of ECG changes. "
        "The diagnosis of STEMI requires <b>ST-segment elevation ≥1mm in ≥2 contiguous leads</b> "
        "(or ≥2mm in V1-V3) in the context of symptoms. "
        "Reciprocal ST depression appears in leads opposite the infarct territory. "
        "<b>Pathological Q waves</b> (>40 ms wide or >25% of R wave height) develop over hours-days, "
        "indicating irreversible myocardial necrosis.", body_style))

    stemi_ecg = make_stemi_ecg(w=int(W), h=90)
    story.append(stemi_ecg)
    story.append(Paragraph("Figure 3. STEMI: Tombstone ST elevation, pathological Q waves, T-wave changes.",
                           caption_style))

    stemi_steps = [
        ["Step", "Finding", "Significance"],
        ["1", "ST elevation ≥1mm in ≥2 contiguous leads", "Active transmural ischemia/infarction"],
        ["2", "Hyperacute T waves (tall, peaked)", "Earliest sign — often missed (minutes)"],
        ["3", "Pathological Q waves (>40ms or >25% R)", "Necrosis — develops over hours"],
        ["4", "T-wave inversion", "Evolving phase (hours to days after onset)"],
        ["5", "Reciprocal ST depression", "Confirms true ST elevation (not spurious)"],
        ["6", "Loss of R-wave progression", "Prior anterior infarction"],
    ]
    ss_style = TableStyle([
        ('BACKGROUND', (0,0), (-1,0), ECG_RED),
        ('TEXTCOLOR', (0,0), (-1,0), colors.white),
        ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
        ('FONTSIZE', (0,0), (-1,-1), 8.5),
        ('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, BOX_RED]),
        ('GRID', (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
        ('LEFTPADDING', (0,0), (-1,-1), 5),
        ('BOTTOMPADDING', (0,0), (-1,-1), 3),
    ])
    ss = Table(stemi_steps, colWidths=[W*0.07, W*0.48, W*0.45])
    ss.setStyle(ss_style)
    story.append(ss)
    story.append(Spacer(1, 0.2*cm))

    # Territory table
    story.append(Paragraph("STEMI Territory Localization", h3_style))
    terr_data = [
        ["Territory", "Leads with ST Elevation", "Culprit Artery"],
        ["Anterior", "V1-V4", "Left Anterior Descending (LAD)"],
        ["Lateral", "I, aVL, V5-V6", "Left Circumflex (LCx)"],
        ["Inferior", "II, III, aVF", "Right Coronary Artery (RCA) 80%"],
        ["Posterior", "Tall R in V1-V2, ST depression V1-V3", "RCA or LCx"],
        ["Right Ventricle", "ST elevation V3R-V4R + inferior changes", "Proximal RCA"],
        ["Anterolateral", "V1-V6, I, aVL", "Large LAD or left main"],
    ]
    tt_style = TableStyle([
        ('BACKGROUND', (0,0), (-1,0), colors.HexColor("#922B21")),
        ('TEXTCOLOR', (0,0), (-1,0), colors.white),
        ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
        ('FONTSIZE', (0,0), (-1,-1), 8.5),
        ('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, BOX_RED]),
        ('GRID', (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
        ('LEFTPADDING', (0,0), (-1,-1), 5),
        ('BOTTOMPADDING', (0,0), (-1,-1), 3),
    ])
    tt = Table(terr_data, colWidths=[W*0.22, W*0.40, W*0.38])
    tt.setStyle(tt_style)
    story.append(tt)
    story.append(Spacer(1, 0.2*cm))
    story.append(Paragraph(
        "<i>Reference: Harrison's 22E pp.1915-1917, Goldberger's Clinical Electrocardiography 10e; "
        "Tintinalli's Emergency Medicine</i>", ref_style))

    # ── Section 7: Arrhythmias ─────────────────────────────────────────────
    story.append(PageBreak())
    story.append(SectionHeader("SECTION 7 — Pathology: Arrhythmias", color=ECG_BLUE))
    story.append(Spacer(1, 0.3*cm))

    # AF
    story.append(Paragraph("Atrial Fibrillation (AF)", h2_style))
    story.append(Paragraph(
        "AF is the most common sustained cardiac arrhythmia, affecting 1-2% of the population. "
        "It results from chaotic, rapid atrial depolarizations (350-600/min) causing an <b>irregularly "
        "irregular ventricular response</b>. The atria quiver rather than contract.", body_style))
    afib_ecg = make_afib_ecg(w=int(W), h=90)
    story.append(afib_ecg)
    story.append(Paragraph("Figure 4. Atrial fibrillation: absent P waves, irregular baseline, "
                           "irregularly irregular QRS complexes.", caption_style))
    af_data = [
        ["Feature", "Finding"],
        ["P waves", "Absent — replaced by irregular fibrillatory baseline ('f' waves, 350-600/min)"],
        ["RR intervals", "Irregularly irregular (no two identical)"],
        ["QRS complex", "Usually narrow (<110 ms) unless aberrant conduction"],
        ["Ventricular rate", "Uncontrolled: 100-180 bpm; controlled: 60-100 bpm"],
        ["Baseline", "Undulating, coarse or fine fibrillatory activity"],
    ]
    af_style = TableStyle([
        ('BACKGROUND', (0,0), (-1,0), ECG_BLUE),
        ('TEXTCOLOR', (0,0), (-1,0), colors.white),
        ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
        ('FONTSIZE', (0,0), (-1,-1), 9),
        ('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, BOX_BLUE]),
        ('GRID', (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
        ('LEFTPADDING', (0,0), (-1,-1), 5),
        ('BOTTOMPADDING', (0,0), (-1,-1), 3),
    ])
    af = Table(af_data, colWidths=[W*0.25, W*0.75])
    af.setStyle(af_style)
    story.append(af)

    story.append(Spacer(1, 0.3*cm))

    # VT
    story.append(Paragraph("Ventricular Tachycardia (VT)", h2_style))
    story.append(Paragraph(
        "VT is a potentially life-threatening arrhythmia defined as ≥3 consecutive ventricular beats "
        "at a rate ≥100 bpm. Because it originates below the bundle of His, conduction is "
        "abnormal producing <b>wide, bizarre QRS complexes</b>. Sustained VT (>30 seconds) requires "
        "immediate treatment.", body_style))
    vt_ecg = make_vt_ecg(w=int(W), h=90)
    story.append(vt_ecg)
    story.append(Paragraph("Figure 5. Ventricular tachycardia: fast rate, wide bizarre QRS, no P waves.",
                           caption_style))
    vt_data = [
        ["Feature", "Finding"],
        ["Rate", "100-250 bpm (often 150-200 bpm)"],
        ["QRS complex", "Wide (>120 ms), bizarre morphology"],
        ["P waves", "Absent or dissociated (AV dissociation)"],
        ["Regularity", "Usually regular"],
        ["Fusion beats", "Pathognomonic of VT (capture/fusion beats)"],
        ["Concordance", "All precordial leads point same direction (positive or negative)"],
    ]
    vt = Table(vt_data, colWidths=[W*0.25, W*0.75])
    vt.setStyle(TableStyle([
        ('BACKGROUND', (0,0), (-1,0), ECG_RED),
        ('TEXTCOLOR', (0,0), (-1,0), colors.white),
        ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
        ('FONTSIZE', (0,0), (-1,-1), 9),
        ('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, BOX_RED]),
        ('GRID', (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
        ('LEFTPADDING', (0,0), (-1,-1), 5),
        ('BOTTOMPADDING', (0,0), (-1,-1), 3),
    ]))
    story.append(vt)
    story.append(Spacer(1, 0.2*cm))
    story.append(Paragraph(
        "<i>Reference: Harrison's 22E, Chapter 247; Fuster & Hurst's The Heart 15e</i>", ref_style))

    # ── Section 8: Conduction Blocks ──────────────────────────────────────
    story.append(PageBreak())
    story.append(SectionHeader("SECTION 8 — Pathology: Conduction Blocks", color=ECG_PURPLE))
    story.append(Spacer(1, 0.3*cm))

    # Complete heart block
    story.append(Paragraph("Complete (3rd Degree) Heart Block", h2_style))
    story.append(Paragraph(
        "In complete heart block, no atrial impulses are conducted to the ventricles. "
        "The atria and ventricles beat <b>independently</b>: the atrial rate is faster "
        "(60-100 bpm) while the ventricles are driven by a slow escape rhythm (30-60 bpm "
        "if junctional, 20-40 bpm if ventricular — wide QRS).", body_style))
    hb_ecg = make_heart_block_ecg(w=int(W), h=90)
    story.append(hb_ecg)
    story.append(Paragraph("Figure 6. Complete heart block: P waves (orange) march independently of "
                           "wide QRS escape beats (purple). AV dissociation is present.", caption_style))

    hb_data = [
        ["Feature", "Finding"],
        ["P waves", "Present, regular — but have NO relationship to QRS"],
        ["QRS complex", "Wide, slow escape rhythm (if ventricular origin)"],
        ["AV relationship", "Complete AV dissociation — PP ≠ RR"],
        ["Rate", "Ventricular: 20-40 bpm; Atrial: 60-100 bpm"],
        ["PR interval", "Variable — no fixed PR interval"],
    ]
    hb = Table(hb_data, colWidths=[W*0.25, W*0.75])
    hb.setStyle(TableStyle([
        ('BACKGROUND', (0,0), (-1,0), ECG_PURPLE),
        ('TEXTCOLOR', (0,0), (-1,0), colors.white),
        ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
        ('FONTSIZE', (0,0), (-1,-1), 9),
        ('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, colors.HexColor("#F5EEF8")]),
        ('GRID', (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
        ('LEFTPADDING', (0,0), (-1,-1), 5),
        ('BOTTOMPADDING', (0,0), (-1,-1), 3),
    ]))
    story.append(hb)
    story.append(Spacer(1, 0.3*cm))

    # Heart block classification
    story.append(Paragraph("AV Block Classification", h3_style))
    av_data = [
        ["Type", "PR Interval", "Dropped Beats?", "Typical Cause"],
        ["1st Degree AV block", "Prolonged >200ms", "No — all P's conduct", "Vagal tone, inferior MI, drugs"],
        ["2nd Degree Mobitz I\n(Wenckebach)", "Progressive ↑ then drop", "Yes — after longest PR", "AV node ischemia, inferior MI"],
        ["2nd Degree Mobitz II", "Fixed PR, sudden drop", "Yes — without warning", "His-Purkinje disease, anterior MI"],
        ["3rd Degree (Complete)", "No relationship", "All P's fail to conduct", "Ischemia, Lyme disease, surgery"],
    ]
    av = Table(av_data, colWidths=[W*0.25, W*0.22, W*0.22, W*0.31])
    av.setStyle(TableStyle([
        ('BACKGROUND', (0,0), (-1,0), colors.HexColor("#6C3483")),
        ('TEXTCOLOR', (0,0), (-1,0), colors.white),
        ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
        ('FONTSIZE', (0,0), (-1,-1), 8),
        ('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, colors.HexColor("#F5EEF8")]),
        ('GRID', (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
        ('LEFTPADDING', (0,0), (-1,-1), 4),
        ('BOTTOMPADDING', (0,0), (-1,-1), 3),
        ('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
    ]))
    story.append(av)
    story.append(Spacer(1, 0.3*cm))

    # LBBB
    story.append(Paragraph("Left Bundle Branch Block (LBBB)", h2_style))
    story.append(Paragraph(
        "LBBB occurs when conduction through the left bundle branch fails. "
        "Depolarization must spread slowly through myocardium rather than rapidly via Purkinje fibers, "
        "producing a <b>wide QRS >120 ms</b> with characteristic morphology. "
        "New LBBB in the context of chest pain should be treated as STEMI equivalent.", body_style))
    lbbb_ecg = make_lbbb_ecg(w=int(W), h=90)
    story.append(lbbb_ecg)
    story.append(Paragraph("Figure 7. LBBB: broad notched R waves (William's pattern), "
                           "T-wave inversion, QRS >120 ms.", caption_style))
    lbbb_data = [
        ["Lead", "LBBB Pattern"],
        ["V1, V2", "Deep, broad rS or QS pattern (dominant negativity)"],
        ["V5, V6, I, aVL", "Broad notched R wave (M pattern — 'William's' pattern)"],
        ["ST-T changes", "ST depression + T inversion in lateral leads (discordant)"],
        ["QRS duration", ">120 ms (often >140 ms)"],
        ["No Q waves", "Absence of septal q in I, V5, V6 (first sign to look for)"],
    ]
    lbbb = Table(lbbb_data, colWidths=[W*0.25, W*0.75])
    lbbb.setStyle(TableStyle([
        ('BACKGROUND', (0,0), (-1,0), ECG_GREEN),
        ('TEXTCOLOR', (0,0), (-1,0), colors.white),
        ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
        ('FONTSIZE', (0,0), (-1,-1), 9),
        ('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, BOX_GREEN]),
        ('GRID', (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
        ('LEFTPADDING', (0,0), (-1,-1), 5),
        ('BOTTOMPADDING', (0,0), (-1,-1), 3),
    ]))
    story.append(lbbb)

    # ── Section 9: WPW & Long QT ──────────────────────────────────────────
    story.append(PageBreak())
    story.append(SectionHeader("SECTION 9 — Pathology: WPW & Long QT Syndrome", color=ECG_PURPLE))
    story.append(Spacer(1, 0.3*cm))

    story.append(Paragraph("Wolff-Parkinson-White (WPW) Syndrome", h2_style))
    story.append(Paragraph(
        "WPW results from an <b>accessory pathway</b> (Bundle of Kent) that bypasses the AV node, "
        "allowing pre-excitation of the ventricle. The hallmarks are a <b>short PR interval</b> "
        "(<120 ms) and a <b>delta wave</b> (slurred upstroke of the QRS), causing a wide QRS. "
        "It predisposes to re-entrant supraventricular tachycardia and, if associated with AF, "
        "can cause dangerously fast ventricular rates.", body_style))
    wpw_ecg = make_wpw_ecg(w=int(W), h=90)
    story.append(wpw_ecg)
    story.append(Paragraph("Figure 8. WPW: short PR interval, delta wave (slurred QRS upstroke), "
                           "broad QRS complex.", caption_style))
    wpw_data = [
        ["Feature", "Finding"],
        ["PR interval", "Short (<120 ms) — pre-excitation bypasses AV node delay"],
        ["Delta wave", "Slurred initial upstroke of QRS (pre-excitation of ventricle)"],
        ["QRS duration", "Wide (>120 ms) due to abnormal ventricular activation"],
        ["ST/T changes", "Discordant — secondary to abnormal depolarization"],
        ["Risk", "AVRT (re-entry); if AF present → rapid ventricular response → VF"],
    ]
    wpw = Table(wpw_data, colWidths=[W*0.25, W*0.75])
    wpw.setStyle(TableStyle([
        ('BACKGROUND', (0,0), (-1,0), ECG_PURPLE),
        ('TEXTCOLOR', (0,0), (-1,0), colors.white),
        ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
        ('FONTSIZE', (0,0), (-1,-1), 9),
        ('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, colors.HexColor("#F5EEF8")]),
        ('GRID', (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
        ('LEFTPADDING', (0,0), (-1,-1), 5),
        ('BOTTOMPADDING', (0,0), (-1,-1), 3),
    ]))
    story.append(wpw)
    story.append(Spacer(1, 0.3*cm))

    # Long QT
    story.append(Paragraph("Long QT Syndrome", h2_style))
    story.append(Paragraph(
        "Prolongation of the QT interval reflects delayed ventricular repolarization, "
        "predisposing to <b>Torsades de Pointes (TdP)</b> — a polymorphic VT that can degenerate "
        "into ventricular fibrillation. QTc >450 ms in men and >460 ms in women is abnormal. "
        "Causes include drugs (amiodarone, antipsychotics), electrolyte abnormalities, "
        "and congenital channelopathies (Romano-Ward, Jervell-Lange-Nielsen).", body_style))
    lqt_ecg = make_long_qt_ecg(w=int(W), h=90)
    story.append(lqt_ecg)
    story.append(Paragraph("Figure 9. Long QT: prolonged QT interval before T wave; predisposes to Torsades de Pointes.",
                           caption_style))
    lqt_data = [
        ["Feature", "Finding"],
        ["QTc", ">450 ms (men) / >460 ms (women) — corrected for heart rate"],
        ["T wave", "May be broad, notched, biphasic, or late-appearing"],
        ["Risk", "Torsades de Pointes → VF → sudden cardiac death"],
        ["Drugs causing QT↑", "Amiodarone, sotalol, haloperidol, erythromycin, fluconazole"],
        ["Electrolytes", "Hypokalemia, hypomagnesaemia, hypocalcaemia all prolong QT"],
    ]
    lqt = Table(lqt_data, colWidths=[W*0.25, W*0.75])
    lqt.setStyle(TableStyle([
        ('BACKGROUND', (0,0), (-1,0), colors.HexColor("#6C3483")),
        ('TEXTCOLOR', (0,0), (-1,0), colors.white),
        ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
        ('FONTSIZE', (0,0), (-1,-1), 9),
        ('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, colors.HexColor("#F5EEF8")]),
        ('GRID', (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
        ('LEFTPADDING', (0,0), (-1,-1), 5),
        ('BOTTOMPADDING', (0,0), (-1,-1), 3),
    ]))
    story.append(lqt)
    story.append(Spacer(1, 0.2*cm))
    story.append(Paragraph(
        "<i>Reference: Harrison's 22E pp.1918-1920; Fuster & Hurst's The Heart 15e</i>", ref_style))

    # ── Section 10: Electrolyte Disturbances ──────────────────────────────
    story.append(PageBreak())
    story.append(SectionHeader("SECTION 10 — Pathology: Electrolyte Disturbances", color=ECG_ORANGE))
    story.append(Spacer(1, 0.3*cm))

    story.append(Paragraph("Hyperkalemia ECG Changes", h2_style))
    story.append(Paragraph(
        "Hyperkalemia produces a characteristic sequence of ECG changes that correlate "
        "with the serum potassium level. These changes reflect progressive depolarization "
        "of cardiac muscle membranes and are <b>potentially life-threatening</b>.", body_style))
    hk_ecg = make_hyperkalemia_ecg(w=int(W), h=90)
    story.append(hk_ecg)
    story.append(Paragraph("Figure 10. Hyperkalemia: tall peaked (tented) T waves, widened QRS, "
                           "flattened P waves.", caption_style))

    hk_data = [
        ["Serum K+", "ECG Change", "Mechanism"],
        ["5.5-6.5 mEq/L", "Tall, peaked (tented) T waves; shortened QT", "Accelerated repolarization"],
        ["6.5-7.5 mEq/L", "Prolonged PR interval; flattened P waves", "Impaired atrial conduction"],
        ["7.0-8.0 mEq/L", "Widened QRS complex; bundle branch patterns", "Impaired ventricular conduction"],
        [">8.0 mEq/L", "'Sine wave' pattern; loss of P wave", "SA arrest; ventricular standstill"],
        [">10 mEq/L", "Ventricular fibrillation → asystole", "Complete electrical failure"],
    ]
    hk = Table(hk_data, colWidths=[W*0.20, W*0.45, W*0.35])
    hk.setStyle(TableStyle([
        ('BACKGROUND', (0,0), (-1,0), ECG_ORANGE),
        ('TEXTCOLOR', (0,0), (-1,0), colors.white),
        ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
        ('FONTSIZE', (0,0), (-1,-1), 8.5),
        ('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, BOX_ORANGE]),
        ('GRID', (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
        ('LEFTPADDING', (0,0), (-1,-1), 5),
        ('BOTTOMPADDING', (0,0), (-1,-1), 3),
    ]))
    story.append(hk)
    story.append(Spacer(1, 0.3*cm))

    story.append(Paragraph("Other Electrolyte ECG Changes", h3_style))
    elec_data = [
        ["Condition", "ECG Findings"],
        ["Hypokalemia", "Prominent U waves (>T wave height), ST depression, prolonged QU interval, T flattening"],
        ["Hypercalcemia", "Shortened QT interval, short ST segment; may cause bradycardia, AV block"],
        ["Hypocalcemia", "Prolonged QT (lengthened ST segment); predisposes to TdP"],
        ["Hypomagnesemia", "Prolonged QT, U waves, T wave changes; synergistic with hypokalemia"],
        ["Hypothermia", "Osborn (J) waves at QRS-ST junction, bradycardia, prolonged intervals"],
    ]
    elec = Table(elec_data, colWidths=[W*0.25, W*0.75])
    elec.setStyle(TableStyle([
        ('BACKGROUND', (0,0), (-1,0), colors.HexColor("#784212")),
        ('TEXTCOLOR', (0,0), (-1,0), colors.white),
        ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
        ('FONTSIZE', (0,0), (-1,-1), 9),
        ('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, BOX_ORANGE]),
        ('GRID', (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
        ('LEFTPADDING', (0,0), (-1,-1), 5),
        ('BOTTOMPADDING', (0,0), (-1,-1), 3),
    ]))
    story.append(elec)
    story.append(Spacer(1, 0.3*cm))

    # ── Quick Reference Summary ───────────────────────────────────────────
    story.append(PageBreak())
    story.append(SectionHeader("QUICK REFERENCE — Pathology Summary", color=ECG_BLUE))
    story.append(Spacer(1, 0.3*cm))

    qr_data = [
        ["Pathology", "Key ECG Features", "Emergency?"],
        ["STEMI", "ST elevation ≥1mm ≥2 contiguous leads, Q waves, reciprocal depression",
         "YES — PCI within 90 min"],
        ["NSTEMI/UA", "ST depression, T inversion — no ST elevation, troponin elevated",
         "YES — urgent ACS pathway"],
        ["Atrial Fibrillation", "No P waves, irregularly irregular RR, fibrillatory baseline",
         "If rapid or haemodynamic instability"],
        ["Ventricular Tachycardia", "Wide QRS (>120ms), rate >100, AV dissociation",
         "YES — urgent cardioversion if unstable"],
        ["VF", "Chaotic, irregular, no organised QRS complexes",
         "YES — immediate defibrillation"],
        ["Complete Heart Block", "P waves unrelated to QRS, slow wide escape, AV dissociation",
         "YES — transcutaneous/transvenous pacing"],
        ["1st Degree AV Block", "PR >200ms, every P conducts",
         "No — monitor only"],
        ["LBBB (new)", "QRS >120ms, broad R in V5-V6, no septal q, new with chest pain",
         "Treat as STEMI if new"],
        ["RBBB", "rSR' in V1 ('M' pattern), wide S in I, V5-V6 ('W' pattern)",
         "No — unless new with symptoms"],
        ["WPW", "Short PR, delta wave, wide QRS",
         "Yes if tachycardia — avoid AV nodal drugs"],
        ["Long QT", "QTc >450ms (M) / >460ms (F)",
         "Yes if Torsades present"],
        ["Hyperkalemia", "Peaked T, wide QRS, flat P, sine wave",
         "YES >6.5 mEq/L — give calcium"],
        ["Pericarditis", "Diffuse saddle-shaped ST elevation, PR depression (all leads)",
         "Usually not — but exclude STEMI"],
    ]
    qr_style = TableStyle([
        ('BACKGROUND', (0,0), (-1,0), ECG_BLUE),
        ('TEXTCOLOR', (0,0), (-1,0), colors.white),
        ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
        ('FONTSIZE', (0,0), (-1,-1), 7.5),
        ('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, BOX_BLUE]),
        ('GRID', (0,0), (-1,-1), 0.4, colors.HexColor("#CCCCCC")),
        ('LEFTPADDING', (0,0), (-1,-1), 4),
        ('BOTTOMPADDING', (0,0), (-1,-1), 3),
        ('TEXTCOLOR', (2,1), (2,-1), ECG_RED),
        ('FONTNAME', (2,1), (2,-1), 'Helvetica-Bold'),
        ('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
    ])
    qr = Table(qr_data, colWidths=[W*0.22, W*0.56, W*0.22])
    qr.setStyle(qr_style)
    story.append(qr)
    story.append(Spacer(1, 0.3*cm))

    # References
    story.append(HRFlowable(width=W, thickness=0.5, color=ECG_BLUE))
    story.append(Spacer(1, 0.2*cm))
    story.append(Paragraph("<b>References</b>", h3_style))
    refs = [
        "1. Longo DL, et al. <i>Harrison's Principles of Internal Medicine, 22nd Edition</i>. McGraw-Hill, 2025. Chapter 247: Electrocardiography.",
        "2. Hall JE. <i>Guyton and Hall Textbook of Medical Physiology, 14th Edition</i>. Elsevier, 2021. Chapter 11: Fundamentals of Electrocardiography.",
        "3. Goldberger AL, et al. <i>Goldberger's Clinical Electrocardiography: A Simplified Approach, 10th ed.</i> Elsevier, 2024.",
        "4. Fuster V, et al. <i>Fuster and Hurst's The Heart, 15th Edition</i>. McGraw-Hill, 2022.",
        "5. Tintinalli JE, et al. <i>Tintinalli's Emergency Medicine: A Comprehensive Study Guide, 9th ed.</i> McGraw-Hill, 2020.",
        "6. Goldman L, Schafer AI. <i>Goldman-Cecil Medicine, 27th Edition</i>. Elsevier, 2024.",
    ]
    for r in refs:
        story.append(Paragraph(r, ref_style))

    doc.build(story)
    print(f"PDF created: {out_path}")

if __name__ == "__main__":
    out = "/home/daytona/workspace/ecg-pdf/ECG_Basics_and_Pathology.pdf"
    build_pdf(out)
Running Command

cd /home/daytona/workspace/ecg-pdf && python generate_ecg_pdf.py 2>&1

Running Command

ls -lh /home/daytona/workspace/ecg-pdf/ECG_Basics_and_Pathology.pdf

Running Command

python3 -c "from pypdf import PdfReader; r = PdfReader('/home/daytona/workspace/ecg-pdf/ECG_Basics_and_Pathology.pdf'); print(f'Pages: {len(r.pages)}')"

Your PDF is ready! Download it here:

ECG Basics and Pathology

PDF Document · PDF


Here's what's inside the 15-page PDF:

Structure & Content

SectionContent
CoverTitle banner + live ECG trace + table of contents
Section 1Fundamentals of ECG — what it measures, the cardiac conduction system (SA node → AV node → His-Purkinje), conduction velocities table
Section 2ECG Waveforms & Intervals — fully labeled diagram (P, QRS, T, U, PR/QRS/QT intervals) with normal values table
Section 312-Lead System — lead groups, territories, normal sinus rhythm tracing
Section 4Cardiac Axis — color-coded frontal plane diagram, LAD/RAD/normal zones, quick identification method
Section 5Systematic 8-Step ECG Interpretation method + heart rate calculation table
Section 6STEMI — tombstone ST elevation ECG, identification steps, territory localization table (Anterior/Inferior/Lateral/Posterior/RV)
Section 7Arrhythmias — AF (fibrillatory baseline, no P waves) + VT (wide fast bizarre QRS) with labeled diagrams
Section 8Conduction Blocks — Complete heart block (AV dissociation), AV block classification table, LBBB (William's pattern)
Section 9WPW (delta wave, short PR) + Long QT (TdP risk, drug causes)
Section 10Electrolyte disturbances — hyperkalemia sequence, hypo/hypercalcemia, hypothermia J waves
Quick RefFull pathology summary table with 13 diagnoses, key ECG features, and emergency flags

References Used

  • Harrison's Principles of Internal Medicine 22E (2025)
  • Guyton and Hall Textbook of Medical Physiology
  • Goldman-Cecil Medicine 27E
  • Fuster & Hurst's The Heart 15E
  • Tintinalli's Emergency Medicine
This is a shared conversation. Sign in to Orris to start your own chat.