~/peds-gi-schema/generate_schema.py
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_RIGHT
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, KeepTogether, PageBreak
)
from reportlab.pdfgen import canvas
from reportlab.lib.colors import HexColor
import os
# ── Colour palette ──────────────────────────────────────────────
TEAL = HexColor("#0D6E6E") # dominant
TEAL_LIGHT = HexColor("#E8F5F5") # row shading
TEAL_MID = HexColor("#B2DFDB") # sub-header bg
ORANGE = HexColor("#E65100") # red-flag accent
ORANGE_PALE = HexColor("#FFF3E0") # red-flag row bg
GREY_DARK = HexColor("#37474F") # body text
GREY_LIGHT = HexColor("#F5F7F8") # alt row
WHITE = colors.white
BLACK = colors.black
PAGE_W, PAGE_H = A4
OUTPUT = "/home/daytona/workspace/peds-gi-schema/Pediatric_GI_Examination_Schema.pdf"
# ── Page template with header/footer ────────────────────────────
class DocTemplate(SimpleDocTemplate):
def __init__(self, filename, **kw):
super().__init__(filename, **kw)
def handle_pageBegin(self):
self._handle_pageBegin()
def add_header_footer(canvas_obj, doc):
canvas_obj.saveState()
w, h = A4
# Top bar
canvas_obj.setFillColor(TEAL)
canvas_obj.rect(0, h - 22*mm, w, 22*mm, fill=1, stroke=0)
canvas_obj.setFillColor(WHITE)
canvas_obj.setFont("Helvetica-Bold", 13)
canvas_obj.drawString(18*mm, h - 13*mm, "PEDIATRIC GI EXAMINATION SCHEMA")
canvas_obj.setFont("Helvetica", 9)
canvas_obj.drawRightString(w - 18*mm, h - 10*mm, "All Ages | Paired with GI History Format")
canvas_obj.drawRightString(w - 18*mm, h - 15*mm, f"Page {doc.page}")
# Bottom bar
canvas_obj.setFillColor(TEAL)
canvas_obj.rect(0, 0, w, 8*mm, fill=1, stroke=0)
canvas_obj.setFillColor(WHITE)
canvas_obj.setFont("Helvetica-Oblique", 7.5)
canvas_obj.drawString(18*mm, 2.5*mm,
"Clinical examination findings to pair with Pediatric GI History Format | "
"Sources: Tintinalli's Emergency Medicine; Bailey & Love's Surgery; Family Medicine 9e")
canvas_obj.restoreState()
# ── Style helpers ────────────────────────────────────────────────
styles = getSampleStyleSheet()
def S(name, **kw):
base = styles["Normal"]
return ParagraphStyle(name, parent=base, **kw)
BODY = S("body", fontSize=8.5, leading=12, textColor=GREY_DARK)
BOLD = S("bold", fontSize=8.5, leading=12, textColor=GREY_DARK, fontName="Helvetica-Bold")
RED = S("red", fontSize=8.5, leading=12, textColor=ORANGE, fontName="Helvetica-Bold")
SMALL = S("small", fontSize=7.5, leading=10, textColor=GREY_DARK, fontName="Helvetica-Oblique")
SMRED = S("smred", fontSize=7.5, leading=10, textColor=ORANGE, fontName="Helvetica-Bold")
TITLE_S= S("titls", fontSize=11, leading=14, textColor=WHITE, fontName="Helvetica-Bold")
SEC = S("sec", fontSize=9.5, leading=13, textColor=WHITE, fontName="Helvetica-Bold")
SECBIG = S("secbig", fontSize=10, leading=14, textColor=WHITE, fontName="Helvetica-Bold")
COL_W = [5.5*cm, 6.5*cm, 6.5*cm] # Finding | Normal/Abnormal | Clinical Significance
COL_W2 = [5.5*cm, 13*cm] # two-col rows
def section_header(title, color=TEAL):
"""Full-width coloured section banner."""
t = Table([[Paragraph(title, SEC)]], colWidths=[PAGE_W - 36*mm])
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), color),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING",(0,0),(-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 8),
]))
return t
def col_header_row(labels):
row = [Paragraph(f"<b>{l}</b>", S("ch", fontSize=8, leading=11,
textColor=WHITE, fontName="Helvetica-Bold")) for l in labels]
return row
def make_table(rows, col_widths=None, alt=True, header_row=None):
"""Build a styled 3-col table."""
cw = col_widths or COL_W
data = []
if header_row:
data.append(header_row)
data.extend(rows)
t = Table(data, colWidths=cw, repeatRows=1 if header_row else 0)
style_cmds = [
("FONTNAME", (0,0), (-1,-1), "Helvetica"),
("FONTSIZE", (0,0), (-1,-1), 8.5),
("LEADING", (0,0), (-1,-1), 12),
("VALIGN", (0,0), (-1,-1), "TOP"),
("TOPPADDING",(0,0), (-1,-1), 4),
("BOTTOMPADDING",(0,0),(-1,-1), 4),
("LEFTPADDING",(0,0),(-1,-1), 5),
("RIGHTPADDING",(0,0),(-1,-1), 5),
("GRID", (0,0), (-1,-1), 0.25, HexColor("#CFD8DC")),
]
if header_row:
style_cmds += [
("BACKGROUND",(0,0),(-1,0), TEAL_MID),
("FONTNAME", (0,0),(-1,0), "Helvetica-Bold"),
("FONTSIZE", (0,0),(-1,0), 8),
("TEXTCOLOR", (0,0),(-1,0), GREY_DARK),
]
if alt:
start = 1 if header_row else 0
for i in range(start, len(data)):
bg = GREY_LIGHT if i % 2 == (1 if header_row else 0) else WHITE
style_cmds.append(("BACKGROUND",(0,i),(-1,i), bg))
t.setStyle(TableStyle(style_cmds))
return t
def red_flag_table(rows):
"""Red-flag styled 3-col table."""
cw = [5*cm, 6*cm, 7.5*cm]
header = [Paragraph(f"<b>{l}</b>", S("rh", fontSize=8, leading=11,
textColor=WHITE, fontName="Helvetica-Bold"))
for l in ["Red Flag Sign", "How to Elicit", "Clinical Significance"]]
data = [header] + rows
t = Table(data, colWidths=cw, repeatRows=1)
style_cmds = [
("FONTNAME", (0,0),(-1,-1), "Helvetica"),
("FONTSIZE", (0,0),(-1,-1), 8.5),
("LEADING", (0,0),(-1,-1), 12),
("VALIGN", (0,0),(-1,-1), "TOP"),
("TOPPADDING",(0,0),(-1,-1), 4),
("BOTTOMPADDING",(0,0),(-1,-1), 4),
("LEFTPADDING",(0,0),(-1,-1), 5),
("RIGHTPADDING",(0,0),(-1,-1), 5),
("GRID", (0,0),(-1,-1), 0.25, HexColor("#FFCCBC")),
("BACKGROUND",(0,0),(-1,0), ORANGE),
("FONTNAME", (0,0),(-1,0), "Helvetica-Bold"),
("TEXTCOLOR", (0,0),(-1,0), WHITE),
]
for i in range(1, len(data)):
bg = ORANGE_PALE if i % 2 == 1 else WHITE
style_cmds.append(("BACKGROUND",(0,i),(-1,i), bg))
t.setStyle(TableStyle(style_cmds))
return t
def note_box(text):
data = [[Paragraph(text, S("nb", fontSize=8, leading=11,
textColor=GREY_DARK, fontName="Helvetica-Oblique"))]]
t = Table(data, colWidths=[PAGE_W - 36*mm])
t.setStyle(TableStyle([
("BACKGROUND",(0,0),(-1,-1), HexColor("#E3F2FD")),
("LEFTPADDING",(0,0),(-1,-1), 8),
("TOPPADDING", (0,0),(-1,-1), 5),
("BOTTOMPADDING",(0,0),(-1,-1), 5),
("BOX",(0,0),(-1,-1), 0.5, HexColor("#1565C0")),
]))
return t
# ── CONTENT BUILDER ──────────────────────────────────────────────
story = []
# ── INTRO BOX ──
intro_data = [[
Paragraph("<b>PURPOSE</b>", S("ib", fontSize=9, fontName="Helvetica-Bold", textColor=TEAL)),
Paragraph(
"Systematic physical examination guide for paediatric GI complaints. "
"Each finding paired with its clinical significance and age-specific notes. "
"Always examine the <b>anxious child last</b> (leave painful areas to the end). "
"Warm hands, distract with conversation, and observe from the end of the bed first.",
S("ib2", fontSize=8.5, leading=12, textColor=GREY_DARK))
]]
it = Table(intro_data, colWidths=[3*cm, PAGE_W - 36*mm - 3*cm])
it.setStyle(TableStyle([
("BACKGROUND",(0,0),(-1,-1), TEAL_LIGHT),
("VALIGN",(0,0),(-1,-1),"MIDDLE"),
("TOPPADDING",(0,0),(-1,-1),7),
("BOTTOMPADDING",(0,0),(-1,-1),7),
("LEFTPADDING",(0,0),(-1,-1),8),
("BOX",(0,0),(-1,-1),0.75,TEAL),
]))
story.append(it)
story.append(Spacer(1, 5*mm))
# ═══════════════════════════════════════════════════════════════
# SECTION 1: GENERAL INSPECTION
# ═══════════════════════════════════════════════════════════════
story.append(section_header("1. GENERAL INSPECTION (Before Touching the Child)"))
story.append(Spacer(1, 1*mm))
hdr = [Paragraph(f"<b>{l}</b>", S("h", fontSize=8, textColor=GREY_DARK, fontName="Helvetica-Bold"))
for l in ["What to Look For", "Normal Finding", "Abnormal / Significance"]]
gen_rows = [
[Paragraph("Appearance & distress", BOLD),
Paragraph("Comfortable, playing, alert", BODY),
Paragraph("Lying still, knees drawn up (peritonitis). Rolling/writhing (colic). Lethargic (dehydration, sepsis).", BODY)],
[Paragraph("Nutritional state / growth", BOLD),
Paragraph("Weight appropriate for age", BODY),
Paragraph("Wasted/thin = chronic disease (IBD, coeliac, CF, malignancy). Oedema = hypoalbuminaemia (liver failure, protein-losing enteropathy).", BODY)],
[Paragraph("Skin colour", BOLD),
Paragraph("Pink, well-perfused", BODY),
Paragraph("Jaundice (bilirubin >35 µmol/L). Pallor (anaemia from GI bleed, coeliac). Mottling/grey = shock.", BODY)],
[Paragraph("Face: eyes, mouth", BOLD),
Paragraph("Moist mucous membranes, clear eyes", BODY),
Paragraph("Sunken eyes, dry lips = dehydration. Aphthous ulcers = Crohn's disease. Angular stomatitis = iron/B12 deficiency (coeliac).", BODY)],
[Paragraph("Hands & nails", BOLD),
Paragraph("Pink, normal capillary refill <2 sec", BODY),
Paragraph("Clubbing = IBD, CF, liver cirrhosis. Koilonychia = iron deficiency. Leuconychia = hypoalbuminaemia. Palmar erythema = liver disease.", BODY)],
[Paragraph("Posture / gait", BOLD),
Paragraph("Normal movement", BODY),
Paragraph("Child walks bent over / guards abdomen = peritonism. Refuses to jump/walk on heels = peritoneal irritation (ask child to hop).", BODY)],
]
story.append(make_table(gen_rows, col_widths=COL_W, header_row=hdr))
story.append(Spacer(1, 3*mm))
# ═══════════════════════════════════════════════════════════════
# SECTION 2: VITAL SIGNS
# ═══════════════════════════════════════════════════════════════
story.append(section_header("2. VITAL SIGNS (Age-Referenced)"))
story.append(Spacer(1, 1*mm))
vs_rows = [
[Paragraph("Temperature", BOLD),
Paragraph("36.5-37.5 °C", BODY),
Paragraph("Fever >38°C: appendicitis, gastroenteritis, IBD, mesenteric adenitis, cholangitis. Hypothermia in neonates = sepsis.", BODY)],
[Paragraph("Heart Rate\n<i>(Tachycardia = early dehydration/pain)</i>", S("bi", fontSize=8.5, leading=12, textColor=GREY_DARK, fontName="Helvetica-BoldOblique")),
Paragraph("Neonate: 120-160\nInfant: 100-160\nToddler: 90-150\nSchool-age: 70-120", BODY),
Paragraph("Persistent tachycardia beyond pain control = dehydration or haemorrhage. Bradycardia in sick child = pre-arrest.", BODY)],
[Paragraph("Blood Pressure", BOLD),
Paragraph("Age-appropriate (roughly 90 + 2×age mmHg systolic)", BODY),
Paragraph("Hypotension = late dehydration, haemorrhagic shock, sepsis. Hypertension in GI context = Henoch-Schonlein purpura (renal involvement).", BODY)],
[Paragraph("Respiratory Rate", BOLD),
Paragraph("Infant: <50/min\nToddler: <40/min\nSchool-age: <30/min", BODY),
Paragraph("Tachypnoea = metabolic acidosis (severe dehydration), right lower lobe pneumonia (mimics abdominal pain), diaphragmatic irritation.", BODY)],
[Paragraph("Capillary Refill Time", BOLD),
Paragraph("<2 seconds centrally", BODY),
Paragraph("2-3 sec = mild-moderate dehydration. >3 sec = poor perfusion, shock. Assess centrally (sternum) not peripherally in cold child.", BODY)],
[Paragraph("Weight (plot on chart)", BOLD),
Paragraph("Following expected centile", BODY),
Paragraph("Crossing 2 centile lines downward = significant failure to thrive. Essential for fluid/drug dosing and nutritional assessment.", BODY)],
]
story.append(make_table(vs_rows, col_widths=COL_W, header_row=hdr))
story.append(Spacer(1, 3*mm))
# ═══════════════════════════════════════════════════════════════
# SECTION 3: HANDS, EYES, MOUTH (PERIPHERAL SIGNS)
# ═══════════════════════════════════════════════════════════════
story.append(section_header("3. PERIPHERAL SIGNS (Hands → Eyes → Mouth → Neck)"))
story.append(Spacer(1, 1*mm))
periph_rows = [
[Paragraph("HANDS", RED),
Paragraph("", BODY), Paragraph("", BODY)],
[Paragraph("Clubbing", BOLD),
Paragraph("Absent (Schamroth's window visible)", BODY),
Paragraph("IBD (especially Crohn's), CF, liver cirrhosis, coeliac. Confirm: loss of Schamroth's window, fluctuant nail base.", BODY)],
[Paragraph("Koilonychia (spoon nails)", BOLD),
Paragraph("Normal convex nails", BODY),
Paragraph("Chronic iron deficiency = coeliac disease, IBD, GI blood loss.", BODY)],
[Paragraph("Leuconychia", BOLD),
Paragraph("Pink nails, no white bands", BODY),
Paragraph("Hypoalbuminaemia = liver failure, protein-losing enteropathy, malabsorption.", BODY)],
[Paragraph("Palmar erythema", BOLD),
Paragraph("Normal skin tone", BODY),
Paragraph("Chronic liver disease (hyperdynamic circulation). Also physiological in pregnancy.", BODY)],
[Paragraph("Dupuytren's contracture", BOLD),
Paragraph("Full hand extension", BODY),
Paragraph("Rare in children; if present = liver disease (alcoholic cirrhosis in adults; in children, consider genetic liver disease).", BODY)],
[Paragraph("EYES", RED),
Paragraph("", BODY), Paragraph("", BODY)],
[Paragraph("Scleral icterus (jaundice)", BOLD),
Paragraph("White sclera", BODY),
Paragraph("Earliest sign of jaundice. Appears at bilirubin ~35 µmol/L (before skin jaundice). Confirms hepatic/haemolytic pathology.", BODY)],
[Paragraph("Conjunctival pallor", BOLD),
Paragraph("Pink conjunctivae", BODY),
Paragraph("Anaemia from: GI blood loss, coeliac (B12/folate/iron malabsorption), IBD. Pull down lower lid gently.", BODY)],
[Paragraph("Sunken eyes", BOLD),
Paragraph("Normal eye fullness", BODY),
Paragraph("Dehydration. Assess alongside skin turgor and mucous membranes. Sunken eyes = moderate-severe dehydration (>5-10% body weight).", BODY)],
[Paragraph("Kayser-Fleischer rings", BOLD),
Paragraph("Clear iris/cornea", BODY),
Paragraph("Wilson's disease (hepatolenticular degeneration) - copper deposits in Descemet's membrane. Requires slit-lamp. Consider in child with liver disease + neuro symptoms.", BODY)],
[Paragraph("MOUTH", RED),
Paragraph("", BODY), Paragraph("", BODY)],
[Paragraph("Mucous membranes - moisture", BOLD),
Paragraph("Moist, pink", BODY),
Paragraph("Dry/tacky = dehydration. Key dehydration sign in infants (crying without tears is complementary).", BODY)],
[Paragraph("Aphthous ulcers", BOLD),
Paragraph("Absent", BODY),
Paragraph("Crohn's disease. Can precede intestinal symptoms by years. Also seen in coeliac, Behcet's, nutritional deficiency.", BODY)],
[Paragraph("Angular stomatitis / glossitis", BOLD),
Paragraph("Intact lip angles, normal tongue", BODY),
Paragraph("Iron, B2, B12 deficiency = malabsorption (coeliac, IBD). Smooth glossy tongue = B12/folate deficiency.", BODY)],
[Paragraph("Dental erosions", BOLD),
Paragraph("Normal enamel", BODY),
Paragraph("Chronic GERD (acid erosion of posterior teeth). Important sign in older children/adolescents with reflux.", BODY)],
[Paragraph("NECK", RED),
Paragraph("", BODY), Paragraph("", BODY)],
[Paragraph("Lymphadenopathy", BOLD),
Paragraph("No palpable nodes", BODY),
Paragraph("Mesenteric adenitis (enlarged mesenteric nodes causing abdominal pain, often confused with appendicitis). Generalised nodes = IBD, lymphoma.", BODY)],
]
# mark section sub-rows
periph_data = periph_rows
t = Table([[Paragraph(f"<b>{l}</b>", S("h", fontSize=8, textColor=GREY_DARK, fontName="Helvetica-Bold"))
for l in ["Sign", "Normal", "Abnormal / Significance"]]] + periph_data,
colWidths=COL_W, repeatRows=1)
style = [
("FONTNAME", (0,0),(-1,-1),"Helvetica"),
("FONTSIZE", (0,0),(-1,-1),8.5),
("LEADING", (0,0),(-1,-1),12),
("VALIGN", (0,0),(-1,-1),"TOP"),
("TOPPADDING",(0,0),(-1,-1),4),
("BOTTOMPADDING",(0,0),(-1,-1),4),
("LEFTPADDING",(0,0),(-1,-1),5),
("GRID", (0,0),(-1,-1),0.25,HexColor("#CFD8DC")),
("BACKGROUND",(0,0),(-1,0), TEAL_MID),
("FONTNAME", (0,0),(-1,0),"Helvetica-Bold"),
("FONTSIZE", (0,0),(-1,0),8),
]
# highlight section dividers (HANDS, EYES, MOUTH, NECK rows)
divider_rows = [i+1 for i,r in enumerate(periph_data)
if isinstance(r[0], Paragraph) and r[0].style.name == "red"]
for dr in divider_rows:
style.append(("BACKGROUND",(0,dr),(-1,dr), HexColor("#FFF8E1")))
style.append(("FONTNAME",(0,dr),(-1,dr),"Helvetica-Bold"))
for i in range(1, len(periph_data)+1):
if i not in divider_rows:
bg = GREY_LIGHT if i % 2 == 1 else WHITE
style.append(("BACKGROUND",(0,i),(-1,i),bg))
t.setStyle(TableStyle(style))
story.append(t)
story.append(Spacer(1, 3*mm))
# ═══════════════════════════════════════════════════════════════
# SECTION 4: ABDOMINAL EXAMINATION
# ═══════════════════════════════════════════════════════════════
story.append(section_header("4. ABDOMINAL EXAMINATION"))
story.append(Spacer(1, 1*mm))
story.append(note_box(
"SEQUENCE: Inspection → Auscultation → LIGHT palpation → DEEP palpation → Percussion → Special tests | "
"Always ask 'Show me where it hurts' first and leave that area last | "
"Watch the child's face, not your hand, during palpation"))
story.append(Spacer(1, 2*mm))
# 4a Inspection
story.append(Table([[Paragraph("4a. INSPECTION", S("sub", fontSize=9, textColor=TEAL, fontName="Helvetica-Bold"))]],
colWidths=[PAGE_W-36*mm]))
story.append(Spacer(1,1*mm))
ins_rows = [
[Paragraph("Abdominal shape / contour", BOLD),
Paragraph("Slightly protuberant in toddlers (normal). Flat in older children.", BODY),
Paragraph("Distension: gaseous (obstruction, aerophagia, constipation) vs fluid (ascites - flanks full). Scaphoid = severe malnutrition.", BODY)],
[Paragraph("Visible peristalsis", BOLD),
Paragraph("Absent", BODY),
Paragraph("Visible gastric peristalsis (left → right) in infant = pyloric stenosis. Visible bowel loops = obstruction.", BODY)],
[Paragraph("Umbilicus", BOLD),
Paragraph("Central, inverted", BODY),
Paragraph("Everted = raised intra-abdominal pressure (ascites, mass, organomegaly). Displaced = mass effect.", BODY)],
[Paragraph("Skin: scars, striae, collateral veins", BOLD),
Paragraph("Intact skin, no scarring", BODY),
Paragraph("Surgical scars = adhesions risk. Caput medusae (dilated veins radiating from umbilicus) = portal hypertension. Striae = rapid weight change, Cushing's.", BODY)],
[Paragraph("Visible mass or asymmetry", BOLD),
Paragraph("Symmetric", BODY),
Paragraph("Wilms tumour (nephroblastoma), intussusception mass, hepatosplenomegaly, faecal loading (left iliac fossa). Any visible mass = urgent imaging.", BODY)],
[Paragraph("Movement with respiration", BOLD),
Paragraph("Abdomen moves freely with breathing", BODY),
Paragraph("Abdomen not moving with respiration = peritonitis (child splints abdomen). Paradoxical breathing in infant = respiratory distress.", BODY)],
]
story.append(make_table(ins_rows, col_widths=COL_W, header_row=hdr))
story.append(Spacer(1,2*mm))
# 4b Auscultation
story.append(Table([[Paragraph("4b. AUSCULTATION (Before palpation - palpation changes bowel sounds)", S("sub", fontSize=9, textColor=TEAL, fontName="Helvetica-Bold"))]],
colWidths=[PAGE_W-36*mm]))
story.append(Spacer(1,1*mm))
aus_rows = [
[Paragraph("Bowel sounds", BOLD),
Paragraph("Intermittent gurgles every 5-10 sec", BODY),
Paragraph("Absent (>2 min silence) = ileus (post-op, peritonitis, severe inflammation). High-pitched/tinkling = mechanical obstruction. Borborygmi = gastroenteritis, IBS.", BODY)],
[Paragraph("Hepatic bruit", BOLD),
Paragraph("Absent", BODY),
Paragraph("Hepatocellular carcinoma, hepatic haemangioma (infant), AV malformation.", BODY)],
[Paragraph("Renal bruit (flanks)", BOLD),
Paragraph("Absent", BODY),
Paragraph("Renal artery stenosis (rare in children; consider in resistant hypertension).", BODY)],
[Paragraph("Succussion splash", BOLD),
Paragraph("Absent when shaking abdomen", BODY),
Paragraph("Audible splash >3 hrs post-feed = gastric outlet obstruction (pyloric stenosis, antral web).", BODY)],
]
story.append(make_table(aus_rows, col_widths=COL_W, header_row=hdr))
story.append(Spacer(1,2*mm))
# 4c Palpation
story.append(Table([[Paragraph("4c. PALPATION (Start away from pain, use warm hands, child supine with knees flexed)", S("sub", fontSize=9, textColor=TEAL, fontName="Helvetica-Bold"))]],
colWidths=[PAGE_W-36*mm]))
story.append(Spacer(1,1*mm))
palp_rows = [
[Paragraph("Voluntary guarding", BOLD),
Paragraph("Abdomen soft, relaxed", BODY),
Paragraph("Child tenses abdomen when you approach = voluntary (pain anticipation). Distinguish from involuntary guarding (persists despite distraction).", BODY)],
[Paragraph("Involuntary guarding / rigidity", BOLD),
Paragraph("Absent - abdomen remains relaxed", BODY),
Paragraph("Board-like rigidity = peritonitis (perforation, appendicitis, ruptured viscus). Rigid abdomen is a surgical emergency.", BODY)],
[Paragraph("Tenderness - general vs localised", BOLD),
Paragraph("Non-tender on gentle palpation", BODY),
Paragraph("Localised RIF = appendicitis (McBurney's point: 1/3 from ASIS to umbilicus). Generalised = peritonitis, gastroenteritis. Epigastric = peptic ulcer, pancreatitis.", BODY)],
[Paragraph("Liver edge (start RIF, move upward)", BOLD),
Paragraph("Up to 2 cm below costal margin is normal in infants/toddlers", BODY),
Paragraph(">3 cm in infants or any in school-age = hepatomegaly. Note texture: smooth (hepatitis, congestion) vs nodular (cirrhosis, storage disease, malignancy).", BODY)],
[Paragraph("Spleen (start RIF, move toward LUQ)", BOLD),
Paragraph("Not palpable (or barely tip in young infant)", BODY),
Paragraph("Splenomegaly: portal hypertension, haemolytic anaemia (sickle cell, spherocytosis), EBV, storage diseases. Massive = malaria, CML.", BODY)],
[Paragraph("Hepatosplenomegaly", BOLD),
Paragraph("Not present", BODY),
Paragraph("Both enlarged = portal hypertension, storage disease (Gaucher's, Niemann-Pick), EBV, leukaemia, haemolytic conditions.", BODY)],
[Paragraph("Kidneys (ballottement)", BOLD),
Paragraph("Not palpable in older children. Neonates: tip occasionally felt.", BODY),
Paragraph("Palpable enlarged kidney = Wilms tumour (hard, smooth, crosses midline), hydronephrosis, PCKD. Wilms is the most common paediatric abdominal tumour.", BODY)],
[Paragraph("Palpable faecal mass", BOLD),
Paragraph("Absent", BODY),
Paragraph("Left iliac fossa or suprapubic = constipation (indentable, non-tender). Right iliac fossa mass = appendix mass/abscess, Crohn's ileocaecal mass, caecal carcinoma.", BODY)],
[Paragraph("Palpable bowel loop / sausage shape", BOLD),
Paragraph("Absent", BODY),
Paragraph("'Sausage-shaped mass' in RUQ or across abdomen = intussusception. Often with empty RIF (Dance's sign = absence of bowel in RIF). Urgent US needed.", BODY)],
[Paragraph("Inguinal hernias / testes", BOLD),
Paragraph("Absent hernias. Testes descended bilaterally.", BODY),
Paragraph("Incarcerated inguinal hernia = obstruction. Undescended testes. Always examine groin in any child with abdominal pain.", BODY)],
]
story.append(make_table(palp_rows, col_widths=COL_W, header_row=hdr))
story.append(Spacer(1,2*mm))
# 4d Percussion
story.append(Table([[Paragraph("4d. PERCUSSION", S("sub", fontSize=9, textColor=TEAL, fontName="Helvetica-Bold"))]],
colWidths=[PAGE_W-36*mm]))
story.append(Spacer(1,1*mm))
perc_rows = [
[Paragraph("General tympany vs dullness", BOLD),
Paragraph("Tympanic over most of abdomen", BODY),
Paragraph("Dullness = fluid, solid mass, organomegaly. Hyper-resonance = gaseous distension (obstruction, aerophagia).", BODY)],
[Paragraph("Liver dullness span", BOLD),
Paragraph("Dull zone: ~6-8 cm in school-age", BODY),
Paragraph("Increased liver span = hepatomegaly. Loss of liver dullness = pneumoperitoneum (air under diaphragm = perforation = emergency).", BODY)],
[Paragraph("Shifting dullness", BOLD),
Paragraph("Absent - dullness does not shift with position", BODY),
Paragraph("Positive = ascites (>500 mL). Causes: liver failure, nephrotic syndrome, protein-losing enteropathy, malignancy.", BODY)],
[Paragraph("Fluid thrill", BOLD),
Paragraph("Absent", BODY),
Paragraph("Tense ascites (large volume). One hand on flank, assistant's hand on midline to prevent fat conduction, tap opposite flank.", BODY)],
[Paragraph("Renal angle percussion (CVA tenderness)", BOLD),
Paragraph("Non-tender", BODY),
Paragraph("Positive (pain on fist percussion over costovertebral angle) = pyelonephritis, renal calculi. UTI commonly mimics abdominal pain in children.", BODY)],
[Paragraph("Percuss for bladder", BOLD),
Paragraph("Dull only if distended", BODY),
Paragraph("Suprapubic dullness = urinary retention (uncommon in children but seen post-surgery, with neurogenic bladder).", BODY)],
]
story.append(make_table(perc_rows, col_widths=COL_W, header_row=hdr))
story.append(Spacer(1, 3*mm))
# ═══════════════════════════════════════════════════════════════
# SECTION 5: SPECIAL / ELICITED SIGNS
# ═══════════════════════════════════════════════════════════════
story.append(section_header("5. SPECIAL SIGNS (Elicited Tests)"))
story.append(Spacer(1, 1*mm))
spec_hdr = [Paragraph(f"<b>{l}</b>", S("sh", fontSize=8, textColor=GREY_DARK, fontName="Helvetica-Bold"))
for l in ["Sign", "How to Elicit", "Positive Finding / Significance"]]
spec_rows = [
[Paragraph("Rebound tenderness\n(Blumberg's sign)", BOLD),
Paragraph("Press slowly and firmly, then release suddenly. Alternatively: percussion tenderness (gentler, preferred in children).", BODY),
Paragraph("Pain worse on RELEASE than press = peritoneal irritation. Appendicitis, perforated viscus. Percussion tenderness = same significance, less distressing for child.", BODY)],
[Paragraph("Rovsing's sign", BOLD),
Paragraph("Press LIF (left iliac fossa) while watching/asking about RIF pain.", BODY),
Paragraph("Positive = pain in RIF when LIF compressed = appendicitis (referred peritoneal irritation from right side).", BODY)],
[Paragraph("Psoas sign", BOLD),
Paragraph("Child lies on left side. Extend right hip passively (or ask child to raise right leg against resistance).", BODY),
Paragraph("Positive = pain in RIF = retrocaecal appendix (lying on psoas muscle). Also positive in right-sided psoas abscess.", BODY)],
[Paragraph("Obturator sign", BOLD),
Paragraph("Flex and internally rotate right hip (with knee flexed).", BODY),
Paragraph("Positive = RIF pain = pelvic appendix or pelvic abscess (appendix lies near obturator internus).", BODY)],
[Paragraph("Heel-drop test\n(Markle's sign)", BOLD),
Paragraph("Ask child to stand on tiptoe then drop onto heels (or lower child firmly onto heels).", BODY),
Paragraph("Pain on heel strike = peritoneal irritation. Sensitive, non-invasive, child-friendly alternative to rebound tenderness.", BODY)],
[Paragraph("Carnett's sign", BOLD),
Paragraph("Locate tender area. Ask child to tense abdomen (lift head or legs). Palpate same area.", BODY),
Paragraph("Pain UNCHANGED or WORSE with tensed muscles = abdominal wall pain (not visceral). Pain improves = intra-abdominal cause.", BODY)],
[Paragraph("Murphy's sign", BOLD),
Paragraph("Deep palpation in RUQ. Ask child to breathe in deeply.", BODY),
Paragraph("Positive = sudden inspiration arrest due to pain = cholecystitis (gallbladder descends onto examining hand). Uncommon in children but seen in sickle cell, CF.", BODY)],
[Paragraph("Dance's sign", BOLD),
Paragraph("Palpate right iliac fossa.", BODY),
Paragraph("Empty RIF (absent bowel gas/loops) = intussusception (bowel telescoped away from RIF). Paired with sausage-shaped mass elsewhere.", BODY)],
[Paragraph("Cullen's sign / Grey Turner's sign", BOLD),
Paragraph("Inspect periumbilical (Cullen's) and flank (Grey Turner's) skin.", BODY),
Paragraph("Bruising = retroperitoneal haemorrhage = severe pancreatitis, ruptured AAA (rare in children, but pancreatitis occurs).", BODY)],
[Paragraph("Succussion splash", BOLD),
Paragraph("Hands on sides of abdomen, shake patient side-to-side. Listen with stethoscope or unaided.", BODY),
Paragraph("Audible splash = fluid + air in obstructed hollow viscus. Gastric outlet obstruction (pyloric stenosis, antral pathology).", BODY)],
]
story.append(make_table(spec_rows, col_widths=[5*cm, 6*cm, 7.5*cm], header_row=spec_hdr))
story.append(Spacer(1, 3*mm))
# ═══════════════════════════════════════════════════════════════
# SECTION 6: HERNIAL ORIFICES & GENITALIA
# ═══════════════════════════════════════════════════════════════
story.append(section_header("6. HERNIAL ORIFICES, GENITALIA & RECTUM"))
story.append(Spacer(1, 1*mm))
hern_rows = [
[Paragraph("Inguinal hernias", BOLD),
Paragraph("No bulge at rest or on coughing/crying", BODY),
Paragraph("Indirect inguinal hernia most common in boys <1 year. Risk of incarceration = strangulation = obstruction. ALWAYS examine groin in child with vomiting.", BODY)],
[Paragraph("Umbilical hernia", BOLD),
Paragraph("No persistent defect in school-age (usually closes by age 5)", BODY),
Paragraph("Persistent in >5 years = consider repair. Incarceration rare. More common in African children and premature infants.", BODY)],
[Paragraph("Femoral hernia", BOLD),
Paragraph("Absent", BODY),
Paragraph("Rare in children. Below and lateral to pubic tubercle. Higher incarceration risk than inguinal hernias.", BODY)],
[Paragraph("Testes (males)", BOLD),
Paragraph("Both testes palpable in scrotum bilaterally", BODY),
Paragraph("Undescended = orchidopexy needed by age 1. Testicular torsion = sudden severe scrotal pain, absent cremasteric reflex, high-riding testis. Presents as abdominal pain - ALWAYS examine scrotum in boys with abdominal pain.", BODY)],
[Paragraph("Cremasteric reflex", BOLD),
Paragraph("Present (stroking medial thigh raises ipsilateral testis)", BODY),
Paragraph("Absent in testicular torsion. Sensitive test - if absent, urgent surgical exploration needed.", BODY)],
[Paragraph("Perianal inspection", BOLD),
Paragraph("Intact skin, no fissures, normal tone", BODY),
Paragraph("Anal fissure = most common cause of rectal bleeding in infants/toddlers (hard stool + pain + surface blood). Skin tags = Crohn's. Warts = consider abuse. Patulous anus = neurological cause of constipation.", BODY)],
[Paragraph("Digital Rectal Examination\n(in children: consider carefully, only if indicated)", BOLD),
Paragraph("Performed only when clinically necessary with consent/assent", BODY),
Paragraph("Empty rectum in constipated child = high impaction (Hirschsprung's). Hard stool = functional constipation. Blood on glove = colitis. Tender anteriorly = pelvic appendicitis/abscess. Not routinely needed for appendicitis diagnosis (evidence does not support diagnostic utility).", BODY)],
]
story.append(make_table(hern_rows, col_widths=COL_W, header_row=hdr))
story.append(Spacer(1, 3*mm))
# ═══════════════════════════════════════════════════════════════
# SECTION 7: EXTRA-INTESTINAL MANIFESTATIONS
# ═══════════════════════════════════════════════════════════════
story.append(section_header("7. EXTRA-INTESTINAL MANIFESTATIONS (Examine These in Every GI Case)"))
story.append(Spacer(1, 1*mm))
ext_rows = [
[Paragraph("Skin: erythema nodosum", BOLD),
Paragraph("Painful red nodules on shins/extensors", BODY),
Paragraph("IBD (Crohn's > UC), sarcoidosis, Behcet's. Signals systemic inflammation.", BODY)],
[Paragraph("Skin: pyoderma gangrenosum", BOLD),
Paragraph("Necrotic ulcers with violaceous edge", BODY),
Paragraph("IBD (UC > Crohn's). Pathergy present. Indicates severe/active disease.", BODY)],
[Paragraph("Skin: dermatitis herpetiformis", BOLD),
Paragraph("Itchy vesicular rash elbows, knees, buttocks", BODY),
Paragraph("Gluten sensitivity = coeliac disease. Nearly pathognomonic.", BODY)],
[Paragraph("Skin: jaundice (sclera first)", BOLD),
Paragraph("Yellow discolouration sclera → skin", BODY),
Paragraph("Pre-hepatic (haemolysis), hepatic (hepatitis, Wilson's, metabolic), post-hepatic (biliary atresia, choledochal cyst).", BODY)],
[Paragraph("Joints: arthritis/arthralgia", BOLD),
Paragraph("Swollen, warm, tender joints", BODY),
Paragraph("IBD (peripheral arthritis - large joints, parallels bowel activity). Reactive arthritis post-GI infection (Salmonella, Campylobacter, Yersinia).", BODY)],
[Paragraph("Eyes: uveitis / episcleritis", BOLD),
Paragraph("Red, painful eye; photophobia", BODY),
Paragraph("IBD extra-intestinal manifestation. Uveitis can cause blindness if untreated - always refer ophthalmology.", BODY)],
[Paragraph("Mouth: angular cheilitis, glossitis", BOLD),
Paragraph("Inflamed lip angles; smooth red tongue", BODY),
Paragraph("Nutritional deficiency (iron, B12, folate, B2) = coeliac, IBD, chronic malabsorption.", BODY)],
[Paragraph("Growth: height, weight, puberty stage", BOLD),
Paragraph("Appropriate for age on growth chart", BODY),
Paragraph("Delayed growth/puberty = classic in Crohn's disease. Can be presenting sign before bowel symptoms. Always plot and compare to previous measurements.", BODY)],
]
story.append(make_table(ext_rows, col_widths=[5.5*cm, 5.5*cm, 7.5*cm], header_row=[
Paragraph("<b>Manifestation</b>", S("h", fontSize=8, textColor=GREY_DARK, fontName="Helvetica-Bold")),
Paragraph("<b>What You See</b>", S("h", fontSize=8, textColor=GREY_DARK, fontName="Helvetica-Bold")),
Paragraph("<b>Significance / Diagnosis</b>", S("h", fontSize=8, textColor=GREY_DARK, fontName="Helvetica-Bold")),
]))
story.append(Spacer(1, 3*mm))
# ═══════════════════════════════════════════════════════════════
# SECTION 8: DEHYDRATION ASSESSMENT
# ═══════════════════════════════════════════════════════════════
story.append(section_header("8. DEHYDRATION ASSESSMENT (Critical in Paediatric GI)"))
story.append(Spacer(1, 1*mm))
dehyd_hdr = [Paragraph(f"<b>{l}</b>", S("dh", fontSize=8, textColor=GREY_DARK, fontName="Helvetica-Bold"))
for l in ["Clinical Sign", "Mild (<5%)", "Moderate (5-10%)", "Severe (>10%)"]]
dehyd_rows = [
[Paragraph("General appearance", BODY),
Paragraph("Alert, normal", BODY),
Paragraph("Restless, irritable", BODY),
Paragraph("Lethargic, limp, unresponsive", BODY)],
[Paragraph("Eyes", BODY),
Paragraph("Normal", BODY),
Paragraph("Slightly sunken", BODY),
Paragraph("Deeply sunken, no tears", BODY)],
[Paragraph("Mucous membranes", BODY),
Paragraph("Moist", BODY),
Paragraph("Dry", BODY),
Paragraph("Parched, cracked", BODY)],
[Paragraph("Skin turgor\n(pinch abdomen/thigh)", BODY),
Paragraph("Returns instantly", BODY),
Paragraph("Returns slowly (1-2 sec)", BODY),
Paragraph("Returns very slowly (>2 sec) or stays tented", BODY)],
[Paragraph("Capillary refill", BODY),
Paragraph("<2 sec", BODY),
Paragraph("2-3 sec", BODY),
Paragraph(">3 sec", BODY)],
[Paragraph("Heart rate", BODY),
Paragraph("Normal", BODY),
Paragraph("Mildly elevated", BODY),
Paragraph("Significantly elevated", BODY)],
[Paragraph("Blood pressure", BODY),
Paragraph("Normal", BODY),
Paragraph("Normal or slightly low", BODY),
Paragraph("Hypotensive, undetectable", BODY)],
[Paragraph("Urine output", BODY),
Paragraph("Normal", BODY),
Paragraph("Reduced, concentrated", BODY),
Paragraph("Oliguric/anuric", BODY)],
[Paragraph("Fontanelle (infants)", BODY),
Paragraph("Flat", BODY),
Paragraph("Slightly sunken", BODY),
Paragraph("Markedly sunken", BODY)],
[Paragraph("MANAGEMENT", RED),
Paragraph("Oral rehydration\nsolution (ORS)", BODY),
Paragraph("ORS 50-100 mL/kg\nover 2-4 hours", BODY),
Paragraph("IV fluids STAT\n20 mL/kg 0.9% NaCl bolus\nRepeat as needed", BODY)],
]
dt = Table([dehyd_hdr] + dehyd_rows, colWidths=[5*cm, 4*cm, 4.5*cm, 5*cm], repeatRows=1)
dstyle = [
("FONTNAME",(0,0),(-1,-1),"Helvetica"),
("FONTSIZE",(0,0),(-1,-1),8.5),
("LEADING",(0,0),(-1,-1),11),
("VALIGN",(0,0),(-1,-1),"TOP"),
("TOPPADDING",(0,0),(-1,-1),4),
("BOTTOMPADDING",(0,0),(-1,-1),4),
("LEFTPADDING",(0,0),(-1,-1),5),
("GRID",(0,0),(-1,-1),0.25,HexColor("#CFD8DC")),
("BACKGROUND",(0,0),(-1,0),TEAL_MID),
("FONTNAME",(0,0),(-1,0),"Helvetica-Bold"),
("FONTSIZE",(0,0),(-1,0),8),
# Colour severity columns
("BACKGROUND",(1,1), (1,-1), HexColor("#F1F8E9")),
("BACKGROUND",(2,1), (2,-1), HexColor("#FFF9C4")),
("BACKGROUND",(3,1), (3,-1), HexColor("#FFEBEE")),
# Last row (management) - bold
("FONTNAME",(0,-1),(-1,-1),"Helvetica-Bold"),
("BACKGROUND",(0,-1),(-1,-1), HexColor("#E8EAF6")),
]
dt.setStyle(TableStyle(dstyle))
story.append(dt)
story.append(Spacer(1, 3*mm))
# ═══════════════════════════════════════════════════════════════
# SECTION 9: RED FLAG SIGNS
# ═══════════════════════════════════════════════════════════════
story.append(section_header("9. RED FLAG SIGNS (Require Immediate Action)", color=ORANGE))
story.append(Spacer(1, 1*mm))
rf_rows = [
[Paragraph("Bilious (green) vomiting", RED),
Paragraph("Inspect vomitus / ask specifically. Green colour confirms bile.", BODY),
Paragraph("Bowel obstruction DISTAL to ampulla of Vater. In neonate = malrotation + volvulus (life-threatening). Surgical emergency until proven otherwise.", BODY)],
[Paragraph("Board-like rigidity", RED),
Paragraph("Involuntary muscle contraction persists despite distraction.", BODY),
Paragraph("Peritonitis = perforation until proven otherwise. Immediate surgical referral + resuscitation.", BODY)],
[Paragraph("Absent bowel sounds (>2 min)", RED),
Paragraph("Auscultate all four quadrants for minimum 2 minutes.", BODY),
Paragraph("Paralytic ileus (peritonitis, post-perforation, severe electrolyte disturbance). Or mechanical obstruction (late).", BODY)],
[Paragraph("Loss of liver dullness", RED),
Paragraph("Percussion over liver area - resonant instead of dull.", BODY),
Paragraph("Pneumoperitoneum = air under diaphragm = perforated viscus. Confirm with erect CXR (free air under diaphragm).", BODY)],
[Paragraph("Positive heel-drop / rebound", RED),
Paragraph("Pain on heel strike or release of pressure.", BODY),
Paragraph("Peritoneal irritation = appendicitis, perforated viscus, ruptured ectopic (adolescent girls).", BODY)],
[Paragraph("Palpable RIF mass + colicky pain + currant-jelly stool", RED),
Paragraph("Sausage-shaped mass in RUQ/transverse + empty RIF (Dance's sign).", BODY),
Paragraph("Intussusception. Peak age 3 months - 3 years. Urgent USS + pneumatic or hydrostatic reduction.", BODY)],
[Paragraph("Absent cremasteric reflex in boys", RED),
Paragraph("Stroke medial thigh - no testicular elevation.", BODY),
Paragraph("Testicular torsion until proven otherwise. 6-hour window for salvage. Urgent surgical exploration - do NOT await USS.", BODY)],
[Paragraph("Severe dehydration signs", RED),
Paragraph("Sunken eyes, no tears, skin tenting, CRT >3 sec, altered consciousness.", BODY),
Paragraph("Hypovolaemic shock. IV bolus 20 mL/kg 0.9% NaCl immediately. Reassess and repeat. ICU escalation if no response.", BODY)],
[Paragraph("Haematemesis / large PR bleed", RED),
Paragraph("Fresh blood or coffee grounds in vomit / significant rectal blood.", BODY),
Paragraph("Active GI haemorrhage. IV access, crossmatch, surgical + GI endoscopy team. Proton pump inhibitor if upper GI source likely.", BODY)],
]
story.append(red_flag_table(rf_rows))
story.append(Spacer(1, 3*mm))
# ═══════════════════════════════════════════════════════════════
# SECTION 10: EXAMINATION SUMMARY CHECKLIST
# ═══════════════════════════════════════════════════════════════
story.append(section_header("10. EXAMINATION SUMMARY CHECKLIST (Quick-Reference Tick List)"))
story.append(Spacer(1, 1*mm))
check_items = [
("GENERAL", [
"Child appearance (comfortable / distressed / peritonitic posture)",
"Nutritional status (weight on centile chart)",
"Skin colour (jaundice / pallor / perfusion)",
"Vital signs all documented with age-referenced ranges",
]),
("PERIPHERAL", [
"Hands: clubbing, koilonychia, leuconychia, palmar erythema",
"Eyes: scleral icterus, conjunctival pallor, sunken eyes",
"Mouth: mucous membrane moisture, ulcers, glossitis, dental erosions",
"Neck: lymphadenopathy",
]),
("ABDOMEN - INSPECTION", [
"Shape (distension / scaphoid / asymmetry)",
"Visible peristalsis (especially infants)",
"Umbilicus (central / everted / displaced)",
"Skin (scars / caput medusae / striae)",
"Movement with respiration (free / restricted)",
]),
("ABDOMEN - AUSCULTATION", [
"Bowel sounds (normal / absent / high-pitched / tinkling)",
"Succussion splash if gastric outlet obstruction suspected",
]),
("ABDOMEN - PALPATION", [
"Systematic 9-quadrant or 4-quadrant light palpation (start away from pain)",
"Deep palpation: tenderness site and character",
"Liver: size, edge character, tenderness",
"Spleen: size, direction of enlargement",
"Kidneys: ballottement bilaterally",
"Faecal mass / abnormal mass",
]),
("ABDOMEN - PERCUSSION", [
"Liver dullness and span",
"Splenic dullness (Traube's space)",
"Shifting dullness (ascites)",
"Renal angle tenderness",
]),
("SPECIAL SIGNS", [
"Rebound / percussion tenderness (if peritonism suspected)",
"Rovsing's sign (appendicitis)",
"Heel-drop test (peritoneal irritation)",
"Carnett's sign (if abdominal wall pain considered)",
]),
("HERNIAS / GENITALIA", [
"Inguinal orifices (bilateral)",
"Testes palpated bilaterally (males) - cremasteric reflex",
"Perianal inspection (fissure, skin tags, warts)",
]),
("EXTRA-INTESTINAL", [
"Skin rashes (erythema nodosum, pyoderma gangrenosum, dermatitis herpetiformis)",
"Joints examined if arthralgia reported",
"Growth: height and weight plotted on centile chart",
]),
]
for section_name, items in check_items:
check_data = []
check_data.append([
Paragraph(f"<b>{section_name}</b>",
S("cl", fontSize=8, textColor=TEAL, fontName="Helvetica-Bold")),
Paragraph("", BODY)
])
for item in items:
check_data.append([
Paragraph("☐ " + item, BODY),
Paragraph("", BODY)
])
ct = Table(check_data, colWidths=[PAGE_W - 36*mm - 3*cm, 3*cm])
cstyle = [
("FONTNAME",(0,0),(-1,-1),"Helvetica"),
("FONTSIZE",(0,0),(-1,-1),8.5),
("VALIGN",(0,0),(-1,-1),"TOP"),
("TOPPADDING",(0,0),(-1,-1),3),
("BOTTOMPADDING",(0,0),(-1,-1),3),
("LEFTPADDING",(0,0),(-1,-1),5),
("BACKGROUND",(0,0),(-1,0), TEAL_LIGHT),
("GRID",(0,0),(-1,-1),0.2,HexColor("#CFD8DC")),
]
for i in range(1, len(check_data)):
bg = GREY_LIGHT if i%2==1 else WHITE
cstyle.append(("BACKGROUND",(0,i),(-1,i),bg))
ct.setStyle(TableStyle(cstyle))
story.append(ct)
story.append(Spacer(1, 1.5*mm))
story.append(Spacer(1, 3*mm))
# ── FOOTER NOTE ──
story.append(HRFlowable(width="100%", thickness=0.5, color=TEAL))
story.append(Spacer(1, 2*mm))
fn = Table([[
Paragraph("Sources: Tintinalli's Emergency Medicine (Vomiting, Diarrhea & Dehydration in Children); "
"Bailey & Love's Short Practice of Surgery 28e (Clinical Signs in Appendicitis); "
"Textbook of Family Medicine 9e (Common Paediatric GI Disorders). "
"For educational/study use. Always apply clinical judgement.",
S("fn", fontSize=7, leading=10, textColor=GREY_DARK, fontName="Helvetica-Oblique"))
]], colWidths=[PAGE_W - 36*mm])
fn.setStyle(TableStyle([
("LEFTPADDING",(0,0),(-1,-1),0),
("TOPPADDING",(0,0),(-1,-1),0),
]))
story.append(fn)
# ── BUILD ────────────────────────────────────────────────────────
doc = SimpleDocTemplate(
OUTPUT,
pagesize=A4,
leftMargin=18*mm,
rightMargin=18*mm,
topMargin=28*mm,
bottomMargin=14*mm,
title="Pediatric GI Examination Schema",
author="Orris Medical Notes",
subject="Paediatric GI Physical Examination",
)
doc.build(story, onFirstPage=add_header_footer, onLaterPages=add_header_footer)
print(f"PDF generated: {OUTPUT}")