Explain me 10 common heart diseases with ECG changes and ECG images of those diseases

Reading File
Finding Sources
Searching Images

myocardial infarction STEMI ECG ST elevation

Searching Images

atrial fibrillation ECG electrocardiogram

Finding Sources
Searching Images

heart block complete AV block third degree ECG

Searching Images

ventricular tachycardia ECG wide QRS tachycardia

Searching Images

hypertrophic cardiomyopathy ECG left ventricular hypertrophy

Searching Images

pulmonary embolism ECG S1Q3T3 right heart strain

Searching Images

pericarditis ECG saddle shaped ST elevation diffuse

Searching Images

Wolff-Parkinson-White syndrome WPW ECG delta wave pre-excitation

Searching Images

heart failure dilated cardiomyopathy ECG left bundle branch block

Searching Images

long QT syndrome QTc prolonged ECG torsades de pointes

Here's a complete breakdown of all 10 conditions with their ECG images and key findings:

Coverage summary:
  1. STEMI — ST elevation, Q-waves, reciprocal changes; localized by lead territory to identify culprit artery
  2. Atrial Fibrillation — Absent P-waves, irregularly irregular rhythm; the most common sustained arrhythmia
  3. Complete Heart Block — AV dissociation; P-waves and QRS march independently; requires pacemaker
  4. Ventricular Tachycardia — Wide-complex tachycardia with AV dissociation; fusion/capture beats are diagnostic
  5. Hypertrophic Cardiomyopathy — Giant T-wave inversions (especially apical HCM) + LVH voltage criteria
  6. Pulmonary Embolism — Sinus tachycardia + S1Q3T3 + RBBB pattern; reflects acute right heart strain
  7. Acute Pericarditis — Diffuse saddle-shaped ST elevation + PR depression; distinguished from STEMI by diffuse distribution and concave morphology
  8. WPW Syndrome — Short PR + delta wave + wide QRS; risk of sudden death with rapid AF conduction
  9. Dilated Cardiomyopathy — LBBB pattern dominates; basis for cardiac resynchronization therapy (CRT)
  10. Long QT Syndrome — Prolonged QTc ≥500 ms is high risk; T-wave morphology differs by genetic subtype; can precipitate fatal Torsades de Pointes

Create a detailed ECG interpretation guide as a PDF handout

Reading File
Reading File
Reading File
Running Command

ls -la /home/daytona/workspace/ 2>/dev/null || echo "workspace empty or missing"

Running Command

mkdir -p /home/daytona/workspace/ecg-guide && echo "done"

Writing File

~/ecg-guide/generate_ecg_guide.py

#!/usr/bin/env python3
"""
ECG Interpretation Guide - Comprehensive PDF Handout
Generated using ReportLab
"""

import json
import subprocess
import base64
import os
from io import BytesIO

from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import mm, cm
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_JUSTIFY
from reportlab.platypus import (
    SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
    PageBreak, HRFlowable, KeepTogether, Image
)
from reportlab.platypus.flowables import Flowable
from reportlab.graphics.shapes import Drawing, Rect, Line, String, Circle, Polygon
from reportlab.graphics import renderPDF
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont

# ── Colour palette ──────────────────────────────────────────────────────────
DARK_BLUE   = colors.HexColor("#0D2B45")
MED_BLUE    = colors.HexColor("#1A5276")
ACCENT_BLUE = colors.HexColor("#2E86C1")
LIGHT_BLUE  = colors.HexColor("#D6EAF8")
RED         = colors.HexColor("#C0392B")
ORANGE      = colors.HexColor("#E67E22")
GREEN       = colors.HexColor("#1E8449")
YELLOW_BG   = colors.HexColor("#FEF9E7")
LIGHT_GREY  = colors.HexColor("#F2F3F4")
MID_GREY    = colors.HexColor("#717D7E")
WHITE       = colors.white
BLACK       = colors.black

W, H = A4  # 595 x 842 pts

OUTPUT = "/home/daytona/workspace/ecg-guide/ECG_Interpretation_Guide.pdf"

# ── ECG waveform drawing ─────────────────────────────────────────────────────
class ECGWaveform(Flowable):
    """Draws a stylised ECG trace as a vector graphic."""
    def __init__(self, width=500, height=60, label="Normal Sinus Rhythm",
                 color=ACCENT_BLUE):
        super().__init__()
        self.width  = width
        self.height = height
        self.label  = label
        self.color  = color

    def draw(self):
        c = self.canv
        w, h = self.width, self.height
        mid = h / 2

        # Grid lines
        c.setStrokeColor(colors.HexColor("#F5B7B1"))
        c.setLineWidth(0.3)
        for i in range(0, int(w), 20):
            c.line(i, 0, i, h)
        for j in range(0, int(h), 10):
            c.line(0, j, w, j)

        # ECG path
        c.setStrokeColor(self.color)
        c.setLineWidth(1.5)
        p = c.beginPath()

        # Baseline
        p.moveTo(0, mid)
        p.lineTo(30, mid)
        # P wave
        p.curveTo(35, mid, 38, mid+10, 45, mid+10)
        p.curveTo(52, mid+10, 55, mid, 60, mid)
        # Baseline
        p.lineTo(70, mid)
        # Q dip
        p.lineTo(74, mid - 6)
        # R spike
        p.lineTo(78, mid + 35)
        # S dip
        p.lineTo(82, mid - 8)
        # Return
        p.lineTo(86, mid)
        # ST segment
        p.lineTo(100, mid)
        # T wave
        p.curveTo(108, mid, 112, mid+18, 118, mid+18)
        p.curveTo(124, mid+18, 128, mid, 135, mid)
        # Baseline
        p.lineTo(180, mid)
        # Second beat (smaller)
        p.lineTo(184, mid - 3)
        p.lineTo(188, mid + 25)
        p.lineTo(192, mid - 5)
        p.lineTo(196, mid)
        p.lineTo(204, mid)
        p.curveTo(210, mid, 214, mid+13, 218, mid+13)
        p.curveTo(224, mid+13, 227, mid, 232, mid)
        p.lineTo(w, mid)

        c.drawPath(p, stroke=1, fill=0)

        # Label
        c.setFont("Helvetica-Bold", 7)
        c.setFillColor(MID_GREY)
        c.drawString(4, 3, self.label)


# ── Reusable section header ───────────────────────────────────────────────────
class SectionBanner(Flowable):
    def __init__(self, number, title, subtitle="", bg=MED_BLUE, width=None):
        super().__init__()
        self.number   = number
        self.title    = title
        self.subtitle = subtitle
        self.bg       = bg
        self.width    = width or (W - 30*mm)
        self.height   = 26 if subtitle else 22

    def draw(self):
        c = self.canv
        w, h = self.width, self.height
        # Background
        c.setFillColor(self.bg)
        c.roundRect(0, 0, w, h, 4, stroke=0, fill=1)
        # Number circle
        c.setFillColor(WHITE)
        c.circle(12, h/2, 9, stroke=0, fill=1)
        c.setFillColor(self.bg)
        c.setFont("Helvetica-Bold", 9)
        c.drawCentredString(12, h/2 - 3.5, str(self.number))
        # Title
        c.setFillColor(WHITE)
        c.setFont("Helvetica-Bold", 11)
        c.drawString(28, h/2 + (3 if self.subtitle else -4), self.title)
        if self.subtitle:
            c.setFont("Helvetica", 7)
            c.setFillColor(colors.HexColor("#AED6F1"))
            c.drawString(28, h/2 - 7, self.subtitle)


