~/gerd-guide/generate_gerd_pdf.py
#!/usr/bin/env python3
"""
GERD Quick-Reference Guide PDF Generator
Uses reportlab with a clean, clinical design.
"""
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import cm, mm
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_JUSTIFY
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, KeepTogether, PageBreak
)
from reportlab.platypus.flowables import HRFlowable
from reportlab.lib.colors import HexColor
# ── Colour palette ────────────────────────────────────────────────────────────
BLUE_DARK = HexColor("#1a3a5c") # dark navy – headings / header
BLUE_MID = HexColor("#2e6da4") # medium blue – section bars
BLUE_LIGHT = HexColor("#dbeaf7") # pale blue – table alternating rows
RED_ALERT = HexColor("#c0392b") # alarm / warning red
GREEN_OK = HexColor("#1e8449") # treatment green
ORANGE = HexColor("#d35400") # highlights
GREY_TEXT = HexColor("#2c3e50") # body text
GREY_LINE = HexColor("#bdc3c7")
WHITE = colors.white
BG_HEADER = BLUE_DARK
PAGE_W, PAGE_H = A4 # 595 x 842 pt
MARGIN = 1.8 * cm
OUTPUT = "/home/daytona/workspace/gerd-guide/GERD_Quick_Reference.pdf"
# ── Styles ────────────────────────────────────────────────────────────────────
def make_styles():
base = getSampleStyleSheet()
s = {}
s["doc_title"] = ParagraphStyle(
"doc_title",
fontName="Helvetica-Bold",
fontSize=22,
textColor=WHITE,
alignment=TA_CENTER,
spaceAfter=2,
leading=26,
)
s["doc_subtitle"] = ParagraphStyle(
"doc_subtitle",
fontName="Helvetica",
fontSize=11,
textColor=HexColor("#aed6f1"),
alignment=TA_CENTER,
spaceAfter=4,
)
s["section_header"] = ParagraphStyle(
"section_header",
fontName="Helvetica-Bold",
fontSize=11,
textColor=WHITE,
alignment=TA_LEFT,
spaceBefore=4,
spaceAfter=4,
leftIndent=6,
)
s["subsection"] = ParagraphStyle(
"subsection",
fontName="Helvetica-Bold",
fontSize=9.5,
textColor=BLUE_DARK,
spaceBefore=6,
spaceAfter=2,
)
s["body"] = ParagraphStyle(
"body",
fontName="Helvetica",
fontSize=8.5,
textColor=GREY_TEXT,
leading=12,
spaceAfter=3,
alignment=TA_JUSTIFY,
)
s["bullet"] = ParagraphStyle(
"bullet",
fontName="Helvetica",
fontSize=8.5,
textColor=GREY_TEXT,
leading=12,
leftIndent=10,
firstLineIndent=-8,
spaceAfter=2,
)
s["small_bold"] = ParagraphStyle(
"small_bold",
fontName="Helvetica-Bold",
fontSize=8,
textColor=BLUE_DARK,
)
s["table_header"] = ParagraphStyle(
"table_header",
fontName="Helvetica-Bold",
fontSize=8,
textColor=WHITE,
alignment=TA_CENTER,
)
s["table_cell"] = ParagraphStyle(
"table_cell",
fontName="Helvetica",
fontSize=8,
textColor=GREY_TEXT,
leading=10,
)
s["table_cell_bold"] = ParagraphStyle(
"table_cell_bold",
fontName="Helvetica-Bold",
fontSize=8,
textColor=GREY_TEXT,
leading=10,
)
s["alert"] = ParagraphStyle(
"alert",
fontName="Helvetica-Bold",
fontSize=8.5,
textColor=RED_ALERT,
leading=12,
leftIndent=10,
firstLineIndent=-8,
)
s["footer"] = ParagraphStyle(
"footer",
fontName="Helvetica",
fontSize=7,
textColor=HexColor("#7f8c8d"),
alignment=TA_CENTER,
)
return s
# ── Helper builders ───────────────────────────────────────────────────────────
def section_bar(title, s):
"""Returns a coloured section-header bar."""
p = Paragraph(title, s["section_header"])
tbl = Table([[p]], colWidths=[PAGE_W - 2 * MARGIN])
tbl.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, -1), BLUE_MID),
("TOPPADDING", (0, 0), (-1, -1), 5),
("BOTTOMPADDING", (0, 0), (-1, -1), 5),
("LEFTPADDING", (0, 0), (-1, -1), 8),
("RIGHTPADDING", (0, 0), (-1, -1), 8),
("ROUNDEDCORNERS", [4]),
]))
return tbl
def two_col_table(data, col_widths, s, header_bg=BLUE_DARK):
"""Styled two-column table with alternating rows."""
rows = []
for i, row in enumerate(data):
rows.append([
Paragraph(str(row[0]), s["table_cell_bold"] if i == 0 else s["table_cell"]),
Paragraph(str(row[1]), s["table_header"] if i == 0 else s["table_cell"]),
])
style = [
("BACKGROUND", (0, 0), (-1, 0), header_bg),
("TEXTCOLOR", (0, 0), (-1, 0), WHITE),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("FONTSIZE", (0, 0), (-1, 0), 8),
("ALIGN", (0, 0), (-1, 0), "CENTER"),
("TOPPADDING", (0, 0), (-1, -1), 4),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
("LEFTPADDING", (0, 0), (-1, -1), 6),
("RIGHTPADDING", (0, 0), (-1, -1), 6),
("GRID", (0, 0), (-1, -1), 0.4, GREY_LINE),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
]
for i in range(1, len(rows)):
bg = BLUE_LIGHT if i % 2 == 0 else WHITE
style.append(("BACKGROUND", (0, i), (-1, i), bg))
tbl = Table(rows, colWidths=col_widths)
tbl.setStyle(TableStyle(style))
return tbl
def three_col_table(data, col_widths, s, header_bg=BLUE_DARK):
rows = []
for i, row in enumerate(data):
if i == 0:
rows.append([Paragraph(str(c), s["table_header"]) for c in row])
else:
rows.append([Paragraph(str(c), s["table_cell"]) for c in row])
style = [
("BACKGROUND", (0, 0), (-1, 0), header_bg),
("TEXTCOLOR", (0, 0), (-1, 0), WHITE),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("FONTSIZE", (0, 0), (-1, 0), 8),
("ALIGN", (0, 0), (-1, 0), "CENTER"),
("TOPPADDING", (0, 0), (-1, -1), 4),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
("LEFTPADDING", (0, 0), (-1, -1), 6),
("RIGHTPADDING", (0, 0), (-1, -1), 6),
("GRID", (0, 0), (-1, -1), 0.4, GREY_LINE),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
]
for i in range(1, len(rows)):
bg = BLUE_LIGHT if i % 2 == 0 else WHITE
style.append(("BACKGROUND", (0, i), (-1, i), bg))
tbl = Table(rows, colWidths=col_widths)
tbl.setStyle(TableStyle(style))
return tbl
def alert_box(text_lines, s):
"""Red-bordered alert box."""
content = [Paragraph(f"⚠ {line}", s["alert"]) for line in text_lines]
inner = Table([[c] for c in content], colWidths=[PAGE_W - 2*MARGIN - 20])
inner.setStyle(TableStyle([
("TOPPADDING", (0, 0), (-1, -1), 2),
("BOTTOMPADDING", (0, 0), (-1, -1), 2),
("LEFTPADDING", (0, 0), (-1, -1), 4),
]))
outer = Table([[inner]], colWidths=[PAGE_W - 2*MARGIN])
outer.setStyle(TableStyle([
("BOX", (0, 0), (-1, -1), 1.5, RED_ALERT),
("BACKGROUND", (0, 0), (-1, -1), HexColor("#fdf2f2")),
("TOPPADDING", (0, 0), (-1, -1), 6),
("BOTTOMPADDING", (0, 0), (-1, -1), 6),
("LEFTPADDING", (0, 0), (-1, -1), 8),
("RIGHTPADDING", (0, 0), (-1, -1), 8),
]))
return outer
def info_box(text_lines, s, bg=HexColor("#eafaf1"), border=GREEN_OK):
content = [Paragraph(f"✓ {line}", s["bullet"]) for line in text_lines]
inner = Table([[c] for c in content], colWidths=[PAGE_W - 2*MARGIN - 20])
inner.setStyle(TableStyle([
("TOPPADDING", (0, 0), (-1, -1), 2),
("BOTTOMPADDING", (0, 0), (-1, -1), 2),
]))
outer = Table([[inner]], colWidths=[PAGE_W - 2*MARGIN])
outer.setStyle(TableStyle([
("BOX", (0, 0), (-1, -1), 1.5, border),
("BACKGROUND", (0, 0), (-1, -1), bg),
("TOPPADDING", (0, 0), (-1, -1), 6),
("BOTTOMPADDING", (0, 0), (-1, -1), 6),
("LEFTPADDING", (0, 0), (-1, -1), 8),
("RIGHTPADDING", (0, 0), (-1, -1), 8),
]))
return outer
# ── Header / footer callbacks ─────────────────────────────────────────────────
def on_first_page(canvas, doc):
w, h = A4
# Header banner
canvas.setFillColor(BG_HEADER)
canvas.rect(0, h - 80, w, 80, fill=1, stroke=0)
# Title
canvas.setFillColor(WHITE)
canvas.setFont("Helvetica-Bold", 22)
canvas.drawCentredString(w / 2, h - 38, "GASTROESOPHAGEAL REFLUX DISEASE (GERD)")
canvas.setFont("Helvetica", 11)
canvas.setFillColor(HexColor("#aed6f1"))
canvas.drawCentredString(w / 2, h - 56, "Quick-Reference Management Guide")
canvas.setFont("Helvetica", 9)
canvas.drawCentredString(w / 2, h - 70, "Based on Goldman-Cecil Medicine | Robbins Pathology | Yamada's Gastroenterology | Schwartz's Surgery")
# Footer
canvas.setFillColor(HexColor("#7f8c8d"))
canvas.setFont("Helvetica", 7)
canvas.drawCentredString(w / 2, 18, f"Page {doc.page} | GERD Quick-Reference Guide | For educational use only")
canvas.setStrokeColor(GREY_LINE)
canvas.line(MARGIN, 28, w - MARGIN, 28)
def on_later_pages(canvas, doc):
w, h = A4
# Slim header bar
canvas.setFillColor(BLUE_MID)
canvas.rect(0, h - 28, w, 28, fill=1, stroke=0)
canvas.setFillColor(WHITE)
canvas.setFont("Helvetica-Bold", 9)
canvas.drawCentredString(w / 2, h - 16, "GERD Quick-Reference Guide")
# Footer
canvas.setFillColor(HexColor("#7f8c8d"))
canvas.setFont("Helvetica", 7)
canvas.drawCentredString(w / 2, 18, f"Page {doc.page} | GERD Quick-Reference Guide | For educational use only")
canvas.setStrokeColor(GREY_LINE)
canvas.line(MARGIN, 28, w - MARGIN, 28)
# ── Main content builder ──────────────────────────────────────────────────────
def build_story(s):
story = []
W = PAGE_W - 2 * MARGIN # usable width
def sp(n=6): return Spacer(1, n)
def hr(): return HRFlowable(width=W, thickness=0.5, color=GREY_LINE, spaceAfter=4, spaceBefore=4)
# ── PAGE 1 ───────────────────────────────────────────────────────────────
story.append(sp(10))
# ── 1. DEFINITION & EPIDEMIOLOGY ─────────────────────────────────────────
story.append(section_bar("1. DEFINITION & EPIDEMIOLOGY", s))
story.append(sp(4))
story.append(Paragraph(
"GERD is a <b>chronic disorder</b> in which gastric contents (acid, pepsin, bile) reflux "
"into the esophagus, causing mucosal injury and/or troublesome symptoms — distinguished "
"from physiological GER by its clinical impact.",
s["body"]
))
epi_data = [
["Parameter", "Key Figure"],
["Global prevalence (Western)", "20–40% | East Asia: <10%"],
["US annual outpatient visits", "Most common GI outpatient diagnosis"],
["Obesity (BMI >30) OR for GERD", "1.94 vs. normal weight"],
["GERD → Barrett's esophagus OR", "4.0 (95% CI 1.4–11.1)"],
["PPI symptom relief rate", "Up to 90% of patients"],
]
story.append(two_col_table(epi_data, [W*0.45, W*0.55], s))
story.append(sp(8))
# ── 2. RISK FACTORS ───────────────────────────────────────────────────────
story.append(section_bar("2. RISK FACTORS / CAUSES", s))
story.append(sp(4))
rf_data = [
["Category", "Specific Factors"],
["Lifestyle", "Obesity (esp. abdominal), smoking, alcohol, caffeine, fatty/spicy foods, large late meals"],
["Anatomical", "Hiatal hernia (sliding >95%), short intraabdominal esophageal segment"],
["Physiological", "Pregnancy, delayed gastric emptying, increased gastric volume/acid secretion"],
["Drugs", "Anticholinergics, CCBs, theophylline, beta-agonists, nitrates, NSAIDs, bisphosphonates"],
["Systemic disease", "Scleroderma, CREST syndrome (oesophageal dysmotility), Zollinger-Ellison syndrome"],
["Other", "CNS depressants, nasogastric tubes, post-surgical (e.g. Heller myotomy)"],
]
story.append(two_col_table(rf_data, [W*0.25, W*0.75], s))
story.append(sp(8))
# ── 3. PATHOPHYSIOLOGY ───────────────────────────────────────────────────
story.append(section_bar("3. PATHOPHYSIOLOGY", s))
story.append(sp(4))
story.append(Paragraph("<b>Three-layer failure model:</b>", s["subsection"]))
patho_data = [
["Mechanism", "Detail"],
["Transient LES Relaxation (tLESR) — PRIMARY",
"Vagally-mediated; triggered by gastric distension; unaccompanied by peristalsis → prolonged acid exposure"],
["Decreased basal LES pressure",
"Drugs, alcohol, tobacco, obesity, scleroderma → persistent LES incompetence"],
["Hiatal hernia",
"Displaces GEJ into thorax; disrupts crural diaphragm augmentation; creates acid reservoir above diaphragm"],
["Impaired esophageal clearance",
"Weak peristalsis → prolonged acid contact; reduced saliva (bicarbonate) — worsened by smoking"],
["Increased intraabdominal pressure",
"Obesity, pregnancy, coughing, straining, bending → overcomes LES barrier transiently"],
["Mucosal defence failure",
"Breakdown of tight junctions, mucus layer, bicarbonate secretion → intracellular acidification → inflammation"],
]
story.append(two_col_table(patho_data, [W*0.35, W*0.65], s))
story.append(sp(4))
story.append(Paragraph("<b>Injury cascade:</b> Acid exposure → intercellular oedema → basal hyperplasia / papillary elongation "
"→ eosinophil infiltration → erosions / ulcers → fibrosis (stricture) → columnar metaplasia "
"(Barrett's) → adenocarcinoma", s["body"]))
story.append(sp(8))
# ── 4. SIGNS & SYMPTOMS ──────────────────────────────────────────────────
story.append(section_bar("4. SIGNS & SYMPTOMS", s))
story.append(sp(4))
# Two side-by-side mini tables
typical_data = [
["Typical (Esophageal)", "Description"],
["Heartburn", "Substernal burning, postprandial / supine, worsened bending forward"],
["Acid regurgitation", "Sour/bitter taste reaching pharynx or mouth"],
["Waterbrash", "Sudden flood of saliva (reflex hypersalivation)"],
["Belching", "Air swallowing / gas reflux"],
["Dysphagia*", "Solid food sticking — may signal stricture (alarm)"],
["Odynophagia*", "Painful swallowing — suggests ulceration"],
["Chest pain", "May mimic cardiac angina; exclude cardiac cause first"],
]
atypical_data = [
["Extraesophageal (Atypical)", "Mechanism"],
["Chronic cough", "Microaspiration / vagal reflex"],
["Laryngitis / hoarseness", "Acid injury to larynx"],
["Asthma exacerbation", "Vagal reflex bronchospasm / aspiration"],
["Dental enamel erosion", "Acid dissolves palatal enamel (irreversible)"],
["Sinusitis / otitis media", "Proposed — less clear causality"],
["Pharyngitis / throat clearing", "Laryngopharyngeal reflux"],
["Aspiration pneumonia", "Microaspiration of gastric contents"],
]
t1 = two_col_table(typical_data, [W*0.3, W*0.2], s)
t2 = two_col_table(atypical_data, [W*0.3, W*0.2], s)
side_by_side = Table([[t1, sp(6), t2]], colWidths=[W*0.48, 6, W*0.48])
side_by_side.setStyle(TableStyle([("VALIGN", (0,0), (-1,-1), "TOP")]))
story.append(side_by_side)
story.append(sp(4))
story.append(Paragraph(
"<i>*Severity of symptoms does NOT correlate with degree of histologic damage (Robbins Pathology).</i>",
s["body"]
))
story.append(sp(6))
# Alarm features
story.append(alert_box([
"ALARM FEATURES — require urgent endoscopy / investigation:",
"Dysphagia or odynophagia | Unintentional weight loss | GI bleeding (haematemesis / melaena)",
"Iron-deficiency anaemia | Persistent symptoms despite 4–8 weeks PPI | Age >45, new-onset symptoms",
], s))
story.append(sp(8))
# ── COMPLICATIONS ─────────────────────────────────────────────────────────
story.append(section_bar("5. COMPLICATIONS", s))
story.append(sp(4))
comp_data = [
["Complication", "Features", "Management Overview"],
["Erosive Esophagitis\n(LA Grade A–D)",
"Grade A: <5 mm breaks; Grade D: >75% circumference with ulcers",
"PPI therapy; heal in 4–8 weeks"],
["Peptic Stricture",
"Progressive solid-food dysphagia; fibrous narrowing distal esophagus",
"Endoscopic dilation (balloon/bougie) + long-term PPI"],
["Barrett's Esophagus",
"Columnar (intestinal) metaplasia; salmon-pink mucosa on endoscopy; OR 4.0 with obesity",
"Surveillance EGD q3–5 yr; RFA / cryoablation for dysplasia"],
["Esophageal Adenocarcinoma",
"Arises from Barrett's high-grade dysplasia; RR 4.8 in obese; dysphagia + weight loss",
"EMR (early) / esophagectomy + neo-adjuvant CRT"],
["Aspiration Pneumonia",
"Nocturnal microaspiration; recurrent lower lobe infiltrates",
"Optimize acid suppression; consider antireflux surgery"],
]
cw = [W*0.23, W*0.42, W*0.35]
story.append(three_col_table(comp_data, cw, s))
story.append(sp(10))
# ── PAGE 2 ───────────────────────────────────────────────────────────────
story.append(PageBreak())
# ── 6. INVESTIGATIONS ────────────────────────────────────────────────────
story.append(sp(10))
story.append(section_bar("6. INVESTIGATIONS", s))
story.append(sp(4))
story.append(Paragraph(
"<b>Clinical diagnosis is sufficient</b> when classic heartburn + regurgitation respond to empiric PPI therapy. "
"Investigate when alarm features are present, symptoms are atypical, or treatment fails after 4–8 weeks.",
s["body"]
))
story.append(sp(4))
inv_data = [
["Investigation", "Indication", "Key Findings / Notes"],
["Empiric PPI Trial\n(4–8 weeks)",
"Typical symptoms, no alarm features",
"~70% sensitivity; response supports GERD diagnosis"],
["Upper GI Endoscopy (EGD)",
"Alarm symptoms; PPI failure; Barrett's screening",
"Erosions, ulcers, stricture, Barrett's; ~2/3 GERD pts have NORMAL endoscopy"],
["24-hr Ambulatory pH\nMonitoring",
"EGD normal + symptoms persist (OFF PPI)",
"Gold standard for acid exposure; pH <4 for >4–6% = abnormal; correlates symptom-reflux"],
["48–96 hr Wireless pH\nCapsule (Bravo)",
"Better tolerated alternative to nasal catheter",
"Higher diagnostic yield; day-to-day variation captured"],
["Combined Impedance-pH\nMonitoring (MII-pH)",
"Persistent symptoms ON PPI",
"Detects acid AND non-acid reflux; best for PPI-refractory GERD"],
["High-Resolution\nManometry (HRM)",
"Pre-surgical evaluation; exclude achalasia",
"Measures LES pressure + peristalsis; identifies poor motility (risk for wrap dysphagia)"],
["Barium Swallow",
"Structural assessment (stricture, hernia)\nPaediatric workup",
"NOT recommended for routine uncomplicated GERD; limited sensitivity/specificity"],
["Gastric Emptying Scan\n(Tc-99m)",
"Suspected gastroparesis contributing to GERD",
"Normal: 50% emptied at 60 min, ~80% at 90 min"],
]
story.append(three_col_table(inv_data, [W*0.24, W*0.31, W*0.45], s))
story.append(sp(4))
story.append(Paragraph(
"<b>Histology on biopsy:</b> Basal zone hyperplasia | Papillary elongation | Intercellular oedema "
"| Eosinophil infiltration | 'Balloon' cells | Erosions (severe). "
"Biopsies should be taken >2 cm above GEJ. Changes most prominent in distal 8–10 cm.",
s["body"]
))
story.append(sp(8))
# ── 7. TREATMENT ─────────────────────────────────────────────────────────
story.append(section_bar("7. TREATMENT — STEPWISE APPROACH", s))
story.append(sp(4))
# Step 1 – Lifestyle
story.append(Paragraph("STEP 1 — Lifestyle Modifications", s["subsection"]))
story.append(info_box([
"Weight loss (strongest evidence — reduces intraabdominal pressure and gastric acid production)",
"Elevate head of bed 6–8 inches / wedge pillow — for nocturnal heartburn",
"Avoid meals <3 hours before lying down",
"Avoid triggers: alcohol, caffeine, spicy/fatty foods, chocolate, peppermint, carbonated drinks",
"Small, frequent meals; eat slowly",
"Smoking cessation (reduces LES tone + impairs mucosal defence)",
"Avoid tight abdominal clothing",
"Review medications that lower LES tone (CCBs, nitrates, anticholinergics)",
], s))
story.append(sp(6))
# Step 2 – Pharmacotherapy
story.append(Paragraph("STEP 2 — Pharmacological Treatment", s["subsection"]))
story.append(sp(3))
drug_data = [
["Drug Class", "Agent / Dose", "Mechanism", "Role / Notes"],
["Antacids",
"Al/Mg hydroxide 15 mL QID\n1 hr after meals + bedtime",
"Neutralise gastric acid; transiently increase LES pressure",
"Rapid but short-lived relief; mild / infrequent GERD; NOT for maintenance"],
["Alginates\n(Gaviscon)",
"2–4 tablets QID + bedtime\n(Al(OH)3 + NaHCO3 + alginic acid)",
"Forms viscous raft on gastric surface as mechanical barrier; buffers acid",
"Post-prandial heartburn; safe in pregnancy; adjunct to PPI"],
["H2-Receptor\nAntagonists",
"Famotidine 20–40 mg BD\nRanitidine 150 mg BD–QID\nCimetidine 400 mg BD\nNizatidine 150 mg BD",
"Block parietal-cell H2 receptors → reduce acid secretion",
"Second-line; useful for nocturnal breakthrough; subject to tachyphylaxis; inferior to PPIs for esophagitis healing"],
["Proton Pump\nInhibitors (PPIs)\n— FIRST-LINE",
"Omeprazole 20–40 mg/day\nLansoprazole 15–30 mg/day\nPantoprazole 40 mg/day\nEsomeprazole 20–40 mg/day\nRabeprazole 20 mg/day\nDexlansoprazole 30–60 mg/day",
"Irreversibly inhibit H+/K+-ATPase on parietal-cell canalicular membrane; activated in acidic environment",
"CORNERSTONE of treatment. Take 30 min before meals. Superior to H2RAs. Relief in up to 90%. Titrate to lowest effective dose for maintenance."],
["Long-term PPI\nAdverse Effects",
"— (monitoring required)",
"Prolonged acid suppression",
"Vit B12 deficiency | C. difficile infection | Community pneumonia | Hip fracture | Hypomagnesaemia | SIBO | Diabetes (weak signal)"],
["Prokinetics\n(NOT routinely used)",
"Metoclopramide",
"Increases LES pressure + gastric emptying",
"No high-quality RCT data; risk of tardive dyskinesia; NOT recommended as monotherapy or adjunct"],
]
story.append(three_col_table(drug_data, [W*0.16, W*0.24, W*0.28, W*0.32], s))
story.append(sp(6))
# Step 3 – Endoscopic
story.append(Paragraph("STEP 3 — Endoscopic / Minimally Invasive Therapies (selected patients)", s["subsection"]))
endo_data = [
["Procedure", "Description", "Efficacy"],
["Transoral Incisionless\nFundoplication (TIF)",
"Endoscopic reconstruction of gastroesophageal valve; no incision",
"Normalises acid exposure in ~50%; suitable for small hernias"],
["Radiofrequency Ablation\n(Stretta)",
"RF energy delivered to LES/cardia; improves barrier function via fibrosis",
"Modest symptom improvement; reduces PPI use"],
["LINX (Magnetic\nSphincteR Augmentation)",
"Ring of magnetic beads implanted laparoscopically around LES",
"Prevents reflux while allowing swallowing; reversible; avoid in large hernias"],
]
story.append(three_col_table(endo_data, [W*0.25, W*0.42, W*0.33], s))
story.append(sp(6))
# Step 4 – Surgery
story.append(Paragraph("STEP 4 — Surgical Treatment (Anti-reflux Surgery)", s["subsection"]))
story.append(sp(3))
surg_ind = [
"Documented esophagitis not responding to or intolerant of PPIs",
"Persistent voluminous regurgitation despite PPI (PPIs control acid, NOT volume)",
"Patient preference to avoid lifelong medication",
"Complications — symptomatic hiatal hernia, peptic stricture",
"Pre-Barrett's dysplasia management (selected cases)",
]
story.append(info_box(surg_ind, s, bg=HexColor("#eaf2fb"), border=BLUE_MID))
story.append(sp(4))
surg_data = [
["Procedure", "Description", "Outcomes / Notes"],
["Nissen Fundoplication\n(360° wrap) — GOLD STANDARD",
"Fundus wrapped 360° around distal esophagus; laparoscopic approach standard",
"Equivalent to PPIs for esophagitis healing; SUPERIOR for persistent regurgitation; 15–20% recurrence"],
["Toupet Fundoplication\n(270° posterior wrap)",
"Partial posterior wrap; preferred when esophageal motility is poor",
"Lower dysphagia rate than Nissen; similar reflux control"],
["Dor Fundoplication\n(270° anterior wrap)",
"Partial anterior wrap; often used after Heller myotomy for achalasia",
"Reduces post-myotomy reflux"],
["Complications of Surgery",
"Dysphagia (tight wrap), gas-bloat syndrome, vagal nerve injury, diarrhoea",
"Require careful patient selection + meticulous technique"],
]
story.append(three_col_table(surg_data, [W*0.26, W*0.40, W*0.34], s))
story.append(sp(8))
# ── 8. MANAGEMENT ALGORITHM ──────────────────────────────────────────────
story.append(section_bar("8. MANAGEMENT ALGORITHM (SUMMARY FLOWCHART)", s))
story.append(sp(4))
algo_rows = [
["Typical symptoms\n(heartburn + regurgitation)",
"→",
"Empiric PPI\n(4–8 wks once daily,\n30 min before meals)\n+ Lifestyle measures",
"→",
"Symptoms resolve?\nYes → Lowest-dose\nmaintenance or PRN\nNo → Step up"],
["Alarm features\npresent or PPI failure",
"→",
"Upper GI Endoscopy\n(EGD)",
"→",
"Erosive esophagitis →\nIntensify PPI (BD dose)\nBarrett's → Surveillance\nNormal → pH monitoring"],
["PPI-refractory\n(symptoms persist on PPI)",
"→",
"MII-pH monitoring ON PPI\n(combined impedance-pH)",
"→",
"Acid breakthrough →\nDose adjust / switch PPI\nNon-acid reflux →\nSurgical referral"],
["Surgical candidate\n(regurgitation dominant\nor PPI intolerant)",
"→",
"Manometry +\npH testing OFF PPI",
"→",
"Laparoscopic Nissen\nFundoplication\n(or Toupet if poor motility)"],
]
algo_col = [W*0.21, W*0.04, W*0.26, W*0.04, W*0.45]
algo_table_data = []
for row in algo_rows:
tr = []
for i, cell in enumerate(row):
if i in (1, 3):
tr.append(Paragraph("<b>→</b>", ParagraphStyle("arr", fontName="Helvetica-Bold",
fontSize=14, textColor=BLUE_MID, alignment=TA_CENTER)))
elif i == 0:
tr.append(Paragraph(cell, ParagraphStyle("box0", fontName="Helvetica-Bold",
fontSize=7.5, textColor=BLUE_DARK, alignment=TA_CENTER, leading=10)))
elif i == 2:
tr.append(Paragraph(cell, ParagraphStyle("box2", fontName="Helvetica",
fontSize=7.5, textColor=GREY_TEXT, alignment=TA_CENTER, leading=10)))
else:
tr.append(Paragraph(cell, ParagraphStyle("box4", fontName="Helvetica",
fontSize=7.5, textColor=GREY_TEXT, alignment=TA_LEFT, leading=10)))
algo_table_data.append(tr)
algo_tbl = Table(algo_table_data, colWidths=algo_col, rowHeights=None)
algo_style = [
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("TOPPADDING", (0, 0), (-1, -1), 6),
("BOTTOMPADDING", (0, 0), (-1, -1), 6),
("LEFTPADDING", (0, 0), (-1, -1), 6),
("RIGHTPADDING", (0, 0), (-1, -1), 6),
("GRID", (0, 0), (0, -1), 0.5, GREY_LINE),
("GRID", (2, 0), (2, -1), 0.5, GREY_LINE),
("GRID", (4, 0), (4, -1), 0.5, GREY_LINE),
("ROWBACKGROUNDS", (0, 0), (-1, -1), [BLUE_LIGHT, WHITE, BLUE_LIGHT, WHITE]),
]
for r in range(len(algo_rows)):
bg = BLUE_LIGHT if r % 2 == 0 else WHITE
algo_style += [
("BACKGROUND", (0, r), (0, r), HexColor("#dbeef9")),
("BACKGROUND", (2, r), (2, r), HexColor("#eafaf1")),
("BACKGROUND", (4, r), (4, r), bg),
]
algo_tbl.setStyle(TableStyle(algo_style))
story.append(algo_tbl)
story.append(sp(8))
# ── 9. SPECIAL POPULATIONS ───────────────────────────────────────────────
story.append(section_bar("9. SPECIAL POPULATIONS", s))
story.append(sp(4))
sp_data = [
["Population", "Key Considerations"],
["Pregnancy",
"First-line: antacids + alginates (safest). H2RAs (famotidine) if needed. PPI: generally avoid in T1; "
"use only if clearly indicated in T2/T3. Positional measures essential."],
["Elderly",
"Atypical presentation — regurgitation, dyspepsia, noncardiac chest pain rather than heartburn. "
"Higher risk of complications (strictures, Barrett's). Caution with long-term PPI (fracture, B12 deficiency)."],
["Paediatric / Infants",
"Physiological GER normal until 6–12 months. GERD diagnosed with failure to thrive, apnea, recurrent aspiration. "
"Formula thickening + positioning first; H2RA/PPI if needed; Nissen fundoplication for severe/refractory cases."],
["Scleroderma / CTD",
"Profound esophageal dysmotility; high aspiration risk. PPI backbone; avoid fundoplication "
"(complete wrap worsens dysphagia); partial wrap or no surgery."],
["Non-erosive Reflux\nDisease (NERD)",
"~2/3 of GERD patients have normal EGD. Diagnose with pH monitoring. "
"Respond less well to PPIs; consider MII-pH; exclude functional heartburn."],
]
story.append(two_col_table(sp_data, [W*0.22, W*0.78], s))
story.append(sp(8))
# ── 10. PROGNOSIS ─────────────────────────────────────────────────────────
story.append(section_bar("10. PROGNOSIS", s))
story.append(sp(4))
story.append(Paragraph(
"The majority of patients with GERD do well with conservative antireflux measures and PPI therapy. "
"Long-term maintenance therapy is often required for erosive esophagitis. When anti-reflux surgery is performed "
"in appropriately selected patients, the outcome is usually excellent. Patients with Barrett's esophagus "
"require endoscopic surveillance; ablative therapy for dysplasia has excellent results. "
"The overall 5-year survival for esophageal adenocarcinoma detected early (T1) approaches 80%, "
"underlining the importance of Barrett's surveillance.",
s["body"]
))
story.append(sp(6))
# Sources
story.append(hr())
story.append(Paragraph(
"<b>Sources:</b> Goldman-Cecil Medicine (26th ed.) | Robbins Pathologic Basis of Disease (10th ed.) | "
"Yamada's Textbook of Gastroenterology (7th ed.) | Clinical Gastrointestinal Endoscopy (3rd ed.) | "
"Schwartz's Principles of Surgery (11th ed.) | Sabiston Textbook of Surgery",
s["footer"]
))
return story
# ── Entry point ───────────────────────────────────────────────────────────────
def main():
s = make_styles()
doc = SimpleDocTemplate(
OUTPUT,
pagesize=A4,
leftMargin=MARGIN,
rightMargin=MARGIN,
topMargin=88, # room for header on p1
bottomMargin=36,
)
story = build_story(s)
doc.build(
story,
onFirstPage=on_first_page,
onLaterPages=on_later_pages,
)
print(f"PDF saved to: {OUTPUT}")
if __name__ == "__main__":
main()