Prepare radiology long cases total 5 from textbook of radiology and for Radiology residents pdf include images also
ls -la /home/daytona/workspace/ && mkdir -p /home/daytona/workspace/radiology-long-cases
I want only cases from msk Radiology
~/radiology-long-cases/build_msk_cases.py
#!/usr/bin/env python3
"""
MSK Radiology Long Cases PDF Generator
Source: Grainger & Allison's Diagnostic Radiology
"""
import requests
import os
from io import BytesIO
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import cm
from reportlab.lib.colors import HexColor, white, black
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
Image, HRFlowable, PageBreak, KeepTogether
)
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_JUSTIFY
from reportlab.pdfgen import canvas
from reportlab.platypus.doctemplate import PageTemplate, BaseDocTemplate
from reportlab.platypus.frames import Frame
OUTPUT_PATH = "/home/daytona/workspace/radiology-long-cases/MSK_Radiology_Long_Cases.pdf"
# ─────────── Colours ───────────
DARK_BLUE = HexColor("#1B3A6B")
MID_BLUE = HexColor("#2563EB")
LIGHT_BLUE = HexColor("#DBEAFE")
ACCENT_TEAL = HexColor("#0F766E")
GOLD = HexColor("#D97706")
LIGHT_GREY = HexColor("#F1F5F9")
MED_GREY = HexColor("#64748B")
DARK_GREY = HexColor("#1E293B")
RED_ALERT = HexColor("#DC2626")
GREEN_OK = HexColor("#16A34A")
# ─────────── Styles ───────────
styles = getSampleStyleSheet()
def mks(name, **kw):
base = kw.pop("parent", "Normal")
s = ParagraphStyle(name, parent=styles[base], **kw)
styles.add(s)
return s
COVER_TITLE = mks("CoverTitle", fontSize=30, leading=36, textColor=white, alignment=TA_CENTER, spaceAfter=8)
COVER_SUB = mks("CoverSub", fontSize=16, leading=20, textColor=HexColor("#BFDBFE"), alignment=TA_CENTER, spaceAfter=6)
COVER_BODY = mks("CoverBody", fontSize=11, leading=14, textColor=HexColor("#E0F2FE"), alignment=TA_CENTER)
CASE_NUM = mks("CaseNum", fontSize=11, leading=13, textColor=white, alignment=TA_LEFT, spaceAfter=0)
CASE_TITLE = mks("CaseTitle", fontSize=22, leading=26, textColor=white, alignment=TA_LEFT, spaceAfter=0)
SECTION_HDR = mks("SectionHdr", fontSize=13, leading=16, textColor=white, alignment=TA_LEFT, spaceBefore=2, spaceAfter=2)
BODY = mks("Body", fontSize=10.5, leading=15, textColor=DARK_GREY, alignment=TA_JUSTIFY, spaceAfter=5)
BULLET = mks("Bullet", fontSize=10.5, leading=14, textColor=DARK_GREY, leftIndent=14, spaceAfter=3)
KEY_FIND = mks("KeyFind", fontSize=10.5, leading=14, textColor=DARK_GREY, leftIndent=14, spaceAfter=3)
CAPTION = mks("Caption", fontSize=8.5, leading=11, textColor=MED_GREY, alignment=TA_CENTER, spaceAfter=4)
TABLE_HDR = mks("TableHdr", fontSize=9.5, leading=12, textColor=white, alignment=TA_CENTER)
TABLE_CELL = mks("TableCell", fontSize=9, leading=12, textColor=DARK_GREY, alignment=TA_LEFT)
FOOTER_STYLE = mks("Footer", fontSize=8, leading=10, textColor=MED_GREY, alignment=TA_CENTER)
ANSWER_LBL = mks("AnswerLbl", fontSize=10, leading=12, textColor=ACCENT_TEAL, spaceAfter=2)
REF_STYLE = mks("RefStyle", fontSize=8.5, leading=11, textColor=MED_GREY, spaceAfter=2)
# ─────────── Image helper ───────────
def fetch_image(url, max_w=14*cm, max_h=9*cm):
"""Download image from URL and return a ReportLab Image flowable."""
try:
r = requests.get(url, timeout=15)
r.raise_for_status()
img_data = BytesIO(r.content)
from PIL import Image as PILImage
pil = PILImage.open(BytesIO(r.content))
w_px, h_px = pil.size
ratio = h_px / w_px
w = min(max_w, max_w)
h = w * ratio
if h > max_h:
h = max_h
w = h / ratio
return Image(BytesIO(r.content), width=w, height=h)
except Exception as e:
print(f" [WARN] Could not fetch {url}: {e}")
return None
# ─────────── Section box helper ───────────
def section_box(title, color=DARK_BLUE):
data = [[Paragraph(title, SECTION_HDR)]]
t = Table(data, colWidths=[17*cm])
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), color),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 10),
("RIGHTPADDING", (0,0), (-1,-1), 10),
]))
return t
def case_header(number, title, subtitle):
data = [
[Paragraph(f"LONG CASE {number}", CASE_NUM)],
[Paragraph(title, CASE_TITLE)],
[Paragraph(subtitle, mks(f"CS{number}", fontSize=11, leading=14, textColor=HexColor("#BAE6FD"), alignment=TA_LEFT))],
]
t = Table(data, colWidths=[17*cm])
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), DARK_BLUE),
("TOPPADDING", (0,0), (0,0), 12),
("BOTTOMPADDING", (0,0), (-1,-1), 10),
("LEFTPADDING", (0,0), (-1,-1), 14),
("RIGHTPADDING", (0,0), (-1,-1), 14),
]))
return t
def info_row(label, value):
data = [[Paragraph(f"<b>{label}</b>", TABLE_HDR), Paragraph(value, TABLE_CELL)]]
t = Table(data, colWidths=[4*cm, 13*cm])
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (0,0), MID_BLUE),
("BACKGROUND", (1,0), (1,0), LIGHT_BLUE),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING", (0,0), (-1,-1), 8),
("GRID", (0,0), (-1,-1), 0.4, HexColor("#CBD5E1")),
]))
return t
def two_col_table(rows, hdr1, hdr2, col1w=5.5*cm, col2w=11.5*cm):
data = [[Paragraph(hdr1, TABLE_HDR), Paragraph(hdr2, TABLE_HDR)]]
for r in rows:
data.append([Paragraph(r[0], TABLE_CELL), Paragraph(r[1], TABLE_CELL)])
t = Table(data, colWidths=[col1w, col2w])
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), DARK_BLUE),
("ROWBACKGROUNDS",(0,1), (-1,-1), [LIGHT_GREY, white]),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 7),
("RIGHTPADDING", (0,0), (-1,-1), 7),
("GRID", (0,0), (-1,-1), 0.4, HexColor("#CBD5E1")),
("VALIGN", (0,0), (-1,-1), "TOP"),
]))
return t
def key_points_box(points, color=HexColor("#F0FDF4"), border=GREEN_OK):
items = "".join(f"<br/>• {p}" for p in points)
data = [[Paragraph(f"<b>KEY IMAGING POINTS</b>{items}", mks("KP", fontSize=10, leading=14, textColor=DARK_GREY))]]
t = Table(data, colWidths=[17*cm])
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), color),
("TOPPADDING", (0,0), (-1,-1), 8),
("BOTTOMPADDING", (0,0), (-1,-1), 8),
("LEFTPADDING", (0,0), (-1,-1), 12),
("RIGHTPADDING", (0,0), (-1,-1), 12),
("BOX", (0,0), (-1,-1), 1.5, border),
]))
return t
def answer_box(items):
rows = []
for label, text in items:
rows.append([Paragraph(f"<b>{label}</b>", mks(f"AH{label[:3]}", fontSize=10, leading=12, textColor=white)),
Paragraph(text, mks(f"AB{label[:3]}", fontSize=10, leading=13, textColor=DARK_GREY))])
t = Table(rows, colWidths=[3.5*cm, 13.5*cm])
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (0,-1), ACCENT_TEAL),
("BACKGROUND", (1,0), (1,-1), HexColor("#F0FDFA")),
("ROWBACKGROUNDS",(1,0), (1,-1), [HexColor("#F0FDFA"), white]),
("TOPPADDING", (0,0), (-1,-1), 6),
("BOTTOMPADDING", (0,0), (-1,-1), 6),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING", (0,0), (-1,-1), 8),
("GRID", (0,0), (-1,-1), 0.4, HexColor("#CBD5E1")),
("VALIGN", (0,0), (-1,-1), "TOP"),
]))
return t
# ════════════════════════════════════════════════════════════
# CASE DATA
# ════════════════════════════════════════════════════════════
CASES = [
# ─── CASE 1 ─────────────────────────────────────────────
{
"number": "01",
"title": "ACL Tear with Associated Knee Injuries",
"subtitle": "MRI Knee | Musculoskeletal Radiology",
"clinical": (
"A 24-year-old male professional footballer presents with acute onset of right knee pain and swelling "
"following a valgus stress injury during a match. He reports a 'pop' at the time of injury, followed by "
"rapid haemarthrosis. On examination there is a positive Lachman test, positive pivot-shift and anterior "
"drawer sign. The knee is grossly swollen. X-ray shows a subtle avulsion fracture at the lateral tibial plateau."
),
"questions": [
("Q1", "What is the most appropriate next imaging investigation and why?"),
("Q2", "Describe the MRI anatomy of the ACL and its normal appearance."),
("Q3", "What are the primary and secondary MRI signs of a complete ACL tear?"),
("Q4", "What is O'Donoghue's 'unhappy triad'? What is a Segond fracture?"),
("Q5", "How would you manage this patient?"),
],
"answers": [
("Investigation", "MRI of the knee (1.5T or 3T) is the gold-standard investigation. It directly visualises the ACL, associated ligaments, menisci and bone marrow, with near 100% accuracy for complete tears. No ionising radiation is used."),
("ACL Anatomy", "The ACL attaches proximally at the posteromedial margin of the lateral femoral condyle in the intercondylar fossa and distally at the anterior tibial intercondylar eminence. It acts to restrain anterior tibial translation and internal rotation. On MRI (sagittal plane), it appears as fan-shaped bundles of taut low-signal fibres running parallel to Blumensaat's line. Two bundles (anteromedial and posterolateral) may be differentiated on coronal/axial sequences."),
("ACL Tear Signs", "PRIMARY: Discontinuity of fibres, increased (high) signal within the ligament on T2/PD fat-sat sequences, laxity or abnormal angulation of the ligament. SECONDARY: (1) 'Pivot-shift' bone marrow contusions - kissing contusions of posterolateral tibial plateau and lateral femoral condyle. (2) Anterior tibial translation ≥7 mm relative to femur. (3) Posterior cruciate ligament (PCL) buckling. (4) Deep condylopatellar sulcus sign - impaction fracture of lateral femoral condyle. (5) Segond fracture."),
("Unhappy Triad", "O'Donoghue's unhappy triad: simultaneous tears of ACL + medial collateral ligament (MCL) + medial meniscus from valgus stress. Segond fracture: avulsion fracture fragment from the lateral tibial plateau margin at the attachment of joint capsule/supporting ligaments - has HIGH association with ACL injury; often subtle on MRI but better shown on plain radiograph."),
("Management", "Conservative: RICE, physiotherapy, and dynamic stabilisation for low-demand patients. Surgical: ACL reconstruction (patellar tendon or hamstring autograft) for high-demand athletes. Concurrent MCL tears often heal conservatively; meniscal tears may require repair or partial meniscectomy at time of ACL reconstruction. MRI guides surgical planning by defining concomitant injuries."),
],
"imaging_table": [
("Modality", "Role"),
("Plain X-ray", "First-line; excludes fractures, shows subtle Segond avulsion, assesses alignment"),
("MRI (Gold Standard)", "Defines ACL tear, secondary signs, meniscal injury, bone contusions, collateral ligaments"),
("CT", "Used if bony injury complex; limited soft-tissue evaluation"),
("Ultrasound", "Limited role in knee ligament assessment; useful for popliteal cyst or soft-tissue mass"),
],
"key_points": [
"Complete ACL tear: fibre discontinuity + high T2/PD signal + laxity on MRI sagittal sequence",
"Secondary signs: 'pivot-shift' bone marrow contusions, PCL buckling, anterior tibial translation ≥7 mm",
"50% of ACL tears have associated meniscal tear (usually peripheral vertical or posterior radial)",
"Segond fracture (lateral tibial plateau avulsion) is highly specific for ACL injury",
"MRI must assess full extent: ACL, PCL, MCL, LCL, menisci, bone marrow, posterolateral corner",
],
"images": [
{
"url": "https://cdn.orris.care/cdss_images/ce38c9a48e4eea94a5b707eb58cda5f57fdb5537d54741aad5ceab2f04791581.png",
"caption": "Fig. 39.53 Complete intrasubstance ACL tear. Sagittal PD fat-sat image reveals retracted distal ACL fibres with no intact proximal fibres (arrow). Source: Grainger & Allison's Diagnostic Radiology."
},
{
"url": "https://cdn.orris.care/cdss_images/d8da53fa068baba7d9d8b556170adee96842fc5eb1ebc4c23a72a2153ed78fe3.png",
"caption": "Fig. 39.54 'Pivot-shift' injury contusion pattern. Sagittal PD fat-sat shows contusions of lateral femoral condyle and posterior tibial plateau with subcortical impaction fracture of lateral femoral condyle (arrow). Source: Grainger & Allison's Diagnostic Radiology."
},
],
"reference": "Grainger & Allison's Diagnostic Radiology, 7th Ed. Chapter 39: Musculoskeletal MRI - The Knee. Block 7, lines 4129-4184.",
},
# ─── CASE 2 ─────────────────────────────────────────────
{
"number": "02",
"title": "Osteosarcoma of the Distal Femur",
"subtitle": "Plain X-ray + MRI + CT | Bone Tumour Radiology",
"clinical": (
"A 15-year-old male presents with a 6-week history of progressive pain and swelling around the right knee, "
"worse at night. He is otherwise well. On examination, there is a firm, warm, non-mobile mass around the distal femur. "
"Inflammatory markers are mildly elevated. Plain radiograph shows a metaphyseal lesion with mixed lytic and sclerotic "
"areas, cortical destruction, and sunburst periosteal reaction. Serum alkaline phosphatase is raised."
),
"questions": [
("Q1", "What is the most likely diagnosis and what is its incidence?"),
("Q2", "Describe the radiographic features of conventional osteosarcoma."),
("Q3", "What is the role of MRI in osteosarcoma staging?"),
("Q4", "What is the significance of skip metastases and pulmonary metastases?"),
("Q5", "Describe the WHO classification of osteosarcoma subtypes."),
],
"answers": [
("Diagnosis", "Conventional (central) osteosarcoma - the commonest non-haematological primary bone malignancy. Annual incidence ~4 per million population. Bimodal age distribution: first peak 10-14 years; second peak in older adults (often secondary osteosarcoma on Paget's disease). Slight male preponderance (1.35:1). Located characteristically in the metaphysis of long bones (distal femur, proximal tibia, proximal humerus)."),
("X-ray Features", "Metaphyseal location with moth-eaten or permeative pattern of bone destruction. Mixed lytic (13% purely lytic) to sclerotic appearance with cloud-like/ivory-like mineralised tumour osteoid. Cortical destruction with eccentric extra-osseous soft-tissue mass showing amorphous mineralisation. Aggressive periosteal reactions: sunburst/spiculated, lamellated (onion-skin), and/or Codman triangle (reactive periosteal elevation at tumour margins) - any combination may coexist."),
("MRI Role", "MRI is MANDATORY for local surgical staging. It accurately defines: (1) extent of tumour within medullary canal, (2) cortical destruction and soft-tissue mass extent, (3) neurovascular involvement, (4) joint involvement. At least one sequence must image the WHOLE affected bone to detect skip metastases (~5% of cases). Dynamic contrast-enhanced MRI (pre- and post-chemotherapy) assesses tumour response. Static post-gadolinium sequences add little to local staging."),
("Metastases", "Skip metastases: foci of tumour within the same bone separate from the primary, or trans-articular (very rare). Found in ~5% of cases - missed if the whole bone is not imaged. Pulmonary metastases: haematogenous spread; typically peripheral, may be mineralised (calcified). Calcified lung metastases may show uptake on bone scintigraphy. Rarely result in chronic pneumothorax. CT chest is the staging investigation of choice for pulmonary metastases."),
("WHO Classification", "2013 WHO Committee for Bone Tumours: (1) Conventional osteosarcoma - 75%, high-grade intramedullary; (2) Telangiectatic osteosarcoma; (3) Low-grade central osteosarcoma; (4) Small cell osteosarcoma; (5) Parosteal osteosarcoma; (6) Periosteal sarcoma; (7) High-grade surface osteosarcoma. Secondary osteosarcoma occurs on background of Paget's disease, radiotherapy, or dedifferentiated lower-grade sarcoma."),
],
"imaging_table": [
("Modality", "Role"),
("Plain X-ray", "First-line; demonstrates matrix mineralisation, periosteal reaction, Codman triangle"),
("MRI (Mandatory)", "Local staging: medullary extent, soft tissue, neurovascular, skip metastases, joint involvement"),
("CT Chest", "Staging: pulmonary metastases (calcified nodules)"),
("Bone Scintigraphy / FDG-PET", "Skip metastases, multifocal disease; PET assesses response to chemotherapy"),
("Biopsy (image-guided)", "Histological confirmation; must plan biopsy track to be included in surgical resection"),
],
"key_points": [
"Commonest non-haematological primary bone malignancy; peak 10-14 years; distal femur/proximal tibia",
"Radiograph: metaphyseal, mixed lytic-sclerotic, sunburst periosteal reaction, Codman triangle",
"MRI is mandatory for local staging - must image the WHOLE bone to exclude skip metastases",
"Pulmonary metastases may be mineralised and detected on bone scintigraphy",
"Dynamic MRI pre- and post-chemotherapy assesses histological response (% necrosis predictor of outcome)",
],
"images": [
{
"url": "https://cdn.orris.care/cdss_images/6f23f9305fb931f4bfc24c17ee2e15f3095932325f0d0c8655f7628ab82b2ad5.png",
"caption": "Fig. 41.27 AP radiographs in three patients demonstrating lytic, mixed lytic-sclerotic, and sclerotic types of conventional osteosarcoma, reflecting variable patterns of osteoid matrix deposition. Source: Grainger & Allison's Diagnostic Radiology."
},
{
"url": "https://cdn.orris.care/cdss_images/2f55ca7dfcf7268c67e51c8914df7869bb1a37f7d45aa9f241580174350a7fd2.png",
"caption": "Fig. 41.28 Coronal T1 MRI of proximal tibial osteosarcoma with a skip metastasis more distally in the tibia. Whole-bone imaging is essential to detect skip lesions. Source: Grainger & Allison's Diagnostic Radiology."
},
{
"url": "https://cdn.orris.care/cdss_images/e8f79f5b4174d4693f189bca95e591a316e11ab39af6327b5b1c8d5a97458f57.png",
"caption": "Fig. 41.29 Chest radiograph demonstrating calcified lung metastases from osteosarcoma and large left-sided pneumothorax. Source: Grainger & Allison's Diagnostic Radiology."
},
],
"reference": "Grainger & Allison's Diagnostic Radiology, 7th Ed. Chapter 41: Bone Tumours. Block 8, lines 1194-1281.",
},
# ─── CASE 3 ─────────────────────────────────────────────
{
"number": "03",
"title": "Rotator Cuff Tear - Full Thickness Supraspinatus",
"subtitle": "MRI / Ultrasound Shoulder | Musculoskeletal Radiology",
"clinical": (
"A 56-year-old female teacher presents with a 4-month history of right shoulder pain, worse on abduction "
"and at night, with progressive weakness. She cannot abduct beyond 90 degrees actively. There is a positive "
"Neer's and Hawkins impingement sign. Plain X-ray shows a subacromial enthesophyte on the lateral acromion "
"and mild narrowing of the subacromial space. She has no history of significant trauma."
),
"questions": [
("Q1", "Describe the pathophysiology of rotator cuff disease and impingement."),
("Q2", "What are the plain radiograph findings in rotator cuff disease?"),
("Q3", "What are the MRI and ultrasound features of a full-thickness supraspinatus tear?"),
("Q4", "How do you differentiate a partial-thickness from a full-thickness tear?"),
("Q5", "What other structures must be assessed and how does this guide management?"),
],
"answers": [
("Pathophysiology", "External impingement is the commonest cause (>40 years). The rotator cuff tendons are compressed between the humeral head and coracoacromial arch during abduction. Progression: reversible oedema/haemorrhage → tendinopathy → partial tear → full-thickness tear. Subacromial space is reduced by bony acromial variants (type II/III hooked acromion), ACJ osteophytes. Secondary impingement from abnormal rotator cuff muscle coordination also occurs. Calcific tendinopathy results from hydroxyapatite crystal deposition."),
("X-ray Findings", "(1) Subacromial enthesophyte on lateral acromion margin. (2) Acromial shape (type I flat, type II curved, type III hooked - type III increases impingement risk). (3) Subacromial space narrowing (<7 mm is significant; <5 mm highly specific for full-thickness tear). (4) Superior migration of humeral head relative to glenoid. (5) Cortical sclerosis and cystic change at the greater tuberosity. (6) ACJ osteophytes."),
("FTT Signs", "MRI: Primary sign - focal DEFECT in the supraspinatus tendon filled with HIGH SIGNAL (fluid) on T2/PD fat-sat sequences. Fluid communication between glenohumeral joint and subacromial bursa (abnormal communication). Supraspinatus muscle atrophy (occupation of supraspinatus fossa by fat). Tendon retraction in large tears. Ultrasound: Focal hypoechoic or absent tendon with loss of normal fibrillar architecture at footprint. Fluid in subacromial bursa."),
("Partial vs Full", "Partial-thickness tear (PTT): involves articular surface (commonest) OR bursal surface only - does NOT communicate GHJ with SAB. Full-thickness tear (FTT): extends from articular to bursal surface - creates ABNORMAL COMMUNICATION between GHJ and SAB (demonstrated by fluid extending between the two on MRI/arthrogram, or dye injection at arthrography). FTT is further classified by size: small (<1 cm), medium (1-3 cm), large (3-5 cm), massive (≥5 cm or involving ≥2 tendons)."),
("Key Structures", "Must assess: (1) Size and shape of tear. (2) Tendon retraction and muscle atrophy (fatty infiltration of supraspinatus fossa - Goutallier grade). (3) Long head of biceps (LHB) tendon - subluxation or rupture due to pulley tear. (4) Subscapularis tendon. (5) Infraspinatus tendon. (6) GHJ arthrosis. (7) ACJ - local osteophytes. These findings determine candidacy for surgical repair vs conservative physiotherapy and guide operative approach."),
],
"imaging_table": [
("Modality", "Findings"),
("Plain X-ray", "Enthesophyte, subacromial space, humeral head migration, greater tuberosity cysts"),
("MRI (preferred)", "Gold standard for soft tissue; T2/PD fat-sat shows fluid-filled defect, muscle atrophy"),
("Ultrasound", "Comparable accuracy to MRI for FTT; dynamic assessment; guided injection; operator dependent"),
("MR Arthrography", "Preferred for labral pathology; not routinely indicated for primary rotator cuff assessment"),
],
"key_points": [
"Full-thickness tear (FTT): fluid-filled defect communicating GHJ with subacromial bursa on T2/PD MRI",
"Subacromial space <7 mm on X-ray is suspicious; <5 mm is highly specific for FTT",
"Assess Goutallier grade of fatty muscle atrophy - grade ≥3 (>50% fat) predicts poor surgical outcome",
"LHB subluxation/rupture and subscapularis tears are important co-pathologies to exclude",
"Massive tear = ≥5 cm or ≥2 tendons involved; requires specific surgical planning (e.g. reverse shoulder arthroplasty)",
],
"images": [
{
"url": "https://cdn.orris.care/cdss_images/8d9891c2e5b7ef9e5473e8349c1a16ffbdd0dac1b32c00e8f8bd8c6934fa1d65.png",
"caption": "Fig. 39.1 AP shoulder radiograph demonstrating bony enthesophyte on lateral acromion and greater tuberosity secondary to external impingement. Source: Grainger & Allison's Diagnostic Radiology."
},
{
"url": "https://cdn.orris.care/cdss_images/4b73b78dd5c6218fbcaa4f136af661c64a16d01e54217b36712133df08810a44.png",
"caption": "Fig. 39.4 Coronal oblique T2FS MRI: full-thickness supraspinatus tear filled with high-signal fluid (white arrow). Fluid in glenohumeral joint around biceps tendon sheath (black arrow) and subacromial bursa (curved arrow) demonstrates abnormal communication. Source: Grainger & Allison's Diagnostic Radiology."
},
{
"url": "https://cdn.orris.care/cdss_images/2e501b34e02dd852db74c40e7ed43273ed8f7c7cdb7b86ee7135043fbd2c1b1a.png",
"caption": "Fig. 39.2 AP shoulder radiograph showing marked subacromial space narrowing and bony impingement of humerus and acromion secondary to chronic rotator cuff tear (rotator cuff arthropathy). Source: Grainger & Allison's Diagnostic Radiology."
},
],
"reference": "Grainger & Allison's Diagnostic Radiology, 7th Ed. Chapter 39: Musculoskeletal MRI - The Shoulder. Block 7, lines 3422-3496.",
},
# ─── CASE 4 ─────────────────────────────────────────────
{
"number": "04",
"title": "Acute Haematogenous Osteomyelitis",
"subtitle": "Plain X-ray + MRI + Nuclear Medicine | MSK Infection",
"clinical": (
"A 9-year-old boy presents with a 5-day history of fever (38.9°C), right leg pain, refusal to bear weight, "
"and localised tenderness over the distal tibia metaphysis. CRP is 142 mg/L, WCC 18.4 x10⁹/L. "
"Plain X-ray of the right tibia shows soft-tissue swelling only. Blood cultures have been taken. "
"The paediatric orthopaedic team requests further imaging to confirm diagnosis and assess extent."
),
"questions": [
("Q1", "Why are X-ray findings often normal in acute osteomyelitis and when do changes appear?"),
("Q2", "What is the role of MRI in acute osteomyelitis and what are the expected findings?"),
("Q3", "Describe the imaging features of chronic osteomyelitis."),
("Q4", "What is the role of nuclear medicine imaging in osteomyelitis?"),
("Q5", "Summarise the typical imaging findings by stage (Acute, Subacute, Chronic) in a table."),
],
"answers": [
("X-ray Timing", "Plain radiographs are NORMAL in early acute osteomyelitis (first 7-14 days). Bone destruction is not visible until 30-50% of bone mineral has been lost. Early changes include: periosteal reaction and soft-tissue swelling (visible at 5-7 days). Later (2 weeks+): focal lytic areas, cortical erosion, periosteal reaction become evident. X-ray has LOW sensitivity for acute osteomyelitis but is always performed first to exclude other diagnoses (fracture, tumour)."),
("MRI Findings", "MRI is the investigation of choice for acute osteomyelitis (sensitivity ~95%). Bone marrow oedema appears as early as 24-48 hours after onset: LOW T1 signal, HIGH T2/STIR signal within marrow. Additional findings: cortical thickening, periosteal oedema (elevated periosteum), soft-tissue oedema (high T2), subperiosteal/soft-tissue abscess (rim-enhancing collection on post-gadolinium T1). Gadolinium contrast delineates abscess cavities clearly. MRI also defines extent of marrow involvement and guides surgical drainage if required."),
("Chronic Osteomyelitis", "Plain X-ray: bone sclerosis, cortical thickening, sequestrum (dead bone fragment), cloaca (cortical defect allowing pus drainage), involucrum (new bone surrounding sequestrum), bone destruction and deformity. CT: superior to X-ray for demonstrating cloaca and sequestrum, periosteal new bone. MRI: shows medullary and cortical changes clearly; sequestra appear as low-signal fragments surrounded by high T2 signal; sinus tracts and soft-tissue abscesses are well characterised. Chronic ulcers associated with sinuses can undergo malignant transformation to squamous cell carcinoma."),
("Nuclear Medicine", "Three-phase bone scintigraphy (Tc-99m): sensitive but non-specific; useful for multifocal disease and paediatric patients. Shows increased uptake at all three phases (blood flow, blood pool, delayed) in acute infection. ¹¹¹In white blood cell (WBC) scintigraphy: more specific for infection; combined WBC + bone marrow scintigraphy improves specificity. FDG-PET/CT: high sensitivity and specificity for osteomyelitis; particularly useful in chronic/prosthetic joint infections but not routine for uncomplicated paediatric haematogenous osteomyelitis."),
("Imaging by Stage", "See imaging table below for summary of findings by stage across all modalities."),
],
"imaging_table": [
("Stage", "MRI Findings"),
("Acute (0-2 weeks)", "Low T1 / High STIR marrow oedema; periosteal oedema; soft-tissue swelling. X-ray: normal or soft-tissue swelling only."),
("Subacute (2-6 weeks)", "Bone abscess (Brodie's abscess) - well-defined rim-enhancing cavity; periosteal reaction. X-ray: lucent/sclerotic lesion."),
("Chronic (>6 weeks)", "Sequestrum (low T1/T2 fragment), involucrum, cloaca, sinus tracts, soft-tissue abscess. X-ray: sclerosis, cortical thickening, Cloaca."),
],
"key_points": [
"X-ray is normal in first 7-14 days of acute osteomyelitis - do NOT use to exclude diagnosis",
"MRI is gold standard: bone marrow oedema (low T1, high STIR) within 24-48 hours of infection",
"Gadolinium contrast defines abscess cavities (rim-enhancing collections) for surgical planning",
"Chronic osteomyelitis: sequestrum (dead bone), involucrum, cloaca are pathognomonic on X-ray/CT",
"Three-phase bone scan useful for multifocal disease; WBC scintigraphy more specific for infection",
],
"images": [
{
"url": "https://cdn.orris.care/cdss_images/91c9bd83f820f961483a7b207ea270a3f17eb3b934a9ca4b2ed8ac5b93ca4ae7.png",
"caption": "Fig. 46.7 Chronic osteomyelitis of the tibia. Cortical thickening and chronic periosteal new bone (involucrum, long arrows) surround the medullary cavity. A cloaca is visible (short arrow). Source: Grainger & Allison's Diagnostic Radiology."
},
{
"url": "https://cdn.orris.care/cdss_images/4d9ec2bc32c6004e4477b4fdb511bb0bf28d1af3cedf979836954a27c30f6c1c.png",
"caption": "Fig. 46.5 Chronic recurrent multifocal osteomyelitis (CRMO) of the left clavicle. Radiograph shows osteitis/hyperostosis; CT shows sclerosis with lucent areas; coronal T1 and fat-sat T2 MRI show clavicular hypertrophy with intraosseous and periosteal oedema. Source: Grainger & Allison's Diagnostic Radiology."
},
],
"reference": "Grainger & Allison's Diagnostic Radiology, 7th Ed. Chapter 46: MSK Infections. Block 9, lines 1335-1555.",
},
# ─── CASE 5 ─────────────────────────────────────────────
{
"number": "05",
"title": "Tuberculous Vertebral Osteomyelitis (Pott's Disease)",
"subtitle": "Plain X-ray + CT + MRI | MSK Infection / Spine",
"clinical": (
"A 38-year-old male from South Asia presents with a 3-month history of progressive lower thoracic back pain, "
"low-grade fever, night sweats, and 8 kg weight loss. He has mild lower limb weakness (power 4/5 bilateral). "
"ESR is 95 mm/hr, CRP 68 mg/L. Mantoux test is strongly positive (20 mm). Chest X-ray shows subtle left upper "
"lobe shadowing. Plain X-ray of the spine shows loss of disc height at T8-T9 with some sclerosis of adjacent "
"vertebral end plates."
),
"questions": [
("Q1", "What is Pott's disease and what is the pattern of spinal involvement?"),
("Q2", "What are the plain radiograph features of tuberculous vertebral osteomyelitis?"),
("Q3", "Describe the CT and MRI features in detail."),
("Q4", "What is a 'cold abscess' and how does TB differ radiologically from pyogenic vertebral osteomyelitis?"),
("Q5", "What is the management approach?"),
],
"answers": [
("Pott's Disease", "Tuberculous (TB) spondylitis (Pott's disease) is the commonest form of musculoskeletal TB (~50% of skeletal TB cases). Mycobacterium tuberculosis reaches the spine via haematogenous spread from a primary pulmonary focus. It preferentially involves: (1) anterior subchondral bone (paradiscal), (2) central vertebral body, or (3) posterior elements (less common). Thoracolumbar junction (T10-L2) is most commonly affected. Typically affects TWO adjacent vertebrae with intervening disc involvement - this distinguishes it from metastatic disease (disc is usually spared in metastases)."),
("X-ray Features", "Reduction in disc height (early, due to disc ischaemia from vascular compromise). Destruction of vertebral end plates (irregular, poorly defined). Anterior wedging/collapse of vertebral body → gibbus deformity (angular kyphosis). Paravertebral soft-tissue shadow: fusiform paraspinal mass (abscess). Calcification within paravertebral abscess (pathognomonic). Late: sclerosis and vertebral fusion (ankylosis). Chest X-ray: evidence of pulmonary TB in ~50% of cases."),
("CT/MRI Features", "CT: Better defines extent of bony destruction, cortical erosion, sequestrum, and paraspinal abscess. Shows intra-osseous gas (rare). MRI: Investigation of choice for spinal cord compression assessment. LOW T1 / HIGH T2 signal in vertebral bodies and disc. Disc height loss with HIGH T2 signal in disc (disc is involved - unlike metastases). Post-gadolinium: peripheral rim enhancement of abscess. Cold abscess: epidural abscess with cord compression (myelopathy). Subligamentous spread: abscess tracks under the anterior/posterior longitudinal ligament, causing scalloping of multiple vertebral bodies."),
("Cold Abscess", "A 'cold abscess' is a TB paravertebral/psoas abscess with NO signs of acute inflammation (no warmth, redness - hence 'cold'). It can track along fascial planes: cervical TB → retropharyngeal abscess; thoracic TB → posterior mediastinum / chest wall; lumbar TB → psoas abscess (presents in groin/iliac fossa). TB vs Pyogenic: TB - slow onset, multiple levels, disc involvement, subligamentous spread, calcified abscess, gibbus. Pyogenic - rapid onset, usually single level, aggressive end plate destruction, no calcification, no subligamentous spread."),
("Management", "Anti-tuberculous chemotherapy (HRZE for 6-12 months) is the mainstay. Surgical indications: spinal instability, progressive neurological deficit, failure of medical therapy, or large abscess. Percutaneous CT-guided biopsy for diagnosis (culture + histology). Image-guided aspiration of psoas abscess if large. MRI is used to monitor treatment response (reduction in bone oedema and abscess resolution)."),
],
"imaging_table": [
("Modality", "Key Finding"),
("Plain X-ray", "Disc height loss, end plate erosion, anterior wedging, paravertebral shadow/calcification"),
("CT", "Bony destruction detail, sequestrum, paraspinal abscess extent, calcification"),
("MRI (Gold Standard)", "Marrow oedema (low T1/high T2), disc involvement, cord compression, epidural abscess"),
("Nuclear Medicine", "Bone scan: increased uptake (less specific); useful for multifocal survey"),
("CT-guided biopsy", "Tissue diagnosis: histology (caseating granuloma) + culture + sensitivity"),
],
"key_points": [
"Pott's disease: two adjacent vertebral bodies + disc involvement - disc sparing is more typical of metastases",
"Thoracolumbar junction (T10-L2) most commonly affected; anterior subchondral/paradiscal pattern",
"Calcified paravertebral 'cold' abscess is pathognomonic of TB (not seen in pyogenic infection)",
"MRI is essential for: cord compression, epidural abscess, subligamentous spread, surgical planning",
"Gibbus deformity (angular kyphosis from anterior wedge collapse) is a classic late complication",
],
"images": [
{
"url": "https://cdn.orris.care/cdss_images/4d9ec2bc32c6004e4477b4fdb511bb0bf28d1af3cedf979836954a27c30f6c1c.png",
"caption": "Chronic recurrent multifocal osteomyelitis: radiograph, CT and MRI appearances demonstrating periosteal oedema, sclerosis, and bone marrow changes. Source: Grainger & Allison's Diagnostic Radiology."
},
],
"reference": "Grainger & Allison's Diagnostic Radiology, 7th Ed. Chapter 46: MSK Infections - Vertebral Osteomyelitis. Block 9, lines 1874-2019.",
},
]
# ════════════════════════════════════════════════════════════
# PAGE TEMPLATE
# ════════════════════════════════════════════════════════════
class NumberedCanvas(canvas.Canvas):
def __init__(self, *args, **kwargs):
canvas.Canvas.__init__(self, *args, **kwargs)
self._saved_page_states = []
def showPage(self):
self._saved_page_states.append(dict(self.__dict__))
self._startPage()
def save(self):
num_pages = len(self._saved_page_states)
for state in self._saved_page_states:
self.__dict__.update(state)
self.draw_page_footer(num_pages)
canvas.Canvas.showPage(self)
canvas.Canvas.save(self)
def draw_page_footer(self, page_count):
self.saveState()
self.setFont("Helvetica", 8)
self.setFillColor(MED_GREY)
self.drawCentredString(A4[0]/2, 1.5*cm,
f"MSK Radiology Long Cases | Grainger & Allison's Diagnostic Radiology | Page {self._pageNumber} of {page_count}")
self.setStrokeColor(HexColor("#CBD5E1"))
self.setLineWidth(0.5)
self.line(2*cm, 1.9*cm, A4[0]-2*cm, 1.9*cm)
self.restoreState()
# ════════════════════════════════════════════════════════════
# BUILD PDF
# ════════════════════════════════════════════════════════════
def build_pdf():
doc = SimpleDocTemplate(
OUTPUT_PATH,
pagesize=A4,
leftMargin=2*cm, rightMargin=2*cm,
topMargin=2.5*cm, bottomMargin=2.5*cm,
title="MSK Radiology Long Cases",
author="Grainger & Allison's Diagnostic Radiology",
)
story = []
# ── COVER PAGE ──
story.append(Spacer(1, 2*cm))
cover_header_data = [
[Paragraph("MUSCULOSKELETAL RADIOLOGY", COVER_TITLE)],
[Paragraph("LONG CASES FOR RADIOLOGY RESIDENTS", COVER_SUB)],
[Spacer(1, 0.4*cm)],
[Paragraph("5 Comprehensive Case Studies with Imaging", COVER_BODY)],
[Paragraph("Source: Grainger & Allison's Diagnostic Radiology, 7th Edition", COVER_BODY)],
[Spacer(1, 0.5*cm)],
[Paragraph("Topics Covered:", mks("CvTpc", fontSize=12, leading=15, textColor=HexColor("#93C5FD"), alignment=TA_CENTER))],
[Paragraph("ACL Tear · Osteosarcoma · Rotator Cuff Tear · Osteomyelitis · Pott's Disease",
mks("CvItems", fontSize=11, leading=14, textColor=HexColor("#BFDBFE"), alignment=TA_CENTER))],
]
cover_table = Table(cover_header_data, colWidths=[17*cm])
cover_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), DARK_BLUE),
("TOPPADDING", (0,0), (-1,-1), 10),
("BOTTOMPADDING", (0,0), (-1,-1), 10),
("LEFTPADDING", (0,0), (-1,-1), 20),
("RIGHTPADDING", (0,0), (-1,-1), 20),
("ROWBACKGROUNDS",(0,0), (-1,-1), [DARK_BLUE]),
]))
story.append(cover_table)
story.append(Spacer(1, 1*cm))
# cover decorative boxes
cases_overview = [
["Case", "Topic", "Modality", "Key Diagnosis"],
["01", "ACL Tear", "MRI Knee", "Complete ACL rupture + pivot-shift contusions"],
["02", "Bone Tumour", "X-ray + MRI + CT", "Conventional osteosarcoma, distal femur"],
["03", "Shoulder Pain", "MRI / Ultrasound", "Full-thickness supraspinatus tear"],
["04", "Paediatric Fever", "X-ray + MRI + NM", "Acute haematogenous osteomyelitis"],
["05", "Back Pain + TB", "X-ray + CT + MRI", "Tuberculous vertebral osteomyelitis (Pott's)"],
]
cw = [1.5*cm, 3.5*cm, 4.5*cm, 7.5*cm]
ov_data = []
for i, row in enumerate(cases_overview):
if i == 0:
ov_data.append([Paragraph(c, TABLE_HDR) for c in row])
else:
ov_data.append([Paragraph(c, TABLE_CELL) for c in row])
ov_table = Table(ov_data, colWidths=cw)
ov_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), DARK_BLUE),
("ROWBACKGROUNDS",(0,1), (-1,-1), [LIGHT_GREY, white]),
("TOPPADDING", (0,0), (-1,-1), 6),
("BOTTOMPADDING", (0,0), (-1,-1), 6),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING", (0,0), (-1,-1), 8),
("GRID", (0,0), (-1,-1), 0.5, HexColor("#CBD5E1")),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
]))
story.append(ov_table)
story.append(Spacer(1, 0.6*cm))
disclaimer_data = [[Paragraph(
"<b>For Educational Use Only.</b> All content is sourced from Grainger & Allison's Diagnostic Radiology (7th Ed.). "
"Images are reproduced for educational/teaching purposes. Clinical cases are constructed to represent "
"typical presentations encountered in radiology residency examinations.",
mks("Disc", fontSize=9, leading=12, textColor=DARK_GREY))]]
disc_table = Table(disclaimer_data, colWidths=[17*cm])
disc_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), LIGHT_GREY),
("TOPPADDING", (0,0), (-1,-1), 8),
("BOTTOMPADDING", (0,0), (-1,-1), 8),
("LEFTPADDING", (0,0), (-1,-1), 12),
("RIGHTPADDING", (0,0), (-1,-1), 12),
("BOX", (0,0), (-1,-1), 0.8, MED_GREY),
]))
story.append(disc_table)
story.append(PageBreak())
# ── CASES ──
for case in CASES:
story.append(case_header(case["number"], case["title"], case["subtitle"]))
story.append(Spacer(1, 0.4*cm))
# Clinical scenario
story.append(section_box("CLINICAL SCENARIO", DARK_BLUE))
story.append(Spacer(1, 0.15*cm))
story.append(Paragraph(case["clinical"], BODY))
story.append(Spacer(1, 0.4*cm))
# Questions
story.append(section_box("EXAMINATION QUESTIONS", MID_BLUE))
story.append(Spacer(1, 0.15*cm))
for q_label, q_text in case["questions"]:
story.append(Paragraph(f"<b>{q_label}:</b> {q_text}", BULLET))
story.append(Spacer(1, 0.4*cm))
# Images
if case.get("images"):
story.append(section_box("IMAGING", ACCENT_TEAL))
story.append(Spacer(1, 0.2*cm))
for img_info in case["images"]:
img = fetch_image(img_info["url"], max_w=13*cm, max_h=8*cm)
if img:
img_table = Table([[img]], colWidths=[17*cm])
img_table.setStyle(TableStyle([
("ALIGN", (0,0), (-1,-1), "CENTER"),
("TOPPADDING", (0,0), (-1,-1), 6),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("BACKGROUND", (0,0), (-1,-1), LIGHT_GREY),
("BOX", (0,0), (-1,-1), 0.5, HexColor("#CBD5E1")),
]))
story.append(img_table)
story.append(Paragraph(img_info["caption"], CAPTION))
story.append(Spacer(1, 0.2*cm))
# Imaging modality table
if case.get("imaging_table"):
story.append(section_box("IMAGING MODALITIES & FINDINGS", MID_BLUE))
story.append(Spacer(1, 0.15*cm))
rows = [case["imaging_table"][0]] # header
for r in case["imaging_table"][1:]:
rows.append(r)
tbl_data = []
for i, row in enumerate(rows):
if i == 0:
tbl_data.append([Paragraph(c, TABLE_HDR) for c in row])
else:
tbl_data.append([Paragraph(c, TABLE_CELL) for c in row])
mod_table = Table(tbl_data, colWidths=[5*cm, 12*cm])
mod_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), DARK_BLUE),
("ROWBACKGROUNDS",(0,1), (-1,-1), [LIGHT_GREY, white]),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING", (0,0), (-1,-1), 8),
("GRID", (0,0), (-1,-1), 0.4, HexColor("#CBD5E1")),
("VALIGN", (0,0), (-1,-1), "TOP"),
]))
story.append(mod_table)
story.append(Spacer(1, 0.4*cm))
# Answers
story.append(section_box("MODEL ANSWERS", ACCENT_TEAL))
story.append(Spacer(1, 0.15*cm))
ans_data = []
for a_label, a_text in case["answers"]:
ans_data.append([
Paragraph(f"<b>{a_label}</b>", mks(f"AHL{a_label[:4]}", fontSize=10, leading=12, textColor=white)),
Paragraph(a_text, mks(f"ABL{a_label[:4]}", fontSize=10, leading=13, textColor=DARK_GREY))
])
ans_table = Table(ans_data, colWidths=[3.2*cm, 13.8*cm])
ans_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (0,-1), ACCENT_TEAL),
("BACKGROUND", (1,0), (1,-1), HexColor("#F0FDFA")),
("ROWBACKGROUNDS",(1,0), (1,-1), [HexColor("#F0FDFA"), white]),
("TOPPADDING", (0,0), (-1,-1), 7),
("BOTTOMPADDING", (0,0), (-1,-1), 7),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING", (0,0), (-1,-1), 8),
("GRID", (0,0), (-1,-1), 0.4, HexColor("#CBD5E1")),
("VALIGN", (0,0), (-1,-1), "TOP"),
]))
story.append(ans_table)
story.append(Spacer(1, 0.4*cm))
# Key points
story.append(key_points_box(case["key_points"]))
story.append(Spacer(1, 0.3*cm))
# Reference
story.append(Paragraph(f"<i>Reference: {case['reference']}</i>", REF_STYLE))
story.append(HRFlowable(width="100%", thickness=0.5, color=HexColor("#CBD5E1"), spaceAfter=5))
story.append(PageBreak())
doc.build(story, canvasmaker=NumberedCanvas)
print(f"\n✓ PDF saved to: {OUTPUT_PATH}")
if __name__ == "__main__":
build_pdf()
cd /home/daytona/workspace/radiology-long-cases && python build_msk_cases.py 2>&1
~/radiology-long-cases/build_msk_cases.py
Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/radiology-long-cases/build_msk_cases.py'
cd /home/daytona/workspace/radiology-long-cases && python build_msk_cases.py 2>&1
~/radiology-long-cases/build_msk_cases.py
Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/radiology-long-cases/build_msk_cases.py'
~/radiology-long-cases/build_msk_cases.py
Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/radiology-long-cases/build_msk_cases.py'
~/radiology-long-cases/build_msk_cases.py
Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/radiology-long-cases/build_msk_cases.py'
~/radiology-long-cases/build_msk_cases.py
Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/radiology-long-cases/build_msk_cases.py'
~/radiology-long-cases/build_msk_cases.py
Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/radiology-long-cases/build_msk_cases.py'
~/radiology-long-cases/build_msk_cases.py
Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/radiology-long-cases/build_msk_cases.py'
cd /home/daytona/workspace/radiology-long-cases && python build_msk_cases.py 2>&1
cd /home/daytona/workspace/radiology-long-cases && sed -i 's/MSKBullet/BULLET/' build_msk_cases.py && sed -i 's/^BULLET.*$/BULLET = mks("MSKBullet2", fontSize=10.5, leading=14, textColor=HexColor("#1E293B"), leftIndent=14, spaceAfter=3)/' build_msk_cases.py && python build_msk_cases.py 2>&1
cd /home/daytona/workspace/radiology-long-cases && python3 -c " content = open('build_msk_cases.py').read() content = content.replace('MSKBullet', 'BULLET') content = content.replace( 'BULLET = mks(\"MSKBullet\", fontSize=10.5, leading=14, textColor=DARK_GREY, leftIndent=14, spaceAfter=3)', 'BULLET = mks(\"MSKBulletX\", fontSize=10.5, leading=14, textColor=HexColor(\"#1E293B\"), leftIndent=14, spaceAfter=3)' ) open('build_msk_cases.py','w').write(content) print('done') " && python build_msk_cases.py 2>&1
Continue
MSK Radiology Long Cases
PDF Document · PDF
| Case | Topic | Modality | Key Diagnosis |
|---|---|---|---|
| 01 | ACL Tear with Associated Knee Injuries | MRI Knee | Complete ACL rupture + pivot-shift bone contusions |
| 02 | Osteosarcoma of the Distal Femur | X-ray + MRI + CT | Conventional osteosarcoma, metaphyseal, distal femur |
| 03 | Rotator Cuff Tear | MRI / Ultrasound Shoulder | Full-thickness supraspinatus tear with impingement |
| 04 | Acute Haematogenous Osteomyelitis | X-ray + MRI + Nuclear Medicine | Paediatric tibial metaphysis osteomyelitis |
| 05 | Pott's Disease | X-ray + CT + MRI | Tuberculous vertebral osteomyelitis (T8-T9) |
Send examination based msk cases and also remove description of images keep it question format only
~/radiology-long-cases/build_msk_exam.py
#!/usr/bin/env python3
"""
MSK Radiology Long Cases - EXAMINATION FORMAT
Layout per case:
PAGE 1: Case number | Clinical history | Image(s) only (NO captions) | Questions
PAGE 2: Answers | Key Points | Imaging Table | Reference
Source: Grainger & Allison's Diagnostic Radiology, 7th Ed.
"""
import requests
from io import BytesIO
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import cm
from reportlab.lib.colors import HexColor, white, black
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
Image, HRFlowable, PageBreak, KeepTogether
)
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_JUSTIFY
from reportlab.pdfgen import canvas
OUTPUT_PATH = "/home/daytona/workspace/radiology-long-cases/MSK_Radiology_Exam_Cases.pdf"
# ── Colours ──────────────────────────────────────────────────────────────────
DARK_BLUE = HexColor("#1B3A6B")
MID_BLUE = HexColor("#2563EB")
LIGHT_BLUE = HexColor("#DBEAFE")
TEAL = HexColor("#0F766E")
GOLD = HexColor("#92400E")
GOLD_BG = HexColor("#FEF3C7")
LIGHT_GREY = HexColor("#F1F5F9")
MED_GREY = HexColor("#64748B")
DARK_GREY = HexColor("#1E293B")
GREEN_DK = HexColor("#166534")
GREEN_BG = HexColor("#F0FDF4")
GREEN_BDR = HexColor("#16A34A")
RED = HexColor("#DC2626")
# ── Style registry ────────────────────────────────────────────────────────────
styles = getSampleStyleSheet()
_sc = {}
def S(name, **kw):
if name in _sc:
return _sc[name]
base = kw.pop("parent", "Normal")
s = ParagraphStyle(name, parent=styles[base], **kw)
try:
styles.add(s)
except KeyError:
pass
_sc[name] = s
return s
# Pre-build all styles
def init_styles():
S("CoverTitle", fontSize=28, leading=34, textColor=white, alignment=TA_CENTER, spaceAfter=6)
S("CoverSub", fontSize=15, leading=19, textColor=HexColor("#BFDBFE"), alignment=TA_CENTER, spaceAfter=4)
S("CoverNote", fontSize=10, leading=13, textColor=HexColor("#E0F2FE"), alignment=TA_CENTER)
S("CaseNum", fontSize=10, leading=12, textColor=HexColor("#BAE6FD"), alignment=TA_LEFT)
S("CaseTitle", fontSize=21, leading=26, textColor=white, alignment=TA_LEFT)
S("CaseSub", fontSize=11, leading=14, textColor=HexColor("#BAE6FD"), alignment=TA_LEFT)
S("SecHdr", fontSize=12, leading=15, textColor=white, alignment=TA_LEFT)
S("Body", fontSize=10.5, leading=15, textColor=DARK_GREY, alignment=TA_JUSTIFY, spaceAfter=4)
S("BodyB", fontSize=10.5, leading=15, textColor=DARK_GREY, spaceAfter=3)
S("QText", fontSize=11, leading=15, textColor=DARK_GREY, leftIndent=10, spaceAfter=5)
S("QLabel", fontSize=11, leading=15, textColor=MID_BLUE, spaceAfter=1)
S("ALabel", fontSize=10, leading=12, textColor=white)
S("ABody", fontSize=10, leading=13, textColor=DARK_GREY)
S("KPText", fontSize=10, leading=14, textColor=DARK_GREY)
S("TblHdr", fontSize=9.5, leading=12, textColor=white, alignment=TA_CENTER)
S("TblCell", fontSize=9, leading=12, textColor=DARK_GREY, alignment=TA_LEFT)
S("RefText", fontSize=8.5, leading=11, textColor=MED_GREY, spaceAfter=2)
S("PageLabel", fontSize=9, leading=11, textColor=white, alignment=TA_CENTER)
S("Disclaimer", fontSize=9, leading=12, textColor=DARK_GREY)
S("AnswerNote", fontSize=9.5, leading=12, textColor=HexColor("#92400E"), alignment=TA_CENTER)
init_styles()
# ── Helpers ───────────────────────────────────────────────────────────────────
def fetch_img(url, max_w=14*cm, max_h=9*cm):
try:
r = requests.get(url, timeout=15)
r.raise_for_status()
raw = r.content
from PIL import Image as PILImage
pil = PILImage.open(BytesIO(raw))
w_px, h_px = pil.size
ratio = h_px / w_px
w = max_w
h = w * ratio
if h > max_h:
h = max_h
w = h / ratio
return Image(BytesIO(raw), width=w, height=h)
except Exception as e:
print(f" [WARN] Image fetch failed: {e}")
return None
def box(title, bg=DARK_BLUE, width=17*cm):
t = Table([[Paragraph(title, S("SecHdr"))]], colWidths=[width])
t.setStyle(TableStyle([
("BACKGROUND", (0,0),(-1,-1), bg),
("TOPPADDING", (0,0),(-1,-1), 6),
("BOTTOMPADDING", (0,0),(-1,-1), 6),
("LEFTPADDING", (0,0),(-1,-1), 10),
("RIGHTPADDING", (0,0),(-1,-1), 10),
]))
return t
def case_header(num, title, subtitle, page_label):
data = [
[Paragraph(f"LONG CASE {num} | {page_label}", S("CaseNum"))],
[Paragraph(title, S("CaseTitle"))],
[Paragraph(subtitle, S("CaseSub"))],
]
t = Table(data, colWidths=[17*cm])
t.setStyle(TableStyle([
("BACKGROUND", (0,0),(-1,-1), DARK_BLUE),
("TOPPADDING", (0,0),(0,0), 12),
("BOTTOMPADDING", (0,0),(-1,-1), 10),
("LEFTPADDING", (0,0),(-1,-1), 14),
("RIGHTPADDING", (0,0),(-1,-1), 14),
]))
return t
def modality_table(rows):
data = [[Paragraph("Modality", S("TblHdr")), Paragraph("Finding / Role", S("TblHdr"))]]
for r in rows:
data.append([Paragraph(r[0], S("TblCell")), Paragraph(r[1], S("TblCell"))])
t = Table(data, colWidths=[4.5*cm, 12.5*cm])
t.setStyle(TableStyle([
("BACKGROUND", (0,0),(-1,0), DARK_BLUE),
("ROWBACKGROUNDS",(0,1),(-1,-1),[LIGHT_GREY, white]),
("TOPPADDING", (0,0),(-1,-1), 5),
("BOTTOMPADDING", (0,0),(-1,-1), 5),
("LEFTPADDING", (0,0),(-1,-1), 7),
("RIGHTPADDING", (0,0),(-1,-1), 7),
("GRID", (0,0),(-1,-1), 0.4, HexColor("#CBD5E1")),
("VALIGN", (0,0),(-1,-1), "TOP"),
]))
return t
def answer_table(answers):
data = []
for lbl, txt in answers:
data.append([
Paragraph(f"<b>{lbl}</b>", S("ALabel")),
Paragraph(txt, S("ABody"))
])
t = Table(data, colWidths=[3*cm, 14*cm])
t.setStyle(TableStyle([
("BACKGROUND", (0,0),(0,-1), TEAL),
("ROWBACKGROUNDS",(1,0),(1,-1),[HexColor("#F0FDFA"), white]),
("TOPPADDING", (0,0),(-1,-1), 7),
("BOTTOMPADDING", (0,0),(-1,-1), 7),
("LEFTPADDING", (0,0),(-1,-1), 8),
("RIGHTPADDING", (0,0),(-1,-1), 8),
("GRID", (0,0),(-1,-1), 0.4, HexColor("#CBD5E1")),
("VALIGN", (0,0),(-1,-1), "TOP"),
]))
return t
def key_points_box(points):
items = "\n".join(f"• {p}" for p in points)
data = [[Paragraph(f"<b>KEY IMAGING POINTS</b><br/><br/>" + "<br/>".join(f"• {p}" for p in points), S("KPText"))]]
t = Table(data, colWidths=[17*cm])
t.setStyle(TableStyle([
("BACKGROUND", (0,0),(-1,-1), GREEN_BG),
("BOX", (0,0),(-1,-1), 1.5, GREEN_BDR),
("TOPPADDING", (0,0),(-1,-1), 10),
("BOTTOMPADDING", (0,0),(-1,-1), 10),
("LEFTPADDING", (0,0),(-1,-1), 14),
("RIGHTPADDING", (0,0),(-1,-1), 14),
]))
return t
def page_break_with_ans_note():
data = [[Paragraph("▼ ANSWERS ON NEXT PAGE ▼", S("AnswerNote"))]]
t = Table(data, colWidths=[17*cm])
t.setStyle(TableStyle([
("BACKGROUND", (0,0),(-1,-1), GOLD_BG),
("BOX", (0,0),(-1,-1), 1, HexColor("#D97706")),
("TOPPADDING", (0,0),(-1,-1), 6),
("BOTTOMPADDING", (0,0),(-1,-1), 6),
("ALIGN", (0,0),(-1,-1), "CENTER"),
]))
return t
# ── Numbered Canvas ────────────────────────────────────────────────────────────
class NC(canvas.Canvas):
def __init__(self, *args, **kwargs):
canvas.Canvas.__init__(self, *args, **kwargs)
self._saved = []
def showPage(self):
self._saved.append(dict(self.__dict__))
self._startPage()
def save(self):
n = len(self._saved)
for state in self._saved:
self.__dict__.update(state)
self._footer(n)
canvas.Canvas.showPage(self)
canvas.Canvas.save(self)
def _footer(self, total):
self.saveState()
self.setFont("Helvetica", 7.5)
self.setFillColor(MED_GREY)
self.drawCentredString(
A4[0]/2, 1.4*cm,
f"MSK Radiology Examination Cases | Grainger & Allison's Diagnostic Radiology | Page {self._pageNumber} of {total}"
)
self.setStrokeColor(HexColor("#CBD5E1"))
self.setLineWidth(0.5)
self.line(2*cm, 1.85*cm, A4[0]-2*cm, 1.85*cm)
self.restoreState()
# ══════════════════════════════════════════════════════════════════════════════
# CASE DATA
# ══════════════════════════════════════════════════════════════════════════════
CASES = [
# ──────────────────────────────────────────────────────────────────────────
{
"num": "01",
"title": "ACL Tear with Associated Knee Injuries",
"subtitle": "MRI Knee | Sports Injury",
"clinical": (
"A 24-year-old male professional footballer sustains a valgus stress injury to the right knee during a match. "
"He reports a loud 'pop' at the time of injury followed by rapid onset of haemarthrosis and inability to continue playing. "
"Examination reveals a positive Lachman test, positive pivot-shift sign, and positive anterior drawer test. "
"There is gross swelling and the patient is unable to fully extend the knee. "
"Plain radiograph of the right knee demonstrates a subtle cortical avulsion fragment at the lateral tibial plateau margin."
),
"questions": [
("Q1.", "Name the most appropriate investigation and state why it is preferred over other modalities."),
("Q2.", "Describe the normal MRI anatomy of the ACL. On which MRI plane is it best visualised, and what is its normal signal characteristic?"),
("Q3.", "List the PRIMARY and SECONDARY MRI signs of a complete ACL tear."),
("Q4.", "What is the name of the avulsion fracture seen at the lateral tibial plateau? What is its clinical significance and with which ligament injury is it most associated?"),
("Q5.", "What is O'Donoghue's 'unhappy triad'? What percentage of ACL tears have an associated meniscal injury?"),
],
"images": [
"https://cdn.orris.care/cdss_images/ce38c9a48e4eea94a5b707eb58cda5f57fdb5537d54741aad5ceab2f04791581.png",
"https://cdn.orris.care/cdss_images/d8da53fa068baba7d9d8b556170adee96842fc5eb1ebc4c23a72a2153ed78fe3.png",
],
"answers": [
("Q1", "MRI knee (1.5T or 3T). Gold standard for soft-tissue injuries — directly visualises ACL fibres, menisci, collateral ligaments, cartilage and bone marrow. Near 100% accuracy for complete tears. No ionising radiation. Allows surgical planning with single examination."),
("Q2", "The ACL runs from the posteromedial aspect of the lateral femoral condyle (intercondylar fossa) to the anterior tibial intercondylar eminence. Best seen on SAGITTAL sequences. Normal appearance: low-signal fan-shaped taut fibres running parallel to Blumensaat's line (roof of intercondylar notch). Two bundles (anteromedial + posterolateral) may be differentiated on coronal/axial sequences. Fluid and loose connective tissue between bundles means ACL normally appears larger and mixed signal compared to PCL."),
("Q3", "PRIMARY: (1) Fibre discontinuity. (2) High T2/PD signal within or replacing ligament fibres. (3) Laxity or abnormal angulation of the ligament. SECONDARY: (1) 'Pivot-shift' bone marrow kissing contusions — posterolateral tibial plateau + lateral femoral condyle. (2) Anterior tibial translation ≥7 mm. (3) PCL buckling (wavy appearance). (4) Deep condylopatellar sulcus sign — subcortical impaction fracture of lateral femoral condyle. (5) Segond fracture."),
("Q4", "Segond fracture — avulsion of cortical bone from the lateral tibial plateau at the attachment of the joint capsule and supporting ligaments. Highly specific for ACL injury. It is often subtle on MRI (better seen on plain radiograph). Indicates significant rotational/valgus stress mechanism."),
("Q5", "O'Donoghue's unhappy triad: simultaneous tears of ACL + medial collateral ligament (MCL) + medial meniscus, from valgus stress. 50% of ACL tears have an associated meniscal tear — most commonly peripheral vertical tears or posterior radial tears of either meniscus secondary to rotational stress."),
],
"modality_table": [
("Plain X-ray", "First-line; identifies Segond fracture, joint effusion, alignment; limited soft-tissue detail"),
("MRI (gold standard)", "ACL integrity, bone marrow contusions, menisci, collateral ligaments, posterolateral corner"),
("CT", "Bony detail if complex fracture pattern; poor soft-tissue contrast"),
("Ultrasound", "Limited for intra-articular structures; useful for popliteal cyst / soft-tissue pathology"),
],
"key_points": [
"Complete ACL tear: fibre discontinuity + high T2/PD signal + laxity on sagittal MRI",
"Secondary signs: pivot-shift bone marrow contusions, PCL buckling, anterior tibial translation ≥7 mm",
"Segond fracture (lateral tibial avulsion) is highly specific for ACL injury",
"50% of ACL tears have concurrent meniscal tear",
"Whole-knee MRI protocol must evaluate ACL, PCL, MCL, LCL, posterolateral corner, menisci and bone marrow",
],
"reference": "Grainger & Allison's Diagnostic Radiology, 7th Ed. Ch.39 MSK MRI — The Knee. Block 7, lines 4129–4184.",
},
# ──────────────────────────────────────────────────────────────────────────
{
"num": "02",
"title": "Osteosarcoma — Distal Femur",
"subtitle": "Plain X-ray + MRI + CT Chest | Bone Tumour",
"clinical": (
"A 15-year-old male presents with a 6-week history of progressive pain and swelling of the right distal femur, "
"worse at night and unrelieved by simple analgesia. He has no history of trauma. "
"Examination reveals a firm, warm, non-mobile mass around the right knee with restricted range of movement. "
"Bloods: WCC normal, ESR 45 mm/hr, CRP 18 mg/L, alkaline phosphatase significantly elevated. "
"Plain radiograph of the right distal femur is shown."
),
"questions": [
("Q1.", "What is the most likely diagnosis? State its incidence and typical demographic."),
("Q2.", "Describe the radiographic appearances of conventional osteosarcoma. What periosteal reaction patterns may be seen?"),
("Q3.", "What is the role of MRI in osteosarcoma? What must ALWAYS be included in the MRI examination and why?"),
("Q4.", "What are skip metastases? In what percentage of cases do they occur? What is the clinical significance?"),
("Q5.", "Name the 2013 WHO classification of osteosarcoma subtypes and state which subtype accounts for 75% of all cases."),
],
"images": [
"https://cdn.orris.care/cdss_images/6f23f9305fb931f4bfc24c17ee2e15f3095932325f0d0c8655f7628ab82b2ad5.png",
"https://cdn.orris.care/cdss_images/2f55ca7dfcf7268c67e51c8914df7869bb1a37f7d45aa9f241580174350a7fd2.png",
"https://cdn.orris.care/cdss_images/e8f79f5b4174d4693f189bca95e591a316e11ab39af6327b5b1c8d5a97458f57.png",
],
"answers": [
("Q1", "Conventional (central) osteosarcoma. Commonest non-haematological primary bone malignancy. Incidence ~4 per million/year. Bimodal age distribution: first peak 10–14 years; second peak in older adults (secondary osteosarcoma). Slight male preponderance (1.35:1). Predilection for metaphysis of long bones: distal femur (most common) > proximal tibia > proximal humerus."),
("Q2", "Metaphyseal location. Mixed lytic and sclerotic pattern (or purely lytic 13%, or sclerotic). Moth-eaten / permeative bone destruction. Cortical destruction with eccentric extra-osseous soft-tissue mass showing amorphous mineralisation. Periosteal reactions: (1) Sunburst / spiculated (perpendicular to cortex), (2) Lamellated / onion-skin, (3) Codman triangle (reactive periosteal elevation at tumour margins) — any combination may coexist."),
("Q3", "MRI is MANDATORY for local surgical staging. Accurately defines: (1) intramedullary extent of tumour, (2) cortical destruction and soft-tissue mass, (3) neurovascular involvement, (4) joint involvement. At least ONE sequence must image the WHOLE affected bone (including joint above and below) — this is to detect SKIP METASTASES, which would be missed on limited field-of-view sequences."),
("Q4", "Skip metastases: discrete foci of tumour within the SAME bone, separate from the main tumour (or rarely trans-articular). Occur in ~5% of cases. Clinical significance: alters surgical planning — limb-salvage surgery may not be feasible if skip metastasis is present near a joint; changes prognosis."),
("Q5", "2013 WHO Classification: (1) Conventional osteosarcoma — 75%, high-grade intramedullary; (2) Telangiectatic; (3) Low-grade central; (4) Small cell; (5) Parosteal; (6) Periosteal; (7) High-grade surface osteosarcoma. Secondary osteosarcoma occurs on Paget's disease, post-radiotherapy, or dedifferentiated lower-grade sarcoma background."),
],
"modality_table": [
("Plain X-ray", "First-line; matrix mineralisation, periosteal reaction, Codman triangle, cortical destruction"),
("MRI (mandatory)", "Local staging: medullary extent, soft tissue, neurovascular, joint, WHOLE bone for skip metastases"),
("CT Chest", "Pulmonary metastases staging (calcified peripheral nodules)"),
("FDG-PET/CT", "Chemotherapy response assessment; recurrence detection; complementary to CT staging"),
("Bone scintigraphy", "Multifocal/skip lesions; calcified pulmonary metastases may show tracer uptake"),
],
"key_points": [
"Commonest non-haematological primary bone malignancy — peak age 10–14 years — distal femur/proximal tibia",
"Radiograph: metaphyseal mixed lytic-sclerotic lesion + sunburst periosteal reaction + Codman triangle",
"MRI is mandatory for local staging — whole bone must be imaged to exclude skip metastases (~5%)",
"CT chest for pulmonary metastases (may be calcified, may cause pneumothorax)",
"Dynamic MRI pre- and post-neoadjuvant chemotherapy assesses histological response (>90% necrosis = good prognosis)",
],
"reference": "Grainger & Allison's Diagnostic Radiology, 7th Ed. Ch.41 Bone Tumours. Block 8, lines 1194–1281.",
},
# ──────────────────────────────────────────────────────────────────────────
{
"num": "03",
"title": "Full-Thickness Rotator Cuff Tear",
"subtitle": "MRI / Ultrasound Shoulder | Impingement Syndrome",
"clinical": (
"A 56-year-old female presents with a 4-month history of right shoulder pain, worse on active abduction above 90 degrees "
"and at night. She reports progressive weakness and difficulty reaching overhead. "
"Examination: positive Neer's impingement sign, positive Hawkins–Kennedy test, weakness of active abduction. "
"Plain radiograph demonstrates a subacromial enthesophyte on the lateral acromion and mild narrowing of the subacromial space. "
"No history of acute trauma. MRI of the right shoulder is performed."
),
"questions": [
("Q1.", "Describe the pathophysiology of external impingement syndrome and rotator cuff disease. What anatomical variants predispose to it?"),
("Q2.", "What are the plain radiograph findings in rotator cuff disease? At what subacromial space measurement should a full-thickness tear be suspected?"),
("Q3.", "What is the PRIMARY MRI sign of a full-thickness supraspinatus tear? What abnormal communication does it create and how is this demonstrated?"),
("Q4.", "Distinguish between a partial-thickness tear (PTT) and a full-thickness tear (FTT). Define a 'massive' rotator cuff tear."),
("Q5.", "List FIVE features that must be reported on MRI/ultrasound of the rotator cuff that directly influence clinical management."),
],
"images": [
"https://cdn.orris.care/cdss_images/8d9891c2e5b7ef9e5473e8349c1a16ffbdd0dac1b32c00e8f8bd8c6934fa1d65.png",
"https://cdn.orris.care/cdss_images/4b73b78dd5c6218fbcaa4f136af661c64a16d01e54217b36712133df08810a44.png",
"https://cdn.orris.care/cdss_images/2e501b34e02dd852db74c40e7ed43273ed8f7c7cdb7b86ee7135043fbd2c1b1a.png",
],
"answers": [
("Q1", "External impingement: rotator cuff tendons (predominantly supraspinatus) are compressed between the humeral head and the coracoacromial arch during abduction. Progression: reversible oedema/haemorrhage → tendinopathy (with mucoid degeneration) → partial tear → full-thickness tear → rotator cuff arthropathy. Predisposing anatomical variants: Type III (hooked) acromion, ACJ osteophytes, os acromiale. Secondary impingement can result from abnormal rotator cuff muscle coordination / scapulothoracic dyskinesia."),
("Q2", "Radiograph findings: (1) Subacromial enthesophyte on lateral acromion / greater tuberosity. (2) Acromion shape (type I flat, II curved, III hooked). (3) ACJ osteophytes. (4) Greater tuberosity sclerosis and cystic change. (5) Superior migration of humeral head. (6) Narrowing of subacromial space. A subacromial space <7 mm is suspicious for full-thickness tear; <5 mm is highly specific."),
("Q3", "Primary MRI sign: focal DEFECT in the supraspinatus tendon at its footprint, filled with HIGH SIGNAL FLUID on T2/PD fat-saturated sequences. A full-thickness tear creates an ABNORMAL COMMUNICATION between the glenohumeral joint (GHJ) and subacromial bursa (SAB) — normally separate compartments. Demonstrated on MRI by fluid signal extending from GHJ through the tear into the SAB. On arthrography, contrast injected into GHJ fills the SAB through the tear."),
("Q4", "PTT (partial-thickness): involves either articular surface (commonest) or bursal surface, but does NOT extend through the full tendon thickness — NO communication between GHJ and SAB. FTT (full-thickness): extends from articular to bursal surface — creates abnormal GHJ-SAB communication. 'Massive' tear: ≥5 cm in short axis OR involves ≥2 rotator cuff tendons. Massive tears are associated with superior humeral migration and rotator cuff arthropathy."),
("Q5", "(1) Size and shape of tear (AP and mediolateral dimensions). (2) Location of tear (footprint, myotendinous junction). (3) Degree of tendon RETRACTION (medial displacement of torn edge). (4) Rotator cuff muscle ATROPHY with fatty infiltration — Goutallier grade (0–4); grade ≥3 predicts poor surgical repair outcome. (5) Long head of biceps (LHB) tendon status — subluxation from bicipital groove or rupture (associated with pulley tear and subscapularis injury). Also: subscapularis tendon integrity, GHJ arthrosis, ACJ impinging osteophytes."),
],
"modality_table": [
("Plain X-ray", "Enthesophyte, subacromial space (<7 mm suspicious, <5 mm specific for FTT), humeral migration"),
("MRI (preferred)", "Gold standard: T2/PD fat-sat for tear characterisation, muscle atrophy, all cuff tendons"),
("Ultrasound", "Comparable accuracy for FTT; dynamic assessment; guided injection; operator dependent"),
("MR Arthrography", "Preferred for labral pathology; not routinely needed for primary rotator cuff disease"),
],
"key_points": [
"FTT primary MRI sign: fluid-filled tendon defect on T2/PD fat-sat with GHJ–SAB communication",
"Subacromial space <7 mm suspicious; <5 mm highly specific for full-thickness tear on X-ray",
"Goutallier grade ≥3 (>50% fatty infiltration) predicts poor outcome after surgical repair",
"LHB tendon subluxation/rupture is a key co-pathology — indicates pulley and subscapularis injury",
"Massive tear = ≥5 cm or ≥2 tendons — may require reverse shoulder arthroplasty rather than repair",
],
"reference": "Grainger & Allison's Diagnostic Radiology, 7th Ed. Ch.39 MSK MRI — The Shoulder. Block 7, lines 3422–3496.",
},
# ──────────────────────────────────────────────────────────────────────────
{
"num": "04",
"title": "Acute Haematogenous Osteomyelitis",
"subtitle": "Plain X-ray + MRI + Nuclear Medicine | Paediatric MSK Infection",
"clinical": (
"A 9-year-old boy is referred with a 5-day history of high fever (39.1°C), right leg pain, refusal to bear weight, "
"and point tenderness over the distal tibial metaphysis. He has no history of trauma or recent surgery. "
"Blood cultures have been taken. Investigations: CRP 168 mg/L, WCC 19.2 × 10⁹/L, ESR 88 mm/hr. "
"Plain radiograph of the right tibia/fibula is reported as showing soft-tissue swelling only with no bony abnormality. "
"The paediatric orthopaedic team requests urgent further imaging."
),
"questions": [
("Q1.", "Why is the plain radiograph normal in early acute osteomyelitis? When do radiographic changes become visible?"),
("Q2.", "What is the investigation of choice in this clinical scenario? What findings would you expect and how early can they be detected?"),
("Q3.", "Describe the MRI features of a Brodie's abscess. How does it differ from a simple bone abscess?"),
("Q4.", "Describe the imaging features of CHRONIC osteomyelitis on (a) plain radiograph, (b) CT, and (c) MRI. Define: sequestrum, involucrum, cloaca."),
("Q5.", "What is the role of nuclear medicine imaging in osteomyelitis? Name TWO scintigraphic techniques used and state their relative advantages."),
],
"images": [
"https://cdn.orris.care/cdss_images/91c9bd83f820f961483a7b207ea270a3f17eb3b934a9ca4b2ed8ac5b93ca4ae7.png",
"https://cdn.orris.care/cdss_images/4d9ec2bc32c6004e4477b4fdb511bb0bf28d1af3cedf979836954a27c30f6c1c.png",
],
"answers": [
("Q1", "Bone destruction is not visible on plain radiograph until 30–50% of bone mineral density has been lost. This typically takes 7–14 days. Early changes (5–7 days): periosteal reaction and deep soft-tissue swelling (loss of fat planes). Later (2+ weeks): focal lytic areas, cortical erosion, periosteal reaction. Plain X-ray has LOW sensitivity for acute osteomyelitis but is always performed first to exclude alternative diagnoses (fracture, tumour, Ewing's sarcoma)."),
("Q2", "MRI is the investigation of choice (sensitivity ~95%, specificity ~92%). Findings detectable within 24–48 hours of infection onset. MRI features: (1) LOW T1 signal in marrow (replacing normal fatty marrow). (2) HIGH STIR/T2 signal in marrow (bone marrow oedema). (3) Periosteal oedema / elevation. (4) Adjacent soft-tissue oedema (high T2). (5) Post-gadolinium T1: rim-enhancing abscess (if present). (6) Subperiosteal abscess: fluid collection beneath elevated periosteum."),
("Q3", "Brodie's abscess: chronic bone abscess, most commonly in the tibial metaphysis of children/adolescents. MRI: central PUSCAVITY (low T1, high T2), surrounded by a SCLEROTIC RIM (low T1/T2), with surrounding bone marrow oedema (high STIR). The 'penumbra sign': intermediate T1 signal layer inside the sclerotic rim (represents granulation tissue) — this distinguishes Brodie's abscess from other lesions. The 'double-line sign' on MRI: inner high-T2 granulation tissue ring + outer low-T2 sclerotic bone ring."),
("Q4", "(a) Plain X-ray: bone sclerosis, cortical thickening (involucrum), intraosseous cavity, sequestrum as a dense fragment, cloaca (cortical defect), bone deformity, periosteal new bone. (b) CT: superior to X-ray for demonstrating cloaca and sequestrum (better cortical detail); shows periosteal new bone, soft-tissue abscess, sinus tracts. (c) MRI: sequestrum (low T1/T2 fragment surrounded by high T2 signal); sinus tracts (linear high T2 tracks to skin); soft-tissue abscess (rim-enhancing); involucrum (low T1/T2 thick cortical bone). Definitions: Sequestrum = dead bone fragment; Involucrum = new bone surrounding dead bone; Cloaca = defect in involucrum allowing pus drainage."),
("Q5", "Three-phase bone scintigraphy (Tc-99m MDP): sensitive but non-specific; useful for MULTIFOCAL disease, paediatric patients (avoids radiation of CT). Shows increased uptake at all three phases in acute infection. ¹¹¹In White Blood Cell (WBC) scintigraphy: more specific for active infection (labels leukocytes); combined WBC + bone marrow scintigraphy improves specificity (bone marrow background subtracted). FDG-PET/CT: high sensitivity and specificity, particularly for chronic/prosthetic joint infection; shorter imaging time than WBC scan; increasingly used in complex cases."),
],
"modality_table": [
("Plain X-ray", "First-line; normal in first 7–14 days; later shows periosteal reaction, lytic areas, sequestrum"),
("MRI (gold standard)", "Bone marrow oedema within 24–48 h; subperiosteal/soft-tissue abscess; cord/epidural extension"),
("Ultrasound", "Subperiosteal abscess, soft-tissue collection, periosteal elevation; guides aspiration"),
("Tc-99m Bone Scan", "Multifocal disease survey; sensitive but non-specific"),
("¹¹¹In WBC Scan", "More specific for active infection; combined with bone marrow scan for best specificity"),
],
"key_points": [
"Plain X-ray is NORMAL in first 7–14 days — never use to exclude acute osteomyelitis",
"MRI detects bone marrow oedema (low T1 / high STIR) within 24–48 hours of infection",
"Gadolinium contrast defines abscess cavities (rim enhancement) — essential for surgical planning",
"Chronic osteomyelitis triad: sequestrum + involucrum + cloaca (best shown on CT)",
"Brodie's abscess: 'penumbra sign' on MRI (granulation tissue layer) distinguishes it from tumour",
],
"reference": "Grainger & Allison's Diagnostic Radiology, 7th Ed. Ch.46 MSK Infections. Block 9, lines 1184–1555.",
},
# ──────────────────────────────────────────────────────────────────────────
{
"num": "05",
"title": "Tuberculous Vertebral Osteomyelitis (Pott's Disease)",
"subtitle": "Plain X-ray + CT + MRI | Spinal Infection",
"clinical": (
"A 38-year-old male, originally from South Asia, presents with a 3-month history of progressive lower thoracic "
"back pain, low-grade fever, profuse night sweats, and 9 kg unintentional weight loss. "
"He reports new onset of bilateral lower limb weakness and difficulty walking for the past 2 weeks. "
"Neurological examination reveals pyramidal weakness (power 4/5 bilaterally), bilateral upgoing plantars, "
"and a sensory level at T9. Mantoux test: 22 mm induration. ESR 98 mm/hr, CRP 72 mg/L. "
"Chest X-ray shows left upper lobe consolidation with cavitation. "
"Plain thoracic spine radiograph is shown."
),
"questions": [
("Q1.", "What is Pott's disease? Describe its pattern of spinal involvement and which spinal level is most commonly affected."),
("Q2.", "What are the plain radiograph features of tuberculous vertebral osteomyelitis? What is a 'gibbus deformity'?"),
("Q3.", "Describe the MRI findings in spinal TB. What does a 'cold abscess' mean and how does a psoas abscess present clinically?"),
("Q4.", "Compare and contrast tuberculous vertebral osteomyelitis with PYOGENIC vertebral osteomyelitis on imaging — give at least FIVE differentiating features."),
("Q5.", "What investigation is required for tissue diagnosis? What histological and microbiological findings confirm the diagnosis?"),
],
"images": [
"https://cdn.orris.care/cdss_images/4d9ec2bc32c6004e4477b4fdb511bb0bf28d1af3cedf979836954a27c30f6c1c.png",
],
"answers": [
("Q1", "Pott's disease = tuberculous spondylitis; the commonest form of skeletal TB (~50% of all MSK TB). Mycobacterium tuberculosis reaches the spine via haematogenous spread from a primary pulmonary focus. Preferential involvement: (1) Paradiscal/subchondral (commonest — anterior vertebral body, end plates and disc), (2) Central vertebral body, (3) Posterior elements (rare). Affects TWO or more adjacent vertebral bodies WITH the intervening disc — disc involvement distinguishes TB from metastases (metastases classically spare the disc). Most common level: thoracolumbar junction T10–L2."),
("Q2", "Plain X-ray features: (1) Reduction in disc height (early — due to disc ischaemia). (2) Destruction of adjacent vertebral end plates (irregular, poorly defined margins). (3) Anterior wedge collapse of vertebral body → gibbus deformity (sharp angular kyphosis — pathognomonic of Pott's disease). (4) Fusiform paravertebral soft-tissue shadow (paraspinal/psoas abscess). (5) CALCIFICATION within paravertebral abscess — pathognomonic of TB (not seen in pyogenic infection). (6) Late: vertebral fusion/ankylosis. Chest X-ray: pulmonary TB in ~50% of cases (apical shadowing, cavitation)."),
("Q3", "MRI findings: LOW T1 / HIGH STIR signal in affected vertebral bodies and disc. Disc height loss with HIGH T2 disc signal (disc is INVOLVED — unlike metastases). Post-gadolinium T1: rim-enhancing paravertebral/psoas abscess (peripheral enhancement of cold abscess walls). Subligamentous spread: abscess tracks under anterior/posterior longitudinal ligament → scalloping of multiple vertebral bodies. Epidural abscess → cord compression (enhancement of thecal sac margin). Cold abscess: TB abscess with NO signs of acute inflammation (no heat, redness) due to chronic indolent nature of TB. Psoas abscess: tracks down psoas sheath → presents in groin/iliac fossa as fluctuant mass."),
("Q4", "TB vs Pyogenic — five differentiating features: (1) ONSET: TB slow/insidious (months); pyogenic rapid/acute (days-weeks). (2) LEVELS: TB often multilevel, subligamentous spread; pyogenic usually single level. (3) DISC: TB — disc INVOLVED (disc height loss, high T2); pyogenic — disc ALSO involved but more aggressively destroyed. (4) ABSCESS CALCIFICATION: TB — present (pathognomonic); pyogenic — absent. (5) SUBLIGAMENTOUS SPREAD: TB — characteristic (scallops multiple vertebral bodies); pyogenic — rare. (6) POSTERIOR ELEMENTS: TB — may involve; pyogenic — uncommon. (7) GIBBUS DEFORMITY: TB — classic late finding; pyogenic — uncommon."),
("Q5", "CT-guided percutaneous biopsy (transpedicular or paravertebral approach) under local anaesthesia. Material sent for: (1) Histology — caseating granulomata with Langhans giant cells (pathognomonic of TB). (2) ZN stain (Ziehl-Neelsen) — acid-fast bacilli (positive in ~40%). (3) Mycobacterial culture (Lowenstein-Jensen medium) — gold standard but takes 6–8 weeks. (4) PCR for M. tuberculosis — rapid, high sensitivity. (5) Drug sensitivity testing (DST) — essential for guiding therapy (especially for MDR-TB). Biopsy is mandatory before starting anti-TB therapy."),
],
"modality_table": [
("Plain X-ray", "Disc height loss, end plate erosion, anterior wedge, paraspinal shadow, abscess calcification"),
("CT", "Bony destruction detail, sequestrum, extent of paraspinal/psoas abscess, calcification"),
("MRI (gold standard)", "Marrow oedema, disc involvement, cord compression, epidural/paravertebral abscess, subligamentous spread"),
("CT-guided biopsy", "Tissue for histology (caseating granuloma), culture, PCR, drug sensitivity"),
("Nuclear medicine", "Bone scan: increased uptake (non-specific); useful for multifocal skeletal TB survey"),
],
"key_points": [
"Pott's disease: two adjacent vertebral bodies + disc involvement; spares disc more suggestive of metastases",
"Gibbus deformity (angular kyphosis from anterior wedge collapse) is classic late complication",
"Calcified paravertebral 'cold' abscess is PATHOGNOMONIC of TB — not seen in pyogenic infection",
"MRI essential for: cord compression assessment, epidural abscess, subligamentous spread, surgical planning",
"CT-guided biopsy mandatory before therapy — histology (caseating granuloma) + culture + DST",
],
"reference": "Grainger & Allison's Diagnostic Radiology, 7th Ed. Ch.46 MSK TB — Vertebral Osteomyelitis. Block 9, lines 1874–2019.",
},
]
# ══════════════════════════════════════════════════════════════════════════════
# BUILD PDF
# ══════════════════════════════════════════════════════════════════════════════
def build():
doc = SimpleDocTemplate(
OUTPUT_PATH,
pagesize=A4,
leftMargin=2*cm, rightMargin=2*cm,
topMargin=2.5*cm, bottomMargin=2.5*cm,
title="MSK Radiology Examination Cases",
author="Grainger & Allison's Diagnostic Radiology",
)
story = []
# ─── COVER ────────────────────────────────────────────────────────────────
story.append(Spacer(1, 1.5*cm))
cover_data = [
[Paragraph("MUSCULOSKELETAL RADIOLOGY", S("CoverTitle"))],
[Paragraph("EXAMINATION LONG CASES", S("CoverSub"))],
[Spacer(1, 0.3*cm)],
[Paragraph("For Radiology Residents and Fellowship Candidates", S("CoverNote"))],
[Spacer(1, 0.5*cm)],
[Paragraph("5 Cases · Question & Answer Format · Textbook Images", S("CoverNote"))],
[Spacer(1, 0.3*cm)],
[Paragraph("Source: Grainger & Allison's Diagnostic Radiology, 7th Edition", S("CoverNote"))],
]
ct = Table(cover_data, colWidths=[17*cm])
ct.setStyle(TableStyle([
("BACKGROUND", (0,0),(-1,-1), DARK_BLUE),
("TOPPADDING", (0,0),(-1,-1), 10),
("BOTTOMPADDING", (0,0),(-1,-1), 10),
("LEFTPADDING", (0,0),(-1,-1), 20),
("RIGHTPADDING", (0,0),(-1,-1), 20),
]))
story.append(ct)
story.append(Spacer(1, 0.8*cm))
# overview table
ov = [
[Paragraph(c, S("TblHdr")) for c in ["Case", "Diagnosis", "Modality", "System"]],
*[
[Paragraph(x, S("TblCell")) for x in row]
for row in [
["01", "ACL Tear + Knee Injuries", "MRI Knee", "Sports / Knee"],
["02", "Osteosarcoma — Distal Femur", "X-ray + MRI + CT", "Bone Tumour"],
["03", "Rotator Cuff FTT — Supraspinatus","MRI / Ultrasound", "Shoulder"],
["04", "Acute Haematogenous Osteomyelitis","X-ray + MRI + NM","Paediatric Infection"],
["05", "Pott's Disease (Spinal TB)", "X-ray + CT + MRI", "Spinal Infection"],
]
]
]
ov_t = Table(ov, colWidths=[1.4*cm, 5.5*cm, 5*cm, 5.1*cm])
ov_t.setStyle(TableStyle([
("BACKGROUND", (0,0),(-1,0), DARK_BLUE),
("ROWBACKGROUNDS",(0,1),(-1,-1),[LIGHT_GREY, white]),
("TOPPADDING", (0,0),(-1,-1), 6),
("BOTTOMPADDING", (0,0),(-1,-1), 6),
("LEFTPADDING", (0,0),(-1,-1), 8),
("RIGHTPADDING", (0,0),(-1,-1), 8),
("GRID", (0,0),(-1,-1), 0.5, HexColor("#CBD5E1")),
("VALIGN", (0,0),(-1,-1), "MIDDLE"),
]))
story.append(ov_t)
story.append(Spacer(1, 0.6*cm))
disc = Table([[Paragraph(
"<b>Examination Format:</b> Each case presents a clinical scenario and imaging. "
"Attempt all questions before turning to the answers page. "
"All content sourced from Grainger & Allison's Diagnostic Radiology (7th Ed.). "
"For educational use only.",
S("Disclaimer"))]], colWidths=[17*cm])
disc.setStyle(TableStyle([
("BACKGROUND", (0,0),(-1,-1), GOLD_BG),
("BOX", (0,0),(-1,-1), 0.8, HexColor("#D97706")),
("TOPPADDING", (0,0),(-1,-1), 8),
("BOTTOMPADDING", (0,0),(-1,-1), 8),
("LEFTPADDING", (0,0),(-1,-1), 12),
("RIGHTPADDING", (0,0),(-1,-1), 12),
]))
story.append(disc)
story.append(PageBreak())
# ─── CASES ────────────────────────────────────────────────────────────────
for case in CASES:
# ── QUESTION PAGE ──────────────────────────────────────────────────
story.append(case_header(case["num"], case["title"], case["subtitle"], "QUESTIONS"))
story.append(Spacer(1, 0.35*cm))
# Clinical history
story.append(box("CLINICAL HISTORY", DARK_BLUE))
story.append(Spacer(1, 0.15*cm))
story.append(Paragraph(case["clinical"], S("Body")))
story.append(Spacer(1, 0.35*cm))
# Images — NO captions, NO descriptions
story.append(box("IMAGING", TEAL))
story.append(Spacer(1, 0.2*cm))
imgs = []
for url in case["images"]:
img = fetch_img(url, max_w=12.5*cm, max_h=7.5*cm)
if img:
imgs.append(img)
if len(imgs) == 1:
img_row = Table([[imgs[0]]], colWidths=[17*cm])
img_row.setStyle(TableStyle([
("ALIGN", (0,0),(-1,-1),"CENTER"),
("TOPPADDING", (0,0),(-1,-1), 8),
("BOTTOMPADDING", (0,0),(-1,-1), 8),
("BACKGROUND", (0,0),(-1,-1), LIGHT_GREY),
("BOX", (0,0),(-1,-1), 0.5, HexColor("#CBD5E1")),
]))
story.append(img_row)
elif len(imgs) >= 2:
# show first 2 side by side, rest below
side_imgs = imgs[:2]
rest_imgs = imgs[2:]
row_data = [side_imgs]
img_row = Table(row_data, colWidths=[8.3*cm, 8.3*cm])
img_row.setStyle(TableStyle([
("ALIGN", (0,0),(-1,-1),"CENTER"),
("VALIGN", (0,0),(-1,-1),"MIDDLE"),
("TOPPADDING", (0,0),(-1,-1), 8),
("BOTTOMPADDING", (0,0),(-1,-1), 8),
("LEFTPADDING", (0,0),(-1,-1), 4),
("RIGHTPADDING", (0,0),(-1,-1), 4),
("BACKGROUND", (0,0),(-1,-1), LIGHT_GREY),
("BOX", (0,0),(-1,-1), 0.5, HexColor("#CBD5E1")),
]))
story.append(img_row)
for ri in rest_imgs:
extra = Table([[ri]], colWidths=[17*cm])
extra.setStyle(TableStyle([
("ALIGN", (0,0),(-1,-1),"CENTER"),
("TOPPADDING", (0,0),(-1,-1), 6),
("BOTTOMPADDING",(0,0),(-1,-1), 6),
("BACKGROUND", (0,0),(-1,-1), LIGHT_GREY),
("BOX", (0,0),(-1,-1), 0.5, HexColor("#CBD5E1")),
]))
story.append(Spacer(1, 0.15*cm))
story.append(extra)
story.append(Spacer(1, 0.35*cm))
# Questions
story.append(box("QUESTIONS", MID_BLUE))
story.append(Spacer(1, 0.2*cm))
for q_lbl, q_text in case["questions"]:
q_row = Table(
[[Paragraph(f"<b>{q_lbl}</b>", S("QLabel")),
Paragraph(q_text, S("QText"))]],
colWidths=[1.2*cm, 15.8*cm]
)
q_row.setStyle(TableStyle([
("VALIGN", (0,0),(-1,-1),"TOP"),
("TOPPADDING", (0,0),(-1,-1), 5),
("BOTTOMPADDING", (0,0),(-1,-1), 5),
("LEFTPADDING", (0,0),(-1,-1), 4),
("RIGHTPADDING", (0,0),(-1,-1), 4),
("LINEBELOW", (0,0),(-1,-1), 0.3, HexColor("#E2E8F0")),
]))
story.append(q_row)
story.append(Spacer(1, 0.35*cm))
story.append(page_break_with_ans_note())
story.append(PageBreak())
# ── ANSWER PAGE ─────────────────────────────────────────────────────
story.append(case_header(case["num"], case["title"], case["subtitle"], "ANSWERS"))
story.append(Spacer(1, 0.35*cm))
story.append(box("MODEL ANSWERS", TEAL))
story.append(Spacer(1, 0.15*cm))
story.append(answer_table(case["answers"]))
story.append(Spacer(1, 0.4*cm))
story.append(box("IMAGING MODALITIES", MID_BLUE))
story.append(Spacer(1, 0.15*cm))
story.append(modality_table(case["modality_table"]))
story.append(Spacer(1, 0.4*cm))
story.append(key_points_box(case["key_points"]))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph(f"<i>Reference: {case['reference']}</i>", S("RefText")))
story.append(HRFlowable(width="100%", thickness=0.5, color=HexColor("#CBD5E1"), spaceAfter=4))
story.append(PageBreak())
doc.build(story, canvasmaker=NC)
print(f"\n✓ PDF saved: {OUTPUT_PATH}")
if __name__ == "__main__":
build()
cd /home/daytona/workspace/radiology-long-cases && python build_msk_exam.py 2>&1
MSK Radiology Exam Cases
PDF Document · PDF
| Feature | New Exam Format |
|---|---|
| Image captions | Removed completely |
| Image descriptions | Removed completely |
| Layout | Clinical history → Image(s) → Questions (all on one page) |
| Answers | Separate page — resident attempts questions first |
| "Answers on next page" banner | Gold warning strip before each page break |
| Questions | Numbered Q1–Q5, bold labels, clean layout |