# ── Callout box ───────────────────────────────────────────────────────────────
class CalloutBox(Flowable):
    def __init__(self, text, bg=LIGHT_BLUE, border=ACCENT_BLUE,
                 icon="!", width=None, height=None):
        super().__init__()
        self.text   = text
        self.bg     = bg
        self.border = border
        self.icon   = icon
        self._w     = width  or (W - 30*mm)
        self._h     = height or 32

    def wrap(self, availW, availH):
        return self._w, self._h

    def draw(self):
        c = self.canv
        w, h = self._w, self._h
        c.setFillColor(self.bg)
        c.roundRect(0, 0, w, h, 4, stroke=0, fill=1)
        c.setStrokeColor(self.border)
        c.setLineWidth(2)
        c.line(0, 0, 0, h)
        c.setFillColor(self.border)
        c.setFont("Helvetica-Bold", 9)
        c.drawString(8, h/2 - 4, self.icon)
        c.setFillColor(DARK_BLUE)
        c.setFont("Helvetica", 8)
        # Wrap text manually
        words = self.text.split()
        lines, line = [], []
        max_w = w - 30
        for word in words:
            test = " ".join(line + [word])
            if c.stringWidth(test, "Helvetica", 8) < max_w:
                line.append(word)
            else:
                lines.append(" ".join(line))
                line = [word]
        if line:
            lines.append(" ".join(line))
        total = len(lines) * 10
        y_start = h/2 + total/2 - 5
        for i, ln in enumerate(lines):
            c.drawString(22, y_start - i*10, ln)


# ── Helper: download ECG images ──────────────────────────────────────────────
ECG_IMAGES = {
    "stemi":     "https://cdn.orris.care/cdss_images/pmc_clinical_VQA_0f5964997b5f333f6a7b13ecdc70a73ef6e81127eaee03ef38d9c724b26bbf31.jpg",
    "afib":      "https://cdn.orris.care/cdss_images/pmc_clinical_VQA_55b3585ba004b0ca69f159c22eb5fbb4d8995fc007e77627c0d37922e4d88973.jpg",
    "avblock":   "https://cdn.orris.care/cdss_images/pmc_clinical_VQA_f7f184aa3dd18a90e08320307c4bd48435e91df0d5a1b07166dff3c95e739778.jpg",
    "vt":        "https://cdn.orris.care/cdss_images/pmc_clinical_VQA_d15a92de4e89b46f273b22d254d1e30fcd6a190c6d581b193e6dd338338c6bb6.jpg",
    "hcm":       "https://cdn.orris.care/cdss_images/pmc_clinical_VQA_f9136e8a77fe104cfbe3b8d6deb3a988a0925e95c5f6acba5afb957453257c47.jpg",
    "pe":        "https://cdn.orris.care/cdss_images/pmc_clinical_VQA_5f391caa5a24ec425e9f48d4d9f73620a3b91f98df1f92cea400318772fda12e.jpg",
    "pericarditis": "https://cdn.orris.care/cdss_images/pmc_clinical_VQA_cd4008531c1b1a2a99ab6d9b4282dcdcedfcf77c302b6ecbb7f7e01c5c985cf9.jpg",
    "wpw":       "https://cdn.orris.care/cdss_images/pmc_clinical_VQA_53c53d9ed7100de15d6d512e52e2a269252a79c85c12a60e54a4ce2fc4d574ec.jpg",
    "dcm":       "https://cdn.orris.care/cdss_images/pmc_clinical_VQA_14df779e8d0d5e091053384c92cf19022571ff72ddce618daa86c349739ad451.jpg",
    "lqts":      "https://cdn.orris.care/cdss_images/pmc_clinical_VQA_e4cf687e3417d7c536bebc83c4c367527e3ec6bc5ab1f314f252ca25c3be49b8.jpg",
}

def fetch_images(urls):
    """Download images via shared fetch_images.js helper, return {url: path}."""
    img_dir = "/home/daytona/workspace/ecg-guide/images"
    os.makedirs(img_dir, exist_ok=True)
    result_map = {}
    url_list = list(urls.values())
    try:
        raw = subprocess.check_output(
            ["node", "/home/daytona/skills/shared/scripts/fetch_images.js"] + url_list,
            timeout=60
        )
        data = json.loads(raw)
        for key, url in urls.items():
            for item in data:
                if item.get("url") == url and item.get("base64"):
                    b64 = item["base64"]
                    if "," in b64:
                        b64 = b64.split(",", 1)[1]
                    img_bytes = base64.b64decode(b64)
                    path = os.path.join(img_dir, f"{key}.jpg")
                    with open(path, "wb") as f:
                        f.write(img_bytes)
                    result_map[key] = path
    except Exception as e:
        print(f"Image fetch error: {e}")
    return result_map


# ── Styles ────────────────────────────────────────────────────────────────────
def make_styles():
    base = getSampleStyleSheet()

    styles = {
        "cover_title": ParagraphStyle(
            "cover_title", fontName="Helvetica-Bold", fontSize=28,
            textColor=WHITE, alignment=TA_CENTER, leading=34, spaceAfter=6
        ),
        "cover_sub": ParagraphStyle(
            "cover_sub", fontName="Helvetica", fontSize=13,
            textColor=colors.HexColor("#AED6F1"), alignment=TA_CENTER, leading=18
        ),
        "section_title": ParagraphStyle(
            "section_title", fontName="Helvetica-Bold", fontSize=13,
            textColor=DARK_BLUE, spaceBefore=12, spaceAfter=4
        ),
        "h2": ParagraphStyle(
            "h2", fontName="Helvetica-Bold", fontSize=10,
            textColor=MED_BLUE, spaceBefore=8, spaceAfter=3
        ),
        "body": ParagraphStyle(
            "body", fontName="Helvetica", fontSize=8.5,
            textColor=BLACK, leading=13, spaceAfter=4, alignment=TA_JUSTIFY
        ),
        "body_bold": ParagraphStyle(
            "body_bold", fontName="Helvetica-Bold", fontSize=8.5,
            textColor=DARK_BLUE, leading=13
        ),
        "bullet": ParagraphStyle(
            "bullet", fontName="Helvetica", fontSize=8.5,
            textColor=BLACK, leading=13, leftIndent=12,
            firstLineIndent=-10, spaceAfter=2
        ),
        "caption": ParagraphStyle(
            "caption", fontName="Helvetica-Oblique", fontSize=7,
            textColor=MID_GREY, alignment=TA_CENTER, spaceAfter=6
        ),
        "table_header": ParagraphStyle(
            "table_header", fontName="Helvetica-Bold", fontSize=8,
            textColor=WHITE, alignment=TA_CENTER
        ),
        "table_cell": ParagraphStyle(
            "table_cell", fontName="Helvetica", fontSize=7.5,
            textColor=BLACK, leading=11
        ),
        "table_cell_bold": ParagraphStyle(
            "table_cell_bold", fontName="Helvetica-Bold", fontSize=7.5,
            textColor=DARK_BLUE, leading=11
        ),
        "normal": base["Normal"],
        "small": ParagraphStyle(
            "small", fontName="Helvetica", fontSize=7,
            textColor=MID_GREY, leading=10
        ),
        "footer": ParagraphStyle(
            "footer", fontName="Helvetica", fontSize=7,
            textColor=MID_GREY, alignment=TA_CENTER
        ),
        "toc_entry": ParagraphStyle(
            "toc_entry", fontName="Helvetica", fontSize=9,
            textColor=DARK_BLUE, leading=16, leftIndent=8
        ),
        "toc_number": ParagraphStyle(
            "toc_number", fontName="Helvetica-Bold", fontSize=9,
            textColor=ACCENT_BLUE, leading=16
        ),
        "red_label": ParagraphStyle(
            "red_label", fontName="Helvetica-Bold", fontSize=8,
            textColor=RED
        ),
        "green_label": ParagraphStyle(
            "green_label", fontName="Helvetica-Bold", fontSize=8,
            textColor=GREEN
        ),
    }
    return styles


