Periodic Assessment Examination on Abdominal Examination Examination Stations: Inspection-4 marks Palpation4 marks Percussion -4 marks. Auscultation-3 marks. Total Marks: 15. With images make pdf very easy to understand
abdominal examination inspection palpation percussion auscultation clinical images diagram
mkdir -p /home/daytona/workspace/abdominal-exam-pae && ls /home/daytona/workspace/
~/abdominal-exam-pae/generate_pdf.py
#!/usr/bin/env python3
"""
Periodic Assessment Examination - Abdominal Examination
Generates a comprehensive, visually rich PDF study guide.
"""
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, white, black, Color
)
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, KeepTogether, PageBreak
)
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
from reportlab.platypus import Flowable
from reportlab.lib import colors
import io
# ── Color Palette ──────────────────────────────────────────────────────────────
NAVY = HexColor("#1A3A5C")
TEAL = HexColor("#2E7D8C")
AMBER = HexColor("#E8A020")
GREEN = HexColor("#2E7D4F")
RED = HexColor("#C0392B")
LIGHT_BG = HexColor("#F0F6FA")
LIGHT_GRN = HexColor("#EAF5EE")
LIGHT_AMB = HexColor("#FDF5E0")
LIGHT_RED = HexColor("#FDEDEB")
PURPLE = HexColor("#6C3483")
LIGHT_PUR = HexColor("#F5EEF8")
MID_GREY = HexColor("#BDC3C7")
DARK_GREY = HexColor("#5D6D7E")
PALE_TEAL = HexColor("#E8F4F8")
# ── Custom Flowable: Colored Box with Icon ──────────────────────────────────────
class StationHeader(Flowable):
def __init__(self, title, marks, color, bg_color, icon_text):
super().__init__()
self.title = title
self.marks = marks
self.color = color
self.bg_color = bg_color
self.icon_text = icon_text
self.width = 17*cm
self.height = 2.2*cm
def draw(self):
c = self.canv
# Background rounded rect
c.setFillColor(self.bg_color)
c.roundRect(0, 0, self.width, self.height, 8, fill=1, stroke=0)
# Left accent bar
c.setFillColor(self.color)
c.roundRect(0, 0, 0.6*cm, self.height, 4, fill=1, stroke=0)
# Icon circle
c.setFillColor(self.color)
c.circle(1.5*cm, self.height/2, 0.55*cm, fill=1, stroke=0)
# Icon text
c.setFillColor(white)
c.setFont("Helvetica-Bold", 16)
c.drawCentredString(1.5*cm, self.height/2 - 0.18*cm, self.icon_text)
# Title
c.setFillColor(self.color)
c.setFont("Helvetica-Bold", 15)
c.drawString(2.5*cm, self.height/2 + 0.1*cm, self.title)
# Marks badge
badge_x = self.width - 3*cm
c.setFillColor(self.color)
c.roundRect(badge_x, 0.3*cm, 2.8*cm, 1.5*cm, 6, fill=1, stroke=0)
c.setFillColor(white)
c.setFont("Helvetica-Bold", 9)
c.drawCentredString(badge_x + 1.4*cm, 1.25*cm, "MARKS")
c.setFont("Helvetica-Bold", 18)
c.drawCentredString(badge_x + 1.4*cm, 0.55*cm, str(self.marks))
class DiagramFlowable(Flowable):
"""Draw abdomen quadrant diagram."""
def __init__(self, width=8*cm, height=8*cm):
super().__init__()
self.width = width
self.height = height
def draw(self):
c = self.canv
w, h = self.width, self.height
cx, cy = w/2, h/2
r = min(w, h)*0.42
# Oval abdomen outline
c.setFillColor(HexColor("#FFF8F0"))
c.setStrokeColor(NAVY)
c.setLineWidth(2)
c.ellipse(cx - r, cy - r*1.1, cx + r, cy + r*1.1, fill=1, stroke=1)
# Quadrant lines
c.setStrokeColor(TEAL)
c.setLineWidth(1.5)
c.setDash(4, 3)
c.line(cx - r, cy, cx + r, cy) # horizontal
c.line(cx, cy - r*1.1, cx, cy + r*1.1) # vertical
c.setDash()
# Quadrant labels
c.setFont("Helvetica-Bold", 7.5)
c.setFillColor(NAVY)
c.drawCentredString(cx - r*0.5, cy + r*0.55, "RUQ")
c.drawCentredString(cx + r*0.5, cy + r*0.55, "LUQ")
c.drawCentredString(cx - r*0.5, cy - r*0.65, "RLQ")
c.drawCentredString(cx + r*0.5, cy - r*0.65, "LLQ")
# Organ labels
c.setFont("Helvetica", 6)
c.setFillColor(GREEN)
c.drawCentredString(cx - r*0.5, cy + r*0.3, "Liver")
c.drawCentredString(cx - r*0.5, cy + r*0.1, "Gallbladder")
c.drawCentredString(cx + r*0.5, cy + r*0.3, "Stomach")
c.drawCentredString(cx + r*0.5, cy + r*0.1, "Spleen")
c.drawCentredString(cx - r*0.5, cy - r*0.3, "Appendix")
c.drawCentredString(cx - r*0.5, cy - r*0.5, "Cecum")
c.drawCentredString(cx + r*0.5, cy - r*0.3, "Sigmoid")
c.drawCentredString(cx + r*0.5, cy - r*0.5, "Colon")
# Umbilicus dot
c.setFillColor(AMBER)
c.circle(cx, cy, 0.18*cm, fill=1, stroke=0)
c.setFont("Helvetica", 5.5)
c.setFillColor(DARK_GREY)
c.drawCentredString(cx + 0.6*cm, cy, "Umbilicus")
# Title
c.setFont("Helvetica-Bold", 8)
c.setFillColor(NAVY)
c.drawCentredString(cx, 0.2*cm, "4 Quadrants of the Abdomen")
class PercussionDiagram(Flowable):
"""Shows percussion zones."""
def __init__(self, width=7.5*cm, height=7.5*cm):
super().__init__()
self.width = width
self.height = height
def draw(self):
c = self.canv
w, h = self.width, self.height
cx, cy = w/2, h/2
r = min(w, h)*0.42
# Oval outline
c.setFillColor(HexColor("#FFF8F0"))
c.setStrokeColor(NAVY)
c.setLineWidth(2)
c.ellipse(cx - r, cy - r*1.1, cx + r, cy + r*1.1, fill=1, stroke=1)
# Liver dullness zone (RUQ)
c.setFillColor(HexColor("#FADBD8"))
c.setStrokeColor(RED)
c.setLineWidth(0.5)
c.wedge(cx - r*0.1, cy - r*0.1, cx + r*0.9, cy + r*1.0,
90, 90, fill=1, stroke=1)
# Spleen dullness zone (LUQ)
c.setFillColor(HexColor("#D5F5E3"))
c.setStrokeColor(GREEN)
c.wedge(cx - r*0.9, cy - r*0.1, cx + r*0.1, cy + r*1.0,
0, 90, fill=1, stroke=1)
# Tympanic zones
c.setFillColor(HexColor("#D6EAF8"))
c.setStrokeColor(TEAL)
c.roundRect(cx - r*0.4, cy - r*0.3, r*0.8, r*0.5, 4, fill=1, stroke=1)
# Labels
c.setFont("Helvetica-Bold", 7)
c.setFillColor(RED)
c.drawCentredString(cx + r*0.55, cy + r*0.6, "Dull")
c.drawCentredString(cx + r*0.55, cy + r*0.42, "(Liver)")
c.setFillColor(GREEN)
c.drawCentredString(cx - r*0.55, cy + r*0.6, "Dull")
c.drawCentredString(cx - r*0.55, cy + r*0.42, "(Spleen)")
c.setFillColor(TEAL)
c.drawCentredString(cx, cy - r*0.05, "Tympanic")
c.drawCentredString(cx, cy - r*0.22, "(Gas/Bowel)")
# Flanks dull (ascites)
c.setFont("Helvetica", 6)
c.setFillColor(PURPLE)
c.drawCentredString(cx - r*0.85, cy - r*0.6, "Dull flanks")
c.drawCentredString(cx - r*0.85, cy - r*0.78, "= Ascites?")
c.setFont("Helvetica-Bold", 8)
c.setFillColor(NAVY)
c.drawCentredString(cx, 0.2*cm, "Percussion Zones")
# ── Styles ──────────────────────────────────────────────────────────────────────
def make_styles():
base = getSampleStyleSheet()
styles = {
"normal": ParagraphStyle(
"Normal2",
parent=base["Normal"],
fontSize=10, leading=14,
textColor=HexColor("#2C3E50"),
spaceAfter=4
),
"small": ParagraphStyle(
"Small",
parent=base["Normal"],
fontSize=8.5, leading=12,
textColor=HexColor("#2C3E50"),
),
"h1": ParagraphStyle(
"H1",
parent=base["Heading1"],
fontSize=22, leading=28,
textColor=NAVY,
fontName="Helvetica-Bold",
spaceAfter=6, spaceBefore=8
),
"h2": ParagraphStyle(
"H2",
parent=base["Heading2"],
fontSize=14, leading=18,
textColor=TEAL,
fontName="Helvetica-Bold",
spaceAfter=4, spaceBefore=10
),
"h3": ParagraphStyle(
"H3",
parent=base["Heading3"],
fontSize=11, leading=15,
textColor=NAVY,
fontName="Helvetica-Bold",
spaceAfter=3, spaceBefore=6
),
"center": ParagraphStyle(
"Center",
parent=base["Normal"],
fontSize=10, leading=14,
alignment=TA_CENTER,
textColor=NAVY,
),
"bullet": ParagraphStyle(
"Bullet",
parent=base["Normal"],
fontSize=9.5, leading=13,
leftIndent=14,
textColor=HexColor("#2C3E50"),
spaceAfter=2,
bulletIndent=4,
),
"important": ParagraphStyle(
"Important",
parent=base["Normal"],
fontSize=9.5, leading=14,
textColor=RED,
fontName="Helvetica-Bold",
spaceAfter=3,
),
"caption": ParagraphStyle(
"Caption",
parent=base["Normal"],
fontSize=8, leading=11,
textColor=DARK_GREY,
alignment=TA_CENTER,
fontName="Helvetica-Oblique",
),
}
return styles
def bullet(text, st):
return Paragraph(f"<bullet>•</bullet> {text}", st["bullet"])
def check_bullet(text, st, mark_val="1"):
return Paragraph(f"<bullet>☐</bullet> {text} <font color='#27AE60' size='8'>[ /{mark_val}]</font>", st["bullet"])
def info_box(content_list, bg_color, border_color, st, title=None):
"""Create a styled info box."""
rows = []
if title:
rows.append([Paragraph(f"<font color='white'><b>{title}</b></font>",
ParagraphStyle("bt", parent=st["normal"],
fontSize=10, textColor=white,
fontName="Helvetica-Bold"))])
for item in content_list:
rows.append([item])
ts = [
("BACKGROUND", (0, 0), (-1, -1), bg_color),
("LEFTPADDING", (0, 0), (-1, -1), 8),
("RIGHTPADDING", (0, 0), (-1, -1), 8),
("TOPPADDING", (0, 0), (-1, -1), 5),
("BOTTOMPADDING", (0, 0), (-1, -1), 5),
("BOX", (0, 0), (-1, -1), 1.5, border_color),
("ROUNDEDCORNERS", [6, 6, 6, 6]),
]
if title:
ts.insert(0, ("BACKGROUND", (0, 0), (-1, 0), border_color))
return Table(rows, colWidths=[17*cm], style=TableStyle(ts))
def two_col_table(left_items, right_items, st, left_bg=PALE_TEAL, right_bg=LIGHT_AMB):
"""Create a 2-column layout table."""
rows = []
max_len = max(len(left_items), len(right_items))
for i in range(max_len):
l = left_items[i] if i < len(left_items) else Spacer(1, 0.1*cm)
r = right_items[i] if i < len(right_items) else Spacer(1, 0.1*cm)
rows.append([l, r])
ts = [
("BACKGROUND", (0, 0), (0, -1), left_bg),
("BACKGROUND", (1, 0), (1, -1), right_bg),
("VALIGN", (0, 0), (-1, -1), "TOP"),
("LEFTPADDING", (0, 0), (-1, -1), 8),
("RIGHTPADDING", (0, 0), (-1, -1), 8),
("TOPPADDING", (0, 0), (-1, -1), 4),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
("BOX", (0, 0), (-1, -1), 1, MID_GREY),
("INNERGRID", (0, 0), (-1, -1), 0.5, MID_GREY),
]
return Table(rows, colWidths=[8.3*cm, 8.3*cm], style=TableStyle(ts))
def mark_table(items, st, color=TEAL):
"""Checklist table for marks."""
rows = []
total = 0
for label, mark, note in items:
total += mark
rows.append([
Paragraph(f"<b>{label}</b>", ParagraphStyle("mt1", parent=st["normal"],
fontSize=9, textColor=NAVY)),
Paragraph(note or "", ParagraphStyle("mt2", parent=st["small"],
fontSize=8, textColor=DARK_GREY)),
Paragraph(f"<b>{mark}</b>", ParagraphStyle("mt3", parent=st["normal"],
fontSize=10, textColor=color,
alignment=TA_CENTER)),
])
# Total row
rows.append([
Paragraph("<b>TOTAL</b>", ParagraphStyle("mtt", parent=st["normal"],
fontSize=10, textColor=white,
fontName="Helvetica-Bold")),
Paragraph("", st["small"]),
Paragraph(f"<b>{total}</b>", ParagraphStyle("mttv", parent=st["normal"],
fontSize=12, textColor=white,
fontName="Helvetica-Bold",
alignment=TA_CENTER)),
])
ts = [
("BACKGROUND", (0, 0), (-1, -2), HexColor("#F8FBFD")),
("BACKGROUND", (0, -1), (-1, -1), color),
("GRID", (0, 0), (-1, -1), 0.5, MID_GREY),
("BOX", (0, 0), (-1, -1), 1.5, color),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("LEFTPADDING", (0, 0), (-1, -1), 8),
("RIGHTPADDING", (0, 0), (-1, -1), 8),
("TOPPADDING", (0, 0), (-1, -1), 5),
("BOTTOMPADDING", (0, 0), (-1, -1), 5),
# Stripe alternate rows
("ROWBACKGROUNDS", (0, 0), (-1, -2), [HexColor("#FFFFFF"), HexColor("#F0F6FA")]),
]
return Table(rows, colWidths=[7*cm, 7*cm, 2.5*cm], style=TableStyle(ts))
def build_pdf():
output_path = "/home/daytona/workspace/abdominal-exam-pae/Abdominal_Examination_PAE.pdf"
doc = SimpleDocTemplate(
output_path,
pagesize=A4,
leftMargin=2*cm, rightMargin=2*cm,
topMargin=1.8*cm, bottomMargin=1.8*cm,
title="PAE - Abdominal Examination",
author="Medical Education"
)
st = make_styles()
story = []
# ── COVER / TITLE ──────────────────────────────────────────────────────────
# Header bar
story.append(Spacer(1, 0.3*cm))
cover_data = [[
Paragraph("<font color='white' size='11'><b>PERIODIC ASSESSMENT EXAMINATION</b></font>",
ParagraphStyle("covhdr", parent=st["center"], textColor=white, fontSize=11)),
]]
story.append(Table(cover_data, colWidths=[17*cm],
style=TableStyle([
("BACKGROUND", (0, 0), (-1, -1), NAVY),
("LEFTPADDING", (0, 0), (-1, -1), 12),
("RIGHTPADDING", (0, 0), (-1, -1), 12),
("TOPPADDING", (0, 0), (-1, -1), 10),
("BOTTOMPADDING", (0, 0), (-1, -1), 10),
("BOX", (0, 0), (-1, -1), 2, AMBER),
])))
story.append(Spacer(1, 0.4*cm))
story.append(Paragraph("<b>ABDOMINAL EXAMINATION</b>", ParagraphStyle(
"title", parent=st["h1"], fontSize=26, alignment=TA_CENTER,
textColor=NAVY, spaceAfter=2)))
story.append(Paragraph("Clinical Skills Assessment Guide",
ParagraphStyle("sub", parent=st["center"], fontSize=12,
textColor=TEAL)))
story.append(Spacer(1, 0.4*cm))
# Summary score box
score_data = [
[
Paragraph("<b>Station</b>", ParagraphStyle("sh", parent=st["center"],
textColor=white, fontSize=10,
fontName="Helvetica-Bold")),
Paragraph("<b>Component</b>", ParagraphStyle("sh2", parent=st["center"],
textColor=white, fontSize=10,
fontName="Helvetica-Bold")),
Paragraph("<b>Marks</b>", ParagraphStyle("sh3", parent=st["center"],
textColor=white, fontSize=10,
fontName="Helvetica-Bold")),
],
[Paragraph("1", st["center"]), Paragraph("Inspection", st["center"]), Paragraph("4", st["center"])],
[Paragraph("2", st["center"]), Paragraph("Palpation", st["center"]), Paragraph("4", st["center"])],
[Paragraph("3", st["center"]), Paragraph("Percussion", st["center"]), Paragraph("4", st["center"])],
[Paragraph("4", st["center"]), Paragraph("Auscultation", st["center"]), Paragraph("3", st["center"])],
[
Paragraph("<b>TOTAL</b>", ParagraphStyle("tot", parent=st["center"],
textColor=white, fontSize=12,
fontName="Helvetica-Bold")),
Paragraph("", st["center"]),
Paragraph("<b>15</b>", ParagraphStyle("tot2", parent=st["center"],
textColor=white, fontSize=14,
fontName="Helvetica-Bold")),
],
]
score_ts = [
("BACKGROUND", (0, 0), (-1, 0), NAVY),
("BACKGROUND", (0, 1), (-1, 1), PALE_TEAL),
("BACKGROUND", (0, 2), (-1, 2), HexColor("#FFFFFF")),
("BACKGROUND", (0, 3), (-1, 3), PALE_TEAL),
("BACKGROUND", (0, 4), (-1, 4), HexColor("#FFFFFF")),
("BACKGROUND", (0, 5), (-1, 5), TEAL),
("GRID", (0, 0), (-1, -1), 0.8, MID_GREY),
("BOX", (0, 0), (-1, -1), 2, NAVY),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("TOPPADDING", (0, 0), (-1, -1), 7),
("BOTTOMPADDING", (0, 0), (-1, -1), 7),
("ALIGN", (0, 0), (-1, -1), "CENTER"),
]
story.append(Table(score_data, colWidths=[4*cm, 9.5*cm, 3.5*cm],
style=TableStyle(score_ts)))
story.append(Spacer(1, 0.5*cm))
# Exam instructions
inst = info_box([
bullet("<b>Patient position:</b> Supine, arms by the side, one pillow under head, abdomen exposed from nipples to inguinal ligaments.", st),
bullet("<b>Examiner position:</b> Stand to the right of the patient (right-handed examiner).", st),
bullet("<b>Sequence:</b> Inspection → Auscultation (before palpation) → Percussion → Palpation (in abdominal exam, auscultation comes before palpation)", st),
bullet("<b>Always wash hands / don gloves</b> before starting. Ensure good lighting.", st),
], LIGHT_BG, TEAL, st, title=" GENERAL INSTRUCTIONS")
story.append(inst)
story.append(Spacer(1, 0.5*cm))
# Anatomy diagram + 9 regions
story.append(HRFlowable(width="100%", thickness=1.5, color=TEAL, spaceAfter=6))
story.append(Paragraph("Anatomical Orientation", st["h2"]))
regions_data = [
[
Paragraph("<b>9 Regions</b>", ParagraphStyle("rh", parent=st["center"],
textColor=white, fontSize=9,
fontName="Helvetica-Bold")),
Paragraph("<b>Key Organs</b>", ParagraphStyle("rh2", parent=st["center"],
textColor=white, fontSize=9,
fontName="Helvetica-Bold")),
],
[Paragraph("Right Hypochondrium", st["small"]),
Paragraph("Liver (R lobe), Gallbladder, Hepatic flexure", st["small"])],
[Paragraph("Epigastrium", st["small"]),
Paragraph("Stomach, Duodenum, Pancreas, Transverse colon", st["small"])],
[Paragraph("Left Hypochondrium", st["small"]),
Paragraph("Spleen, Stomach (fundus), Splenic flexure", st["small"])],
[Paragraph("Right Lumbar (Flank)", st["small"]),
Paragraph("Ascending colon, Right kidney, Ureter", st["small"])],
[Paragraph("Umbilical", st["small"]),
Paragraph("Small intestine, Transverse colon, Aorta", st["small"])],
[Paragraph("Left Lumbar (Flank)", st["small"]),
Paragraph("Descending colon, Left kidney, Ureter", st["small"])],
[Paragraph("Right Iliac Fossa (RIF)", st["small"]),
Paragraph("Cecum, Appendix, Ileum", st["small"])],
[Paragraph("Hypogastrium (Pubic)", st["small"]),
Paragraph("Bladder, Uterus, Sigmoid colon", st["small"])],
[Paragraph("Left Iliac Fossa (LIF)", st["small"]),
Paragraph("Sigmoid colon, Left ovary/tube", st["small"])],
]
r_ts = [
("BACKGROUND", (0, 0), (-1, 0), NAVY),
("ROWBACKGROUNDS", (0, 1), (-1, -1), [HexColor("#FFFFFF"), LIGHT_BG]),
("GRID", (0, 0), (-1, -1), 0.5, MID_GREY),
("BOX", (0, 0), (-1, -1), 1.5, NAVY),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("TOPPADDING", (0, 0), (-1, -1), 4),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
("LEFTPADDING", (0, 0), (-1, -1), 8),
]
diag = DiagramFlowable(8.5*cm, 8.5*cm)
anatomy_table = Table(
[[diag, Table(regions_data, colWidths=[4.5*cm, 4.5*cm],
style=TableStyle(r_ts))]],
colWidths=[8.8*cm, 9.5*cm],
style=TableStyle([
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("LEFTPADDING", (0, 0), (-1, -1), 4),
("RIGHTPADDING", (0, 0), (-1, -1), 4),
])
)
story.append(anatomy_table)
story.append(Spacer(1, 0.4*cm))
# ══════════════════════════════════════════════════════════════════════════
# STATION 1 — INSPECTION (4 marks)
# ══════════════════════════════════════════════════════════════════════════
story.append(PageBreak())
story.append(StationHeader("STATION 1: INSPECTION", 4, NAVY, PALE_TEAL, "👁"))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph(
"Inspection is the <b>first and most important step</b>. The examiner looks at the abdomen "
"systematically from the <i>foot of the bed</i>, then from the <i>sides</i>, and finally "
"<i>tangentially</i> at skin level. The entire abdomen from <b>nipples to inguinal ligaments</b> "
"must be exposed in good lighting.", st["normal"]))
story.append(Spacer(1, 0.3*cm))
# What to look for - 2 column layout
story.append(Paragraph("What to Look For (Systematic Approach)", st["h3"]))
left_col = [
Paragraph("<b>SKIN & SURFACE</b>", ParagraphStyle("lh", parent=st["small"],
textColor=NAVY, fontName="Helvetica-Bold")),
bullet("Scars — linear (1° intention) or broad/irregular (wound infection); note site", st),
bullet("Dilated veins: periumbilical = portal hypertension (caput medusae); lateral = IVC obstruction", st),
bullet("Striae (stretch marks) — silvery = old; purple-red = Cushing's syndrome", st),
bullet("Erythema / discoloration — hot-water bottle marks indicate site of pain", st),
bullet("Cullen sign: periumbilical bruising — retroperitoneal hemorrhage (pancreatitis, ruptured AAA)", st),
bullet("Grey-Turner sign: flank bruising — same causes", st),
bullet("Herpes zoster rash — dermatomal distribution", st),
Spacer(1, 0.2*cm),
Paragraph("<b>UMBILICUS</b>", ParagraphStyle("lh2", parent=st["small"],
textColor=NAVY, fontName="Helvetica-Bold")),
bullet("Normally at mid-point of xiphoid-to-pubis line", st),
bullet("Everted: ascites, pregnancy, large mass", st),
bullet("Deeply inverted: obesity", st),
bullet("Displaced upwards: pelvic mass; downward: ascites (Tanyol's sign)", st),
bullet("Displaced laterally: swelling on opposite side", st),
bullet("Sister Mary Joseph nodule: periumbilical hard nodule = intra-abdominal malignancy", st),
]
right_col = [
Paragraph("<b>CONTOUR & SHAPE</b>", ParagraphStyle("rh3", parent=st["small"],
textColor=NAVY, fontName="Helvetica-Bold")),
bullet("Flat / scaphoid: normal in thin individuals, malnutrition", st),
bullet("Generalized distension: <b>5 F's</b> — Fat, Fluid, Flatus, Faeces, Foetus (Fetus)", st),
bullet("Upper abdominal fullness: hepatomegaly, splenomegaly, gastric distension", st),
bullet("Lower abdominal fullness: pelvic mass, bladder distension, pregnancy", st),
bullet("Right iliac fossa fullness: caecal mass, appendiceal abscess", st),
Spacer(1, 0.2*cm),
Paragraph("<b>MOVEMENT</b>", ParagraphStyle("rh4", parent=st["small"],
textColor=NAVY, fontName="Helvetica-Bold")),
bullet("Respiratory movement: reduced / absent in peritonitis", st),
bullet("Visible peristalsis: pyloric obstruction (left-to-right wave) or small bowel obstruction", st),
bullet("Visible pulsations: aortic aneurysm (transmitted vs. expansile)", st),
Spacer(1, 0.2*cm),
Paragraph("<b>HERNIAS & SWELLINGS</b>", ParagraphStyle("rh5", parent=st["small"],
textColor=NAVY, fontName="Helvetica-Bold")),
bullet("Ask patient to cough / raise head: reveals hernias and diastasis recti", st),
bullet("Note: size, site, shape, surface, consistency of any visible swelling", st),
]
story.append(two_col_table(left_col, right_col, st, PALE_TEAL, LIGHT_AMB))
story.append(Spacer(1, 0.3*cm))
# Special signs box
story.append(info_box([
Paragraph(
"<b>Cullen Sign</b>: Periumbilical ecchymosis (bruising) "
"— Acute hemorrhagic pancreatitis, ruptured ectopic pregnancy, ruptured AAA",
st["small"]),
Paragraph(
"<b>Grey-Turner Sign</b>: Flank/loin ecchymosis "
"— Same causes. Indicates retroperitoneal hemorrhage. Takes 24-48 hours to appear.",
st["small"]),
Paragraph(
"<b>Caput Medusae</b>: Dilated periumbilical veins radiating outward "
"— Portal hypertension. Blood flows away from umbilicus.",
st["small"]),
Paragraph(
"<b>5 F's of Abdominal Distension</b>: Fat | Fluid (ascites) | Flatus | Faeces | Foetus",
ParagraphStyle("5f", parent=st["small"], textColor=NAVY, fontName="Helvetica-Bold")),
], LIGHT_AMB, AMBER, st, title=" KEY CLINICAL SIGNS — INSPECTION"))
story.append(Spacer(1, 0.3*cm))
# Mark scheme
story.append(Paragraph("Inspection Mark Scheme (4 Marks)", st["h3"]))
story.append(mark_table([
("Patient Positioning & Exposure", 1,
"Supine, arms by sides, abdomen exposed nipples to groin, good lighting, patient comfort"),
("Skin & Surface Features", 1,
"Correctly identifies scars, veins, striae, ecchymosis, erythema, umbilicus changes"),
("Contour & Shape", 1,
"Describes flat/distended abdomen, identifies 5 F's, localizes fullness to correct region"),
("Movement & Special Signs", 1,
"Notes respiratory movement, visible peristalsis, pulsations, hernias (ask to cough)"),
], st, color=NAVY))
story.append(Spacer(1, 0.4*cm))
# ══════════════════════════════════════════════════════════════════════════
# STATION 2 — PALPATION (4 marks)
# ══════════════════════════════════════════════════════════════════════════
story.append(PageBreak())
story.append(StationHeader("STATION 2: PALPATION", 4, GREEN, LIGHT_GRN, "✋"))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph(
"Palpation is performed with <b>warm hands</b>, asking the patient about tenderness first. "
"Always start <b>away from the painful area</b> (as reported or detected by percussion). "
"Use <b>light palpation first</b>, then deep palpation. Watch the patient's face throughout.", st["normal"]))
story.append(Spacer(1, 0.3*cm))
# Light vs Deep palpation
story.append(Paragraph("Types of Palpation", st["h3"]))
lp_right = [
Paragraph("<b>LIGHT PALPATION</b>", ParagraphStyle("lph", parent=st["small"],
textColor=GREEN, fontName="Helvetica-Bold")),
bullet("Depth: 1-2 cm", st),
bullet("Purpose: Detect tenderness, guarding, rigidity, superficial masses", st),
bullet("Method: Flat hand, gentle pressure, systematic — all 9 regions", st),
bullet("<b>Guarding</b>: Voluntary increase in muscle tone (patient cooperation)", st),
bullet("<b>Rigidity</b>: Involuntary board-like hardness = peritonitis", st),
Spacer(1, 0.15*cm),
Paragraph("<b>Rebound Tenderness (Blumberg's Sign)</b>", ParagraphStyle(
"rbs", parent=st["small"], textColor=RED, fontName="Helvetica-Bold")),
Paragraph("Press deeply, release suddenly — pain on release > pain on pressing "
"= peritoneal irritation. (Note: this test is now considered unnecessarily "
"painful; prefer gentle percussion instead)", st["small"]),
]
lp_left = [
Paragraph("<b>DEEP PALPATION</b>", ParagraphStyle("dph", parent=st["small"],
textColor=NAVY, fontName="Helvetica-Bold")),
bullet("Depth: 4-6 cm", st),
bullet("Purpose: Palpate organs — liver, spleen, kidneys, aorta; detect deep masses", st),
bullet("Method: Two-handed (one guiding, one pressing) or bimanual", st),
bullet("Masses: Note size, site, shape, surface, consistency, mobility, tenderness", st),
Spacer(1, 0.15*cm),
Paragraph("<b>Organ Palpation</b>", ParagraphStyle("oph", parent=st["small"],
textColor=NAVY, fontName="Helvetica-Bold")),
bullet("Liver: RCM, start from RIF, move upward during inspiration", st),
bullet("Spleen: Start from RIF, move toward LUQ during inspiration", st),
bullet("Kidneys: Bimanual ballottement in loin region", st),
bullet("Aorta: Midline — pulsatile + expansile = aneurysm", st),
]
story.append(two_col_table(lp_right, lp_left, st, LIGHT_GRN, PALE_TEAL))
story.append(Spacer(1, 0.3*cm))
# Liver palpation details
story.append(Paragraph("Organ-Specific Palpation Techniques", st["h3"]))
organ_data = [
[
Paragraph("<b>Organ</b>", ParagraphStyle("oh", parent=st["center"],
textColor=white, fontSize=9,
fontName="Helvetica-Bold")),
Paragraph("<b>Technique</b>", ParagraphStyle("oh2", parent=st["center"],
textColor=white, fontSize=9,
fontName="Helvetica-Bold")),
Paragraph("<b>Normal Finding</b>", ParagraphStyle("oh3", parent=st["center"],
textColor=white, fontSize=9,
fontName="Helvetica-Bold")),
Paragraph("<b>Abnormal</b>", ParagraphStyle("oh4", parent=st["center"],
textColor=white, fontSize=9,
fontName="Helvetica-Bold")),
],
[
Paragraph("<b>Liver</b>", ParagraphStyle("lib", parent=st["small"],
fontName="Helvetica-Bold", textColor=NAVY)),
Paragraph("Right hand along costal margin, fingers pointing toward axilla. "
"Ask patient to breathe in deeply. Move hand up from RIF with each breath.", st["small"]),
Paragraph("Not palpable (2 cm below costal margin in children)", st["small"]),
Paragraph("Hepatomegaly: tender (hepatitis, CCF), hard (cirrhosis, malignancy), "
"pulsatile (tricuspid regurgitation)", st["small"]),
],
[
Paragraph("<b>Spleen</b>", ParagraphStyle("spb", parent=st["small"],
fontName="Helvetica-Bold", textColor=NAVY)),
Paragraph("Start RIF, move toward LUQ. Left hand behind left lower ribs to "
"lift spleen forward. Right hand palpates below left costal margin on inspiration.", st["small"]),
Paragraph("Not normally palpable (must be 2-3x enlarged to be felt)", st["small"]),
Paragraph("Splenomegaly: note notch on medial border, can't get above it, "
"moves with respiration, dull to percussion", st["small"]),
],
[
Paragraph("<b>Kidney</b>", ParagraphStyle("kib", parent=st["small"],
fontName="Helvetica-Bold", textColor=NAVY)),
Paragraph("Bimanual: one hand in loin, one on abdomen. "
"Ballottement: the kidney can be bounced between hands.", st["small"]),
Paragraph("Right kidney may be felt at lower pole. "
"Left kidney rarely palpable normally.", st["small"]),
Paragraph("Renal mass: ballottable, resonant on percussion (bowel in front), "
"can get above it", st["small"]),
],
[
Paragraph("<b>Bladder</b>", ParagraphStyle("blab", parent=st["small"],
fontName="Helvetica-Bold", textColor=NAVY)),
Paragraph("Palpate suprapubic region — smooth midline mass rising from pelvis.", st["small"]),
Paragraph("Not palpable when empty", st["small"]),
Paragraph("Urinary retention: smooth, tender, dull to percussion, "
"can't get below it", st["small"]),
],
[
Paragraph("<b>Aorta</b>", ParagraphStyle("aob", parent=st["small"],
fontName="Helvetica-Bold", textColor=NAVY)),
Paragraph("Press deeply in midline above umbilicus, fingers spread apart.", st["small"]),
Paragraph("Pulsatile (transmitted) felt in thin patients", st["small"]),
Paragraph("Expansile pulsation (fingers pushed apart) = aortic aneurysm", st["small"]),
],
]
organ_ts = [
("BACKGROUND", (0, 0), (-1, 0), NAVY),
("ROWBACKGROUNDS", (0, 1), (-1, -1), [HexColor("#FFFFFF"), LIGHT_GRN]),
("GRID", (0, 0), (-1, -1), 0.5, MID_GREY),
("BOX", (0, 0), (-1, -1), 1.5, GREEN),
("VALIGN", (0, 0), (-1, -1), "TOP"),
("TOPPADDING", (0, 0), (-1, -1), 5),
("BOTTOMPADDING", (0, 0), (-1, -1), 5),
("LEFTPADDING", (0, 0), (-1, -1), 6),
("RIGHTPADDING", (0, 0), (-1, -1), 6),
]
story.append(Table(organ_data, colWidths=[2.5*cm, 5.5*cm, 4*cm, 4.5*cm],
style=TableStyle(organ_ts)))
story.append(Spacer(1, 0.3*cm))
# Special signs
story.append(info_box([
bullet("<b>McBurney's Point</b>: Tenderness 1/3 of the way from ASIS to umbilicus — appendicitis", st),
bullet("<b>Murphy's Sign</b>: Arrest of inspiration on deep palpation of RUQ — acute cholecystitis (inflamed gallbladder)", st),
bullet("<b>Rovsing's Sign</b>: LIF pressure causes RIF pain — appendicitis", st),
bullet("<b>Psoas Sign</b>: Pain on hip extension — retrocecal appendicitis or psoas abscess", st),
bullet("<b>Obturator Sign</b>: Pain on internal hip rotation — pelvic appendicitis or pelvic abscess", st),
], LIGHT_GRN, GREEN, st, title=" IMPORTANT SPECIAL SIGNS IN PALPATION"))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("Palpation Mark Scheme (4 Marks)", st["h3"]))
story.append(mark_table([
("Technique & Approach", 1,
"Warm hands, starts away from pain, watches face, light then deep, systematic"),
("Tenderness & Guarding", 1,
"Identifies site, distinguishes voluntary guarding vs. rigidity; tests rebound correctly"),
("Liver Palpation", 1,
"Starts from RIF, uses inspiration, correctly identifies hepatomegaly; notes character"),
("Spleen / Kidney / Bladder / Aorta", 1,
"Correct technique for at least 2 other organ palpations; bimanual for kidney"),
], st, color=GREEN))
# ══════════════════════════════════════════════════════════════════════════
# STATION 3 — PERCUSSION (4 marks)
# ══════════════════════════════════════════════════════════════════════════
story.append(PageBreak())
story.append(StationHeader("STATION 3: PERCUSSION", 4, TEAL, PALE_TEAL, "🖐"))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph(
"Percussion of the abdomen provides information about <b>organ size</b>, "
"<b>gas distribution</b>, <b>fluid (ascites)</b>, and <b>peritoneal irritation</b>. "
"Use the <b>middle finger of the left hand as pleximeter</b> and tap with the middle "
"finger of the right hand (plexor) — one or two sharp strokes.", st["normal"]))
story.append(Spacer(1, 0.3*cm))
# Percussion notes
story.append(Paragraph("Percussion Notes & Their Meaning", st["h3"]))
perc_notes = [
[
Paragraph("<b>Note</b>", ParagraphStyle("pnh", parent=st["center"], textColor=white,
fontSize=9, fontName="Helvetica-Bold")),
Paragraph("<b>Sound</b>", ParagraphStyle("pnh2", parent=st["center"], textColor=white,
fontSize=9, fontName="Helvetica-Bold")),
Paragraph("<b>Found Over</b>", ParagraphStyle("pnh3", parent=st["center"], textColor=white,
fontSize=9, fontName="Helvetica-Bold")),
Paragraph("<b>Clinical Significance</b>", ParagraphStyle("pnh4", parent=st["center"],
textColor=white, fontSize=9,
fontName="Helvetica-Bold")),
],
[
Paragraph("<b>Tympanic</b>", ParagraphStyle("ty", parent=st["small"],
fontName="Helvetica-Bold", textColor=TEAL)),
Paragraph("Drum-like, resonant", st["small"]),
Paragraph("Gas-filled bowel (normal), stomach", st["small"]),
Paragraph("Normal bowel; hyperresonant = excessive gas, ileus, obstruction", st["small"]),
],
[
Paragraph("<b>Dull</b>", ParagraphStyle("du", parent=st["small"],
fontName="Helvetica-Bold", textColor=RED)),
Paragraph("Flat, muffled", st["small"]),
Paragraph("Solid organs (liver, spleen, full bladder, tumor, ascites)", st["small"]),
Paragraph("Loss of liver dullness = gas (pneumoperitoneum, perforation)", st["small"]),
],
[
Paragraph("<b>Stony Dull</b>", ParagraphStyle("sd", parent=st["small"],
fontName="Helvetica-Bold", textColor=NAVY)),
Paragraph("Very flat, wooden", st["small"]),
Paragraph("Fluid-filled (ascites, large cyst)", st["small"]),
Paragraph("Massive ascites; differentiate from solid mass", st["small"]),
],
]
perc_ts = [
("BACKGROUND", (0, 0), (-1, 0), TEAL),
("ROWBACKGROUNDS", (0, 1), (-1, -1), [HexColor("#FFFFFF"), PALE_TEAL]),
("GRID", (0, 0), (-1, -1), 0.5, MID_GREY),
("BOX", (0, 0), (-1, -1), 1.5, TEAL),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("TOPPADDING", (0, 0), (-1, -1), 5),
("BOTTOMPADDING", (0, 0), (-1, -1), 5),
("LEFTPADDING", (0, 0), (-1, -1), 6),
]
story.append(Table(perc_notes, colWidths=[3.5*cm, 3.5*cm, 5*cm, 5.5*cm],
style=TableStyle(perc_ts)))
story.append(Spacer(1, 0.3*cm))
# Percussion diagram + Liver percussion
story.append(Paragraph("Percussion Techniques", st["h3"]))
tech_left = [
Paragraph("<b>LIVER SIZE (Midclavicular Line)</b>",
ParagraphStyle("lsl", parent=st["small"], fontName="Helvetica-Bold", textColor=NAVY)),
bullet("Upper border: Percuss downward from 4th intercostal space — resonant to dull = upper border (normally 5th ICS, MCL)", st),
bullet("Lower border: Percuss upward from RIF — tympanic to dull = lower border (normally costal margin)", st),
bullet("Normal liver span: <b>6-12 cm</b> in midclavicular line", st),
bullet("Hepatomegaly: span > 12 cm or liver palpable > 2 cm below costal margin", st),
Spacer(1, 0.15*cm),
Paragraph("<b>SPLEEN</b>",
ParagraphStyle("lss", parent=st["small"], fontName="Helvetica-Bold", textColor=NAVY)),
bullet("Traube's space (LUQ): Normally tympanic (gas in stomach). "
"Dullness in Traube's space = splenomegaly", st),
bullet("Nixon's method: Percuss from lower border of lung downward — dullness > 8 cm suggests splenomegaly", st),
]
tech_right = [
Paragraph("<b>ASCITES DETECTION</b>",
ParagraphStyle("lsa", parent=st["small"], fontName="Helvetica-Bold", textColor=PURPLE)),
bullet("<b>Shifting Dullness</b>: Flank dullness in supine — "
"ask patient to roll away — dullness shifts to the dependent side. "
"Confirms free fluid (>1.5 L needed). Most sensitive bedside test.", st),
bullet("<b>Fluid Thrill</b>: One hand on one flank, assistant places edge of hand on midline. "
"Flick the other flank — wave palpable on opposite side. "
"Only in massive ascites. Less reliable.", st),
Spacer(1, 0.15*cm),
Paragraph("<b>GENERAL PERCUSSION</b>",
ParagraphStyle("lsg", parent=st["small"], fontName="Helvetica-Bold", textColor=NAVY)),
bullet("Percuss all 4 quadrants / 9 regions systematically", st),
bullet("Painful on light percussion = peritoneal irritation (preferred to rebound test)", st),
bullet("Bladder: Percuss suprapubic region — dull if full", st),
bullet("Loss of liver dullness = Free gas (perforation emergency!)", st),
]
story.append(two_col_table(tech_left, tech_right, st, PALE_TEAL, LIGHT_PUR))
story.append(Spacer(1, 0.3*cm))
# Percussion diagram
perc_diag = PercussionDiagram(8*cm, 8*cm)
diag_table = Table(
[[perc_diag,
info_box([
Paragraph("<b>Shifting Dullness — Step by Step</b>",
ParagraphStyle("sds", parent=st["small"], fontName="Helvetica-Bold",
textColor=PURPLE)),
bullet("1. Patient supine, percuss from umbilicus to right flank", st),
bullet("2. Mark where resonance changes to dullness", st),
bullet("3. Keep finger at that spot", st),
bullet("4. Ask patient to roll onto LEFT side (towards you)", st),
bullet("5. Wait 30 seconds for fluid to shift", st),
bullet("6. Percuss again at same spot", st),
bullet("✓ Positive: dullness disappears (now resonant) = Shifting Dullness = Ascites", st),
Spacer(1, 0.1*cm),
Paragraph("<b>Fluid Thrill (Fluid Wave)</b>",
ParagraphStyle("fts", parent=st["small"], fontName="Helvetica-Bold",
textColor=TEAL)),
bullet("1. Patient supine", st),
bullet("2. Place one hand flat on one flank", st),
bullet("3. Assistant places ulnar edge of hand on midline abdomen", st),
bullet("4. Flick (tap) the opposite flank sharply", st),
bullet("✓ Positive: impulse felt on receiving hand = Fluid Thrill = Massive ascites", st),
], LIGHT_PUR, PURPLE, st)]],
colWidths=[8.5*cm, 8.5*cm],
style=TableStyle([
("VALIGN", (0, 0), (-1, -1), "TOP"),
("LEFTPADDING", (0, 0), (-1, -1), 2),
])
)
story.append(diag_table)
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("Percussion Mark Scheme (4 Marks)", st["h3"]))
story.append(mark_table([
("Technique", 1, "Correct pleximeter/plexor method; systematic percussion all regions"),
("Liver Percussion", 1, "Identifies upper and lower borders; estimates liver span correctly"),
("Spleen / Traube's Space", 1, "Percusses Traube's space correctly; detects dullness suggesting splenomegaly"),
("Ascites Detection", 1, "Correctly demonstrates shifting dullness or fluid thrill; explains significance"),
], st, color=TEAL))
# ══════════════════════════════════════════════════════════════════════════
# STATION 4 — AUSCULTATION (3 marks)
# ══════════════════════════════════════════════════════════════════════════
story.append(PageBreak())
story.append(StationHeader("STATION 4: AUSCULTATION", 3, AMBER, LIGHT_AMB, "🩺"))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph(
"Auscultation of the abdomen should be performed <b>before percussion and palpation</b> "
"(stimulation of the bowel can alter sounds). Use the <b>diaphragm</b> of the stethoscope "
"lightly applied. Listen in all four quadrants. <b>Normal bowel sounds</b> are intermittent "
"clicks and gurgles, 5-30 per minute.", st["normal"]))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("Auscultatory Findings", st["h3"]))
bowel_data = [
[
Paragraph("<b>Finding</b>", ParagraphStyle("bsh", parent=st["center"], textColor=white,
fontSize=9, fontName="Helvetica-Bold")),
Paragraph("<b>Description</b>", ParagraphStyle("bsh2", parent=st["center"], textColor=white,
fontSize=9, fontName="Helvetica-Bold")),
Paragraph("<b>Clinical Meaning</b>", ParagraphStyle("bsh3", parent=st["center"], textColor=white,
fontSize=9, fontName="Helvetica-Bold")),
],
[
Paragraph("<b>Normal Bowel Sounds</b>", ParagraphStyle("nb", parent=st["small"],
fontName="Helvetica-Bold", textColor=GREEN)),
Paragraph("Intermittent clicks, gurgles; 5-30/min", st["small"]),
Paragraph("Normal intestinal peristalsis", st["small"]),
],
[
Paragraph("<b>Absent / No Bowel Sounds</b>", ParagraphStyle("ab", parent=st["small"],
fontName="Helvetica-Bold", textColor=RED)),
Paragraph("Complete silence after listening for 2-3 full minutes", st["small"]),
Paragraph("<b>Paralytic ileus</b> (adynamic ileus) — postoperative, peritonitis, electrolyte imbalance, "
"mesenteric ischemia. Absence must be confirmed over 2-3 minutes.", st["small"]),
],
[
Paragraph("<b>Increased / Hyperactive</b>", ParagraphStyle("hb", parent=st["small"],
fontName="Helvetica-Bold", textColor=AMBER)),
Paragraph("High-pitched, rushing, tinkling sounds; 'borborygmi'", st["small"]),
Paragraph("<b>Early mechanical obstruction</b> (small bowel obstruction) — "
"high-pitched tinkling sounds in synchrony with colicky pain. Also: "
"diarrhea, early peritonitis, post-meal", st["small"]),
],
[
Paragraph("<b>Reduced / Hypoactive</b>", ParagraphStyle("rb", parent=st["small"],
fontName="Helvetica-Bold", textColor=DARK_GREY)),
Paragraph("Infrequent sounds, long pauses", st["small"]),
Paragraph("Late obstruction, peritonitis (late), post-surgery, hypothyroidism", st["small"]),
],
[
Paragraph("<b>Succussion Splash</b>", ParagraphStyle("ss", parent=st["small"],
fontName="Helvetica-Bold", textColor=PURPLE)),
Paragraph("Splashing sound on shaking the patient; audible without stethoscope", st["small"]),
Paragraph("<b>Gastric outlet obstruction</b> (pyloric stenosis) — retained food and fluid in stomach. "
"Also seen in ileus if >4 hrs after meal", st["small"]),
],
]
bowel_ts = [
("BACKGROUND", (0, 0), (-1, 0), AMBER),
("ROWBACKGROUNDS", (0, 1), (-1, -1), [HexColor("#FFFFFF"), LIGHT_AMB]),
("GRID", (0, 0), (-1, -1), 0.5, MID_GREY),
("BOX", (0, 0), (-1, -1), 1.5, AMBER),
("VALIGN", (0, 0), (-1, -1), "TOP"),
("TOPPADDING", (0, 0), (-1, -1), 5),
("BOTTOMPADDING", (0, 0), (-1, -1), 5),
("LEFTPADDING", (0, 0), (-1, -1), 6),
]
story.append(Table(bowel_data, colWidths=[4.5*cm, 5*cm, 7*cm],
style=TableStyle(bowel_ts)))
story.append(Spacer(1, 0.3*cm))
# Vascular sounds + technique
story.append(Paragraph("Vascular Bruits & Technique", st["h3"]))
vasc_left = [
Paragraph("<b>WHERE TO AUSCULTATE</b>",
ParagraphStyle("wta", parent=st["small"], fontName="Helvetica-Bold", textColor=AMBER)),
bullet("All 4 quadrants for bowel sounds (start RUQ, move clockwise)", st),
bullet("Epigastrium / Aortic area: renal artery bruits", st),
bullet("Right of midline (2 cm): Right renal artery bruit", st),
bullet("Left of midline (2 cm): Left renal artery bruit", st),
bullet("Iliac fossae: Iliac artery bruits", st),
bullet("Liver (RUQ): Hepatic arterial bruit — hepatocellular carcinoma, AV fistula", st),
bullet("Periumbilical: Aortic bruit — aortic aneurysm, stenosis", st),
]
vasc_right = [
Paragraph("<b>VASCULAR BRUITS</b>",
ParagraphStyle("vb", parent=st["small"], fontName="Helvetica-Bold", textColor=RED)),
bullet("<b>Renal bruit</b>: Systolic bruit near umbilicus, lateral — renal artery stenosis (renovascular hypertension)", st),
bullet("<b>Aortic bruit</b>: Midline, above umbilicus — aortic aneurysm or atherosclerosis", st),
bullet("<b>Hepatic bruit</b>: Heard over liver — hepatocellular carcinoma, hepatic AV fistula", st),
bullet("<b>Peritoneal friction rub</b>: Scratchy sound synchronous with respiration over liver/spleen — infarction, peritonitis, malignancy", st),
Spacer(1, 0.15*cm),
Paragraph("<b>TECHNIQUE TIPS</b>",
ParagraphStyle("tt", parent=st["small"], fontName="Helvetica-Bold", textColor=TEAL)),
bullet("Use diaphragm lightly — don't press hard", st),
bullet("Keep room quiet; ask patient to breathe normally", st),
bullet("Listen in each area for at least 15 seconds before declaring absent", st),
bullet("Absent bowel sounds: listen for minimum 2 full minutes", st),
]
story.append(two_col_table(vasc_left, vasc_right, st, LIGHT_AMB, PALE_TEAL))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("Auscultation Mark Scheme (3 Marks)", st["h3"]))
story.append(mark_table([
("Technique & Sequence", 1,
"Auscultates before palpation/percussion; uses diaphragm; quiet environment; systematic 4 quadrants"),
("Bowel Sound Assessment", 1,
"Correctly describes normal sounds; identifies absent (2 min), hyperactive or hypoactive sounds with clinical interpretation"),
("Vascular Bruits & Special Sounds", 1,
"Auscultates for aortic/renal bruits; identifies succussion splash; notes friction rubs"),
], st, color=AMBER))
story.append(Spacer(1, 0.4*cm))
# ══════════════════════════════════════════════════════════════════════════
# SUMMARY MARK SCHEME
# ══════════════════════════════════════════════════════════════════════════
story.append(PageBreak())
story.append(Paragraph("COMPLETE MARK SCHEME SUMMARY", ParagraphStyle(
"cms", parent=st["h1"], fontSize=18, alignment=TA_CENTER, textColor=NAVY)))
story.append(Spacer(1, 0.3*cm))
final_data = [
[
Paragraph("<b>Station</b>", ParagraphStyle("fh1", parent=st["center"], textColor=white,
fontSize=10, fontName="Helvetica-Bold")),
Paragraph("<b>Criterion</b>", ParagraphStyle("fh2", parent=st["center"], textColor=white,
fontSize=10, fontName="Helvetica-Bold")),
Paragraph("<b>Key Points</b>", ParagraphStyle("fh3", parent=st["center"], textColor=white,
fontSize=10, fontName="Helvetica-Bold")),
Paragraph("<b>Marks</b>", ParagraphStyle("fh4", parent=st["center"], textColor=white,
fontSize=10, fontName="Helvetica-Bold")),
],
# Inspection
[Paragraph("<b>INSPECTION</b>", ParagraphStyle("ins", parent=st["small"], fontName="Helvetica-Bold",
textColor=NAVY)),
Paragraph("Patient Positioning & Exposure", st["small"]),
Paragraph("Supine, arms by sides, nipples to groin exposed, good lighting", st["small"]),
Paragraph("1", ParagraphStyle("m1", parent=st["center"], textColor=NAVY, fontName="Helvetica-Bold"))],
[Paragraph("", st["small"]),
Paragraph("Skin & Surface Features", st["small"]),
Paragraph("Scars, dilated veins, striae, ecchymosis, umbilicus changes", st["small"]),
Paragraph("1", ParagraphStyle("m2", parent=st["center"], textColor=NAVY, fontName="Helvetica-Bold"))],
[Paragraph("", st["small"]),
Paragraph("Contour & Shape", st["small"]),
Paragraph("Describes flat/distended, 5 F's, correct region localization", st["small"]),
Paragraph("1", ParagraphStyle("m3", parent=st["center"], textColor=NAVY, fontName="Helvetica-Bold"))],
[Paragraph("", st["small"]),
Paragraph("Movement & Special Signs", st["small"]),
Paragraph("Respiratory movement, peristalsis, pulsations, hernias (cough test)", st["small"]),
Paragraph("1", ParagraphStyle("m4", parent=st["center"], textColor=NAVY, fontName="Helvetica-Bold"))],
# Palpation
[Paragraph("<b>PALPATION</b>", ParagraphStyle("pal", parent=st["small"], fontName="Helvetica-Bold",
textColor=GREEN)),
Paragraph("Technique & Approach", st["small"]),
Paragraph("Warm hands, starts away from pain, watches face, light then deep", st["small"]),
Paragraph("1", ParagraphStyle("m5", parent=st["center"], textColor=GREEN, fontName="Helvetica-Bold"))],
[Paragraph("", st["small"]),
Paragraph("Tenderness & Guarding", st["small"]),
Paragraph("Identifies site, guarding vs. rigidity, rebound tenderness", st["small"]),
Paragraph("1", ParagraphStyle("m6", parent=st["center"], textColor=GREEN, fontName="Helvetica-Bold"))],
[Paragraph("", st["small"]),
Paragraph("Liver Palpation", st["small"]),
Paragraph("Starts from RIF, uses inspiration, hepatomegaly assessment", st["small"]),
Paragraph("1", ParagraphStyle("m7", parent=st["center"], textColor=GREEN, fontName="Helvetica-Bold"))],
[Paragraph("", st["small"]),
Paragraph("Spleen / Kidney / Bladder / Aorta", st["small"]),
Paragraph("Correct technique, bimanual kidney, organ characteristics", st["small"]),
Paragraph("1", ParagraphStyle("m8", parent=st["center"], textColor=GREEN, fontName="Helvetica-Bold"))],
# Percussion
[Paragraph("<b>PERCUSSION</b>", ParagraphStyle("per", parent=st["small"], fontName="Helvetica-Bold",
textColor=TEAL)),
Paragraph("Technique", st["small"]),
Paragraph("Correct pleximeter/plexor method; systematic all regions", st["small"]),
Paragraph("1", ParagraphStyle("m9", parent=st["center"], textColor=TEAL, fontName="Helvetica-Bold"))],
[Paragraph("", st["small"]),
Paragraph("Liver Percussion", st["small"]),
Paragraph("Upper and lower borders; estimates liver span (6-12 cm normal)", st["small"]),
Paragraph("1", ParagraphStyle("m10", parent=st["center"], textColor=TEAL, fontName="Helvetica-Bold"))],
[Paragraph("", st["small"]),
Paragraph("Spleen / Traube's Space", st["small"]),
Paragraph("Percusses Traube's space; detects dullness = splenomegaly", st["small"]),
Paragraph("1", ParagraphStyle("m11", parent=st["center"], textColor=TEAL, fontName="Helvetica-Bold"))],
[Paragraph("", st["small"]),
Paragraph("Ascites Detection", st["small"]),
Paragraph("Shifting dullness or fluid thrill; explains significance", st["small"]),
Paragraph("1", ParagraphStyle("m12", parent=st["center"], textColor=TEAL, fontName="Helvetica-Bold"))],
# Auscultation
[Paragraph("<b>AUSCULTATION</b>", ParagraphStyle("aus", parent=st["small"], fontName="Helvetica-Bold",
textColor=AMBER)),
Paragraph("Technique & Sequence", st["small"]),
Paragraph("Before palpation; diaphragm; quiet; systematic 4 quadrants", st["small"]),
Paragraph("1", ParagraphStyle("m13", parent=st["center"], textColor=AMBER, fontName="Helvetica-Bold"))],
[Paragraph("", st["small"]),
Paragraph("Bowel Sound Assessment", st["small"]),
Paragraph("Normal/absent/hyperactive/hypoactive; clinical interpretation", st["small"]),
Paragraph("1", ParagraphStyle("m14", parent=st["center"], textColor=AMBER, fontName="Helvetica-Bold"))],
[Paragraph("", st["small"]),
Paragraph("Vascular Bruits & Special Sounds", st["small"]),
Paragraph("Aortic/renal bruits; succussion splash; friction rubs", st["small"]),
Paragraph("1", ParagraphStyle("m15", parent=st["center"], textColor=AMBER, fontName="Helvetica-Bold"))],
# Total
[
Paragraph("<b>TOTAL</b>", ParagraphStyle("tot3", parent=st["center"], textColor=white,
fontSize=12, fontName="Helvetica-Bold")),
Paragraph("", st["small"]),
Paragraph("", st["small"]),
Paragraph("<b>15</b>", ParagraphStyle("tot4", parent=st["center"], textColor=white,
fontSize=14, fontName="Helvetica-Bold")),
],
]
final_ts = [
("BACKGROUND", (0, 0), (-1, 0), NAVY),
("BACKGROUND", (0, -1), (-1, -1), NAVY),
# Group backgrounds
("BACKGROUND", (0, 1), (-1, 4), HexColor("#EAF0F6")), # inspection
("BACKGROUND", (0, 5), (-1, 8), LIGHT_GRN), # palpation
("BACKGROUND", (0, 9), (-1, 12), PALE_TEAL), # percussion
("BACKGROUND", (0, 13), (-1, 15), LIGHT_AMB), # auscultation
("GRID", (0, 0), (-1, -1), 0.5, MID_GREY),
("BOX", (0, 0), (-1, -1), 2, NAVY),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("TOPPADDING", (0, 0), (-1, -1), 5),
("BOTTOMPADDING", (0, 0), (-1, -1), 5),
("LEFTPADDING", (0, 0), (-1, -1), 6),
("RIGHTPADDING", (0, 0), (-1, -1), 6),
("ALIGN", (3, 0), (3, -1), "CENTER"),
("SPAN", (0, 1), (0, 4)), # inspection label spans
("SPAN", (0, 5), (0, 8)), # palpation label spans
("SPAN", (0, 9), (0, 12)), # percussion label spans
("SPAN", (0, 13), (0, 15)), # auscultation label spans
("VALIGN", (0, 1), (0, 15), "MIDDLE"),
]
story.append(Table(final_data, colWidths=[3.5*cm, 4.5*cm, 7*cm, 2*cm],
style=TableStyle(final_ts)))
story.append(Spacer(1, 0.4*cm))
# Quick Reference Mnemonics
story.append(Paragraph("Quick Reference Mnemonics", st["h2"]))
mnem_data = [
[
Paragraph("<b>5 F's of Abdominal Distension</b>",
ParagraphStyle("mn1", parent=st["small"], fontName="Helvetica-Bold", textColor=NAVY)),
Paragraph("<b>Fat · Fluid · Flatus · Faeces · Foetus</b>", st["small"]),
],
[
Paragraph("<b>Order of Abdominal Examination</b>",
ParagraphStyle("mn2", parent=st["small"], fontName="Helvetica-Bold", textColor=TEAL)),
Paragraph("<b>I</b>nspection → <b>A</b>uscultation → <b>P</b>ercussion → <b>P</b>alpation "
"<i>(\"I Always Pass Patients\")</i>", st["small"]),
],
[
Paragraph("<b>Liver Palpation Rule</b>",
ParagraphStyle("mn3", parent=st["small"], fontName="Helvetica-Bold", textColor=GREEN)),
Paragraph("Start from RIF, move up with <b>each inspiration</b>. "
"Normal span: 6-12 cm (midclavicular line).", st["small"]),
],
[
Paragraph("<b>Spleen vs Kidney</b>",
ParagraphStyle("mn4", parent=st["small"], fontName="Helvetica-Bold", textColor=RED)),
Paragraph("<b>Spleen</b>: Cannot get above · notch on medial border · dull · moves with breathing · not ballottable | "
"<b>Kidney</b>: Can get above · resonant (bowel in front) · ballottable · bimanually palpable", st["small"]),
],
[
Paragraph("<b>Absent Bowel Sounds</b>",
ParagraphStyle("mn5", parent=st["small"], fontName="Helvetica-Bold", textColor=AMBER)),
Paragraph("Must listen for <b>2-3 full minutes</b> before declaring absent. "
"Absent = Paralytic Ileus / Peritonitis (late).", st["small"]),
],
]
mnem_ts = [
("ROWBACKGROUNDS", (0, 0), (-1, -1), [LIGHT_BG, HexColor("#FFFFFF")]),
("BOX", (0, 0), (-1, -1), 1.5, TEAL),
("GRID", (0, 0), (-1, -1), 0.5, MID_GREY),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("TOPPADDING", (0, 0), (-1, -1), 6),
("BOTTOMPADDING", (0, 0), (-1, -1), 6),
("LEFTPADDING", (0, 0), (-1, -1), 8),
("RIGHTPADDING", (0, 0), (-1, -1), 8),
]
story.append(Table(mnem_data, colWidths=[5.5*cm, 11.5*cm],
style=TableStyle(mnem_ts)))
story.append(Spacer(1, 0.3*cm))
# Footer note
story.append(HRFlowable(width="100%", thickness=1, color=MID_GREY, spaceAfter=4))
story.append(Paragraph(
"<i>Sources: Goldman-Cecil Medicine 26e | Yamada's Textbook of Gastroenterology 7e | "
"Bailey & Love's Short Practice of Surgery 28e | S Das Manual on Clinical Surgery 13e | "
"Berek & Novak's Gynecology</i>",
ParagraphStyle("src", parent=st["caption"], fontSize=7.5)
))
# Build
doc.build(story)
print(f"PDF generated: {output_path}")
return output_path
if __name__ == "__main__":
build_pdf()
cd /home/daytona/workspace/abdominal-exam-pae && python3 generate_pdf.py 2>&1
ls -lh /home/daytona/workspace/abdominal-exam-pae/Abdominal_Examination_PAE.pdf
Abdominal Examination PAE
PDF Document · PDF
Generate a quick reference laminated card version of key percussion/palpation findings
ls /home/daytona/workspace/abdominal-exam-pae/
~/abdominal-exam-pae/laminated_card.py
#!/usr/bin/env python3
"""
Abdominal Exam Quick Reference — Laminated Card
Two A5 landscape pages (front + back), print & laminate.
"""
from reportlab.lib.pagesizes import A5, landscape
from reportlab.lib.units import cm, mm
from reportlab.lib.colors import HexColor, white, black
from reportlab.platypus import (
SimpleDocTemplate, Table, TableStyle, Paragraph,
Spacer, HRFlowable, KeepTogether
)
from reportlab.lib.styles import ParagraphStyle
from reportlab.lib.enums import TA_CENTER, TA_LEFT
from reportlab.platypus import Flowable
# ── Palette ───────────────────────────────────────────────────────────────────
NAVY = HexColor("#1A3A5C")
TEAL = HexColor("#1A7A8A")
GREEN = HexColor("#1E6B40")
RED = HexColor("#B03025")
AMBER = HexColor("#C47F0A")
PURPLE = HexColor("#5B2C8D")
LGREY = HexColor("#F2F5F7")
MGREY = HexColor("#BDC3C7")
DGREY = HexColor("#5D6D7E")
PALE_T = HexColor("#E2F0F3")
PALE_G = HexColor("#E4F5EA")
PALE_A = HexColor("#FDF4E0")
PALE_R = HexColor("#FDECEB")
PALE_P = HexColor("#F3EDFB")
PAGE_W, PAGE_H = landscape(A5) # 210 x 148 mm landscape → usable ~196 x 134 mm
MARGIN = 7*mm
COL_W = (PAGE_W - 2*MARGIN) # full width to divide into columns
# ── Styles ────────────────────────────────────────────────────────────────────
def S(name, **kw):
defaults = dict(fontName="Helvetica", fontSize=7, leading=9.5,
textColor=HexColor("#1C2833"), spaceAfter=0, spaceBefore=0)
defaults.update(kw)
return ParagraphStyle(name, **defaults)
TITLE = S("TIT", fontName="Helvetica-Bold", fontSize=8.5, textColor=white, alignment=TA_CENTER, leading=11)
HDNG = S("HDG", fontName="Helvetica-Bold", fontSize=7.5, textColor=white, alignment=TA_CENTER, leading=10)
SUBHDG = S("SHD", fontName="Helvetica-Bold", fontSize=7, textColor=NAVY, leading=9)
BODY = S("BDY", fontName="Helvetica", fontSize=6.8, leading=9)
BODYBLD = S("BBD", fontName="Helvetica-Bold", fontSize=6.8, leading=9)
SMALL = S("SML", fontName="Helvetica", fontSize=6, textColor=DGREY, leading=8)
CENTER = S("CTR", fontName="Helvetica", fontSize=6.8, alignment=TA_CENTER, leading=9)
CENTBLD = S("CBL", fontName="Helvetica-Bold", fontSize=6.8, alignment=TA_CENTER, leading=9)
CENTRED_WHITE = S("CW", fontName="Helvetica-Bold", fontSize=7, textColor=white, alignment=TA_CENTER, leading=9.5)
RED_BLD = S("RBL", fontName="Helvetica-Bold", fontSize=6.8, textColor=RED, leading=9)
GRN_BLD = S("GBL", fontName="Helvetica-Bold", fontSize=6.8, textColor=GREEN, leading=9)
AMB_BLD = S("ABL", fontName="Helvetica-Bold", fontSize=6.8, textColor=AMBER, leading=9)
PUR_BLD = S("PBL", fontName="Helvetica-Bold", fontSize=6.8, textColor=PURPLE,leading=9)
TEA_BLD = S("TBL", fontName="Helvetica-Bold", fontSize=6.8, textColor=TEAL, leading=9)
def P(text, style=BODY): return Paragraph(text, style)
def B(text): return Paragraph(f"<b>{text}</b>", BODY)
def bul(text, color=None):
if color:
return Paragraph(f'<font color="{color}">●</font> {text}', BODY)
return Paragraph(f"● {text}", BODY)
def cell_ts(bg, border=None, pad=3):
ts = [
("BACKGROUND", (0,0), (-1,-1), bg),
("TOPPADDING", (0,0), (-1,-1), pad),
("BOTTOMPADDING", (0,0), (-1,-1), pad),
("LEFTPADDING", (0,0), (-1,-1), pad+1),
("RIGHTPADDING", (0,0), (-1,-1), pad),
("VALIGN", (0,0), (-1,-1), "TOP"),
]
if border:
ts.append(("BOX", (0,0), (-1,-1), 0.5, border))
return ts
def hdr_row(text, color, colspan=1):
return [P(text, HDNG) if colspan == 1 else P(text, HDNG)]
# ── Helper: mini section header ───────────────────────────────────────────────
def sec(text, color):
t = Table([[P(text, HDNG)]], colWidths=[None],
style=TableStyle([
("BACKGROUND", (0,0),(-1,-1), color),
("TOPPADDING", (0,0),(-1,-1), 2),
("BOTTOMPADDING",(0,0),(-1,-1), 2),
("LEFTPADDING",(0,0),(-1,-1), 4),
]))
return t
# ══════════════════════════════════════════════════════════════════════════════
# FRONT: PERCUSSION (left half) + PALPATION ORGANS (right half)
# ══════════════════════════════════════════════════════════════════════════════
def make_front():
C1 = 97*mm # left column
C2 = 96*mm # right column
GAP = 3*mm
# ── LEFT: PERCUSSION ─────────────────────────────────────────────────────
# 1. Percussion notes
notes = [
[P("<b>Note</b>", HDNG), P("<b>Sound</b>", HDNG), P("<b>Means</b>", HDNG)],
[P("<b>Tympanic</b>",TEA_BLD), P("Drum-like/resonant",BODY), P("Gas-filled bowel (normal)",BODY)],
[P("<b>Dull</b>",RED_BLD), P("Flat/muffled",BODY), P("Solid organ / fluid / mass",BODY)],
[P("<b>Stony dull</b>",PUR_BLD),P("Wooden/flat",BODY), P("Massive ascites / large cyst",BODY)],
[P("<b>Hyperresonant</b>",GRN_BLD),P("Very loud/hollow",BODY),P("Excessive gas, obstruction",BODY)],
]
notes_ts = [
("BACKGROUND",(0,0),(-1,0), TEAL),
("ROWBACKGROUNDS",(0,1),(-1,-1),[white, PALE_T]),
("GRID",(0,0),(-1,-1),0.4, MGREY),
("BOX",(0,0),(-1,-1),0.8, TEAL),
("TOPPADDING",(0,0),(-1,-1),2),("BOTTOMPADDING",(0,0),(-1,-1),2),
("LEFTPADDING",(0,0),(-1,-1),3),("RIGHTPADDING",(0,0),(-1,-1),3),
("VALIGN",(0,0),(-1,-1),"MIDDLE"),
]
notes_t = Table(notes, colWidths=[22*mm, 28*mm, 44*mm], style=TableStyle(notes_ts))
# 2. Liver span
liver_rows = [
[P("<b>LIVER SPAN (Midclavicular Line)</b>", HDNG)],
[P("Normal: <b>6–12 cm</b> | Upper border: 5th ICS (dull) | Lower: costal margin (dull)", BODY)],
[P("Percuss <b>downward</b> from 4th ICS → resonant→<b>dull</b> = upper border", BODY)],
[P("Percuss <b>upward</b> from RIF → tympanic→<b>dull</b> = lower border", BODY)],
[P("Loss of liver dullness = <b>FREE GAS</b> (perforation — emergency!)", RED_BLD)],
]
liver_ts = [
("BACKGROUND",(0,0),(-1,0), NAVY),
("BACKGROUND",(0,1),(-1,-1), PALE_T),
("BOX",(0,0),(-1,-1),0.8,NAVY),
("GRID",(0,1),(-1,-1),0.3,MGREY),
("TOPPADDING",(0,0),(-1,-1),2),("BOTTOMPADDING",(0,0),(-1,-1),2),
("LEFTPADDING",(0,0),(-1,-1),4),("RIGHTPADDING",(0,0),(-1,-1),3),
("VALIGN",(0,0),(-1,-1),"MIDDLE"),
]
liver_t = Table(liver_rows, colWidths=[C1], style=TableStyle(liver_ts))
# 3. Spleen / Traube's
spleen_rows = [
[P("<b>SPLEEN — Traube's Space</b>", HDNG), P("<b>Result</b>", HDNG)],
[P("Normally tympanic (stomach gas)", BODY), P("Normal spleen", BODY)],
[P("Dull in Traube's space", BODY), P("<b>Splenomegaly</b>", RED_BLD)],
[P("Nixon's method: dullness >8 cm", BODY), P("Splenomegaly likely", BODY)],
]
spleen_ts = [
("BACKGROUND",(0,0),(-1,0), GREEN),
("ROWBACKGROUNDS",(0,1),(-1,-1),[white,PALE_G]),
("GRID",(0,0),(-1,-1),0.4,MGREY),
("BOX",(0,0),(-1,-1),0.8,GREEN),
("TOPPADDING",(0,0),(-1,-1),2),("BOTTOMPADDING",(0,0),(-1,-1),2),
("LEFTPADDING",(0,0),(-1,-1),3),("RIGHTPADDING",(0,0),(-1,-1),3),
("VALIGN",(0,0),(-1,-1),"MIDDLE"),
]
spleen_t = Table(spleen_rows, colWidths=[52*mm, 42*mm], style=TableStyle(spleen_ts))
# 4. Ascites detection
ascites_rows = [
[P("<b>TEST</b>", HDNG), P("<b>METHOD</b>", HDNG), P("<b>+ve result</b>", HDNG)],
[P("<b>Shifting Dullness</b>", PUR_BLD),
P("Percuss flank dullness supine → roll patient away → re-percuss", BODY),
P("Dullness disappears on rolling = fluid shifts. Best test (>1.5 L)", BODY)],
[P("<b>Fluid Thrill</b>", PUR_BLD),
P("Assistant's hand on midline; flick one flank; feel other flank", BODY),
P("Impulse felt. Only massive ascites. Less reliable.", BODY)],
]
ascites_ts = [
("BACKGROUND",(0,0),(-1,0), PURPLE),
("ROWBACKGROUNDS",(0,1),(-1,-1),[white,PALE_P]),
("GRID",(0,0),(-1,-1),0.4,MGREY),
("BOX",(0,0),(-1,-1),0.8,PURPLE),
("TOPPADDING",(0,0),(-1,-1),2),("BOTTOMPADDING",(0,0),(-1,-1),2),
("LEFTPADDING",(0,0),(-1,-1),3),
("VALIGN",(0,0),(-1,-1),"TOP"),
]
ascites_t = Table(ascites_rows, colWidths=[28*mm, 40*mm, 29*mm], style=TableStyle(ascites_ts))
left_items = [
[P("PERCUSSION", TITLE)],
[notes_t],
[Spacer(1,2*mm)],
[liver_t],
[Spacer(1,2*mm)],
[spleen_t],
[Spacer(1,2*mm)],
[ascites_t],
]
left_outer = Table(left_items, colWidths=[C1],
style=TableStyle([
("BACKGROUND",(0,0),(-1,0), TEAL),
("TOPPADDING",(0,0),(-1,0),3),("BOTTOMPADDING",(0,0),(-1,0),3),
("LEFTPADDING",(0,0),(-1,-1),0),("RIGHTPADDING",(0,0),(-1,-1),0),
("TOPPADDING",(0,1),(-1,-1),0),("BOTTOMPADDING",(0,1),(-1,-1),0),
("BOX",(0,0),(-1,-1),1.2,TEAL),
]))
# ── RIGHT: PALPATION — ORGAN FINDINGS ────────────────────────────────────
organ_rows = [
[P("<b>ORGAN</b>", HDNG), P("<b>TECHNIQUE</b>", HDNG),
P("<b>NORMAL</b>", HDNG), P("<b>ABNORMAL / SIGNS</b>", HDNG)],
[P("<b>LIVER</b>", TEA_BLD),
P("R hand at RCM; start RIF; move up per inspiration", BODY),
P("Not palpable (or ≤2cm below CM)", BODY),
P("Tender→hepatitis/CCF; Hard/nodular→cirrhosis/Ca; Pulsatile→TR", BODY)],
[P("<b>SPLEEN</b>", GRN_BLD),
P("Start RIF→LUQ; L hand lifts L ribs; inspire", BODY),
P("Not palpable normally", BODY),
P("Notch on medial border; can't get above it; moves with breath; dull", BODY)],
[P("<b>KIDNEY</b>", AMB_BLD),
P("Bimanual: one in loin, one abdomen; ballottement", BODY),
P("R lower pole occasionally palpable", BODY),
P("Ballottable; resonant (bowel in front); can get above it; enlarged→hydronephrosis/tumor/cyst", BODY)],
[P("<b>BLADDER</b>", NAVY.__class__("#1A3A5C") and BODY.__class__("BLB", fontName="Helvetica-Bold", fontSize=6.8, textColor=NAVY, leading=9)),
P("Suprapubic; smooth midline mass from pelvis", BODY),
P("Not palpable if empty", BODY),
P("Smooth, tender, dull to perc, can't get below→retention", BODY)],
[P("<b>AORTA</b>", RED_BLD),
P("Midline above umbilicus; fingers spread apart", BODY),
P("Pulsatile (transmitted) in thin patients", BODY),
P("Expansile (fingers pushed apart) = AAA", BODY)],
[P("<b>GALLBLADDER</b>", PUR_BLD),
P("Deep palpation RUQ below liver; Murphy's sign on inspiration", BODY),
P("Not palpable normally", BODY),
P("Murphy's sign +ve = cholecystitis; Palpable + painless = Courvoisier's (obstructive jaundice)", BODY)],
]
organ_ts = [
("BACKGROUND",(0,0),(-1,0), NAVY),
("ROWBACKGROUNDS",(0,1),(-1,-1),[white, LGREY]),
("GRID",(0,0),(-1,-1),0.4,MGREY),
("BOX",(0,0),(-1,-1),1,NAVY),
("TOPPADDING",(0,0),(-1,-1),2),("BOTTOMPADDING",(0,0),(-1,-1),2),
("LEFTPADDING",(0,0),(-1,-1),3),("RIGHTPADDING",(0,0),(-1,-1),2),
("VALIGN",(0,0),(-1,-1),"TOP"),
]
organ_t = Table(organ_rows, colWidths=[17*mm, 26*mm, 22*mm, 31*mm],
style=TableStyle(organ_ts))
# Spleen vs Kidney quick compare
sv_rows = [
[P("<b>FEATURE</b>", HDNG), P("<b>SPLEEN</b>", HDNG), P("<b>KIDNEY</b>", HDNG)],
[P("Get above", BODY), P("<b>NO</b> ✗", RED_BLD), P("<b>YES</b> ✓", GRN_BLD)],
[P("Notch", BODY), P("<b>YES</b> (medial)", GRN_BLD), P("<b>NO</b>", RED_BLD)],
[P("Percussion", BODY), P("Dull", BODY), P("Resonant (bowel)", BODY)],
[P("Ballottable", BODY), P("<b>NO</b>", RED_BLD), P("<b>YES</b>", GRN_BLD)],
[P("Moves with resp.", BODY), P("<b>YES</b>", GRN_BLD), P("Slightly", BODY)],
[P("Bimanual feel", BODY), P("<b>NO</b>", RED_BLD), P("<b>YES</b>", GRN_BLD)],
]
sv_ts = [
("BACKGROUND",(0,0),(-1,0), GREEN),
("ROWBACKGROUNDS",(0,1),(-1,-1),[white,PALE_G]),
("GRID",(0,0),(-1,-1),0.4,MGREY),
("BOX",(0,0),(-1,-1),0.8,GREEN),
("TOPPADDING",(0,0),(-1,-1),2),("BOTTOMPADDING",(0,0),(-1,-1),2),
("LEFTPADDING",(0,0),(-1,-1),3),
("ALIGN",(1,1),(2,-1),"CENTER"),
]
sv_t = Table(sv_rows, colWidths=[22*mm, 22*mm, 22*mm], style=TableStyle(sv_ts))
right_items = [
[P("PALPATION — ORGANS", TITLE)],
[organ_t],
[Spacer(1,2*mm)],
[sv_t],
]
right_outer = Table(right_items, colWidths=[C2],
style=TableStyle([
("BACKGROUND",(0,0),(-1,0), NAVY),
("TOPPADDING",(0,0),(-1,0),3),("BOTTOMPADDING",(0,0),(-1,0),3),
("LEFTPADDING",(0,0),(-1,-1),0),("RIGHTPADDING",(0,0),(-1,-1),0),
("TOPPADDING",(0,1),(-1,-1),0),("BOTTOMPADDING",(0,1),(-1,-1),0),
("BOX",(0,0),(-1,-1),1.2,NAVY),
]))
# Combine columns
front = Table([[left_outer, Spacer(GAP,1), right_outer]],
colWidths=[C1, GAP, C2],
style=TableStyle([
("VALIGN",(0,0),(-1,-1),"TOP"),
("LEFTPADDING",(0,0),(-1,-1),0),
("RIGHTPADDING",(0,0),(-1,-1),0),
("TOPPADDING",(0,0),(-1,-1),0),
("BOTTOMPADDING",(0,0),(-1,-1),0),
]))
return front
# ══════════════════════════════════════════════════════════════════════════════
# BACK: SPECIAL SIGNS + TENDERNESS GUIDE + QUICK RULES
# ══════════════════════════════════════════════════════════════════════════════
def make_back():
C1 = 97*mm
C2 = 96*mm
GAP = 3*mm
# ── LEFT: SPECIAL SIGNS IN PALPATION ────────────────────────────────────
signs_rows = [
[P("<b>SIGN</b>", HDNG), P("<b>HOW TO ELICIT</b>", HDNG), P("<b>+ve = Suggests</b>", HDNG)],
[P("<b>McBurney's</b>", RED_BLD),
P("Pressure at 1/3 ASIS-to-umbilicus point (McBurney's point)", BODY),
P("Appendicitis", BODY)],
[P("<b>Murphy's</b>", AMB_BLD),
P("Deep palpation RUQ; ask patient to breathe in deeply", BODY),
P("Arrest of inspiration + pain = Acute Cholecystitis", BODY)],
[P("<b>Rovsing's</b>", RED_BLD),
P("Press LIF firmly", BODY),
P("Pain felt in RIF = Appendicitis", BODY)],
[P("<b>Psoas</b>", GRN_BLD),
P("Extend right hip with patient on left side (or flex hip against resistance)", BODY),
P("RIF pain = retrocecal appendicitis / psoas abscess", BODY)],
[P("<b>Obturator</b>", GRN_BLD),
P("Flex+internally rotate right hip (patient supine)", BODY),
P("Hypogastric pain = pelvic appendix / pelvic abscess", BODY)],
[P("<b>Blumberg's (Rebound)</b>", PUR_BLD),
P("Deep pressure then sudden release", BODY),
P("Release pain > pressing = peritoneal irritation (use percussion instead)", BODY)],
[P("<b>Courvoisier's</b>", TEA_BLD),
P("Palpable, non-tender gallbladder with jaundice", BODY),
P("Obstructive jaundice (NOT gallstones = Courvoisier's law)", BODY)],
[P("<b>Carnett's</b>", DGREY.__class__("DGB") and BODY.__class__("CRN", fontName="Helvetica-Bold", fontSize=6.8, textColor=DGREY, leading=9)),
P("Tenderness increases when patient lifts head (tenses abdominal wall)", BODY),
P("Pain in abdominal wall (not intra-abdominal)", BODY)],
]
signs_ts = [
("BACKGROUND",(0,0),(-1,0), RED),
("ROWBACKGROUNDS",(0,1),(-1,-1),[white,PALE_R]),
("GRID",(0,0),(-1,-1),0.4,MGREY),
("BOX",(0,0),(-1,-1),1,RED),
("TOPPADDING",(0,0),(-1,-1),2),("BOTTOMPADDING",(0,0),(-1,-1),2),
("LEFTPADDING",(0,0),(-1,-1),3),("RIGHTPADDING",(0,0),(-1,-1),2),
("VALIGN",(0,0),(-1,-1),"TOP"),
]
signs_t = Table(signs_rows, colWidths=[20*mm, 39*mm, 36*mm], style=TableStyle(signs_ts))
# Tenderness localization
tend_rows = [
[P("<b>LOCATION OF TENDERNESS</b>", HDNG), P("<b>THINK OF</b>", HDNG)],
[P("RUQ", BODY), P("Cholecystitis, hepatitis, peptic ulcer, pneumonia (R lower lobe)", BODY)],
[P("Epigastrium", BODY), P("Peptic ulcer, pancreatitis, AAA, MI (referred)", BODY)],
[P("LUQ", BODY), P("Splenomegaly, gastric ulcer, splenic infarct, pancreatitis (tail)", BODY)],
[P("RIF", BODY), P("Appendicitis, Crohn's, ovarian cyst (R), Meckel's", BODY)],
[P("Suprapubic", BODY), P("Cystitis, pelvic inflammatory disease, ectopic, fibroids", BODY)],
[P("LIF", BODY), P("Diverticulitis, ovarian cyst (L), sigmoid volvulus, constipation", BODY)],
[P("Generalized", BODY), P("Peritonitis, mesenteric ischemia, IBD flare, bowel perforation", BODY)],
[P("Loin / Flank", BODY), P("Renal colic (ureteric stone), pyelonephritis, renal cell Ca", BODY)],
]
tend_ts = [
("BACKGROUND",(0,0),(-1,0), AMBER),
("ROWBACKGROUNDS",(0,1),(-1,-1),[white,PALE_A]),
("GRID",(0,0),(-1,-1),0.4,MGREY),
("BOX",(0,0),(-1,-1),1,AMBER),
("TOPPADDING",(0,0),(-1,-1),2),("BOTTOMPADDING",(0,0),(-1,-1),2),
("LEFTPADDING",(0,0),(-1,-1),3),
("VALIGN",(0,0),(-1,-1),"MIDDLE"),
]
tend_t = Table(tend_rows, colWidths=[20*mm, 74*mm], style=TableStyle(tend_ts))
left_items = [
[P("SPECIAL SIGNS — PALPATION", TITLE)],
[signs_t],
[Spacer(1,2*mm)],
[P("TENDERNESS LOCALIZATION", P("x",TITLE).__class__("TL", fontName="Helvetica-Bold", fontSize=8.5, textColor=white, alignment=TA_CENTER, leading=11))],
[tend_t],
]
left_outer = Table(left_items, colWidths=[C1],
style=TableStyle([
("BACKGROUND",(0,0),(0,0), RED),
("BACKGROUND",(0,3),(0,3), AMBER),
("TOPPADDING",(0,0),(-1,0),3),("BOTTOMPADDING",(0,0),(-1,0),3),
("TOPPADDING",(0,3),(-1,3),3),("BOTTOMPADDING",(0,3),(-1,3),3),
("LEFTPADDING",(0,0),(-1,-1),0),("RIGHTPADDING",(0,0),(-1,-1),0),
("TOPPADDING",(0,1),(-1,2),0),("BOTTOMPADDING",(0,1),(-1,2),0),
("TOPPADDING",(0,4),(-1,4),0),("BOTTOMPADDING",(0,4),(-1,4),0),
("BOX",(0,0),(-1,-1),1.2,RED),
]))
# ── RIGHT: PERCUSSION FINDINGS + QUICK RULES + MNEMONICS ─────────────────
# Percussion at specific sites
perc_site_rows = [
[P("<b>SITE</b>", HDNG), P("<b>NORMAL</b>", HDNG), P("<b>ABNORMAL</b>", HDNG), P("<b>MEANS</b>", HDNG)],
[P("RUQ (liver)", BODY), P("Dull", BODY), P("Tympanic", BODY), P("Loss of liver dullness = pneumoperitoneum", BODY)],
[P("Epigastrium", BODY), P("Tympanic",BODY), P("Dull",BODY), P("Gastric mass / hepatomegaly", BODY)],
[P("LUQ (Traube)", BODY),P("Tympanic",BODY), P("Dull",BODY), P("Splenomegaly", BODY)],
[P("Flanks",BODY), P("Tympanic",BODY), P("Dull",BODY), P("Ascites / solid flank mass", BODY)],
[P("Suprapubic",BODY), P("Tympanic",BODY), P("Dull",BODY), P("Distended bladder / pelvic mass", BODY)],
[P("Umbilical",BODY), P("Tympanic",BODY), P("Hyper-resonant",BODY),P("Intestinal obstruction / ileus", BODY)],
]
perc_site_ts = [
("BACKGROUND",(0,0),(-1,0), TEAL),
("ROWBACKGROUNDS",(0,1),(-1,-1),[white,PALE_T]),
("GRID",(0,0),(-1,-1),0.4,MGREY),
("BOX",(0,0),(-1,-1),1,TEAL),
("TOPPADDING",(0,0),(-1,-1),2),("BOTTOMPADDING",(0,0),(-1,-1),2),
("LEFTPADDING",(0,0),(-1,-1),3),("RIGHTPADDING",(0,0),(-1,-1),2),
("VALIGN",(0,0),(-1,-1),"TOP"),
]
perc_site_t = Table(perc_site_rows, colWidths=[19*mm, 16*mm, 18*mm, 40*mm],
style=TableStyle(perc_site_ts))
# Golden rules
rules_rows = [
[P("<b>THE GOLDEN RULES</b>", TITLE)],
[P("1. Examination order: <b>Inspection → Auscultation → Percussion → Palpation</b>", BODY)],
[P("2. <b>Auscultate before palpation</b> — palpation alters bowel sounds", BODY)],
[P("3. Always start palpation <b>away from the painful area</b>", BODY)],
[P("4. Use <b>light palpation</b> first, then deep; watch patient's face throughout", BODY)],
[P("5. Absent bowel sounds: must listen for <b>2–3 full minutes</b> to confirm", BODY)],
[P("6. <b>Loss of liver dullness</b> on percussion = free gas = surgical emergency", RED_BLD)],
[P("7. Liver span normal: <b>6–12 cm</b> (midclavicular line)", BODY)],
[P("8. Shifting dullness detects ascites when fluid <b>>1.5 L</b>", BODY)],
[P("9. <b>Courvoisier's law</b>: Palpable painless GB + jaundice = NOT gallstones", BODY)],
[P("10. Spleen <b>cannot</b> be got above; Kidney <b>can</b>", BODY)],
]
rules_ts = [
("BACKGROUND",(0,0),(-1,0), NAVY),
("BACKGROUND",(0,1),(-1,-1), LGREY),
("ROWBACKGROUNDS",(0,1),(-1,-1),[LGREY, white]),
("BOX",(0,0),(-1,-1),1,NAVY),
("GRID",(0,1),(-1,-1),0.3,MGREY),
("TOPPADDING",(0,0),(-1,0),3),("BOTTOMPADDING",(0,0),(-1,0),3),
("TOPPADDING",(0,1),(-1,-1),2),("BOTTOMPADDING",(0,1),(-1,-1),2),
("LEFTPADDING",(0,0),(-1,-1),4),
]
rules_t = Table(rules_rows, colWidths=[C2], style=TableStyle(rules_ts))
# Mnemonics strip
mnem_rows = [
[
P("<b>5 F's</b>", CENTBLD),
P("<b>Exam Order</b>", CENTBLD),
P("<b>Peritonism</b>", CENTBLD),
],
[
P("Fat · Fluid · Flatus\nFaeces · Foetus", CENTER),
P("I Always Pass Patients\n(Inspect·Ausc·Perc·Palp)", CENTER),
P("Rigid abdomen\n+ Rebound + Guarding", CENTER),
],
]
mnem_ts = [
("BACKGROUND",(0,0),(-1,0), HexColor("#2C3E50")),
("BACKGROUND",(0,1),(-1,-1), HexColor("#F8F9FA")),
("BOX",(0,0),(-1,-1),1,HexColor("#2C3E50")),
("INNERGRID",(0,0),(-1,-1),0.4,MGREY),
("TOPPADDING",(0,0),(-1,-1),2),("BOTTOMPADDING",(0,0),(-1,-1),2),
("LEFTPADDING",(0,0),(-1,-1),3),
("ALIGN",(0,0),(-1,-1),"CENTER"),
("VALIGN",(0,0),(-1,-1),"MIDDLE"),
]
mnem_t = Table(mnem_rows, colWidths=[32*mm, 33*mm, 31*mm], style=TableStyle(mnem_ts))
right_items = [
[P("PERCUSSION — SITE FINDINGS", TITLE)],
[perc_site_t],
[Spacer(1,2*mm)],
[rules_t],
[Spacer(1,2*mm)],
[mnem_t],
]
right_outer = Table(right_items, colWidths=[C2],
style=TableStyle([
("BACKGROUND",(0,0),(-1,0), TEAL),
("TOPPADDING",(0,0),(-1,0),3),("BOTTOMPADDING",(0,0),(-1,0),3),
("LEFTPADDING",(0,0),(-1,-1),0),("RIGHTPADDING",(0,0),(-1,-1),0),
("TOPPADDING",(0,1),(-1,-1),0),("BOTTOMPADDING",(0,1),(-1,-1),0),
("BOX",(0,0),(-1,-1),1.2,TEAL),
]))
back = Table([[left_outer, Spacer(GAP,1), right_outer]],
colWidths=[C1, GAP, C2],
style=TableStyle([
("VALIGN",(0,0),(-1,-1),"TOP"),
("LEFTPADDING",(0,0),(-1,-1),0),
("RIGHTPADDING",(0,0),(-1,-1),0),
("TOPPADDING",(0,0),(-1,-1),0),
("BOTTOMPADDING",(0,0),(-1,-1),0),
]))
return back
# ══════════════════════════════════════════════════════════════════════════════
# BUILD
# ══════════════════════════════════════════════════════════════════════════════
def build():
out = "/home/daytona/workspace/abdominal-exam-pae/Abdominal_Laminated_Card.pdf"
doc = SimpleDocTemplate(
out,
pagesize=landscape(A5),
leftMargin=MARGIN, rightMargin=MARGIN,
topMargin=MARGIN, bottomMargin=MARGIN,
title="Abdominal Exam Quick Reference Card",
author="Medical Education",
)
story = []
# ── FRONT ─────────────────────────────────────────────────────────────────
front = make_front()
story.append(front)
# Page break → BACK
from reportlab.platypus import PageBreak
story.append(PageBreak())
# ── BACK ──────────────────────────────────────────────────────────────────
back = make_back()
story.append(back)
doc.build(story)
print(f"Card generated: {out}")
if __name__ == "__main__":
build()
cd /home/daytona/workspace/abdominal-exam-pae && python3 laminated_card.py 2>&1
~/abdominal-exam-pae/laminated_card.py
Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/abdominal-exam-pae/laminated_card.py'
cd /home/daytona/workspace/abdominal-exam-pae && python3 laminated_card.py 2>&1
~/abdominal-exam-pae/laminated_card.py
Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/abdominal-exam-pae/laminated_card.py'
~/abdominal-exam-pae/laminated_card.py
Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/abdominal-exam-pae/laminated_card.py'
cd /home/daytona/workspace/abdominal-exam-pae && sed -i \ 's/("TOPPADDING",(0,0),(-1,-1),2)/("TOPPADDING",(0,0),(-1,-1),1)/g; s/("BOTTOMPADDING",(0,0),(-1,-1),2)/("BOTTOMPADDING",(0,0),(-1,-1),1)/g; s/("TOPPADDING",(0,1),(-1,-1),2)/("TOPPADDING",(0,1),(-1,-1),1)/g; s/("BOTTOMPADDING",(0,1),(-1,-1),2)/("BOTTOMPADDING",(0,1),(-1,-1),1)/g; s/("TOPPADDING",(0,0),(-1,0),3)/("TOPPADDING",(0,0),(-1,0),2)/g; s/("BOTTOMPADDING",(0,0),(-1,0),3)/("BOTTOMPADDING",(0,0),(-1,0),2)/g; s/("TOPPADDING",(0,3),(-1,3),3)/("TOPPADDING",(0,3),(-1,3),2)/g; s/("BOTTOMPADDING",(0,3),(-1,3),3)/("BOTTOMPADDING",(0,3),(-1,3),2)/g' \ laminated_card.py && python3 laminated_card.py 2>&1
cd /home/daytona/workspace/abdominal-exam-pae && python3 -c " content = open('laminated_card.py').read() content = content.replace(',(0,0),(-1,-1),2)', ',(0,0),(-1,-1),1)') content = content.replace(',(0,1),(-1,-1),2)', ',(0,1),(-1,-1),1)') content = content.replace(',(0,0),(-1,0),3)', ',(0,0),(-1,0),2)') content = content.replace(',(0,3),(-1,3),3)', ',(0,3),(-1,3),2)') content = content.replace(',(0,1),(-1,2),3)', ',(0,1),(-1,2),2)') content = content.replace(',(0,4),(-1,4),3)', ',(0,4),(-1,4),2)') with open('laminated_card.py','w') as f: f.write(content) print('done') " && python3 laminated_card.py 2>&1
ls -lh /home/daytona/workspace/abdominal-exam-pae/Abdominal_Laminated_Card.pdf
Abdominal Laminated Card
PDF Document · PDF
| Left Column - PERCUSSION | Right Column - PALPATION ORGANS |
|---|---|
| Percussion notes table (Tympanic / Dull / Stony dull / Hyperresonant) | Organ-by-organ table: Liver, Spleen, Kidney, Bladder, Aorta, Gallbladder |
| Liver span technique (6-12 cm normal, upper + lower borders) | Technique, normal finding, abnormal finding for each |
| Spleen / Traube's space (dullness = splenomegaly) | Spleen vs Kidney quick comparison (6 features: notch, ballottement, percussion note, can get above, moves with breath, bimanual) |
| Ascites: Shifting Dullness vs Fluid Thrill (side-by-side) |
| Left Column - SPECIAL SIGNS | Right Column - PERCUSSION SITES + RULES |
|---|---|
| 8 named signs: McBurney's, Murphy's, Rovsing's, Psoas, Obturator, Blumberg's, Courvoisier's, Carnett's | Percussion at each abdominal region - normal vs abnormal |
| How to elicit each sign + what it indicates | 10 Golden Rules (laminated cheat list) |
| Tenderness localization table (RUQ/Epi/LUQ/RIF/LIF/Suprapubic/Flank + differentials) | 3-column mnemonic strip: 5 F's / Exam Order / Peritonism triad |