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 like for MI, different arrythmias). Use standard medical book as reference
mkdir -p /home/daytona/workspace/ecg-guide && ls /home/daytona/workspace/ecg-guide/
~/ecg-guide/build_ecg_pdf.py
#!/usr/bin/env python3
"""
Comprehensive ECG Guide PDF Generator
References: Guyton & Hall Medical Physiology, Goldman-Cecil Medicine,
Harriet Lane Handbook, Tintinalli's Emergency Medicine,
Braunwald's Heart Disease
"""
import urllib.request
import io
import os
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm, cm
from reportlab.lib import colors
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, Image, HRFlowable, PageBreak,
KeepTogether)
from reportlab.platypus.flowables import Flowable
from reportlab.graphics.shapes import Drawing, Rect, Line, String, Circle, Polygon
from reportlab.graphics import renderPDF
from reportlab.graphics.shapes import Drawing
from PIL import Image as PILImage
import requests
# ─── Color Palette ──────────────────────────────────────────────────────────
DARK_BLUE = colors.HexColor('#1A3A5C')
MED_BLUE = colors.HexColor('#2E6DA4')
LIGHT_BLUE = colors.HexColor('#D6E8F7')
ACCENT_RED = colors.HexColor('#C0392B')
ACCENT_GREEN = colors.HexColor('#27AE60')
ACCENT_ORANGE = colors.HexColor('#E67E22')
ACCENT_PURPLE = colors.HexColor('#8E44AD')
LIGHT_GRAY = colors.HexColor('#F5F7FA')
MID_GRAY = colors.HexColor('#BDC3C7')
DARK_GRAY = colors.HexColor('#2C3E50')
ECG_GRID = colors.HexColor('#FADADD')
ECG_LINE = colors.HexColor('#C0392B')
WHITE = colors.white
OUTPUT_DIR = '/home/daytona/workspace/ecg-guide'
OUTPUT_PDF = os.path.join(OUTPUT_DIR, 'ECG_Complete_Guide.pdf')
# ─── Image Download ──────────────────────────────────────────────────────────
IMAGES = {
'ecg_waveform': 'https://cdn.orris.care/cdss_images/ef815eeb06c673b405e3c7f9112d1e9ff6fbea96a00a2723b233d384156386bd.png',
'lead_placement': 'https://cdn.orris.care/cdss_images/980d49638880f123e60f04d038c1af30751f3ad125962f137d98b37cf6e5d83e.png',
'st_changes': 'https://cdn.orris.care/cdss_images/249616e55c8c3e08b4eaeef83a907da6231c6fb45b7dd20ba383052d65ea96cb.png',
'arrhythmias': 'https://cdn.orris.care/cdss_images/dca93d3986d7506a1f58255c3278d526c6d7df5dbd273b96e4c3018aa098d95d.png',
'bundle_branch': 'https://cdn.orris.care/cdss_images/df73363e4b0e2bfe9a324ca22201347a6a70539e616312069ca79d14374d1c41.png',
}
img_cache = {}
def get_image(key, width, height=None):
url = IMAGES[key]
if url not in img_cache:
try:
resp = requests.get(url, timeout=15)
img_cache[url] = io.BytesIO(resp.content)
except Exception as e:
print(f"Could not fetch {key}: {e}")
return None
img_cache[url].seek(0)
pil = PILImage.open(img_cache[url])
aspect = pil.height / pil.width
if height is None:
height = width * aspect
img_cache[url].seek(0)
return Image(img_cache[url], width=width, height=height)
# ─── Styles ──────────────────────────────────────────────────────────────────
styles = getSampleStyleSheet()
def make_style(name, **kwargs):
return ParagraphStyle(name, **kwargs)
TITLE_STYLE = make_style('DocTitle',
fontSize=28, fontName='Helvetica-Bold', textColor=WHITE,
alignment=TA_CENTER, spaceAfter=6)
CHAPTER_STYLE = make_style('Chapter',
fontSize=16, fontName='Helvetica-Bold', textColor=WHITE,
alignment=TA_LEFT, spaceBefore=4, spaceAfter=4)
SECTION_STYLE = make_style('Section',
fontSize=13, fontName='Helvetica-Bold', textColor=DARK_BLUE,
spaceBefore=10, spaceAfter=4)
SUBSECTION_STYLE = make_style('Subsection',
fontSize=11, fontName='Helvetica-Bold', textColor=MED_BLUE,
spaceBefore=6, spaceAfter=2)
BODY_STYLE = make_style('Body',
fontSize=10, fontName='Helvetica', textColor=DARK_GRAY,
leading=15, alignment=TA_JUSTIFY, spaceAfter=4)
BULLET_STYLE = make_style('Bullet',
fontSize=10, fontName='Helvetica', textColor=DARK_GRAY,
leading=14, leftIndent=16, firstLineIndent=-10, spaceAfter=2)
CAPTION_STYLE = make_style('Caption',
fontSize=8.5, fontName='Helvetica-Oblique', textColor=colors.HexColor('#555555'),
alignment=TA_CENTER, spaceAfter=6)
REF_STYLE = make_style('Ref',
fontSize=8, fontName='Helvetica', textColor=colors.HexColor('#666666'),
alignment=TA_LEFT)
KEY_POINT_STYLE = make_style('KeyPoint',
fontSize=10, fontName='Helvetica-Bold', textColor=DARK_BLUE,
leftIndent=12, spaceAfter=3)
WARNING_STYLE = make_style('Warning',
fontSize=10, fontName='Helvetica-Bold', textColor=ACCENT_RED,
leftIndent=12, spaceAfter=3)
TABLE_HEADER_STYLE = make_style('TableHeader',
fontSize=9.5, fontName='Helvetica-Bold', textColor=WHITE,
alignment=TA_CENTER)
TABLE_BODY_STYLE = make_style('TableBody',
fontSize=9, fontName='Helvetica', textColor=DARK_GRAY,
alignment=TA_LEFT, leading=13)
# ─── Helper Flowables ────────────────────────────────────────────────────────
class ChapterHeader(Flowable):
def __init__(self, title, subtitle='', color=DARK_BLUE, width=None):
Flowable.__init__(self)
self.title = title
self.subtitle = subtitle
self.color = color
self.width = width or (A4[0] - 40*mm)
self.height = 52
def draw(self):
c = self.canv
w, h = self.width, self.height
# Background
c.setFillColor(self.color)
c.roundRect(0, 0, w, h, 8, fill=1, stroke=0)
# Accent strip
c.setFillColor(ACCENT_RED)
c.roundRect(0, 0, 6, h, 3, fill=1, stroke=0)
# Title
c.setFillColor(WHITE)
c.setFont('Helvetica-Bold', 17)
c.drawString(16, h - 26, self.title)
if self.subtitle:
c.setFont('Helvetica', 10)
c.setFillColor(colors.HexColor('#D0E8FF'))
c.drawString(16, h - 40, self.subtitle)
class SectionBox(Flowable):
"""Colored left-border box for section titles."""
def __init__(self, text, color=MED_BLUE, width=None):
Flowable.__init__(self)
self.text = text
self.color = color
self.width = width or (A4[0] - 40*mm)
self.height = 26
def draw(self):
c = self.canv
w, h = self.width, self.height
c.setFillColor(colors.HexColor('#EBF5FB'))
c.roundRect(0, 0, w, h, 4, fill=1, stroke=0)
c.setFillColor(self.color)
c.rect(0, 0, 5, h, fill=1, stroke=0)
c.setFillColor(self.color)
c.setFont('Helvetica-Bold', 12)
c.drawString(12, 7, self.text)
class KeyBox(Flowable):
"""Yellow key point box."""
def __init__(self, text, width=None, box_color=None, text_color=None, label='KEY POINT'):
Flowable.__init__(self)
self.text = text
self.label = label
self.box_color = box_color or colors.HexColor('#FEF9E7')
self.border_color = colors.HexColor('#F1C40F')
self.text_color = text_color or DARK_GRAY
self.width = width or (A4[0] - 40*mm)
self.height = 42
def draw(self):
c = self.canv
w, h = self.width, self.height
c.setFillColor(self.box_color)
c.setStrokeColor(self.border_color)
c.setLineWidth(1.5)
c.roundRect(0, 0, w, h, 5, fill=1, stroke=1)
c.setFillColor(self.border_color)
c.setFont('Helvetica-Bold', 8)
c.drawString(8, h - 13, f' {self.label} ')
c.setFillColor(self.text_color)
c.setFont('Helvetica', 9)
# Simple single-line truncation
line = self.text[:120] + ('...' if len(self.text) > 120 else '')
c.drawString(8, 10, line)
class ECGDiagram(Flowable):
"""Draws a simplified annotated ECG waveform for illustration."""
def __init__(self, width=160*mm, height=50*mm, title='Normal Sinus Rhythm'):
Flowable.__init__(self)
self.width = width
self.height = height
self.title = title
def draw(self):
c = self.canv
w, h = self.width, self.height
# Grid background
c.setFillColor(ECG_GRID)
c.rect(0, 0, w, h, fill=1, stroke=0)
# Grid lines
c.setStrokeColor(colors.HexColor('#E8C0C0'))
c.setLineWidth(0.3)
step_x = w / 20
step_y = h / 10
for i in range(21):
c.line(i*step_x, 0, i*step_x, h)
for i in range(11):
c.line(0, i*step_y, w, i*step_y)
# Thick grid lines every 5
c.setStrokeColor(colors.HexColor('#D09090'))
c.setLineWidth(0.6)
for i in range(5):
c.line(i*5*step_x, 0, i*5*step_x, h)
c.line(0, i*2*step_y, w, i*2*step_y)
# Draw ECG waveform (2 beats)
c.setStrokeColor(ECG_LINE)
c.setLineWidth(1.8)
baseline = h * 0.45
beat_w = w * 0.45
start_x = w * 0.05
def draw_beat(sx):
pts = []
# Flat start
pts.append((sx, baseline))
pts.append((sx + beat_w*0.08, baseline))
# P wave
pts.append((sx + beat_w*0.12, baseline + h*0.07))
pts.append((sx + beat_w*0.16, baseline))
# PR segment
pts.append((sx + beat_w*0.22, baseline))
# Q wave
pts.append((sx + beat_w*0.26, baseline - h*0.05))
# R wave
pts.append((sx + beat_w*0.30, baseline + h*0.38))
# S wave
pts.append((sx + beat_w*0.34, baseline - h*0.10))
# J point / ST start
pts.append((sx + beat_w*0.38, baseline))
# ST segment
pts.append((sx + beat_w*0.50, baseline))
# T wave
pts.append((sx + beat_w*0.58, baseline + h*0.12))
pts.append((sx + beat_w*0.66, baseline + h*0.14))
pts.append((sx + beat_w*0.74, baseline + h*0.06))
# return to baseline
pts.append((sx + beat_w*0.80, baseline))
pts.append((sx + beat_w, baseline))
c.lines([(pts[i][0], pts[i][1], pts[i+1][0], pts[i+1][1]) for i in range(len(pts)-1)])
return pts
pts1 = draw_beat(start_x)
pts2 = draw_beat(start_x + beat_w + w*0.05)
# Labels for first beat
sx = start_x
bw = beat_w
c.setFont('Helvetica-Bold', 7)
c.setFillColor(DARK_BLUE)
# P
c.drawCentredString(sx + bw*0.14, baseline + h*0.10, 'P')
# QRS
c.drawCentredString(sx + bw*0.30, baseline + h*0.43, 'R')
c.drawCentredString(sx + bw*0.26, baseline - h*0.14, 'Q')
c.drawCentredString(sx + bw*0.34, baseline - h*0.18, 'S')
# T
c.drawCentredString(sx + bw*0.66, baseline + h*0.20, 'T')
# Intervals
c.setFont('Helvetica', 6)
c.setFillColor(MED_BLUE)
# PR interval arrow
c.setStrokeColor(MED_BLUE)
c.setLineWidth(0.6)
c.line(sx + bw*0.08, baseline - h*0.25, sx + bw*0.26, baseline - h*0.25)
c.drawCentredString(sx + bw*0.17, baseline - h*0.32, 'PR interval')
# QRS arrow
c.setStrokeColor(ACCENT_GREEN)
c.setFillColor(ACCENT_GREEN)
c.line(sx + bw*0.26, baseline - h*0.38, sx + bw*0.38, baseline - h*0.38)
c.drawCentredString(sx + bw*0.32, baseline - h*0.45, 'QRS')
# ST segment
c.setStrokeColor(ACCENT_ORANGE)
c.setFillColor(ACCENT_ORANGE)
c.line(sx + bw*0.38, baseline - h*0.28, sx + bw*0.50, baseline - h*0.28)
c.drawCentredString(sx + bw*0.44, baseline - h*0.35, 'ST')
# QT arrow
c.setStrokeColor(ACCENT_PURPLE)
c.setFillColor(ACCENT_PURPLE)
c.line(sx + bw*0.26, baseline - h*0.52, sx + bw*0.80, baseline - h*0.52)
c.drawCentredString(sx + bw*0.53, baseline - h*0.59, 'QT interval')
# Title
c.setFont('Helvetica-Bold', 9)
c.setFillColor(DARK_BLUE)
c.drawString(4, h - 10, self.title)
class MIDiagram(Flowable):
"""Shows ECG changes in MI (STEMI pattern) with annotations."""
def __init__(self, width=160*mm, height=55*mm):
Flowable.__init__(self)
self.width = width
self.height = height
def draw(self):
c = self.canv
w, h = self.width, self.height
# Draw 3 panels: Normal, Acute MI (STEMI), Old MI
panel_w = w / 3.2
gap = w * 0.03
labels = ['Normal', 'Acute STEMI', 'Old MI (Q wave)']
colors_list = [ACCENT_GREEN, ACCENT_RED, ACCENT_ORANGE]
for idx, (lbl, col) in enumerate(zip(labels, colors_list)):
px = idx * (panel_w + gap) + gap/2
# Panel border
c.setStrokeColor(col)
c.setLineWidth(1)
c.setFillColor(ECG_GRID)
c.roundRect(px, 15, panel_w, h - 20, 4, fill=1, stroke=1)
# Label
c.setFillColor(col)
c.setFont('Helvetica-Bold', 8)
c.drawCentredString(px + panel_w/2, 5, lbl)
baseline = 15 + (h-20)*0.45
bw = panel_w * 0.85
sx = px + panel_w*0.08
c.setStrokeColor(ECG_LINE)
c.setLineWidth(1.5)
if idx == 0: # Normal
pts = [
(sx, baseline),
(sx+bw*0.10, baseline),
(sx+bw*0.14, baseline+(h-20)*0.08), # P
(sx+bw*0.18, baseline),
(sx+bw*0.26, baseline),
(sx+bw*0.30, baseline-(h-20)*0.06), # Q
(sx+bw*0.35, baseline+(h-20)*0.32), # R
(sx+bw*0.40, baseline-(h-20)*0.08), # S
(sx+bw*0.44, baseline),
(sx+bw*0.52, baseline), # ST normal
(sx+bw*0.60, baseline+(h-20)*0.10), # T
(sx+bw*0.68, baseline+(h-20)*0.12),
(sx+bw*0.76, baseline),
(sx+bw, baseline),
]
elif idx == 1: # Acute STEMI - ST elevation
pts = [
(sx, baseline),
(sx+bw*0.10, baseline),
(sx+bw*0.14, baseline+(h-20)*0.08),
(sx+bw*0.18, baseline),
(sx+bw*0.26, baseline),
(sx+bw*0.30, baseline-(h-20)*0.04),
(sx+bw*0.35, baseline+(h-20)*0.32),
(sx+bw*0.40, baseline-(h-20)*0.06),
# ST elevation - tombstone
(sx+bw*0.44, baseline+(h-20)*0.18),
(sx+bw*0.55, baseline+(h-20)*0.20), # elevated ST
(sx+bw*0.65, baseline+(h-20)*0.25), # Hyperacute T
(sx+bw*0.73, baseline+(h-20)*0.18),
(sx+bw*0.80, baseline),
(sx+bw, baseline),
]
else: # Old MI - pathological Q wave, T inversion
pts = [
(sx, baseline),
(sx+bw*0.10, baseline),
(sx+bw*0.14, baseline+(h-20)*0.07),
(sx+bw*0.18, baseline),
(sx+bw*0.26, baseline),
# Deep Q wave
(sx+bw*0.30, baseline-(h-20)*0.20),
(sx+bw*0.34, baseline+(h-20)*0.24),
(sx+bw*0.39, baseline-(h-20)*0.04),
(sx+bw*0.43, baseline),
(sx+bw*0.52, baseline),
# T wave inversion
(sx+bw*0.58, baseline-(h-20)*0.08),
(sx+bw*0.68, baseline-(h-20)*0.10),
(sx+bw*0.76, baseline),
(sx+bw, baseline),
]
c.lines([(pts[i][0], pts[i][1], pts[i+1][0], pts[i+1][1]) for i in range(len(pts)-1)])
# Annotations
c.setFont('Helvetica', 6)
if idx == 1:
c.setFillColor(ACCENT_RED)
c.drawCentredString(sx+bw*0.55, baseline+(h-20)*0.24, 'ST')
c.drawCentredString(sx+bw*0.55, baseline+(h-20)*0.30, 'Elevation')
elif idx == 2:
c.setFillColor(ACCENT_ORANGE)
c.drawCentredString(sx+bw*0.30, baseline-(h-20)*0.28, 'Path Q')
c.drawCentredString(sx+bw*0.64, baseline-(h-20)*0.16, 'T inv.')
# ─── Document Builder ────────────────────────────────────────────────────────
def build_pdf():
doc = SimpleDocTemplate(
OUTPUT_PDF,
pagesize=A4,
leftMargin=20*mm,
rightMargin=20*mm,
topMargin=18*mm,
bottomMargin=18*mm,
title='ECG Complete Guide',
author='Medical Reference - Orris',
)
story = []
W = A4[0] - 40*mm # usable width
# ── COVER PAGE ──────────────────────────────────────────────────────────
def cover_page(canvas, doc):
canvas.saveState()
# Full background
canvas.setFillColor(DARK_BLUE)
canvas.rect(0, 0, A4[0], A4[1], fill=1, stroke=0)
# Top accent bar
canvas.setFillColor(ACCENT_RED)
canvas.rect(0, A4[1]-14*mm, A4[0], 14*mm, fill=1, stroke=0)
# Bottom bar
canvas.setFillColor(MED_BLUE)
canvas.rect(0, 0, A4[0], 10*mm, fill=1, stroke=0)
# Title
canvas.setFillColor(WHITE)
canvas.setFont('Helvetica-Bold', 34)
canvas.drawCentredString(A4[0]/2, A4[1]*0.72, 'COMPLETE ECG GUIDE')
canvas.setFont('Helvetica', 16)
canvas.setFillColor(colors.HexColor('#AED6F1'))
canvas.drawCentredString(A4[0]/2, A4[1]*0.65, 'Basics, Interpretation & Pathology')
# Divider
canvas.setStrokeColor(ACCENT_RED)
canvas.setLineWidth(2)
canvas.line(A4[0]*0.25, A4[1]*0.62, A4[0]*0.75, A4[1]*0.62)
# Subtitle block
canvas.setFont('Helvetica', 11)
canvas.setFillColor(colors.HexColor('#D5E8F7'))
lines = [
'Waveforms & Intervals | 12-Lead System | Axis',
'Myocardial Infarction | Arrhythmias | Conduction Blocks',
'Systematic ECG Interpretation Approach',
]
y = A4[1]*0.55
for line in lines:
canvas.drawCentredString(A4[0]/2, y, line)
y -= 14
# References
canvas.setFont('Helvetica-Oblique', 8)
canvas.setFillColor(colors.HexColor('#85929E'))
refs = 'References: Guyton & Hall Medical Physiology | Goldman-Cecil Medicine | Harriet Lane Handbook | Tintinalli\'s EM | Braunwald\'s Heart Disease'
canvas.drawCentredString(A4[0]/2, 18*mm, refs)
canvas.restoreState()
# ── PAGE HEADER/FOOTER ───────────────────────────────────────────────────
def later_pages(canvas, doc):
canvas.saveState()
canvas.setFillColor(DARK_BLUE)
canvas.rect(0, A4[1]-10*mm, A4[0], 10*mm, fill=1, stroke=0)
canvas.setFillColor(WHITE)
canvas.setFont('Helvetica-Bold', 8)
canvas.drawString(22*mm, A4[1]-7*mm, 'COMPLETE ECG GUIDE')
canvas.setFont('Helvetica', 8)
canvas.drawRightString(A4[0]-22*mm, A4[1]-7*mm, 'Medical Reference')
# Footer
canvas.setFillColor(LIGHT_BLUE)
canvas.rect(0, 0, A4[0], 8*mm, fill=1, stroke=0)
canvas.setFillColor(DARK_BLUE)
canvas.setFont('Helvetica', 7.5)
canvas.drawString(22*mm, 2.5*mm, 'Based on standard medical textbooks. For educational purposes.')
canvas.drawRightString(A4[0]-22*mm, 2.5*mm, f'Page {doc.page}')
canvas.restoreState()
# ── COVER (blank page then content) ──────────────────────────────────────
story.append(Spacer(1, A4[1]*0.30))
# ECG Diagram on cover area
story.append(ECGDiagram(width=W, height=55*mm, title=''))
story.append(PageBreak())
# ── SECTION 1: WHAT IS AN ECG ─────────────────────────────────────────
story.append(ChapterHeader('SECTION 1', 'Fundamentals of Electrocardiography', DARK_BLUE, W))
story.append(Spacer(1, 6))
story.append(SectionBox('1.1 What is an ECG?'))
story.append(Spacer(1, 4))
story.append(Paragraph(
'When a cardiac impulse passes through the heart, electrical current spreads from the '
'heart into adjacent tissues and to the surface of the body. If electrodes are placed on '
'the skin on opposite sides of the heart, electrical potentials generated by this current '
'can be recorded. The recording is known as an <b>electrocardiogram (ECG)</b>.',
BODY_STYLE))
story.append(Paragraph(
'The ECG is recorded on paper (or digitally) with 1-mm ("small") boxes and 5-mm ("big") '
'boxes. Voltage amplitude is on the vertical axis (10 mm = 1 mV) and time on the '
'horizontal axis. At the standard recording speed of <b>25 mm/sec</b>: each small box = '
'<b>0.04 sec (40 ms)</b> and each large box = <b>0.2 sec (200 ms)</b>.',
BODY_STYLE))
story.append(Spacer(1, 4))
# ECG waveform image from textbook
img = get_image('ecg_waveform', W*0.82)
if img:
story.append(Table([[img]], colWidths=[W], style=[('ALIGN',(0,0),(0,0),'CENTER')]))
story.append(Paragraph(
'Figure 1.1 - Normal ECG waveform showing P, QRS, T, U waves with PR, QRS and QT intervals labeled. '
'(Goldman-Cecil Medicine, 15th ed.)',
CAPTION_STYLE))
story.append(Spacer(1, 4))
# ── SECTION 1.2: WAVEFORMS ────────────────────────────────────────────
story.append(SectionBox('1.2 The ECG Waveforms'))
story.append(Spacer(1, 4))
waveform_data = [
[Paragraph('<b>Wave / Interval</b>', TABLE_HEADER_STYLE),
Paragraph('<b>What it Represents</b>', TABLE_HEADER_STYLE),
Paragraph('<b>Normal Values</b>', TABLE_HEADER_STYLE)],
[Paragraph('<b>P wave</b>', TABLE_BODY_STYLE),
Paragraph('Atrial depolarization (SA node to AV node)', TABLE_BODY_STYLE),
Paragraph('Duration <0.12 sec; Amplitude <2.5 mm in limb leads', TABLE_BODY_STYLE)],
[Paragraph('<b>PR interval</b>', TABLE_BODY_STYLE),
Paragraph('Conduction through atrial muscle, AV node, and His-Purkinje system', TABLE_BODY_STYLE),
Paragraph('0.12 - 0.20 sec (3-5 small boxes)', TABLE_BODY_STYLE)],
[Paragraph('<b>QRS complex</b>', TABLE_BODY_STYLE),
Paragraph('Ventricular depolarization (simultaneous). Q = first negative deflection; R = first positive; S = negative after R', TABLE_BODY_STYLE),
Paragraph('Duration 0.06 - 0.10 sec (<3 small boxes)\nAmplitude varies by lead', TABLE_BODY_STYLE)],
[Paragraph('<b>ST segment</b>', TABLE_BODY_STYLE),
Paragraph('Period between end of ventricular depolarization (J point) and start of repolarization', TABLE_BODY_STYLE),
Paragraph('Isoelectric (flat); elevation or depression >1 mm in 2 contiguous limb leads or >2 mm in 2 precordial leads = abnormal', TABLE_BODY_STYLE)],
[Paragraph('<b>T wave</b>', TABLE_BODY_STYLE),
Paragraph('Ventricular repolarization; should be in same direction as QRS', TABLE_BODY_STYLE),
Paragraph('Upright in I, II, V3-V6; inverted in aVR normal; amplitude <6 mm limb leads, <10 mm precordial', TABLE_BODY_STYLE)],
[Paragraph('<b>QT interval</b>', TABLE_BODY_STYLE),
Paragraph('Total ventricular electrical activity (depolarization + repolarization)', TABLE_BODY_STYLE),
Paragraph('Corrected QTc <440 ms (male), <460 ms (female)\nQTc = QT / sqrt(RR)', TABLE_BODY_STYLE)],
[Paragraph('<b>U wave</b>', TABLE_BODY_STYLE),
Paragraph('Late ventricular repolarization (Purkinje fibers)', TABLE_BODY_STYLE),
Paragraph('Small, follows T wave; prominent in hypokalemia', TABLE_BODY_STYLE)],
]
t = Table(waveform_data, colWidths=[W*0.18, W*0.45, W*0.37])
t.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), DARK_BLUE),
('BACKGROUND', (0,1), (-1,1), LIGHT_BLUE),
('BACKGROUND', (0,2), (-1,2), WHITE),
('BACKGROUND', (0,3), (-1,3), LIGHT_BLUE),
('BACKGROUND', (0,4), (-1,4), WHITE),
('BACKGROUND', (0,5), (-1,5), LIGHT_BLUE),
('BACKGROUND', (0,6), (-1,6), WHITE),
('BACKGROUND', (0,7), (-1,7), LIGHT_BLUE),
('ROWBACKGROUNDS', (0,1), (-1,-1), [LIGHT_BLUE, WHITE]),
('GRID', (0,0), (-1,-1), 0.5, MID_GRAY),
('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(t)
story.append(Paragraph(
'Table 1.1 - ECG Waveforms and Normal Values. Reference: Guyton & Hall Medical Physiology, 14e; Goldman-Cecil Medicine, 26e; Harriet Lane Handbook, 23e',
CAPTION_STYLE))
story.append(Spacer(1, 4))
story.append(KeyBox(
'The T wave normally occurs 0.25-0.35 sec after ventricular depolarization. '
'Atrial repolarization is hidden within the QRS complex and not visible on surface ECG.',
label='KEY POINT'))
story.append(Spacer(1, 8))
story.append(PageBreak())
# ── SECTION 2: 12-LEAD SYSTEM ─────────────────────────────────────────
story.append(ChapterHeader('SECTION 2', 'The 12-Lead ECG System', MED_BLUE, W))
story.append(Spacer(1, 6))
story.append(SectionBox('2.1 Lead Placement & Einthoven\'s Triangle', MED_BLUE))
story.append(Spacer(1, 4))
story.append(Paragraph(
'A standard 12-lead ECG uses <b>10 electrodes</b> to produce 12 different views of cardiac '
'electrical activity. Leads are divided into two groups:',
BODY_STYLE))
lead_data = [
[Paragraph('<b>Group</b>', TABLE_HEADER_STYLE),
Paragraph('<b>Leads</b>', TABLE_HEADER_STYLE),
Paragraph('<b>What they "see"</b>', TABLE_HEADER_STYLE)],
[Paragraph('<b>Limb Leads (Frontal Plane)</b>', TABLE_BODY_STYLE),
Paragraph('I, II, III (bipolar)\naVR, aVL, aVF (augmented unipolar)', TABLE_BODY_STYLE),
Paragraph('Superior-inferior and left-right electrical forces', TABLE_BODY_STYLE)],
[Paragraph('<b>Precordial (Chest) Leads (Horizontal Plane)</b>', TABLE_BODY_STYLE),
Paragraph('V1, V2, V3, V4, V5, V6', TABLE_BODY_STYLE),
Paragraph('Anterior-posterior and left-right forces', TABLE_BODY_STYLE)],
]
t2 = Table(lead_data, colWidths=[W*0.30, W*0.32, W*0.38])
t2.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), MED_BLUE),
('ROWBACKGROUNDS', (0,1), (-1,-1), [LIGHT_BLUE, WHITE]),
('GRID', (0,0), (-1,-1), 0.5, MID_GRAY),
('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(t2)
story.append(Spacer(1, 6))
# Lead placement image
img2 = get_image('lead_placement', W*0.52)
if img2:
story.append(Table([[img2]], colWidths=[W], style=[('ALIGN',(0,0),(0,0),'CENTER')]))
story.append(Paragraph(
'Figure 2.1 - Einthoven\'s Triangle: Bipolar limb lead arrangement. Lead I = RA(-) to LA(+); '
'Lead II = RA(-) to LL(+); Lead III = LA(-) to LL(+). (Guyton & Hall Medical Physiology, 14e)',
CAPTION_STYLE))
story.append(Spacer(1, 6))
story.append(SectionBox('2.2 Precordial Lead Positions', MED_BLUE))
story.append(Spacer(1, 4))
chest_data = [
[Paragraph('<b>Lead</b>', TABLE_HEADER_STYLE), Paragraph('<b>Position</b>', TABLE_HEADER_STYLE),
Paragraph('<b>Area Viewed</b>', TABLE_HEADER_STYLE)],
['V1', '4th intercostal space, right sternal border', 'Septal, right ventricle'],
['V2', '4th intercostal space, left sternal border', 'Septal, anterior'],
['V3', 'Between V2 and V4', 'Anterior wall'],
['V4', '5th intercostal space, midclavicular line', 'Anterior wall, apex'],
['V5', '5th intercostal space, anterior axillary line', 'Lateral wall'],
['V6', '5th intercostal space, midaxillary line', 'Lateral wall'],
]
chest_formatted = [[Paragraph(str(c), TABLE_BODY_STYLE) if isinstance(c, str) else c for c in row]
for row in chest_data]
chest_formatted[0] = [Paragraph('<b>Lead</b>', TABLE_HEADER_STYLE),
Paragraph('<b>Position</b>', TABLE_HEADER_STYLE),
Paragraph('<b>Area Viewed</b>', TABLE_HEADER_STYLE)]
tc = Table(chest_formatted, colWidths=[W*0.10, W*0.52, W*0.38])
tc.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), MED_BLUE),
('ROWBACKGROUNDS', (0,1), (-1,-1), [LIGHT_BLUE, WHITE]),
('GRID', (0,0), (-1,-1), 0.5, MID_GRAY),
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
('TOPPADDING', (0,0), (-1,-1), 4),
('BOTTOMPADDING', (0,0), (-1,-1), 4),
('LEFTPADDING', (0,0), (-1,-1), 6),
]))
story.append(tc)
story.append(Spacer(1, 6))
# ── SECTION 2.3: LEAD TERRITORY ──────────────────────────────────────
story.append(SectionBox('2.3 Coronary Territory by Lead Group', MED_BLUE))
story.append(Spacer(1, 4))
territory_data = [
[Paragraph('<b>Lead Group</b>', TABLE_HEADER_STYLE),
Paragraph('<b>Area of Heart</b>', TABLE_HEADER_STYLE),
Paragraph('<b>Coronary Artery</b>', TABLE_HEADER_STYLE)],
['II, III, aVF', 'Inferior wall', 'RCA (right coronary artery)'],
['I, aVL, V5, V6', 'Lateral wall', 'LCx (left circumflex)'],
['V1-V4', 'Anterior wall / Septum', 'LAD (left anterior descending)'],
['V1-V2', 'Septal', 'Septal branches of LAD'],
['V1-V4 + tall R in V1', 'Posterior wall (reciprocal)', 'RCA or LCx'],
['aVR', 'Right ventricular outflow / LMCA territory', 'Left main or proximal LAD'],
]
territory_formatted = [[Paragraph(str(c), TABLE_BODY_STYLE) for c in row] for row in territory_data]
territory_formatted[0] = [Paragraph('<b>Lead Group</b>', TABLE_HEADER_STYLE),
Paragraph('<b>Area of Heart</b>', TABLE_HEADER_STYLE),
Paragraph('<b>Coronary Artery</b>', TABLE_HEADER_STYLE)]
tt = Table(territory_formatted, colWidths=[W*0.22, W*0.40, W*0.38])
tt.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), DARK_BLUE),
('ROWBACKGROUNDS', (0,1), (-1,-1), [LIGHT_BLUE, WHITE]),
('GRID', (0,0), (-1,-1), 0.5, MID_GRAY),
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
('TOPPADDING', (0,0), (-1,-1), 4),
('BOTTOMPADDING', (0,0), (-1,-1), 4),
('LEFTPADDING', (0,0), (-1,-1), 6),
]))
story.append(tt)
story.append(Paragraph('Table 2.2 - Coronary territory by lead group.', CAPTION_STYLE))
story.append(Spacer(1, 8))
story.append(PageBreak())
# ── SECTION 3: SYSTEMATIC APPROACH ───────────────────────────────────
story.append(ChapterHeader('SECTION 3', 'Systematic ECG Interpretation', DARK_GRAY, W))
story.append(Spacer(1, 6))
story.append(SectionBox('3.1 Step-by-Step Approach', DARK_GRAY))
story.append(Spacer(1, 4))
story.append(Paragraph(
'A structured approach ensures no finding is missed. Use the mnemonic <b>RRIAQST</b>:',
BODY_STYLE))
steps = [
('1. Rate', 'Count R-R intervals. Normal: 60-100 bpm. Use 300/large boxes between 2 R waves '
'(1 box=300; 2=150; 3=100; 4=75; 5=60; 6=50 bpm). Bradycardia <60; tachycardia >100.'),
('2. Rhythm', 'Is it regular or irregular? Are P waves present before each QRS? '
'Normal sinus rhythm: regular, P before each QRS, upright P in II, rate 60-100.'),
('3. Intervals', 'PR interval (normal 0.12-0.20 sec). QRS duration (normal <0.12 sec). '
'QT/QTc interval (normal QTc <440 ms men, <460 ms women).'),
('4. Axis', 'Normal axis: -30 to +90 degrees. Check leads I and aVF: '
'Both positive = normal. I positive, aVF negative = left axis deviation. '
'I negative, aVF positive = right axis deviation.'),
('5. QRS Morphology', 'Look for Q waves (pathological if >0.04 sec or >25% R height), '
'R wave progression (R wave should increase V1-V5), bundle branch block patterns.'),
('6. ST Segment & T waves', 'ST elevation/depression? T wave inversion? Compare to prior ECG. '
'Elevation >1 mm in 2 contiguous limb leads or >2 mm in 2 precordial leads = abnormal.'),
('7. Overall Interpretation', 'Put all findings together. Consider clinical context. '
'Always compare with previous ECGs when available.'),
]
for step, detail in steps:
story.append(Paragraph(f'<b>{step}:</b> {detail}', BULLET_STYLE))
story.append(Spacer(1, 2))
story.append(Spacer(1, 4))
story.append(SectionBox('3.2 Normal ECG Parameters (Adult)', DARK_GRAY))
story.append(Spacer(1, 4))
normal_data = [
[Paragraph('<b>Parameter</b>', TABLE_HEADER_STYLE), Paragraph('<b>Normal Range</b>', TABLE_HEADER_STYLE), Paragraph('<b>Note</b>', TABLE_HEADER_STYLE)],
['Heart Rate', '60-100 bpm', 'Sinus bradycardia <60; Tachycardia >100'],
['PR Interval', '0.12-0.20 sec (3-5 small boxes)', '>0.20 sec = 1st degree AV block'],
['QRS Duration', '0.06-0.10 sec (<2.5 small boxes)', '>0.12 sec = bundle branch block'],
['QT Interval', 'QTc <440 ms (M), <460 ms (F)', 'Measure in lead II or V5'],
['P wave axis', '0 to +75 degrees', 'Upright in I, II, aVF; inverted in aVR'],
['QRS axis', '-30 to +90 degrees', 'LAD: <-30; RAD: >+90'],
['R wave progression', 'R wave increases V1→V5', 'Poor progression may suggest anterior MI'],
]
normal_formatted = [[Paragraph(str(c), TABLE_BODY_STYLE) for c in row] for row in normal_data]
normal_formatted[0] = [Paragraph('<b>Parameter</b>', TABLE_HEADER_STYLE),
Paragraph('<b>Normal Range</b>', TABLE_HEADER_STYLE),
Paragraph('<b>Note</b>', TABLE_HEADER_STYLE)]
tn = Table(normal_formatted, colWidths=[W*0.28, W*0.32, W*0.40])
tn.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), DARK_GRAY),
('ROWBACKGROUNDS', (0,1), (-1,-1), [LIGHT_BLUE, WHITE]),
('GRID', (0,0), (-1,-1), 0.5, MID_GRAY),
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
('TOPPADDING', (0,0), (-1,-1), 4),
('BOTTOMPADDING', (0,0), (-1,-1), 4),
('LEFTPADDING', (0,0), (-1,-1), 6),
]))
story.append(tn)
story.append(Paragraph('Table 3.1 - Normal Adult ECG Parameters. Harriet Lane Handbook 23e, Goldman-Cecil Medicine.', CAPTION_STYLE))
story.append(PageBreak())
# ── SECTION 4: MYOCARDIAL INFARCTION ─────────────────────────────────
story.append(ChapterHeader('SECTION 4', 'Myocardial Infarction (MI) on ECG', ACCENT_RED, W))
story.append(Spacer(1, 6))
story.append(SectionBox('4.1 ECG Changes in Acute MI', ACCENT_RED))
story.append(Spacer(1, 4))
story.append(Paragraph(
'Acute myocardial infarction produces a characteristic sequence of ECG changes. '
'The typical evolution follows this timeline:',
BODY_STYLE))
mi_stages = [
('Hyperacute (minutes - hours)',
'Tall, peaked ("hyperacute") T waves - earliest sign of ischemia. '
'Seen before ST elevation. Often missed clinically.', ACCENT_ORANGE),
('Acute (hours)',
'ST segment elevation (STEMI): >1 mm in 2 contiguous limb leads or >2 mm in '
'2 contiguous precordial leads. "Tombstone" morphology in severe cases. '
'Reciprocal ST depression in mirror leads.', ACCENT_RED),
('Evolving (hours - days)',
'Pathological Q waves develop: duration >0.04 sec or depth >25% of R wave amplitude. '
'T wave begins to invert. ST elevation starts to resolve.', ACCENT_ORANGE),
('Old/Established (days - permanent)',
'Deep Q waves persist. T wave inversion may persist. '
'ST elevation resolves (persistent elevation >2 weeks suggests ventricular aneurysm).', DARK_GRAY),
]
for stage, desc, col in mi_stages:
story.append(Paragraph(f'<font color="#{col.hexval()[1:]}"><b>● {stage}:</b></font> {desc}', BULLET_STYLE))
story.append(Spacer(1, 2))
story.append(Spacer(1, 6))
story.append(MIDiagram(width=W, height=58*mm))
story.append(Paragraph(
'Figure 4.1 - ECG evolution in MI. Left: Normal. Middle: Acute STEMI (ST elevation, hyperacute T). '
'Right: Old MI (pathological Q wave, T-wave inversion). Based on Tintinalli\'s EM; Goldman-Cecil Medicine.',
CAPTION_STYLE))
story.append(Spacer(1, 6))
story.append(SectionBox('4.2 How to Identify MI by Location', ACCENT_RED))
story.append(Spacer(1, 4))
story.append(Paragraph(
'Localize the infarct by identifying which lead group shows the changes:',
BODY_STYLE))
loc_data = [
[Paragraph('<b>Location</b>', TABLE_HEADER_STYLE),
Paragraph('<b>Leads with ST Elevation</b>', TABLE_HEADER_STYLE),
Paragraph('<b>Reciprocal Changes</b>', TABLE_HEADER_STYLE),
Paragraph('<b>Artery</b>', TABLE_HEADER_STYLE)],
['Inferior', 'II, III, aVF', 'I, aVL', 'RCA (80%), LCx (20%)'],
['Anterior', 'V1-V4', 'II, III, aVF', 'LAD'],
['Anteroseptal', 'V1-V3', '-', 'Proximal LAD'],
['Anterolateral', 'V1-V6, I, aVL', 'II, III, aVF', 'LAD + LCx'],
['Lateral', 'I, aVL, V5-V6', 'II, III, aVF', 'LCx'],
['Posterior', 'Tall R in V1, V2; ST depression V1-V3', 'V7-V9 ST elevation', 'RCA or LCx'],
['Right Ventricular', 'V1; ST elevation V3R, V4R', 'Inferior MI often coexists', 'Proximal RCA'],
]
loc_formatted = [[Paragraph(str(c), TABLE_BODY_STYLE) for c in row] for row in loc_data]
loc_formatted[0] = [Paragraph('<b>Location</b>', TABLE_HEADER_STYLE),
Paragraph('<b>ST Elevation Leads</b>', TABLE_HEADER_STYLE),
Paragraph('<b>Reciprocal Changes</b>', TABLE_HEADER_STYLE),
Paragraph('<b>Artery</b>', TABLE_HEADER_STYLE)]
tl = Table(loc_formatted, colWidths=[W*0.18, W*0.27, W*0.27, W*0.28])
tl.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), ACCENT_RED),
('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.HexColor('#FDEDEC'), WHITE]),
('GRID', (0,0), (-1,-1), 0.5, MID_GRAY),
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
('TOPPADDING', (0,0), (-1,-1), 4),
('BOTTOMPADDING', (0,0), (-1,-1), 4),
('LEFTPADDING', (0,0), (-1,-1), 6),
]))
story.append(tl)
story.append(Paragraph('Table 4.1 - MI Localization Guide. Tintinalli\'s EM; Braunwald\'s Heart Disease, 12e; Goldman-Cecil Medicine.', CAPTION_STYLE))
story.append(Spacer(1, 4))
story.append(KeyBox(
'NEW LBBB (Left Bundle Branch Block) in the context of chest pain is treated as STEMI equivalent - '
'activate the cath lab. Sgarbossa criteria help diagnose MI in the presence of LBBB.',
label='CRITICAL POINT', box_color=colors.HexColor('#FDEDEC'),
text_color=ACCENT_RED))
story.append(Spacer(1, 4))
story.append(SectionBox('4.3 ST Changes: Ischemia vs Injury', ACCENT_RED))
story.append(Spacer(1, 4))
img_st = get_image('st_changes', W*0.85)
if img_st:
story.append(Table([[img_st]], colWidths=[W], style=[('ALIGN',(0,0),(0,0),'CENTER')]))
story.append(Paragraph(
'Figure 4.2 - ST and T changes: (A) J-depression - non-ischemic, upward-sloping ST; '
'(B) Downward-sloping ST = ischemic; (C) Horizontal ST depression = ischemic. '
'(Harriet Lane Handbook, 23e - Park MK, Guntheroth WG)',
CAPTION_STYLE))
story.append(PageBreak())
# ── SECTION 5: ARRHYTHMIAS ────────────────────────────────────────────
story.append(ChapterHeader('SECTION 5', 'Cardiac Arrhythmias on ECG', ACCENT_ORANGE, W))
story.append(Spacer(1, 6))
story.append(SectionBox('5.1 Bradyarrhythmias', ACCENT_ORANGE))
story.append(Spacer(1, 4))
brady_data = [
[Paragraph('<b>Arrhythmia</b>', TABLE_HEADER_STYLE),
Paragraph('<b>ECG Features</b>', TABLE_HEADER_STYLE),
Paragraph('<b>Key Points</b>', TABLE_HEADER_STYLE)],
['Sinus Bradycardia',
'Normal P-QRS-T morphology; rate <60 bpm; regular rhythm',
'Causes: athletes, vasovagal, hypothyroidism, beta blockers, inferior MI'],
['Sinus Arrest / Pause',
'Absent P wave for one or more cycles; pause not a multiple of P-P interval',
'If >3 sec, may need pacemaker'],
['1st Degree AV Block',
'PR interval >0.20 sec (>1 large box); every P followed by QRS',
'Benign; no treatment usually needed'],
['2nd Degree AV Block\n(Mobitz I / Wenckebach)',
'Progressively lengthening PR interval until a P wave is not conducted (dropped QRS); '
'then cycle repeats',
'Usually benign; seen in inferior MI; rarely needs pacemaker'],
['2nd Degree AV Block\n(Mobitz II)',
'Fixed PR interval with sudden non-conducted P wave (dropped QRS, no warning)',
'HIGH RISK: can progress to complete heart block; pacemaker often needed'],
['3rd Degree (Complete) AV Block',
'P waves and QRS complexes completely dissociated; regular P-P and R-R intervals '
'but unrelated to each other; escape rhythm (junctional 40-60, ventricular 20-40 bpm)',
'EMERGENCY: requires pacemaker; causes: inferior MI (often reversible), Lyme disease, medications'],
]
brady_formatted = [[Paragraph(str(c), TABLE_BODY_STYLE) for c in row] for row in brady_data]
brady_formatted[0] = [Paragraph('<b>Arrhythmia</b>', TABLE_HEADER_STYLE),
Paragraph('<b>ECG Features</b>', TABLE_HEADER_STYLE),
Paragraph('<b>Key Points</b>', TABLE_HEADER_STYLE)]
tb = Table(brady_formatted, colWidths=[W*0.22, W*0.42, W*0.36])
tb.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), colors.HexColor('#784212')),
('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.HexColor('#FEF9E7'), WHITE]),
('GRID', (0,0), (-1,-1), 0.5, MID_GRAY),
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
('TOPPADDING', (0,0), (-1,-1), 4),
('BOTTOMPADDING', (0,0), (-1,-1), 5),
('LEFTPADDING', (0,0), (-1,-1), 6),
]))
story.append(tb)
story.append(Paragraph('Table 5.1 - Bradyarrhythmias. References: Harriet Lane Handbook 23e, Braunwald\'s Heart Disease.', CAPTION_STYLE))
story.append(Spacer(1, 8))
story.append(SectionBox('5.2 Supraventricular Tachyarrhythmias', ACCENT_ORANGE))
story.append(Spacer(1, 4))
supra_data = [
[Paragraph('<b>Arrhythmia</b>', TABLE_HEADER_STYLE),
Paragraph('<b>Rate</b>', TABLE_HEADER_STYLE),
Paragraph('<b>ECG Features</b>', TABLE_HEADER_STYLE),
Paragraph('<b>Key ID Steps</b>', TABLE_HEADER_STYLE)],
['Sinus Tachycardia', '>100 bpm',
'Normal P before each QRS; upright P in II; normal QRS',
'Look for cause: pain, fever, hypovolemia, PE'],
['Premature Atrial Contraction (PAC)', 'Variable',
'Early P wave with different morphology from sinus P; usually followed by normal QRS',
'Ectopic atrial focus; usually benign'],
['Atrial Tachycardia (AT)', '150-250 bpm',
'Abnormal P wave morphology (ectopic); regular; may have AV block',
'P waves differ from sinus; often "warm up" at onset'],
['Atrial Flutter', '250-350 bpm\n(atrial); ventricular rate depends on AV conduction',
'"Sawtooth" flutter waves at ~300 bpm; most common 2:1 AV conduction giving ventricular rate ~150 bpm; '
'no isoelectric baseline between flutter waves (best seen in II, III, aVF)',
'Rate ~150 bpm should always raise suspicion for atrial flutter with 2:1 block'],
['Atrial Fibrillation (AF)', 'Irregular; atrial 400-600 bpm',
'Absent P waves; irregular baseline (fibrillatory "f" waves); irregularly irregular QRS; '
'normal QRS morphology (unless aberrant conduction)',
'Irregularly irregular = AF until proven otherwise'],
['AVNRT (SVT)', '150-250 bpm',
'Narrow QRS tachycardia; P waves buried in QRS or just after (pseudo-R\' in V1, pseudo-S in II); '
'abrupt onset/offset',
'Most common form of paroxysmal SVT; responds to vagal maneuvers/adenosine'],
['AVRT (WPW)', '150-250 bpm',
'Delta wave (slurred QRS upstroke), short PR (<0.12 sec), widened QRS in sinus rhythm. '
'During tachycardia: narrow (orthodromic) or wide (antidromic) QRS',
'Risk of rapid conduction in AF - AVOID adenosine/digoxin'],
['Junctional Tachycardia', '60-130 bpm',
'No visible P waves or retrograde P waves (negative in II, positive in aVR); normal narrow QRS; '
'regular',
'Caused by digitalis toxicity, inferior MI, post-cardiac surgery'],
]
supra_formatted = [[Paragraph(str(c), TABLE_BODY_STYLE) for c in row] for row in supra_data]
supra_formatted[0] = [Paragraph('<b>Arrhythmia</b>', TABLE_HEADER_STYLE),
Paragraph('<b>Rate</b>', TABLE_HEADER_STYLE),
Paragraph('<b>ECG Features</b>', TABLE_HEADER_STYLE),
Paragraph('<b>ID Steps</b>', TABLE_HEADER_STYLE)]
ts = Table(supra_formatted, colWidths=[W*0.17, W*0.11, W*0.40, W*0.32])
ts.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), ACCENT_ORANGE),
('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.HexColor('#FEF5E7'), WHITE]),
('GRID', (0,0), (-1,-1), 0.5, MID_GRAY),
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
('TOPPADDING', (0,0), (-1,-1), 4),
('BOTTOMPADDING', (0,0), (-1,-1), 5),
('LEFTPADDING', (0,0), (-1,-1), 6),
]))
story.append(ts)
story.append(Paragraph('Table 5.2 - Supraventricular Tachyarrhythmias. Braunwald\'s Heart Disease 12e; Harriet Lane Handbook 23e.', CAPTION_STYLE))
story.append(Spacer(1, 6))
# Arrhythmia ECG image
img_arr = get_image('arrhythmias', W*0.95)
if img_arr:
story.append(Table([[img_arr]], colWidths=[W], style=[('ALIGN',(0,0),(0,0),'CENTER')]))
story.append(Paragraph(
'Figure 5.1 - Supraventricular arrhythmias. PAC (early P wave), Atrial tachycardia (ectopic P waves), '
'Atrial flutter (sawtooth), SVT (no visible P), Atrial fibrillation (irregular baseline, irregular QRS). '
'(Harriet Lane Handbook 23e - Park MK, Guntheroth WG)',
CAPTION_STYLE))
story.append(PageBreak())
# ── SECTION 5.3: VENTRICULAR ARRHYTHMIAS ─────────────────────────────
story.append(ChapterHeader('SECTION 5 (continued)', 'Ventricular Arrhythmias', ACCENT_RED, W))
story.append(Spacer(1, 6))
story.append(SectionBox('5.3 Ventricular Arrhythmias', ACCENT_RED))
story.append(Spacer(1, 4))
vent_data = [
[Paragraph('<b>Arrhythmia</b>', TABLE_HEADER_STYLE),
Paragraph('<b>ECG Features</b>', TABLE_HEADER_STYLE),
Paragraph('<b>Clinical Notes</b>', TABLE_HEADER_STYLE)],
['Premature Ventricular\nContraction (PVC)',
'Early, wide (>0.12 sec), bizarre QRS complex; full compensatory pause; '
'T wave in opposite direction to QRS; no preceding P wave',
'Bigeminy: alternating PVC with normal beat. Trigeminy: 2 normal then 1 PVC. '
'Couplets: 2 consecutive PVCs. Benign unless underlying disease.'],
['Ventricular Tachycardia (VT)',
'3 or more PVCs at rapid rate (120-250 bpm); wide QRS (>0.12 sec); '
'AV dissociation (P waves unrelated to QRS); '
'fusion beats and capture beats (pathognomonic); '
'monomorphic (same QRS morphology) or polymorphic',
'EMERGENCY if hemodynamically unstable: cardioversion.\n'
'Stable VT: antiarrhythmics (amiodarone, lidocaine).\n'
'70% have underlying heart disease.'],
['Torsades de Pointes\n(Polymorphic VT)',
'Polymorphic VT with "twisting" QRS axis around the isoelectric line; '
'long QTc (>500 ms) during baseline sinus rhythm precedes it; '
'rate 200-250 bpm; initiates with long-short coupling',
'Causes: prolonged QT (drugs, electrolyte abnormalities - hypoK, hypoMg, hypoCa).\n'
'Treatment: IV magnesium, correct electrolytes, remove offending drugs, temporary pacing.'],
['Ventricular Fibrillation (VF)',
'Completely disorganized electrical activity; no identifiable QRS, P or T waves; '
'chaotic waveforms of varying amplitude and morphology; '
'completely irregular',
'CARDIAC ARREST: immediate defibrillation + CPR.\n'
'No cardiac output possible during VF.'],
['Accelerated\nIdioventricular Rhythm',
'Wide QRS at 40-100 bpm; AV dissociation; fusion beats common; '
'occurs when ventricular rate exceeds sinus rate',
'Common after reperfusion (thrombolysis/PCI) - "reperfusion arrhythmia".\n'
'Usually self-limiting; generally benign if hemodynamically stable.'],
]
vent_formatted = [[Paragraph(str(c), TABLE_BODY_STYLE) for c in row] for row in vent_data]
vent_formatted[0] = [Paragraph('<b>Arrhythmia</b>', TABLE_HEADER_STYLE),
Paragraph('<b>ECG Features</b>', TABLE_HEADER_STYLE),
Paragraph('<b>Clinical Notes</b>', TABLE_HEADER_STYLE)]
tv = Table(vent_formatted, colWidths=[W*0.20, W*0.42, W*0.38])
tv.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), ACCENT_RED),
('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.HexColor('#FDEDEC'), WHITE]),
('GRID', (0,0), (-1,-1), 0.5, MID_GRAY),
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
('TOPPADDING', (0,0), (-1,-1), 4),
('BOTTOMPADDING', (0,0), (-1,-1), 5),
('LEFTPADDING', (0,0), (-1,-1), 6),
]))
story.append(tv)
story.append(Paragraph('Table 5.3 - Ventricular Arrhythmias. Harriet Lane Handbook 23e; Braunwald\'s Heart Disease 12e; Tintinalli\'s EM.', CAPTION_STYLE))
story.append(PageBreak())
# ── SECTION 6: CONDUCTION BLOCKS ──────────────────────────────────────
story.append(ChapterHeader('SECTION 6', 'Conduction Blocks & Bundle Branch Blocks', ACCENT_PURPLE, W))
story.append(Spacer(1, 6))
story.append(SectionBox('6.1 Bundle Branch Blocks', ACCENT_PURPLE))
story.append(Spacer(1, 4))
story.append(Paragraph(
'Bundle branch blocks occur when conduction is blocked in the right or left bundle branch, '
'causing the ventricles to depolarize sequentially rather than simultaneously. '
'Key criterion: <b>QRS duration >0.12 sec (3 small boxes)</b>.',
BODY_STYLE))
bbb_data = [
[Paragraph('<b>Block</b>', TABLE_HEADER_STYLE),
Paragraph('<b>ECG Criteria</b>', TABLE_HEADER_STYLE),
Paragraph('<b>Mnemonic / Remember</b>', TABLE_HEADER_STYLE)],
['Right Bundle\nBranch Block (RBBB)',
'1. QRS >0.12 sec\n'
'2. rsR\' ("rabbit ears") or RSR\' in V1, V2\n'
'3. Wide, slurred S wave in I, V5, V6\n'
'4. ST depression, T inversion in V1-V3\n'
'5. Normal septal Q waves preserved',
'MaRRoW: M-shaped in V1 (RBBB)\n'
'Wide S in V5/V6\n'
'Can be normal variant'],
['Left Bundle\nBranch Block (LBBB)',
'1. QRS >0.12 sec\n'
'2. Broad, notched R wave (or M-shaped) in I, aVL, V5, V6\n'
'3. No septal Q waves in I, V5, V6\n'
'4. Small r then deep, wide S in V1, V2\n'
'5. ST & T waves discordant throughout',
'WiLLiaM: W in V1, M in V5 (LBBB)\n'
'New LBBB + chest pain = treat as STEMI\n'
'Concordant ST changes in LBBB = very high specificity for MI (Sgarbossa)'],
['Left Anterior\nFascicular Block (LAFB)',
'1. Left axis deviation (axis between -45 and -90 degrees)\n'
'2. Normal QRS duration (<0.12 sec)\n'
'3. Small r, deep S in II, III, aVF\n'
'4. Small Q, tall R in I, aVL\n'
'5. Late precordial transition',
'Most common fascicular block\n'
'Causes: anterior MI, hypertension, cardiomyopathy'],
['Left Posterior\nFascicular Block (LPFB)',
'1. Right axis deviation (axis +90 to +180 degrees)\n'
'2. Normal QRS duration\n'
'3. Small Q, tall R in II, III, aVF\n'
'4. Small r, deep S in I, aVL\n'
'5. Exclude other causes of RAD first',
'Rare; diagnosis of exclusion\n'
'Exclude: RVH, vertical heart, lateral MI, PE'],
['Bifascicular Block\n(RBBB + LAFB)',
'RBBB pattern + Left axis deviation',
'Risk of progression to complete heart block\n'
'Consider pacemaker if symptomatic'],
]
bbb_formatted = [[Paragraph(str(c), TABLE_BODY_STYLE) for c in row] for row in bbb_data]
bbb_formatted[0] = [Paragraph('<b>Block</b>', TABLE_HEADER_STYLE),
Paragraph('<b>ECG Criteria</b>', TABLE_HEADER_STYLE),
Paragraph('<b>Mnemonic / Note</b>', TABLE_HEADER_STYLE)]
tbbb = Table(bbb_formatted, colWidths=[W*0.19, W*0.45, W*0.36])
tbbb.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), ACCENT_PURPLE),
('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.HexColor('#F5EEF8'), WHITE]),
('GRID', (0,0), (-1,-1), 0.5, MID_GRAY),
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
('TOPPADDING', (0,0), (-1,-1), 4),
('BOTTOMPADDING', (0,0), (-1,-1), 5),
('LEFTPADDING', (0,0), (-1,-1), 6),
]))
story.append(tbbb)
story.append(Paragraph('Table 6.1 - Bundle Branch Blocks. Goldman-Cecil Medicine 26e; Braunwald\'s Heart Disease 12e.', CAPTION_STYLE))
story.append(Spacer(1, 4))
# Bundle branch image
img_bb = get_image('bundle_branch', W*0.90)
if img_bb:
story.append(Table([[img_bb]], colWidths=[W], style=[('ALIGN',(0,0),(0,0),'CENTER')]))
story.append(Paragraph(
'Figure 6.1 - Left Anterior Fascicular Block (LAFB) in a standard 12-lead ECG. Note left axis deviation '
'and delayed precordial transition. (Goldman-Cecil Medicine, 26e)',
CAPTION_STYLE))
story.append(PageBreak())
# ── SECTION 7: OTHER PATHOLOGIES ──────────────────────────────────────
story.append(ChapterHeader('SECTION 7', 'Other Important ECG Patterns', ACCENT_GREEN, W))
story.append(Spacer(1, 6))
story.append(SectionBox('7.1 Chamber Hypertrophy', ACCENT_GREEN))
story.append(Spacer(1, 4))
hyper_data = [
[Paragraph('<b>Condition</b>', TABLE_HEADER_STYLE),
Paragraph('<b>ECG Criteria</b>', TABLE_HEADER_STYLE)],
['Left Ventricular\nHypertrophy (LVH)',
'Sokolow-Lyon: S in V1 + R in V5 or V6 > 35 mm\n'
'Cornell: R in aVL + S in V3 > 28 mm (M) or > 20 mm (F)\n'
'Left axis deviation; prolonged QRS; ST depression and T inversion in V4-V6 (strain pattern)'],
['Right Ventricular\nHypertrophy (RVH)',
'Tall R wave in V1 (R > S in V1)\n'
'Right axis deviation (>+90 degrees)\n'
'Deep S in V5, V6\n'
'ST depression, T inversion V1-V3\n'
'Causes: pulmonary hypertension, COPD, pulmonic stenosis'],
['Left Atrial\nEnlargement (LAE)',
'P wave duration >0.12 sec (3 small boxes)\n'
'Notched "bifid" P wave in limb leads (P mitrale)\n'
'Biphasic P in V1 with prominent negative terminal component\n'
'Causes: mitral stenosis, LVH, heart failure'],
['Right Atrial\nEnlargement (RAE)',
'P wave amplitude >2.5 mm in II (P pulmonale)\n'
'Tall, peaked P waves in II, III, aVF\n'
'Causes: pulmonary hypertension, COPD, tricuspid stenosis'],
]
hyper_formatted = [[Paragraph(str(c), TABLE_BODY_STYLE) for c in row] for row in hyper_data]
hyper_formatted[0] = [Paragraph('<b>Condition</b>', TABLE_HEADER_STYLE),
Paragraph('<b>ECG Criteria</b>', TABLE_HEADER_STYLE)]
th = Table(hyper_formatted, colWidths=[W*0.22, W*0.78])
th.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), ACCENT_GREEN),
('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.HexColor('#EAFAF1'), WHITE]),
('GRID', (0,0), (-1,-1), 0.5, MID_GRAY),
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
('TOPPADDING', (0,0), (-1,-1), 4),
('BOTTOMPADDING', (0,0), (-1,-1), 5),
('LEFTPADDING', (0,0), (-1,-1), 6),
]))
story.append(th)
story.append(Paragraph('Table 7.1 - Chamber Hypertrophy. Goldman-Cecil Medicine 26e; Braunwald\'s Heart Disease.', CAPTION_STYLE))
story.append(Spacer(1, 6))
story.append(SectionBox('7.2 Electrolyte & Drug Effects', ACCENT_GREEN))
story.append(Spacer(1, 4))
elec_data = [
[Paragraph('<b>Condition</b>', TABLE_HEADER_STYLE),
Paragraph('<b>ECG Changes</b>', TABLE_HEADER_STYLE)],
['Hyperkalemia',
'Peaked (tall, narrow "tent-shaped") T waves (early)\n'
'Flattening/disappearance of P waves\n'
'Widening of QRS\n'
'Sine wave pattern (severe, pre-arrest)\n'
'Ventricular fibrillation (late)'],
['Hypokalemia',
'Flattened or inverted T waves\n'
'Prominent U waves (U > T amplitude)\n'
'Apparent QT prolongation (actually QU prolongation)\n'
'Increased risk of TdP and ventricular arrhythmias'],
['Hypercalcemia',
'Short QT interval (shortened ST segment)\n'
'J waves (Osborn waves) occasionally\n'
'Bradycardia in severe cases'],
['Hypocalcemia',
'Prolonged QT interval (prolonged ST segment)\n'
'T wave usually normal; no effect on QRS or P'],
['Digitalis Effect\n(therapeutic)',
'"Salvador Dali mustache" - scooped or "reverse tick" ST depression\n'
'Shortened QT interval\n'
'PR prolongation (slows AV conduction)\n'
'T wave flattening or inversion'],
['Digitalis Toxicity',
'Ventricular ectopy (bigeminy)\n'
'Regularization of AF (junctional tachycardia + AV block)\n'
'Bidirectional VT (pathognomonic)\n'
'Accelerated junctional rhythm'],
['QT-Prolonging Drugs',
'Prolonged QTc; risk of TdP\n'
'Common culprits: antipsychotics (haloperidol, quetiapine), antibiotics (azithromycin, fluoroquinolones), '
'antiarrhythmics (amiodarone, sotalol, quinidine), methadone, tricyclic antidepressants'],
]
elec_formatted = [[Paragraph(str(c), TABLE_BODY_STYLE) for c in row] for row in elec_data]
elec_formatted[0] = [Paragraph('<b>Condition</b>', TABLE_HEADER_STYLE),
Paragraph('<b>ECG Changes</b>', TABLE_HEADER_STYLE)]
te = Table(elec_formatted, colWidths=[W*0.24, W*0.76])
te.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), ACCENT_GREEN),
('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.HexColor('#EAFAF1'), WHITE]),
('GRID', (0,0), (-1,-1), 0.5, MID_GRAY),
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
('TOPPADDING', (0,0), (-1,-1), 4),
('BOTTOMPADDING', (0,0), (-1,-1), 5),
('LEFTPADDING', (0,0), (-1,-1), 6),
]))
story.append(te)
story.append(Paragraph('Table 7.2 - Electrolyte and Drug Effects. Guyton & Hall; Braunwald\'s Heart Disease; Goldman-Cecil Medicine.', CAPTION_STYLE))
story.append(Spacer(1, 6))
story.append(SectionBox('7.3 Other Distinctive Patterns', ACCENT_GREEN))
story.append(Spacer(1, 4))
other_data = [
[Paragraph('<b>Pattern</b>', TABLE_HEADER_STYLE),
Paragraph('<b>ECG Features</b>', TABLE_HEADER_STYLE),
Paragraph('<b>Cause / Significance</b>', TABLE_HEADER_STYLE)],
['Pericarditis',
'Diffuse (saddle-shaped) ST elevation in multiple leads\n'
'PR depression (most specific)\n'
'No reciprocal ST depression (except in aVR)\n'
'Evolves through 4 stages',
'Pericardial inflammation; distinguish from MI by diffuse distribution, PR depression'],
['Pulmonary Embolism (PE)',
'Sinus tachycardia (most common)\n'
'S1Q3T3 pattern: deep S in I, Q wave and T inversion in III\n'
'New RBBB\n'
'Right heart strain pattern\n'
'T inversion V1-V3',
'S1Q3T3 is specific but not sensitive; occurs in ~20%\n'
'Sinus tachycardia is the most common finding in PE'],
['Hypothermia',
'J waves (Osborn waves) - positive deflection at J point\n'
'Prolonged all intervals\n'
'Sinus bradycardia\n'
'Atrial fibrillation common',
'J waves best seen in V4-V6; size correlates with degree of hypothermia'],
['Brugada Syndrome',
'Type 1 (diagnostic): Coved ST elevation >2 mm in V1-V2 with RBBB morphology, followed by negative T wave\n'
'Type 2/3: "Saddle-back" ST elevation\n'
'Can be dynamic (concealed at baseline)',
'Sodium channelopathy; risk of sudden cardiac death\n'
'Unmask with ajmaline/flecainide challenge\n'
'ICD for survivors'],
['Long QT Syndrome',
'QTc >440 ms (M) or >460 ms (F)\n'
'T wave morphology abnormalities\n'
'Risk of Torsades de Pointes\n'
'Triggered by exercise or emotion (congenital); drugs (acquired)',
'LQT1: broad-based T wave; triggered by exercise\n'
'LQT2: notched T wave; triggered by sudden noise\n'
'LQT3: long flat ST; risk at rest/sleep'],
['Early Repolarization',
'J point elevation at end of QRS in multiple leads\n'
'Notched J point or slurred S wave terminal portion\n'
'Usually inferior and lateral leads\n'
'ST elevation <1 mm, concave upward, with upright T waves',
'Usually benign variant; prevalence ~2-5%\n'
'High-risk pattern: horizontal/descending ST, inferior leads - small VF risk'],
['WPW (Pre-excitation)',
'Short PR interval (<0.12 sec)\n'
'Delta wave (slurred QRS upstroke)\n'
'Widened QRS\n'
'ST-T changes discordant with QRS',
'Accessory pathway (Bundle of Kent)\n'
'Can cause rapid AF with wide complex tachycardia - dangerous\n'
'Refer for electrophysiology study'],
]
other_formatted = [[Paragraph(str(c), TABLE_BODY_STYLE) for c in row] for row in other_data]
other_formatted[0] = [Paragraph('<b>Pattern</b>', TABLE_HEADER_STYLE),
Paragraph('<b>ECG Features</b>', TABLE_HEADER_STYLE),
Paragraph('<b>Cause / Significance</b>', TABLE_HEADER_STYLE)]
to = Table(other_formatted, colWidths=[W*0.18, W*0.44, W*0.38])
to.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), colors.HexColor('#1A5276')),
('ROWBACKGROUNDS', (0,1), (-1,-1), [LIGHT_BLUE, WHITE]),
('GRID', (0,0), (-1,-1), 0.5, MID_GRAY),
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
('TOPPADDING', (0,0), (-1,-1), 4),
('BOTTOMPADDING', (0,0), (-1,-1), 5),
('LEFTPADDING', (0,0), (-1,-1), 6),
]))
story.append(to)
story.append(Paragraph('Table 7.3 - Other ECG Patterns. Goldman-Cecil Medicine 26e; Braunwald\'s Heart Disease 12e; Tintinalli\'s EM.', CAPTION_STYLE))
story.append(PageBreak())
# ── SECTION 8: QUICK REFERENCE ────────────────────────────────────────
story.append(ChapterHeader('SECTION 8', 'Quick Identification Reference Cards', DARK_BLUE, W))
story.append(Spacer(1, 6))
story.append(SectionBox('8.1 Wide Complex Tachycardia Algorithm'))
story.append(Spacer(1, 4))
story.append(Paragraph(
'When faced with a wide complex tachycardia (WCT = rate >100 bpm, QRS >0.12 sec):',
BODY_STYLE))
wct_steps = [
'1. Is the patient hemodynamically stable? If NO - synchronized cardioversion immediately.',
'2. Is it regular or irregular?',
' - Regular WCT: VT (most likely, especially if >45 yrs or structural heart disease), '
'SVT with aberrancy, SVT with bundle branch block, antidromic AVRT (WPW)',
' - Irregular WCT: AF with aberrancy (RBBB or LBBB), AF with WPW (MOST dangerous), '
'Polymorphic VT, Torsades de Pointes',
'3. Features favoring VT over SVT with aberrancy:',
' - AV dissociation (independent P waves) - most specific',
' - Capture beats or fusion beats - pathognomonic',
' - Extreme axis deviation (northwest axis: -90 to -180 degrees)',
' - QRS concordance across precordial leads (all positive or all negative V1-V6)',
' - QRS width >0.14 sec (RBBB morphology) or >0.16 sec (LBBB morphology)',
' - Age >35 years and structural heart disease strongly favor VT',
'4. When in doubt: treat as VT - amiodarone is safe for both VT and SVT with aberrancy',
' NEVER give adenosine or verapamil for irregular WCT (risk in AF+WPW - can cause VF)',
]
for step in wct_steps:
story.append(Paragraph(step, BULLET_STYLE))
story.append(Spacer(1, 6))
story.append(SectionBox('8.2 Differential Diagnosis: ST Elevation'))
story.append(Spacer(1, 4))
st_elev_causes = [
('STEMI', 'Focal leads, reciprocal changes, hyperacute T, new LBBB', ACCENT_RED),
('Pericarditis', 'Diffuse leads, saddle-shaped, PR depression, no reciprocal changes', ACCENT_ORANGE),
('Early Repolarization', 'Young, inferior/lateral, notched J point, <1 mm, benign', ACCENT_GREEN),
('Brugada', 'V1-V2 only, coved morphology, no dynamic ischemic changes', ACCENT_PURPLE),
('LVH / LBBB', 'Discordant ST in appropriate leads, no ischemic evolution', DARK_GRAY),
('Hyperkalemia', 'Diffuse, peaked T waves, wide QRS, absent P waves', MED_BLUE),
('Pulmonary Embolism', 'V1-V3 in setting of tachycardia, RBBB, S1Q3T3', MED_BLUE),
('Ventricular Aneurysm', 'Persistent ST elevation >2 weeks in old MI leads', DARK_GRAY),
('Vasospasm (Prinzmetal)', 'Transient, resolves with nitroglycerin, may be severe', ACCENT_RED),
('Myocarditis', 'Diffuse ST elevation, fever, elevated troponin, young patient', ACCENT_ORANGE),
]
for diagnosis, features, col in st_elev_causes:
story.append(Paragraph(
f'<font color="#{col.hexval()[1:]}"><b>• {diagnosis}:</b></font> {features}',
BULLET_STYLE))
story.append(Spacer(1, 1))
story.append(Spacer(1, 8))
story.append(SectionBox('8.3 Narrow vs Wide Complex Tachycardia at a Glance'))
story.append(Spacer(1, 4))
nw_data = [
[Paragraph('<b>Feature</b>', TABLE_HEADER_STYLE),
Paragraph('<b>Narrow Complex (<0.12 sec)</b>', TABLE_HEADER_STYLE),
Paragraph('<b>Wide Complex (>0.12 sec)</b>', TABLE_HEADER_STYLE)],
['QRS Width', '<0.12 sec (3 small boxes)', '>0.12 sec - BBB, aberrancy, or ventricular'],
['Origin', 'Supraventricular (above His bundle)', 'Ventricular or SVT with aberrant conduction'],
['P waves', 'Often visible; may be before, during, or after QRS', 'Often dissociated or absent'],
['Regular rhythm examples', 'Sinus tachycardia, AVNRT, AT, Atrial flutter', 'Monomorphic VT, SVT+BBB, antidromic AVRT'],
['Irregular rhythm examples', 'Atrial fibrillation, multifocal AT', 'AF+WPW, polymorphic VT, TdP'],
['Safe drug choices', 'Adenosine, beta-blockers, calcium channel blockers', 'Amiodarone, lidocaine (NOT adenosine for WCT unless certain SVT)'],
]
nw_formatted = [[Paragraph(str(c), TABLE_BODY_STYLE) for c in row] for row in nw_data]
nw_formatted[0] = [Paragraph('<b>Feature</b>', TABLE_HEADER_STYLE),
Paragraph('<b>Narrow Complex</b>', TABLE_HEADER_STYLE),
Paragraph('<b>Wide Complex</b>', TABLE_HEADER_STYLE)]
tnw = Table(nw_formatted, colWidths=[W*0.22, W*0.39, W*0.39])
tnw.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), DARK_BLUE),
('ROWBACKGROUNDS', (0,1), (-1,-1), [LIGHT_BLUE, WHITE]),
('GRID', (0,0), (-1,-1), 0.5, MID_GRAY),
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
('TOPPADDING', (0,0), (-1,-1), 4),
('BOTTOMPADDING', (0,0), (-1,-1), 4),
('LEFTPADDING', (0,0), (-1,-1), 6),
]))
story.append(tnw)
story.append(Spacer(1, 8))
# ── REFERENCES ────────────────────────────────────────────────────────
story.append(HRFlowable(width=W, thickness=1, color=MID_GRAY))
story.append(Spacer(1, 6))
story.append(Paragraph('<b>References</b>', SECTION_STYLE))
refs = [
'1. Hall JE, Hall ME. Guyton and Hall Textbook of Medical Physiology, 14th Edition. Philadelphia: Elsevier; 2021.',
'2. Goldman L, Schafer AI, eds. Goldman-Cecil Medicine, 26th Edition. Philadelphia: Elsevier; 2020.',
'3. Hughes HK, Kahl LK, eds. The Harriet Lane Handbook, 23rd Edition. Philadelphia: Elsevier; 2021.',
'4. Tintinalli JE, et al. Tintinalli\'s Emergency Medicine: A Comprehensive Study Guide, 9th Edition. New York: McGraw-Hill; 2020.',
'5. Libby P, et al. Braunwald\'s Heart Disease, 12th Edition. Philadelphia: Elsevier; 2022.',
'6. Park MK, Guntheroth WG. How to Read Pediatric ECGs, 4th Edition. Philadelphia: Elsevier; 2006.',
]
for ref in refs:
story.append(Paragraph(ref, REF_STYLE))
story.append(Spacer(1, 2))
# ── BUILD ─────────────────────────────────────────────────────────────
doc.build(story, onFirstPage=cover_page, onLaterPages=later_pages)
print(f'PDF created: {OUTPUT_PDF}')
if __name__ == '__main__':
build_pdf()
cd /home/daytona/workspace/ecg-guide && python3 build_ecg_pdf.py 2>&1
python3 -c "from reportlab.lib import colors; c = colors.HexColor('#E67E22'); print(repr(c.hexval())); print(c.hexval()); print('%06x' % c.rgb()[0] if False else 'ok'); print(hex(int(c.red*255))[2:] + hex(int(c.green*255))[2:] + hex(int(c.blue*255))[2:])"
cd /home/daytona/workspace/ecg-guide && sed -i "s/col\.hexval()\[1:\]/col.hexval().replace('0x','').zfill(6)/g" build_ecg_pdf.py && python3 build_ecg_pdf.py 2>&1
cd /home/daytona/workspace/ecg-guide && python3 -c " content = open('build_ecg_pdf.py').read() content = content.replace(\"col.hexval()[1:]\", \"col.hexval().replace('0x','').zfill(6)\") open('build_ecg_pdf.py', 'w').write(content) print('done') " && python3 build_ecg_pdf.py 2>&1