lung examination
lung examination percussion auscultation breath sounds
chest auscultation zones respiratory system clinical examination
barrel chest hyperinflation COPD emphysema

This diagnostic image pair displays thoracic radiology findings in an 80-year-old male with Chronic Obstructive Pulmonary Disease (COPD). The left panel shows a frontal chest radiograph (X-ray) exhibiting hallmark signs of hyperinflation, including a widened anteroposterior diameter and flattened diaphragmatic domes. Both lung fields demonstrate sparse bronchovascular markings and increased lucency, indicative of diffuse emphysema. The right panel shows an axial CT scan of the thorax, further characterizing the pulmonary pathology. It reveals significant emphysematous destruction of the lung parenchyma with the presence of multiple bullae and areas of decreased attenuation. Additionally, subtle interstitial lesions are visible. The imaging illustrates the classic presentation of severe COPD, characterized by permanent enlargement of airspaces distal to the terminal bronchioles and associated structural changes to the chest wall often described as a barrel chest.

This diagnostic image displays a two-panel chest roentgenogram (X-ray) from a 65-year-old male, highlighting radiographic features of severe pulmonary emphysema and chronic obstructive pulmonary disease (COPD). Panel A presents a postero-anterior (PA) view, while Panel B provides a lateral view. Key findings include significant hyperinflation of the lung fields with marked flattening of the hemidiaphragms (indicated by white arrows), which loss their normal convex shape. In Panel A, a yellow arrow identifies areas of increased translucency and attenuation of vascular markings consistent with emphysematous parenchymal changes. A red arrow highlights the decreased zone of apposition, representing the diminished area where the diaphragm sits parallel to the inner rib cage. Panel B further illustrates the flattened diaphragmatic contour and an increased retrosternal clear space, reflecting an increased anterior-posterior thoracic diameter (barrel chest). These visual markers are clinically significant as they correlate with the mechanical basis of Hoover's sign, where the horizontal orientation of the diaphragm causes inward rib cage retraction during inspiration.
pleural effusion consolidation pneumothorax percussion findings comparison

