I want to learn see images with little explanation of this topic for my spotting exam Types of squint Chadocks sign Atrial fibrillation graph ape hand Scoliometer 90 -90 SLR test Serratus anterior MMT Grover's sign RA deformity Pathological gait Pathological reflex Special test - LSST NCV - r wave graph NCV - condition wise graph Orthosis Prosthesis
types of squint strabismus clinical photos esotropia exotropia hypertropia
Chaddock sign elicitation clinical photo foot
atrial fibrillation ECG graph irregularly irregular waveform
ape hand deformity median nerve palsy clinical photo
scoliometer angle trunk rotation ATR measurement clinical
Make a visual study PDF for quick revision
ls /home/daytona/workspace/ 2>/dev/null && mkdir -p /home/daytona/workspace/spotting-exam-pdf
node /home/daytona/skills/shared/scripts/fetch_images.js \ "https://2020sim.com/wp-content/uploads/2021/02/strabismus-diagram.png" \ "https://www.dranishagupta.com/wp-content/uploads/2023/10/What-Are-The-Different-Types-of-Squints-Dr-Anisha-Gupta-Eye-Specialist-in-Delhi-Excel-Eye-01.jpg" \ "https://wikism.org/w/images/thumb/b/b4/Chaddock_sign.jpeg/1200px-Chaddock_sign.jpeg" \ "https://litfl.com/wp-content/uploads/2025/05/Chaddock-sign-positive-1911.png" \ "https://drlomonacocardiologist.co.uk/wp-content/uploads/2026/06/Atrial-Fibrillation-ECG-Wave-1-1024x402.webp" \ "https://www.researchgate.net/profile/Ashwin-Parchani-2/publication/360858934/figure/fig4/AS:1159993566994435@1653575426576/ECG-showing-irregularly-irregular-rhythm-suggestive-of-atrial-fibrillation.png" \ "https://media-us.amboss.com/media/thumbs/big_60c05ec607ece.jpg" \ "https://medical-junction.com/wp-content/uploads/2022/03/median-nerve-lesion.jpg" \ 2>&1 | head -5
node /home/daytona/skills/shared/scripts/fetch_images.js \ "https://2020sim.com/wp-content/uploads/2021/02/strabismus-diagram.png" \ 2>&1 | python3 -c "import sys,json; d=json.load(sys.stdin); print('ok' if d[0].get('base64') else d[0].get('error'))"
~/spotting-exam-pdf/build_pdf.py
#!/usr/bin/env python3
"""
Spotting Exam Quick Revision PDF
16 topics with images, color-coded sections, tables
"""
import json, subprocess, base64, os, io
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import cm, 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, Image, PageBreak, HRFlowable,
KeepTogether)
from reportlab.pdfgen import canvas
from PIL import Image as PILImage
OUT = "/home/daytona/workspace/spotting-exam-pdf/Spotting_Exam_Visual_Guide.pdf"
IMG_DIR = "/home/daytona/workspace/spotting-exam-pdf/imgs"
os.makedirs(IMG_DIR, exist_ok=True)
W, H = A4 # 595 x 842 pts
# ── colour palette ────────────────────────────────────────────────────────────
C_TITLE = colors.HexColor("#1a237e") # deep indigo
C_HEAD = colors.HexColor("#283593") # indigo header bar
C_HEAD2 = colors.HexColor("#1565c0") # blue
C_ACCENT = colors.HexColor("#e3f2fd") # light blue bg
C_ROW1 = colors.HexColor("#e8eaf6")
C_ROW2 = colors.white
C_LINE = colors.HexColor("#5c6bc0")
C_WARN = colors.HexColor("#fff3e0")
C_WARN_BD = colors.HexColor("#ef6c00")
C_GREEN = colors.HexColor("#e8f5e9")
C_GREEN_BD= colors.HexColor("#2e7d32")
C_GREY = colors.HexColor("#f5f5f5")
# ── styles ────────────────────────────────────────────────────────────────────
styles = getSampleStyleSheet()
def S(name, **kw):
return ParagraphStyle(name, **kw)
sTitle = S("sTitle", fontSize=22, textColor=C_TITLE, fontName="Helvetica-Bold",
spaceAfter=4, alignment=TA_CENTER)
sSub = S("sSub", fontSize=11, textColor=colors.HexColor("#455a64"),
fontName="Helvetica", alignment=TA_CENTER, spaceAfter=14)
sH1 = S("sH1", fontSize=13, textColor=colors.white,
fontName="Helvetica-Bold", spaceBefore=2, spaceAfter=2, leading=16)
sH2 = S("sH2", fontSize=11, textColor=C_HEAD2,
fontName="Helvetica-Bold", spaceBefore=6, spaceAfter=3)
sBody = S("sBody", fontSize=9, textColor=colors.HexColor("#212121"),
fontName="Helvetica", spaceAfter=4, leading=13)
sBold = S("sBold", fontSize=9, textColor=colors.HexColor("#212121"),
fontName="Helvetica-Bold", spaceAfter=2, leading=13)
sSmall = S("sSmall", fontSize=8, textColor=colors.HexColor("#546e7a"),
fontName="Helvetica", spaceAfter=2, leading=11)
sCaption = S("sCaption", fontSize=7.5, textColor=colors.HexColor("#607d8b"),
fontName="Helvetica-Oblique", alignment=TA_CENTER, spaceAfter=4)
sBullet = S("sBullet", fontSize=9, textColor=colors.HexColor("#212121"),
fontName="Helvetica", leftIndent=12, spaceAfter=2, leading=13,
bulletText="•")
# ── helpers ───────────────────────────────────────────────────────────────────
def fetch_img(url, fname):
path = os.path.join(IMG_DIR, fname)
if os.path.exists(path):
return path
try:
result = json.loads(subprocess.check_output(
["node", "/home/daytona/skills/shared/scripts/fetch_images.js", url],
timeout=25))
b64 = result[0].get("base64")
if b64:
raw = base64.b64decode(b64.split(",")[1])
with open(path, "wb") as f:
f.write(raw)
return path
except Exception as e:
print(f" WARN: could not fetch {url}: {e}")
return None
def rl_image(path, max_w, max_h):
"""Return a ReportLab Image flowable, resized to fit."""
if not path or not os.path.exists(path):
return None
try:
pil = PILImage.open(path)
iw, ih = pil.size
scale = min(max_w / iw, max_h / ih, 1.0)
return Image(path, width=iw*scale, height=ih*scale)
except Exception as e:
print(f" WARN: rl_image {path}: {e}")
return None
def section_header(num, title, color=C_HEAD):
"""Coloured header bar for each topic."""
data = [[Paragraph(f"{num}. {title}", sH1)]]
t = Table(data, colWidths=[W - 4*cm])
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), color),
("TOPPADDING", (0,0), (-1,-1), 6),
("BOTTOMPADDING", (0,0), (-1,-1), 6),
("LEFTPADDING", (0,0), (-1,-1), 10),
("RIGHTPADDING", (0,0), (-1,-1), 10),
("ROUNDEDCORNERS", [4]),
]))
return t
def key_box(items, bg=C_ACCENT, border=C_LINE):
"""Shaded key-points box."""
rows = [[Paragraph(f"<b>KEY POINTS</b>", sBold)]]
for item in items:
rows.append([Paragraph(f"• {item}", sBody)])
t = Table(rows, colWidths=[W - 4*cm])
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), bg),
("LINEABOVE", (0,0), (-1,0), 1, border),
("LINEBELOW", (0,-1),(-1,-1), 1, border),
("LINEBEFORE", (0,0), (0,-1), 3, border),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 10),
]))
return t
def fancy_table(headers, rows, col_widths=None):
data = [headers] + rows
if col_widths is None:
n = len(headers)
col_widths = [(W - 4*cm) / n] * n
t = Table(data, colWidths=col_widths, repeatRows=1)
style = [
("BACKGROUND", (0,0), (-1,0), C_HEAD),
("TEXTCOLOR", (0,0), (-1,0), colors.white),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("FONTSIZE", (0,0), (-1,-1), 8.5),
("FONTNAME", (0,1), (-1,-1), "Helvetica"),
("ROWBACKGROUNDS",(0,1), (-1,-1), [C_ROW1, C_ROW2]),
("GRID", (0,0), (-1,-1), 0.4, colors.HexColor("#bdbdbd")),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 6),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
]
t.setStyle(TableStyle(style))
return t
def two_col(left_items, right_items):
"""Two-column layout using a 2-cell table."""
ldata = [[x] for x in left_items]
rdata = [[x] for x in right_items]
cw = (W - 4*cm) / 2 - 5
rows = []
maxr = max(len(ldata), len(rdata))
for i in range(maxr):
l = ldata[i][0] if i < len(ldata) else Paragraph("", sBody)
r = rdata[i][0] if i < len(rdata) else Paragraph("", sBody)
rows.append([l, r])
t = Table(rows, colWidths=[cw, cw])
t.setStyle(TableStyle([
("VALIGN", (0,0),(-1,-1),"TOP"),
("LEFTPADDING", (0,0),(-1,-1), 4),
("RIGHTPADDING",(0,0),(-1,-1), 4),
]))
return t
SP = lambda n=1: Spacer(1, n*4)
HR = lambda: HRFlowable(width="100%", thickness=0.5, color=C_LINE, spaceAfter=6)
# ── image URLs ────────────────────────────────────────────────────────────────
IMGS = {
"squint_diagram": ("https://2020sim.com/wp-content/uploads/2021/02/strabismus-diagram.png", "squint_diagram.png"),
"squint_types": ("https://www.dranishagupta.com/wp-content/uploads/2023/10/What-Are-The-Different-Types-of-Squints-Dr-Anisha-Gupta-Eye-Specialist-in-Delhi-Excel-Eye-01.jpg", "squint_types.jpg"),
"chaddock1": ("https://wikism.org/w/images/thumb/b/b4/Chaddock_sign.jpeg/1200px-Chaddock_sign.jpeg", "chaddock1.jpg"),
"chaddock2": ("https://litfl.com/wp-content/uploads/2025/05/Chaddock-sign-positive-1911.png", "chaddock2.png"),
"af_ecg1": ("https://www.researchgate.net/profile/Ashwin-Parchani-2/publication/360858934/figure/fig4/AS:1159993566994435@1653575426576/ECG-showing-irregularly-irregular-rhythm-suggestive-of-atrial-fibrillation.png", "af_ecg1.png"),
"af_ecg2": ("https://www.aclsmedicaltraining.com/sites/acls/files/inline-images/ACLS-Figure-30.jpg", "af_ecg2.jpg"),
"ape_hand1": ("https://media-us.amboss.com/media/thumbs/big_60c05ec607ece.jpg", "ape_hand1.jpg"),
"ape_hand2": ("https://medical-junction.com/wp-content/uploads/2022/03/median-nerve-lesion.jpg", "ape_hand2.jpg"),
"scoliometer1": ("https://omirouphysiotherapy.com/wp-content/uploads/2018/08/afp20140201p193-f2-1024x875.jpg", "scoliometer1.jpg"),
"scoliometer2": ("https://osteopilates.com/wp-content/uploads/2020/04/scoliometer.jpg", "scoliometer2.jpg"),
"slr90": ("https://brownmedpedsresidency.org/wp-content/uploads/2022/06/Screen-Shot-2022-06-21-at-9.40.39-PM.png", "slr90.png"),
"serratus": ("https://cdn.cemah.net/wp-content/uploads/sites/90/2018/10/HPIM2059.jpg", "serratus.jpg"),
"ra_hand": ("https://cdn.orris.care/cdss_images/f348778ff4ee835fe29f3a1694dd3a92db1cb3b7a8279044e5d8a2efaa88cfe5.png", "ra_hand.png"),
"boutonniere": ("https://cdn.orris.care/cdss_images/e5dc32fb4d19ef05290bbccf94189221f44ff022890880a2ad9a4cccf191129e.png", "boutonniere.png"),
"swan_neck": ("https://cdn.orris.care/cdss_images/ae5bdfa13ac345d70aee9cea79372b74b4a86fa8b49d8567c9474c6d88e910a8.png", "swan_neck.png"),
"ulnar_drift": ("https://cdn.orris.care/cdss_images/7f326f12ef9928b34aa826472cad9bd48dbf00acac9b2e93d0e0221a74c8525f.png", "ulnar_drift.png"),
"ncv_waveform": ("https://d1j63owfs0b5j3.cloudfront.net/term/images/median-neuropathy-2226.jpg", "ncv_waveform.jpg"),
}
print("Fetching images...")
paths = {}
for key, (url, fname) in IMGS.items():
print(f" {key}...", end=" ", flush=True)
p = fetch_img(url, fname)
paths[key] = p
print("ok" if p else "FAILED")
# ── cover page number ─────────────────────────────────────────────────────────
def add_page_number(canvas_obj, doc):
canvas_obj.saveState()
canvas_obj.setFont("Helvetica", 8)
canvas_obj.setFillColor(colors.HexColor("#90a4ae"))
canvas_obj.drawRightString(W - 2*cm, 1.2*cm, f"Page {doc.page}")
canvas_obj.drawString(2*cm, 1.2*cm, "Spotting Exam - Visual Quick Revision Guide")
canvas_obj.restoreState()
# ── build story ───────────────────────────────────────────────────────────────
story = []
# ===== COVER =====
story += [
Spacer(1, 2*cm),
Paragraph("SPOTTING EXAM", sTitle),
Paragraph("Visual Quick Revision Guide", sSub),
HRFlowable(width="60%", thickness=2, color=C_LINE, spaceAfter=10),
Paragraph("16 Topics | Images + Key Points + Tables", sSub),
Spacer(1, 0.5*cm),
]
topics_list = [
"1. Types of Squint", "2. Chaddock's Sign", "3. Atrial Fibrillation ECG",
"4. Ape Hand", "5. Scoliometer", "6. 90-90 SLR Test",
"7. Serratus Anterior MMT", "8. Grover's Sign", "9. RA Hand Deformity",
"10. Pathological Gait", "11. Pathological Reflexes", "12. LSST Special Tests",
"13. NCV - R Wave Graph", "14. NCV - Condition-wise", "15. Orthosis", "16. Prosthesis"
]
toc_rows = []
for i in range(0, len(topics_list), 2):
l = Paragraph(topics_list[i], sBody)
r = Paragraph(topics_list[i+1] if i+1 < len(topics_list) else "", sBody)
toc_rows.append([l, r])
cw2 = (W - 4*cm) / 2
toc_t = Table(toc_rows, colWidths=[cw2, cw2])
toc_t.setStyle(TableStyle([
("BACKGROUND", (0,0),(-1,-1), C_ACCENT),
("GRID", (0,0),(-1,-1), 0.4, C_LINE),
("LEFTPADDING", (0,0),(-1,-1), 10),
("TOPPADDING", (0,0),(-1,-1), 5),
("BOTTOMPADDING", (0,0),(-1,-1), 5),
("FONTSIZE", (0,0),(-1,-1), 9),
]))
story.append(toc_t)
story.append(PageBreak())
# helper: add image row (1 or 2 side-by-side)
def add_imgs(keys, captions, max_w_each, max_h):
imgs = [rl_image(paths.get(k), max_w_each, max_h) for k in keys]
row = [i for i in imgs if i]
caps = [Paragraph(c, sCaption) for c in captions]
if not row:
return
if len(row) == 1:
story.append(row[0])
story.append(caps[0])
else:
t = Table([[row[0], row[1]]], colWidths=[(W-4*cm)/2]*2)
t.setStyle(TableStyle([("ALIGN",(0,0),(-1,-1),"CENTER"),
("VALIGN",(0,0),(-1,-1),"MIDDLE")]))
story.append(t)
cap_t = Table([[caps[0], caps[1]]], colWidths=[(W-4*cm)/2]*2)
cap_t.setStyle(TableStyle([("ALIGN",(0,0),(-1,-1),"CENTER")]))
story.append(cap_t)
# ===== 1. TYPES OF SQUINT =====
story += [section_header(1, "Types of Squint (Strabismus)"), SP(2)]
add_imgs(["squint_diagram","squint_types"],
["Strabismus directions diagram","Clinical types of squint"],
(W-4*cm)/2-5, 130)
story += [SP(2),
fancy_table(
[Paragraph("<b>Type</b>",sH1), Paragraph("<b>Eye Direction</b>",sH1), Paragraph("<b>Key Fact</b>",sH1)],
[
[Paragraph("Esotropia",sBody), Paragraph("Inward (convergent)",sBody), Paragraph("Most common in children",sBody)],
[Paragraph("Exotropia",sBody), Paragraph("Outward (divergent)",sBody), Paragraph("Often intermittent",sBody)],
[Paragraph("Hypertropia",sBody), Paragraph("Upward",sBody), Paragraph("One eye higher than other",sBody)],
[Paragraph("Hypotropia",sBody), Paragraph("Downward",sBody), Paragraph("One eye lower than other",sBody)],
[Paragraph("Paralytic",sBody), Paragraph("Any direction",sBody), Paragraph("CN III/IV/VI palsy",sBody)],
], [(W-4*cm)*0.25, (W-4*cm)*0.35, (W-4*cm)*0.40]),
SP(2),
key_box(["Cover test - covered eye moves to fix = manifest squint",
"Hirschberg test - corneal light reflex asymmetry",
"Pseudo-squint: wide epicanthal folds (no true deviation)",
"Paralytic squint: diplopia, limited movement, head tilt"]),
PageBreak()
]
# ===== 2. CHADDOCK'S SIGN =====
story += [section_header(2, "Chaddock's Sign"), SP(2)]
add_imgs(["chaddock1","chaddock2"],
["Stroke below lateral malleolus","Positive response: toe dorsiflexion"],
(W-4*cm)/2-5, 140)
story += [SP(2),
key_box(["STIMULUS: Stroke lateral aspect of foot below lateral malleolus (heel to little toe)",
"POSITIVE: Dorsiflexion of great toe +/- fanning of other toes",
"INDICATES: Upper Motor Neuron (UMN) lesion",
"Same significance as Babinski sign - both test pyramidal tract integrity",
"Normal adults: Plantar flexion (downward) of toes"]),
SP(2),
fancy_table(
[Paragraph("<b>Babinski Equivalents</b>",sH1), Paragraph("<b>Stimulus Site</b>",sH1)],
[
[Paragraph("Babinski",sBody), Paragraph("Lateral sole of foot",sBody)],
[Paragraph("Chaddock",sBody), Paragraph("Lateral foot below malleolus",sBody)],
[Paragraph("Oppenheim",sBody), Paragraph("Stroke anterior tibia downward",sBody)],
[Paragraph("Gordon",sBody), Paragraph("Squeeze calf muscle",sBody)],
[Paragraph("Schafer",sBody), Paragraph("Squeeze Achilles tendon",sBody)],
], [(W-4*cm)*0.45, (W-4*cm)*0.55]),
PageBreak()
]
# ===== 3. ATRIAL FIBRILLATION ECG =====
story += [section_header(3, "Atrial Fibrillation - ECG Graph"), SP(2)]
add_imgs(["af_ecg1","af_ecg2"],
["AF: irregularly irregular RR, absent P waves","AF with rapid ventricular response"],
(W-4*cm)/2-5, 130)
story += [SP(2),
key_box(["NO P waves (fibrillatory baseline - f waves, 350-600/min)",
"Irregularly IRREGULAR RR intervals (hallmark)",
"Narrow QRS complexes (unless aberrant conduction/WPW)",
"Ventricular rate: 100-160/min (uncontrolled); <100 (controlled)",
"Coarse vs. fine fibrillation based on f-wave amplitude"]),
SP(2),
fancy_table(
[Paragraph("<b>Feature</b>",sH1), Paragraph("<b>AF</b>",sH1), Paragraph("<b>Atrial Flutter</b>",sH1)],
[
[Paragraph("P waves",sBody), Paragraph("Absent (f-waves)",sBody), Paragraph("Sawtooth F-waves (~300/min)",sBody)],
[Paragraph("Rhythm",sBody), Paragraph("Irregularly irregular",sBody), Paragraph("Regular (2:1, 3:1, 4:1 block)",sBody)],
[Paragraph("Ventricular rate",sBody), Paragraph("Variable",sBody), Paragraph("150/min (2:1 most common)",sBody)],
], [(W-4*cm)*0.3, (W-4*cm)*0.35, (W-4*cm)*0.35]),
PageBreak()
]
# ===== 4. APE HAND =====
story += [section_header(4, "Ape Hand (Median Nerve Palsy)"), SP(2)]
add_imgs(["ape_hand1","ape_hand2"],
["Flattened thenar, adducted thumb","Median nerve palsy - ape hand appearance"],
(W-4*cm)/2-5, 150)
story += [SP(2),
key_box(["Thumb is ADDUCTED, EXTENDED, cannot OPPOSE (flattened thenar eminence)",
"Caused by median nerve injury (at wrist or proximal)",
"Wasted thenar muscles: opponens pollicis, abductor pollicis brevis, flexor pollicis brevis (superficial head)",
"Cannot make 'OK sign' or pinch",
"Carpal tunnel syndrome = most common cause of distal median nerve palsy",
"High lesion (elbow): also loses wrist flexion, pronation, FDS/FDP (radial half)"]),
SP(2),
fancy_table(
[Paragraph("<b>Median Nerve</b>",sH1), Paragraph("<b>Radial Nerve</b>",sH1), Paragraph("<b>Ulnar Nerve</b>",sH1)],
[
[Paragraph("Ape hand",sBody), Paragraph("Wrist drop",sBody), Paragraph("Claw hand (ring+little)",sBody)],
[Paragraph("Thenar wasting",sBody), Paragraph("Finger drop",sBody), Paragraph("Hypothenar wasting",sBody)],
[Paragraph("Loss of opposition",sBody), Paragraph("Loss of wrist extension",sBody), Paragraph("Loss of intrinsics",sBody)],
], [(W-4*cm)/3]*3),
PageBreak()
]
# ===== 5. SCOLIOMETER =====
story += [section_header(5, "Scoliometer"), SP(2)]
add_imgs(["scoliometer1","scoliometer2"],
["Scoliometer on rib hump during Adam's test","ATR measurement with scoliometer"],
(W-4*cm)/2-5, 150)
story += [SP(2),
key_box(["Used during Adam's Forward Bending Test",
"Measures Angle of Trunk Rotation (ATR)",
"ATR >= 7 degrees = refer for spinal X-ray (Cobb angle)",
"Placed at APEX of rib hump perpendicular to spine",
"Reliable to within 3 degrees",
"Scoliosis screening in schools: ATR >= 5-7 = refer"]),
SP(2),
fancy_table(
[Paragraph("<b>ATR (degrees)</b>",sH1), Paragraph("<b>Action</b>",sH1)],
[
[Paragraph("< 5",sBody), Paragraph("Normal, routine follow-up",sBody)],
[Paragraph("5 - 7",sBody), Paragraph("Monitor, repeat in 6 months",sBody)],
[Paragraph(">= 7",sBody), Paragraph("Refer for X-ray (Cobb angle measurement)",sBody)],
], [(W-4*cm)*0.3, (W-4*cm)*0.7]),
PageBreak()
]
# ===== 6. 90-90 SLR TEST =====
story += [section_header(6, "90-90 Straight Leg Raise Test"), SP(2)]
img_slr = rl_image(paths.get("slr90"), W-4*cm-20, 160)
if img_slr:
story.append(img_slr)
story.append(Paragraph("90-90 SLR: Hip and knee each flexed to 90 degrees, patient actively extends knee", sCaption))
story += [SP(2),
key_box(["Position: Supine, hip flexed to 90 degrees, knee flexed to 90 degrees",
"Patient ACTIVELY extends the knee (or examiner passively extends)",
"POSITIVE (tight hamstrings): Cannot extend knee to within 20 degrees of full extension",
"Normal: Popliteal angle < 20 degrees (near-full extension achieved)",
"Uses: Hamstring tightness assessment, scoliosis screening, pediatric evaluation",
"Also used in spine rehab to isolate hamstrings from lumbar flexion"]),
SP(2),
fancy_table(
[Paragraph("<b>Popliteal Angle</b>",sH1), Paragraph("<b>Interpretation</b>",sH1)],
[
[Paragraph("0-20 degrees", sBody), Paragraph("Normal hamstring flexibility",sBody)],
[Paragraph("20-45 degrees",sBody), Paragraph("Mild tightness",sBody)],
[Paragraph("> 45 degrees", sBody), Paragraph("Significant hamstring tightness - clinically relevant",sBody)],
], [(W-4*cm)*0.35, (W-4*cm)*0.65]),
PageBreak()
]
# ===== 7. SERRATUS ANTERIOR MMT =====
story += [section_header(7, "Serratus Anterior MMT"), SP(2)]
img_ser = rl_image(paths.get("serratus"), W-4*cm-20, 160)
if img_ser:
story.append(img_ser)
story.append(Paragraph("Forward shoulder thrust / wall push test for serratus anterior; winging = weakness", sCaption))
story += [SP(2),
key_box(["Nerve: Long thoracic nerve (C5, C6, C7)",
"Action: Protracts scapula, holds medial border against chest wall",
"MMT Test: Patient pushes against wall or forward shoulder thrust",
"Positive (Grade 0-2): WINGING of scapula (medial border lifts off chest wall)",
"Grade 3: Can protract against gravity, Grade 4: Protract against resistance, Grade 5: Normal"]),
SP(2),
fancy_table(
[Paragraph("<b>MMT Grade</b>",sH1), Paragraph("<b>Meaning</b>",sH1), Paragraph("<b>Test Position</b>",sH1)],
[
[Paragraph("0",sBody), Paragraph("No contraction",sBody), Paragraph("Palpate only",sBody)],
[Paragraph("1",sBody), Paragraph("Flicker of contraction",sBody), Paragraph("Palpate during attempt",sBody)],
[Paragraph("2",sBody), Paragraph("Full ROM, gravity eliminated",sBody), Paragraph("Side-lying",sBody)],
[Paragraph("3",sBody), Paragraph("Full ROM against gravity",sBody), Paragraph("Wall push",sBody)],
[Paragraph("4",sBody), Paragraph("Against moderate resistance",sBody), Paragraph("Wall push + resistance",sBody)],
[Paragraph("5",sBody), Paragraph("Against full resistance (normal)",sBody), Paragraph("Full resistance",sBody)],
], [(W-4*cm)*0.15, (W-4*cm)*0.4, (W-4*cm)*0.45]),
PageBreak()
]
# ===== 8. GROVER'S SIGN =====
story += [section_header(8, "Grover's Sign / Grover's Disease"), SP(2),
Paragraph("Grover's Disease (Transient Acantholytic Dermatosis)", sH2), SP(1),
key_box(["Intensely itchy, discrete red papules and papulovesicles on the TRUNK (chest, back)",
"Mainly affects MIDDLE-AGED to OLDER MEN",
"Triggered by: HEAT, sweating, UV exposure, febrile illness",
"Histology: Acantholysis (loss of intercellular cohesion) in epidermis - 4 patterns (Darier-like, Hailey-Hailey-like, pemphigus-like, spongiotic)",
"TRANSIENT - resolves in weeks to months (can be recurrent)",
"Spot diagnosis: Distribution on trunk + age + itch + heat trigger"]),
SP(2),
fancy_table(
[Paragraph("<b>Feature</b>",sH1), Paragraph("<b>Grover's Disease</b>",sH1)],
[
[Paragraph("Site",sBody), Paragraph("Chest, upper back (trunk)",sBody)],
[Paragraph("Lesion",sBody), Paragraph("Papules, papulovesicles, crusted papules",sBody)],
[Paragraph("Itch",sBody), Paragraph("Intense",sBody)],
[Paragraph("Patient",sBody), Paragraph("Middle-aged/elderly men",sBody)],
[Paragraph("Trigger",sBody), Paragraph("Heat, sweating, UV, fever",sBody)],
[Paragraph("Histology",sBody), Paragraph("Focal acantholysis",sBody)],
[Paragraph("Treatment",sBody), Paragraph("Topical steroids, retinoids, avoid triggers",sBody)],
], [(W-4*cm)*0.3, (W-4*cm)*0.7]),
PageBreak()
]
# ===== 9. RA DEFORMITY =====
story += [section_header(9, "RA Hand Deformity"), SP(2)]
img_ra = rl_image(paths.get("ra_hand"), W-4*cm-20, 130)
if img_ra:
story.append(img_ra)
story.append(Paragraph("A = Ulnar deviation at MCPJs B = Swan-neck C = Boutonniere deformity", sCaption))
story += [SP(1)]
# small images row
add_imgs(["boutonniere","swan_neck","ulnar_drift"],
["Boutonniere","Swan-neck","Ulnar drift"],
(W-4*cm)/3-5, 90)
story += [SP(2),
fancy_table(
[Paragraph("<b>Deformity</b>",sH1), Paragraph("<b>PIP</b>",sH1), Paragraph("<b>DIP</b>",sH1), Paragraph("<b>Mechanism</b>",sH1)],
[
[Paragraph("Swan-neck",sBody), Paragraph("Hyperextension",sBody), Paragraph("Flexion",sBody), Paragraph("Intrinsic tightness / FDS rupture",sBody)],
[Paragraph("Boutonniere",sBody), Paragraph("Flexion",sBody), Paragraph("Hyperextension",sBody), Paragraph("Central slip rupture",sBody)],
[Paragraph("Ulnar drift",sBody), Paragraph("MCPJs deviate ulnarly",sBody), Paragraph("-",sBody), Paragraph("Radial deviation of wrist (compensatory)",sBody)],
[Paragraph("Z-thumb",sBody), Paragraph("MCP hyperextension",sBody), Paragraph("IP flexion",sBody), Paragraph("Thenar weakness + ligament laxity",sBody)],
[Paragraph("Mallet finger",sBody), Paragraph("-",sBody), Paragraph("Flexion",sBody), Paragraph("Extensor tendon rupture at DIP",sBody)],
], [(W-4*cm)*0.18, (W-4*cm)*0.18, (W-4*cm)*0.14, (W-4*cm)*0.50]),
PageBreak()
]
# ===== 10. PATHOLOGICAL GAIT =====
story += [section_header(10, "Pathological Gait"), SP(2),
fancy_table(
[Paragraph("<b>Gait</b>",sH1), Paragraph("<b>Cause</b>",sH1), Paragraph("<b>Appearance / Mechanism</b>",sH1)],
[
[Paragraph("Scissor gait",sBody), Paragraph("Spastic CP, bilateral UMN",sBody), Paragraph("Thighs adduct/cross midline, toe-walking, stiff legs",sBody)],
[Paragraph("Steppage gait",sBody), Paragraph("Foot drop (common peroneal n.)",sBody), Paragraph("High steps, foot slap on ground (L4-L5)",sBody)],
[Paragraph("Trendelenburg",sBody), Paragraph("Gluteus medius weakness",sBody), Paragraph("Pelvis drops on swing side (contralateral hip dips)",sBody)],
[Paragraph("Waddling gait",sBody), Paragraph("Proximal myopathy, bilateral DDH",sBody), Paragraph("Bilateral Trendelenburg, side-to-side swaying",sBody)],
[Paragraph("Hemiplegic gait",sBody), Paragraph("UMN lesion (hemiplegia)",sBody), Paragraph("Circumduction of affected leg, arm in flexion",sBody)],
[Paragraph("Parkinsonian",sBody), Paragraph("Parkinson's disease",sBody), Paragraph("Shuffling, festination, en-bloc turning, flexed posture",sBody)],
[Paragraph("Ataxic (cerebellar)",sBody), Paragraph("Cerebellar disease",sBody), Paragraph("Wide-based, staggering, cannot tandem walk",sBody)],
[Paragraph("Antalgic gait",sBody), Paragraph("Pain (hip/knee/foot)",sBody), Paragraph("Short stance phase on painful limb",sBody)],
[Paragraph("High-stepping",sBody), Paragraph("Sensory ataxia",sBody), Paragraph("Stamps heel down, watches feet (proprioception loss)",sBody)],
[Paragraph("Marche a petits pas",sBody), Paragraph("Frontal lobe / NPH",sBody), Paragraph("Very small shuffling steps, preserved arm swing",sBody)],
], [(W-4*cm)*0.22, (W-4*cm)*0.28, (W-4*cm)*0.50]),
PageBreak()
]
# ===== 11. PATHOLOGICAL REFLEXES =====
story += [section_header(11, "Pathological Reflexes"), SP(2),
fancy_table(
[Paragraph("<b>Reflex</b>",sH1), Paragraph("<b>Stimulus</b>",sH1), Paragraph("<b>Positive Response</b>",sH1), Paragraph("<b>Indicates</b>",sH1)],
[
[Paragraph("Babinski",sBody), Paragraph("Stroke lateral sole",sBody), Paragraph("Toe dorsiflexion + fanning",sBody), Paragraph("UMN lesion",sBody)],
[Paragraph("Chaddock",sBody), Paragraph("Below lateral malleolus",sBody), Paragraph("Same as Babinski",sBody), Paragraph("UMN lesion",sBody)],
[Paragraph("Oppenheim",sBody), Paragraph("Stroke anterior tibia",sBody), Paragraph("Toe dorsiflexion",sBody), Paragraph("UMN lesion",sBody)],
[Paragraph("Gordon",sBody), Paragraph("Squeeze calf",sBody), Paragraph("Toe dorsiflexion",sBody), Paragraph("UMN lesion",sBody)],
[Paragraph("Schafer",sBody), Paragraph("Squeeze Achilles",sBody), Paragraph("Toe dorsiflexion",sBody), Paragraph("UMN lesion",sBody)],
[Paragraph("Hoffmann",sBody), Paragraph("Flick middle fingernail",sBody), Paragraph("Thumb + index finger flex",sBody), Paragraph("Cervical UMN lesion",sBody)],
[Paragraph("Clonus",sBody), Paragraph("Sudden ankle dorsiflexion",sBody), Paragraph("Rhythmic beats (>3 = abnormal)",sBody), Paragraph("UMN lesion",sBody)],
[Paragraph("Grasp reflex",sBody), Paragraph("Stroke palm",sBody), Paragraph("Involuntary grasping",sBody), Paragraph("Frontal lobe lesion",sBody)],
[Paragraph("Glabellar tap",sBody), Paragraph("Tap glabella repeatedly",sBody), Paragraph("Persistent blinking (Myerson's sign)",sBody), Paragraph("Parkinson's disease",sBody)],
[Paragraph("Rooting reflex",sBody), Paragraph("Stroke corner of mouth",sBody), Paragraph("Head turns, mouth opens",sBody), Paragraph("Frontal release sign (normal in neonates)",sBody)],
[Paragraph("Snout reflex",sBody), Paragraph("Tap upper lip",sBody), Paragraph("Lip pucker",sBody), Paragraph("Frontal release sign",sBody)],
], [(W-4*cm)*0.18, (W-4*cm)*0.25, (W-4*cm)*0.30, (W-4*cm)*0.27]),
PageBreak()
]
# ===== 12. LSST =====
story += [section_header(12, "Special Tests - LSST (Lumbar Spine Special Tests)"), SP(2),
key_box(["LSST = Lumbar Spine Special Tests - a battery of provocative tests for lumbar pathology",
"Used to identify: nerve root compression, disc herniation, SIJ dysfunction, facet irritation, neural tension"]),
SP(2),
fancy_table(
[Paragraph("<b>Test</b>",sH1), Paragraph("<b>Procedure</b>",sH1), Paragraph("<b>Positive Finding</b>",sH1), Paragraph("<b>Structure Tested</b>",sH1)],
[
[Paragraph("SLR (Lasegue)",sBody), Paragraph("Supine, passive hip flexion with knee extended",sBody), Paragraph("Radicular pain 30-70 degrees",sBody), Paragraph("L4-S1 nerve roots",sBody)],
[Paragraph("SLUMP test",sBody), Paragraph("Seated: spine flexion, neck flexion, knee extension",sBody), Paragraph("Radicular pain reproduced",sBody), Paragraph("Neural tension / dural irritation",sBody)],
[Paragraph("Femoral Nerve Stretch",sBody), Paragraph("Prone, knee flexion / hip extension",sBody), Paragraph("Anterior thigh pain",sBody), Paragraph("L2, L3, L4 nerve roots",sBody)],
[Paragraph("Kemp's test",sBody), Paragraph("Standing: extension + rotation toward pain",sBody), Paragraph("Ipsilateral leg pain",sBody), Paragraph("Facet joint / foraminal stenosis",sBody)],
[Paragraph("Sacral thrust",sBody), Paragraph("Prone, direct AP force on sacrum",sBody), Paragraph("Posterior SIJ pain",sBody), Paragraph("Sacroiliac joint",sBody)],
[Paragraph("FABER (Patrick)",sBody), Paragraph("Flexion, Abduction, External Rotation of hip",sBody), Paragraph("Groin or SIJ pain",sBody), Paragraph("SIJ or hip",sBody)],
[Paragraph("Centralization",sBody), Paragraph("Repeated extension movements",sBody), Paragraph("Pain centralizes = disc",sBody), Paragraph("McKenzie assessment",sBody)],
], [(W-4*cm)*0.18, (W-4*cm)*0.28, (W-4*cm)*0.27, (W-4*cm)*0.27]),
PageBreak()
]
# ===== 13. NCV R-WAVE GRAPH =====
story += [section_header(13, "NCV - R Wave / Waveform Graph"), SP(2)]
img_ncv = rl_image(paths.get("ncv_waveform"), W-4*cm-20, 150)
if img_ncv:
story.append(img_ncv)
story.append(Paragraph("CMAP waveform: onset latency, peak, amplitude, duration. SNAP triphasic waveform.", sCaption))
story += [SP(2),
key_box(["CMAP (Compound Muscle Action Potential) = Motor NCV recording",
"SNAP (Sensory Nerve Action Potential) = Sensory NCV recording",
"KEY MEASUREMENTS: Onset latency (ms), Amplitude (mV for CMAP / uV for SNAP), Conduction velocity (m/s), Duration (ms), F-wave latency",
"Normal motor CV: > 50 m/s in upper limb, > 40 m/s in lower limb",
"Normal distal motor latency (median): < 4.2 ms",
"Normal CMAP amplitude (median): > 4 mV"]),
SP(2),
fancy_table(
[Paragraph("<b>Component</b>",sH1), Paragraph("<b>Represents</b>",sH1), Paragraph("<b>Clinical Use</b>",sH1)],
[
[Paragraph("Onset latency",sBody), Paragraph("Fastest conducting fibers",sBody), Paragraph("Prolonged in demyelination",sBody)],
[Paragraph("Amplitude",sBody), Paragraph("Number of functional axons",sBody), Paragraph("Reduced in axonal loss",sBody)],
[Paragraph("Conduction velocity",sBody), Paragraph("Myelin integrity",sBody), Paragraph("Reduced in demyelination",sBody)],
[Paragraph("F-wave latency",sBody), Paragraph("Proximal conduction",sBody), Paragraph("Abnormal in GBS, proximal lesions",sBody)],
[Paragraph("H-reflex",sBody), Paragraph("S1 root (monosynaptic reflex arc)",sBody), Paragraph("Absent in S1 radiculopathy",sBody)],
], [(W-4*cm)*0.28, (W-4*cm)*0.36, (W-4*cm)*0.36]),
PageBreak()
]
# ===== 14. NCV CONDITION-WISE =====
story += [section_header(14, "NCV - Condition-wise Graph Findings"), SP(2),
fancy_table(
[Paragraph("<b>Condition</b>",sH1), Paragraph("<b>CV</b>",sH1), Paragraph("<b>Amplitude</b>",sH1), Paragraph("<b>Distal Latency</b>",sH1), Paragraph("<b>F-wave</b>",sH1)],
[
[Paragraph("Normal",sBody), Paragraph(">50 m/s",sBody), Paragraph("Normal",sBody), Paragraph("Normal",sBody), Paragraph("Normal",sBody)],
[Paragraph("Axonal neuropathy (DM, alcohol, toxic)",sBody), Paragraph("Normal or mildly reduced",sBody), Paragraph("Reduced (axon loss)",sBody), Paragraph("Normal / mildly increased",sBody), Paragraph("Normal / mildly prolonged",sBody)],
[Paragraph("Demyelinating (GBS, CIDP, CMT)",sBody), Paragraph("Severely reduced (<38 m/s)",sBody), Paragraph("Normal or reduced",sBody), Paragraph("Prolonged",sBody), Paragraph("Prolonged / absent",sBody)],
[Paragraph("Carpal Tunnel Syndrome",sBody), Paragraph("Normal proximal",sBody), Paragraph("Reduced",sBody), Paragraph("Prolonged (median wrist)",sBody), Paragraph("Normal",sBody)],
[Paragraph("Complete nerve transection",sBody), Paragraph("Absent",sBody), Paragraph("Absent (no CMAP/SNAP)",sBody), Paragraph("No response",sBody), Paragraph("Absent",sBody)],
[Paragraph("Conduction block (focal demyelination)",sBody), Paragraph("Normal",sBody), Paragraph("Reduced distal to block",sBody), Paragraph("Prolonged",sBody), Paragraph("Prolonged",sBody)],
[Paragraph("Radiculopathy",sBody), Paragraph("Normal",sBody), Paragraph("Normal (NCV normal in radiculopathy)",sBody), Paragraph("Normal",sBody), Paragraph("H-reflex absent",sBody)],
], [(W-4*cm)*0.28, (W-4*cm)*0.14, (W-4*cm)*0.18, (W-4*cm)*0.20, (W-4*cm)*0.20]),
SP(2),
key_box(["AXONAL: Low amplitude, relatively preserved CV (axon = amplitude)",
"DEMYELINATING: Slow CV, prolonged latencies (myelin = speed)",
"Mixed picture: both CV reduction AND amplitude loss = mixed axonal-demyelinating",
"NCV is NORMAL in radiculopathy and NMJ disorders - use EMG/H-reflex for those"]),
PageBreak()
]
# ===== 15. ORTHOSIS =====
story += [section_header(15, "Orthosis"), SP(2),
key_box(["Orthosis = External device applied to body to support, correct, or improve function of a body part",
"Named by body parts controlled: Ankle-Foot Orthosis (AFO), KAFO, TLSO, etc.",
"Static = no joints; Dynamic = hinged; Functional = for activity"]),
SP(2),
fancy_table(
[Paragraph("<b>Orthosis</b>",sH1), Paragraph("<b>Full Name</b>",sH1), Paragraph("<b>Indication</b>",sH1)],
[
[Paragraph("AFO",sBody), Paragraph("Ankle-Foot Orthosis",sBody), Paragraph("Foot drop, hemiplegia, spastic CP",sBody)],
[Paragraph("KAFO",sBody), Paragraph("Knee-Ankle-Foot Orthosis",sBody), Paragraph("Post-polio, quadriceps weakness, knee instability",sBody)],
[Paragraph("HKAFO",sBody), Paragraph("Hip-Knee-Ankle-Foot Orthosis",sBody), Paragraph("Paraplegia (para-walking programs)",sBody)],
[Paragraph("TLSO",sBody), Paragraph("Thoraco-Lumbo-Sacral Orthosis",sBody), Paragraph("Scoliosis (Boston/Milwaukee brace), spinal fractures",sBody)],
[Paragraph("LSO",sBody), Paragraph("Lumbo-Sacral Orthosis",sBody), Paragraph("Low back pain, lumbar compression fracture",sBody)],
[Paragraph("CTLSO",sBody), Paragraph("Cervico-Thoraco-Lumbo-Sacral Orthosis",sBody), Paragraph("Milwaukee brace for high thoracic scoliosis",sBody)],
[Paragraph("Cock-up splint",sBody), Paragraph("Wrist extension orthosis",sBody), Paragraph("Radial nerve palsy (wrist drop)",sBody)],
[Paragraph("Thumb spica",sBody), Paragraph("Thumb + wrist immobilizer",sBody), Paragraph("De Quervain's, scaphoid fracture",sBody)],
[Paragraph("Cervical collar",sBody), Paragraph("Soft / Philadelphia / SOMI",sBody), Paragraph("Cervical spondylosis, post-whiplash, cervical fracture",sBody)],
[Paragraph("Knee orthosis",sBody), Paragraph("Swedish knee cage, GII",sBody), Paragraph("Genu valgum/varum, ACL instability",sBody)],
], [(W-4*cm)*0.14, (W-4*cm)*0.32, (W-4*cm)*0.54]),
PageBreak()
]
# ===== 16. PROSTHESIS =====
story += [section_header(16, "Prosthesis"), SP(2),
key_box(["Prosthesis = Artificial device to REPLACE a missing body part (limb/organ)",
"Named by amputation level: BK (below-knee/transtibial), AK (above-knee/transfemoral)",
"Components: Socket + Suspension + Shank + Terminal device (foot/hand)"]),
SP(2),
Paragraph("Lower Limb Prostheses", sH2),
fancy_table(
[Paragraph("<b>Amputation Level</b>",sH1), Paragraph("<b>Prosthesis Type</b>",sH1), Paragraph("<b>Key Feature</b>",sH1)],
[
[Paragraph("Partial foot",sBody), Paragraph("Custom insole / toe filler",sBody), Paragraph("Maintains toe-off",sBody)],
[Paragraph("Syme's (ankle disarticulation)",sBody), Paragraph("End-bearing prosthesis",sBody), Paragraph("Full end-weight bearing",sBody)],
[Paragraph("Below-knee (transtibial)",sBody), Paragraph("PTB (Patellar Tendon Bearing)",sBody), Paragraph("Load via patellar tendon + tibial flares",sBody)],
[Paragraph("Knee disarticulation",sBody), Paragraph("4-bar linkage knee",sBody), Paragraph("Better proprioception than AK",sBody)],
[Paragraph("Above-knee (transfemoral)",sBody), Paragraph("Quadrilateral / Ischial containment socket",sBody), Paragraph("Ischial seat for weight bearing",sBody)],
[Paragraph("Hip disarticulation",sBody), Paragraph("Canadian hip disarticulation",sBody), Paragraph("Hip pivot mechanism",sBody)],
], [(W-4*cm)*0.28, (W-4*cm)*0.32, (W-4*cm)*0.40]),
SP(2),
Paragraph("Prosthetic Components", sH2),
fancy_table(
[Paragraph("<b>Component</b>",sH1), Paragraph("<b>Options</b>",sH1), Paragraph("<b>Used For</b>",sH1)],
[
[Paragraph("Foot",sBody), Paragraph("SACH foot (Solid Ankle Cushion Heel)",sBody), Paragraph("Basic activities, low activity level",sBody)],
[Paragraph("Foot",sBody), Paragraph("Dynamic response / Energy-storing (Flex-foot)",sBody), Paragraph("Active/athletic users",sBody)],
[Paragraph("Knee",sBody), Paragraph("Single axis",sBody), Paragraph("Simple, stable walking",sBody)],
[Paragraph("Knee",sBody), Paragraph("Polycentric (4-bar)",sBody), Paragraph("Stability in stance phase",sBody)],
[Paragraph("Knee",sBody), Paragraph("Microprocessor (C-Leg)",sBody), Paragraph("Variable cadence, advanced users",sBody)],
[Paragraph("Suspension",sBody), Paragraph("Pin lock, suction, elevated vacuum, belt",sBody), Paragraph("Varies by activity level",sBody)],
], [(W-4*cm)*0.18, (W-4*cm)*0.42, (W-4*cm)*0.40]),
SP(2),
Paragraph("Upper Limb Prostheses", sH2),
fancy_table(
[Paragraph("<b>Type</b>",sH1), Paragraph("<b>Mechanism</b>",sH1), Paragraph("<b>Pros</b>",sH1)],
[
[Paragraph("Body-powered hook",sBody), Paragraph("Shoulder harness + cable",sBody), Paragraph("Durable, tactile feedback, low cost",sBody)],
[Paragraph("Body-powered hand",sBody), Paragraph("Cosmetic + functional",sBody), Paragraph("Better cosmesis",sBody)],
[Paragraph("Myoelectric hand",sBody), Paragraph("EMG signals from residual muscles",sBody), Paragraph("No harness, intuitive control",sBody)],
[Paragraph("Activity-specific",sBody), Paragraph("Sports hooks, swim prosthesis",sBody), Paragraph("High performance",sBody)],
], [(W-4*cm)*0.25, (W-4*cm)*0.38, (W-4*cm)*0.37]),
]
# ── final page: summary cheatsheet ───────────────────────────────────────────
story += [PageBreak(),
Paragraph("QUICK CHEATSHEET", sTitle), SP(2),
fancy_table(
[Paragraph("<b>#</b>",sH1), Paragraph("<b>Topic</b>",sH1), Paragraph("<b>1-Line Spot Clue</b>",sH1)],
[
[Paragraph("1",sBody), Paragraph("Squint",sBody), Paragraph("Eye deviation: in=eso, out=exo, up=hyper, down=hypo",sBody)],
[Paragraph("2",sBody), Paragraph("Chaddock",sBody), Paragraph("Stroke below lateral malleolus -> toe up = UMN lesion",sBody)],
[Paragraph("3",sBody), Paragraph("AF ECG",sBody), Paragraph("No P waves + irregularly irregular = AF",sBody)],
[Paragraph("4",sBody), Paragraph("Ape hand",sBody), Paragraph("Flat thenar, adducted thumb = median nerve palsy",sBody)],
[Paragraph("5",sBody), Paragraph("Scoliometer",sBody), Paragraph("Measures ATR at rib hump; >=7 degrees = X-ray referral",sBody)],
[Paragraph("6",sBody), Paragraph("90-90 SLR",sBody), Paragraph("Hip+knee 90, extend knee; tight hamstrings = can't straighten",sBody)],
[Paragraph("7",sBody), Paragraph("Serratus anterior MMT",sBody), Paragraph("Wall push; winging = long thoracic nerve palsy",sBody)],
[Paragraph("8",sBody), Paragraph("Grover's sign",sBody), Paragraph("Itchy papules on trunk, middle-aged men, heat trigger",sBody)],
[Paragraph("9",sBody), Paragraph("RA deformity",sBody), Paragraph("Swan-neck, Boutonniere, Ulnar drift, Z-thumb",sBody)],
[Paragraph("10",sBody), Paragraph("Path. gait",sBody), Paragraph("Scissor=CP, Steppage=foot drop, Trendelenburg=glut med weak",sBody)],
[Paragraph("11",sBody), Paragraph("Path. reflex",sBody), Paragraph("Babinski/Chaddock/Oppenheim/Gordon = UMN; Hoffmann = cervical UMN",sBody)],
[Paragraph("12",sBody), Paragraph("LSST",sBody), Paragraph("SLR(L4-S1), SLUMP(neural), FNS(L2-L4), Kemp(facet), FABER(SIJ)",sBody)],
[Paragraph("13",sBody), Paragraph("NCV R-wave",sBody), Paragraph("CMAP: amplitude=axons, CV=myelin, latency=distal conduction",sBody)],
[Paragraph("14",sBody), Paragraph("NCV conditions",sBody), Paragraph("Axonal=low amplitude; Demyelinating=slow CV; CTS=prolonged distal latency",sBody)],
[Paragraph("15",sBody), Paragraph("Orthosis",sBody), Paragraph("AFO=foot drop, KAFO=polio, TLSO=scoliosis, LSO=back pain",sBody)],
[Paragraph("16",sBody), Paragraph("Prosthesis",sBody), Paragraph("PTB=BK, Ischial socket=AK, SACH=basic foot, C-leg=micro knee",sBody)],
], [(W-4*cm)*0.05, (W-4*cm)*0.22, (W-4*cm)*0.73])
]
# ── build PDF ─────────────────────────────────────────────────────────────────
doc = SimpleDocTemplate(
OUT,
pagesize=A4,
leftMargin=2*cm, rightMargin=2*cm,
topMargin=1.8*cm, bottomMargin=1.8*cm,
title="Spotting Exam Visual Quick Revision Guide",
author="Orris Medical AI"
)
doc.build(story, onFirstPage=add_page_number, onLaterPages=add_page_number)
print(f"\nPDF saved to: {OUT}")
cd /home/daytona/workspace/spotting-exam-pdf && python3 build_pdf.py 2>&1
python3 -c " from pypdf import PdfReader r = PdfReader('/home/daytona/workspace/spotting-exam-pdf/Spotting_Exam_Visual_Guide.pdf') print(f'Pages: {len(r.pages)}') import os size = os.path.getsize('/home/daytona/workspace/spotting-exam-pdf/Spotting_Exam_Visual_Guide.pdf') print(f'Size: {size/1024:.1f} KB') "
Spotting Exam Visual Guide
PDF Document · PDF
| Pages | Content |
|---|---|
| 1 | Cover + Table of Contents |
| 2 | Types of Squint - diagram + clinical images + table |
| 3 | Chaddock's Sign - images + comparison table of all Babinski equivalents |
| 4 | Atrial Fibrillation ECG - 2 ECG images + AF vs Flutter comparison |
| 5 | Ape Hand - 2 clinical images + nerve palsy comparison table |
| 6 | Scoliometer - 2 images + ATR referral thresholds |
| 7 | 90-90 SLR Test - image + popliteal angle grading |
| 8 | Serratus Anterior MMT - image + full MMT grading 0-5 |
| 9 | Grover's Sign - features + diagnostic table |
| 10 | RA Hand Deformity - 4 textbook images + deformity table |
| 11 | Pathological Gait - 10-row table (scissor, steppage, Trendelenburg...) |
| 12 | Pathological Reflexes - 11-row table |
| 13 | LSST Special Tests - 7-test table |
| 14 | NCV R-Wave Graph - image + waveform components table |
| 15 | NCV Condition-wise - 7-condition comparison table |
| 16 | Orthosis - 10-row table (AFO, KAFO, TLSO...) |
| 17 | Prosthesis - lower limb + components + upper limb tables |
| 18 | 1-line Quick Cheatsheet for all 16 topics |