Pdf of stroke imaging from textbook
mkdir -p /home/daytona/workspace/stroke-imaging && ls /home/daytona/workspace/stroke-imaging/
~/stroke-imaging/generate_pdf.py
#!/usr/bin/env python3
"""Generate a comprehensive stroke imaging PDF from textbook content."""
import requests
import io
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import cm, mm
from reportlab.lib.colors import HexColor, black, white, Color
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Image, Table, TableStyle,
PageBreak, HRFlowable, KeepTogether
)
from reportlab.platypus.tableofcontents import TableOfContents
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
from reportlab.pdfgen import canvas
from reportlab.pdfbase import pdfmetrics
from PIL import Image as PILImage
OUTPUT = "/home/daytona/workspace/stroke-imaging/Stroke_Imaging_Textbook.pdf"
# Colors
DARK_BLUE = HexColor("#1a3a5c")
MED_BLUE = HexColor("#2e6da4")
LIGHT_BLUE = HexColor("#d6e9f8")
ACCENT = HexColor("#e84040")
GRAY = HexColor("#555555")
LIGHT_GRAY = HexColor("#f5f5f5")
styles = getSampleStyleSheet()
# Custom styles
title_style = ParagraphStyle(
"CoverTitle", parent=styles["Title"],
fontSize=32, textColor=white, leading=40, alignment=TA_CENTER, spaceAfter=10
)
subtitle_style = ParagraphStyle(
"CoverSub", parent=styles["Normal"],
fontSize=14, textColor=HexColor("#cce4f7"), alignment=TA_CENTER, spaceAfter=6
)
h1_style = ParagraphStyle(
"H1", parent=styles["Heading1"],
fontSize=18, textColor=DARK_BLUE, leading=22, spaceBefore=18, spaceAfter=8,
borderPad=4, borderColor=DARK_BLUE
)
h2_style = ParagraphStyle(
"H2", parent=styles["Heading2"],
fontSize=13, textColor=MED_BLUE, leading=17, spaceBefore=14, spaceAfter=6
)
h3_style = ParagraphStyle(
"H3", parent=styles["Heading3"],
fontSize=11, textColor=DARK_BLUE, leading=15, spaceBefore=10, spaceAfter=4
)
body_style = ParagraphStyle(
"Body", parent=styles["Normal"],
fontSize=10, leading=15, textColor=HexColor("#222222"), alignment=TA_JUSTIFY,
spaceAfter=8
)
caption_style = ParagraphStyle(
"Caption", parent=styles["Normal"],
fontSize=8.5, leading=12, textColor=GRAY, alignment=TA_CENTER,
spaceBefore=4, spaceAfter=12, fontName="Helvetica-Oblique"
)
source_style = ParagraphStyle(
"Source", parent=styles["Normal"],
fontSize=8, leading=11, textColor=MED_BLUE, alignment=TA_CENTER, spaceBefore=2
)
callout_style = ParagraphStyle(
"Callout", parent=styles["Normal"],
fontSize=9.5, leading=14, textColor=DARK_BLUE, leftIndent=10, rightIndent=10,
spaceBefore=6, spaceAfter=6
)
def fetch_image_flowable(url, width_cm=12, caption=None, source=None):
"""Download an image and return a list of flowables."""
flowables = []
try:
resp = requests.get(url, timeout=15)
resp.raise_for_status()
img_data = io.BytesIO(resp.content)
pil_img = PILImage.open(img_data)
orig_w, orig_h = pil_img.size
target_w = width_cm * cm
aspect = orig_h / orig_w
target_h = target_w * aspect
# Cap height
max_h = 8 * cm
if target_h > max_h:
target_h = max_h
target_w = target_h / aspect
img_data.seek(0)
img = Image(img_data, width=target_w, height=target_h)
img.hAlign = "CENTER"
flowables.append(img)
if caption:
flowables.append(Paragraph(caption, caption_style))
if source:
flowables.append(Paragraph(f"<i>Source: {source}</i>", source_style))
except Exception as e:
flowables.append(Paragraph(f"[Image not available: {e}]", caption_style))
return flowables
def make_callout(text, bg_color=LIGHT_BLUE):
"""Create a highlighted callout box."""
data = [[Paragraph(text, callout_style)]]
t = Table(data, colWidths=[16 * cm])
t.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, -1), bg_color),
("ROUNDEDCORNERS", [4, 4, 4, 4]),
("LEFTPADDING", (0, 0), (-1, -1), 12),
("RIGHTPADDING", (0, 0), (-1, -1), 12),
("TOPPADDING", (0, 0), (-1, -1), 8),
("BOTTOMPADDING", (0, 0), (-1, -1), 8),
("BOX", (0, 0), (-1, -1), 1, MED_BLUE),
]))
return t
def make_table(headers, rows, col_widths=None):
"""Create a styled table."""
data = [[Paragraph(str(h), ParagraphStyle("TH", parent=styles["Normal"],
fontSize=9, textColor=white, fontName="Helvetica-Bold")) for h in headers]]
for row in rows:
data.append([Paragraph(str(c), ParagraphStyle("TD", parent=styles["Normal"],
fontSize=9, leading=13)) for c in row])
if not col_widths:
col_widths = [16 * cm / len(headers)] * len(headers)
t = Table(data, colWidths=col_widths)
t.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, 0), DARK_BLUE),
("ROWBACKGROUNDS", (0, 1), (-1, -1), [white, LIGHT_GRAY]),
("GRID", (0, 0), (-1, -1), 0.5, HexColor("#bbbbbb")),
("TOPPADDING", (0, 0), (-1, -1), 5),
("BOTTOMPADDING", (0, 0), (-1, -1), 5),
("LEFTPADDING", (0, 0), (-1, -1), 7),
("RIGHTPADDING", (0, 0), (-1, -1), 7),
("VALIGN", (0, 0), (-1, -1), "TOP"),
]))
return t
class NumberedCanvas(canvas.Canvas):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._saved_page_states = []
def showPage(self):
self._saved_page_states.append(dict(self.__dict__))
self._startPage()
def save(self):
num_pages = len(self._saved_page_states)
for state in self._saved_page_states:
self.__dict__.update(state)
self.draw_page_number(num_pages)
super().showPage()
super().save()
def draw_page_number(self, page_count):
page_num = self._saved_page_states.index(dict(self.__dict__)) + 1 if dict(self.__dict__) in self._saved_page_states else 0
# Footer bar
self.setFillColor(DARK_BLUE)
self.rect(0, 0, A4[0], 1.2 * cm, fill=1, stroke=0)
self.setFillColor(white)
self.setFont("Helvetica", 8)
self.drawCentredString(A4[0] / 2, 0.4 * cm, f"Stroke Imaging — Medical Textbook Reference")
self.setFont("Helvetica-Bold", 8)
self.drawRightString(A4[0] - 1.5 * cm, 0.4 * cm, f"Page {self._pageNumber}")
def build_cover(elements):
# Cover page with blue background table
cover_data = [[
Paragraph("STROKE IMAGING", title_style),
Paragraph("A Comprehensive Medical Textbook Reference", subtitle_style),
Paragraph("CT · MRI · DWI · CTA · Perfusion Imaging", subtitle_style),
Paragraph(" ", subtitle_style),
Paragraph("Sources: Adams & Victor's Principles of Neurology (12e) ·<br/>"
"Grainger & Allison's Diagnostic Radiology · "
"Bradley & Daroff's Neurology in Clinical Practice", subtitle_style),
]]
# Simple approach: blue rect via table
cover = Table([[Paragraph(
'<font color="white"><b>STROKE IMAGING</b></font><br/>'
'<font color="#cce4f7" size="14">A Comprehensive Medical Textbook Reference</font><br/><br/>'
'<font color="#cce4f7" size="12">CT · MRI · DWI · CTA · Perfusion Imaging</font><br/><br/>'
'<font color="#aacde8" size="9">Sources: Adams & Victor\'s Principles of Neurology (12e) · '
'Grainger & Allison\'s Diagnostic Radiology · '
'Bradley & Daroff\'s Neurology in Clinical Practice</font>',
ParagraphStyle("CoverBox", parent=styles["Normal"], fontSize=28,
leading=38, alignment=TA_CENTER, spaceAfter=0)
)]],
colWidths=[17 * cm],
rowHeights=[14 * cm]
)
cover.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, -1), DARK_BLUE),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("LEFTPADDING", (0, 0), (-1, -1), 24),
("RIGHTPADDING", (0, 0), (-1, -1), 24),
("TOPPADDING", (0, 0), (-1, -1), 30),
("BOTTOMPADDING", (0, 0), (-1, -1), 30),
("ROUNDEDCORNERS", [8, 8, 8, 8]),
]))
elements.append(Spacer(1, 3 * cm))
elements.append(cover)
elements.append(PageBreak())
def build_contents(elements):
"""Build the main document content."""
# ─────────────────────────────────────────────
# SECTION 1: OVERVIEW OF STROKE IMAGING
# ─────────────────────────────────────────────
elements.append(Paragraph("1. Overview of Stroke Imaging", h1_style))
elements.append(HRFlowable(width="100%", thickness=2, color=MED_BLUE, spaceAfter=8))
elements.append(Paragraph(
"Advances in neuroimaging have transformed the clinical evaluation of patients with stroke, "
"allowing simultaneous demonstration of the cerebral lesion and the affected blood vessel. "
"Choosing the right imaging modality — and interpreting it correctly — is essential for "
"triage, treatment selection, and prognostication.",
body_style
))
elements.append(make_callout(
"<b>Key principle:</b> In suspected acute stroke, imaging serves four goals: "
"(1) exclude haemorrhage, (2) confirm ischaemia and define its territory, "
"(3) identify the occluded vessel (large vessel occlusion, LVO), and "
"(4) assess the ischaemic penumbra to guide reperfusion therapy."
))
elements.append(Spacer(1, 0.3 * cm))
# ─────────────────────────────────────────────
# SECTION 2: CT IMAGING IN ACUTE STROKE
# ─────────────────────────────────────────────
elements.append(Paragraph("2. CT Imaging in Acute Stroke", h1_style))
elements.append(HRFlowable(width="100%", thickness=2, color=MED_BLUE, spaceAfter=8))
elements.append(Paragraph("2.1 Non-Enhanced CT (NECT)", h2_style))
elements.append(Paragraph(
"Non-enhanced computed tomography (NECT) remains the standard first-line imaging investigation "
"in acute stroke assessment because of its wide availability, speed, and sensitivity to haemorrhage. "
"The primary objectives of NECT are:",
body_style
))
nect_obj = [
["Objective", "Clinical Relevance"],
["Exclude intracranial haemorrhage", "Determines eligibility for thrombolysis and thrombectomy"],
["Exclude non-vascular cause (~30% of stroke-like episodes)", "Tumours, abscesses, demyelination may mimic stroke"],
["Assess infarct volume (ASPECTS score)", "ASPECTS < 6 predicts poor outcome; guides thrombectomy eligibility"],
["Detect hyperdense vessel sign", "Suggests large vessel occlusion (MCA, ICA, basilar)"],
["Assess for early ischaemic changes", "Loss of grey-white differentiation, sulcal effacement, insular ribbon sign"],
]
elements.append(make_table(nect_obj[0], nect_obj[1:], col_widths=[8*cm, 8*cm]))
elements.append(Spacer(1, 0.4 * cm))
elements.append(Paragraph("2.2 ASPECTS Score", h2_style))
elements.append(Paragraph(
"The Alberta Stroke Program Early CT Score (ASPECTS) is a systematic 10-point scoring method "
"applied to NECT to quantify the extent of early ischaemic changes in the MCA territory. "
"Each of 10 predefined regions scores 1 point; a point is subtracted for each region showing "
"early ischaemic change. A score of 10 is normal; a score of 0 indicates diffuse ischaemia "
"across the entire MCA territory. An ASPECTS score <6 is associated with poor functional "
"outcome and is used as a threshold in many thrombectomy trials.",
body_style
))
elements.append(make_callout(
"<b>Clinical tip (from Grainger & Allison):</b> Around 30% of patients presenting with a "
"stroke-like episode have a non-vascular cause. NECT is essential to identify these mimics "
"before administering thrombolytic therapy."
))
elements.append(Spacer(1, 0.3 * cm))
elements.append(Paragraph("2.3 CT Angiography (CTA)", h2_style))
elements.append(Paragraph(
"CT angiography — acquired from the aortic arch to the intracranial circulation — is now "
"mandatory for all patients being considered for mechanical thrombectomy. CTA accurately "
"demonstrates stenoses and occlusions of both intracranial and extracranial vessels, aneurysms, "
"and arteriovenous malformations. A proportion of large vessel occlusions (LVOs) will be missed "
"if the 'hyperdense vessel sign' is relied upon as the sole surrogate marker.",
body_style
))
elements.append(Paragraph("2.4 CT Perfusion (CTP)", h2_style))
elements.append(Paragraph(
"CT perfusion generates parametric maps of cerebral blood volume (CBV), cerebral blood flow (CBF), "
"mean transit time (MTT), and time to peak (Tmax). The ischaemic core is identified by markedly "
"reduced CBV and CBF; the penumbra is identified by prolonged MTT/Tmax with relatively preserved "
"CBV. The mismatch between core and penumbra guides decisions about late-window reperfusion therapy "
"(6-24 hours after onset, per DAWN and DEFUSE-3 trials).",
body_style
))
# Borderzone ischaemia image
elements.append(Spacer(1, 0.2 * cm))
img_flowables = fetch_image_flowable(
"https://cdn.orris.care/cdss_images/68fd6d96550ff0f3c01a37ec05370211ff721346f32e80cec9c516ef1ecb2ec9.png",
width_cm=14,
caption="Fig. 1 — Borderzone Ischaemia. Severe impairment of flow/occlusion in the left ICA (A) with acute infarcts on DWI in a typical borderzone distribution in the left frontal and parietal lobes (B–D), between MCA and ACA territories. A linear distribution deep to the superior frontal sulcus is very suggestive of ACA-MCA borderzone ischaemia.",
source="Grainger & Allison's Diagnostic Radiology"
)
for f in img_flowables:
elements.append(f)
# ─────────────────────────────────────────────
# SECTION 3: MRI IN ACUTE STROKE
# ─────────────────────────────────────────────
elements.append(PageBreak())
elements.append(Paragraph("3. MRI in Acute Stroke", h1_style))
elements.append(HRFlowable(width="100%", thickness=2, color=MED_BLUE, spaceAfter=8))
elements.append(Paragraph(
"MRI is superior to CT in several important clinical situations: small deep hemispheric and "
"brainstem infarcts, posterior fossa lesions (obscured by bone on CT), 'wake-up' strokes "
"where time of onset is unknown, and cases where thrombolysis eligibility is uncertain. "
"The multimodal MRI protocol — combining DWI, ADC, FLAIR, T2, T1, MRA, and perfusion sequences "
"— provides comprehensive information on infarct core, penumbra, and vessel status.",
body_style
))
elements.append(Paragraph("3.1 MRI Sequences: Reference Table", h2_style))
mri_seq = [
["Sequence", "Key Features in Stroke", "Bright Signal", "Dark Signal"],
["DWI", "Detects acute ischaemia within minutes; high sensitivity and specificity", "Acute infarct, abscess", "Normal brain, chronic infarct (T2 dark-out)"],
["ADC map", "Confirms restricted diffusion; estimates lesion age", "Chronic infarct (>7-10 days), vasogenic oedema", "Acute infarct (<7-10 days)"],
["FLAIR", "Detects ischaemia after ~6h; good for cortical/WM lesions; arterial hyperintensity sign", "Subacute/chronic infarct, leukoaraiosis", "CSF, very acute infarct"],
["T2 FSE", "Oedema, gliosis, chronic infarct; becomes abnormal ~4-8h", "Oedema, chronic infarct, CSF", "Haemosiderin, flow voids"],
["T1", "Subacute haematoma, structural anatomy", "Subacute blood (metHb), fat", "CSF, acute blood, oedema"],
["GRE / SWI", "Microhaemorrhages, haemosiderin, CAA", "—", "Blood products, calcium, air"],
["MRA (TOF)", "Intracranial vessel patency, LVO, dissection", "Flowing blood", "Slow flow, thrombus"],
["PWI (DSC)", "Ischaemic penumbra (CBF, CBV, MTT, Tmax)", "Perfusion deficit on MTT/Tmax maps", "Core on CBV maps"],
]
elements.append(make_table(mri_seq[0], mri_seq[1:], col_widths=[2.5*cm, 5*cm, 4.5*cm, 4*cm]))
elements.append(Spacer(1, 0.4 * cm))
elements.append(Paragraph("<i>Source: Adams & Victor's Principles of Neurology, 12th Edition — Table 33-3</i>", source_style))
elements.append(Spacer(1, 0.3 * cm))
elements.append(Paragraph("3.2 Diffusion-Weighted Imaging (DWI)", h2_style))
elements.append(Paragraph(
"DWI is the most sensitive imaging sequence for the detection of acute ischaemic infarction. "
"It can show infarcted tissue within minutes of stroke onset — considerably earlier than CT "
"and other MRI sequences. The hyperintense signal on DWI in the acute phase is caused by "
"decreased water diffusivity due to swelling of ischaemic neurones (cytotoxic oedema), "
"resulting in 'restricted diffusion'.",
body_style
))
elements.append(Paragraph(
"DWI should always be interpreted in conjunction with the ADC map. Restricted diffusion "
"returns <b>high signal on DWI</b> and appears <b>dark on the ADC map</b>. T2 shine-through "
"— where T2 prolongation creates false DWI hyperintensity — is excluded by confirming ADC "
"hypointensity. DWI is particularly advantageous in demonstrating small ischaemic lesions "
"deep in the hemispheres and brainstem, a region somewhat obscured by adjacent bone on CT.",
body_style
))
# MCA infarct image (DWI/ADC)
img_flowables = fetch_image_flowable(
"https://cdn.orris.care/cdss_images/783c4c9969b6c071852848653cf19417684cbad0502c76eeeb27a28839e21d79.png",
width_cm=12,
caption="Fig. 2 — Acute MCA Ischaemic Stroke. (A) DWI shows hyperintense restricted diffusion in the left MCA territory with evolving mass effect. (B) ADC map shows corresponding hypointensity confirming restricted diffusion.",
source="Bradley & Daroff's Neurology in Clinical Practice"
)
for f in img_flowables:
elements.append(f)
elements.append(Paragraph("3.3 ADC Maps and Lesion Dating", h2_style))
elements.append(Paragraph(
"ADC maps are quantitative and allow estimation of lesion age. ADC values decrease "
"progressively after ischaemia onset, reaching a nadir at 3-5 days, remaining significantly "
"low until approximately day 7. After this time, ADC values increase (pseudonormalisation) "
"and return to baseline within 1-4 weeks (usually 7-10 days). Therefore:",
body_style
))
adc_dating = [
["ADC Map Signal", "Lesion Age Estimate"],
["Hypointense (dark)", "< 7-10 days (acute/subacute)"],
["Isointense (pseudonormalisation)", "~7-14 days"],
["Hyperintense (bright)", "> 10-14 days (chronic, gliosis/encephalomalacia)"],
]
elements.append(make_table(adc_dating[0], adc_dating[1:], col_widths=[8*cm, 8*cm]))
elements.append(Spacer(1, 0.3 * cm))
elements.append(make_callout(
"<b>Important:</b> Although ADC values change predictably over time, DWI images "
"remain hyperintense throughout — even in chronic infarcts — due to T2 shine-through. "
"Never use DWI signal alone to date a stroke; always check the ADC map."
))
elements.append(Spacer(1, 0.3 * cm))
elements.append(Paragraph("3.4 FLAIR and T2 Sequences", h2_style))
elements.append(Paragraph(
"On T2-weighted (including FLAIR) images, the signal intensity of the ischaemic area is "
"normal in the initial hyperacute stage (<6 hours), increases markedly over the first "
"4 days, then becomes stable. In a research setting, computing the numerical values of "
"hyperintensity on serial T2 scans can demonstrate a consistent sharp signal increase after "
"36 hours, distinguishing lesions younger or older than 36 hours — though this is not "
"possible by visual inspection in clinical practice.",
body_style
))
elements.append(Paragraph(
"FLAIR shows an important diagnostic sign in acute stroke: <b>arterial hyperintensity</b> — "
"high signal within a patent vessel due to altered (slow) flow, a useful qualitative sign "
"of reduced perfusion even when the brain parenchyma still appears normal. Intravascular "
"enhancement due to sluggish flow may also be observed.",
body_style
))
# ─────────────────────────────────────────────
# SECTION 4: SPECIFIC VASCULAR TERRITORIES
# ─────────────────────────────────────────────
elements.append(PageBreak())
elements.append(Paragraph("4. Stroke in Specific Vascular Territories", h1_style))
elements.append(HRFlowable(width="100%", thickness=2, color=MED_BLUE, spaceAfter=8))
elements.append(Paragraph("4.1 Middle Cerebral Artery (MCA) Territory", h2_style))
elements.append(Paragraph(
"MCA territory infarction is the most common ischaemic stroke. On DWI, a hyperintense "
"area of restricted diffusion is seen in the lateral frontal, temporal, and parietal lobes "
"with involvement of the insular ribbon, basal ganglia (lentiform nucleus), and internal "
"capsule in complete MCA occlusions. Mass effect with midline shift may develop over 24-72 "
"hours in malignant MCA infarction.",
body_style
))
# MCA DWI/FLAIR image from Adams & Victor
img_flowables = fetch_image_flowable(
"https://cdn.orris.care/cdss_images/477b24b8bc148d0fc80561057f32e8daedff3a8ef5f8f8ddaf7d583fbf0fcc07.png",
width_cm=10,
caption="Fig. 3 — Right MCA Infarction on MRI. DWI (left) shows bright signal in the right MCA territory; T2-FLAIR (right) shows subtle hyperintensity representing early vasogenic oedema.",
source="Adams & Victor's Principles of Neurology, 12th Edition — Fig. 33-3"
)
for f in img_flowables:
elements.append(f)
elements.append(Paragraph("4.2 Anterior Cerebral Artery (ACA) Territory", h2_style))
elements.append(Paragraph(
"ACA territory infarction manifests as restricted diffusion in the medial frontal lobe, "
"superior to the corpus callosum. Clinical presentation includes contralateral lower-limb "
"weakness, abulia, and urinary incontinence. ACA infarcts may occur in isolation or as part "
"of borderzone ischaemia when ICA flow is critically impaired.",
body_style
))
img_flowables = fetch_image_flowable(
"https://cdn.orris.care/cdss_images/b29c9f41bc07175f375d921188a6c1f9cef841a8cc1ec65afba4bf04eb3de937.png",
width_cm=10,
caption="Fig. 4 — Acute ACA Territory Ischaemic Stroke. (A) DWI shows hyperintense restricted diffusion in the right medial frontal lobe in the ACA territory. (B) ADC map shows corresponding hypointensity.",
source="Bradley & Daroff's Neurology in Clinical Practice"
)
for f in img_flowables:
elements.append(f)
elements.append(Paragraph("4.3 Posterior Cerebral Artery (PCA) Territory", h2_style))
elements.append(Paragraph(
"PCA territory infarction produces restricted diffusion in the medial occipital lobe, "
"thalamus, and/or midbrain. Clinical features include contralateral homonymous hemianopia, "
"alexia without agraphia (dominant hemisphere), and thalamic syndromes. PCA infarcts are "
"commonly cardioembolic or due to basilar artery disease.",
body_style
))
img_flowables = fetch_image_flowable(
"https://cdn.orris.care/cdss_images/03f4d1f754b4b5032059decd40c3400342c5f3fa044bf08710490bd09768ea70.png",
width_cm=10,
caption="Fig. 5 — Acute PCA Territory Ischaemic Stroke. (A) DWI shows hyperintense restricted diffusion in the left medial occipital lobe in the PCA territory. (B) ADC map shows corresponding hypointensity.",
source="Bradley & Daroff's Neurology in Clinical Practice"
)
for f in img_flowables:
elements.append(f)
elements.append(Paragraph("4.4 Cerebellar / PICA Territory", h2_style))
elements.append(Paragraph(
"Cerebellar infarction in the territory of the posterior inferior cerebellar artery (PICA) "
"appears as bright DWI signal in the inferior cerebellum. It may be faintly bright on FLAIR. "
"Previous infarctions appear dark on DWI and bright on T2 due to established gliosis. "
"Cerebellar infarcts are frequently missed on CT due to bone artefact; MRI is the investigation "
"of choice for posterior fossa ischaemia.",
body_style
))
img_flowables = fetch_image_flowable(
"https://cdn.orris.care/cdss_images/125fef012b5c33effa97ac5995db6a5626cfff16f217f28f4cae157c83f23a9d.png",
width_cm=9,
caption="Fig. 6 — Acute Cerebellar (PICA) Infarction. DWI shows bright signal in the PICA territory (lower left); T2-FLAIR (arrow) shows faint hyperintensity. A previous infarction anterior to the acute lesion appears dark on DWI.",
source="Adams & Victor's Principles of Neurology, 12th Edition"
)
for f in img_flowables:
elements.append(f)
elements.append(Paragraph("4.5 Watershed / Borderzone Ischaemia", h2_style))
elements.append(Paragraph(
"Watershed infarcts occur between the territories of major cerebral arteries (ACA-MCA, "
"MCA-PCA, or deep/internal borderzone) due to haemodynamic compromise — typically from "
"severe ICA stenosis or occlusion, or systemic hypotension. On DWI, they appear as linear "
"or 'string of beads' hyperintensities along the watershed zones. The ACA-MCA borderzone "
"is characterised by a linear distribution of infarcts deep to the superior frontal sulcus.",
body_style
))
# ─────────────────────────────────────────────
# SECTION 5: DWI-FLAIR MISMATCH & WAKE-UP STROKE
# ─────────────────────────────────────────────
elements.append(PageBreak())
elements.append(Paragraph("5. DWI-FLAIR Mismatch and Wake-Up Stroke", h1_style))
elements.append(HRFlowable(width="100%", thickness=2, color=MED_BLUE, spaceAfter=8))
elements.append(Paragraph(
"For patients who awaken with new neurological deficits ('wake-up stroke'), the exact time "
"of onset is unknown. MRI-based tissue clocks use the DWI-FLAIR mismatch concept to "
"estimate stroke onset time:",
body_style
))
mismatch = [
["DWI", "FLAIR", "Estimated Onset", "Implication"],
["Positive (bright)", "Negative (normal)", "< 4.5 hours", "May be eligible for thrombolysis (WAKE-UP trial)"],
["Positive (bright)", "Positive (bright)", "> 4.5–6 hours", "Likely outside thrombolysis window"],
["Negative", "Positive", "Chronic / TIA (no acute diffusion restriction)", "No acute infarction"],
]
elements.append(make_table(mismatch[0], mismatch[1:], col_widths=[3.5*cm, 3.5*cm, 3.5*cm, 5.5*cm]))
elements.append(Spacer(1, 0.3 * cm))
elements.append(Paragraph(
"The WAKE-UP randomised trial (2018) demonstrated that MRI-guided thrombolysis "
"(DWI-positive, FLAIR-negative mismatch) improved functional outcomes in wake-up stroke "
"compared to placebo, supporting the use of this imaging paradigm.",
body_style
))
elements.append(make_callout(
"<b>From Grainger & Allison:</b> MRI is frequently used to assess the extent of the "
"core infarct in 'wake-up' strokes, as MR perfusion-weighted imaging can be utilised to "
"assess the ischaemic penumbra and allow patient risk stratification for potential "
"revascularisation treatments that fall outside of conventional time frames."
))
elements.append(Spacer(1, 0.3 * cm))
# ─────────────────────────────────────────────
# SECTION 6: DWI-PWI MISMATCH AND PENUMBRA
# ─────────────────────────────────────────────
elements.append(Paragraph("6. DWI-PWI Mismatch and the Ischaemic Penumbra", h1_style))
elements.append(HRFlowable(width="100%", thickness=2, color=MED_BLUE, spaceAfter=8))
elements.append(Paragraph(
"The ischaemic penumbra represents brain tissue that is functionally impaired but "
"structurally viable — the target for reperfusion therapy. The DWI-PWI mismatch model "
"is the principal imaging framework used to identify salvageable tissue:",
body_style
))
penumbra_data = [
["Imaging Finding", "Pathophysiology", "Therapeutic Significance"],
["DWI hyperintensity", "Cytotoxic oedema — irreversible ischaemic core (usually)", "Cannot be salvaged by reperfusion"],
["PWI deficit > DWI (mismatch)", "Hypoperfused but viable penumbral tissue", "Target for thrombectomy up to 24h (DAWN/DEFUSE-3)"],
["DWI = PWI (no mismatch)", "Core = entire perfusion deficit; no salvageable tissue", "Reperfusion unlikely to benefit"],
["DWI > PWI (reverse mismatch)", "Reperfusion has already occurred; luxury perfusion", "Reperfusion complete; haemorrhage risk monitoring"],
]
elements.append(make_table(penumbra_data[0], penumbra_data[1:], col_widths=[4.5*cm, 5.5*cm, 6*cm]))
elements.append(Spacer(1, 0.3 * cm))
elements.append(Paragraph(
"In clinical practice, automated perfusion analysis software (e.g. RAPID) is used to "
"quantify core (rCBF < 30%) and penumbra (Tmax >6s) volumes. A mismatch ratio "
"≥1.8 and mismatch volume ≥15 mL are the thresholds used in the DEFUSE-3 trial for "
"selecting patients for late-window (6-16 hours) mechanical thrombectomy.",
body_style
))
img_flowables = fetch_image_flowable(
"https://cdn.orris.care/cdss_images/f21b8f8a2e49c9312007f51c926f295190aaae70d770ebb7b0dc5c4e4daea471.png",
width_cm=13,
caption="Fig. 7 — Diffusion Tensor Imaging (DTI) showing fibre tract disruption (advanced structural neuroimaging). DTI is used alongside DWI for surgical planning and defining the extent of white matter involvement in large infarcts.",
source="Bradley & Daroff's Neurology in Clinical Practice"
)
for f in img_flowables:
elements.append(f)
# ─────────────────────────────────────────────
# SECTION 7: HAEMORRHAGIC STROKE IMAGING
# ─────────────────────────────────────────────
elements.append(PageBreak())
elements.append(Paragraph("7. Imaging in Haemorrhagic Stroke", h1_style))
elements.append(HRFlowable(width="100%", thickness=2, color=MED_BLUE, spaceAfter=8))
elements.append(Paragraph(
"CT demonstrates and accurately localises acute haemorrhages, haemorrhagic infarcts, "
"subarachnoid blood, clots in and around aneurysms, and arteriovenous malformations (AVMs). "
"NECT is the investigation of choice for suspected intracerebral haemorrhage (ICH) because "
"of its immediate availability and high sensitivity.",
body_style
))
elements.append(Paragraph("7.1 Evolution of Haematoma on CT", h2_style))
ich_ct = [
["Time", "CT Appearance", "Pathology"],
["Hyperacute (<1h)", "Isodense or slightly hyperdense", "Active bleeding, unclotted blood"],
["Acute (1h – 3 days)", "Hyperdense (50-80 HU)", "Clot retraction, protein concentration"],
["Subacute early (3-10 days)", "Isodense rim, hyperdense centre", "Peripheral clot lysis"],
["Subacute late (10 days – weeks)", "Isodense to hypodense", "Progressive clot lysis, oedema resolution"],
["Chronic (>1 month)", "Hypodense (encephalomalacia)", "Haemosiderin, gliosis, cavity formation"],
]
elements.append(make_table(ich_ct[0], ich_ct[1:], col_widths=[3.5*cm, 6*cm, 6.5*cm]))
elements.append(Spacer(1, 0.3 * cm))
elements.append(Paragraph("7.2 MRI for Haemorrhage", h2_style))
elements.append(Paragraph(
"MRI with gradient echo (GRE) or susceptibility-weighted imaging (SWI) is more sensitive "
"than CT for detecting microhaemorrhages, chronic haemosiderin deposits, and cerebral "
"amyloid angiopathy (CAA). SWI can detect microbleeds as small as 1-2 mm that are entirely "
"invisible on CT. Haemosiderin and iron pigment from prior microhaemorrhages appear as "
"dark blooming artefacts on GRE/SWI.",
body_style
))
# SAH image
img_flowables = fetch_image_flowable(
"https://cdn.orris.care/cdss_images/2abb1214c7a576d0117c54346cef88dbce66ec82b699942983376c33979975b9.png",
width_cm=10,
caption="Fig. 8 — Acute haemorrhagic infarction on CT, showing hyperdense blood products within an area of established infarction.",
source="Adams & Victor's Principles of Neurology, 12th Edition"
)
for f in img_flowables:
elements.append(f)
# ─────────────────────────────────────────────
# SECTION 8: MECHANICAL THROMBECTOMY — IMAGING SELECTION
# ─────────────────────────────────────────────
elements.append(PageBreak())
elements.append(Paragraph("8. Imaging-Based Patient Selection for Mechanical Thrombectomy", h1_style))
elements.append(HRFlowable(width="100%", thickness=2, color=MED_BLUE, spaceAfter=8))
elements.append(Paragraph(
"The publication of the MR CLEAN trial (2015) and subsequent HERMES meta-analysis "
"established mechanical thrombectomy as a standard-of-care treatment for acute anterior "
"circulation stroke secondary to large vessel occlusion (LVO). Imaging plays a central "
"role in patient selection.",
body_style
))
elements.append(Paragraph("8.1 Standard Window (0-6 hours)", h2_style))
tx_table = [
["Imaging Step", "Modality", "Purpose"],
["1. Exclude haemorrhage", "NECT", "Mandatory before any reperfusion therapy"],
["2. Confirm LVO", "CTA (arch to vertex)", "Identifies M1/ICA occlusion; ASPECTS assessed on NECT"],
["3. Assess collateral circulation", "CTA multiphase / MRA", "Good collaterals predict favourable response to thrombectomy"],
["4. Arterial puncture target ≤6h", "CTA/DSA guidance", "HERMES: treat up to 6h from onset"],
]
elements.append(make_table(tx_table[0], tx_table[1:], col_widths=[4.5*cm, 4.5*cm, 7*cm]))
elements.append(Spacer(1, 0.3 * cm))
elements.append(Paragraph("8.2 Extended Window (6-24 hours)", h2_style))
elements.append(Paragraph(
"Patients presenting beyond 6 hours or with unknown onset time require advanced "
"perfusion imaging to identify a favourable penumbra-to-core mismatch profile. "
"The DAWN and DEFUSE-3 trials established that selected patients can benefit from "
"thrombectomy up to 24 hours after last known well.",
body_style
))
extended = [
["Trial", "Window", "Imaging Criteria", "Key Finding"],
["DAWN", "6-24h (wake-up)", "CT/MRI perfusion mismatch + clinical-core mismatch", "mRS 0-2 at 90d: 49% thrombectomy vs 13% control"],
["DEFUSE-3", "6-16h", "Ischaemic core <70mL, mismatch ratio ≥1.8, mismatch vol ≥15mL (RAPID)", "mRS 0-2 at 90d: 45% vs 17%; stopped early for benefit"],
]
elements.append(make_table(extended[0], extended[1:], col_widths=[2.5*cm, 2.5*cm, 6*cm, 5*cm]))
elements.append(Spacer(1, 0.4 * cm))
elements.append(make_callout(
"<b>From Grainger & Allison:</b> New UK guidance recommends mechanical thrombectomy "
"for patients with NIHSS >5, occlusion of the intracranial ICA or proximal MCA, and "
"pre-stroke mRS <3. Eligible patients should receive IV r-tPA within 4.5 hours and "
"undergo arterial puncture within 6 hours."
))
elements.append(Spacer(1, 0.3 * cm))
# ─────────────────────────────────────────────
# SECTION 9: QUICK REFERENCE SUMMARY
# ─────────────────────────────────────────────
elements.append(PageBreak())
elements.append(Paragraph("9. Quick Reference Summary", h1_style))
elements.append(HRFlowable(width="100%", thickness=2, color=MED_BLUE, spaceAfter=8))
summary = [
["Modality", "Key Use", "Advantages", "Limitations"],
["NECT", "First-line acute stroke imaging", "Fast, available, detects haemorrhage", "Insensitive to early ischaemia <6h, posterior fossa artefact"],
["CTA", "LVO detection; vessel roadmap", "Fast, high sensitivity for LVO; detects collaterals", "Iodinated contrast; radiation"],
["CTP", "Penumbra-core mismatch", "Rapid, widely available; RAPID automated", "Limited coverage; radiation; contrast"],
["DWI-MRI", "Gold standard for acute ischaemia", "Detects infarct within minutes; no radiation", "Time-consuming; availability; patient cooperation"],
["FLAIR-MRI", "Lesion dating; wake-up stroke", "DWI-FLAIR mismatch identifies <4.5h lesions", "Normal in first few hours of ischaemia"],
["SWI / GRE", "Microhaemorrhages, CAA", "Highly sensitive for blood products", "Time, availability"],
["MRA", "Non-contrast vessel imaging", "No contrast; identifies LVO, dissection", "Motion artefact; overestimates stenosis"],
["DSA", "Gold standard angiography; thrombectomy", "Real-time; therapeutic; highest resolution", "Invasive; radiation; contrast; risk of dissection"],
]
elements.append(make_table(summary[0], summary[1:], col_widths=[2.5*cm, 4*cm, 4.5*cm, 5*cm]))
elements.append(Spacer(1, 0.4 * cm))
elements.append(Paragraph(
"<b>Sources:</b> Adams & Victor's Principles of Neurology, 12th Edition (ISBN 9781264264520); "
"Grainger & Allison's Diagnostic Radiology (ISBN 9780323760751); "
"Bradley & Daroff's Neurology in Clinical Practice (ISBN 9780323642613).",
source_style
))
def main():
doc = SimpleDocTemplate(
OUTPUT,
pagesize=A4,
leftMargin=2*cm, rightMargin=2*cm,
topMargin=2.2*cm, bottomMargin=2*cm,
title="Stroke Imaging — Medical Textbook Reference",
author="Orris Medical Library",
subject="Stroke Imaging: CT, MRI, DWI, CTA, Perfusion"
)
elements = []
build_cover(elements)
build_contents(elements)
doc.build(elements, canvasmaker=NumberedCanvas)
print(f"PDF generated: {OUTPUT}")
if __name__ == "__main__":
main()
cd /home/daytona/workspace/stroke-imaging && python generate_pdf.py 2>&1
ls -lh /home/daytona/workspace/stroke-imaging/Stroke_Imaging_Textbook.pdf
Stroke Imaging Textbook
PDF Document · PDF
| Section | Content |
|---|---|
| 1 | Overview of stroke imaging goals |
| 2 | CT (NECT, ASPECTS, CTA, CTP) |
| 3 | MRI sequences reference table + DWI, ADC dating, FLAIR |
| 4 | Vascular territory infarcts — MCA, ACA, PCA, PICA, watershed |
| 5 | DWI-FLAIR mismatch for wake-up stroke |
| 6 | DWI-PWI mismatch and ischaemic penumbra |
| 7 | Haemorrhagic stroke — CT evolution table, SWI/GRE |
| 8 | Thrombectomy patient selection (HERMES, DAWN, DEFUSE-3) |
| 9 | Quick-reference summary table |