How to read brain CT and MRI and differentiate between bleed and infarct along with the timeline of events, acute, chronic, subacute
| Structure | HU |
|---|---|
| Air | -1000 |
| Fat | -100 |
| Water/CSF | 0 |
| Normal gray matter | ~35-40 |
| Normal white matter | ~25-30 |
| Acute blood | ~50-90 |
| Bone | ~700-1000 |
"Fresh intracranial hemorrhage coagulates nearly immediately and therefore shows up on CT scans as hyperdense areas relative to brain. Fresh hemorrhage may appear about as white as bone."
- Neuroanatomy through Clinical Cases, 3rd Ed.
"The area of ischemic injury on brain CT scan appears as a relative hypodensity, in contrast to brain hemorrhage, which appears hyperdense compared with the surrounding brain."
- Goldman-Cecil Medicine
| Phase | Time | CT Appearance |
|---|---|---|
| Hyperacute | 0-6 hrs | Hyperdense (white, ~60-80 HU) |
| Acute | 1-3 days | Hyperdense, may develop hypodense ring (edema) |
| Subacute | 3-14 days | Becomes isodense (similar to brain, ~35 HU) - can be missed! |
| Chronic | >14 days | Hypodense (darker than brain), may calcify; subdurals form crescent-shaped dark collections |
"Brain swelling on CT without accompanying low density does not always progress to infarction. Such cases may be due to abnormal perfusion, but a compensatory increase in CBV rather than a reduction."
- Grainger & Allison's Diagnostic Radiology
| Phase | Time | CT Appearance |
|---|---|---|
| Hyperacute | 0-6 hrs | Normal OR very subtle hypodensity, sulcal effacement |
| Acute | 6-24 hrs | Definite hypodensity in affected territory, gray-white blurring |
| Subacute | 1-7 days | Progressively more hypodense, mass effect peaks at 3-5 days |
| Fogging effect | ~1-3 wks | Temporarily appears isodense (macrophage infiltration, petechial hemorrhage) - can be falsely normal |
| Chronic | >3 wks | Well-defined hypodense area, ex-vacuo ventricular dilation (encephalomalacia) |
| Sequence | Best For | Bright | Dark |
|---|---|---|---|
| T1 | Anatomy, subacute blood | Subacute blood, fat, methemoglobin | CSF, edema, deoxyhemoglobin |
| T2 | Edema, infarcts, inflammation | CSF, edema, late subacute blood | Acute blood (deoxyhemoglobin), calcium |
| FLAIR | Periventricular lesions, SAH, subacute infarct | Edema, gliosis, SAH | CSF (suppressed) |
| DWI | Acute infarction (gold standard) | Acute ischemia (minutes to ~7 days) | Blood after several days |
| ADC map | Confirm true restriction vs. T2 shine-through | - | True infarct (dark = restricted diffusion) |
| SWI/GRE | Microhemorrhages, venous blood | - | All blood products, calcification (blooms dark) |
"DWI has a preeminent role in acute stroke imaging due to its extremely high sensitivity and specificity, with parenchymal hyperintensity as early as 5 minutes following the onset of infarction."
- Grainger & Allison's Diagnostic Radiology


