~/periodontal-flashcards/make_flashcards.py
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import mm
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, KeepTogether
)
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
from reportlab.platypus import PageBreak
OUTPUT = "/tmp/workspace/periodontal-flashcards/Periodontal_Histology_Flashcards.pdf"
# ── Colour palette ──────────────────────────────────────────────────
C_DARK_TEAL = colors.HexColor("#0D4F5C") # header bg
C_TEAL = colors.HexColor("#1A7A8A") # card header band
C_STAGE1 = colors.HexColor("#2E86AB") # Stage 1 – blue
C_STAGE2 = colors.HexColor("#4CAF82") # Stage 2 – green
C_STAGE3 = colors.HexColor("#E67E22") # Stage 3 – orange (gingivitis)
C_STAGE4 = colors.HexColor("#C0392B") # Stage 4 – red (periodontitis)
C_LIGHT_BLUE = colors.HexColor("#EAF6FB")
C_LIGHT_GREEN = colors.HexColor("#EAFAF1")
C_LIGHT_ORANGE= colors.HexColor("#FEF9E7")
C_LIGHT_RED = colors.HexColor("#FDEDEC")
C_LIGHT_TEAL = colors.HexColor("#E8F6F8")
C_GOLD = colors.HexColor("#F1C40F")
C_WHITE = colors.white
C_DARK = colors.HexColor("#1C1C1C")
C_GREY = colors.HexColor("#666666")
C_LIGHT_GREY = colors.HexColor("#F5F5F5")
C_MID_GREY = colors.HexColor("#DDDDDD")
W, H = A4 # 595.27 x 841.89 pt
styles = getSampleStyleSheet()
def ps(name, **kw):
return ParagraphStyle(name, **kw)
# ── custom styles ────────────────────────────────────────────────────
COVER_TITLE = ps("CoverTitle", fontName="Helvetica-Bold",
fontSize=28, textColor=C_WHITE, alignment=TA_CENTER, leading=34)
COVER_SUB = ps("CoverSub", fontName="Helvetica",
fontSize=14, textColor=colors.HexColor("#AADDE6"),
alignment=TA_CENTER, leading=20)
COVER_NOTE = ps("CoverNote", fontName="Helvetica-Oblique",
fontSize=10, textColor=colors.HexColor("#CCCCCC"),
alignment=TA_CENTER)
CARD_TITLE = ps("CardTitle", fontName="Helvetica-Bold",
fontSize=13, textColor=C_WHITE, alignment=TA_CENTER, leading=16)
CARD_SUB = ps("CardSub", fontName="Helvetica-Bold",
fontSize=9.5, textColor=C_WHITE, alignment=TA_CENTER)
SEC_TITLE = ps("SecTitle", fontName="Helvetica-Bold",
fontSize=10, textColor=C_DARK_TEAL, leading=14)
BODY = ps("Body", fontName="Helvetica",
fontSize=8.5, textColor=C_DARK, leading=12)
BODY_B = ps("BodyB", fontName="Helvetica-Bold",
fontSize=8.5, textColor=C_DARK, leading=12)
SMALL = ps("Small", fontName="Helvetica",
fontSize=7.5, textColor=C_GREY, leading=10)
TABLE_HDR = ps("TblHdr", fontName="Helvetica-Bold",
fontSize=8, textColor=C_WHITE, alignment=TA_CENTER, leading=11)
TABLE_CELL = ps("TblCell", fontName="Helvetica",
fontSize=7.8, textColor=C_DARK, alignment=TA_LEFT, leading=11)
TABLE_CELL_B= ps("TblCellB", fontName="Helvetica-Bold",
fontSize=7.8, textColor=C_DARK, alignment=TA_LEFT, leading=11)
BULLET = ps("Bullet", fontName="Helvetica",
fontSize=8.5, textColor=C_DARK, leading=12, leftIndent=12,
bulletIndent=0, bulletFontName="Helvetica", bulletFontSize=8.5)
FOOT = ps("Foot", fontName="Helvetica-Oblique",
fontSize=7, textColor=C_GREY, alignment=TA_CENTER)
KEYBOX_TXT = ps("KeyboxTxt", fontName="Helvetica-Bold",
fontSize=9, textColor=C_DARK_TEAL, alignment=TA_CENTER, leading=13)
# ── helper: coloured card header table ──────────────────────────────
def card_header(stage_num, stage_name, subtext, bg_color, light_bg):
data = [[Paragraph(f"STAGE {stage_num} · {stage_name}", CARD_TITLE)],
[Paragraph(subtext, CARD_SUB)]]
t = Table(data, colWidths=[W - 28*mm])
t.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, 0), bg_color),
("BACKGROUND", (0, 1), (-1, 1), bg_color),
("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", [5]),
]))
return t
# ── helper: two-column feature table ────────────────────────────────
def feature_table(rows, col1_w=52*mm, col2_w=None, hdr_bg=C_TEAL):
if col2_w is None:
col2_w = W - 28*mm - col1_w
header_row = [Paragraph("FEATURE", TABLE_HDR),
Paragraph("DETAIL", TABLE_HDR)]
table_data = [header_row]
for feat, detail in rows:
table_data.append([
Paragraph(feat, TABLE_CELL_B),
Paragraph(detail, TABLE_CELL),
])
t = Table(table_data, colWidths=[col1_w, col2_w])
style = TableStyle([
("BACKGROUND", (0, 0), (-1, 0), hdr_bg),
("ROWBACKGROUNDS",(0, 1), (-1, -1), [C_WHITE, C_LIGHT_GREY]),
("GRID", (0, 0), (-1, -1), 0.4, C_MID_GREY),
("TOPPADDING", (0, 0), (-1, -1), 4),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
("LEFTPADDING", (0, 0), (-1, -1), 5),
("RIGHTPADDING", (0, 0), (-1, -1), 5),
("VALIGN", (0, 0), (-1, -1), "TOP"),
])
t.setStyle(style)
return t
# ── helper: key cell callout ─────────────────────────────────────────
def key_cell_box(cell_name, bg):
data = [[Paragraph(f"🔑 KEY CELL: {cell_name}", KEYBOX_TXT)]]
t = Table(data, colWidths=[W - 28*mm])
t.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, -1), bg),
("TOPPADDING", (0, 0), (-1, -1), 6),
("BOTTOMPADDING", (0, 0), (-1, -1), 6),
("LEFTPADDING", (0, 0), (-1, -1), 8),
("RIGHTPADDING", (0, 0), (-1, -1), 8),
("BOX", (0, 0), (-1, -1), 1, C_TEAL),
]))
return t
# ── helper: reversibility badge ──────────────────────────────────────
def rev_badge(text, bg):
data = [[Paragraph(text, ps("Rev", fontName="Helvetica-Bold",
fontSize=9, textColor=C_WHITE,
alignment=TA_CENTER))]]
t = Table(data, colWidths=[W - 28*mm])
t.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, -1), bg),
("TOPPADDING", (0, 0), (-1, -1), 5),
("BOTTOMPADDING", (0, 0), (-1, -1), 5),
]))
return t
# ── helper: section label ────────────────────────────────────────────
def sec(txt, color=C_DARK_TEAL):
return Paragraph(f"<b><font color='#{color.hexval()[2:]}'>▸ {txt}</font></b>", SEC_TITLE)
def sp(h=4): return Spacer(1, h)
def hr(color=C_MID_GREY): return HRFlowable(width="100%", thickness=0.5, color=color)
# ════════════════════════════════════════════════════════════════════
# Build content
# ════════════════════════════════════════════════════════════════════
story = []
# ── PAGE 1: COVER ────────────────────────────────────────────────────
def cover_page(canvas, doc):
canvas.saveState()
# Full-page gradient-ish background
canvas.setFillColor(C_DARK_TEAL)
canvas.rect(0, 0, W, H, fill=1, stroke=0)
# Decorative top stripe
canvas.setFillColor(C_TEAL)
canvas.rect(0, H - 20*mm, W, 20*mm, fill=1, stroke=0)
# Bottom stripe
canvas.setFillColor(colors.HexColor("#0A3A45"))
canvas.rect(0, 0, W, 18*mm, fill=1, stroke=0)
canvas.restoreState()
# Cover content as a table (centred vertically)
cover_data = [
[Paragraph("🦷", ps("Em", fontName="Helvetica", fontSize=40,
textColor=C_WHITE, alignment=TA_CENTER))],
[Spacer(1, 6)],
[Paragraph("PERIODONTAL DISEASE", COVER_TITLE)],
[Paragraph("Histological Features & Staging", COVER_SUB)],
[Spacer(1, 10)],
[HRFlowable(width="60%", thickness=1.5, color=C_GOLD)],
[Spacer(1, 10)],
[Paragraph("Quick Reference Flashcard Set", ps("QR", fontName="Helvetica-Bold",
fontSize=13, textColor=C_GOLD, alignment=TA_CENTER))],
[Spacer(1, 6)],
[Paragraph("Page & Schroeder (1976) Classification", COVER_SUB)],
[Spacer(1, 30)],
[Paragraph("Based on: Robbins & Cotran Pathologic Basis of Disease · Sherris & Ryan's Medical Microbiology<br/>"
"Ross & Pawlina Histology · Junqueira's Basic Histology · Tintinalli's Emergency Medicine",
COVER_NOTE)],
[Spacer(1, 8)],
[Paragraph("July 2026", COVER_NOTE)],
]
cover_tbl = Table(cover_data, colWidths=[W - 40*mm])
cover_tbl.setStyle(TableStyle([
("ALIGN", (0, 0), (-1, -1), "CENTER"),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("TOPPADDING", (0, 0), (-1, -1), 2),
("BOTTOMPADDING", (0, 0), (-1, -1), 2),
]))
story.append(Spacer(1, 55*mm))
story.append(cover_tbl)
story.append(PageBreak())
# ── PAGE 2: NORMAL ANATOMY + OVERVIEW ────────────────────────────────
story.append(sp(4))
# Title banner
anat_hdr = Table([[Paragraph("NORMAL PERIODONTIUM — ANATOMY FLASHCARD", CARD_TITLE)]],
colWidths=[W - 28*mm])
anat_hdr.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, -1), C_DARK_TEAL),
("TOPPADDING", (0, 0), (-1, -1), 7),
("BOTTOMPADDING", (0, 0), (-1, -1), 7),
("LEFTPADDING", (0, 0), (-1, -1), 8),
]))
story.append(anat_hdr)
story.append(sp(6))
anatomy_rows = [
("Gingiva",
"Keratinized stratified squamous epithelium over lamina propria. "
"Free gingiva forms 2–3 mm sulcus. Attached gingiva adheres to alveolar bone."),
("Junctional (Attachment) Epithelium",
"Adheres to tooth via basal lamina + hemidesmosomes (= epithelial attachment). "
"In young: to enamel. In older: migrates to cementum."),
("Crevicular Epithelium",
"Lines the gingival sulcus; non-keratinized; continuous with junctional epithelium. "
"First site of bacterial contact."),
("Periodontal Ligament (PDL)",
"Short collagen bundles (Sharpey fibers) binding cementum to alveolar bone. "
"Highly vascular + innervated. Width: 150–350 µm. Fibroblasts both produce AND resorb collagen."),
("Cementum",
"Covers root dentin. Avascular. Bone-like (cementocytes in lacunae). "
"Thickest at root apices."),
("Alveolar Bone",
"Woven (non-lamellar) bone pattern. Continuous osteoblast/osteoclast remodeling. "
"Surrounds root sockets."),
]
story.append(feature_table(anatomy_rows, col1_w=58*mm, hdr_bg=C_DARK_TEAL))
story.append(sp(8))
# Disease overview box
story.append(sec("DISEASE CONTINUUM"))
story.append(sp(3))
flow_data = [[
Paragraph("PLAQUE<br/>ACCUMULATION", ps("F0", fontName="Helvetica-Bold",
fontSize=8, textColor=C_DARK, alignment=TA_CENTER)),
Paragraph("→", ps("AR", fontName="Helvetica-Bold", fontSize=14,
textColor=C_TEAL, alignment=TA_CENTER)),
Paragraph("STAGE 1–2<br/><font color='#2E86AB'>Initial/Early</font>",
ps("F1", fontName="Helvetica-Bold", fontSize=8,
textColor=C_DARK, alignment=TA_CENTER)),
Paragraph("→", ps("AR2", fontName="Helvetica-Bold", fontSize=14,
textColor=C_TEAL, alignment=TA_CENTER)),
Paragraph("STAGE 3<br/><font color='#E67E22'>Gingivitis</font>",
ps("F2", fontName="Helvetica-Bold", fontSize=8,
textColor=C_DARK, alignment=TA_CENTER)),
Paragraph("→", ps("AR3", fontName="Helvetica-Bold", fontSize=14,
textColor=C_TEAL, alignment=TA_CENTER)),
Paragraph("STAGE 4<br/><font color='#C0392B'>Periodontitis</font>",
ps("F3", fontName="Helvetica-Bold", fontSize=8,
textColor=C_DARK, alignment=TA_CENTER)),
]]
flow_col = (W - 28*mm) / 7
flow_tbl = Table(flow_data, colWidths=[flow_col]*7)
flow_tbl.setStyle(TableStyle([
("BACKGROUND", (0, 0), (0, 0), C_LIGHT_GREY),
("BACKGROUND", (2, 0), (2, 0), C_LIGHT_BLUE),
("BACKGROUND", (4, 0), (4, 0), C_LIGHT_ORANGE),
("BACKGROUND", (6, 0), (6, 0), C_LIGHT_RED),
("ALIGN", (0, 0), (-1, -1), "CENTER"),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("TOPPADDING", (0, 0), (-1, -1), 8),
("BOTTOMPADDING", (0, 0), (-1, -1), 8),
("BOX", (0, 0), (0, 0), 0.5, C_MID_GREY),
("BOX", (2, 0), (2, 0), 0.5, C_STAGE1),
("BOX", (4, 0), (4, 0), 0.5, C_STAGE3),
("BOX", (6, 0), (6, 0), 0.5, C_STAGE4),
]))
story.append(flow_tbl)
story.append(PageBreak())
# ── PAGE 3: STAGE 1 ──────────────────────────────────────────────────
story.append(card_header(1, "INITIAL LESION", "2–4 Days After Plaque Accumulation · Subclinical",
C_STAGE1, C_LIGHT_BLUE))
story.append(sp(6))
story.append(sec("CLINICAL FEATURES", C_STAGE1))
story.append(sp(3))
story.append(Paragraph("● Clinically <b>silent</b> — no visible signs to patient or clinician", BULLET))
story.append(Paragraph("● Only <b>subclinical vascular changes</b> detectable histologically", BULLET))
story.append(sp(6))
story.append(sec("HISTOLOGICAL FEATURES", C_STAGE1))
story.append(sp(3))
s1_rows = [
("Vascular changes",
"Vasodilation of vessels in gingival plexus beneath sulcular epithelium; acute inflammatory response begins"),
("Vascular permeability ↑",
"Plasma proteins (fibrinogen, complement, immunoglobulins) extravasate into perivascular connective tissue"),
("Dominant cells",
"PMNs (polymorphonuclear neutrophils) — FIRST cells to migrate through junctional epithelium into sulcus"),
("GCF (Gingival Crevicular Fluid)",
"Increased flow of GCF into gingival sulcus; early warning marker"),
("Collagen",
"Minimal perivascular collagen degradation begins — earliest structural change"),
("Bacterial invasion",
"NO direct bacterial invasion of tissues; bacteria remain in sulcus"),
]
story.append(feature_table(s1_rows, hdr_bg=C_STAGE1))
story.append(sp(8))
story.append(key_cell_box("PMN / Neutrophil (Polymorphonuclear Leucocyte)", C_LIGHT_BLUE))
story.append(sp(6))
story.append(rev_badge("✔ FULLY REVERSIBLE — removal of plaque returns tissue to normal", C_STAGE1))
story.append(PageBreak())
# ── PAGE 4: STAGE 2 ──────────────────────────────────────────────────
story.append(card_header(2, "EARLY LESION", "4–14 Days · Peaks at ~14 Days",
C_STAGE2, C_LIGHT_GREEN))
story.append(sp(6))
story.append(sec("CLINICAL FEATURES", C_STAGE2))
story.append(sp(3))
story.append(Paragraph("● Early <b>erythema</b> (redness) — first visible sign", BULLET))
story.append(Paragraph("● <b>Bleeding on probing</b> begins", BULLET))
story.append(Paragraph("● Mild gingival edema", BULLET))
story.append(sp(6))
story.append(sec("HISTOLOGICAL FEATURES", C_STAGE2))
story.append(sp(3))
s2_rows = [
("Dominant cells",
"T-LYMPHOCYTES predominate (>75% of infiltrate) in the connective tissue — hallmark of this stage"),
("PMN infiltration",
"Accentuated neutrophil band persists in junctional + sulcular epithelium"),
("Collagen loss",
"60–70% collagen loss in the INFILTRATED CONNECTIVE TISSUE zone beneath junctional epithelium"),
("Fibroblasts",
"Cytopathic changes: fibroblasts show altered morphology, reduced collagen-producing function"),
("Junctional epithelium",
"Rete pegs begin to ELONGATE and proliferate laterally — early structural remodelling"),
("GCF",
"Continues to increase; now contains inflammatory mediators and enzymes"),
("Vasodilation",
"Persistent; capillary loops become prominent — explains visible erythema"),
("Bacterial invasion",
"Still NO direct tissue invasion by bacteria"),
]
story.append(feature_table(s2_rows, hdr_bg=C_STAGE2))
story.append(sp(8))
story.append(key_cell_box("T-Lymphocyte (CD4+ helper T cells predominate)", C_LIGHT_GREEN))
story.append(sp(6))
story.append(rev_badge("✔ REVERSIBLE — effective oral hygiene restores normal histology", C_STAGE2))
story.append(PageBreak())
# ── PAGE 5: STAGE 3 ──────────────────────────────────────────────────
story.append(card_header(3, "ESTABLISHED LESION", "Weeks to Months · Chronic GINGIVITIS",
C_STAGE3, C_LIGHT_ORANGE))
story.append(sp(6))
story.append(sec("CLINICAL FEATURES", C_STAGE3))
story.append(sp(3))
story.append(Paragraph("● <b>Frank gingivitis</b> — bright red or bluish-red, oedematous, spongy gingiva", BULLET))
story.append(Paragraph("● Spontaneous or easy <b>bleeding</b>", BULLET))
story.append(Paragraph("● Gingival contour changes; pseudo-pocket may form", BULLET))
story.append(Paragraph("● <b>No alveolar bone loss</b> — inflammation confined to soft tissue only", BULLET))
story.append(sp(6))
story.append(sec("HISTOLOGICAL FEATURES", C_STAGE3))
story.append(sp(3))
s3_rows = [
("Dominant cells",
"PLASMA CELLS dominate — hallmark of established lesion; B lymphocytes → plasma cells producing IgG, IgA, IgM"),
("Overall infiltrate",
"Dense mixed chronic infiltrate: PMNs + lymphocytes + PLASMA CELLS + macrophages in connective tissue"),
("Collagen loss",
"Continued progressive degradation of perivascular and subepithelial collagen"),
("Pocket epithelium",
"Junctional epithelium proliferates apically → becomes 'pocket epithelium': thin, ulcerated, highly permeable"),
("Epithelial migration",
"Coronal epithelium spreads apically along root surface — sulcus deepens slightly"),
("Vascular proliferation",
"Increased vascularity and capillary loops — explains spontaneous bleeding"),
("Alveolar bone",
"STILL NORMAL — no bone resorption; this is the critical distinction from Stage 4"),
("Immunoglobulins",
"Plasma cells actively secrete Ig into tissues and GCF — attempts at humoral immunity"),
]
story.append(feature_table(s3_rows, hdr_bg=C_STAGE3))
story.append(sp(8))
story.append(key_cell_box("Plasma Cell (B-lymphocyte lineage — IgG/IgA/IgM secreting)", C_LIGHT_ORANGE))
story.append(sp(6))
story.append(rev_badge("✔ REVERSIBLE — returns to Stage 0 with effective plaque control", C_STAGE3))
story.append(PageBreak())
# ── PAGE 6: STAGE 4 ──────────────────────────────────────────────────
story.append(card_header(4, "ADVANCED LESION", "Months to Years · PERIODONTITIS · IRREVERSIBLE",
C_STAGE4, C_LIGHT_RED))
story.append(sp(6))
story.append(sec("CLINICAL FEATURES", C_STAGE4))
story.append(sp(3))
story.append(Paragraph("● Deep <b>periodontal pockets</b> (>3 mm on probing)", BULLET))
story.append(Paragraph("● <b>Alveolar bone loss</b> — horizontal and/or vertical", BULLET))
story.append(Paragraph("● Gingival recession; roots exposed; 'teeth appear longer'", BULLET))
story.append(Paragraph("● <b>Tooth mobility</b>; possible suppuration", BULLET))
story.append(Paragraph("● Eventual <b>tooth loss</b> if untreated", BULLET))
story.append(sp(6))
story.append(sec("HISTOLOGICAL FEATURES", C_STAGE4))
story.append(sp(3))
s4_rows = [
("Defining event",
"OSTEOCLASTIC ALVEOLAR BONE RESORPTION — distinguishes periodontitis from gingivitis absolutely"),
("Infiltrate extension",
"Inflammatory cells extend from soft tissue INTO alveolar bone and periodontal ligament"),
("PDL destruction",
"Loss of Sharpey fiber insertions from cementum and alveolar bone; ligament destroyed"),
("Periodontal pocket",
"True pocket: apical migration of junctional epithelium + coronal displacement of gingival margin"),
("Pocket epithelium",
"Thin, non-keratinized, ulcerated; permeable to bacteria and their products; may show microulcerations"),
("Cementum changes",
"Bacterial invasion of cementum surface; necrotic/infected cementum (requires root planing)"),
("Fibroblast destruction",
"Extensive fibroblast + collagen destruction by P. gingivalis proteases and MMPs"),
("Plasma cells",
"Dense plasma cell-rich infiltrate continues; immunoglobulin deposition in connective tissue"),
("Periodontal abscess",
"Acute complication: when pocket occludes → acute neutrophilic abscess forms in periodontal pocket"),
("Regeneration",
"LOST BONE AND PDL DO NOT REGENERATE — even if inflammation halted (irreversible)"),
]
story.append(feature_table(s4_rows, hdr_bg=C_STAGE4))
story.append(sp(8))
story.append(key_cell_box("Plasma Cells + Osteoclasts (bone-resorbing cells)", C_LIGHT_RED))
story.append(sp(6))
story.append(rev_badge("✘ IRREVERSIBLE — lost bone & PDL do not regenerate; only progression can be halted", C_STAGE4))
story.append(PageBreak())
# ── PAGE 7: SUMMARY COMPARISON TABLE ─────────────────────────────────
story.append(sp(4))
summary_hdr = Table([[Paragraph("MASTER SUMMARY — ALL 4 STAGES AT A GLANCE", CARD_TITLE)]],
colWidths=[W - 28*mm])
summary_hdr.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, -1), C_DARK_TEAL),
("TOPPADDING", (0, 0), (-1, -1), 8),
("BOTTOMPADDING", (0, 0), (-1, -1), 8),
("LEFTPADDING", (0, 0), (-1, -1), 8),
]))
story.append(summary_hdr)
story.append(sp(6))
TH = ps("SumHdr", fontName="Helvetica-Bold", fontSize=7.5,
textColor=C_WHITE, alignment=TA_CENTER, leading=10)
TC = ps("SumCell", fontName="Helvetica", fontSize=7.5,
textColor=C_DARK, alignment=TA_CENTER, leading=10)
TC_L = ps("SumCellL", fontName="Helvetica", fontSize=7.5,
textColor=C_DARK, alignment=TA_LEFT, leading=10)
TC_B = ps("SumCellB", fontName="Helvetica-Bold", fontSize=7.5,
textColor=C_DARK, alignment=TA_LEFT, leading=10)
cw = (W - 28*mm) / 5
sum_data = [
[Paragraph("PARAMETER", TH),
Paragraph("STAGE 1\nInitial", TH),
Paragraph("STAGE 2\nEarly", TH),
Paragraph("STAGE 3\nEstablished\n(Gingivitis)", TH),
Paragraph("STAGE 4\nAdvanced\n(Periodontitis)", TH)],
[Paragraph("Time onset", TC_B),
Paragraph("2–4 days", TC), Paragraph("4–14 days", TC),
Paragraph("Weeks–months", TC), Paragraph("Months–years", TC)],
[Paragraph("Dominant cells", TC_B),
Paragraph("PMNs", TC), Paragraph("T-lymphocytes", TC),
Paragraph("Plasma cells", TC), Paragraph("Plasma cells +\nOsteoclasts", TC)],
[Paragraph("Collagen loss", TC_B),
Paragraph("Minimal\n(perivascular)", TC), Paragraph("60–70%\n(infiltrated zone)", TC),
Paragraph("Progressive\n(subepithelial)", TC), Paragraph("Extensive +\nPDL destroyed", TC)],
[Paragraph("Junctional\nEpithelium", TC_B),
Paragraph("Intact", TC), Paragraph("Rete peg\nproliferation", TC),
Paragraph("Pocket epithelium\nforming", TC), Paragraph("Deep pocket,\nulcerated, thin", TC)],
[Paragraph("Alveolar bone", TC_B),
Paragraph("Normal", TC), Paragraph("Normal", TC),
Paragraph("Normal\n(no loss)", TC), Paragraph("OSTEOCLASTIC\nRESORP-TION ⚠", TC)],
[Paragraph("PDL", TC_B),
Paragraph("Intact", TC), Paragraph("Intact", TC),
Paragraph("Intact", TC), Paragraph("DESTROYED", TC)],
[Paragraph("Vasculature", TC_B),
Paragraph("Vasodilation\nbegins", TC), Paragraph("Capillary loops\nprominent", TC),
Paragraph("Proliferation\n(explains bleeding)", TC), Paragraph("Continued\nproliferation", TC)],
[Paragraph("Clinical signs", TC_B),
Paragraph("NONE\n(subclinical)", TC), Paragraph("Early erythema,\nbleeding on probing", TC),
Paragraph("Frank gingivitis:\nred, oedematous,\nbleeding", TC),
Paragraph("Pockets, bone\nloss, mobility,\ntooth loss", TC)],
[Paragraph("Reversible?", TC_B),
Paragraph("✔ YES", TC), Paragraph("✔ YES", TC),
Paragraph("✔ YES", TC), Paragraph("✘ NO", TC)],
]
sum_tbl = Table(sum_data, colWidths=[cw]*5)
sum_tbl.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, 0), C_DARK_TEAL),
("BACKGROUND", (1, 1), (1, -1), C_LIGHT_BLUE),
("BACKGROUND", (2, 1), (2, -1), C_LIGHT_GREEN),
("BACKGROUND", (3, 1), (3, -1), C_LIGHT_ORANGE),
("BACKGROUND", (4, 1), (4, -1), C_LIGHT_RED),
("BACKGROUND", (0, 1), (0, -1), C_LIGHT_GREY),
("ROWBACKGROUNDS",(0, 1), (0, -1), [C_LIGHT_GREY]),
# Alternate row tinting on feature column
("GRID", (0, 0), (-1, -1), 0.4, C_MID_GREY),
("TOPPADDING", (0, 0), (-1, -1), 4),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
("LEFTPADDING", (0, 0), (-1, -1), 4),
("RIGHTPADDING", (0, 0), (-1, -1), 4),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("FONTNAME", (4, 9), (4, 9), "Helvetica-Bold"),
("TEXTCOLOR", (4, 9), (4, 9), C_STAGE4),
("FONTNAME", (1, 9), (3, 9), "Helvetica-Bold"),
("TEXTCOLOR", (1, 9), (3, 9), C_STAGE2),
]))
story.append(sum_tbl)
story.append(PageBreak())
# ── PAGE 8: MICROBIOLOGY + PATHOMECHANISM ────────────────────────────
story.append(sp(4))
micro_hdr = Table([[Paragraph("MICROBIOLOGY & PATHOMECHANISM FLASHCARD", CARD_TITLE)]],
colWidths=[W - 28*mm])
micro_hdr.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, -1), C_DARK_TEAL),
("TOPPADDING", (0, 0), (-1, -1), 7),
("BOTTOMPADDING", (0, 0), (-1, -1), 7),
("LEFTPADDING", (0, 0), (-1, -1), 8),
]))
story.append(micro_hdr)
story.append(sp(6))
story.append(sec("KEY PERIODONTAL PATHOGENS"))
story.append(sp(3))
micro_rows = [
("Aggregatibacter\nactinomycetemcomitans",
"Gram-negative capnophilic rod. Produces LEUKOTOXIN (kills PMNs). "
"Key in localized aggressive periodontitis (adolescents)."),
("Porphyromonas\ngingivalis",
"Gram-negative anaerobe. Potent extracellular PROTEASES (tissue injury). "
"Central to chronic periodontitis. Works synergistically with T. denticola."),
("Prevotella intermedia",
"Gram-negative anaerobe. Associated with adult chronic periodontitis "
"and pregnancy gingivitis (metabolizes progesterone)."),
("Treponema denticola",
"Anaerobic spirochete. Binds serum factors → INTERFERES WITH COMPLEMENT. "
"Synergistic growth with P. gingivalis; drives progression from gingivitis to periodontitis."),
]
story.append(feature_table(micro_rows, col1_w=52*mm, hdr_bg=C_DARK_TEAL))
story.append(sp(8))
story.append(sec("MECHANISMS OF TISSUE DESTRUCTION"))
story.append(sp(3))
mech_rows = [
("Bacterial enzymes",
"P. gingivalis proteases (gingipains) degrade collagen, fibrinogen, complement factors"),
("Leukotoxin",
"A. actinomycetemcomitans leukotoxin kills PMNs → immune evasion"),
("Complement evasion",
"T. denticola binds serum complement inhibitors → survives in tissue"),
("TLR activation",
"LPS (gram-negative cell walls) trigger Toll-like receptors → IL-1β, TNF-α, prostaglandins"),
("Host MMPs",
"Matrix metalloproteinases from host cells degrade collagen and ECM (most destruction is HOST-mediated)"),
("Osteoclast activation",
"IL-1β, TNF-α, RANKL → osteoclast differentiation and activation → alveolar bone resorption"),
("Direct invasion",
"Minimal in early stages; bacteria remain in sulcus/pocket. Direct invasion occurs in ADVANCED disease only."),
]
story.append(feature_table(mech_rows, col1_w=52*mm, hdr_bg=C_TEAL))
story.append(sp(8))
# Microbial shift box
shift_data = [
[Paragraph("HEALTHY SITES", ps("SHdr", fontName="Helvetica-Bold",
fontSize=9, textColor=C_WHITE, alignment=TA_CENTER)),
Paragraph(" → ", ps("SAr", fontName="Helvetica-Bold", fontSize=14,
textColor=C_DARK, alignment=TA_CENTER)),
Paragraph("DISEASED SITES", ps("SDHdr", fontName="Helvetica-Bold",
fontSize=9, textColor=C_WHITE, alignment=TA_CENTER))],
[Paragraph("Facultative gram-positive\norganisms dominate", TABLE_CELL),
Paragraph("", TABLE_CELL),
Paragraph("Anaerobic + microaerophilic\ngram-negative flora dominate", TABLE_CELL)],
]
shift_tbl = Table(shift_data, colWidths=[(W-28*mm)*0.44, (W-28*mm)*0.12, (W-28*mm)*0.44])
shift_tbl.setStyle(TableStyle([
("BACKGROUND", (0, 0), (0, 0), C_STAGE2),
("BACKGROUND", (2, 0), (2, 0), C_STAGE4),
("BACKGROUND", (0, 1), (0, 1), C_LIGHT_GREEN),
("BACKGROUND", (2, 1), (2, 1), C_LIGHT_RED),
("ALIGN", (0, 0), (-1, -1), "CENTER"),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("TOPPADDING", (0, 0), (-1, -1), 6),
("BOTTOMPADDING", (0, 0), (-1, -1), 6),
("GRID", (0, 0), (-1, -1), 0.5, C_MID_GREY),
]))
story.append(shift_tbl)
story.append(PageBreak())
# ── PAGE 9: CLINICAL ASSESSMENT + TREATMENT ──────────────────────────
story.append(sp(4))
tx_hdr = Table([[Paragraph("CLINICAL ASSESSMENT & TREATMENT FLASHCARD", CARD_TITLE)]],
colWidths=[W - 28*mm])
tx_hdr.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, -1), C_DARK_TEAL),
("TOPPADDING", (0, 0), (-1, -1), 7),
("BOTTOMPADDING", (0, 0), (-1, -1), 7),
("LEFTPADDING", (0, 0), (-1, -1), 8),
]))
story.append(tx_hdr)
story.append(sp(6))
story.append(sec("CLINICAL ASSESSMENT PARAMETERS"))
story.append(sp(3))
assess_rows = [
("Periodontal probe",
"Gold standard. Probe inserted into sulcus; pocket depth measured in mm. Normal ≤3 mm."),
("Bleeding on probing (BOP)",
"Reflects ACTIVE gingival inflammation. Present from Stage 2 onwards."),
("Attachment loss",
"Distance from CEJ to base of pocket. Key indicator of periodontitis severity."),
("Tooth mobility",
"Graded I–III. Reflects extent of PDL and bone destruction. Stage 4 finding."),
("Radiographs",
"Detect alveolar bone loss (horizontal/vertical patterns). Stage 4 prerequisite."),
]
story.append(feature_table(assess_rows, col1_w=52*mm, hdr_bg=C_TEAL))
story.append(sp(7))
story.append(sec("TREATMENT OVERVIEW BY STAGE"))
story.append(sp(3))
tx_rows = [
("Stage 1–2", "Oral hygiene instruction (brushing ×2/day, flossing). Plaque removal → REVERSIBLE."),
("Stage 3\n(Gingivitis)",
"Scaling + root planing (supragingival). Chlorhexidine rinse 0.12–0.2%. "
"Hydrogen peroxide (3%, diluted 1:1). NSAIDs for pain. REVERSIBLE."),
("Stage 4\n(Periodontitis)",
"Subgingival scaling + root planing (SRP). Referral to periodontist. "
"Antibiotics for severe/spreading disease. Periodontal surgery in refractory cases. IRREVERSIBLE."),
("Antibiotics\n(severe disease)",
"Penicillin V 500mg TID-QID × 10d; Amoxicillin/Clavulanate 500/125mg TID × 10d; "
"Metronidazole 500mg BID × 10d (PCN allergy); Clindamycin 300mg QID × 10d (PCN allergy)"),
("Abscess",
"Periodontal abscess requires DRAINAGE. Acute exacerbation needs urgent intervention."),
("Risk modification",
"Smoking cessation (most important in HIV-negative). Glycaemic control in diabetics. "
"Hormone management. Regular recall every 3–6 months."),
]
story.append(feature_table(tx_rows, col1_w=36*mm, hdr_bg=C_DARK_TEAL))
story.append(sp(7))
# Systemic associations box
story.append(sec("SYSTEMIC ASSOCIATIONS (BIDIRECTIONAL RELATIONSHIPS)"))
story.append(sp(3))
sys_data = [
[Paragraph("DISEASE", TABLE_HDR), Paragraph("RELATIONSHIP", TABLE_HDR)],
[Paragraph("Diabetes mellitus", TABLE_CELL_B),
Paragraph("Bidirectional: poor glycaemic control → worse periodontitis; periodontitis → worse glycaemia", TABLE_CELL)],
[Paragraph("HIV/AIDS", TABLE_CELL_B),
Paragraph("Increased severity; necrotizing periodontal disease; linear gingival erythema", TABLE_CELL)],
[Paragraph("Cardiovascular disease", TABLE_CELL_B),
Paragraph("Associated but no proven independent causal link (Goldman-Cecil)", TABLE_CELL)],
[Paragraph("Pregnancy", TABLE_CELL_B),
Paragraph("Pregnancy gingivitis: oestrogen/progesterone alter gingival response to plaque", TABLE_CELL)],
[Paragraph("Infective endocarditis", TABLE_CELL_B),
Paragraph("Periodontal bacteria (P. gingivalis, Streptococci) can seed cardiac valves", TABLE_CELL)],
]
sys_tbl = Table(sys_data, colWidths=[42*mm, W - 28*mm - 42*mm])
sys_tbl.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, 0), C_TEAL),
("ROWBACKGROUNDS",(0, 1), (-1, -1), [C_WHITE, C_LIGHT_TEAL]),
("GRID", (0, 0), (-1, -1), 0.4, C_MID_GREY),
("TOPPADDING", (0, 0), (-1, -1), 4),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
("LEFTPADDING", (0, 0), (-1, -1), 5),
("RIGHTPADDING", (0, 0), (-1, -1), 5),
("VALIGN", (0, 0), (-1, -1), "TOP"),
]))
story.append(sys_tbl)
story.append(PageBreak())
# ── PAGE 10: HIGH-YIELD RAPID RECALL ─────────────────────────────────
story.append(sp(4))
hyr_hdr = Table([[Paragraph("HIGH-YIELD RAPID RECALL FLASHCARD", CARD_TITLE)]],
colWidths=[W - 28*mm])
hyr_hdr.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, -1), C_GOLD),
("TOPPADDING", (0, 0), (-1, -1), 7),
("BOTTOMPADDING", (0, 0), (-1, -1), 7),
("LEFTPADDING", (0, 0), (-1, -1), 8),
]))
story.append(hyr_hdr)
story.append(sp(6))
hy_items = [
("Q: First cell to arrive in periodontal inflammation?",
"A: PMN (Neutrophil) — Stage 1 Initial Lesion"),
("Q: Dominant cell in Early Lesion (Stage 2)?",
"A: T-Lymphocyte (>75% of connective tissue infiltrate)"),
("Q: Dominant cell in Established Lesion (Stage 3 = Gingivitis)?",
"A: PLASMA CELL — hallmark of chronic gingivitis"),
("Q: What DEFINES periodontitis histologically?",
"A: Osteoclastic alveolar bone resorption + PDL destruction — Stage 4"),
("Q: Collagen loss % in Stage 2 (Early Lesion)?",
"A: 60–70% in the infiltrated connective tissue zone"),
("Q: Is gingivitis reversible?",
"A: YES (Stages 1–3 all reversible with plaque removal)"),
("Q: Is periodontitis reversible?",
"A: NO — lost alveolar bone and PDL do NOT regenerate"),
("Q: Normal gingival sulcus depth?",
"A: 2–3 mm (disease-free state)"),
("Q: Which organism produces leukotoxin?",
"A: Aggregatibacter actinomycetemcomitans → kills PMNs"),
("Q: 'Red complex' bacteria in periodontitis?",
"A: P. gingivalis, T. denticola, T. forsythia (Socransky classification)"),
("Q: Key synergistic pair in periodontitis progression?",
"A: Porphyromonas gingivalis + Treponema denticola"),
("Q: What separates gingivitis from periodontitis?",
"A: BONE LOSS — no bone loss = gingivitis; bone loss = periodontitis"),
("Q: Histology of gingival sulcus lining?",
"A: Non-keratinized crevicular epithelium (→ ulcerated pocket epithelium in disease)"),
("Q: Global rank of severe periodontal disease?",
"A: 11th most prevalent disease globally (2016) — Park's Preventive Medicine"),
]
col1 = (W - 28*mm) * 0.45
col2 = (W - 28*mm) * 0.55
for i, (q, a) in enumerate(hy_items):
bg1 = C_LIGHT_TEAL if i % 2 == 0 else C_LIGHT_GREY
bg2 = C_WHITE if i % 2 == 0 else C_LIGHT_GREY
row = Table(
[[Paragraph(q, ps("QQ", fontName="Helvetica-Bold", fontSize=8,
textColor=C_DARK_TEAL, leading=11)),
Paragraph(a, ps("AA", fontName="Helvetica", fontSize=8,
textColor=C_DARK, leading=11))]],
colWidths=[col1, col2]
)
row.setStyle(TableStyle([
("BACKGROUND", (0, 0), (0, 0), bg1),
("BACKGROUND", (1, 0), (1, 0), bg2),
("GRID", (0, 0), (-1, -1), 0.3, C_MID_GREY),
("TOPPADDING", (0, 0), (-1, -1), 4),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
("LEFTPADDING", (0, 0), (-1, -1), 5),
("RIGHTPADDING", (0, 0), (-1, -1), 5),
("VALIGN", (0, 0), (-1, -1), "TOP"),
]))
story.append(row)
story.append(sp(8))
story.append(HRFlowable(width="100%", thickness=1, color=C_MID_GREY))
story.append(sp(4))
story.append(Paragraph(
"Sources: Robbins & Cotran Pathologic Basis of Disease 10e · Sherris & Ryan's Medical Microbiology 8e · "
"Ross & Pawlina Histology 8e · Junqueira's Basic Histology 17e · Tintinalli's Emergency Medicine · "
"Park's Textbook of Preventive & Social Medicine · Page & Schroeder (1976) Histological Classification",
FOOT))
# ════════════════════════════════════════════════════════════════════
# Build PDF
# ════════════════════════════════════════════════════════════════════
def on_first_page(canvas, doc):
cover_page(canvas, doc)
canvas.saveState()
# Footer on cover
canvas.setFont("Helvetica", 8)
canvas.setFillColor(colors.HexColor("#AAAAAA"))
canvas.drawCentredString(W/2, 12*mm, "Periodontal Histology Flashcards · Quick Reference")
canvas.restoreState()
def on_later_pages(canvas, doc):
canvas.saveState()
canvas.setFont("Helvetica", 7)
canvas.setFillColor(C_GREY)
canvas.drawString(14*mm, 10*mm, "Periodontal Disease — Histology Flashcards")
canvas.drawRightString(W - 14*mm, 10*mm, f"Page {doc.page}")
canvas.setStrokeColor(C_MID_GREY)
canvas.setLineWidth(0.5)
canvas.line(14*mm, 13*mm, W - 14*mm, 13*mm)
canvas.restoreState()
doc = SimpleDocTemplate(
OUTPUT,
pagesize=A4,
leftMargin=14*mm, rightMargin=14*mm,
topMargin=14*mm, bottomMargin=18*mm,
title="Periodontal Disease — Histological Flashcards",
author="Orris Medical AI",
subject="Histology of Gingivitis and Periodontitis — Page & Schroeder Staging",
)
doc.build(story, onFirstPage=on_first_page, onLaterPages=on_later_pages)
print("PDF created successfully:", OUTPUT)