~/clinical-signs/generate_pdf.py
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import cm, mm
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, KeepTogether, PageBreak
)
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_JUSTIFY
from reportlab.pdfgen import canvas
from reportlab.platypus.flowables import Flowable
OUTPUT = "/tmp/workspace/clinical-signs/General_Physical_Examination_Signs.pdf"
# ── Colors ────────────────────────────────────────────────────────────────────
NAVY = colors.HexColor("#1B3A6B")
TEAL = colors.HexColor("#1A7A8A")
LIGHTBLUE= colors.HexColor("#E8F4F8")
AMBER = colors.HexColor("#F5A623")
LIGHT_AMBER = colors.HexColor("#FFF8EC")
GREEN = colors.HexColor("#2E7D32")
LIGHT_GREEN = colors.HexColor("#E8F5E9")
RED = colors.HexColor("#C62828")
LIGHT_RED = colors.HexColor("#FFEBEE")
PURPLE = colors.HexColor("#6A1B9A")
LIGHT_PURPLE = colors.HexColor("#F3E5F5")
ORANGE = colors.HexColor("#E65100")
LIGHT_ORANGE = colors.HexColor("#FFF3E0")
CYAN = colors.HexColor("#006064")
LIGHT_CYAN = colors.HexColor("#E0F7FA")
DARK_GRAY= colors.HexColor("#424242")
MID_GRAY = colors.HexColor("#757575")
LIGHT_GRAY = colors.HexColor("#F5F5F5")
WHITE = colors.white
BLACK = colors.black
# ── Page numbering canvas ─────────────────────────────────────────────────────
def add_page_number(canvas, doc):
canvas.saveState()
canvas.setFont("Helvetica", 8)
canvas.setFillColor(MID_GRAY)
w, h = A4
canvas.drawCentredString(w / 2, 15 * mm,
f"General Physical Examination Signs | Page {doc.page}")
canvas.setStrokeColor(colors.HexColor("#DDDDDD"))
canvas.setLineWidth(0.5)
canvas.line(20 * mm, 20 * mm, w - 20 * mm, 20 * mm)
canvas.restoreState()
# ── Document setup ────────────────────────────────────────────────────────────
doc = SimpleDocTemplate(
OUTPUT,
pagesize=A4,
rightMargin=2*cm, leftMargin=2*cm,
topMargin=2.5*cm, bottomMargin=2.5*cm,
)
styles = getSampleStyleSheet()
# Custom styles
def S(name, **kw):
base = kw.pop("parent", "Normal")
s = ParagraphStyle(name, parent=styles[base], **kw)
return s
cover_title = S("CoverTitle", fontSize=30, textColor=WHITE, alignment=TA_CENTER,
leading=38, fontName="Helvetica-Bold")
cover_sub = S("CoverSub", fontSize=14, textColor=colors.HexColor("#B0D4E8"),
alignment=TA_CENTER, leading=20)
cover_src = S("CoverSrc", fontSize=10, textColor=colors.HexColor("#90CAF9"),
alignment=TA_CENTER)
sign_title = S("SignTitle", fontSize=18, textColor=WHITE,
fontName="Helvetica-Bold", leading=24, alignment=TA_LEFT)
section_h = S("SectionH", fontSize=11, textColor=NAVY,
fontName="Helvetica-Bold", leading=16, spaceAfter=4)
body = S("Body", fontSize=10, textColor=DARK_GRAY,
leading=15, spaceAfter=4, alignment=TA_JUSTIFY)
bullet_style = S("Bullet", fontSize=10, textColor=DARK_GRAY,
leading=14, leftIndent=12, firstLineIndent=-8, spaceAfter=2)
note_style = S("Note", fontSize=9, textColor=colors.HexColor("#555555"),
leading=13, leftIndent=8, fontName="Helvetica-Oblique")
tbl_hdr = S("TblHdr", fontSize=9, textColor=WHITE,
fontName="Helvetica-Bold", alignment=TA_CENTER)
tbl_cell = S("TblCell", fontSize=9, textColor=DARK_GRAY,
leading=12, alignment=TA_LEFT)
tbl_cell_c = S("TblCellC", fontSize=9, textColor=DARK_GRAY,
leading=12, alignment=TA_CENTER)
summary_h = S("SummaryH", fontSize=9, textColor=WHITE,
fontName="Helvetica-Bold", alignment=TA_CENTER)
# ── Helper: colored header banner for each sign ───────────────────────────────
class ColorBanner(Flowable):
def __init__(self, text, bg_color, width=None, height=42):
super().__init__()
self.text = text
self.bg = bg_color
self._w = width or (A4[0] - 4*cm)
self._h = height
def wrap(self, aW, aH):
self._w = aW
return aW, self._h
def draw(self):
c = self.canv
c.setFillColor(self.bg)
c.roundRect(0, 0, self._w, self._h, 6, fill=1, stroke=0)
c.setFillColor(WHITE)
c.setFont("Helvetica-Bold", 18)
c.drawString(14, 12, self.text)
def make_table(headers, rows, col_widths, header_color, row_colors=None):
data = [[Paragraph(h, tbl_hdr) for h in headers]]
for row in rows:
data.append([Paragraph(str(c), tbl_cell) for c in row])
if row_colors is None:
row_colors = [LIGHT_GRAY, WHITE]
ts = TableStyle([
("BACKGROUND", (0, 0), (-1, 0), header_color),
("ROWBACKGROUNDS", (0, 1), (-1, -1), row_colors),
("GRID", (0, 0), (-1, -1), 0.4, colors.HexColor("#CCCCCC")),
("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),
("ROUNDEDCORNERS", [4]),
])
t = Table(data, colWidths=col_widths)
t.setStyle(ts)
return t
def bullet(text):
return Paragraph(f"<b>\u2022</b> {text}", bullet_style)
def note(text):
return Paragraph(f"\u2139\ufe0f {text}", note_style)
def sp(n=6):
return Spacer(1, n)
story = []
W = A4[0] - 4*cm # usable width
# ══════════════════════════════════════════════════════════════════════════════
# COVER PAGE
# ══════════════════════════════════════════════════════════════════════════════
class CoverPage(Flowable):
def wrap(self, aW, aH):
return aW, aH
def draw(self):
c = self.canv
w, h = A4
# Background gradient (simulate with rectangles)
for i in range(30):
frac = i / 30
r = 0.106 + frac * 0.04
g = 0.227 + frac * 0.05
b = 0.420 + frac * 0.08
c.setFillColorRGB(r, g, b)
c.rect(0, h - (i+1)*(h/30), w, h/30, fill=1, stroke=0)
# Accent bar
c.setFillColor(TEAL)
c.rect(0, h*0.38, w, 4, fill=1, stroke=0)
# Title
c.setFillColor(WHITE)
c.setFont("Helvetica-Bold", 34)
c.drawCentredString(w/2, h*0.72, "General Physical")
c.drawCentredString(w/2, h*0.65, "Examination Signs")
# Subtitle
c.setFont("Helvetica", 15)
c.setFillColor(colors.HexColor("#B0D4E8"))
c.drawCentredString(w/2, h*0.58, "Pallor · Icterus · Lymphadenopathy · Cyanosis")
c.drawCentredString(w/2, h*0.54, "Clubbing · Edema · Dehydration")
# Divider
c.setStrokeColor(AMBER)
c.setLineWidth(2)
c.line(w*0.2, h*0.50, w*0.8, h*0.50)
# Source line
c.setFont("Helvetica-Oblique", 10)
c.setFillColor(colors.HexColor("#90CAF9"))
c.drawCentredString(w/2, h*0.45,
"Sources: Harrison's Principles of Internal Medicine 22E (2025),")
c.drawCentredString(w/2, h*0.42,
"Goldman-Cecil Medicine, Brenner & Rector's The Kidney,")
c.drawCentredString(w/2, h*0.39,
"Schwartz's Principles of Surgery, Tintinalli's Emergency Medicine")
# Bottom strip
c.setFillColor(TEAL)
c.rect(0, 0, w, h*0.12, fill=1, stroke=0)
c.setFillColor(WHITE)
c.setFont("Helvetica-Bold", 12)
c.drawCentredString(w/2, h*0.06, "Clinical Medicine Reference")
c.setFont("Helvetica", 9)
c.setFillColor(colors.HexColor("#B0D4E8"))
c.drawCentredString(w/2, h*0.04, "Based on authoritative medical textbooks")
story.append(CoverPage())
story.append(PageBreak())
# ══════════════════════════════════════════════════════════════════════════════
# 1. PALLOR
# ══════════════════════════════════════════════════════════════════════════════
story.append(ColorBanner("1. PALLOR", RED))
story.append(sp(10))
story.append(Paragraph(
"Pallor refers to a reduction in the redness of the skin and mucous membranes, "
"most often reflecting decreased hemoglobin concentration, decreased blood flow, or vasoconstriction.",
body))
story.append(sp(8))
story.append(Paragraph("Best Sites to Assess", section_h))
tbl = make_table(
["Site", "Clinical Significance"],
[
["Conjunctival mucosa", "Most specific — pallor here indicates Hb < 9 g/dL"],
["Palm creases", "Pale creases suggest Hb < 7 g/dL"],
["Nail beds", "Pallor visible with moderate-severe anemia"],
["Tongue / buccal mucosa", "Seen in severe anemia"],
],
[W*0.35, W*0.65], RED, [LIGHT_RED, WHITE]
)
story.append(tbl)
story.append(sp(10))
story.append(Paragraph("Common Causes", section_h))
for c_ in [
"<b>Anemia</b> (most common) — iron deficiency, hemolytic anemia, aplastic anemia, B12/folate deficiency, blood loss",
"<b>Shock / acute blood loss</b> — cutaneous vasoconstriction shunts blood centrally",
"<b>Vasospasm</b> — cold exposure, anxiety, Raynaud's phenomenon",
"<b>Hypothyroidism</b> — myxedema causes waxy pallor",
"<b>Malignancy</b> — leukemia, lymphoma, bone marrow infiltration",
]:
story.append(bullet(c_))
story.append(sp(10))
story.append(Paragraph("Grading of Pallor", section_h))
tbl2 = make_table(
["Grade", "Finding", "Approx. Hb"],
[
["Mild", "Pallor only on close inspection of conjunctiva", "> 9 g/dL"],
["Moderate", "Clear conjunctival and palmar pallor", "7–9 g/dL"],
["Severe", "Pallor of tongue, buccal mucosa, nail beds", "< 7 g/dL"],
],
[W*0.15, W*0.55, W*0.30], RED, [LIGHT_RED, WHITE]
)
story.append(tbl2)
story.append(sp(8))
story.append(note(
'Goldman-Cecil Medicine: "Pallor of the conjunctival mucosa generally indicates '
'a hemoglobin concentration less than 9 g/dL"'
))
story.append(PageBreak())
# ══════════════════════════════════════════════════════════════════════════════
# 2. ICTERUS (JAUNDICE)
# ══════════════════════════════════════════════════════════════════════════════
story.append(ColorBanner("2. ICTERUS (Jaundice)", colors.HexColor("#F57F17")))
story.append(sp(10))
story.append(Paragraph(
"Icterus is a yellow discoloration of the skin, sclera, and mucous membranes due to "
"accumulation of bilirubin (> 2–3 mg/dL in serum). It is first visible in the sclera "
"and should be assessed in natural light.",
body))
story.append(sp(8))
story.append(Paragraph("Classification by Type", section_h))
tbl = make_table(
["Type", "Bilirubin Fraction", "Causes"],
[
["Pre-hepatic\n(Hemolytic)", "Indirect (unconjugated)", "Hemolytic anemia, G6PD deficiency, sickle cell, thalassemia"],
["Hepatic\n(Hepatocellular)", "Mixed", "Viral hepatitis, cirrhosis, drugs, Wilson's disease, alcoholic hepatitis"],
["Post-hepatic\n(Obstructive)", "Direct (conjugated)", "Gallstones, carcinoma head of pancreas, cholangitis, cholestasis"],
],
[W*0.22, W*0.25, W*0.53], colors.HexColor("#F57F17"), [LIGHT_AMBER, WHITE]
)
story.append(tbl)
story.append(sp(10))
story.append(Paragraph("Key Clinical Associations", section_h))
for c_ in [
"<b>Dark urine + pale stools</b> — obstructive (conjugated) jaundice",
"<b>Charcot's triad</b> (fever + rigors + jaundice) — ascending cholangitis",
"<b>Jaundice + raised JVP</b> — congestive hepatomegaly, right heart failure",
"<b>Painless jaundice + palpable gallbladder</b> — Courvoisier's sign (carcinoma head of pancreas)",
"<b>Jaundice + splenomegaly + anemia</b> — hemolytic jaundice",
"<b>Jaundice in newborn</b> — physiological vs. pathological (Rh/ABO incompatibility)",
]:
story.append(bullet(c_))
story.append(sp(8))
story.append(note(
'Harrison\'s: "Jaundice, which may be visible first in the sclerae, has a broad differential '
'diagnosis but, in the appropriate setting, can be consistent with advanced right heart failure '
'and congestive hepatomegaly"'
))
story.append(PageBreak())
# ══════════════════════════════════════════════════════════════════════════════
# 3. LYMPHADENOPATHY
# ══════════════════════════════════════════════════════════════════════════════
story.append(ColorBanner("3. LYMPHADENOPATHY", PURPLE))
story.append(sp(10))
story.append(Paragraph(
"Pathological enlargement of lymph nodes. Normal nodes are < 1 cm (< 1.5 cm in the inguinal region). "
"Key initial assessment: generalized vs. localized, size, consistency, tenderness, fixation, "
"and overlying skin changes.",
body))
story.append(sp(8))
story.append(Paragraph("Causes by Region", section_h))
tbl = make_table(
["Region", "Common Causes"],
[
["Cervical", "EBV (mononucleosis), TB, oral/pharyngeal infections, lymphoma, thyroid cancer"],
["Axillary", "Breast cancer, cat-scratch disease, melanoma, local infections"],
["Inguinal", "STIs (syphilis, chancroid), lower limb infections, lymphoma"],
["Mediastinal", "Sarcoidosis, lymphoma, TB, lung cancer, histoplasmosis"],
["Generalized", "HIV, EBV, CMV, SLE, sarcoidosis, leukemia, lymphoma, drug reactions"],
],
[W*0.22, W*0.78], PURPLE, [LIGHT_PURPLE, WHITE]
)
story.append(tbl)
story.append(sp(10))
story.append(Paragraph("Red Flag Features (biopsy indicated)", section_h))
for c_ in [
"Firm/hard, non-tender, fixed to underlying structures",
"Size > 2 cm, especially if progressive",
"<b>Supraclavicular location</b> — always sinister (Virchow's node / Troisier's sign)",
"Associated constitutional symptoms: fever, night sweats, weight loss (B symptoms of lymphoma)",
"Duration > 6 weeks without obvious cause",
]:
story.append(bullet(c_))
story.append(sp(10))
story.append(Paragraph("Node Characteristics", section_h))
tbl2 = make_table(
["Feature", "Likely Benign", "Likely Malignant"],
[
["Consistency", "Soft, rubbery", "Hard, stony"],
["Tenderness", "Tender (infection)", "Usually non-tender"],
["Mobility", "Mobile", "Fixed, matted"],
["Skin", "Normal", "Erythema, sinus tract"],
],
[W*0.25, W*0.375, W*0.375], PURPLE, [LIGHT_PURPLE, WHITE]
)
story.append(tbl2)
story.append(sp(8))
story.append(note(
'Harrison\'s: "Determining whether the patient has generalized versus localized lymphadenopathy '
'can help narrow the differential diagnosis, as various infections present differently"'
))
story.append(PageBreak())
# ══════════════════════════════════════════════════════════════════════════════
# 4. CYANOSIS
# ══════════════════════════════════════════════════════════════════════════════
story.append(ColorBanner("4. CYANOSIS", NAVY))
story.append(sp(10))
story.append(Paragraph(
"Cyanosis is a bluish discoloration of skin and mucous membranes due to increased "
"deoxyhemoglobin (> 5 g/dL in capillaries). It requires at least 5 g/dL of reduced Hb "
"to be clinically visible. <b>Note:</b> In severe anemia, cyanosis may be absent even "
"with very low O2 saturation because total Hb is too low.",
body))
story.append(sp(8))
story.append(Paragraph("Types of Cyanosis", section_h))
tbl = make_table(
["Type", "Location", "Mechanism", "Causes"],
[
["Central",
"Tongue, lips, oral mucosa (warm areas)",
"Arterial desaturation or R-to-L shunting",
"COPD, pneumonia, pulmonary edema, cyanotic CHD (Fallot's, Eisenmenger's), pulmonary AV malformation"],
["Peripheral\n(Acrocyanosis)",
"Fingers, toes, ears, nose",
"Reduced extremity blood flow, increased O2 extraction",
"Heart failure, shock, peripheral vascular disease, cold exposure, Raynaud's"],
["Differential\nCyanosis",
"Lower limbs only (not upper limbs)",
"PDA with pulmonary HTN, R-to-L shunt at great vessel level",
"Large patent ductus arteriosus + Eisenmenger syndrome"],
["Peripheral only\n(no central)",
"Extremities, spares tongue",
"Venous stasis / vasoconstriction",
"Cold, beta-blockers with unopposed alpha, venous occlusion"],
],
[W*0.18, W*0.22, W*0.28, W*0.32], NAVY, [LIGHTBLUE, WHITE]
)
story.append(tbl)
story.append(sp(10))
story.append(Paragraph("Key Rule", section_h))
story.append(bullet(
"<b>Central cyanosis</b> — affects the tongue and warm mucous membranes; "
"implies arterial desaturation; always pathological"))
story.append(bullet(
"<b>Peripheral cyanosis</b> — spares the tongue; may be benign (cold) or pathological"))
story.append(bullet(
"Check SaO2 with pulse oximetry to confirm; however, methemoglobinemia gives "
"falsely high/normal pulse ox reading"))
story.append(sp(8))
story.append(note(
'Harrison\'s: "Central cyanosis occurs with significant right-to-left shunting at the level '
'of the heart or lungs... Peripheral cyanosis or acrocyanosis is usually related to reduced '
'extremity blood flow due to small vessel constriction"'
))
story.append(PageBreak())
# ══════════════════════════════════════════════════════════════════════════════
# 5. CLUBBING
# ══════════════════════════════════════════════════════════════════════════════
story.append(ColorBanner("5. CLUBBING", TEAL))
story.append(sp(10))
story.append(Paragraph(
"Clubbing is the bulbous enlargement of distal fingers and toes due to proliferation of "
"connective tissue on the dorsal surface, with increased sponginess at the nail base. "
"The mechanism involves humoral substances causing dilation of distal digital vessels "
"and growth factors from platelet precursors in digital circulation.",
body))
story.append(sp(8))
story.append(Paragraph("Stages / Clinical Features", section_h))
tbl = make_table(
["Stage", "Finding"],
[
["Stage 1", "Increased fluctuation/sponginess of nail bed"],
["Stage 2", "Loss of normal angle between nail base and skin (Lovibond angle > 180 deg)"],
["Stage 3", "Drumstick / parrot-beak appearance; nail curves over the fingertip"],
["Stage 4", "Hypertrophic osteoarthropathy — periosteal new bone, painful joints in shoulders, knees, wrists"],
],
[W*0.15, W*0.85], TEAL, [LIGHT_CYAN, WHITE]
)
story.append(tbl)
story.append(sp(10))
story.append(Paragraph("Causes", section_h))
tbl2 = make_table(
["System", "Examples"],
[
["Respiratory (most common)", "Lung cancer, bronchiectasis, lung abscess, cystic fibrosis, TB, mesothelioma, sarcoidosis, asbestosis"],
["Cardiac", "Cyanotic congenital heart disease, infective endocarditis"],
["Gastrointestinal", "Inflammatory bowel disease (Crohn's > UC), hepatic cirrhosis, celiac disease"],
["Hereditary / Idiopathic", "Primary hypertrophic osteoarthropathy (pachydermoperiostosis)"],
["Occupational", "Jackhammer operators (vibration)"],
],
[W*0.30, W*0.70], TEAL, [LIGHT_CYAN, WHITE]
)
story.append(tbl2)
story.append(sp(10))
story.append(Paragraph("Schamroth's Window Test", section_h))
story.append(Paragraph(
"Place dorsal surfaces of the same fingers of each hand together. "
"Normally, a diamond-shaped window is visible at the base of the nails. "
"<b>Loss of this window = positive test (clubbing present).</b>",
body))
story.append(sp(8))
story.append(note(
'Harrison\'s: "Clubbing may be hereditary, idiopathic, or acquired and associated with '
'a variety of disorders, including cyanotic congenital heart disease, infective endocarditis, '
'and a variety of pulmonary conditions... In certain circumstances, clubbing is reversible, '
'such as following lung transplantation for cystic fibrosis"'
))
story.append(PageBreak())
# ══════════════════════════════════════════════════════════════════════════════
# 6. EDEMA
# ══════════════════════════════════════════════════════════════════════════════
story.append(ColorBanner("6. EDEMA", GREEN))
story.append(sp(10))
story.append(Paragraph(
"Edema is clinically evident excess of interstitial fluid. Approximately 4–5 liters of "
"excess fluid must accumulate before pitting edema becomes palpable. It represents an "
"imbalance between forces promoting fluid movement into the interstitium and those "
"promoting its return to the vasculature.",
body))
story.append(sp(8))
story.append(Paragraph("Mechanisms", section_h))
for i, c_ in enumerate([
"Increased capillary hydrostatic pressure — heart failure, venous obstruction, portal hypertension",
"Decreased plasma oncotic pressure — hypoalbuminemia (nephrotic syndrome, cirrhosis, malnutrition, protein-losing enteropathy)",
"Increased capillary permeability — inflammation, anaphylaxis, burns, sepsis, angioedema",
"Lymphatic obstruction — lymphoedema, filariasis, malignant obstruction, post-surgical",
"Na+/water retention — renal failure, hyperaldosteronism, RAAS activation",
], start=1):
story.append(bullet(f"<b>{i}.</b> {c_}"))
story.append(sp(10))
story.append(Paragraph("Types of Edema", section_h))
tbl = make_table(
["Type", "Features", "Causes"],
[
["Pitting", "Pit remains after finger pressure", "Cardiac, renal, hepatic, nutritional"],
["Non-pitting", "No pit (firm / brawny)", "Hypothyroidism (myxedema), lymphedema"],
["Dependent", "Feet/ankles in ambulatory; sacrum in bedridden", "Right heart failure (most common)"],
["Periorbital", "Around eyes, worse in morning", "Nephrotic syndrome, hypothyroidism"],
["Anasarca", "Generalized body edema including ascites", "Severe hypoalbuminemia, advanced heart failure"],
["Pulmonary", "Crepitations at lung bases, orthopnoea", "Left heart failure, ARDS"],
],
[W*0.18, W*0.40, W*0.42], GREEN, [LIGHT_GREEN, WHITE]
)
story.append(tbl)
story.append(sp(10))
story.append(Paragraph("Grading of Pitting Edema", section_h))
tbl2 = make_table(
["Grade", "Pit Depth", "Recovery Time"],
[
["1+", "2 mm", "< 2 seconds"],
["2+", "4 mm", "2–5 seconds"],
["3+", "6 mm", "5–30 seconds"],
["4+", "> 8 mm", "> 30 seconds"],
],
[W*0.20, W*0.40, W*0.40], GREEN, [LIGHT_GREEN, WHITE]
)
story.append(tbl2)
story.append(sp(8))
story.append(note(
'Harrison\'s: "Edema represents an excess of interstitial fluid that has become evident '
'clinically." — Elevated JVP + bilateral dependent edema = right heart failure etiology'
))
story.append(PageBreak())
# ══════════════════════════════════════════════════════════════════════════════
# 7. DEHYDRATION
# ══════════════════════════════════════════════════════════════════════════════
story.append(ColorBanner("7. DEHYDRATION", ORANGE))
story.append(sp(10))
story.append(Paragraph(
"Dehydration is a deficit of total body water, most commonly from inadequate intake "
"or excess losses (vomiting, diarrhea, burns, excessive sweating, diabetes insipidus). "
"Clinical assessment relies on a combination of signs, as no single sign is perfectly reliable.",
body))
story.append(sp(8))
story.append(Paragraph("Clinical Signs by Severity", section_h))
tbl = make_table(
["Severity", "% Body Wt Loss", "Signs & Symptoms"],
[
["Mild", "< 5%", "Thirst, dry mouth, slightly decreased urine output"],
["Moderate", "5–10%", "Tachycardia, reduced skin turgor, sunken eyes, dry mucous membranes, oliguria, irritability"],
["Severe", "> 10%", "Altered sensorium/confusion, hypotension, absent urine output, delayed capillary refill (> 2 sec), shock"],
],
[W*0.15, W*0.18, W*0.67], ORANGE, [LIGHT_ORANGE, WHITE]
)
story.append(tbl)
story.append(sp(10))
story.append(Paragraph("Most Reliable Clinical Signs (Evidence-Based)", section_h))
for c_ in [
"<b>Delayed capillary refill time</b> (> 2 sec) — most reliable single sign",
"<b>Reduced skin turgor / tenting</b> — assessed over abdomen/chest; less reliable in elderly",
"<b>Dry mucous membranes</b> — tongue and oral mucosa",
"<b>Sunken fontanelle</b> — in infants",
"<b>Abnormal respiratory pattern</b> — Kussmaul breathing in metabolic acidosis from severe dehydration",
"<b>Tachycardia</b> — earliest cardiovascular sign",
]:
story.append(bullet(c_))
story.append(sp(10))
story.append(Paragraph("Types of Dehydration", section_h))
tbl2 = make_table(
["Type", "Serum Na", "Cause"],
[
["Isotonic", "Normal (135–145 mEq/L)", "Diarrhea, vomiting (most common)"],
["Hypertonic", "> 145 mEq/L", "Diabetes insipidus, inadequate free water intake, fever with insensible losses"],
["Hypotonic", "< 135 mEq/L", "Adrenal insufficiency, excessive hypotonic fluid losses, over-correction"],
],
[W*0.18, W*0.25, W*0.57], ORANGE, [LIGHT_ORANGE, WHITE]
)
story.append(tbl2)
story.append(sp(8))
story.append(note(
'Brenner & Rector\'s The Kidney: "Delayed capillary refill time, reduced skin turgor, and '
'deep respirations were the most useful clinical signs [of dehydration]"'
))
story.append(PageBreak())
# ══════════════════════════════════════════════════════════════════════════════
# SUMMARY TABLE
# ══════════════════════════════════════════════════════════════════════════════
story.append(ColorBanner("QUICK SUMMARY TABLE", DARK_GRAY))
story.append(sp(12))
summary_data = [
[Paragraph("<b>Sign</b>", tbl_hdr), Paragraph("<b>Key Site</b>", tbl_hdr),
Paragraph("<b>Core Cause Group</b>", tbl_hdr), Paragraph("<b>Key Associations</b>", tbl_hdr)],
[Paragraph("Pallor", tbl_cell), Paragraph("Conjunctiva, palm creases", tbl_cell),
Paragraph("Anemia, shock", tbl_cell), Paragraph("Hb < 9 g/dL (conjunctival)", tbl_cell)],
[Paragraph("Icterus", tbl_cell), Paragraph("Sclera first", tbl_cell),
Paragraph("Pre/intra/post-hepatic", tbl_cell), Paragraph("Dark urine = conjugated; pale stools", tbl_cell)],
[Paragraph("Lymphadenopathy", tbl_cell), Paragraph("All nodal groups", tbl_cell),
Paragraph("Infection, malignancy, autoimmune", tbl_cell), Paragraph("Supraclavicular = always sinister", tbl_cell)],
[Paragraph("Cyanosis", tbl_cell), Paragraph("Tongue (central), fingers (peripheral)", tbl_cell),
Paragraph("Cardiopulmonary, R-L shunt", tbl_cell), Paragraph("Central > peripheral in severity", tbl_cell)],
[Paragraph("Clubbing", tbl_cell), Paragraph("Fingers/toes (dorsal surface)", tbl_cell),
Paragraph("Lung, heart, GI, hereditary", tbl_cell), Paragraph("Schamroth's sign for detection", tbl_cell)],
[Paragraph("Edema", tbl_cell), Paragraph("Dependent areas, periorbital", tbl_cell),
Paragraph("Cardiac, renal, hepatic, nutritional", tbl_cell), Paragraph("Pitting vs. non-pitting key distinction", tbl_cell)],
[Paragraph("Dehydration", tbl_cell), Paragraph("Skin turgor, mucous membranes", tbl_cell),
Paragraph("Fluid loss / inadequate intake", tbl_cell), Paragraph("Capillary refill most reliable sign", tbl_cell)],
]
row_bg = [
LIGHT_RED, LIGHT_AMBER, LIGHT_PURPLE, LIGHTBLUE,
LIGHT_CYAN, LIGHT_GREEN, LIGHT_ORANGE
]
ts = TableStyle([
("BACKGROUND", (0, 0), (-1, 0), DARK_GRAY),
("GRID", (0, 0), (-1, -1), 0.4, colors.HexColor("#BBBBBB")),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("TOPPADDING", (0, 0), (-1, -1), 6),
("BOTTOMPADDING", (0, 0), (-1, -1), 6),
("LEFTPADDING", (0, 0), (-1, -1), 6),
("RIGHTPADDING", (0, 0), (-1, -1), 6),
])
for i, bg in enumerate(row_bg, start=1):
ts.add("BACKGROUND", (0, i), (-1, i), bg)
summary_tbl = Table(summary_data, colWidths=[W*0.18, W*0.20, W*0.28, W*0.34])
summary_tbl.setStyle(ts)
story.append(summary_tbl)
story.append(sp(16))
# References
story.append(HRFlowable(width="100%", thickness=1, color=colors.HexColor("#CCCCCC")))
story.append(sp(8))
story.append(Paragraph("<b>References</b>", section_h))
refs = [
"Harrison's Principles of Internal Medicine, 22nd Edition (2025) — McGraw Hill Medical",
"Goldman-Cecil Medicine, International Edition, 2-Volume Set — Elsevier",
"Brenner and Rector's The Kidney, 2-Volume Set — Elsevier",
"Schwartz's Principles of Surgery, 11th Edition — McGraw Hill",
"Tintinalli's Emergency Medicine: A Comprehensive Study Guide — McGraw Hill",
"Barash, Cullen, and Stoelting's Clinical Anesthesia, 9th Edition",
]
for r in refs:
story.append(Paragraph(f"• {r}", note_style))
# ── Build ─────────────────────────────────────────────────────────────────────
doc.build(story, onFirstPage=add_page_number, onLaterPages=add_page_number)
print(f"PDF created: {OUTPUT}")