/handwritten notes on glioblastoma with textbook references and images pdf
glioblastoma treatment molecular markers
mkdir -p /home/daytona/workspace/glioblastoma-notes && ls /home/daytona/workspac…
~/glioblastoma-notes/generate_notes.py
#!/usr/bin/env python3
"""
Glioblastoma Handwritten-Style Notes PDF Generator
Uses reportlab to create a notebook-paper style PDF with handwritten font simulation.
"""
import requests
import os
from io import BytesIO
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import cm, mm
from reportlab.lib.colors import HexColor, black, white, Color
from reportlab.pdfgen import canvas
from reportlab.platypus import (SimpleDocTemplate, Paragraph, Spacer, Image,
Table, TableStyle, HRFlowable, KeepTogether,
PageBreak)
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_JUSTIFY
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from PIL import Image as PILImage
import urllib.request
import tempfile
OUTPUT_PATH = "/home/daytona/workspace/glioblastoma-notes/Glioblastoma_Handwritten_Notes.pdf"
# ── Colour palette (notebook feel) ──────────────────────────────────────────
PAGE_BG = HexColor("#FDFAF4") # warm off-white / aged paper
LINE_COLOR = HexColor("#C9D8E8") # faint blue ruled lines
MARGIN_LINE = HexColor("#FFB3B3") # pink margin line
TITLE_COLOR = HexColor("#1A3A5C") # dark navy
HEADING_COLOR= HexColor("#1B5E20") # dark green (section headings)
SUBHEAD_COLOR= HexColor("#4527A0") # indigo
BODY_COLOR = HexColor("#1C1C1C") # near-black ink
HIGHLIGHT = HexColor("#FFF59D") # yellow highlight
RED_INK = HexColor("#B71C1C") # red for important items
ORANGE_INK = HexColor("#E65100") # orange for warnings/notes
CAPTION_COLOR= HexColor("#37474F") # grey for captions
BOX_BLUE = HexColor("#E3F2FD") # light blue box fill
BOX_GREEN = HexColor("#E8F5E9") # light green box fill
BOX_YELLOW = HexColor("#FFFDE7") # yellow box fill
BOX_RED = HexColor("#FFEBEE") # red box fill
BOX_BORDER_B = HexColor("#1565C0")
BOX_BORDER_G = HexColor("#2E7D32")
BOX_BORDER_Y = HexColor("#F57F17")
BOX_BORDER_R = HexColor("#C62828")
W, H = A4 # 595.28 x 841.89 pts
# ── Helper: download image to temp file ─────────────────────────────────────
def download_image(url, suffix=".png"):
try:
resp = requests.get(url, timeout=15)
if resp.status_code == 200:
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)
tmp.write(resp.content)
tmp.close()
return tmp.name
except Exception as e:
print(f"Failed to download {url}: {e}")
return None
# ── Build PDF ────────────────────────────────────────────────────────────────
def draw_page_background(canvas_obj, doc):
"""Draw ruled-paper background on every page."""
canvas_obj.saveState()
canvas_obj.setFillColor(PAGE_BG)
canvas_obj.rect(0, 0, W, H, fill=1, stroke=0)
# Horizontal ruled lines (every 22 pt ≈ 7.8mm)
canvas_obj.setStrokeColor(LINE_COLOR)
canvas_obj.setLineWidth(0.5)
y = H - 2.5*cm
while y > 1.5*cm:
canvas_obj.line(1.8*cm, y, W - 1.8*cm, y)
y -= 22
# Vertical red margin line (left)
canvas_obj.setStrokeColor(MARGIN_LINE)
canvas_obj.setLineWidth(1.0)
canvas_obj.line(2.6*cm, H - 1.5*cm, 2.6*cm, 1.5*cm)
# Page number (bottom centre)
canvas_obj.setFont("Helvetica", 8)
canvas_obj.setFillColor(HexColor("#888888"))
canvas_obj.drawCentredString(W/2, 0.8*cm, f"Page {doc.page}")
canvas_obj.restoreState()
def build_styles():
styles = getSampleStyleSheet()
base = dict(fontName="Helvetica", leading=20, textColor=BODY_COLOR,
spaceAfter=4, spaceBefore=2)
title_style = ParagraphStyle(
"NoteTitle",
fontName="Helvetica-Bold",
fontSize=22,
leading=28,
textColor=TITLE_COLOR,
alignment=TA_CENTER,
spaceAfter=6,
)
subtitle_style = ParagraphStyle(
"Subtitle",
fontName="Helvetica-Oblique",
fontSize=11,
leading=16,
textColor=HexColor("#5C6BC0"),
alignment=TA_CENTER,
spaceAfter=12,
)
section_style = ParagraphStyle(
"Section",
fontName="Helvetica-Bold",
fontSize=14,
leading=20,
textColor=HEADING_COLOR,
spaceBefore=14,
spaceAfter=4,
borderPad=3,
)
subsection_style = ParagraphStyle(
"SubSection",
fontName="Helvetica-BoldOblique",
fontSize=11,
leading=16,
textColor=SUBHEAD_COLOR,
spaceBefore=10,
spaceAfter=3,
)
body_style = ParagraphStyle(
"Body",
fontName="Helvetica",
fontSize=10,
leading=20,
textColor=BODY_COLOR,
alignment=TA_JUSTIFY,
spaceAfter=4,
)
bullet_style = ParagraphStyle(
"Bullet",
fontName="Helvetica",
fontSize=10,
leading=18,
textColor=BODY_COLOR,
leftIndent=18,
bulletIndent=6,
spaceAfter=2,
)
important_style = ParagraphStyle(
"Important",
fontName="Helvetica-Bold",
fontSize=10,
leading=18,
textColor=RED_INK,
spaceAfter=4,
)
caption_style = ParagraphStyle(
"Caption",
fontName="Helvetica-Oblique",
fontSize=8.5,
leading=13,
textColor=CAPTION_COLOR,
alignment=TA_CENTER,
spaceAfter=8,
)
ref_style = ParagraphStyle(
"Ref",
fontName="Helvetica-Oblique",
fontSize=8,
leading=13,
textColor=HexColor("#546E7A"),
leftIndent=12,
spaceAfter=2,
)
orange_note = ParagraphStyle(
"OrangeNote",
fontName="Helvetica-BoldOblique",
fontSize=9.5,
leading=16,
textColor=ORANGE_INK,
leftIndent=10,
spaceAfter=4,
)
return dict(
title=title_style, subtitle=subtitle_style, section=section_style,
subsection=subsection_style, body=body_style, bullet=bullet_style,
important=important_style, caption=caption_style, ref=ref_style,
orange=orange_note,
)
def colored_box(content_para, bg_color, border_color, padding=8):
"""Wrap a paragraph in a coloured rounded box using a Table."""
t = Table([[content_para]], colWidths=[W - 5.8*cm])
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), bg_color),
("ROUNDEDCORNERS",(0,0), (-1,-1), [4,4,4,4]),
("BOX", (0,0), (-1,-1), 1.5, border_color),
("TOPPADDING", (0,0), (-1,-1), padding),
("BOTTOMPADDING", (0,0), (-1,-1), padding),
("LEFTPADDING", (0,0), (-1,-1), padding+2),
("RIGHTPADDING", (0,0), (-1,-1), padding),
]))
return t
def make_two_col_table(left_items, right_items, styles):
"""Two-column table for comparison / paired info."""
data = []
max_rows = max(len(left_items), len(right_items))
for i in range(max_rows):
l = Paragraph(left_items[i], styles["bullet"]) if i < len(left_items) else Paragraph("", styles["bullet"])
r = Paragraph(right_items[i], styles["bullet"]) if i < len(right_items) else Paragraph("", styles["bullet"])
data.append([l, r])
col_w = (W - 5.8*cm) / 2
t = Table(data, colWidths=[col_w, col_w])
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (0,-1), BOX_BLUE),
("BACKGROUND", (1,0), (1,-1), BOX_GREEN),
("BOX", (0,0), (-1,-1), 1, HexColor("#BDBDBD")),
("INNERGRID", (0,0), (-1,-1), 0.5, HexColor("#E0E0E0")),
("VALIGN", (0,0), (-1,-1), "TOP"),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING",(0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 6),
("RIGHTPADDING", (0,0), (-1,-1), 6),
]))
return t
def build_pdf():
doc = SimpleDocTemplate(
OUTPUT_PATH,
pagesize=A4,
leftMargin=2.8*cm,
rightMargin=1.8*cm,
topMargin=2.2*cm,
bottomMargin=2.0*cm,
title="Glioblastoma - Handwritten Study Notes",
author="Orris Medical Notes",
subject="Glioblastoma Multiforme | Neuro-Oncology",
)
s = build_styles()
story = []
# ── Download images ───────────────────────────────────────────────────
img_urls = {
"mri1": "https://cdn.orris.care/cdss_images/f32c7bddd283a2a5351e36cdca0a93d848aac49dc166501ff368deb5d305226d.png",
"gross_histo": "https://cdn.orris.care/cdss_images/5833f33aa53e57e77a4a03ddd2e82641b148aa2a21088a4ad06a42800b7edea0.png",
"histo_vasc": "https://cdn.orris.care/cdss_images/88f88f1d8dfced1529a81a257fdcf1be99acac6e8345529688f0b3bdc80df729.png",
"mri2": "https://cdn.orris.care/cdss_images/b758702ae37cee473ca40abe31d7d5508e7459ea98f4f732d7ddb7ee72a8e620.png",
}
imgs = {}
for key, url in img_urls.items():
path = download_image(url)
if path:
imgs[key] = path
print(f" Downloaded {key} -> {path}")
else:
print(f" FAILED: {key}")
# ══════════════════════════════════════════════════════════════════════
# PAGE 1 - TITLE + OVERVIEW
# ══════════════════════════════════════════════════════════════════════
story.append(Spacer(1, 0.4*cm))
story.append(Paragraph("Glioblastoma (GBM)", s["title"]))
story.append(Paragraph("IDH-Wildtype | WHO CNS Grade 4 | Neuro-Oncology Notes", s["subtitle"]))
story.append(HRFlowable(width="100%", thickness=2, color=TITLE_COLOR, spaceAfter=10))
# Quick-ref box
qr = Paragraph(
"<b>Quick Reference:</b> Most common malignant primary brain tumor in adults. "
"Peak age 60 yrs. M:F ratio ~1.6:1. Median survival ~15 months with treatment. "
"IDH-wildtype by definition. WHO CNS Grade 4.",
s["body"]
)
story.append(colored_box(qr, BOX_BLUE, BOX_BORDER_B, padding=10))
story.append(Spacer(1, 0.3*cm))
# ── 1. DEFINITION & EPIDEMIOLOGY ──
story.append(Paragraph("1. Definition & Epidemiology", s["section"]))
story.append(Paragraph(
"Glioblastoma (GBM) is an <b>aggressive diffuse astrocytic glioma</b>, always assigned "
"<b>WHO CNS Grade 4</b>. It is the most common malignant primary brain tumor in adults, "
"accounting for about <b>50% of all primary malignant brain tumors</b> and roughly "
"<b>14% of all primary CNS tumors</b>.",
s["body"]
))
epi_bullets = [
"Peak incidence: <b>55–65 years</b> (mean ~60 yrs for GBM; ~46 yrs for anaplastic astrocytoma)",
"Male predominance: <b>M:F ~1.6:1</b>",
"~80% of infiltrating gliomas in adults are GBM",
"Almost all arise <b>sporadically</b>; familial predilection is rare",
"De novo (primary) GBM: >90% — arise without lower-grade precursor",
"Secondary GBM: ~10% — evolve from IDH-mutant lower-grade astrocytoma",
"Location: deep white matter, basal ganglia, thalamus; rarely cerebellum/spinal cord",
]
for b in epi_bullets:
story.append(Paragraph(f"• {b}", s["bullet"]))
story.append(Paragraph(
"<b>★ NOTE:</b> Secondary GBM tends to have <b>MGMT methylation</b> and better prognosis "
"than primary GBM.",
s["orange"]
))
# ── 2. PATHOGENESIS & MOLECULAR MARKERS ──
story.append(Paragraph("2. Pathogenesis & Molecular Markers", s["section"]))
story.append(Paragraph(
"The 2021 WHO classification incorporates <b>molecular markers</b> alongside histology. "
"IDH-wildtype GBM is defined by <b>IDH-wildtype status</b> PLUS at least one of three "
"key genetic alterations:",
s["body"]
))
mol_data = [
[Paragraph("<b>KEY GENETIC ALTERATIONS IN GBM (IDH-wildtype)</b>", s["important"])],
[Paragraph("1. <b>TERT promoter mutation</b> — evasion of senescence", s["bullet"])],
[Paragraph("2. <b>EGFR gene amplification</b> — growth factor pathway activation", s["bullet"])],
[Paragraph("3. <b>+7 / −10 chromosome copy-number changes</b> — most common genomic alteration", s["bullet"])],
[Paragraph("4. <b>Homozygous CDKN2A deletion</b> (p16) — escape from normal growth controls", s["bullet"])],
[Paragraph("5. <b>TP53 mutation</b> — resistance to apoptosis", s["bullet"])],
[Paragraph("6. <b>PDGFR amplification</b> — alternative growth factor signalling", s["bullet"])],
[Paragraph("7. <b>MGMT promoter methylation</b> — DNA repair gene silenced; predicts temozolomide response", s["bullet"])],
]
mol_table = Table(mol_data, colWidths=[W - 5.8*cm])
mol_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), HexColor("#FFCDD2")),
("BACKGROUND", (0,1), (-1,-1), BOX_YELLOW),
("BOX", (0,0), (-1,-1), 1.5, BOX_BORDER_R),
("INNERGRID", (0,0), (-1,-1), 0.4, HexColor("#FFCCBC")),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 8),
]))
story.append(mol_table)
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("<u>IDH Status - Why it matters:</u>", s["subsection"]))
idh_items = [
"IDH-wildtype (~80%) - de novo, older pts, <b>worse prognosis</b>",
"IDH-mutant (~20%) - secondary, younger pts (median 38 yrs), <b>better prognosis</b>",
]
idh_right = [
"Wild-type: gains chr 7 + loss chr 10 (classic pattern)",
"Mutant: IDH1 R132H most common; produces oncometabolite 2-HG",
]
story.append(make_two_col_table(idh_items, idh_right, s))
story.append(Spacer(1, 0.2*cm))
# ── 3. GROSS & MICROSCOPIC PATHOLOGY ──
story.append(Paragraph("3. Gross & Microscopic Pathology", s["section"]))
story.append(Paragraph(
"GBM has a <b>variegated appearance</b> — reflecting its internal heterogeneity. "
"Gross sections show mottled grey, red, orange, or brown regions depending on degree of "
"necrosis and haemorrhage. On cut surface the tumor may appear deceptively "
"circumscribed but microscopically infiltrates widely.",
s["body"]
))
# Gross/histo image
if "gross_histo" in imgs:
try:
pil = PILImage.open(imgs["gross_histo"])
iw, ih = pil.size
max_w = W - 5.8*cm
max_h = 7.5*cm
scale = min(max_w/iw, max_h/ih)
story.append(Image(imgs["gross_histo"], width=iw*scale, height=ih*scale))
story.append(Paragraph(
"Fig 1A: Gross section showing necrotic, haemorrhagic GBM mass (Robbins & Kumar Basic Pathology). "
"Fig 1B: Histology showing palisading necrosis with microvascular proliferation (inset). "
"Serpiginous bands of necrosis with tumour cells palisading around anucleate zones are characteristic.",
s["caption"]
))
except Exception as e:
print(f"Image error gross_histo: {e}")
story.append(Paragraph("<u>Histological Features (WHO Grade 4 criteria):</u>", s["subsection"]))
histo_bullets = [
"<b>High cellularity</b> with poorly differentiated pleomorphic cells",
"<b>Nuclear atypia</b> and marked pleomorphism",
"<b>Brisk mitotic activity</b>",
"<b>Palisading necrosis</b>: serpiginous necrotic foci with tumour nuclei lining the borders",
"<b>Microvascular proliferation</b>: glomeruloid multilayered vessels (endothelial hyperplasia)",
"Histological variants: giant-cell GBM, small-cell GBM, gliosarcoma",
"TERT/EGFR/+7−10 alone = Grade 4 even without necrosis or MVP",
]
for b in histo_bullets:
story.append(Paragraph(f"• {b}", s["bullet"]))
# Histology vascular image
if "histo_vasc" in imgs:
try:
pil = PILImage.open(imgs["histo_vasc"])
iw, ih = pil.size
max_w = (W - 5.8*cm) * 0.6
max_h = 5.5*cm
scale = min(max_w/iw, max_h/ih)
story.append(Image(imgs["histo_vasc"], width=iw*scale, height=ih*scale))
story.append(Paragraph(
"Fig 2: Endothelial hyperplasia - glomeruloid multilayered vessel in GBM (H&E ×400). "
"Source: Bradley and Daroff's Neurology in Clinical Practice.",
s["caption"]
))
except Exception as e:
print(f"Image error histo_vasc: {e}")
# ══════════════════════════════════════════════════════════════════════
# PAGE 2 - IMAGING + CLINICAL FEATURES + TREATMENT
# ══════════════════════════════════════════════════════════════════════
story.append(PageBreak())
# ── 4. NEUROIMAGING ──
story.append(Paragraph("4. Neuroimaging", s["section"]))
story.append(Paragraph(
"MRI with gadolinium is the modality of choice. GBM produces a <b>characteristic "
"ring-enhancing lesion</b>:",
s["body"]
))
mri_bullets = [
"<b>T1+Gd:</b> Irregular rim of avid contrast enhancement",
"<b>Central hypointensity:</b> necrotic core (non-enhancing)",
"<b>Surrounding T2/FLAIR hyperintensity:</b> vasogenic oedema infiltrated by tumour cells",
"<b>ADC:</b> reduced diffusion in solid tumour components (not specific)",
"Nodular satellite lesions adjacent to main mass may be present",
"70% show restricted diffusion in post-op period (hypercellular tumour / ischaemia)",
"<b>Butterfly glioma:</b> bilateral spread across corpus callosum in ~3–6% (mimics metastases)",
]
for b in mri_bullets:
story.append(Paragraph(f"• {b}", s["bullet"]))
story.append(Spacer(1, 0.2*cm))
# MRI images - side by side
mri_row = []
captions = []
for key, cap in [
("mri1", "Fig 3: Contrast-enhanced T1 MRI - large ring-enhancing GBM with internal necrosis, left hemisphere. (Adams & Victor's Neurology, 12th Ed.)"),
("mri2", "Fig 4: Axial post-contrast T1 MRI - rim-enhancing right frontal GBM mass. (Robbins & Kumar Basic Pathology)")
]:
if key in imgs:
try:
pil = PILImage.open(imgs[key])
iw, ih = pil.size
max_w = (W - 6.5*cm) / 2
max_h = 7*cm
scale = min(max_w/iw, max_h/ih)
mri_row.append(Image(imgs[key], width=iw*scale, height=ih*scale))
captions.append(cap)
except Exception as e:
print(f"Image error {key}: {e}")
if len(mri_row) == 2:
img_table = Table([mri_row], colWidths=[(W-5.8*cm)/2, (W-5.8*cm)/2])
img_table.setStyle(TableStyle([
("ALIGN", (0,0), (-1,-1), "CENTER"),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
]))
story.append(img_table)
for cap in captions:
story.append(Paragraph(cap, s["caption"]))
elif len(mri_row) == 1:
story.append(mri_row[0])
story.append(Paragraph(captions[0], s["caption"]))
story.append(Paragraph(
"<b>★ Key Imaging Tip:</b> Early post-op MRI should be performed within 48 hrs "
"before linear enhancement of surgical margins develops (Grainger & Allison's Diagnostic Radiology).",
s["orange"]
))
# ── 5. CLINICAL FEATURES ──
story.append(Paragraph("5. Clinical Features", s["section"]))
cf_left = [
"<b>Symptoms at presentation:</b>",
"• Seizures (first presentation ~25%)",
"• Headache (raised ICP pattern)",
"• Focal neurological deficits",
"• Neurocognitive impairment",
"• Nausea, vomiting (ICP)",
"• Personality/behaviour change",
]
cf_right = [
"<b>Signs / Examination:</b>",
"• Papilloedema (raised ICP)",
"• Hemiparesis / hemisensory loss",
"• Aphasia (dominant hemisphere)",
"• Visual field defect",
"• CSF: may show ↑ protein (>100 mg/dL), pleocytosis",
"• Corpus callosum spread → bilateral deficits",
]
story.append(make_two_col_table(cf_left, cf_right, s))
story.append(Spacer(1, 0.2*cm))
natural_hx = Paragraph(
"<b>⚠ Natural History (untreated):</b> <20% survive 1 year; <10% survive 2 years. "
"Cerebral oedema and raised ICP are the usual causes of death. "
"The clinical course is typically rapid — weeks to a few months.",
s["body"]
)
story.append(colored_box(natural_hx, BOX_RED, BOX_BORDER_R, padding=10))
story.append(Spacer(1, 0.3*cm))
# ── 6. STAGING / CLASSIFICATION ──
story.append(Paragraph("6. WHO Classification (2021 CNS5)", s["section"]))
class_data = [
[
Paragraph("<b>Tumour Type</b>", s["important"]),
Paragraph("<b>IDH</b>", s["important"]),
Paragraph("<b>Other Markers</b>", s["important"]),
Paragraph("<b>Grade</b>", s["important"]),
Paragraph("<b>Prognosis</b>", s["important"]),
],
[
Paragraph("Glioblastoma", s["bullet"]),
Paragraph("Wildtype", s["bullet"]),
Paragraph("TERT mut / EGFR amp / +7−10", s["bullet"]),
Paragraph("CNS 4", s["bullet"]),
Paragraph("~15 mo", s["bullet"]),
],
[
Paragraph("Astrocytoma", s["bullet"]),
Paragraph("Mutant", s["bullet"]),
Paragraph("TP53, ATRX mutations", s["bullet"]),
Paragraph("CNS 2–4", s["bullet"]),
Paragraph("Better (3–5+ yrs)", s["bullet"]),
],
[
Paragraph("Oligodendroglioma", s["bullet"]),
Paragraph("Mutant", s["bullet"]),
Paragraph("1p/19q codeletion", s["bullet"]),
Paragraph("CNS 2–3", s["bullet"]),
Paragraph("Best", s["bullet"]),
],
]
class_table = Table(class_data, colWidths=[
3.5*cm, 2.2*cm, 5.5*cm, 1.8*cm, 2.3*cm
])
class_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), HexColor("#1A3A5C")),
("TEXTCOLOR", (0,0), (-1,0), white),
("BACKGROUND", (0,1), (-1,1), HexColor("#FFEBEE")),
("BACKGROUND", (0,2), (-1,2), BOX_GREEN),
("BACKGROUND", (0,3), (-1,3), BOX_BLUE),
("BOX", (0,0), (-1,-1), 1.5, HexColor("#455A64")),
("INNERGRID", (0,0), (-1,-1), 0.5, HexColor("#B0BEC5")),
("ALIGN", (3,0), (4,-1), "CENTER"),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 6),
]))
story.append(class_table)
# ══════════════════════════════════════════════════════════════════════
# PAGE 3 - TREATMENT + PROGNOSIS
# ══════════════════════════════════════════════════════════════════════
story.append(PageBreak())
story.append(Paragraph("7. Treatment", s["section"]))
story.append(Paragraph(
"The <b>Stupp Protocol</b> (2005) remains the backbone of GBM treatment. "
"The standard of care is a trimodal approach:",
s["body"]
))
# Treatment flow table
tx_data = [
[
Paragraph("<b>Step 1: Surgery</b>", s["subsection"]),
Paragraph("<b>Step 2: Radiotherapy</b>", s["subsection"]),
Paragraph("<b>Step 3: Chemotherapy</b>", s["subsection"]),
],
[
Paragraph(
"• Maximum safe surgical resection (craniotomy)\n"
"• Stereotactic biopsy if not resectable\n"
"• Aim: maximal tumour debulking\n"
"• Early post-op MRI within 48 hrs",
s["bullet"]
),
Paragraph(
"• Standard dose: <b>60 Gy / 30 fractions</b>\n"
"• Hypofractionated RT preferred in age >70 yrs\n"
"• Concurrent with temozolomide (75 mg/m² daily)\n"
"• External beam RT to tumour + margins",
s["bullet"]
),
Paragraph(
"• <b>Temozolomide</b> 150–200 mg/m² days 1–5\n"
"• Every 28-day cycle × 6 cycles (adjuvant)\n"
"• MGMT-methylated tumours benefit most\n"
"• Improves 2-yr survival from 10.4% → 26%",
s["bullet"]
),
],
]
tx_table = Table(tx_data, colWidths=[(W-5.8*cm)/3]*3)
tx_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), HexColor("#263238")),
("TEXTCOLOR", (0,0), (-1,0), white),
("BACKGROUND", (0,1), (0,1), HexColor("#E8EAF6")),
("BACKGROUND", (1,1), (1,1), HexColor("#E0F7FA")),
("BACKGROUND", (2,1), (2,1), HexColor("#F3E5F5")),
("BOX", (0,0), (-1,-1), 1.5, HexColor("#455A64")),
("INNERGRID", (0,0), (-1,-1), 0.5, HexColor("#B0BEC5")),
("VALIGN", (0,0), (-1,-1), "TOP"),
("TOPPADDING", (0,0), (-1,-1), 7),
("BOTTOMPADDING", (0,0), (-1,-1), 7),
("LEFTPADDING", (0,0), (-1,-1), 7),
]))
story.append(tx_table)
story.append(Spacer(1, 0.3*cm))
# MGMT box
mgmt_txt = Paragraph(
"<b>MGMT Promoter Methylation:</b> O⁶-methylguanine-DNA methyltransferase (MGMT) is a DNA "
"repair enzyme. Methylation of its promoter silences the gene → reduced DNA repair → "
"<b>increased sensitivity to temozolomide alkylation</b>. Present in ~45% of GBMs. "
"More common in secondary GBM. Key predictive AND prognostic biomarker.",
s["body"]
)
story.append(colored_box(mgmt_txt, BOX_GREEN, BOX_BORDER_G, padding=10))
story.append(Spacer(1, 0.2*cm))
# Tumour treating fields
story.append(Paragraph("<u>Tumour Treating Fields (TTFields / Optune®):</u>", s["subsection"]))
story.append(Paragraph(
"Low-intensity (200 kHz), alternating electric fields delivered via scalp electrodes. "
"When added to maintenance temozolomide in newly diagnosed GBM, TTFields improve "
"<b>progression-free survival and overall survival</b> vs. temozolomide alone. "
"Applied as wearable device (portable). Approved for both newly diagnosed and "
"recurrent GBM.",
s["body"]
))
# Bevacizumab note
story.append(Paragraph("<u>Bevacizumab (anti-VEGF):</u>", s["subsection"]))
bev_para = Paragraph(
"<b>★ Anti-VEGF monoclonal antibody.</b> Effective at controlling cerebral oedema. "
"However, does NOT prolong overall survival in newly diagnosed GBM. "
"<b>Not recommended for routine first-line use.</b> May be used for recurrent GBM or "
"refractory oedema.",
s["important"]
)
story.append(colored_box(bev_para, BOX_RED, BOX_BORDER_R, padding=8))
story.append(Spacer(1, 0.2*cm))
# Recurrent GBM
story.append(Paragraph("8. Recurrent GBM", s["section"]))
story.append(Paragraph(
"<b>Almost all GBMs recur</b> following initial therapy. Options include:",
s["body"]
))
recur_bullets = [
"Re-operation (if surgically accessible)",
"Stereotactic radiosurgery (SRS)",
"TTFields continuation",
"Temozolomide rechallenge",
"Bevacizumab monotherapy",
"Lomustine (CCNU) ± procarbazine + vincristine (PCV) — under expert supervision",
"Second-course RT ± concurrent bevacizumab",
"<b>Clinical trial enrolment strongly encouraged</b> — no proven survival benefit from any single agent",
]
for b in recur_bullets:
story.append(Paragraph(f"• {b}", s["bullet"]))
# ── 9. PROGNOSIS ──
story.append(Paragraph("9. Prognosis & Key Prognostic Factors", s["section"]))
prog_data = [
[Paragraph("<b>Survival Metric</b>", s["important"]), Paragraph("<b>Figure</b>", s["important"])],
[Paragraph("Median overall survival (standard Rx)", s["bullet"]), Paragraph("~15 months", s["bullet"])],
[Paragraph("2-year survival (with temozolomide)", s["bullet"]), Paragraph("~26%", s["bullet"])],
[Paragraph("2-year survival (RT alone)", s["bullet"]), Paragraph("~10.4%", s["bullet"])],
[Paragraph("Median survival, primary GBM (Grainger)", s["bullet"]), Paragraph("12–14 months", s["bullet"])],
[Paragraph("Survival if age >60 yrs at 18 months", s["bullet"]), Paragraph("<10%", s["bullet"])],
[Paragraph("Survival if age <40 yrs at 18 months", s["bullet"]), Paragraph("~66%", s["bullet"])],
[Paragraph("Anaplastic astrocytoma (IDH-mut)", s["bullet"]), Paragraph("3–5 years", s["bullet"])],
]
prog_table = Table(prog_data, colWidths=[10*cm, 5.3*cm])
prog_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), HexColor("#4527A0")),
("TEXTCOLOR", (0,0), (-1,0), white),
("ROWBACKGROUNDS",(0,1), (-1,-1), [white, HexColor("#EDE7F6")]),
("BOX", (0,0), (-1,-1), 1.5, HexColor("#7B1FA2")),
("INNERGRID", (0,0), (-1,-1), 0.5, HexColor("#CE93D8")),
("ALIGN", (1,0), (1,-1), "CENTER"),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 8),
]))
story.append(prog_table)
story.append(Spacer(1, 0.2*cm))
prognosis_box = Paragraph(
"<b>Key Prognostic Factors:</b> (1) Patient age (2) ECOG performance status "
"(3) IDH mutation status (4) MGMT promoter methylation (5) Extent of surgical resection "
"(6) Tumour location (7) Multicentricity",
s["body"]
)
story.append(colored_box(prognosis_box, BOX_YELLOW, BOX_BORDER_Y, padding=10))
# ══════════════════════════════════════════════════════════════════════
# PAGE 4 - DIFFERENTIAL DIAGNOSIS + RECENT EVIDENCE + REFERENCES
# ══════════════════════════════════════════════════════════════════════
story.append(PageBreak())
# ── 10. DIFFERENTIAL DIAGNOSIS ──
story.append(Paragraph("10. Differential Diagnosis", s["section"]))
story.append(Paragraph(
"Any ring-enhancing brain lesion on MRI should be considered in the DDx:",
s["body"]
))
dd_left = [
"<b>Neoplastic:</b>",
"• Brain metastases (lung, breast, melanoma, renal)",
"• Primary CNS lymphoma (PCNSL)",
"• Anaplastic astrocytoma (IDH-mutant)",
"• Oligodendroglioma",
"• Ependymoma",
"• Gliosarcoma (GBM variant)",
]
dd_right = [
"<b>Non-Neoplastic:</b>",
"• Brain abscess (bacterial/fungal)",
"• Cerebral toxoplasmosis (immunocompromised)",
"• Tumefactive MS",
"• Radiation necrosis (post-Rx)",
"• Subacute cerebral infarct",
"• Neurocysticercosis",
]
story.append(make_two_col_table(dd_left, dd_right, s))
story.append(Spacer(1, 0.2*cm))
story.append(Paragraph(
"<b>★ Key Differentiator:</b> Brain metastases are the most common intracranial malignancy "
"overall. Multiple ring-enhancing lesions favour metastases. GBM can simulate metastases "
"when multicentric (3–6% of cases). MR spectroscopy, perfusion MRI, and tissue biopsy "
"help distinguish.",
s["orange"]
))
# ── 11. BUTTERFLY GLIOMA ──
story.append(Paragraph("11. Special Patterns", s["section"]))
story.append(Paragraph(
"<b>Butterfly Glioma:</b> Bilateral symmetrical lesion resulting from rapid infiltration "
"of the corpus callosum with spread to the contralateral hemisphere. "
"Pathognomonic appearance on MRI. Usually indicates very advanced / unresectable disease.",
s["body"]
))
story.append(Paragraph(
"<b>Multifocal GBM:</b> 3–6% of cases show multicentric foci of growth, simulating "
"metastatic cancer. Whether this represents true multicentricity or CSF pathway spread "
"is debated.",
s["body"]
))
# ── 12. RECENT EVIDENCE ──
story.append(Paragraph("12. Recent Evidence (2023–2025)", s["section"]))
recent_bullets = [
"<b>Survival prediction (ML/DL):</b> Systematic review (Poursaeed et al., BMC Cancer 2024, PMID 39731064) "
"shows machine learning models show promise for personalised survival prediction in GBM.",
"<b>Invasion patterns:</b> Systematic review (Percuoco et al., Neurosurg Rev 2024, PMID 39570467) "
"characterises GBM invasion corridors — relevant for surgical planning and margins.",
"<b>Recurrent GBM prognosis:</b> Systematic review (Aman et al., Folia Med 2025, PMID 40524591) "
"identifies age, KPS, and MGMT methylation as key prognostic factors at recurrence.",
"<b>Chemoresistance:</b> Meta-analysis (Bajwa et al., J Neurooncol 2025, PMID 41128938) "
"identifies miRNA signatures driving temozolomide resistance — potential therapeutic targets.",
"<b>MTAP deletion:</b> Systematic review (Clouser et al., Cancer Treat Res Commun 2025, PMID 40729867) "
"shows MTAP deletion is present in a subset of GBMs and may be a future therapeutic target.",
]
for b in recent_bullets:
story.append(Paragraph(f"• {b}", s["bullet"]))
# ── 13. TEXTBOOK REFERENCES ──
story.append(Paragraph("13. Textbook References", s["section"]))
refs = [
"Adams & Victor's Principles of Neurology, 12th Edition — Ropper AH, Samuels MA, Klein JP, Prasad S. "
"McGraw-Hill. Chapter 30: Intracranial Neoplasms and Paraneoplastic Disorders.",
"Robbins & Kumar Basic Pathology — Kumar V, Abbas AK, Aster JC. 10th Edition, Elsevier. "
"Chapter 21: Central Nervous System. pp. 858–860.",
"Robbins, Cotran & Kumar Pathologic Basis of Disease — Kumar V, Abbas AK, Aster JC. 10th Ed., Elsevier. "
"Chapter 28: Central Nervous System. pp. 1278–1283.",
"Bradley and Daroff's Neurology in Clinical Practice — Daroff RB, Jankovic J, Mazziotta JC, Pomeroy SL. "
"8th Edition, Elsevier. Chapter 72: Neoplasms Affecting the Nervous System.",
"Grainger & Allison's Diagnostic Radiology — 7th Edition. Chapter 55: Neuro-Oncology Imaging. "
"Glioblastoma: pp. 2710–2717.",
"Goldman-Cecil Medicine International Edition — Goldman L, Schafer AI. 27th Edition. "
"Chapter on Brain Tumours. Vol. 2.",
"Quick Compendium of Clinical Pathology — Layfield LJ. 5th Edition. "
"Section 7.4.9.1.3: IDH-wildtype Glioblastoma.",
"Henry's Clinical Diagnosis and Management by Laboratory Methods — McPherson RA, Pincus MR. "
"24th Edition. Glioblastoma Multiforme section.",
]
for r in refs:
story.append(Paragraph(f"[{refs.index(r)+1}] {r}", s["ref"]))
story.append(Spacer(1, 0.5*cm))
story.append(HRFlowable(width="100%", thickness=1.5, color=TITLE_COLOR, spaceAfter=6))
story.append(Paragraph(
"Notes compiled by Orris AI | Sources: Medical Textbook Library + PubMed | Date: July 2026",
ParagraphStyle("Footer", fontName="Helvetica-Oblique", fontSize=8, textColor=HexColor("#9E9E9E"),
alignment=TA_CENTER)
))
# ── BUILD ─────────────────────────────────────────────────────────────
doc.build(story, onFirstPage=draw_page_background, onLaterPages=draw_page_background)
print(f"\n✓ PDF generated: {OUTPUT_PATH}")
return OUTPUT_PATH
if __name__ == "__main__":
build_pdf()
cd /home/daytona/workspace/glioblastoma-notes && python3 generate_notes.py 2>&1
ls -lh /home/daytona/workspace/glioblastoma-notes/Glioblastoma_Handwritten_Notes…
Glioblastoma Handwritten Notes
PDF Document · PDF