Boy aged 3 years diagnosis ALL give complete pediatric history and examination for this case
| Parameter | Details |
|---|---|
| Name | Muhammad Ibrahim Shaikh |
| Age | 3 years |
| Sex | Male |
| UHID | 5464810 |
| IP No. | 2106261920688 |
| Address | No. 19/10, 5th Main, Bismillah Nagar, Karnataka - 560029 |
| Ward | PHO Common Ward, Floor I, B Block / 1043-C |
| Unit | PAEDHEMAT (Pediatric Hematology-Oncology) |
| Date of Admission | 21 June 2026 |
| Date of Discharge | 03 July 2026 |
| Informant | Parent (mother/father - primary caregiver) |
| Reliability | Reliable (parent) |
| Parameter | Details |
|---|---|
| DOB | Not documented in available records |
| Antenatal history | Not documented |
| Intrapartum events | Not documented |
| Type of delivery | Not documented |
| Term / Preterm | Not documented |
| Birth weight | Not documented |
| Birth asphyxia | Not documented |
| Neonatal jaundice | Not documented |
| Neonatal seizures | Not documented |
| Vaccine | Doses Documented |
|---|---|
| BCG / OPV / HBV | Given (1 dose) |
| Pentavalent / Rotavirus | 1 dose |
| IPV | 1 dose |
| PCV-10 | 1 dose |
| PCV-13 | 1 dose |
| MR / MMR | 1 dose |
| DPT / Td | Not documented |
| Influenza | Not documented |
| Typhoid / Hepatitis | Not documented |
| Varicella | Not documented |
| HPV | Not documented |
| System | Documented Findings |
|---|---|
| Constitutional | Fever, weight loss, anorexia - YES |
| Respiratory | No cough, cold, respiratory distress |
| Cardiovascular | No documented cardiac symptoms |
| Gastrointestinal | No abdominal pain, no loose stools, no vomiting |
| Genitourinary | No dysuria, no UTI symptoms at presentation |
| Musculoskeletal | No bone pain, no joint swelling documented |
| Skin | No rash, no bruising reported |
| Haematological | No bleeding from any site at initial presentation |
| Neurological | No seizures, no focal deficits reported |
| Parameter | Findings |
|---|---|
| General condition | Moderately built and nourished |
| Consciousness | Alert and conscious |
| Activity | Active (not documented as lethargic) |
| Temperature | Afebrile at time of examination (on 22/06/2026) |
| Pallor | Absent (no pallor noted) |
| Icterus | Absent |
| Cyanosis | Absent |
| Clubbing | Absent |
| Lymphadenopathy | Absent (no palpable lymphadenopathy noted) |
| Pedal oedema | Absent |
| Pulse rate | 122 bpm (mildly tachycardic - likely due to anaemia/fever) |
| Blood pressure | 98/60 mmHg (normal for 3-year-old) |
| JVP | Normal |
| Skin | Normal |
| Thyroid | No thyroid swelling |
| Investigation | Result | Unit | Significance |
|---|---|---|---|
| Haemoglobin (Hb) | 6.4 | g/dL | Severe anaemia |
| Total Leucocyte Count (TLC) | 50.92 | ×10³/µL | Markedly elevated (hyperleukocytosis) |
| Blasts | 93% | % of WBC | Diagnostic - massively elevated |
| Neutrophils | 1% | % | Severely reduced |
| Lymphocytes | 6% | % | Relatively low % |
| Platelet Count | 13 | ×10³/µL | Severe thrombocytopenia |
| PCV | 21.9 | % | Low |
| MCV | 71.3 | fL | Low (microcytic) |
| MCH | 29.2 | pg | Low-normal |
| MCHC | 15.4 | g/dL | Low |
| ANC | 0.51 | ×10³/µL | Severe neutropenia (<500 = profound) |
| ALC | 3.06 | ×10³/µL | |
| AEC | 0.00 | ×10³/µL | |
| ABC | 0.00 | ×10³/µL | |
| AMC | 0.00 | ×10³/µL | |
| Nucleated RBC | 0.0 | /100 WBC | |
| RBC Count | 3.07 | million/µL | Low |
| Reticulocyte Count | 0.17% | % | Low - inadequate marrow response |
| RDW-SD | 39.3 | fL |
Give Performa for respiratory system examination for medicine according to macleoids
| Feature | Normal | Abnormal |
|---|---|---|
| Clubbing | Absent | Present (Grade I-IV) - causes: fibrosis, bronchiectasis, lung cancer, empyema |
| Peripheral cyanosis | Absent | Present - cold hands, reduced perfusion |
| Tar staining | Absent | Present - smoking history |
| Fine tremor | Absent | Present - beta-2 agonist use |
| Asterixis (CO₂ flap) | Absent | Present - CO₂ retention (Type 2 respiratory failure) |
| Warm peripheries | Present | Cold - poor perfusion / vasospasm |
| Pulse (radial) | Regular, normal rate | Pulsus paradoxus (severe asthma), tachycardia (infection/hypoxia) |
| Feature | Normal | Abnormal |
|---|---|---|
| Central cyanosis | Absent | Present - blue tongue/lips → SpO₂ <85% |
| Anaemia (conjunctival pallor) | Absent | Present - malignancy, chronic disease |
| Horner's syndrome | Absent | Ptosis + miosis + anhidrosis → Pancoast tumour |
| Plethora / suffused face | Absent | SVC obstruction |
| Cushingoid facies | Absent | Long-term steroid use |
| Pursed lip breathing | Absent | COPD (auto-PEEP) |
| Feature | Normal | Abnormal |
|---|---|---|
| JVP | Normal (3-4 cm above sternal angle) | Elevated - cor pulmonale, SVC obstruction, cardiac failure |
| Trachea position | Midline | Deviated - pushed (effusion, tension pneumothorax) or pulled (fibrosis, collapse) |
| Lymphadenopathy | Absent | Cervical / supraclavicular nodes → malignancy, TB, lymphoma |
| Accessory muscle use | Absent | SCM and scalene hypertrophy → chronic severe airway obstruction |
| Feature | Normal | Abnormal |
|---|---|---|
| AP diameter | Less than transverse | Barrel chest (AP = transverse) → hyperinflation (COPD, asthma) |
| Chest wall symmetry | Symmetric | Asymmetry → previous surgery, scoliosis, pleural disease |
| Kyphosis / scoliosis | Absent | Restricts lung expansion |
| Harrison's sulcus | Absent | Bilateral groove at lower ribs → childhood asthma / rickets |
| Pectus excavatum | Absent | Funnel chest - may restrict lung expansion |
| Pectus carinatum | Absent | Pigeon chest - childhood respiratory disease |
| Intercostal recession | Absent | Severe airway obstruction / inspiratory stridor |
| Subcostal recession | Absent | Seen in infants (more compliant chest) |
| Feature | Normal | Abnormal |
|---|---|---|
| Rate (adults) | 12-20 breaths/min | Tachypnoea (>20), bradypnoea (<12), apnoea |
| Depth | Normal tidal volume | Hyperpnoea, Kussmaul breathing (DKA) |
| Rhythm | Regular | Cheyne-Stokes (CNS, cardiac failure), Biot's (meningitis) |
| Type | Diaphragmatic + costal | Paradoxical movement (flail chest) |
| Feature | Normal | Abnormal |
|---|---|---|
| Expansion - upper zones | Equal bilaterally | Reduced unilaterally or bilaterally |
| Expansion - lower zones | Equal bilaterally | Unilateral reduction → consolidation, effusion, collapse |
| Intercostal spaces | Normal | Bulging (effusion) / Sunken (collapse/fibrosis) |
| Feature | Finding |
|---|---|
| Scars | Thoracotomy / VATS / chest drain sites |
| Radiotherapy marks | Relevant to malignancy |
| Dilated veins | SVC obstruction - flow upwards |
| Spider naevi | Hepatic cause (hepatopulmonary syndrome) |
| Subcutaneous emphysema | Pneumothorax / trauma |
| Chest drains in situ | Note position and output |
| Finding | Interpretation |
|---|---|
| Normal fremitus | Clear transmission through air-filled lung |
| Increased fremitus | Consolidation (solid lung conducts vibrations better) |
| Decreased/absent fremitus | Effusion (fluid blocks transmission), pneumothorax (air blocks), collapse (blocked bronchus), obesity |
| Note | Quality | Causes |
|---|---|---|
| Resonant | Hollow, drum-like | Normal aerated lung |
| Hyper-resonant | Louder, more hollow than normal | Pneumothorax, emphysema (hyperinflation) |
| Dull | Thud-like | Consolidation, collapse, tumour |
| Stony dull | Extremely dull, no resonance | Pleural effusion (pathognomonic) |
| Tympanic | Drum-like (gas-filled cavity) | Large pneumothorax, gastric air bubble (LLL) |
| Breath Sound | Site Heard | Quality | Abnormal if |
|---|---|---|---|
| Vesicular | Normal lung | Soft, rustling; inspiration > expiration; no gap between I and E | Reduced/absent over effusion, pneumothorax |
| Bronchial | Normally over trachea and main bronchi only | Harsh, tubular; expiration ≥ inspiration; gap between I and E | Abnormal when heard over lung periphery → consolidation, above effusion, lung abscess, fibrosis |
| Bronchovesicular | 1st and 2nd ICS anteriorly; between scapulae | Intermediate character | Normal in these zones only |
| Intensity | Interpretation |
|---|---|
| Normal | Clear vesicular |
| Reduced (bilaterally) | COPD, obesity, poor respiratory effort |
| Reduced (unilaterally) | Pleural effusion, pneumothorax, collapse, consolidation (severe) |
| Absent | Tension pneumothorax, massive effusion |
| Sound | Character | Timing | Causes |
|---|---|---|---|
| Fine crackles (crepitations) | Fine, high-pitched, brief; like rubbing hair between fingers | End-inspiratory; do not clear with coughing | Pulmonary fibrosis, early pulmonary oedema, early consolidation |
| Coarse crackles | Loud, low-pitched, bubbling | Early inspiratory; may clear with coughing | Secretions in large airways - bronchiectasis, pulmonary oedema, COPD |
| Wheeze (rhonchi) | Musical, continuous | Expiratory (or pan-inspiratory in severe obstruction) | Asthma, COPD, bronchospasm (polyphonic = widespread; monophonic = localised) |
| Stridor | Harsh, high-pitched, loud | Inspiratory | Upper airway obstruction - larynx, trachea (NOT a wheeze - audible without stethoscope) |
| Pleural rub | Leathery, creaking, like walking on fresh snow | Inspiratory AND expiratory; localised; does not change with coughing | Pleurisy (viral, bacterial, PE, RA, SLE) |
| Pleural knock | Low-pitched sound on inspiration | Inspiratory | Constrictive pericarditis (also heard) |
| Finding | Quality | Interpretation |
|---|---|---|
| Normal resonance | Muffled sounds | Normal |
| Increased resonance | Louder, clearer | Consolidation |
| Bronchophony | Words clearly audible | Consolidation (same mechanism as bronchial breathing) |
| Whispering pectoriloquy | Whispered sounds clearly heard ("1-2-3") | Consolidation (most sensitive sign) |
| Aegophony | "E" sounds like "A" (nasal, bleating quality) | Above a pleural effusion (at fluid-air interface) |
| Decreased resonance | Muffled, absent | Effusion, pneumothorax |
| Test | Method | Interpretation |
|---|---|---|
| Coin test (Skodaic resonance) | Percuss coin placed on chest with another coin | Hyperresonance above an effusion |
| Shifting dullness | Percuss for dullness, roll patient - dullness shifts | Pleural effusion (large, free) |
| Grocco's triangle | Paravertebral triangular area of dullness on opposite side | Large contralateral effusion |
| Brockenborough's sign | Auscultate over spine - breath sounds heard if consolidation | Consolidation of a lower lobe |
| Feature | Significance |
|---|---|
| Bilateral pitting oedema | Cor pulmonale, right heart failure (chronic respiratory disease) |
| Unilateral calf swelling/tenderness | DVT → pulmonary embolism |
| Calf tenderness, Homan's sign | DVT (note: Homan's sign is non-specific, not relied upon alone) |
| Investigation | Parameter |
|---|---|
| Peak Flow (PEFR) | Note predicted vs actual; % predicted |
| SpO₂ (Pulse oximetry) | Normal >94%; in COPD may accept 88-92% |
| Sputum | Note: quantity, colour (white/yellow/green/brown/blood-stained), consistency, smell |
| Temperature chart | Fever pattern |
| Fluid balance chart | If relevant |
| Chest X-ray (PA) | Correlate clinical findings |
| Condition | Trachea | Expansion | Fremitus/VR | Percussion | Breath Sounds | Added Sounds |
|---|---|---|---|---|---|---|
| Normal | Central | Equal | Normal | Resonant | Vesicular | None |
| Consolidation | Central | Reduced (affected side) | Increased | Dull | Bronchial | Fine crackles |
| Pleural effusion | Central (or pushed away if large) | Reduced | Absent | Stony dull | Absent/reduced | Pleural rub (above fluid) |
| Pneumothorax | Central (or pushed away if tension) | Reduced | Absent | Hyper-resonant | Absent | None |
| Collapse (atelectasis) | Pulled towards | Reduced | Absent/reduced | Dull | Absent/bronchial | None |
| Fibrosis (diffuse) | Central | Reduced bilaterally | Increased | Dull | Bronchial | Fine end-inspiratory crackles |
| COPD/Emphysema | Central | Reduced bilaterally | Reduced | Hyper-resonant | Vesicular reduced | Wheeze (expiratory) |
| Asthma (acute) | Central | Reduced bilaterally | Reduced | Resonant | Vesicular reduced | Polyphonic wheeze |
| Bronchiectasis | Central | Reduced | Normal/increased | Dull (affected areas) | Bronchial | Coarse crackles |
RESPIRATORY SYSTEM EXAMINATION
GENERAL: Patient is/is not in respiratory distress. RR = __/min.
Breathing is / is not laboured. No / Yes use of accessory muscles.
No / Yes cyanosis (peripheral / central).
HANDS: Clubbing Grade __/ absent. Tar staining / none.
CO₂ flap / absent. Pulse = __ bpm, regular / irregular.
Pulsus paradoxus absent / present.
FACE: Central cyanosis absent / present.
Horner's syndrome absent / present. Plethora absent / present.
NECK: Trachea central / deviated to ___. JVP normal / elevated __ cm.
Lymphadenopathy: absent / present (site: ___).
CHEST INSPECTION:
Shape: normal / barrel / pectus carinatum / excavatum / kyphoscoliosis.
Scars: none / ___. Chest movements: symmetric / asymmetric.
Intercostal / subcostal recession: absent / present.
PALPATION:
Trachea: central / deviated to ___.
Expansion: upper zone = equal / reduced on ___ side.
Lower zone = equal / reduced on ___ side (measure = __ cm).
Vocal fremitus: normal / increased over ___ / reduced over ___.
PERCUSSION:
Right: resonant / dull / stony dull / hyper-resonant at ___.
Left: resonant / dull / stony dull / hyper-resonant at ___.
Liver upper border: ___ ICS right.
AUSCULTATION:
Breath sounds: vesicular / bronchial / absent / reduced over ___.
Vocal resonance: normal / increased / decreased over ___.
Added sounds: nil / crackles (fine/coarse) at ___
/ wheeze (poly/monophonic) / pleural rub at ___.
Whispering pectoriloquy: absent / present.
LEGS:
Bilateral pitting oedema: absent / present up to ___.
Calf tenderness: absent / present on ___ side.
PEAK FLOW: ___ L/min (predicted: ___)
SpO₂: ___% (on air / ___ L O₂)
SUMMARY:
Examination is consistent with / suggests _______________
Create a respiratory exam checklist PDF for quick clinical reference
ls /home/daytona/workspace/ 2>/dev/null && echo "---" && python3 -c "import reportlab; print('reportlab ok')" && python3 -c "from reportlab.lib.pagesizes import A4; print('A4 ok')"
mkdir -p /home/daytona/workspace/resp-checklist
~/resp-checklist/build_pdf.py
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_RIGHT
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, KeepTogether
)
from reportlab.graphics.shapes import Drawing, Rect, String, Line
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
import os
OUTPUT = "/home/daytona/workspace/resp-checklist/Respiratory_Exam_Checklist.pdf"
# ── Colour palette ───────────────────────────────────────────────────────────
C_NAVY = colors.HexColor("#1a3a5c")
C_TEAL = colors.HexColor("#0d7377")
C_LTBLUE = colors.HexColor("#e8f4f8")
C_MINT = colors.HexColor("#e6f4f1")
C_ORANGE = colors.HexColor("#e8622a")
C_YELLOW = colors.HexColor("#fff3cd")
C_RED = colors.HexColor("#c0392b")
C_GREEN = colors.HexColor("#1e8449")
C_LGREY = colors.HexColor("#f2f2f2")
C_DGREY = colors.HexColor("#4a4a4a")
C_WHITE = colors.white
C_BLACK = colors.black
C_BORDER = colors.HexColor("#b0ccd8")
C_HEADBG = colors.HexColor("#0d4f6c")
W, H = A4 # 595 x 842
doc = SimpleDocTemplate(
OUTPUT,
pagesize=A4,
leftMargin=14*mm, rightMargin=14*mm,
topMargin=22*mm, bottomMargin=18*mm,
title="Respiratory System Examination Checklist (MacLeod's)",
author="Orris Clinical Reference"
)
styles = getSampleStyleSheet()
# ── Custom paragraph styles ──────────────────────────────────────────────────
def ps(name, parent="Normal", **kw):
return ParagraphStyle(name, parent=styles[parent], **kw)
sTitle = ps("sTitle", fontSize=15, textColor=C_WHITE, fontName="Helvetica-Bold",
alignment=TA_CENTER, spaceAfter=0, spaceBefore=0, leading=19)
sSubtit = ps("sSubtit", fontSize=8.5, textColor=colors.HexColor("#cce6f0"),
fontName="Helvetica", alignment=TA_CENTER, spaceAfter=0, leading=11)
sSecHdr = ps("sSecHdr", fontSize=8.5, textColor=C_WHITE, fontName="Helvetica-Bold",
alignment=TA_LEFT, spaceAfter=0, spaceBefore=0, leading=11)
sBody = ps("sBody", fontSize=7.5, textColor=C_DGREY, fontName="Helvetica",
spaceAfter=1, leading=10)
sBold = ps("sBold", fontSize=7.5, textColor=C_NAVY, fontName="Helvetica-Bold",
spaceAfter=1, leading=10)
sSmall = ps("sSmall", fontSize=6.8, textColor=C_DGREY, fontName="Helvetica",
spaceAfter=0, leading=9)
sWarn = ps("sWarn", fontSize=7, textColor=C_RED, fontName="Helvetica-Bold",
spaceAfter=0, leading=9)
sNorm = ps("sNorm", fontSize=7, textColor=C_GREEN, fontName="Helvetica-Bold",
spaceAfter=0, leading=9)
sFooter = ps("sFooter", fontSize=6.5, textColor=colors.HexColor("#888888"),
fontName="Helvetica", alignment=TA_CENTER, leading=8)
# ── Helper: checkbox symbol ──────────────────────────────────────────────────
def cb(): return "☐ "
def bull(): return "• "
# ── Helper: section header row (coloured band) ───────────────────────────────
def sec_header(text, bg=C_HEADBG, icon=""):
full = f"{icon} {text}" if icon else text
return Table(
[[Paragraph(full, sSecHdr)]],
colWidths=[doc.width],
style=TableStyle([
("BACKGROUND", (0,0), (-1,-1), bg),
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING", (0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 6),
("RIGHTPADDING", (0,0), (-1,-1), 4),
("ROUNDEDCORNERS", (0,0), (-1,-1), [3,3,3,3]),
])
)
# ── Helper: two-column table ──────────────────────────────────────────────────
def two_col(rows, col1=82*mm, col2=None, bg_alt=True):
col2 = col2 or (doc.width - col1)
style = [
("FONTNAME", (0,0), (-1,-1), "Helvetica"),
("FONTSIZE", (0,0), (-1,-1), 7.2),
("TOPPADDING", (0,0), (-1,-1), 2),
("BOTTOMPADDING", (0,0), (-1,-1), 2),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 4),
("GRID", (0,0), (-1,-1), 0.3, C_BORDER),
("VALIGN", (0,0), (-1,-1), "TOP"),
]
if bg_alt:
for i in range(0, len(rows), 2):
style.append(("BACKGROUND", (0,i), (-1,i), C_LGREY))
return Table(rows, colWidths=[col1, col2], style=TableStyle(style))
# ── Helper: three-column table (finding | normal | abnormal) ─────────────────
def three_col(header_row, rows, widths=None):
widths = widths or [60*mm, 50*mm, doc.width-110*mm]
all_rows = [header_row] + rows
style = [
("FONTNAME", (0,0), (-1,-1), "Helvetica"),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("FONTSIZE", (0,0), (-1,-1), 7),
("FONTSIZE", (0,0), (-1,0), 7.5),
("TOPPADDING", (0,0), (-1,-1), 2),
("BOTTOMPADDING", (0,0), (-1,-1), 2),
("LEFTPADDING", (0,0), (-1,-1), 4),
("RIGHTPADDING", (0,0), (-1,-1), 4),
("GRID", (0,0), (-1,-1), 0.3, C_BORDER),
("BACKGROUND", (0,0), (-1,0), C_TEAL),
("TEXTCOLOR", (0,0), (-1,0), C_WHITE),
("VALIGN", (0,0), (-1,-1), "TOP"),
]
for i in range(1, len(all_rows), 2):
style.append(("BACKGROUND", (0,i), (-1,i), C_LGREY))
return Table(all_rows, colWidths=widths, style=TableStyle(style))
# ── Helper: checklist block ───────────────────────────────────────────────────
def checklist(items, cols=2):
"""items = list of strings; rendered as 2-column checkbox list"""
rows = []
for i in range(0, len(items), cols):
row = []
for j in range(cols):
if i+j < len(items):
row.append(Paragraph(f"{cb()}{items[i+j]}", sBody))
else:
row.append(Paragraph("", sBody))
rows.append(row)
w = doc.width / cols
return Table(rows, colWidths=[w]*cols,
style=TableStyle([
("TOPPADDING", (0,0),(-1,-1), 1),
("BOTTOMPADDING", (0,0),(-1,-1), 1),
("LEFTPADDING", (0,0),(-1,-1), 3),
("RIGHTPADDING", (0,0),(-1,-1), 3),
("VALIGN", (0,0),(-1,-1), "TOP"),
])
)
# ── Helper: summary comparison table (big clinical patterns table) ────────────
def pattern_table():
header = ["Condition", "Trachea", "Expansion", "Fremitus/VR", "Percussion", "Breath Sounds", "Added Sounds"]
rows = [
["Normal", "Central", "Equal", "Normal", "Resonant", "Vesicular", "None"],
["Consolidation", "Central", "↓ (affected)", "↑ Increased", "Dull", "Bronchial", "Fine crackles"],
["Pleural Effusion","→ Away", "↓ (affected)", "Absent", "STONY DULL", "Absent/↓", "Rub (above fluid)"],
["Pneumothorax", "→ Away*", "↓ (affected)", "Absent", "Hyper-resonant","Absent", "None"],
["Collapse", "← Towards", "↓ (affected)", "Absent/↓", "Dull", "Absent/Bronchial", "None"],
["Fibrosis", "Central", "↓ Bilateral", "↑ Increased", "Dull", "Bronchial", "Fine end-insp crackles"],
["COPD/Emphysema", "Central", "↓ Bilateral", "↓ Reduced", "Hyper-resonant","Vesicular ↓", "Expiratory wheeze"],
["Asthma (acute)", "Central", "↓ Bilateral", "↓ Reduced", "Resonant", "Vesicular ↓", "Polyphonic wheeze"],
["Bronchiectasis", "Central", "↓ (affected)", "Normal/↑", "Dull", "Bronchial", "Coarse crackles"],
]
W_cols = [30*mm, 18*mm, 22*mm, 22*mm, 24*mm, 26*mm, doc.width-142*mm]
all_rows = [[Paragraph(h, ps("ph", fontSize=6.5, textColor=C_WHITE, fontName="Helvetica-Bold", leading=8)) for h in header]]
for r in rows:
all_rows.append([Paragraph(c, ps("pc", fontSize=6.5, textColor=C_DGREY, fontName="Helvetica", leading=8)) for c in r])
style = [
("BACKGROUND", (0,0),(-1,0), C_NAVY),
("TEXTCOLOR", (0,0),(-1,0), C_WHITE),
("FONTNAME", (0,0),(-1,-1), "Helvetica"),
("FONTSIZE", (0,0),(-1,-1), 6.5),
("TOPPADDING", (0,0),(-1,-1), 2),
("BOTTOMPADDING", (0,0),(-1,-1), 2),
("LEFTPADDING", (0,0),(-1,-1), 3),
("RIGHTPADDING", (0,0),(-1,-1), 3),
("GRID", (0,0),(-1,-1), 0.3, C_BORDER),
("VALIGN", (0,0),(-1,-1), "TOP"),
# highlight stony dull
("BACKGROUND", (4,2),(4,2), colors.HexColor("#fde8e8")),
("TEXTCOLOR", (4,2),(4,2), C_RED),
("FONTNAME", (4,2),(4,2), "Helvetica-Bold"),
]
for i in range(1, len(all_rows), 2):
style.append(("BACKGROUND", (0,i),(-1,i), C_LGREY))
return Table(all_rows, colWidths=W_cols, style=TableStyle(style))
# ── PAGE BACKGROUND & HEADER canvas callback ─────────────────────────────────
def on_page(canvas, doc):
canvas.saveState()
# Top banner
canvas.setFillColor(C_NAVY)
canvas.rect(0, H-18*mm, W, 18*mm, fill=1, stroke=0)
# Title
canvas.setFont("Helvetica-Bold", 13)
canvas.setFillColor(C_WHITE)
canvas.drawCentredString(W/2, H-11*mm, "RESPIRATORY SYSTEM EXAMINATION — QUICK REFERENCE CHECKLIST")
canvas.setFont("Helvetica", 7.5)
canvas.setFillColor(colors.HexColor("#aad4e8"))
canvas.drawCentredString(W/2, H-16*mm, "Based on MacLeod's Clinical Examination, 14th Edition | For Clinical & OSCE Use")
# Bottom bar
canvas.setFillColor(C_NAVY)
canvas.rect(0, 0, W, 12*mm, fill=1, stroke=0)
canvas.setFont("Helvetica", 6.5)
canvas.setFillColor(colors.HexColor("#aad4e8"))
canvas.drawString(14*mm, 4.5*mm, "MacLeod's Clinical Examination 14e | Orris Clinical Reference")
canvas.drawRightString(W-14*mm, 4.5*mm, f"Page {doc.page}")
canvas.restoreState()
# ════════════════════════════════════════════════════════════════════════════
# BUILD CONTENT
# ════════════════════════════════════════════════════════════════════════════
story = []
SP = lambda n=3: Spacer(1, n*mm)
# ── SECTION 1: End-of-Bed + General ─────────────────────────────────────────
story.append(SP(1))
story.append(sec_header("① END-OF-BED ASSESSMENT & GENERAL PHYSICAL EXAMINATION", C_HEADBG, "👁"))
story.append(SP(1))
story.append(checklist([
"Breathlessness at rest / on talking",
"Use of accessory muscles",
"Pursed lip breathing (COPD)",
"Audible wheeze / stridor",
"Tripod positioning",
"Cough (note: productive/dry)",
"Cachexia / wasting",
"Level of consciousness / distress",
], cols=2))
story.append(SP(2))
# ── SECTION 2: Hands ────────────────────────────────────────────────────────
story.append(sec_header("② HANDS", C_TEAL, "🤚"))
story.append(SP(1))
story.append(three_col(
["Finding", "Normal", "Abnormal — Significance"],
[
["Clubbing (grade I–IV)", "Absent", "Fibrosis, bronchiectasis, lung Ca, empyema"],
["Peripheral cyanosis", "Absent", "Cold/blue fingertips → poor perfusion / hypoxia"],
["Tar staining", "Absent", "Smoking history"],
["Fine tremor", "Absent", "Beta-2 agonist use (salbutamol)"],
["Asterixis / CO₂ flap", "Absent", "CO₂ retention → Type 2 respiratory failure"],
["Radial pulse rate/rhythm", "60–100 bpm, regular", "Tachycardia (hypoxia/infection); pulsus paradoxus (severe asthma)"],
["Peripheral warmth", "Warm", "Cold → vasospasm, poor perfusion"],
],
widths=[52*mm, 35*mm, doc.width-87*mm]
))
story.append(SP(2))
# ── SECTION 3: Face, Eyes, Mouth + Neck ──────────────────────────────────────
story.append(sec_header("③ FACE / EYES / MOUTH + NECK", C_HEADBG, "👁"))
story.append(SP(1))
face_neck = [
[Paragraph("<b>FACE</b>", sBold), Paragraph("<b>NECK</b>", sBold)],
[
checklist(["Central cyanosis (tongue/lips)",
"Conjunctival pallor (anaemia)",
"Horner's syndrome (Pancoast tumour)",
"Plethora / SVC obstruction",
"Cushingoid facies (steroids)",
"Pursed lip breathing"], cols=1),
checklist(["Tracheal position — central / deviated",
"Direction of deviation (push vs pull)",
"JVP — normal / elevated",
"Cervical / supraclavicular LN",
"Accessory muscle hypertrophy (SCM)",
"Goitre / neck mass"], cols=1),
]
]
story.append(Table(face_neck,
colWidths=[doc.width/2, doc.width/2],
style=TableStyle([
("TOPPADDING", (0,0),(-1,-1), 2),
("BOTTOMPADDING", (0,0),(-1,-1), 2),
("LEFTPADDING", (0,0),(-1,-1), 4),
("RIGHTPADDING", (0,0),(-1,-1), 4),
("BOX", (0,0),(-1,-1), 0.5, C_BORDER),
("LINEAFTER", (0,0),(0,-1), 0.5, C_BORDER),
("VALIGN", (0,0),(-1,-1), "TOP"),
])
))
story.append(SP(1))
# Trachea deviation quick-reference
trachea_note = Table([
[Paragraph("⚠ TRACHEA DEVIATION:", sWarn),
Paragraph("PUSHED AWAY from lesion → Large effusion, Tension pneumothorax", sBody),
Paragraph("PULLED TOWARDS lesion → Collapse, Fibrosis, Pneumonectomy", sBody)],
], colWidths=[38*mm, 80*mm, doc.width-118*mm],
style=TableStyle([
("BACKGROUND", (0,0),(-1,-1), C_YELLOW),
("TOPPADDING", (0,0),(-1,-1), 2), ("BOTTOMPADDING",(0,0),(-1,-1), 2),
("LEFTPADDING", (0,0),(-1,-1), 4), ("RIGHTPADDING", (0,0),(-1,-1), 4),
("BOX", (0,0),(-1,-1), 0.5, colors.HexColor("#f0c060")),
("VALIGN", (0,0),(-1,-1), "MIDDLE"),
]))
story.append(trachea_note)
story.append(SP(2))
# ── SECTION 4: INSPECTION of Chest ──────────────────────────────────────────
story.append(sec_header("④ CHEST — INSPECTION", C_TEAL, "🔍"))
story.append(SP(1))
insp_rows = [
[Paragraph("<b>SHAPE / DEFORMITY</b>", sBold),
Paragraph("<b>RESPIRATORY RATE & PATTERN</b>", sBold),
Paragraph("<b>SKIN / SCARS</b>", sBold)],
[
checklist(["Normal elliptical shape",
"Barrel chest (AP = transverse) — COPD",
"Pectus excavatum (funnel)",
"Pectus carinatum (pigeon)",
"Kyphoscoliosis",
"Harrison's sulcus (childhood asthma)"], cols=1),
checklist(["Rate: 12–20/min (adults)",
"Tachypnoea >20 / Bradypnoea <12",
"Cheyne-Stokes (CNS/cardiac failure)",
"Kussmaul (DKA — deep, regular)",
"Paradoxical movement (flail chest)",
"Intercostal/subcostal recession"], cols=1),
checklist(["Thoracotomy / VATS scars",
"Chest drain sites",
"Radiotherapy marks",
"Dilated veins (SVC obstruction)",
"Subcutaneous emphysema",
"Symmetry of chest movement"], cols=1),
]
]
story.append(Table(insp_rows,
colWidths=[doc.width/3]*3,
style=TableStyle([
("TOPPADDING", (0,0),(-1,-1), 2),
("BOTTOMPADDING", (0,0),(-1,-1), 2),
("LEFTPADDING", (0,0),(-1,-1), 4),
("RIGHTPADDING", (0,0),(-1,-1), 4),
("BOX", (0,0),(-1,-1), 0.5, C_BORDER),
("LINEAFTER", (0,0),(1,-1), 0.5, C_BORDER),
("VALIGN", (0,0),(-1,-1), "TOP"),
])
))
story.append(SP(2))
# ── SECTION 5: PALPATION ─────────────────────────────────────────────────────
story.append(sec_header("⑤ CHEST — PALPATION", C_HEADBG, "🖐"))
story.append(SP(1))
palp_left = [
Paragraph("<b>Expansion — Upper Zone</b>", sBold),
Paragraph("• Thumbs at clavicles, fingers over trapezii", sBody),
Paragraph("• Deep breath → thumbs separate symmetrically", sBody),
Paragraph("• Normal separation: <b>3–5 cm</b>", sBold),
SP(1),
Paragraph("<b>Expansion — Lower Zone</b>", sBold),
Paragraph("• Thumbs either side of xiphisternum", sBody),
Paragraph("• Fingers wrap around lower costal margin", sBody),
Paragraph("• Normal separation: <b>3–5 cm bilaterally</b>", sBold),
SP(1),
Paragraph("<b>Tactile Vocal Fremitus (TVF)</b>", sBold),
Paragraph("• Ulnar border / palm on chest wall", sBody),
Paragraph("• Patient says: \"ninety-nine\" or \"one-one-one\"", sBody),
Paragraph("• Compare zone by zone, left vs right", sBody),
Paragraph(f"{bull()}<b>Increased TVF</b> → Consolidation", sBold),
Paragraph(f"{bull()}<b>Decreased TVF</b> → Effusion, Pneumothorax, Collapse", sBody),
]
palp_right = [
Paragraph("<b>Tracheal Position (confirm)</b>", sBold),
Paragraph("• Index finger in suprasternal notch", sBody),
Paragraph("• Assess deviation from midline", sBody),
SP(1),
Paragraph("<b>Apex Beat</b>", sBold),
Paragraph("• Normally 5th ICS mid-clavicular line", sBody),
Paragraph("• Displaced laterally → cor pulmonale", sBody),
SP(1),
Paragraph("<b>Subcutaneous Emphysema</b>", sBold),
Paragraph("• Crackling on palpation → pneumothorax / trauma", sBody),
SP(1),
Paragraph("<b>Rib Tenderness / Springing</b>", sBold),
Paragraph("• Localised tenderness → rib fracture, pleurisy", sBody),
Paragraph("• AP compression → pain at fracture site", sBody),
SP(1),
Paragraph("<b>Lymph Nodes — Axillae</b>", sBold),
Paragraph("• Axillary nodes — malignancy, lymphoma", sBody),
]
story.append(Table([[palp_left, palp_right]],
colWidths=[doc.width/2, doc.width/2],
style=TableStyle([
("TOPPADDING", (0,0),(-1,-1), 3),
("BOTTOMPADDING", (0,0),(-1,-1), 3),
("LEFTPADDING", (0,0),(-1,-1), 5),
("RIGHTPADDING", (0,0),(-1,-1), 5),
("BOX", (0,0),(-1,-1), 0.5, C_BORDER),
("LINEAFTER", (0,0),(0,-1), 0.5, C_BORDER),
("VALIGN", (0,0),(-1,-1), "TOP"),
])
))
story.append(SP(2))
# ── SECTION 6: PERCUSSION ────────────────────────────────────────────────────
story.append(sec_header("⑥ CHEST — PERCUSSION", C_TEAL, "🥁"))
story.append(SP(1))
perc_notes = three_col(
["Percussion Note", "Quality", "Causes"],
[
["Resonant", "Hollow, drum-like", "Normal aerated lung"],
["Hyper-resonant","Louder, more hollow", "Pneumothorax, Emphysema (hyperinflation)"],
["Dull", "Thud-like, short", "Consolidation, Collapse, Tumour"],
["STONY DULL", "Extremely dull — no resonance","PLEURAL EFFUSION (pathognomonic)"],
["Tympanic", "Drum-like, hollow", "Large pneumothorax, Gastric bubble (LLL area)"],
],
widths=[38*mm, 58*mm, doc.width-96*mm]
)
story.append(perc_notes)
story.append(SP(1))
story.append(Paragraph("<b>Zones to Percuss (compare left vs right at each level):</b>", sBold))
zones = Table([
[Paragraph("<b>ANTERIOR</b>", ps("z1", fontSize=7, fontName="Helvetica-Bold", textColor=C_TEAL, leading=9)),
Paragraph("<b>POSTERIOR</b>", ps("z2", fontSize=7, fontName="Helvetica-Bold", textColor=C_TEAL, leading=9))],
[checklist(["Above clavicles (apices)",
"Below clavicles (upper zone)",
"2nd–3rd ICS (mid zone)",
"Cardiac area (note dullness)",
"Lower zones (4th–6th ICS)",
"Traube's space (LIF — gastric bubble)"], cols=1),
checklist(["Suprascapular (apices — TB)",
"Interscapular upper",
"Interscapular lower",
"Infrascapular (lower zones)",
"Axillae (lateral lower zones)",
"Liver upper border (R 5th–6th ICS)"], cols=1)],
], colWidths=[doc.width/2, doc.width/2],
style=TableStyle([
("TOPPADDING", (0,0),(-1,-1), 2),
("BOTTOMPADDING", (0,0),(-1,-1), 2),
("LEFTPADDING", (0,0),(-1,-1), 4),
("RIGHTPADDING", (0,0),(-1,-1), 4),
("BOX", (0,0),(-1,-1), 0.5, C_BORDER),
("LINEAFTER", (0,0),(0,-1), 0.5, C_BORDER),
("VALIGN", (0,0),(-1,-1), "TOP"),
]))
story.append(zones)
story.append(SP(2))
# ── SECTION 7: AUSCULTATION ──────────────────────────────────────────────────
story.append(sec_header("⑦ CHEST — AUSCULTATION", C_HEADBG, "🔊"))
story.append(SP(1))
# Breath sounds
story.append(Paragraph("<b>A. BREATH SOUNDS — CHARACTER</b>", sBold))
story.append(SP(0.5))
story.append(three_col(
["Type", "Site", "Quality", "Abnormal if heard over..."],
[
["Vesicular", "Normal peripheral lung", "Soft, rustling; Insp > Exp; no gap", "Absent over effusion/pneumothorax"],
["Bronchial", "Trachea/main bronchi only","Harsh, tubular; Exp ≥ Insp; GAP between I & E","Peripheral lung → consolidation, abscess"],
["Bronchovesicular","1st–2nd ICS ant.; interscapular","Intermediate — I = E","Abnormal elsewhere"],
],
widths=[28*mm, 40*mm, 50*mm, doc.width-118*mm]
))
story.append(SP(1))
# Added sounds
story.append(Paragraph("<b>B. ADDED (ADVENTITIOUS) SOUNDS</b>", sBold))
story.append(SP(0.5))
story.append(three_col(
["Sound", "Character", "Timing", "Causes"],
[
["Fine crackles", "Fine, high-pitched; like rubbing hair between fingers", "End-inspiratory; ✗ don't clear with cough", "Pulmonary fibrosis, early pulmonary oedema, early consolidation"],
["Coarse crackles","Loud, low-pitched, bubbling", "Early inspiratory; ✓ may clear with cough", "Bronchiectasis, pulmonary oedema, COPD, secretions"],
["Wheeze (rhonchi)","Musical, continuous", "Expiratory (or pan-insp in severe obstruction)", "Asthma/COPD = polyphonic; Tumour/FB = monophonic"],
["Stridor", "Harsh, high-pitched, LOUD — audible without stethoscope","Inspiratory", "Upper airway obstruction — larynx, trachea"],
["Pleural rub", "Leathery/creaking; like walking on fresh snow", "Insp AND Exp; localised; ✗ don't change with cough", "Pleurisy — viral, bacterial, PE, RA, SLE"],
],
widths=[28*mm, 50*mm, 38*mm, doc.width-116*mm]
))
story.append(SP(1))
# Vocal resonance
story.append(Paragraph("<b>C. VOCAL RESONANCE (patient says \"ninety-nine\" while you auscultate)</b>", sBold))
story.append(SP(0.5))
story.append(three_col(
["Sign", "Method", "Finding", "Interpretation"],
[
["Normal resonance", "Say \"ninety-nine\"", "Muffled words", "Normal"],
["Bronchophony", "Say \"ninety-nine\"", "Words clearly audible", "Consolidation"],
["Whispering pectoriloquy","Whisper \"1-2-3\"", "Whisper clearly audible", "Consolidation (most sensitive sign)"],
["Aegophony", "Say \"E\"", "Heard as \"A\" (nasal, bleating)","Above pleural effusion"],
["Reduced resonance", "Say \"ninety-nine\"", "Muffled / absent", "Effusion, Pneumothorax"],
],
widths=[38*mm, 36*mm, 40*mm, doc.width-114*mm]
))
story.append(SP(2))
# ── SECTION 8: LEGS + BEDSIDE Ix ─────────────────────────────────────────────
story.append(sec_header("⑧ LEGS + BEDSIDE INVESTIGATIONS", C_TEAL, "🦵"))
story.append(SP(1))
legs_ix = Table([
[
[
Paragraph("<b>LEGS</b>", sBold),
checklist(["Bilateral pitting oedema (cor pulmonale / RHF)",
"Unilateral calf swelling (DVT → PE)",
"Calf tenderness on palpation",
"Varicosities (chronic venous insufficiency)"], cols=1),
],
[
Paragraph("<b>BEDSIDE INVESTIGATIONS</b>", sBold),
two_col([
[Paragraph("Peak Flow (PEFR)", sBody), Paragraph("Note actual vs predicted; % predicted", sSmall)],
[Paragraph("SpO₂ (pulse oximetry)", sBody), Paragraph("Normal >94%; COPD target 88–92%", sSmall)],
[Paragraph("Sputum inspection", sBody), Paragraph("Volume, colour (white/yellow/green/blood), smell", sSmall)],
[Paragraph("Chest X-ray (PA)", sBody), Paragraph("Correlate clinical findings", sSmall)],
[Paragraph("Temperature chart", sBody), Paragraph("Fever pattern", sSmall)],
], col1=30*mm, col2=None),
]
]
], colWidths=[doc.width/2, doc.width/2],
style=TableStyle([
("TOPPADDING", (0,0),(-1,-1), 3),
("BOTTOMPADDING", (0,0),(-1,-1), 3),
("LEFTPADDING", (0,0),(-1,-1), 4),
("RIGHTPADDING", (0,0),(-1,-1), 4),
("BOX", (0,0),(-1,-1), 0.5, C_BORDER),
("LINEAFTER", (0,0),(0,-1), 0.5, C_BORDER),
("VALIGN", (0,0),(-1,-1), "TOP"),
]))
story.append(legs_ix)
story.append(SP(2))
# ── SECTION 9: CLINICAL PATTERNS SUMMARY ─────────────────────────────────────
story.append(sec_header("⑨ CLASSIC CLINICAL PATTERNS — AT-A-GLANCE SUMMARY", C_NAVY, "📊"))
story.append(SP(1))
story.append(pattern_table())
story.append(SP(1))
story.append(Paragraph("* Tension pneumothorax — trachea pushed AWAY; medical emergency (needle decompression 2nd ICS MCL)", sWarn))
story.append(SP(2))
# ── SECTION 10: DOCUMENTATION TEMPLATE ───────────────────────────────────────
story.append(sec_header("⑩ DOCUMENTATION TEMPLATE (fill at bedside)", C_TEAL, "📝"))
story.append(SP(1))
doc_template = """<b>GENERAL:</b> Patient is / is not in respiratory distress. RR = ___/min. SpO₂ = ___% (on air / ___L O₂)
Accessory muscle use: absent / present. Central cyanosis: absent / present.
<b>HANDS:</b> Clubbing: absent / Grade ___. Tar staining: absent / present. CO₂ flap: absent / present.
Pulse: ___ bpm, regular / irregular. Pulsus paradoxus: absent / present.
<b>FACE/NECK:</b> Cyanosis: absent / present. Anaemia: absent / present. Horner's: absent / present.
Trachea: central / deviated to ___ (pushed / pulled). JVP: normal / elevated ___ cm.
Lymphadenopathy: absent / present (site: ___).
<b>INSPECTION:</b> Shape: normal / barrel / ______________. Scars: none / _______________.
Chest movement: symmetric / asymmetric. Recession: absent / intercostal / subcostal.
<b>PALPATION:</b> Trachea: central / deviated ___.
Expansion — upper: equal / reduced on ___ side. Lower: equal / reduced on ___ side (=___ cm).
Vocal fremitus: normal / increased over ___ / reduced over ___.
<b>PERCUSSION:</b> Right: resonant / dull / stony dull / hyper-resonant at ___.
Left: resonant / dull / stony dull / hyper-resonant at ___.
<b>AUSCULTATION:</b> Breath sounds: vesicular / bronchial / absent / reduced over ___.
Vocal resonance: normal / bronchophony / whispering pectoriloquy / aegophony over ___.
Added sounds: nil / fine crackles / coarse crackles at ___ / wheeze (poly/mono) / pleural rub at ___.
<b>LEGS:</b> Bilateral pitting oedema: absent / present up to ___. Calf tenderness: absent / ___ side.
<b>SUMMARY:</b> Examination is consistent with / suggests _______________________________________________"""
story.append(Table([[Paragraph(doc_template, ps("dt", fontSize=7, fontName="Helvetica", leading=10, textColor=C_DGREY))]],
colWidths=[doc.width],
style=TableStyle([
("BACKGROUND", (0,0),(-1,-1), C_MINT),
("BOX", (0,0),(-1,-1), 0.6, C_TEAL),
("TOPPADDING", (0,0),(-1,-1), 5),
("BOTTOMPADDING", (0,0),(-1,-1), 5),
("LEFTPADDING", (0,0),(-1,-1), 7),
("RIGHTPADDING", (0,0),(-1,-1), 7),
])
))
story.append(SP(3))
story.append(HRFlowable(width="100%", thickness=0.5, color=C_BORDER))
story.append(SP(1))
story.append(Paragraph(
"Reference: MacLeod's Clinical Examination, 14th Edition — Douglas G, Nicol F, Robertson C | Churchill Livingstone, Elsevier | For educational use only",
sFooter))
# ── BUILD ──────────────────────────────────────────────────────────────────
doc.build(story, onFirstPage=on_page, onLaterPages=on_page)
print(f"PDF created: {OUTPUT}")
cd /home/daytona/workspace/resp-checklist && python3 build_pdf.py 2>&1
ls -lh /home/daytona/workspace/resp-checklist/Respiratory_Exam_Checklist.pdf
Respiratory Exam Checklist
PDF Document · PDF
| Section | Content |
|---|---|
| ① End-of-Bed | Breathing, accessory muscles, audible sounds, distress |
| ② Hands | Clubbing, cyanosis, tar staining, CO₂ flap, pulse |
| ③ Face / Neck | Central cyanosis, Horner's, JVP, tracheal deviation with push/pull guide |
| ④ Inspection | Chest shape, RR & pattern, scars, skin |
| ⑤ Palpation | Expansion technique, TVF, trachea, rib spring |
| ⑥ Percussion | All 5 percussion notes with causes, anterior + posterior zones |
| ⑦ Auscultation | Breath sound types, all added sounds table, vocal resonance (bronchophony, WPL, aegophony) |
| ⑧ Legs + Bedside Ix | DVT signs, oedema, PEFR, SpO₂, sputum |
| ⑨ Clinical Patterns | Full comparison table - all 9 conditions (consolidation, effusion, pneumothorax, collapse, fibrosis, COPD, asthma, bronchiectasis) |
| ⑩ Documentation template | Fill-in-the-blank proforma for bedside use |
Respiratory system Performa according to archith boloor examination
Archith Boloor clinical examination respiratory system proforma medicine
Archith Boloor "Manual of Clinical Medicine" respiratory examination
https://www.jaypeedigital.com/book/9789389587876/chapter/ch3
~/resp-checklist/boloor_resp.py
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import mm
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_LEFT, TA_CENTER
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, KeepTogether, PageBreak
)
OUTPUT = "/home/daytona/workspace/resp-checklist/Boloor_Respiratory_Proforma.pdf"
# ── Colours (Boloor book style — blue-green academic) ────────────────────────
C_NAVY = colors.HexColor("#1b2d5b")
C_BLUE = colors.HexColor("#1a5276")
C_TEAL = colors.HexColor("#148f77")
C_LTBLUE = colors.HexColor("#d6eaf8")
C_LTGREEN = colors.HexColor("#d5f5e3")
C_LTYELLOW= colors.HexColor("#fef9e7")
C_LTORANGE= colors.HexColor("#fdebd0")
C_LTRED = colors.HexColor("#fadbd8")
C_LGREY = colors.HexColor("#f2f3f4")
C_MGREY = colors.HexColor("#d5d8dc")
C_DGREY = colors.HexColor("#2c3e50")
C_RED = colors.HexColor("#c0392b")
C_ORANGE = colors.HexColor("#d35400")
C_WHITE = colors.white
W, H = A4
doc = SimpleDocTemplate(
OUTPUT,
pagesize=A4,
leftMargin=13*mm, rightMargin=13*mm,
topMargin=24*mm, bottomMargin=16*mm,
title="Respiratory System Proforma — Archith Boloor",
author="Based on: An Insider's Guide to Clinical Medicine, Archith Boloor"
)
styles = getSampleStyleSheet()
def ps(name, **kw):
return ParagraphStyle(name, parent=styles.get("Normal", styles["Normal"]), **kw)
# Style library
sTitle = ps("T", fontSize=14, fontName="Helvetica-Bold", textColor=C_WHITE, alignment=TA_CENTER, leading=18)
sSub = ps("S", fontSize=8, fontName="Helvetica", textColor=colors.HexColor("#aed6f1"), alignment=TA_CENTER, leading=10)
sSecH = ps("SH", fontSize=9, fontName="Helvetica-Bold", textColor=C_WHITE, leading=12)
sSecH2 = ps("S2", fontSize=8, fontName="Helvetica-Bold", textColor=C_NAVY, leading=11)
sBody = ps("B", fontSize=7.5,fontName="Helvetica", textColor=C_DGREY, leading=10)
sBold = ps("BB", fontSize=7.5,fontName="Helvetica-Bold", textColor=C_NAVY, leading=10)
sItalic = ps("BI", fontSize=7.2,fontName="Helvetica-Oblique", textColor=colors.HexColor("#555555"), leading=9)
sSmall = ps("SM", fontSize=6.8,fontName="Helvetica", textColor=colors.HexColor("#666666"), leading=9)
sWarn = ps("W", fontSize=7, fontName="Helvetica-Bold", textColor=C_RED, leading=9)
sGreen = ps("G", fontSize=7, fontName="Helvetica-Bold", textColor=C_TEAL, leading=9)
sOrange = ps("O", fontSize=7, fontName="Helvetica-Bold", textColor=C_ORANGE, leading=9)
sFooter = ps("F", fontSize=6.5,fontName="Helvetica-Oblique", textColor=colors.HexColor("#888888"), alignment=TA_CENTER, leading=8)
sLabel = ps("L", fontSize=7, fontName="Helvetica-Bold", textColor=C_BLUE, leading=9)
SP = lambda n=2: Spacer(1, n*mm)
# ── Canvas callbacks ─────────────────────────────────────────────────────────
def header_footer(canvas, doc):
canvas.saveState()
# Top banner
canvas.setFillColor(C_NAVY)
canvas.rect(0, H-20*mm, W, 20*mm, fill=1, stroke=0)
canvas.setFillColor(C_TEAL)
canvas.rect(0, H-22*mm, W, 2*mm, fill=1, stroke=0)
canvas.setFont("Helvetica-Bold", 13)
canvas.setFillColor(C_WHITE)
canvas.drawCentredString(W/2, H-13*mm, "RESPIRATORY SYSTEM EXAMINATION — PROFORMA")
canvas.setFont("Helvetica", 7.5)
canvas.setFillColor(colors.HexColor("#aed6f1"))
canvas.drawCentredString(W/2, H-18*mm,
"Based on: An Insider's Guide to Clinical Medicine | Dr Archith Boloor | Jaypee Medical Publishers")
# Bottom bar
canvas.setFillColor(C_NAVY)
canvas.rect(0, 0, W, 11*mm, fill=1, stroke=0)
canvas.setFillColor(C_TEAL)
canvas.rect(0, 11*mm, W, 1*mm, fill=1, stroke=0)
canvas.setFont("Helvetica", 6.5)
canvas.setFillColor(colors.HexColor("#aed6f1"))
canvas.drawString(13*mm, 4*mm, "Archith Boloor — An Insider's Guide to Clinical Medicine | KMC Mangaluru")
canvas.drawRightString(W-13*mm, 4*mm, f"Page {doc.page}")
canvas.restoreState()
# ── Helpers ──────────────────────────────────────────────────────────────────
def sec(text, bg=C_BLUE, icon=""):
txt = f"{icon} {text}" if icon else text
return KeepTogether([
Table([[Paragraph(txt, sSecH)]],
colWidths=[doc.width],
style=TableStyle([
("BACKGROUND", (0,0),(-1,-1), bg),
("TOPPADDING", (0,0),(-1,-1), 4),
("BOTTOMPADDING", (0,0),(-1,-1), 4),
("LEFTPADDING", (0,0),(-1,-1), 7),
]))
])
def subsec(text):
return Table([[Paragraph(text, sSecH2)]],
colWidths=[doc.width],
style=TableStyle([
("BACKGROUND", (0,0),(-1,-1), C_LTBLUE),
("TOPPADDING", (0,0),(-1,-1), 2),
("BOTTOMPADDING", (0,0),(-1,-1), 2),
("LEFTPADDING", (0,0),(-1,-1), 7),
("LINEBELOW", (0,0),(-1,-1), 0.5, C_BLUE),
]))
def field_row(label, blank_w=None, note=""):
bw = blank_w or (doc.width - 55*mm)
note_p = Paragraph(note, sItalic) if note else Paragraph("", sBody)
return [Paragraph(label, sBold),
Table([[Paragraph("", sBody)]], colWidths=[bw],
style=TableStyle([("LINEBELOW",(0,0),(-1,-1),0.4,C_MGREY),
("TOPPADDING",(0,0),(-1,-1),4),
("BOTTOMPADDING",(0,0),(-1,-1),0)])),
note_p]
def grid(rows, w1=55*mm, w2=None, w3=35*mm, alt=True):
w2 = w2 or (doc.width - w1 - w3)
style = [
("FONTNAME", (0,0),(-1,-1),"Helvetica"),
("FONTSIZE", (0,0),(-1,-1),7.2),
("TOPPADDING", (0,0),(-1,-1),2),
("BOTTOMPADDING",(0,0),(-1,-1),2),
("LEFTPADDING", (0,0),(-1,-1),4),
("RIGHTPADDING",(0,0),(-1,-1),4),
("GRID", (0,0),(-1,-1),0.3,C_MGREY),
("VALIGN", (0,0),(-1,-1),"TOP"),
]
if alt:
for i in range(0,len(rows),2):
style.append(("BACKGROUND",(0,i),(-1,i),C_LGREY))
return Table(rows, colWidths=[w1,w2,w3], style=TableStyle(style))
def col2_table(rows, w1=70*mm, bg_alt=True):
w2 = doc.width - w1
st = [
("FONTNAME", (0,0),(-1,-1),"Helvetica"),
("FONTSIZE", (0,0),(-1,-1),7.2),
("TOPPADDING", (0,0),(-1,-1),2),
("BOTTOMPADDING",(0,0),(-1,-1),2),
("LEFTPADDING", (0,0),(-1,-1),5),
("RIGHTPADDING",(0,0),(-1,-1),4),
("GRID", (0,0),(-1,-1),0.3,C_MGREY),
("VALIGN", (0,0),(-1,-1),"TOP"),
]
if bg_alt:
for i in range(0,len(rows),2):
st.append(("BACKGROUND",(0,i),(-1,i),C_LGREY))
return Table(rows, colWidths=[w1,w2], style=TableStyle(st))
def cb_line(items, cols=2):
rows=[]
for i in range(0,len(items),cols):
row=[]
for j in range(cols):
txt = f"☐ {items[i+j]}" if i+j<len(items) else ""
row.append(Paragraph(txt, sBody))
rows.append(row)
return Table(rows, colWidths=[doc.width/cols]*cols,
style=TableStyle([
("TOPPADDING",(0,0),(-1,-1),1),
("BOTTOMPADDING",(0,0),(-1,-1),1),
("LEFTPADDING",(0,0),(-1,-1),3),
("RIGHTPADDING",(0,0),(-1,-1),3),
]))
def hdr_row(cells, bg=C_TEAL):
return [Paragraph(f"<b>{c}</b>", ps("hc",fontSize=7,fontName="Helvetica-Bold",
textColor=C_WHITE,leading=9)) for c in cells]
def info_box(text, bg=C_LTYELLOW, border=colors.HexColor("#f0c060")):
return Table([[Paragraph(text, ps("ib",fontSize=7.2,fontName="Helvetica",
textColor=C_DGREY,leading=10))]],
colWidths=[doc.width],
style=TableStyle([
("BACKGROUND",(0,0),(-1,-1),bg),
("BOX",(0,0),(-1,-1),0.6,border),
("TOPPADDING",(0,0),(-1,-1),4),
("BOTTOMPADDING",(0,0),(-1,-1),4),
("LEFTPADDING",(0,0),(-1,-1),6),
("RIGHTPADDING",(0,0),(-1,-1),6),
]))
def warn_box(text):
return info_box(text, bg=C_LTRED, border=C_RED)
# ════════════════════════════════════════════════════════════════════════════
# STORY
# ════════════════════════════════════════════════════════════════════════════
story = []
# ──────────────────────────────────────────────────────────────────────────
# PART A: CASE SHEET FORMAT (Boloor Section A)
# ──────────────────────────────────────────────────────────────────────────
story.append(SP(1))
story.append(sec("PART A — CASE SHEET FORMAT", C_NAVY, "📋"))
story.append(SP(1))
# Patient bio
story.append(subsec("PATIENT IDENTIFICATION"))
story.append(SP(0.5))
bio = col2_table([
[Paragraph("<b>Name:</b>", sBold), Paragraph("_"*40, sBody)],
[Paragraph("<b>Age / Sex:</b>", sBold), Paragraph("___ years / M / F", sBody)],
[Paragraph("<b>Occupation:</b>", sBold), Paragraph("_"*40, sBody)],
[Paragraph("<b>Address / Residence:</b>", sBold), Paragraph("_"*40, sBody)],
[Paragraph("<b>Informant:</b>", sBold), Paragraph("_"*30+" Reliable / Unreliable", sBody)],
[Paragraph("<b>IP No. / OP No.:</b>", sBold), Paragraph("_"*30, sBody)],
[Paragraph("<b>Date of Admission:</b>", sBold), Paragraph("_"*20, sBody)],
], w1=45*mm)
story.append(bio)
story.append(SP(2))
# Chief Complaints
story.append(subsec("CHIEF COMPLAINTS (in patient's own words, in order of duration)"))
story.append(SP(0.5))
cc_rows = [[Paragraph(f"{i+1}.", sBold),
Table([[Paragraph("",sBody)]],colWidths=[doc.width-38*mm],
style=TableStyle([("LINEBELOW",(0,0),(-1,-1),0.4,C_MGREY),
("TOPPADDING",(0,0),(-1,-1),5),("BOTTOMPADDING",(0,0),(-1,-1),0)])),
Paragraph("since ___________", sItalic)] for i in range(5)]
story.append(Table(cc_rows, colWidths=[8*mm, doc.width-38*mm, 30*mm],
style=TableStyle([
("TOPPADDING",(0,0),(-1,-1),1),("BOTTOMPADDING",(0,0),(-1,-1),2),
("LEFTPADDING",(0,0),(-1,-1),3),("RIGHTPADDING",(0,0),(-1,-1),3),
("VALIGN",(0,0),(-1,-1),"BOTTOM"),
])))
story.append(SP(2))
# HOPI - Cardinal Symptoms
story.append(subsec("HISTORY OF PRESENTING ILLNESS — CARDINAL RESPIRATORY SYMPTOMS (Boloor's framework)"))
story.append(SP(0.5))
story.append(info_box(
"Boloor's 6 Cardinal Respiratory Symptoms: "
"<b>1. Cough 2. Sputum 3. Haemoptysis 4. Breathlessness / Dyspnoea 5. Chest pain 6. Wheeze</b> "
"— Enquire each in detail using the SOCRATES framework + specific respiratory descriptors."
))
story.append(SP(1))
# 1. Cough
story.append(Paragraph("<b>1. COUGH</b>", sLabel))
story.append(SP(0.5))
cough_items = [
("Onset", "Sudden / Gradual"),
("Duration", "___ days / weeks / months / years"),
("Type", "Dry / Productive"),
("Character", "Barking (croup) / Brassy (tracheal) / Bovine (RLN palsy) / Whooping (pertussis)"),
("Timing", "Morning (bronchiectasis/COPD) / Night (asthma/GORD) / Postural (bronchiectasis)"),
("Aggravating factors", "Cold air / Exercise / Lying flat / Smoke"),
("Relieving factors", "Medications / Posture"),
("Associated with haemoptysis", "Yes / No"),
]
c_rows = [[Paragraph(f"<b>{k}:</b>",sBold), Paragraph(v,sBody)] for k,v in cough_items]
story.append(col2_table(c_rows, w1=45*mm))
story.append(SP(1))
# 2. Sputum
story.append(Paragraph("<b>2. SPUTUM</b>", sLabel))
story.append(SP(0.5))
sputum_items = [
("Amount (24 hr)", "< 10 mL / 10–100 mL (moderate) / > 100 mL (copious — bronchiectasis)"),
("Colour", "White/grey (viral) / Yellow (bacterial) / Green (Pseudomonas) / Rust (pneumococcal pneumonia) / Black (coal dust/smoke) / Pink frothy (pulmonary oedema)"),
("Consistency", "Mucoid / Mucopurulent / Purulent / Watery"),
("Odour", "Foul-smelling (lung abscess, bronchiectasis) / Odourless"),
("Blood in sputum", "Yes / No → see Haemoptysis below"),
("Postural variation", "Increases on lying down on one side (bronchiectasis)"),
("3 layers on standing", "Top (frothy) / Middle (mucoid) / Bottom (purulent) — Bronchiectasis"),
]
s_rows = [[Paragraph(f"<b>{k}:</b>",sBold), Paragraph(v,sBody)] for k,v in sputum_items]
story.append(col2_table(s_rows, w1=45*mm))
story.append(SP(1))
# 3. Haemoptysis
story.append(Paragraph("<b>3. HAEMOPTYSIS</b>", sLabel))
story.append(SP(0.5))
h_items = [
("Amount", "Streaks / Moderate (< 200 mL/day) / Massive (> 200 mL/day or > 600 mL/24 hrs)"),
("Colour", "Bright red (TB, bronchiectasis, carcinoma) / Dark (old blood) / Pink frothy (pulmonary oedema — NOT true haemoptysis)"),
("Mixed with sputum", "Yes / No"),
("Preceded by cough", "Yes / No (distinguishes from haematemesis)"),
("Common causes to ask", "TB / Carcinoma lung / Bronchiectasis / Mitral stenosis / PE"),
("Pseudohaemoptysis", "Blood from nose/throat/gums? — examine oropharynx"),
]
h_rows = [[Paragraph(f"<b>{k}:</b>",sBold), Paragraph(v,sBody)] for k,v in h_items]
story.append(col2_table(h_rows, w1=45*mm))
story.append(SP(1))
# 4. Breathlessness
story.append(Paragraph("<b>4. BREATHLESSNESS / DYSPNOEA</b>", sLabel))
story.append(SP(0.5))
story.append(info_box(
"<b>GRADE by MRC Dyspnoea Scale (Boloor's preferred grading):</b> "
"Grade I — Breathless on strenuous exertion | "
"Grade II — Breathless walking uphill / hurrying | "
"Grade III — Walks slower than people of same age on level | "
"Grade IV — Stops for breath after 100 m on level | "
"Grade V — Too breathless to leave the house / breathless on dressing",
bg=C_LTGREEN, border=C_TEAL
))
story.append(SP(0.5))
d_items = [
("Onset", "Sudden (pneumothorax, PE, foreign body) / Gradual (COPD, fibrosis, effusion)"),
("Progression", "Static / Slowly progressive / Rapidly worsening / Episodic"),
("Orthopnoea", "Breathless lying flat — number of pillows: ___ (cardiac cause / bilateral diaphragm palsy)"),
("PND", "Paroxysmal nocturnal dyspnoea — wakes from sleep (cardiac asthma / LVF)"),
("Diurnal variation", "Morning dipping (asthma) / Evening worsening (occupational)"),
("Wheeze associated", "Yes / No (inspiratory = laryngeal/tracheal; expiratory = lower airways)"),
("Stridor associated", "Yes / No (upper airway obstruction — larynx, trachea)"),
("Associated chest pain", "Yes / No"),
]
d_rows = [[Paragraph(f"<b>{k}:</b>",sBold), Paragraph(v,sBody)] for k,v in d_items]
story.append(col2_table(d_rows, w1=42*mm))
story.append(SP(1))
# 5. Chest pain
story.append(Paragraph("<b>5. CHEST PAIN</b>", sLabel))
story.append(SP(0.5))
cp_items = [
("Site", "Localised / Diffuse / Retrosternal / Lateral chest wall"),
("Onset", "Sudden (pneumothorax, PE) / Gradual (pleurisy, tumour)"),
("Character", "Sharp / Stabbing / Dull / Crushing"),
("Radiation", "To shoulder (diaphragmatic) / To arm (cardiac) / No radiation"),
("Aggravating", "Deep inspiration / Coughing / Movement (pleuritic) vs Rest (cardiac)"),
("Relieving", "Leaning forward (pericarditis) / Analgesics / GTN (cardiac)"),
("Pleuritic vs cardiac", "Pleuritic: sharp, worse on breathing/coughing, positional; Cardiac: heavy, radiating, exertional"),
]
cp_rows = [[Paragraph(f"<b>{k}:</b>",sBold), Paragraph(v,sBody)] for k,v in cp_items]
story.append(col2_table(cp_rows, w1=42*mm))
story.append(SP(1))
# 6. Wheeze
story.append(Paragraph("<b>6. WHEEZE & STRIDOR</b>", sLabel))
story.append(SP(0.5))
w_items = [
("Wheeze onset", "Episodic / Continuous"),
("Timing", "Expiratory (lower airway — asthma/COPD) / Inspiratory (upper airway obstruction)"),
("Diurnal variation", "Early morning / Nocturnal (asthma) / Exercise-induced"),
("Triggers", "Allergens / Smoke / Cold air / Exercise / Drugs (beta-blockers, NSAIDs, aspirin)"),
("Stridor", "Inspiratory (extrathoracic obstruction) / Expiratory (intrathoracic obstruction)"),
("Associated with", "Fever / Allergic rhinitis / Eczema / Family history of atopy"),
]
w_rows = [[Paragraph(f"<b>{k}:</b>",sBold), Paragraph(v,sBody)] for k,v in w_items]
story.append(col2_table(w_rows, w1=42*mm))
story.append(SP(2))
# Associated symptoms
story.append(subsec("ASSOCIATED / CONSTITUTIONAL SYMPTOMS"))
story.append(SP(0.5))
story.append(cb_line([
"Fever (grade / character / duration)",
"Weight loss (quantify: ___ kg over ___ months)",
"Anorexia / Loss of appetite",
"Night sweats (TB, lymphoma, malignancy)",
"Hoarseness of voice (RLN palsy, laryngeal TB)",
"Dysphagia (mediastinal mass / oesophageal Ca)",
"Facial puffiness (SVC syndrome)",
"Ankle swelling / leg oedema (cor pulmonale)",
"Headache / neurological symptoms (metastasis)",
"Skin changes / rash (sarcoidosis, lupus)",
"Joint pains (pulmonary-rheumatological)",
"Smoking history: ___ pack-years",
], cols=2))
story.append(SP(2))
# Past history
story.append(subsec("PAST HISTORY"))
story.append(SP(0.5))
past = col2_table([
[Paragraph("<b>Previous similar illness:</b>", sBold), Paragraph("Yes / No — details: ___________", sBody)],
[Paragraph("<b>TB / contact with TB:</b>", sBold), Paragraph("Yes / No — treated (duration/drugs): ___________", sBody)],
[Paragraph("<b>Asthma / Atopy:</b>", sBold), Paragraph("Yes / No — childhood onset / adult onset", sBody)],
[Paragraph("<b>Previous pneumonia / effusion:</b>", sBold), Paragraph("Yes / No", sBody)],
[Paragraph("<b>COPD / Bronchiectasis:</b>", sBold), Paragraph("Yes / No", sBody)],
[Paragraph("<b>Malignancy:</b>", sBold), Paragraph("Yes / No — type / treatment", sBody)],
[Paragraph("<b>Surgeries / procedures:</b>", sBold), Paragraph("Yes / No — thoracotomy / bronchoscopy / VATS: ___", sBody)],
[Paragraph("<b>Diabetes / Hypertension:</b>", sBold), Paragraph("Yes / No (risk for infections, vascular disease)", sBody)],
[Paragraph("<b>Immunodeficiency / HIV:</b>", sBold), Paragraph("Yes / No (opportunistic infections)", sBody)],
[Paragraph("<b>Drug allergy:</b>", sBold), Paragraph("Yes / No — specify: ___________", sBody)],
], w1=52*mm)
story.append(past)
story.append(SP(2))
# Drug / Treatment history
story.append(subsec("TREATMENT HISTORY & DRUG HISTORY"))
story.append(SP(0.5))
story.append(cb_line([
"Inhalers (SABA / LABA / ICS / LAMA) — type & duration",
"Anti-TB drugs — HRZE / currently on treatment",
"ACE inhibitors (dry cough side effect)",
"Beta-blockers (bronchoconstriction in asthma)",
"NSAIDs / Aspirin (analgesic asthma)",
"Amiodarone (pulmonary toxicity / fibrosis)",
"Methotrexate (interstitial lung disease)",
"Steroids — oral / inhaled / duration",
"Antibiotics — type / duration / response",
"Chemotherapy / Radiotherapy — type & dates",
"Home oxygen therapy — flow rate / duration",
"Nebulisation — frequency / type",
], cols=2))
story.append(SP(2))
# Family history
story.append(subsec("FAMILY HISTORY"))
story.append(SP(0.5))
story.append(cb_line([
"Tuberculosis in family / close contacts",
"Asthma / Atopy / Allergic rhinitis",
"Malignancy (lung / head and neck)",
"Alpha-1 antitrypsin deficiency (early-onset emphysema)",
"Cystic fibrosis (recurrent chest infections in siblings)",
"Consanguinity",
], cols=2))
story.append(SP(2))
# Personal / Social
story.append(subsec("PERSONAL HISTORY & SOCIO-OCCUPATIONAL HISTORY"))
story.append(SP(0.5))
pers = col2_table([
[Paragraph("<b>Smoking:</b>",sBold), Paragraph("Yes / No / Ex-smoker — Pack-years = (no. of packs/day × no. of years): ___ pack-years", sBody)],
[Paragraph("<b>Alcohol:</b>",sBold), Paragraph("Yes / No — units/week: ___ (aspiration pneumonia risk)", sBody)],
[Paragraph("<b>Occupation:</b>",sBold), Paragraph("Farmer / Miner / Welder / Textile / Chemical / Healthcare worker", sBody)],
[Paragraph("<b>Occupational exposure:</b>",sBold), Paragraph("Silica / Asbestos / Coal dust / Bird droppings / Organic antigens → Hypersensitivity pneumonitis", sBody)],
[Paragraph("<b>Diet / Nutrition:</b>",sBold), Paragraph("Vegetarian / Non-vegetarian — relevant for TB (poor nutrition)", sBody)],
[Paragraph("<b>Pets / Birds:</b>",sBold), Paragraph("Yes / No — type: ___ (Psittacosis / extrinsic allergic alveolitis)", sBody)],
[Paragraph("<b>Travel history:</b>",sBold), Paragraph("TB endemic area / tropical infections (melioidosis, paragonimiasis)", sBody)],
[Paragraph("<b>Socioeconomic status:</b>",sBold), Paragraph("Upper / Middle / Lower — overcrowding (TB risk)", sBody)],
], w1=52*mm)
story.append(pers)
story.append(SP(3))
# ──────────────────────────────────────────────────────────────────────────
# PART B: GENERAL EXAMINATION (Boloor's emphasis on respiratory signs)
# ──────────────────────────────────────────────────────────────────────────
story.append(sec("PART B — GENERAL EXAMINATION", C_NAVY, "🩺"))
story.append(SP(1))
story.append(subsec("VITALS"))
story.append(SP(0.5))
vitals = col2_table([
[Paragraph("<b>Temperature:</b>",sBold), Paragraph("___°F / ___°C Febrile / Afebrile", sBody)],
[Paragraph("<b>Pulse rate:</b>",sBold), Paragraph("___ bpm Regular / Irregular Pulsus paradoxus: Yes / No", sBody)],
[Paragraph("<b>Respiratory Rate:</b>",sBold), Paragraph("___ breaths/min (Normal 14–20/min) Tachypnoea: Yes / No", sBody)],
[Paragraph("<b>Blood pressure:</b>",sBold), Paragraph("___ / ___ mmHg R arm / L arm", sBody)],
[Paragraph("<b>SpO₂:</b>",sBold), Paragraph("___% on room air / ___% on ___ L/min O₂", sBody)],
[Paragraph("<b>Peak Flow (PEFR):</b>",sBold), Paragraph("___ L/min Predicted: ___ L/min %Predicted: ___", sBody)],
], w1=52*mm)
story.append(vitals)
story.append(SP(1))
story.append(subsec("GENERAL PHYSICAL EXAMINATION (Boloor's systematic approach)"))
story.append(SP(0.5))
gpe = [
[hdr_row(["Parameter","Finding","Significance"],bg=C_NAVY)[0],
hdr_row(["Parameter","Finding","Significance"],bg=C_NAVY)[1],
hdr_row(["Parameter","Finding","Significance"],bg=C_NAVY)[2]],
[Paragraph("Built & Nourishment",sBody), Paragraph("Well / Moderately / Poorly built & nourished",sBody), Paragraph("Cachexia → malignancy, TB",sBody)],
[Paragraph("Conscious level",sBody), Paragraph("Alert / Confused / Drowsy / GCS = ___",sBody), Paragraph("CO₂ narcosis, hypoxia",sBody)],
[Paragraph("<b>Clubbing</b>",sBold), Paragraph("Absent / Grade I / II / III / IV",sBody), Paragraph("Fibrosis, bronchiectasis, Ca lung, suppurative",sBody)],
[Paragraph("Cyanosis — peripheral",sBody), Paragraph("Absent / Present",sBody), Paragraph("Cold blue fingers, reduced perfusion",sBody)],
[Paragraph("<b>Cyanosis — central</b>",sBold), Paragraph("Absent / Present (check tongue/lips)",sBody), Paragraph("SpO₂ <85%; Type I or II resp failure",sBody)],
[Paragraph("<b>Anaemia</b>",sBold), Paragraph("Absent / Mild / Moderate / Severe",sBody), Paragraph("Chronic disease, malignancy, TB",sBody)],
[Paragraph("Icterus",sBody), Paragraph("Absent / Present",sBody), Paragraph("Right heart failure, hepatic secondaries",sBody)],
[Paragraph("Lymphadenopathy",sBody), Paragraph("Absent / Present — site: ___",sBody), Paragraph("TB, lymphoma, malignancy, sarcoidosis",sBody)],
[Paragraph("<b>Pedal oedema</b>",sBold), Paragraph("Absent / Present — pitting / non-pitting up to ___",sBody), Paragraph("Cor pulmonale, right heart failure",sBody)],
[Paragraph("Tar staining",sBody), Paragraph("Absent / Present",sBody), Paragraph("Heavy smoker",sBody)],
[Paragraph("<b>CO₂ flap (asterixis)</b>",sBold), Paragraph("Absent / Present",sBody), Paragraph("CO₂ retention → Type 2 resp failure",sBody)],
[Paragraph("Fine tremor (hands)",sBody), Paragraph("Absent / Present",sBody), Paragraph("Beta-2 agonist excess",sBody)],
[Paragraph("Horner's syndrome",sBody), Paragraph("Absent / Present (ptosis + miosis + anhidrosis)",sBody), Paragraph("Pancoast tumour (apex lung Ca)",sBody)],
[Paragraph("Facial puffiness",sBody), Paragraph("Absent / Present",sBody), Paragraph("SVC syndrome (dilated neck veins, non-pulsatile)",sBody)],
[Paragraph("JVP",sBody), Paragraph("Normal / Elevated ___ cm above sternal angle",sBody), Paragraph("Cor pulmonale, right heart failure",sBody)],
[Paragraph("Skin rash",sBody), Paragraph("Absent / Present — describe: ___",sBody), Paragraph("Sarcoidosis (lupus pernio), SLE, vasculitis",sBody)],
[Paragraph("Joints",sBody), Paragraph("Normal / Swollen / Tender — site: ___",sBody), Paragraph("Hypertrophic pulmonary osteoarthropathy (HPOA)",sBody)],
]
# set column widths
gpe_t = Table(gpe, colWidths=[40*mm, 75*mm, doc.width-115*mm],
style=TableStyle([
("BACKGROUND", (0,0),(-1,0), C_NAVY),
("TEXTCOLOR", (0,0),(-1,0), C_WHITE),
("FONTNAME", (0,0),(-1,0), "Helvetica-Bold"),
("FONTSIZE", (0,0),(-1,-1), 7),
("TOPPADDING", (0,0),(-1,-1), 2),
("BOTTOMPADDING", (0,0),(-1,-1), 2),
("LEFTPADDING", (0,0),(-1,-1), 4),
("RIGHTPADDING", (0,0),(-1,-1), 4),
("GRID", (0,0),(-1,-1), 0.3, C_MGREY),
("VALIGN", (0,0),(-1,-1), "TOP"),
] + [("BACKGROUND",(0,i),(-1,i),C_LGREY) for i in range(1,len(gpe),2)])
)
story.append(gpe_t)
story.append(SP(3))
# ──────────────────────────────────────────────────────────────────────────
# PART C: SYSTEMIC EXAMINATION — RESPIRATORY SYSTEM
# ──────────────────────────────────────────────────────────────────────────
story.append(sec("PART C — SYSTEMIC EXAMINATION: RESPIRATORY SYSTEM", C_NAVY, "🫁"))
story.append(SP(1))
story.append(info_box(
"<b>Boloor's sequence:</b> "
"NECK (trachea) → INSPECTION of chest → PALPATION (expansion + TVF) → PERCUSSION → AUSCULTATION "
"Perform ANTERIORLY first, then POSTERIORLY, then AXILLAE. Always compare left vs right at each level.",
bg=C_LTBLUE, border=C_BLUE
))
story.append(SP(1))
# ── C1: NECK / TRACHEA ────────────────────────────────────────────────────
story.append(subsec("C1. NECK — TRACHEAL POSITION (Boloor: examine before chest)"))
story.append(SP(0.5))
trachea = col2_table([
[Paragraph("<b>Position:</b>",sBold), Paragraph("Central / Deviated to the RIGHT / Deviated to the LEFT",sBody)],
[Paragraph("<b>Direction:</b>",sBold), Paragraph("Pushed AWAY from lesion / Pulled TOWARDS lesion",sBody)],
[Paragraph("<b>If pushed away:</b>",sGreen), Paragraph("Large pleural effusion / Tension pneumothorax / Large tumour",sBody)],
[Paragraph("<b>If pulled towards:</b>",sWarn), Paragraph("Collapse / Fibrosis / Pneumonectomy (ipsilateral volume loss)",sBody)],
[Paragraph("<b>Tracheal tug:</b>",sBold), Paragraph("Absent / Present (downward movement on inspiration → severe hyperinflation)",sBody)],
], w1=48*mm)
story.append(trachea)
story.append(SP(2))
# ── C2: INSPECTION ────────────────────────────────────────────────────────
story.append(subsec("C2. INSPECTION OF CHEST (anterior + posterior)"))
story.append(SP(0.5))
insp_data = [
[hdr_row(["Feature","Abnormality","Boloor's Clinical Note"],bg=C_TEAL)[0],
hdr_row(["Feature","Abnormality","Boloor's Clinical Note"],bg=C_TEAL)[1],
hdr_row(["Feature","Abnormality","Boloor's Clinical Note"],bg=C_TEAL)[2]],
["Shape of chest","Normal elliptical / Barrel (AP = transverse) / Pigeon / Funnel / Kyphoscoliosis","Barrel chest → hyperinflation; Harrison's sulcus → childhood asthma"],
["Symmetry","Symmetric / Asymmetric — which side full/flat","Full side → effusion/pneumothorax; Flat side → collapse/fibrosis"],
["Respiratory rate","___ /min — Normal 14–20/min","Tachypnoea (>20) common in pneumonia, PE, acidosis"],
["Respiratory pattern","Normal / Laboured / Paradoxical / Cheyne-Stokes / Kussmaul","Paradoxical → flail chest; Cheyne-Stokes → CNS/cardiac failure"],
["Intercostal spaces","Normal / Bulging / Sunken","Bulging → large effusion; Sunken → collapse/fibrosis"],
["Use of accessory muscles","Absent / SCM / scalene / abdominal","Severe obstruction (asthma, COPD) or respiratory failure"],
["Recession","Absent / Intercostal / Subcostal / Supraclavicular","Severe upper airway obstruction (stridor), acute asthma"],
["Chest movements","Equal bilaterally / Reduced on ___ side","Unilateral reduction → effusion, collapse, consolidation, pneumothorax"],
["Scars","None / Thoracotomy / VATS / Chest drain sites at ___","Relevant to post-surgical changes on X-ray and examination"],
["Dilated veins","Absent / Present — direction of flow: upwards","SVC obstruction — flow is from below upwards (opposite of normal)"],
["Subcutaneous emphysema","Absent / Present","Pneumothorax / ruptured airway / post-procedure"],
]
insp_t = Table(
[[Paragraph(c if isinstance(c,str) else c,
ps("ic",fontSize=7,fontName="Helvetica",textColor=C_DGREY,leading=9)) for c in r]
if i>0 else r for i,r in enumerate(insp_data)],
colWidths=[38*mm, 62*mm, doc.width-100*mm],
style=TableStyle([
("BACKGROUND",(0,0),(-1,0),C_TEAL),
("TEXTCOLOR",(0,0),(-1,0),C_WHITE),
("FONTNAME",(0,0),(-1,-1),"Helvetica"),
("FONTNAME",(0,0),(-1,0),"Helvetica-Bold"),
("FONTSIZE",(0,0),(-1,-1),7),
("TOPPADDING",(0,0),(-1,-1),2),("BOTTOMPADDING",(0,0),(-1,-1),2),
("LEFTPADDING",(0,0),(-1,-1),4),("RIGHTPADDING",(0,0),(-1,-1),4),
("GRID",(0,0),(-1,-1),0.3,C_MGREY),
("VALIGN",(0,0),(-1,-1),"TOP"),
] + [("BACKGROUND",(0,i),(-1,i),C_LGREY) for i in range(1,len(insp_data),2)])
)
story.append(insp_t)
story.append(SP(2))
# ── C3: PALPATION ─────────────────────────────────────────────────────────
story.append(subsec("C3. PALPATION"))
story.append(SP(0.5))
palp_data = [
[hdr_row(["Step / Finding","Method (Boloor's description)","Result / Interpretation"],bg=C_BLUE)[0],
hdr_row(["Step / Finding","Method (Boloor's description)","Result / Interpretation"],bg=C_BLUE)[1],
hdr_row(["Step / Finding","Method (Boloor's description)","Result / Interpretation"],bg=C_BLUE)[2]],
["Apex beat","Normally 5th ICS, MCL — note if displaced laterally","Displaced laterally → cor pulmonale"],
["Chest expansion — Upper zone","Thumbs at level of clavicles, fingers over trapezii; patient takes deep breath","Equal / Reduced on ___ side (normal: 3–5 cm separation)"],
["Chest expansion — Lower zone","Thumbs parallel at xiphisternum; fingers along lower costal margin","Equal / Reduced on ___ side (normal: 3–5 cm)"],
["Tactile vocal fremitus (TVF)","Ulnar border of right hand on chest wall; patient says 'ninety-nine'; compare zones","↑ Increased (consolidation) / ↓ Decreased (effusion, pneumothorax, collapse)"],
["Subcutaneous emphysema","Palpate chest wall for crackling sensation","Present → pneumothorax, trauma, ruptured airway"],
["Rib tenderness","Localised pressure + AP springing","Present → fracture, pleurisy"],
["Lymph nodes — axillae","Palpate axillary nodes while supporting the arm","Enlarged → malignancy, lymphoma"],
]
palp_t = Table(
[[Paragraph(c,ps("pc",fontSize=7,fontName="Helvetica",textColor=C_DGREY,leading=9)) for c in r]
if i>0 else r for i,r in enumerate(palp_data)],
colWidths=[40*mm, 73*mm, doc.width-113*mm],
style=TableStyle([
("BACKGROUND",(0,0),(-1,0),C_BLUE),
("TEXTCOLOR",(0,0),(-1,0),C_WHITE),
("FONTNAME",(0,0),(-1,0),"Helvetica-Bold"),
("FONTNAME",(0,0),(-1,-1),"Helvetica"),
("FONTSIZE",(0,0),(-1,-1),7),
("TOPPADDING",(0,0),(-1,-1),2),("BOTTOMPADDING",(0,0),(-1,-1),2),
("LEFTPADDING",(0,0),(-1,-1),4),("RIGHTPADDING",(0,0),(-1,-1),4),
("GRID",(0,0),(-1,-1),0.3,C_MGREY),
("VALIGN",(0,0),(-1,-1),"TOP"),
] + [("BACKGROUND",(0,i),(-1,i),C_LGREY) for i in range(1,len(palp_data),2)])
)
story.append(palp_t)
story.append(SP(1))
story.append(warn_box(
"⚠ BOLOOR'S NOTE on TVF: "
"Increased TVF = consolidation (solid transmits vibrations better). "
"Absent TVF over dullness = STONY DULL → EFFUSION (fluid blocks transmission). "
"Absent TVF over hyper-resonance = PNEUMOTHORAX (air does not transmit vibrations)."
))
story.append(SP(2))
# ── C4: PERCUSSION ────────────────────────────────────────────────────────
story.append(subsec("C4. PERCUSSION"))
story.append(SP(0.5))
story.append(info_box(
"<b>Technique (Boloor):</b> Pleximeter finger (middle finger of left hand) placed firmly in intercostal space, "
"parallel to ribs. Strike sharply with flexed middle finger of right hand using wrist movement only. "
"Compare symmetrical areas. Percuss ANTERIOR → POSTERIOR → AXILLAE. Start at apices.",
bg=C_LTGREEN, border=C_TEAL
))
story.append(SP(0.5))
perc_notes = [
[hdr_row(["Note","Character","Causes"],bg=C_TEAL)[0],
hdr_row(["Note","Character","Causes"],bg=C_TEAL)[1],
hdr_row(["Note","Character","Causes"],bg=C_TEAL)[2]],
["Resonant","Hollow, drum-like","Normal aerated lung"],
["Hyper-resonant","Louder, more hollow than normal","Pneumothorax / Emphysema (hyperinflation)"],
["Dull","Thud-like, shortened","Consolidation / Collapse / Tumour / Elevated hemidiaphragm"],
["STONY DULL","Extreme dullness — no resonance at all","PLEURAL EFFUSION — pathognomonic"],
["Tympanic","Drum-like, hollow (gas-filled)","Large pneumothorax / Gastric bubble (left lower zone)"],
]
pn_t = Table(
[[Paragraph(c,ps("pn",fontSize=7,fontName="Helvetica-Bold" if i==0 else "Helvetica",
textColor=C_WHITE if i==0 else (C_RED if j==0 and i==4 else C_DGREY),leading=9)) for j,c in enumerate(r)]
for i,r in enumerate(perc_notes)],
colWidths=[32*mm, 50*mm, doc.width-82*mm],
style=TableStyle([
("BACKGROUND",(0,0),(-1,0),C_TEAL),
("BACKGROUND",(0,4),(-1,4),C_LTRED),
("FONTNAME",(0,4),(-1,4),"Helvetica-Bold"),
("FONTSIZE",(0,0),(-1,-1),7),
("TOPPADDING",(0,0),(-1,-1),2),("BOTTOMPADDING",(0,0),(-1,-1),2),
("LEFTPADDING",(0,0),(-1,-1),4),("RIGHTPADDING",(0,0),(-1,-1),4),
("GRID",(0,0),(-1,-1),0.3,C_MGREY),
("VALIGN",(0,0),(-1,-1),"TOP"),
("BACKGROUND",(0,2),(-1,2),C_LGREY),
])
)
story.append(pn_t)
story.append(SP(1))
# Percussion zones
perc_zones = col2_table([
[Paragraph("<b>ANTERIOR ZONES</b>",ps("az",fontSize=7.5,fontName="Helvetica-Bold",textColor=C_BLUE,leading=9)),
Paragraph("<b>POSTERIOR ZONES</b>",ps("pz",fontSize=7.5,fontName="Helvetica-Bold",textColor=C_BLUE,leading=9))],
[
Paragraph("☐ Above clavicle — apices (TB)\n☐ Below clavicle — upper zone\n☐ 2nd–3rd ICS — mid zone\n☐ Cardiac area — note cardiac dullness\n☐ Lower zones (4th–6th ICS)\n☐ Traube's space (L lower — gastric tympany)",
ps("az2",fontSize=7,fontName="Helvetica",textColor=C_DGREY,leading=11)),
Paragraph("☐ Suprascapular — apices (TB, apical fibrosis)\n☐ Interscapular — upper zone\n☐ Interscapular — lower zone\n☐ Infrascapular — lower zones\n☐ Axillae — lateral lower zones\n☐ Liver upper border R (5th–6th ICS anteriorly)",
ps("pz2",fontSize=7,fontName="Helvetica",textColor=C_DGREY,leading=11)),
]
], w1=doc.width/2)
story.append(perc_zones)
story.append(SP(2))
# ── C5: AUSCULTATION ──────────────────────────────────────────────────────
story.append(subsec("C5. AUSCULTATION"))
story.append(SP(0.5))
story.append(info_box(
"<b>Boloor's technique:</b> Use diaphragm. Warm stethoscope. Patient breathes slightly deeper than normal "
"with mouth open. Listen to complete I + E cycle at each position. Same zones as percussion. "
"Always compare left vs right. Assess: (a) Breath sounds — type & intensity "
"(b) Added sounds (c) Vocal resonance.",
bg=C_LTBLUE, border=C_BLUE
))
story.append(SP(0.5))
# Breath sounds
story.append(Paragraph("<b>A. BREATH SOUNDS</b>", sLabel))
story.append(SP(0.3))
bs_data = [
[hdr_row(["Type","Normal Site","Character","Abnormal if"],bg=C_BLUE)[0],
hdr_row(["Type","Normal Site","Character","Abnormal if"],bg=C_BLUE)[1],
hdr_row(["Type","Normal Site","Character","Abnormal if"],bg=C_BLUE)[2],
hdr_row(["Type","Normal Site","Character","Abnormal if"],bg=C_BLUE)[3]],
["Vesicular","All peripheral lung","Soft, rustling; Insp > Exp; no pause between I & E","Absent/reduced over effusion, pneumothorax, obesity"],
["Bronchial","Only over trachea & main bronchi","Harsh, tubular; Exp ≥ Insp; PAUSE between I & E","Consolidation / Lung abscess / Above effusion (abnormal over periphery)"],
["Bronchovesicular","1st–2nd ICS ant., interscapular post.","I = E; intermediate quality","Abnormal in any other location"],
]
bs_t = Table(
[[Paragraph(c,ps("bsc",fontSize=7,fontName="Helvetica-Bold" if i==0 else "Helvetica",
textColor=C_WHITE if i==0 else C_DGREY,leading=9)) for c in r]
for i,r in enumerate(bs_data)],
colWidths=[28*mm, 35*mm, 48*mm, doc.width-111*mm],
style=TableStyle([
("BACKGROUND",(0,0),(-1,0),C_BLUE),
("FONTSIZE",(0,0),(-1,-1),7),
("TOPPADDING",(0,0),(-1,-1),2),("BOTTOMPADDING",(0,0),(-1,-1),2),
("LEFTPADDING",(0,0),(-1,-1),4),("RIGHTPADDING",(0,0),(-1,-1),4),
("GRID",(0,0),(-1,-1),0.3,C_MGREY),("VALIGN",(0,0),(-1,-1),"TOP"),
("BACKGROUND",(0,2),(-1,2),C_LGREY),
])
)
story.append(bs_t)
story.append(SP(0.8))
# Added sounds
story.append(Paragraph("<b>B. ADDED (ADVENTITIOUS) SOUNDS — Boloor's Classification</b>", sLabel))
story.append(SP(0.3))
as_data = [
[hdr_row(["Sound","Character","Timing","Clears with cough?","Causes"],bg=C_TEAL)[0],
hdr_row(["Sound","Character","Timing","Clears with cough?","Causes"],bg=C_TEAL)[1],
hdr_row(["Sound","Character","Timing","Clears with cough?","Causes"],bg=C_TEAL)[2],
hdr_row(["Sound","Character","Timing","Clears with cough?","Causes"],bg=C_TEAL)[3],
hdr_row(["Sound","Character","Timing","Clears with cough?","Causes"],bg=C_TEAL)[4]],
["Fine crackles","Fine, high-pitched; like rubbing hair","End-inspiratory","No","Fibrosis (basal), early oedema, early consolidation"],
["Coarse crackles","Loud, low-pitched, bubbling/gurgling","Early inspiratory","Yes","Bronchiectasis, oedema (florid), secretions in COPD"],
["Wheeze — polyphonic","Musical, multiple pitches","Expiratory","No","Asthma, COPD (widespread bronchospasm)"],
["Wheeze — monophonic","Single musical note","Insp or Exp","No","Partial obstruction of one airway (tumour, foreign body)"],
["Stridor","Harsh, very loud, audible WITHOUT stethoscope","Inspiratory","No","Laryngeal/tracheal obstruction (croup, foreign body, Ca)"],
["Pleural rub","Leathery creaking; like footsteps on fresh snow","Both Insp & Exp; localised","No","Pleurisy — viral, bacterial, PE, RA, SLE"],
]
as_t = Table(
[[Paragraph(c,ps("asc",fontSize=6.8,fontName="Helvetica-Bold" if i==0 else "Helvetica",
textColor=C_WHITE if i==0 else C_DGREY,leading=9)) for c in r]
for i,r in enumerate(as_data)],
colWidths=[26*mm, 40*mm, 22*mm, 18*mm, doc.width-106*mm],
style=TableStyle([
("BACKGROUND",(0,0),(-1,0),C_TEAL),
("FONTSIZE",(0,0),(-1,-1),6.8),
("TOPPADDING",(0,0),(-1,-1),2),("BOTTOMPADDING",(0,0),(-1,-1),2),
("LEFTPADDING",(0,0),(-1,-1),3),("RIGHTPADDING",(0,0),(-1,-1),3),
("GRID",(0,0),(-1,-1),0.3,C_MGREY),("VALIGN",(0,0),(-1,-1),"TOP"),
] + [("BACKGROUND",(0,i),(-1,i),C_LGREY) for i in range(1,len(as_data),2)])
)
story.append(as_t)
story.append(SP(0.8))
# Vocal resonance
story.append(Paragraph("<b>C. VOCAL RESONANCE (Boloor emphasises this as the auscultatory parallel to TVF)</b>", sLabel))
story.append(SP(0.3))
vr_data = [
[hdr_row(["Sign","Instruction to patient","Finding heard","Interpretation"],bg=C_BLUE)[0],
hdr_row(["Sign","Instruction to patient","Finding heard","Interpretation"],bg=C_BLUE)[1],
hdr_row(["Sign","Instruction to patient","Finding heard","Interpretation"],bg=C_BLUE)[2],
hdr_row(["Sign","Instruction to patient","Finding heard","Interpretation"],bg=C_BLUE)[3]],
["Normal resonance","Say 'ninety-nine'","Muffled, indistinct sounds","Normal lung"],
["Bronchophony","Say 'ninety-nine'","Words clearly heard through stethoscope","Consolidation"],
["Whispering pectoriloquy","Whisper '1-2-3'","Whisper clearly transmitted","Consolidation (most sensitive sign — Boloor emphasis)"],
["Aegophony","Say 'E'","Heard as 'A' (nasal/bleating quality)","Above a pleural effusion at fluid-air interface"],
["Decreased resonance","Say 'ninety-nine'","Absent / reduced transmission","Effusion / Pneumothorax"],
]
vr_t = Table(
[[Paragraph(c,ps("vrc",fontSize=6.8,fontName="Helvetica-Bold" if i==0 else "Helvetica",
textColor=C_WHITE if i==0 else C_DGREY,leading=9)) for c in r]
for i,r in enumerate(vr_data)],
colWidths=[30*mm, 35*mm, 42*mm, doc.width-107*mm],
style=TableStyle([
("BACKGROUND",(0,0),(-1,0),C_BLUE),
("FONTSIZE",(0,0),(-1,-1),6.8),
("TOPPADDING",(0,0),(-1,-1),2),("BOTTOMPADDING",(0,0),(-1,-1),2),
("LEFTPADDING",(0,0),(-1,-1),3),("RIGHTPADDING",(0,0),(-1,-1),3),
("GRID",(0,0),(-1,-1),0.3,C_MGREY),("VALIGN",(0,0),(-1,-1),"TOP"),
] + [("BACKGROUND",(0,i),(-1,i),C_LGREY) for i in range(1,len(vr_data),2)])
)
story.append(vr_t)
story.append(SP(3))
# ──────────────────────────────────────────────────────────────────────────
# PART D: DIAGNOSIS FORMAT (Boloor's unique 3-step diagnosis)
# ──────────────────────────────────────────────────────────────────────────
story.append(sec("PART D — DIAGNOSIS FORMAT (Boloor's 3-Step Framework)", C_NAVY, "🎯"))
story.append(SP(1))
story.append(info_box(
"<b>Boloor's Unique Approach:</b> Always state diagnosis in THREE parts: "
"<b>1. Anatomical diagnosis</b> (where is the lesion?) → "
"<b>2. Pathological diagnosis</b> (what is the pathology?) → "
"<b>3. Etiological diagnosis</b> (what is the cause?)",
bg=C_LTBLUE, border=C_BLUE
))
story.append(SP(1))
diag = col2_table([
[Paragraph("<b>Step 1 — ANATOMICAL DIAGNOSIS:</b>",sBold),
Paragraph("This patient has a lesion involving the _______________________________________________", sBody)],
[Paragraph("<b>Step 2 — PATHOLOGICAL DIAGNOSIS:</b>",sBold),
Paragraph("The pathological process is _________________________________________________________", sBody)],
[Paragraph("<b>Step 3 — ETIOLOGICAL DIAGNOSIS:</b>",sBold),
Paragraph("The cause/etiology is _______________________________________________________________", sBody)],
[Paragraph("<b>Complete diagnosis:</b>",ps("cd",fontSize=7.5,fontName="Helvetica-Bold",textColor=C_TEAL,leading=10)),
Paragraph("_" * 60, sBody)],
], w1=58*mm)
story.append(diag)
story.append(SP(1))
# Anatomical possibilities
story.append(Paragraph("<b>Anatomical diagnosis options (Boloor's list):</b>", sLabel))
story.append(SP(0.3))
story.append(cb_line([
"Lesion in the lung parenchyma (upper / middle / lower lobe — Right / Left)",
"Lesion in the pleural cavity",
"Lesion in the bronchial tree",
"Lesion in the mediastinum (anterior / middle / posterior)",
"Lesion involving the chest wall",
"Diffuse bilateral pulmonary lesion",
], cols=2))
story.append(SP(1))
story.append(Paragraph("<b>Pathological diagnosis options:</b>", sLabel))
story.append(SP(0.3))
story.append(cb_line([
"Consolidation / Pneumonia", "Pleural effusion / Empyema",
"Pneumothorax / Hydropneumothorax", "Collapse / Atelectasis",
"Fibrosis / Fibrothorax", "Cavitation / Lung abscess",
"Tumour / Mass lesion", "Interstitial lung disease",
"Bronchiectasis", "Emphysema / Hyperinflation",
], cols=2))
story.append(SP(3))
# ──────────────────────────────────────────────────────────────────────────
# PART E: SUMMARY OF FINDINGS IN COMMON RESPIRATORY DISEASES (Boloor)
# ──────────────────────────────────────────────────────────────────────────
story.append(sec("PART E — SUMMARY OF FINDINGS IN COMMON RESPIRATORY DISEASES", C_NAVY, "📊"))
story.append(SP(1))
story.append(info_box(
"<b>Boloor's Master Summary Table</b> — for rapid correlation of clinical signs to diagnosis. "
"TVF/VR always change in the SAME direction. Stony dull = effusion (never missed in exams).",
bg=C_LTBLUE, border=C_BLUE
))
story.append(SP(1))
# Big summary table
diseases = [
["Consolidation", "Central", "↓ Ipsilateral", "↑ Increased", "Dull", "Bronchial", "Fine crackles", "↑ Increased (bronchophony, WPL)"],
["Pleural Effusion", "→ Away (if large)", "↓ Ipsilateral","Absent", "STONY DULL", "Absent/↓ Vesicular", "Pleural rub (if pleurisy)", "Absent/Aegophony above"],
["Pneumothorax", "→ Away (if tension)","↓ Ipsilateral","Absent", "Hyper-resonant","Absent", "None", "Absent"],
["Collapse (obstructed)", "← Towards", "↓ Ipsilateral", "Absent/↓", "Dull", "Absent/Bronchial", "None", "Absent/reduced"],
["Fibrosis (diffuse)", "Central", "↓ Bilateral", "↑ Increased", "Dull", "Bronchial (basal)", "Fine end-insp crackles","↑ Increased"],
["COPD/Emphysema", "Central", "↓ Bilateral", "↓ Reduced", "Hyper-resonant","↓ Vesicular", "Expiratory wheeze + coarse crackles","↓ Reduced"],
["Bronchial Asthma", "Central", "↓ Bilateral", "↓ Reduced", "Resonant", "↓ Vesicular", "Polyphonic expiratory wheeze","↓ Reduced"],
["Bronchiectasis", "Central", "↓ Affected area", "↑ Increased", "Dull (local)", "Bronchial", "Coarse crackles", "↑ Increased"],
["Lung Abscess", "Central", "↓ Ipsilateral", "↑ Increased", "Dull (wall)/ amphoric over cavity","Bronchial/Cavernous","Coarse crackles/amphoric", "↑ Increased"],
["Hydropneumothorax", "Central", "↓ Ipsilateral", "Absent", "Upper = hyper-resonant; Lower = stony dull","Absent/↓", "Succussion splash","Absent"],
]
hdr = ["Condition","Trachea","Expansion","TVF/VR","Percussion","Breath Sounds","Added Sounds","Vocal Resonance"]
sum_rows = [
[Paragraph(f"<b>{h}</b>",ps("sh",fontSize=6.5,fontName="Helvetica-Bold",textColor=C_WHITE,leading=8)) for h in hdr]
] + [
[Paragraph(c,ps("sc",fontSize=6.2,fontName="Helvetica" if j>0 else "Helvetica-Bold",
textColor=C_DGREY if j>0 else C_NAVY,leading=8)) for j,c in enumerate(r)]
for r in diseases
]
wds = [30*mm, 20*mm, 20*mm, 17*mm, 25*mm, 28*mm, 32*mm, doc.width-172*mm]
sum_t = Table(sum_rows, colWidths=wds,
style=TableStyle([
("BACKGROUND",(0,0),(-1,0),C_NAVY),
("FONTSIZE",(0,0),(-1,-1),6.2),
("TOPPADDING",(0,0),(-1,-1),2),("BOTTOMPADDING",(0,0),(-1,-1),2),
("LEFTPADDING",(0,0),(-1,-1),2),("RIGHTPADDING",(0,0),(-1,-1),2),
("GRID",(0,0),(-1,-1),0.3,C_MGREY),("VALIGN",(0,0),(-1,-1),"TOP"),
# Highlight stony dull
("BACKGROUND",(4,2),(-1,2),C_LTRED),
("TEXTCOLOR",(4,2),(4,2),C_RED),
("FONTNAME",(4,2),(4,2),"Helvetica-Bold"),
] + [("BACKGROUND",(0,i),(-1,i),C_LGREY) for i in range(1,len(sum_rows),2)])
)
story.append(sum_t)
story.append(SP(1))
story.append(warn_box(
"⚠ BOLOOR'S KEY EXAM POINTS: "
"(1) Stony dull = ALWAYS effusion — no other cause. "
"(2) TVF and Vocal Resonance ALWAYS change together. "
"(3) Bronchial breathing over periphery = consolidation (most common) / above effusion (compressed lung). "
"(4) Whispering pectoriloquy = most sensitive sign of consolidation. "
"(5) Tracheal deviation: PUSH (effusion/tension pneumothorax) vs PULL (collapse/fibrosis/pneumonectomy)."
))
story.append(SP(3))
# ──────────────────────────────────────────────────────────────────────────
# PART F: DOCUMENTATION TEMPLATE
# ──────────────────────────────────────────────────────────────────────────
story.append(sec("PART F — BEDSIDE DOCUMENTATION TEMPLATE", C_NAVY, "📝"))
story.append(SP(1))
doc_text = """<b>PRESENTING COMPLAINT:</b> ___________________________________________ × _______ days/months
<b>HISTORY:</b> Cough: dry/productive, ___ days. Sputum: ___mL/day, colour ___, odour ___.
Haemoptysis: Y/N, amount ___. Breathlessness: MRC Grade ___, onset ___, orthopnoea: Y/N, PND: Y/N.
Chest pain: site ___, character ___, pleuritic: Y/N. Wheeze: Y/N, timing ___.
Fever: Y/N. Weight loss: ___ kg. Night sweats: Y/N. Smoking: ___ pack-years. Occupation: ___.
<b>GENERAL EXAMINATION:</b> Built & nourished: ___. Temp: ___. PR: ___ bpm. RR: ___/min. BP: ___ mmHg. SpO₂: ___%.
Clubbing: absent/Grade___. Central cyanosis: Y/N. Anaemia: Y/N. LN: absent/present(site:___).
Pedal oedema: Y/N. CO₂ flap: Y/N. JVP: normal/elevated.
<b>NECK:</b> Trachea: central / deviated to ___ (pushed / pulled).
<b>INSPECTION:</b> Shape: normal / barrel / ___. RR: ___/min. Accessory muscles: Y/N. Recession: Y/N.
Chest movement: equal bilaterally / reduced on ___ side. Scars: none / ___.
<b>PALPATION:</b> Expansion upper: equal / reduced on ___ (=___ cm). Lower: equal / reduced on ___ (=___ cm).
TVF: normal / ↑ over ___ / ↓ over ___.
<b>PERCUSSION:</b> R: resonant/dull/stony dull/hyper-resonant at ___. L: resonant/dull/stony dull/hyper-resonant at ___.
<b>AUSCULTATION:</b> Breath sounds: vesicular / bronchial / absent / reduced over ___.
Added sounds: nil / fine crackles / coarse crackles / wheeze (poly/mono) / pleural rub — site: ___.
Vocal resonance: normal / bronchophony / whispering pectoriloquy / aegophony over ___.
<b>DIAGNOSIS (Boloor's 3-step):</b>
Anatomical: ___________________________ Pathological: ___________________________ Etiological: ___________________________"""
story.append(Table([[Paragraph(doc_text,ps("dt",fontSize=7,fontName="Helvetica",textColor=C_DGREY,leading=11))]],
colWidths=[doc.width],
style=TableStyle([
("BACKGROUND",(0,0),(-1,-1),C_LTGREEN),
("BOX",(0,0),(-1,-1),0.7,C_TEAL),
("TOPPADDING",(0,0),(-1,-1),6),("BOTTOMPADDING",(0,0),(-1,-1),6),
("LEFTPADDING",(0,0),(-1,-1),8),("RIGHTPADDING",(0,0),(-1,-1),8),
])
))
story.append(SP(3))
story.append(HRFlowable(width="100%",thickness=0.5,color=C_MGREY))
story.append(SP(1))
story.append(Paragraph(
"Reference: An Insider's Guide to Clinical Medicine, Dr Archith Boloor & Dr Anudeep Padakanti | "
"Exam Preparatory Manual for Undergraduates — Medicine, Archith Boloor | Jaypee Brothers Medical Publishers | "
"KMC Mangaluru / Manipal Academy of Higher Education | For educational use only.",
sFooter))
# BUILD
doc.build(story, onFirstPage=header_footer, onLaterPages=header_footer)
print(f"PDF created: {OUTPUT}")
cd /home/daytona/workspace/resp-checklist && python3 boloor_resp.py 2>&1
~/resp-checklist/boloor_resp.py
Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/resp-checklist/boloor_resp.py'
cd /home/daytona/workspace/resp-checklist && python3 boloor_resp.py 2>&1
ps() calls that create style name conflicts. I'll refactor to use pre-defined styles throughout.cd /home/daytona/workspace/resp-checklist && python3 -c " from reportlab.lib.pagesizes import A4 from reportlab.lib.styles import ParagraphStyle from reportlab.platypus import Paragraph # test: does repeated same-name ps cause issues? s1 = ParagraphStyle('test1', fontSize=7) s2 = ParagraphStyle('test1', fontSize=8) # same name p = Paragraph('hello world test', s2) print('ok:', p.style.fontSize) " 2>&1
cd /home/daytona/workspace/resp-checklist && python3 -c " # find which line causes the non-break space issue import re with open('boloor_resp.py') as f: lines = f.readlines() # Search for non-breaking spaces (xa0) for i, l in enumerate(lines, 1): if '\xa0' in l or '\\xa0' in l: print(f'Line {i}: {repr(l[:80])}') " 2>&1
cd /home/daytona/workspace/resp-checklist && python3 2>&1 <<'PYEOF' # minimal repro - find the real issue from reportlab.lib.styles import ParagraphStyle from reportlab.lib import colors from reportlab.platypus import Paragraph, Table, TableStyle from reportlab.lib.units import mm C_WHITE = colors.white C_DGREY = colors.HexColor("#2c3e50") def ps(name, **kw): from reportlab.lib.styles import getSampleStyleSheet styles = getSampleStyleSheet() return ParagraphStyle(name, parent=styles["Normal"], **kw) # simulate the failing pattern bs_data = [ ["Type","Site","Character","Abnormal if"], ["Vesicular","All peripheral lung","Soft, rustling; Insp > Exp; no pause","Absent/reduced over effusion"], ] bs_t = Table( [[Paragraph(c,ps("bsc",fontSize=7, fontName="Helvetica-Bold" if i==0 else "Helvetica", textColor=C_WHITE if i==0 else C_DGREY,leading=9)) for c in r] for i,r in enumerate(bs_data)], colWidths=[28*mm, 35*mm, 48*mm, 45*mm], ) from reportlab.lib.pagesizes import A4 from reportlab.platypus import SimpleDocTemplate import io buf = io.BytesIO() doc2 = SimpleDocTemplate(buf, pagesize=A4) doc2.build([bs_t]) print("ok, size:", buf.tell()) PYEOF
cd /home/daytona/workspace/resp-checklist && python3 -c " # run the script up to line 686 to see what r is when it fails import sys # patch to print row contents around line 686 with open('boloor_resp.py') as f: src = f.read() # find the offending block lines = src.split('\n') for i,l in enumerate(lines[680:695], 681): print(i, repr(l[:90])) " 2>&1
cd /home/daytona/workspace/resp-checklist && python3 2>&1 <<'PYEOF' # check if the bs_data contains the hdr_row objects (Paragraph objects) or strings # The issue is bs_data starts with hdr_row() result which are already Paragraphs # but the comprehension tries to wrap them in Paragraph() again # let's check from reportlab.lib.styles import ParagraphStyle from reportlab.lib import colors from reportlab.platypus import Paragraph C_WHITE = colors.white C_DGREY = colors.HexColor("#2c3e50") C_BLUE = colors.HexColor("#1a5276") C_TEAL = colors.HexColor("#148f77") def ps(name, **kw): from reportlab.lib.styles import getSampleStyleSheet return ParagraphStyle(name, parent=getSampleStyleSheet()["Normal"], **kw) def hdr_row(cells, bg=C_TEAL): return [Paragraph(f"<b>{c}</b>", ps("hc",fontSize=7,fontName="Helvetica-Bold", textColor=C_WHITE,leading=9)) for c in cells] # the problem: bs_data[0] is a list of Paragraphs (from hdr_row) # then the comprehension calls Paragraph(c, ...) where c IS already a Paragraph # Paragraph(paragraph_object, style) -> TypeError bs_data = [ [hdr_row(["Type","Site","Character","Abnormal if"],bg=C_BLUE)[0], # Already a Paragraph! hdr_row(["Type","Site","Character","Abnormal if"],bg=C_BLUE)[1], hdr_row(["Type","Site","Character","Abnormal if"],bg=C_BLUE)[2], hdr_row(["Type","Site","Character","Abnormal if"],bg=C_BLUE)[3]], ["Vesicular","All peripheral","Soft","Absent"], ] print("row 0 type:", type(bs_data[0][0])) print("row 1 type:", type(bs_data[1][0])) # The comprehension wraps row0 cells (already Paragraphs) in Paragraph() -> error PYEOF
hdr_row() function returns Paragraph objects, but then the table-building comprehension tries to wrap them in Paragraph() again. I need to fix all those table constructions to handle mixed rows properly.cd /home/daytona/workspace/resp-checklist && python3 -c " import re with open('boloor_resp.py') as f: content = f.read() # count how many tables use the hdr_row + string data pattern matches = re.findall(r'for i,r in enumerate\((\w+_data)\)', content) print('tables using enumerate pattern:', matches) " 2>&1
cd /home/daytona/workspace/resp-checklist && python3 << 'PYEOF' # Read and patch the file - fix all 5 enumerate-pattern tables with open('boloor_resp.py') as f: src = f.read() # The fix: change the comprehension to check if cell is already a Paragraph old = ''' [[Paragraph(c,ps("bsc",fontSize=7,fontName="Helvetica-Bold" if i==0 else "Helvetica", textColor=C_WHITE if i==0 else C_DGREY,leading=9)) for c in r] for i,r in enumerate(bs_data)],''' new = ''' [[c if hasattr(c,'wrap') else Paragraph(c,ps(f"bsc{i}",fontSize=7, fontName="Helvetica-Bold" if i==0 else "Helvetica", textColor=C_WHITE if i==0 else C_DGREY,leading=9)) for c in r] for i,r in enumerate(bs_data)],''' src = src.replace(old, new) old2 = ''' [[Paragraph(c,ps("asc",fontSize=6.8,fontName="Helvetica-Bold" if i==0 else "Helvetica", textColor=C_WHITE if i==0 else C_DGREY,leading=9)) for c in r] for i,r in enumerate(as_data)],''' new2 = ''' [[c if hasattr(c,'wrap') else Paragraph(c,ps(f"asc{i}",fontSize=6.8, fontName="Helvetica-Bold" if i==0 else "Helvetica", textColor=C_WHITE if i==0 else C_DGREY,leading=9)) for c in r] for i,r in enumerate(as_data)],''' src = src.replace(old2, new2) old3 = ''' [[Paragraph(c,ps("vrc",fontSize=6.8,fontName="Helvetica-Bold" if i==0 else "Helvetica", textColor=C_WHITE if i==0 else C_DGREY,leading=9)) for c in r] for i,r in enumerate(vr_data)],''' new3 = ''' [[c if hasattr(c,'wrap') else Paragraph(c,ps(f"vrc{i}",fontSize=6.8, fontName="Helvetica-Bold" if i==0 else "Helvetica", textColor=C_WHITE if i==0 else C_DGREY,leading=9)) for c in r] for i,r in enumerate(vr_data)],''' src = src.replace(old3, new3) old4 = ''' [[Paragraph(c if isinstance(c,str) else c, ps("ic",fontSize=7,fontName="Helvetica",textColor=C_DGREY,leading=9)) for c in r] if i>0 else r for i,r in enumerate(insp_data)],''' new4 = ''' [[c if hasattr(c,'wrap') else Paragraph(c,ps(f"ic{i}",fontSize=7, fontName="Helvetica",textColor=C_DGREY,leading=9)) for c in r] for i,r in enumerate(insp_data)],''' src = src.replace(old4, new4) old5 = ''' [[Paragraph(c,ps("pc",fontSize=7,fontName="Helvetica",textColor=C_DGREY,leading=9)) for c in r] if i>0 else r for i,r in enumerate(palp_data)],''' new5 = ''' [[c if hasattr(c,'wrap') else Paragraph(c,ps(f"pc{i}",fontSize=7, fontName="Helvetica",textColor=C_DGREY,leading=9)) for c in r] for i,r in enumerate(palp_data)],''' src = src.replace(old5, new5) with open('boloor_resp.py', 'w') as f: f.write(src) print("Replacements done") # verify for tag in ['bsc', 'asc', 'vrc']: cnt = src.count(f'hasattr(c,\'wrap\') else Paragraph(c,ps(f"{tag}') print(f" {tag}: {'ok' if cnt>0 else 'MISSING'}") PYEOF
cd /home/daytona/workspace/resp-checklist && python3 boloor_resp.py 2>&1
ls -lh /home/daytona/workspace/resp-checklist/Boloor_Respiratory_Proforma.pdf
Boloor Respiratory Proforma
PDF Document · PDF
| Part | Content (Boloor's structure) |
|---|---|
| A - Case Sheet Format | Patient ID, Chief Complaints (with duration), History of presenting illness covering all 6 Cardinal Symptoms (Cough, Sputum, Haemoptysis, Breathlessness/MRC grading, Chest pain, Wheeze), Associated symptoms, Past/Drug/Family/Socio-occupational history |
| B - General Examination | Vitals (including SpO₂ and PEFR), Full GPE table with clubbing grading, CO₂ flap, Horner's, SVC obstruction, HPOA — all with Boloor's clinical significance column |
| C - Systemic Examination | Boloor's sequence: Neck (trachea) → Inspection → Palpation (expansion technique, TVF) → Percussion (5 notes, anterior + posterior zones) → Auscultation (breath sounds, added sounds with "clears with cough?" column, vocal resonance with aegophony/WPL) |
| D - Diagnosis Format | Boloor's unique 3-step diagnosis: Anatomical → Pathological → Etiological diagnosis (his signature teaching method) |
| E - Summary of Findings | Master comparison table of 10 conditions including Hydropneumothorax and Lung Abscess — with Boloor's exam tips highlighted |
| F - Documentation Template | Single-page fill-in proforma for bedside use |
Case Performa for pvd surgery
~/resp-checklist/pvd_proforma.py
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import mm
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_LEFT, TA_CENTER
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, KeepTogether
)
OUTPUT = "/home/daytona/workspace/resp-checklist/PVD_Surgery_Case_Proforma.pdf"
# ── Colour palette (surgical/vascular theme — deep red + steel blue) ─────────
C_DARK = colors.HexColor("#1c2841") # deep navy
C_RED = colors.HexColor("#8b1a1a") # surgical red
C_STEEL = colors.HexColor("#2e6da4") # steel blue
C_TEAL = colors.HexColor("#1a7a6e") # teal
C_ORANGE = colors.HexColor("#c0550d") # gangrene warning
C_LTRED = colors.HexColor("#fce8e8")
C_LTBLUE = colors.HexColor("#e3eef8")
C_LTGREEN = colors.HexColor("#e4f4ee")
C_LTYELL = colors.HexColor("#fefbe6")
C_LTORANG = colors.HexColor("#fdf0e0")
C_LGREY = colors.HexColor("#f3f4f6")
C_MGREY = colors.HexColor("#c8cdd4")
C_DGREY = colors.HexColor("#2d3748")
C_WHITE = colors.white
W, H = A4
doc = SimpleDocTemplate(
OUTPUT,
pagesize=A4,
leftMargin=13*mm, rightMargin=13*mm,
topMargin=23*mm, bottomMargin=15*mm,
title="PVD Surgery Case Proforma",
author="Peripheral Vascular Disease — Surgical Case Sheet"
)
styles = getSampleStyleSheet()
def ps(name, **kw):
return ParagraphStyle(name, parent=styles["Normal"], **kw)
sSecH = ps("SH", fontSize=9, fontName="Helvetica-Bold", textColor=C_WHITE, leading=12)
sSecH2 = ps("S2", fontSize=8, fontName="Helvetica-Bold", textColor=C_DARK, leading=11)
sBody = ps("B", fontSize=7.5, fontName="Helvetica", textColor=C_DGREY, leading=10)
sBold = ps("BB", fontSize=7.5, fontName="Helvetica-Bold", textColor=C_DARK, leading=10)
sItal = ps("BI", fontSize=7, fontName="Helvetica-Oblique", textColor=colors.HexColor("#555"), leading=9)
sSmall = ps("SM", fontSize=6.8, fontName="Helvetica", textColor=colors.HexColor("#666"), leading=9)
sWarn = ps("W", fontSize=7.2, fontName="Helvetica-Bold", textColor=C_RED, leading=9)
sGreen = ps("G", fontSize=7.2, fontName="Helvetica-Bold", textColor=C_TEAL, leading=9)
sLabel = ps("L", fontSize=7.5, fontName="Helvetica-Bold", textColor=C_STEEL, leading=9)
sFooter= ps("F", fontSize=6.2, fontName="Helvetica-Oblique", textColor=colors.HexColor("#999"), alignment=TA_CENTER, leading=8)
SP = lambda n=2: Spacer(1, n*mm)
# ── Canvas ────────────────────────────────────────────────────────────────────
def page_cb(canvas, doc):
canvas.saveState()
# Top
canvas.setFillColor(C_DARK)
canvas.rect(0, H-21*mm, W, 21*mm, fill=1, stroke=0)
canvas.setFillColor(C_RED)
canvas.rect(0, H-23*mm, W, 2*mm, fill=1, stroke=0)
canvas.setFont("Helvetica-Bold", 13)
canvas.setFillColor(C_WHITE)
canvas.drawCentredString(W/2, H-13*mm, "PERIPHERAL VASCULAR DISEASE (PVD) — SURGICAL CASE PROFORMA")
canvas.setFont("Helvetica", 7.5)
canvas.setFillColor(colors.HexColor("#a0b8d0"))
canvas.drawCentredString(W/2, H-19*mm,
"General Surgery / Vascular Surgery | Based on Bailey & Love's Surgery, Mulholland & Greenfield's Surgery, SRB's Manual")
# Bottom
canvas.setFillColor(C_DARK)
canvas.rect(0, 0, W, 11*mm, fill=1, stroke=0)
canvas.setFillColor(C_RED)
canvas.rect(0, 11*mm, W, 0.8*mm, fill=1, stroke=0)
canvas.setFont("Helvetica", 6.2)
canvas.setFillColor(colors.HexColor("#a0b8d0"))
canvas.drawString(13*mm, 4*mm, "PVD Surgery Case Proforma | For Educational / Clinical Use")
canvas.drawRightString(W-13*mm, 4*mm, f"Page {doc.page}")
canvas.restoreState()
# ── Helpers ───────────────────────────────────────────────────────────────────
def sec(text, bg=C_DARK, icon=""):
t = f"{icon} {text}" if icon else text
return KeepTogether([Table([[Paragraph(t, sSecH)]], colWidths=[doc.width],
style=TableStyle([
("BACKGROUND",(0,0),(-1,-1),bg),
("TOPPADDING",(0,0),(-1,-1),4),("BOTTOMPADDING",(0,0),(-1,-1),4),
("LEFTPADDING",(0,0),(-1,-1),8),
]))])
def subsec(text, bg=C_LTBLUE, tc=C_DARK):
return Table([[Paragraph(text, ps("ss",fontSize=8,fontName="Helvetica-Bold",textColor=tc,leading=10))]],
colWidths=[doc.width],
style=TableStyle([
("BACKGROUND",(0,0),(-1,-1),bg),
("TOPPADDING",(0,0),(-1,-1),2),("BOTTOMPADDING",(0,0),(-1,-1),2),
("LEFTPADDING",(0,0),(-1,-1),8),
("LINEBELOW",(0,0),(-1,-1),0.6,C_STEEL),
]))
def c2(rows, w1=55*mm, alt=True):
w2 = doc.width - w1
st = [
("FONTNAME",(0,0),(-1,-1),"Helvetica"),("FONTSIZE",(0,0),(-1,-1),7.2),
("TOPPADDING",(0,0),(-1,-1),2),("BOTTOMPADDING",(0,0),(-1,-1),2),
("LEFTPADDING",(0,0),(-1,-1),5),("RIGHTPADDING",(0,0),(-1,-1),4),
("GRID",(0,0),(-1,-1),0.3,C_MGREY),("VALIGN",(0,0),(-1,-1),"TOP"),
]
if alt:
for i in range(0,len(rows),2): st.append(("BACKGROUND",(0,i),(-1,i),C_LGREY))
return Table(rows, colWidths=[w1,w2], style=TableStyle(st))
def c3(hdr, rows, w1=40*mm, w2=None, w3=None, hbg=C_STEEL):
w2 = w2 or 60*mm; w3 = w3 or (doc.width-w1-w2)
hcells = [Paragraph(f"<b>{h}</b>",ps(f"h3{i}",fontSize=7,fontName="Helvetica-Bold",textColor=C_WHITE,leading=9)) for i,h in enumerate(hdr)]
all_rows = [hcells] + [[Paragraph(c,ps(f"c3r{i}j{j}",fontSize=7,fontName="Helvetica",textColor=C_DGREY,leading=9)) for j,c in enumerate(r)] for i,r in enumerate(rows)]
st = [
("BACKGROUND",(0,0),(-1,0),hbg),
("FONTSIZE",(0,0),(-1,-1),7),
("TOPPADDING",(0,0),(-1,-1),2),("BOTTOMPADDING",(0,0),(-1,-1),2),
("LEFTPADDING",(0,0),(-1,-1),4),("RIGHTPADDING",(0,0),(-1,-1),4),
("GRID",(0,0),(-1,-1),0.3,C_MGREY),("VALIGN",(0,0),(-1,-1),"TOP"),
]
for i in range(1,len(all_rows),2): st.append(("BACKGROUND",(0,i),(-1,i),C_LGREY))
return Table(all_rows, colWidths=[w1,w2,w3], style=TableStyle(st))
def info_box(text, bg=C_LTYELL, border=colors.HexColor("#e0c060")):
return Table([[Paragraph(text,ps("ib",fontSize=7.2,fontName="Helvetica",textColor=C_DGREY,leading=10))]],
colWidths=[doc.width],
style=TableStyle([
("BACKGROUND",(0,0),(-1,-1),bg),("BOX",(0,0),(-1,-1),0.6,border),
("TOPPADDING",(0,0),(-1,-1),4),("BOTTOMPADDING",(0,0),(-1,-1),4),
("LEFTPADDING",(0,0),(-1,-1),7),("RIGHTPADDING",(0,0),(-1,-1),7),
]))
def warn(text):
return info_box(text, bg=C_LTRED, border=C_RED)
def green_box(text):
return info_box(text, bg=C_LTGREEN, border=C_TEAL)
def cb_list(items, cols=2):
rows=[]
for i in range(0,len(items),cols):
row=[]
for j in range(cols):
t = f"☐ {items[i+j]}" if i+j<len(items) else ""
row.append(Paragraph(t,sBody))
rows.append(row)
return Table(rows, colWidths=[doc.width/cols]*cols,
style=TableStyle([("TOPPADDING",(0,0),(-1,-1),1),("BOTTOMPADDING",(0,0),(-1,-1),1),
("LEFTPADDING",(0,0),(-1,-1),3),("RIGHTPADDING",(0,0),(-1,-1),3)]))
def blank_line(label, note=""):
return [Paragraph(label,sBold),
Table([[Paragraph("",sBody)]],colWidths=[doc.width-60*mm],
style=TableStyle([("LINEBELOW",(0,0),(-1,-1),0.4,C_MGREY),
("TOPPADDING",(0,0),(-1,-1),5),("BOTTOMPADDING",(0,0),(-1,-1),0)])),
Paragraph(note,sItal)]
def pulse_table():
"""Special pulse examination table for vascular cases"""
sites = ["Radial (R/L)","Brachial (R/L)","Carotid (R/L)","Abdominal Aorta","Common Femoral (R/L)","Popliteal (R/L)","Posterior Tibial (R/L)","Dorsalis Pedis (R/L)"]
hdr = [Paragraph(f"<b>{h}</b>",ps(f"ph{i}",fontSize=6.8,fontName="Helvetica-Bold",textColor=C_WHITE,leading=8)) for i,h in enumerate(["Pulse Site","Present","Absent","Diminished","Bruit","Notes"])]
rows = [hdr]
for site in sites:
rows.append([
Paragraph(site,ps(f"ps{site[:4]}",fontSize=7,fontName="Helvetica-Bold",textColor=C_DARK,leading=9)),
Paragraph("☐",sBody), Paragraph("☐",sBody), Paragraph("☐",sBody), Paragraph("☐",sBody),
Paragraph("_"*18,sSmall)
])
st = [
("BACKGROUND",(0,0),(-1,0),C_DARK),
("FONTSIZE",(0,0),(-1,-1),7),
("TOPPADDING",(0,0),(-1,-1),2),("BOTTOMPADDING",(0,0),(-1,-1),2),
("LEFTPADDING",(0,0),(-1,-1),4),("RIGHTPADDING",(0,0),(-1,-1),4),
("GRID",(0,0),(-1,-1),0.3,C_MGREY),("VALIGN",(0,0),(-1,-1),"MIDDLE"),
]
for i in range(1,len(rows),2): st.append(("BACKGROUND",(0,i),(-1,i),C_LGREY))
return Table(rows, colWidths=[42*mm,16*mm,16*mm,20*mm,16*mm,doc.width-110*mm], style=TableStyle(st))
# ════════════════════════════════════════════════════════════════════════════
# STORY
# ════════════════════════════════════════════════════════════════════════════
story = []
# ─── SECTION 1: PATIENT IDENTIFICATION ───────────────────────────────────────
story.append(SP(1))
story.append(sec("SECTION 1 — PATIENT IDENTIFICATION & DEMOGRAPHICS", C_DARK, "🪪"))
story.append(SP(1))
story.append(c2([
[Paragraph("<b>Name:</b>",sBold), Paragraph("_"*45,sBody)],
[Paragraph("<b>Age / Sex:</b>",sBold), Paragraph("___ years / M / F",sBody)],
[Paragraph("<b>Occupation:</b>",sBold), Paragraph("_"*45,sBody)],
[Paragraph("<b>Address / Residence:</b>",sBold), Paragraph("Urban / Rural — _"*1+"_"*25,sBody)],
[Paragraph("<b>IP No. / OP No.:</b>",sBold), Paragraph("_"*30,sBody)],
[Paragraph("<b>Date of Admission:</b>",sBold), Paragraph("_"*20,sBody)],
[Paragraph("<b>Informant / Reliability:</b>",sBold), Paragraph("Patient / Relative — Reliable / Unreliable",sBody)],
[Paragraph("<b>Referred from:</b>",sBold), Paragraph("_"*35,sBody)],
]))
story.append(SP(2))
# ─── SECTION 2: CHIEF COMPLAINTS ─────────────────────────────────────────────
story.append(sec("SECTION 2 — CHIEF COMPLAINTS", C_DARK, "📋"))
story.append(SP(1))
story.append(info_box(
"<b>Common presenting complaints in PVD:</b> "
"Pain in limb (claudication / rest pain) | Ulcer / wound on foot or leg | "
"Darkening / gangrene of toes/foot | Swelling of limb | Cold / pale limb | "
"Absent pulses | Erectile dysfunction (Leriche syndrome)",
bg=C_LTBLUE, border=C_STEEL
))
story.append(SP(0.5))
cc_rows = [[Paragraph(f"{i+1}.",sBold),
Table([[Paragraph("",sBody)]],colWidths=[doc.width-38*mm],
style=TableStyle([("LINEBELOW",(0,0),(-1,-1),0.4,C_MGREY),
("TOPPADDING",(0,0),(-1,-1),5),("BOTTOMPADDING",(0,0),(-1,-1),0)])),
Paragraph("since ___________",sItal)] for i in range(5)]
story.append(Table(cc_rows,colWidths=[8*mm,doc.width-38*mm,30*mm],
style=TableStyle([("TOPPADDING",(0,0),(-1,-1),1),("BOTTOMPADDING",(0,0),(-1,-1),2),
("LEFTPADDING",(0,0),(-1,-1),3),("RIGHTPADDING",(0,0),(-1,-1),3),
("VALIGN",(0,0),(-1,-1),"BOTTOM")])))
story.append(SP(2))
# ─── SECTION 3: HISTORY OF PRESENTING ILLNESS ────────────────────────────────
story.append(sec("SECTION 3 — HISTORY OF PRESENTING ILLNESS", C_DARK, "📖"))
story.append(SP(1))
# 3A: PAIN
story.append(subsec("3A. PAIN (most important symptom in PVD)"))
story.append(SP(0.5))
story.append(info_box(
"<b>Distinguish:</b> "
"Intermittent claudication (exertional, reproducible, relieved by rest in 2–5 min) vs "
"Rest pain (persistent, worse at night, relieved by hanging leg down) vs "
"Acute ischaemia (sudden onset — 6 Ps) vs "
"Pseudoclaudication / neurogenic (spinal stenosis — worse standing, relieved by sitting/bending forward)",
bg=C_LTYELL, border=colors.HexColor("#d0a030")
))
story.append(SP(0.5))
pain_items = [
("Onset","Sudden (acute ischaemia) / Insidious (chronic PAD)"),
("Duration","___ days / weeks / months / years"),
("Site of pain","Calf / Thigh / Buttock / Foot / Toes — R / L / Bilateral"),
("Character","Cramping / Burning / Aching / Throbbing / Shooting / Numbness"),
("Claudication distance","Distance walked before onset of pain: ___ metres / yards"),
("Claudication type","Calf (femoropopliteal disease) / Thigh+buttock (aortoiliac disease = Leriche syndrome)"),
("Rest pain","Yes / No — onset during rest / night, position of relief: leg hanging down / walking"),
("Radiation","Localised / Radiates to: ___"),
("Aggravating factors","Walking / Exercise / Cold / Elevation of limb"),
("Relieving factors","Rest (claudication) / Hanging leg down / Dependency (rest pain) / Analgesics"),
("Progression","Stable / Worsening claudication distance / Rest pain now developing / Tissue loss"),
("Acute vs Chronic","Acute onset → ? Embolism / Thrombosis | Chronic progressive → Atherosclerosis"),
]
story.append(c2([[Paragraph(f"<b>{k}:</b>",sBold), Paragraph(v,sBody)] for k,v in pain_items], w1=48*mm))
story.append(SP(1))
# 3B: ULCER/GANGRENE
story.append(subsec("3B. ULCER / WOUND / GANGRENE"))
story.append(SP(0.5))
ulcer_items = [
("Site","Toes (tip / between toes) / Heel / Dorsum of foot / Lateral malleolus / Gaiter area"),
("Onset","Spontaneous / Following trauma / After nail trim / After minor injury"),
("Duration","___ days / weeks / months"),
("Progression","Healing / Static / Enlarging / Spreading proximally"),
("Pain","Painful (arterial ulcer) / Painless (neuropathic / venous)"),
("Discharge","None / Serous / Purulent / Haemoserous / Foul-smelling"),
("Colour change","Blackening / Gangrene — Wet (infected, spreading) / Dry (mummification, self-limiting)"),
("Associated fever","Yes / No — (wet gangrene / infected ulcer)"),
("Previous episodes","Yes / No — healed / amputated previously"),
]
story.append(c2([[Paragraph(f"<b>{k}:</b>",sBold), Paragraph(v,sBody)] for k,v in ulcer_items], w1=48*mm))
story.append(SP(1))
# 3C: ACUTE LIMB ISCHAEMIA SYMPTOMS
story.append(subsec("3C. ACUTE LIMB ISCHAEMIA — 6 Ps (if acute onset)"))
story.append(SP(0.5))
story.append(warn(
"⚠ SURGICAL EMERGENCY | The 6 Ps of Acute Limb Ischaemia: "
"<b>Pain</b> (sudden, severe) | "
"<b>Pallor</b> (white/marble-coloured) | "
"<b>Pulselessness</b> (absent distal pulses) | "
"<b>Paraesthesia</b> (tingling/numbness — nerve ischaemia) | "
"<b>Paralysis</b> (muscle ischaemia — irreversible after 6 hrs) | "
"<b>Perishing cold</b> "
" — Paraesthesia + Paralysis = IRREVERSIBLE ISCHAEMIA: EMERGENCY REVASCULARISATION"
))
story.append(SP(0.5))
story.append(c2([
[Paragraph("<b>Time of onset:</b>",sBold), Paragraph("Exact time: ___ hrs ago — critical for management window",sBody)],
[Paragraph("<b>Pain:</b>",sBold), Paragraph("Sudden, severe — Yes / No",sBody)],
[Paragraph("<b>Pallor:</b>",sBold), Paragraph("Yes / No — marble white appearance",sBody)],
[Paragraph("<b>Pulselessness:</b>",sBold), Paragraph("Level: ___ (helps localise embolus)",sBody)],
[Paragraph("<b>Paraesthesia:</b>",sBold), Paragraph("Yes / No — tingling / numbness (early nerve ischaemia)",sBody)],
[Paragraph("<b>Paralysis:</b>",sBold), Paragraph("Yes / No — unable to move toes/foot (LATE sign — irreversible ischaemia)",sBody)],
[Paragraph("<b>Perishing cold:</b>",sBold), Paragraph("Yes / No",sBody)],
[Paragraph("<b>Source of embolus:</b>",sBold), Paragraph("Known AF / Recent MI / Valvular HD / Aortic aneurysm / Mural thrombus",sBody)],
], w1=42*mm))
story.append(SP(2))
# ─── SECTION 4: PAST HISTORY ─────────────────────────────────────────────────
story.append(sec("SECTION 4 — PAST HISTORY & VASCULAR RISK FACTORS", C_DARK, "🏥"))
story.append(SP(1))
story.append(subsec("4A. CARDIOVASCULAR RISK FACTORS (mandatory in all PVD cases)"))
story.append(SP(0.5))
story.append(info_box(
"<b>Atherosclerotic risk factor assessment is MANDATORY in PVD: "
"Smoking + Diabetes + Hypertension + Dyslipidaemia = major modifiable risk factors.</b> "
"PVD is a coronary artery disease equivalent — assess for CAD, cerebrovascular disease simultaneously.",
bg=C_LTORANG, border=C_ORANGE
))
story.append(SP(0.5))
risk_items = [
("Smoking","Yes / Ex-smoker / No — Pack-years: ___ (No. packs/day × years) — cessation date: ___"),
("Diabetes mellitus","Yes / No — Type 1 / Type 2 — Duration: ___ years — HbA1c: ___ % — Insulin / OHA"),
("Hypertension","Yes / No — Duration: ___ — On treatment: ___ — Controlled: Y/N"),
("Dyslipidaemia","Yes / No — On statin: ___ — LDL: ___ — Total cholesterol: ___"),
("Previous cardiac events","MI / Angina / CABG / PCI / AF / Valvular heart disease / Heart failure"),
("Cerebrovascular disease","TIA / Stroke / Carotid stenosis — residual deficit: ___"),
("Previous PVD events","Previous angioplasty / bypass / endarterectomy / amputation — site & date: ___"),
("Renal disease","CKD — eGFR: ___ (contrast nephropathy risk for angiography)"),
("Obesity","BMI: ___ kg/m²"),
("Hypercoagulable state","Antiphospholipid syndrome / Factor V Leiden / Protein C/S deficiency"),
("Buerger's disease history","Young male smoker — digital ischaemia, migratory thrombophlebitis"),
]
story.append(c2([[Paragraph(f"<b>{k}:</b>",sBold), Paragraph(v,sBody)] for k,v in risk_items], w1=50*mm))
story.append(SP(1))
story.append(subsec("4B. OTHER PAST HISTORY"))
story.append(SP(0.5))
story.append(cb_list([
"Previous DVT / pulmonary embolism",
"Varicose veins / CVI treatment",
"Previous abdominal aortic aneurysm",
"Renal / hepatic disease",
"Blood disorders (polycythaemia, thrombocytosis)",
"Connective tissue disease (SLE, RA, scleroderma)",
"Malignancy (hypercoagulable)",
"Previous surgery / trauma to limb",
"Raynaud's phenomenon",
"Erectile dysfunction (Leriche syndrome)",
], cols=2))
story.append(SP(2))
# ─── SECTION 5: DRUG / TREATMENT HISTORY ─────────────────────────────────────
story.append(sec("SECTION 5 — DRUG & TREATMENT HISTORY", C_DARK, "💊"))
story.append(SP(1))
story.append(cb_list([
"Antiplatelet agents (Aspirin / Clopidogrel)",
"Anticoagulants (Warfarin / DOAC / LMWH)",
"Statins (Atorvastatin / Rosuvastatin)",
"Antihypertensives (ACE-I / ARB / Beta-blocker / CCB)",
"Antidiabetics (Metformin / Insulin / SGLT2i)",
"Vasodilators (Cilostazol / Naftidrofuryl)",
"Steroids / Immunosuppressants",
"NSAIDs (prostaglandin synthesis inhibition → vasoconstriction)",
"Ergotamine / Triptans (vasoconstrictors)",
"Beta-blockers (may worsen claudication — document)",
"Previous thrombolysis — agent & date",
"Contrast dye allergy / previous reactions",
], cols=2))
story.append(SP(2))
# ─── SECTION 6: PERSONAL & SOCIO-OCCUPATIONAL HISTORY ────────────────────────
story.append(sec("SECTION 6 — PERSONAL & SOCIO-OCCUPATIONAL HISTORY", C_DARK, "👤"))
story.append(SP(1))
story.append(c2([
[Paragraph("<b>Smoking:</b>",sBold), Paragraph("Pack-years = (cigarettes/day ÷ 20) × years = ___ pack-years Current / Ex / Non-smoker",sBody)],
[Paragraph("<b>Alcohol:</b>",sBold), Paragraph("Yes / No — units/week: ___",sBody)],
[Paragraph("<b>Diet:</b>",sBold), Paragraph("High fat / High calorie / Diabetic diet — relevant for atherosclerosis risk",sBody)],
[Paragraph("<b>Exercise / Activity:</b>",sBold), Paragraph("Sedentary / Active — exercise capacity before illness: ___",sBody)],
[Paragraph("<b>Occupation:</b>",sBold), Paragraph("Prolonged standing (varicose veins) / Heavy manual labour / Sedentary",sBody)],
[Paragraph("<b>Socioeconomic status:</b>",sBold), Paragraph("Upper / Middle / Lower — footwear habits (diabetic foot risk)",sBody)],
], w1=50*mm))
story.append(SP(2))
# ─── SECTION 7: FAMILY HISTORY ───────────────────────────────────────────────
story.append(sec("SECTION 7 — FAMILY HISTORY", C_DARK, "👨👩👦"))
story.append(SP(1))
story.append(cb_list([
"Premature coronary artery disease (< 55 M / < 65 F)",
"Peripheral vascular disease / amputation",
"Diabetes mellitus",
"Hypertension",
"Hyperlipidaemia / Familial hypercholesterolaemia",
"Aortic aneurysm",
"Hypercoagulable states",
"Buerger's disease",
], cols=2))
story.append(SP(2))
# ─── SECTION 8: GENERAL PHYSICAL EXAMINATION ─────────────────────────────────
story.append(sec("SECTION 8 — GENERAL PHYSICAL EXAMINATION", C_RED, "🩺"))
story.append(SP(1))
story.append(subsec("VITALS"))
story.append(SP(0.5))
story.append(c2([
[Paragraph("<b>Blood pressure:</b>",sBold), Paragraph("R arm: ___ / ___ mmHg L arm: ___ / ___ mmHg (Difference >15 mmHg → subclavian stenosis)",sBody)],
[Paragraph("<b>Pulse rate:</b>",sBold), Paragraph("___ bpm Regular / Irregular — AF present: Yes / No (source of embolism)",sBody)],
[Paragraph("<b>Temperature:</b>",sBold), Paragraph("___°F / ___°C Febrile: Yes / No (wet gangrene / infected ulcer)",sBody)],
[Paragraph("<b>Respiratory rate:</b>",sBold), Paragraph("___ /min",sBody)],
[Paragraph("<b>BMI:</b>",sBold), Paragraph("Weight: ___ kg Height: ___ cm BMI: ___ kg/m²",sBody)],
[Paragraph("<b>SpO₂:</b>",sBold), Paragraph("___% (peripheral hypoxia may worsen ischaemia)",sBody)],
], w1=50*mm))
story.append(SP(1))
story.append(subsec("GENERAL PHYSICAL EXAMINATION"))
story.append(SP(0.5))
gpe = [
[Paragraph("<b>Parameter</b>",sBold), Paragraph("<b>Finding</b>",sBold), Paragraph("<b>Vascular Significance</b>",sBold)],
[Paragraph("Pallor",sBody), Paragraph("Absent / Present",sBody), Paragraph("Severe anaemia worsens ischaemia",sBody)],
[Paragraph("Icterus",sBody), Paragraph("Absent / Present",sBody), Paragraph("Liver disease → coagulopathy",sBody)],
[Paragraph("Cyanosis",sBody), Paragraph("Central / Peripheral / Absent",sBody), Paragraph("Peripheral cyanosis → reduced perfusion",sBody)],
[Paragraph("Clubbing",sBody), Paragraph("Absent / Present",sBody), Paragraph("Rare in PVD; cardiac source of emboli",sBody)],
[Paragraph("Xanthelasma / Xanthoma",sBody), Paragraph("Absent / Present — site: ___",sBody), Paragraph("Familial hypercholesterolaemia → accelerated atherosclerosis",sBody)],
[Paragraph("Corneal arcus",sBody), Paragraph("Absent / Present (< 50 yrs significant)",sBody), Paragraph("Hyperlipidaemia",sBody)],
[Paragraph("Lymphadenopathy",sBody), Paragraph("Absent / Present",sBody), Paragraph("Inguinal LN — infection from ulcerated limb",sBody)],
[Paragraph("Pedal / sacral oedema",sBody), Paragraph("Absent / Present — pitting up to ___",sBody), Paragraph("Dependent oedema if hanging leg down for rest pain",sBody)],
[Paragraph("JVP",sBody), Paragraph("Normal / Elevated",sBody), Paragraph("Right heart failure → venous hypertension",sBody)],
[Paragraph("Cardiac examination",sBody), Paragraph("AF / Murmurs / Added sounds",sBody), Paragraph("Source of emboli (AF, valvular HD, mural thrombus)",sBody)],
[Paragraph("Abdominal examination",sBody), Paragraph("Pulsatile mass in midline?",sBody), Paragraph("AAA — source of emboli / limb ischaemia",sBody)],
]
gpe_widths = [45*mm, 55*mm, doc.width-100*mm]
gpe_t = Table([[Paragraph(c.text if hasattr(c,'text') else "",
ps(f"gpe{i}{j}",fontSize=7,
fontName="Helvetica-Bold" if i==0 else "Helvetica",
textColor=C_DARK if i==0 else C_DGREY,leading=9)) if isinstance(c,Paragraph) else c
for j,c in enumerate(r)] for i,r in enumerate(gpe)],
colWidths=gpe_widths,
style=TableStyle([
("BACKGROUND",(0,0),(-1,0),C_DARK),("TEXTCOLOR",(0,0),(-1,0),C_WHITE),
("FONTNAME",(0,0),(-1,0),"Helvetica-Bold"),
("FONTSIZE",(0,0),(-1,-1),7),
("TOPPADDING",(0,0),(-1,-1),2),("BOTTOMPADDING",(0,0),(-1,-1),2),
("LEFTPADDING",(0,0),(-1,-1),4),("RIGHTPADDING",(0,0),(-1,-1),4),
("GRID",(0,0),(-1,-1),0.3,C_MGREY),("VALIGN",(0,0),(-1,-1),"TOP"),
] + [("BACKGROUND",(0,i),(-1,i),C_LGREY) for i in range(1,len(gpe),2)])
)
story.append(gpe_t)
story.append(SP(2))
# ─── SECTION 9: LOCAL EXAMINATION (MOST IMPORTANT) ───────────────────────────
story.append(sec("SECTION 9 — LOCAL EXAMINATION OF THE AFFECTED LIMB (Core of PVD Exam)", C_RED, "🦵"))
story.append(SP(1))
story.append(info_box(
"<b>Examine BOTH limbs — compare normal vs affected side throughout. "
"Remove all footwear and socks before examination. "
"Examine from groin to toes: position — lying flat, then dependent (hanging), then elevated.</b>",
bg=C_LTBLUE, border=C_STEEL
))
story.append(SP(1))
# 9A: Inspection
story.append(subsec("9A. INSPECTION (Look — compare both limbs)"))
story.append(SP(0.5))
insp_data = [
["Feature","Normal Side","Affected Side / Finding"],
["Skin colour","Normal pink","Pale (elevated) / Dusky red (dependent) / Mottled / Cyanotic"],
["Guttering sign (Buerger's sign)","Normal colour on elevation","Marked pallor on elevation (positive Buerger's sign = severe ischaemia)"],
["Reactive hyperaemia (Buerger's sign)","Normal on dependency","Dependent rubor — dusky red on dependent position (positive)"],
["Trophic changes — skin","Normal","Thin / Shiny / Taut / Hairless skin (chronic ischaemia)"],
["Trophic changes — nails","Normal","Thick / Brittle / Ridged / Onychogryphosis / Ingrowing"],
["Hair loss","Present over dorsum of foot","Absent over dorsum of foot and lower leg"],
["Muscle wasting","Normal","Calf wasting / Quadriceps wasting (chronic ischaemia / disuse)"],
["Varicosities","Absent","Present — distribution: great/short saphenous; Spider veins"],
["Venous guttering","Normal veins visible","Veins empty on elevation — guttering sign (confirms arterial ischaemia)"],
["Swelling / oedema","Absent","Present — pitting / non-pitting; lipodermatosclerosis (venous)"],
["Skin changes","Normal","Haemosiderin staining / Lipodermatosclerosis / Eczema (venous)"],
]
insp_t_rows = [[Paragraph(c, ps(f"ins{i}{j}", fontSize=7,
fontName="Helvetica-Bold" if i==0 else "Helvetica",
textColor=C_WHITE if i==0 else C_DGREY, leading=9)) for j,c in enumerate(r)]
for i,r in enumerate(insp_data)]
insp_t = Table(insp_t_rows, colWidths=[45*mm, 42*mm, doc.width-87*mm],
style=TableStyle([
("BACKGROUND",(0,0),(-1,0),C_STEEL),
("FONTSIZE",(0,0),(-1,-1),7),
("TOPPADDING",(0,0),(-1,-1),2),("BOTTOMPADDING",(0,0),(-1,-1),2),
("LEFTPADDING",(0,0),(-1,-1),4),("RIGHTPADDING",(0,0),(-1,-1),4),
("GRID",(0,0),(-1,-1),0.3,C_MGREY),("VALIGN",(0,0),(-1,-1),"TOP"),
] + [("BACKGROUND",(0,i),(-1,i),C_LGREY) for i in range(1,len(insp_data),2)])
)
story.append(insp_t)
story.append(SP(1))
# Buerger's test box
story.append(green_box(
"<b>BUERGER'S TEST (Postural test for arterial insufficiency):</b> "
"1. Elevate both legs to 45° for 1–2 min → Ischaemic limb becomes PALE (positive = ischaemia). "
"2. Ask patient to sit up and hang legs over side → Ischaemic limb turns DUSKY RED (reactive hyperaemia). "
"Buerger's angle = angle at which pallor occurs: "
"Normal: no pallor even at 90° | <20° = critical ischaemia | 20–30° = severe | 30–45° = moderate. "
"Result: Positive / Negative — Angle: ___°"
))
story.append(SP(1))
# 9B: Ulcer examination
story.append(subsec("9B. ULCER EXAMINATION (if present — MANDATORY detailed description)"))
story.append(SP(0.5))
ulcer_data = [
["Feature","Arterial Ulcer","Venous Ulcer","Neuropathic (Diabetic) Ulcer"],
["Site","Tips of toes, heel, lateral malleolus","Gaiter area (medial malleolus)","Pressure points (heel, 1st/5th MT head, big toe)"],
["Pain","Very painful","Mildly painful","Painless"],
["Edge","Punched out, well-defined","Sloping / irregular","Punched out"],
["Floor","Pale, sloughy, no granulation","Sloughy / granulation","Pale / granulation"],
["Surrounding skin","Pale, thin, atrophic","Lipodermatosclerosis, pigmented","Callus, neuropathic skin"],
["Discharge","Minimal / serous","Moderate, serous","Variable, may be purulent"],
["Pulses","Absent / diminished","Normal","Variable (may have neuropathy without PAD)"],
["Sensation","Normal until late","Normal","Reduced / absent (glove-stocking)"],
]
ulcer_t_rows = [[Paragraph(c, ps(f"ulc{i}{j}", fontSize=6.5,
fontName="Helvetica-Bold" if i==0 or j==0 else "Helvetica",
textColor=C_WHITE if i==0 else (C_DARK if j==0 else C_DGREY), leading=8)) for j,c in enumerate(r)]
for i,r in enumerate(ulcer_data)]
ulcer_t = Table(ulcer_t_rows, colWidths=[30*mm, 40*mm, 42*mm, doc.width-112*mm],
style=TableStyle([
("BACKGROUND",(0,0),(-1,0),C_RED),
("BACKGROUND",(0,1),(-1,1),C_LGREY),("BACKGROUND",(0,3),(-1,3),C_LGREY),
("BACKGROUND",(0,5),(-1,5),C_LGREY),("BACKGROUND",(0,7),(-1,7),C_LGREY),
("FONTSIZE",(0,0),(-1,-1),6.5),
("TOPPADDING",(0,0),(-1,-1),2),("BOTTOMPADDING",(0,0),(-1,-1),2),
("LEFTPADDING",(0,0),(-1,-1),3),("RIGHTPADDING",(0,0),(-1,-1),3),
("GRID",(0,0),(-1,-1),0.3,C_MGREY),("VALIGN",(0,0),(-1,-1),"TOP"),
])
)
story.append(ulcer_t)
story.append(SP(0.5))
story.append(c2([
[Paragraph("<b>Number of ulcers:</b>",sBold), Paragraph("Single / Multiple",sBody)],
[Paragraph("<b>Size:</b>",sBold), Paragraph("___ cm × ___ cm",sBody)],
[Paragraph("<b>Depth:</b>",sBold), Paragraph("Superficial (skin only) / Deep (subcutaneous) / Tendon/bone visible (Wagner Grade 3+)",sBody)],
[Paragraph("<b>Gangrene:</b>",sBold), Paragraph("Absent / Present — Dry (mummified, clear line of demarcation) / Wet (infected, spreading, foul smell)",sBody)],
[Paragraph("<b>Wagner's Grade:</b>",sBold), Paragraph("0 (no ulcer) / 1 (superficial) / 2 (deep to tendon) / 3 (deep+infection/abscess) / 4 (partial foot gangrene) / 5 (whole foot gangrene)",sBody)],
], w1=45*mm))
story.append(SP(1))
# 9C: Palpation
story.append(subsec("9C. PALPATION"))
story.append(SP(0.5))
story.append(c2([
[Paragraph("<b>Skin temperature:</b>",sBold), Paragraph("Normal / Cold — level of coldness: foot / ankle / calf / knee Compare both limbs using dorsum of examiner's hand",sBody)],
[Paragraph("<b>Capillary refill time:</b>",sBold), Paragraph("___ seconds (Normal: <2 sec) — site tested: nail bed / pulp",sBody)],
[Paragraph("<b>Venous filling time:</b>",sBold), Paragraph("___ seconds after legs elevated (Normal: <15 sec on dependency) — prolonged = arterial ischaemia",sBody)],
[Paragraph("<b>Skin sensation:</b>",sBold), Paragraph("Normal / Reduced / Absent — light touch / pin prick / vibration Level of loss: ___",sBody)],
[Paragraph("<b>Muscle tenderness:</b>",sBold), Paragraph("Absent / Present — compartment syndrome, DVT",sBody)],
[Paragraph("<b>Deep vein palpation:</b>",sBold), Paragraph("Calf tenderness / Homan's sign — DVT (note: non-specific)",sBody)],
[Paragraph("<b>Varicosities (palpation):</b>",sBold), Paragraph("Soft / Hard / Tender — Thrill on cough at SFJ",sBody)],
[Paragraph("<b>Limb girth:</b>",sBold), Paragraph("Thigh: ___ cm / Calf: ___ cm (compare both sides — note difference)",sBody)],
[Paragraph("<b>Tourniquet test:</b>",sBold), Paragraph("(if varicose veins) — Trendelenburg test: tourniquet at SFJ / mid-thigh / below knee",sBody)],
], w1=52*mm))
story.append(SP(1))
# 9D: PULSE EXAMINATION (full table)
story.append(subsec("9D. PULSE EXAMINATION (most important physical finding in PVD)"))
story.append(SP(0.5))
story.append(warn(
"⚠ Examine ALL peripheral pulses systematically: "
"Upper limbs (radial, brachial) → Neck (carotid) → Abdomen (aorta) → Lower limbs (femoral, popliteal, PT, DP). "
"Level of absent pulse = level of arterial occlusion. "
"Auscultate for BRUITS over carotid, aorta, renal arteries, femoral arteries."
))
story.append(SP(0.5))
story.append(pulse_table())
story.append(SP(1))
story.append(subsec("9E. AUSCULTATION FOR BRUITS"))
story.append(SP(0.5))
story.append(c2([
[Paragraph("<b>Carotid artery bruit:</b>",sBold), Paragraph("Absent / Present — R / L (carotid stenosis — TIA / stroke risk)",sBody)],
[Paragraph("<b>Aortic bruit:</b>",sBold), Paragraph("Absent / Present (aortic aneurysm / stenosis)",sBody)],
[Paragraph("<b>Renal artery bruit:</b>",sBold), Paragraph("Absent / Present — R / L (renal artery stenosis → hypertension)",sBody)],
[Paragraph("<b>Femoral artery bruit:</b>",sBold), Paragraph("Absent / Present — R / L (iliac / femoral stenosis)",sBody)],
[Paragraph("<b>Popliteal bruit:</b>",sBold), Paragraph("Absent / Present — R / L",sBody)],
], w1=52*mm))
story.append(SP(2))
# ─── SECTION 10: FONTAINE & RUTHERFORD CLASSIFICATION ───────────────────────
story.append(sec("SECTION 10 — CLASSIFICATION OF CHRONIC LIMB ISCHAEMIA", C_STEEL, "📊"))
story.append(SP(1))
fontaine_rows = [
[Paragraph("<b>Fontaine Stage</b>",ps("fh0",fontSize=7.5,fontName="Helvetica-Bold",textColor=C_WHITE,leading=9)),
Paragraph("<b>Rutherford Category</b>",ps("fh1",fontSize=7.5,fontName="Helvetica-Bold",textColor=C_WHITE,leading=9)),
Paragraph("<b>Clinical Description</b>",ps("fh2",fontSize=7.5,fontName="Helvetica-Bold",textColor=C_WHITE,leading=9)),
Paragraph("<b>ABI (approx.)</b>",ps("fh3",fontSize=7.5,fontName="Helvetica-Bold",textColor=C_WHITE,leading=9))],
[Paragraph("Stage I",sBody), Paragraph("0 — Asymptomatic",sBody), Paragraph("Asymptomatic — abnormal haemodynamics only",sBody), Paragraph(">0.9",sBody)],
[Paragraph("Stage IIa",sBody), Paragraph("1 — Mild claudication",sBody), Paragraph("Claudication > 200 metres",sBody), Paragraph("0.6–0.9",sBody)],
[Paragraph("Stage IIb",sBody), Paragraph("2/3 — Moderate/severe claudication",sBody), Paragraph("Claudication < 200 metres",sBody), Paragraph("0.5–0.8",sBody)],
[Paragraph("Stage III",sBody), Paragraph("4 — Ischaemic rest pain",sBody), Paragraph("Rest pain (night pain, relieved by dependency)",sBody), Paragraph("0.3–0.5",sBody)],
[Paragraph("Stage IVa",sBody), Paragraph("5 — Minor tissue loss",sBody), Paragraph("Ischaemic ulcer / focal gangrene",sBody), Paragraph("<0.3",sBody)],
[Paragraph("Stage IVb",sBody), Paragraph("6 — Major tissue loss",sBody), Paragraph("Extensive gangrene / limb loss",sBody), Paragraph("<0.2",sBody)],
]
fontaine_t = Table(fontaine_rows, colWidths=[25*mm, 40*mm, 75*mm, doc.width-140*mm],
style=TableStyle([
("BACKGROUND",(0,0),(-1,0),C_DARK),
("FONTSIZE",(0,0),(-1,-1),7.2),
("TOPPADDING",(0,0),(-1,-1),3),("BOTTOMPADDING",(0,0),(-1,-1),3),
("LEFTPADDING",(0,0),(-1,-1),5),("RIGHTPADDING",(0,0),(-1,-1),4),
("GRID",(0,0),(-1,-1),0.3,C_MGREY),("VALIGN",(0,0),(-1,-1),"MIDDLE"),
# Critical limb ischaemia = Stage III+
("BACKGROUND",(0,4),(-1,6),C_LTRED),
("FONTNAME",(0,4),(0,6),"Helvetica-Bold"),
] + [("BACKGROUND",(0,i),(-1,i),C_LGREY) for i in [2,4]])
)
story.append(fontaine_t)
story.append(SP(0.5))
story.append(warn(
"⚠ CRITICAL LIMB ISCHAEMIA (CLI) = Fontaine Stage III or IV "
"(Rest pain for >2 weeks AND/OR ischaemic ulceration/gangrene) "
"— requires URGENT REVASCULARISATION to prevent limb loss."
))
story.append(SP(1))
story.append(Paragraph("<b>Patient's Fontaine Stage:</b> I / IIa / IIb / III / IVa / IVb", sBold))
story.append(SP(2))
# ─── SECTION 11: ABI TABLE ───────────────────────────────────────────────────
story.append(sec("SECTION 11 — ANKLE BRACHIAL INDEX (ABI) & NON-INVASIVE VASCULAR STUDIES", C_STEEL, "📏"))
story.append(SP(1))
story.append(green_box(
"<b>ABI = Ankle systolic pressure ÷ Highest brachial systolic pressure. "
"Technique:</b> Doppler probe over dorsalis pedis / posterior tibial artery. "
"Inflate BP cuff above ankle until signal disappears, deflate until signal returns = ankle systolic pressure. "
"Divide by highest brachial pressure. "
"Normal: 1.0–1.3 | >1.3: Non-compressible (calcified) — diabetic / renal failure | "
"0.9–1.0: Borderline | 0.6–0.9: Claudication | 0.3–0.6: Rest pain | <0.3: Impending tissue loss"
))
story.append(SP(0.5))
abi_rows = [
[Paragraph("<b>ABI Value</b>",ps("ah0",fontSize=7.5,fontName="Helvetica-Bold",textColor=C_WHITE,leading=9)),
Paragraph("<b>Clinical Correlation</b>",ps("ah1",fontSize=7.5,fontName="Helvetica-Bold",textColor=C_WHITE,leading=9)),
Paragraph("<b>Management Implication</b>",ps("ah2",fontSize=7.5,fontName="Helvetica-Bold",textColor=C_WHITE,leading=9))],
[Paragraph(">1.3",sBody), Paragraph("Non-compressible arteries (calcification)",sBody), Paragraph("Diabetic / renal failure — use toe-brachial index (TBI)",sBody)],
[Paragraph("1.0–1.3",sBody), Paragraph("Normal",sBody), Paragraph("No significant PAD",sBody)],
[Paragraph("0.9–0.99",sBody), Paragraph("Borderline / equivocal",sBody), Paragraph("Exercise ABI; risk factor modification",sBody)],
[Paragraph("0.6–0.89",sBody), Paragraph("Mild-moderate PAD (claudication)",sBody), Paragraph("Conservative + supervised exercise + risk factor control",sBody)],
[Paragraph("0.3–0.59",sBody), Paragraph("Severe PAD (rest pain)",sBody), Paragraph("Consider revascularisation (angioplasty / bypass)",sBody)],
[Paragraph("<0.3",sBody), Paragraph("Critical ischaemia / impending tissue loss",sBody), Paragraph("Urgent revascularisation / amputation decision",sBody)],
]
abi_t = Table(abi_rows, colWidths=[22*mm, 65*mm, doc.width-87*mm],
style=TableStyle([
("BACKGROUND",(0,0),(-1,0),C_TEAL),
("FONTSIZE",(0,0),(-1,-1),7.2),
("TOPPADDING",(0,0),(-1,-1),2),("BOTTOMPADDING",(0,0),(-1,-1),2),
("LEFTPADDING",(0,0),(-1,-1),5),("RIGHTPADDING",(0,0),(-1,-1),4),
("GRID",(0,0),(-1,-1),0.3,C_MGREY),("VALIGN",(0,0),(-1,-1),"MIDDLE"),
("BACKGROUND",(0,6),(-1,6),C_LTRED),("FONTNAME",(0,6),(-1,6),"Helvetica-Bold"),
] + [("BACKGROUND",(0,i),(-1,i),C_LGREY) for i in [2,4,6]])
)
story.append(abi_t)
story.append(SP(1))
story.append(c2([
[Paragraph("<b>ABI — Right limb:</b>",sBold), Paragraph("Ankle pressure: ___ mmHg Brachial: ___ mmHg ABI = ___",sBody)],
[Paragraph("<b>ABI — Left limb:</b>",sBold), Paragraph("Ankle pressure: ___ mmHg Brachial: ___ mmHg ABI = ___",sBody)],
[Paragraph("<b>Toe-Brachial Index (TBI):</b>",sBold), Paragraph("If ABI >1.3 — TBI: ___ (Normal >0.7)",sBody)],
], w1=52*mm))
story.append(SP(2))
# ─── SECTION 12: INVESTIGATIONS ──────────────────────────────────────────────
story.append(sec("SECTION 12 — INVESTIGATIONS", C_DARK, "🔬"))
story.append(SP(1))
story.append(subsec("12A. BLOOD INVESTIGATIONS"))
story.append(SP(0.5))
story.append(cb_list([
"CBC (Hb, WBC, Platelets — polycythaemia, thrombocytosis, infection)",
"Blood glucose (fasting + postprandial) / HbA1c",
"Lipid profile (Total cholesterol, LDL, HDL, TG)",
"Renal function tests (urea, creatinine, eGFR — contrast nephropathy risk)",
"Liver function tests (albumin — wound healing)",
"Coagulation profile (PT, aPTT, INR — if on anticoagulants)",
"Urine routine (proteinuria, glycosuria — diabetic nephropathy)",
"Thyroid function tests (hypothyroidism → dyslipidaemia)",
"ESR / CRP (inflammatory markers — vasculitis, infection)",
"Blood cultures (if febrile / septic — wet gangrene)",
"Wound swab culture & sensitivity",
"Thrombophilia screen (if young patient — Factor V Leiden, Protein C/S, ATIII, APLA)",
], cols=2))
story.append(SP(1))
story.append(subsec("12B. NON-INVASIVE VASCULAR STUDIES"))
story.append(SP(0.5))
noninv_rows = [
["Investigation","Principle","When Used","Key Finding"],
["Ankle Brachial Index (ABI)","Doppler + BP cuff","Screening, severity assessment","<0.9 = PAD; >1.3 = calcified"],
["Duplex Ultrasound","US imaging + Doppler velocity","Localise lesion, pre-intervention planning",">50% stenosis = significant (PSV ratio >2)"],
["Exercise ABI (treadmill)","ABI before and after exercise","Atypical claudication / normal resting ABI","ABI drop >20% post-exercise = PAD"],
["Transcutaneous PO₂ (TcPO₂)","Skin oxygen tension","Wound healing potential","<30 mmHg = poor healing; <20 mmHg = amputation likely"],
["Toe pressure / TBI","Digital plethysmography","Calcified vessels (DM/CKD)","Normal >50 mmHg; <30 mmHg = critical ischaemia"],
["Segmental limb pressures","BP at multiple levels","Localise level of disease","Gradient >20 mmHg = significant disease at that segment"],
]
story.append(c3(noninv_rows[0], noninv_rows[1:], w1=40*mm, w2=45*mm, hbg=C_TEAL))
story.append(SP(1))
story.append(subsec("12C. IMAGING INVESTIGATIONS"))
story.append(SP(0.5))
img_rows = [
["Investigation","Details / Indications","Advantages","Limitations"],
["Duplex Ultrasound","First-line imaging; aortoiliac & femoropopliteal segments","Non-invasive, no radiation, real-time flow","Operator-dependent; bowel gas obscures aorta"],
["CT Angiography (CTA)","Gold standard for planning revascularisation; 3D reconstruction","Fast, widely available, shows calcification, stents visible","Radiation + iodinated contrast (renal risk); calcification artefact"],
["MR Angiography (MRA)","Alternative to CTA — no iodinated contrast","No radiation; excellent soft tissue detail","No ferromagnetic implants; gadolinium → NSF (if eGFR <30); overestimates stenosis"],
["Digital Subtraction Angiography (DSA)","Pre-operative planning + simultaneous intervention","Definitive; allows immediate treatment","Invasive; radiation; contrast nephropathy; access site complications"],
["Chest X-ray","Cardiac size, aortic calcification, lung status","Rapid, widely available","Limited vascular detail"],
["ECG + Echocardiogram","Cardiac source of emboli; LV function","Source of embolism; EF for operative risk","Indirect information"],
["X-ray foot/limb","Bony involvement — osteomyelitis, calcification","Available; shows gas in tissue (wet gangrene)","Poor soft tissue detail"],
]
img_t_rows = [[Paragraph(c,ps(f"img{i}{j}",fontSize=6.5,
fontName="Helvetica-Bold" if i==0 or j==0 else "Helvetica",
textColor=C_WHITE if i==0 else (C_DARK if j==0 else C_DGREY),leading=8)) for j,c in enumerate(r)]
for i,r in enumerate(img_rows)]
img_t = Table(img_t_rows, colWidths=[32*mm,48*mm,38*mm,doc.width-118*mm],
style=TableStyle([
("BACKGROUND",(0,0),(-1,0),C_DARK),
("FONTSIZE",(0,0),(-1,-1),6.5),
("TOPPADDING",(0,0),(-1,-1),2),("BOTTOMPADDING",(0,0),(-1,-1),2),
("LEFTPADDING",(0,0),(-1,-1),3),("RIGHTPADDING",(0,0),(-1,-1),3),
("GRID",(0,0),(-1,-1),0.3,C_MGREY),("VALIGN",(0,0),(-1,-1),"TOP"),
] + [("BACKGROUND",(0,i),(-1,i),C_LGREY) for i in range(1,len(img_rows),2)])
)
story.append(img_t)
story.append(SP(2))
# ─── SECTION 13: DIFFERENTIAL DIAGNOSIS ─────────────────────────────────────
story.append(sec("SECTION 13 — DIFFERENTIAL DIAGNOSIS", C_DARK, "🔁"))
story.append(SP(1))
story.append(c3(
["Condition","Distinguishing Features","Key Test"],
[
["Neurogenic claudication (spinal stenosis)","Pain in buttock/thigh on walking AND standing; relieved by bending forward/sitting; normal ABI; occurs uphill and downhill equally","MRI spine; ABI normal"],
["Musculoskeletal pain","Not reproducible with fixed distance; worse on specific movements; tenderness over joints","X-ray joint; orthopaedic assessment"],
["Venous claudication (DVT sequelae)","Bursting pain after walking; relieved by elevation; history of DVT; oedema","Duplex veins; ABI normal"],
["Popliteal artery entrapment","Young athletes; claudication on plantarflexion; absent pulses on plantar flexion","MRI/CTA; provocation ABI"],
["Cystic adventitial disease","Young person; claudication; no risk factors; ABI drops on knee flexion","US/MRI of popliteal artery"],
["Peripheral neuropathy","Burning/tingling at rest; not related to exertion; reduced sensation; normal pulses","Nerve conduction study; ABI"],
["Acute DVT","Calf pain + swelling + redness; Homan's sign; warm; not related to exercise","Duplex veins; D-dimer"],
["Embolism vs Thrombosis","Embolism: sudden, sharp demarcation, known cardiac source, no prior claudication; Thrombosis: gradual, prior claudication","Doppler; CTA; DSA"],
],
w1=42*mm, w2=80*mm, hbg=C_RED
))
story.append(SP(2))
# ─── SECTION 14: SURGICAL MANAGEMENT OUTLINE ─────────────────────────────────
story.append(sec("SECTION 14 — MANAGEMENT OUTLINE (Surgery-Oriented)", C_RED, "⚕"))
story.append(SP(1))
story.append(subsec("14A. CONSERVATIVE MANAGEMENT"))
story.append(SP(0.5))
story.append(cb_list([
"Smoking cessation (MOST important — doubles 5yr bypass patency)",
"Exercise rehabilitation (supervised walking programme — 3×/week × 3 months)",
"Risk factor control: DM / HTN / Dyslipidaemia",
"Antiplatelet therapy: Aspirin 75–100 mg/day OR Clopidogrel 75 mg/day",
"Statin therapy: High-intensity (Atorvastatin 40–80 mg) — reduce LDL <1.8 mmol/L",
"ACE inhibitor / ARB (vascular protection, BP control)",
"Cilostazol (phosphodiesterase inhibitor) — improves claudication distance",
"Foot care: daily inspection, appropriate footwear, podiatry",
"Wound care: debridement, dressings, offloading (diabetic foot)",
"Analgesia for rest pain: tramadol / opioids (while awaiting revascularisation)",
"Nutritional optimisation: albumin, vitamins (wound healing)",
"Treat infection: IV antibiotics (if infected ulcer / wet gangrene)",
], cols=2))
story.append(SP(1))
story.append(subsec("14B. ENDOVASCULAR INTERVENTIONS"))
story.append(SP(0.5))
story.append(cb_list([
"Percutaneous Transluminal Angioplasty (PTA) — balloon dilatation of stenosis/short occlusion",
"Stenting — metallic stent placement (iliac preferred; femoropopliteal controversial)",
"Drug-eluting stent / drug-coated balloon (DCB) — femoropopliteal disease",
"Subintimal angioplasty — chronic total occlusion",
"Mechanical thrombectomy / Aspiration thrombectomy — acute ischaemia",
"Catheter-directed thrombolysis (CDT) — acute/subacute ischaemia <14 days",
"Atherectomy devices — heavily calcified lesions",
], cols=2))
story.append(SP(1))
story.append(subsec("14C. SURGICAL REVASCULARISATION"))
story.append(SP(0.5))
story.append(c2([
[Paragraph("<b>Aortobifemoral bypass:</b>",sBold), Paragraph("Aortoiliac disease (Leriche syndrome) — gold standard; Dacron / PTFE graft",sBody)],
[Paragraph("<b>Femoropopliteal bypass:</b>",sBold), Paragraph("SFA/popliteal disease — autologous GSV preferred (better patency); PTFE if no vein",sBody)],
[Paragraph("<b>Femoro-distal bypass:</b>",sBold), Paragraph("Tibial / peroneal vessel — for critical limb ischaemia; GSV mandatory",sBody)],
[Paragraph("<b>Endarterectomy:</b>",sBold), Paragraph("Short segment disease — CFA endarterectomy most common in peripheral surgery",sBody)],
[Paragraph("<b>Profundoplasty:</b>",sBold), Paragraph("Profunda femoris artery reconstruction — improves collateral flow when bypass not feasible",sBody)],
[Paragraph("<b>Embolectomy (Fogarty):</b>",sBold), Paragraph("Acute arterial embolism — Fogarty balloon catheter passed through arteriotomy",sBody)],
[Paragraph("<b>Sympathectomy:</b>",sBold), Paragraph("Lumbar sympathectomy — inoperable cases, non-healing ulcers, minor tissue loss (limited benefit)",sBody)],
[Paragraph("<b>Amputation:</b>",sBold), Paragraph("Non-viable limb / failed revascularisation — level: digital / ray / TMA / BKA / AKA / hip disarticulation",sBody)],
], w1=48*mm))
story.append(SP(1))
story.append(subsec("14D. AMPUTATION LEVEL DECISION"))
story.append(SP(0.5))
story.append(green_box(
"<b>Amputation level selection:</b> "
"Toe / Ray amputation (Wagner 1–2 with good vascularity) → "
"Transmetatarsal amputation (TMA) (multiple toe gangrene, good ABI) → "
"Below-knee amputation (BKA) (preferred — better rehabilitation; requires popliteal artery patency) → "
"Above-knee amputation (AKA) (BKA not feasible; high infection; flexion contracture) → "
"Hip disarticulation (rare — thigh gangrene / failed AKA). "
"BKA:AKA ratio target = 3:1. "
"TcPO₂ >30 mmHg at proposed site = adequate healing."
))
story.append(SP(2))
# ─── SECTION 15: DOCUMENTATION TEMPLATE ─────────────────────────────────────
story.append(sec("SECTION 15 — BEDSIDE DOCUMENTATION TEMPLATE", C_DARK, "📝"))
story.append(SP(1))
doc_txt = """<b>CHIEF COMPLAINT:</b> _________________________ × _____ duration
<b>HISTORY:</b>
Pain: site ___, claudication distance ___ m / rest pain, onset ___, character ___, relieved by ___.
Ulcer/gangrene: site ___, duration ___, painful Y/N, wet/dry, Wagner grade ___.
Acute ischaemia: onset ___ hrs ago, pallor Y/N, pulselessness Y/N, paraesthesia Y/N, paralysis Y/N.
Risk factors: Smoking ___ pack-yrs | DM ___ yrs, HbA1c ___ | HTN Y/N | Dyslipidaemia Y/N | CAD/AF Y/N.
<b>GENERAL EXAMINATION:</b> BP: ___ (R) / ___ (L) mmHg. PR: ___ bpm — regular / AF. Temp: ___°F.
Pallor/Icterus/Clubbing/Cyanosis/LN/Oedema: _____. Cardiac: _____. Abdomen: pulsatile mass Y/N.
<b>LOCAL EXAMINATION:</b>
Inspection: colour ___, trophic changes ___, hair loss Y/N, ulcer (site/size/type) ___, gangrene Y/N (wet/dry).
Buerger's test: Positive / Negative — Buerger's angle: ___°.
Palpation: Temperature ___ (level: ___). CRT: ___ sec. Venous filling time: ___ sec.
Sensation: Normal / Reduced to ___.
Pulses: Femoral R___/L___ | Popliteal R___/L___ | PT R___/L___ | DP R___/L___.
Bruits: Carotid ___ / Aortic ___ / Femoral ___.
<b>ABI:</b> Right: ___ Left: ___ Fontaine Stage: I / IIa / IIb / III / IVa / IVb
<b>DIAGNOSIS:</b>
Anatomical: _______________________________________________
Pathological: _____________________________________________
Etiological: ______________________________________________
<b>PLAN:</b> Conservative / Endovascular (PTA/stent) / Surgical (bypass/endarterectomy/embolectomy) / Amputation
Investigations ordered: ___________________________________
Referrals: Vascular surgery / Diabetologist / Cardiologist / Podiatry / Physiotherapy"""
story.append(Table([[Paragraph(doc_txt,ps("dt",fontSize=7,fontName="Helvetica",textColor=C_DGREY,leading=11))]],
colWidths=[doc.width],
style=TableStyle([
("BACKGROUND",(0,0),(-1,-1),C_LTGREEN),
("BOX",(0,0),(-1,-1),0.7,C_TEAL),
("TOPPADDING",(0,0),(-1,-1),6),("BOTTOMPADDING",(0,0),(-1,-1),6),
("LEFTPADDING",(0,0),(-1,-1),8),("RIGHTPADDING",(0,0),(-1,-1),8),
])
))
story.append(SP(3))
story.append(HRFlowable(width="100%",thickness=0.4,color=C_MGREY))
story.append(SP(1))
story.append(Paragraph(
"References: Bailey & Love's Short Practice of Surgery 28e | "
"Mulholland & Greenfield's Surgery 7e (ABI Table 85.3) | "
"Goldman-Cecil Medicine — Chronic Limb Ischaemia (Fontaine/Rutherford) | "
"Pye's Surgical Handicraft 22e (Doppler ABI technique) | For educational use only.",
sFooter))
doc.build(story, onFirstPage=page_cb, onLaterPages=page_cb)
print(f"PDF created: {OUTPUT}")
cd /home/daytona/workspace/resp-checklist && python3 pvd_proforma.py 2>&1
ls -lh /home/daytona/workspace/resp-checklist/PVD_Surgery_Case_Proforma.pdf
PVD Surgery Case Proforma
PDF Document · PDF
| Section | Content |
|---|---|
| 1. Patient Identification | Demographics, referral source |
| 2. Chief Complaints | Common PVD presenting complaints with prompts |
| 3. History of Presenting Illness | 3A. Pain (claudication distance, rest pain, Buerger's angle, acute vs chronic) 3B. Ulcer/Gangrene (type, Wagner grade, wet vs dry) 3C. Acute Limb Ischaemia — the 6 Ps with emergency warning |
| 4. Past History & Risk Factors | Full atherosclerotic risk factor table (smoking, DM, HTN, dyslipidaemia, CAD, CVD, hypercoagulable states) |
| 5. Drug History | Antiplatelets, anticoagulants, statins, vasodilators, beta-blockers |
| 6-7. Personal/Social/Family History | Pack-years formula, occupational risk, family vascular history |
| 8. General Examination | Bilateral BP comparison (subclavian stenosis), AF detection, abdominal pulsatile mass (AAA), xanthelasma |
| 9. Local Examination | 9A. Inspection (Buerger's test with angle, trophic changes, guttering sign) 9B. Ulcer (3-column arterial vs venous vs neuropathic comparison table) 9C. Palpation (CRT, venous filling time, compartment syndrome) 9D. Full pulse examination table (all 8 sites — Present/Absent/Diminished/Bruit) 9E. Auscultation for bruits |
| 10. Fontaine + Rutherford Classification | Complete table with ABI correlations; CLI highlighted in red |
| 11. ABI Table | Technique, interpretation, fill-in boxes for R and L limb, TBI |
| 12. Investigations | Blood tests, non-invasive studies (Duplex, TcPO₂, segmental pressures), imaging comparison (Duplex/CTA/MRA/DSA) |
| 13. Differential Diagnosis | Embolism vs thrombosis, neurogenic claudication, DVT, popliteal entrapment |
| 14. Management Outline | Conservative + Endovascular (PTA/stent/CDT) + Surgical (bypass/endarterectomy/embolectomy) + Amputation level decision with TcPO₂ criteria |
| 15. Documentation Template | Single fill-in sheet for bedside use |
Case Performa for PVD according to S Das
S Das "A Manual on Clinical Surgery" peripheral vascular disease examination proforma
~/resp-checklist/sdas_pvd.py
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import mm
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_LEFT, TA_CENTER
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, KeepTogether
)
OUTPUT = "/home/daytona/workspace/resp-checklist/SDas_PVD_Proforma.pdf"
# ── Colour palette (S Das book feel — classic green academic) ────────────────
C_DARK = colors.HexColor("#1a3325") # dark forest green
C_GREEN = colors.HexColor("#1e6b3a") # S Das green
C_DKRED = colors.HexColor("#7b1818") # warning red
C_TEAL = colors.HexColor("#1a6b6b") # teal
C_LTGREEN = colors.HexColor("#e6f4ec")
C_LTBLUE = colors.HexColor("#e6eef6")
C_LTYELL = colors.HexColor("#fefbe6")
C_LTORANG = colors.HexColor("#fdf0e0")
C_LTRED = colors.HexColor("#fae8e8")
C_LGREY = colors.HexColor("#f3f4f6")
C_MGREY = colors.HexColor("#c5cbd4")
C_DGREY = colors.HexColor("#2d3440")
C_WHITE = colors.white
C_ORANGE = colors.HexColor("#b85000")
W, H = A4
doc = SimpleDocTemplate(
OUTPUT,
pagesize=A4,
leftMargin=13*mm, rightMargin=13*mm,
topMargin=24*mm, bottomMargin=15*mm,
title="PVD Case Proforma — S Das Manual on Clinical Surgery",
author="Based on S Das, A Manual on Clinical Surgery, 13th Edition"
)
styles = getSampleStyleSheet()
def ps(name, **kw):
return ParagraphStyle(name, parent=styles["Normal"], **kw)
sSecH = ps("SH", fontSize=9.5, fontName="Helvetica-Bold", textColor=C_WHITE, leading=13)
sSecH2 = ps("S2", fontSize=8, fontName="Helvetica-Bold", textColor=C_DARK, leading=11)
sBody = ps("B", fontSize=7.5, fontName="Helvetica", textColor=C_DGREY, leading=10)
sBold = ps("BB", fontSize=7.5, fontName="Helvetica-Bold", textColor=C_DARK, leading=10)
sItal = ps("BI", fontSize=7.2, fontName="Helvetica-Oblique", textColor=colors.HexColor("#555"), leading=9)
sSmall = ps("SM", fontSize=6.8, fontName="Helvetica", textColor=colors.HexColor("#666"), leading=9)
sWarn = ps("W", fontSize=7.2, fontName="Helvetica-Bold", textColor=C_DKRED, leading=9)
sGreen = ps("G", fontSize=7.2, fontName="Helvetica-Bold", textColor=C_GREEN, leading=9)
sLabel = ps("L", fontSize=7.5, fontName="Helvetica-Bold", textColor=C_TEAL, leading=9)
sQuote = ps("Q", fontSize=7.2, fontName="Helvetica-Oblique", textColor=C_DARK, leading=10)
sFooter= ps("F", fontSize=6.5, fontName="Helvetica-Oblique", textColor=colors.HexColor("#888"), alignment=TA_CENTER, leading=8)
sNum = ps("N", fontSize=8, fontName="Helvetica-Bold", textColor=C_GREEN, leading=10)
SP = lambda n=2: Spacer(1, n*mm)
# ── Canvas ────────────────────────────────────────────────────────────────────
def page_cb(canvas, doc):
canvas.saveState()
canvas.setFillColor(C_DARK)
canvas.rect(0, H-22*mm, W, 22*mm, fill=1, stroke=0)
canvas.setFillColor(C_GREEN)
canvas.rect(0, H-24*mm, W, 2*mm, fill=1, stroke=0)
canvas.setFont("Helvetica-Bold", 13)
canvas.setFillColor(C_WHITE)
canvas.drawCentredString(W/2, H-13.5*mm, "PERIPHERAL VASCULAR DISEASE — CLINICAL CASE PROFORMA")
canvas.setFont("Helvetica", 7.5)
canvas.setFillColor(colors.HexColor("#90c8a0"))
canvas.drawCentredString(W/2, H-19.5*mm,
"Based on: S Das — A Manual on Clinical Surgery, 13th Edition | Chapter 6: Ischaemia of Limb & Gangrene")
canvas.setFillColor(C_DARK)
canvas.rect(0, 0, W, 11*mm, fill=1, stroke=0)
canvas.setFillColor(C_GREEN)
canvas.rect(0, 11*mm, W, 0.8*mm, fill=1, stroke=0)
canvas.setFont("Helvetica", 6.5)
canvas.setFillColor(colors.HexColor("#90c8a0"))
canvas.drawString(13*mm, 4*mm, "S Das — A Manual on Clinical Surgery 13e | Chapter 6")
canvas.drawRightString(W-13*mm, 4*mm, f"Page {doc.page}")
canvas.restoreState()
# ── Helpers ───────────────────────────────────────────────────────────────────
def sec(text, bg=C_DARK, icon=""):
t = f"{icon} {text}" if icon else text
return KeepTogether([Table([[Paragraph(t, sSecH)]], colWidths=[doc.width],
style=TableStyle([
("BACKGROUND",(0,0),(-1,-1),bg),
("TOPPADDING",(0,0),(-1,-1),4),("BOTTOMPADDING",(0,0),(-1,-1),4),
("LEFTPADDING",(0,0),(-1,-1),8),
]))])
def subsec(text, bg=C_LTGREEN, tc=C_DARK, border=C_GREEN):
return Table([[Paragraph(text, ps(f"ss{text[:6]}", fontSize=8, fontName="Helvetica-Bold", textColor=tc, leading=10))]],
colWidths=[doc.width],
style=TableStyle([
("BACKGROUND",(0,0),(-1,-1),bg),
("TOPPADDING",(0,0),(-1,-1),2),("BOTTOMPADDING",(0,0),(-1,-1),2),
("LEFTPADDING",(0,0),(-1,-1),8),
("LINEBELOW",(0,0),(-1,-1),0.7,border),
]))
def c2(rows, w1=52*mm, alt=True):
w2 = doc.width - w1
st = [
("FONTNAME",(0,0),(-1,-1),"Helvetica"),("FONTSIZE",(0,0),(-1,-1),7.2),
("TOPPADDING",(0,0),(-1,-1),2),("BOTTOMPADDING",(0,0),(-1,-1),2),
("LEFTPADDING",(0,0),(-1,-1),5),("RIGHTPADDING",(0,0),(-1,-1),4),
("GRID",(0,0),(-1,-1),0.3,C_MGREY),("VALIGN",(0,0),(-1,-1),"TOP"),
]
if alt:
for i in range(0,len(rows),2): st.append(("BACKGROUND",(0,i),(-1,i),C_LGREY))
return Table(rows, colWidths=[w1,w2], style=TableStyle(st))
def c3(hdr_texts, rows, w1=40*mm, w2=None, w3=None, hbg=C_GREEN):
w2 = w2 or 55*mm; w3 = w3 or (doc.width-w1-w2)
hdr = [Paragraph(f"<b>{h}</b>", ps(f"c3h{i}", fontSize=7, fontName="Helvetica-Bold", textColor=C_WHITE, leading=9)) for i,h in enumerate(hdr_texts)]
all_rows = [hdr] + [[Paragraph(c, ps(f"c3r{i}{j}", fontSize=7, fontName="Helvetica", textColor=C_DGREY, leading=9)) for j,c in enumerate(r)] for i,r in enumerate(rows)]
st = [
("BACKGROUND",(0,0),(-1,0),hbg),
("FONTSIZE",(0,0),(-1,-1),7),
("TOPPADDING",(0,0),(-1,-1),2),("BOTTOMPADDING",(0,0),(-1,-1),2),
("LEFTPADDING",(0,0),(-1,-1),4),("RIGHTPADDING",(0,0),(-1,-1),4),
("GRID",(0,0),(-1,-1),0.3,C_MGREY),("VALIGN",(0,0),(-1,-1),"TOP"),
]
for i in range(1,len(all_rows),2): st.append(("BACKGROUND",(0,i),(-1,i),C_LGREY))
return Table(all_rows, colWidths=[w1,w2,w3], style=TableStyle(st))
def info_box(text, bg=C_LTYELL, border=colors.HexColor("#c8a030")):
return Table([[Paragraph(text, ps("ib", fontSize=7.2, fontName="Helvetica", textColor=C_DGREY, leading=10))]],
colWidths=[doc.width],
style=TableStyle([
("BACKGROUND",(0,0),(-1,-1),bg),("BOX",(0,0),(-1,-1),0.6,border),
("TOPPADDING",(0,0),(-1,-1),4),("BOTTOMPADDING",(0,0),(-1,-1),4),
("LEFTPADDING",(0,0),(-1,-1),7),("RIGHTPADDING",(0,0),(-1,-1),7),
]))
def warn(text):
return info_box(text, bg=C_LTRED, border=C_DKRED)
def green_box(text):
return info_box(text, bg=C_LTGREEN, border=C_GREEN)
def sdas_quote(text):
"""Box styled as a direct S Das quotation"""
return Table([[Paragraph(f'"{text}"', sQuote)]], colWidths=[doc.width],
style=TableStyle([
("BACKGROUND",(0,0),(-1,-1),colors.HexColor("#f0f8f4")),
("BOX",(0,0),(-1,-1),1,C_GREEN),
("LEFTBORDER",(0,0),(0,-1),4,C_GREEN),
("TOPPADDING",(0,0),(-1,-1),4),("BOTTOMPADDING",(0,0),(-1,-1),4),
("LEFTPADDING",(0,0),(-1,-1),10),("RIGHTPADDING",(0,0),(-1,-1),8),
]))
def cb_list(items, cols=2):
rows=[]
for i in range(0,len(items),cols):
row=[]
for j in range(cols):
t = f"☐ {items[i+j]}" if i+j<len(items) else ""
row.append(Paragraph(t, sBody))
rows.append(row)
return Table(rows, colWidths=[doc.width/cols]*cols,
style=TableStyle([("TOPPADDING",(0,0),(-1,-1),1),("BOTTOMPADDING",(0,0),(-1,-1),1),
("LEFTPADDING",(0,0),(-1,-1),3),("RIGHTPADDING",(0,0),(-1,-1),3)]))
# ════════════════════════════════════════════════════════════════════════════
# STORY
# ════════════════════════════════════════════════════════════════════════════
story = []
# ─── HEADER NOTE ─────────────────────────────────────────────────────────────
story.append(SP(1))
story.append(green_box(
"<b>S Das Chapter 6 Framework:</b> History (13 numbered points) → "
"Physical Examination [Constitutional + Local {Inspection → Palpation → Special Tests → Auscultation}] → "
"Special Investigations → Signs of Gangrene → Causes → Treatment. "
"S Das always numbers his points — this proforma follows his exact numbered sequence."
))
story.append(SP(1))
# ─── SECTION 1: PATIENT BIO ──────────────────────────────────────────────────
story.append(sec("IDENTIFICATION", C_DARK, "📋"))
story.append(SP(1))
story.append(c2([
[Paragraph("<b>Name:</b>",sBold), Paragraph("_"*45,sBody)],
[Paragraph("<b>Age / Sex:</b>",sBold), Paragraph("___ yrs / M / F (S Das: Atherosclerosis = old age, males > females; Buerger's = 20–40 yrs men; Raynaud's = young women)",sBody)],
[Paragraph("<b>Occupation:</b>",sBold), Paragraph("_"*40,sBody)],
[Paragraph("<b>Address:</b>",sBold), Paragraph("Urban / Rural (cold climate exposure relevant for Raynaud's/Frostbite)",sBody)],
[Paragraph("<b>IP / OP No.:</b>",sBold), Paragraph("_"*25+" Date of admission: ___________",sBody)],
[Paragraph("<b>Informant:</b>",sBold), Paragraph("Patient / Relative — Reliable / Unreliable",sBody)],
], w1=40*mm))
story.append(SP(2))
# ─── SECTION 2: HISTORY (S Das numbered 1–13) ────────────────────────────────
story.append(sec("HISTORY (S Das 13-Point System — Chapter 6)", C_DARK, "📖"))
story.append(SP(1))
story.append(info_box(
"<b>S Das key opening point:</b> Age and Sex determine the probable diagnosis before examination begins. "
"Atherosclerosis = old men. Buerger's (TAO) = young men, heavy smokers. "
"Raynaud's = young women. Diabetic arteriopathy = middle age.",
bg=C_LTGREEN, border=C_GREEN
))
story.append(SP(1))
# Points 1-5
story.append(subsec("HISTORY POINTS 1–5 (S Das sequence)"))
story.append(SP(0.5))
h_rows = [
[Paragraph("<b>1. Age & Sex</b>",sNum),
Paragraph("Age: ___ years Sex: M / F (Atherosclerosis: old, M>F | Buerger's: 20–40 yrs, men, smokers | Raynaud's: young women | Diabetic: middle age)",sBody)],
[Paragraph("<b>2. Limbs affected</b>",sNum),
Paragraph("Right / Left / Bilateral Upper / Lower (Buerger's + atherosclerosis = lower limbs; Raynaud's = upper limbs; Embolism = usually unilateral)",sBody)],
[Paragraph("<b>3. Bilateral or Unilateral</b>",sNum),
Paragraph("Buerger's and Raynaud's = bilateral; Embolic gangrene = mostly unilateral; Atherosclerotic = may start unilateral; Diabetic = unilateral or bilateral",sBody)],
[Paragraph("<b>4. Mode of Onset</b>",sNum),
Paragraph("Gradual / Spontaneous (atherosclerosis, Buerger's, Raynaud's) / Sudden severe pain radiating down artery (embolism) / After minor trauma e.g. nail paring (diabetic gangrene)",sBody)],
[Paragraph("<b>5. Pain</b>",sNum),
Paragraph("Site: ___ | Character: cramping / burning / aching | On walking: Yes/No | Disappears on rest: Yes/No | Worse on warmth: Yes/No (Two types — see below)",sBody)],
]
story.append(Table(h_rows, colWidths=[32*mm, doc.width-32*mm],
style=TableStyle([
("FONTSIZE",(0,0),(-1,-1),7.2),
("TOPPADDING",(0,0),(-1,-1),3),("BOTTOMPADDING",(0,0),(-1,-1),3),
("LEFTPADDING",(0,0),(-1,-1),4),("RIGHTPADDING",(0,0),(-1,-1),4),
("GRID",(0,0),(-1,-1),0.3,C_MGREY),("VALIGN",(0,0),(-1,-1),"TOP"),
] + [("BACKGROUND",(0,i),(-1,i),C_LGREY) for i in range(0,5,2)]))
)
story.append(SP(1))
# Pain sub-section
story.append(subsec("5A. INTERMITTENT CLAUDICATION (S Das definition)"))
story.append(SP(0.3))
story.append(sdas_quote(
"'Though literally claudio means I limp, yet the term claudication is used here to describe the muscle pain "
"due to accumulation of the excessive P-substance owing to inadequate blood flow. It is a pain in the muscles, "
"usually the calf and it is described by the patient as a cramp.' — S Das, Chapter 6"
))
story.append(SP(0.5))
claud_items = [
("Site of claudication","Foot (Buerger's — tibial/plantar arteries affected) / Calf (femoro-popliteal junction) / Thigh (superficial femoral artery opening) / Buttock (iliac bifurcation or aorta — Leriche syndrome)"),
("Claudication distance","Metres walked before pain onset: ___ metres — increases / decreases over time"),
("Character","Cramping / P-substance accumulation type muscle pain"),
("Relief","Stops COMPLETELY on ceasing exercise — differentiates from rest pain and neurogenic pain"),
("Progression","Walking distance worsening: Yes / No — now rest pain: Yes / No"),
]
story.append(c2([[Paragraph(f"<b>{k}:</b>",sBold), Paragraph(v,sBody)] for k,v in claud_items], w1=44*mm))
story.append(SP(1))
story.append(subsec("5B. REST PAIN (S Das: more severe — pre-gangrenous)"))
story.append(SP(0.3))
rest_items = [
("Character","Burning / Aching / Severe — constant, not related to exercise"),
("Timing","Night pain / Present at rest — patient wakes from sleep"),
("Posture","Relieved by hanging leg down / sleeping in chair / dependency — gravity improves perfusion"),
("Worsened by","Elevation of limb / warmth (S Das: 'worse on application of warmth') / cardiac output reduction"),
("Duration","Since: ___ — now present: Yes / No"),
]
story.append(c2([[Paragraph(f"<b>{k}:</b>",sBold), Paragraph(v,sBody)] for k,v in rest_items], w1=40*mm))
story.append(SP(1))
# Points 6-13
story.append(subsec("HISTORY POINTS 6–13 (S Das sequence continued)"))
story.append(SP(0.5))
h2_rows = [
[Paragraph("<b>6. Colour change</b>",sNum),
Paragraph("Pallor / Cyanosis (blue-purple) / Black (gangrene) — when noticed: ___ — spreading: Yes/No",sBody)],
[Paragraph("<b>7. Paraesthesia</b>",sNum),
Paragraph('"When muscle pain begins, numbness, pins and needles in skin of foot — due to shunting of blood from skin to muscle" (S Das) Present: Yes / No Site: ___',sBody)],
[Paragraph("<b>8. Superficial phlebitis</b>",sNum),
Paragraph("Swelling + redness + minor pain in affected part — occurs in HIGH PROPORTION of Buerger's disease cases History: Yes / No Site: ___",sBody)],
[Paragraph("<b>9. Involvement of other arteries</b>",sNum),
Paragraph("Fainting / Transient blackout (carotid/vertebral) / Chest pain (coronary) / Weakness or paraesthesia upper limbs / Blurred vision / Abdominal pain (mesenteric) — Yes/No for each",sBody)],
[Paragraph("<b>10. Impotence</b>",sNum),
Paragraph("Failure of erection — 'not uncommon in case of bilateral internal iliac artery occlusion' (S Das — Leriche syndrome) Yes / No",sBody)],
[Paragraph("<b>11. Past history</b>",sNum),
Paragraph("Previous cardiac attacks / embolic syndromes / Diabetes / Exposure to cold (frost bite) / Previous amputations / Vascular procedures",sBody)],
[Paragraph("<b>12. Personal history</b>",sNum),
Paragraph('"Excessive smoking incriminated as causing thromboangiitis obliterans and worsening atherosclerotic disease" (S Das) Smoking: Pack-years = ___ Alcohol: Yes/No',sBody)],
[Paragraph("<b>13. Family history</b>",sNum),
Paragraph('"Arterial disease particularly atherosclerosis is often familial" (S Das) Family history of: Atherosclerosis / CAD / PVD / Amputation / Diabetes / Hypertension / Hyperlipidaemia Yes / No / Details: ___',sBody)],
]
story.append(Table(h2_rows, colWidths=[35*mm, doc.width-35*mm],
style=TableStyle([
("FONTSIZE",(0,0),(-1,-1),7.2),
("TOPPADDING",(0,0),(-1,-1),3),("BOTTOMPADDING",(0,0),(-1,-1),3),
("LEFTPADDING",(0,0),(-1,-1),4),("RIGHTPADDING",(0,0),(-1,-1),4),
("GRID",(0,0),(-1,-1),0.3,C_MGREY),("VALIGN",(0,0),(-1,-1),"TOP"),
] + [("BACKGROUND",(0,i),(-1,i),C_LGREY) for i in range(0,8,2)]))
)
story.append(SP(2))
# ─── SECTION 3: PHYSICAL EXAMINATION ─────────────────────────────────────────
story.append(sec("PHYSICAL EXAMINATION (S Das Structure)", C_DARK, "🩺"))
story.append(SP(1))
story.append(info_box(
"<b>S Das structure:</b> "
"(A) Constitutional Examination → (B) Local Examination "
"[Inspection → Palpation (numbered 1–15 by S Das) → Special Tests → Auscultation] "
"S Das emphasis: 'Considerable constitutional disturbances may be observed in severe acute ischaemia from embolus and in gas gangrene.'",
bg=C_LTGREEN, border=C_GREEN
))
story.append(SP(1))
# Constitutional
story.append(subsec("A. CONSTITUTIONAL EXAMINATION"))
story.append(SP(0.5))
story.append(c2([
[Paragraph("<b>General condition:</b>",sBold), Paragraph("Good / Moderate / Poor Comfortable / Distressed / Toxic",sBody)],
[Paragraph("<b>Build & nutrition:</b>",sBold), Paragraph("Well / Moderately / Poorly built and nourished",sBody)],
[Paragraph("<b>Temperature:</b>",sBold), Paragraph("___°F / °C Febrile (acute ischaemia/gas gangrene) / Afebrile",sBody)],
[Paragraph("<b>Pulse rate:</b>",sBold), Paragraph("___ bpm Regular / Irregular (Irregular = AF → source of embolism)",sBody)],
[Paragraph("<b>Blood pressure:</b>",sBold), Paragraph("R arm: ___ / ___ L arm: ___ / ___ (S Das: lowering of BP in severe acute ischaemia + gas gangrene)",sBody)],
[Paragraph("<b>Respiratory rate:</b>",sBold), Paragraph("___ /min",sBody)],
[Paragraph("<b>Pallor:</b>",sBold), Paragraph("Absent / Present (anaemia worsens ischaemia)",sBody)],
[Paragraph("<b>Icterus:</b>",sBold), Paragraph("Absent / Present",sBody)],
[Paragraph("<b>Cyanosis:</b>",sBold), Paragraph("Central / Peripheral / Absent",sBody)],
[Paragraph("<b>Clubbing:</b>",sBold), Paragraph("Absent / Present",sBody)],
[Paragraph("<b>Lymphadenopathy:</b>",sBold), Paragraph("Absent / Present — site: ___ (inguinal — infected ulcer/gangrene)",sBody)],
[Paragraph("<b>Oedema:</b>",sBold), Paragraph("Absent / Present — pitting up to ___ (hanging leg for rest pain relief → dependent oedema)",sBody)],
[Paragraph("<b>JVP:</b>",sBold), Paragraph("Normal / Elevated",sBody)],
[Paragraph("<b>Cardiac:</b>",sBold), Paragraph("Murmurs: ___ / AF: Yes/No / Added sounds: ___ (source of cardiac embolism)",sBody)],
[Paragraph("<b>Abdomen:</b>",sBold), Paragraph("Pulsatile midline mass: Yes/No (AAA — source of emboli)",sBody)],
], w1=45*mm))
story.append(SP(2))
# Local Examination
story.append(sec("LOCAL EXAMINATION (S Das Detailed Sequence)", C_GREEN, "🦵"))
story.append(SP(1))
story.append(info_box(
"<b>S Das instruction:</b> 'To detect minor change in colour the clinician should put the affected limb and its "
"fellow side by side.' Always compare BOTH LIMBS throughout examination. Remove footwear and expose fully.",
bg=C_LTGREEN, border=C_GREEN
))
story.append(SP(1))
# A: Inspection
story.append(subsec("A. INSPECTION (S Das numbered signs)"))
story.append(SP(0.5))
insp_rows = [
[Paragraph("<b>1. Colour change</b>",sNum),
Paragraph("Normal pink / Marked pallor (acute embolism, Raynaud's spasm) / Congestion + purple-blue cyanosis (severe ischaemia, pre-gangrenous) / Pallor on elevation [S Das: 'Change in colour is the MOST NOTICEABLE feature of an ischaemic limb']",sBody)],
[Paragraph("<b>2. Signs of ischaemia (trophic)</b>",sNum),
Paragraph("Thinning of skin: Y/N | Diminished hair growth: Y/N | Loss of subcutaneous fat: Y/N | Shininess: Y/N | Nail changes (brittle, transverse ridges): Y/N | Ulceration at pressure areas (heel, malleoli, ball of foot, tips of toes): Y/N",sBody)],
[Paragraph("<b>3. Gangrene signs</b>",sNum),
Paragraph("Change of colour (pale → bluish purple → black): Y/N | Loss of temperature: Y/N | Loss of sensation: Y/N | Loss of pulsation: Y/N | Loss of function: Y/N (S Das 5 signs of gangrene)",sBody)],
[Paragraph("<b>4. Dry vs Wet Gangrene</b>",sNum),
Paragraph("Dry: Hard, shrivelled, mummified, discoloured (gradual occlusion — atherosclerosis) | Wet: Oedematous with blebs, crepitus if gas (sudden blockage — embolism/infection) Type: ___",sBody)],
[Paragraph("<b>5. Scars</b>",sNum),
Paragraph("Thoracotomy / bypass / endarterectomy / amputation stumps — site: ___",sBody)],
[Paragraph("<b>6. Muscle wasting</b>",sNum),
Paragraph("Absent / Present — calf wasting / thigh wasting (chronic ischaemia/disuse) Side: ___",sBody)],
[Paragraph("<b>7. Limb swelling</b>",sNum),
Paragraph("Absent / Present — DVT / dependent oedema / lymphoedema / inflammation (wet gangrene)",sBody)],
]
story.append(Table(insp_rows, colWidths=[32*mm, doc.width-32*mm],
style=TableStyle([
("FONTSIZE",(0,0),(-1,-1),7.2),
("TOPPADDING",(0,0),(-1,-1),3),("BOTTOMPADDING",(0,0),(-1,-1),3),
("LEFTPADDING",(0,0),(-1,-1),4),("RIGHTPADDING",(0,0),(-1,-1),4),
("GRID",(0,0),(-1,-1),0.3,C_MGREY),("VALIGN",(0,0),(-1,-1),"TOP"),
] + [("BACKGROUND",(0,i),(-1,i),C_LGREY) for i in range(0,7,2)])
))
story.append(SP(2))
# B: Buerger's postural test
story.append(subsec("B. BUERGER'S POSTURAL TEST (S Das — must be in broad daylight)"))
story.append(SP(0.3))
story.append(sdas_quote(
"'This test must be carried out in broad day light. The patient lies on his back on the examining table. "
"The patient is asked to raise his legs one after the other keeping the knees straight. The legs of a normal "
"individual remain pink even if they are raised to 90°. But in case of an ischaemic limb elevation to a certain "
"degree will cause marked pallor and the veins will be empty and guttered. The angle (between the limb and the "
"horizontal plane) at which such pallor appears is called Buerger's angle or the Vascular angle. A vascular "
"angle of less than 30° indicates severe ischaemia.' — S Das, Chapter 6"
))
story.append(SP(0.5))
story.append(c2([
[Paragraph("<b>Step 1 — Elevation:</b>",sBold), Paragraph("Raise both legs with knees straight. Angle at which pallor appears = Buerger's angle / Vascular angle",sBody)],
[Paragraph("<b>Buerger's angle:</b>",sBold), Paragraph("R: ___° L: ___° Normal: no pallor even at 90° | <20°: critical ischaemia | 20–30°: severe | 30–45°: moderate",sBody)],
[Paragraph("<b>Venous guttering:</b>",sBold), Paragraph("Veins empty and guttered on elevation: Yes / No (confirms severe arterial ischaemia)",sBody)],
[Paragraph("<b>Step 2 (if doubtful):</b>",sBold), Paragraph("Elevated legs supported; patient flexes and extends ankles/toes to fatigue → sole assumes cadaveric pallor if occlusive disease",sBody)],
[Paragraph("<b>Step 3 — Dependency:</b>",sBold), Paragraph("Patient sits up and hangs legs → within 2–3 min cyanotic hue spreads over affected foot (reactive hyperaemia/cyanotic congestion) — Positive: Yes / No",sBody)],
[Paragraph("<b>Overall result:</b>",sBold), Paragraph("Positive / Negative Side: R / L / Bilateral",sBody)],
], w1=46*mm))
story.append(SP(2))
# C: Palpation (S Das numbered 1-15)
story.append(subsec("C. PALPATION (S Das numbered examination points)"))
story.append(SP(0.5))
palp_rows = [
[Paragraph("<b>1. Skin temperature</b>",sNum),
Paragraph("Use dorsum of hand. Compare both limbs. Level of coldness: foot / ankle / calf / knee / thigh Affected side: R / L",sBody)],
[Paragraph("<b>2. Capillary refill</b>",sNum),
Paragraph("Compress nail bed / toe pulp for 3 sec, release → Normal: <2 sec Result: ___ sec Prolonged = arterial ischaemia",sBody)],
[Paragraph("<b>3. Venous filling time</b>",sNum),
Paragraph("Elevate limb for 1 min → sit patient and hang legs → time for veins to fill Normal: <15 sec Prolonged (>40 sec) = arterial ischaemia",sBody)],
[Paragraph("<b>4. Tissue pressure</b>",sNum),
Paragraph("Gentle pressure on gangrenous area — Dry gangrene: hard, shrivelled (S Das: 'hard as leather') Wet gangrene: oedematous, soft, crepitus (gas in tissue)",sBody)],
[Paragraph("<b>5. Line of demarcation</b>",sNum),
Paragraph("Absent / Present — clear / gradual boundary between gangrenous and viable tissue Level: toes / transmetatarsal / foot / ankle / calf",sBody)],
[Paragraph("<b>6. Sensation</b>",sNum),
Paragraph("Normal / Reduced / Absent — test with pin prick + cotton wool Level: ___ (S Das: 'Sensation of the gangrenous area is often lost') Hyperaesthesia at border of gangrene: Yes/No",sBody)],
[Paragraph("<b>7. Motor function</b>",sNum),
Paragraph("Able to move toes: Yes / No Foot dorsiflexion: Present / Absent (S Das: 'Patient is unable to move the part when viability of deeper tissues is at stake')",sBody)],
[Paragraph("<b>8. Muscle tenderness</b>",sNum),
Paragraph("Absent / Present — calf tenderness (DVT) / compartment tenderness / tenderness over artery (embolism)",sBody)],
[Paragraph("<b>9. Limb girth</b>",sNum),
Paragraph("Thigh: R ___ / L ___ cm Calf: R ___ / L ___ cm (Compare both sides)",sBody)],
[Paragraph("<b>10. Varicosities</b>",sNum),
Paragraph("Absent / Present — site: great saphenous (medial) / short saphenous (posterior) Consistency: soft / firm / hard Thrill on coughing at SFJ: Yes/No",sBody)],
]
story.append(Table(palp_rows, colWidths=[32*mm, doc.width-32*mm],
style=TableStyle([
("FONTSIZE",(0,0),(-1,-1),7.2),
("TOPPADDING",(0,0),(-1,-1),3),("BOTTOMPADDING",(0,0),(-1,-1),3),
("LEFTPADDING",(0,0),(-1,-1),4),("RIGHTPADDING",(0,0),(-1,-1),4),
("GRID",(0,0),(-1,-1),0.3,C_MGREY),("VALIGN",(0,0),(-1,-1),"TOP"),
] + [("BACKGROUND",(0,i),(-1,i),C_LGREY) for i in range(0,10,2)])
))
story.append(SP(1))
# Pulse examination — S Das exact descriptions
story.append(subsec("11. PULSE EXAMINATION (S Das exact anatomy — numbered arteries)"))
story.append(SP(0.3))
story.append(green_box(
"<b>S Das on pulse examination:</b> "
"'In embolism, the pulse can usually be traced down to the point where it meets the obstruction. "
"Here there is often a small tender swelling and the pulse is lost below.' "
"'When the pulse is feeble or absent, the pulsation of the examiner's own finger may be mistaken for that of the patient.' "
"Note: S Das states dorsalis pedis artery is absent in 10% of cases (normally)."
))
story.append(SP(0.5))
pulse_sites = [
("Dorsalis pedis","Just LATERAL to tendon of extensor hallucis longus; absent in 10% cases normally; disappears proximal to 1st MT space"),
("Posterior tibial","Just BEHIND medial malleolus, midway between it and tendo Achilles"),
("Anterior tibial","Midway anteriorly between two malleoli against lower end of tibia, just lateral to extensor hallucis longus tendon — extend great toe to make tendon prominent"),
("Popliteal","Knee flexed to 40°, heel resting on bed, muscles relaxed. Fingers over lower popliteal fossa, moved sideways against posterior tibial condyles. OR: prone position, passive knee flexion"),
("Femoral","At groin just BELOW inguinal ligament, MIDWAY between ASIS and symphysis pubis"),
("Radial","Lateral aspect of volar wrist"),
("Ulnar","Medial aspect of volar wrist"),
("Brachial","In front of elbow, just MEDIAL to tendon of biceps"),
("Subclavian","Just ABOVE middle of clavicle"),
("Carotid","Carotid triangle in front of SCM against carotid tubercle of 6th cervical vertebra"),
("Superficial temporal","Just in front of tragus of ear"),
("Abdominal aorta","Midline, deep palpation — pulsatile ± expansile mass"),
]
hdr_p = [Paragraph(f"<b>{h}</b>", ps(f"puh{i}", fontSize=6.8, fontName="Helvetica-Bold", textColor=C_WHITE, leading=8)) for i,h in enumerate(["Artery","S Das Anatomical Landmark","Present","Absent","Diminished","Bruit"])]
pulse_rows = [hdr_p]
for art, desc in pulse_sites:
pulse_rows.append([
Paragraph(art, ps(f"pa{art[:4]}", fontSize=7, fontName="Helvetica-Bold", textColor=C_DARK, leading=9)),
Paragraph(desc, ps(f"pd{art[:4]}", fontSize=6.5, fontName="Helvetica", textColor=C_DGREY, leading=8)),
Paragraph("☐", sBody), Paragraph("☐", sBody), Paragraph("☐", sBody), Paragraph("☐", sBody),
])
pulse_t = Table(pulse_rows, colWidths=[24*mm, 82*mm, 10*mm, 10*mm, 17*mm, 14*mm],
style=TableStyle([
("BACKGROUND",(0,0),(-1,0),C_DARK),
("FONTSIZE",(0,0),(-1,-1),6.8),
("TOPPADDING",(0,0),(-1,-1),2),("BOTTOMPADDING",(0,0),(-1,-1),2),
("LEFTPADDING",(0,0),(-1,-1),3),("RIGHTPADDING",(0,0),(-1,-1),3),
("GRID",(0,0),(-1,-1),0.3,C_MGREY),("VALIGN",(0,0),(-1,-1),"MIDDLE"),
("ALIGN",(2,0),(-1,-1),"CENTER"),
] + [("BACKGROUND",(0,i),(-1,i),C_LGREY) for i in range(1,len(pulse_rows),2)])
)
story.append(pulse_t)
story.append(SP(0.5))
story.append(info_box(
"<b>S Das on artery wall:</b> "
"(a) Pulse — volume and tension "
"(b) Condition of arterial wall — atheromatous or not "
"(c) Thrombosis — 'feels midway between firm and soft and one can feel something within the artery' "
"(d) In Buerger's disease — 'not only distal arteries but also the VEINS are thrombosed' (important differentiator) "
"<b>Disappearing pulse:</b> Normal pulse may disappear after exercise to claudication point — 'unmasking preliminary stage of arterial occlusion' — reappears 1–2 min after rest.",
bg=C_LTYELL, border=colors.HexColor("#c8a030")
))
story.append(SP(1))
# Special tests (S Das numbered 1–11)
story.append(subsec("D. SPECIAL TESTS (S Das numbered 1–11)"))
story.append(SP(0.5))
st_rows = [
[Paragraph("<b>1. Trendelenburg test</b>",sNum),
Paragraph("For varicose veins: compress SFJ with tourniquet while elevated, lower limb, release tourniquet → rapid filling from above = SFJ incompetence (Positive Trendelenburg)",sBody)],
[Paragraph("<b>2. Tourniquet test</b>",sNum),
Paragraph("Tourniquet at different levels to locate perforator incompetence Level of positive test: ___",sBody)],
[Paragraph("<b>3. Homan's test (DVT)</b>",sNum),
Paragraph("Dorsiflexion of foot with knee extended → calf pain = positive (S Das notes as non-specific) R: + / − L: + / −",sBody)],
[Paragraph("<b>4. Moses test (DVT)</b>",sNum),
Paragraph("Squeezing calf from side to side → pain = positive DVT R: + / − L: + / −",sBody)],
[Paragraph("<b>5. Lewis-Pratt test</b>",sNum),
Paragraph("Knee flexed 90°, squeezing calf → pain in popliteal fossa = DVT R: + / − L: + / −",sBody)],
[Paragraph("<b>6. Elevated arms test</b>",sNum),
Paragraph("Thoracic outlet syndrome: abduct shoulders 90°, externally rotate, open-close hands ×5 min. TOS: fatigue + paraesthesia + arm dropped. Cervical disc: neck/shoulder pain only.",sBody)],
[Paragraph("<b>7. Allen's test</b>",sNum),
Paragraph("Radial/ulnar patency: clench fist, compress both arteries at wrist, open fist (palm white), release one artery — colour return = patent Radial: patent/blocked Ulnar: patent/blocked",sBody)],
[Paragraph("<b>8. Branham's sign (AV fistula)</b>",sNum),
Paragraph("Compress artery proximal to fistula → swelling reduces + bruit disappears + pulse slows + pulse pressure normalises R: + / −",sBody)],
[Paragraph("<b>9. Costoclavicular compression</b>",sNum),
Paragraph("Thoracic outlet: throw shoulders back + down (military posture) → subclavian artery compression → radial pulse reduces/disappears + subclavian bruit R: + / − L: + / −",sBody)],
[Paragraph("<b>10. Hyperabduction manoeuvre</b>",sNum),
Paragraph("Pectoralis minor syndrome: passively hyperabduct affected arm → radial pulse reduces/disappears + axillary bruit near pectoralis minor tendon R: + / − L: + / −",sBody)],
[Paragraph("<b>11. Gangrenous area assessment</b>",sNum),
Paragraph("Feel gangrenous area: Dry gangrene = hard + shrivelled Wet gangrene = oedematous ± crepitation (gas) Type: Dry / Wet / Gas",sBody)],
]
story.append(Table(st_rows, colWidths=[38*mm, doc.width-38*mm],
style=TableStyle([
("FONTSIZE",(0,0),(-1,-1),7.2),
("TOPPADDING",(0,0),(-1,-1),3),("BOTTOMPADDING",(0,0),(-1,-1),3),
("LEFTPADDING",(0,0),(-1,-1),4),("RIGHTPADDING",(0,0),(-1,-1),4),
("GRID",(0,0),(-1,-1),0.3,C_MGREY),("VALIGN",(0,0),(-1,-1),"TOP"),
] + [("BACKGROUND",(0,i),(-1,i),C_LGREY) for i in range(0,11,2)])
))
story.append(SP(1))
# Neurological examination
story.append(subsec("E. NEUROLOGICAL EXAMINATION (S Das point 15)"))
story.append(SP(0.5))
story.append(c2([
[Paragraph("<b>Sensation:</b>",sBold), Paragraph("Normal / Reduced / Absent — light touch: ___ / Pin prick: ___ / Vibration: ___ Level: ___",sBody)],
[Paragraph("<b>Motor function:</b>",sBold), Paragraph("Movement of toes: Present / Absent Foot dorsiflexion: Normal / Reduced / Absent",sBody)],
[Paragraph("<b>Border hyperaesthesia:</b>",sBold),Paragraph("Hyperaesthetic skin at edge of gangrene: Yes / No (S Das: 'On the border line of the gangrene the skin becomes hyperaesthetic')",sBody)],
[Paragraph("<b>Sensation in ulcer:</b>",sBold), Paragraph("Painful (arterial ulcer) / Painless (neuropathic) / Mildly painful (venous ulcer)",sBody)],
], w1=46*mm))
story.append(SP(1))
# Auscultation
story.append(subsec("F. AUSCULTATION (S Das — arterial bruits)"))
story.append(SP(0.5))
story.append(c2([
[Paragraph("<b>Carotid bruit:</b>",sBold), Paragraph("Absent / Present R / L (carotid stenosis)",sBody)],
[Paragraph("<b>Aortic bruit:</b>",sBold), Paragraph("Absent / Present (aortic aneurysm / stenosis)",sBody)],
[Paragraph("<b>Femoral bruit:</b>",sBold), Paragraph("Absent / Present R / L (iliac / femoral stenosis — S Das emphasises)",sBody)],
[Paragraph("<b>Popliteal bruit:</b>",sBold),Paragraph("Absent / Present R / L",sBody)],
[Paragraph("<b>AV fistula bruit:</b>",sBold),Paragraph("Absent / Continuous machinery murmur — site: ___ (Branham's sign — see tests)",sBody)],
], w1=42*mm))
story.append(SP(2))
# ─── SECTION 4: ULCER EXAMINATION ────────────────────────────────────────────
story.append(sec("ULCER EXAMINATION (S Das Chapter 5 — when present)", C_TEAL, "🔍"))
story.append(SP(1))
story.append(sdas_quote(
"'The first thing to do when examining an ulcer is to feel for the edge, floor and base.' — S Das"
))
story.append(SP(0.5))
ulc_rows = [
[Paragraph("<b>Site:</b>",sBold), Paragraph("Heel / Malleoli / Ball of foot / Toe tips / Between toes (S Das: 'minor ulceration in pressure areas')",sBody)],
[Paragraph("<b>Number:</b>",sBold), Paragraph("Single / Multiple",sBody)],
[Paragraph("<b>Size:</b>",sBold), Paragraph("___ cm × ___ cm",sBody)],
[Paragraph("<b>Shape:</b>",sBold), Paragraph("Round / Oval / Irregular",sBody)],
[Paragraph("<b>Edge:</b>",sBold), Paragraph("Punched out (arterial/neuropathic) / Sloping (venous/healing) / Undermined (TB) / Everted (carcinoma)",sBody)],
[Paragraph("<b>Floor:</b>",sBold), Paragraph("Pale/sloughy (arterial) / Granulation (venous/healing) / Bone visible (neuropathic, Wagner 3+)",sBody)],
[Paragraph("<b>Base:</b>",sBold), Paragraph("Mobile / Fixed (fixed = malignant change / bone involvement)",sBody)],
[Paragraph("<b>Discharge:</b>",sBold), Paragraph("None / Serous / Purulent / Haemoserous / Foul-smelling (anaerobic infection)",sBody)],
[Paragraph("<b>Surrounding skin:</b>",sBold), Paragraph("Pale + atrophic (arterial) / Pigmented + lipodermatosclerosis (venous) / Callus (neuropathic)",sBody)],
[Paragraph("<b>Pain:</b>",sBold), Paragraph("Painful (arterial) / Painless (neuropathic) / Mildly tender (venous)",sBody)],
[Paragraph("<b>Lymphatics:</b>",sBold), Paragraph("Lymphangitis: Yes/No Inguinal LN: enlarged / tender",sBody)],
]
story.append(c2(ulc_rows, w1=42*mm))
story.append(SP(2))
# ─── SECTION 5: SIGNS OF GANGRENE ────────────────────────────────────────────
story.append(sec("SIGNS OF GANGRENE (S Das 5 Classic Signs)", C_DKRED, "⚠"))
story.append(SP(1))
story.append(sdas_quote(
"'1. Change of colour — pale, bluish purple and finally black; 2. Loss of temperature; "
"3. Loss of sensation; 4. Loss of pulsation and 5. Loss of function.' — S Das, Chapter 6"
))
story.append(SP(0.5))
story.append(c2([
[Paragraph("<b>1. Change of colour:</b>",sBold), Paragraph("Pale / Bluish-purple / Black Extent: toes / foot / ankle / calf",sBody)],
[Paragraph("<b>2. Loss of temperature:</b>",sBold),Paragraph("Cold up to level: ___ Compare with contralateral limb",sBody)],
[Paragraph("<b>3. Loss of sensation:</b>",sBold), Paragraph("Reduced / Absent Level: ___",sBody)],
[Paragraph("<b>4. Loss of pulsation:</b>",sBold), Paragraph("Last palpable pulse: ___ (level of occlusion above this)",sBody)],
[Paragraph("<b>5. Loss of function:</b>",sBold), Paragraph("Unable to move toes: Yes/No Foot drop: Yes/No",sBody)],
[Paragraph("<b>Type:</b>",sBold), Paragraph("Dry (atherosclerosis — gradual, hard, mummified, clear demarcation) / Wet (embolism/infection — soft, oedematous, blebs, crepitus) / Gas (crepitation present)",sBody)],
[Paragraph("<b>Line of demarcation:</b>",sBold), Paragraph("Present / Absent — clear / gradual Level: ___",sBody)],
[Paragraph("<b>Pre-gangrene:</b>",sBold), Paragraph("S Das: 'Pregangrene — changes indicating blood supply is precarious; will soon be inadequate to keep tissue alive' Yes/No",sBody)],
], w1=46*mm))
story.append(SP(2))
# ─── SECTION 6: SPECIAL INVESTIGATIONS ──────────────────────────────────────
story.append(sec("SPECIAL INVESTIGATIONS (S Das sequence)", C_DARK, "🔬"))
story.append(SP(1))
story.append(info_box(
"<b>S Das numbered investigations for arterial disease (Chapter 6):</b> "
"Doppler ultrasound / Doppler ankle pressure → Oscillometry → Plethysmography → "
"Arteriography (DSA) → MRA / CTA → Duplex scanning. "
"S Das: 'Arteriography remains the investigation of choice for deciding on treatment options once the decision to treat has been made.'",
bg=C_LTGREEN, border=C_GREEN
))
story.append(SP(0.5))
inv_rows = [
["Investigation","S Das Description","Key Result"],
["Blood investigations","CBC, blood glucose, HbA1c, lipid profile, RFT, LFT, coagulation, blood culture","FBS: ___ / HbA1c: ___ / LDL: ___ / Creatinine: ___"],
["Doppler ankle pressure (ABI)","ABI = Ankle systolic ÷ Brachial systolic. 'In patients with true intermittent claudication the resting ABI will be LOW and will FALL further after exercise, taking several minutes to return to normal' (S Das)","R ABI: ___ / L ABI: ___ Normal: 1.0–1.3; Claudication: 0.6–0.9; Rest pain: 0.3–0.6; Tissue loss: <0.3"],
["Oscillometry","'Of particular value in detecting arterial pulsation at different levels. In embolism, sudden decrease at level of occlusion. In TAO: if no pulsation in leg = amputate thigh; if oscillation in leg = lower amputation' (S Das)","Level of absent oscillations: ___"],
["Plethysmography","'One of earliest methods of measuring blood flow. Venous outflow briefly arrested while allowing arterial inflow to measure volume change.' Segmental plethysmography: cuffs at thigh, calf, ankle at 65 mmHg","Arterial inflow: ___"],
["Duplex scanning","'Increasingly used to assess aortoiliac and femoropopliteal segments; with care and enthusiasm practically the whole peripheral arterial tree can be insonated' (S Das)","Lesion site: ___ / Stenosis: ___% / Occlusion: Y/N"],
["Arteriography (DSA)","S Das: 'Remains the investigation of choice for deciding on treatment options once decision to treat has been made. Gives outline of which part of arterial tree is active, enabling decision on treatment option'","Occlusion level: ___ / Distal run-off: ___"],
["CT Angiography (CTA)","3D reconstruction of arterial tree; shows calcification and stents; fast and widely available","Findings: ___"],
["MR Angiography (MRA)","No iodinated contrast; no radiation; ideal if contrast nephropathy risk or calcified vessels","Findings: ___"],
["X-ray limb","Calcification of arteries; osteomyelitis; gas in tissue (wet gangrene); periosteal reaction","Findings: ___"],
["Urine examination","Glycosuria (diabetes); proteinuria (renal involvement)","Glucose: ___ / Protein: ___"],
["ECG + Echocardiogram","Cardiac source of embolism — AF, LV thrombus, valvular HD, EF (operative risk)","Rhythm: ___ / EF: ___% / Thrombus: Y/N"],
["Brown's vasomotor index","Rise skin temp – Rise mouth temp ÷ Rise mouth temp. 'Operation advisable only when index is 3.5 or more' (S Das — for lumbar sympathectomy decision)","Index: ___"],
]
inv_t_rows = [[Paragraph(c, ps(f"inv{i}{j}", fontSize=6.8,
fontName="Helvetica-Bold" if i==0 else ("Helvetica-Bold" if j==0 else "Helvetica"),
textColor=C_WHITE if i==0 else (C_DARK if j==0 else C_DGREY), leading=8)) for j,c in enumerate(r)]
for i,r in enumerate(inv_rows)]
inv_t = Table(inv_t_rows, colWidths=[30*mm, 83*mm, doc.width-113*mm],
style=TableStyle([
("BACKGROUND",(0,0),(-1,0),C_DARK),
("FONTSIZE",(0,0),(-1,-1),6.8),
("TOPPADDING",(0,0),(-1,-1),2),("BOTTOMPADDING",(0,0),(-1,-1),2),
("LEFTPADDING",(0,0),(-1,-1),3),("RIGHTPADDING",(0,0),(-1,-1),3),
("GRID",(0,0),(-1,-1),0.3,C_MGREY),("VALIGN",(0,0),(-1,-1),"TOP"),
] + [("BACKGROUND",(0,i),(-1,i),C_LGREY) for i in range(1,len(inv_rows),2)])
)
story.append(inv_t)
story.append(SP(2))
# ─── SECTION 7: CAUSES OF ISCHAEMIA ──────────────────────────────────────────
story.append(sec("CAUSES OF ISCHAEMIA (S Das Classification)", C_DARK, "📊"))
story.append(SP(1))
story.append(sdas_quote(
"'Causes of ischaemia can be broadly classified as: Large artery occlusion: (1) Atherosclerosis (Chronic). "
"(2) Embolism (Acute). | Small artery occlusion: (1) Buerger's disease. (2) Raynaud's disease. "
"(3) Embolism. (4) Diabetes.' — S Das, Chapter 6"
))
story.append(SP(0.5))
story.append(c3(
["Cause","Key Features (S Das)","Distinguishing Points"],
[
["Atherosclerosis","Old age, males > females, bilateral often, gradual onset, claudication → rest pain → gangrene","ABI low; calcified vessels on X-ray; DRY gangrene; risk factors: smoking, DM, HTN, dyslipidaemia"],
["Embolism (large artery)","Sudden onset, severe pain radiating down course of artery, young may be affected, usually unilateral","Source: AF, MI, valvular HD, AAA. Level: bifurcation sites (aortic, iliac, femoral). Pulse traceable to obstruction — tender swelling — lost below"],
["Buerger's disease (TAO)","Men 20–40 yrs, heavy smokers, bilateral, lower limb, superficial phlebitis history, VEINS also thrombosed","Small artery occlusion; normal ABI at knee; 'not only arteries but also veins are thrombosed' (S Das)"],
["Raynaud's disease","Young women, upper limb, bilateral, 3 stages: pallor → cyanosis → recovery (spasm → stasis → recovery)","Triggered by cold/emotion; no fixed occlusion initially; local gangrene at fingertips late"],
["Diabetic arteriopathy","Middle age, unilateral or bilateral, may be painless (neuropathy), minor trauma precipitates (nail paring)","Neuropathy + ischaemia; neuropathic ulcers at pressure points; normal proximal pulses possible"],
["Acute limb ischaemia","Sudden: 6 Ps — Pain, Pallor, Pulselessness, Paraesthesia, Paralysis, Perishing cold","Embolism vs thrombosis; >6 hrs = irreversible; Fogarty embolectomy or thrombolysis"],
],
w1=32*mm, w2=72*mm, hbg=C_GREEN
))
story.append(SP(2))
# ─── SECTION 8: DIAGNOSIS FORMAT ─────────────────────────────────────────────
story.append(sec("DIAGNOSIS (S Das Format)", C_DARK, "🎯"))
story.append(SP(1))
story.append(c2([
[Paragraph("<b>Clinical diagnosis:</b>",sBold), Paragraph("_"*55,sBody)],
[Paragraph("<b>Type of ischaemia:</b>",sBold), Paragraph("Acute / Subacute / Chronic Critical / Non-critical",sBody)],
[Paragraph("<b>Cause (S Das):</b>",sBold), Paragraph("Atherosclerosis / Embolism / Buerger's / Raynaud's / Diabetic / Other: ___",sBody)],
[Paragraph("<b>Limb / site:</b>",sBold), Paragraph("R / L Upper / Lower Level of occlusion: ___",sBody)],
[Paragraph("<b>Stage:</b>",sBold), Paragraph("Claudication / Rest pain / Tissue loss — Fontaine: I / IIa / IIb / III / IVa / IVb",sBody)],
[Paragraph("<b>Complications:</b>",sBold), Paragraph("Gangrene (dry/wet/gas) / Infection / Septicaemia / Osteomyelitis / Renal failure / Cardiac",sBody)],
], w1=45*mm))
story.append(SP(2))
# ─── SECTION 9: TREATMENT OUTLINE ────────────────────────────────────────────
story.append(sec("TREATMENT OUTLINE (S Das Principles)", C_GREEN, "⚕"))
story.append(SP(1))
story.append(cb_list([
"Conservative: Smoking cessation (MOST important), risk factor control, antiplatelet (Aspirin/Clopidogrel), statin, ACE-I",
"Exercise rehabilitation (supervised walking)",
"Vasodilators: Cilostazol / Naftidrofuryl (claudication)",
"Foot care: daily inspection, appropriate footwear, podiatry",
"Treat infection: antibiotics (IV for wet gangrene) — culture-directed",
"Analgesics for rest pain (tramadol / opioids)",
"Lumbar sympathectomy: Brown's vasomotor index ≥3.5; minor tissue loss; inoperable",
"Endovascular: PTA ± stent (aortoiliac/SFA) / catheter-directed thrombolysis (CDT)",
"Fogarty embolectomy: acute embolism <6 hrs — saddle/femoral",
"Bypass surgery: femoropopliteal (GSV preferred) / femoro-distal / aortobifemoral",
"Endarterectomy: CFA or localised short segment disease",
"Amputation: level decision by oscillometry/TcPO₂/ABI — digital/ray/TMA/BKA/AKA",
], cols=2))
story.append(SP(2))
# ─── SECTION 10: DOCUMENTATION TEMPLATE ──────────────────────────────────────
story.append(sec("BEDSIDE DOCUMENTATION (S Das Format)", C_DARK, "📝"))
story.append(SP(1))
doc_txt = """<b>HISTORY (S Das 13 points):</b>
1. Age/Sex: ___ 2. Limbs: R/L/Bilateral, Upper/Lower 3. Bilateral/Unilateral: ___ 4. Mode of onset: Gradual/Sudden
5. Pain: Claudication dist ___ m / Rest pain Y/N, site ___, relieved by dependency Y/N
6. Colour change: ___ 7. Paraesthesia: Y/N 8. Superficial phlebitis: Y/N
9. Other arteries: Cardiac/Cerebral/Abdominal involvement: ___ 10. Impotence: Y/N
11. Past history: ___ 12. Smoking: ___ pack-yrs 13. Family history: ___
<b>CONSTITUTIONAL EXAMINATION:</b> BP: ___ (R) / ___ (L). PR: ___ — regular/AF. Temp: ___°F. Pallor/Icterus/Clubbing/LN/Oedema: ___
Cardiac: murmur/AF: ___ Abdominal pulsatile mass: Y/N
<b>LOCAL EXAMINATION — INSPECTION:</b>
Colour: ___ Trophic changes: skin/nails/hair ___ Gangrene: Dry/Wet — toes/foot/level ___ Ulcer: site/size ___
<b>BUERGER'S TEST:</b> Positive / Negative Buerger's angle: R ___° / L ___° Dependency rubor: Y/N
<b>PALPATION:</b> Temp: cold to level ___ CRT: ___ sec Venous filling time: ___ sec
Sensation: Normal/Reduced/Absent to ___ Motor: toes moving Y/N Line of demarcation: ___
Gangrene type: Dry/Wet Crepitation: Y/N
<b>PULSES:</b> DP: R___/L___ PT: R___/L___ Popliteal: R___/L___ Femoral: R___/L___ Carotid: R___/L___
Bruits: Femoral R___/L___ Aortic: ___ Carotid: ___
<b>SPECIAL TESTS:</b> Allen's: Radial ___/Ulnar ___ Branham's: ___ Tourniquet: ___
<b>ABI:</b> R: ___ L: ___ Stage (Fontaine): I / IIa / IIb / III / IVa / IVb
<b>DIAGNOSIS (S Das):</b> ___________________________________________________________
Cause: Atherosclerosis / Embolism / Buerger's / Raynaud's / Diabetic / Other: ___
<b>PLAN:</b> Conservative / Endovascular / Fogarty embolectomy / Bypass / Sympathectomy / Amputation (level: ___)"""
story.append(Table([[Paragraph(doc_txt,ps("dt",fontSize=7,fontName="Helvetica",textColor=C_DGREY,leading=11))]],
colWidths=[doc.width],
style=TableStyle([
("BACKGROUND",(0,0),(-1,-1),C_LTGREEN),("BOX",(0,0),(-1,-1),0.7,C_GREEN),
("TOPPADDING",(0,0),(-1,-1),6),("BOTTOMPADDING",(0,0),(-1,-1),6),
("LEFTPADDING",(0,0),(-1,-1),8),("RIGHTPADDING",(0,0),(-1,-1),8),
])
))
story.append(SP(3))
story.append(HRFlowable(width="100%",thickness=0.4,color=C_MGREY))
story.append(SP(1))
story.append(Paragraph(
"Reference: S Das — A Manual on Clinical Surgery, 13th Edition | Chapter 6: Ischaemia of Limb and Gangrene | "
"S Das Publications, Kolkata | For educational use only.",
sFooter))
doc.build(story, onFirstPage=page_cb, onLaterPages=page_cb)
print(f"PDF created: {OUTPUT}")
cd /home/daytona/workspace/resp-checklist && python3 sdas_pvd.py 2>&1
ls -lh /home/daytona/workspace/resp-checklist/SDas_PVD_Proforma.pdf
SDas PVD Proforma
PDF Document · PDF
| Section | S Das Content |
|---|---|
| Identification | Age/sex with diagnostic pointers (Atherosclerosis = old men; Buerger's = 20-40 yr men; Raynaud's = young women) |
| History — 13 numbered points | S Das's own numbered sequence: Age/Sex → Limbs affected → Bilateral/Unilateral → Mode of onset → Pain (claudication + rest pain) → Colour change → Paraesthesia → Superficial phlebitis → Other arteries → Impotence (internal iliac occlusion) → Past history → Personal history (smoking) → Family history |
| Intermittent claudication | S Das's definition verbatim ("P-substance accumulation") + site by level of occlusion (foot/calf/thigh/buttock) |
| Constitutional examination | BP both arms, AF detection (embolic source), AAA screening |
| Local — Inspection | S Das's 7 inspection points including trophic changes, 5 signs of gangrene |
| Buerger's postural test | Full S Das quotation + 3-step procedure + Buerger's angle recording (< 30° = severe) |
| Palpation (S Das 1-15) | Temperature, CRT, venous filling time, tissue pressure, line of demarcation, sensation, motor, varicosities |
| Pulse examination | All 12 arteries with S Das's exact anatomical landmarks (including "dorsalis pedis absent in 10% normally") |
| Special tests (1-11) | Allen's, Branham's, Trendelenburg, Elevated arms, Costoclavicular, Hyperabduction, Gangrenous area assessment |
| Special investigations | Doppler/ABI with S Das interpretation, Oscillometry (amputation level decision), Plethysmography, DSA, Brown's vasomotor index (≥3.5 for sympathectomy) |
| Causes of ischaemia | S Das's exact table: Large artery (atherosclerosis/embolism) vs Small artery (Buerger's/Raynaud's/diabetes) |
| 5 Signs of gangrene | S Das verbatim: colour change → temperature → sensation → pulsation → function + Dry vs Wet differentiation |
| Treatment outline | Conservative → sympathectomy (Brown's index) → endovascular → Fogarty → bypass → amputation level |
| Bedside documentation | S Das 13-point format ready to fill at bedside |