~/ortho-xray-pptx/build_pptx.py
from pptx import Presentation
from pptx.util import Inches, Pt, Emu
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.enum.shapes import MSO_SHAPE_TYPE
from io import BytesIO
import copy
# ── Colour palette ─────────────────────────────────────────────────────────────
DARK_BLUE = RGBColor(0x0D, 0x2B, 0x55) # slide background / header
MID_BLUE = RGBColor(0x1A, 0x5A, 0x96) # accent bars
LIGHT_BLUE = RGBColor(0xD6, 0xE8, 0xF7) # content card background
RED_ALERT = RGBColor(0xC0, 0x39, 0x2B) # emergency / red flags
GOLD = RGBColor(0xF3, 0x9C, 0x12) # highlight / key points
WHITE = RGBColor(0xFF, 0xFF, 0xFF)
DARK_GREY = RGBColor(0x2C, 0x3E, 0x50)
LIGHT_GREY = RGBColor(0xEC, 0xF0, 0xF1)
GREEN = RGBColor(0x27, 0xAE, 0x60)
ORANGE = RGBColor(0xE6, 0x7E, 0x22)
W = Inches(13.333)
H = Inches(7.5)
prs = Presentation()
prs.slide_width = W
prs.slide_height = H
blank = prs.slide_layouts[6]
# ── Helper functions ────────────────────────────────────────────────────────────
def add_rect(slide, x, y, w, h, fill_color, line_color=None, line_width=None):
from pptx.enum.shapes import MSO_SHAPE
shp = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, x, y, w, h)
shp.fill.solid()
shp.fill.fore_color.rgb = fill_color
if line_color:
shp.line.color.rgb = line_color
shp.line.width = Pt(line_width or 1)
else:
shp.line.fill.background()
shp.shadow.inherit = False
return shp
def add_tb(slide, x, y, w, h, text, font_size, bold=False, color=WHITE,
align=PP_ALIGN.LEFT, italic=False, word_wrap=True, v_anchor=MSO_ANCHOR.TOP):
tb = slide.shapes.add_textbox(x, y, w, h)
tf = tb.text_frame
tf.word_wrap = word_wrap
tf.vertical_anchor = v_anchor
tf.margin_left = 0; tf.margin_right = 0; tf.margin_top = 0; tf.margin_bottom = 0
p = tf.paragraphs[0]
p.alignment = align
run = p.add_run()
run.text = text
run.font.name = "Calibri"
run.font.size = Pt(font_size)
run.font.bold = bold
run.font.italic = italic
run.font.color.rgb = color
return tb
def add_multiline_tb(slide, x, y, w, h, lines, font_size, bold=False, color=WHITE,
align=PP_ALIGN.LEFT, line_spacing_pt=None, indent_first=False):
"""lines = list of (text, bold_override, color_override, font_size_override)"""
tb = slide.shapes.add_textbox(x, y, w, h)
tf = tb.text_frame
tf.word_wrap = True
tf.margin_left = Pt(2)
tf.margin_right = Pt(2)
tf.margin_top = Pt(4)
tf.margin_bottom= Pt(4)
first = True
for item in lines:
if isinstance(item, str):
text, b, c, fs = item, bold, color, font_size
else:
text = item[0]
b = item[1] if len(item) > 1 else bold
c = item[2] if len(item) > 2 else color
fs = item[3] if len(item) > 3 else font_size
if first:
p = tf.paragraphs[0]
first = False
else:
p = tf.add_paragraph()
p.alignment = align
run = p.add_run()
run.text = text
run.font.name = "Calibri"
run.font.size = Pt(fs)
run.font.bold = b
run.font.color.rgb = c
return tb
def slide_bg(slide, color=DARK_BLUE):
add_rect(slide, 0, 0, W, H, color)
def header_bar(slide, title, subtitle=None, bar_color=MID_BLUE):
add_rect(slide, 0, 0, W, Inches(1.25), bar_color)
add_tb(slide, Inches(0.4), Inches(0.1), Inches(12.5), Inches(0.7),
title, 28, bold=True, color=WHITE, align=PP_ALIGN.LEFT)
if subtitle:
add_tb(slide, Inches(0.4), Inches(0.75), Inches(12.5), Inches(0.4),
subtitle, 14, bold=False, color=LIGHT_BLUE, align=PP_ALIGN.LEFT)
def section_label(slide, text, x, y, w=Inches(2.5), h=Inches(0.35)):
add_rect(slide, x, y, w, h, GOLD)
add_tb(slide, x+Inches(0.1), y, w-Inches(0.1), h, text, 11, bold=True,
color=DARK_BLUE, align=PP_ALIGN.LEFT, v_anchor=MSO_ANCHOR.MIDDLE)
def card(slide, x, y, w, h, bg=LIGHT_BLUE):
return add_rect(slide, x, y, w, h, bg, line_color=MID_BLUE, line_width=0.5)
def divider_line(slide, x, y, w, color=GOLD, thickness=2):
from pptx.enum.shapes import MSO_CONNECTOR
ln = slide.shapes.add_connector(MSO_CONNECTOR.STRAIGHT, x, y, x+w, y)
ln.line.color.rgb = color
ln.line.width = Pt(thickness)
# ══════════════════════════════════════════════════════════════════════════════
# SLIDE 1 – TITLE SLIDE
# ══════════════════════════════════════════════════════════════════════════════
slide = prs.slides.add_slide(blank)
slide_bg(slide, DARK_BLUE)
# Decorative left bar
add_rect(slide, 0, 0, Inches(0.45), H, GOLD)
# Central panel
add_rect(slide, Inches(0.85), Inches(1.8), Inches(11.6), Inches(4.0), MID_BLUE,
line_color=GOLD, line_width=2)
add_tb(slide, Inches(1.2), Inches(2.1), Inches(11.0), Inches(1.2),
"Imaging Interpretation for", 32, bold=False, color=LIGHT_BLUE, align=PP_ALIGN.CENTER)
add_tb(slide, Inches(1.2), Inches(2.9), Inches(11.0), Inches(1.4),
"Orthopedic X-Rays", 54, bold=True, color=WHITE, align=PP_ALIGN.CENTER)
divider_line(slide, Inches(2.5), Inches(4.45), Inches(8.3), GOLD, 2)
add_tb(slide, Inches(1.2), Inches(4.6), Inches(11.0), Inches(0.5),
"OSCE Preparation Guide | MBBS / MD Level", 18, bold=False,
color=LIGHT_BLUE, align=PP_ALIGN.CENTER)
add_tb(slide, Inches(1.2), Inches(5.1), Inches(11.0), Inches(0.4),
"Sources: Rockwood & Green 10th Ed 2025 • Campbell's Operative Orthopaedics 15th Ed 2026 • ROSEN's Emergency Medicine",
11, bold=False, color=LIGHT_BLUE, align=PP_ALIGN.CENTER)
# Bottom strip
add_rect(slide, 0, Inches(6.8), W, Inches(0.7), GOLD)
add_tb(slide, Inches(0.3), Inches(6.82), Inches(12.7), Inches(0.4),
"Fractures • Dislocations • Soft Tissue • Spine • Paediatric • Pathological",
13, bold=False, color=DARK_BLUE, align=PP_ALIGN.CENTER)
# ══════════════════════════════════════════════════════════════════════════════
# SLIDE 2 – X-RAY INTERPRETATION FRAMEWORK (ABCDS)
# ══════════════════════════════════════════════════════════════════════════════
slide = prs.slides.add_slide(blank)
slide_bg(slide, DARK_GREY)
header_bar(slide, "The ABCDS Framework", "Systematic Approach to Every Orthopaedic X-Ray")
letters = [
("A", "Alignment", "Are bones in normal anatomical alignment? Is there a dislocation or subluxation?", RGBColor(0x1A,0xBC,0x9C)),
("B", "Bones", "Look for cortical breaks, lucent fracture lines, impaction, comminution, sclerosis.", RGBColor(0x27,0xAE,0x60)),
("C", "Cartilage", "Assess joint spaces — narrowed (arthritis/infection), widened (effusion/ligament injury).", RGBColor(0x29,0x80,0xB9)),
("D", "Density", "Increased density = sclerosis (stress/healing). Decreased = lytic lesion, osteoporosis, AVN.", RGBColor(0x85,0x44,0xBF)),
("S", "Soft Tissue", "Swelling, fat-pad displacement, foreign bodies, gas (infection), calcification.", GOLD),
]
x_start = Inches(0.35)
box_w = Inches(2.5)
box_h = Inches(4.8)
gap = Inches(0.15)
for i, (letter, label, desc, col) in enumerate(letters):
bx = x_start + i*(box_w + gap)
add_rect(slide, bx, Inches(1.45), box_w, box_h, col)
# Big letter
add_tb(slide, bx+Inches(0.1), Inches(1.5), box_w-Inches(0.2), Inches(1.4),
letter, 72, bold=True, color=WHITE, align=PP_ALIGN.CENTER)
# Divider
divider_line(slide, bx+Inches(0.2), Inches(2.95), box_w-Inches(0.4), WHITE, 1)
# Label
add_tb(slide, bx+Inches(0.1), Inches(3.0), box_w-Inches(0.2), Inches(0.5),
label, 15, bold=True, color=WHITE, align=PP_ALIGN.CENTER)
# Description
add_multiline_tb(slide, bx+Inches(0.15), Inches(3.55), box_w-Inches(0.3), Inches(2.5),
[desc], 12, color=WHITE, align=PP_ALIGN.LEFT)
# Bottom note
add_rect(slide, 0, Inches(6.6), W, Inches(0.9), DARK_BLUE)
add_tb(slide, Inches(0.4), Inches(6.65), Inches(12.5), Inches(0.5),
"Always state: 1) Type of view (AP/Lateral/Oblique) 2) Date & adequacy (two views? two joints for long bones?) 3) Your systematic findings before giving a diagnosis",
12, color=LIGHT_BLUE, align=PP_ALIGN.LEFT)
# ══════════════════════════════════════════════════════════════════════════════
# SLIDE 3 – FRACTURE DESCRIPTION TEMPLATE
# ══════════════════════════════════════════════════════════════════════════════
slide = prs.slides.add_slide(blank)
slide_bg(slide, DARK_BLUE)
header_bar(slide, "Describing a Fracture on X-Ray", "Use this template for every fracture OSCE station")
# Left column: step-by-step
card(slide, Inches(0.3), Inches(1.4), Inches(6.0), Inches(5.8))
section_label(slide, "STEP-BY-STEP DESCRIPTION", Inches(0.35), Inches(1.45), Inches(5.9))
steps = [
("1", "Which bone?", "Name the specific bone and side (e.g. left distal radius)"),
("2", "Fracture site", "Proximal / middle / distal third; epiphysis / metaphysis / diaphysis"),
("3", "Pattern", "Transverse, spiral, oblique, comminuted, impacted, avulsion, greenstick"),
("4", "Displacement", "None / partial / complete; direction (dorsal, volar, lateral)"),
("5", "Angulation", "Degrees and direction (varus, valgus, dorsal, volar)"),
("6", "Shortening", "Estimated mm; overlapping fragments"),
("7", "Open vs Closed", "Look for soft tissue gas; skin integrity (clinical)"),
("8", "Intra/extra-articular", "Does the fracture line cross the joint?"),
("9", "Associated findings","Dislocation, other fractures, foreign bodies, AVN"),
]
y = Inches(1.9)
for num, title, desc in steps:
add_rect(slide, Inches(0.4), y, Inches(0.45), Inches(0.42), GOLD)
add_tb(slide, Inches(0.4), y, Inches(0.45), Inches(0.42), num, 12, bold=True,
color=DARK_BLUE, align=PP_ALIGN.CENTER, v_anchor=MSO_ANCHOR.MIDDLE)
add_tb(slide, Inches(0.95), y+Inches(0.01), Inches(1.45), Inches(0.4),
title, 11, bold=True, color=GOLD, align=PP_ALIGN.LEFT)
add_tb(slide, Inches(2.45), y+Inches(0.01), Inches(3.65), Inches(0.4),
desc, 11, color=WHITE, align=PP_ALIGN.LEFT)
y += Inches(0.5)
# Right column: fracture pattern table
card(slide, Inches(6.65), Inches(1.4), Inches(6.35), Inches(5.8), DARK_BLUE)
add_rect(slide, Inches(6.65), Inches(1.4), Inches(6.35), Inches(0.35), MID_BLUE)
add_tb(slide, Inches(6.75), Inches(1.4), Inches(6.15), Inches(0.35),
"FRACTURE PATTERN REFERENCE", 12, bold=True, color=WHITE, align=PP_ALIGN.LEFT,
v_anchor=MSO_ANCHOR.MIDDLE)
patterns = [
("PATTERN", "MECHANISM", "COMMON SITE", True),
("Transverse", "Direct blow / bending", "Tibia, phalanges", False),
("Spiral", "Twisting / torsion", "Tib/Fib, humerus", False),
("Oblique", "Axial + angulation", "Femur, tibia", False),
("Comminuted", "High energy", "Tibial plateau", False),
("Avulsion", "Ligament/tendon pull", "Lat. malleolus", False),
("Impacted", "Axial load", "Distal radius, NOF", False),
("Stress", "Repetitive loading", "2nd MT, tibia", False),
("Pathological", "Minimal trauma", "Metastasis, MM", False),
("Greenstick", "Children", "Radius / ulna", False),
("Buckle/Torus", "Children – axial load", "Distal radius", False),
]
col_widths = [Inches(1.55), Inches(2.0), Inches(2.4)]
col_x = [Inches(6.75), Inches(8.35), Inches(10.35)]
row_h = Inches(0.44)
y = Inches(1.8)
for row in patterns:
bg = MID_BLUE if row[3] else (DARK_GREY if patterns.index(row) % 2 == 0 else DARK_BLUE)
txt_col = WHITE
add_rect(slide, Inches(6.65), y, Inches(6.35), row_h, bg)
for ci, text in enumerate(row[:3]):
add_tb(slide, col_x[ci], y+Inches(0.04), col_widths[ci], row_h-Inches(0.08),
text, 11 if not row[3] else 11, bold=row[3], color=txt_col,
align=PP_ALIGN.LEFT, v_anchor=MSO_ANCHOR.MIDDLE)
y += row_h
# ══════════════════════════════════════════════════════════════════════════════
# SLIDE 4 – COMMON FRACTURES: UPPER LIMB
# ══════════════════════════════════════════════════════════════════════════════
slide = prs.slides.add_slide(blank)
slide_bg(slide, DARK_GREY)
header_bar(slide, "Upper Limb Fractures", "Key X-Ray Findings & Clinical Pearls")
cases = [
{
"title": "Colles' Fracture",
"subtitle": "Distal Radius",
"icon": "FOOSH",
"findings": [
"Transverse # at distal radius metaphysis",
"Dorsal displacement & angulation",
"Radial shortening + impaction",
"\"Dinner fork\" deformity",
"Associated ulnar styloid #",
],
"pearl": "Check median nerve! CTS complication. Smith's = volar displacement (reversed Colles).",
"color": RGBColor(0x1A,0x5A,0x96),
},
{
"title": "Scaphoid Fracture",
"subtitle": "Wrist (FOOSH)",
"icon": "AVN",
"findings": [
"Lucent line at scaphoid waist (70-80%)",
"May be X-ray NEGATIVE initially",
"Loss of scaphoid cortex",
"3 views: PA, lateral, oblique",
"MRI if X-ray normal but clinically +ve",
],
"pearl": "Anatomical snuffbox tenderness = treat as scaphoid # even if X-ray normal! Risk of AVN of proximal pole.",
"color": RGBColor(0xC0,0x39,0x2B),
},
{
"title": "Proximal Humerus #",
"subtitle": "Shoulder",
"icon": "Neer",
"findings": [
"AP + axial + Y-view required",
"Neer 2-4 part classification",
"Greater tuberosity avulsion",
"Look for Hill-Sachs (post-lat head)",
"Bankart (ant-inf glenoid rim)",
],
"pearl": "Hill-Sachs = humeral head compression after anterior dislocation. Axillary nerve injury common.",
"color": RGBColor(0x27,0xAE,0x60),
},
{
"title": "Paediatric Elbow",
"subtitle": "Supracondylar / Radial Head",
"icon": "CRITOE",
"findings": [
"Posterior fat-pad sign = effusion",
"Anterior fat-pad (sail sign) = effusion",
"Anterior humeral line alignment",
"Radiocapitellar line (radial head)",
"CRITOE ossification sequence",
],
"pearl": "CRITOE: 1-3-5-7-9-11 years. Posterior fat pad ALWAYS abnormal — treat as radial head # or supracondylar.",
"color": RGBColor(0x85,0x44,0xBF),
},
]
cx = Inches(0.3)
cw = Inches(3.1)
ch = Inches(5.65)
for case in cases:
c = case["color"]
# Card bg
add_rect(slide, cx, Inches(1.35), cw, ch, DARK_BLUE, line_color=c, line_width=1.5)
# Colour header
add_rect(slide, cx, Inches(1.35), cw, Inches(0.75), c)
add_tb(slide, cx+Inches(0.1), Inches(1.37), cw-Inches(0.2), Inches(0.45),
case["title"], 16, bold=True, color=WHITE, align=PP_ALIGN.LEFT)
add_tb(slide, cx+Inches(0.1), Inches(1.78), cw-Inches(0.2), Inches(0.28),
case["subtitle"], 11, color=LIGHT_BLUE, align=PP_ALIGN.LEFT)
# Badge
add_rect(slide, cx+cw-Inches(0.9), Inches(1.38), Inches(0.8), Inches(0.3), GOLD)
add_tb(slide, cx+cw-Inches(0.9), Inches(1.38), Inches(0.8), Inches(0.3),
case["icon"], 9, bold=True, color=DARK_BLUE, align=PP_ALIGN.CENTER,
v_anchor=MSO_ANCHOR.MIDDLE)
# Findings header
add_tb(slide, cx+Inches(0.1), Inches(2.15), cw-Inches(0.2), Inches(0.3),
"X-RAY FINDINGS", 9, bold=True, color=GOLD)
# Findings list
y = Inches(2.42)
for f in case["findings"]:
add_tb(slide, cx+Inches(0.25), y, cw-Inches(0.35), Inches(0.36),
f"• {f}", 10.5, color=WHITE)
y += Inches(0.38)
# Pearl box
add_rect(slide, cx+Inches(0.1), Inches(4.55), cw-Inches(0.2), Inches(0.05), GOLD)
add_rect(slide, cx+Inches(0.1), Inches(4.62), cw-Inches(0.2), Inches(1.2), RGBColor(0x1C,0x2F,0x4A))
add_tb(slide, cx+Inches(0.15), Inches(4.65), cw-Inches(0.3), Inches(0.28),
"CLINICAL PEARL", 9, bold=True, color=GOLD)
add_multiline_tb(slide, cx+Inches(0.15), Inches(4.93), cw-Inches(0.3), Inches(0.85),
[case["pearl"]], 10, color=LIGHT_BLUE, align=PP_ALIGN.LEFT)
cx += cw + Inches(0.17)
# ══════════════════════════════════════════════════════════════════════════════
# SLIDE 5 – LOWER LIMB FRACTURES
# ══════════════════════════════════════════════════════════════════════════════
slide = prs.slides.add_slide(blank)
slide_bg(slide, DARK_BLUE)
header_bar(slide, "Lower Limb Fractures", "Key X-Ray Findings, Classifications & Management")
cases = [
{
"title": "Neck of Femur",
"findings": [
"AP pelvis + lateral hip views",
"Disruption of Shenton's line",
"Garden classification I–IV",
"Loss of medial cortex continuity",
"Check trabecular pattern",
],
"classification": "Garden I: Incomplete/valgus impacted\nGarden II: Complete undisplaced\nGarden III: Partial displacement\nGarden IV: Full displacement",
"mgmt": "I/II: Cannulated screws / DHS\nIII/IV (elderly): Hemiarthroplasty\nIII/IV (young): ORIF (preserve head)",
"color": MID_BLUE,
},
{
"title": "Tibial Plateau",
"findings": [
"AP + lateral + 45° oblique views",
"CT essential for surgical planning",
"Depression of lateral plateau",
"Schatzker I–VI classification",
"Check fibular head for LCL injury",
],
"classification": "Schatzker I-III: Lateral plateau\nSchatzker IV: Medial plateau\nSchatzker V: Bicondylar\nSchatzker VI: Metaphyseal dissoc.",
"mgmt": "Low energy (I-III): ORIF if depressed >5mm\nHigh energy (IV-VI): Complex ORIF\nRule out compartment syndrome!",
"color": GREEN,
},
{
"title": "Tibial Shaft",
"findings": [
"AP + lateral including knee & ankle",
"Oblique view for spiral fracture",
"Note: open wound / soft tissue gas",
"Assess shortening and rotation",
"Look for ipsilateral femur / fibula #",
],
"classification": "Open # Gustilo-Anderson:\nI: <1cm, clean\nII: 1-10cm\nIIIA/B/C: >10cm ± vascular",
"mgmt": "Stable/undisplaced: Cast\nDisplaced/unstable: IMN (gold std)\nOpen: Abx + debridement + IMN/ex-fix",
"color": ORANGE,
},
{
"title": "Ankle Fracture",
"findings": [
"AP + lateral + mortise (15° IR) views",
"Weber A/B/C (fibula level)",
"Check medial clear space (<4mm)",
"Talar tilt / shift = instability",
"Look for posterior malleolus #",
],
"classification": "Weber A: Below syndesmosis (stable)\nWeber B: At syndesmosis\nWeber C: Above syndesmosis (UNSTABLE)\nLauge-Hansen: SA/PA/SE/PE",
"mgmt": "Weber A: Walking boot / cast\nWeber B: Depends on medial injury\nWeber C: Usually ORIF (unstable)",
"color": RED_ALERT,
},
]
cx = Inches(0.3)
cw = Inches(3.1)
ch = Inches(5.65)
for case in cases:
c = case["color"]
add_rect(slide, cx, Inches(1.35), cw, ch, DARK_GREY, line_color=c, line_width=1.5)
add_rect(slide, cx, Inches(1.35), cw, Inches(0.6), c)
add_tb(slide, cx+Inches(0.1), Inches(1.37), cw-Inches(0.2), Inches(0.56),
case["title"], 16, bold=True, color=WHITE, align=PP_ALIGN.LEFT,
v_anchor=MSO_ANCHOR.MIDDLE)
add_tb(slide, cx+Inches(0.1), Inches(2.0), cw-Inches(0.2), Inches(0.28),
"X-RAY FINDINGS", 9, bold=True, color=GOLD)
y = Inches(2.28)
for f in case["findings"]:
add_tb(slide, cx+Inches(0.2), y, cw-Inches(0.3), Inches(0.35),
f"• {f}", 10, color=WHITE)
y += Inches(0.35)
# Classification
add_rect(slide, cx+Inches(0.1), Inches(4.08), cw-Inches(0.2), Inches(0.28), c)
add_tb(slide, cx+Inches(0.15), Inches(4.08), cw-Inches(0.3), Inches(0.28),
"CLASSIFICATION", 9, bold=True, color=DARK_BLUE, v_anchor=MSO_ANCHOR.MIDDLE)
add_multiline_tb(slide, cx+Inches(0.15), Inches(4.38), cw-Inches(0.3), Inches(1.0),
[case["classification"]], 9.5, color=LIGHT_BLUE)
# Management
add_rect(slide, cx+Inches(0.1), Inches(5.42), cw-Inches(0.2), Inches(0.28), GOLD)
add_tb(slide, cx+Inches(0.15), Inches(5.42), cw-Inches(0.3), Inches(0.28),
"MANAGEMENT", 9, bold=True, color=DARK_BLUE, v_anchor=MSO_ANCHOR.MIDDLE)
add_multiline_tb(slide, cx+Inches(0.15), Inches(5.72), cw-Inches(0.3), Inches(0.9),
[case["mgmt"]], 9.5, color=WHITE)
cx += cw + Inches(0.17)
# ══════════════════════════════════════════════════════════════════════════════
# SLIDE 6 – DISLOCATIONS
# ══════════════════════════════════════════════════════════════════════════════
slide = prs.slides.add_slide(blank)
slide_bg(slide, DARK_GREY)
header_bar(slide, "Joint Dislocations on X-Ray", "Always check neurovascular status before and after reduction")
dislocations = [
{
"joint": "Shoulder (GHJ)",
"type": "95% Anterior",
"xray": [
"AP: humeral head below/medial to glenoid",
"Axial/Y-view: humeral head anterior to glenoid",
"Hill-Sachs: post-lateral humeral head defect",
"Bankart: ant-inferior glenoid rim fracture",
"Always take post-reduction X-ray",
],
"nerve": "Axillary nerve (deltoid, lateral deltoid sensation)",
"color": MID_BLUE,
"treat": "Reduction (Hennepin / Cunningham / Kocher); Sling 3-6 wks; Physio",
},
{
"joint": "Hip",
"type": "90% Posterior",
"xray": [
"AP pelvis: femoral head superior & lateral",
"Loss of Shenton's line",
"Limb: shortened, adducted, internally rotated",
"CT after reduction to check joint congruence",
"Look for posterior acetabular wall #",
],
"nerve": "Sciatic nerve (esp. peroneal division: foot drop)",
"color": RED_ALERT,
"treat": "EMERGENCY reduction within 6h (AVN risk). Allis / Stimson technique. ORIF if wall # present.",
},
{
"joint": "Knee (Patella)",
"type": "Lateral > Medial",
"xray": [
"AP knee: patella lateral to trochlea",
"Axial (skyline) view most helpful",
"Look for osteochondral fragment",
"Medial patellofemoral ligament avulsion",
"Trochlear dysplasia predisposes",
],
"nerve": "Usually no neurovascular injury",
"color": GREEN,
"treat": "Extension of knee usually reduces it. Medial press to reduce. Immobilise 4-6 wks. MRI to assess MPFL.",
},
{
"joint": "Elbow",
"type": "Posterior (most common)",
"xray": [
"Lateral: olecranon posterior to humerus",
"AP: widened joint space",
"Look for coronoid # (terrible triad)",
"Radial head #: 'Terrible Triad'",
"Monteggia: proximal ulna # + radial head disloc.",
],
"nerve": "Brachial artery, ulnar/radial nerve injury",
"color": GOLD,
"treat": "Conscious sedation + reduction. Check radio-capitellar line post-reduction. Terrible triad = ORIF.",
},
{
"joint": "Ankle (Talar)",
"type": "Fracture-dislocation",
"xray": [
"AP + lateral + mortise views",
"Widened medial clear space (>4mm)",
"Talar tilt on mortise view",
"Look for trimalleolar fracture",
"Weber C with diastasis = unstable",
],
"nerve": "Posterior tibial artery / nerve",
"color": ORANGE,
"treat": "Urgent reduction (skin necrosis risk). Splint. ORIF for fracture-dislocation.",
},
]
cx = Inches(0.25)
cw = Inches(2.52)
ch = Inches(5.6)
for d in dislocations:
c = d["color"]
add_rect(slide, cx, Inches(1.38), cw, ch, DARK_BLUE, line_color=c, line_width=1.2)
add_rect(slide, cx, Inches(1.38), cw, Inches(0.68), c)
add_tb(slide, cx+Inches(0.08), Inches(1.38), cw-Inches(0.16), Inches(0.45),
d["joint"], 15, bold=True, color=WHITE)
add_tb(slide, cx+Inches(0.08), Inches(1.78), cw-Inches(0.16), Inches(0.25),
d["type"], 10, color=LIGHT_BLUE)
add_tb(slide, cx+Inches(0.08), Inches(2.1), cw, Inches(0.25),
"X-RAY SIGNS", 8.5, bold=True, color=GOLD)
y = Inches(2.35)
for f in d["xray"]:
add_tb(slide, cx+Inches(0.12), y, cw-Inches(0.2), Inches(0.33),
f"• {f}", 9.5, color=WHITE)
y += Inches(0.34)
# Nerve warning
add_rect(slide, cx+Inches(0.08), Inches(4.07), cw-Inches(0.16), Inches(0.22), RED_ALERT)
add_tb(slide, cx+Inches(0.1), Inches(4.07), cw-Inches(0.2), Inches(0.22),
f"NERVE: {d['nerve']}", 8.5, bold=True, color=WHITE, v_anchor=MSO_ANCHOR.MIDDLE)
# Treatment
add_multiline_tb(slide, cx+Inches(0.1), Inches(4.35), cw-Inches(0.2), Inches(1.3),
[d["treat"]], 9.5, color=LIGHT_BLUE)
cx += cw + Inches(0.14)
# ══════════════════════════════════════════════════════════════════════════════
# SLIDE 7 – SPINE X-RAYS
# ══════════════════════════════════════════════════════════════════════════════
slide = prs.slides.add_slide(blank)
slide_bg(slide, DARK_BLUE)
header_bar(slide, "Spine X-Ray Interpretation", "Cervical, Thoracic & Lumbar — Systematic Approach")
# Left half: Cervical
add_rect(slide, Inches(0.3), Inches(1.35), Inches(6.0), Inches(5.85), DARK_GREY)
add_rect(slide, Inches(0.3), Inches(1.35), Inches(6.0), Inches(0.45), MID_BLUE)
add_tb(slide, Inches(0.4), Inches(1.38), Inches(5.8), Inches(0.4),
"CERVICAL SPINE", 14, bold=True, color=WHITE)
cerv_items = [
("ALIGNMENT (4 lines)", [
"Anterior vertebral line — smooth lordosis",
"Posterior vertebral line — smooth",
"Spinolaminar line",
"Posterior spinous line",
"BROKEN LINE = dislocation / subluxation",
], GOLD),
("BONES", [
"Vertebral body height (anterior = posterior)",
"Disc space: uniform height",
"Odontoid peg: central, no gap >3mm (ADI)",
"Facet joints: overlapping 'shingles'",
], MID_BLUE),
("SOFT TISSUE", [
"Pre-vertebral soft tissue: <7mm at C2, <22mm at C6",
"Widened = haematoma (high injury suspicion)",
], GREEN),
("PAEDIATRIC PEARL", [
"Pseudo-subluxation C2/3 or C3/4 in children — normal variant",
"Swischuk's line: spinolaminar line C1-C3",
], ORANGE),
]
y = Inches(1.9)
for title, items, col in cerv_items:
add_rect(slide, Inches(0.4), y, Inches(5.8), Inches(0.28), col)
add_tb(slide, Inches(0.5), y, Inches(5.6), Inches(0.28),
title, 10, bold=True, color=DARK_BLUE, v_anchor=MSO_ANCHOR.MIDDLE)
y += Inches(0.3)
for item in items:
add_tb(slide, Inches(0.55), y, Inches(5.55), Inches(0.32),
f"• {item}", 10, color=WHITE)
y += Inches(0.32)
y += Inches(0.05)
# Right half: Lumbar + emergencies
add_rect(slide, Inches(6.65), Inches(1.35), Inches(6.35), Inches(5.85), DARK_GREY)
add_rect(slide, Inches(6.65), Inches(1.35), Inches(6.35), Inches(0.45), GOLD)
add_tb(slide, Inches(6.75), Inches(1.38), Inches(6.15), Inches(0.4),
"LUMBAR SPINE & RED FLAG FINDINGS", 14, bold=True, color=DARK_BLUE)
lumbar = [
("LUMBAR X-RAY CHECKLIST", [
"Alignment: lordosis preserved?",
"Vertebral height: anterior vs posterior (wedge # = compression)",
"Disc spaces: uniform (narrowed = DDD/infection)",
"Pedicles: oval shadows — 'winking owl sign' if pedicle eroded = malignancy",
"SI joints: symmetric (asymmetry = AS / fracture)",
"Spinous processes: interspinous widening = ligament disruption",
], MID_BLUE),
("SPINE INJURY CLASSIFICATIONS", [
"AO Spine: Type A (compression), B (distraction), C (rotation/translation)",
"SLIC cervical: morphology + DLC + neuro; score >4 = surgical",
"TLICS: thoracolumbar; score >4 = surgical",
], GREEN),
("EMERGENCY RED FLAGS", [
"Cauda equina: saddle anaesthesia + bladder/bowel = MRI NOW",
"Cord compression: bilateral neurology, Lhermitte's sign",
"Instability: >3.5mm translation or >11° angulation vs. adjacent",
"Pathological fracture: lytic + minimal trauma = urgent CT/MRI",
], RED_ALERT),
]
y = Inches(1.9)
for title, items, col in lumbar:
add_rect(slide, Inches(6.75), y, Inches(6.15), Inches(0.28), col)
add_tb(slide, Inches(6.85), y, Inches(6.0), Inches(0.28),
title, 10, bold=True, color=DARK_BLUE, v_anchor=MSO_ANCHOR.MIDDLE)
y += Inches(0.3)
for item in items:
add_tb(slide, Inches(6.9), y, Inches(5.95), Inches(0.32),
f"• {item}", 10, color=WHITE)
y += Inches(0.32)
y += Inches(0.08)
# ══════════════════════════════════════════════════════════════════════════════
# SLIDE 8 – CLASSIFICATIONS TABLE
# ══════════════════════════════════════════════════════════════════════════════
slide = prs.slides.add_slide(blank)
slide_bg(slide, DARK_GREY)
header_bar(slide, "Must-Know Classifications", "Examiners expect these classification systems by name")
rows = [
("Fracture / Condition", "Classification System", "Grades / Types", "Key Clinical Point", True),
("NOF Fracture", "Garden I – IV", "I=impacted; II=undisplaced;\nIII=partial disp; IV=full disp", "IV elderly → hemiarthroplasty", False),
("Tibial Plateau", "Schatzker I – VI", "I-IV lateral/medial;\nV bicondylar; VI metaphyseal", "VI = high energy, surgical", False),
("Open Fractures", "Gustilo-Anderson I – III", "I<1cm; II 1-10cm;\nIIIA/B/C >10cm ± vascular", "IIIC = vascular = emergency", False),
("Ankle Fracture", "Weber A / B / C", "A below syndesm; B at;\nC above (unstable)", "Weber C almost always ORIF", False),
("Distal Radius", "Frykman / AO", "Colles=dorsal; Smith=volar;\nBarton=shear", "Check median nerve (CTS)", False),
("Shoulder Dislocation", "Direction + lesion", "Anterior 95%; Posterior (seizure);\nInferior (luxatio erecta)","Post: axillary nerve; Hill-Sachs", False),
("Spinal Cord Injury", "ASIA A – E", "A=complete; B=sensory;\nC/D=motor incomplete; E=normal", "Immediate decompression if incomplete", False),
("Cervical Spine Injury", "SLIC Score", "Morphology + DLC + Neuro\nScore 0-11; >4 = surgery", "Check pre-vertebral soft tissue", False),
("Periprosthetic Hip #", "Vancouver A / B / C", "B1=well-fixed (ORIF);\nB2=loose stem (revision); B3", "B2 most important: revision surgery", False),
("Femoral Head AVN", "Ficat-Arlet I – IV", "I: normal X-ray;\nII: cysts; III: crescent; IV: collapse", "MRI detects pre-collapse (Ficat I/II)", False),
("Paediatric # Physeal", "Salter-Harris I – V", "I: through physis; II: metaphysis;\nIV: epiphysis; V: crush", "V = worst prognosis (growth arrest)", False),
]
col_w = [Inches(2.0), Inches(2.3), Inches(3.5), Inches(3.5), Inches(1.8)]
col_x = [Inches(0.25), Inches(2.35), Inches(4.75), Inches(8.35), Inches(11.95)]
row_h = Inches(0.48)
y = Inches(1.38)
for ri, row in enumerate(rows):
if row[4]:
bg = MID_BLUE
fc = WHITE
fs = 11
bold = True
else:
bg = DARK_BLUE if ri % 2 == 0 else DARK_GREY
fc = WHITE
fs = 10.5
bold = False
add_rect(slide, Inches(0.25), y, Inches(13.0), row_h, bg)
for ci, text in enumerate(row[:4]):
add_multiline_tb(slide, col_x[ci]+Inches(0.05), y+Inches(0.03),
col_w[ci+1 if ci < 3 else 3]-Inches(0.1) if ci < 3 else Inches(1.3),
row_h-Inches(0.06),
[text], fs, bold=bold, color=fc)
y += row_h
# ══════════════════════════════════════════════════════════════════════════════
# SLIDE 9 – PATHOLOGICAL & SPECIAL X-RAYS
# ══════════════════════════════════════════════════════════════════════════════
slide = prs.slides.add_slide(blank)
slide_bg(slide, DARK_BLUE)
header_bar(slide, "Pathological, Paediatric & Special Findings",
"Patterns to recognise beyond standard trauma fractures")
# Three columns
sections = [
{
"title": "PATHOLOGICAL FRACTURES",
"color": RED_ALERT,
"items": [
("What it is", "Fracture through abnormal / weakened bone from minimal trauma"),
("Common causes", "Metastases (breast, lung, prostate, renal, thyroid)\nMultiple myeloma\nOsteoporosis (vertebral wedge)\nPaget's disease\nPrimary bone tumours (rare)"),
("X-ray clues", "Lytic lesion at fracture site\nMixed lytic/sclerotic (Paget's)\nVertebral body collapse without proportional trauma\nMultiple fractures at low energy"),
("Management", "Biopsy if unknown primary\nOrthopedic fixation (prophylactic if >50% cortex involved)\nOncology referral"),
],
},
{
"title": "PAEDIATRIC FRACTURES",
"color": GREEN,
"items": [
("Unique patterns", "Greenstick (buckled cortex one side)\nTorus/buckle (axial compression)\nPlastic deformation (bowing)\nSalter-Harris physeal injuries I–V"),
("Salter-Harris", "I: Physis only (X-ray may be normal)\nII: Physis + metaphysis (most common)\nIII: Physis + epiphysis\nIV: All three\nV: Crush — worst prognosis"),
("Non-accidental injury", "Multiple # at different healing stages\nPosterior rib # (highly specific)\nMetaphyseal corner # (classic)\nSubdural haematoma + spiral #"),
("Elbow CRITOE", "Capitellum 1yr → Radial head 3yr\nInt epicondyle 5yr → Trochlea 7yr\nOlecranon 9yr → Ext epicondyle 11yr"),
],
},
{
"title": "SOFT TISSUE & SPECIAL SIGNS",
"color": GOLD,
"items": [
("Fat pad signs (elbow)", "Anterior fat pad (sail sign): effusion\nPosterior fat pad: ALWAYS ABNORMAL — treat as occult # / radial head #"),
("Segond fracture", "Small avulsion lateral tibial plateau\nHIGHLY SPECIFIC for ACL tear\nLook carefully on AP knee X-ray"),
("Pellegrini-Stieda", "Calcification at medial femoral condyle\nIndicates old MCL injury / ossification"),
("Bankart & Hill-Sachs", "Bankart: ant-inferior glenoid rim avulsion\nHill-Sachs: posterolateral humeral head impaction\nBoth = anterior shoulder instability"),
("Winking Owl Sign", "Spine X-ray: one pedicle shadow absent\n= Pedicle erosion by metastasis"),
],
},
]
cx = Inches(0.3)
cw = Inches(4.2)
for sec in sections:
c = sec["color"]
add_rect(slide, cx, Inches(1.38), cw, Inches(5.85), DARK_GREY, line_color=c, line_width=1.5)
add_rect(slide, cx, Inches(1.38), cw, Inches(0.45), c)
add_tb(slide, cx+Inches(0.1), Inches(1.4), cw-Inches(0.2), Inches(0.4),
sec["title"], 13, bold=True, color=DARK_BLUE)
y = Inches(1.9)
for label, content in sec["items"]:
add_rect(slide, cx+Inches(0.1), y, cw-Inches(0.2), Inches(0.26), MID_BLUE)
add_tb(slide, cx+Inches(0.15), y, cw-Inches(0.3), Inches(0.26),
label.upper(), 9, bold=True, color=WHITE, v_anchor=MSO_ANCHOR.MIDDLE)
y += Inches(0.28)
add_multiline_tb(slide, cx+Inches(0.15), y, cw-Inches(0.3), Inches(1.0),
[content], 10.5, color=WHITE)
lines = content.count("\n") + 1
y += Inches(0.35 + 0.3*(lines-1))
y += Inches(0.06)
cx += cw + Inches(0.17)
# ══════════════════════════════════════════════════════════════════════════════
# SLIDE 10 – OSCE QUICK REFERENCE / SUMMARY
# ══════════════════════════════════════════════════════════════════════════════
slide = prs.slides.add_slide(blank)
slide_bg(slide, DARK_GREY)
header_bar(slide, "OSCE Quick Reference Card", "High-yield summary — last review before your exam")
# 6-item grid
items = [
("ABCDS", "Alignment • Bones • Cartilage\nDensity • Soft Tissue\nUse for EVERY X-ray", MID_BLUE),
("6 Ps Vascular", "Pain (passive stretch) • Pallor\nPulselessness • Paraesthesia\nParalysis • Perishingly Cold", RED_ALERT),
("Ottawa Rules", "X-ray ankle if:\n• Malleous tenderness\n• Navicular tenderness\n• 5th MT base tenderness\n• Cannot weight-bear 4 steps", GOLD),
("Scaphoid Rule", "Snuffbox tenderness = treat\nas scaphoid # even if X-ray normal\nRisk: AVN proximal pole\nMRI or repeat X-ray at 14 days", GREEN),
("NOF Fracture", "Shortened + externally rotated\nGarden I–IV\nIntracapsular: hemiarthroplasty\nExtracapsular: DHS / PFN", MID_BLUE),
("Compartment Sx", "EMERGENCY: pain on\npassive stretch + tense compartment\n→ Fasciotomy IMMEDIATELY\nCommon: tibial #, forearm #", RED_ALERT),
("ACL Injury", "Haemarthrosis + pop\nLachman's most sensitive\nSegond # on X-ray = ACL\nMRI to confirm", ORANGE),
("Open Fracture", "Gustilo-Anderson I–III\nAbx within 1 hour\nPhoto → sterile dressing\nTheatre washout <6h (IIIB/C)", GREEN),
("Cauda Equina", "EMERGENCY: saddle anaesthesia\n+ bladder/bowel dysfunction\n+ bilateral leg weakness\n→ MRI + surgical decompression", RED_ALERT),
("Elbow Paediatric", "Posterior fat pad = ABNORMAL\nCRITOE (1-3-5-7-9-11 years)\nAnterior humeral line\nRadiocapitellar line", GOLD),
("Shoulder Disloc.", "95% anterior\nAxillary nerve: test BEFORE + AFTER\nHill-Sachs + Bankart\nPosterior = seizure/electrocution", MID_BLUE),
("Spinal Trauma", "ASIA A–E\nCauda equina = emergency\nSLIC >4 = surgery\nPre-vertebral STW on C-spine X-ray", ORANGE),
]
col_w = Inches(3.15)
col_h = Inches(1.55)
gx = Inches(0.3)
gy = Inches(1.38)
cols = 4
for i, (title, content, col) in enumerate(items):
row = i // cols
c_ = i % cols
bx = gx + c_*(col_w + Inches(0.12))
by = gy + row*(col_h + Inches(0.1))
add_rect(slide, bx, by, col_w, col_h, DARK_BLUE, line_color=col, line_width=1.5)
add_rect(slide, bx, by, col_w, Inches(0.3), col)
add_tb(slide, bx+Inches(0.1), by, col_w-Inches(0.2), Inches(0.3),
title, 11, bold=True, color=DARK_BLUE, v_anchor=MSO_ANCHOR.MIDDLE)
add_multiline_tb(slide, bx+Inches(0.1), by+Inches(0.32), col_w-Inches(0.2),
col_h-Inches(0.38), [content], 10, color=WHITE)
# ══════════════════════════════════════════════════════════════════════════════
# SAVE
# ══════════════════════════════════════════════════════════════════════════════
out_path = "/tmp/workspace/ortho-xray-pptx/Ortho_Xray_Interpretation_OSCE.pptx"
buf = BytesIO()
prs.save(buf)
with open(out_path, "wb") as f:
f.write(buf.getvalue())
print(f"Saved → {out_path}")