~/cbc_report/generate_cbc_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
)
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
from reportlab.pdfbase import pdfmetrics
OUTPUT = "/home/daytona/workspace/cbc_report/CBC_Report_Interpretation.pdf"
doc = SimpleDocTemplate(
OUTPUT,
pagesize=A4,
rightMargin=2*cm, leftMargin=2*cm,
topMargin=2*cm, bottomMargin=2*cm,
title="CBC Report Interpretation",
author="Orris Medical Assistant"
)
W = A4[0] - 4*cm # usable width
styles = getSampleStyleSheet()
# Custom styles
title_style = ParagraphStyle(
"Title", parent=styles["Title"],
fontSize=22, textColor=colors.HexColor("#1a3a5c"),
spaceAfter=4, alignment=TA_CENTER, fontName="Helvetica-Bold"
)
subtitle_style = ParagraphStyle(
"Subtitle", parent=styles["Normal"],
fontSize=11, textColor=colors.HexColor("#4a6fa5"),
spaceAfter=14, alignment=TA_CENTER, fontName="Helvetica"
)
h1_style = ParagraphStyle(
"H1", parent=styles["Heading1"],
fontSize=13, textColor=colors.white,
fontName="Helvetica-Bold", spaceAfter=0, spaceBefore=10,
leftIndent=0
)
h2_style = ParagraphStyle(
"H2", parent=styles["Heading2"],
fontSize=11, textColor=colors.HexColor("#1a3a5c"),
fontName="Helvetica-Bold", spaceAfter=4, spaceBefore=8,
leftIndent=0
)
body_style = ParagraphStyle(
"Body", parent=styles["Normal"],
fontSize=9.5, leading=14, textColor=colors.HexColor("#2c2c2c"),
spaceAfter=4, alignment=TA_JUSTIFY
)
bullet_style = ParagraphStyle(
"Bullet", parent=styles["Normal"],
fontSize=9, leading=13, textColor=colors.HexColor("#2c2c2c"),
leftIndent=14, bulletIndent=4, spaceAfter=2
)
source_style = ParagraphStyle(
"Source", parent=styles["Normal"],
fontSize=7.5, textColor=colors.HexColor("#777777"),
spaceAfter=2, fontName="Helvetica-Oblique"
)
pearl_style = ParagraphStyle(
"Pearl", parent=styles["Normal"],
fontSize=9, leading=13, textColor=colors.HexColor("#1a3a5c"),
leftIndent=10, spaceAfter=2, fontName="Helvetica-Oblique"
)
def section_header(title):
"""Returns a blue-background section header row."""
tbl = Table([[Paragraph(title, h1_style)]], colWidths=[W])
tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), colors.HexColor("#1a3a5c")),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING", (0,0), (-1,-1), 8),
("ROUNDEDCORNERS", [4,4,4,4]),
]))
return tbl
def make_table(headers, rows, col_widths=None):
data = [[Paragraph(f"<b>{h}</b>", ParagraphStyle("TH", parent=styles["Normal"],
fontSize=9, textColor=colors.white, fontName="Helvetica-Bold")) for h in headers]]
for row in rows:
data.append([Paragraph(str(cell), ParagraphStyle("TD", parent=styles["Normal"],
fontSize=8.5, leading=12, textColor=colors.HexColor("#2c2c2c"))) for cell in row])
tbl = Table(data, colWidths=col_widths or [W/len(headers)]*len(headers), repeatRows=1)
tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), colors.HexColor("#2d6a9f")),
("ROWBACKGROUNDS", (0,1), (-1,-1), [colors.HexColor("#eef4fb"), colors.white]),
("GRID", (0,0), (-1,-1), 0.4, colors.HexColor("#b0c4de")),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 6),
("RIGHTPADDING", (0,0), (-1,-1), 6),
("VALIGN", (0,0), (-1,-1), "TOP"),
]))
return tbl
# ─── CONTENT ───────────────────────────────────────────────────────────────
story = []
# Title block
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("Complete Blood Count (CBC)", title_style))
story.append(Paragraph("A Comprehensive Guide to Report Interpretation", subtitle_style))
story.append(HRFlowable(width=W, thickness=2, color=colors.HexColor("#2d6a9f"), spaceAfter=10))
# Intro
story.append(Paragraph(
"A CBC is one of the most commonly ordered laboratory test panels. It measures circulating "
"blood cells — red blood cells (RBCs), white blood cells (WBCs), and platelets — using "
"automated flow cytometry-based analyzers. The results provide critical diagnostic information "
"across a wide range of clinical conditions.",
body_style
))
story.append(Spacer(1, 0.3*cm))
# ── SECTION 1: RBC ────────────────────────────────────────────────────────
story.append(section_header("1. RED BLOOD CELL (RBC) PARAMETERS"))
story.append(Spacer(1, 0.2*cm))
story.append(Paragraph("Hemoglobin (Hb)", h2_style))
story.append(Paragraph(
"Primary measure of the blood's oxygen-carrying capacity.",
body_style
))
hb_table = make_table(
["Group", "Normal Range"],
[["Males", "13.5 – 17.5 g/dL"],
["Females", "12.0 – 16.0 g/dL"]],
col_widths=[W*0.4, W*0.6]
)
story.append(hb_table)
story.append(Spacer(1, 0.2*cm))
story.append(Paragraph("• <b>Low Hb (Anemia):</b> Blood loss, iron/B12/folate deficiency, chronic disease, hemolysis, bone marrow disorders, sickle cell disease.", bullet_style))
story.append(Paragraph("• <b>High Hb (Polycythemia):</b> Polycythemia vera, chronic hypoxia, high altitude, erythropoietin-secreting tumors.", bullet_style))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("Hematocrit (Hct) / Packed Cell Volume (PCV)", h2_style))
story.append(Paragraph("Percentage of blood volume occupied by RBCs. Roughly 3× the Hb value.", body_style))
hct_table = make_table(
["Group", "Normal Range"],
[["Males", "39 – 49%"],
["Females", "35 – 45%"]],
col_widths=[W*0.4, W*0.6]
)
story.append(hct_table)
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("RBC Count", h2_style))
rbc_table = make_table(
["Group", "Normal Range"],
[["Males", "4.5 – 6.0 × 10⁶/μL"],
["Females", "4.0 – 5.4 × 10⁶/μL"]],
col_widths=[W*0.4, W*0.6]
)
story.append(rbc_table)
story.append(Spacer(1, 0.4*cm))
# ── SECTION 2: RED CELL INDICES ────────────────────────────────────────────
story.append(section_header("2. RED CELL INDICES (Key for Anemia Classification)"))
story.append(Spacer(1, 0.2*cm))
story.append(Paragraph(
"These calculated values determine the <i>type</i> of anemia and significantly narrow the differential diagnosis.",
body_style
))
story.append(Spacer(1, 0.2*cm))
story.append(Paragraph("MCV — Mean Corpuscular Volume | Normal: 81–99 fL", h2_style))
story.append(Paragraph("The most important index. Classifies anemia into three types:", body_style))
mcv_table = make_table(
["MCV", "Classification", "Common Causes"],
[["< 80 fL", "Microcytic", "Iron deficiency (most common), thalassemia, sideroblastic anemia, chronic disease"],
["80–100 fL", "Normocytic", "Acute blood loss, hemolytic anemia, renal insufficiency, anemia of chronic disease, mixed deficiency"],
["> 100 fL", "Macrocytic", "Vit B12 deficiency, folate deficiency, alcohol, liver disease, hypothyroidism, hydroxyurea, methotrexate"]],
col_widths=[W*0.15, W*0.2, W*0.65]
)
story.append(mcv_table)
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("MCH — Mean Corpuscular Hemoglobin | Normal: 30–34 pg", h2_style))
story.append(Paragraph("Amount of Hb in an average RBC. Trends with MCV.", body_style))
story.append(Paragraph("• <b>Low MCH (Hypochromic):</b> Iron deficiency, thalassemia.", bullet_style))
story.append(Paragraph("• <b>High MCH (Hyperchromic):</b> Hereditary spherocytosis.", bullet_style))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("MCHC — Mean Corpuscular Hb Concentration | Normal: 30–36 g/dL", h2_style))
story.append(Paragraph("Hb concentration per unit of RBC volume.", body_style))
story.append(Paragraph("• <b>Low MCHC:</b> Iron deficiency, thalassemia.", bullet_style))
story.append(Paragraph("• <b>High MCHC:</b> Hereditary spherocytosis (key diagnostic clue).", bullet_style))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("RDW — RBC Distribution Width | Normal: 12–15% (CV) / 37–47 fL (SD)", h2_style))
story.append(Paragraph("Measures variation in RBC size (anisocytosis). Useful for distinguishing anemias:", body_style))
story.append(Paragraph("• <b>High RDW + Low MCV:</b> Iron deficiency anemia (thalassemia trait typically has normal RDW).", bullet_style))
story.append(Paragraph("• <b>High RDW + High MCV:</b> B12 or folate deficiency.", bullet_style))
story.append(Spacer(1, 0.4*cm))
# ── SECTION 3: WBC ────────────────────────────────────────────────────────
story.append(section_header("3. WHITE BLOOD CELL (WBC) COUNT & DIFFERENTIAL"))
story.append(Spacer(1, 0.2*cm))
story.append(Paragraph("Total WBC Count | Normal: 4,000 – 10,000 cells/mm³", h2_style))
story.append(Paragraph("• <b>Leukocytosis (WBC > 10,000/mm³):</b> Infection, inflammation, burns, trauma, pregnancy, steroids. If WBC > 100,000/mm³ (hyperleukocytosis), strongly suspect leukemia.", bullet_style))
story.append(Paragraph("• <b>Leukopenia (WBC < 4,000/mm³):</b> Chemotherapy/radiation, aplastic anemia, HIV/AIDS, autoimmune disease, antipsychotics, antiepileptics.", bullet_style))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("5-Part WBC Differential", h2_style))
diff_table = make_table(
["Cell Type", "Normal %", "Elevated (Causes)", "Decreased (Causes)"],
[["Neutrophils", "50–70%", "Bacterial infections, trauma, leukemia, steroids, rheumatic disease", "Overwhelming infection, viral illness, drug reactions, aplastic anemia, autoimmune"],
["Lymphocytes", "20–40%", "Viral infections (EBV, CMV), CLL, pertussis", "HIV/immunodeficiency, steroids, chemotherapy, irradiation"],
["Monocytes", "2–8%", "TB, SBE, sarcoidosis, IBD, recovering marrow", "Steroid therapy, hairy cell leukemia"],
["Eosinophils", "1–4%", "Parasites, allergic diseases, asthma, drug reactions, Hodgkin's", "Acute stress, Cushing's syndrome, corticosteroids"],
["Basophils", "0–1%", "CML, allergic reactions, polycythemia vera", "Corticosteroid use, acute infection, hyperthyroidism"]],
col_widths=[W*0.14, W*0.1, W*0.38, W*0.38]
)
story.append(diff_table)
story.append(Spacer(1, 0.2*cm))
story.append(Paragraph(
"⚑ 'Left shift' = increased band cells (immature neutrophils) — indicates active bacterial infection or bone marrow stress.",
pearl_style
))
story.append(Paragraph(
"⚑ Absolute Neutrophil Count (ANC) < 1,500/mm³ = neutropenia (significant infection risk).",
pearl_style
))
story.append(Spacer(1, 0.4*cm))
# ── SECTION 4: PLATELETS ──────────────────────────────────────────────────
story.append(section_header("4. PLATELETS"))
story.append(Spacer(1, 0.2*cm))
story.append(Paragraph("Platelet Count | Normal: 150,000 – 400,000/μL", h2_style))
plt_table = make_table(
["Range", "Severity", "Clinical Implication"],
[["50,000–150,000/μL", "Mild thrombocytopenia", "Bleeding uncommon unless trauma or surgery"],
["10,000–50,000/μL", "Moderate", "Bleeding risk with trauma; consider transfusion pre-procedure"],
["< 10,000–20,000/μL", "Severe", "Risk of spontaneous major bleeding; urgent management"]],
col_widths=[W*0.28, W*0.24, W*0.48]
)
story.append(plt_table)
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("Causes of Thrombocytopenia", h2_style))
cause_table = make_table(
["Category", "Causes"],
[["Decreased production", "Bone marrow suppression, leukemia, aplastic anemia, B12/folate deficiency, chemotherapy, viral infections"],
["Increased destruction", "ITP, DIC, TTP/HUS, drug-induced (heparin-HIT), SLE"],
["Sequestration", "Hypersplenism (liver disease, portal hypertension)"]],
col_widths=[W*0.28, W*0.72]
)
story.append(cause_table)
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("Thrombocytosis (Platelets > 400,000/μL)", h2_style))
story.append(Paragraph("• <b>Reactive:</b> Iron deficiency, infection, inflammation, post-splenectomy.", bullet_style))
story.append(Paragraph("• <b>Primary (Essential thrombocythemia):</b> Myeloproliferative disorder — requires further workup.", bullet_style))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("Mean Platelet Volume (MPV)", h2_style))
story.append(Paragraph("• <b>High MPV:</b> Young, active platelets — seen in ITP (compensatory production).", bullet_style))
story.append(Paragraph("• <b>Low MPV:</b> Wiskott-Aldrich syndrome, bone marrow failure.", bullet_style))
story.append(Spacer(1, 0.4*cm))
# ── SECTION 5: RETICULOCYTE ───────────────────────────────────────────────
story.append(section_header("5. RETICULOCYTE COUNT"))
story.append(Spacer(1, 0.2*cm))
story.append(Paragraph("Reticulocytes are immature RBCs released from the bone marrow. This parameter separates hypoproliferative from hyperproliferative anemias.", body_style))
retic_table = make_table(
["Parameter", "Normal Range"],
[["Reticulocyte %", "0.5 – 1.5%"],
["Absolute reticulocyte count", "20,000 – 100,000/μL"],
["Reticulocyte Production Index (RPI)", "0.5 – 2.5"]],
col_widths=[W*0.5, W*0.5]
)
story.append(retic_table)
story.append(Spacer(1, 0.2*cm))
story.append(Paragraph("• <b>RPI < 2 in anemia:</b> Bone marrow not responding (hypoproliferative) — iron/B12/folate deficiency, aplastic anemia, anemia of chronic disease.", bullet_style))
story.append(Paragraph("• <b>RPI > 3 in anemia:</b> Bone marrow responding appropriately (hyperproliferative) — hemolysis or acute blood loss.", bullet_style))
story.append(Spacer(1, 0.4*cm))
# ── SECTION 6: BLOOD SMEAR ───────────────────────────────────────────────
story.append(section_header("6. PERIPHERAL BLOOD SMEAR FINDINGS"))
story.append(Spacer(1, 0.2*cm))
story.append(Paragraph("Indicated when automated analyzers flag abnormal results. Key morphological findings:", body_style))
smear_table = make_table(
["Finding", "Significance"],
[["Target cells (codocytes)", "Thalassemia, liver disease, HbC disease"],
["Schistocytes (fragmented RBCs)", "TTP, DIC, mechanical hemolysis (prosthetic valves)"],
["Howell-Jolly bodies", "Functional or anatomic asplenia"],
["Hypersegmented neutrophils", "Vitamin B12 or folate deficiency (megaloblastic anemia)"],
["Spherocytes", "Hereditary spherocytosis, autoimmune hemolytic anemia"],
["Sickle cells", "Sickle cell disease"],
["Tear-drop cells (dacrocytes)", "Myelofibrosis"],
["Large vacuoles in neutrophils", "Chediak-Higashi syndrome"],
["Small platelets", "Wiskott-Aldrich syndrome"]],
col_widths=[W*0.38, W*0.62]
)
story.append(smear_table)
story.append(Spacer(1, 0.4*cm))
# ── SECTION 7: QUICK SUMMARY ─────────────────────────────────────────────
story.append(section_header("7. QUICK INTERPRETATION SUMMARY"))
story.append(Spacer(1, 0.2*cm))
summary_table = make_table(
["Parameter", "If LOW", "If HIGH"],
[["Hb / Hct", "Anemia", "Polycythemia"],
["MCV", "Microcytic anemia (iron, thalassemia)", "Macrocytic anemia (B12, folate, alcohol)"],
["RDW", "(rarely significant)", "Anisocytosis — iron deficiency, mixed deficiency"],
["WBC (Total)", "Leukopenia (viral, drugs, immunosuppression)", "Leukocytosis (infection, inflammation, leukemia)"],
["Neutrophils", "Neutropenia — bacterial infection risk", "Neutrophilia — bacterial infection, steroids"],
["Lymphocytes", "Immunodeficiency, HIV, steroids", "Viral infection, CLL"],
["Eosinophils", "Corticosteroids, acute stress", "Parasites, allergy, asthma"],
["Platelets", "Thrombocytopenia — bleeding risk", "Thrombocytosis (reactive or myeloproliferative)"]],
col_widths=[W*0.2, W*0.4, W*0.4]
)
story.append(summary_table)
story.append(Spacer(1, 0.5*cm))
# ── SOURCES ──────────────────────────────────────────────────────────────
story.append(HRFlowable(width=W, thickness=0.8, color=colors.HexColor("#b0c4de"), spaceAfter=6))
story.append(Paragraph("<b>Sources</b>", ParagraphStyle("SrcHead", parent=styles["Normal"], fontSize=8.5, fontName="Helvetica-Bold", textColor=colors.HexColor("#444444"))))
story.append(Paragraph("Histology: A Text and Atlas with Correlated Cell and Molecular Biology (Pawlina), pp. 728–730", source_style))
story.append(Paragraph("Textbook of Family Medicine, 9th Edition, p. 227", source_style))
story.append(Paragraph("Goldman-Cecil Medicine, International Edition — Tables 144-2, 144-3", source_style))
story.append(Paragraph("Henry's Clinical Diagnosis and Management by Laboratory Methods", source_style))
# ── BUILD ─────────────────────────────────────────────────────────────────
doc.build(story)
print(f"PDF saved to: {OUTPUT}")