sarcoidosis detailsb
sarcoidosis diagnosis treatment

| Stage | Findings | Frequency | Prognosis |
|---|---|---|---|
| 0 | Normal CXR | <15% | - |
| I | Bilateral hilar adenopathy only | 30-50% | Excellent; often spontaneous remission |
| II | Hilar adenopathy + pulmonary infiltrates | 40-60% | Variable |
| III | Pulmonary infiltrates only (no adenopathy) | 10-20% | Variable |
| IV | Pulmonary fibrosis/fibrocystic changes | <15% | Poor; mycetoma risk |

| Test | Finding | Notes |
|---|---|---|
| Serum ACE (SACE) | Elevated in ~60% | Non-specific; correlates with granuloma burden; not diagnostic alone |
| Calcium | Hypercalciuria > hypercalcemia | Due to granuloma 1α-hydroxylase |
| 25-OH Vitamin D | Low | 1,25-OH Vit D often elevated |
| LFTs | Mildly elevated | Hepatic involvement |
| CBC | Lymphopenia, anemia | Non-specific |
| sIL-2R | Elevated | More sensitive than ACE but not specific |
| BAL | CD4:CD8 ratio >3.5 | Supportive but not diagnostic |
| Form | Prognosis |
|---|---|
| Lofgren syndrome | 70-80% remission without treatment |
| Stage I asymptomatic | >70% spontaneous remission |
| Stage II/III | ~50-70% remission with treatment |
| Stage IV (fibrocystic) | Poor; risk of mycetoma, respiratory failure |
| Cardiac sarcoidosis | Increased mortality from arrhythmia/sudden death |
| Neurosarcoidosis | Increased morbidity; unpredictable |
| Finding | Association |
|---|---|
| Bilateral hilar adenopathy | Most common intrathoracic finding |
| Lupus pernio | Chronic disease, upper airway involvement, pulmonary fibrosis |
| Erythema nodosum + BHA | Lofgren syndrome (good prognosis) |
| Heerfordt syndrome | Parotid + uveitis + CN VII palsy + fever |
| Hypercalcemia | 1α-hydroxylase activity in granuloma macrophages |
| Noncaseating granuloma | Pathologic hallmark |
| ACE elevated | ~60%; correlates with granuloma burden |
| CN VII most affected cranial nerve | Sarcoidosis neurologic involvement |
Create a reference PDF guide on sarcoidosis diagnosis and staging
mkdir -p /tmp/workspace/sarcoidosis-pdf
~/sarcoidosis-pdf/generate_pdf.py
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, KeepTogether, PageBreak
)
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm, cm
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
from reportlab.platypus.flowables import HRFlowable
# ── Color palette ──────────────────────────────────────────────────────────────
DARK_TEAL = colors.HexColor("#1B4F6B")
MID_TEAL = colors.HexColor("#2980B9")
LIGHT_TEAL = colors.HexColor("#D6EAF8")
ACCENT = colors.HexColor("#E74C3C")
GOLD = colors.HexColor("#F39C12")
LIGHT_GREY = colors.HexColor("#F2F3F4")
MID_GREY = colors.HexColor("#BDC3C7")
DARK_GREY = colors.HexColor("#2C3E50")
WHITE = colors.white
W, H = A4
MARGIN = 20 * mm
# ── Document ───────────────────────────────────────────────────────────────────
doc = SimpleDocTemplate(
"/tmp/workspace/sarcoidosis-pdf/Sarcoidosis_Diagnosis_Staging_Guide.pdf",
pagesize=A4,
leftMargin=MARGIN, rightMargin=MARGIN,
topMargin=25 * mm, bottomMargin=20 * mm,
title="Sarcoidosis: Diagnosis & Staging Reference Guide",
author="Orris Medical Reference",
subject="Sarcoidosis Clinical Reference",
)
styles = getSampleStyleSheet()
# Custom styles
def S(name, **kw):
return ParagraphStyle(name, **kw)
COVER_TITLE = S("CoverTitle", fontSize=28, textColor=WHITE,
fontName="Helvetica-Bold", alignment=TA_CENTER, leading=36)
COVER_SUB = S("CoverSub", fontSize=14, textColor=LIGHT_TEAL,
fontName="Helvetica", alignment=TA_CENTER, leading=20)
COVER_DATE = S("CoverDate", fontSize=10, textColor=MID_GREY,
fontName="Helvetica", alignment=TA_CENTER)
SEC_TITLE = S("SecTitle", fontSize=14, textColor=WHITE,
fontName="Helvetica-Bold", leading=18)
SUBSEC = S("SubSec", fontSize=11, textColor=DARK_TEAL,
fontName="Helvetica-Bold", leading=15, spaceBefore=8)
BODY = S("Body", fontSize=9, textColor=DARK_GREY,
fontName="Helvetica", leading=13, alignment=TA_JUSTIFY)
BULLET = S("Bullet", fontSize=9, textColor=DARK_GREY,
fontName="Helvetica", leading=13, leftIndent=12,
firstLineIndent=-8)
BOLD_BODY = S("BoldBody", fontSize=9, textColor=DARK_GREY,
fontName="Helvetica-Bold", leading=13)
CAPTION = S("Caption", fontSize=8, textColor=colors.HexColor("#7F8C8D"),
fontName="Helvetica-Oblique", alignment=TA_CENTER, leading=11)
TABLE_HDR = S("TblHdr", fontSize=8.5, textColor=WHITE,
fontName="Helvetica-Bold", alignment=TA_CENTER, leading=11)
TABLE_CELL = S("TblCell", fontSize=8.5, textColor=DARK_GREY,
fontName="Helvetica", leading=11)
TABLE_CELL_C= S("TblCellC", fontSize=8.5, textColor=DARK_GREY,
fontName="Helvetica", alignment=TA_CENTER, leading=11)
WARN_TEXT = S("WarnText", fontSize=8.5, textColor=colors.HexColor("#6C1B1B"),
fontName="Helvetica-Bold", leading=12)
NOTE_TEXT = S("NoteText", fontSize=8.5, textColor=colors.HexColor("#154360"),
fontName="Helvetica", leading=12, alignment=TA_JUSTIFY)
FOOTER_TEXT = S("FooterText", fontSize=7, textColor=MID_GREY,
fontName="Helvetica-Oblique", alignment=TA_CENTER)
TW = W - 2 * MARGIN # usable text width
# ─────────────────────────────────────────────────────────────────────────────
# Helper functions
# ─────────────────────────────────────────────────────────────────────────────
def section_header(title, color=DARK_TEAL):
tbl = Table([[Paragraph(title, SEC_TITLE)]], colWidths=[TW])
tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), color),
("TOPPADDING", (0,0), (-1,-1), 6),
("BOTTOMPADDING",(0,0),(-1,-1), 6),
("LEFTPADDING", (0,0), (-1,-1), 10),
("RIGHTPADDING",(0,0), (-1,-1), 10),
("ROUNDEDCORNERS", [4,4,4,4]),
]))
return tbl
def bullet(text, bold_prefix=""):
if bold_prefix:
content = f"<b>{bold_prefix}</b> {text}"
else:
content = text
return Paragraph(f"• {content}", BULLET)
def hr(color=MID_GREY, thickness=0.5):
return HRFlowable(width="100%", thickness=thickness, color=color, spaceAfter=4, spaceBefore=4)
def note_box(text, bg=colors.HexColor("#EBF5FB"), border=MID_TEAL):
tbl = Table([[Paragraph(text, NOTE_TEXT)]], colWidths=[TW])
tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), bg),
("LINEABOVE", (0,0), (-1, 0), 2, border),
("TOPPADDING", (0,0), (-1,-1), 8),
("BOTTOMPADDING",(0,0),(-1,-1), 8),
("LEFTPADDING", (0,0), (-1,-1), 10),
("RIGHTPADDING",(0,0), (-1,-1), 10),
]))
return tbl
def warn_box(text):
return note_box(f"⚠ {text}",
bg=colors.HexColor("#FDEDEC"),
border=ACCENT)
def key_box(text):
return note_box(f"🔑 {text}",
bg=colors.HexColor("#FDFEFE"),
border=GOLD)
def styled_table(header_row, data_rows, col_widths, alt_color=LIGHT_GREY,
header_color=DARK_TEAL):
"""Build a styled table with alternating row shading."""
all_rows = [header_row] + data_rows
# convert strings to Paragraphs
formatted = []
for i, row in enumerate(all_rows):
if i == 0:
fmt_row = [Paragraph(str(c), TABLE_HDR) for c in row]
else:
fmt_row = [Paragraph(str(c), TABLE_CELL) for c in row]
formatted.append(fmt_row)
tbl = Table(formatted, colWidths=col_widths, repeatRows=1)
style_cmds = [
("BACKGROUND", (0,0), (-1,0), header_color),
("TEXTCOLOR", (0,0), (-1,0), WHITE),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("FONTSIZE", (0,0), (-1,-1), 8.5),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING",(0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 6),
("RIGHTPADDING", (0,0), (-1,-1), 6),
("GRID", (0,0), (-1,-1), 0.4, MID_GREY),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
]
for i in range(1, len(all_rows)):
if i % 2 == 0:
style_cmds.append(("BACKGROUND", (0,i), (-1,i), alt_color))
tbl.setStyle(TableStyle(style_cmds))
return tbl
# ─────────────────────────────────────────────────────────────────────────────
# COVER PAGE
# ─────────────────────────────────────────────────────────────────────────────
def make_cover():
elements = []
# Full-width teal banner
banner = Table(
[[Paragraph("SARCOIDOSIS", COVER_TITLE)],
[Paragraph("Diagnosis & Staging", COVER_TITLE)],
[Paragraph("Reference Guide", COVER_TITLE)],
[Spacer(1, 6)],
[Paragraph("Pulmonary · Extrapulmonary · Radiologic Staging · Diagnostic Workup", COVER_SUB)],
],
colWidths=[TW]
)
banner.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), DARK_TEAL),
("TOPPADDING", (0,0), (-1,-1), 14),
("BOTTOMPADDING",(0,0), (-1,-1), 14),
("LEFTPADDING", (0,0), (-1,-1), 20),
("RIGHTPADDING", (0,0), (-1,-1), 20),
]))
elements.append(Spacer(1, 20*mm))
elements.append(banner)
elements.append(Spacer(1, 10*mm))
# Subtitle strip
strip = Table([[
Paragraph("Clinical Quick Reference for Physicians & Medical Students", COVER_SUB)
]], colWidths=[TW])
strip.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), MID_TEAL),
("TOPPADDING", (0,0), (-1,-1), 8),
("BOTTOMPADDING",(0,0), (-1,-1), 8),
]))
elements.append(strip)
elements.append(Spacer(1, 8*mm))
# Contents overview box
contents = [
["01", "Definition & Pathogenesis"],
["02", "Epidemiology & Risk Factors"],
["03", "Clinical Presentations (Acute vs. Chronic)"],
["04", "Organ System Involvement"],
["05", "Scadding Radiologic Staging System"],
["06", "Diagnostic Criteria & Workup"],
["07", "Laboratory & Biomarker Findings"],
["08", "Indications for Treatment"],
["09", "Prognostic Features"],
["10", "Key Clinical Associations"],
]
tbl = Table(
[[Paragraph(f"<b>{n}</b>", ParagraphStyle("cn", fontSize=11, textColor=MID_TEAL,
fontName="Helvetica-Bold")),
Paragraph(t, ParagraphStyle("ct", fontSize=11, textColor=DARK_GREY,
fontName="Helvetica"))]
for n, t in contents],
colWidths=[18*mm, TW - 18*mm]
)
tbl.setStyle(TableStyle([
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING",(0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 8),
("LINEBELOW", (0,0), (-1,-2), 0.3, MID_GREY),
("BACKGROUND", (0,0), (-1,-1), LIGHT_GREY),
("BACKGROUND", (0,0), (-1, 0), colors.HexColor("#FDFEFE")),
]))
elements.append(tbl)
elements.append(Spacer(1, 8*mm))
elements.append(Paragraph(
"Sources: Fishman's Pulmonary Diseases · Textbook of Family Medicine 9e · "
"Fitzpatrick's Dermatology · Cummings Otolaryngology · Harrison's Principles of "
"Internal Medicine 22E · Thorax 2025 (PMID 40393718)",
FOOTER_TEXT
))
elements.append(PageBreak())
return elements
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 1 – Definition & Pathogenesis
# ─────────────────────────────────────────────────────────────────────────────
def sec1():
E = []
E.append(section_header("01 DEFINITION & PATHOGENESIS"))
E.append(Spacer(1, 4))
E.append(Paragraph(
"Sarcoidosis is a <b>multisystem granulomatous disease of unknown etiology</b>, "
"most likely caused by an aberrant immune response to an environmental or infectious "
"antigen in a genetically susceptible host. It is the most common granulomatous lung "
"disease not attributed to a known infectious cause.",
BODY))
E.append(Spacer(1, 5))
E.append(Paragraph("Pathogenesis Sequence", SUBSEC))
steps = [
("Step 1 – Antigen exposure",
"An unidentified antigen (possibly mycobacterial, organic dust, or beryllium) is "
"inhaled or encountered in susceptible individuals."),
("Step 2 – Alveolitis",
"CD4+ T lymphocytes accumulate at sites of inflammation, releasing IL-2, IFN-γ, "
"and TNF-α, driving macrophage activation."),
("Step 3 – Granuloma formation",
"Activated macrophages differentiate into epithelioid cells, which cluster to form "
"noncaseating granulomas with multinucleated Langhans-type giant cells."),
("Step 4 – Resolution or fibrosis",
"In ~70% of cases, granulomas resolve spontaneously. In chronic disease, persistent "
"inflammation leads to irreversible fibrosis (stage IV)."),
]
for bold, text in steps:
E.append(bullet(text, bold_prefix=bold))
E.append(Spacer(1, 4))
E.append(key_box(
"Hallmark Pathology: Noncaseating (non-necrotizing) epithelioid granulomas — "
"NO central necrosis. Central necrosis suggests tuberculosis or fungal infection."
))
E.append(Spacer(1, 5))
E.append(Paragraph("Implicated Triggers", SUBSEC))
triggers = [
("Infectious", "Mycobacteria, Propionibacterium acnes"),
("Environmental", "Organic dusts, mold, pine tree pollen, insecticides"),
("Occupational", "Beryllium (chronic beryllium disease must be excluded)"),
("Genetic", "HLA-DRB1 alleles, BTNL2 gene polymorphisms (SAGA study — African Americans)"),
]
tbl = styled_table(
["Category", "Proposed Trigger"],
[[a, b] for a, b in triggers],
col_widths=[35*mm, TW - 35*mm],
header_color=MID_TEAL,
)
E.append(tbl)
E.append(Spacer(1, 8))
return E
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 2 – Epidemiology
# ─────────────────────────────────────────────────────────────────────────────
def sec2():
E = []
E.append(section_header("02 EPIDEMIOLOGY & RISK FACTORS"))
E.append(Spacer(1, 4))
epi = [
["Peak age of onset", "20–40 years (bimodal peak in women: 45–65 yrs)"],
["Sex", "Slight female predominance"],
["African Americans (US)", "Incidence ~35.5 per 100,000; more severe, chronic course"],
["Scandinavians", "High incidence; often acute (Lofgren syndrome)"],
["European whites", "Lower incidence; favor acute presentation"],
["Worldwide", "Any race/ethnicity; global distribution"],
]
tbl = styled_table(
["Parameter", "Detail"],
epi,
col_widths=[55*mm, TW - 55*mm],
header_color=DARK_TEAL,
)
E.append(tbl)
E.append(Spacer(1, 6))
E.append(note_box(
"<b>Note:</b> Sarcoidosis is considered a 'great imitator' alongside syphilis due to its "
"protean presentations and ability to mimic almost any organ system disease."
))
E.append(Spacer(1, 8))
return E
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 3 – Clinical Presentations
# ─────────────────────────────────────────────────────────────────────────────
def sec3():
E = []
E.append(section_header("03 CLINICAL PRESENTATIONS"))
E.append(Spacer(1, 4))
# Two-column layout
col1 = [
Paragraph("Acute – Lofgren Syndrome", SUBSEC),
Spacer(1, 3),
bullet("Erythema nodosum (tender, red-brown shin nodules)"),
bullet("Bilateral hilar adenopathy on CXR"),
bullet("Polyarthritis — ankles, feet, knees"),
bullet("Low-grade fever and uveitis"),
Spacer(1, 4),
Paragraph("Prognosis: <b>70–80% remission</b> within months", BODY),
Spacer(1, 4),
note_box("• More common in Europeans\n• <5% of Black patients\n• ~10% have normal CXR"),
]
col2 = [
Paragraph("Chronic / Progressive", SUBSEC),
Spacer(1, 3),
bullet("Insidious dyspnea and dry, nonproductive cough"),
bullet("Bilateral hilar adenopathy + infiltrates on CXR"),
bullet("Skin lesions: trunk and extremities"),
bullet("Uveitis, vision changes"),
bullet("Multiple flares requiring repeated treatment"),
Spacer(1, 4),
Paragraph("Course: relapsing-remitting; may progress to fibrosis", BODY),
Spacer(1, 4),
note_box("Poor prognostic features:\n• Lupus pernio\n• Chronic uveitis\n• Cardiac/neuro involvement"),
]
content = [[col1, col2]]
side_tbl = Table(content, colWidths=[(TW/2)-3, (TW/2)-3], hAlign="LEFT")
side_tbl.setStyle(TableStyle([
("VALIGN", (0,0), (-1,-1), "TOP"),
("LEFTPADDING", (0,0), (-1,-1), 4),
("RIGHTPADDING", (0,0), (-1,-1), 4),
]))
E.append(side_tbl)
E.append(Spacer(1, 8))
# Also asymptomatic
E.append(Paragraph("Asymptomatic Sarcoidosis", SUBSEC))
E.append(Paragraph(
"Up to <b>two-thirds of patients</b> are asymptomatic and diagnosed incidentally after "
"a routine chest radiograph showing bilateral hilar adenopathy. This pattern is more "
"common in white patients. Many of these patients will have spontaneous resolution "
"without any treatment.",
BODY))
E.append(Spacer(1, 8))
return E
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 4 – Organ System Involvement
# ─────────────────────────────────────────────────────────────────────────────
def sec4():
E = []
E.append(section_header("04 ORGAN SYSTEM INVOLVEMENT"))
E.append(Spacer(1, 4))
organs = [
["Pulmonary", "90%",
"Cough (nonproductive), dyspnea on exertion, chest tightness. "
"Crackles in <20%. Restrictive pattern + ↓DLCO on PFTs in progressive disease."],
["Cutaneous", "25–35%",
"Red-brown papules/plaques on face (nose, mouth). Lupus pernio (alar nose). "
"Lesions in scars/tattoos. Erythema nodosum (Lofgren)."],
["Ocular", "~25%",
"Anterior uveitis (most common). Posterior uveitis, lacrimal gland enlargement, "
"keratic precipitates, optic nerve involvement → blindness."],
["Lymphatic", "Common",
"Bilateral hilar adenopathy is the most common intrathoracic finding. "
"Peripheral lymphadenopathy in ~40%."],
["Hepatic/GI", "~50–70% subclinical",
"Granulomatous hepatitis, hepatosplenomegaly, elevated LFTs. "
"Bowel involvement rare."],
["Cardiac", "~5–10%",
"Conduction defects (AV block, BBB), ventricular arrhythmias, sudden death, "
"cardiomyopathy. FDG-PET/CMR for detection."],
["Neurologic", "~5–10%",
"CN VII palsy (most common). Hypothalamic/pituitary involvement (DI). "
"Optic chiasm → blindness. Meningitis, hydrocephalus, myelopathy."],
["Renal", "Underestimated",
"Nephrolithiasis (most common). Granulomatous interstitial nephritis. "
"Nephrocalcinosis from hypercalcemia."],
["ENT", "Variable",
"Nasal mucosal disease, sinusitis. SNHL, CN VII palsy (Heerfordt syndrome). "
"Laryngeal involvement."],
["Musculoskeletal", "10–40%",
"Polyarthritis (esp. Lofgren). Bone cysts (lacy pattern). "
"Myopathy with ↑CPK (uncommon)."],
["Endocrine/Metabolic", "10–20%",
"Hypercalciuria > hypercalcemia. ↑1,25-OH-Vit D from granuloma 1α-hydroxylase. "
"Hypopituitarism in neurosarcoidosis."],
["Exocrine Glands", "<5%",
"Heerfordt syndrome: parotitis + uveitis + CN VII palsy + fever. "
"Sicca syndrome (dry mouth/eyes)."],
]
tbl = styled_table(
["System", "Frequency", "Key Features"],
organs,
col_widths=[32*mm, 22*mm, TW - 54*mm],
header_color=DARK_TEAL,
)
E.append(tbl)
E.append(Spacer(1, 8))
return E
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 5 – Scadding Staging
# ─────────────────────────────────────────────────────────────────────────────
def sec5():
E = []
E.append(section_header("05 SCADDING RADIOLOGIC STAGING SYSTEM", color=colors.HexColor("#117864")))
E.append(Spacer(1, 4))
E.append(Paragraph(
"Chest radiography is abnormal in >90% of cases and provides important prognostic "
"information. The international Scadding system classifies findings into five stages:",
BODY))
E.append(Spacer(1, 5))
stages = [
["Stage 0", "Normal", "<15%",
"Normal chest radiograph. May still have active extrapulmonary disease.",
"Spontaneous remission likely if acute presentation."],
["Stage I", "BHA only", "30–50%",
"Bilateral hilar adenopathy (BHA). Often with right paratracheal adenopathy. "
"Symmetric 'potato node' appearance. Calcification uncommon.",
"Excellent — most resolve spontaneously. Systemic therapy usually NOT needed."],
["Stage II", "BHA + Infiltrates", "40–60%",
"Pulmonary infiltrates WITH bilateral hilar adenopathy. Linear or "
"reticulonodular markings in middle/upper lung fields.",
"Variable; ~50–70% improve with treatment."],
["Stage III", "Infiltrates only", "10–20%",
"Pulmonary infiltrates WITHOUT hilar adenopathy. May mimic infectious "
"pneumonia, GPA, or malignancy.",
"Variable; tends toward more chronic disease."],
["Stage IV", "Fibrosis", "<15%",
"Fibrocystic changes, cephalad hilar retraction, honeycombing, "
"bronchiectasis. Mycetoma complication risk.",
"Poor. Irreversible fibrosis. Risk of mycetoma, respiratory failure."],
]
# Custom colors per stage
stage_colors = [
MID_GREY,
colors.HexColor("#27AE60"), # green – good
colors.HexColor("#F39C12"), # gold – variable
colors.HexColor("#E67E22"), # orange – chronic
colors.HexColor("#C0392B"), # red – poor
]
# header
hdr = [Paragraph(h, TABLE_HDR) for h in
["Stage", "Descriptor", "Freq.", "CXR Findings", "Prognosis"]]
rows = []
for s in stages:
rows.append([Paragraph(s[i], TABLE_CELL) for i in range(5)])
tbl = Table([hdr] + rows, colWidths=[17*mm, 22*mm, 13*mm, 65*mm, TW-117*mm], repeatRows=1)
ts = [
("FONTNAME", (0,0), (-1, 0), "Helvetica-Bold"),
("FONTSIZE", (0,0), (-1,-1), 8.5),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING",(0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 5),
("GRID", (0,0), (-1,-1), 0.4, MID_GREY),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("BACKGROUND", (0,0), (-1, 0), colors.HexColor("#117864")),
("TEXTCOLOR", (0,0), (-1, 0), WHITE),
]
for i, sc in enumerate(stage_colors):
ts.append(("BACKGROUND", (0, i+1), (0, i+1), sc))
ts.append(("TEXTCOLOR", (0, i+1), (0, i+1), WHITE))
ts.append(("FONTNAME", (0, i+1), (0, i+1), "Helvetica-Bold"))
tbl.setStyle(TableStyle(ts))
E.append(tbl)
E.append(Spacer(1, 6))
E.append(note_box(
"<b>Important:</b> There is only a weak correlation between stages I, II, and III and "
"actual clinical status (ACCESS study). Stage IV is associated with poor prognosis. "
"Always correlate radiograph stage with symptoms, PFTs, and HRCT findings."
))
E.append(Spacer(1, 6))
E.append(Paragraph("HRCT Chest Findings in Sarcoidosis", SUBSEC))
hrct = [
["Perilymphatic nodules", "Peribronchovascular, subpleural — highly characteristic"],
["Upper lobe predominance", "Most common distribution; middle zones also affected"],
["'Galaxy sign'", "Central mass of nodules with satellite lesions — specific for sarcoidosis"],
["'Sausage sign'", "Nodular beading along bronchovascular bundles"],
["Ground-glass opacity", "Less common; suggests active alveolitis"],
["Fibrocystic changes", "Stage IV: honeycombing, traction bronchiectasis, hilar retraction"],
["Lymphadenopathy", "Bilateral hilar; right paratracheal; mediastinal"],
]
tbl2 = styled_table(
["HRCT Feature", "Description"],
hrct,
col_widths=[45*mm, TW - 45*mm],
header_color=colors.HexColor("#117864"),
)
E.append(tbl2)
E.append(Spacer(1, 8))
return E
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 6 – Diagnostic Criteria & Workup
# ─────────────────────────────────────────────────────────────────────────────
def sec6():
E = []
E.append(PageBreak())
E.append(section_header("06 DIAGNOSTIC CRITERIA & WORKUP"))
E.append(Spacer(1, 4))
E.append(warn_box(
"Sarcoidosis is a DIAGNOSIS OF EXCLUSION. Always rule out tuberculosis, fungal "
"infections, berylliosis, Langerhans cell histiocytosis, and lymphoma before confirming."
))
E.append(Spacer(1, 5))
E.append(Paragraph("Diagnostic Triad", SUBSEC))
E.append(bullet("Compatible <b>clinical and radiographic findings</b>"))
E.append(bullet("<b>Tissue biopsy</b> showing noncaseating granulomas"))
E.append(bullet("<b>Exclusion</b> of other known causes of noncaseating granuloma"))
E.append(Spacer(1, 5))
E.append(Paragraph("Biopsy Sites — Yield by Location", SUBSEC))
biopsy = [
["Transbronchial lung biopsy (TBLB)", "60–90%", "First-line in pulmonary disease"],
["EBUS-guided mediastinal LN biopsy", "80–90%", "Preferred over mediastinoscopy"],
["Skin lesions (if present)", "~95%", "Highly accessible; avoid erythema nodosum"],
["Peripheral lymph node", "~75%", "If palpable"],
["Liver biopsy", "~60%", "Low specificity for sarcoidosis"],
["Conjunctival biopsy", "~20–30%", "Low yield but easy access"],
["Bone marrow biopsy", "Variable", "For suspected marrow involvement"],
]
tbl = styled_table(
["Biopsy Site", "Yield", "Notes"],
biopsy,
col_widths=[55*mm, 18*mm, TW-73*mm],
header_color=DARK_TEAL,
)
E.append(tbl)
E.append(Spacer(1, 6))
E.append(Paragraph("Minimum Diagnostic Workup", SUBSEC))
workup = [
("History & Physical", "Exposure history, race/ethnicity, symptom duration, organ review"),
("Chest radiograph (PA + Lateral)", "Initial imaging; Scadding staging"),
("HRCT chest", "Superior to CXR; define extent of parenchymal disease"),
("Pulmonary function tests", "Spirometry + DLCO (restrictive pattern, ↓DLCO in advanced disease)"),
("Ophthalmologic evaluation", "Slit-lamp exam; mandatory even if asymptomatic"),
("ECG", "Screen for cardiac conduction disease"),
("Serum ACE", "Elevated in ~60%; supports but does not confirm diagnosis"),
("Serum calcium + 24-h urine calcium", "Hypercalciuria > hypercalcemia"),
("LFTs, renal function (BMP)", "Hepatic and renal involvement"),
("CBC with differential", "Lymphopenia, anemia"),
("Urinalysis", "Granulomatous nephritis, hematuria"),
("Tissue biopsy", "Noncaseating granuloma on H&E; special stains for AFB and fungi"),
]
tbl2 = styled_table(
["Investigation", "Purpose / Findings"],
workup,
col_widths=[60*mm, TW-60*mm],
header_color=DARK_TEAL,
)
E.append(tbl2)
E.append(Spacer(1, 6))
E.append(Paragraph("Additional Investigations by Suspected Organ", SUBSEC))
extra = [
["Cardiac", "Echo, 24-h Holter, CMR with gadolinium, FDG-PET/CT"],
["Neurologic", "Brain/spine MRI with contrast, CSF analysis, nerve biopsy"],
["Skin", "Punch biopsy (avoid erythema nodosum — non-specific panniculitis)"],
["Calcium", "PTH, 25-OH-Vit D, 1,25-OH-Vit D, 24-h urine Ca"],
["Bone", "Skeletal survey, bone scan"],
["Endocrine", "AM cortisol, TFTs, prolactin, serum osmolality (if DI suspected)"],
]
tbl3 = styled_table(
["Organ System", "Targeted Tests"],
extra,
col_widths=[35*mm, TW-35*mm],
header_color=MID_TEAL,
)
E.append(tbl3)
E.append(Spacer(1, 8))
return E
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 7 – Labs & Biomarkers
# ─────────────────────────────────────────────────────────────────────────────
def sec7():
E = []
E.append(section_header("07 LABORATORY & BIOMARKER FINDINGS"))
E.append(Spacer(1, 4))
labs = [
["Serum ACE (SACE)", "Elevated in ~60%",
"Correlates with granuloma burden. Not diagnostic alone. Decreases with "
"corticosteroids or disease remission. Highly variable; poor prognostic value."],
["Serum calcium", "Elevated in ~10%",
"Due to granuloma 1α-hydroxylase converting 25-OH-D → 1,25(OH)₂-D."],
["24-h urine calcium", "Elevated in ~40%",
"More sensitive than serum calcium. Can cause nephrolithiasis and nephrocalcinosis."],
["1,25-OH Vitamin D", "Often elevated",
"Produced by granuloma macrophages. 25-OH-Vit D may be low."],
["Serum sIL-2R", "Elevated",
"More sensitive than ACE; reflects T-cell activation. Not widely standardized."],
["BAL CD4:CD8 ratio", ">3.5 (supportive)",
"Elevated ratio supports diagnosis. Neither sensitive nor specific enough alone."],
["LFTs (AST/ALT/ALP/GGT)", "May be elevated",
"Hepatic granulomatous involvement; ALP often elevated disproportionately."],
["CBC", "Lymphopenia, mild anemia",
"Non-specific; may see hypersplenism if splenic involvement."],
["CRP / ESR", "Elevated in active disease",
"Non-specific inflammatory markers; may track disease activity."],
["Ferritin", "May be elevated",
"Especially in acute disease; non-specific."],
["Urinalysis", "Hematuria, proteinuria",
"Granulomatous interstitial nephritis; renal tubular acidosis possible."],
]
tbl = styled_table(
["Biomarker", "Typical Finding", "Clinical Significance"],
labs,
col_widths=[40*mm, 28*mm, TW-68*mm],
header_color=DARK_TEAL,
)
E.append(tbl)
E.append(Spacer(1, 6))
E.append(note_box(
"<b>No single biomarker is diagnostic of sarcoidosis.</b> ACE and calcium levels are "
"helpful supporting evidence but must always be interpreted alongside clinical, "
"radiologic, and histologic findings. Transcriptomic signatures are under investigation "
"as future prognostic tools."
))
E.append(Spacer(1, 8))
return E
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 8 – Indications for Treatment
# ─────────────────────────────────────────────────────────────────────────────
def sec8():
E = []
E.append(section_header("08 INDICATIONS FOR TREATMENT"))
E.append(Spacer(1, 4))
E.append(Paragraph(
"There are <b>no FDA-approved systemic therapies</b> for sarcoidosis. Treatment "
"decisions are guided by organ involvement, disease severity, and rate of progression. "
"Many patients, especially those with stage I disease, do NOT require systemic therapy.",
BODY))
E.append(Spacer(1, 5))
# Two-column: treat vs. observe
col1_items = [
Paragraph("<b>TREAT — Indications</b>", ParagraphStyle(
"th", fontSize=9.5, textColor=WHITE, fontName="Helvetica-Bold",
alignment=TA_CENTER)),
Spacer(1, 3),
bullet("Threatened organ failure (cardiac, neurologic, ocular)"),
bullet("Uveitis unresponsive to topical corticosteroids"),
bullet("Progressive symptomatic pulmonary disease"),
bullet("Disfiguring cutaneous disease (esp. lupus pernio)"),
bullet("Persistent hypercalcemia or hypercalciuria with renal risk"),
bullet("Renal or hepatic dysfunction"),
bullet("Symptomatic hepatic / splenic enlargement"),
bullet("Neurosarcoidosis"),
bullet("Cardiac conduction disease or ventricular arrhythmias"),
bullet("Disabling arthritis (Lofgren)"),
]
col2_items = [
Paragraph("<b>OBSERVE — No Immediate Treatment</b>", ParagraphStyle(
"th2", fontSize=9.5, textColor=WHITE, fontName="Helvetica-Bold",
alignment=TA_CENTER)),
Spacer(1, 3),
bullet("Stage I asymptomatic — spontaneous remission common"),
bullet("Lofgren syndrome (NSAID + rest first)"),
bullet("Stable, mild pulmonary disease without functional impairment"),
bullet("Incidental bilateral hilar adenopathy in young patient"),
Spacer(1, 6),
Paragraph("Monitoring for ≥3 years after 'remission'", BOLD_BODY),
Spacer(1, 3),
Paragraph(
"Longer observation required for serious pulmonary or "
"extrapulmonary manifestations. Relapse is common.",
BODY),
]
cols_tbl = Table(
[[col1_items, col2_items]],
colWidths=[(TW/2)-2, (TW/2)-2],
)
cols_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (0,0), colors.HexColor("#E74C3C")),
("BACKGROUND", (1,0), (1,0), colors.HexColor("#117864")),
("TOPPADDING", (0,0), (-1,-1), 8),
("BOTTOMPADDING",(0,0), (-1,-1), 8),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING", (0,0), (-1,-1), 8),
("VALIGN", (0,0), (-1,-1), "TOP"),
]))
E.append(cols_tbl)
E.append(Spacer(1, 6))
E.append(Paragraph("Treatment Ladder", SUBSEC))
tx = [
["First Line", "Corticosteroids",
"Prednisone 20–40 mg/day (0.5 mg/kg/day). Tapered over 12–24 months. "
"Renal sarcoidosis: 1 mg/kg/day (max 60 mg/day)."],
["Second Line (steroid-sparing)", "Methotrexate",
"Most commonly used. 10–15 mg/week + folic acid. Monitor LFTs."],
["Second Line", "Hydroxychloroquine",
"Best for cutaneous disease and hypercalcemia. 200–400 mg/day."],
["Second Line", "Azathioprine / MMF",
"Alternative steroid-sparing agents for pulmonary/extrapulmonary."],
["Third Line (refractory)", "Infliximab (anti-TNF-α)",
"Most evidence among biologics. Effective for refractory pulmonary, "
"cutaneous, and neurologic disease. 2025 meta-analysis (PMID 40393718) confirms efficacy."],
["Third Line", "Adalimumab",
"Alternative anti-TNF agent. Less data than infliximab."],
["Local", "Intralesional corticosteroids",
"For localized cutaneous disease. Repeated every 2–4 weeks."],
]
tbl = styled_table(
["Tier", "Agent", "Details"],
tx,
col_widths=[28*mm, 35*mm, TW-63*mm],
header_color=DARK_TEAL,
)
E.append(tbl)
E.append(Spacer(1, 8))
return E
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 9 – Prognosis
# ─────────────────────────────────────────────────────────────────────────────
def sec9():
E = []
E.append(section_header("09 PROGNOSTIC FEATURES"))
E.append(Spacer(1, 4))
E.append(Paragraph("Prognosis by Presentation / Stage", SUBSEC))
prog = [
["Lofgren syndrome", "Excellent", "70–80% remission; often without treatment"],
["Stage I (asymptomatic BHA)", "Excellent", ">70% spontaneous remission"],
["Stage II–III", "Variable", "~50–70% improve with treatment; some progress"],
["Stage IV (fibrocystic)", "Poor", "Irreversible fibrosis; mycetoma risk; respiratory failure"],
["Cardiac sarcoidosis", "Guarded", "Increased mortality; sudden cardiac death; ICD may be needed"],
["Neurosarcoidosis", "Guarded", "Unpredictable; high morbidity; may require long-term immunosuppression"],
["Black race, chronic disease", "Worse", "Higher rates of persistent disease, organ damage, mortality"],
]
tbl = styled_table(
["Presentation / Stage", "Prognosis", "Notes"],
prog,
col_widths=[50*mm, 22*mm, TW-72*mm],
header_color=DARK_TEAL,
)
E.append(tbl)
E.append(Spacer(1, 5))
E.append(Paragraph("Poor Prognostic Markers", SUBSEC))
poor = [
"Lupus pernio",
"Chronic uveitis",
"Hypercalcemia or nephrocalcinosis",
"Nasal mucosal involvement",
"Bone cysts",
"Neurosarcoidosis",
"Cardiac involvement",
"Age >40 at onset",
"Disease duration >2 years",
"African American race",
"Stage III–IV radiographic disease",
"Severe restrictive defect on PFTs (FVC <60% predicted)",
]
# 2-column bullet layout
mid = len(poor) // 2
col1 = [bullet(p) for p in poor[:mid]]
col2 = [bullet(p) for p in poor[mid:]]
bt = Table([[col1, col2]], colWidths=[TW/2, TW/2])
bt.setStyle(TableStyle([
("VALIGN", (0,0), (-1,-1), "TOP"),
("LEFTPADDING", (0,0), (-1,-1), 2),
("RIGHTPADDING", (0,0), (-1,-1), 2),
]))
E.append(bt)
E.append(Spacer(1, 8))
return E
# ─────────────────────────────────────────────────────────────────────────────
# SECTION 10 – Key Associations
# ─────────────────────────────────────────────────────────────────────────────
def sec10():
E = []
E.append(section_header("10 KEY CLINICAL ASSOCIATIONS"))
E.append(Spacer(1, 4))
assoc = [
["Bilateral hilar adenopathy", "Most common intrathoracic radiographic finding"],
["Lupus pernio", "Violaceous nasal plaques → chronic disease, upper airway involvement, pulmonary fibrosis"],
["Erythema nodosum + BHA", "Lofgren syndrome — good prognosis (NOT granuloma on biopsy — panniculitis)"],
["Heerfordt syndrome", "Parotitis + uveitis + CN VII palsy + fever (uveoparotid fever)"],
["Hypercalcemia / hypercalciuria", "Granuloma 1α-hydroxylase activity → ↑1,25-OH-Vit D"],
["Noncaseating granuloma", "Pathologic hallmark — absence of necrosis differentiates from TB"],
["Elevated ACE", "~60% of patients; reflects granuloma burden; not diagnostic"],
["CN VII most affected cranial nerve", "Often bilateral; may be sudden onset; Heerfordt syndrome"],
["'Great imitator'", "Mimics TB, lymphoma, fungal disease, vasculitis — always biopsy"],
["Mycetoma", "Complication of stage IV fibrocystic sarcoidosis"],
["Galaxy sign on HRCT", "Highly specific for sarcoidosis — central mass of nodules with satellites"],
["Potato nodes", "Symmetric bilateral hilar lymphadenopathy appearance on CXR"],
["Stage I — no fibrosis, good prognosis", "Systemic steroids usually not indicated"],
["Spontaneous remission", "Occurs in ~70% of acute-onset cases within 2–3 years"],
]
tbl = styled_table(
["Finding / Association", "Clinical Significance"],
assoc,
col_widths=[65*mm, TW-65*mm],
header_color=colors.HexColor("#6C3483"),
)
E.append(tbl)
E.append(Spacer(1, 6))
E.append(hr(color=MID_TEAL, thickness=1))
E.append(Spacer(1, 4))
E.append(Paragraph(
"References: Fishman's Pulmonary Diseases and Disorders, 2-Volume Set · "
"Textbook of Family Medicine 9e · Fitzpatrick's Dermatology, Vol 1–2 · "
"Andrews' Diseases of the Skin · Cummings Otolaryngology Head and Neck Surgery · "
"Harrison's Principles of Internal Medicine 22E (2025) · "
"Bechman K et al. Thorax 2025 (PMID 40393718) — Systematic review of biologics in sarcoidosis",
FOOTER_TEXT
))
E.append(Spacer(1, 3))
E.append(Paragraph(
"Generated by Orris Medical Reference · July 2026 · For educational use only — "
"not a substitute for clinical judgment.",
FOOTER_TEXT
))
return E
# ─────────────────────────────────────────────────────────────────────────────
# PAGE NUMBERS
# ─────────────────────────────────────────────────────────────────────────────
def on_each_page(canvas, doc):
canvas.saveState()
page_num = canvas.getPageNumber()
if page_num > 1:
canvas.setFont("Helvetica", 7)
canvas.setFillColor(MID_GREY)
canvas.drawRightString(W - MARGIN, 12*mm, f"Page {page_num}")
canvas.drawString(MARGIN, 12*mm, "Sarcoidosis: Diagnosis & Staging Reference Guide")
canvas.setStrokeColor(MID_GREY)
canvas.setLineWidth(0.3)
canvas.line(MARGIN, 14.5*mm, W - MARGIN, 14.5*mm)
canvas.restoreState()
# ─────────────────────────────────────────────────────────────────────────────
# BUILD
# ─────────────────────────────────────────────────────────────────────────────
story = []
story += make_cover()
story += sec1()
story += sec2()
story += sec3()
story += sec4()
story += sec5()
story += sec6()
story += sec7()
story += sec8()
story += sec9()
story += sec10()
doc.build(story, onFirstPage=on_each_page, onLaterPages=on_each_page)
print("PDF generated successfully.")
cd /tmp/workspace/sarcoidosis-pdf && python generate_pdf.py 2>&1
ls -lh /tmp/workspace/sarcoidosis-pdf/Sarcoidosis_Diagnosis_Staging_Guide.pdf
Sarcoidosis Diagnosis Staging Guide
PDF Document · PDF
| Section | Content |
|---|---|
| 01 | Definition & Pathogenesis (step-by-step granuloma formation, implicated triggers table) |
| 02 | Epidemiology & Risk Factors (incidence by race/geography) |
| 03 | Clinical Presentations - Acute (Lofgren) vs. Chronic vs. Asymptomatic (two-column layout) |
| 04 | Organ System Involvement - 12 systems with frequency and key features |
| 05 | Scadding Radiologic Staging - color-coded stage 0-IV table + HRCT features |
| 06 | Diagnostic Criteria & Workup - biopsy yields by site, minimum workup checklist |
| 07 | Laboratory & Biomarker Findings - ACE, calcium, sIL-2R, BAL, etc. |
| 08 | Indications for Treatment - Treat vs. Observe layout + full treatment ladder |
| 09 | Prognostic Features - by stage/presentation + poor prognostic markers |
| 10 | Key Clinical Associations - 14-row quick-reference table |