# ── Page template with header/footer ─────────────────────────────────────────
def on_page(canvas, doc):
    canvas.saveState()
    w, h = A4

    # Header bar (skip cover page = page 1)
    if doc.page > 1:
        canvas.setFillColor(DARK_BLUE)
        canvas.rect(0, h - 18*mm, w, 18*mm, stroke=0, fill=1)
        canvas.setFillColor(WHITE)
        canvas.setFont("Helvetica-Bold", 9)
        canvas.drawString(15*mm, h - 11*mm, "ECG INTERPRETATION GUIDE")
        canvas.setFont("Helvetica", 8)
        canvas.drawRightString(w - 15*mm, h - 11*mm, f"Page {doc.page}")

        # Footer
        canvas.setStrokeColor(LIGHT_BLUE)
        canvas.setLineWidth(0.5)
        canvas.line(15*mm, 12*mm, w - 15*mm, 12*mm)
        canvas.setFillColor(MID_GREY)
        canvas.setFont("Helvetica", 7)
        canvas.drawString(15*mm, 8*mm,
            "For educational use only — always correlate with clinical context.")
        canvas.drawRightString(w - 15*mm, 8*mm,
            "ECG Guide | Orris Medical Education")

    canvas.restoreState()


# ── Cover page ────────────────────────────────────────────────────────────────
def build_cover(styles):
    story = []

    # Full-page dark background via a big coloured table
    cover_data = [[""]]
    cover_table = Table(cover_data, colWidths=[W - 30*mm], rowHeights=[H - 30*mm])
    cover_table.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, -1), DARK_BLUE),
        ("ROUNDEDCORNERS", [8]),
        ("BOX", (0, 0), (-1, -1), 0, DARK_BLUE),
    ]))

    # We'll build the cover as a sequence of centred elements
    story.append(Spacer(1, 38*mm))

    # Top ECG waveform decoration
    story.append(ECGWaveform(width=W - 60*mm, height=55, label="", color=ACCENT_BLUE))
    story.append(Spacer(1, 8*mm))

    # Title box
    title_data = [[
        Paragraph("ECG", styles["cover_title"]),
    ], [
        Paragraph("INTERPRETATION GUIDE", styles["cover_title"]),
    ], [
        Spacer(1, 4),
    ], [
        Paragraph("A Systematic Approach to 12-Lead ECG Analysis", styles["cover_sub"]),
    ], [
        Paragraph("Including 10 Common Cardiac Conditions with Diagnostic Criteria",
                  styles["cover_sub"]),
    ]]
    t = Table(title_data, colWidths=[W - 60*mm])
    t.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, -1), DARK_BLUE),
        ("ALIGN", (0, 0), (-1, -1), "CENTER"),
        ("TOPPADDING", (0, 0), (-1, -1), 4),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 4),
    ]))
    story.append(t)
    story.append(Spacer(1, 10*mm))

    # Divider
    story.append(HRFlowable(width=120*mm, thickness=1.5,
                              color=ACCENT_BLUE, spaceBefore=2, spaceAfter=8))

    # Badges row
    badge_data = [[
        Paragraph('<font color="#AED6F1"><b>  Rhythm Analysis  </b></font>', styles["small"]),
        Paragraph('<font color="#AED6F1"><b>  Intervals &amp; Axes  </b></font>', styles["small"]),
        Paragraph('<font color="#AED6F1"><b>  10 Disease Patterns  </b></font>', styles["small"]),
        Paragraph('<font color="#AED6F1"><b>  Quick Reference  </b></font>', styles["small"]),
    ]]
    bt = Table(badge_data, colWidths=[(W - 30*mm)/4]*4)
    bt.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, -1), MED_BLUE),
        ("ALIGN", (0, 0), (-1, -1), "CENTER"),
        ("ROUNDEDCORNERS", [4]),
        ("TOPPADDING", (0, 0), (-1, -1), 6),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 6),
    ]))
    story.append(bt)

    story.append(Spacer(1, 20*mm))
    story.append(Paragraph(
        '<font color="#5D6D7E">Orris Medical Education  |  2026 Edition</font>',
        styles["cover_sub"]
    ))
    story.append(PageBreak())
    return story


# ── Table of Contents ─────────────────────────────────────────────────────────
def build_toc(styles):
    story = []
    story.append(Spacer(1, 5*mm))
    story.append(Paragraph("Table of Contents", styles["section_title"]))
    story.append(HRFlowable(width="100%", thickness=1, color=ACCENT_BLUE,
                             spaceBefore=2, spaceAfter=6))

    sections = [
        ("1", "ECG Basics — Paper, Speed & Calibration"),
        ("2", "Systematic 8-Step Approach"),
        ("3", "Normal Intervals & Axes Reference"),
        ("4", "P-Wave Analysis"),
        ("5", "PR Interval & AV Conduction"),
        ("6", "QRS Complex Analysis"),
        ("7", "ST Segment & T-Wave Changes"),
        ("8", "QT Interval & Repolarisation"),
        ("9", "10 Common Cardiac Conditions"),
        ("  9.1", "ST-Elevation Myocardial Infarction (STEMI)"),
        ("  9.2", "Atrial Fibrillation"),
        ("  9.3", "Complete Heart Block"),
        ("  9.4", "Ventricular Tachycardia"),
        ("  9.5", "Hypertrophic Cardiomyopathy"),
        ("  9.6", "Pulmonary Embolism"),
        ("  9.7", "Acute Pericarditis"),
        ("  9.8", "WPW Syndrome"),
        ("  9.9", "Dilated Cardiomyopathy"),
        ("  9.10", "Long QT Syndrome"),
        ("10", "Quick Reference Summary Table"),
        ("11", "Lead Localisation of MI"),
    ]

    toc_data = []
    for num, title in sections:
        toc_data.append([
            Paragraph(f"<b>{num}</b>", styles["toc_number"]),
            Paragraph(title, styles["toc_entry"]),
            Paragraph("· " * 18, styles["small"]),
        ])

    toc_table = Table(toc_data, colWidths=[12*mm, 100*mm, None])
    toc_table.setStyle(TableStyle([
        ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
        ("TOPPADDING", (0, 0), (-1, -1), 2),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 2),
        ("ROWBACKGROUNDS", (0, 0), (-1, -1), [WHITE, LIGHT_GREY]),
        ("LEFTPADDING", (0, 0), (-1, -1), 4),
    ]))
    story.append(toc_table)
    story.append(PageBreak())
    return story


