~/peds-abdominal-mass/generate_pdf.py
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import cm, mm
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, KeepTogether
)
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT
from reportlab.platypus import PageBreak
OUTPUT = "/home/daytona/workspace/peds-abdominal-mass/Pediatric_Abdominal_Mass_DDx.pdf"
# ── Colours ──────────────────────────────────────────────────────────────────
NAVY = colors.HexColor("#1a3a5c")
TEAL = colors.HexColor("#1e7f8e")
AMBER = colors.HexColor("#e67e22")
RED = colors.HexColor("#c0392b")
GREEN = colors.HexColor("#27ae60")
PURPLE = colors.HexColor("#8e44ad")
LTGRAY = colors.HexColor("#f5f6fa")
MIDGRAY = colors.HexColor("#dfe3e8")
DARKGRAY = colors.HexColor("#4a4a5a")
WHITE = colors.white
BLACK = colors.black
# ── Styles ────────────────────────────────────────────────────────────────────
styles = getSampleStyleSheet()
def S(name, **kw):
"""Create a named ParagraphStyle extending 'Normal'."""
return ParagraphStyle(name, parent=styles["Normal"], **kw)
Title = S("MyTitle", fontSize=22, textColor=WHITE, fontName="Helvetica-Bold",
alignment=TA_CENTER, leading=28, spaceAfter=4)
Subtitle = S("MySub", fontSize=11, textColor=WHITE, fontName="Helvetica",
alignment=TA_CENTER, leading=16)
SectionH = S("SH", fontSize=12, textColor=WHITE, fontName="Helvetica-Bold",
alignment=TA_LEFT, leading=16, leftIndent=6)
Body = S("Body", fontSize=8.5, textColor=DARKGRAY, fontName="Helvetica",
leading=12, spaceAfter=2)
Bold = S("Bold", fontSize=8.5, textColor=BLACK, fontName="Helvetica-Bold",
leading=12)
SmallBold = S("SmallBold", fontSize=7.5, textColor=DARKGRAY, fontName="Helvetica-Bold",
leading=11)
Small = S("Small", fontSize=7.5, textColor=DARKGRAY, fontName="Helvetica",
leading=11)
TblHdr = S("TblHdr", fontSize=8, textColor=WHITE, fontName="Helvetica-Bold",
alignment=TA_CENTER, leading=11)
TblCell = S("TblCell", fontSize=7.5, textColor=DARKGRAY, fontName="Helvetica",
leading=11, leftIndent=3)
TblCellB = S("TblCellB", fontSize=7.5, textColor=BLACK, fontName="Helvetica-Bold",
leading=11, leftIndent=3)
FooterS = S("Footer", fontSize=7, textColor=DARKGRAY, fontName="Helvetica-Oblique",
alignment=TA_CENTER)
RedAlert = S("RedAlert", fontSize=8, textColor=RED, fontName="Helvetica-Bold",
leading=12)
GreenText = S("GreenText", fontSize=8, textColor=GREEN, fontName="Helvetica-Bold",
leading=12)
# ── Page canvas (header/footer) ───────────────────────────────────────────────
PAGE_W, PAGE_H = A4
MARGIN = 1.5*cm
def on_page(canvas, doc):
canvas.saveState()
# top stripe
canvas.setFillColor(NAVY)
canvas.rect(0, PAGE_H - 1.2*cm, PAGE_W, 1.2*cm, fill=1, stroke=0)
canvas.setFillColor(WHITE)
canvas.setFont("Helvetica-Bold", 8)
canvas.drawString(MARGIN, PAGE_H - 0.82*cm,
"PEDIATRIC ABDOMINAL MASS — QUICK REFERENCE")
canvas.setFont("Helvetica", 8)
canvas.drawRightString(PAGE_W - MARGIN, PAGE_H - 0.82*cm,
f"Page {doc.page}")
# bottom stripe
canvas.setFillColor(MIDGRAY)
canvas.rect(0, 0, PAGE_W, 0.9*cm, fill=1, stroke=0)
canvas.setFillColor(DARKGRAY)
canvas.setFont("Helvetica-Oblique", 6.5)
canvas.drawCentredString(PAGE_W/2, 0.32*cm,
"For educational use only. Clinical decisions require full patient assessment.")
canvas.restoreState()
def on_first_page(canvas, doc):
on_page(canvas, doc)
# ── Helpers ───────────────────────────────────────────────────────────────────
def section_banner(text, color=TEAL):
"""Coloured banner that acts as a section header."""
data = [[Paragraph(text, SectionH)]]
t = Table(data, colWidths=[PAGE_W - 2*MARGIN])
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]),
]))
return t
def colored_table(headers, rows, col_widths, hdr_color=NAVY, alt_color=LTGRAY):
"""Generic coloured table."""
hdr_row = [Paragraph(h, TblHdr) for h in headers]
data = [hdr_row]
for i, row in enumerate(rows):
data.append([Paragraph(str(c), TblCell) for c in row])
t = Table(data, colWidths=col_widths, repeatRows=1)
style = [
("BACKGROUND", (0,0), (-1,0), hdr_color),
("ROWBACKGROUNDS",(0,1), (-1,-1), [WHITE, alt_color]),
("GRID", (0,0), (-1,-1), 0.4, MIDGRAY),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 4),
("RIGHTPADDING", (0,0), (-1,-1), 4),
("VALIGN", (0,0), (-1,-1), "TOP"),
]
t.setStyle(TableStyle(style))
return t
def mini_box(title, lines, bg=LTGRAY, title_color=NAVY):
"""Small info box with a title bar."""
content = [[Paragraph(title, ParagraphStyle("BT", parent=styles["Normal"],
fontSize=8, textColor=WHITE, fontName="Helvetica-Bold", leading=11))]]
body_rows = [[Paragraph(l, Small)] for l in lines]
t_title = Table(content, colWidths=[PAGE_W - 2*MARGIN])
t_title.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), title_color),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 8),
]))
t_body = Table(body_rows, colWidths=[PAGE_W - 2*MARGIN])
t_body.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), bg),
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING", (0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 10),
("GRID", (0,0), (-1,-1), 0.2, MIDGRAY),
]))
return [t_title, t_body]
# ── Document build ────────────────────────────────────────────────────────────
doc = SimpleDocTemplate(
OUTPUT,
pagesize=A4,
leftMargin=MARGIN, rightMargin=MARGIN,
topMargin=2.0*cm, bottomMargin=1.5*cm,
title="Pediatric Abdominal Mass - Quick Reference",
author="Orris Medical",
subject="Pediatric Radiology - Differential Diagnosis"
)
story = []
SP = lambda n=4: Spacer(1, n)
# ══════════════════════════════════════════════════════
# COVER BLOCK
# ══════════════════════════════════════════════════════
cover_data = [[
Paragraph("PEDIATRIC ABDOMINAL MASS", Title),
Paragraph("Differential Diagnosis · Quick Reference Card", Subtitle),
Paragraph("Radiology · Pathology · Key Distinguishing Features", Subtitle),
]]
cover = Table(cover_data, colWidths=[PAGE_W - 2*MARGIN])
cover.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), NAVY),
("TOPPADDING", (0,0), (-1,-1), 18),
("BOTTOMPADDING", (0,0), (-1,-1), 18),
("LEFTPADDING", (0,0), (-1,-1), 12),
("RIGHTPADDING", (0,0), (-1,-1), 12),
("ROUNDEDCORNERS", [6]),
]))
story.append(cover)
story.append(SP(10))
# ══════════════════════════════════════════════════════
# SECTION 1 - OVERVIEW TABLE (by age)
# ══════════════════════════════════════════════════════
story.append(section_banner("1. AGE-BASED DIFFERENTIAL OVERVIEW", NAVY))
story.append(SP(4))
age_headers = ["Age Group", "Most Likely Diagnosis", "Other Diagnoses to Consider"]
age_rows = [
["Neonate\n(0–1 month)",
"Hydronephrosis (most common)\nMulticystic dysplastic kidney\nAdrenal haemorrhage",
"Ovarian cyst (girls)\nMesenteric/enteric cyst\nNeuroblastoma (cystic)\nHepatoblastoma"],
["Infant\n(1–12 months)",
"Wilms tumour (nephroblastoma)\nNeuroblastoma\nHepatoblastoma",
"Hydronephrosis\nSplenic cyst\nLymphoma\nTeratoma (sacrococcygeal)"],
["Toddler\n(1–5 years)",
"Wilms tumour (peak 3 yrs)\nNeuroblastoma (peak 2 yrs)\nHepatoblastoma",
"Lymphoma\nRhabdomyosarcoma\nGerm cell tumour\nDuplication cyst"],
["School age\n(5–10 years)",
"Wilms tumour\nLymphoma (NHL)\nHepatocellular carcinoma",
"Rhabdomyosarcoma\nNeuroblastoma (rare)\nGastrointestinal stromal tumour\nTeratoma"],
["Adolescent\n(>10 years)",
"Lymphoma (most common solid mass)\nOvarian tumour/cyst (girls)\nHepatocellular carcinoma",
"Rhabdomyosarcoma\nDesmoplastic small round cell tumour\nGastrointestinal tumours\nRenal cell carcinoma"],
]
W = PAGE_W - 2*MARGIN
story.append(colored_table(age_headers, age_rows,
col_widths=[3.2*cm, W*0.38, W*0.42], hdr_color=TEAL))
story.append(SP(10))
# ══════════════════════════════════════════════════════
# SECTION 2 - MAJOR DIAGNOSES COMPARISON TABLE
# ══════════════════════════════════════════════════════
story.append(section_banner("2. MAJOR DIAGNOSES — COMPARISON TABLE", NAVY))
story.append(SP(4))
comp_headers = ["Feature", "Neuroblastoma", "Nephroblastoma\n(Wilms)", "Hepatoblastoma",
"Lymphoma", "Hydronephrosis"]
comp_rows = [
["Origin",
"Adrenal / sympathetic chain (neural crest)",
"Kidney (metanephric blastema)",
"Liver (hepatic progenitors)",
"Lymph nodes / reticuloendothelial",
"Renal collecting system"],
["Peak Age",
"2 yrs\n(90% < 5 yrs)",
"3 yrs\n(2–5 yrs)",
"< 3 yrs\n(90%)",
"Any; NHL peak\n5–10 yrs",
"Any age\n(often prenatal)"],
["Location",
"Retroperitoneal;\nsuprarenal;\ncan be thoracic/cervical",
"Intrarenal;\nRUQ or LUQ",
"RUQ; hepatic",
"Mesenteric, retroperitoneal, splenic",
"Flank; renal fossa"],
["Bilateral",
"Rare (< 5%)",
"10% of cases",
"Rare",
"Diffuse/multi-nodal",
"10–30% depending on cause"],
["Calcification on CT",
"Very common\n(up to 85%)",
"Rare (< 10%)",
"Present in 50%\n(dystrophic)",
"Rare; post-treatment",
"None"],
["Vessel relationship",
"Encases aorta/IVC\n(does NOT displace)",
"Displaces vessels;\nIVC tumour thrombus",
"Hepatic vein/IVC\ninvasion possible",
"Displacement;\nno encasement",
"Hydronephrotic pelvis; ureter dilated"],
["Tumour thrombus",
"Absent",
"IVC/renal vein\n(up to 10%)",
"Hepatic vein/IVC",
"Absent",
"Not applicable"],
["Crosses midline",
"Common (large masses)",
"Less common",
"Can cross midline\nif large",
"Often bilateral/midline\nnodal disease",
"Does not cross"],
["US key finding",
"Anterior displacement\nof aorta & IVC;\nhyperechoic foci (Ca++)",
"Intrarenal mass;\nclaw of normal\nrenal tissue;\nIVC thrombus",
"Hepatic mass;\nhyperechoic or mixed",
"Hypoechoic nodal\nmasses; splenomegaly",
"Dilated pelvicalyceal\nsystem with anechoic\npelvis"],
["CT hallmark",
"Calcification 85%;\nencasement of vessels;\nextension into spinal canal\n(dumb-bell)",
"Intrarenal; claw sign;\nlow-attenuation areas;\nheterogeneous enhancement",
"Large liver mass;\nmay show tumour\nthrombus in hepatic veins",
"Bulky retroperitoneal\nor mesenteric nodes;\nhomogeneous",
"Dilated pelvis ± ureter;\nthin cortex if longstanding"],
["MRI best use",
"Spinal canal invasion;\nbone marrow metastases",
"Gold standard for\nbilateral disease;\nnephroblastomatosis",
"Resectability;\nvascular involvement",
"Extent of disease;\nspinal cord compression",
"Anatomy of\nobstruction; duplex"],
["Nuclear medicine",
"¹²³I-mIBG (specific);\n⁹⁹ᵐTc bone scan",
"Not used",
"Not standard",
"FDG-PET/CT",
"MAG3 / DTPA renogram\n(function + drainage)"],
["Biochemical marker",
"Urine VMA/HVA ↑\n(~90% of cases)",
"None specific",
"AFP ↑↑ (> 90%)",
"LDH ↑; uric acid ↑",
"None specific"],
["Metastases",
"Bone, bone marrow,\nliver, LN, skin\n(~50% at diagnosis)",
"Lung (first), liver\n(stage IV ~10%)",
"Lung most common;\nliver",
"Widespread by staging;\nbone marrow in NHL",
"Not a malignancy;\nbilateral = systemic cause"],
["Prognosis",
"Stage & MYCN dependent;\noverall 5-yr ~75%;\nhigh-risk ~40%",
"Excellent overall;\n5-yr OS 86–96% (I–III);\n70% bilateral (V)",
"Good if resectable;\n5-yr OS ~70%",
"Good with\nchemotherapy",
"Excellent after\nrelief of obstruction"],
]
cw2 = [2.8*cm, (W-2.8*cm)/5, (W-2.8*cm)/5, (W-2.8*cm)/5, (W-2.8*cm)/5, (W-2.8*cm)/5]
story.append(colored_table(comp_headers, comp_rows, col_widths=cw2, hdr_color=NAVY))
story.append(SP(10))
# ══════════════════════════════════════════════════════
# SECTION 3 - NEUROBLASTOMA vs NEPHROBLASTOMA (focused)
# ══════════════════════════════════════════════════════
story.append(section_banner("3. NEUROBLASTOMA vs. NEPHROBLASTOMA — KEY DIFFERENTIATORS", TEAL))
story.append(SP(4))
diff_headers = ["Criterion", "Neuroblastoma", "Nephroblastoma (Wilms)"]
diff_rows = [
["Kidney relationship", "DISPLACES kidney — kidney is separate, intact, with own capsule visible", "ARISES FROM kidney — residual tissue forms a 'claw/beak' around mass"],
["Calcification (CT)", "Very common (85%) — coarse, amorphous", "Rare (< 10%)"],
["Vessel encasement", "Encases aorta/IVC/mesenteric vessels — classic NB pattern", "Displaces vessels; IVC tumour thrombus in up to 10%"],
["mIBG scintigraphy", "POSITIVE in ~80% — pathognomonic", "NEGATIVE — no catecholamine uptake"],
["Urine catecholamines", "VMA/HVA elevated in ~90%", "Normal"],
["Spinal extension", "Dumb-bell through foramina — MRI mandatory", "Not a feature"],
["Bilateral disease", "Rare", "10% — MRI gold standard for bilateral"],
["Crossing midline", "Common (large retroperitoneal masses)", "Less common"],
["IVC thrombus", "Absent", "Present in up to 10% — assess on US and CT"],
["Associated syndromes", "Familial ALK mutations", "WAGR, Denys-Drash, Beckwith-Wiedemann"],
]
cw3 = [3.8*cm, (W-3.8*cm)/2, (W-3.8*cm)/2]
story.append(colored_table(diff_headers, diff_rows, col_widths=cw3, hdr_color=PURPLE))
story.append(SP(10))
# ══════════════════════════════════════════════════════
# SECTION 4 - IMAGING APPROACH ALGORITHM
# ══════════════════════════════════════════════════════
story.append(section_banner("4. RECOMMENDED IMAGING APPROACH", NAVY))
story.append(SP(4))
algo_data = [
[Paragraph("<b>STEP 1 — ULTRASOUND (FIRST LINE for all)</b>", Bold)],
[Paragraph("• Determine organ of origin (renal vs. hepatic vs. retroperitoneal vs. pelvic)\n"
"• Assess vascularity with Colour Doppler\n"
"• Identify IVC / renal vein tumour thrombus (critical for Wilms)\n"
"• Assess for calcification (hyperechoic foci)\n"
"• Determine cystic vs. solid vs. mixed nature", Body)],
[Paragraph("<b>STEP 2 — CT ABDOMEN/PELVIS with IV contrast (staging & characterisation)</b>", Bold)],
[Paragraph("• NB: chest + abdomen + pelvis — early contrast phase for chest, portal phase for abdomen\n"
"• Wilms: abdomen + pelvis; add chest CT if CXR equivocal for pulmonary metastases\n"
"• Assess calcification, vessel encasement vs. displacement, lymphadenopathy\n"
"• Evaluate contralateral kidney (bilateral Wilms / NB)", Body)],
[Paragraph("<b>STEP 3 — MRI (specific indications)</b>", Bold)],
[Paragraph("• Paraspinal NB: MANDATORY to evaluate spinal canal extension (dumb-bell)\n"
"• Bilateral Wilms: GOLD STANDARD — superior to CT for bilateral disease\n"
"• Nephroblastomatosis: gadolinium required to detect nephrogenic rests\n"
"• Hepatic tumours: vascular anatomy for surgical planning\n"
"• Preferred over CT in neonates/infants (no ionising radiation)", Body)],
[Paragraph("<b>STEP 4 — NUCLEAR MEDICINE (disease-specific)</b>", Bold)],
[Paragraph("• ¹²³I-mIBG scintigraphy: NEUROBLASTOMA only — characterisation + metastasis staging\n"
"• ⁹⁹ᵐTc-MDP bone scan: NB skeletal metastases (+ mIBG)\n"
"• MAG3 / DTPA renogram: HYDRONEPHROSIS — functional assessment\n"
"• FDG-PET/CT: LYMPHOMA staging and treatment response\n"
"• Hepatobiliary scan (HIDA): biliary atresia / choledochal cyst", Body)],
]
algo_t = Table([[row[0]] for row in algo_data], colWidths=[W])
algo_style = []
for i in range(len(algo_data)):
if i % 2 == 0:
algo_style.append(("BACKGROUND", (0,i), (-1,i), LTGRAY))
else:
algo_style.append(("BACKGROUND", (0,i), (-1,i), WHITE))
algo_style += [
("GRID", (0,0), (-1,-1), 0.3, MIDGRAY),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING", (0,0), (-1,-1), 8),
("VALIGN", (0,0), (-1,-1), "TOP"),
]
flat_algo = [[Paragraph(row[0].text if hasattr(row[0], 'text') else "", Body)] for row in algo_data]
# Build as simple alternating table
algo_rows_built = []
for i, row in enumerate(algo_data):
algo_rows_built.append([row[0]])
algo_t2 = Table(algo_rows_built, colWidths=[W])
algo_t2.setStyle(TableStyle([
("ROWBACKGROUNDS", (0,0), (-1,-1), [LTGRAY, WHITE]),
("GRID", (0,0), (-1,-1), 0.3, MIDGRAY),
("TOPPADDING", (0,0), (-1,-1), 6),
("BOTTOMPADDING", (0,0), (-1,-1), 6),
("LEFTPADDING", (0,0), (-1,-1), 10),
("RIGHTPADDING", (0,0), (-1,-1), 10),
("VALIGN", (0,0), (-1,-1), "TOP"),
]))
story.append(algo_t2)
story.append(SP(10))
# ══════════════════════════════════════════════════════
# SECTION 5 - BIOCHEMICAL & CLINICAL RED FLAGS
# ══════════════════════════════════════════════════════
story.append(section_banner("5. CLINICAL & BIOCHEMICAL CLUES", TEAL))
story.append(SP(4))
clue_headers = ["Finding", "Think of...", "Notes"]
clue_rows = [
["Elevated urine VMA/HVA", "Neuroblastoma", "Present in ~90%; sufficient with +ve bone marrow to confirm Dx"],
["AFP markedly elevated", "Hepatoblastoma", "AFP normally high in neonates; use age-adjusted reference ranges"],
["Haematuria", "Wilms tumour", "Microscopic haematuria in 25%; macroscopic less common"],
["Hypertension", "Neuroblastoma, Wilms, Phaeochromocytoma, Renovascular", "Catecholamines (NB) vs. renin excess (Wilms)"],
["Horner syndrome", "Cervical/thoracic Neuroblastoma", "Stellate ganglion involvement"],
["Opsoclonus-myoclonus\n(dancing eyes/feet)", "Neuroblastoma", "Paraneoplastic — MEI; may have small/occult primary"],
["Watery diarrhoea\n(intractable)", "Neuroblastoma", "VIP (vasoactive intestinal peptide) production"],
["Proptosis /\n'raccoon eyes'", "Neuroblastoma\n(orbital metastases)", "Periorbital ecchymosis — classic NB presentation"],
["Aniridia", "Wilms tumour (WAGR syndrome)", "WT1 deletion on 11p13"],
["Hemihypertrophy /\nmacroglossia", "Wilms tumour\n(Beckwith-Wiedemann)", "Also hepatoblastoma risk; screen with USS"],
["Blueberry muffin skin\nnodules (neonate)", "Neuroblastoma\n(Stage 4S)", "Excellent prognosis; often undergoes spontaneous regression"],
["Elevated LDH + uric acid", "Lymphoma", "Tumour lysis risk; treat before biopsy"],
["Abdominal mass +\nback pain/leg weakness", "NB (dumb-bell tumour)", "Emergency MRI to rule out cord compression"],
["Painless abdominal mass,\ncrossing midline", "Neuroblastoma or\nLymphoma", "Wilms rarely crosses midline"],
["Palpable mass with\nrapid growth + fever", "Wilms tumour, Lymphoma", "Ruptured Wilms: peritoneal seeding → upstages to Stage III"],
]
cw5 = [3.8*cm, 3.5*cm, W - 3.8*cm - 3.5*cm]
story.append(colored_table(clue_headers, clue_rows, col_widths=cw5, hdr_color=AMBER))
story.append(SP(10))
# ══════════════════════════════════════════════════════
# SECTION 6 - STAGING SYSTEMS
# ══════════════════════════════════════════════════════
story.append(section_banner("6. STAGING SYSTEMS", NAVY))
story.append(SP(4))
# Two-column layout: NB staging | Wilms staging
nb_stage = [
["<b>NEUROBLASTOMA — INSS Staging</b>"],
["Stage 1: Localised, complete gross excision ± microscopic residual"],
["Stage 2A: Localised, incomplete excision; ipsilateral LN negative"],
["Stage 2B: Localised ± incomplete excision; ipsilateral LN positive"],
["Stage 3: Unresectable unilateral tumour infiltrating across midline ± LN; or contralateral LN +ve"],
["Stage 4: Distant mets (bone, BM, liver, LN, other) except 4S"],
["Stage 4S: Localised primary + spread limited to liver, skin, BM only (no bone); age < 1 yr — excellent prognosis"],
["<b>Risk Groups:</b> Low / Intermediate / High (based on INSS + MYCN + ploidy + histology + ALK)"],
["<b>MYCN amplification</b> → auto High Risk regardless of stage"],
]
wilms_stage = [
["<b>NEPHROBLASTOMA — NWTSG Staging (post-surgical)</b>"],
["Stage I (43%): Confined to kidney; complete excision"],
["Stage II (23%): Beyond renal capsule; complete excision; vessel infiltration or biopsy pre-resection"],
["Stage III (23%): +ve abdominopelvic LN; peritoneal invasion; residual tumour or unresectable"],
["Stage IV (10%): Haematogenous spread (lung, liver, bone, brain) or outside abdomen/pelvis"],
["Stage V (5%): Bilateral tumours at diagnosis — nephron-sparing surgery goal"],
["<b>SIOP (European):</b> Biopsy + neoadjuvant chemo first; then surgical staging"],
["<b>Anaplasia:</b> Focal (5%) — better outcome; Diffuse — very poor (4-yr OS: III=45%, IV=7%)"],
["<b>4-yr OS:</b> Stage I–III: 86–96% | Stage IV: 83% | Stage V: 70%"],
]
def stage_table(rows, color):
data = [[Paragraph(r[0], Small)] for r in rows]
t = Table(data, colWidths=[(W/2) - 0.3*cm])
row_styles = []
for i in range(len(data)):
bg = LTGRAY if i % 2 == 0 else WHITE
row_styles.append(("BACKGROUND", (0,i), (-1,i), bg))
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), color),
("ROWBACKGROUNDS",(0,1), (-1,-1), [LTGRAY, WHITE]),
("GRID", (0,0), (-1,-1), 0.3, MIDGRAY),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 6),
("RIGHTPADDING", (0,0), (-1,-1), 6),
("VALIGN", (0,0), (-1,-1), "TOP"),
]))
return t
# Override first row text color
nb_t = stage_table(nb_stage, TEAL)
wilms_t = stage_table(wilms_stage, PURPLE)
combo = Table([[nb_t, wilms_t]], colWidths=[W/2 - 0.1*cm, W/2 - 0.1*cm],
hAlign="LEFT")
combo.setStyle(TableStyle([
("VALIGN", (0,0), (-1,-1), "TOP"),
("LEFTPADDING", (0,0), (-1,-1), 0),
("RIGHTPADDING", (0,0), (-1,-1), 3),
]))
story.append(combo)
story.append(SP(10))
# ══════════════════════════════════════════════════════
# SECTION 7 - OTHER IMPORTANT ABDOMINAL MASSES
# ══════════════════════════════════════════════════════
story.append(section_banner("7. OTHER IMPORTANT PEDIATRIC ABDOMINAL MASSES", TEAL))
story.append(SP(4))
other_headers = ["Tumour / Mass", "Key Features", "Radiology Clues", "Biochemistry"]
other_rows = [
["Hepatoblastoma",
"Most common hepatic malignancy in children < 3 yrs. Associated with BWS, prematurity, FAP.",
"US/CT: hepatic mass, often heterogeneous, calcification in 50%. MRI best for resectability & vascular involvement.",
"AFP markedly elevated (> 1,000 ng/mL)"],
["Hepatic haemangioma\n(infantile)",
"Most common benign hepatic tumour in infants. Often multiple. Most regress by age 5.",
"US: hyperechoic nodules. Contrast CT/MRI: peripheral nodular enhancement with centripetal fill-in. No AFP rise.",
"Normal AFP; Kasabach-Merritt if large"],
["Ovarian cyst / tumour",
"Most common abdominal mass in newborn girls. Follicular cysts common. Germ cell tumours in adolescents.",
"US: anechoic (simple cyst) or complex (teratoma — fat + calcification on CT). MRI for complex masses.",
"AFP/βhCG elevated in germ cell tumours"],
["Sacrococcygeal teratoma",
"Most common neonatal tumour. 80% benign in neonates; risk of malignancy rises with age.",
"US/MRI: mixed cystic-solid mass at sacrococcygeal region extending into pelvis. CT: calcification.",
"AFP elevated if malignant"],
["Choledochal cyst",
"Congenital biliary dilatation. Presents with pain, jaundice, mass (Charcot's triad). Risk of cholangiocarcinoma.",
"US: cystic mass in RUQ closely related to bile duct. MRCP for delineation of biliary anatomy.",
"ALP/GGT elevated; bilirubin"],
["Mesenteric / duplication cyst",
"Benign; may be symptomatic from torsion/bleeding. Duplication cysts share bowel wall.",
"US: anechoic thin-walled cyst. Duplication cysts: thick muscular wall with 'gut signature' on US.",
"Normal"],
["Lymphoma (NHL / Hodgkin)",
"Most common abdominal malignancy in > 5 yrs. Burkitt NHL common in ileocaecal region.",
"US/CT: bulky homogeneous hypoechoic retroperitoneal/mesenteric lymphadenopathy. Splenomegaly. FDG-PET/CT for staging.",
"LDH ↑, uric acid ↑; β2-microglobulin ↑"],
["Rhabdomyosarcoma",
"Most common soft tissue sarcoma in children. Abdominopelvic sites: bladder, prostate, vagina, retroperitoneum.",
"US/CT: heterogeneous soft tissue mass. Bladder RMS: irregular filling defect. MRI for pelvic extension.",
"Non-specific; urinalysis if bladder involved"],
["Multicystic dysplastic\nkidney (MCDK)",
"Benign non-functioning kidney replaced by non-communicating cysts. Usually unilateral. Contralateral UPJO in 15%.",
"US: multiple non-communicating cysts of varying sizes with no central large cyst. No reniform shape.",
"MAG3 confirms non-function"],
["Hydronephrosis",
"Most common cause of neonatal abdominal mass. UPJO (ureteropelvic junction obstruction) most common cause.",
"US: dilated pelvicalyceal system. MAG3 renogram for function and drainage. MRI urography for anatomy.",
"Creatinine if bilateral"],
]
cw7 = [3.0*cm, (W-3.0*cm)*0.34, (W-3.0*cm)*0.40, (W-3.0*cm)*0.26]
story.append(colored_table(other_headers, other_rows, col_widths=cw7, hdr_color=GREEN))
story.append(SP(10))
# ══════════════════════════════════════════════════════
# SECTION 8 - RED FLAG BOX
# ══════════════════════════════════════════════════════
story.append(section_banner("8. RED FLAGS — URGENT ASSESSMENT REQUIRED", RED))
story.append(SP(4))
red_lines = [
"• Abdominal mass + lower limb weakness / bladder/bowel dysfunction → MRI SPINE STAT (NB dumb-bell / lymphoma cord compression)",
"• Hypertensive crisis + abdominal mass → Neuroblastoma / Phaeochromocytoma (urine catecholamines urgently)",
"• Rapidly expanding mass + fever + anaemia → Wilms tumour (avoid palpation — risk of rupture)",
"• Bilateral renal masses → Wilms Stage V — nephron-sparing imperative; consult paediatric oncology",
"• IVC tumour thrombus extending to right atrium (Wilms) → cardiac surgery team involvement pre-op",
"• Opsoclonus-myoclonus (dancing eyes) in an infant → Search for occult neuroblastoma (mIBG + urine catecholamines)",
"• Neonatal abdominal mass crossing the midline → Most likely neuroblastoma; urgent US + catecholamines",
"• Wilms tumour rupture (peritoneal seeding) → Upstages to Stage III; bilateral coverage chemotherapy required",
]
red_data = [[Paragraph(l, Body)] for l in red_lines]
red_t = Table(red_data, colWidths=[W])
red_t.setStyle(TableStyle([
("ROWBACKGROUNDS", (0,0), (-1,-1), [colors.HexColor("#fff5f5"), WHITE]),
("GRID", (0,0), (-1,-1), 0.3, colors.HexColor("#f5c6c6")),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING", (0,0), (-1,-1), 8),
("VALIGN", (0,0), (-1,-1), "TOP"),
]))
story.append(red_t)
story.append(SP(10))
# ══════════════════════════════════════════════════════
# SECTION 9 - MNEMONICS
# ══════════════════════════════════════════════════════
story.append(section_banner("9. MNEMONICS & MEMORY AIDS", TEAL))
story.append(SP(4))
mnem_data = [
[Paragraph("<b>Neuroblastoma — 'NEAT'</b>", SmallBold),
Paragraph("<b>Wilms — 'RAVEN'</b>", SmallBold),
Paragraph("<b>Hepatoblastoma — 'HALF'</b>", SmallBold)],
[Paragraph("<b>N</b>eural crest origin\n<b>E</b>levated VMA/HVA\n<b>A</b>drenal / paraaortic\n<b>T</b>umour calcification 85%", Small),
Paragraph("<b>R</b>enal origin\n<b>A</b>FP normal\n<b>V</b>ein (IVC) thrombus\n<b>E</b>xcellent prognosis\n<b>N</b>o calcification usually", Small),
Paragraph("<b>H</b>epatic location\n<b>A</b>FP very high\n<b>L</b>iver mass < 3 yrs\n<b>F</b>AP / BWS association", Small)],
]
mnem_t = Table(mnem_data, colWidths=[W/3, W/3, W/3])
mnem_t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), LTGRAY),
("ROWBACKGROUNDS",(0,1), (-1,-1), [WHITE]),
("GRID", (0,0), (-1,-1), 0.4, MIDGRAY),
("TOPPADDING", (0,0), (-1,-1), 6),
("BOTTOMPADDING", (0,0), (-1,-1), 6),
("LEFTPADDING", (0,0), (-1,-1), 8),
("VALIGN", (0,0), (-1,-1), "TOP"),
("ALIGN", (0,0), (-1,-1), "LEFT"),
]))
story.append(mnem_t)
story.append(SP(10))
# ══════════════════════════════════════════════════════
# SECTION 10 - SOURCES
# ══════════════════════════════════════════════════════
story.append(HRFlowable(width="100%", thickness=0.5, color=MIDGRAY))
story.append(SP(4))
story.append(Paragraph(
"<b>Sources:</b> Grainger & Allison's Diagnostic Radiology (Elsevier) · "
"Robbins & Kumar Basic Pathology (Elsevier) · "
"Campbell Walsh Wein Urology · "
"Robbins, Cotran & Kumar Pathologic Basis of Disease",
FooterS))
# ── Build ──────────────────────────────────────────────────────────────────────
doc.build(story, onFirstPage=on_first_page, onLaterPages=on_page)
print(f"PDF created: {OUTPUT}")