Nuclear medicine in orthopedics
Nuclear medicine in orthopedics. Introduction 2. History (if relevant) 3. Anatomy 4. Etiology 5. Mechanism (if applicable) 6. Pathology 7. Clinical Features 8. Investigation 9. Radiograph 10. CT 11. MRI 12. Classification 13. Treatment A. Non-operative B. Operative 14. Recent Advances. Follow above pattern for answers
"nuclear medicine" AND "orthopedics" AND bone scan
PMID: 39283958
nuclear medicine orthopedics bone scan SPECT PET applications clinical
nuclear medicine orthopedics history radiopharmaceuticals gallium indium WBC scan recent advances 2024
| Feature | Bone Scintigraphy | Gallium | In-111 WBC | FDG-PET/CT |
|---|---|---|---|---|
| Tracer | 99mTc-MDP | Ga-67 citrate | In-111 oxine-WBC | 18F-FDG |
| Imaging time | 2-4 hours (delayed phase) | 24-48 hours | 24 hours | 1-1.5 hours |
| Sensitivity | High | Moderate | Moderate-High | High |
| Specificity | Low | Moderate | High (with marrow scan) | High |
| Radiation dose | Low-moderate | Moderate | Moderate | Moderate |
| Best for | Screening, whole-body survey, fractures | Osteomyelitis (combined with bone scan) | Periprosthetic infection, peripheral osteomyelitis | Spine infection, tumors, metabolic, PJI |
| Condition | X-ray Sensitivity | Bone Scan Sensitivity |
|---|---|---|
| Acute osteomyelitis (first 10-14 days) | Very low (changes appear after 30-50% bone destruction) | ~90% within 72 hours |
| Stress fractures | Low (~50%) | ~95-100% |
| Occult hip fracture | ~65-80% | ~95% |
| Early metastases | Low (cortical destruction required) | High (osteoblastic activity precedes lysis) |
| Early AVN | Very low | Moderate (but MRI superior) |
| Parameter | MRI | Nuclear Medicine |
|---|---|---|
| Soft tissue detail | Superior | Poor |
| Sensitivity for osteomyelitis | 96% | ~90% (bone scan) |
| Specificity for vertebral osteomyelitis | 92% | 78% |
| Whole-body survey | Limited / time-consuming | Excellent (single scan) |
| Implants/hardware | Contraindicated/limited | No contraindication |
| Metabolic activity | Cannot assess | Primary capability |
| Cost | High | Moderate |
| Radiation | None | Yes |
| Availability | Wide | Specialized centers |
Generate a quick reference PDF comparing bone scan vs. PET/CT vs. SPECT for common orthopedic conditions
ls -la /home/daytona/workspace/ 2>/dev/null || echo "workspace not found"
mkdir -p /home/daytona/workspace/nuclear-ortho-pdf && echo "created"
~/nuclear-ortho-pdf/generate_pdf.py
#!/usr/bin/env python3
"""
Quick Reference PDF: Nuclear Medicine in Orthopedics
Bone Scan vs. PET/CT vs. SPECT for Common Orthopedic Conditions
"""
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import mm, cm
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, KeepTogether, PageBreak
)
from reportlab.lib.colors import HexColor, white, black
import os
OUTPUT_PATH = "/home/daytona/workspace/nuclear-ortho-pdf/Nuclear_Medicine_Ortho_Quick_Ref.pdf"
# ── Colour palette ──────────────────────────────────────────────────────────
C_NAVY = HexColor("#1a3a5c") # header / title bar
C_TEAL = HexColor("#1a7a7a") # section headers
C_BLUE = HexColor("#2c6fad") # bone scan column header
C_ORANGE = HexColor("#d96b1e") # PET/CT column header
C_GREEN = HexColor("#2e7d32") # SPECT column header
C_LTBLUE = HexColor("#dde8f5") # bone scan row bg
C_LTORANGE= HexColor("#fef0e3") # PET/CT row bg
C_LTGREEN = HexColor("#e8f5e9") # SPECT row bg
C_LGREY = HexColor("#f4f6f8") # alternating row bg
C_MGREY = HexColor("#d0d5db") # table border
C_HEADING_BG = HexColor("#e8edf2")
C_GOLD = HexColor("#c49a00") # warnings / notes
# ── Styles ──────────────────────────────────────────────────────────────────
styles = getSampleStyleSheet()
def S(name, **kw):
return ParagraphStyle(name, **kw)
TITLE_STYLE = S("DocTitle",
fontName="Helvetica-Bold", fontSize=22, textColor=white,
alignment=TA_CENTER, spaceAfter=4, leading=28)
SUBTITLE_STYLE = S("DocSubtitle",
fontName="Helvetica", fontSize=11, textColor=HexColor("#cce0ff"),
alignment=TA_CENTER, spaceAfter=2, leading=14)
DATE_STYLE = S("DocDate",
fontName="Helvetica-Oblique", fontSize=9, textColor=HexColor("#aaccee"),
alignment=TA_CENTER, spaceAfter=0)
SECTION_TITLE = S("SectionTitle",
fontName="Helvetica-Bold", fontSize=13, textColor=white,
alignment=TA_LEFT, spaceBefore=6, spaceAfter=2, leading=16,
leftIndent=6)
BODY = S("Body",
fontName="Helvetica", fontSize=8.5, textColor=HexColor("#1a1a1a"),
alignment=TA_LEFT, leading=12, spaceAfter=3)
BODY_BOLD = S("BodyBold",
fontName="Helvetica-Bold", fontSize=8.5, textColor=HexColor("#1a1a1a"),
alignment=TA_LEFT, leading=12)
CELL = S("Cell",
fontName="Helvetica", fontSize=7.5, textColor=HexColor("#1a1a1a"),
alignment=TA_LEFT, leading=10, spaceAfter=1)
CELL_BOLD = S("CellBold",
fontName="Helvetica-Bold", fontSize=8, textColor=HexColor("#1a1a1a"),
alignment=TA_LEFT, leading=11)
CELL_CENTER = S("CellCenter",
fontName="Helvetica", fontSize=7.5, textColor=HexColor("#1a1a1a"),
alignment=TA_CENTER, leading=10)
COL_HDR = S("ColHdr",
fontName="Helvetica-Bold", fontSize=9, textColor=white,
alignment=TA_CENTER, leading=12)
COL_HDR_SUB = S("ColHdrSub",
fontName="Helvetica-Oblique", fontSize=7.5, textColor=HexColor("#e8f0ff"),
alignment=TA_CENTER, leading=10)
ROW_HDR = S("RowHdr",
fontName="Helvetica-Bold", fontSize=8, textColor=white,
alignment=TA_LEFT, leading=11, leftIndent=3)
LEGEND_STYLE = S("Legend",
fontName="Helvetica", fontSize=7.5, textColor=HexColor("#444444"),
alignment=TA_LEFT, leading=11)
NOTE_STYLE = S("Note",
fontName="Helvetica-Oblique", fontSize=8, textColor=C_NAVY,
alignment=TA_LEFT, leading=11, leftIndent=8, rightIndent=8,
spaceBefore=4, spaceAfter=4)
FOOTER_STYLE = S("Footer",
fontName="Helvetica-Oblique", fontSize=7, textColor=HexColor("#888888"),
alignment=TA_CENTER, leading=9)
# ── Helpers ──────────────────────────────────────────────────────────────────
def p(text, style=CELL):
return Paragraph(text, style)
def section_banner(text, color=C_TEAL):
"""A colored banner paragraph acting as a section title."""
para = Paragraph(f"<font color='white'><b>{text}</b></font>", SECTION_TITLE)
t = Table([[para]], colWidths=[175*mm])
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), 8),
("RIGHTPADDING",(0,0), (-1,-1), 8),
("ROUNDEDCORNERS", [4, 4, 4, 4]),
]))
return t
def divider(color=C_MGREY, thickness=0.6):
return HRFlowable(width="100%", thickness=thickness, color=color, spaceAfter=4, spaceBefore=4)
# ── Rating symbols ───────────────────────────────────────────────────────────
def stars(n, total=5):
"""Return filled/empty star string."""
return "★" * n + "☆" * (total - n)
def badge(text, bg):
"""Inline colored badge using a 1-cell table."""
para = Paragraph(f"<font color='white'><b>{text}</b></font>",
S("badge", fontName="Helvetica-Bold", fontSize=7.5, textColor=white,
alignment=TA_CENTER, leading=10))
t = Table([[para]], colWidths=[22*mm], rowHeights=[6*mm])
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), bg),
("TOPPADDING", (0,0),(-1,-1), 1),
("BOTTOMPADDING",(0,0),(-1,-1), 1),
("LEFTPADDING", (0,0),(-1,-1), 2),
("RIGHTPADDING",(0,0),(-1,-1), 2),
]))
return t
# ── Page geometry ─────────────────────────────────────────────────────────────
PAGE_W, PAGE_H = A4
MARGIN = 12*mm
CONTENT_W = PAGE_W - 2*MARGIN
# ── Build document ────────────────────────────────────────────────────────────
def build_pdf():
doc = SimpleDocTemplate(
OUTPUT_PATH,
pagesize=A4,
leftMargin=MARGIN, rightMargin=MARGIN,
topMargin=MARGIN, bottomMargin=15*mm,
title="Nuclear Medicine in Orthopedics – Quick Reference",
author="Orris Medical Reference",
subject="Bone Scan vs PET/CT vs SPECT – Orthopedic Conditions"
)
story = []
# ═══════════════════════════════════════════════════════════════════════
# HEADER BLOCK
# ═══════════════════════════════════════════════════════════════════════
header_title = Paragraph("NUCLEAR MEDICINE IN ORTHOPEDICS", TITLE_STYLE)
header_sub = Paragraph("Quick Reference: Bone Scan | PET/CT | SPECT/CT", SUBTITLE_STYLE)
header_date = Paragraph("Orris Medical Reference • July 2026", DATE_STYLE)
header_table = Table(
[[header_title], [header_sub], [header_date]],
colWidths=[CONTENT_W]
)
header_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), C_NAVY),
("TOPPADDING", (0,0), (-1,-1), 8),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 12),
("RIGHTPADDING", (0,0), (-1,-1), 12),
]))
story.append(header_table)
story.append(Spacer(1, 6*mm))
# ═══════════════════════════════════════════════════════════════════════
# SECTION 1 – MODALITY OVERVIEW
# ═══════════════════════════════════════════════════════════════════════
story.append(section_banner("1. MODALITY OVERVIEW"))
story.append(Spacer(1, 3*mm))
overview_col_w = [CONTENT_W * 0.18, CONTENT_W * 0.27, CONTENT_W * 0.18,
CONTENT_W * 0.18, CONTENT_W * 0.19]
overview_header = [
p("Parameter", COL_HDR),
p("Bone Scan\n(99mTc-MDP)", COL_HDR),
p("SPECT/CT\n(99mTc-MDP)", COL_HDR),
p("FDG-PET/CT\n(18F-FDG)", COL_HDR),
p("18F-NaF PET/CT", COL_HDR),
]
overview_rows = [
["Tracer",
"99mTc-MDP diphosphonate",
"99mTc-MDP (tomographic)",
"18F-Fluorodeoxyglucose",
"18F-Sodium fluoride"],
["Mechanism",
"Chemisorption to hydroxyapatite; proportional to osteoblastic activity + regional blood flow",
"Same as bone scan – adds 3D CT anatomic correlation",
"FDG phosphorylation by hexokinase; accumulates in metabolically active cells",
"Ion exchange at hydroxyapatite; bone-specific PET tracer"],
["Imaging time\npost-injection",
"2–4 hours (delayed phase)\n+ flow & pool phases for 3-phase",
"2–4 hours + CT acquisition",
"60–90 min whole-body",
"30–60 min"],
["Spatial resolution",
"Low (~10 mm)",
"Moderate (7–8 mm) + CT detail",
"Moderate–High (4–6 mm)",
"High (3–4 mm)"],
["Sensitivity",
"High (screening)",
"High + improved specificity",
"Very high",
"Very high (>99%)"],
["Specificity",
"Low (many false positives)",
"Improved (CT correlation)",
"High",
"High"],
["Radiation dose",
"~3–5 mSv",
"~7–10 mSv (SPECT + CT)",
"~7–10 mSv",
"~5–7 mSv"],
["Availability",
"Widely available",
"Widely available",
"Specialized centres",
"Limited (increasing)"],
["Relative cost",
"Low–Moderate",
"Moderate",
"High",
"High"],
["Whole-body\nsurvey",
"Yes – single session",
"Limited by time",
"Yes – 1 hour",
"Yes"],
["Metallic implants",
"No contraindication",
"No contraindication",
"No contraindication",
"No contraindication"],
]
ov_data = [overview_header]
for row in overview_rows:
ov_data.append([p(row[0], CELL_BOLD)] + [p(row[i], CELL) for i in range(1, 5)])
ov_table = Table(ov_data, colWidths=overview_col_w, repeatRows=1)
ov_table.setStyle(TableStyle([
# Header
("BACKGROUND", (0,0), (-1,0), C_NAVY),
("TEXTCOLOR", (0,0), (-1,0), white),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("FONTSIZE", (0,0), (-1,0), 8.5),
("ALIGN", (0,0), (-1,0), "CENTER"),
("VALIGN", (0,0), (-1,-1), "TOP"),
# Col 1 highlight
("BACKGROUND", (1,1), (1,-1), C_LTBLUE),
# Col 3 highlight
("BACKGROUND", (3,1), (3,-1), C_LTORANGE),
# Alternating on even rows for non-highlighted
*[("BACKGROUND", (0,i), (0,i), C_LGREY) for i in range(1, len(ov_data), 2)],
*[("BACKGROUND", (2,i), (2,i), HexColor("#e8f5e9")) for i in range(1, len(ov_data), 2)],
*[("BACKGROUND", (4,i), (4,i), HexColor("#fdf5dc")) for i in range(1, len(ov_data), 2)],
# Grid
("GRID", (0,0), (-1,-1), 0.4, C_MGREY),
("LINEBELOW", (0,0), (-1,0), 1.5, C_NAVY),
# Padding
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING", (0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 4),
("RIGHTPADDING", (0,0), (-1,-1), 4),
]))
story.append(ov_table)
story.append(Spacer(1, 5*mm))
# ═══════════════════════════════════════════════════════════════════════
# SECTION 2 – CONDITION-BY-CONDITION COMPARISON
# ═══════════════════════════════════════════════════════════════════════
story.append(section_banner("2. CONDITION-BY-CONDITION COMPARISON"))
story.append(Spacer(1, 3*mm))
# Legend
legend_items = [
("●●●", "Preferred / First-line"),
("●●○", "Useful / Second-line"),
("●○○", "Limited value"),
("○○○", "Not recommended"),
("—", "Not applicable / Not used"),
]
leg_data = [[p(sym + " " + desc, LEGEND_STYLE) for sym, desc in legend_items]]
leg_t = Table(leg_data, colWidths=[CONTENT_W/5]*5)
leg_t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), HexColor("#f0f4f8")),
("GRID", (0,0), (-1,-1), 0.3, C_MGREY),
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING", (0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 5),
]))
story.append(leg_t)
story.append(Spacer(1, 3*mm))
# Column widths for main comparison table
cw = [
CONTENT_W * 0.22, # Condition
CONTENT_W * 0.195, # Bone Scan
CONTENT_W * 0.195, # SPECT/CT
CONTENT_W * 0.195, # FDG-PET/CT
CONTENT_W * 0.195, # Notes
]
def cond_header():
return [
p("Condition", COL_HDR),
p("Bone Scan\n(99mTc-MDP)", COL_HDR),
p("SPECT/CT\n(99mTc-MDP)", COL_HDR),
p("FDG-PET/CT\n(18F-FDG)", COL_HDR),
p("Key Notes", COL_HDR),
]
def cond_row(condition, bone, spect, pet, notes, cat_color=None):
row = [
p(condition, CELL_BOLD),
p(bone, CELL),
p(spect, CELL),
p(pet, CELL),
p(notes, CELL),
]
return row
# Category sub-header row
def cat_row(text, color):
cell = p(f"<b>{text}</b>",
S("cat", fontName="Helvetica-Bold", fontSize=8.5, textColor=white,
alignment=TA_LEFT, leading=12, leftIndent=4))
return [cell, p(""), p(""), p(""), p("")]
conditions_data = [cond_header()]
# ── TRAUMA ──────────────────────────────────────────────────────────────
conditions_data.append(cat_row(" TRAUMA & FRACTURES", HexColor("#2e5490")))
conditions_data.append(cond_row(
"Occult / stress\nfracture",
"●●● Highly sensitive (95% at 72h)\nFocal uptake at fracture site",
"●●○ Better localisation\n(spine, foot, ankle)",
"●○○ Rarely needed;\nuseful if bone scan equivocal",
"3-phase scan adds hyperaemia info;\nMRI preferred if available"
))
conditions_data.append(cond_row(
"Sacral insufficiency\nfracture",
"●●● Classic H-pattern\n('Honda sign')",
"●●● Confirms H-pattern\nwith CT anatomy",
"●○○ Not first-line",
"Elderly, post-radiation, osteoporosis;\nMRI alternative"
))
conditions_data.append(cond_row(
"Scaphoid fracture\n(radiograph negative)",
"●●● Sensitive within 72h",
"●●● Best nuclear option;\nprecise anatomic localisation",
"—",
"MRI is gold standard;\nSPECT/CT useful where MRI unavailable"
))
conditions_data.append(cond_row(
"Delayed union /\nnon-union",
"●●○ Persistent uptake\nindex predicts complications",
"●●○ Localises to fracture\ngap / callus",
"—",
"Decreasing uptake = healing;\npersistent = non-union risk"
))
conditions_data.append(cond_row(
"Non-accidental\ntrauma (child abuse)",
"●●○ Detects 25% injuries\nmissed on skeletal survey",
"●●○ Metaphyseal lesions",
"—",
"Skeletal survey = first-line;\nbone scan used adjunctively"
))
# ── INFECTION ────────────────────────────────────────────────────────────
conditions_data.append(cat_row(" INFECTION", HexColor("#1a5c3a")))
conditions_data.append(cond_row(
"Acute osteomyelitis\n(peripheral)",
"●●● 3-phase: sensitivity ~90%;\nspecificity increases 74→94%",
"●●● Improved specificity;\ndifferentiates cellulitis vs bone",
"●●○ High accuracy;\noption if bone scan equivocal",
"Hot scan = intact vascularity;\nCold scan = ischaemic/sequestrum"
))
conditions_data.append(cond_row(
"Vertebral\nosteomyelitis /\nspondylodiskitis",
"●●○ Sensitivity ~90%;\nspecificity only 78%",
"●●○ Better than planar",
"●●● First-line nuclear;\nsensitivity + specificity >90%;\nsuperiority over Ga-67+bone scan",
"In-111 WBC has LOW sensitivity\nfor spine; FDG-PET preferred"
))
conditions_data.append(cond_row(
"Periprosthetic\njoint infection (PJI)\nvs aseptic loosening",
"●○○ Positive up to 2yr\npost-op (remodelling);\nnot reliable alone",
"●●○ SPECT/CT improves\nlocalisation of uptake\naround hardware",
"●●● Sensitivity 95%,\nspecificity 93%;\noutperforms bone+WBC combo",
"Bone + Ga-67: useful if PET unavailable;\nIn-111 WBC + marrow scan = high specificity"
))
conditions_data.append(cond_row(
"Diabetic foot /\nperipheral\nosteomyelitis",
"●●○ 3-phase: increased\nall 3 phases in osteomyelitis",
"●●● Distinguishes soft\ntissue from bone infection;\nhigh specificity",
"●●○ Useful;\n18F-FDG not impaired\nby hyperglycaemia if <200 mg/dL",
"In-111 WBC + marrow scan:\ngold standard for diabetic foot"
))
# ── TUMOURS ──────────────────────────────────────────────────────────────
conditions_data.append(cat_row(" BONE TUMOURS & METASTASES", HexColor("#5c1a1a")))
conditions_data.append(cond_row(
"Bone metastases\nstaging / follow-up",
"●●● Standard of care;\nwhole-body osteoblastic\nmetastasis survey",
"●●● SPECT/CT dramatically\nimproves specificity\n(78.8%→97.4%)",
"●●● High S+S; detects\nlytic lesions bone scan misses;\nwhole-body in 90 min",
"Myeloma: bone scan UNRELIABLE\n(lytic; little osteoblastic response)\n→ use FDG-PET or whole-body MRI"
))
conditions_data.append(cond_row(
"Multiple myeloma",
"○○○ Often FALSE NEGATIVE\n(no osteoblastic response)",
"○○○ Same limitation",
"●●● Preferred;\ndetects active disease\nand treatment response",
"Whole-body MRI also first-line;\nFDG-PET for extramedullary disease"
))
conditions_data.append(cond_row(
"Osteosarcoma /\nEwing sarcoma",
"●●● Hypervascular tumours;\npositive bone scan;\nuse for skip lesions",
"●●○ Localisation of\nprimary tumour extent",
"●●● All osteosarcomas\nPET-avid; staging + response\nmonitoring",
"FDG-PET/CT for pulmonary mets\n(bone scan misses lung); MRI\nfor local staging"
))
conditions_data.append(cond_row(
"Benign bone tumour\n(differentiation)",
"●●○ Detects active lesions;\nnon-specific",
"●●● Best nuclear option;\ncharacterises anatomic\nrelationship",
"●●○ Dual time-point FDG\nhelps benign vs malignant\ndifferentiation",
"Biopsy remains gold standard;\nnuclear guides biopsy site"
))
# ── ARTHRITIS ────────────────────────────────────────────────────────────
conditions_data.append(cat_row(" ARTHRITIS & INFLAMMATION", HexColor("#4a3a00")))
conditions_data.append(cond_row(
"Sacroiliitis /\nspondyloarthritis",
"●●○ Elevated SI:sacrum\nratio; low specificity;\nwhole-body surveys\nperipheral joints",
"●●● Best nuclear study;\nprecise localisation;\nidentifies active joints",
"●●○ FDG-PET detects\nactive axial + peripheral\ninflammation",
"MRI preferred for early sacroiliitis;\nnuclear useful when MRI unavailable\nor for whole-body mapping"
))
conditions_data.append(cond_row(
"Osteoarthritis\nprogression",
"●●○ Detects subchondral\nactivity before X-ray;\nnormal scan = low\nprogression risk at 5yr",
"●●● SPECT/CT identifies\ncompartment-specific\nactive OA; guides\ninjection/surgery",
"●○○ Not routine;\nresearch use",
"Low clinical utility for routine OA;\nSPECT/CT most useful for knee,\nfacet, foot/ankle OA"
))
conditions_data.append(cond_row(
"Gout / crystal\narthropathy",
"●○○ Non-specific\nincreased uptake",
"●●● SPECT/CT: confirms\narticular nature of tophi;\nidentifies active joints",
"●○○ Not routine",
"SPECT/CT specifically identifies\ntophi vs erosions vs degenerative\nchanges"
))
conditions_data.append(cond_row(
"CRPS / Reflex\nSympathetic\nDystrophy",
"●●● 3-phase: diffuse\nperiarticular hyperaemia;\nincreased delayed uptake",
"●●○ Confirms and\nlocalises findings",
"●○○ Limited data",
"Three-phase bone scan is the\nnuclear study of choice for CRPS;\nMRI and clinical diagnosis also key"
))
# ── METABOLIC BONE ───────────────────────────────────────────────────────
conditions_data.append(cat_row(" METABOLIC BONE DISEASE", HexColor("#1a3a5c")))
conditions_data.append(cond_row(
"Paget's disease",
"●●● Intense uptake in\naffected bone; whole-body\nextent mapping",
"●●● Precise extent\nassessment; pre-surgical\nplanning",
"●○○ Not routinely used",
"Bone scan best for whole-body\nextent; X-ray for lytic/sclerotic\nphase characterisation"
))
conditions_data.append(cond_row(
"Avascular necrosis\n(AVN)",
"●●○ Early: 'cold' scan\n(ischaemia); Reparative:\n'hot' scan",
"●●○ Better localisation",
"●○○ Not first-line",
"MRI is gold standard for early AVN;\nbone scan useful when MRI\nunavailable"
))
conditions_data.append(cond_row(
"Superscan\n(diffuse metabolic)",
"●●● Diffusely increased\nuptake; absent renal\nactivity",
"●●○ Confirms distribution",
"●●○ Differentiates\nmetastatic vs metabolic",
"Causes: hyperparathyroidism,\nrenal osteodystrophy, diffuse\nmetastatic disease"
))
conditions_data.append(cond_row(
"Looser zones\n(osteomalacia)",
"●●● Multiple symmetric\nfocal uptake at\npseudofracture sites",
"●●○ Localisation",
"—",
"Characteristic bilateral symmetric\nfoci at neck of femur, ribs,\nscapulae, pubic rami"
))
conditions_data.append(cond_row(
"Oncogenic\nosteomalacia\n(FGF-23 tumour)",
"●○○ Non-specific",
"●○○",
"●○○",
"68Ga-DOTATATE or 68Ga-DOTA-SST\nPET/CT is GOLD STANDARD for\nlocalising the culprit tumour"
))
# ── SPINE ────────────────────────────────────────────────────────────────
conditions_data.append(cat_row(" SPINE", HexColor("#3a1a5c")))
conditions_data.append(cond_row(
"Low back pain\n(facet / pars)",
"●○○ Planar cannot\ndistinguish facet vs\nbody vs pars",
"●●● SPECT/CT: identifies\nfacet OA, pars defect,\nBertolotti syndrome;\nguides injections",
"●○○ Not first-line\nfor mechanical LBP",
"SPECT/CT is preferred nuclear study\nfor complex spinal pain;\nchanges management in ~30% cases"
))
conditions_data.append(cond_row(
"Post-fusion\nassessment",
"●●○ Detects persistent\nactivity at non-union\nor loose hardware",
"●●● Identifies loose\nscrew, failed fusion\nsegment, iliac crest\ndonor site fracture",
"●○○ Limited data for\nspinal hardware",
"SPECT/CT: radiotracer uptake\naround hardware = loosening;\nuptake at graft = active fusion"
))
# Build main table
main_t = Table(conditions_data, colWidths=cw, repeatRows=1)
# Build style commands
ts_cmds = [
# Header row
("BACKGROUND", (0,0), (-1,0), C_NAVY),
("TEXTCOLOR", (0,0), (-1,0), white),
("ALIGN", (0,0), (-1,0), "CENTER"),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("FONTSIZE", (0,0), (-1,0), 8.5),
# General
("VALIGN", (0,0), (-1,-1), "TOP"),
("GRID", (0,0), (-1,-1), 0.3, C_MGREY),
("LINEBELOW", (0,0), (-1,0), 1.5, C_NAVY),
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING", (0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 4),
("RIGHTPADDING", (0,0), (-1,-1), 4),
# Condition column always light
("BACKGROUND", (0,1), (0,-1), C_LGREY),
]
# Category header rows – colour entire row
cat_indices = []
for idx, row in enumerate(conditions_data):
if idx == 0:
continue
# Category rows have empty cells 1-4
if all(isinstance(row[i], Paragraph) and row[i].text in ("", "<b></b>") for i in range(1,5)):
cat_indices.append(idx)
# Re-detect category rows by checking second cell text
cat_row_indices = []
for idx, row in enumerate(conditions_data):
if idx == 0:
continue
# Category rows have blank paragraphs in cols 1-4
texts = [row[i].text.strip() for i in range(1, 5)]
if all(t == "" for t in texts):
cat_row_indices.append(idx)
cat_colors_map = {}
cat_color_list = [
HexColor("#2e5490"), HexColor("#1a5c3a"), HexColor("#5c1a1a"),
HexColor("#4a3a00"), HexColor("#1a3a5c"), HexColor("#3a1a5c")
]
ci = 0
for idx in cat_row_indices:
c_col = cat_color_list[ci % len(cat_color_list)]
ts_cmds.append(("BACKGROUND", (0, idx), (-1, idx), c_col))
ts_cmds.append(("SPAN", (0, idx), (-1, idx)))
ts_cmds.append(("TEXTCOLOR", (0, idx), (-1, idx), white))
ts_cmds.append(("FONTNAME", (0, idx), (-1, idx), "Helvetica-Bold"))
ts_cmds.append(("FONTSIZE", (0, idx), (-1, idx), 8.5))
ts_cmds.append(("TOPPADDING", (0, idx), (-1, idx), 4))
ts_cmds.append(("BOTTOMPADDING", (0, idx), (-1, idx), 4))
ci += 1
# Alternating row colors for non-category rows
for idx in range(1, len(conditions_data)):
if idx in cat_row_indices:
continue
if idx % 2 == 0:
ts_cmds.append(("BACKGROUND", (1, idx), (1, idx), C_LTBLUE))
ts_cmds.append(("BACKGROUND", (2, idx), (2, idx), HexColor("#e8f5e9")))
ts_cmds.append(("BACKGROUND", (3, idx), (3, idx), C_LTORANGE))
ts_cmds.append(("BACKGROUND", (4, idx), (4, idx), HexColor("#fdf8ee")))
else:
ts_cmds.append(("BACKGROUND", (1, idx), (1, idx), HexColor("#eef4fc")))
ts_cmds.append(("BACKGROUND", (2, idx), (2, idx), HexColor("#f1f9f1")))
ts_cmds.append(("BACKGROUND", (3, idx), (3, idx), HexColor("#fef8f2")))
ts_cmds.append(("BACKGROUND", (4, idx), (4, idx), white))
main_t.setStyle(TableStyle(ts_cmds))
story.append(main_t)
story.append(Spacer(1, 5*mm))
# ═══════════════════════════════════════════════════════════════════════
# PAGE BREAK → Radiopharmaceuticals + Special Agents
# ═══════════════════════════════════════════════════════════════════════
story.append(PageBreak())
# ── Header on page 2
story.append(header_table)
story.append(Spacer(1, 5*mm))
# ═══════════════════════════════════════════════════════════════════════
# SECTION 3 – THREE-PHASE BONE SCAN PROTOCOL
# ═══════════════════════════════════════════════════════════════════════
story.append(section_banner("3. THREE-PHASE BONE SCAN – PHASES & INTERPRETATION"))
story.append(Spacer(1, 3*mm))
phase_cw = [CONTENT_W*0.12, CONTENT_W*0.16, CONTENT_W*0.16, CONTENT_W*0.28, CONTENT_W*0.28]
phase_data = [
[p("Phase", COL_HDR), p("Timing", COL_HDR), p("Images", COL_HDR),
p("Normal Appearance", COL_HDR), p("Abnormal / Clinical Significance", COL_HDR)],
[p("Phase 1\nFlow", CELL_BOLD),
p("0–60 sec\n(dynamic)", CELL),
p("Radionuclide angiogram", CELL),
p("Symmetric, rapid arterial transit", CELL),
p("Asymmetric hyperaemia → osteomyelitis, tumour, CRPS, acute fracture", CELL)],
[p("Phase 2\nBlood Pool", CELL_BOLD),
p("5–10 min", CELL),
p("Static soft tissue images", CELL),
p("Slight soft tissue background; symmetric", CELL),
p("Increased soft tissue uptake → cellulitis, synovitis, hyperaemia. Differentiates from Phase 3 bone uptake", CELL)],
[p("Phase 3\nDelayed", CELL_BOLD),
p("2–4 hours", CELL),
p("Skeletal delayed images", CELL),
p("Symmetric skeletal uptake; axial > appendicular; avid growth plates in children", CELL),
p("Focal increased uptake → osteomyelitis, fracture, metastasis, Paget's, OA, tumour; focal decreased ('cold') → AVN, sequestrum, aggressive tumour", CELL)],
[p("Phase 4\n(optional)", CELL_BOLD),
p("24 hours", CELL),
p("24h delayed images", CELL),
p("Further background clearance", CELL),
p("Increased specificity for chronic low-grade osteomyelitis; spine infection assessment", CELL)],
]
phase_t = Table(phase_data, colWidths=phase_cw, repeatRows=1)
phase_t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), C_TEAL),
("TEXTCOLOR", (0,0), (-1,0), white),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("ALIGN", (0,0), (-1,0), "CENTER"),
("VALIGN", (0,0), (-1,-1), "TOP"),
("GRID", (0,0), (-1,-1), 0.4, C_MGREY),
("LINEBELOW", (0,0), (-1,0), 1.2, C_TEAL),
("BACKGROUND", (0,1), (-1,1), C_LTBLUE),
("BACKGROUND", (0,2), (-1,2), HexColor("#e8f5e9")),
("BACKGROUND", (0,3), (-1,3), C_LTORANGE),
("BACKGROUND", (0,4), (-1,4), HexColor("#fdf5dc")),
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING", (0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 4),
("RIGHTPADDING", (0,0), (-1,-1), 4),
]))
story.append(phase_t)
story.append(Spacer(1, 4*mm))
# ═══════════════════════════════════════════════════════════════════════
# SECTION 4 – RADIOPHARMACEUTICALS REFERENCE
# ═══════════════════════════════════════════════════════════════════════
story.append(section_banner("4. RADIOPHARMACEUTICAL QUICK REFERENCE", C_NAVY))
story.append(Spacer(1, 3*mm))
rp_cw = [CONTENT_W*0.18, CONTENT_W*0.12, CONTENT_W*0.14, CONTENT_W*0.16,
CONTENT_W*0.14, CONTENT_W*0.26]
rp_data = [
[p("Agent", COL_HDR), p("Half-life", COL_HDR), p("Imaging time", COL_HDR),
p("Primary mechanism", COL_HDR), p("Dose (mSv)", COL_HDR), p("Key orthopedic use", COL_HDR)],
[p("99mTc-MDP\n(Tc-99m)", CELL_BOLD), p("6 hours", CELL), p("2–4 hours", CELL),
p("Hydroxyapatite chemisorption; osteoblastic activity", CELL), p("3–5 mSv", CELL),
p("Bone scan (fracture, osteomyelitis, metastasis, Paget's, AVN, CRPS)", CELL)],
[p("67Ga-Citrate\n(Gallium-67)", CELL_BOLD), p("78 hours", CELL), p("24–48 hours", CELL),
p("Transferrin/lactoferrin binding; leukocyte uptake at infection", CELL), p("10–15 mSv", CELL),
p("Osteomyelitis (combined with bone scan); tumours; spinal infection", CELL)],
[p("111In-WBC\n(Indium-111)", CELL_BOLD), p("67 hours", CELL), p("24 hours", CELL),
p("Labelled patient's own granulocytes migrate to infection", CELL), p("8–12 mSv", CELL),
p("Peripheral osteomyelitis; periprosthetic infection (use with marrow scan)", CELL)],
[p("99mTc-HMPAO-WBC", CELL_BOLD), p("6 hours", CELL), p("4 hours", CELL),
p("HMPAO crosses cell membranes; labels granulocytes", CELL), p("5–8 mSv", CELL),
p("Faster alternative to In-111 WBC; peripheral osteomyelitis; diabetic foot", CELL)],
[p("99mTc-Sulfur\nColloid", CELL_BOLD), p("6 hours", CELL), p("20 min", CELL),
p("RES phagocytosis; marrow mapping", CELL), p("2–4 mSv", CELL),
p("Marrow imaging (adjunct to WBC scan for osteomyelitis / PJI diagnosis)", CELL)],
[p("18F-FDG\n(PET)", CELL_BOLD), p("110 min", CELL), p("60–90 min", CELL),
p("Glucose analogue; hexokinase phosphorylation; metabolic trapping", CELL), p("7–10 mSv", CELL),
p("Bone tumours; spinal infection; periprosthetic infection; multiple myeloma", CELL)],
[p("18F-NaF\n(PET)", CELL_BOLD), p("110 min", CELL), p("30–60 min", CELL),
p("Fluoride ion exchange at hydroxyapatite; bone-specific", CELL), p("5–7 mSv", CELL),
p("High-resolution bone scan; metastasis detection (sensitivity >99%)", CELL)],
[p("223Ra-Dichloride\n(Radium-223)", CELL_BOLD), p("11.4 days", CELL), p("Therapeutic", CELL),
p("Calcium mimetic; alpha emitter; targets osteoblastic metastases", CELL), p("Therapeutic", CELL),
p("Palliation + survival benefit in castrate-resistant prostate cancer with bone mets", CELL)],
[p("68Ga-DOTATATE\n(PET)", CELL_BOLD), p("68 min", CELL), p("60–90 min", CELL),
p("Somatostatin receptor targeting", CELL), p("5–7 mSv", CELL),
p("Oncogenic osteomalacia tumour localisation (FGF-23 secreting tumours)", CELL)],
]
rp_t = Table(rp_data, colWidths=rp_cw, repeatRows=1)
rp_ts = [
("BACKGROUND", (0,0), (-1,0), C_NAVY),
("TEXTCOLOR", (0,0), (-1,0), white),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("ALIGN", (0,0), (-1,0), "CENTER"),
("VALIGN", (0,0), (-1,-1), "TOP"),
("GRID", (0,0), (-1,-1), 0.4, C_MGREY),
("LINEBELOW", (0,0), (-1,0), 1.5, C_NAVY),
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING", (0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 4),
("RIGHTPADDING", (0,0), (-1,-1), 4),
]
for i in range(1, len(rp_data)):
bg = C_LGREY if i % 2 == 0 else white
rp_ts.append(("BACKGROUND", (0, i), (0, i), HexColor("#e4eaf4")))
if i % 2 == 0:
rp_ts.append(("BACKGROUND", (1, i), (-1, i), C_LGREY))
rp_t.setStyle(TableStyle(rp_ts))
story.append(rp_t)
story.append(Spacer(1, 4*mm))
# ═══════════════════════════════════════════════════════════════════════
# SECTION 5 – SENSITIVITY & SPECIFICITY
# ═══════════════════════════════════════════════════════════════════════
story.append(section_banner("5. SENSITIVITY & SPECIFICITY SUMMARY", C_TEAL))
story.append(Spacer(1, 3*mm))
ss_cw = [CONTENT_W*0.24, CONTENT_W*0.19, CONTENT_W*0.19, CONTENT_W*0.19, CONTENT_W*0.19]
ss_data = [
[p("Indication", COL_HDR), p("Bone Scan\nSens / Spec", COL_HDR),
p("SPECT/CT\nSens / Spec", COL_HDR), p("FDG-PET/CT\nSens / Spec", COL_HDR),
p("18F-NaF PET\nSens / Spec", COL_HDR)],
[p("Bone metastases", CELL_BOLD), p("93% / 78%", CELL), p("97% / 97%", CELL), p("97% / 95%", CELL), p(">99% / 97%", CELL)],
[p("Osteomyelitis\n(peripheral)", CELL_BOLD), p("90% / 74–94%", CELL), p("90–95% / 90–95%", CELL), p("92% / 87%", CELL), p("—", CELL)],
[p("Spinal osteomyelitis", CELL_BOLD), p("90% / 78%", CELL), p("90% / 83%", CELL), p(">95% / >90%", CELL), p("—", CELL)],
[p("Periprosthetic\njoint infection", CELL_BOLD), p("50% / 95%\n(Tc+In-111 combo)", CELL), p("Improved", CELL), p("95% / 93%", CELL), p("—", CELL)],
[p("Acute fracture\n(occult)", CELL_BOLD), p("95% / moderate", CELL), p("95–98% / high", CELL), p("—", CELL), p("~99% / high", CELL)],
[p("AVN (early)", CELL_BOLD), p("Moderate", CELL), p("Moderate–high", CELL), p("Variable", CELL), p("High", CELL)],
]
ss_t = Table(ss_data, colWidths=ss_cw, repeatRows=1)
ss_ts = [
("BACKGROUND", (0,0), (-1,0), C_TEAL),
("TEXTCOLOR", (0,0), (-1,0), white),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("ALIGN", (0,0), (-1,0), "CENTER"),
("VALIGN", (0,0), (-1,-1), "TOP"),
("GRID", (0,0), (-1,-1), 0.4, C_MGREY),
("LINEBELOW", (0,0), (-1,0), 1.2, C_TEAL),
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING", (0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 4),
("RIGHTPADDING", (0,0), (-1,-1), 4),
]
for i in range(1, len(ss_data)):
if i % 2 == 0:
ss_ts.append(("BACKGROUND", (0,i), (-1,i), C_LGREY))
ss_ts.append(("BACKGROUND", (1,i), (1,i), C_LTBLUE))
ss_ts.append(("BACKGROUND", (2,i), (2,i), HexColor("#e8f5e9")))
ss_ts.append(("BACKGROUND", (3,i), (3,i), C_LTORANGE))
ss_ts.append(("BACKGROUND", (4,i), (4,i), HexColor("#fdf5dc")))
ss_t.setStyle(TableStyle(ss_ts))
story.append(ss_t)
story.append(Spacer(1, 4*mm))
# ═══════════════════════════════════════════════════════════════════════
# SECTION 6 – RECENT ADVANCES
# ═══════════════════════════════════════════════════════════════════════
story.append(section_banner("6. RECENT ADVANCES (2020–2026)", C_NAVY))
story.append(Spacer(1, 3*mm))
advances = [
("Digital PET / Total-Body PET",
"Silicon photomultiplier (SiPM) detectors improve sensitivity and resolution. Total-body PET (EXPLORER) images entire body simultaneously at ultra-low radiation doses – valuable for repeated imaging and paediatric patients."),
("PET/MRI",
"Combines FDG metabolic data with MRI soft-tissue contrast. Reduced radiation vs PET/CT (MRI is radiation-free). Superior for soft-tissue sarcoma adjacent to bone, paediatric imaging, and periprosthetic infection. Technical challenges (MRI-based attenuation correction, cost) limit availability."),
("18F-NaF PET/CT",
"Bone-specific PET tracer; sensitivity >99% for bone metastases. Superior pharmacokinetics, spatial resolution, and quantification vs 99mTc-MDP. FDA-approved; rapidly replacing conventional bone scan in high-end centres."),
("PSMA-targeted PET",
"68Ga-PSMA-11 / 18F-DCFPyL: higher sensitivity than bone scan for early prostate cancer bone metastases. Revolutionised staging of prostate cancer skeletal disease."),
("Theranostics",
"Diagnostic + therapeutic use of same molecular target: 68Ga-PSMA (imaging) → 177Lu-PSMA-617 (therapy); Ra-223 (first bone-targeted agent proven to improve overall survival – ALSYMPCA trial); 177Lu-DOTATATE for neuroendocrine bone mets. Alpha therapy (Pb-212, Ac-225) in trials."),
("CZT-SPECT Cameras",
"Cadmium zinc telluride solid-state detectors: faster acquisition, smaller footprint, lower dose, higher resolution than conventional SPECT. Improving bone SPECT/CT performance in clinical practice."),
("AI Integration",
"AI-assisted bone scan interpretation for automated lesion detection. Deep learning for SPECT/PET reconstruction (noise reduction, improved conspicuity). Quantitative Bone Scan Index (BSI) for objective tumour burden and treatment response monitoring."),
("Appropriate Use Criteria",
"SNMMI/EANM published formal Appropriate Use Criteria for musculoskeletal infection imaging (2022–2024), guiding rational selection between bone scan, WBC scan, Ga-67, and FDG-PET/CT based on clinical scenario and anatomic site."),
("68Ga-DOTATATE / FAPI PET",
"68Ga-DOTATATE PET/CT is gold standard for localising FGF-23-secreting tumours causing oncogenic osteomalacia. FAPI (fibroblast activation protein inhibitors) PET shows promise for musculoskeletal tumours with low background bone uptake."),
]
adv_cw = [CONTENT_W*0.27, CONTENT_W*0.73]
adv_data = [[p("Advance", COL_HDR), p("Summary", COL_HDR)]]
for title_adv, desc in advances:
adv_data.append([p(title_adv, CELL_BOLD), p(desc, CELL)])
adv_t = Table(adv_data, colWidths=adv_cw, repeatRows=1)
adv_ts = [
("BACKGROUND", (0,0), (-1,0), C_NAVY),
("TEXTCOLOR", (0,0), (-1,0), white),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("ALIGN", (0,0), (-1,0), "CENTER"),
("VALIGN", (0,0), (-1,-1), "TOP"),
("GRID", (0,0), (-1,-1), 0.4, C_MGREY),
("LINEBELOW", (0,0), (-1,0), 1.5, C_NAVY),
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING", (0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 5),
]
for i in range(1, len(adv_data)):
if i % 2 == 0:
adv_ts.append(("BACKGROUND", (0,i), (0,i), HexColor("#dde8f5")))
adv_ts.append(("BACKGROUND", (1,i), (1,i), C_LGREY))
else:
adv_ts.append(("BACKGROUND", (0,i), (0,i), HexColor("#e8edf2")))
adv_t.setStyle(TableStyle(adv_ts))
story.append(adv_t)
story.append(Spacer(1, 4*mm))
# ═══════════════════════════════════════════════════════════════════════
# CLINICAL PEARLS BOX
# ═══════════════════════════════════════════════════════════════════════
story.append(section_banner(" CLINICAL PEARLS & PITFALLS", HexColor("#7b3f00")))
story.append(Spacer(1, 3*mm))
pearls = [
("PEARL", "A normal bone scan has HIGH negative predictive value – disease very unlikely if normal.",
HexColor("#1a5c3a")),
("EXCEPTION", "Multiple myeloma – bone scan often FALSE NEGATIVE (lytic; no osteoblastic response). Use FDG-PET/CT.",
HexColor("#5c1a1a")),
("PITFALL", "Bone scan remains positive for up to 2 years post-arthroplasty due to physiologic remodelling – cannot reliably diagnose periprosthetic infection on bone scan alone.",
HexColor("#4a3a00")),
("PEARL", "Three-phase bone scan improves specificity for osteomyelitis: 74% → 94%.",
HexColor("#1a3a5c")),
("PITFALL", "'Cold' scan in osteomyelitis = disrupted blood flow (periosteal pus, sequestrum, necrosis) – do not exclude osteomyelitis based on cold scan alone.",
HexColor("#5c1a1a")),
("PEARL", "Gallium-67 (not bone scan) is used to MONITOR treatment response in osteomyelitis – bone scan uptake persists long after infection resolution.",
HexColor("#1a5c3a")),
("PITFALL", "Indium-111 WBC scan has LOW sensitivity for SPINAL osteomyelitis – prefer FDG-PET/CT for vertebral infections.",
HexColor("#4a3a00")),
("PEARL", "SPECT/CT is the key upgrade from planar scintigraphy – always preferred for complex anatomy (spine, foot/ankle, wrist, hip) and when specificity matters.",
HexColor("#1a3a5c")),
("PEARL", "Superscan = diffusely increased whole-body uptake + absent renal activity: think hyperparathyroidism, renal osteodystrophy, or diffuse blastic metastases.",
HexColor("#3a1a5c")),
]
pearl_cw = [CONTENT_W*0.10, CONTENT_W*0.90]
pearl_data = []
for ptype, ptext, pcolor in pearls:
badge_p = Paragraph(f"<font color='white'><b>{ptype}</b></font>",
S("pb", fontName="Helvetica-Bold", fontSize=7.5, textColor=white,
alignment=TA_CENTER, leading=10))
pearl_data.append([badge_p, p(ptext, CELL)])
pearl_t = Table(pearl_data, colWidths=pearl_cw)
pearl_ts = [
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("GRID", (0,0), (-1,-1), 0.4, C_MGREY),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 4),
("RIGHTPADDING", (0,0), (-1,-1), 4),
]
pearl_colors_map = [p[2] for p in pearls]
for i, (ptype, _, pc) in enumerate(pearls):
pearl_ts.append(("BACKGROUND", (0,i), (0,i), pc))
if i % 2 == 0:
pearl_ts.append(("BACKGROUND", (1,i), (1,i), C_LGREY))
pearl_t.setStyle(TableStyle(pearl_ts))
story.append(pearl_t)
story.append(Spacer(1, 5*mm))
# ═══════════════════════════════════════════════════════════════════════
# FOOTER
# ═══════════════════════════════════════════════════════════════════════
divider(C_NAVY, 1.0)
story.append(divider(C_NAVY, 1.0))
story.append(Paragraph(
"References: Rockwood & Green's Fractures in Adults 10e (2025) | Campbell's Operative Orthopaedics 15e (2026) | "
"Miller's Review of Orthopaedics 9e | Rheumatology 2-Vol Set (Elsevier 2022) | "
"Grainger & Allison's Diagnostic Radiology | "
"Ibaseta A et al. JBJS Rev 2024 (PMID: 39283958) | "
"SNMMI Appropriate Use Criteria 2024",
FOOTER_STYLE
))
story.append(Spacer(1, 2*mm))
story.append(Paragraph(
"This document is for educational reference only. Clinical decisions should be based on complete clinical context and institutional protocols.",
FOOTER_STYLE
))
doc.build(story)
print(f"PDF saved: {OUTPUT_PATH}")
if __name__ == "__main__":
build_pdf()
cd /home/daytona/workspace/nuclear-ortho-pdf && python3 generate_pdf.py 2>&1
ls -lh /home/daytona/workspace/nuclear-ortho-pdf/Nuclear_Medicine_Ortho_Quick_Re…
Nuclear Medicine Ortho Quick Ref
PDF Document · PDF