# ── Section 1: ECG Basics ─────────────────────────────────────────────────────
def build_basics(styles):
    story = []
    story.append(Spacer(1, 5*mm))

    story.append(SectionBanner(1, "ECG BASICS",
                                "Paper Speed, Calibration & Grid"))
    story.append(Spacer(1, 4))

    story.append(ECGWaveform(width=W - 30*mm, height=65,
                              label="Standard 12-lead ECG strip — 25 mm/s",
                              color=RED))
    story.append(Spacer(1, 3))

    # Grid explanation table
    grid_data = [
        [Paragraph("<b>Grid</b>", styles["table_header"]),
         Paragraph("<b>Size</b>", styles["table_header"]),
         Paragraph("<b>Time</b>", styles["table_header"]),
         Paragraph("<b>Voltage</b>", styles["table_header"])],
        [Paragraph("Small square", styles["table_cell"]),
         Paragraph("1 mm × 1 mm", styles["table_cell"]),
         Paragraph("0.04 s (40 ms)", styles["table_cell"]),
         Paragraph("0.1 mV", styles["table_cell"])],
        [Paragraph("Large square", styles["table_cell"]),
         Paragraph("5 mm × 5 mm", styles["table_cell"]),
         Paragraph("0.20 s (200 ms)", styles["table_cell"]),
         Paragraph("0.5 mV", styles["table_cell"])],
        [Paragraph("Standard speed", styles["table_cell"]),
         Paragraph("25 mm/s", styles["table_cell"]),
         Paragraph("1 large sq = 0.2 s", styles["table_cell"]),
         Paragraph("Standard gain = 10 mm/mV", styles["table_cell"])],
    ]
    gt = Table(grid_data, colWidths=[45*mm, 40*mm, 50*mm, 50*mm])
    gt.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, 0), MED_BLUE),
        ("ROWBACKGROUNDS", (0, 1), (-1, -1), [LIGHT_GREY, WHITE]),
        ("GRID", (0, 0), (-1, -1), 0.4, colors.HexColor("#BFC9CA")),
        ("ALIGN", (0, 0), (-1, -1), "CENTER"),
        ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
        ("TOPPADDING", (0, 0), (-1, -1), 5),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 5),
        ("FONTNAME", (0, 1), (-1, -1), "Helvetica"),
        ("FONTSIZE", (0, 1), (-1, -1), 8),
    ]))
    story.append(gt)
    story.append(Spacer(1, 4))

    story.append(Paragraph("Heart Rate Calculation", styles["h2"]))
    hr_data = [
        [Paragraph("<b>Method</b>", styles["table_header"]),
         Paragraph("<b>Formula</b>", styles["table_header"]),
         Paragraph("<b>Best For</b>", styles["table_header"])],
        [Paragraph("300 Rule", styles["table_cell"]),
         Paragraph("300 ÷ (number of large squares between R-R)", styles["table_cell"]),
         Paragraph("Regular rhythms — quick mental estimate", styles["table_cell"])],
        [Paragraph("1500 Rule", styles["table_cell"]),
         Paragraph("1500 ÷ (number of small squares between R-R)", styles["table_cell"]),
         Paragraph("Regular rhythms — more precise", styles["table_cell"])],
        [Paragraph("6-Second Method", styles["table_cell"]),
         Paragraph("Count QRS complexes in 6 s strip × 10", styles["table_cell"]),
         Paragraph("Irregular rhythms (AF, etc.)", styles["table_cell"])],
    ]
    ht = Table(hr_data, colWidths=[35*mm, 80*mm, 70*mm])
    ht.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, 0), ACCENT_BLUE),
        ("ROWBACKGROUNDS", (0, 1), (-1, -1), [LIGHT_GREY, WHITE]),
        ("GRID", (0, 0), (-1, -1), 0.4, colors.HexColor("#BFC9CA")),
        ("ALIGN", (0, 0), (-1, 0), "CENTER"),
        ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
        ("TOPPADDING", (0, 0), (-1, -1), 5),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 5),
    ]))
    story.append(ht)
    story.append(Spacer(1, 4))

    # 12-lead layout
    story.append(Paragraph("12-Lead Layout & Views", styles["h2"]))
    lead_data = [
        [Paragraph("<b>Lead Group</b>", styles["table_header"]),
         Paragraph("<b>Leads</b>", styles["table_header"]),
         Paragraph("<b>Cardiac Region Viewed</b>", styles["table_header"])],
        [Paragraph("Inferior", styles["table_cell"]),
         Paragraph("II, III, aVF", styles["table_cell"]),
         Paragraph("Inferior wall — RCA territory", styles["table_cell"])],
        [Paragraph("Lateral", styles["table_cell"]),
         Paragraph("I, aVL, V5, V6", styles["table_cell"]),
         Paragraph("Lateral wall — LCx territory", styles["table_cell"])],
        [Paragraph("Anterior", styles["table_cell"]),
         Paragraph("V1, V2, V3, V4", styles["table_cell"]),
         Paragraph("Anterior wall — LAD territory", styles["table_cell"])],
        [Paragraph("Septal", styles["table_cell"]),
         Paragraph("V1, V2", styles["table_cell"]),
         Paragraph("Interventricular septum — proximal LAD", styles["table_cell"])],
        [Paragraph("aVR", styles["table_cell"]),
         Paragraph("aVR", styles["table_cell"]),
         Paragraph("Cavity; reciprocal to inferolateral leads", styles["table_cell"])],
    ]
    lt = Table(lead_data, colWidths=[35*mm, 40*mm, 110*mm])
    lt.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, 0), GREEN),
        ("ROWBACKGROUNDS", (0, 1), (-1, -1), [LIGHT_GREY, WHITE]),
        ("GRID", (0, 0), (-1, -1), 0.4, colors.HexColor("#BFC9CA")),
        ("ALIGN", (0, 0), (-1, 0), "CENTER"),
        ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
        ("TOPPADDING", (0, 0), (-1, -1), 5),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 5),
    ]))
    story.append(lt)
    story.append(PageBreak())
    return story


# ── Section 2: Systematic approach ───────────────────────────────────────────
def build_systematic(styles):
    story = []
    story.append(Spacer(1, 5*mm))
    story.append(SectionBanner(2, "SYSTEMATIC 8-STEP APPROACH",
                                "Never miss a finding — always follow this order"))
    story.append(Spacer(1, 5))

    steps = [
        ("1", "Rate", RED,
         "Calculate ventricular rate. Normal: 60–100 bpm. "
         "Bradycardia: <60. Tachycardia: >100. "
         "Use 300 rule for regular, 6-second strip for irregular rhythms."),
        ("2", "Rhythm", ORANGE,
         "Is the rhythm regular or irregular? Identify if P-waves precede every QRS. "
         "Irregularly irregular → think AF. Regularly irregular → think Mobitz II or "
         "2nd-degree block. Regular with wide QRS → VT vs SVT with aberrancy."),
        ("3", "P-wave", colors.HexColor("#8E44AD"),
         "Present? Upright in I and II (sinus)? One P per QRS? Peaked (>2.5 mm in II) = "
         "P pulmonale (RA enlargement). Notched/bifid in II = P mitrale (LA enlargement). "
         "Absent P = AF or junctional rhythm."),
        ("4", "PR Interval", MED_BLUE,
         "Normal: 120–200 ms (3–5 small squares). "
         "Short PR (<120 ms) = pre-excitation (WPW) or junctional rhythm. "
         "Long PR (>200 ms) = 1st-degree AV block. Progressive lengthening = Mobitz I. "
         "Constant long PR with dropped beats = Mobitz II."),
        ("5", "QRS Width", ACCENT_BLUE,
         "Normal: <120 ms (3 small squares). "
         "Wide QRS (>120 ms): LBBB, RBBB, ventricular rhythm, WPW, hyperkalaemia, "
         "sodium channel blockade. Narrow QRS = supraventricular origin."),
        ("6", "Axis", GREEN,
         "Normal axis: −30° to +90°. "
         "LAD (−30° to −90°): LBBB, inferior MI, LAFB, WPW. "
         "RAD (+90° to +180°): RBBB, RVH, PE, lateral MI, WPW. "
         "Quick check: Lead I + aVF both positive = normal."),
        ("7", "ST Segment & T-waves", colors.HexColor("#D35400"),
         "ST elevation: STEMI (convex, localised), pericarditis (concave, diffuse), "
         "Brugada, early repolarisation. "
         "ST depression: ischaemia, NSTEMI, digoxin effect, LVH strain. "
         "T-wave inversions: ischaemia, PE (V1–V4), HCM, PE, Wellens."),
        ("8", "QT Interval", RED,
         "Measured from start of QRS to end of T-wave. "
         "Correct with Bazett formula: QTc = QT / √RR. "
         "Normal QTc: <440 ms (men), <460 ms (women). "
         ">500 ms = high risk for Torsades de Pointes."),
    ]

    for num, title, col, desc in steps:
        row_data = [[
            Paragraph(f"<b>{num}</b>", ParagraphStyle(
                "stepnum", fontName="Helvetica-Bold", fontSize=14,
                textColor=WHITE, alignment=TA_CENTER)),
            Paragraph(f"<b>{title}</b>", ParagraphStyle(
                "steptitle", fontName="Helvetica-Bold", fontSize=10,
                textColor=col, spaceAfter=3)),
            Paragraph(desc, styles["body"]),
        ]]
        st = Table(row_data, colWidths=[12*mm, 30*mm, None])
        st.setStyle(TableStyle([
            ("BACKGROUND", (0, 0), (0, 0), col),
            ("BACKGROUND", (1, 0), (-1, 0), LIGHT_GREY),
            ("VALIGN", (0, 0), (-1, -1), "TOP"),
            ("TOPPADDING", (0, 0), (-1, -1), 7),
            ("BOTTOMPADDING", (0, 0), (-1, -1), 7),
            ("LEFTPADDING", (0, 0), (0, 0), 3),
            ("LEFTPADDING", (1, 0), (-1, -1), 6),
            ("BOX", (0, 0), (-1, -1), 0.5, colors.HexColor("#BFC9CA")),
        ]))
        story.append(st)
        story.append(Spacer(1, 2))

    story.append(PageBreak())
    return story