| Finding | Cause |
|---|---|
| Dullness / Flatness | Pleural effusion, consolidation (pneumonia), collapse |
| Hyperresonance / Tympany | Pneumothorax, emphysema/hyperinflation |
| Stony dullness | Large pleural effusion |
| Type | Location | Character |
|---|---|---|
| Vesicular | Peripheral lung | Soft, low-pitched, continuous; inspiration longer than expiration |
| Bronchial (tubular) | Over trachea | Loud, high-pitched; expiration longer than inspiration |
| Bronchovesicular | Main bronchi (sternal angle area) | Intermediate |
| Sound | Description | Association |
|---|---|---|
| Crackles (rales) | Discontinuous, popping sounds | Fine crackles at bases: pulmonary edema; fine "Velcro" crackles: IPF; coarse crackles: pneumonia, bronchiectasis |
| Wheezes | Continuous, high-pitched, musical, usually expiratory | Asthma, COPD (airway narrowing) |
| Rhonchi | Low-pitched, continuous | Mucus in large airways; bronchitis, bronchiectasis |
| Stridor | High-pitched, monophonic inspiratory sound, louder over neck | Upper airway obstruction (laryngeal edema, foreign body) |
| Pleural rub | Creaking or leathery sound synchronous with breathing | Pleuritis; disappears when significant effusion develops |
| Disorder | Inspection | Palpation | Percussion | Auscultation |
|---|---|---|---|---|
| Asthma (acute) | Hyperinflation; accessory muscles | Impaired expansion; decreased fremitus | Hyperresonance; low diaphragm | Prolonged expiration; inspiratory + expiratory wheeze |
| Pneumothorax | Lag on affected side | Absent fremitus | Hyperresonant / tympanitic | Absent breath sounds |
| Pleural effusion | Lag on affected side | Decreased fremitus; trachea shifted away | Stony dullness | Absent breath sounds |
| Atelectasis | Lag on affected side | Decreased fremitus; trachea shifted toward | Dullness | Absent breath sounds |
| Consolidation (pneumonia) | Possible splinting | Increased fremitus | Dullness | Bronchial sounds; bronchophony; egophony; crackles |
| Emphysema/COPD | Barrel chest; Hoover sign | Decreased expansion | Hyperresonance | Decreased vesicular; prolonged expiration; possible wheeze |
Create a quick reference PDF guide for lung exam findings
ls -la /tmp/workspace/ 2>/dev/null || echo "workspace empty or missing"
mkdir -p /tmp/workspace/lung-exam-pdf
~/lung-exam-pdf/build_pdf.py
from reportlab.platypus import (
SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer,
HRFlowable, KeepTogether
)
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import cm, mm
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
from reportlab.platypus import PageBreak
OUTPUT = "/tmp/workspace/lung-exam-pdf/Lung_Exam_Quick_Reference.pdf"
# ── Colour palette ──────────────────────────────────────────────────────────
C_DARK_BLUE = colors.HexColor("#1a3a5c")
C_MID_BLUE = colors.HexColor("#2a6496")
C_LIGHT_BLUE = colors.HexColor("#d6eaf8")
C_ACCENT = colors.HexColor("#e8f4fd")
C_ORANGE = colors.HexColor("#e67e22")
C_GREEN = colors.HexColor("#1e8449")
C_RED = colors.HexColor("#c0392b")
C_HEADER_ROW = colors.HexColor("#1a3a5c")
C_ALT_ROW = colors.HexColor("#eaf4fb")
C_WHITE = colors.white
C_LIGHT_GREY = colors.HexColor("#f4f6f7")
C_BORDER = colors.HexColor("#85929e")
PAGE_W, PAGE_H = A4
MARGIN = 1.8 * cm
# ── Document ─────────────────────────────────────────────────────────────────
doc = SimpleDocTemplate(
OUTPUT,
pagesize=A4,
leftMargin=MARGIN, rightMargin=MARGIN,
topMargin=MARGIN, bottomMargin=MARGIN,
title="Lung Examination Quick Reference",
author="Orris Medical"
)
styles = getSampleStyleSheet()
W = PAGE_W - 2 * MARGIN # usable width
# ── Custom styles ─────────────────────────────────────────────────────────────
def S(name, **kw):
return ParagraphStyle(name, **kw)
title_style = S("Title2",
fontName="Helvetica-Bold", fontSize=22,
textColor=C_WHITE, alignment=TA_CENTER,
spaceAfter=4, leading=28)
subtitle_style = S("Subtitle2",
fontName="Helvetica", fontSize=11,
textColor=colors.HexColor("#d6eaf8"), alignment=TA_CENTER,
spaceAfter=2)
section_style = S("Section",
fontName="Helvetica-Bold", fontSize=12,
textColor=C_WHITE, backColor=C_DARK_BLUE,
leftIndent=6, rightIndent=6,
spaceBefore=10, spaceAfter=4, leading=16,
borderPad=4)
sub_section_style = S("SubSection",
fontName="Helvetica-Bold", fontSize=10,
textColor=C_MID_BLUE,
spaceBefore=6, spaceAfter=2)
body_style = S("Body2",
fontName="Helvetica", fontSize=9,
leading=13, spaceAfter=3,
textColor=colors.HexColor("#1c2833"))
bullet_style = S("Bullet2",
fontName="Helvetica", fontSize=9,
leading=13, leftIndent=12, spaceAfter=2,
textColor=colors.HexColor("#1c2833"))
cell_style = S("Cell",
fontName="Helvetica", fontSize=8.5,
leading=12, textColor=colors.HexColor("#1c2833"))
cell_bold = S("CellBold",
fontName="Helvetica-Bold", fontSize=8.5,
leading=12, textColor=colors.HexColor("#1c2833"))
cell_header = S("CellHeader",
fontName="Helvetica-Bold", fontSize=9,
leading=13, textColor=C_WHITE, alignment=TA_CENTER)
caption_style = S("Caption",
fontName="Helvetica-Oblique", fontSize=7.5,
textColor=C_BORDER, spaceAfter=6)
note_style = S("Note",
fontName="Helvetica-Oblique", fontSize=8,
textColor=colors.HexColor("#5d6d7e"),
spaceAfter=4)
# ── Helpers ───────────────────────────────────────────────────────────────────
def section_header(text):
tbl = Table([[Paragraph(f" {text}", section_style)]], colWidths=[W])
tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), C_DARK_BLUE),
("ROWBACKGROUNDS", (0,0), (-1,-1), [C_DARK_BLUE]),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING",(0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 8),
("ROUNDEDCORNERS", [3,3,3,3]),
]))
return tbl
def mini_header(text, color=C_MID_BLUE):
return Paragraph(f"<font color='#{color.hexval()[2:]}'>■</font> <b>{text}</b>", sub_section_style)
def hr():
return HRFlowable(width=W, thickness=0.5, color=C_BORDER, spaceAfter=4, spaceBefore=2)
def make_table(data, col_widths, alt=True, header_bg=C_HEADER_ROW, font_size=8.5):
"""data: list of lists (first row = header)"""
parsed = []
for ri, row in enumerate(data):
parsed_row = []
for ci, cell in enumerate(row):
if isinstance(cell, str):
if ri == 0:
parsed_row.append(Paragraph(cell, cell_header))
else:
parsed_row.append(Paragraph(cell, cell_style))
else:
parsed_row.append(cell)
parsed.append(parsed_row)
t = Table(parsed, colWidths=col_widths, repeatRows=1)
style_cmds = [
("BACKGROUND", (0,0), (-1,0), header_bg),
("TEXTCOLOR", (0,0), (-1,0), C_WHITE),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("FONTSIZE", (0,0), (-1,-1), font_size),
("ALIGN", (0,0), (-1,0), "CENTER"),
("ALIGN", (0,1), (-1,-1), "LEFT"),
("VALIGN", (0,0), (-1,-1), "TOP"),
("ROWBACKGROUNDS",(0,1), (-1,-1), [C_WHITE, C_ALT_ROW] if alt else [C_WHITE]),
("GRID", (0,0), (-1,-1), 0.4, C_BORDER),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 5),
]
t.setStyle(TableStyle(style_cmds))
return t
def colored_cell(text, bg, fg=C_WHITE, bold=True):
st = S("cc", fontName="Helvetica-Bold" if bold else "Helvetica",
fontSize=8.5, textColor=fg, alignment=TA_CENTER, leading=12)
t = Table([[Paragraph(text, st)]], colWidths=[None])
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), bg),
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING",(0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 4),
("RIGHTPADDING", (0,0), (-1,-1), 4),
]))
return t
# ── TITLE BLOCK ───────────────────────────────────────────────────────────────
def title_block():
title_tbl = Table(
[[Paragraph("LUNG EXAMINATION", title_style)],
[Paragraph("Quick Reference Guide", subtitle_style)],
[Paragraph("Harrison's · Murray & Nadel · Goldman-Cecil · Textbook of Family Medicine", caption_style)]],
colWidths=[W]
)
title_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), C_DARK_BLUE),
("TOPPADDING", (0,0), (-1,-1), 10),
("BOTTOMPADDING", (0,0), (-1,-1), 8),
("LEFTPADDING", (0,0), (-1,-1), 10),
("RIGHTPADDING", (0,0), (-1,-1), 10),
("ROUNDEDCORNERS",[5,5,5,5]),
]))
return title_tbl
# ── PAGE 1: INSPECT / PALPATE / PERCUSS ──────────────────────────────────────
def page1():
els = []
els.append(title_block())
els.append(Spacer(1, 8))
# ── 1. INSPECTION ──────────────────────────────────────────────
els.append(section_header("1 INSPECTION"))
els.append(Spacer(1, 4))
# Two-column layout: General | Chest Wall
col_w = W / 2 - 3
gen_data = [
["GENERAL SIGNS"],
["Tachypnea (>20/min) — increased drive"],
["Central cyanosis — >5 g/dL deoxy-Hb"],
["Clubbing — IPF, lung ca., CF, bronchiectasis"],
["Pursed-lip breathing — COPD"],
["3-4 word dyspnea — severe limitation"],
]
chest_data = [
["CHEST WALL"],
["Barrel chest (↑AP diameter) — emphysema"],
["Pectus excavatum / carinatum — restriction"],
["Kyphoscoliosis — extrapulmonary restriction"],
["Intercostal retractions — ↑ work of breathing"],
["Lag on one side — unilateral pathology"],
]
def mini_box(rows, header_col):
trows = []
for i, r in enumerate(rows):
bg = header_col if i == 0 else (C_ALT_ROW if i % 2 == 0 else C_WHITE)
fg = C_WHITE if i == 0 else colors.HexColor("#1c2833")
fn = "Helvetica-Bold" if i == 0 else "Helvetica"
trows.append([Paragraph(r[0],
ParagraphStyle("mb", fontName=fn, fontSize=8.5,
textColor=fg, leading=12))])
t = Table(trows, colWidths=[col_w])
cmds = [
("GRID", (0,0), (-1,-1), 0.4, C_BORDER),
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING", (0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 6),
]
for i in range(len(rows)):
bg = header_col if i == 0 else (C_ALT_ROW if i % 2 == 0 else C_WHITE)
cmds.append(("BACKGROUND", (0,i), (-1,i), bg))
t.setStyle(TableStyle(cmds))
return t
two_col = Table(
[[mini_box(gen_data, C_MID_BLUE), mini_box(chest_data, C_GREEN)]],
colWidths=[col_w + 3, col_w + 3]
)
two_col.setStyle(TableStyle([
("VALIGN", (0,0), (-1,-1), "TOP"),
("LEFTPADDING", (0,0), (-1,-1), 0),
("RIGHTPADDING", (0,0), (-1,-1), 0),
("TOPPADDING", (0,0), (-1,-1), 0),
("BOTTOMPADDING",(0,0), (-1,-1), 0),
]))
els.append(two_col)
# Breathing pattern sub-box
els.append(Spacer(1, 6))
els.append(mini_header("Breathing Patterns"))
bp_data = [
["Sign", "Description", "Cause"],
["Accessory muscle use", "SCM, scalene activation on breathing", "COPD, severe asthma, resp. muscle weakness"],
["Hoover sign", "Inward lower lateral rib cage on inspiration", "Emphysema (flattened diaphragm)"],
["Paradoxical abdomen", "Abdomen moves inward on inspiration", "Diaphragmatic weakness / fatigue"],
["Kussmaul breathing", "Deep rapid breaths", "Severe metabolic acidosis"],
["Abdominal rounding", "Periumbilical outward on exhalation", "Acute pulmonary oedema (↑ PEEP attempt)"],
]
els.append(make_table(bp_data, [3.5*cm, 5.5*cm, 6.5*cm]))
els.append(Spacer(1, 8))
# ── 2. PALPATION ───────────────────────────────────────────────
els.append(section_header("2 PALPATION"))
els.append(Spacer(1, 4))
palp_data = [
["Finding", "Normal", "Abnormal — Shifted TOWARD affected side", "Abnormal — Shifted AWAY"],
["Trachea", "Central", "Collapse / Atelectasis", "Large effusion · Tension pneumothorax"],
["Apex beat", "5th ICS, MCL", "Collapse / Fibrosis", "Large effusion"],
["Chest expansion", "Symmetrical", "Ipsilateral pathology", "—"],
["Tactile fremitus", "Present / equal", "↑ = Consolidation", "↓/Absent = Effusion, Pneumothorax, Obstruction"],
]
els.append(make_table(palp_data, [3.2*cm, 2.8*cm, 5.5*cm, 4*cm], header_bg=C_GREEN))
els.append(Spacer(1, 8))
# ── 3. PERCUSSION ──────────────────────────────────────────────
els.append(section_header("3 PERCUSSION"))
els.append(Spacer(1, 4))
perc_data = [
["Note", "Quality", "Conditions"],
["Resonant", "Normal lung sound", "Normal aerated lung"],
["Dullness", "Thud-like, flat", "Consolidation (pneumonia), Pleural effusion, Collapse"],
["Stony dullness", "Extreme flatness", "Large pleural effusion"],
["Hyperresonance", "Drum-like, louder", "Pneumothorax, Emphysema / severe asthma"],
["Tympany", "Hollow, musical", "Large pneumothorax, Gastric bubble (left base)"],
]
color_map = {1: C_LIGHT_BLUE, 2: C_RED, 3: colors.HexColor("#fadbd8"),
4: colors.HexColor("#d5f5e3"), 5: colors.HexColor("#fef9e7")}
els.append(make_table(perc_data, [3.2*cm, 3.8*cm, 8.5*cm], header_bg=C_ORANGE))
els.append(Spacer(1, 6))
els.append(Paragraph(
"<b>Technique:</b> Place middle finger of non-dominant hand flat on the chest — strike the middle phalanx "
"sharply with the tip of the dominant middle finger. Compare symmetrically. Map diaphragm by percussing "
"from resonance to dullness (normally at T10 posteriorly); measure excursion (normal 3-5 cm).",
note_style))
return els
# ── PAGE 2: AUSCULTATION + COMPARISON TABLE ───────────────────────────────────
def page2():
els = []
els.append(PageBreak())
# ── 4. AUSCULTATION ────────────────────────────────────────────
els.append(section_header("4 AUSCULTATION"))
els.append(Spacer(1, 4))
els.append(mini_header("Normal Breath Sounds"))
norm_data = [
["Sound", "Location", "Inspiration : Expiration", "Character"],
["Vesicular", "Peripheral lung fields", "I > E (3:1)", "Soft, low-pitched, continuous"],
["Bronchovesicular", "1st/2nd ICS anteriorly, between scapulae", "I = E (1:1)", "Medium pitch, medium intensity"],
["Bronchial (tubular)", "Over trachea / manubrium", "E > I (2:3)", "Loud, high-pitched, gap between I and E"],
]
els.append(make_table(norm_data, [3.2*cm, 5.0*cm, 3.2*cm, 4.1*cm]))
els.append(Spacer(1, 6))
els.append(Paragraph(
"<b>Note:</b> Bronchial breath sounds heard in the <b>peripheral</b> lung fields = pathological "
"(indicates consolidation beneath the stethoscope).",
note_style))
els.append(Spacer(1, 6))
els.append(mini_header("Added (Adventitious) Sounds"))
adv_data = [
["Sound", "Type", "Character", "Key Associations"],
["Fine crackles", "Discontinuous", "Soft, high-pitched, end-inspiratory; like Velcro or hair rubbed near ear",
"Pulmonary oedema (basal), IPF (bibasal Velcro), early pneumonia"],
["Coarse crackles", "Discontinuous", "Loud, lower-pitched, inspiratory ± expiratory",
"Bronchiectasis, bronchitis, resolving pneumonia"],
["Wheeze", "Continuous", "Musical, high-pitched, mainly expiratory",
"Asthma, COPD, bronchospasm (any cause)"],
["Rhonchi", "Continuous", "Low-pitched, snoring quality",
"Secretions in large airways; bronchitis, COPD"],
["Stridor", "Continuous", "High-pitched, monophonic, mainly inspiratory, loudest over neck",
"Upper airway obstruction (foreign body, laryngeal oedema, croup)"],
["Pleural rub", "Discontinuous", "Creaking / leathery, both phases, not cleared by coughing",
"Pleuritis; disappears when large effusion develops"],
]
els.append(make_table(adv_data, [2.5*cm, 2.6*cm, 5.0*cm, 5.4*cm]))
els.append(Spacer(1, 6))
els.append(mini_header("Vocal Resonance Tests"))
vr_data = [
["Test", "Technique", "Consolidation", "Pleural Effusion"],
["Bronchophony", "Say '99' — auscultate", "Louder, clearer", "Absent / muffled"],
["Whispered pectoriloquy", "Whisper '1-2-3' — auscultate", "Clearly transmitted", "Absent"],
["Egophony", "Say 'EEE' — auscultate", "Sounds like 'AAY'", "Absent (except top edge of effusion)"],
]
els.append(make_table(vr_data, [3.8*cm, 4.0*cm, 3.5*cm, 4.2*cm]))
els.append(Spacer(1, 10))
# ── 5. COMPARISON TABLE ────────────────────────────────────────
els.append(section_header("5 CLINICAL FINDINGS COMPARISON TABLE"))
els.append(Spacer(1, 4))
comp_data = [
["Disorder", "Inspection", "Trachea", "Fremitus", "Percussion", "Breath Sounds", "Added Sounds"],
["Consolidation\n(Pneumonia)",
"Possible splinting / lag",
"Central",
"↑ Increased",
"Dull",
"Bronchial (pathological)",
"Crackles, egophony, bronchophony"],
["Pleural Effusion\n(large)",
"Lag on affected side",
"Shifted AWAY",
"↓ Decreased",
"Stony dull",
"Absent",
"Pleural rub (early); egophony at top edge"],
["Pneumothorax\n(complete)",
"Lag on affected side",
"Shifted AWAY\n(tension)",
"Absent",
"Hyperresonant / tympanitic",
"Absent",
"None"],
["Atelectasis\n(lobar collapse)",
"Lag on affected side",
"Shifted TOWARD",
"↓ Decreased",
"Dull",
"Absent / reduced",
"None"],
["Asthma\n(acute attack)",
"Hyperinflation;\naccessory muscles",
"Central",
"↓ Decreased",
"Hyperresonant;\nlow diaphragm",
"Prolonged expiration;\nreduced",
"Expiratory + inspiratory wheeze"],
["COPD /\nEmphysema",
"Barrel chest;\nHoover sign;\npursed-lip breathing",
"Central",
"↓ Decreased",
"Hyperresonant",
"Globally ↓ (quiet chest)",
"Wheeze; prolonged expiration"],
["IPF\n(Pulmonary Fibrosis)",
"Clubbing; cyanosis\n(advanced)",
"Central",
"Normal / ↑",
"Resonant",
"Vesicular (reduced volume)",
"Fine bibasal Velcro crackles"],
]
# Build with wrapped paragraphs
cw = [3.0*cm, 2.8*cm, 2.2*cm, 2.0*cm, 2.5*cm, 3.0*cm, 4.0*cm]
parsed = []
for ri, row in enumerate(comp_data):
pr = []
for ci, cell in enumerate(row):
if ri == 0:
pr.append(Paragraph(cell, cell_header))
else:
pr.append(Paragraph(cell.replace("\n", "<br/>"),
ParagraphStyle("ctab", fontName="Helvetica", fontSize=7.8,
leading=11, textColor=colors.HexColor("#1c2833"))))
parsed.append(pr)
t = Table(parsed, colWidths=cw, repeatRows=1)
style_cmds = [
("BACKGROUND", (0,0), (-1,0), C_DARK_BLUE),
("TEXTCOLOR", (0,0), (-1,0), C_WHITE),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("ALIGN", (0,0), (-1,0), "CENTER"),
("ALIGN", (0,1), (-1,-1), "LEFT"),
("VALIGN", (0,0), (-1,-1), "TOP"),
("GRID", (0,0), (-1,-1), 0.4, C_BORDER),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 4),
("RIGHTPADDING", (0,0), (-1,-1), 4),
# Alternate row colours
("ROWBACKGROUNDS",(0,1), (-1,-1), [C_WHITE, C_ALT_ROW]),
# Highlight specific percussion findings
("BACKGROUND", (4,1), (4,1), colors.HexColor("#fdecea")), # dull - pneumonia
("BACKGROUND", (4,2), (4,2), colors.HexColor("#fdecea")), # dull - effusion
("BACKGROUND", (4,4), (4,4), colors.HexColor("#d5f5e3")), # hyper - asthma
("BACKGROUND", (4,5), (4,5), colors.HexColor("#d5f5e3")), # hyper - COPD
]
t.setStyle(TableStyle(style_cmds))
els.append(t)
return els
# ── PAGE 3: SPECIAL SIGNS + QUICK TIPS ───────────────────────────────────────
def page3():
els = []
els.append(PageBreak())
# ── 6. IMPORTANT CLINICAL SIGNS ────────────────────────────────
els.append(section_header("6 IMPORTANT CLINICAL SIGNS"))
els.append(Spacer(1, 4))
signs_data = [
["Sign", "Definition", "Clinical Significance"],
["Pulsus paradoxus\n(>10 mmHg)", "Systolic BP falls >10 mmHg on inspiration",
"Severe asthma / COPD (impending resp. failure); pericardial tamponade"],
["Cor pulmonale signs", "JVD, right ventricular heave, hepatomegaly, peripheral oedema",
"Evidence of severe chronic respiratory impairment affecting right heart"],
["Clubbing", "Loss of nail-bed angle; increased sponginess; Schamroth sign",
"Lung cancer, IPF, CF, bronchiectasis, mesothelioma; NOT COPD"],
["Central cyanosis", "Blue discolouration of lips/tongue",
">5 g/dL deoxygenated Hb; seen in hypoxaemic respiratory disorders"],
["Peripheral oedema\n(bilateral)", "Pitting oedema lower limbs",
"Cor pulmonale; consider also DVT/PE if unilateral"],
["JVD", "Visible jugular venous pulsations at >3 cm above sternal angle sitting at 45°",
"Volume overload; right heart failure; tension pneumothorax"],
["Hoover sign", "Inward motion of lower lateral rib cage on inspiration",
"Emphysema — horizontal diaphragm pulls costal margin inward"],
["Tracheal tug", "Larynx descends on inspiration", "Severe airway obstruction; dynamic hyperinflation"],
]
els.append(make_table(signs_data, [3.5*cm, 6.0*cm, 6.0*cm], header_bg=C_RED))
els.append(Spacer(1, 8))
# ── 7. AUSCULTATION ZONES ──────────────────────────────────────
els.append(section_header("7 AUSCULTATION ZONES & TIPS"))
els.append(Spacer(1, 4))
zones_data = [
["Posterior zones (3 each side)", "Upper zone T2-T4 · Middle zone T4-T6 · Lower zone T6-T9"],
["Anterior zones", "Apices above clavicles · Upper chest · Lower chest down to 6th rib"],
["Right middle lobe / Lingula", "Heard ONLY anteriorly (right 4th-6th ICS, left 4th-6th ICS)"],
["Lateral zones", "Axillary lines — important for effusions and pneumothorax"],
]
t_zones = Table(zones_data, colWidths=[5*cm, W - 5*cm])
t_zones.setStyle(TableStyle([
("ROWBACKGROUNDS", (0,0), (-1,-1), [C_ACCENT, C_WHITE]),
("GRID", (0,0), (-1,-1), 0.4, C_BORDER),
("FONTNAME", (0,0), (0,-1), "Helvetica-Bold"),
("FONTSIZE", (0,0), (-1,-1), 8.5),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 6),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
]))
els.append(t_zones)
els.append(Spacer(1, 6))
tips = [
("Always compare", "auscultate one level on the right, then the same level on the left — before moving down."),
("Breath-sound quality", "a vesicular sound becoming bronchial at the periphery = consolidation until proven otherwise."),
("Crackle timing", "early inspiratory crackles = large-airway disease (COPD, bronchiectasis); late inspiratory fine crackles = alveolar/interstitial disease (oedema, IPF, pneumonia)."),
("Cleared by cough?", "rhonchi and some coarse crackles clear with coughing; wheeze and fine crackles do not."),
("Silent chest", "no wheeze despite severe asthma = no airflow — an emergency, not improvement."),
("Pleural rub timing", "present in both phases but disappears when significant effusion cushions the pleural surfaces."),
]
els.append(mini_header("Examination Tips"))
for heading, body in tips:
els.append(Paragraph(
f"<b>• {heading}:</b> {body}", bullet_style))
els.append(Spacer(1, 8))
# ── 8. SYSTEMATIC APPROACH SUMMARY ────────────────────────────
els.append(section_header("8 SYSTEMATIC APPROACH — BEDSIDE CHECKLIST"))
els.append(Spacer(1, 4))
steps = [
("1", "General", "Rate, SpO2, SpO2 on exertion, cyanosis, clubbing, JVD, oedema, use of accessory muscles"),
("2", "Inspect chest", "Shape, symmetry, movement, AP diameter, scars, deformities, retractions"),
("3", "Palpate", "Trachea, apex beat, chest expansion (hands on back), tactile fremitus ('99')"),
("4", "Percuss", "All zones posteriorly → anteriorly → laterally; map diaphragm; measure excursion"),
("5", "Auscultate", "All zones; normal sounds → quality → added sounds; vocal resonance if needed"),
("6", "Synthesise", "Integrate inspection + palpation + percussion + auscultation → pattern recognition"),
]
step_tbl = Table(
[[Paragraph(n, ParagraphStyle("step_n", fontName="Helvetica-Bold", fontSize=10,
textColor=C_WHITE, alignment=TA_CENTER, leading=13)),
Paragraph(h, ParagraphStyle("step_h", fontName="Helvetica-Bold", fontSize=9,
textColor=C_DARK_BLUE, leading=12)),
Paragraph(d, ParagraphStyle("step_d", fontName="Helvetica", fontSize=8.5,
textColor=colors.HexColor("#1c2833"), leading=12))]
for n, h, d in steps],
colWidths=[1.1*cm, 2.8*cm, W - 3.9*cm],
repeatRows=0
)
cmds = [
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING",(0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 6),
("RIGHTPADDING", (0,0), (-1,-1), 6),
("GRID", (0,0), (-1,-1), 0.4, C_BORDER),
("ROWBACKGROUNDS",(0,0), (-1,-1), [C_ACCENT, C_WHITE]),
]
for i, (n, h, d) in enumerate(steps):
cmds.append(("BACKGROUND", (0,i), (0,i), C_MID_BLUE))
step_tbl.setStyle(TableStyle(cmds))
els.append(step_tbl)
els.append(Spacer(1, 10))
# ── Footer ─────────────────────────────────────────────────────
els.append(hr())
els.append(Paragraph(
"Sources: Harrison's Principles of Internal Medicine 22E (2025) · Murray & Nadel's Textbook of Respiratory Medicine · "
"Textbook of Family Medicine 9e · Goldman-Cecil Medicine · Orris Medical Reference",
ParagraphStyle("footer", fontName="Helvetica-Oblique", fontSize=7, textColor=C_BORDER,
alignment=TA_CENTER)))
return els
# ── BUILD ──────────────────────────────────────────────────────────────────────
all_elements = page1() + page2() + page3()
doc.build(all_elements)
print(f"PDF saved to: {OUTPUT}")
cd /tmp/workspace/lung-exam-pdf && python build_pdf.py 2>&1
ls -lh /tmp/workspace/lung-exam-pdf/Lung_Exam_Quick_Reference.pdf
Lung Exam Quick Reference
PDF Document · PDF