| Stage | Timeframe | Hemoglobin Form | T1 | T2 | Mechanism |
|---|---|---|---|---|---|
| Hyperacute | 0-6 hours | Intracellular oxyhemoglobin | Gray (isointense) | Light gray (slightly bright) | Oxyhemoglobin has no unpaired electrons - no T1 shortening |
| Acute | 1-3 days | Intracellular deoxyhemoglobin | Gray (isointense) | Dark gray (hypointense) | Deoxyhemoglobin has unpaired electrons - T2 shortening; intact RBCs prevent T1 effect |
| Early subacute | 3-7 days | Intracellular methemoglobin | White (bright) | Dark gray (hypointense) | Methemoglobin shortens T1 strongly; still intracellular so T2 remains dark |
| Late subacute | 7->30 days | Extracellular methemoglobin | White (bright) | White (bright) | RBC lysis releases methemoglobin extracellularly - now both T1 and T2 bright |
| Chronic | >14 days | Hemosiderin (outer rim) | Dark gray | Black (markedly hypointense) | Hemosiderin (ferritin/hemosiderin in macrophages) causes susceptibility effect - "blooms" dark |
"The actual sequence of changes can be fairly complicated and variable, depending on brain microenvironment and individual scanners."
| Phase | Timeframe | DWI | ADC | T2/FLAIR | T1 |
|---|---|---|---|---|---|
| Hyperacute | 0-6 hrs | Bright (from ~5 min) | Dark | Subtle/normal | Normal |
| Acute | 6 hrs - 5 days | Bright | Dark | Clearly hyperintense + gyral swelling | Hypointense |
| Subacute | 5-14 days | Fading bright (T2 shine-through takes over) | Normalizing (pseudonormalization ~days 7-10) | Bright, mass effect peaks then resolves | Hypointense |
| Chronic | >3 weeks | Dark (gliosis, unrestricted) | Bright | Bright (gliosis) | Hypointense; may see gyral enhancement with contrast |
"Initially, the hyperintense signal on DWI is caused by decreased water diffusivity due to swelling of the ischemic nerve cells (for the first 5-7 days); then it increasingly results from the abnormal T2 properties of the infarcted tissue (T2 shine-through). For this reason, a reliable estimation of the age of the ischemic lesion is not possible by looking at DWI images alone."
- Bradley and Daroff's Neurology in Clinical Practice
| Feature | Hemorrhage | Infarct |
|---|---|---|
| Density | Hyperdense (white) acutely | Hypodense (dark) |
| Timing of visibility | Immediately visible | Subtle <6 hrs; clear >6 hrs |
| Shape | Round/oval, well-defined | Follows vascular territory (wedge-shaped or territorial) |
| Location | Any (hypertensive: basal ganglia, thalamus, cerebellum, pons) | Cortex + subcortex in artery territory |
| Mass effect | Yes - surrounding edema | Peaks at 3-5 days with large infarcts |
| Hemorrhagic transformation | N/A | Can occur (petechial or confluent) in large infarcts |
| Contrast enhancement | Peripheral ring (subacute) | Gyral enhancement after ~1-2 weeks |
| Phase | Infarct DWI | Infarct ADC | Hemorrhage T1 | Hemorrhage T2/SWI |
|---|---|---|---|---|
| Hyperacute (0-6h) | Bright | Dark | Gray | Light gray / Bright |
| Acute (1-3d) | Bright | Dark | Gray | Dark (deoxy) |
| Early subacute (3-7d) | Bright | Dark | Bright | Dark (intracel methHb) |
| Late subacute (7d-4wks) | Fading | Pseudonorm | Bright | Bright (extracel methHb) |
| Chronic (>4wks) | Dark | Bright | Dark | Black rim (hemosiderin) |
"DWI is the most sensitive imaging sequence for the detection of acute ischemic infarction and can show infarcted tissue within minutes of stroke onset, considerably earlier than CT and other MRI sequences."
- Adams and Victor's Principles of Neurology, 12th Ed.
I need images for everything so that it’s easy to Understand
Create a PDF reference guide with timeline diagrams and labeled imaging examples
Create a PDF reference guide with timeline diagrams and labeled imaging examples
mkdir -p /home/daytona/workspace/brain-imaging-guide && ls /home/daytona/workspace/brain-imaging-guide/
~/brain-imaging-guide/build_pdf.py
#!/usr/bin/env python3
"""
Brain CT & MRI Reference Guide PDF Generator
Uses ReportLab to create a comprehensive, visually rich reference guide.
"""
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import cm, mm
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, KeepTogether, PageBreak
)
from reportlab.platypus.flowables import Flowable
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
from reportlab.graphics.shapes import Drawing, Rect, String, Line, Circle, Polygon, Path
from reportlab.graphics.shapes import PolyLine, Group
from reportlab.graphics import renderPDF
from reportlab.platypus import Image as RLImage
import io
import math
# ── Color Palette ─────────────────────────────────────────────────────────────
C_NAVY = colors.HexColor('#0D1B2A')
C_BLUE = colors.HexColor('#1B4F72')
C_LIGHTBLUE = colors.HexColor('#2E86AB')
C_TEAL = colors.HexColor('#17A589')
C_RED = colors.HexColor('#C0392B')
C_ORANGE = colors.HexColor('#E67E22')
C_YELLOW = colors.HexColor('#F1C40F')
C_GREEN = colors.HexColor('#1E8449')
C_PURPLE = colors.HexColor('#7D3C98')
C_DARK_GRAY = colors.HexColor('#2C3E50')
C_MID_GRAY = colors.HexColor('#7F8C8D')
C_LIGHT_GRAY= colors.HexColor('#ECF0F1')
C_WHITE = colors.white
C_BLACK = colors.black
C_CREAM = colors.HexColor('#FDFEFE')
# CT density shades
C_CT_BLOOD = colors.HexColor('#F0F0F0') # hyperdense (bright white)
C_CT_BRAIN = colors.HexColor('#AAAAAA') # normal brain gray
C_CT_INFARCT= colors.HexColor('#606060') # hypodense (dark)
C_CT_CSF = colors.HexColor('#222222') # very dark (CSF/old infarct)
PAGE_W, PAGE_H = A4
# ── Helper Flowable: SectionHeader ───────────────────────────────────────────
class SectionHeader(Flowable):
def __init__(self, title, subtitle="", bg=C_BLUE, w=None):
Flowable.__init__(self)
self.title = title
self.subtitle = subtitle
self.bg = bg
self.w = w or (PAGE_W - 4*cm)
self.h = 1.4*cm if subtitle else 1.1*cm
def draw(self):
c = self.canv
c.setFillColor(self.bg)
c.roundRect(0, 0, self.w, self.h, 5, fill=1, stroke=0)
c.setFillColor(C_WHITE)
c.setFont("Helvetica-Bold", 13)
c.drawString(0.4*cm, self.h - 0.55*cm, self.title)
if self.subtitle:
c.setFont("Helvetica", 9)
c.setFillColor(colors.HexColor('#D6EAF8'))
c.drawString(0.4*cm, 0.2*cm, self.subtitle)
def wrap(self, aw, ah):
return self.w, self.h
# ── Helper Flowable: LegendBox ────────────────────────────────────────────────
class LegendBox(Flowable):
"""Small colored square + label pair"""
def __init__(self, items, cols=3, w=None):
Flowable.__init__(self)
self.items = items # list of (color, label)
self.cols = cols
self.w = w or (PAGE_W - 4*cm)
rows = math.ceil(len(items)/cols)
self.h = rows * 0.55*cm + 0.2*cm
def draw(self):
c = self.canv
col_w = self.w / self.cols
for i, (clr, lbl) in enumerate(self.items):
col = i % self.cols
row = i // self.cols
x = col * col_w
y = self.h - (row+1)*0.55*cm
c.setFillColor(clr)
c.setStrokeColor(C_DARK_GRAY)
c.rect(x, y+0.05*cm, 0.45*cm, 0.38*cm, fill=1, stroke=1)
c.setFillColor(C_DARK_GRAY)
c.setFont("Helvetica", 8)
c.drawString(x + 0.55*cm, y+0.1*cm, lbl)
def wrap(self, aw, ah):
return self.w, self.h
# ── Flowable: CT Brain Schematic ─────────────────────────────────────────────
class CTBrainSchematic(Flowable):
"""
Side-by-side CT schematic: Left = hemorrhage (bright), Right = infarct (dark)
"""
def __init__(self, w=None, h=None):
Flowable.__init__(self)
self.w = w or (PAGE_W - 4*cm)
self.h = h or 6.5*cm
def draw(self):
c = self.canv
w, h = self.w, self.h
mid = w / 2
r = min(h*0.42, mid*0.7)
# ---- LEFT: HEMORRHAGE ----
cx1 = mid * 0.5
cy = h * 0.52
# Skull ring
c.setFillColor(colors.HexColor('#E8E8E8'))
c.setStrokeColor(C_BLACK)
c.setLineWidth(0.5)
c.circle(cx1, cy, r+0.18*cm, fill=1, stroke=1)
# Brain background
c.setFillColor(C_CT_BRAIN)
c.circle(cx1, cy, r, fill=1, stroke=0)
# Ventricles (dark)
c.setFillColor(C_CT_CSF)
c.ellipse(cx1-0.25*cm, cy-0.15*cm, cx1+0.25*cm, cy+0.3*cm, fill=1, stroke=0)
c.ellipse(cx1-0.18*cm, cy-0.4*cm, cx1-0.02*cm, cy-0.15*cm, fill=1, stroke=0)
c.ellipse(cx1+0.02*cm, cy-0.4*cm, cx1+0.18*cm, cy-0.15*cm, fill=1, stroke=0)
# Hemorrhage spot - bright white
c.setFillColor(C_CT_BLOOD)
c.setStrokeColor(colors.HexColor('#CCCCCC'))
c.setLineWidth(0.3)
c.circle(cx1+0.55*cm, cy+0.25*cm, 0.45*cm, fill=1, stroke=1)
# Surrounding edema halo
c.setFillColor(colors.HexColor('#888888'))
# Draw as a ring manually
c.saveState()
c.setFillColor(colors.HexColor('#888888'))
c.circle(cx1+0.55*cm, cy+0.25*cm, 0.62*cm, fill=1, stroke=0)
c.setFillColor(C_CT_BLOOD)
c.circle(cx1+0.55*cm, cy+0.25*cm, 0.45*cm, fill=1, stroke=0)
c.restoreState()
# Arrow + label
c.setStrokeColor(C_RED)
c.setLineWidth(1.2)
c.line(cx1+0.85*cm, cy+0.55*cm, cx1+1.5*cm, cy+1.1*cm)
c.setFillColor(C_RED)
c.setFont("Helvetica-Bold", 7.5)
c.drawString(cx1+1.55*cm, cy+1.08*cm, "HYPERDENSE")
c.setFont("Helvetica", 7)
c.drawString(cx1+1.55*cm, cy+0.88*cm, "(Acute Blood ~60-80 HU)")
# Title
c.setFillColor(C_RED)
c.setFont("Helvetica-Bold", 10)
c.drawCentredString(cx1, 0.3*cm, "HEMORRHAGE on CT")
# ---- DIVIDER ----
c.setStrokeColor(C_MID_GRAY)
c.setLineWidth(0.8)
c.setDash(4, 3)
c.line(mid, 0.1*cm, mid, h-0.1*cm)
c.setDash()
# ---- RIGHT: INFARCT ----
cx2 = mid + mid * 0.5
c.setFillColor(colors.HexColor('#E8E8E8'))
c.setStrokeColor(C_BLACK)
c.setLineWidth(0.5)
c.circle(cx2, cy, r+0.18*cm, fill=1, stroke=1)
c.setFillColor(C_CT_BRAIN)
c.circle(cx2, cy, r, fill=1, stroke=0)
# Ventricles
c.setFillColor(C_CT_CSF)
c.ellipse(cx2-0.25*cm, cy-0.15*cm, cx2+0.25*cm, cy+0.3*cm, fill=1, stroke=0)
c.ellipse(cx2-0.18*cm, cy-0.4*cm, cx2-0.02*cm, cy-0.15*cm, fill=1, stroke=0)
c.ellipse(cx2+0.02*cm, cy-0.4*cm, cx2+0.18*cm, cy-0.15*cm, fill=1, stroke=0)
# Infarct - wedge shaped, dark
c.setFillColor(C_CT_INFARCT)
# Draw wedge (MCA territory) using a path
c.saveState()
p = c.beginPath()
cx_off = cx2 - 0.3*cm
p.moveTo(cx_off, cy+0.1*cm)
p.lineTo(cx2 - r*0.85, cy + r*0.45)
p.lineTo(cx2 - r*0.55, cy + r*0.78)
p.lineTo(cx2 - r*0.1, cy + r*0.75)
p.lineTo(cx2 + r*0.1, cy + r*0.55)
p.lineTo(cx_off, cy+0.1*cm)
p.close()
c.setFillColor(C_CT_INFARCT)
c.drawPath(p, fill=1, stroke=0)
c.restoreState()
# Sulcal effacement hint (lines closer together)
c.setStrokeColor(colors.HexColor('#777777'))
c.setLineWidth(0.4)
for i in range(3):
angle = 110 + i*15
rad = math.radians(angle)
x1 = cx2 + (r*0.55)*math.cos(rad)
y1 = cy + (r*0.55)*math.sin(rad)
x2 = cx2 + (r*0.72)*math.cos(rad)
y2 = cy + (r*0.72)*math.sin(rad)
c.line(x1, y1, x2, y2)
# Arrow + label
c.setStrokeColor(C_BLUE)
c.setLineWidth(1.2)
c.line(cx2 - r*0.5, cy + r*0.65, cx2 - r*0.9, cy + r*1.05)
c.setFillColor(C_BLUE)
c.setFont("Helvetica-Bold", 7.5)
# left-align label
lx = cx2 - r*0.9 - 1.8*cm
c.drawString(lx, cy + r*1.05, "HYPODENSE")
c.setFont("Helvetica", 7)
c.drawString(lx, cy + r*0.85, "(Ischemic territory, <30 HU)")
# Title
c.setFillColor(C_BLUE)
c.setFont("Helvetica-Bold", 10)
c.drawCentredString(cx2, 0.3*cm, "INFARCT on CT")
# Top labels
c.setFillColor(C_DARK_GRAY)
c.setFont("Helvetica-BoldOblique", 8)
c.drawCentredString(w/2, h-0.2*cm, "NON-CONTRAST CT — Axial Brain Window | Left = Hemorrhage | Right = Infarct")
def wrap(self, aw, ah):
return self.w, self.h
# ── Flowable: CT Timeline Diagram ────────────────────────────────────────────
class CTTimelineDiagram(Flowable):
"""Timeline bar showing CT changes for hemorrhage and infarct"""
def __init__(self, w=None, h=None):
Flowable.__init__(self)
self.w = w or (PAGE_W - 4*cm)
self.h = h or 5.8*cm
def draw(self):
c = self.canv
w, h = self.w, self.h
phases = [
("0-6 hrs\nHyperacute", 0.18),
("6-24 hrs\nAcute", 0.18),
("1-7 days\nSubacute", 0.22),
("1-3 wks\nLate\nSubacute", 0.22),
(">3 wks\nChronic", 0.20),
]
hem_colors = [
colors.HexColor('#F5F5F5'), # hyperacute - bright white
colors.HexColor('#EEEEEE'), # acute - white
colors.HexColor('#AAAAAA'), # isodense
colors.HexColor('#888888'), # becoming darker
colors.HexColor('#555555'), # chronic - dark
]
hem_labels = [
"BRIGHT\nHyperdense",
"BRIGHT\nHyperdense",
"ISODENSE\n(may be missed!)",
"ISODENSE\n→ Hypodense",
"HYPODENSE\n(dark)",
]
inf_colors = [
colors.HexColor('#AAAAAA'), # normal / subtle
colors.HexColor('#888888'), # early hypodense
colors.HexColor('#666666'), # clearly dark
colors.HexColor('#555555'), # fogging
colors.HexColor('#3A3A3A'), # chronic dark
]
inf_labels = [
"NORMAL or\nSubtle",
"HYPODENSE\nGray-white blur",
"CLEARLY\nHypodense",
"FOGGING\n(may look normal)",
"ENCEPHALO-\nMALACIA",
]
# Row heights
label_h = 0.8*cm
row_h = 1.6*cm
gap = 0.25*cm
y_hem = h - label_h - row_h
y_inf = y_hem - row_h - gap
bar_top = h - label_h
# Phase separators & labels
x = 0.0
for i, (phase_label, frac) in enumerate(phases):
bw = w * frac
# Phase header box
bg = C_LIGHT_GRAY if i%2==0 else colors.HexColor('#D5D8DC')
c.setFillColor(bg)
c.setStrokeColor(C_DARK_GRAY)
c.setLineWidth(0.4)
c.rect(x, bar_top - label_h, bw, label_h, fill=1, stroke=1)
c.setFillColor(C_DARK_GRAY)
c.setFont("Helvetica-Bold", 7)
lines = phase_label.split('\n')
for li, ln in enumerate(lines):
c.drawCentredString(x + bw/2, bar_top - 0.25*cm - li*0.28*cm, ln)
# Hemorrhage row
c.setFillColor(hem_colors[i])
c.setStrokeColor(C_DARK_GRAY)
c.setLineWidth(0.4)
c.rect(x, y_hem, bw, row_h, fill=1, stroke=1)
c.setFillColor(C_DARK_GRAY if hem_colors[i].hexval() > '#999999' else C_WHITE)
# text color contrast
hex_v = hem_colors[i]
# just use dark text for bright backgrounds, white for dark
r_val = int(hem_colors[i].hexval()[1:3], 16)
txt_c = C_DARK_GRAY if r_val > 160 else C_WHITE
c.setFillColor(txt_c)
c.setFont("Helvetica-Bold", 6.5)
lns2 = hem_labels[i].split('\n')
for li, ln in enumerate(lns2):
c.drawCentredString(x + bw/2, y_hem + row_h - 0.28*cm - li*0.28*cm, ln)
# Infarct row
c.setFillColor(inf_colors[i])
c.setStrokeColor(C_DARK_GRAY)
c.setLineWidth(0.4)
c.rect(x, y_inf, bw, row_h, fill=1, stroke=1)
r_val2 = int(inf_colors[i].hexval()[1:3], 16)
txt_c2 = C_DARK_GRAY if r_val2 > 160 else C_WHITE
c.setFillColor(txt_c2)
c.setFont("Helvetica-Bold", 6.5)
lns3 = inf_labels[i].split('\n')
for li, ln in enumerate(lns3):
c.drawCentredString(x + bw/2, y_inf + row_h - 0.28*cm - li*0.28*cm, ln)
x += bw
# Row labels on right
c.setFillColor(C_RED)
c.setFont("Helvetica-Bold", 8)
c.drawRightString(-0.15*cm, y_hem + row_h*0.45, "BLEED")
c.setFillColor(C_BLUE)
c.drawRightString(-0.15*cm, y_inf + row_h*0.45, "INFARCT")
# Bottom note
c.setFillColor(C_MID_GRAY)
c.setFont("Helvetica-Oblique", 7)
c.drawString(0, y_inf - 0.35*cm, "* Fogging effect: ~1-3 wks post-infarct, CT can appear falsely normal due to macrophage infiltration")
def wrap(self, aw, ah):
return self.w, self.h
# ── Flowable: MRI Sequence Grid ───────────────────────────────────────────────
class MRISequenceGrid(Flowable):
"""
Grid showing simulated MRI signal for each sequence across phases
for HEMORRHAGE.
"""
def __init__(self, w=None, h=None):
Flowable.__init__(self)
self.w = w or (PAGE_W - 4*cm)
self.h = h or 7.5*cm
def draw(self):
c = self.canv
w, h = self.w, self.h
phases = ["Hyperacute\n0-6 h\nOxyHb", "Acute\n1-3 d\nDeoxyHb",
"Early\nSubacute\n3-7 d\nIntracel\nMetHb",
"Late\nSubacute\n7-30 d\nExtracel\nMetHb",
"Chronic\n>14 d\nHemosiderin"]
seqs = ["T1", "T2", "SWI/GRE"]
# signal: 0=dark, 0.5=iso, 1=bright
# Based on standard MRI hemorrhage table
signals = {
"T1": [0.5, 0.5, 1.0, 1.0, 0.2],
"T2": [0.8, 0.1, 0.15, 0.9, 0.0],
"SWI/GRE": [0.3, 0.05, 0.05, 0.1, 0.0],
}
labels_map = {
"T1": ["Isointense\n(Gray)", "Isointense\n(Gray)", "BRIGHT\n(White)", "BRIGHT\n(White)", "Hypointense\n(Dark)"],
"T2": ["Bright\n(Lt Gray)", "DARK\n(Black)", "DARK\n(Black)", "BRIGHT\n(White)", "BLACK\n(Very dark)"],
"SWI/GRE": ["Dark", "Very Dark\n'Blooms'", "Very Dark\n'Blooms'", "Dark", "BLACK\n'Blooms'"],
}
n_phases = len(phases)
n_seqs = len(seqs)
header_w = 1.5*cm
col_w = (w - header_w) / n_phases
row_h = (h - 1.2*cm) / (n_seqs + 1) # +1 for phase header row
# Draw phase headers
x = header_w
for i, ph in enumerate(phases):
bg = C_LIGHT_GRAY if i%2==0 else colors.HexColor('#D5D8DC')
c.setFillColor(bg)
c.setStrokeColor(C_DARK_GRAY)
c.setLineWidth(0.4)
c.rect(x, h - row_h, col_w, row_h, fill=1, stroke=1)
c.setFillColor(C_DARK_GRAY)
c.setFont("Helvetica-Bold", 6.0)
lines = ph.split('\n')
n = len(lines)
for li, ln in enumerate(lines):
c.drawCentredString(x + col_w/2,
h - 0.18*cm - li*(row_h/(n+0.5)), ln)
x += col_w
# Corner cell
c.setFillColor(C_NAVY)
c.setStrokeColor(C_DARK_GRAY)
c.setLineWidth(0.4)
c.rect(0, h - row_h, header_w, row_h, fill=1, stroke=1)
c.setFillColor(C_WHITE)
c.setFont("Helvetica-Bold", 7)
c.drawCentredString(header_w/2, h - row_h/2 - 0.1*cm, "SEQ")
# Draw sequence rows
for si, seq in enumerate(seqs):
y = h - (si+2)*row_h
# Seq label
c.setFillColor(C_NAVY)
c.setStrokeColor(C_DARK_GRAY)
c.setLineWidth(0.4)
c.rect(0, y, header_w, row_h, fill=1, stroke=1)
c.setFillColor(C_WHITE)
c.setFont("Helvetica-Bold", 8)
c.drawCentredString(header_w/2, y + row_h/2 - 0.1*cm, seq)
x = header_w
for pi in range(n_phases):
sig = signals[seq][pi]
# signal → gray shade (0=black, 1=white)
shade = sig
cell_color = colors.Color(shade, shade, shade)
c.setFillColor(cell_color)
c.setStrokeColor(C_DARK_GRAY)
c.setLineWidth(0.3)
c.rect(x, y, col_w, row_h, fill=1, stroke=1)
# text
txt_c = C_WHITE if shade < 0.55 else C_DARK_GRAY
c.setFillColor(txt_c)
c.setFont("Helvetica-Bold", 6.0)
lns = labels_map[seq][pi].split('\n')
n_l = len(lns)
for li, ln in enumerate(lns):
c.drawCentredString(x + col_w/2,
y + row_h - 0.22*cm - li*0.26*cm, ln)
x += col_w
# Title
c.setFillColor(C_DARK_GRAY)
c.setFont("Helvetica-BoldOblique", 8)
c.drawCentredString(w/2, 0.05*cm,
"Signal shade = actual MRI appearance (white = bright, black = dark/signal void)")
def wrap(self, aw, ah):
return self.w, self.h
# ── Flowable: MRI Infarct Timeline ────────────────────────────────────────────
class MRIInfarctTimeline(Flowable):
"""Timeline of ischemic infarct on MRI sequences"""
def __init__(self, w=None, h=None):
Flowable.__init__(self)
self.w = w or (PAGE_W - 4*cm)
self.h = h or 6.0*cm
def draw(self):
c = self.canv
w, h = self.w, self.h
phases = ["Hyperacute\n0-6 h", "Acute\n6h-5d",
"Subacute\n5-14 d", "Late Sub\n14-21 d", "Chronic\n>3 wks"]
seqs = ["DWI", "ADC", "FLAIR/T2", "T1"]
# signal shade 0=dark/black, 1=bright/white
signals = {
"DWI": [0.95, 0.95, 0.85, 0.7, 0.1],
"ADC": [0.05, 0.05, 0.1, 0.55, 0.85],
"FLAIR/T2": [0.3, 0.8, 0.9, 0.85, 0.85],
"T1": [0.45, 0.3, 0.3, 0.4, 0.3],
}
labels_map = {
"DWI": ["BRIGHT\n★", "BRIGHT\n★", "Bright\n(fading)", "Fading\nT2 shine", "DARK"],
"ADC": ["DARK\n★", "DARK\n★", "Dark\n→ pseudo\nnormalizing", "Pseudo-\nnormal", "BRIGHT"],
"FLAIR/T2": ["Normal/\nSubtle", "BRIGHT\n★", "BRIGHT\n+mass\neffect", "Bright", "Bright\nGliosis"],
"T1": ["Normal", "Hypo-\nintense", "Hypo-\nintense", "Gyral\nenhance\n(+Gd)", "Hypo-\nencephalo"],
}
n_phases = len(phases)
n_seqs = len(seqs)
header_w = 1.6*cm
col_w = (w - header_w) / n_phases
row_h = (h - 1.1*cm) / (n_seqs + 1)
# Phase headers
x = header_w
for i, ph in enumerate(phases):
bg = colors.HexColor('#EBF5FB') if i%2==0 else colors.HexColor('#D6EAF8')
c.setFillColor(bg)
c.setStrokeColor(C_DARK_GRAY)
c.setLineWidth(0.4)
c.rect(x, h - row_h, col_w, row_h, fill=1, stroke=1)
c.setFillColor(C_NAVY)
c.setFont("Helvetica-Bold", 6.5)
lns = ph.split('\n')
for li, ln in enumerate(lns):
c.drawCentredString(x+col_w/2, h - 0.22*cm - li*0.3*cm, ln)
x += col_w
# Corner
c.setFillColor(C_TEAL)
c.setStrokeColor(C_DARK_GRAY)
c.setLineWidth(0.4)
c.rect(0, h - row_h, header_w, row_h, fill=1, stroke=1)
c.setFillColor(C_WHITE)
c.setFont("Helvetica-Bold", 7)
c.drawCentredString(header_w/2, h - row_h/2 - 0.1*cm, "SEQ")
# Seq rows
for si, seq in enumerate(seqs):
y = h - (si+2)*row_h
c.setFillColor(C_TEAL)
c.setStrokeColor(C_DARK_GRAY)
c.setLineWidth(0.4)
c.rect(0, y, header_w, row_h, fill=1, stroke=1)
c.setFillColor(C_WHITE)
c.setFont("Helvetica-Bold", 8)
c.drawCentredString(header_w/2, y + row_h/2 - 0.1*cm, seq)
x = header_w
for pi in range(n_phases):
sig = signals[seq][pi]
cell_color = colors.Color(sig, sig, sig)
c.setFillColor(cell_color)
c.setStrokeColor(C_DARK_GRAY)
c.setLineWidth(0.3)
c.rect(x, y, col_w, row_h, fill=1, stroke=1)
txt_c = C_WHITE if sig < 0.5 else C_DARK_GRAY
c.setFillColor(txt_c)
c.setFont("Helvetica-Bold", 5.8)
lns2 = labels_map[seq][pi].split('\n')
for li, ln in enumerate(lns2):
c.drawCentredString(x+col_w/2,
y + row_h - 0.2*cm - li*0.24*cm, ln)
x += col_w
c.setFillColor(C_DARK_GRAY)
c.setFont("Helvetica-BoldOblique", 7.5)
c.drawString(0, 0.1*cm, "★ = Diagnostic window: DWI bright + ADC dark = ACUTE INFARCT (confirmed restricted diffusion)")
def wrap(self, aw, ah):
return self.w, self.h
# ── Flowable: DWI-ADC Schematic ───────────────────────────────────────────────
class DWIADCSchematic(Flowable):
"""3-panel: Acute infarct | Chronic gliosis | Hemorrhage on SWI"""
def __init__(self, w=None, h=None):
Flowable.__init__(self)
self.w = w or (PAGE_W - 4*cm)
self.h = h or 5.5*cm
def draw(self):
c = self.canv
w, h = self.w, self.h
n = 3
pw = w / n
r = min(h*0.35, pw*0.38)
cy = h * 0.53
panels = [
{
"title": "ACUTE INFARCT",
"subtitle": "DWI bright + ADC dark\n= INFARCTION",
"tc": C_RED,
"dwi_lesion": 1.0, # bright
"adc_lesion": 0.0, # dark
"label_d": "DWI",
"label_a": "ADC",
},
{
"title": "CHRONIC / GLIOSIS",
"subtitle": "DWI dark + ADC bright\n= T2 shine-through\n(NOT acute infarct)",
"tc": C_GREEN,
"dwi_lesion": 0.15,
"adc_lesion": 0.9,
"label_d": "DWI",
"label_a": "ADC",
},
{
"title": "HEMORRHAGE",
"subtitle": "SWI/GRE: BLACK\n'blooming'\n= Blood product",
"tc": C_PURPLE,
"dwi_lesion": None, # SWI panel
"adc_lesion": 0.0, # very dark
"label_d": "SWI",
"label_a": "T2*",
},
]
for i, p in enumerate(panels):
cx = pw * i + pw/2
# Panel bg
c.setFillColor(colors.HexColor('#F8F9FA'))
c.setStrokeColor(p["tc"])
c.setLineWidth(1.2)
c.roundRect(pw*i + 0.1*cm, 0.4*cm, pw - 0.2*cm, h-0.5*cm, 4, fill=1, stroke=1)
# Brain circle
brain_bg = 0.65 # gray
c.setFillColor(colors.Color(brain_bg, brain_bg, brain_bg))
c.setStrokeColor(C_DARK_GRAY)
c.setLineWidth(0.5)
c.circle(cx, cy, r, fill=1, stroke=1)
# Lesion
if p["dwi_lesion"] is not None:
lv = p["dwi_lesion"]
c.setFillColor(colors.Color(lv, lv, lv))
c.setStrokeColor(C_DARK_GRAY)
c.setLineWidth(0.3)
c.circle(cx + r*0.25, cy + r*0.2, r*0.38, fill=1, stroke=1)
else:
# SWI bloom effect - add bright ring + dark center
c.setFillColor(colors.Color(0.3, 0.3, 0.3))
c.circle(cx + r*0.25, cy + r*0.2, r*0.42, fill=1, stroke=0)
c.setFillColor(colors.Color(0.02, 0.02, 0.02))
c.circle(cx + r*0.25, cy + r*0.2, r*0.32, fill=1, stroke=0)
# bloom glow
c.setFillColor(colors.Color(0.0, 0.0, 0.0))
c.setStrokeColor(colors.HexColor('#555555'))
c.setLineWidth(0.5)
c.circle(cx + r*0.25, cy + r*0.2, r*0.38, fill=0, stroke=1)
# Sequence labels
c.setFillColor(p["tc"])
c.setFont("Helvetica-Bold", 7)
c.drawCentredString(cx, 0.65*cm, p["label_d"] if p["dwi_lesion"] is None else
(p["label_d"] + " " + ("BRIGHT" if p["dwi_lesion"] > 0.5 else "DARK")))
# Title
c.setFillColor(p["tc"])
c.setFont("Helvetica-Bold", 8.5)
c.drawCentredString(cx, h-0.2*cm, p["title"])
# Subtitle
c.setFillColor(C_DARK_GRAY)
c.setFont("Helvetica", 6.8)
lns = p["subtitle"].split('\n')
for li, ln in enumerate(lns):
c.drawCentredString(cx, h - 0.58*cm - li*0.26*cm, ln)
def wrap(self, aw, ah):
return self.w, self.h
# ── Flowable: Hemorrhage Types Diagram ───────────────────────────────────────
class HemorrhageTypesDiagram(Flowable):
"""
Schematic cross-section showing EDH, SDH, SAH, ICH locations
"""
def __init__(self, w=None, h=None):
Flowable.__init__(self)
self.w = w or (PAGE_W - 4*cm)
self.h = h or 7.0*cm
def draw(self):
c = self.canv
w, h = self.w, self.h
cx = w / 2
cy = h * 0.52
# Skull outer
skull_r = min(h*0.44, w*0.28)
c.setFillColor(colors.HexColor('#D5D8DC'))
c.setStrokeColor(C_BLACK)
c.setLineWidth(1.0)
c.circle(cx, cy, skull_r, fill=1, stroke=1)
# Skull inner (diploe)
c.setFillColor(colors.HexColor('#BDC3C7'))
c.circle(cx, cy, skull_r*0.88, fill=1, stroke=0)
# Dura
c.setFillColor(colors.HexColor('#C0392B'))
c.circle(cx, cy, skull_r*0.87, fill=0, stroke=1)
c.setLineWidth(2.0)
c.setStrokeColor(colors.HexColor('#C0392B'))
c.circle(cx, cy, skull_r*0.87, fill=0, stroke=1)
# Brain parenchyma
c.setFillColor(C_CT_BRAIN)
c.setStrokeColor(colors.HexColor('#888888'))
c.setLineWidth(0.5)
c.circle(cx, cy, skull_r*0.75, fill=1, stroke=1)
# Gyri texture
c.setStrokeColor(colors.HexColor('#999999'))
c.setLineWidth(0.6)
for angle in range(0, 360, 30):
rad = math.radians(angle)
x1 = cx + skull_r*0.66*math.cos(rad)
y1 = cy + skull_r*0.66*math.sin(rad)
x2 = cx + skull_r*0.74*math.cos(rad)
y2 = cy + skull_r*0.74*math.sin(rad)
c.line(x1, y1, x2, y2)
# Ventricles
c.setFillColor(C_CT_CSF)
c.ellipse(cx-0.28*cm, cy-0.1*cm, cx+0.28*cm, cy+0.35*cm, fill=1, stroke=0)
c.ellipse(cx-0.2*cm, cy-0.45*cm, cx-0.03*cm, cy-0.1*cm, fill=1, stroke=0)
c.ellipse(cx+0.03*cm, cy-0.45*cm, cx+0.2*cm, cy-0.1*cm, fill=1, stroke=0)
# ── EDH (lenticular / biconvex) at top right ──
edh_angle = 50 # degrees from top
edh_rad = math.radians(edh_angle)
edh_cx = cx + skull_r*0.82*math.cos(edh_rad)
edh_cy = cy + skull_r*0.82*math.sin(edh_rad)
c.setFillColor(colors.HexColor('#E74C3C'))
c.saveState()
c.translate(edh_cx, edh_cy)
c.rotate(edh_angle - 90)
c.ellipse(-0.35*cm, -0.12*cm, 0.35*cm, 0.12*cm, fill=1, stroke=0)
c.restoreState()
# ── SDH (crescent) at upper left ──
sdh_angle = 130
sdh_rad = math.radians(sdh_angle)
# Draw crescent as thick arc
c.setFillColor(colors.HexColor('#E67E22'))
c.saveState()
c.translate(cx, cy)
c.setFillColor(colors.HexColor('#E67E22'))
# approximate crescent with a wide stroke arc
c.setStrokeColor(colors.HexColor('#E67E22'))
c.setLineWidth(5.0)
c.arc(-skull_r*0.80, -skull_r*0.80, skull_r*0.80, skull_r*0.80,
startAng=95, extent=85)
c.restoreState()
# ── SAH (thin layer in subarachnoid) at bottom ──
c.setFillColor(colors.HexColor('#F39C12'))
c.saveState()
c.translate(cx, cy)
c.setStrokeColor(colors.HexColor('#F39C12'))
c.setLineWidth(3.5)
c.arc(-skull_r*0.76, -skull_r*0.76, skull_r*0.76, skull_r*0.76,
startAng=210, extent=60)
c.restoreState()
# ── ICH (parenchymal) - bright spot in right basal ganglia area ──
ich_x = cx + 0.35*cm
ich_y = cy + 0.1*cm
c.setFillColor(colors.HexColor('#ECF0F1'))
c.setStrokeColor(colors.HexColor('#BDC3C7'))
c.setLineWidth(0.3)
c.circle(ich_x, ich_y, 0.32*cm, fill=1, stroke=1)
# Labels with arrows
label_data = [
(cx + skull_r*0.95, cy + skull_r*0.6, cx + skull_r*1.3, cy + skull_r*0.9, "EDH\n(Biconvex)\nArt. bleed", colors.HexColor('#E74C3C')),
(cx - skull_r*0.92, cy + skull_r*0.55, cx - skull_r*1.35, cy + skull_r*0.9, "SDH\n(Crescent)\nVenous", colors.HexColor('#E67E22')),
(cx - skull_r*0.55, cy - skull_r*0.85, cx - skull_r*0.8, cy - skull_r*1.25, "SAH\n(Cisterns/\nSulci)", colors.HexColor('#F39C12')),
(cx + 0.35*cm, cy + 0.1*cm, cx + skull_r*1.15, cy - skull_r*0.5, "ICH\n(Parenchymal)\nBasal ganglia", colors.HexColor('#AAAAAA')),
]
for x1, y1, x2, y2, lbl, clr in label_data:
c.setStrokeColor(clr)
c.setLineWidth(0.8)
c.line(x1, y1, x2, y2)
c.setFillColor(clr)
c.setFont("Helvetica-Bold", 7.5)
lns = lbl.split('\n')
align_right = x2 < cx
for li, ln in enumerate(lns):
if align_right:
c.drawRightString(x2, y2 - li*0.25*cm, ln)
else:
c.drawString(x2, y2 - li*0.25*cm, ln)
# Title
c.setFillColor(C_DARK_GRAY)
c.setFont("Helvetica-BoldOblique", 8)
c.drawCentredString(w/2, 0.1*cm,
"Schematic cross-section of cranium showing types & locations of intracranial hemorrhage")
def wrap(self, aw, ah):
return self.w, self.h
# ── Flowable: HU Scale Bar ────────────────────────────────────────────────────
class HUScaleBar(Flowable):
def __init__(self, w=None, h=None):
Flowable.__init__(self)
self.w = w or (PAGE_W - 4*cm)
self.h = h or 2.2*cm
def draw(self):
c = self.canv
w, h = self.w, self.h
bar_h = 0.6*cm
bar_y = h*0.5
bar_x = 1.0*cm
bar_w = w - 2.0*cm
n = 200
# Gradient bar: black → white
for i in range(n):
frac = i / n
shade = frac
c.setFillColor(colors.Color(shade, shade, shade))
c.setStrokeColor(colors.Color(shade, shade, shade))
c.rect(bar_x + frac*bar_w, bar_y, bar_w/n + 0.5, bar_h, fill=1, stroke=0)
# Border
c.setStrokeColor(C_DARK_GRAY)
c.setLineWidth(0.5)
c.rect(bar_x, bar_y, bar_w, bar_h, fill=0, stroke=1)
# Tick marks & labels
ticks = [
(0.0, "< -100\nFat/Air"),
(0.15, "0\nWater/CSF"),
(0.32, "~25-40\nBrain"),
(0.52, "~50-80\nBlood"),
(0.68, "~100\nCalcification"),
(1.0, ">400\nBone"),
]
for frac, lbl in ticks:
tx = bar_x + frac*bar_w
c.setStrokeColor(C_DARK_GRAY)
c.setLineWidth(0.5)
c.line(tx, bar_y-0.05*cm, tx, bar_y-0.25*cm)
c.setFillColor(C_DARK_GRAY)
c.setFont("Helvetica", 6.5)
lns = lbl.split('\n')
for li, ln in enumerate(lns):
c.drawCentredString(tx, bar_y - 0.32*cm - li*0.2*cm, ln)
# Arrow labels above bar
blood_x = bar_x + 0.52*bar_w
c.setFillColor(C_RED)
c.setFont("Helvetica-Bold", 7)
c.drawCentredString(blood_x, bar_y + bar_h + 0.2*cm, "BLOOD\n(Hyperdense)")
c.setFillColor(C_BLUE)
infarct_x = bar_x + 0.22*bar_w
c.drawCentredString(infarct_x, bar_y + bar_h + 0.2*cm, "INFARCT\n(Hypodense)")
c.setFillColor(C_DARK_GRAY)
c.setFont("Helvetica-Bold", 8)
c.drawString(0, bar_y + bar_h*0.2, "HU:")
c.drawString(w - 0.8*cm, bar_y + bar_h*0.2, "DENSE")
c.setFont("Helvetica", 8)
c.drawString(0, bar_y - 0.1*cm, "")
def wrap(self, aw, ah):
return self.w, self.h
# ── Build PDF ─────────────────────────────────────────────────────────────────
def build_pdf(output_path):
doc = SimpleDocTemplate(
output_path,
pagesize=A4,
leftMargin=2*cm, rightMargin=2*cm,
topMargin=2*cm, bottomMargin=2*cm,
title="Brain CT & MRI: Bleed vs Infarct - Reference Guide",
author="Orris Medical Reference"
)
styles = getSampleStyleSheet()
avail_w = PAGE_W - 4*cm
# Custom styles
title_style = ParagraphStyle('Title2',
fontName='Helvetica-Bold', fontSize=20, leading=24,
textColor=C_WHITE, alignment=TA_CENTER, spaceAfter=4)
subtitle_style = ParagraphStyle('Subtitle2',
fontName='Helvetica', fontSize=11, leading=14,
textColor=colors.HexColor('#AED6F1'), alignment=TA_CENTER, spaceAfter=6)
h2_style = ParagraphStyle('H2',
fontName='Helvetica-Bold', fontSize=11, leading=14,
textColor=C_NAVY, spaceAfter=4, spaceBefore=8)
body_style = ParagraphStyle('Body2',
fontName='Helvetica', fontSize=8.5, leading=12,
textColor=C_DARK_GRAY, spaceAfter=4, alignment=TA_JUSTIFY)
small_style = ParagraphStyle('Small',
fontName='Helvetica', fontSize=7.5, leading=10,
textColor=C_DARK_GRAY, spaceAfter=2)
callout_style = ParagraphStyle('Callout',
fontName='Helvetica-BoldOblique', fontSize=8.5, leading=12,
textColor=C_NAVY, spaceAfter=3,
borderPad=6, borderColor=C_LIGHTBLUE,
backColor=colors.HexColor('#EBF5FB'))
note_style = ParagraphStyle('Note',
fontName='Helvetica-Oblique', fontSize=7.5, leading=10,
textColor=C_MID_GRAY, spaceAfter=4)
story = []
# ══════════════════════════════════════════════════════════════════════════
# COVER PAGE
# ══════════════════════════════════════════════════════════════════════════
class CoverPage(Flowable):
def __init__(self):
Flowable.__init__(self)
self.w = avail_w
self.h = PAGE_H - 4*cm
def draw(self):
c = self.canv
w, h = self.w, self.h
# Background gradient block
c.setFillColor(C_NAVY)
c.rect(0, h*0.45, w, h*0.55, fill=1, stroke=0)
c.setFillColor(C_BLUE)
c.rect(0, h*0.3, w, h*0.15, fill=1, stroke=0)
c.setFillColor(C_LIGHTBLUE)
c.rect(0, 0, w, h*0.3, fill=1, stroke=0)
# Title
c.setFillColor(C_WHITE)
c.setFont("Helvetica-Bold", 22)
c.drawCentredString(w/2, h*0.82, "BRAIN CT & MRI")
c.setFont("Helvetica-Bold", 18)
c.drawCentredString(w/2, h*0.74, "READING GUIDE")
c.setFont("Helvetica", 12)
c.setFillColor(colors.HexColor('#AED6F1'))
c.drawCentredString(w/2, h*0.66, "Hemorrhage vs. Infarction")
c.drawCentredString(w/2, h*0.61, "Acute • Subacute • Chronic")
# Decorative CT circle (brain schematic)
brain_cx = w/2
brain_cy = h*0.44
brain_r = 1.8*cm
c.setFillColor(colors.HexColor('#1A1A2E'))
c.circle(brain_cx, brain_cy, brain_r*1.15, fill=1, stroke=0)
c.setFillColor(colors.HexColor('#888888'))
c.circle(brain_cx, brain_cy, brain_r, fill=1, stroke=0)
c.setFillColor(colors.HexColor('#111111'))
c.ellipse(brain_cx-0.3*cm, brain_cy-0.1*cm, brain_cx+0.3*cm, brain_cy+0.35*cm, fill=1, stroke=0)
# Bleed spot
c.setFillColor(colors.HexColor('#F0F0F0'))
c.circle(brain_cx+0.6*cm, brain_cy+0.3*cm, 0.38*cm, fill=1, stroke=0)
# Infarct
c.setFillColor(colors.HexColor('#555555'))
c.circle(brain_cx-0.55*cm, brain_cy+0.2*cm, 0.32*cm, fill=1, stroke=0)
c.setFillColor(C_WHITE)
c.setFont("Helvetica-Bold", 7)
c.drawCentredString(brain_cx+0.6*cm, brain_cy+0.1*cm, "BLEED")
c.drawCentredString(brain_cx-0.55*cm, brain_cy+0.02*cm, "INFARCT")
# Bottom section
c.setFillColor(C_WHITE)
c.setFont("Helvetica-Bold", 10)
c.drawCentredString(w/2, h*0.22, "Complete Timeline Reference")
c.setFont("Helvetica", 9)
c.setFillColor(colors.HexColor('#1B2631'))
c.drawCentredString(w/2, h*0.15,
"CT Density | MRI Signal | DWI/ADC | SWI | Hemorrhage Types")
c.drawCentredString(w/2, h*0.09,
"Based on: Adams & Victor's Neurology • Bradley & Daroff • Grainger & Allison")
c.setFillColor(C_NAVY)
c.setFont("Helvetica-Oblique", 7.5)
c.drawCentredString(w/2, h*0.02, "Orris Medical Reference | For educational purposes only")
def wrap(self, aw, ah):
return self.w, self.h
story.append(CoverPage())
story.append(PageBreak())
# ══════════════════════════════════════════════════════════════════════════
# PAGE 2: CT FUNDAMENTALS
# ══════════════════════════════════════════════════════════════════════════
story.append(SectionHeader("1. HOW TO READ A BRAIN CT",
"Non-contrast CT (NCCT) — First-line for ALL stroke and head injury presentations", bg=C_NAVY, w=avail_w))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("The Hounsfield Unit (HU) Scale", h2_style))
story.append(Paragraph(
"Every CT pixel is assigned a Hounsfield Unit (HU) value representing tissue density. "
"Air = −1000 HU, water = 0 HU, bone = ~700+ HU. <b>Blood clots are denser than brain "
"(~60–80 HU vs ~35 HU), so they appear bright white (hyperdense). Infarcted brain loses "
"fluid equilibrium and becomes hypodense (dark, <30 HU).</b>", body_style))
story.append(HUScaleBar(w=avail_w, h=2.4*cm))
story.append(Spacer(1, 0.5*cm))
story.append(Paragraph("CT Window Settings", h2_style))
win_data = [
["Window Setting", "Width (WW)", "Level (WL)", "Best For"],
["Brain window", "~80", "~35", "Parenchyma, gray-white differentiation, blood"],
["Bone window", "~2000","~300", "Skull fractures, calvarium"],
["Stroke window", "~35", "~35", "Subtle early ischemia, improves gray-white contrast"],
["Subdural window", "~200", "~60", "Subdural hematoma vs. brain"],
]
win_table = Table(win_data, colWidths=[3.5*cm, 2.2*cm, 2.2*cm, 8.1*cm])
win_table.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), C_BLUE),
('TEXTCOLOR', (0,0), (-1,0), C_WHITE),
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
('FONTSIZE', (0,0), (-1,-1), 8),
('ROWBACKGROUNDS', (0,1), (-1,-1), [C_CREAM, C_LIGHT_GRAY]),
('GRID', (0,0), (-1,-1), 0.4, C_MID_GRAY),
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
('TOPPADDING', (0,0), (-1,-1), 3),
('BOTTOMPADDING', (0,0), (-1,-1), 3),
]))
story.append(win_table)
story.append(Spacer(1, 0.4*cm))
story.append(Paragraph("CT Schematic: Hemorrhage vs. Infarct", h2_style))
story.append(CTBrainSchematic(w=avail_w, h=6.5*cm))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph(
"<b>Key early CT signs of ischemic stroke (first 6 hours):</b> "
"(1) Loss of gray-white differentiation at the basal ganglia or insular cortex. "
"(2) <b>Insular ribbon sign</b> — loss of sharp insular cortex outline (MCA territory). "
"(3) <b>Sulcal effacement</b> — swollen gyri obliterate normal sulci. "
"(4) <b>Hyperdense vessel sign</b> — bright MCA or basilar artery = acute thrombus. "
"(5) <b>1/3 MCA rule</b> — if >1/3 MCA territory is already hypodense, IV tPA is contraindicated.",
body_style))
story.append(PageBreak())
# ══════════════════════════════════════════════════════════════════════════
# PAGE 3: CT TIMELINE
# ══════════════════════════════════════════════════════════════════════════
story.append(SectionHeader("2. CT TIMELINE — HEMORRHAGE vs. INFARCT",
"How CT appearance changes over time for each pathology", bg=C_RED, w=avail_w))
story.append(Spacer(1, 0.3*cm))
story.append(CTTimelineDiagram(w=avail_w, h=5.8*cm))
story.append(Spacer(1, 0.4*cm))
story.append(Paragraph("Detailed CT Timeline Table", h2_style))
ct_data = [
["Phase", "Time", "Hemorrhage on CT", "Infarct on CT"],
["Hyperacute", "0–6 hrs",
"HYPERDENSE (white, ~60-80 HU)\nClot formed, protein concentrated",
"NORMAL or very subtle\nPossible sulcal effacement only"],
["Acute", "6–24 hrs",
"HYPERDENSE\nPeripheral edema halo may appear",
"HYPODENSE territory\nGray-white differentiation lost\nInsular ribbon sign"],
["Early Subacute", "1–7 days",
"HYPERDENSE → becoming ISODENSE\nEdema increases, mass effect",
"CLEARLY HYPODENSE\nMass effect peaks day 3–5\nMidline shift possible"],
["Late Subacute", "1–3 wks",
"ISODENSE — can be MISSED!\nLook for mass effect clue",
"FOGGING EFFECT (~1-3 wks)\nMay look falsely normal\n(macrophage infiltration)"],
["Chronic", ">3 wks",
"HYPODENSE (dark)\nMay calcify; subdural → crescent",
"ENCEPHALOMALACIA\nHypodense cavity\nEx-vacuo ventricular dilation"],
]
ct_table = Table(ct_data,
colWidths=[2.8*cm, 2.0*cm, 6.2*cm, 5.0*cm])
ct_table.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), C_DARK_GRAY),
('TEXTCOLOR', (0,0), (-1,0), C_WHITE),
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
('BACKGROUND', (2,1), (2,-1), colors.HexColor('#FDFEFE')),
('BACKGROUND', (3,1), (3,-1), colors.HexColor('#EBF5FB')),
('FONTSIZE', (0,0), (-1,-1), 7.5),
('ROWBACKGROUNDS', (0,1), (1,-1), [colors.HexColor('#FDEBD0'), colors.HexColor('#FEF9E7')]),
('GRID', (0,0), (-1,-1), 0.4, C_MID_GRAY),
('VALIGN', (0,0), (-1,-1), 'TOP'),
('TOPPADDING', (0,0), (-1,-1), 4),
('BOTTOMPADDING', (0,0), (-1,-1), 4),
('FONTNAME', (2,1), (2,-1), 'Helvetica-Bold'),
]))
story.append(ct_table)
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph(
"<b>Pitfall — Isodense Subdural Hematoma:</b> "
"At ~7–14 days, subdural blood becomes isodense with brain on CT. "
"Clues: asymmetric sulci, displaced cortex, midline shift without visible hyperdensity. "
"MRI (T1) will show it as bright (methemoglobin).", callout_style))
story.append(PageBreak())
# ══════════════════════════════════════════════════════════════════════════
# PAGE 4: HEMORRHAGE TYPES
# ══════════════════════════════════════════════════════════════════════════
story.append(SectionHeader("3. TYPES OF INTRACRANIAL HEMORRHAGE",
"Location determines shape, likely etiology, and clinical presentation", bg=C_RED, w=avail_w))
story.append(Spacer(1, 0.3*cm))
story.append(HemorrhageTypesDiagram(w=avail_w, h=7.0*cm))
story.append(Spacer(1, 0.4*cm))
hem_type_data = [
["Type", "CT Shape", "Location", "Typical Cause", "Key Clue"],
["EDH\n(Epidural)", "BICONVEX\n(lens-shaped)", "Between skull & dura\nDoes NOT cross sutures",
"Middle meningeal\nartery (temporal\nfracture)", "Lucid interval\nTemporal fracture"],
["SDH\n(Subdural)", "CRESCENT\n(concave inner edge)", "Between dura & brain\nCrosses sutures freely",
"Bridging veins\n(shear force)", "Elderly, falls\nAnticoagulants"],
["SAH\n(Subarachnoid)", "Fills sulci/basal\ncisterns — 'star'\npattern", "Subarachnoid space",
"Ruptured Berry\naneurysm, AVM,\nTrauma", "Thunderclap HA\nFisher grade"],
["ICH\n(Intracerebral)", "Round/oval\nparenchymal", "Basal ganglia\nThalamus, pons,\ncerebellum",
"HTN (most common)\nAmyloid angiopathy", "Location predicts\ncause: deep=HTN\ncortical=amyloid"],
]
hem_t = Table(hem_type_data, colWidths=[2.0*cm, 2.8*cm, 3.2*cm, 3.2*cm, 4.8*cm])
hem_t.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), C_RED),
('TEXTCOLOR', (0,0), (-1,0), C_WHITE),
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
('FONTSIZE', (0,0), (-1,-1), 7.5),
('ROWBACKGROUNDS', (0,1), (-1,-1),
[colors.HexColor('#FDEDEC'), colors.HexColor('#FEF9E7'),
colors.HexColor('#FEF5E7'), colors.HexColor('#FDFEFE')]),
('GRID', (0,0), (-1,-1), 0.4, C_MID_GRAY),
('VALIGN', (0,0), (-1,-1), 'TOP'),
('TOPPADDING', (0,0), (-1,-1), 4),
('BOTTOMPADDING', (0,0), (-1,-1), 4),
('FONTNAME', (0,1), (0,-1), 'Helvetica-Bold'),
('TEXTCOLOR', (0,1), (0,-1), C_RED),
]))
story.append(hem_t)
story.append(PageBreak())
# ══════════════════════════════════════════════════════════════════════════
# PAGE 5: MRI SEQUENCES
# ══════════════════════════════════════════════════════════════════════════
story.append(SectionHeader("4. MRI SEQUENCES — WHAT EACH ONE SHOWS",
"Understanding each sequence is key to accurate interpretation", bg=C_TEAL, w=avail_w))
story.append(Spacer(1, 0.3*cm))
mri_seq_data = [
["Sequence", "TE/TR", "Bright (hyperintense)", "Dark (hypointense)", "Best Used For"],
["T1", "Short TE\nShort TR",
"Fat, methemoglobin,\nprotein-rich fluid,\ngadolinium (contrast)",
"CSF, edema,\ndeoxyhemoglobin,\nacute infarct",
"Anatomy, subacute\nblood, contrast\nenhancement"],
["T2", "Long TE\nLong TR",
"CSF, edema, gliosis,\nlate subacute blood\n(extracellular methHb)",
"Acute blood (deoxy),\nhemosiderin,\ncalcification, cortical bone",
"Infarcts, edema,\ntumors, inflammation"],
["FLAIR", "Long TE\nLong TR\n+inversion",
"Edema, gliosis,\nSAH (bloody CSF bright),\nperiventricular lesions",
"Normal CSF\n(suppressed — black)",
"Periventricular MS,\nSAH, infarct age\n(DWI-FLAIR mismatch)"],
["DWI", "EPI\nsequence",
"ACUTE INFARCT ★\n(restricted diffusion)\nAlso: abscess, epidermoid",
"Chronic lesions,\nblood after several days,\nCSF",
"ACUTE STROKE\ndiagnosis — positive\nwithin minutes"],
["ADC", "Derived\nfrom DWI",
"Vasogenic edema,\nchronic gliosis\n(unrestricted diffusion)",
"ACUTE INFARCT ★\n(true restriction)\nAlso abscess",
"Confirm restricted\ndiffusion; exclude\nT2 shine-through"],
["SWI/GRE", "T2*\ngradient\necho",
"None significant",
"ALL blood products\n★ 'Blooms' dark\nCalcification, iron, veins",
"Microhemorrhages,\nvenous thrombosis,\nhemorrhagic transform."],
["MRA", "TOF or\ncontrast",
"Flowing blood\n(patent vessel)",
"Thrombosed vessel\n(flow void lost)",
"Intracranial\nvessel patency,\naneurysm, LVO"],
]
mri_t = Table(mri_seq_data, colWidths=[1.8*cm, 1.5*cm, 4.0*cm, 3.8*cm, 4.9*cm])
mri_t.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), C_TEAL),
('TEXTCOLOR', (0,0), (-1,0), C_WHITE),
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
('FONTSIZE', (0,0), (-1,-1), 7.5),
('ROWBACKGROUNDS', (0,1), (-1,-1),
[colors.HexColor('#E8F8F5'), colors.HexColor('#FDFEFE')]*5),
('GRID', (0,0), (-1,-1), 0.4, C_MID_GRAY),
('VALIGN', (0,0), (-1,-1), 'TOP'),
('TOPPADDING', (0,0), (-1,-1), 4),
('BOTTOMPADDING', (0,0), (-1,-1), 4),
('FONTNAME', (0,1), (0,-1), 'Helvetica-Bold'),
('TEXTCOLOR', (0,1), (0,-1), C_TEAL),
]))
story.append(mri_t)
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph(
"<b>The DWI-ADC Rule (most important concept in acute stroke MRI):</b><br/>"
"• <b>DWI bright + ADC dark</b> = TRUE restricted diffusion = ACUTE INFARCTION ✓<br/>"
"• <b>DWI bright + ADC bright/normal</b> = T2 shine-through (NOT acute infarct)<br/>"
"• <b>DWI dark + ADC bright</b> = chronic gliosis / encephalomalacia",
callout_style))
story.append(DWIADCSchematic(w=avail_w, h=5.5*cm))
story.append(PageBreak())
# ══════════════════════════════════════════════════════════════════════════
# PAGE 6: MRI HEMORRHAGE TIMELINE
# ══════════════════════════════════════════════════════════════════════════
story.append(SectionHeader("5. MRI HEMORRHAGE SIGNAL TIMELINE",
"Signal depends on hemoglobin oxidation state and RBC integrity", bg=C_PURPLE, w=avail_w))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph(
"Hemoglobin undergoes a predictable series of chemical transformations after bleeding. "
"Each stage has unique magnetic properties that create characteristic MRI signal patterns. "
"The key variable is whether hemoglobin has <b>unpaired electrons</b> (paramagnetic = shortens T2) "
"and whether it is <b>intracellular or extracellular</b> (determines T1 effect).",
body_style))
story.append(MRISequenceGrid(w=avail_w, h=7.5*cm))
story.append(Spacer(1, 0.3*cm))
mri_hem_data = [
["Stage", "Time", "Hb Form", "Location", "T1", "T2", "SWI", "Mechanism"],
["Hyperacute", "0–6 hrs", "Oxyhemoglobin\n(OxyHb)", "Intracellular",
"Gray\n(iso)", "Lt Gray\n(slightly\nbright)", "Dark", "OxyHb has NO unpaired electrons\n→ no T1 shortening; T2 bright from\nwater in intact RBCs"],
["Acute", "1–3 days", "Deoxyhemoglobin\n(DeoxyHb)", "Intracellular",
"Gray\n(iso)", "DARK\n(black)", "VERY\nDARK", "DeoxyHb has 4 unpaired electrons\n→ strong T2 shortening; intact cell\nmembrane prevents T1 effect"],
["Early\nSubacute", "3–7 days", "Intracellular\nMethemoglobin", "Intracellular",
"BRIGHT\n(white)", "DARK\n(black)", "DARK", "MetHb has 5 unpaired electrons\n→ strong T1 shortening; RBCs still\nintact → T2 still dark"],
["Late\nSubacute", "7–30 days", "Extracellular\nMethemoglobin", "Extracellular\n(RBC lysis)",
"BRIGHT\n(white)", "BRIGHT\n(white)", "Dark", "RBC lysis → MetHb disperses\nextracellularly → T2 shortening\nrelieved → both T1 & T2 bright"],
["Chronic", ">14 days", "Hemosiderin +\nFerritin", "In macrophages\n(outer rim)",
"Dk Gray\n(hypo)", "BLACK\n(very\ndark)", "BLACK\n'Blooms'", "Hemosiderin = highly paramagnetic\n→ profound T2* susceptibility\neffect → 'blooming' on SWI/GRE"],
]
mri_h_t = Table(mri_hem_data,
colWidths=[1.7*cm, 1.5*cm, 2.2*cm, 2.0*cm, 1.2*cm, 1.2*cm, 1.2*cm, 5.0*cm])
mri_h_t.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), C_PURPLE),
('TEXTCOLOR', (0,0), (-1,0), C_WHITE),
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
('FONTSIZE', (0,0), (-1,-1), 7),
# Color-code T1/T2 columns by signal
('BACKGROUND', (4,2), (4,2), colors.HexColor('#888888')), # hyper T1
('BACKGROUND', (5,2), (5,2), colors.HexColor('#111111')), # dark T2 acute
('TEXTCOLOR', (5,2), (5,2), C_WHITE),
('BACKGROUND', (4,3), (4,3), colors.HexColor('#F0F0F0')), # bright T1 early sub
('BACKGROUND', (5,3), (5,3), colors.HexColor('#111111')), # dark T2
('TEXTCOLOR', (5,3), (5,3), C_WHITE),
('BACKGROUND', (4,4), (4,4), colors.HexColor('#F5F5F5')), # bright T1 late sub
('BACKGROUND', (5,4), (5,4), colors.HexColor('#F5F5F5')), # bright T2 late sub
('BACKGROUND', (5,5), (5,5), colors.HexColor('#111111')), # black chronic
('TEXTCOLOR', (5,5), (5,5), C_WHITE),
('BACKGROUND', (6,5), (6,5), colors.HexColor('#000000')), # SWI black
('TEXTCOLOR', (6,5), (6,5), C_WHITE),
('ROWBACKGROUNDS', (0,1), (3,-1),
[colors.HexColor('#F9EBF8'), colors.HexColor('#F5EEF8'),
colors.HexColor('#EBF5FB'), colors.HexColor('#E8F8F5'),
colors.HexColor('#FDFEFE')]),
('GRID', (0,0), (-1,-1), 0.4, C_MID_GRAY),
('VALIGN', (0,0), (-1,-1), 'TOP'),
('TOPPADDING', (0,0), (-1,-1), 3),
('BOTTOMPADDING', (0,0), (-1,-1), 3),
('FONTNAME', (0,1), (0,-1), 'Helvetica-Bold'),
('TEXTCOLOR', (0,1), (0,-1), C_PURPLE),
]))
story.append(mri_h_t)
story.append(Spacer(1, 0.2*cm))
story.append(Paragraph(
"<b>Memory aid — T1/T2 for hemorrhage:</b> "
"<i>\"It's Bright After Bleeding\"</i> — "
"Iso → Iso → <b>T1 Bright</b> → <b>Both Bright</b> → Dark (hemosiderin ring)",
note_style))
story.append(PageBreak())
# ══════════════════════════════════════════════════════════════════════════
# PAGE 7: MRI INFARCT TIMELINE
# ══════════════════════════════════════════════════════════════════════════
story.append(SectionHeader("6. MRI INFARCTION TIMELINE",
"How ischemic infarct evolves on each MRI sequence", bg=C_TEAL, w=avail_w))
story.append(Spacer(1, 0.3*cm))
story.append(MRIInfarctTimeline(w=avail_w, h=6.0*cm))
story.append(Spacer(1, 0.4*cm))
mri_inf_data = [
["Phase", "Time", "DWI", "ADC", "T2/FLAIR", "T1", "Clinical Note"],
["Hyperacute", "0–6 hrs",
"BRIGHT ★\n(from 5 min)", "DARK ★",
"Normal or\nvery subtle", "Normal",
"DWI+/ADC− = infarction\nconfirmed. CT often normal.\nStart tPA workup."],
["Acute", "6 hrs–5 days",
"BRIGHT ★", "DARK ★",
"Clearly\nBRIGHT\n+ mass effect", "Hypointense",
"Full extent visible on DWI.\nMass effect may mimic tumor.\nPeak cytotoxic edema."],
["Subacute\n(early)", "5–14 days",
"Fading\n(T2 shine-\nthrough)", "Pseudonorm-\nalizing\n(~day 7-10)",
"BRIGHT\nMass effect\nresolvingv", "Hypointense\nGyral\nenhancement",
"ADC PSEUDONORMALIZATION\nTrap! ADC looks normal but\nT2/FLAIR still bright."],
["Chronic", ">3 wks",
"DARK\n(unrestricted)", "BRIGHT\n(free water)",
"BRIGHT\ngliosis\n(permanent)", "Hypointense\n(cavity)",
"Encephalomalacia.\nEx-vacuo dilation of\nadjacent ventricle."],
]
mri_inf_t = Table(mri_inf_data,
colWidths=[2.2*cm, 1.8*cm, 1.8*cm, 1.8*cm, 2.2*cm, 2.2*cm, 4.0*cm])
mri_inf_t.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), C_TEAL),
('TEXTCOLOR', (0,0), (-1,0), C_WHITE),
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
('FONTSIZE', (0,0), (-1,-1), 7.5),
('ROWBACKGROUNDS', (0,1), (-1,-1),
[colors.HexColor('#E8F8F5'), colors.HexColor('#FDFEFE'),
colors.HexColor('#EBF5FB'), colors.HexColor('#F0F9FF')]),
('GRID', (0,0), (-1,-1), 0.4, C_MID_GRAY),
('VALIGN', (0,0), (-1,-1), 'TOP'),
('TOPPADDING', (0,0), (-1,-1), 4),
('BOTTOMPADDING', (0,0), (-1,-1), 4),
('FONTNAME', (2,1), (3,2), 'Helvetica-Bold'),
('BACKGROUND', (2,1), (2,2), colors.HexColor('#F5F5F5')), # DWI bright
('BACKGROUND', (3,1), (3,2), colors.HexColor('#111111')), # ADC dark
('TEXTCOLOR', (3,1), (3,2), C_WHITE),
('BACKGROUND', (2,4), (2,4), colors.HexColor('#333333')), # DWI dark chronic
('TEXTCOLOR', (2,4), (2,4), C_WHITE),
('BACKGROUND', (3,4), (3,4), colors.HexColor('#EEEEEE')), # ADC bright chronic
]))
story.append(mri_inf_t)
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph(
"<b>DWI-FLAIR Mismatch (Wake-up Stroke):</b> "
"If DWI is bright but FLAIR is normal/subtle → infarct is likely <4.5 hours old "
"→ consider thrombolysis even if time of onset is unknown. "
"If both DWI and FLAIR are bright → infarct is probably >4.5–6 hours.", callout_style))
story.append(Spacer(1, 0.2*cm))
story.append(Paragraph(
"<b>ADC Pseudonormalization Trap:</b> "
"Around days 7–14, vasogenic edema water dilutes the restricted diffusion signal "
"and ADC may normalize. Do not mistake this for normal or resolving infarct — "
"FLAIR and clinical history will clarify.", callout_style))
story.append(PageBreak())
# ══════════════════════════════════════════════════════════════════════════
# PAGE 8: MASTER COMPARISON + WORKFLOW
# ══════════════════════════════════════════════════════════════════════════
story.append(SectionHeader("7. MASTER COMPARISON: BLEED vs. INFARCT",
"Quick-reference summary for clinical decision-making", bg=C_NAVY, w=avail_w))
story.append(Spacer(1, 0.3*cm))
master_data = [
["Feature", "HEMORRHAGE", "INFARCT"],
["CT density\n(acute)", "HYPERDENSE (white)\n~60–80 HU", "HYPODENSE (dark)\n<30 HU (after 6 hrs)"],
["CT visibility", "Immediately visible", "Subtle <6 hrs; clear >6 hrs"],
["CT shape", "Round/oval, well-defined", "Wedge-shaped, follows\nvascular territory"],
["Location clue", "Deep (HTN): BG, thalamus\npons, cerebellum", "Cortex + subcortex\nfollowing arterial supply"],
["MRI T1", "Iso → Iso → BRIGHT →\nBRIGHT → Dark (rim)", "Normal → Hypo → Hypo"],
["MRI T2", "Bright → DARK → DARK\n→ BRIGHT → BLACK", "Normal → BRIGHT →\nBRIGHT (permanent gliosis)"],
["MRI DWI", "Dark (blood quenches\nDWI after few days)", "BRIGHT ★ (from 5 min\nonset — gold standard)"],
["MRI SWI", "BLACK 'blooms'\n(all stages)", "Negative\n(unless hemorrhagic Tx)"],
["Mass effect", "Yes, immediate +\nsurrounding edema", "Peaks day 3–5 (large\ninfarcts)"],
["Enhancement\n(+contrast)", "Peripheral ring in\nsubacute phase", "Gyral enhancement\n>1–2 weeks"],
["Chronic\nappearance", "Hypodense cavity ±\ncalcification; hemosiderin\nring on MRI", "Encephalomalacia\nEx-vacuo dilation"],
]
master_t = Table(master_data, colWidths=[3.5*cm, 6.5*cm, 6.0*cm])
master_t.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), C_NAVY),
('TEXTCOLOR', (0,0), (-1,0), C_WHITE),
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
('FONTSIZE', (0,0), (-1,-1), 8),
('BACKGROUND', (1,1), (1,-1), colors.HexColor('#FDEDEC')),
('BACKGROUND', (2,1), (2,-1), colors.HexColor('#EBF5FB')),
('ROWBACKGROUNDS', (0,1), (0,-1),
[C_LIGHT_GRAY, colors.HexColor('#D5D8DC')]*10),
('GRID', (0,0), (-1,-1), 0.4, C_MID_GRAY),
('VALIGN', (0,0), (-1,-1), 'TOP'),
('TOPPADDING', (0,0), (-1,-1), 4),
('BOTTOMPADDING', (0,0), (-1,-1), 4),
('FONTNAME', (0,1), (0,-1), 'Helvetica-Bold'),
('FONTNAME', (1,1), (1,-1), 'Helvetica'),
('FONTNAME', (2,1), (2,-1), 'Helvetica'),
]))
story.append(master_t)
story.append(Spacer(1, 0.4*cm))
# Emergency workflow
story.append(SectionHeader("8. EMERGENCY IMAGING WORKFLOW",
"Step-by-step approach for acute stroke / head injury", bg=C_GREEN, w=avail_w))
story.append(Spacer(1, 0.3*cm))
workflow_data = [
["Step", "Modality", "Question Asked", "Action Based On Result"],
["1", "NCCT Brain\n(Non-contrast CT)",
"Is there hemorrhage?\nIs there a mass?",
"YES hemorrhage → manage accordingly, NO tPA\nNO hemorrhage → proceed to step 2"],
["2", "CT Angiography\n(CTA) Arch→Skull",
"Is there a Large\nVessel Occlusion (LVO)?",
"LVO present → consider thrombectomy\nCheck for hyperdense vessel sign"],
["3", "CT Perfusion\n(if available)",
"Is there salvageable\npenumbra vs. core?",
"Mismatch (large penumbra) → proceed with\nthrombectomy even at extended time window"],
["4", "MRI DWI+ADC\n+FLAIR+SWI\n(if CT negative\nor wake-up stroke)",
"Confirm infarct?\nHow old? Microbleeds?",
"DWI+/ADC− → acute infarct\nDWI-FLAIR mismatch → <4.5 hrs window\nSWI dark → hemorrhagic component"],
["5", "MRA or CTA\nfollow-up",
"Vessel patency?\nRecanalisation?",
"Assess response to thrombolysis/thrombectomy\nLook for residual stenosis"],
]
wf_t = Table(workflow_data, colWidths=[0.7*cm, 3.0*cm, 4.0*cm, 8.3*cm])
wf_t.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), C_GREEN),
('TEXTCOLOR', (0,0), (-1,0), C_WHITE),
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
('FONTSIZE', (0,0), (-1,-1), 7.5),
('ROWBACKGROUNDS', (0,1), (-1,-1),
[colors.HexColor('#EAFAF1'), colors.HexColor('#FDFEFE')]*4),
('GRID', (0,0), (-1,-1), 0.4, C_MID_GRAY),
('VALIGN', (0,0), (-1,-1), 'TOP'),
('TOPPADDING', (0,0), (-1,-1), 4),
('BOTTOMPADDING', (0,0), (-1,-1), 4),
('FONTNAME', (0,1), (0,-1), 'Helvetica-Bold'),
('TEXTCOLOR', (0,1), (0,-1), C_GREEN),
('ALIGN', (0,1), (0,-1), 'CENTER'),
]))
story.append(wf_t)
story.append(Spacer(1, 0.3*cm))
# FLAIR vascular sign note
story.append(Paragraph(
"<b>FLAIR Vascular Sign:</b> Hyperintense signal within an artery on FLAIR (not DWI) "
"indicates slow/absent flow distal to an occlusion — a marker of perfusion failure "
"even before infarction is visible. On SWI, a thrombosed vessel shows as a markedly "
"hypointense 'blooming' serpiginous structure — the <b>susceptibility vessel sign</b>.",
callout_style))
story.append(Spacer(1, 0.3*cm))
# Sources
story.append(HRFlowable(width=avail_w, thickness=0.5, color=C_MID_GRAY))
story.append(Spacer(1, 0.15*cm))
story.append(Paragraph(
"<b>Sources:</b> Adams and Victor's Principles of Neurology 12th Ed. | "
"Bradley and Daroff's Neurology in Clinical Practice | "
"Grainger & Allison's Diagnostic Radiology | "
"Neuroanatomy through Clinical Cases 3rd Ed. | Goldman-Cecil Medicine | "
"Orris Medical Library",
note_style))
# ── Page number footer ────────────────────────────────────────────────────
def add_page_number(canvas, doc):
canvas.saveState()
canvas.setFont("Helvetica", 8)
canvas.setFillColor(C_MID_GRAY)
canvas.drawCentredString(PAGE_W/2, 1.2*cm,
f"Brain CT & MRI Reference Guide | Page {doc.page}")
canvas.setFillColor(C_LIGHTBLUE)
canvas.rect(2*cm, 1.0*cm, PAGE_W-4*cm, 0.05*cm, fill=1, stroke=0)
canvas.restoreState()
doc.build(story, onFirstPage=add_page_number, onLaterPages=add_page_number)
print(f"PDF saved to: {output_path}")
if __name__ == "__main__":
build_pdf("/home/daytona/workspace/brain-imaging-guide/Brain_CT_MRI_Reference_Guide.pdf")
cd /home/daytona/workspace/brain-imaging-guide && python build_pdf.py 2>&1
~/brain-imaging-guide/build_pdf.py
Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/brain-imaging-guide/build_pdf.py'
wrap that was already there and fix the issue. Let me check the cover page section:"def wrap" in ~/brain-imaging-guide
wrap is already there at line 926. The problem is the cover page height PAGE_H - 4*cm is too large for the frame. The issue is the cover page + the story.append(CoverPage()) - the cover page height should not exceed the available frame height. Let me fix this to use a smaller height and also ensure the cover page fits within a single frame:~/brain-imaging-guide/build_pdf.py
Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/brain-imaging-guide/build_pdf.py'
cd /home/daytona/workspace/brain-imaging-guide && python build_pdf.py 2>&1
hexval() format issue. Let me fix all the color contrast logic to use a simpler approach:~/brain-imaging-guide/build_pdf.py
Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/brain-imaging-guide/build_pdf.py'
cd /home/daytona/workspace/brain-imaging-guide && python build_pdf.py 2>&1
ls -lh /home/daytona/workspace/brain-imaging-guide/Brain_CT_MRI_Reference_Guide.pdf && python3 -c "from pypdf import PdfReader; r=PdfReader('/home/daytona/workspace/brain-imaging-guide/Brain_CT_MRI_Reference_Guide.pdf'); print(f'Pages: {len(r.pages)}'); print('Title:', r.metadata.title)"
Brain CT MRI Reference Guide
PDF Document · PDF
| Page | Content |
|---|---|
| 1 - Cover | Color-coded title page with brain schematic showing bleed vs. infarct |
| 2 - CT Fundamentals | Hounsfield Unit gradient scale bar, window settings table, side-by-side CT brain schematics (hyperdense hemorrhage vs. hypodense infarct), early CT signs of stroke |
| 3 - CT Timeline | Color-coded timeline diagram (darker = darker on CT) for both hemorrhage and infarct across all 5 phases, plus detailed comparison table with the fogging effect warning |
| 4 - Hemorrhage Types | Labeled cross-section schematic of EDH/SDH/SAH/ICH with table of shapes, locations, causes, and key clues |
| 5 - MRI Sequences | Full table of all sequences (T1/T2/FLAIR/DWI/ADC/SWI/MRA) with what each shows, plus 3-panel DWI-ADC schematic (acute infarct vs. gliosis vs. SWI hemorrhage) |
| 6 - MRI Hemorrhage Timeline | Signal grid (actual gray shades showing MRI appearance) across 5 stages with mechanism table - oxyhemoglobin through hemosiderin |
| 7 - MRI Infarct Timeline | Color-coded grid for DWI/ADC/FLAIR/T1 across phases, with DWI-FLAIR mismatch rule and ADC pseudonormalization trap |
| 8 - Master Comparison | Side-by-side bleed vs. infarct on all modalities + Emergency imaging workflow (NCCT → CTA → Perfusion → MRI) |
I didn’t find any real images in the pdf so that it’s easy for me to learn :(
mkdir -p /home/daytona/workspace/brain-imaging-guide/images && cd /home/daytona/workspace/brain-imaging-guide/images && \ # Download all real brain imaging images from textbooks wget -q -O img_acute_parenchymal_hemorrhage.png "https://cdn.orris.care/cdss_images/f51679d1ddf27c82f627a9f081af7dee8466a719aaa36dc9bdca4b952a53be5f.png" && \ wget -q -O img_late_subacute_hemorrhage_t1.png "https://cdn.orris.care/cdss_images/1205f4b4d48201e219935c5f7534d06061e9034356aa9a09a9ab333c9afb898e.png" && \ wget -q -O img_late_subacute_hemorrhage_t2.png "https://cdn.orris.care/cdss_images/7ba0bee47186749ebec8e59795dcd43a9a8901f4aca0380c9e6e7f973d8a532f.png" && \ wget -q -O img_chronic_hemorrhage_t2.png "https://cdn.orris.care/cdss_images/0c46318256dd35ec1a08044f57aa7014e683e0ec2a365c8e15863301e31b0337.png" && \ wget -q -O img_chronic_hemorrhage_gre.png "https://cdn.orris.care/cdss_images/b6320e816bd8d3df79044c080d4b8cd2bfa8d1330069b0a86fe21e6157a7e2df.png" && \ wget -q -O img_skull_fracture_sah_ct.png "https://cdn.orris.care/cdss_images/2ac36e75c6d7e939cbe429dc63c2e8eefa524a95b43de00172f73ebe50e5194c.png" && \ wget -q -O img_acute_subdural_mri.png "https://cdn.orris.care/cdss_images/9ba034e262eb2d087b9ef35c1a0138698277e5859176b22a4ed314e679fc4dce.png" && \ wget -q -O img_haemorrhagic_transformation.png "https://cdn.orris.care/cdss_images/88464415e093083fe605fd4da166aad72244d208a5a82d16e93e85cb5e1e7118.png" && \ wget -q -O img_wake_up_stroke_dwi_flair.png "https://cdn.orris.care/cdss_images/c5b7a81bb16ee2aab6a978589938740e8b6bbe1efde86ba8a14e110a1213efe8.png" && \ wget -q -O img_subacute_chronic_infarct.png "https://cdn.orris.care/cdss_images/b4116dbe264a05fa9994ecd4bae07349f577c1a0432fdf3861e4b142f7e4a08d.png" && \ wget -q -O img_acute_mca_infarct_dwi.png "https://cdn.orris.care/cdss_images/783c4c9969b6c071852848653cf19417684cbad0502c76eeeb27a28839e21d79.png" && \ wget -q -O img_acute_stroke_ct_mri.png "https://cdn.orris.care/cdss_images/2fdbf6e88505f6867580e6f0f8290b0263b159b81638edd0ecd044fd045ee505.png" && \ wget -q -O img_chronic_infarct_free_diffusion.png "https://cdn.orris.care/cdss_images/b4116dbe264a05fa9994ecd4bae07349f577c1a0432fdf3861e4b142f7e4a08d.png" && \ echo "Done" && ls -la
~/brain-imaging-guide/build_pdf_v2.py
#!/usr/bin/env python3
"""
Brain CT & MRI Reference Guide - Version 2
Includes REAL textbook images alongside explanatory diagrams.
"""
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import cm, mm
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, PageBreak, Image as RLImage, KeepTogether
)
from reportlab.platypus.flowables import Flowable
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY, TA_RIGHT
import os, math
IMG_DIR = "/home/daytona/workspace/brain-imaging-guide/images"
OUT_PATH = "/home/daytona/workspace/brain-imaging-guide/Brain_CT_MRI_Reference_Guide_v2.pdf"
# ── Colors ────────────────────────────────────────────────────────────────────
C_NAVY = colors.HexColor('#0D1B2A')
C_BLUE = colors.HexColor('#1B4F72')
C_LIGHTBLUE = colors.HexColor('#2E86AB')
C_TEAL = colors.HexColor('#17A589')
C_RED = colors.HexColor('#C0392B')
C_ORANGE = colors.HexColor('#E67E22')
C_GREEN = colors.HexColor('#1E8449')
C_PURPLE = colors.HexColor('#7D3C98')
C_DARK_GRAY = colors.HexColor('#2C3E50')
C_MID_GRAY = colors.HexColor('#7F8C8D')
C_LIGHT_GRAY= colors.HexColor('#ECF0F1')
C_WHITE = colors.white
C_BLACK = colors.black
C_CREAM = colors.HexColor('#FDFEFE')
PAGE_W, PAGE_H = A4
AVAIL_W = PAGE_W - 4*cm
# ── Styles ─────────────────────────────────────────────────────────────────────
styles = getSampleStyleSheet()
def make_style(name, **kw):
return ParagraphStyle(name, **kw)
TITLE_S = make_style('T1', fontName='Helvetica-Bold', fontSize=18, leading=22,
textColor=C_WHITE, alignment=TA_CENTER)
HEAD_S = make_style('H1', fontName='Helvetica-Bold', fontSize=13, leading=16,
textColor=C_WHITE, alignment=TA_LEFT)
H2_S = make_style('H2', fontName='Helvetica-Bold', fontSize=10.5, leading=13,
textColor=C_NAVY, spaceAfter=3, spaceBefore=6)
BODY_S = make_style('BD', fontName='Helvetica', fontSize=8.5, leading=12,
textColor=C_DARK_GRAY, spaceAfter=4, alignment=TA_JUSTIFY)
CAPTION_S= make_style('CP', fontName='Helvetica-Oblique', fontSize=7.5, leading=10,
textColor=C_MID_GRAY, spaceAfter=4, alignment=TA_CENTER)
LABEL_S = make_style('LB', fontName='Helvetica-Bold', fontSize=8, leading=10,
textColor=C_NAVY, spaceAfter=2)
NOTE_S = make_style('NT', fontName='Helvetica-Oblique', fontSize=7.5, leading=10,
textColor=C_MID_GRAY, spaceAfter=3)
CALLOUT_S= make_style('CO', fontName='Helvetica-Bold', fontSize=8.5, leading=12,
textColor=C_NAVY, spaceAfter=4,
backColor=colors.HexColor('#EBF5FB'),
borderPad=5)
WARN_S = make_style('WN', fontName='Helvetica-Bold', fontSize=8.5, leading=12,
textColor=colors.HexColor('#7B241C'), spaceAfter=4,
backColor=colors.HexColor('#FDEDEC'),
borderPad=5)
# ── Helper: section banner ────────────────────────────────────────────────────
class Banner(Flowable):
def __init__(self, title, subtitle="", bg=C_BLUE, w=None, h=None):
Flowable.__init__(self)
self.title = title
self.subtitle = subtitle
self.bg = bg
self.w = w or AVAIL_W
self.h = h or (1.5*cm if subtitle else 1.1*cm)
def wrap(self, aw, ah):
return self.w, self.h
def draw(self):
c = self.canv
c.setFillColor(self.bg)
c.roundRect(0, 0, self.w, self.h, 6, fill=1, stroke=0)
c.setFillColor(C_WHITE)
c.setFont("Helvetica-Bold", 13)
c.drawString(0.4*cm, self.h - 0.58*cm, self.title)
if self.subtitle:
c.setFont("Helvetica", 8.5)
c.setFillColor(colors.HexColor('#AED6F1'))
c.drawString(0.4*cm, 0.22*cm, self.subtitle)
# ── Helper: image with border + caption ──────────────────────────────────────
def img_block(path, width, caption, label_color=C_NAVY):
"""Returns a list of flowables: bordered image + caption paragraph."""
items = []
if os.path.exists(path):
img = RLImage(path, width=width)
img.hAlign = 'CENTER'
items.append(img)
if caption:
items.append(Paragraph(caption, CAPTION_S))
return items
def img_table(rows):
"""
rows: list of lists of flowables (table cells)
Creates a centered table of images with spacing.
"""
col_w = AVAIL_W / max(len(r) for r in rows)
flat = []
for row in rows:
flat.append(row)
t = Table(flat, colWidths=[col_w]*len(flat[0]) if flat else [AVAIL_W])
t.setStyle(TableStyle([
('VALIGN', (0,0), (-1,-1), 'TOP'),
('ALIGN', (0,0), (-1,-1), 'CENTER'),
('TOPPADDING', (0,0), (-1,-1), 4),
('BOTTOMPADDING',(0,0),(-1,-1), 4),
('LEFTPADDING', (0,0), (-1,-1), 3),
('RIGHTPADDING',(0,0), (-1,-1), 3),
]))
return t
# ── Helper: CT timeline diagram ───────────────────────────────────────────────
class CTTimelineBar(Flowable):
def __init__(self, w=None, h=3.8*cm):
Flowable.__init__(self)
self.w = w or AVAIL_W
self.h = h
def wrap(self, aw, ah): return self.w, self.h
def draw(self):
c = self.canv
w, h = self.w, self.h
phases = [("Hyperacute\n0-6 hrs",0.18),("Acute\n6-24 hrs",0.18),
("Subacute\n1-7 days",0.22),("Late Sub\n1-3 wks",0.22),("Chronic\n>3 wks",0.20)]
hem_shade = [0.94, 0.92, 0.55, 0.45, 0.28]
inf_shade = [0.58, 0.42, 0.33, 0.40, 0.22]
hem_lbl = ["BRIGHT\nHyperdense","BRIGHT\nHyperdense","ISODENSE\n(can miss!)","ISODENSE\n→Hypodense","HYPODENSE"]
inf_lbl = ["NORMAL\nor Subtle","HYPODENSE\nGW blur","CLEARLY\nHypodense","FOGGING\nEffect","ENCEPHALO-\nMALACIA"]
hdr_h = h*0.28; row_h = h*0.34
y_hem = h - hdr_h - row_h
y_inf = h - hdr_h - 2*row_h
x = 0.0
for i,(ph,frac) in enumerate(phases):
bw = w*frac
bg = C_LIGHT_GRAY if i%2==0 else colors.HexColor('#D5D8DC')
c.setFillColor(bg); c.setStrokeColor(C_DARK_GRAY); c.setLineWidth(0.4)
c.rect(x, h-hdr_h, bw, hdr_h, fill=1, stroke=1)
c.setFillColor(C_DARK_GRAY); c.setFont("Helvetica-Bold",7)
for li,ln in enumerate(ph.split('\n')):
c.drawCentredString(x+bw/2, h-0.2*cm-li*0.28*cm, ln)
sh = hem_shade[i]
c.setFillColor(colors.Color(sh,sh,sh)); c.setStrokeColor(C_DARK_GRAY); c.setLineWidth(0.4)
c.rect(x, y_hem, bw, row_h, fill=1, stroke=1)
tc = C_DARK_GRAY if sh>0.6 else C_WHITE
c.setFillColor(tc); c.setFont("Helvetica-Bold",6.5)
for li,ln in enumerate(hem_lbl[i].split('\n')):
c.drawCentredString(x+bw/2, y_hem+row_h-0.22*cm-li*0.26*cm, ln)
si = inf_shade[i]
c.setFillColor(colors.Color(si,si,si)); c.setStrokeColor(C_DARK_GRAY); c.setLineWidth(0.4)
c.rect(x, y_inf, bw, row_h, fill=1, stroke=1)
tc2 = C_DARK_GRAY if si>0.6 else C_WHITE
c.setFillColor(tc2); c.setFont("Helvetica-Bold",6.5)
for li,ln in enumerate(inf_lbl[i].split('\n')):
c.drawCentredString(x+bw/2, y_inf+row_h-0.22*cm-li*0.26*cm, ln)
x += bw
c.setFillColor(C_RED); c.setFont("Helvetica-Bold",8)
c.drawRightString(-0.1*cm, y_hem+row_h*0.4, "BLEED")
c.setFillColor(C_BLUE)
c.drawRightString(-0.1*cm, y_inf+row_h*0.4, "INFARCT")
# ── Helper: MRI hemorrhage signal grid ────────────────────────────────────────
class MRIHemGrid(Flowable):
def __init__(self, w=None, h=5.5*cm):
Flowable.__init__(self)
self.w = w or AVAIL_W
self.h = h
def wrap(self, aw, ah): return self.w, self.h
def draw(self):
c = self.canv
w, h = self.w, self.h
phases=["Hyperacute\n0-6h\nOxyHb","Acute\n1-3d\nDeoxyHb",
"Early Sub\n3-7d\nIntracel\nMetHb","Late Sub\n7-30d\nExtracel\nMetHb","Chronic\n>14d\nHemosiderin"]
seqs=["T1","T2","SWI"]
sigs={"T1":[0.5,0.5,1.0,1.0,0.15],"T2":[0.85,0.05,0.1,0.9,0.0],"SWI":[0.25,0.02,0.03,0.1,0.0]}
lbls={"T1":["Iso\n(Gray)","Iso\n(Gray)","BRIGHT\n(White)","BRIGHT\n(White)","Hypo\n(Dark)"],
"T2":["Bright\nLt Gray","DARK\nBlack","DARK\nBlack","BRIGHT\nWhite","BLACK"],
"SWI":["Dark","VERY\nDARK","VERY\nDARK","Dark","BLACK\n'Blooms'"]}
hw = 1.3*cm; col_w = (w-hw)/len(phases)
hdr_h = h*0.22; row_h = (h-hdr_h)/(len(seqs)+0.2)
# Corner
c.setFillColor(C_PURPLE); c.setStrokeColor(C_DARK_GRAY); c.setLineWidth(0.4)
c.rect(0, h-hdr_h, hw, hdr_h, fill=1, stroke=1)
c.setFillColor(C_WHITE); c.setFont("Helvetica-Bold",7)
c.drawCentredString(hw/2, h-hdr_h/2-0.1*cm, "SEQ")
# Phase headers
x = hw
for i,ph in enumerate(phases):
bg = colors.HexColor('#F9EBF8') if i%2==0 else colors.HexColor('#E8DAEF')
c.setFillColor(bg); c.setStrokeColor(C_DARK_GRAY); c.setLineWidth(0.4)
c.rect(x, h-hdr_h, col_w, hdr_h, fill=1, stroke=1)
c.setFillColor(C_PURPLE); c.setFont("Helvetica-Bold",6)
lns=ph.split('\n')
for li,ln in enumerate(lns):
c.drawCentredString(x+col_w/2, h-0.15*cm-li*0.22*cm, ln)
x += col_w
# Rows
for si,seq in enumerate(seqs):
y = h - hdr_h - (si+1)*row_h
c.setFillColor(C_PURPLE); c.setStrokeColor(C_DARK_GRAY); c.setLineWidth(0.4)
c.rect(0, y, hw, row_h, fill=1, stroke=1)
c.setFillColor(C_WHITE); c.setFont("Helvetica-Bold",8)
c.drawCentredString(hw/2, y+row_h/2-0.1*cm, seq)
x = hw
for pi in range(len(phases)):
sg = sigs[seq][pi]
c.setFillColor(colors.Color(sg,sg,sg)); c.setStrokeColor(C_DARK_GRAY); c.setLineWidth(0.3)
c.rect(x, y, col_w, row_h, fill=1, stroke=1)
tc = C_WHITE if sg<0.5 else C_DARK_GRAY
c.setFillColor(tc); c.setFont("Helvetica-Bold",6.5)
lns2=lbls[seq][pi].split('\n')
for li,ln in enumerate(lns2):
c.drawCentredString(x+col_w/2, y+row_h-0.18*cm-li*0.24*cm, ln)
x += col_w
# ── Helper: MRI infarct signal grid ──────────────────────────────────────────
class MRIInfarctGrid(Flowable):
def __init__(self, w=None, h=4.5*cm):
Flowable.__init__(self)
self.w = w or AVAIL_W
self.h = h
def wrap(self, aw, ah): return self.w, self.h
def draw(self):
c = self.canv
w, h = self.w, self.h
phases=["Hyperacute\n0-6h","Acute\n6h-5d","Subacute\n5-14d","Late Sub\n2-3wk","Chronic\n>3wks"]
seqs=["DWI","ADC","FLAIR"]
sigs={"DWI":[0.95,0.95,0.80,0.65,0.1],"ADC":[0.05,0.05,0.15,0.55,0.88],"FLAIR":[0.30,0.82,0.88,0.85,0.82]}
lbls={"DWI":["BRIGHT★","BRIGHT★","Bright\n(fading)","Fading\nshine","DARK"],"ADC":["DARK★","DARK★","→Pseudo\nnorm","Pseudo-\nnormal","BRIGHT"],"FLAIR":["Normal\nor subtle","BRIGHT","BRIGHT\n+Swelling","Bright","Bright\nGliosis"]}
hw = 1.3*cm; col_w = (w-hw)/len(phases)
hdr_h = h*0.24; row_h = (h-hdr_h)/(len(seqs)+0.2)
c.setFillColor(C_TEAL); c.setStrokeColor(C_DARK_GRAY); c.setLineWidth(0.4)
c.rect(0, h-hdr_h, hw, hdr_h, fill=1, stroke=1)
c.setFillColor(C_WHITE); c.setFont("Helvetica-Bold",7)
c.drawCentredString(hw/2, h-hdr_h/2-0.1*cm, "SEQ")
x = hw
for i,ph in enumerate(phases):
bg = colors.HexColor('#E8F8F5') if i%2==0 else colors.HexColor('#D1F2EB')
c.setFillColor(bg); c.setStrokeColor(C_DARK_GRAY); c.setLineWidth(0.4)
c.rect(x, h-hdr_h, col_w, hdr_h, fill=1, stroke=1)
c.setFillColor(C_TEAL); c.setFont("Helvetica-Bold",6.5)
for li,ln in enumerate(ph.split('\n')):
c.drawCentredString(x+col_w/2, h-0.18*cm-li*0.27*cm, ln)
x += col_w
for si,seq in enumerate(seqs):
y = h - hdr_h - (si+1)*row_h
c.setFillColor(C_TEAL); c.setStrokeColor(C_DARK_GRAY); c.setLineWidth(0.4)
c.rect(0, y, hw, row_h, fill=1, stroke=1)
c.setFillColor(C_WHITE); c.setFont("Helvetica-Bold",8)
c.drawCentredString(hw/2, y+row_h/2-0.1*cm, seq)
x = hw
for pi in range(len(phases)):
sg = sigs[seq][pi]
c.setFillColor(colors.Color(sg,sg,sg)); c.setStrokeColor(C_DARK_GRAY); c.setLineWidth(0.3)
c.rect(x, y, col_w, row_h, fill=1, stroke=1)
tc = C_WHITE if sg<0.5 else C_DARK_GRAY
c.setFillColor(tc); c.setFont("Helvetica-Bold",6.5)
lns2=lbls[seq][pi].split('\n')
for li,ln in enumerate(lns2):
c.drawCentredString(x+col_w/2, y+row_h-0.18*cm-li*0.24*cm, ln)
x += col_w
c.setFillColor(C_DARK_GRAY); c.setFont("Helvetica-Oblique",7)
c.drawString(0, -0.3*cm, "★ DWI bright + ADC dark = TRUE restricted diffusion = ACUTE INFARCT (gold standard diagnosis)")
# ── HU Scale Bar ──────────────────────────────────────────────────────────────
class HUBar(Flowable):
def __init__(self, w=None, h=1.8*cm):
Flowable.__init__(self)
self.w = w or AVAIL_W
self.h = h
def wrap(self, aw, ah): return self.w, self.h
def draw(self):
c = self.canv; w,h = self.w,self.h
bx=1.0*cm; bw=w-2.0*cm; by=h*0.45; bh=0.55*cm
n=150
for i in range(n):
s=i/n; c.setFillColor(colors.Color(s,s,s))
c.rect(bx+s*bw, by, bw/n+0.5, bh, fill=1, stroke=0)
c.setStrokeColor(C_DARK_GRAY); c.setLineWidth(0.5)
c.rect(bx, by, bw, bh, fill=0, stroke=1)
tks=[(0.0,"-1000\nAir"),(0.14,"0\nCSF"),(0.30,"~35\nBrain"),(0.52,"~70\nBlood"),(0.72,"~130\nCalc."),(1.0,">400\nBone")]
for f,lbl in tks:
tx=bx+f*bw
c.setStrokeColor(C_DARK_GRAY); c.setLineWidth(0.5); c.line(tx,by-0.05*cm,tx,by-0.22*cm)
c.setFillColor(C_DARK_GRAY); c.setFont("Helvetica",6.5)
for li,ln in enumerate(lbl.split('\n')):
c.drawCentredString(tx, by-0.3*cm-li*0.18*cm, ln)
c.setFillColor(C_RED); c.setFont("Helvetica-Bold",7)
c.drawCentredString(bx+0.52*bw, by+bh+0.15*cm, "BLOOD ~60-80 HU (BRIGHT)")
c.setFillColor(C_BLUE)
c.drawCentredString(bx+0.22*bw, by+bh+0.15*cm, "INFARCT <30 HU (DARK)")
# ══════════════════════════════════════════════════════════════════════════════
# BUILD PDF
# ══════════════════════════════════════════════════════════════════════════════
def build():
doc = SimpleDocTemplate(OUT_PATH, pagesize=A4,
leftMargin=2*cm, rightMargin=2*cm, topMargin=2*cm, bottomMargin=2*cm,
title="Brain CT & MRI: Bleed vs Infarct - Visual Reference Guide",
author="Orris Medical Reference")
story = []
# ── COVER ──────────────────────────────────────────────────────────────────
class Cover(Flowable):
def __init__(self):
Flowable.__init__(self)
self.w = AVAIL_W; self.h = 24*cm
def wrap(self,aw,ah): return self.w,self.h
def draw(self):
c=self.canv; w,h=self.w,self.h
c.setFillColor(C_NAVY); c.rect(0,h*0.42,w,h*0.58,fill=1,stroke=0)
c.setFillColor(C_BLUE); c.rect(0,h*0.28,w,h*0.14,fill=1,stroke=0)
c.setFillColor(colors.HexColor('#1A5276')); c.rect(0,0,w,h*0.28,fill=1,stroke=0)
c.setFillColor(C_WHITE); c.setFont("Helvetica-Bold",24)
c.drawCentredString(w/2,h*0.82,"BRAIN CT & MRI")
c.setFont("Helvetica-Bold",17)
c.drawCentredString(w/2,h*0.74,"VISUAL READING GUIDE")
c.setFont("Helvetica",13); c.setFillColor(colors.HexColor('#AED6F1'))
c.drawCentredString(w/2,h*0.66,"Hemorrhage • Infarction • Timeline")
c.drawCentredString(w/2,h*0.61,"Acute | Subacute | Chronic")
# Brain icon
bx=w/2; by=h*0.43; br=2.0*cm
c.setFillColor(colors.HexColor('#1A1A2E'))
c.circle(bx,by,br*1.18,fill=1,stroke=0)
c.setFillColor(colors.HexColor('#7F8C8D'))
c.circle(bx,by,br,fill=1,stroke=0)
c.setFillColor(colors.HexColor('#111111'))
c.ellipse(bx-0.3*cm,by-0.1*cm,bx+0.3*cm,by+0.38*cm,fill=1,stroke=0)
c.setFillColor(colors.HexColor('#F5F5F5'))
c.circle(bx+0.65*cm,by+0.35*cm,0.4*cm,fill=1,stroke=0)
c.setFillColor(colors.HexColor('#505050'))
c.circle(bx-0.6*cm,by+0.2*cm,0.35*cm,fill=1,stroke=0)
c.setFillColor(C_WHITE); c.setFont("Helvetica-Bold",7)
c.drawCentredString(bx+0.65*cm,by+0.15*cm,"BLEED")
c.drawCentredString(bx-0.6*cm,by+0.02*cm,"INFARCT")
# Bottom text
c.setFillColor(C_WHITE); c.setFont("Helvetica-Bold",11)
c.drawCentredString(w/2,h*0.22,"Real Textbook Images + Explanatory Diagrams")
c.setFont("Helvetica",9); c.setFillColor(colors.HexColor('#AED6F1'))
c.drawCentredString(w/2,h*0.16,"CT Density | MRI Sequences | DWI/ADC | SWI | Hemorrhage Types")
c.setFont("Helvetica-Oblique",8.5); c.setFillColor(colors.HexColor('#85C1E9'))
c.drawCentredString(w/2,h*0.1,"Sources: Adams & Victor's Neurology • Bradley & Daroff • Grainger & Allison")
c.setFont("Helvetica",7); c.setFillColor(colors.HexColor('#5D6D7E'))
c.drawCentredString(w/2,h*0.03,"Orris Medical Reference | For educational purposes only")
story.append(Cover())
story.append(PageBreak())
# ── PAGE 1: CT BASICS + HU SCALE ──────────────────────────────────────────
story.append(Banner("1. NON-CONTRAST CT BRAIN BASICS",
"First-line investigation for ALL stroke and head injury presentations", bg=C_NAVY))
story.append(Spacer(1,0.3*cm))
story.append(Paragraph("Hounsfield Unit (HU) Density Scale", H2_S))
story.append(Paragraph(
"Every CT pixel has a Hounsfield Unit (HU) value. Brain tissue is ~25-40 HU (gray). "
"<b>Blood clots are denser (~60-80 HU) and appear BRIGHT WHITE (hyperdense). "
"Infarcted brain loses fluid balance and appears DARK (hypodense, <30 HU).</b> "
"This single principle lets you differentiate bleed from infarct at a glance.", BODY_S))
story.append(HUBar(w=AVAIL_W, h=1.9*cm))
story.append(Spacer(1,0.4*cm))
story.append(Paragraph("CT Window Settings", H2_S))
wt = Table([
["Window","Width (WW)","Level (WL)","Use For"],
["Brain window","~80","~35","Parenchyma, hemorrhage, edema"],
["Bone window","~2000","~300","Skull fractures"],
["Stroke window","~35","~35","Early subtle ischemia - widens gray/white contrast"],
["Subdural window","~200","~60","Subdural hematoma detection"],
], colWidths=[3.5*cm,2.3*cm,2.3*cm,7.9*cm])
wt.setStyle(TableStyle([
('BACKGROUND',(0,0),(-1,0),C_BLUE),('TEXTCOLOR',(0,0),(-1,0),C_WHITE),
('FONTNAME',(0,0),(-1,0),'Helvetica-Bold'),('FONTSIZE',(0,0),(-1,-1),8),
('ROWBACKGROUNDS',(0,1),(-1,-1),[C_CREAM,C_LIGHT_GRAY]),
('GRID',(0,0),(-1,-1),0.4,C_MID_GRAY),('VALIGN',(0,0),(-1,-1),'MIDDLE'),
('TOPPADDING',(0,0),(-1,-1),3),('BOTTOMPADDING',(0,0),(-1,-1),3),
]))
story.append(wt)
story.append(Spacer(1,0.4*cm))
# ── REAL IMAGE: SAH + CT skull fracture ────────────────────────────────────
story.append(Paragraph("Real CT Example: Skull Fracture + Subarachnoid Hemorrhage", H2_S))
story.append(Paragraph(
"Below: axial CT scans showing bone window (fracture), brain window (hyperdense SAH in sulci), "
"and FLAIR MRI confirming subarachnoid blood as hyperintense signal in the cortical sulci.",BODY_S))
ipath = os.path.join(IMG_DIR, "img_skull_fracture_sah_ct.png")
if os.path.exists(ipath):
img = RLImage(ipath, width=AVAIL_W, height=5.5*cm)
img.hAlign = 'CENTER'
story.append(img)
story.append(Paragraph(
"Fig. A: Bone window - right temporal fracture (arrow). "
"Fig. B: Brain window - hyperdense contusion + SAH in sulci (arrowheads). "
"Fig. C: FLAIR MRI - hyperintense subarachnoid blood in sulci. "
"[Bradley & Daroff's Neurology in Clinical Practice]", CAPTION_S))
story.append(Spacer(1,0.2*cm))
story.append(Paragraph(
"<b>Key early CT signs of ischemic stroke:</b> "
"(1) Loss of gray-white differentiation at basal ganglia/insular cortex. "
"(2) <b>Insular ribbon sign</b> — blurring of the insular cortex (MCA territory). "
"(3) <b>Sulcal effacement</b> — swollen gyri obliterate normal sulci. "
"(4) <b>Hyperdense vessel sign</b> — bright MCA/basilar = acute thrombus. "
"(5) <b>One-third MCA rule</b> — >1/3 MCA hypodense = contraindication to tPA.", BODY_S))
story.append(PageBreak())
# ── PAGE 2: CT TIMELINE ────────────────────────────────────────────────────
story.append(Banner("2. CT TIMELINE — HEMORRHAGE vs. INFARCT",
"How CT density changes over time for each pathology", bg=C_RED))
story.append(Spacer(1,0.3*cm))
story.append(CTTimelineBar(w=AVAIL_W, h=3.8*cm))
story.append(Spacer(1,0.5*cm))
# ── REAL IMAGE: Subacute & chronic infarct on CT+MRI ──────────────────────
story.append(Paragraph("Real Images: Subacute & Chronic Infarct Progression", H2_S))
story.append(Paragraph(
"The panels below show progressive CT and MRI changes of an infarct as it matures: "
"CT becomes progressively more hypodense (darker) with volume loss and ventricular dilation; "
"MRI shows increasing T2/FLAIR hyperintensity and ultimately encephalomalacia.", BODY_S))
ipath2 = os.path.join(IMG_DIR, "img_subacute_chronic_infarct.png")
if os.path.exists(ipath2):
img2 = RLImage(ipath2, width=AVAIL_W, height=5.5*cm)
img2.hAlign = 'CENTER'
story.append(img2)
story.append(Paragraph(
"Subacute and chronic infarct: CT (A,B) shows progressive hypodensity and volume loss; "
"MRI T2/FLAIR (C,D) shows progressive hyperintensity; chronic stage (E) = encephalomalacia "
"approaching CSF signal. [Grainger & Allison's Diagnostic Radiology]", CAPTION_S))
story.append(Spacer(1,0.3*cm))
ct_detail = [
["Phase","Time","HEMORRHAGE on CT","INFARCT on CT"],
["Hyperacute","0-6 hrs","HYPERDENSE (white)\n~60-80 HU, immediate","Normal CT or sulcal effacement only"],
["Acute","6-24 hrs","HYPERDENSE\nPeripheral edema halo","Definite HYPODENSE territory\nGray-white differentiation lost"],
["Early Sub","1-7 days","Hyperdense → ISODENSE\nMass effect increasing","CLEARLY HYPODENSE\nMass effect peaks day 3-5"],
["Late Sub","1-3 wks","ISODENSE — easy to miss!\nCheck for displaced cortex","FOGGING EFFECT\nMay look falsely normal"],
["Chronic",">3 wks","HYPODENSE (dark)\nMay calcify; subdural = crescent","ENCEPHALOMALACIA\nEx-vacuo ventricular dilation"],
]
ctdt = Table(ct_detail, colWidths=[2.5*cm,1.8*cm,7.0*cm,4.7*cm])
ctdt.setStyle(TableStyle([
('BACKGROUND',(0,0),(-1,0),C_DARK_GRAY),('TEXTCOLOR',(0,0),(-1,0),C_WHITE),
('FONTNAME',(0,0),(-1,0),'Helvetica-Bold'),('FONTSIZE',(0,0),(-1,-1),7.5),
('BACKGROUND',(2,1),(2,-1),colors.HexColor('#FEF9E7')),
('BACKGROUND',(3,1),(3,-1),colors.HexColor('#EBF5FB')),
('ROWBACKGROUNDS',(0,1),(1,-1),[colors.HexColor('#FDEBD0'),colors.HexColor('#FEF9E7')]),
('GRID',(0,0),(-1,-1),0.4,C_MID_GRAY),('VALIGN',(0,0),(-1,-1),'TOP'),
('TOPPADDING',(0,0),(-1,-1),3),('BOTTOMPADDING',(0,0),(-1,-1),3),
]))
story.append(ctdt)
story.append(Spacer(1,0.15*cm))
story.append(Paragraph(
"<b>Pitfall:</b> At 7-14 days a subdural hematoma becomes ISODENSE with brain and can "
"be completely invisible on CT without contrast. Clues: displaced cortex, asymmetric sulci, "
"midline shift without visible hyperdensity.", WARN_S))
story.append(PageBreak())
# ── PAGE 3: HEMORRHAGE TYPES ───────────────────────────────────────────────
story.append(Banner("3. TYPES OF INTRACRANIAL HEMORRHAGE",
"Location determines shape, etiology, and clinical presentation", bg=C_RED))
story.append(Spacer(1,0.3*cm))
story.append(Paragraph("Real CT + MRI: Acute Subdural Hematoma", H2_S))
story.append(Paragraph(
"Acute SDH is typically hyperdense (white) on CT - the classic crescent shape "
"follows the brain surface and crosses suture lines. On MRI, acute SDH is "
"ISOINTENSE on T1 (hard to see) but HYPERINTENSE on T2 - explaining why T2 is "
"crucial for acute SDH detection.", BODY_S))
ipath3 = os.path.join(IMG_DIR, "img_acute_subdural_mri.png")
if os.path.exists(ipath3):
img3 = RLImage(ipath3, width=AVAIL_W*0.75, height=4.5*cm)
img3.hAlign = 'CENTER'
story.append(img3)
story.append(Paragraph(
"Acute Subdural Hematoma on MRI. A: T1 - subdural collection over left temporal lobe "
"is isointense to brain (arrowheads) - nearly invisible! B: T2 - the same collection is "
"now clearly HYPERINTENSE (bright). [Bradley & Daroff's Neurology in Clinical Practice]", CAPTION_S))
story.append(Spacer(1,0.3*cm))
hem_types = [
["Type","CT Shape","Location","Typical Cause","Key Features"],
["EDH\n(Epidural)","BICONVEX\n(lens-shaped)\nDoes NOT cross\nsutures","Between skull\n& dura","Middle meningeal\nartery (temporal\nfracture)","Lucid interval;\ntemporal fracture;\narterial speed"],
["SDH\n(Subdural)","CRESCENT\n(concave inner)\nCrosses sutures\nfreely","Between dura\n& brain","Bridging veins\n(shear force)","Elderly; falls;\nanticoag use;\n venous bleeder"],
["SAH","Fills sulci &\nbasal cisterns;\n'star' pattern","Subarachnoid\nspace","Berry aneurysm;\nAVM; trauma","Thunderclap HA;\nFisher grade;\nCSF xanthochromia"],
["ICH\n(Parenchymal)","Round/oval;\nwell defined","Basal ganglia;\nthalamus; pons;\ncerebellum","HTN (deep);\nAmyloid\nangiopathy\n(cortical)","Location predicts\ncause: deep=HTN;\ncortical=amyloid"],
]
htt = Table(hem_types, colWidths=[1.9*cm,2.8*cm,2.6*cm,3.2*cm,5.5*cm])
htt.setStyle(TableStyle([
('BACKGROUND',(0,0),(-1,0),C_RED),('TEXTCOLOR',(0,0),(-1,0),C_WHITE),
('FONTNAME',(0,0),(-1,0),'Helvetica-Bold'),('FONTSIZE',(0,0),(-1,-1),7.5),
('ROWBACKGROUNDS',(0,1),(-1,-1),[colors.HexColor('#FDEDEC'),colors.HexColor('#FEF9E7'),
colors.HexColor('#FEF5E7'),colors.HexColor('#FDFEFE')]),
('GRID',(0,0),(-1,-1),0.4,C_MID_GRAY),('VALIGN',(0,0),(-1,-1),'TOP'),
('TOPPADDING',(0,0),(-1,-1),3),('BOTTOMPADDING',(0,0),(-1,-1),3),
('FONTNAME',(0,1),(0,-1),'Helvetica-Bold'),('TEXTCOLOR',(0,1),(0,-1),C_RED),
]))
story.append(htt)
story.append(PageBreak())
# ── PAGE 4: MRI SEQUENCES ──────────────────────────────────────────────────
story.append(Banner("4. MRI SEQUENCES — WHAT EACH SHOWS",
"Understand each sequence before reading an MRI", bg=C_TEAL))
story.append(Spacer(1,0.3*cm))
mri_seq = [
["Sequence","Bright (hyperintense)","Dark (hypointense)","Best For"],
["T1","Fat, methemoglobin (subacute blood),\ngadolinium, protein-rich fluid","CSF, water, edema,\ndeoxyhemoglobin","Anatomy; subacute\nblood; contrast enhancement"],
["T2","CSF, water, edema, gliosis,\nlate subacute blood (extracel MetHb)","Acute blood (deoxy),\nhemosiderin, calcium","Infarcts, edema,\ntumors, inflammation"],
["FLAIR","Edema, gliosis, subarachnoid blood,\nperiventricular lesions","CSF (suppressed = BLACK)","SAH; periventricular MS;\ninfarct staging (DWI-FLAIR mismatch)"],
["DWI ★","ACUTE INFARCT (restricted diffusion)\nAbscess, epidermoid","Chronic lesions, blood\n(after several days)","ACUTE STROKE DIAGNOSIS\nPositive from 5 minutes!"],
["ADC ★","Vasogenic edema, chronic gliosis,\nfree diffusion","ACUTE INFARCT (dark = true\nrestriction), abscess","Confirm true restriction;\nexclude T2 shine-through"],
["SWI/GRE","Nothing significant","ALL blood products ('blooms')\ncalcification, iron, veins","Microhemorrhages;\nvenous thrombosis; amyloid"],
]
msqt = Table(mri_seq, colWidths=[2.0*cm,4.8*cm,3.8*cm,5.4*cm])
msqt.setStyle(TableStyle([
('BACKGROUND',(0,0),(-1,0),C_TEAL),('TEXTCOLOR',(0,0),(-1,0),C_WHITE),
('FONTNAME',(0,0),(-1,0),'Helvetica-Bold'),('FONTSIZE',(0,0),(-1,-1),7.5),
('ROWBACKGROUNDS',(0,1),(-1,-1),[colors.HexColor('#E8F8F5'),C_CREAM]*5),
('GRID',(0,0),(-1,-1),0.4,C_MID_GRAY),('VALIGN',(0,0),(-1,-1),'TOP'),
('TOPPADDING',(0,0),(-1,-1),3),('BOTTOMPADDING',(0,0),(-1,-1),3),
('FONTNAME',(0,4),(0,5),'Helvetica-Bold'),('TEXTCOLOR',(0,4),(0,5),C_TEAL),
]))
story.append(msqt)
story.append(Spacer(1,0.3*cm))
# ── REAL IMAGE: Acute MCA infarct DWI + ADC ────────────────────────────────
story.append(Paragraph("Real MRI: Acute MCA Infarct — DWI vs ADC", H2_S))
story.append(Paragraph(
"The DWI-ADC pair is the single most important MRI finding in stroke. "
"<b>DWI bright + ADC dark = restricted diffusion = acute infarction confirmed.</b> "
"This is positive within minutes of onset — far earlier than CT.", BODY_S))
ipath4 = os.path.join(IMG_DIR, "img_acute_mca_infarct_dwi.png")
if os.path.exists(ipath4):
img4 = RLImage(ipath4, width=AVAIL_W*0.85, height=5.0*cm)
img4.hAlign = 'CENTER'
story.append(img4)
story.append(Paragraph(
"Acute left MCA infarct. A: DWI — large BRIGHT hyperintense territory with sulcal effacement "
"and midline shift. B: ADC map — corresponding DARK hypointensity confirming restricted "
"diffusion = irreversible infarction. [Bradley & Daroff's Neurology in Clinical Practice]", CAPTION_S))
story.append(Spacer(1,0.25*cm))
story.append(Paragraph(
"<b>DWI-ADC Rule:</b><br/>"
"• DWI BRIGHT + ADC DARK = TRUE restricted diffusion = ACUTE INFARCT ✓<br/>"
"• DWI BRIGHT + ADC normal/bright = T2 shine-through = NOT acute infarct<br/>"
"• DWI DARK + ADC BRIGHT = chronic gliosis / encephalomalacia", CALLOUT_S))
story.append(PageBreak())
# ── PAGE 5: ACUTE STROKE CT + MRI COMPARISON ──────────────────────────────
story.append(Banner("5. ACUTE STROKE: CT vs. MRI — REAL IMAGES",
"Why MRI (DWI) is superior to CT in the first 6 hours", bg=C_LIGHTBLUE))
story.append(Spacer(1,0.3*cm))
story.append(Paragraph("Real Images: Acute MCA Infarct on CT AND MRI", H2_S))
story.append(Paragraph(
"This case shows the same left MCA territory infarct imaged at 7 hours after onset. "
"CT already shows subtle hypodensity in the frontal operculum and insula (A). "
"T2/FLAIR shows hyperintensity and gyral swelling (B, C). "
"But the FULL extent — including basal ganglia involvement — is ONLY clearly "
"visible on DWI (D) and its ADC map (E). This is why DWI is the gold standard.", BODY_S))
ipath5 = os.path.join(IMG_DIR, "img_acute_stroke_ct_mri.png")
if os.path.exists(ipath5):
img5 = RLImage(ipath5, width=AVAIL_W, height=5.2*cm)
img5.hAlign = 'CENTER'
story.append(img5)
story.append(Paragraph(
"Acute left MCA infarct at 7 hours. A: CT — low attenuation in left frontal operculum/insula (subtle). "
"B,C: T2/FLAIR — hyperintensity + gyral swelling (more than CT shows). D: DWI — full territory bright "
"including basal ganglia. E: ADC — corresponding dark hypointensity confirms restriction. "
"[Grainger & Allison's Diagnostic Radiology]", CAPTION_S))
story.append(Spacer(1,0.3*cm))
story.append(Paragraph("DWI-FLAIR Mismatch — The Wake-Up Stroke Rule", H2_S))
story.append(Paragraph(
"In patients who wake up with a stroke (unknown onset time), MRI can estimate "
"whether the infarct is within the treatment window. "
"<b>DWI bright but FLAIR normal/subtle = infarct is likely <4.5 hours old = can give tPA.</b> "
"If both DWI and FLAIR are bright = infarct is likely >4.5-6 hours.", BODY_S))
ipath6 = os.path.join(IMG_DIR, "img_wake_up_stroke_dwi_flair.png")
if os.path.exists(ipath6):
img6 = RLImage(ipath6, width=AVAIL_W, height=5.5*cm)
img6.hAlign = 'CENTER'
story.append(img6)
story.append(Paragraph(
"Wake-up strokes. Top row (A-D): Large DWI infarct + clear FLAIR signal + no perfusion mismatch "
"→ beyond treatment window. Bottom row (E-G): Small DWI infarct in left insula + minimal FLAIR "
"signal (within 4.5h window) + large CT perfusion mismatch → treated with tPA. "
"[Grainger & Allison's Diagnostic Radiology]", CAPTION_S))
story.append(PageBreak())
# ── PAGE 6: MRI HEMORRHAGE TIMELINE ───────────────────────────────────────
story.append(Banner("6. MRI HEMORRHAGE SIGNAL TIMELINE",
"Signal evolves as hemoglobin changes form — know each stage", bg=C_PURPLE))
story.append(Spacer(1,0.3*cm))
story.append(MRIHemGrid(w=AVAIL_W, h=5.5*cm))
story.append(Spacer(1,0.5*cm))
# ── REAL IMAGES: Acute parenchymal hemorrhage ──────────────────────────────
story.append(Paragraph("Real MRI: Acute Parenchymal Hemorrhage (T1 + T2)", H2_S))
story.append(Paragraph(
"In ACUTE hemorrhage (deoxyhemoglobin stage, days 1-3): T1 shows the hematoma as "
"ISOINTENSE (gray — hard to see!). T2 shows it as STRIKINGLY HYPOINTENSE (dark/black) "
"due to deoxyhemoglobin's paramagnetic effect. Surrounding edema is hyperintense on T2.", BODY_S))
ipath7 = os.path.join(IMG_DIR, "img_acute_parenchymal_hemorrhage.png")
if os.path.exists(ipath7):
img7 = RLImage(ipath7, width=AVAIL_W, height=6.0*cm)
img7.hAlign = 'CENTER'
story.append(img7)
story.append(Paragraph(
"Two cases of acute parenchymal hemorrhage. A,B: Large acute left basal ganglia hemorrhage — "
"T1 (A) shows faint hypointensity; T2 (B) shows STRIKING DARK hypointensity with bright surrounding "
"edema + mass effect. C,D: More advanced stage — T1 (C) center turning bright (intracellular MetHb "
"developing, arrowheads); T2 (D) still dark. Note ventricular hemorrhage (arrows) = poor prognosis. "
"[Bradley & Daroff's Neurology in Clinical Practice]", CAPTION_S))
story.append(PageBreak())
# ── PAGE 7: SUBACUTE + CHRONIC HEMORRHAGE ─────────────────────────────────
story.append(Banner("7. SUBACUTE & CHRONIC HEMORRHAGE ON MRI",
"Methemoglobin and hemosiderin — the characteristic MRI signatures", bg=C_PURPLE))
story.append(Spacer(1,0.3*cm))
story.append(Paragraph("Real MRI: Late Subacute Hemorrhage (Extracellular MetHb)", H2_S))
story.append(Paragraph(
"In the LATE SUBACUTE stage (7-30+ days), RBCs lyse and methemoglobin spills "
"into the extracellular space. This is the classic 'bright on T1 AND bright on T2' stage. "
"A dark hemosiderin rim beginning to form at the periphery on T2 is an early sign of transition "
"to the chronic phase.", BODY_S))
# Side-by-side: T1 and T2 of late subacute
ip_t1 = os.path.join(IMG_DIR, "img_late_subacute_hemorrhage_t1.png")
ip_t2 = os.path.join(IMG_DIR, "img_late_subacute_hemorrhage_t2.png")
row_imgs = []
cell1 = []
cell2 = []
if os.path.exists(ip_t1):
i_t1 = RLImage(ip_t1, width=AVAIL_W*0.48, height=4.8*cm)
i_t1.hAlign='CENTER'
cell1 = [i_t1, Paragraph("T1: BRIGHT (hyperintense)\nHomogeneous white signal\n= Extracellular MetHb", CAPTION_S)]
if os.path.exists(ip_t2):
i_t2 = RLImage(ip_t2, width=AVAIL_W*0.48, height=4.8*cm)
i_t2.hAlign='CENTER'
cell2 = [i_t2, Paragraph("T2: BRIGHT (hyperintense)\nAlso white signal\n+ dark hemosiderin rim (arrowheads)", CAPTION_S)]
if cell1 and cell2:
t = Table([[cell1, cell2]], colWidths=[AVAIL_W*0.5, AVAIL_W*0.5])
t.setStyle(TableStyle([('VALIGN',(0,0),(-1,-1),'TOP'),('ALIGN',(0,0),(-1,-1),'CENTER'),
('TOPPADDING',(0,0),(-1,-1),2),('BOTTOMPADDING',(0,0),(-1,-1),2)]))
story.append(t)
story.append(Paragraph(
"Late Subacute Hematoma in right corona radiata — T1 (left) AND T2 (right) both show "
"BRIGHT homogeneous signal = extracellular methemoglobin. Note early dark hemosiderin "
"rim on T2 (arrowheads) signaling transition to chronic phase. "
"[Bradley & Daroff's Neurology in Clinical Practice]", CAPTION_S))
story.append(Spacer(1,0.3*cm))
story.append(Paragraph("Real MRI: Chronic Hemorrhage — Hemosiderin Ring", H2_S))
story.append(Paragraph(
"In CHRONIC hemorrhage (>4 weeks), macrophages phagocytose the blood products and "
"deposit hemosiderin and ferritin. This creates a characteristic DARK RIM on T2 and GRE/SWI. "
"The center forms a CSF-like cavity. The dark rim NEVER disappears — it permanently "
"marks the site of prior hemorrhage.", BODY_S))
ip_c1 = os.path.join(IMG_DIR, "img_chronic_hemorrhage_t2.png")
ip_c2 = os.path.join(IMG_DIR, "img_chronic_hemorrhage_gre.png")
cell3=[]; cell4=[]
if os.path.exists(ip_c1):
ic1 = RLImage(ip_c1, width=AVAIL_W*0.48, height=4.5*cm); ic1.hAlign='CENTER'
cell3=[ic1, Paragraph("T2: Slit-like CSF cavity (bright center)\n+ DARK hemosiderin rim (arrowheads)", CAPTION_S)]
if os.path.exists(ip_c2):
ic2 = RLImage(ip_c2, width=AVAIL_W*0.48, height=4.5*cm); ic2.hAlign='CENTER'
cell4=[ic2, Paragraph("GRE (T2*): Markedly DARK hemosiderin\ndeposition — 'blooming' effect\n(more prominent than on T2)", CAPTION_S)]
if cell3 and cell4:
t2 = Table([[cell3, cell4]], colWidths=[AVAIL_W*0.5, AVAIL_W*0.5])
t2.setStyle(TableStyle([('VALIGN',(0,0),(-1,-1),'TOP'),('ALIGN',(0,0),(-1,-1),'CENTER'),
('TOPPADDING',(0,0),(-1,-1),2),('BOTTOMPADDING',(0,0),(-1,-1),2)]))
story.append(t2)
story.append(Paragraph(
"Chronic parietal lobe hemorrhage. T2 (left): slitlike cavity with CSF-bright center + "
"dark hemosiderin rim (arrowheads). GRE (right): markedly hypointense hemosiderin with "
"'blooming' artifact — GRE/SWI is MORE sensitive than T2 for blood products. "
"[Bradley & Daroff's Neurology in Clinical Practice]", CAPTION_S))
story.append(PageBreak())
# ── PAGE 8: MRI INFARCT TIMELINE + HAEMORRHAGIC TRANSFORMATION ────────────
story.append(Banner("8. MRI INFARCT TIMELINE + HAEMORRHAGIC TRANSFORMATION",
"Evolution of ischemic infarct and the complication of hemorrhagic conversion", bg=C_TEAL))
story.append(Spacer(1,0.3*cm))
story.append(MRIInfarctGrid(w=AVAIL_W, h=4.5*cm))
story.append(Spacer(1,0.45*cm))
story.append(Paragraph("Real MRI: Haemorrhagic Transformation of Infarct", H2_S))
story.append(Paragraph(
"Haemorrhagic transformation (HT) occurs in up to 80% of infarcts on MRI. "
"It is more common in large infarcts, cardioembolic strokes, after thrombolysis, "
"and in diabetics. CT shows hyperdense blood within the infarct. "
"SWI is exquisitely sensitive — it detects blood products as strikingly dark 'blooming' "
"even when CT shows only subtle hyperdensity.", BODY_S))
ipath8 = os.path.join(IMG_DIR, "img_haemorrhagic_transformation.png")
if os.path.exists(ipath8):
img8 = RLImage(ipath8, width=AVAIL_W, height=5.5*cm)
img8.hAlign='CENTER'
story.append(img8)
story.append(Paragraph(
"Haemorrhagic Transformation. A: DWI shows large right MCA territory infarct. B: Follow-up "
"CT at 60 hours — acute haemorrhage (hyperdense) within the infarct + midline shift. "
"C: Separate case — large left MCA infarct. D: SWI shows markedly dark 'blooming' blood "
"products in the basal ganglia (white arrows) — barely visible on CT (C). "
"[Grainger & Allison's Diagnostic Radiology]", CAPTION_S))
story.append(Spacer(1,0.3*cm))
mri_inf_detail = [
["Phase","Time","DWI","ADC","T2/FLAIR","Clinical Action"],
["Hyperacute","0-6 hrs","BRIGHT ★\n(5 min+)","DARK ★","Normal/subtle","DWI+ADC− = confirmed infarct\nStart tPA workup immediately"],
["Acute","6h-5d","BRIGHT ★","DARK ★","BRIGHT\n+mass effect","Full DWI extent = treatment target\nMass effect may mimic tumor"],
["Subacute","5-14d","Fading\n(T2 shine-through)","PSEUDO-\nNORMAL\n~day7-10","BRIGHT\n+gyral enhance","ADC trap! Still bright on T2/FLAIR.\nCheck for haemorrhagic transformation"],
["Chronic",">3wks","DARK","BRIGHT\n(free)","BRIGHT\n(gliosis)","Encephalomalacia.\nEx-vacuo ventricular enlargement"],
]
midt = Table(mri_inf_detail, colWidths=[2.2*cm,1.8*cm,2.0*cm,2.0*cm,2.5*cm,5.5*cm])
midt.setStyle(TableStyle([
('BACKGROUND',(0,0),(-1,0),C_TEAL),('TEXTCOLOR',(0,0),(-1,0),C_WHITE),
('FONTNAME',(0,0),(-1,0),'Helvetica-Bold'),('FONTSIZE',(0,0),(-1,-1),7.5),
('ROWBACKGROUNDS',(0,1),(-1,-1),[colors.HexColor('#E8F8F5'),C_CREAM]*4),
('BACKGROUND',(2,1),(3,2),colors.HexColor('#F5F5F5')),
('BACKGROUND',(3,1),(3,2),colors.HexColor('#222222')),('TEXTCOLOR',(3,1),(3,2),C_WHITE),
('GRID',(0,0),(-1,-1),0.4,C_MID_GRAY),('VALIGN',(0,0),(-1,-1),'TOP'),
('TOPPADDING',(0,0),(-1,-1),3),('BOTTOMPADDING',(0,0),(-1,-1),3),
]))
story.append(midt)
story.append(PageBreak())
# ── PAGE 9: MASTER COMPARISON + WORKFLOW ──────────────────────────────────
story.append(Banner("9. MASTER QUICK-REFERENCE + EMERGENCY WORKFLOW",
"Summary comparison and step-by-step imaging algorithm", bg=C_NAVY))
story.append(Spacer(1,0.3*cm))
master = [
["Feature","HEMORRHAGE","INFARCT"],
["CT (acute)","HYPERDENSE (bright white)\n~60-80 HU","HYPODENSE (dark)\n<30 HU after 6 hrs"],
["CT visibility","Immediately visible","Subtle <6 hrs; clear >6 hrs"],
["CT shape","Round/oval, well-defined","Wedge, follows vascular territory"],
["Location","Deep (HTN): BG, thalamus, pons\nCortical: amyloid angiopathy","Cortex+subcortex; arterial territory"],
["MRI T1","Iso → Iso → BRIGHT → BRIGHT → Dark","Normal → Hypointense"],
["MRI T2","Bright → DARK → DARK → BRIGHT → BLACK","Normal → BRIGHT (permanent gliosis)"],
["MRI DWI","Dark (blood suppresses DWI)","BRIGHT ★ from 5 minutes onset"],
["MRI ADC","Variable (not useful for blood)","DARK ★ (confirmed restriction)"],
["SWI/GRE","BLACK 'blooms' — all stages","Negative (unless haemorrhagic Tx)"],
["Mass effect","Yes, immediate + edema halo","Peaks day 3-5 in large infarcts"],
["Chronic","Hypodense cavity; hemosiderin ring\n(permanent dark rim on MRI)","Encephalomalacia; ex-vacuo dilation"],
]
mt = Table(master, colWidths=[3.5*cm,6.5*cm,6.0*cm])
mt.setStyle(TableStyle([
('BACKGROUND',(0,0),(-1,0),C_NAVY),('TEXTCOLOR',(0,0),(-1,0),C_WHITE),
('FONTNAME',(0,0),(-1,0),'Helvetica-Bold'),('FONTSIZE',(0,0),(-1,-1),8),
('BACKGROUND',(1,1),(1,-1),colors.HexColor('#FDEDEC')),
('BACKGROUND',(2,1),(2,-1),colors.HexColor('#EBF5FB')),
('ROWBACKGROUNDS',(0,1),(0,-1),[C_LIGHT_GRAY,colors.HexColor('#D5D8DC')]*8),
('GRID',(0,0),(-1,-1),0.4,C_MID_GRAY),('VALIGN',(0,0),(-1,-1),'TOP'),
('TOPPADDING',(0,0),(-1,-1),3),('BOTTOMPADDING',(0,0),(-1,-1),3),
('FONTNAME',(0,1),(0,-1),'Helvetica-Bold'),
]))
story.append(mt)
story.append(Spacer(1,0.3*cm))
story.append(Paragraph("Emergency Imaging Workflow", H2_S))
wf = [
["Step","Modality","Question","Action"],
["1","NCCT Brain","Hemorrhage? Mass?","YES bleed → no tPA; manage per bleed type\nNO bleed → proceed to step 2"],
["2","CT Angiography\n(arch→skull)","Large Vessel\nOcclusion (LVO)?","LVO present → thrombectomy pathway\nLook for hyperdense vessel sign"],
["3","CT Perfusion\n(if available)","Penumbra vs core?\nSalvageable tissue?","Large mismatch → extended window thrombectomy\nSmall/no mismatch → conservative"],
["4","MRI DWI+ADC\n+FLAIR+SWI","Confirm infarct?\nAge? Microbleeds?","DWI+/ADC− = acute infarct confirmed\nDWI-FLAIR mismatch = <4.5h → tPA eligible\nSWI dark = haemorrhagic component"],
["5","MRA/CTA\nfollow-up","Recanalisation?","Assess response; plan secondary prevention"],
]
wft = Table(wf, colWidths=[0.8*cm,3.0*cm,3.8*cm,8.4*cm])
wft.setStyle(TableStyle([
('BACKGROUND',(0,0),(-1,0),C_GREEN),('TEXTCOLOR',(0,0),(-1,0),C_WHITE),
('FONTNAME',(0,0),(-1,0),'Helvetica-Bold'),('FONTSIZE',(0,0),(-1,-1),7.5),
('ROWBACKGROUNDS',(0,1),(-1,-1),[colors.HexColor('#EAFAF1'),C_CREAM]*4),
('GRID',(0,0),(-1,-1),0.4,C_MID_GRAY),('VALIGN',(0,0),(-1,-1),'TOP'),
('TOPPADDING',(0,0),(-1,-1),3),('BOTTOMPADDING',(0,0),(-1,-1),3),
('FONTNAME',(0,1),(0,-1),'Helvetica-Bold'),('TEXTCOLOR',(0,1),(0,-1),C_GREEN),
('ALIGN',(0,1),(0,-1),'CENTER'),
]))
story.append(wft)
story.append(Spacer(1,0.2*cm))
story.append(HRFlowable(width=AVAIL_W, thickness=0.5, color=C_MID_GRAY))
story.append(Paragraph(
"<b>Sources:</b> Adams and Victor's Principles of Neurology 12th Ed. | "
"Bradley and Daroff's Neurology in Clinical Practice | "
"Grainger & Allison's Diagnostic Radiology | "
"Neuroanatomy through Clinical Cases 3rd Ed. | "
"Plum & Posner's Stupor & Coma | Goldman-Cecil Medicine", NOTE_S))
# ── Footer ─────────────────────────────────────────────────────────────────
def footer(canvas, doc):
canvas.saveState()
canvas.setFont("Helvetica", 8); canvas.setFillColor(C_MID_GRAY)
canvas.drawCentredString(PAGE_W/2, 1.2*cm,
f"Brain CT & MRI Visual Reference Guide | Page {doc.page}")
canvas.setFillColor(C_LIGHTBLUE)
canvas.rect(2*cm, 1.0*cm, PAGE_W-4*cm, 0.04*cm, fill=1, stroke=0)
canvas.restoreState()
doc.build(story, onFirstPage=footer, onLaterPages=footer)
print(f"✓ PDF saved: {OUT_PATH}")
import os as _os
sz = _os.path.getsize(OUT_PATH)
print(f" Size: {sz/1024:.0f} KB | Path: {OUT_PATH}")
build()
cd /home/daytona/workspace/brain-imaging-guide && python build_pdf_v2.py 2>&1
python3 -c "from pypdf import PdfReader; r=PdfReader('/home/daytona/workspace/brain-imaging-guide/Brain_CT_MRI_Reference_Guide_v2.pdf'); print(f'Pages: {len(r.pages)}')"
Brain CT MRI Reference Guide v2
PDF Document · PDF
| Page | Real Images Included |
|---|---|
| 1 - CT Basics | Real CT (bone window fracture + brain window SAH + FLAIR MRI) — skull fracture + subarachnoid hemorrhage case |
| 2 - CT Timeline | Real multi-panel CT/MRI showing subacute and chronic infarct progression with volume loss |
| 3 - Hemorrhage Types | Real MRI of acute subdural hematoma — T1 (nearly invisible, isointense) vs T2 (clearly bright) |
| 4 - MRI Sequences | Real MRI: acute MCA infarct — DWI (bright) vs ADC map (dark) side-by-side |
| 5 - Acute Stroke CT vs MRI | Real 5-panel case: CT + T2 + FLAIR + DWI + ADC of the same MCA infarct at 7 hours; PLUS wake-up stroke DWI-FLAIR mismatch cases |
| 6 - MRI Hemorrhage Timeline | Real MRI: acute basal ganglia hemorrhage — T1 (isointense, subtle) vs T2 (strikingly dark) — two cases at different stages |
| 7 - Subacute & Chronic Hemorrhage | Real MRI: late subacute hemorrhage (T1 bright + T2 bright = MetHb); chronic hemorrhage (T2 slit cavity + dark rim; GRE blooming) |
| 8 - Infarct Timeline + HT | Real 4-panel case of hemorrhagic transformation: DWI infarct → CT hemorrhage → SWI blooming |
| 9 - Master Reference | Summary tables + emergency workflow |