# ── Section 3: Normal values ──────────────────────────────────────────────────
def build_normal_values(styles):
    story = []
    story.append(Spacer(1, 5*mm))
    story.append(SectionBanner(3, "NORMAL INTERVALS & AXES REFERENCE",
                                "Quick reference for normal ECG parameters", bg=GREEN))
    story.append(Spacer(1, 5))

    vals = [
        ["Parameter", "Normal Range", "Significance of Abnormality"],
        ["PR Interval", "120–200 ms", "Short: WPW / junctional   Long: AV block"],
        ["QRS Duration", "< 120 ms", "Wide: BBB, VT, hyperkalaemia, drugs"],
        ["QT Interval (QTc)", "< 440 ms (M)  < 460 ms (F)", "Prolonged: LQTS, drugs, electrolytes"],
        ["QTc HIGH RISK", "> 500 ms", "Torsades de Pointes risk"],
        ["Frontal Axis", "−30° to +90°", "LAD / RAD: see axis section"],
        ["P-wave duration", "< 120 ms", "Wide P: LA enlargement"],
        ["P-wave amplitude", "< 2.5 mm in II", "Tall P: RA enlargement (P pulmonale)"],
        ["R-wave in V5/V6", "< 25 mm", "Tall R: LVH (Sokolow ≥35 mm S-V1+R-V5)"],
        ["ST elevation (limb)", "< 1 mm", "≥1 mm in ≥2 contiguous leads = STEMI"],
        ["ST elevation (chest)", "< 2 mm", "≥2 mm in ≥2 contiguous leads = STEMI"],
    ]

    nv_table = Table(vals, colWidths=[45*mm, 55*mm, None])
    nv_table.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, 0), GREEN),
        ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
        ("FONTSIZE", (0, 0), (-1, 0), 8),
        ("TEXTCOLOR", (0, 0), (-1, 0), WHITE),
        ("ROWBACKGROUNDS", (0, 1), (-1, -1), [WHITE, LIGHT_GREY]),
        ("FONTNAME", (0, 1), (-1, -1), "Helvetica"),
        ("FONTSIZE", (0, 1), (-1, -1), 8),
        ("BACKGROUND", (0, 4), (-1, 4), colors.HexColor("#FADBD8")),
        ("FONTNAME", (0, 4), (-1, 4), "Helvetica-Bold"),
        ("TEXTCOLOR", (0, 4), (-1, 4), RED),
        ("GRID", (0, 0), (-1, -1), 0.4, colors.HexColor("#BFC9CA")),
        ("ALIGN", (0, 0), (-1, -1), "LEFT"),
        ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
        ("TOPPADDING", (0, 0), (-1, -1), 5),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 5),
        ("LEFTPADDING", (0, 0), (-1, -1), 6),
    ]))
    story.append(nv_table)
    story.append(PageBreak())
    return story


# ── Disease disease_card helper ───────────────────────────────────────────────
def disease_card(number, name, subtitle, overview, ecg_changes,
                 key_point, img_path, img_caption, styles, color=MED_BLUE):
    story = []
    story.append(Spacer(1, 4*mm))
    story.append(SectionBanner(number, name.upper(), subtitle, bg=color))
    story.append(Spacer(1, 4))

    # Overview + image side by side
    overview_para = Paragraph(f"<b>Overview:</b> {overview}", styles["body"])

    ecg_items = []
    for item in ecg_changes:
        if item.startswith("**"):
            # bold header item
            bold, rest = item[2:].split("**", 1) if "**" in item[2:] else (item[2:], "")
            ecg_items.append(Paragraph(
                f"<font color='#{MED_BLUE.hexval()[2:]}'><b>&#x2022; {bold}</b></font>{rest}",
                styles["bullet"]
            ))
        else:
            ecg_items.append(Paragraph(f"&#x2022; {item}", styles["bullet"]))

    # Build left column
    left_content = [overview_para, Spacer(1, 4),
                    Paragraph("<b>Key ECG Changes:</b>", styles["h2"])]
    left_content.extend(ecg_items)

    # Image column
    if img_path and os.path.exists(img_path):
        img_obj = Image(img_path, width=85*mm, height=54*mm)
        img_cell = [img_obj,
                    Paragraph(img_caption, styles["caption"])]
    else:
        img_cell = [Paragraph("[ECG image not available]", styles["caption"])]

    # Side-by-side table
    main_table = Table(
        [[left_content, img_cell]],
        colWidths=[None, 90*mm]
    )
    main_table.setStyle(TableStyle([
        ("VALIGN", (0, 0), (-1, -1), "TOP"),
        ("TOPPADDING", (0, 0), (-1, -1), 0),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 0),
        ("LEFTPADDING", (0, 0), (0, 0), 0),
        ("LEFTPADDING", (1, 0), (1, 0), 6),
        ("RIGHTPADDING", (0, 0), (-1, -1), 0),
    ]))
    story.append(main_table)
    story.append(Spacer(1, 4))

    # Key clinical point callout
    box = CalloutBox(
        f"KEY POINT: {key_point}",
        bg=LIGHT_BLUE, border=color,
        icon="!", width=W - 30*mm, height=34
    )
    story.append(box)
    story.append(Spacer(1, 4))
    return story


