~/fracture-study-guide/build_pdf.py
#!/usr/bin/env python3
"""
Fracture Classifications Study Guide PDF Generator
Uses ReportLab for rich PDF layout with images + tables
"""
import os
import requests
from io import BytesIO
from PIL import Image as PILImage
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import cm, mm
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.colors import HexColor, white, black, Color
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
Image, KeepTogether, HRFlowable, PageBreak
)
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
from reportlab.lib import colors
# ── Output path ─────────────────────────────────────────────────────────────
OUTPUT = "/home/daytona/workspace/fracture-study-guide/Fracture_Classifications_Study_Guide.pdf"
IMG_DIR = "/home/daytona/workspace/fracture-study-guide/images"
os.makedirs(IMG_DIR, exist_ok=True)
# ── Colour palette ───────────────────────────────────────────────────────────
C_NAVY = HexColor("#1a2e4a")
C_TEAL = HexColor("#0d7377")
C_LIGHT = HexColor("#e8f4f8")
C_ACCENT = HexColor("#f0a500")
C_HEADER_BG = HexColor("#1a2e4a")
C_ROW_ODD = HexColor("#f5fafd")
C_ROW_EVEN = HexColor("#ddeef5")
C_RED_LIGHT = HexColor("#fdf0f0")
# ── Image registry ───────────────────────────────────────────────────────────
IMAGES = {
"neer_4parts": "https://cdn.orris.care/cdss_images/6c6ffc06744c9b10628ff061059a5cb7e8b1aa7b6d5d5fa3ea12b9ebbf624ebe.png",
"robinson_clav": "https://cdn.orris.care/cdss_images/09d9b1fb1496328f60af806119e6e2ecf611f76586a69b8fe9ea99563326dcf2.png",
"neer_distal_clav": "https://cdn.orris.care/cdss_images/1ee21158a808d73c0094df5ea4111845ce966f6b0442920dcc37973f6b69fc9a.png",
"ada_miller_scap": "https://cdn.orris.care/cdss_images/b39f5d547ad1bd63f4126cd09b369a01139d06ae04c56c4243dd09520e36b517.png",
"condyle_milch": "https://cdn.orris.care/cdss_images/c9f0b92399e18d6af7d7c89fe02b4268dc830910ec4f96173bfb5dcf81d1fcf5.png",
"mason_I": "https://cdn.orris.care/cdss_images/568fa1185bc4d2bf1e94c34db387d4f6cb9fc8f8b22b1667968a71a89e23eb78.png",
"mason_III": "https://cdn.orris.care/cdss_images/88d4e9cfd027256936788ebc05a82af58a421dca6beb0d957a7cd87e38fa446f.png",
"monteggia_xr": "https://cdn.orris.care/cdss_images/2402ebcd7f5d9d0fdad83de75d41085fe2e1c5d5d4b7c43b85509b8fb7f3a43d.png",
"galeazzi_xr": "https://cdn.orris.care/cdss_images/d641870085d39390c1b12dfae9100f236c8161ad7159e0fd3648d4bb30826eff.png",
"distal_radius": "https://cdn.orris.care/cdss_images/e65d21de0fc813faa451e0ce0439fd70476a4b041ff4460b9f713464efe1300d.png",
"ped_radius_a": "https://cdn.orris.care/cdss_images/9a247a27480aef8b4ddb7013db1ea676a17d417746c21213109eda2f75fec0aa.png",
"ped_radius_b": "https://cdn.orris.care/cdss_images/7ab31f3162f6b47f64cdaaff731425e72adf34f2c8b769dad98b20579d06c932.png",
"medial_clav_ct": "https://cdn.orris.care/cdss_images/3e8c4672bc3c4bd91388eb71d67a9d835c1248ed0402853566767081693071db.png",
"glenoid_fract": "https://cdn.orris.care/cdss_images/a077a86a347fc98d0bc6c8585662a14c2145b94554a5020d4fb6ec8b9015d102.png",
}
def download_images():
paths = {}
for name, url in IMAGES.items():
dest = os.path.join(IMG_DIR, f"{name}.png")
if not os.path.exists(dest):
try:
r = requests.get(url, timeout=20)
with open(dest, "wb") as f:
f.write(r.content)
print(f" Downloaded: {name}")
except Exception as e:
print(f" FAILED: {name} — {e}")
dest = None
paths[name] = dest
return paths
def img(paths, key, width=8*cm, max_height=None):
p = paths.get(key)
if not p or not os.path.exists(p):
return Spacer(1, 0.5*cm)
try:
im = PILImage.open(p)
w, h = im.size
aspect = h / w
height = width * aspect
if max_height and height > max_height:
height = max_height
width = height / aspect
return Image(p, width=width, height=height)
except Exception as e:
print(f" Image error {key}: {e}")
return Spacer(1, 0.5*cm)
# ── Styles ───────────────────────────────────────────────────────────────────
def make_styles():
base = getSampleStyleSheet()
styles = {}
styles["cover_title"] = ParagraphStyle(
"cover_title", fontName="Helvetica-Bold", fontSize=28,
textColor=white, alignment=TA_CENTER, leading=34, spaceAfter=10)
styles["cover_sub"] = ParagraphStyle(
"cover_sub", fontName="Helvetica", fontSize=14,
textColor=HexColor("#cce8f0"), alignment=TA_CENTER, leading=18)
styles["section"] = ParagraphStyle(
"section", fontName="Helvetica-Bold", fontSize=15,
textColor=white, alignment=TA_LEFT, leading=20,
leftIndent=8, spaceBefore=4, spaceAfter=4)
styles["subsection"] = ParagraphStyle(
"subsection", fontName="Helvetica-Bold", fontSize=12,
textColor=C_NAVY, leading=16, spaceBefore=8, spaceAfter=4)
styles["body"] = ParagraphStyle(
"body", fontName="Helvetica", fontSize=9.5,
textColor=HexColor("#222222"), leading=14,
spaceBefore=3, spaceAfter=3)
styles["caption"] = ParagraphStyle(
"caption", fontName="Helvetica-Oblique", fontSize=8,
textColor=HexColor("#555555"), alignment=TA_CENTER, leading=11,
spaceBefore=2, spaceAfter=6)
styles["key_point"] = ParagraphStyle(
"key_point", fontName="Helvetica-Bold", fontSize=9,
textColor=C_NAVY, leading=13, leftIndent=10,
bulletIndent=0, spaceBefore=2, spaceAfter=2)
styles["toc_entry"] = ParagraphStyle(
"toc_entry", fontName="Helvetica", fontSize=10,
textColor=C_NAVY, leading=16, leftIndent=16)
return styles
# ── Table helper ─────────────────────────────────────────────────────────────
def styled_table(data, col_widths, header_bg=C_HEADER_BG):
style = TableStyle([
("BACKGROUND", (0, 0), (-1, 0), header_bg),
("TEXTCOLOR", (0, 0), (-1, 0), white),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("FONTSIZE", (0, 0), (-1, 0), 9),
("ALIGN", (0, 0), (-1, 0), "CENTER"),
("ROWBACKGROUNDS",(0, 1), (-1, -1), [C_ROW_ODD, C_ROW_EVEN]),
("FONTNAME", (0, 1), (-1, -1), "Helvetica"),
("FONTSIZE", (0, 1), (-1, -1), 8.5),
("ALIGN", (0, 1), (-1, -1), "LEFT"),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("GRID", (0, 0), (-1, -1), 0.4, HexColor("#b0ccd8")),
("TOPPADDING", (0, 0), (-1, -1), 4),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
("LEFTPADDING", (0, 0), (-1, -1), 6),
("RIGHTPADDING", (0, 0), (-1, -1), 6),
("ROUNDEDCORNERS",[2]),
])
t = Table(data, colWidths=col_widths, repeatRows=1)
t.setStyle(style)
return t
def section_banner(text, styles):
banner_data = [[Paragraph(text, styles["section"])]]
t = Table(banner_data, colWidths=[17.2*cm])
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), C_NAVY),
("LEFTPADDING", (0,0), (-1,-1), 10),
("RIGHTPADDING", (0,0), (-1,-1), 10),
("TOPPADDING", (0,0), (-1,-1), 6),
("BOTTOMPADDING",(0,0), (-1,-1), 6),
("ROUNDEDCORNERS",[3]),
]))
return t
def accent_box(text, styles, bg=C_LIGHT):
data = [[Paragraph(text, styles["body"])]]
t = Table(data, colWidths=[17.2*cm])
t.setStyle(TableStyle([
("BACKGROUND", (0,0),(-1,-1), bg),
("LEFTPADDING", (0,0),(-1,-1), 10),
("RIGHTPADDING", (0,0),(-1,-1), 10),
("TOPPADDING", (0,0),(-1,-1), 6),
("BOTTOMPADDING", (0,0),(-1,-1), 6),
("BOX", (0,0),(-1,-1), 1, C_TEAL),
("ROUNDEDCORNERS",[3]),
]))
return t
# ── Cover page ───────────────────────────────────────────────────────────────
def build_cover(story, styles):
# Blue background band
bg_data = [[
Paragraph("FRACTURE CLASSIFICATIONS", styles["cover_title"]),
]]
t = Table(bg_data, colWidths=[17.2*cm])
t.setStyle(TableStyle([
("BACKGROUND", (0,0),(-1,-1), C_NAVY),
("TOPPADDING", (0,0),(-1,-1), 40),
("BOTTOMPADDING", (0,0),(-1,-1), 10),
("LEFTPADDING", (0,0),(-1,-1), 10),
("RIGHTPADDING", (0,0),(-1,-1), 10),
]))
story.append(t)
sub_data = [[Paragraph("A Comparative Study Guide with Diagrams", styles["cover_sub"])]]
t2 = Table(sub_data, colWidths=[17.2*cm])
t2.setStyle(TableStyle([
("BACKGROUND", (0,0),(-1,-1), C_TEAL),
("TOPPADDING", (0,0),(-1,-1), 14),
("BOTTOMPADDING", (0,0),(-1,-1), 14),
("LEFTPADDING", (0,0),(-1,-1), 10),
("RIGHTPADDING", (0,0),(-1,-1), 10),
]))
story.append(t2)
story.append(Spacer(1, 0.6*cm))
# TOC
toc_items = [
"1. Shoulder Girdle Fractures",
" • Clavicle Fractures — Robinson, Neer, OTA/AO",
" • Scapular Fractures — Ada-Miller, OTA/AO",
"2. Proximal Humerus Fractures — Neer Classification",
"3. Distal Humerus Fractures — Milch, Supracondylar Gartland",
"4. Radial Head Fractures — Mason Classification",
"5. Forearm Fractures",
" • Monteggia — Bado Classification",
" • Galeazzi Fracture-Dislocation",
" • Both-Bone / Ulnar Nightstick",
"6. Distal Radius Fractures — OTA/AO, Named Eponyms",
"7. Scaphoid Fractures",
"8. Pediatric Fractures — Buckle, Greenstick, Salter-Harris",
"9. Quick Reference Comparison Table",
]
toc_entries = []
for item in toc_items:
toc_entries.append([Paragraph(item, styles["toc_entry"])])
toc_table = Table(toc_entries, colWidths=[17.2*cm])
toc_table.setStyle(TableStyle([
("BACKGROUND", (0,0),(-1,-1), C_LIGHT),
("TOPPADDING", (0,0),(-1,-1), 3),
("BOTTOMPADDING", (0,0),(-1,-1), 3),
("LEFTPADDING", (0,0),(-1,-1), 16),
("RIGHTPADDING", (0,0),(-1,-1), 10),
("BOX", (0,0),(-1,-1), 1.2, C_TEAL),
]))
story.append(toc_table)
story.append(Spacer(1, 0.6*cm))
src_text = ("<b>Sources:</b> Miller's Review of Orthopaedics 9th Ed. | "
"Rockwood & Green's Fractures in Adults 10th Ed. 2025 | "
"Tintinalli's Emergency Medicine | Bailey & Love's Short Practice of Surgery 28th Ed. | "
"Campbell's Operative Orthopaedics 15th Ed. 2026")
story.append(accent_box(src_text, styles))
story.append(PageBreak())
# ── Section 1: Shoulder Girdle ───────────────────────────────────────────────
def build_shoulder(story, styles, paths):
story.append(section_banner("SECTION 1 — SHOULDER GIRDLE FRACTURES", styles))
story.append(Spacer(1, 0.3*cm))
# --- Clavicle ---
story.append(Paragraph("A. Clavicle Fractures", styles["subsection"]))
story.append(Paragraph(
"Clavicle fractures account for ~3% of all fractures and ~35% of shoulder girdle injuries. "
"The most common site is the middle third (80%), followed by lateral (15%) and medial (5%).",
styles["body"]))
story.append(Spacer(1,0.2*cm))
# Robinson classification diagram
story.append(Paragraph("Robinson Classification (Edinburgh) — Full Clavicle", styles["key_point"]))
story.append(img(paths, "robinson_clav", width=14*cm))
story.append(Paragraph(
"Figure 1. Robinson Classification of clavicle fractures. Cross-references Allman (Group I-III) "
"and Craig classifications. Source: Rockwood & Green's Fractures in Adults 10th Ed., p.1200",
styles["caption"]))
robinson_data = [
["Robinson Type", "Location", "Sub-types", "Allman Equivalent"],
["Type 1", "Medial third", "1A = undisplaced; 1B = displaced", "Group III"],
["Type 2", "Middle third (shaft)", "2A = cortical alignment; 2B = displaced (simple/comminuted)", "Group I"],
["Type 3", "Lateral third", "3A = extra-articular; 3B = displaced / intra-articular", "Group II"],
]
story.append(styled_table(robinson_data, [3.2*cm, 3.2*cm, 7.5*cm, 3.3*cm]))
story.append(Spacer(1, 0.4*cm))
# Neer distal clavicle
story.append(Paragraph("Neer / Rockwood Classification — Distal Clavicle Fractures", styles["key_point"]))
img_row = [[
img(paths, "neer_distal_clav", width=8*cm),
Table([[
Paragraph("<b>Type I</b> — Distal to CC ligaments; ligaments intact; minimal displacement.<br/><br/>"
"<b>Type IIA</b> — Both conoid and trapezoid attached to distal fragment; medial fragment displaced superiorly.<br/><br/>"
"<b>Type IIB</b> — Conoid detached from medial fragment; most unstable.<br/><br/>"
"<b>Type III</b> — Intra-articular extension into AC joint.<br/><br/>"
"<font color='#c00000'><b>Clinical note:</b></font> Type II = high nonunion risk → ORIF usually required.",
styles["body"])
]], colWidths=[8.5*cm],
style=[("VALIGN",(0,0),(-1,-1),"TOP"),("LEFTPADDING",(0,0),(-1,-1),8)])
]]
story.append(Table(img_row, colWidths=[8.5*cm, 8.7*cm]))
story.append(Paragraph(
"Figure 2. Distal clavicle fracture: (A) Type I — ligaments intact; (B) Type II — ligament rupture with wide displacement. "
"Source: Miller's Review of Orthopaedics 9th Ed., p.900",
styles["caption"]))
# Medial clavicle CT
story.append(Spacer(1, 0.2*cm))
story.append(Paragraph("Medial Clavicle Fracture — Imaging", styles["key_point"]))
story.append(img(paths, "medial_clav_ct", width=14*cm, max_height=7*cm))
story.append(Paragraph(
"Figure 3. Medial clavicle fracture. A: AP radiograph shows subtle asymmetry. "
"B/C: CT demonstrates posterior shaft displacement impinging mediastinal structures (arrows). "
"D: Post-ORIF with plate extending onto sternum. Source: Rockwood & Green's 10th Ed., p.1199",
styles["caption"]))
story.append(Spacer(1, 0.3*cm))
accent_txt = ("<b>Key clinical points — Clavicle fractures:</b><br/>"
"• Nonunion risk increases with >100% displacement of mid-shaft fractures<br/>"
"• Absolute operative indications: open fracture, neurovascular compromise<br/>"
"• Relative indications: shortening >2 cm, significant displacement, nonunion<br/>"
"• Medial fractures require CT to assess posterior displacement and mediastinal compromise")
story.append(accent_box(accent_txt, styles, bg=HexColor("#fff8e6")))
story.append(Spacer(1, 0.4*cm))
# --- Scapula ---
story.append(HRFlowable(width="100%", thickness=1, color=C_TEAL))
story.append(Spacer(1, 0.2*cm))
story.append(Paragraph("B. Scapular Fractures", styles["subsection"]))
story.append(Paragraph(
"Scapular fractures represent <1% of all fractures. Due to the force required, ~80–90% have associated injuries "
"(rib fractures, pneumothorax, brachial plexus injury). Management is usually nonoperative.",
styles["body"]))
story.append(Paragraph("Ada-Miller Classification (Most Widely Used)", styles["key_point"]))
img_row2 = [[
img(paths, "ada_miller_scap", width=8*cm),
Table([[
Paragraph("<b>Type I — Process fractures</b><br/>"
"IA = Acromion<br/>IB = Scapular spine<br/>IC = Coracoid process<br/><br/>"
"<b>Type II — Neck fractures</b><br/>"
"IIA = Surgical neck<br/>IIB = Trans-spinous neck<br/>IIC = Neck inferior to spine<br/><br/>"
"<b>Type III — Glenoid fossa</b> (intra-articular, 25%)<br/><br/>"
"<b>Type IV — Scapular body</b> (~50% of all scapular fractures)",
styles["body"])
]], colWidths=[8.5*cm],
style=[("VALIGN",(0,0),(-1,-1),"TOP"),("LEFTPADDING",(0,0),(-1,-1),8)])
]]
story.append(Table(img_row2, colWidths=[8.5*cm, 8.7*cm]))
story.append(Paragraph(
"Figure 4. Ada-Miller classification of scapular fractures. "
"Source: Rockwood & Green's Fractures in Adults 10th Ed., p.1158",
styles["caption"]))
story.append(Spacer(1, 0.2*cm))
story.append(Paragraph("OTA/AO Glenoid Fossa — Multifragmentary (14F2.2)", styles["key_point"]))
story.append(img(paths, "glenoid_fract", width=7*cm))
story.append(Paragraph(
"Figure 5. OTA/AO group 14F2.2 — central fracture-dislocation of the glenoid fossa "
"(multifragmentary, ≥3 articular fragments). Source: Rockwood & Green's 10th Ed., p.1161",
styles["caption"]))
ada_data = [
["OTA/AO Segment 14", "Sub-type", "Description"],
["14A — Processes", "A1, A2, A3", "Coracoid / Acromion / Scapular spine"],
["14B — Body", "B1, B2", "B1: exits ≤2 points; B2: exits ≥3 points"],
["14F — Glenoid fossa","F0, F1, F2", "F0: neck; F1: simple articular; F2: multifragmentary"],
]
story.append(styled_table(ada_data, [5.2*cm, 3.5*cm, 8.5*cm]))
story.append(PageBreak())
# ── Section 2: Proximal Humerus ──────────────────────────────────────────────
def build_prox_humerus(story, styles, paths):
story.append(section_banner("SECTION 2 — PROXIMAL HUMERUS FRACTURES", styles))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph(
"Proximal humerus fractures account for ~5% of all fractures, commonly in elderly osteoporotic patients. "
"The Neer system is based on displacement of four anatomical segments. A 'part' is defined as "
"displacement >1 cm or angulation >45°.",
styles["body"]))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("Neer Classification — Four Anatomical Parts", styles["key_point"]))
story.append(img(paths, "neer_4parts", width=11*cm))
story.append(Paragraph(
"Figure 6. Proximal humeral fracture — Neer four parts: 1 = articular head, 2 = lesser tuberosity, "
"3 = greater tuberosity, 4 = humeral shaft. Arrows indicate muscle contraction vectors. "
"Source: Miller's Review of Orthopaedics 9th Ed., p.901",
styles["caption"]))
neer_data = [
["Neer Type", "Parts Displaced", "Description", "Treatment"],
["1-Part", "None", "Any # of fracture lines; no significant displacement", "Sling, early ROM"],
["2-Part", "1", "Surgical neck (most common); greater/lesser tuberosity", "ORIF or CRPP"],
["3-Part", "2", "Neck + greater OR lesser tuberosity displaced", "ORIF; consider RSA in elderly"],
["4-Part", "3", "Head + shaft + both tuberosities all displaced", "RSA; ORIF only in young pts"],
["Head-split","—", "Fracture through the articular surface", "Hemiarthroplasty / RSA"],
]
story.append(styled_table(neer_data, [2.5*cm, 2.5*cm, 7.5*cm, 4.7*cm]))
story.append(Spacer(1, 0.3*cm))
accent_txt = ("<b>AVN Risk Factors (Hertel Criteria):</b> medial periosteal hinge disruption | "
"medial metadiaphyseal extension <8 mm (calcar <8 mm) | fracture complexity | "
"displacement >10 mm | angulation >45°<br/>"
"<b>Most common complication:</b> screw cutout | "
"<b>Most common nonunion site:</b> two-part surgical neck fracture")
story.append(accent_box(accent_txt, styles, bg=C_RED_LIGHT))
story.append(PageBreak())
# ── Section 3: Distal Humerus ────────────────────────────────────────────────
def build_distal_humerus(story, styles, paths):
story.append(section_banner("SECTION 3 — DISTAL HUMERUS FRACTURES", styles))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("A. Condyle Fractures — Milch Classification", styles["subsection"]))
story.append(img(paths, "condyle_milch", width=12*cm))
story.append(Paragraph(
"Figure 7. Humeral condyle fractures — Milch Types I and II for lateral (left) and medial (right) condyles. "
"Source: Miller's Review of Orthopaedics 9th Ed., p.903",
styles["caption"]))
milch_data = [
["Type", "Lateral Condyle", "Medial Condyle", "Stability", "Treatment"],
["Type I", "Lateral trochlear ridge INTACT", "Medial trochlear ridge INTACT", "Stable (hinge intact)", "Immobilise in supination (lat) or pronation (med)"],
["Type II", "Fracture THROUGH lateral trochlear ridge", "Fracture THROUGH medial trochlear ridge", "UNSTABLE — elbow may dislocate", "ORIF"],
]
story.append(styled_table(milch_data, [1.8*cm, 3.5*cm, 3.5*cm, 2.5*cm, 5.9*cm]))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("B. Supracondylar Fractures (Pediatric) — Gartland Classification", styles["subsection"]))
gartland_data = [
["Gartland Type", "Description", "Periosteal Hinge", "Treatment"],
["Type I", "Nondisplaced or minimally displaced", "Intact", "Above-elbow cast 3 weeks"],
["Type II", "Displaced with posterior cortex intact","Intact posteriorly", "Closed reduction + cast; K-wire if unstable"],
["Type III","Completely displaced, no cortical contact","Absent", "CRPP (percutaneous K-wires) mandatory"],
["Type IV", "Multidirectional instability (Leitch modification)","Circumferentially disrupted","CRPP in OR"],
]
story.append(styled_table(gartland_data, [3*cm, 5*cm, 3.7*cm, 5.5*cm]))
story.append(Spacer(1, 0.2*cm))
accent_txt = ("<b>Feared complication:</b> Volkmann's ischemic contracture (missed compartment syndrome). "
"Avoid deep elbow flexion if significant swelling present.<br/>"
"<b>Neurovascular check:</b> White pulseless hand = surgical emergency; "
"Pink pulseless hand = reduce first, reassess.")
story.append(accent_box(accent_txt, styles, bg=C_RED_LIGHT))
story.append(PageBreak())
# ── Section 4: Radial Head ───────────────────────────────────────────────────
def build_radial_head(story, styles, paths):
story.append(section_banner("SECTION 4 — RADIAL HEAD FRACTURES (MASON CLASSIFICATION)", styles))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph(
"Radial head fractures are the most common elbow fracture in adults. The Mason classification "
"(modified by Broberg & Morrey, and Johnston) guides treatment decisions.",
styles["body"]))
story.append(Spacer(1, 0.3*cm))
mason_row = [[
Table([[
Paragraph("<b>Type I — Nondisplaced</b>", styles["key_point"]),
img(paths, "mason_I", width=5*cm),
Paragraph("Marginal fracture, nondisplaced", styles["caption"]),
]], style=[("ALIGN",(0,0),(-1,-1),"CENTER")]),
Table([[
Paragraph("<b>Type III — Comminuted</b>", styles["key_point"]),
img(paths, "mason_III", width=5*cm),
Paragraph("Entire head involved, 3+ fragments", styles["caption"]),
]], style=[("ALIGN",(0,0),(-1,-1),"CENTER")]),
]]
story.append(Table(mason_row, colWidths=[8.6*cm, 8.6*cm]))
story.append(Paragraph(
"Figure 8. Mason Classification Types I and III. Source: Miller's Review of Orthopaedics 9th Ed., p.904",
styles["caption"]))
mason_data = [
["Type", "Fracture Pattern", "Block to Motion", "Treatment"],
["I", "Nondisplaced marginal fracture", "None", "Sling ≤7 days, then early ROM"],
["II", "Partial articular, displaced", "May exist","ORIF if block or instability; else early ROM"],
["III","Comminuted, entire radial head involved", "Usually", "≥3 fragments → radial head replacement; <3 → ORIF"],
["IV", "Any radial head fracture + elbow dislocation","Variable","Surgical; excision NEVER without replacement"],
]
story.append(styled_table(mason_data, [1.5*cm, 5.5*cm, 3.5*cm, 6.7*cm]))
story.append(Spacer(1, 0.2*cm))
accent_txt = ("<b>Essex-Lopresti injury:</b> Radial head fracture + interosseous membrane disruption + DRUJ instability. "
"Missing this = progressive radial shortening. Always assess DRUJ when treating radial head fractures.<br/>"
"<b>Safe zone for ORIF:</b> 110° arc on lateral side (between radial styloid and Lister tubercle) — "
"arm in neutral/pronation to protect the posterior interosseous nerve (PIN).")
story.append(accent_box(accent_txt, styles, bg=HexColor("#fff8e6")))
story.append(PageBreak())
# ── Section 5: Forearm ───────────────────────────────────────────────────────
def build_forearm(story, styles, paths):
story.append(section_banner("SECTION 5 — FOREARM FRACTURES", styles))
story.append(Spacer(1, 0.3*cm))
# Monteggia
story.append(Paragraph("A. Monteggia Fracture-Dislocation (Bado Classification)", styles["subsection"]))
story.append(Paragraph(
"Proximal ulna fracture + radial head dislocation. Key rule: the apex of the ulnar fracture "
"<b>points in the direction of radial head dislocation</b>. Missed in ~25% of cases.",
styles["body"]))
story.append(img(paths, "monteggia_xr", width=7*cm, max_height=9*cm))
story.append(Paragraph(
"Figure 9. Monteggia fracture-dislocation — comminuted proximal ulna with anterior radial head dislocation. "
"Source: Tintinalli's Emergency Medicine",
styles["caption"]))
bado_data = [
["Bado Type", "Ulna Fracture", "Radial Head Direction", "Frequency", "Population"],
["I", "Apex anterior angulation", "Anterior dislocation", "Most common in children", "Children"],
["II", "Apex posterior angulation", "Posterior dislocation", "Up to 80% in adults", "Adults (most common)"],
["III","Metaphyseal fracture", "Lateral dislocation", "Rare", "Almost exclusively children"],
["IV", "Proximal ulna + proximal radius","Anterior dislocation", "Rare", "Adults only"],
]
story.append(styled_table(bado_data, [1.8*cm, 4.5*cm, 4.2*cm, 3.5*cm, 3.2*cm]))
story.append(Spacer(1, 0.2*cm))
story.append(accent_box(
"<b>Treatment:</b> All Monteggia fractures in adults → ORIF of ulna. "
"Radial head usually reduces spontaneously after anatomic ulna reduction. "
"If not → nonanatomic ulna reduction is the most common cause; open reduction via separate approach if ulna is anatomic.",
styles, bg=C_LIGHT))
story.append(Spacer(1, 0.4*cm))
# Galeazzi
story.append(HRFlowable(width="100%", thickness=1, color=C_TEAL))
story.append(Spacer(1, 0.2*cm))
story.append(Paragraph("B. Galeazzi Fracture-Dislocation", styles["subsection"]))
story.append(Paragraph(
"Distal radial shaft fracture + DRUJ dislocation. Also called 'fracture of necessity' "
"because ORIF is always required. Reverse of Monteggia: <b>radius fractures, ulna dislocates</b>.",
styles["body"]))
story.append(img(paths, "galeazzi_xr", width=14*cm, max_height=7*cm))
story.append(Paragraph(
"Figure 10. Galeazzi fracture-dislocation — radial shaft fracture with DRUJ disruption (AP and lateral). "
"Source: Tintinalli's Emergency Medicine",
styles["caption"]))
galeazzi_data = [
["Sub-type", "Fracture Location", "DRUJ Instability Risk", "Notes"],
["Type I", "Within 7.5 cm of radial articular surface", "HIGH", "More frequent DRUJ irreducibility"],
["Type II", "More than 7.5 cm proximal", "Lower", "ORIF still required"],
["Complex", "Irreducible DRUJ after radial fixation", "HIGH", "ECU/EDM tendon interposition; needs open DRUJ reduction"],
]
story.append(styled_table(galeazzi_data, [2.5*cm, 5.5*cm, 4.5*cm, 4.7*cm]))
story.append(Spacer(1, 0.3*cm))
# Comparison table
story.append(Paragraph("Monteggia vs. Galeazzi — Comparison", styles["subsection"]))
compare_data = [
["Feature", "Monteggia", "Galeazzi"],
["Bones broken", "Ulna (proximal)", "Radius (distal third)"],
["Dislocation", "Radial head (proximal RU joint)","Distal ulna (DRUJ)"],
["Mnemonic", "Ulna fxd, Radial head dislocates","Radius fxd, Distal ulna dislocates"],
["Treatment", "ORIF ulna; closed reduction RH","ORIF radius; assess DRUJ"],
["Also called", "—", "Fracture of necessity / Piedmont"],
]
story.append(styled_table(compare_data, [4*cm, 6.6*cm, 6.6*cm]))
story.append(PageBreak())
# ── Section 6: Distal Radius ─────────────────────────────────────────────────
def build_distal_radius(story, styles, paths):
story.append(section_banner("SECTION 6 — DISTAL RADIUS FRACTURES", styles))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph(
"Distal radius fractures are the most common fracture in adults (~15% of all adult fractures). "
"Peak incidence in elderly women (osteoporosis) and young adults (high-energy trauma).",
styles["body"]))
story.append(Spacer(1, 0.2*cm))
story.append(Paragraph("OTA/AO Classification (Most Widely Used — 27 Patterns)", styles["key_point"]))
story.append(img(paths, "distal_radius", width=16*cm, max_height=9.5*cm))
story.append(Paragraph(
"Figure 11. OTA/AO classification of distal radius fractures — Groups A (extra-articular), "
"B (partial articular), C (complete articular) with 9 subgroups each. "
"Source: Rockwood & Green's Fractures in Adults 10th Ed. 2025",
styles["caption"]))
ota_data = [
["OTA Group", "Articular Involvement", "Sub-types", "Classic Eponym"],
["A — Extra-articular", "None", "A1: ulnar styloid; A2: simple; A3: comminuted", "Colles' (dorsal angulation)\nSmith's (volar angulation)"],
["B — Partial articular", "Partial", "B1: sagittal; B2: dorsal rim; B3: volar rim", "Barton's (shear)\nChauffeur's (radial styloid)"],
["C — Complete articular", "Full articular","C1: simple; C2: metaphyseal comminution; C3: articular comminution", "Die-punch\nExplosion fracture"],
]
story.append(styled_table(ota_data, [3.8*cm, 3.8*cm, 5.8*cm, 3.8*cm]))
story.append(Spacer(1, 0.2*cm))
eponym_data = [
["Eponym", "Mechanism", "Displacement", "Classic Description"],
["Colles'", "FOOSH", "Dorsal (dinner fork deformity)", "Most common; extra-articular; age >50"],
["Smith's", "Fall on flexed wrist","Volar (garden spade deformity)","Reverse Colles'"],
["Barton's", "Shear / FOOSH", "Dorsal or volar rim fragment", "Intra-articular; rim fracture-dislocation"],
["Chauffeur's","Radial styloid compression","Radial styloid", "Avulsion; associated scapholunate injury"],
["Die-punch", "Axial load", "Lunate facet impaction", "Intra-articular depression"],
]
story.append(Spacer(1, 0.2*cm))
story.append(Paragraph("Named Eponyms of Distal Radius Fractures", styles["key_point"]))
story.append(styled_table(eponym_data, [2.8*cm, 3.5*cm, 4.5*cm, 6.4*cm]))
story.append(PageBreak())
# ── Section 7: Scaphoid ──────────────────────────────────────────────────────
def build_scaphoid(story, styles, paths):
story.append(section_banner("SECTION 7 — SCAPHOID FRACTURES", styles))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph(
"Scaphoid fractures are the most common carpal bone fracture (~60% of carpal fractures). "
"Mechanism: fall on outstretched hand (FOOSH). Up to 20% are initially radiograph-negative — "
"treat on clinical suspicion (anatomical snuffbox tenderness).",
styles["body"]))
story.append(Spacer(1, 0.2*cm))
scaph_data = [
["Location", "Frequency", "Blood Supply", "Healing Rate", "Avg. Healing Time", "AVN Risk"],
["Distal pole", "10%", "Good (from distal)", "~100%", "6–8 weeks", "Low"],
["Waist", "70%", "Retrograde", "80–90%", "10–12 weeks", "Moderate"],
["Proximal pole","20%", "Poor (retrograde, enters distally)", "60–70%", "12–20 weeks", "HIGH (~30%)"],
]
story.append(styled_table(scaph_data, [2.8*cm, 2.3*cm, 3.6*cm, 2.8*cm, 3.2*cm, 2.5*cm]))
story.append(Spacer(1, 0.2*cm))
herbert_data = [
["Herbert Classification", "Description", "Stability", "Treatment"],
["Type A1", "Fracture of tubercle", "Stable", "Cast 4–6 weeks"],
["Type A2", "Incomplete fracture of waist", "Stable", "Cast 6–10 weeks (long arm initially)"],
["Type B1", "Oblique fracture of distal third", "Unstable", "ORIF (headless compression screw)"],
["Type B2", "Complete fracture of waist, displaced", "Unstable", "ORIF"],
["Type B3", "Proximal pole fracture", "Unstable", "ORIF; high AVN risk"],
["Type B4", "Trans-scaphoid perilunate fracture-dislocation", "Unstable", "ORIF + ligament repair"],
["Type C", "Delayed union (>3 months)", "—", "ORIF + bone graft"],
["Type D1", "Fibrous nonunion", "—", "ORIF + bone graft"],
["Type D2", "Sclerotic nonunion", "—", "ORIF + structural graft (Russe/Fernandez)"],
]
story.append(Paragraph("Herbert Classification", styles["key_point"]))
story.append(styled_table(herbert_data, [3*cm, 5.7*cm, 2.7*cm, 5.8*cm]))
story.append(Spacer(1, 0.2*cm))
accent_txt = ("<b>Diagnostic tip:</b> Normal radiographs with anatomical snuffbox tenderness → "
"immobilise and repeat X-ray at 10–14 days. If still negative and symptomatic → MRI (most sensitive) or CT or bone scan.<br/>"
"<b>Displacement threshold:</b> ≥1 mm displacement or angulation on CT → ORIF (headless compression screw).")
story.append(accent_box(accent_txt, styles, bg=HexColor("#fff8e6")))
story.append(PageBreak())
# ── Section 8: Pediatric ─────────────────────────────────────────────────────
def build_pediatric(story, styles, paths):
story.append(section_banner("SECTION 8 — PEDIATRIC FRACTURES", styles))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph(
"Children's bones are more porous and have active growth plates (physes). Periosteum is thicker and stronger "
"than in adults, allowing incomplete fractures that are unique to pediatrics. Significant remodelling potential "
"exists, particularly in the plane of joint motion and in younger patients.",
styles["body"]))
story.append(Spacer(1, 0.3*cm))
ped_row = [[
img(paths, "ped_radius_a", width=7.5*cm, max_height=7*cm),
img(paths, "ped_radius_b", width=7.5*cm, max_height=7*cm),
]]
story.append(Table(ped_row, colWidths=[8.6*cm, 8.6*cm]))
story.append(Paragraph(
"Figure 12. (a) Dorsally angulated metaphyseal radius fracture (buckle/torus) in a 10-year-old. "
"(b) Post-reduction at 8 weeks. (c) Complete remodelling at age 12. Source: Bailey & Love's 28th Ed.",
styles["caption"]))
story.append(Spacer(1, 0.3*cm))
ped_data = [
["Fracture Type", "Description", "Common Location", "Treatment"],
["Buckle (Torus)", "Cortex buckles/bulges; no complete cortical break", "Distal radius/ulna metaphysis", "Cast 3–4 weeks; excellent prognosis"],
["Greenstick", "Incomplete fracture — one cortex breaks, one bends", "Radius/ulna diaphysis", "Reduction if >15° angulation; cast"],
["Plastic deformation","Bone bends without visible fracture line", "Radius, ulna, fibula", "Reduction under GA if significant deformity"],
["Complete", "Both cortices disrupted", "Any long bone", "Reduction ± fixation depending on site"],
]
story.append(styled_table(ped_data, [3.2*cm, 5.5*cm, 4*cm, 4.5*cm]))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("Salter-Harris Classification of Physeal Injuries", styles["subsection"]))
sh_data = [
["Type", "Mnemonic (SALTR)", "Fracture Pattern", "Prognosis", "Treatment"],
["I", "S — Straight across", "Physis only; through growth plate", "Excellent", "Immobilisation"],
["II", "A — Above (metaphysis)", "Through physis + into metaphysis (most common ~75%)", "Excellent", "Closed reduction ± K-wires"],
["III", "L — Lower (epiphysis)", "Through physis + down through epiphysis (articular)", "Guarded", "ORIF (intra-articular)"],
["IV", "T — Through all", "Through metaphysis + physis + epiphysis", "Poor without ORIF","ORIF mandatory"],
["V", "R — Rammed/crush", "Crush injury to physis", "Poor; growth arrest likely","Immobilisation; warn of arrest"],
]
story.append(styled_table(sh_data, [1.5*cm, 3.5*cm, 5.5*cm, 2.8*cm, 3.9*cm]))
story.append(Spacer(1, 0.2*cm))
accent_txt = ("<b>Salter-Harris key rules:</b><br/>"
"• Types I & II = extra-articular → closed treatment usually sufficient<br/>"
"• Types III & IV = intra-articular → anatomic reduction essential to prevent growth arrest and DJD<br/>"
"• Type V = most dangerous; diagnosis often made retrospectively when growth arrest is evident<br/>"
"• Growth arrest is the main long-term complication; corrective osteotomy may be needed")
story.append(accent_box(accent_txt, styles, bg=C_RED_LIGHT))
story.append(PageBreak())
# ── Section 9: Quick Reference ───────────────────────────────────────────────
def build_quick_ref(story, styles, paths):
story.append(section_banner("SECTION 9 — QUICK REFERENCE COMPARISON TABLE", styles))
story.append(Spacer(1, 0.3*cm))
qr_data = [
["Fracture", "Best Classification", "Key Sub-types / Types", "Most Common Pattern", "Must-Know Complication"],
["Clavicle", "Robinson", "Type 1 (medial), 2 (shaft), 3 (distal)", "Middle third (80%)", "Nonunion if >100% displacement"],
["Distal clavicle", "Neer/Rockwood", "I (intact CC), IIA, IIB (CC disrupted), III (AC)", "Type II (unstable)", "Nonunion; malunion"],
["Scapula", "Ada-Miller", "I (processes), II (neck), III (glenoid), IV (body)", "Body fractures (~50%)", "Associated injuries (rib, lung, BPI)"],
["Proximal humerus", "Neer (4-part)", "1-part to 4-part + head-split", "1-part (undisplaced) = 80%", "AVN (esp. 4-part); screw cutout"],
["Humeral shaft", "OTA/AO 12", "A=simple, B=wedge, C=complex", "Spiral/oblique middle third", "Radial nerve palsy (5-22%)"],
["Distal humerus", "Milch / Gartland","Milch I/II; Gartland I-IV (pediatric supracondylar)","Supracondylar (children)", "Volkmann's contracture; cubitus varus"],
["Radial head", "Mason (I-IV)", "I=nondisplaced, II=partial, III=comminuted, IV=+disloc","Type I most common", "Essex-Lopresti (missed DRUJ injury)"],
["Monteggia", "Bado (I-IV)", "I=anterior, II=posterior, III=lateral, IV=anterior+radius","Type I (children), II (adults)","Missed radial head dislocation; PIN palsy"],
["Galeazzi", "Type I/II", "By distance from articular surface (<7.5 cm vs >7.5 cm)","Middle-distal radius", "DRUJ malunion; ECU interposition"],
["Distal radius", "OTA/AO A/B/C", "A=extra-articular, B=partial, C=complete articular", "Colles' (A2)", "Malunion with dorsal angulation; CRPS"],
["Scaphoid", "Herbert (A-D)", "A=stable, B=unstable, C=delayed union, D=nonunion", "Waist fracture (70%)", "AVN of proximal pole; nonunion"],
["Pediatric forearm", "Descriptive + Salter-Harris", "Buckle / Greenstick / SH I-V", "Buckle (torus) distal radius", "Growth arrest (SH III-V); physeal bar"],
]
story.append(styled_table(qr_data, [3.2*cm, 3*cm, 5.3*cm, 3.3*cm, 2.4*cm]))
story.append(Spacer(1, 0.4*cm))
# Final tips box
tips_data = [[
Paragraph(
"<b>HIGH-YIELD MNEMONICS & RULES</b><br/><br/>"
"<b>Monteggia vs Galeazzi:</b> Ulnar shaft fracture → Monteggia (Radial head dislocates). "
"Radial shaft fracture → Galeazzi (distal Ulna dislocates). Mnemonic: <b>MUGR</b> "
"(Monteggia = Ulnar fracture + RH dislocation; Galeazzi = Radial fracture + DRUJ dislocation)<br/><br/>"
"<b>Salter-Harris:</b> <b>SALTR</b> = Straight / Above / Lower / Through / Rammed<br/><br/>"
"<b>Scaphoid blood supply:</b> Enters distally → retrograde to proximal pole → "
"waist fracture cuts off proximal pole blood supply = AVN risk<br/><br/>"
"<b>Neer 'part' definition:</b> >1 cm displacement OR >45° angulation<br/><br/>"
"<b>Bado's rule:</b> Apex of ulna fracture points toward direction of radial head dislocation",
styles["body"])
]]
tips_t = Table(tips_data, colWidths=[17.2*cm])
tips_t.setStyle(TableStyle([
("BACKGROUND", (0,0),(-1,-1), HexColor("#e8f8e8")),
("BOX", (0,0),(-1,-1), 1.5, C_TEAL),
("TOPPADDING", (0,0),(-1,-1), 12),
("BOTTOMPADDING", (0,0),(-1,-1), 12),
("LEFTPADDING", (0,0),(-1,-1), 14),
("RIGHTPADDING", (0,0),(-1,-1), 14),
]))
story.append(tips_t)
# ── Page template (header/footer) ────────────────────────────────────────────
def on_page(canvas, doc):
canvas.saveState()
w, h = A4
# Header bar
canvas.setFillColor(C_NAVY)
canvas.rect(0, h - 25, w, 25, fill=1, stroke=0)
canvas.setFillColor(white)
canvas.setFont("Helvetica-Bold", 9)
canvas.drawString(20, h - 16, "FRACTURE CLASSIFICATIONS — Study Guide")
canvas.setFont("Helvetica", 8)
canvas.drawRightString(w - 20, h - 16, "Orris Medical Library")
# Footer
canvas.setFillColor(C_TEAL)
canvas.rect(0, 0, w, 18, fill=1, stroke=0)
canvas.setFillColor(white)
canvas.setFont("Helvetica", 8)
canvas.drawString(20, 5, "Sources: Miller's Review of Orthopaedics | Rockwood & Green's | Tintinalli's | Bailey & Love's | Campbell's")
canvas.drawRightString(w - 20, 5, f"Page {doc.page}")
canvas.restoreState()
# ── Main builder ─────────────────────────────────────────────────────────────
def main():
print("Downloading images…")
paths = download_images()
print("Building PDF…")
doc = SimpleDocTemplate(
OUTPUT,
pagesize=A4,
leftMargin=2*cm, rightMargin=2*cm,
topMargin=2.5*cm, bottomMargin=1.5*cm,
title="Fracture Classifications Study Guide",
author="Orris Medical Library",
)
styles = make_styles()
story = []
build_cover(story, styles)
build_shoulder(story, styles, paths)
build_prox_humerus(story, styles, paths)
build_distal_humerus(story, styles, paths)
build_radial_head(story, styles, paths)
build_forearm(story, styles, paths)
build_distal_radius(story, styles, paths)
build_scaphoid(story, styles, paths)
build_pediatric(story, styles, paths)
build_quick_ref(story, styles, paths)
doc.build(story, onFirstPage=on_page, onLaterPages=on_page)
print(f"Done → {OUTPUT}")
if __name__ == "__main__":
main()