# ── Section 9: All 10 Diseases ────────────────────────────────────────────────
def build_diseases(styles, img_map):
    story = []
    story.append(Spacer(1, 5*mm))

    # Section header
    hdr_data = [[Paragraph(
        "<b>9. TEN COMMON CARDIAC CONDITIONS — ECG PATTERNS</b>",
        ParagraphStyle("dh", fontName="Helvetica-Bold", fontSize=13,
                       textColor=WHITE, alignment=TA_CENTER)
    )]]
    hdr_t = Table(hdr_data, colWidths=[W - 30*mm])
    hdr_t.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, -1), DARK_BLUE),
        ("TOPPADDING", (0, 0), (-1, -1), 8),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 8),
    ]))
    story.append(hdr_t)
    story.append(PageBreak())

    # ─── 9.1 STEMI ───────────────────────────────────────────────────────────
    story += disease_card(
        "9.1", "ST-Elevation Myocardial Infarction", "STEMI",
        "Acute transmural MI from complete coronary occlusion. Most commonly LAD (anterior), "
        "RCA (inferior), or LCx (lateral). ECG evolves through hyperacute, acute, and "
        "subacute phases.",
        [
            "**ST elevation ≥1 mm** in ≥2 contiguous limb leads OR ≥2 mm in precordial leads",
            "**Hyperacute T-waves** — tall, peaked (earliest sign, minutes)",
            "**Pathological Q-waves** >40 ms wide, >25% R-wave amplitude (necrosis marker)",
            "**Reciprocal ST depression** in mirror-territory leads",
            "**T-wave inversion** in evolving phase (hours–days)",
            "**'Tombstone' pattern** — convex ST elevation fusing with T: proximal LAD occlusion",
            "Anterior (V1–V4) = LAD | Inferior (II, III, aVF) = RCA | Lateral (I, aVL, V5–V6) = LCx",
        ],
        "ST elevation that is CONVEX ('dome-shaped') and in a specific coronary distribution "
        "with reciprocal changes = STEMI until proven otherwise. Call cath lab immediately.",
        img_map.get("stemi"), "12-lead STEMI — ST elevation V2–V5, reciprocal depression in inferior leads",
        styles, color=RED
    )
    story.append(PageBreak())

    # ─── 9.2 AF ──────────────────────────────────────────────────────────────
    story += disease_card(
        "9.2", "Atrial Fibrillation", "AF",
        "Most common sustained arrhythmia. Disorganised atrial electrical activity. "
        "Associated with hypertension, heart failure, valvular disease, thyrotoxicosis, "
        "sleep apnoea, and age.",
        [
            "**Absent organised P-waves** — replaced by chaotic fibrillatory (f) waves",
            "**Irregularly irregular R-R intervals** — the diagnostic hallmark",
            "**Narrow QRS** unless aberrant conduction or pre-existing BBB",
            "f-waves best seen in V1 and lead II",
            "Rate may be fast (uncontrolled: >100 bpm) or controlled (60–80 bpm)",
            "Fine f-waves = chronic AF; Coarse f-waves = recent onset / large atrium",
            "AF + wide complex + rapid rate → consider WPW (pre-excited AF — danger!)",
        ],
        "The key diagnostic features are ABSENT P-waves and IRREGULARLY IRREGULAR R-R intervals. "
        "AF with rapid ventricular rate in WPW can be life-threatening — do not give AV nodal blockers.",
        img_map.get("afib"), "Atrial fibrillation — absent P-waves, fine fibrillatory baseline, irregular R-R intervals",
        styles, color=ORANGE
    )
    story.append(PageBreak())

    # ─── 9.3 Complete Heart Block ─────────────────────────────────────────────
    story += disease_card(
        "9.3", "Complete (Third-Degree) Heart Block", "CHB / 3° AV Block",
        "Total failure of AV conduction — no atrial impulse reaches ventricles. "
        "Escape rhythm maintains cardiac output. Causes: fibrosis, inferior MI, Lyme disease, "
        "drug toxicity (digoxin, beta-blockers, calcium channel blockers), congenital.",
        [
            "**Complete AV dissociation** — P-waves and QRS march independently",
            "**Regular P-P intervals** (atrial rate 60–100 bpm) independent of QRS",
            "**Regular R-R intervals** — slow escape rhythm (ventricular 20–40 bpm, junctional 40–60 bpm)",
            "**Narrow QRS escape** = nodal block (junctional pacemaker, more stable)",
            "**Wide QRS escape** = infranodal block (ventricular pacemaker, unstable — emergency)",
            "P-waves can appear before, within, or after QRS complexes",
            "No P-QRS relationship regardless of P-wave timing",
        ],
        "Complete AV dissociation with a SLOW ESCAPE RHYTHM is third-degree block. "
        "Wide-QRS escape rhythm = infranodal block = unstable — requires urgent pacing.",
        img_map.get("avblock"), "Complete heart block — AV dissociation, wide-complex escape rhythm, P-waves marching independently",
        styles, color=MED_BLUE
    )
    story.append(PageBreak())

    # ─── 9.4 VT ──────────────────────────────────────────────────────────────
    story += disease_card(
        "9.4", "Ventricular Tachycardia", "VT",
        "Life-threatening arrhythmia from ventricular myocardium or His-Purkinje system. "
        "Rate 100–250 bpm. Strongly associated with structural heart disease (post-MI scar, "
        "dilated cardiomyopathy). Monomorphic or polymorphic.",
        [
            "**Wide QRS tachycardia** (>120 ms, often >140 ms) — regular rapid rhythm",
            "**AV dissociation** — P-waves independent of QRS (present in ~50%, pathognomonic)",
            "**Fusion beats** — partially conducted sinus beat fusing with VT complex",
            "**Capture beats** — fully conducted narrow sinus beat during VT (diagnostic)",
            "**Concordance** — all precordial leads positive OR all negative (V1–V6)",
            "**Northwest axis** (negative I AND aVF) strongly favours VT",
            "**Brugada criteria**: if any criterion met, treat as VT",
        ],
        "Any wide-complex tachycardia should be treated as VT until proven otherwise. "
        "AV dissociation, fusion beats, or capture beats = definitive VT. Never give verapamil.",
        img_map.get("vt"), "Monomorphic VT — wide-complex tachycardia, positive concordance across precordial leads",
        styles, color=RED
    )
    story.append(PageBreak())

    # ─── 9.5 HCM ─────────────────────────────────────────────────────────────
    story += disease_card(
        "9.5", "Hypertrophic Cardiomyopathy", "HCM",
        "Genetic disorder (sarcomere protein mutations) causing asymmetric LV hypertrophy. "
        "Leading cause of sudden cardiac death in young athletes. ECG abnormal in ~90% of cases.",
        [
            "**High-voltage R-waves** in precordial leads (LVH: S-V1 + R-V5/V6 ≥35 mm)",
            "**Giant T-wave inversions** in V2–V5 (apical HCM / Yamaguchi syndrome)",
            "**Strain pattern** — ST depression + T inversion in I, aVL, V5–V6",
            "**Absent septal Q-waves** in I, aVL, V5–V6 (abnormal septal depolarisation)",
            "Left axis deviation common",
            "Deep narrow Q-waves in II, III, aVF in asymmetric septal HCM",
            "QTc prolongation possible — arrhythmia risk",
        ],
        "Giant T-wave inversions in V2–V5 with high QRS voltage in a young patient = "
        "apical HCM (Yamaguchi) until proven otherwise. Refer for echocardiography and cardiology.",
        img_map.get("hcm"), "Apical HCM — giant T-wave inversions V2–V5, high precordial QRS voltage",
        styles, color=colors.HexColor("#6C3483")
    )
    story.append(PageBreak())

    # ─── 9.6 PE ──────────────────────────────────────────────────────────────
    story += disease_card(
        "9.6", "Acute Pulmonary Embolism", "PE",
        "Pulmonary vascular obstruction causing acute right ventricular pressure overload. "
        "ECG reflects RV strain, not the pulmonary pathology itself. Most common finding is "
        "sinus tachycardia.",
        [
            "**Sinus tachycardia** — most common (and often only) ECG finding",
            "**S1Q3T3 pattern** (McGinn-White sign) — deep S in I, Q in III, inverted T in III",
            "**Incomplete or complete RBBB** — due to acute RV pressure overload",
            "**T-wave inversions V1–V4** — right ventricular strain pattern",
            "**Right axis deviation** (new or compared to prior ECG)",
            "**P pulmonale** — peaked P >2.5 mm in II (acute RA enlargement)",
            "New-onset AF may occur; low QRS voltage if large effusion",
        ],
        "S1Q3T3 is only present in ~20% of massive PE. Sinus tachycardia + T inversions V1–V3 "
        "+ new RBBB in a breathless patient = PE until proven otherwise. CT-PA urgently.",
        img_map.get("pe"), "PE — S1Q3T3 pattern annotated with arrows; sinus tachycardia, right heart strain",
        styles, color=colors.HexColor("#1A5276")
    )
    story.append(PageBreak())

    # ─── 9.7 Pericarditis ────────────────────────────────────────────────────
    story += disease_card(
        "9.7", "Acute Pericarditis", "Pericarditis",
        "Inflammation of the pericardium, most commonly viral (Coxsackievirus). "
        "Classic: sharp pleuritic chest pain, worse supine, relieved leaning forward. "
        "ECG evolves through 4 stages over weeks.",
        [
            "**Stage 1**: Diffuse CONCAVE ('saddle-shaped') ST elevation all leads except aVR/V1",
            "**PR segment depression** in II, III, aVF, V4–V6 (most specific sign)",
            "**PR elevation in aVR** — reciprocal to PR depression",
            "**Spodick's sign** — downsloping TP segment (specific but subtle)",
            "Stage 2: ST normalises, PR depression persists",
            "Stage 3: Diffuse T-wave inversions",
            "NO reciprocal ST depression (except aVR) — distinguishes from STEMI",
        ],
        "CONCAVE 'saddle-shaped' ST elevation + PR depression = pericarditis. "
        "CONVEX ST elevation + reciprocal changes + no PR depression = STEMI. "
        "This distinction is critical — do not miss STEMI.",
        img_map.get("pericarditis"), "Acute pericarditis — diffuse saddle ST elevation, PR depression in inferior leads, reciprocal changes in aVR",
        styles, color=colors.HexColor("#196F3D")
    )
    story.append(PageBreak())

    # ─── 9.8 WPW ─────────────────────────────────────────────────────────────
    story += disease_card(
        "9.8", "Wolff-Parkinson-White Syndrome", "WPW / Pre-excitation",
        "Congenital accessory pathway (Bundle of Kent) bypasses AV node causing ventricular "
        "pre-excitation. Risk of sudden cardiac death if AF develops with rapid antegrade "
        "conduction to ventricles.",
        [
            "**Short PR interval** <120 ms — AV node bypassed",
            "**Delta wave** — slurred upstroke at start of QRS (hallmark)",
            "**Widened QRS** >120 ms — fusion of pre-excited + normally conducted impulse",
            "**Secondary ST-T changes** — discordant T-waves (pseudo-ischaemia)",
            "**Pseudo-Q waves** — negative delta waves mimic infarction Q-waves",
            "Positive delta in V1 = left-sided pathway; Negative inferior = posteroseptal",
            "Pre-excited AF: rapid irregular wide-complex tachycardia — do NOT give adenosine/verapamil",
        ],
        "Short PR + Delta wave + Wide QRS = WPW. In pre-excited AF, the accessory pathway "
        "can conduct at 300+ bpm → VF. AV nodal blockers are CONTRAINDICATED. Use procainamide or DC cardioversion.",
        img_map.get("wpw"), "WPW syndrome — short PR, prominent delta waves (red arrows), wide QRS, secondary ST-T changes",
        styles, color=colors.HexColor("#117A65")
    )
    story.append(PageBreak())

    # ─── 9.9 DCM ─────────────────────────────────────────────────────────────
    story += disease_card(
        "9.9", "Dilated Cardiomyopathy / Heart Failure (HFrEF)", "DCM / LBBB",
        "LV dilation with systolic dysfunction (EF <40%). Causes: ischaemic, idiopathic, "
        "viral myocarditis, alcohol, genetic, peripartum. ECG reflects ventricular remodelling "
        "and conduction delays.",
        [
            "**Left Bundle Branch Block (LBBB)** — wide QRS >120 ms, broad R in I/V5/V6, QS in V1–V3",
            "LBBB = indication for CRT (cardiac resynchronisation therapy) if EF ≤35%",
            "**Poor R-wave progression** across precordials (pseudo-infarct pattern)",
            "**Non-specific ST-T changes** — secondary repolarisation abnormalities",
            "**First-degree AV block** — prolonged PR common",
            "**LVH voltage criteria** — high QRS amplitude",
            "Non-sustained VT, PVCs — arrhythmic substrate from fibrosis",
        ],
        "New LBBB + acute chest pain = treat as STEMI equivalent (Sgarbossa criteria apply). "
        "LBBB in heart failure with EF ≤35% = refer for CRT assessment.",
        img_map.get("dcm"), "Dilated cardiomyopathy with LBBB — wide QRS 166 ms, QS pattern lateral leads, secondary repolarisation changes",
        styles, color=colors.HexColor("#1F618D")
    )
    story.append(PageBreak())

    # ─── 9.10 LQTS ───────────────────────────────────────────────────────────
    story += disease_card(
        "9.10", "Long QT Syndrome", "LQTS / Torsades de Pointes",
        "Channelopathy (congenital or acquired) causing delayed ventricular repolarisation. "
        "Predisposes to Torsades de Pointes (TdP), a polymorphic VT. "
        "Congenital: LQT1 (KCNQ1), LQT2 (KCNH2), LQT3 (SCN5A). "
        "Acquired: drugs, hypokalaemia, hypomagnesaemia.",
        [
            "**Prolonged QTc** >440 ms (men), >460 ms (women) — measured lead II or V5",
            "**QTc >500 ms** = high risk for Torsades de Pointes",
            "**LQT1**: Broad-based smooth T-waves; triggered by exercise",
            "**LQT2**: Notched/bifid 'double-humped' T-waves; triggered by auditory stimuli",
            "**LQT3**: Late peaked T-waves, long flat ST segment; triggered at rest/sleep",
            "**T-wave alternans** — beat-to-beat T-wave amplitude variation (ominous sign)",
            "**Torsades de Pointes**: polymorphic VT, twisting QRS axis — initiated by R-on-T",
        ],
        "Use Bazett formula: QTc = QT / √RR. QTc >500 ms demands immediate action — stop "
        "culprit drugs, correct electrolytes (K+ >4.5, Mg2+ >1), consider IV magnesium sulphate 2g.",
        img_map.get("lqts"), "Long QT syndrome — prolonged QTc 478 ms in sinus rhythm (top); Torsades de Pointes with twisting axis (bottom)",
        styles, color=RED
    )
    story.append(PageBreak())
    return story


# ── Section 10: Summary table ─────────────────────────────────────────────────
def build_summary(styles):
    story = []
    story.append(Spacer(1, 5*mm))
    story.append(SectionBanner(10, "QUICK REFERENCE SUMMARY", bg=DARK_BLUE))
    story.append(Spacer(1, 5))

    headers = ["#", "Condition", "Rate/Rhythm", "P-wave", "PR", "QRS", "ST/T Changes", "Key Hallmark"]
    rows = [
        ["1", "STEMI", "Sinus / any", "Normal", "Normal", "Normal→Q", "ST ↑ (convex) + reciprocal ↓",
         "Convex ST elevation in vascular territory"],
        ["2", "Atrial Fibrillation", "Irregular", "Absent (f-waves)", "—", "Narrow", "Usually normal",
         "Irregularly irregular, absent P-waves"],
        ["3", "Complete Heart Block", "Slow escape", "Present, regular", "None (dissociated)", "Wide or narrow",
         "Secondary changes", "Complete AV dissociation"],
        ["4", "Ventricular Tachycardia", "Fast (100–250)", "Dissociated", "None", "Wide >120 ms",
         "Concordance", "Wide QRS tachycardia + AV dissociation"],
        ["5", "HCM", "Sinus", "Normal", "Normal", "Normal", "Giant T-wave inversions",
         "Deep T inversions + LVH voltage"],
        ["6", "Pulmonary Embolism", "Sinus tachy", "P pulmonale", "Normal", "RBBB",
         "T inversions V1–V4", "S1Q3T3 + sinus tachycardia"],
        ["7", "Pericarditis", "Sinus (tachy)", "Normal", "PR depression", "Normal",
         "Diffuse saddle ST ↑", "Concave ST elevation + PR depression"],
        ["8", "WPW Syndrome", "Sinus", "Normal", "Short <120 ms", "Wide (delta)",
         "Discordant T-waves", "Short PR + delta wave + wide QRS"],
        ["9", "DCM / Heart Failure", "Sinus", "Normal/P mitrale", "Prolonged", "LBBB >120 ms",
         "Secondary LBBB changes", "LBBB pattern + poor R-wave progression"],
        ["10", "Long QT Syndrome", "Sinus", "Normal", "Normal", "Normal",
         "Prolonged QTc ≥500 ms", "Long QTc + T-wave morphology by subtype"],
    ]

    all_data = [headers] + rows
    col_w = [8*mm, 38*mm, 24*mm, 20*mm, 16*mm, 22*mm, 36*mm, None]

    t = Table([[Paragraph(cell, ParagraphStyle(
        "sc", fontName="Helvetica-Bold" if i == 0 else "Helvetica",
        fontSize=7, textColor=WHITE if i == 0 else BLACK,
        alignment=TA_CENTER if j in [0, 2, 3, 4, 5] else TA_LEFT,
        leading=10
    )) for j, cell in enumerate(row)] for i, row in enumerate(all_data)],
        colWidths=col_w)

    style_cmds = [
        ("BACKGROUND", (0, 0), (-1, 0), DARK_BLUE),
        ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
        ("FONTSIZE", (0, 0), (-1, 0), 7),
        ("TEXTCOLOR", (0, 0), (-1, 0), WHITE),
        ("GRID", (0, 0), (-1, -1), 0.3, colors.HexColor("#AEB6BF")),
        ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
        ("TOPPADDING", (0, 0), (-1, -1), 4),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 4),
        ("LEFTPADDING", (0, 0), (-1, -1), 3),
    ]
    for i in range(1, len(all_data)):
        bg = LIGHT_GREY if i % 2 == 0 else WHITE
        style_cmds.append(("BACKGROUND", (0, i), (-1, i), bg))
        style_cmds.append(("BACKGROUND", (0, i), (0, i),
                            colors.HexColor("#2E4057")))
        style_cmds.append(("FONTNAME", (0, i), (0, i), "Helvetica-Bold"))
        style_cmds.append(("TEXTCOLOR", (0, i), (0, i), WHITE))

    t.setStyle(TableStyle(style_cmds))
    story.append(t)
    story.append(PageBreak())
    return story


# ── Section 11: MI Localisation ───────────────────────────────────────────────
def build_mi_localisation(styles):
    story = []
    story.append(Spacer(1, 5*mm))
    story.append(SectionBanner(11, "LEAD LOCALISATION OF MYOCARDIAL INFARCTION",
                                "Identify culprit artery from ST elevation territory", bg=RED))
    story.append(Spacer(1, 5))

    mi_data = [
        ["MI Territory", "Leads with ST Elevation", "Reciprocal Changes", "Culprit Artery"],
        ["Anterior", "V1, V2, V3, V4", "II, III, aVF (variable)", "LAD (mid)"],
        ["Anteroseptal", "V1, V2 (±V3)", "None typical", "LAD (proximal/septal)"],
        ["Anterolateral", "V1–V6, I, aVL", "II, III, aVF", "Proximal LAD or Left Main"],
        ["Lateral (high)", "I, aVL", "II, III, aVF", "LCx or Diagonal"],
        ["Lateral (low)", "V5, V6", "aVR", "LCx"],
        ["Inferior", "II, III, aVF", "I, aVL", "RCA (85%) / LCx (15%)"],
        ["Posterior", "ST depression V1–V3 (mirror)", "V7, V8, V9 (posterior leads)", "RCA or LCx"],
        ["RV Infarction", "V4R, V3R (ST elevation)", "Inferior leads elevation", "Proximal RCA"],
        ["'Tombstone' pattern", "V1–V4 (convex, tall)", "Inferior leads", "Proximal LAD (critical)"],
    ]

    mi_table = Table(mi_data, colWidths=[38*mm, 45*mm, 45*mm, None])
    mi_table.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, 0), RED),
        ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
        ("FONTSIZE", (0, 0), (-1, 0), 8),
        ("TEXTCOLOR", (0, 0), (-1, 0), WHITE),
        ("ROWBACKGROUNDS", (0, 1), (-1, -1), [WHITE, LIGHT_GREY]),
        ("FONTNAME", (0, 1), (-1, -1), "Helvetica"),
        ("FONTSIZE", (0, 1), (-1, -1), 8),
        ("GRID", (0, 0), (-1, -1), 0.4, colors.HexColor("#BFC9CA")),
        ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
        ("TOPPADDING", (0, 0), (-1, -1), 5),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 5),
        ("LEFTPADDING", (0, 0), (-1, -1), 5),
        ("BACKGROUND", (0, 9), (-1, 9), colors.HexColor("#FADBD8")),
        ("FONTNAME", (0, 9), (-1, 9), "Helvetica-Bold"),
        ("TEXTCOLOR", (0, 9), (-1, 9), RED),
    ]))
    story.append(mi_table)
    story.append(Spacer(1, 6))

    story.append(CalloutBox(
        "STEMI MIMICS TO EXCLUDE: Pericarditis (diffuse, concave ST, PR depression), "
        "LBBB (Sgarbossa criteria needed), Brugada (V1–V2 coved ST), LVH strain, "
        "Benign early repolarisation, Hyperkalaemia (peaked T-waves ± sine wave).",
        bg=YELLOW_BG, border=ORANGE, icon="!", width=W - 30*mm, height=42
    ))
    story.append(Spacer(1, 6))

    # BBB quick guide
    story.append(Paragraph("Bundle Branch Block Quick Reference", styles["h2"]))
    bbb_data = [
        ["Feature", "LBBB", "RBBB"],
        ["QRS width", ">120 ms", ">120 ms"],
        ["V1 pattern", "Deep QS or rS (negative)", "RSR' ('rabbit ears') — positive"],
        ["V5/V6 pattern", "Broad monophasic R (no S)", "Wide S wave"],
        ["Lead I / aVL", "Broad R, no S", "Small R, wide S"],
        ["Clinical significance", "Always abnormal — ischaemia, DCM, hypertension", "May be normal variant or acute PE/RVH"],
        ["Sgarbossa criteria", "Concordant ST elevation ≥1 mm OR concordant ST depression V1–V3 ≥1 mm in LBBB = STEMI equivalent", "N/A"],
    ]
    bbb_t = Table(bbb_data, colWidths=[40*mm, None, None])
    bbb_t.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, 0), MED_BLUE),
        ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
        ("FONTSIZE", (0, 0), (-1, 0), 8),
        ("TEXTCOLOR", (0, 0), (-1, 0), WHITE),
        ("FONTNAME", (0, 1), (0, -1), "Helvetica-Bold"),
        ("FONTSIZE", (0, 0), (-1, -1), 7.5),
        ("ROWBACKGROUNDS", (0, 1), (-1, -1), [WHITE, LIGHT_GREY]),
        ("GRID", (0, 0), (-1, -1), 0.4, colors.HexColor("#BFC9CA")),
        ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
        ("TOPPADDING", (0, 0), (-1, -1), 5),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 5),
        ("LEFTPADDING", (0, 0), (-1, -1), 5),
    ]))
    story.append(bbb_t)
    story.append(Spacer(1, 6))

    # Final disclaimer
    disc = Paragraph(
        "DISCLAIMER: This guide is for educational purposes only. ECG interpretation must always "
        "be performed in the context of the patient's clinical presentation, history, symptoms, "
        "vital signs, and other investigations. No ECG finding should be acted upon in isolation. "
        "Always seek senior clinical review for critical findings.",
        ParagraphStyle("disc", fontName="Helvetica-Oblique", fontSize=7.5,
                       textColor=MID_GREY, alignment=TA_JUSTIFY,
                       borderColor=LIGHT_BLUE, borderWidth=1, borderPadding=6,
                       leading=12)
    )
    story.append(disc)
    return story


# ── MAIN BUILD ────────────────────────────────────────────────────────────────
def main():
    print("Fetching ECG images...")
    img_map = fetch_images(ECG_IMAGES)
    print(f"  Retrieved {len(img_map)}/10 images")

    print("Building PDF...")
    doc = SimpleDocTemplate(
        OUTPUT,
        pagesize=A4,
        leftMargin=15*mm, rightMargin=15*mm,
        topMargin=22*mm, bottomMargin=18*mm,
        title="ECG Interpretation Guide",
        author="Orris Medical Education",
        subject="ECG interpretation, cardiac conditions, arrhythmias",
    )

    styles = make_styles()
    story = []

    # Cover
    story += build_cover(styles)
    # TOC
    story += build_toc(styles)
    # Content sections
    story += build_basics(styles)
    story += build_systematic(styles)
    story += build_normal_values(styles)
    # 10 diseases
    story += build_diseases(styles, img_map)
    # Summary + MI localisation
    story += build_summary(styles)
    story += build_mi_localisation(styles)

    doc.build(story, onFirstPage=on_page, onLaterPages=on_page)
    print(f"PDF written to: {OUTPUT}")


if __name__ == "__main__":
    main()
Your credits are exhausted. Your daily credits reset tomorrow, or you can purchase a top-up for more.
This is a shared conversation. Sign in to Orris to start your own chat.