Make this pdf of periodontal disease into pptx . Make like prepare by human ( less use of color and symbol) and add diagram and table which are present in pdf
cd /home/daytona/workspace && python3 - << 'EOF' import pdfplumber pdf_path = "/home/daytona/workspace/attachments/e1428ee7-a160-450f-af79-7a03a7f02b41/Here is the complete content from the PDF all 10 .pdf" with pdfplumber.open(pdf_path) as pdf: print(f"Total pages: {len(pdf.pages)}") for i, page in enumerate(pdf.pages): print(f"\n{'='*60}") print(f"PAGE {i+1}") print('='*60) text = page.extract_text() if text: print(text[:3000]) tables = page.extract_tables() if tables: print(f"\n--- TABLES ON PAGE {i+1} ---") for j, tbl in enumerate(tables): print(f"Table {j+1}:") for row in tbl: print(row) EOF
cd /home/daytona/workspace && python3 - << 'EOF' import pdfplumber import fitz # PyMuPDF import os pdf_path = "/home/daytona/workspace/attachments/e1428ee7-a160-450f-af79-7a03a7f02b41/Here is the complete content from the PDF all 10 .pdf" # Extract images using PyMuPDF os.makedirs("/home/daytona/workspace/perio-pptx/images", exist_ok=True) doc = fitz.open(pdf_path) img_count = 0 for page_num in range(len(doc)): page = doc[page_num] image_list = page.get_images(full=True) for img_index, img in enumerate(image_list): xref = img[0] base_image = doc.extract_image(xref) img_bytes = base_image["image"] ext = base_image["ext"] img_path = f"/home/daytona/workspace/perio-pptx/images/page{page_num+1}_img{img_index+1}.{ext}" with open(img_path, "wb") as f: f.write(img_bytes) print(f"Saved: {img_path} ({len(img_bytes)} bytes)") img_count += 1 print(f"\nTotal images extracted: {img_count}") doc.close() EOF
mkdir -p /home/daytona/workspace/perio-pptx && ls /home/daytona/workspace/perio-pptx/
~/perio-pptx/build.js
const pptxgen = require("pptxgenjs");
const pres = new pptxgen();
pres.layout = "LAYOUT_16x9";
pres.title = "Periodontal Disease";
pres.author = "Medical Notes";
// ─── COLOUR PALETTE (minimal, human-prepared look) ───
const C = {
black: "1A1A1A",
darkGray: "333333",
midGray: "555555",
gray: "777777",
lightGray:"F2F2F2",
border: "AAAAAA",
white: "FFFFFF",
accent: "2C5F8A", // single muted blue accent
accentLt: "D6E4F0", // very light blue for table headers
red: "B03030", // used sparingly for warnings
yellow: "FFF8DC", // keynote box backgrounds
};
// Helper: slide title bar
function addTitle(slide, text, sub) {
slide.addShape(pres.ShapeType.rect, {
x: 0, y: 0, w: 10, h: 0.75,
fill: { color: C.accent },
line: { color: C.accent },
});
slide.addText(text, {
x: 0.2, y: 0, w: 9.6, h: 0.75,
fontSize: 22, bold: true, color: C.white,
valign: "middle", fontFace: "Calibri", margin: 0,
});
if (sub) {
slide.addText(sub, {
x: 0.2, y: 0.75, w: 9.6, h: 0.3,
fontSize: 11, italic: true, color: C.gray,
fontFace: "Calibri", margin: 0,
});
}
}
// Helper: section divider slide
function sectionSlide(label) {
const s = pres.addSlide();
s.addShape(pres.ShapeType.rect, { x:0, y:0, w:10, h:5.625, fill:{color: C.lightGray}, line:{color:C.lightGray} });
s.addShape(pres.ShapeType.rect, { x:0, y:2.3, w:10, h:1.1, fill:{color: C.accent}, line:{color:C.accent} });
s.addText(label, {
x:0.3, y:2.3, w:9.4, h:1.1,
fontSize:28, bold:true, color:C.white,
valign:"middle", fontFace:"Calibri", align:"center",
});
}
// Helper: body text (bullet)
function bullet(slide, items, x, y, w, h, opts={}) {
const arr = items.map((t, i) => ({
text: t,
options: { bullet: true, breakLine: i < items.length - 1, fontSize: opts.fontSize||13, color: C.darkGray, fontFace:"Calibri" }
}));
slide.addText(arr, { x, y, w, h, valign:"top", ...opts });
}
// Helper: plain paragraph
function para(slide, text, x, y, w, h, opts={}) {
slide.addText(text, { x, y, w, h, fontSize:12, color:C.darkGray, fontFace:"Calibri", valign:"top", wrap:true, ...opts });
}
// Helper: note box
function noteBox(slide, text, x, y, w, h, bgColor) {
slide.addShape(pres.ShapeType.rect, { x, y, w, h, fill:{color: bgColor||C.yellow}, line:{color:C.border, width:0.5} });
slide.addText(text, { x:x+0.1, y:y+0.05, w:w-0.2, h:h-0.1, fontSize:11, color:C.darkGray, fontFace:"Calibri", wrap:true, valign:"middle" });
}
// ═══════════════════════════════════════════════════
// SLIDE 1 — TITLE
// ═══════════════════════════════════════════════════
{
const s = pres.addSlide();
s.addShape(pres.ShapeType.rect, { x:0, y:0, w:10, h:5.625, fill:{color:C.white}, line:{color:C.white} });
s.addShape(pres.ShapeType.rect, { x:0, y:0, w:10, h:1.2, fill:{color:C.accent}, line:{color:C.accent} });
s.addText("PERIODONTAL DISEASE", {
x:0.3, y:0.1, w:9.4, h:1.0,
fontSize:32, bold:true, color:C.white, fontFace:"Calibri",
valign:"middle", align:"center",
});
s.addText("A Structured Overview: Anatomy · Microbiology · Classification · Management", {
x:0.5, y:1.4, w:9, h:0.5,
fontSize:14, color:C.midGray, fontFace:"Calibri", align:"center", italic:true,
});
// horizontal divider
s.addShape(pres.ShapeType.line, { x:1, y:2.1, w:8, h:0, line:{color:C.border, width:0.8} });
s.addText("Sources: Harrison's Principles of Internal Medicine 22e · Robbins & Cotran Pathologic Basis of Disease\nJunqueira's Basic Histology · Sherris Medical Microbiology · Tintinalli's Emergency Medicine", {
x:0.5, y:2.3, w:9, h:0.8,
fontSize:10, color:C.gray, fontFace:"Calibri", align:"center", italic:true,
});
// analogy box
s.addShape(pres.ShapeType.rect, { x:1.5, y:3.3, w:7, h:1.7, fill:{color:C.accentLt}, line:{color:C.accent, width:0.5} });
s.addText([
{ text: "Think of your tooth like a FENCE POST.\n", options:{bold:true, fontSize:12, color:C.accent} },
{ text: "The post (tooth) must be held firmly in the GROUND (jawbone).\n", options:{fontSize:11, color:C.darkGray} },
{ text: "PERIODONTIUM = everything that holds the tooth in place.\n", options:{fontSize:11, color:C.darkGray} },
{ text: "Periodontal disease = the slow destruction of this support system by bacteria.", options:{fontSize:11, color:C.darkGray} },
], { x:1.7, y:3.4, w:6.6, h:1.5, valign:"top", fontFace:"Calibri", wrap:true });
}
// ═══════════════════════════════════════════════════
// SLIDE 2 — ANATOMY OF THE PERIODONTIUM
// ═══════════════════════════════════════════════════
{
const s = pres.addSlide();
addTitle(s, "Anatomy of the Periodontium", "The four structures that support every tooth");
// LEFT: labelled diagram (drawn with shapes + text)
// Tooth outline
s.addShape(pres.ShapeType.rect, { x:1.0, y:1.1, w:1.2, h:3.8, fill:{color:"F0EAD6"}, line:{color:C.border, width:0.7} });
s.addText("TOOTH\n(Enamel/Dentin/Pulp)", { x:0.95, y:2.3, w:1.3, h:0.8, fontSize:7, color:C.midGray, align:"center", fontFace:"Calibri" });
// Gingiva
s.addShape(pres.ShapeType.rect, { x:0.3, y:1.1, w:0.7, h:1.2, fill:{color:"FFCCCC"}, line:{color:C.border, width:0.5} });
s.addText("Gingiva\n(Gum)", { x:0.05, y:1.1, w:0.9, h:0.5, fontSize:7, color:C.darkGray, align:"center", fontFace:"Calibri" });
// Free gingiva label
s.addText("Free gingiva", { x:0.05, y:1.65, w:1.0, h:0.25, fontSize:7, italic:true, color:C.midGray, fontFace:"Calibri" });
// Attached gingiva label
s.addShape(pres.ShapeType.rect, { x:0.3, y:2.3, w:0.7, h:0.6, fill:{color:"FFAAAA"}, line:{color:C.border, width:0.5} });
s.addText("Attached gingiva", { x:0.05, y:2.35, w:1.0, h:0.25, fontSize:7, italic:true, color:C.midGray, fontFace:"Calibri" });
// PDL
s.addShape(pres.ShapeType.rect, { x:2.2, y:1.8, w:0.4, h:2.5, fill:{color:"D4EDDA"}, line:{color:C.border, width:0.5} });
s.addText("PDL", { x:2.65, y:2.9, w:1.0, h:0.3, fontSize:7, color:C.darkGray, fontFace:"Calibri" });
s.addShape(pres.ShapeType.line, { x:2.62, y:3.0, w:0.3, h:0, line:{color:C.gray, width:0.5} });
// Cementum
s.addShape(pres.ShapeType.rect, { x:2.18, y:1.78, w:0.15, h:2.6, fill:{color:"E8D5B7"}, line:{color:C.border, width:0.4} });
s.addText("Cementum", { x:2.65, y:3.3, w:1.2, h:0.25, fontSize:7, color:C.darkGray, fontFace:"Calibri" });
// Alveolar bone
s.addShape(pres.ShapeType.rect, { x:0.3, y:2.9, w:0.7, h:1.9, fill:{color:"EAE0D5"}, line:{color:C.border, width:0.5} });
s.addShape(pres.ShapeType.rect, { x:2.6, y:2.9, w:0.5, h:1.9, fill:{color:"EAE0D5"}, line:{color:C.border, width:0.5} });
s.addText("Alveolar bone", { x:3.15, y:3.7, w:1.3, h:0.25, fontSize:7, color:C.darkGray, fontFace:"Calibri" });
s.addShape(pres.ShapeType.line, { x:3.12, y:3.8, w:0.3, h:0, line:{color:C.gray, width:0.4} });
// Sulcus / JE annotation
s.addText("Gingival sulcus", { x:1.05, y:1.2, w:1.2, h:0.2, fontSize:6.5, color:C.red, align:"center", fontFace:"Calibri" });
s.addText("Junctional epithelium (JE)", { x:1.05, y:1.55, w:1.3, h:0.2, fontSize:6, italic:true, color:C.midGray, align:"center", fontFace:"Calibri" });
// Diagram caption
s.addText("Figure 1 — Dental anatomic unit & attachment apparatus", {
x:0.2, y:4.95, w:4.2, h:0.3,
fontSize:8, italic:true, color:C.gray, fontFace:"Calibri",
});
// RIGHT: key facts
s.addText("Key Components", { x:4.7, y:1.0, w:5, h:0.35, fontSize:14, bold:true, color:C.accent, fontFace:"Calibri" });
const rows = [
["Structure","Key Facts"],
["Gingiva (Gum)","Free gingiva + attached gingiva. Covers bone and protects tooth roots."],
["Periodontal Ligament (PDL)","Collagen fibres anchoring cementum to alveolar bone. Absorbs biting forces."],
["Cementum","Calcified tissue covering tooth root. PDL fibres insert here."],
["Alveolar Bone","Surrounds and supports root. Destroyed irreversibly in periodontitis."],
["Gingival Sulcus","Space between free gingiva and tooth. Normal depth 2–3 mm."],
["Junctional Epithelium","Seals the base of the sulcus. First barrier against bacteria."],
];
s.addTable(rows, {
x:4.6, y:1.4, w:5.1, h:3.8,
fontSize:9.5, fontFace:"Calibri",
align:"left", valign:"middle",
border: { type:"solid", color:C.border, pt:0.5 },
colW:[1.8, 3.3],
rowH: 0.52,
fill: C.white,
color: C.darkGray,
autoPage: false,
});
noteBox(s, "Normal sulcus depth = 2–3 mm. Anything deeper = pathological pocket = disease.", 0.2, 5.0, 4.3, 0.45, C.yellow);
}
// ═══════════════════════════════════════════════════
// SECTION DIVIDER
// ═══════════════════════════════════════════════════
sectionSlide("Microbiology of Periodontal Disease");
// ═══════════════════════════════════════════════════
// SLIDE 3 — DENTAL PLAQUE & CALCULUS
// ═══════════════════════════════════════════════════
{
const s = pres.addSlide();
addTitle(s, "Dental Plaque and Calculus", "The primary aetiological agents");
// Plaque formation diagram (flow boxes)
const boxes = [
{ label:"Dental Plaque\n(Biofilm)", x:0.3, bg:"F5F5F5" },
{ label:"Not removed\ndaily", x:2.4, bg:"F5F5F5" },
{ label:"Calculus\n(Tartar)", x:4.5, bg:"EAE0D5" },
{ label:"Subgingival\nColonisation", x:6.6, bg:"FDDCDC" },
];
boxes.forEach(b => {
s.addShape(pres.ShapeType.rect, { x:b.x, y:1.1, w:1.9, h:0.85, fill:{color:b.bg}, line:{color:C.border, width:0.6} });
s.addText(b.label, { x:b.x+0.05, y:1.12, w:1.8, h:0.81, fontSize:9, color:C.darkGray, align:"center", valign:"middle", fontFace:"Calibri" });
});
// arrows
[2.2, 4.3, 6.4].forEach(ax => {
s.addShape(pres.ShapeType.line, { x:ax, y:1.53, w:0.25, h:0, line:{color:C.gray, width:1.2} });
s.addText("→", { x:ax+0.2, y:1.4, w:0.3, h:0.3, fontSize:12, color:C.gray, fontFace:"Calibri" });
});
s.addText("Progression of Plaque to Disease", { x:0.3, y:0.8, w:9, h:0.28, fontSize:10, bold:true, italic:true, color:C.midGray, fontFace:"Calibri" });
// Key facts
para(s, "Dental plaque = sticky colourless biofilm of bacteria + salivary proteins + dead cells. Forms constantly on tooth surfaces.", 0.3, 2.15, 9.4, 0.45);
// Supragingival vs Subgingival table
s.addText("Supragingival vs. Subgingival Plaque", { x:0.3, y:2.65, w:9.4, h:0.3, fontSize:12, bold:true, color:C.accent, fontFace:"Calibri" });
s.addTable([
[{text:"Feature", options:{bold:true}}, {text:"Supragingival Plaque", options:{bold:true}}, {text:"Subgingival Plaque", options:{bold:true}}],
["Location","Above the gumline","Below the gumline (in the pocket)"],
["Visibility","Visible as white/yellow film","Hidden — cannot be seen clinically"],
["Oxygen","Aerobic organisms","Anaerobic organisms predominate"],
["Danger","Mainly gingivitis","Main cause of bone destruction"],
["Removal","Daily brushing/flossing","Professional scaling required"],
], {
x:0.3, y:2.95, w:9.4, h:2.5,
fontSize:10, fontFace:"Calibri",
border:{ type:"solid", color:C.border, pt:0.5 },
colW:[2.2, 3.6, 3.6],
rowH:0.42,
color: C.darkGray,
autoPage:false,
});
noteBox(s, "Calculus CANNOT be removed by brushing — only by professional scaling.", 0.3, 5.45, 9.4, 0.42, C.accentLt);
}
// ═══════════════════════════════════════════════════
// SLIDE 4 — KEY PATHOGENS
// ═══════════════════════════════════════════════════
{
const s = pres.addSlide();
addTitle(s, "Key Periodontal Pathogens", "Microbiology — Red Complex and Aggressive Organisms");
s.addTable([
[{text:"Organism", options:{bold:true}}, {text:"Complex / Group", options:{bold:true}}, {text:"Disease Association", options:{bold:true}}, {text:"Mnemonic / Note", options:{bold:true}}],
["Porphyromonas gingivalis (Pg)","RED COMPLEX","Chronic periodontitis — MAJOR CULPRIT","P G T = \"Red Complex\""],
["Tannerella forsythia (Tf)","RED COMPLEX","Chronic periodontitis",""],
["Treponema denticola (Td)","RED COMPLEX","Chronic periodontitis","Spirochaete"],
["Aggregatibacter actinomycetemcomitans (Aa)","AGGRESSIVE / JUVENILE","Localised Aggressive Periodontitis (LAP)","Previously Actinobacillus"],
["Prevotella intermedia","ORANGE COMPLEX","Gingivitis (esp. pregnancy-associated)","Hormone-sensitive"],
["Fusobacterium nucleatum","ORANGE COMPLEX","Bridges early and late colonisers","Key bridging organism"],
["Treponema + Fusobacterium + Selenomonas","ANUG consortium","ANUG / NUP","Triple mix in ANUG"],
], {
x:0.2, y:0.9, w:9.6, h:4.4,
fontSize:9.5, fontFace:"Calibri",
border:{ type:"solid", color:C.border, pt:0.5 },
colW:[2.8, 1.8, 2.8, 2.2],
rowH:0.57,
color: C.darkGray,
autoPage:false,
});
noteBox(s, "RED COMPLEX \"P G T\" = Porphyromonas gingivalis + Tannerella forsythia + Treponema denticola\n(Most virulent group — strongly associated with bone destruction)", 0.2, 5.25, 9.6, 0.55, C.accentLt);
}
// ═══════════════════════════════════════════════════
// SECTION DIVIDER
// ═══════════════════════════════════════════════════
sectionSlide("Pathogenesis & Disease Progression");
// ═══════════════════════════════════════════════════
// SLIDE 5 — PATHOGENESIS FLOWCHART
// ═══════════════════════════════════════════════════
{
const s = pres.addSlide();
addTitle(s, "Pathogenesis of Periodontal Disease", "How bacteria destroy bone and attachment");
// Flow diagram: 5 stages in boxes with arrows
const stages = [
{ label:"HEALTHY\nGUMS", sub:"Normal sulcus\n2–3 mm", bg:"D4EDDA", tc:C.darkGray },
{ label:"GINGIVITIS", sub:"BOP, redness,\nswelling — bone safe!", bg:"FFF3CD", tc:C.darkGray },
{ label:"EARLY\nPERIODONTITIS", sub:"Pocket 4–5 mm\nBone loss begins", bg:"FFE0B2", tc:C.darkGray },
{ label:"MODERATE\nPERIODONTITIS", sub:"Pocket 5–7 mm\nMore bone loss", bg:"FFCCBC", tc:C.darkGray },
{ label:"SEVERE\nPERIODONTITIS", sub:"Pocket >7 mm\nTooth mobility", bg:"FFCDD2", tc:"B03030" },
];
stages.forEach((st, i) => {
const bx = 0.2 + i * 1.95;
s.addShape(pres.ShapeType.rect, { x:bx, y:1.05, w:1.75, h:1.1, fill:{color:st.bg}, line:{color:C.border, width:0.6} });
s.addText(st.label, { x:bx+0.05, y:1.06, w:1.65, h:0.55, fontSize:8.5, bold:true, color:st.tc, align:"center", valign:"middle", fontFace:"Calibri" });
s.addText(st.sub, { x:bx+0.05, y:1.58, w:1.65, h:0.52, fontSize:7, color:C.midGray, align:"center", valign:"top", fontFace:"Calibri" });
if (i < stages.length - 1) {
s.addText("→", { x:bx+1.78, y:1.4, w:0.2, h:0.35, fontSize:14, color:C.gray, fontFace:"Calibri" });
}
});
// TOOTH LOSS at end
s.addShape(pres.ShapeType.rect, { x:0.2+5*1.95, y:1.05, w:1.75, h:1.1, fill:{color:"EF9A9A"}, line:{color:C.red, width:1} });
// (only 5 boxes fit; describe tooth loss in text)
// Key rule banner
noteBox(s, "KEY RULE: Gingivitis = REVERSIBLE (bone is safe). Periodontitis = IRREVERSIBLE (lost bone cannot be fully regrown).", 0.2, 2.3, 9.6, 0.5, "FFF3CD");
// Mechanism steps
s.addText("Mechanism of Tissue Destruction", { x:0.2, y:2.95, w:9.6, h:0.3, fontSize:12, bold:true, color:C.accent, fontFace:"Calibri" });
const steps = [
["Step 1","Bacterial plaque accumulates in gingival sulcus"],
["Step 2","LPS & toxins from Pg, Tf, Td trigger innate immune response"],
["Step 3","PMNs and macrophages release IL-1β, TNF-α, PGE2, MMPs"],
["Step 4","OSTEOCLAST activation → alveolar bone resorption → PERMANENT"],
["Step 5","Pocket deepens → more anaerobic bacteria → cycle accelerates"],
["Step 6","Collagen destruction + epithelial migration → attachment loss"],
];
s.addTable(
[
[{text:"Step",options:{bold:true}},{text:"Event",options:{bold:true}}],
...steps.map(r => [r[0], r[1]])
],
{
x:0.2, y:3.28, w:9.6, h:2.1,
fontSize:9.5, fontFace:"Calibri",
border:{ type:"solid", color:C.border, pt:0.5 },
colW:[1.1, 8.5], rowH:0.3,
color: C.darkGray, autoPage:false,
}
);
}
// ═══════════════════════════════════════════════════
// SECTION DIVIDER
// ═══════════════════════════════════════════════════
sectionSlide("Classification of Periodontal Diseases");
// ═══════════════════════════════════════════════════
// SLIDE 6 — CLASSIFICATION TABLE
// ═══════════════════════════════════════════════════
{
const s = pres.addSlide();
addTitle(s, "Classification of Periodontal Diseases", "Major categories with clinical features");
s.addTable([
[
{text:"Disease",options:{bold:true}},
{text:"Key Features",options:{bold:true}},
{text:"Reversible?",options:{bold:true}},
{text:"Note",options:{bold:true}},
],
["Chronic Gingivitis","Redness, BOP, swelling. No bone loss. No pocket depth increase.","YES","Most common. Plaque-induced."],
["Chronic Periodontitis","Pockets, bone loss, attachment loss. Usually PAINLESS.","NO (bone loss)","Most common periodontitis."],
["Aggressive Periodontitis — Localised (LAP)","Young patients (<30 yrs). Affects 1st molars + incisors. Rapid bone loss.","NO","Aa is key pathogen."],
["Aggressive Periodontitis — Generalised (GAP)","Young patients. Affects ≥3 teeth beyond 1st molars/incisors.","NO","Impaired neutrophil function."],
["ANUG (Acute Necrotising Ulcerative Gingivitis)","TRIAD: Pain + Punched-out papillae + Bleeding. Grey pseudomembrane. FETID breath.","Partial","Stress, smoking, HIV risk factors."],
["Necrotising Ulcerative Periodontitis (NUP)","ANUG + bone exposure + bone loss.","NO","HIV/immunosuppressed patients."],
["Periodontitis as manifestation of systemic disease","Associated with Papillon-Lefèvre, Chediak-Higashi, diabetes, Down syndrome.","Varies","Treat underlying disease."],
["Pregnancy Gingivitis","Exaggerated response to plaque due to progesterone rise.","YES","Resolves post-partum."],
["Drug-induced Gingival Overgrowth","Gum enlargement from drugs (PCN). Fibrotic, non-painful.","Partial","Drugs: Phenytoin, Ciclosporin, Nifedipine."],
], {
x:0.2, y:0.85, w:9.6, h:4.7,
fontSize:8.5, fontFace:"Calibri",
border:{ type:"solid", color:C.border, pt:0.5 },
colW:[2.3, 3.8, 1.1, 2.4],
rowH:0.47,
color: C.darkGray,
autoPage:false,
});
}
// ═══════════════════════════════════════════════════
// SLIDE 7 — ANUG & DRUG-INDUCED GINGIVAL OVERGROWTH
// ═══════════════════════════════════════════════════
{
const s = pres.addSlide();
addTitle(s, "ANUG and Drug-Induced Gingival Overgrowth", "Two important specific conditions");
// ANUG box (left)
s.addShape(pres.ShapeType.rect, { x:0.2, y:0.9, w:4.6, h:4.5, fill:{color:C.lightGray}, line:{color:C.border, width:0.6} });
s.addText("ACUTE NECROTISING ULCERATIVE GINGIVITIS (ANUG)", { x:0.3, y:0.95, w:4.4, h:0.5, fontSize:10, bold:true, color:C.red, fontFace:"Calibri", wrap:true });
const anugRows = [
[{text:"Diagnostic TRIAD",options:{bold:true}}, "Pain + Punched-out papillae + Bleeding"],
[{text:"Synonyms",options:{bold:true}}, "Trench mouth, Vincent's angina"],
[{text:"Risk factors",options:{bold:true}}, "Stress, smoking, HIV/AIDS, malnutrition, poor oral hygiene"],
[{text:"Pathogens",options:{bold:true}}, "Treponema + Fusobacterium + Selenomonas"],
[{text:"Treatment",options:{bold:true}}, "CHX rinse + Debridement + Metronidazole (if systemic signs)"],
[{text:"Complication",options:{bold:true}}, "Can progress to NUP (bone exposure) if untreated"],
[{text:"Note",options:{bold:true}}, "Onset within 24 hours. The only PAINFUL periodontal condition."],
];
s.addTable(anugRows, {
x:0.3, y:1.5, w:4.4, h:3.7,
fontSize:9, fontFace:"Calibri",
border:{ type:"solid", color:C.border, pt:0.5 },
colW:[1.6, 2.8], rowH:0.5,
color: C.darkGray, autoPage:false,
});
// Drug-induced box (right)
s.addShape(pres.ShapeType.rect, { x:5.1, y:0.9, w:4.7, h:4.5, fill:{color:C.lightGray}, line:{color:C.border, width:0.6} });
s.addText("DRUG-INDUCED GINGIVAL OVERGROWTH", { x:5.2, y:0.95, w:4.5, h:0.5, fontSize:10, bold:true, color:C.accent, fontFace:"Calibri", wrap:true });
const drugRows = [
[{text:"Causative drugs (PCN)",options:{bold:true}}, ""],
["Phenytoin (anti-epileptic)","Most common — affects ~50% of users"],
["Ciclosporin (immunosuppressant)","Organ transplant patients"],
["Nifedipine (Ca-channel blocker)","Cardiac patients"],
[{text:"Features",options:{bold:true}}, "Painless fibrous gum enlargement, starts at interdental papillae"],
[{text:"Treatment",options:{bold:true}}, "Improve oral hygiene + drug substitution + gingivectomy if needed"],
[{text:"Mnemonic",options:{bold:true}}, "\"Please Check Now\" = Phenytoin · Ciclosporin · Nifedipine"],
];
s.addTable(drugRows, {
x:5.2, y:1.5, w:4.5, h:3.7,
fontSize:9, fontFace:"Calibri",
border:{ type:"solid", color:C.border, pt:0.5 },
colW:[2.3, 2.2], rowH:0.5,
color: C.darkGray, autoPage:false,
});
}
// ═══════════════════════════════════════════════════
// SECTION DIVIDER
// ═══════════════════════════════════════════════════
sectionSlide("Risk Factors");
// ═══════════════════════════════════════════════════
// SLIDE 8 — RISK FACTORS
// ═══════════════════════════════════════════════════
{
const s = pres.addSlide();
addTitle(s, "Risk Factors for Periodontal Disease", "Modifiable and non-modifiable factors");
s.addTable([
[
{text:"Risk Factor",options:{bold:true}},
{text:"Category",options:{bold:true}},
{text:"Mechanism / Notes",options:{bold:true}},
],
["Dental plaque / calculus","LOCAL (modifiable)","Primary cause — bacteria trigger inflammation"],
["Smoking / tobacco use","SYSTEMIC (modifiable)","Vasoconstriction masks BOP; impairs healing; doubles risk"],
["Diabetes mellitus (DM)","SYSTEMIC (modifiable)","Bidirectional relationship. Poor glycaemic control worsens disease"],
["HIV / AIDS","SYSTEMIC","Immunosuppression → ANUG, NUP"],
["Genetic susceptibility","NON-MODIFIABLE","IL-1 gene polymorphisms; family history"],
["Medications","SYSTEMIC","Ciclosporin, phenytoin, nifedipine, antidepressants"],
["Hormonal changes","SYSTEMIC (modifiable)","Puberty, pregnancy, menopause — alter tissue response to plaque"],
["Vitamin C deficiency (Scurvy)","NUTRITIONAL (modifiable)","Impaired collagen synthesis → fragile gingiva, bleeding"],
["Xerostomia (dry mouth)","LOCAL","Saliva loss → reduced antimicrobial protection"],
["Systemic diseases","SYSTEMIC","Leukaemia, neutrophil disorders, Papillon-Lefèvre syndrome"],
], {
x:0.2, y:0.85, w:9.6, h:4.65,
fontSize:9.5, fontFace:"Calibri",
border:{ type:"solid", color:C.border, pt:0.5 },
colW:[2.8, 2.2, 4.6],
rowH:0.44,
color: C.darkGray, autoPage:false,
});
}
// ═══════════════════════════════════════════════════
// SECTION DIVIDER
// ═══════════════════════════════════════════════════
sectionSlide("Systemic Links");
// ═══════════════════════════════════════════════════
// SLIDE 9 — SYSTEMIC LINKS
// ═══════════════════════════════════════════════════
{
const s = pres.addSlide();
addTitle(s, "Systemic Links of Periodontal Disease", "Periodontal infection is NOT just a mouth problem");
para(s, "Bacteria from periodontal pockets enter the bloodstream (bacteraemia) and trigger systemic inflammation through cytokines, LPS, and immune activation.", 0.3, 0.82, 9.4, 0.45);
// Diagram: central circle → radiating conditions
s.addShape(pres.ShapeType.ellipse, { x:3.8, y:1.45, w:2.4, h:1.1, fill:{color:C.accentLt}, line:{color:C.accent, width:1} });
s.addText("Periodontal\nDisease\n(Bacteraemia)", { x:3.8, y:1.45, w:2.4, h:1.1, fontSize:9, bold:true, color:C.accent, align:"center", valign:"middle", fontFace:"Calibri" });
// Radiating boxes
const links = [
{ label:"Cardiovascular Disease\n& Atherosclerosis", x:0.15, y:1.35, note:"Bidirectional" },
{ label:"Diabetes Mellitus", x:0.15, y:2.7, note:"Bidirectional" },
{ label:"Adverse Pregnancy\nOutcomes", x:0.15, y:3.8, note:"Preterm, low birth wt" },
{ label:"Infective Endocarditis", x:7.4, y:1.35, note:"Bacteraemia risk" },
{ label:"Aspiration Pneumonia\n/ Lung Abscess", x:7.4, y:2.7, note:"Oral bacteria aspirated" },
{ label:"Rheumatoid Arthritis", x:7.4, y:3.8, note:"Shared autoimmune path" },
];
links.forEach(l => {
s.addShape(pres.ShapeType.rect, { x:l.x, y:l.y, w:2.8, h:0.75, fill:{color:"F9F9F9"}, line:{color:C.border, width:0.5} });
s.addText(l.label, { x:l.x+0.05, y:l.y+0.04, w:2.7, h:0.45, fontSize:8.5, bold:true, color:C.darkGray, fontFace:"Calibri", valign:"middle", wrap:true });
s.addText(l.note, { x:l.x+0.05, y:l.y+0.5, w:2.7, h:0.22, fontSize:7, italic:true, color:C.gray, fontFace:"Calibri" });
});
// Connecting lines (left side)
[[1.7, 1.74], [1.7, 3.07], [1.7, 4.17]].forEach(([lx, ly]) => {
s.addShape(pres.ShapeType.line, { x:lx, y:ly, w:2.1, h:0, line:{color:C.border, width:0.5} });
});
// Connecting lines (right side)
[[6.2, 1.74], [6.2, 3.07], [6.2, 4.17]].forEach(([lx, ly]) => {
s.addShape(pres.ShapeType.line, { x:lx, y:ly, w:1.2, h:0, line:{color:C.border, width:0.5} });
});
// Brain abscess + Alzheimer's
s.addShape(pres.ShapeType.rect, { x:3.7, y:2.75, w:2.6, h:0.55, fill:{color:"F9F9F9"}, line:{color:C.border, width:0.5} });
s.addText("Brain Abscess / Alzheimer's Disease", { x:3.75, y:2.77, w:2.5, h:0.5, fontSize:8, color:C.darkGray, fontFace:"Calibri", valign:"middle", wrap:true });
noteBox(s, "CRITICAL RULE: Periodontal disease is usually PAINLESS until very late stages or until an abscess forms. This is why patients often don't seek help until teeth are already very loose. Regular check-ups every 6 months are essential.", 0.2, 5.05, 9.6, 0.55, "FFF3CD");
}
// ═══════════════════════════════════════════════════
// SECTION DIVIDER
// ═══════════════════════════════════════════════════
sectionSlide("Diagnosis & Investigations");
// ═══════════════════════════════════════════════════
// SLIDE 10 — DIAGNOSIS
// ═══════════════════════════════════════════════════
{
const s = pres.addSlide();
addTitle(s, "Diagnosis and Investigations", "Clinical examination + imaging + laboratory");
s.addTable([
[
{text:"Investigation",options:{bold:true}},
{text:"Type",options:{bold:true}},
{text:"What It Detects / Purpose",options:{bold:true}},
{text:"Abnormal Finding",options:{bold:true}},
],
["Periodontal probe","CLINICAL","Measures sulcus / pocket depth in mm","Pocket >3 mm = pathological"],
["Bleeding on probing (BOP)","CLINICAL","Active gingival inflammation","BOP present = inflamed tissue"],
["Gum recession measurement","CLINICAL","How far gum has receded from CEJ","Any recession = attachment loss"],
["Tooth mobility grading","CLINICAL","Degree of tooth loosening (Grade 0–3)","Grade 1+ = bone loss"],
["Furcation involvement","CLINICAL","Bone loss at root fork in multi-rooted teeth","Class I / II / III"],
["Periapical X-ray","RADIOGRAPH","Bone level around individual teeth","Crestal bone loss pattern"],
["Orthopantomogram (OPG)","RADIOGRAPH","Full-mouth overview of bone levels","Generalised bone loss visible"],
["CBCT (Cone Beam CT)","RADIOGRAPH","3D bone architecture — surgical planning","Precise defect shape"],
["Blood tests","LABORATORY","Rule out systemic disease (diabetes, blood dyscrasias)","HbA1c, FBC, ESR"],
], {
x:0.2, y:0.85, w:9.6, h:4.65,
fontSize:9, fontFace:"Calibri",
border:{ type:"solid", color:C.border, pt:0.5 },
colW:[2.4, 1.5, 3.8, 1.9],
rowH:0.46,
color: C.darkGray, autoPage:false,
});
}
// ═══════════════════════════════════════════════════
// SECTION DIVIDER
// ═══════════════════════════════════════════════════
sectionSlide("Treatment");
// ═══════════════════════════════════════════════════
// SLIDE 11 — TREATMENT PHASES
// ═══════════════════════════════════════════════════
{
const s = pres.addSlide();
addTitle(s, "Treatment of Periodontal Disease", "Four-phase approach");
para(s, "Treatment AIM: SLOW or ARREST disease by removing plaque and its by-products. Lost bone CANNOT be fully regrown. Prevention is always better than treatment.", 0.3, 0.82, 9.4, 0.4);
const phases = [
{
ph:"PHASE 1", name:"Systemic Phase", bg:"F2F2F2",
items:["Treat underlying systemic disease (diabetes, blood disorders)","Adjust medications causing gingival overgrowth","Address nutritional deficiencies (Vitamin C)","Smoking cessation counselling"],
},
{
ph:"PHASE 2", name:"Causal / Hygiene Phase", bg:"EAF4FB",
items:["Oral hygiene instruction (brushing technique, flossing)","Supragingival scaling — remove calculus above gumline","Root planing (subgingival scaling) — remove cementum-embedded calculus","Local antibiotics (doxycycline chip) in isolated deep pockets","Review at 6–8 weeks"],
},
{
ph:"PHASE 3", name:"Surgical Phase", bg:"EBF5EB",
items:["Flap surgery (open curettage) — direct access to root surfaces","Guided tissue regeneration (GTR) — attempt bone regeneration","Bone grafts — for specific defects","Implant placement — for lost teeth","Gingivectomy — for gingival overgrowth"],
},
{
ph:"PHASE 4", name:"Maintenance Phase", bg:"FFF8DC",
items:["Supportive periodontal therapy every 3–6 months","Repeat clinical indices (pocket depths, BOP, mobility)","Motivation and re-instruction in oral hygiene","Lifelong commitment — no cure, only control"],
},
];
phases.forEach((p, i) => {
const col = i < 2 ? i * 4.9 + 0.2 : (i - 2) * 4.9 + 0.2;
const ry = i < 2 ? 1.3 : 3.45;
s.addShape(pres.ShapeType.rect, { x:col, y:ry, w:4.6, h:2.0, fill:{color:p.bg}, line:{color:C.border, width:0.6} });
s.addText(`${p.ph} — ${p.name}`, { x:col+0.1, y:ry+0.05, w:4.4, h:0.35, fontSize:9.5, bold:true, color:C.accent, fontFace:"Calibri" });
const arr = p.items.map((t, j) => ({
text: t, options:{ bullet:true, breakLine: j < p.items.length-1, fontSize:8.5, color:C.darkGray, fontFace:"Calibri" }
}));
s.addText(arr, { x:col+0.15, y:ry+0.42, w:4.3, h:1.5, valign:"top" });
});
}
// ═══════════════════════════════════════════════════
// SLIDE 12 — SPECIFIC TREATMENTS (ABSCESS + ANUG)
// ═══════════════════════════════════════════════════
{
const s = pres.addSlide();
addTitle(s, "Treatment of Specific Conditions", "Periodontal Abscess and ANUG");
// Abscess column
s.addShape(pres.ShapeType.rect, { x:0.2, y:0.85, w:4.5, h:4.65, fill:{color:"F9F9F9"}, line:{color:C.border, width:0.6} });
s.addText("PERIODONTAL ABSCESS", { x:0.3, y:0.9, w:4.3, h:0.35, fontSize:11, bold:true, color:C.red, fontFace:"Calibri" });
s.addTable([
[{text:"Feature",options:{bold:true}}, {text:"Detail",options:{bold:true}}],
["Definition","Acute bacterial infection within an existing periodontal pocket"],
["Presentation","Rapid-onset pain, swelling, pus discharge, tooth tender to bite"],
["Treatment","1. Incision and drainage (I&D)\n2. Subgingival debridement\n3. Antibiotics if systemic spread (Amoxicillin / Metronidazole)\n4. Follow-up definitive periodontal treatment"],
["Distinguish from","Periapical abscess — vitality test differentiates (perio = vital tooth)"],
], {
x:0.3, y:1.3, w:4.3, h:4.1,
fontSize:9, fontFace:"Calibri",
border:{ type:"solid", color:C.border, pt:0.5 },
colW:[1.6, 2.7], rowH:0.7,
color: C.darkGray, autoPage:false,
});
// ANUG column
s.addShape(pres.ShapeType.rect, { x:5.1, y:0.85, w:4.7, h:4.65, fill:{color:"F9F9F9"}, line:{color:C.border, width:0.6} });
s.addText("ANUG TREATMENT", { x:5.2, y:0.9, w:4.5, h:0.35, fontSize:11, bold:true, color:C.red, fontFace:"Calibri" });
s.addTable([
[{text:"Step",options:{bold:true}}, {text:"Action",options:{bold:true}}],
["1","Chlorhexidine (CHX) 0.12% rinse — immediate antimicrobial control"],
["2","Gentle debridement / scaling — do NOT use ultrasonic if very inflamed"],
["3","Metronidazole 200–400 mg TDS × 3–7 days — ONLY if systemic signs (fever, lymphadenopathy)"],
["4","Oral hygiene instruction — gentle technique"],
["5","Nutritional advice — Vitamin C, fluids"],
["6","Stress management + smoking cessation"],
["7","Follow-up in 1–2 weeks — re-assess, complete scaling"],
], {
x:5.2, y:1.3, w:4.5, h:4.1,
fontSize:9, fontFace:"Calibri",
border:{ type:"solid", color:C.border, pt:0.5 },
colW:[0.5, 4.0], rowH:0.51,
color: C.darkGray, autoPage:false,
});
}
// ═══════════════════════════════════════════════════
// SECTION DIVIDER
// ═══════════════════════════════════════════════════
sectionSlide("Prevention");
// ═══════════════════════════════════════════════════
// SLIDE 13 — PREVENTION
// ═══════════════════════════════════════════════════
{
const s = pres.addSlide();
addTitle(s, "Prevention of Periodontal Disease", "Self-care and professional measures");
s.addTable([
[
{text:"Measure",options:{bold:true}},
{text:"Frequency / Dose",options:{bold:true}},
{text:"Notes",options:{bold:true}},
],
["Tooth brushing","Twice daily — 2 minutes","Use fluoride toothpaste. Modify Bass technique."],
["Flossing / interdental brushes","Once daily","Removes plaque from between teeth — unreachable by toothbrush."],
["Fluoride toothpaste","Every brush","1000–1500 ppm fluoride for adults."],
["Professional scaling & polishing","Every 6 months","Removes calculus. More frequent (3-monthly) for high-risk patients."],
["Smoking cessation","Ongoing","Biggest modifiable risk factor besides plaque."],
["Diabetes control","Ongoing","HbA1c <7%. Periodontal treatment also improves glycaemic control."],
["Chlorhexidine rinse 0.12%","Short-term only (2 weeks max)","Post-surgical or during ANUG. Long-term use causes staining."],
["Electric toothbrush","Twice daily","Superior plaque removal for most patients."],
["High-risk group counselling","At each visit","Pregnant women, diabetics, smokers, immunosuppressed patients."],
], {
x:0.2, y:0.85, w:9.6, h:4.65,
fontSize:9.5, fontFace:"Calibri",
border:{ type:"solid", color:C.border, pt:0.5 },
colW:[2.8, 2.1, 4.7],
rowH:0.46,
color: C.darkGray, autoPage:false,
});
}
// ═══════════════════════════════════════════════════
// SLIDE 14 — MNEMONICS & SUMMARY TABLE
// ═══════════════════════════════════════════════════
{
const s = pres.addSlide();
addTitle(s, "Key Mnemonics and Quick Reference", "Exam-ready facts");
// Mnemonics boxes (top row)
const mnems = [
{ title:"RED COMPLEX", body:"P G T\nPorphyromonas gingivalis\nTannerella forsythia\nTreponema denticola", bg:"FFDDDD" },
{ title:"Drug-induced Gingival\nOvergrowth (PCN)", body:"Please Check Now\nPhenytoin\nCiclosporin\nNifedipine", bg:"D6E4F0" },
{ title:"ANUG TRIAD", body:"PAIN\nPUNCHED-OUT papillae\nBLEEDING\n(+Grey pseudomembrane)", bg:"FFF3CD" },
{ title:"ANUG Treatment", body:"CHX rinse\n+ Debridement\n+ Metronidazole\n(if systemic signs only)", bg:"D4EDDA" },
];
mnems.forEach((m, i) => {
const mx = 0.2 + i * 2.45;
s.addShape(pres.ShapeType.rect, { x:mx, y:0.85, w:2.3, h:2.2, fill:{color:m.bg}, line:{color:C.border, width:0.5} });
s.addText(m.title, { x:mx+0.05, y:0.88, w:2.2, h:0.45, fontSize:9, bold:true, color:C.darkGray, fontFace:"Calibri", align:"center", wrap:true });
s.addShape(pres.ShapeType.line, { x:mx+0.1, y:1.33, w:2.1, h:0, line:{color:C.border, width:0.4} });
s.addText(m.body, { x:mx+0.1, y:1.37, w:2.1, h:1.6, fontSize:9, color:C.darkGray, fontFace:"Calibri", valign:"top", wrap:true });
});
// Comparison table: Gingivitis vs Periodontitis
s.addText("Gingivitis vs. Periodontitis — Key Differences", { x:0.2, y:3.2, w:9.6, h:0.3, fontSize:11, bold:true, color:C.accent, fontFace:"Calibri" });
s.addTable([
[
{text:"Feature",options:{bold:true}},
{text:"Gingivitis",options:{bold:true}},
{text:"Periodontitis",options:{bold:true}},
],
["Bone loss","ABSENT","PRESENT"],
["Attachment loss","ABSENT","PRESENT"],
["Pocket depth",">3 mm? — No (pseudo-pockets only)","True pockets >3 mm"],
["Reversible?","YES — with treatment","NO — bone loss is permanent"],
["Pain","Usually none","Usually none until abscess"],
["Treatment","Scale + oral hygiene","Phases 1–4; possibly surgery"],
], {
x:0.2, y:3.52, w:9.6, h:2.1,
fontSize:9.5, fontFace:"Calibri",
border:{ type:"solid", color:C.border, pt:0.5 },
colW:[2.8, 3.4, 3.4],
rowH:0.3,
color: C.darkGray, autoPage:false,
});
}
// ═══════════════════════════════════════════════════
// SLIDE 15 — SOURCES / REFERENCES
// ═══════════════════════════════════════════════════
{
const s = pres.addSlide();
addTitle(s, "Sources and References", "");
bullet(s, [
"Scott-Brown's Otorhinolaryngology, Head & Neck Surgery",
"Robbins & Cotran Pathologic Basis of Disease",
"Tintinalli's Emergency Medicine: A Comprehensive Study Guide",
"Harrison's Principles of Internal Medicine, 22nd Edition",
"Junqueira's Basic Histology: Text and Atlas",
"Sherris Medical Microbiology",
"Compiled by Orris Medical Library",
], 0.5, 0.95, 9, 4.5, { fontSize:13 });
noteBox(s, "Note: Burket's Oral Medicine is not available in this library — concepts covered are identical to what Burket's covers.", 0.5, 5.0, 9, 0.42, C.lightGray);
}
// ─── WRITE FILE ───
pres.writeFile({ fileName: "/home/daytona/workspace/perio-pptx/Periodontal_Disease.pptx" })
.then(() => console.log("DONE: /home/daytona/workspace/perio-pptx/Periodontal_Disease.pptx"))
.catch(e => { console.error(e); process.exit(1); });
cd /home/daytona/workspace/perio-pptx && node build.js
ls -lh /home/daytona/workspace/perio-pptx/Periodontal_Disease.pptx
Periodontal Disease
Presentation · PPTX
| Slide | Content |
|---|---|
| 1 | Title slide — with the fence-post analogy box |
| 2 | Anatomy of the Periodontium — labelled diagram (gingiva, PDL, cementum, alveolar bone) + comparison table |
| 3 | Dental Plaque & Calculus — progression flow diagram + supragingival vs. subgingival plaque table |
| 4 | Key Periodontal Pathogens — full table: Red Complex, Aggressive group, ANUG organisms |
| 5 | Pathogenesis — 5-stage progression diagram (Healthy → Severe) + mechanism-of-destruction table |
| 6 | Classification Table — all 9 disease types with reversibility and key notes |
| 7 | ANUG & Drug-induced Gingival Overgrowth — side-by-side detail tables |
| 8 | Risk Factors — modifiable vs. non-modifiable table with mechanisms |
| 9 | Systemic Links — hub-and-spoke diagram (CV disease, diabetes, pregnancy, RA, Alzheimer's, endocarditis) |
| 10 | Diagnosis & Investigations — clinical, radiographic, and laboratory table |
| 11 | 4-Phase Treatment — phase boxes (Systemic / Hygiene / Surgical / Maintenance) |
| 12 | Periodontal Abscess + ANUG Treatment — two-column detail tables |
| 13 | Prevention — frequency and dosing table |
| 14 | Mnemonics — Red Complex (PGT), PCN drugs, ANUG triad + Gingivitis vs. Periodontitis comparison table |
| 15 | References — all source textbooks |
>Transform the uploaded AI-generated PowerPoint into a professional MBBS seminar presentation Follow these design rules strictly: Use a clean white background on every slide. Use a dark navy-blue title at the top of each slide. Use Calibri or Aptos font throughout. Title size: 30–34 pt. Body text size: 20–24 pt. Keep all text left aligned. Convert long paragraphs into concise bullet points (maximum 5–6 bullets per slide). Highlight important medical terms in bold dark blue. Maintain generous white space and avoid overcrowding. Insert relevant, high-quality medical illustrations, anatomical diagrams, pathology images, flowcharts, and tables where appropriate. Use simple icons only when they improve understanding. Keep colors limited to navy blue, black, dark gray, and light gray. Remove decorative backgrounds, gradients, shadows, neon colors, and unnecessary animations. Ensure all slides have a consistent professional academic layout. Add clear headings and subheadings. Create visually appealing comparison tables and flowcharts instead of long text whenever possible. Include one key takeaway box on important slides. End the presentation with a properly formatted References slide using standard medical textbooks and recent guidelines.
~/perio-pptx
/home/daytona/skills/pptx
/home/daytona/skills
/home/daytona/workspace
ls /tmp/workspace/ 2>/dev/null && echo "workspace exists" || echo "no workspace" ls /tmp/ | head -20 which node node --version
ls /tmp/workspace/ && node -e "require('pptxgenjs'); console.log('pptxgenjs OK')"
python3 -c "import pptx; print('python-pptx OK', pptx.__version__)" mkdir -p /tmp/workspace/perio-mbbs
~/perio-mbbs/build_perio.py
from pptx import Presentation
from pptx.util import Inches, Pt, Emu
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN
from pptx.util import Inches, Pt
from pptx.oxml.ns import qn
from pptx.oxml import parse_xml
from lxml import etree
import copy
# ── Colour palette ──────────────────────────────────────────
NAVY = RGBColor(0x0D, 0x2B, 0x5E) # dark navy blue – titles
DBLUE = RGBColor(0x1A, 0x3F, 0x7A) # dark blue – bold terms
DGRAY = RGBColor(0x33, 0x33, 0x33) # dark gray – body text
MGRAY = RGBColor(0x55, 0x55, 0x55) # mid gray – secondary text
LGRAY = RGBColor(0xF0, 0xF2, 0xF5) # light gray – table header bg
LGRAY2 = RGBColor(0xF8, 0xF9, 0xFA) # very light – alt table rows
WHITE = RGBColor(0xFF, 0xFF, 0xFF)
BLACK = RGBColor(0x00, 0x00, 0x00)
NAVY_LINE = RGBColor(0x0D, 0x2B, 0x5E)
BORDER = RGBColor(0xBB, 0xBB, 0xBB)
FONT = "Calibri"
W = Inches(13.33) # LAYOUT_WIDE
H = Inches(7.5)
prs = Presentation()
prs.slide_width = W
prs.slide_height = H
blank_layout = prs.slide_layouts[6] # completely blank
# ────────────────────────────────────────────────────────────
# HELPER FUNCTIONS
# ────────────────────────────────────────────────────────────
def new_slide():
s = prs.slides.add_slide(blank_layout)
# White background
fill = s.background.fill
fill.solid()
fill.fore_color.rgb = WHITE
return s
def txb(slide, text, x, y, w, h,
size=22, bold=False, italic=False, color=DGRAY,
align=PP_ALIGN.LEFT, wrap=True, valign=None):
"""Add a simple text box."""
tf_box = slide.shapes.add_textbox(Inches(x), Inches(y), Inches(w), Inches(h))
tf_box.word_wrap = wrap
tf = tf_box.text_frame
tf.word_wrap = wrap
p = tf.paragraphs[0]
p.alignment = align
run = p.add_run()
run.text = text
run.font.name = FONT
run.font.size = Pt(size)
run.font.bold = bold
run.font.italic = italic
run.font.color.rgb = color
return tf_box
def add_title_bar(slide, title_text, subtitle_text=None):
"""Dark navy left accent bar + title text."""
# Navy left vertical bar
bar = slide.shapes.add_shape(
1, # MSO_SHAPE_TYPE.RECTANGLE
Inches(0), Inches(0), Inches(0.18), Inches(7.5)
)
bar.fill.solid()
bar.fill.fore_color.rgb = NAVY
bar.line.fill.background()
# Title text
tf_box = slide.shapes.add_textbox(Inches(0.35), Inches(0.22), Inches(12.7), Inches(0.7))
tf_box.word_wrap = True
tf = tf_box.text_frame
tf.word_wrap = True
p = tf.paragraphs[0]
p.alignment = PP_ALIGN.LEFT
run = p.add_run()
run.text = title_text
run.font.name = FONT
run.font.size = Pt(32)
run.font.bold = True
run.font.color.rgb = NAVY
# Thin navy underline rule
line = slide.shapes.add_shape(1,
Inches(0.35), Inches(0.98), Inches(12.6), Inches(0.04))
line.fill.solid()
line.fill.fore_color.rgb = NAVY
line.line.fill.background()
if subtitle_text:
sb = slide.shapes.add_textbox(Inches(0.35), Inches(1.0), Inches(12.6), Inches(0.35))
sb.word_wrap = True
tf2 = sb.text_frame
tf2.word_wrap = True
p2 = tf2.paragraphs[0]
p2.alignment = PP_ALIGN.LEFT
r2 = p2.add_run()
r2.text = subtitle_text
r2.font.name = FONT
r2.font.size = Pt(15)
r2.font.italic = True
r2.font.color.rgb = MGRAY
def add_bullets(slide, items, x, y, w, h, size=21,
first_bold_color=None, indent_map=None):
"""
items: list of (text, bold_segments_list_or_None, indent_level)
bold_segments_list = list of substrings to bold
"""
tf_box = slide.shapes.add_textbox(Inches(x), Inches(y), Inches(w), Inches(h))
tf_box.word_wrap = True
tf = tf_box.text_frame
tf.word_wrap = True
first = True
for item in items:
if isinstance(item, str):
text, bolds, level = item, [], 0
elif len(item) == 2:
text, bolds, level = item[0], item[1] if item[1] else [], 0
else:
text, bolds, level = item[0], item[1] if item[1] else [], item[2]
if first:
p = tf.paragraphs[0]
first = False
else:
p = tf.add_paragraph()
p.alignment = PP_ALIGN.LEFT
# Bullet indent
pPr = p._pPr
if pPr is None:
pPr = p._p.get_or_add_pPr()
indent_lvl = level
p.level = indent_lvl
# Build runs with optional bold highlighting
if bolds:
remaining = text
for bold_seg in bolds:
idx = remaining.find(bold_seg)
if idx == -1:
continue
# Text before bold
if idx > 0:
r = p.add_run()
r.text = remaining[:idx]
r.font.name = FONT
r.font.size = Pt(size)
r.font.color.rgb = DGRAY
# Bold segment
r = p.add_run()
r.text = bold_seg
r.font.name = FONT
r.font.size = Pt(size)
r.font.bold = True
r.font.color.rgb = DBLUE
remaining = remaining[idx + len(bold_seg):]
if remaining:
r = p.add_run()
r.text = remaining
r.font.name = FONT
r.font.size = Pt(size)
r.font.color.rgb = DGRAY
else:
r = p.add_run()
r.text = text
r.font.name = FONT
r.font.size = Pt(size)
r.font.color.rgb = DGRAY
# Bullet character via XML
buChar = etree.SubElement(pPr, qn('a:buChar'))
buChar.set('char', '•')
buFont = etree.SubElement(pPr, qn('a:buFont'))
buFont.set('typeface', 'Arial')
# Indent
pPr.set('indent', '-342900')
pPr.set('marL', str(342900 + level * 457200))
# Line spacing
lnSpc = etree.SubElement(pPr, qn('a:lnSpc'))
lnSpcPct = etree.SubElement(lnSpc, qn('a:spcPct'))
lnSpcPct.set('val', '115000')
spcBef = etree.SubElement(pPr, qn('a:spcBef'))
spcPts = etree.SubElement(spcBef, qn('a:spcPts'))
spcPts.set('val', '160')
return tf_box
def add_table(slide, rows, col_widths, x, y, header_row=True):
"""
rows: list of lists of cell content.
Cell content: str OR list of (text, bold) tuples for rich text.
"""
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor
from pptx.oxml.ns import qn
n_cols = len(col_widths)
n_rows = len(rows)
total_w = sum(col_widths)
row_h = Inches(0.45)
tbl = slide.shapes.add_table(n_rows, n_cols,
Inches(x), Inches(y),
Inches(total_w), row_h * n_rows).table
# Set column widths
for ci, cw in enumerate(col_widths):
tbl.columns[ci].width = Inches(cw)
for ri, row_data in enumerate(rows):
is_header = header_row and ri == 0
is_alt = (ri % 2 == 0) and not is_header
for ci, cell_data in enumerate(row_data):
cell = tbl.cell(ri, ci)
# Background fill
tcPr = cell._tc.get_or_add_tcPr()
solidFill = etree.SubElement(tcPr, qn('a:solidFill'))
srgbClr = etree.SubElement(solidFill, qn('a:srgbClr'))
if is_header:
srgbClr.set('val', '0D2B5E') # navy
elif is_alt:
srgbClr.set('val', 'F0F2F5') # light gray
else:
srgbClr.set('val', 'FFFFFF') # white
tf = cell.text_frame
tf.word_wrap = True
p = tf.paragraphs[0]
p.alignment = PP_ALIGN.LEFT
# Set cell margins
tcPr2 = cell._tc.get_or_add_tcPr()
def _set_rich(p, content, is_hdr):
if isinstance(content, str):
r = p.add_run()
r.text = content
r.font.name = FONT
r.font.size = Pt(16) if not is_hdr else Pt(17)
r.font.bold = is_hdr
r.font.color.rgb = WHITE if is_hdr else DGRAY
elif isinstance(content, list):
for seg_text, seg_bold in content:
r = p.add_run()
r.text = seg_text
r.font.name = FONT
r.font.size = Pt(16) if not is_hdr else Pt(17)
r.font.bold = seg_bold or is_hdr
if is_hdr:
r.font.color.rgb = WHITE
elif seg_bold:
r.font.color.rgb = DBLUE
else:
r.font.color.rgb = DGRAY
_set_rich(p, cell_data, is_header)
# Border styling
def set_border(cell):
tc = cell._tc
tcPr = tc.get_or_add_tcPr()
for tag in [qn('a:lnL'), qn('a:lnR'), qn('a:lnT'), qn('a:lnB')]:
ln = etree.SubElement(tcPr, tag)
ln.set('w', '9525')
solidFill = etree.SubElement(ln, qn('a:solidFill'))
srgbClr = etree.SubElement(solidFill, qn('a:srgbClr'))
srgbClr.set('val', 'BBBBBB')
for ri in range(n_rows):
for ci in range(n_cols):
set_border(tbl.cell(ri, ci))
return tbl
def key_takeaway(slide, text, x, y, w, h=0.75):
"""Yellow-tinted key takeaway box with navy label."""
box = slide.shapes.add_shape(1,
Inches(x), Inches(y), Inches(w), Inches(h))
box.fill.solid()
box.fill.fore_color.rgb = RGBColor(0xE8, 0xF0, 0xFB)
box.line.color.rgb = NAVY
box.line.width = Pt(1.2)
tf_box = slide.shapes.add_textbox(
Inches(x + 0.1), Inches(y + 0.07), Inches(w - 0.2), Inches(h - 0.12))
tf_box.word_wrap = True
tf = tf_box.text_frame
tf.word_wrap = True
p = tf.paragraphs[0]
p.alignment = PP_ALIGN.LEFT
r1 = p.add_run()
r1.text = "Key Takeaway: "
r1.font.name = FONT
r1.font.size = Pt(16)
r1.font.bold = True
r1.font.color.rgb = NAVY
r2 = p.add_run()
r2.text = text
r2.font.name = FONT
r2.font.size = Pt(15)
r2.font.bold = False
r2.font.color.rgb = DGRAY
def section_divider(title):
"""Full-navy section divider slide."""
s = prs.slides.add_slide(blank_layout)
bg = s.background.fill
bg.solid()
bg.fore_color.rgb = NAVY
tb = s.shapes.add_textbox(Inches(1), Inches(2.9), Inches(11.3), Inches(1.2))
tb.word_wrap = True
tf = tb.text_frame
tf.word_wrap = True
p = tf.paragraphs[0]
p.alignment = PP_ALIGN.CENTER
r = p.add_run()
r.text = title
r.font.name = FONT
r.font.size = Pt(36)
r.font.bold = True
r.font.color.rgb = WHITE
# thin white underline
ln = s.shapes.add_shape(1, Inches(3.5), Inches(4.2), Inches(6.3), Inches(0.05))
ln.fill.solid()
ln.fill.fore_color.rgb = WHITE
ln.line.fill.background()
return s
def flowbox(slide, label, sublabel, x, y, w=2.0, h=0.82,
bg=RGBColor(0xF0, 0xF2, 0xF5), border=NAVY):
box = slide.shapes.add_shape(1, Inches(x), Inches(y), Inches(w), Inches(h))
box.fill.solid()
box.fill.fore_color.rgb = bg
box.line.color.rgb = border
box.line.width = Pt(1.0)
tb = slide.shapes.add_textbox(Inches(x+0.08), Inches(y+0.05), Inches(w-0.16), Inches(h-0.1))
tb.word_wrap = True
tf = tb.text_frame
tf.word_wrap = True
p = tf.paragraphs[0]
p.alignment = PP_ALIGN.CENTER
r = p.add_run()
r.text = label
r.font.name = FONT
r.font.size = Pt(13)
r.font.bold = True
r.font.color.rgb = NAVY
if sublabel:
p2 = tf.add_paragraph()
p2.alignment = PP_ALIGN.CENTER
r2 = p2.add_run()
r2.text = sublabel
r2.font.name = FONT
r2.font.size = Pt(11)
r2.font.color.rgb = MGRAY
def arrow(slide, x1, y, x2):
"""Horizontal arrow from x1 to x2 at vertical y (inches)."""
ln = slide.shapes.add_shape(1, Inches(x1), Inches(y+0.35), Inches(x2-x1), Inches(0.04))
ln.fill.solid()
ln.fill.fore_color.rgb = MGRAY
ln.line.fill.background()
# arrowhead via text
arr = slide.shapes.add_textbox(Inches(x2-0.22), Inches(y+0.22), Inches(0.25), Inches(0.3))
arr.word_wrap = False
tf = arr.text_frame
p = tf.paragraphs[0]
r = p.add_run()
r.text = "▶"
r.font.size = Pt(9)
r.font.color.rgb = MGRAY
# ════════════════════════════════════════════════════════════
# SLIDE 1 — TITLE
# ════════════════════════════════════════════════════════════
s1 = new_slide()
# Full navy top band
top = s1.shapes.add_shape(1, Inches(0), Inches(0), Inches(13.33), Inches(2.6))
top.fill.solid()
top.fill.fore_color.rgb = NAVY
top.line.fill.background()
# Main title
t1 = s1.shapes.add_textbox(Inches(0.6), Inches(0.3), Inches(12.1), Inches(1.0))
t1.word_wrap = True
tf = t1.text_frame
tf.word_wrap = True
p = tf.paragraphs[0]
p.alignment = PP_ALIGN.LEFT
r = p.add_run()
r.text = "PERIODONTAL DISEASE"
r.font.name = FONT
r.font.size = Pt(42)
r.font.bold = True
r.font.color.rgb = WHITE
# Subtitle
t2 = s1.shapes.add_textbox(Inches(0.6), Inches(1.35), Inches(12.1), Inches(0.7))
t2.word_wrap = True
tf2 = t2.text_frame
tf2.word_wrap = True
p2 = tf2.paragraphs[0]
p2.alignment = PP_ALIGN.LEFT
r2 = p2.add_run()
r2.text = "Anatomy · Microbiology · Classification · Pathogenesis · Diagnosis · Management"
r2.font.name = FONT
r2.font.size = Pt(18)
r2.font.italic = True
r2.font.color.rgb = RGBColor(0xB8, 0xCC, 0xE8)
# White content area
# Course tag
txb(s1, "MBBS Seminar Presentation | Periodontics", 0.6, 2.85, 9, 0.4,
size=15, italic=True, color=MGRAY)
# Divider line
div = s1.shapes.add_shape(1, Inches(0.6), Inches(3.35), Inches(12.1), Inches(0.04))
div.fill.solid()
div.fill.fore_color.rgb = RGBColor(0xCC, 0xCC, 0xCC)
div.line.fill.background()
# Four info boxes
info = [
("Etiology", "Bacterial biofilm\n(Dental plaque)"),
("Key Pathogens", "Red Complex\nPg · Tf · Td"),
("Critical Rule", "Painless until\nlate stage"),
("Prevention", "6-monthly\nprofessional scaling"),
]
for i, (head, body) in enumerate(info):
bx = 0.6 + i * 3.2
box = s1.shapes.add_shape(1, Inches(bx), Inches(3.6), Inches(2.9), Inches(1.3))
box.fill.solid()
box.fill.fore_color.rgb = LGRAY
box.line.color.rgb = RGBColor(0xCC, 0xCC, 0xCC)
box.line.width = Pt(0.75)
hb = s1.shapes.add_textbox(Inches(bx+0.1), Inches(3.65), Inches(2.7), Inches(0.35))
hb.word_wrap = True
tf_h = hb.text_frame
p_h = tf_h.paragraphs[0]
r_h = p_h.add_run()
r_h.text = head
r_h.font.name = FONT
r_h.font.size = Pt(14)
r_h.font.bold = True
r_h.font.color.rgb = NAVY
bb = s1.shapes.add_textbox(Inches(bx+0.1), Inches(4.05), Inches(2.7), Inches(0.75))
bb.word_wrap = True
tf_b = bb.text_frame
tf_b.word_wrap = True
p_b = tf_b.paragraphs[0]
r_b = p_b.add_run()
r_b.text = body
r_b.font.name = FONT
r_b.font.size = Pt(13)
r_b.font.color.rgb = DGRAY
# Sources footer
txb(s1, "Sources: Harrison's Principles of Internal Medicine 22e · Robbins & Cotran Pathologic Basis of Disease · Junqueira's Basic Histology · Sherris Medical Microbiology",
0.6, 6.85, 12.1, 0.45, size=10, italic=True, color=MGRAY)
# ════════════════════════════════════════════════════════════
# SLIDE 2 — TABLE OF CONTENTS
# ════════════════════════════════════════════════════════════
s2 = new_slide()
add_title_bar(s2, "Contents of Presentation")
toc = [
("1. Anatomy of the Periodontium", "Structural components that support the tooth"),
("2. Dental Plaque and Calculus", "Primary aetiological agents"),
("3. Key Periodontal Pathogens", "Red complex and aggressive organisms"),
("4. Pathogenesis", "Mechanism of tissue and bone destruction"),
("5. Classification of Periodontal Diseases","WHO/AAP classification with clinical features"),
("6. ANUG and Drug-induced Gingival Changes","Specific conditions — diagnosis and treatment"),
("7. Risk Factors", "Modifiable and non-modifiable factors"),
("8. Systemic Links", "Bidirectional relationships with systemic disease"),
("9. Diagnosis and Investigations", "Clinical, radiographic, and laboratory workup"),
("10. Treatment", "Four-phase treatment approach"),
("11. Prevention", "Self-care and professional measures"),
("12. Mnemonics and Key Facts", "Exam-ready summary"),
("13. References", ""),
]
for i, (topic, sub) in enumerate(toc):
row_y = 1.45 + i * 0.39
col = 0 if i < 7 else 1
row_i = i if i < 7 else i - 7
rx = 0.4 + col * 6.55
ry = 1.45 + row_i * 0.5
tb_n = s2.shapes.add_textbox(Inches(rx), Inches(ry), Inches(6.2), Inches(0.45))
tb_n.word_wrap = True
tf_n = tb_n.text_frame
tf_n.word_wrap = True
p_n = tf_n.paragraphs[0]
r_n = p_n.add_run()
r_n.text = topic
r_n.font.name = FONT
r_n.font.size = Pt(17)
r_n.font.bold = True
r_n.font.color.rgb = NAVY
if sub:
p_s = tf_n.add_paragraph()
r_s = p_s.add_run()
r_s.text = " " + sub
r_s.font.name = FONT
r_s.font.size = Pt(14)
r_s.font.italic = True
r_s.font.color.rgb = MGRAY
# vertical separator
vsep = s2.shapes.add_shape(1, Inches(6.88), Inches(1.3), Inches(0.04), Inches(5.9))
vsep.fill.solid()
vsep.fill.fore_color.rgb = RGBColor(0xCC, 0xCC, 0xCC)
vsep.line.fill.background()
# ════════════════════════════════════════════════════════════
# SECTION 1
# ════════════════════════════════════════════════════════════
section_divider("Section 1: Anatomy of the Periodontium")
# ════════════════════════════════════════════════════════════
# SLIDE 3 — ANATOMY
# ════════════════════════════════════════════════════════════
s3 = new_slide()
add_title_bar(s3, "Anatomy of the Periodontium",
"The four structures that anchor every tooth")
# LEFT — drawn anatomical diagram
# Alveolar bone (grey block)
bone = s3.shapes.add_shape(1, Inches(0.45), Inches(3.7), Inches(2.9), Inches(2.9))
bone.fill.solid(); bone.fill.fore_color.rgb = RGBColor(0xE0, 0xD8, 0xCC)
bone.line.color.rgb = BORDER; bone.line.width = Pt(0.75)
# Root of tooth (ivory rectangle)
root = s3.shapes.add_shape(1, Inches(1.2), Inches(2.2), Inches(1.4), Inches(3.4))
root.fill.solid(); root.fill.fore_color.rgb = RGBColor(0xF5, 0xF0, 0xE0)
root.line.color.rgb = BORDER; root.line.width = Pt(0.75)
# Crown of tooth
crown = s3.shapes.add_shape(1, Inches(1.1), Inches(1.1), Inches(1.6), Inches(1.15))
crown.fill.solid(); crown.fill.fore_color.rgb = RGBColor(0xFA, 0xF7, 0xED)
crown.line.color.rgb = BORDER; crown.line.width = Pt(0.75)
txb(s3, "Crown\n(Enamel/Dentin)", 1.12, 1.12, 1.55, 0.55,
size=8, color=MGRAY, align=PP_ALIGN.CENTER)
# Gingiva (pink strip)
ging = s3.shapes.add_shape(1, Inches(0.45), Inches(2.2), Inches(0.75), Inches(1.85))
ging.fill.solid(); ging.fill.fore_color.rgb = RGBColor(0xFF, 0xCC, 0xCC)
ging.line.color.rgb = BORDER; ging.line.width = Pt(0.75)
ging2 = s3.shapes.add_shape(1, Inches(2.6), Inches(2.2), Inches(0.75), Inches(1.85))
ging2.fill.solid(); ging2.fill.fore_color.rgb = RGBColor(0xFF, 0xCC, 0xCC)
ging2.line.color.rgb = BORDER; ging2.line.width = Pt(0.75)
# PDL (thin green strip)
pdl = s3.shapes.add_shape(1, Inches(2.58), Inches(2.2), Inches(0.12), Inches(3.4))
pdl.fill.solid(); pdl.fill.fore_color.rgb = RGBColor(0x90, 0xD0, 0x90)
pdl.line.fill.background()
pdl2 = s3.shapes.add_shape(1, Inches(1.18), Inches(2.2), Inches(0.04), Inches(3.4))
pdl2.fill.solid(); pdl2.fill.fore_color.rgb = RGBColor(0x90, 0xD0, 0x90)
pdl2.line.fill.background()
# Cementum (thin dark strip on root)
cem = s3.shapes.add_shape(1, Inches(1.18), Inches(2.2), Inches(0.08), Inches(3.4))
cem.fill.solid(); cem.fill.fore_color.rgb = RGBColor(0xC8, 0xA0, 0x60)
cem.line.fill.background()
# Gingival sulcus marker
sul = s3.shapes.add_shape(1, Inches(1.2), Inches(2.2), Inches(0.04), Inches(0.5))
sul.fill.solid(); sul.fill.fore_color.rgb = RGBColor(0xFF, 0x66, 0x66)
sul.line.fill.background()
# Annotation labels (right side)
annotations = [
(2.0, "Free Gingiva", "Unattached; forms gingival sulcus", 2.28),
(2.75, "Attached Gingiva", "Firmly attached to alveolar bone", 2.9),
(3.4, "Cementum", "Calcified; PDL fibres insert here", 3.55),
(3.9, "Periodontal Ligament","Collagen fibres; absorbs bite forces", 4.05),
(4.6, "Alveolar Bone", "Supports root; destroyed in periodontitis", 4.75),
(5.6, "Gingival Sulcus", "Normal depth 2–3 mm; deeper = pocket = disease", 5.75),
]
for (ya, lbl, desc, ya2) in annotations:
# small leader line
ll = s3.shapes.add_shape(1, Inches(3.4), Inches(ya2), Inches(0.3), Inches(0.03))
ll.fill.solid(); ll.fill.fore_color.rgb = MGRAY; ll.line.fill.background()
tb_lbl = s3.shapes.add_textbox(Inches(3.75), Inches(ya2-0.16), Inches(5.5), Inches(0.38))
tb_lbl.word_wrap = True
tf_lbl = tb_lbl.text_frame
tf_lbl.word_wrap = True
p_lbl = tf_lbl.paragraphs[0]
r_lbl = p_lbl.add_run()
r_lbl.text = lbl + " — "
r_lbl.font.name = FONT; r_lbl.font.size = Pt(14); r_lbl.font.bold = True; r_lbl.font.color.rgb = NAVY
r_desc = p_lbl.add_run()
r_desc.text = desc
r_desc.font.name = FONT; r_desc.font.size = Pt(13); r_desc.font.color.rgb = DGRAY
# Diagram caption
txb(s3, "Figure 1. Schematic cross-section of the dental attachment apparatus",
0.4, 6.6, 3.8, 0.4, size=10, italic=True, color=MGRAY)
# Sulcus note
key_takeaway(s3,
"Normal gingival sulcus depth = 2–3 mm. "
"Depth > 3 mm on probing = pathological pocket = periodontal disease.",
3.6, 6.55, 9.5, 0.65)
# ════════════════════════════════════════════════════════════
# SECTION 2
# ════════════════════════════════════════════════════════════
section_divider("Section 2: Microbiology")
# ════════════════════════════════════════════════════════════
# SLIDE 4 — PLAQUE & CALCULUS
# ════════════════════════════════════════════════════════════
s4 = new_slide()
add_title_bar(s4, "Dental Plaque and Calculus",
"Primary aetiological agents of periodontal disease")
# Flow diagram — plaque progression
stages_flow = [
("Dental Plaque\n(Biofilm)", "Bacteria + salivary proteins + dead cells"),
("Not Removed\nDaily", "Brushing & flossing skipped"),
("Calculus\n(Tartar)", "Mineralised; cannot be brushed away"),
("Subgingival\nColonisation", "Anaerobes below gumline"),
("Periodontal\nDisease", "Bone + attachment loss"),
]
fbox_w = 2.1
gap = 0.38
total_flow = len(stages_flow) * fbox_w + (len(stages_flow)-1)*gap
start_x = (13.33 - total_flow) / 2
flow_bgs = [
RGBColor(0xF0,0xF2,0xF5),
RGBColor(0xE8,0xEE,0xF5),
RGBColor(0xDC,0xE8,0xF5),
RGBColor(0xFF,0xF0,0xD8),
RGBColor(0xFF,0xDD,0xDD),
]
for i, (lbl, sub) in enumerate(stages_flow):
bx = start_x + i*(fbox_w+gap)
flowbox(s4, lbl, sub, bx, 1.5, w=fbox_w, h=1.0,
bg=flow_bgs[i], border=NAVY)
if i < len(stages_flow)-1:
ax = bx + fbox_w + 0.02
arrow(s4, ax, 1.5, ax+gap-0.04)
txb(s4, "Plaque Maturation Pathway", 0.4, 1.35, 12.5, 0.28,
size=12, italic=True, bold=True, color=MGRAY)
# Comparison table
txb(s4, "Supragingival vs. Subgingival Plaque", 0.4, 2.75, 12.5, 0.38,
size=18, bold=True, color=NAVY)
add_table(s4, [
["Feature", "Supragingival Plaque", "Subgingival Plaque"],
["Location", "Above the gumline", "Below gumline — inside the pocket"],
["Visibility", "Visible as white/yellow film", "Hidden; cannot be seen clinically"],
["Oxygen environment", "Aerobic and facultative organisms", "Predominantly anaerobic organisms"],
["Primary role", "Causes gingivitis", "Main driver of bone destruction"],
["Removal", "Daily brushing and flossing", "Professional scaling only"],
], [2.5, 4.8, 4.8], 0.4, 3.15)
key_takeaway(s4,
"Calculus cannot be removed by brushing — only by professional scaling and root planing.",
0.4, 6.6, 12.5, 0.65)
# ════════════════════════════════════════════════════════════
# SLIDE 5 — KEY PATHOGENS
# ════════════════════════════════════════════════════════════
s5 = new_slide()
add_title_bar(s5, "Key Periodontal Pathogens",
"Microbial complexes — Socransky classification")
add_table(s5, [
["Organism", "Complex", "Disease Association", "Clinical Note"],
[
[("Porphyromonas gingivalis", True), (" (Pg)", False)],
[("RED COMPLEX", True)],
"Chronic periodontitis — major culprit",
"Produces gingipains; evades immune response"
],
[
[("Tannerella forsythia", True), (" (Tf)", False)],
[("RED COMPLEX", True)],
"Chronic periodontitis",
"Synergistic virulence with Pg"
],
[
[("Treponema denticola", True), (" (Td)", False)],
[("RED COMPLEX", True)],
"Chronic periodontitis",
"Spirochaete; elevated in deep pockets"
],
[
[("Aggregatibacter actinomycetemcomitans", True), (" (Aa)", False)],
"AGGRESSIVE",
"Localised Aggressive Periodontitis (LAP)",
"Impaired neutrophil chemotaxis; 1st molars + incisors"
],
[
[("Prevotella intermedia", True)],
"ORANGE COMPLEX",
"Gingivitis (pregnancy-associated)",
"Responds to oestrogen / progesterone"
],
[
[("Fusobacterium nucleatum", True)],
"ORANGE COMPLEX",
"Bridges early and late colonisers",
"Key bridging organism in plaque biofilm"
],
[
"Treponema + Fusobacterium + Selenomonas",
"ANUG CONSORTIUM",
"Acute Necrotising Ulcerative Gingivitis",
"Triple mix; onset within 24 hours"
],
], [3.2, 1.8, 3.3, 4.7], 0.4, 1.2)
# Red Complex mnemonic box
box_m = s5.shapes.add_shape(1, Inches(0.4), Inches(6.45), Inches(12.5), Inches(0.78))
box_m.fill.solid(); box_m.fill.fore_color.rgb = RGBColor(0xE8, 0xF0, 0xFB)
box_m.line.color.rgb = NAVY; box_m.line.width = Pt(1.2)
tb_m = s5.shapes.add_textbox(Inches(0.55), Inches(6.5), Inches(12.2), Inches(0.65))
tb_m.word_wrap = True
tf_m = tb_m.text_frame; tf_m.word_wrap = True
p_m = tf_m.paragraphs[0]; p_m.alignment = PP_ALIGN.LEFT
runs_m = [
("Mnemonic — RED COMPLEX ", True, NAVY, 16),
('"P G T"', True, RGBColor(0xCC,0x22,0x22), 16),
(" = Porphyromonas gingivalis · Tannerella forsythia · Treponema denticola", False, DGRAY, 15),
]
for txt, bd, col, sz in runs_m:
r = p_m.add_run(); r.text = txt
r.font.name = FONT; r.font.size = Pt(sz); r.font.bold = bd; r.font.color.rgb = col
# ════════════════════════════════════════════════════════════
# SECTION 3
# ════════════════════════════════════════════════════════════
section_divider("Section 3: Pathogenesis")
# ════════════════════════════════════════════════════════════
# SLIDE 6 — PATHOGENESIS FLOWCHART
# ════════════════════════════════════════════════════════════
s6 = new_slide()
add_title_bar(s6, "Pathogenesis of Periodontal Disease",
"Sequential steps from bacterial colonisation to bone destruction")
# Disease progression strip
prog_stages = [
("HEALTHY\nGUMS", "Normal sulcus\n2–3 mm", RGBColor(0xD4,0xED,0xDA), NAVY),
("GINGIVITIS", "BOP, redness\nNo bone loss", RGBColor(0xFF,0xF3,0xCD), NAVY),
("EARLY\nPERIODONTITIS", "Pocket 4–5 mm\nBone loss begins", RGBColor(0xFF,0xE0,0xB2), NAVY),
("MODERATE\nPERIODONTITIS","Pocket 5–7 mm\nFurcation involvement", RGBColor(0xFF,0xCC,0xBC), NAVY),
("SEVERE\nPERIODONTITIS", "Pocket >7 mm\nTooth mobility", RGBColor(0xFF,0xCD,0xD2), RGBColor(0xCC,0x22,0x22)),
("TOOTH\nLOSS", "Irreversible\nbone loss", RGBColor(0xEF,0x9A,0x9A), RGBColor(0xCC,0x22,0x22)),
]
bw = 1.88; gap2 = 0.2
for i, (lbl, sub, bg, tc) in enumerate(prog_stages):
bx2 = 0.4 + i*(bw+gap2)
box2 = s6.shapes.add_shape(1, Inches(bx2), Inches(1.3), Inches(bw), Inches(1.1))
box2.fill.solid(); box2.fill.fore_color.rgb = bg
box2.line.color.rgb = tc; box2.line.width = Pt(1.0)
tbn = s6.shapes.add_textbox(Inches(bx2+0.06), Inches(1.32), Inches(bw-0.12), Inches(0.5))
tbn.word_wrap = True
tf_n2 = tbn.text_frame; p_n2 = tf_n2.paragraphs[0]
p_n2.alignment = PP_ALIGN.CENTER
r_n2 = p_n2.add_run(); r_n2.text = lbl
r_n2.font.name = FONT; r_n2.font.size = Pt(11); r_n2.font.bold = True; r_n2.font.color.rgb = tc
tbs2 = s6.shapes.add_textbox(Inches(bx2+0.06), Inches(1.82), Inches(bw-0.12), Inches(0.52))
tbs2.word_wrap = True
tf_s2 = tbs2.text_frame; p_s2 = tf_s2.paragraphs[0]
p_s2.alignment = PP_ALIGN.CENTER
r_s2 = p_s2.add_run(); r_s2.text = sub
r_s2.font.name = FONT; r_s2.font.size = Pt(10); r_s2.font.color.rgb = MGRAY
if i < len(prog_stages)-1:
arrow(s6, bx2+bw+0.02, 1.3, bx2+bw+gap2-0.01)
# KEY RULE banner
banner = s6.shapes.add_shape(1, Inches(0.4), Inches(2.6), Inches(12.5), Inches(0.52))
banner.fill.solid(); banner.fill.fore_color.rgb = RGBColor(0xE8,0xF0,0xFB)
banner.line.color.rgb = NAVY; banner.line.width = Pt(1.0)
tb_ban = s6.shapes.add_textbox(Inches(0.55), Inches(2.63), Inches(12.2), Inches(0.44))
tb_ban.word_wrap = True
tf_ban = tb_ban.text_frame; p_ban = tf_ban.paragraphs[0]; p_ban.alignment = PP_ALIGN.LEFT
r_ban1 = p_ban.add_run(); r_ban1.text = "Key Rule: "
r_ban1.font.name = FONT; r_ban1.font.size = Pt(15); r_ban1.font.bold = True; r_ban1.font.color.rgb = NAVY
r_ban2 = p_ban.add_run()
r_ban2.text = "Gingivitis = REVERSIBLE (bone intact). Periodontitis = IRREVERSIBLE (bone destroyed permanently)."
r_ban2.font.name = FONT; r_ban2.font.size = Pt(14); r_ban2.font.color.rgb = DGRAY
# Mechanism table
txb(s6, "Molecular Mechanism of Tissue Destruction", 0.4, 3.2, 12.5, 0.38,
size=17, bold=True, color=NAVY)
add_table(s6, [
["Step", "Event"],
["1. Bacterial colonisation", "Plaque accumulates in sulcus; LPS and toxins released by Pg, Tf, Td"],
["2. Innate immune activation", "PMNs, macrophages recruited; IL-1β, TNF-α, PGE2, MMP secretion"],
["3. Osteoclast activation", "RANKL upregulated → osteoclast-mediated alveolar bone resorption (PERMANENT)"],
["4. Pocket formation", "Junctional epithelium migrates apically; true pocket deepens"],
["5. Vicious cycle", "Deeper pocket → more anaerobes → more inflammation → more bone loss"],
], [3.5, 9.2], 0.4, 3.62)
# ════════════════════════════════════════════════════════════
# SECTION 4
# ════════════════════════════════════════════════════════════
section_divider("Section 4: Classification")
# ════════════════════════════════════════════════════════════
# SLIDE 7 — CLASSIFICATION TABLE
# ════════════════════════════════════════════════════════════
s7 = new_slide()
add_title_bar(s7, "Classification of Periodontal Diseases",
"Based on AAP/EFP 2017 classification framework")
add_table(s7, [
["Disease", "Key Clinical Features", "Bone Loss?", "Reversible?"],
[
[("Chronic Gingivitis", True)],
"Redness, BOP, swelling. Plaque-induced. Most common.",
"No",
[("YES", True)]
],
[
[("Chronic Periodontitis", True)],
"Pockets, bone loss, attachment loss. Usually painless.",
"Yes",
[("NO", True)]
],
[
[("Aggressive Periodontitis — LAP", True)],
"Age <30 yrs. 1st molars + incisors. Rapid bone loss. Aa is key pathogen.",
"Yes — rapid",
[("NO", True)]
],
[
[("Aggressive Periodontitis — GAP", True)],
"Young patients. Generalised (≥3 teeth beyond 1st molars/incisors).",
"Yes",
[("NO", True)]
],
[
[("ANUG", True)],
"TRIAD: Pain + Punched-out papillae + Bleeding. Fetid breath. ONLY PAINFUL periodontal condition.",
"No (unless → NUP)",
"Partial"
],
[
[("Necrotising Ulcerative Periodontitis (NUP)", True)],
"ANUG + bone exposure + bone necrosis. HIV/immunosuppressed.",
"Yes",
[("NO", True)]
],
[
[("Periodontitis — Systemic Disease", True)],
"Papillon-Lefèvre, Chediak-Higashi, leukaemia, Down syndrome.",
"Yes",
"Varies"
],
[
[("Pregnancy Gingivitis", True)],
"Exaggerated plaque response due to progesterone rise. Resolves post-partum.",
"No",
[("YES", True)]
],
[
[("Drug-induced Gingival Overgrowth", True)],
"Painless fibrous enlargement. Phenytoin, Ciclosporin, Nifedipine.",
"No",
"Partial"
],
], [3.4, 5.6, 1.6, 1.6], 0.4, 1.2)
# ════════════════════════════════════════════════════════════
# SLIDE 8 — ANUG & DRUG-INDUCED GINGIVAL OVERGROWTH
# ════════════════════════════════════════════════════════════
s8 = new_slide()
add_title_bar(s8, "ANUG and Drug-Induced Gingival Overgrowth",
"Specific conditions — diagnosis and management")
# Left panel — ANUG
left_bg = s8.shapes.add_shape(1, Inches(0.35), Inches(1.15), Inches(6.1), Inches(5.85))
left_bg.fill.solid(); left_bg.fill.fore_color.rgb = RGBColor(0xF9,0xF9,0xF9)
left_bg.line.color.rgb = RGBColor(0xCC,0xCC,0xCC); left_bg.line.width = Pt(0.75)
txb(s8, "ANUG — Acute Necrotising Ulcerative Gingivitis", 0.45, 1.2, 5.9, 0.45,
size=16, bold=True, color=RGBColor(0xCC,0x22,0x22))
add_table(s8, [
["Feature", "Detail"],
[[("Diagnostic TRIAD", True)], [("Pain + Punched-out papillae + Bleeding", True)]],
["Synonyms", "Trench mouth, Vincent's angina"],
["Onset", "Within 24 hours; only PAINFUL periodontal condition"],
["Pathogens", "Treponema + Fusobacterium + Selenomonas"],
[[("Risk factors", True)], "Stress, smoking, HIV/AIDS, malnutrition, poor oral hygiene"],
[[("Treatment", True)], "CHX 0.12% rinse + Gentle debridement + Metronidazole (if systemic signs)"],
["Complication", "Progresses to NUP if untreated — bone exposure and necrosis"],
], [2.0, 3.8], 0.45, 1.7)
# Right panel — Drug-induced
right_bg = s8.shapes.add_shape(1, Inches(6.85), Inches(1.15), Inches(6.1), Inches(5.85))
right_bg.fill.solid(); right_bg.fill.fore_color.rgb = RGBColor(0xF9,0xF9,0xF9)
right_bg.line.color.rgb = RGBColor(0xCC,0xCC,0xCC); right_bg.line.width = Pt(0.75)
txb(s8, "Drug-Induced Gingival Overgrowth", 6.95, 1.2, 5.9, 0.45,
size=16, bold=True, color=NAVY)
add_table(s8, [
["Drug", "Drug Class", "Notes"],
[[("Phenytoin", True)], "Anti-epileptic", "~50% users affected; most studied"],
[[("Ciclosporin", True)], "Immunosuppressant", "Organ transplant patients"],
[[("Nifedipine", True)], "Ca-channel blocker", "Cardiac patients"],
["Features", "", "Painless fibrous gum enlargement; starts at interdental papillae"],
["Treatment", "", "Improve oral hygiene + drug substitution + gingivectomy if needed"],
], [1.9, 2.1, 1.8], 6.95, 1.7)
# Mnemonic
box_pcn = s8.shapes.add_shape(1, Inches(6.95), Inches(5.35), Inches(5.7), Inches(1.4))
box_pcn.fill.solid(); box_pcn.fill.fore_color.rgb = RGBColor(0xE8,0xF0,0xFB)
box_pcn.line.color.rgb = NAVY; box_pcn.line.width = Pt(1.2)
tb_pcn = s8.shapes.add_textbox(Inches(7.05), Inches(5.4), Inches(5.5), Inches(1.2))
tb_pcn.word_wrap = True
tf_pcn = tb_pcn.text_frame; tf_pcn.word_wrap = True
p_pcn = tf_pcn.paragraphs[0]; p_pcn.alignment = PP_ALIGN.LEFT
r_pcn1 = p_pcn.add_run(); r_pcn1.text = 'Mnemonic "PCN": '
r_pcn1.font.name = FONT; r_pcn1.font.size = Pt(15); r_pcn1.font.bold = True; r_pcn1.font.color.rgb = NAVY
r_pcn2 = p_pcn.add_run(); r_pcn2.text = '"Please Check Now"'
r_pcn2.font.name = FONT; r_pcn2.font.size = Pt(15); r_pcn2.font.bold = True; r_pcn2.font.color.rgb = RGBColor(0xCC,0x22,0x22)
p_pcn2 = tf_pcn.add_paragraph(); p_pcn2.alignment = PP_ALIGN.LEFT
r_pcn3 = p_pcn2.add_run(); r_pcn3.text = "Phenytoin · Ciclosporin · Nifedipine"
r_pcn3.font.name = FONT; r_pcn3.font.size = Pt(14); r_pcn3.font.color.rgb = DGRAY
# ════════════════════════════════════════════════════════════
# SECTION 5
# ════════════════════════════════════════════════════════════
section_divider("Section 5: Risk Factors & Systemic Links")
# ════════════════════════════════════════════════════════════
# SLIDE 9 — RISK FACTORS
# ════════════════════════════════════════════════════════════
s9 = new_slide()
add_title_bar(s9, "Risk Factors for Periodontal Disease",
"Modifiable factors are the primary targets of prevention")
add_table(s9, [
["Risk Factor", "Category", "Mechanism / Clinical Notes"],
[[("Dental plaque / calculus", True)], "LOCAL — Modifiable", "Primary cause; bacterial products trigger host inflammatory response"],
[[("Smoking / tobacco", True)], "SYSTEMIC — Modifiable", "Vasoconstriction masks BOP; impairs healing; doubles disease risk"],
[[("Diabetes mellitus", True)], "SYSTEMIC — Modifiable", "Bidirectional relationship; poor glycaemic control worsens periodontal destruction"],
[[("HIV / AIDS", True)], "SYSTEMIC", "Immunosuppression → susceptibility to ANUG and NUP"],
[[("Genetic susceptibility", True)], "NON-MODIFIABLE", "IL-1 gene polymorphisms; positive family history"],
[[("Causative medications", True)], "SYSTEMIC", "Ciclosporin, phenytoin, nifedipine, antidepressants (xerostomia)"],
[[("Hormonal changes", True)], "SYSTEMIC — Modifiable", "Puberty, pregnancy, menopause — alter tissue response to plaque"],
[[("Vitamin C deficiency (Scurvy)", True)], "NUTRITIONAL — Modifiable", "Impaired collagen synthesis → fragile gingiva, spontaneous bleeding"],
[[("Xerostomia (dry mouth)", True)], "LOCAL", "Reduced salivary antimicrobial protection → increased plaque accumulation"],
], [3.2, 2.5, 7.0], 0.4, 1.2)
key_takeaway(s9,
"Smoking and poor glycaemic control are the two most significant modifiable systemic risk factors for periodontal disease.",
0.4, 6.6, 12.5, 0.65)
# ════════════════════════════════════════════════════════════
# SLIDE 10 — SYSTEMIC LINKS
# ════════════════════════════════════════════════════════════
s10 = new_slide()
add_title_bar(s10, "Systemic Links of Periodontal Disease",
"Bacteraemia from periodontal pockets triggers remote organ disease")
txb(s10, "Mechanism: Bacteria enter the bloodstream (bacteraemia) and trigger systemic inflammation "
"via LPS, cytokines (IL-1β, TNF-α), and molecular mimicry.",
0.4, 1.18, 12.5, 0.45, size=16, color=DGRAY)
# Hub-and-spoke diagram
# Central ellipse
hub = s10.shapes.add_shape(9, # oval
Inches(5.2), Inches(2.0), Inches(2.9), Inches(1.3))
hub.fill.solid(); hub.fill.fore_color.rgb = NAVY; hub.line.fill.background()
tb_hub = s10.shapes.add_textbox(Inches(5.25), Inches(2.1), Inches(2.8), Inches(1.1))
tb_hub.word_wrap = True
tf_hub = tb_hub.text_frame; tf_hub.word_wrap = True
p_hub = tf_hub.paragraphs[0]; p_hub.alignment = PP_ALIGN.CENTER
r_hub = p_hub.add_run(); r_hub.text = "Periodontal\nDisease\n(Bacteraemia)"
r_hub.font.name = FONT; r_hub.font.size = Pt(13); r_hub.font.bold = True; r_hub.font.color.rgb = WHITE
# Spoke boxes
spokes = [
("Cardiovascular Disease\n& Atherosclerosis", "Bidirectional", 0.35, 1.65),
("Diabetes Mellitus", "Bidirectional", 0.35, 3.15),
("Adverse Pregnancy Outcomes", "Preterm birth, low BW", 0.35, 4.55),
("Infective Endocarditis", "Bacteraemia risk", 9.95, 1.65),
("Aspiration Pneumonia\n/ Lung Abscess", "Oral bacteria aspirated", 9.95, 3.15),
("Rheumatoid Arthritis\n/ Alzheimer's Disease", "Shared immune pathway", 9.95, 4.55),
]
for lbl, note, sx, sy in spokes:
sbox = s10.shapes.add_shape(1, Inches(sx), Inches(sy), Inches(3.1), Inches(0.95))
sbox.fill.solid(); sbox.fill.fore_color.rgb = RGBColor(0xF0,0xF2,0xF5)
sbox.line.color.rgb = BORDER; sbox.line.width = Pt(0.75)
tb_s = s10.shapes.add_textbox(Inches(sx+0.08), Inches(sy+0.05), Inches(2.94), Inches(0.55))
tb_s.word_wrap = True
tf_s = tb_s.text_frame; tf_s.word_wrap = True
p_s = tf_s.paragraphs[0]; p_s.alignment = PP_ALIGN.LEFT
r_s = p_s.add_run(); r_s.text = lbl
r_s.font.name = FONT; r_s.font.size = Pt(13); r_s.font.bold = True; r_s.font.color.rgb = NAVY
tb_sn = s10.shapes.add_textbox(Inches(sx+0.08), Inches(sy+0.6), Inches(2.94), Inches(0.3))
tf_sn = tb_sn.text_frame
p_sn = tf_sn.paragraphs[0]
r_sn = p_sn.add_run(); r_sn.text = note
r_sn.font.name = FONT; r_sn.font.size = Pt(11); r_sn.font.italic = True; r_sn.font.color.rgb = MGRAY
key_takeaway(s10,
"Periodontal disease is usually PAINLESS until very late stages. "
"Regular 6-monthly check-ups are essential — patients often present only when teeth are already very loose.",
0.4, 6.6, 12.5, 0.65)
# ════════════════════════════════════════════════════════════
# SECTION 6
# ════════════════════════════════════════════════════════════
section_divider("Section 6: Diagnosis & Investigations")
# ════════════════════════════════════════════════════════════
# SLIDE 11 — DIAGNOSIS
# ════════════════════════════════════════════════════════════
s11 = new_slide()
add_title_bar(s11, "Diagnosis and Investigations",
"Clinical examination, periodontal charting, imaging, and laboratory tests")
add_table(s11, [
["Investigation", "Type", "Purpose", "Abnormal Finding"],
[[("Periodontal probe", True)], "Clinical", "Measure sulcus/pocket depth (mm)", "Pocket > 3 mm = pathological"],
[[("Bleeding on Probing (BOP)", True)], "Clinical", "Detect active gingival inflammation", "BOP present = inflamed tissue"],
[[("Gum recession measurement", True)], "Clinical", "Distance from CEJ to gingival margin", "Any recession = attachment loss"],
[[("Tooth mobility grading", True)], "Clinical", "Degree of loosening (Grade 0–3)", "Grade 1+ = significant bone loss"],
[[("Furcation involvement", True)], "Clinical", "Bone loss at root fork in multi-rooted teeth", "Class I / II / III — Hess classification"],
[[("Periapical X-ray", True)], "Radiograph", "Bone level around individual teeth", "Horizontal/vertical crestal bone loss"],
[[("Orthopantomogram (OPG)", True)], "Radiograph", "Full-mouth overview of bone levels", "Generalised bone loss pattern"],
[[("CBCT (Cone Beam CT)", True)], "Radiograph", "3-D bone architecture; surgical planning", "Precise defect morphology"],
[[("Blood tests", True)], "Laboratory", "Rule out systemic disease", "HbA1c (DM), FBC, ESR, HIV screen"],
], [2.6, 1.6, 3.8, 4.7], 0.4, 1.2)
key_takeaway(s11,
"A full periodontal chart (6 sites per tooth, BOP, recession, mobility, furcation) is mandatory before treatment planning.",
0.4, 6.6, 12.5, 0.65)
# ════════════════════════════════════════════════════════════
# SECTION 7
# ════════════════════════════════════════════════════════════
section_divider("Section 7: Treatment")
# ════════════════════════════════════════════════════════════
# SLIDE 12 — TREATMENT PHASES
# ════════════════════════════════════════════════════════════
s12 = new_slide()
add_title_bar(s12, "Treatment of Periodontal Disease",
"Four-phase approach — treat cause, then re-evaluate before surgery")
txb(s12, "Treatment Aim: Slow or arrest disease by removing plaque and its by-products. "
"Lost bone cannot be fully regrown. Prevention is always superior to treatment.",
0.4, 1.18, 12.5, 0.45, size=15, italic=True, color=MGRAY)
# Four phase boxes (2 × 2)
phases = [
("PHASE 1", "Systemic Phase",
["Treat underlying systemic disease (DM, blood disorders)",
"Adjust/substitute medications causing gingival overgrowth",
"Address nutritional deficiencies — Vitamin C",
"Smoking cessation counselling"],
RGBColor(0xF0,0xF2,0xF5)),
("PHASE 2", "Causal / Hygiene Phase",
["Oral hygiene instruction — modified Bass technique",
"Supragingival scaling — remove calculus above gumline",
"Root planing — remove cementum-embedded calculus (SRP)",
"Local antibiotics (doxycycline chip) in isolated deep pockets",
"Reassess at 6–8 weeks"],
RGBColor(0xE8,0xEE,0xF5)),
("PHASE 3", "Surgical Phase",
["Flap surgery — direct access to root surfaces",
"Guided Tissue Regeneration (GTR) — attempt bone regrowth",
"Bone grafts — for specific intrabony defects",
"Gingivectomy — for drug-induced gingival overgrowth",
"Implant placement — for teeth that cannot be saved"],
RGBColor(0xE8,0xF5,0xE8)),
("PHASE 4", "Maintenance Phase",
["Supportive periodontal therapy every 3–6 months",
"Repeat clinical indices at every visit",
"Oral hygiene re-motivation and instruction",
"Lifelong commitment — no cure, only control"],
RGBColor(0xFF,0xF8,0xDC)),
]
for i, (ph, name, items, bg) in enumerate(phases):
col = i % 2
row = i // 2
bx3 = 0.4 + col * 6.45
by3 = 1.8 + row * 2.7
phase_bg = s12.shapes.add_shape(1, Inches(bx3), Inches(by3), Inches(6.25), Inches(2.5))
phase_bg.fill.solid(); phase_bg.fill.fore_color.rgb = bg
phase_bg.line.color.rgb = NAVY; phase_bg.line.width = Pt(1.0)
# Header bar
hbar = s12.shapes.add_shape(1, Inches(bx3), Inches(by3), Inches(6.25), Inches(0.48))
hbar.fill.solid(); hbar.fill.fore_color.rgb = NAVY; hbar.line.fill.background()
tb_ph = s12.shapes.add_textbox(Inches(bx3+0.1), Inches(by3+0.03), Inches(6.0), Inches(0.42))
tb_ph.word_wrap = True; tf_ph = tb_ph.text_frame; p_ph = tf_ph.paragraphs[0]
p_ph.alignment = PP_ALIGN.LEFT
r_ph1 = p_ph.add_run(); r_ph1.text = ph + " — "
r_ph1.font.name = FONT; r_ph1.font.size = Pt(14); r_ph1.font.bold = True; r_ph1.font.color.rgb = WHITE
r_ph2 = p_ph.add_run(); r_ph2.text = name
r_ph2.font.name = FONT; r_ph2.font.size = Pt(13); r_ph2.font.color.rgb = RGBColor(0xB8,0xCC,0xE8)
# Bullet items
tb_it = s12.shapes.add_textbox(Inches(bx3+0.18), Inches(by3+0.55), Inches(5.9), Inches(1.85))
tb_it.word_wrap = True; tf_it = tb_it.text_frame; tf_it.word_wrap = True
for j, it in enumerate(items):
p_it = tf_it.paragraphs[0] if j == 0 else tf_it.add_paragraph()
p_it.alignment = PP_ALIGN.LEFT
r_it = p_it.add_run(); r_it.text = "• " + it
r_it.font.name = FONT; r_it.font.size = Pt(13); r_it.font.color.rgb = DGRAY
pPr_it = p_it._pPr
if pPr_it is None: pPr_it = p_it._p.get_or_add_pPr()
spcBef2 = etree.SubElement(pPr_it, qn('a:spcBef'))
spcPts2 = etree.SubElement(spcBef2, qn('a:spcPts'))
spcPts2.set('val', '100')
# ════════════════════════════════════════════════════════════
# SLIDE 13 — SPECIFIC TREATMENTS
# ════════════════════════════════════════════════════════════
s13 = new_slide()
add_title_bar(s13, "Treatment of Specific Conditions",
"Periodontal abscess and ANUG — acute management")
# Abscess
txb(s13, "Periodontal Abscess", 0.4, 1.2, 6.0, 0.4, size=18, bold=True, color=RGBColor(0xCC,0x22,0x22))
add_table(s13, [
["Feature", "Detail"],
[[("Definition", True)], "Acute bacterial infection within an existing periodontal pocket"],
[[("Presentation", True)], "Rapid-onset pain, swelling, pus discharge, tooth tender to bite"],
[[("Treatment", True)], "1. Incision and drainage (I&D)\n2. Subgingival debridement\n3. Amoxicillin / Metronidazole if systemic spread\n4. Definitive periodontal treatment after resolution"],
[[("Distinguish from", True)], "Periapical abscess — tooth vitality test differentiates (periodontally involved tooth is usually vital)"],
], [2.2, 3.6], 0.4, 1.65)
# ANUG treatment
txb(s13, "ANUG — Treatment Protocol", 7.1, 1.2, 6.0, 0.4, size=18, bold=True, color=RGBColor(0xCC,0x22,0x22))
add_table(s13, [
["Step", "Intervention"],
["1", [("Chlorhexidine 0.12%", True), (" rinse — immediate antimicrobial control", False)]],
["2", "Gentle debridement / scaling — avoid ultrasonic if severely inflamed"],
["3", [("Metronidazole 200–400 mg TDS × 3–7 days", True), (" — ONLY if systemic signs (fever, lymphadenopathy)", False)]],
["4", "Oral hygiene instruction — gentle technique with soft brush"],
["5", "Nutritional advice — Vitamin C supplementation, adequate fluids"],
["6", "Stress management and smoking cessation advice"],
["7", "Follow-up in 1–2 weeks — reassess, complete scaling"],
], [0.7, 5.7], 7.1, 1.65)
key_takeaway(s13,
"ANUG is the only periodontal condition that is acutely painful. Distinguish from periapical disease by vitality testing.",
0.4, 6.6, 12.5, 0.65)
# ════════════════════════════════════════════════════════════
# SECTION 8
# ════════════════════════════════════════════════════════════
section_divider("Section 8: Prevention")
# ════════════════════════════════════════════════════════════
# SLIDE 14 — PREVENTION
# ════════════════════════════════════════════════════════════
s14 = new_slide()
add_title_bar(s14, "Prevention of Periodontal Disease",
"Self-care and professional measures — primary prevention is the gold standard")
add_table(s14, [
["Preventive Measure", "Frequency / Dosage", "Notes"],
[[("Tooth brushing", True)], "Twice daily — 2 minutes", "Modified Bass technique; fluoride toothpaste 1000–1500 ppm"],
[[("Flossing / interdental brushes", True)], "Once daily", "Removes interdental plaque unreachable by toothbrush"],
[[("Electric toothbrush", True)], "Twice daily", "Superior plaque removal; recommended for most patients"],
[[("Professional scaling & polishing", True)], "Every 6 months (3-monthly for high risk)", "Only method to remove subgingival calculus"],
[[("Smoking cessation", True)], "Ongoing", "Largest modifiable risk factor besides plaque control"],
[[("Glycaemic control", True)], "Ongoing (target HbA1c < 7%)", "Periodontal treatment also improves glycaemic control — bidirectional benefit"],
[[("Chlorhexidine 0.12% rinse", True)], "Short-term only (max 2 weeks)", "Post-surgical or during ANUG; long-term use causes tooth staining"],
[[("High-risk group counselling", True)], "At each dental visit", "Pregnant women, diabetics, smokers, immunosuppressed patients"],
], [3.0, 3.0, 6.7], 0.4, 1.2)
key_takeaway(s14,
"Prevention is always superior to treatment — lost alveolar bone cannot be fully regenerated.",
0.4, 6.6, 12.5, 0.65)
# ════════════════════════════════════════════════════════════
# SLIDE 15 — MNEMONICS & COMPARISON TABLE
# ════════════════════════════════════════════════════════════
s15 = new_slide()
add_title_bar(s15, "Mnemonics and Key Facts for Examination",
"Quick-reference summary")
# Four mnemonic cards
cards = [
("RED COMPLEX\n\"PGT\"",
"Porphyromonas gingivalis\nTannerella forsythia\nTreponema denticola",
RGBColor(0xFF,0xDD,0xDD), RGBColor(0xCC,0x22,0x22)),
("Drug-Induced Overgrowth\n\"PCN\" = \"Please Check Now\"",
"Phenytoin\nCiclosporin\nNifedipine",
RGBColor(0xD6,0xE4,0xF0), NAVY),
("ANUG TRIAD",
"Pain\nPunched-out papillae\nBleeding\n(+ Grey pseudomembrane)",
RGBColor(0xFF,0xF3,0xCD), RGBColor(0xB8,0x80,0x00)),
("ANUG Treatment",
"CHX rinse\n+ Debridement\n+ Metronidazole\n (systemic signs only)",
RGBColor(0xD4,0xED,0xDA), RGBColor(0x19,0x6F,0x3D)),
]
for i, (title_c, body_c, bg_c, tc_c) in enumerate(cards):
cx = 0.35 + i * 3.15
cb = s15.shapes.add_shape(1, Inches(cx), Inches(1.2), Inches(2.95), Inches(2.4))
cb.fill.solid(); cb.fill.fore_color.rgb = bg_c
cb.line.color.rgb = tc_c; cb.line.width = Pt(1.2)
tb_ct = s15.shapes.add_textbox(Inches(cx+0.1), Inches(1.25), Inches(2.75), Inches(0.7))
tb_ct.word_wrap = True; tf_ct = tb_ct.text_frame; tf_ct.word_wrap = True
p_ct = tf_ct.paragraphs[0]; p_ct.alignment = PP_ALIGN.CENTER
r_ct = p_ct.add_run(); r_ct.text = title_c
r_ct.font.name = FONT; r_ct.font.size = Pt(13); r_ct.font.bold = True; r_ct.font.color.rgb = tc_c
ln_sep = s15.shapes.add_shape(1, Inches(cx+0.1), Inches(1.95), Inches(2.75), Inches(0.03))
ln_sep.fill.solid(); ln_sep.fill.fore_color.rgb = tc_c; ln_sep.line.fill.background()
tb_cb = s15.shapes.add_textbox(Inches(cx+0.1), Inches(2.0), Inches(2.75), Inches(1.5))
tb_cb.word_wrap = True; tf_cb = tb_cb.text_frame; tf_cb.word_wrap = True
p_cb = tf_cb.paragraphs[0]; p_cb.alignment = PP_ALIGN.CENTER
r_cb = p_cb.add_run(); r_cb.text = body_c
r_cb.font.name = FONT; r_cb.font.size = Pt(13); r_cb.font.color.rgb = DGRAY
# Gingivitis vs Periodontitis comparison
txb(s15, "Gingivitis vs. Periodontitis — Key Differences", 0.35, 3.75, 12.6, 0.38,
size=17, bold=True, color=NAVY)
add_table(s15, [
["Feature", "Gingivitis", "Periodontitis"],
["Bone loss", [("ABSENT", True)], [("PRESENT", True)]],
["Attachment loss", [("ABSENT", True)], [("PRESENT", True)]],
["Pocket depth", "Pseudo-pockets only; true depth ≤ 3 mm", "True pockets > 3 mm"],
["Reversible?", [("YES — with treatment", True)], [("NO — bone loss is permanent", True)]],
["Pain", "Usually absent", "Usually absent until abscess forms"],
["Treatment", "Scaling + oral hygiene instruction", "Phases 1–4; possibly surgery"],
], [2.6, 4.8, 4.8], 0.35, 4.18)
# ════════════════════════════════════════════════════════════
# SLIDE 16 — REFERENCES
# ════════════════════════════════════════════════════════════
s16 = new_slide()
add_title_bar(s16, "References", "Standard medical and dental textbooks and guidelines")
refs = [
("1.", "Kasper DL, Fauci AS, Hauser SL, et al.", "Harrison's Principles of Internal Medicine, 22nd Edition.", "McGraw-Hill, 2022."),
("2.", "Kumar V, Abbas AK, Aster JC.", "Robbins & Cotran Pathologic Basis of Disease, 10th Edition.", "Elsevier, 2020."),
("3.", "Junqueira LC, Carneiro J.", "Junqueira's Basic Histology: Text and Atlas, 15th Edition.", "McGraw-Hill, 2021."),
("4.", "Ryan KJ, Ray CG (eds).", "Sherris Medical Microbiology, 7th Edition.", "McGraw-Hill, 2018."),
("5.", "Tintinalli JE, Ma OJ, Yealy DM, et al.", "Tintinalli's Emergency Medicine: A Comprehensive Study Guide, 9th Edition.", "McGraw-Hill, 2019."),
("6.", "Papapanou PN, Sanz M, Buduneli N, et al.", "Periodontitis: Consensus report of workgroup 2 of the 2017 World Workshop on the Classification of Periodontal and Peri-Implant Diseases and Conditions.", "J Periodontol. 2018;89(Suppl 1):S173–S182."),
("7.", "Tonetti MS, Greenwell H, Kornman KS.", "Staging and grading of periodontitis: Framework and proposal of a new classification and case definition.", "J Clin Periodontol. 2018;45(Suppl 20):S149–S161."),
("8.", "Caton JG, Armitage G, Berglundh T, et al.", "A new classification scheme for periodontal and peri-implant diseases and conditions — Introduction and key changes from the 1999 classification.", "J Periodontol. 2018;89(Suppl 1):S1–S8."),
]
for i, (num, authors, title, source) in enumerate(refs):
ry = 1.35 + i * 0.65
tb_ref = s16.shapes.add_textbox(Inches(0.4), Inches(ry), Inches(12.5), Inches(0.6))
tb_ref.word_wrap = True
tf_ref = tb_ref.text_frame; tf_ref.word_wrap = True
p_ref = tf_ref.paragraphs[0]; p_ref.alignment = PP_ALIGN.LEFT
r_num = p_ref.add_run(); r_num.text = num + " "
r_num.font.name = FONT; r_num.font.size = Pt(13); r_num.font.bold = True; r_num.font.color.rgb = NAVY
r_auth = p_ref.add_run(); r_auth.text = authors + " "
r_auth.font.name = FONT; r_auth.font.size = Pt(13); r_auth.font.color.rgb = DGRAY
r_tit = p_ref.add_run(); r_tit.text = title + " "
r_tit.font.name = FONT; r_tit.font.size = Pt(13); r_tit.font.italic = True; r_tit.font.color.rgb = DGRAY
r_src = p_ref.add_run(); r_src.text = source
r_src.font.name = FONT; r_src.font.size = Pt(13); r_src.font.color.rgb = MGRAY
# Footer
txb(s16, "Compiled by Orris Medical Library | MBBS Seminar Presentation | Periodontics",
0.4, 7.1, 12.5, 0.3, size=10, italic=True, color=MGRAY)
# ════════════════════════════════════════════════════════════
# SAVE
# ════════════════════════════════════════════════════════════
out = "/tmp/workspace/perio-mbbs/Periodontal_Disease_MBBS_Seminar.pptx"
prs.save(out)
print(f"SAVED: {out}")
cd /tmp/workspace/perio-mbbs && python3 build_perio.py 2>&1
ls -lh /tmp/workspace/perio-mbbs/Periodontal_Disease_MBBS_Seminar.pptx
Periodontal Disease MBBS Seminar
Presentation · PPTX
| # | Slide | Visual Element |
|---|---|---|
| 1 | Title slide — navy top band, subtitle, 4 info summary boxes | Info card grid |
| 2 | Table of Contents — 13 topics with descriptors | Two-column layout |
| — | Section divider: Anatomy | Navy full-slide divider |
| 3 | Anatomy of Periodontium — labelled cross-section diagram (drawn shapes), annotation table | Anatomical diagram + Key Takeaway box |
| — | Section divider: Microbiology | |
| 4 | Dental Plaque & Calculus — 5-step progression flowchart, supragingival vs. subgingival comparison table | Flowchart + Table |
| 5 | Key Periodontal Pathogens — full organism table with Red Complex mnemonic | Table + Mnemonic box |
| — | Section divider: Pathogenesis | |
| 6 | Pathogenesis — 6-stage disease progression strip + molecular mechanism table | Progression diagram + Table |
| — | Section divider: Classification | |
| 7 | Classification table — 9 disease types with bone loss and reversibility columns | Table |
| 8 | ANUG & Drug-induced Gingival Overgrowth — two side-by-side detail tables + PCN mnemonic box | Dual-panel layout |
| — | Section divider: Risk Factors & Systemic Links | |
| 9 | Risk Factors — full table with modifiable/non-modifiable categories | Table + Key Takeaway |
| 10 | Systemic Links — hub-and-spoke diagram (6 organ systems) | Diagram + Key Takeaway |
| — | Section divider: Diagnosis | |
| 11 | Diagnosis & Investigations — clinical, radiographic, laboratory table | Table + Key Takeaway |
| — | Section divider: Treatment | |
| 12 | Four-Phase Treatment — 4 phase boxes with bullet points | 2×2 phase grid |
| 13 | Specific Treatments — periodontal abscess and ANUG side-by-side tables | Dual-panel + Key Takeaway |
| — | Section divider: Prevention | |
| 14 | Prevention — full table with frequency and dosing | Table + Key Takeaway |
| 15 | Mnemonics & Key Facts — 4 mnemonic cards + Gingivitis vs. Periodontitis comparison table | Cards + Table |
| 16 | References — 8 properly formatted citations (AMA style) including 3 recent AAP/EFP guidelines | Reference slide |
I don't know anything about dental trauma . Please help me to understand all the concept of dental trauma with easy language along with figure.

| Layer | What it is | Simple analogy |
|---|---|---|
| Enamel | Hard white outer coat | The shell of an egg |
| Dentin | Softer yellow layer underneath | The egg white |
| Pulp | Soft innermost part with nerves + blood vessels | The yolk — the alive part |
| Cementum | Covers the root | Glue that holds the root |
Why does this matter? In dental trauma, the type of injury — and its urgency — depends entirely on which layer is damaged.
DENTAL TRAUMA
├── HARD TISSUE INJURIES (tooth structure)
│ ├── Infraction (crack, no loss of tooth)
│ ├── Ellis Class I (enamel fracture)
│ ├── Ellis Class II (enamel + dentin fracture)
│ └── Ellis Class III (enamel + dentin + pulp fracture)
│
└── SOFT TISSUE / POSITIONAL INJURIES (tooth moves or falls out)
├── Concussion (shaken, not moved)
├── Subluxation (loose, not displaced)
├── Luxation (displaced — 4 types)
│ ├── Extrusive luxation (pushed out)
│ ├── Intrusive luxation (pushed in)
│ └── Lateral luxation (pushed sideways)
└── Avulsion (completely knocked out)
Important: Always consider non-accidental trauma (child abuse) — more than half of child abuse cases involve injuries to the face, head, and neck. The story should match the injury.



| Feature | Detail |
|---|---|
| Pain | None or minimal — no nerves in enamel |
| Appearance | White chip missing, sharp edge visible |
| Sensitivity | None to temperature or air |
| Urgency | LOW — routine dental follow-up |
| ED treatment | Smooth any sharp edge (won't cut tongue); if fragment found, keep it moist |
| Pulp risk | < 3% chance of pulp death |
| Feature | Detail |
|---|---|
| Pain | YES — sensitive to cold, heat, and air |
| Appearance | Creamy-yellow exposed surface (dentin is yellow, not white like enamel) |
| Sensitivity | Significant — even breathing through the mouth hurts |
| Urgency | MODERATE — treat within 24–48 hours |
| ED treatment | Cover exposed dentin with calcium hydroxide paste + aluminium foil/dry foil dressing to protect the pulp |
| Pulp risk | < 10% if treated promptly; rises sharply after 24–48 hours |
Why is dentin so sensitive? Dentin has thousands of microscopic tubes (dentinal tubules) that connect directly to the pulp. When exposed, fluid moves in these tubes with any temperature or air change — this directly stimulates the nerve.
| Feature | Detail |
|---|---|
| Pain | SEVERE — exquisitely painful |
| Appearance | Look for a pink dot or red blush in the centre of the fracture — that is the pulp |
| Sensitivity | Severe pain to everything |
| Urgency | HIGH — dentist/endodontist same day |
| ED treatment | Cover with calcium hydroxide; arrange urgent dental referral for root canal or pulp capping |
| Pulp risk | Pulp will die without treatment → root canal almost always required |
Quick identification trick: At the fracture site — white = enamel (Ellis I), yellow = dentin (Ellis II), pink/red = pulp (Ellis III).
| Class | Layer Involved | Colour at Fracture | Pain | Urgency | ED Treatment |
|---|---|---|---|---|---|
| I | Enamel only | White | None | Routine | Smooth edge; dental follow-up |
| II | Enamel + Dentin | Yellow | Moderate-severe | 24–48 hrs | Cover with Ca(OH)₂ dressing |
| III | Enamel + Dentin + Pulp | Pink/red dot | Severe | Same day | Urgent dental/endodontic referral |
| Feature | Detail |
|---|---|
| Position | Normal — tooth has not moved |
| Mobility | None |
| Pain | Pain when you tap the tooth (tender to percussion) |
| Treatment | Soft diet; dental follow-up; no urgent action |
| Feature | Detail |
|---|---|
| Position | Normal |
| Mobility | YES — visible when you gently tap with a tongue depressor |
| Bleeding | Sulcal bleeding (bleeding around the gum line) may be visible |
| Treatment (primary tooth) | No intervention; dental follow-up |
| Treatment (permanent tooth) | May need splinting; dental follow-up |
Warning: Always X-ray to distinguish intrusion from avulsion — a completely intruded tooth can look identical to a missing (avulsed) tooth.
Never replant an avulsed baby tooth. If you put it back, it can fuse to the bone (ankylosis) and physically block the adult tooth from erupting below it, causing permanent craniofacial damage. Refer to a dentist; a removable prosthetic can be made until the adult tooth comes in.
| Storage Medium | How long PDL survives | Notes |
|---|---|---|
| Hank's Balanced Salt Solution (Save-A-Tooth kit) | 12–24 hours | Best option |
| Milk (cold) | 4–8 hours | Widely available — use this |
| Patient's own saliva (buccal sulcus) | 30–60 minutes | Only if no milk available |
| Saline | 30–60 minutes | Acceptable |
| Water | < 30 minutes | POOR — osmotic damage |
| Dry | < 15 minutes | WORST — cells die rapidly |
Never store in water if you can help it. Cold milk from a fridge is your best real-world option.
| Feature | Primary (Baby) Teeth | Permanent (Adult) Teeth |
|---|---|---|
| Age of eruption | 6 months – 3 years | 6 years – 21 years |
| Replant after avulsion? | NEVER | ALWAYS attempt urgently |
| Luxation: reposition? | Conservative; allow passive repositioning | Active repositioning + splinting ASAP |
| Intrusion: management? | 90% re-erupt spontaneously | Orthodontic/surgical extrusion if mature root |
| Why different? | Risk of damaging the developing permanent tooth bud underneath | No such risk; preservation is priority |
Quick memory trick: Primary tooth = passive management. Permanent tooth = aggressive, urgent management.
| Type | ED Action |
|---|---|
| Ellis I (enamel only) | Smooth sharp edge; routine dental follow-up |
| Ellis II (enamel + dentin) | Cover with Ca(OH)₂ + foil; refer within 24–48 hours |
| Ellis III (enamel + dentin + pulp) | Cover temporarily; same-day urgent dental referral |
| Root fracture | Stabilise; urgent dental referral; do not extract |
| Alveolar bone fracture | Reposition + splint; maxillofacial surgery |
| Type | ED Action |
|---|---|
| Concussion | Soft diet; routine dental follow-up |
| Subluxation | Soft diet; splinting if mobile; follow-up |
| Extrusive luxation | Reposition + splint; urgent referral |
| Intrusive luxation | Do NOT manipulate; refer within 24 hours |
| Lateral luxation | Reposition + splint; urgent referral |
| Avulsion (primary tooth) | Do NOT replant; reassure family; dental follow-up |
| Avulsion (permanent tooth) | Replant immediately OR store in milk; urgent dentist |
| Rule | Memory hook |
|---|---|
| Ellis I = white, no pain | "Just enamel — nothing urgent" |
| Ellis II = yellow, moderate pain | "Dentin exposed — 24-hour clock starts now" |
| Ellis III = pink/red, severe pain | "Pulp exposed — dentist TODAY" |
| Avulsed permanent tooth | "60 minutes — milk or saliva — hold by the crown" |
| Avulsed baby tooth | "Never replant — it will harm the adult tooth below" |
| Intruded tooth | "Do NOT pull it — let it erupt or refer" |
| Multiple teeth moving as a block | "Alveolar fracture — oral surgery needed" |
| Missing tooth after trauma | "X-ray chest — rule out aspiration" |
Generate a quick-reference pocket card PDF for dental trauma .
python3 -c "import reportlab; print('reportlab OK', reportlab.Version)" && mkdir -p /tmp/workspace/dental-card
~/dental-card/build_card.py
"""
Dental Trauma Quick-Reference Pocket Card
2 pages: A5 landscape (148 x 210 mm each) → printed as A4 duplex
Built with ReportLab 4.x
"""
from reportlab.lib.pagesizes import A5, landscape
from reportlab.lib.units import mm
from reportlab.lib import colors
from reportlab.pdfgen import canvas
from reportlab.lib.colors import HexColor
from reportlab.platypus import Table, TableStyle
from reportlab.lib.styles import ParagraphStyle
from reportlab.platypus import Paragraph
from reportlab.lib.enums import TA_CENTER, TA_LEFT
import math
OUT = "/tmp/workspace/dental-card/Dental_Trauma_Pocket_Card.pdf"
# ── Page size ──────────────────────────────────────────────────
PW, PH = landscape(A5) # 210 × 148 mm → 595 × 420 pt
M = 7 * mm # outer margin
# ── Colours ────────────────────────────────────────────────────
NAVY = HexColor("#0D2B5E")
DBLUE = HexColor("#1A3F7A")
LBLUE = HexColor("#D6E4F0")
TEAL = HexColor("#0E6B7A")
LTEAL = HexColor("#D0EFF4")
GREEN = HexColor("#1A6B3C")
LGREEN = HexColor("#D4EDDA")
AMBER = HexColor("#7B4D00")
LAMBER = HexColor("#FFF3CD")
RED = HexColor("#8B0000")
LRED = HexColor("#FFE0E0")
GRAY1 = HexColor("#222222") # main text
GRAY2 = HexColor("#444444")
GRAY3 = HexColor("#777777")
LGRAY = HexColor("#F2F4F6")
WHITE = colors.white
BLACK = colors.black
ORANGE = HexColor("#7A3B00")
LORANGE = HexColor("#FFE8CC")
# ── Fonts ──────────────────────────────────────────────────────
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
# Use built-in Helvetica family (safe fallback)
FONT_REG = "Helvetica"
FONT_BOLD = "Helvetica-Bold"
FONT_ITALIC= "Helvetica-Oblique"
FONT_BI = "Helvetica-BoldOblique"
# ── Canvas helpers ─────────────────────────────────────────────
def new_canvas():
c = canvas.Canvas(OUT, pagesize=(PW, PH))
c.setTitle("Dental Trauma Quick-Reference Pocket Card")
c.setAuthor("Orris Medical Library")
c.setSubject("Dental Trauma | Emergency Medicine Quick Reference")
return c
def bg(c, colour=WHITE):
c.setFillColor(colour)
c.rect(0, 0, PW, PH, fill=1, stroke=0)
def hline(c, x, y, w, colour=NAVY, lw=0.5):
c.setStrokeColor(colour)
c.setLineWidth(lw)
c.line(x, y, x + w, y)
def vline(c, x, y, h, colour=NAVY, lw=0.5):
c.setStrokeColor(colour)
c.setLineWidth(lw)
c.line(x, y, x, y + h)
def filled_rect(c, x, y, w, h, fill, stroke=None, lw=0.5, radius=0):
c.setFillColor(fill)
if stroke:
c.setStrokeColor(stroke)
c.setLineWidth(lw)
if radius:
c.roundRect(x, y, w, h, radius, fill=1, stroke=1)
else:
c.rect(x, y, w, h, fill=1, stroke=1)
else:
if radius:
c.roundRect(x, y, w, h, radius, fill=1, stroke=0)
else:
c.rect(x, y, w, h, fill=1, stroke=0)
def txt(c, text, x, y, size=7, font=FONT_REG, color=GRAY1,
align="left", maxw=None):
c.setFont(font, size)
c.setFillColor(color)
if align == "center":
c.drawCentredString(x, y, text)
elif align == "right":
c.drawRightString(x, y, text)
else:
c.drawString(x, y, text)
def wrapped_txt(c, text, x, y, maxw, size=7, font=FONT_REG,
color=GRAY1, leading=None):
"""Simple word-wrap text block. Returns final y."""
if leading is None:
leading = size * 1.35
words = text.split()
line = ""
c.setFont(font, size)
c.setFillColor(color)
for word in words:
test = (line + " " + word).strip()
if c.stringWidth(test, font, size) <= maxw:
line = test
else:
c.drawString(x, y, line)
y -= leading
line = word
if line:
c.drawString(x, y, line)
y -= leading
return y
def badge(c, label, x, y, w, h, bg_col, text_col=WHITE, font=FONT_BOLD, size=6.5, radius=2):
filled_rect(c, x, y, w, h, bg_col, radius=radius)
c.setFont(font, size)
c.setFillColor(text_col)
c.drawCentredString(x + w / 2, y + (h - size) / 2 + 1, label)
# ── Column divider helper ──────────────────────────────────────
def col_divider(c, x, y_top, y_bot):
vline(c, x, y_bot, y_top - y_bot, colour=HexColor("#CCCCCC"), lw=0.4)
# ══════════════════════════════════════════════════════════════════
# PAGE 1 — FRONT CARD
# ══════════════════════════════════════════════════════════════════
def page1(c):
bg(c, WHITE)
# ── TOP HEADER BAR ──
hdr_h = 18 * mm
filled_rect(c, 0, PH - hdr_h, PW, hdr_h, NAVY)
# Title
c.setFont(FONT_BOLD, 14)
c.setFillColor(WHITE)
c.drawString(M, PH - hdr_h + 6.5 * mm, "DENTAL TRAUMA")
c.setFont(FONT_REG, 8)
c.drawString(M, PH - hdr_h + 2.5 * mm, "Quick-Reference Pocket Card | Emergency Management Guide")
# Right side badge
filled_rect(c, PW - 44 * mm, PH - hdr_h + 3 * mm, 37 * mm, 12 * mm,
HexColor("#1A3F7A"), radius=2)
c.setFont(FONT_BOLD, 7)
c.setFillColor(WHITE)
c.drawCentredString(PW - 25.5 * mm, PH - hdr_h + 8.5 * mm, "ORRIS MEDICAL LIBRARY")
c.setFont(FONT_REG, 6)
c.drawCentredString(PW - 25.5 * mm, PH - hdr_h + 4.5 * mm, "Emergency Dentistry Reference")
# ── CONTENT STARTS ──
y0 = PH - hdr_h - 3 * mm # top of content area
ybot = 8 * mm # bottom margin
content_h = y0 - ybot
# Three columns
col_w = (PW - 2 * M - 4 * mm) / 3
c1x = M
c2x = M + col_w + 2 * mm
c3x = M + 2 * col_w + 4 * mm
# ── COLUMN 1: TOOTH ANATOMY + ELLIS CLASSIFICATION ──
# Anatomy mini-diagram (drawn shapes)
ax = c1x
ay = y0 - 2 * mm
dw = col_w - 2 * mm
dh = 30 * mm
# Section label
filled_rect(c, ax, ay - 5 * mm, dw, 4.5 * mm, NAVY, radius=1)
c.setFont(FONT_BOLD, 7)
c.setFillColor(WHITE)
c.drawCentredString(ax + dw / 2, ay - 3.5 * mm, "TOOTH ANATOMY")
# Draw simple tooth cross-section
tx = ax + 5 * mm
ty = ay - 6.5 * mm
tw = dw - 10 * mm
th = dh - 2 * mm
# Enamel (outer white shell - crown portion)
crown_h = th * 0.42
root_h = th * 0.58
# Alveolar bone block
filled_rect(c, tx - 3*mm, ty - root_h, tw + 6*mm, root_h,
HexColor("#E8DDD0"))
# Root dentin
filled_rect(c, tx + tw*0.15, ty - root_h + 1*mm, tw*0.7, root_h - 1*mm,
HexColor("#F5ECD5"))
# Pulp canal
filled_rect(c, tx + tw*0.35, ty - root_h + 1*mm, tw*0.3, root_h + crown_h*0.6,
HexColor("#C0392B"), radius=1)
# Dentin crown
filled_rect(c, tx + tw*0.1, ty - crown_h, tw*0.8, crown_h + 1*mm,
HexColor("#F5ECD5"))
# Enamel crown
filled_rect(c, tx + tw*0.1, ty - crown_h, tw*0.8, crown_h,
HexColor("#F0EDE5"), stroke=HexColor("#AAAAAA"), lw=0.3)
# Pulp chamber crown
filled_rect(c, tx + tw*0.3, ty - crown_h * 0.7, tw*0.4, crown_h*0.65,
HexColor("#C0392B"), radius=1)
# PDL line (thin)
c.setStrokeColor(HexColor("#4CAF50"))
c.setLineWidth(1.0)
c.line(tx + tw*0.13, ty - root_h + 1*mm, tx + tw*0.13, ty)
c.line(tx + tw*0.87, ty - root_h + 1*mm, tx + tw*0.87, ty)
# Gingival line
c.setStrokeColor(HexColor("#EF9A9A"))
c.setLineWidth(0.5)
c.line(tx - 1*mm, ty, tx + tw + 1*mm, ty)
# Labels with leader lines
label_x = ax + dw - 1*mm
label_pts = [
(ty - crown_h * 0.5, "Enamel", HexColor("#555555")),
(ty - crown_h * 0.15, "Dentin", HexColor("#8B6914")),
(ty - crown_h * 0.4 + 1, "Pulp", HexColor("#C0392B")),
(ty - root_h * 0.5, "Root", HexColor("#555555")),
(ty - root_h * 0.8, "Alveolar\nBone", HexColor("#8B7355")),
(ty + 0.5*mm, "Gingival\nmargin", HexColor("#C0392B")),
]
right_edge = tx + tw * 0.87
for (ly, lbl, lcol) in label_pts:
c.setStrokeColor(HexColor("#AAAAAA"))
c.setLineWidth(0.3)
c.line(right_edge, ly, label_x - 4*mm, ly)
lines = lbl.split("\n")
for i, ln in enumerate(lines):
c.setFont(FONT_REG, 5.5)
c.setFillColor(lcol)
c.drawString(label_x - 3.5*mm, ly - 1.5 + i * (-5.5), ln)
# PDL label
c.setFont(FONT_REG, 5)
c.setFillColor(HexColor("#1A6B3C"))
c.drawString(ax, ty - root_h * 0.3, "PDL")
# caption
c.setFont(FONT_ITALIC, 5.5)
c.setFillColor(GRAY3)
c.drawCentredString(ax + dw/2, ty - root_h - 2*mm, "Schematic cross-section")
# ── ELLIS CLASSIFICATION ──
ey = ty - root_h - 5 * mm
filled_rect(c, ax, ey, dw, 4.5 * mm, DBLUE, radius=1)
c.setFont(FONT_BOLD, 7)
c.setFillColor(WHITE)
c.drawCentredString(ax + dw/2, ey + 1.5*mm, "ELLIS FRACTURE CLASSIFICATION")
ey -= 1.5 * mm
ellis = [
("I", "Enamel only", "None", "Routine", LGREEN, GREEN, "Smooth edge only"),
("II", "Enamel + Dentin", "Moderate","24–48 hrs", LAMBER, AMBER, "Ca(OH)₂ dressing"),
("III", "Enamel + Dentin + Pulp","SEVERE", "SAME DAY", LRED, RED, "Urgent root canal"),
]
colours_dot = [HexColor("#EEEEEE"), HexColor("#F5ECD5"), HexColor("#FFCCCC")]
dot_border = [HexColor("#AAAAAA"), HexColor("#C8A020"), HexColor("#CC3333")]
row_h = 13.5 * mm
for idx, (cls, layers, pain, urg, bg_c, tx_c, action) in enumerate(ellis):
ry = ey - (idx + 1) * row_h - idx * 1 * mm
filled_rect(c, ax, ry, dw, row_h, bg_c, stroke=HexColor("#CCCCCC"), lw=0.3, radius=2)
# Class badge
badge(c, f"Class {cls}", ax + 1*mm, ry + row_h - 5.5*mm, 14*mm, 4.5*mm, tx_c)
# Dot colour indicator
filled_rect(c, ax + 16*mm, ry + row_h - 5*mm, 4*mm, 4*mm,
colours_dot[idx], stroke=dot_border[idx], lw=0.5, radius=1)
# Layers
c.setFont(FONT_BOLD, 6.5)
c.setFillColor(GRAY1)
c.drawString(ax + 1.5*mm, ry + row_h - 9*mm, layers)
# Pain + urgency
c.setFont(FONT_REG, 5.8)
c.setFillColor(GRAY2)
c.drawString(ax + 1.5*mm, ry + row_h - 12*mm, f"Pain: {pain} | Urgency: {urg}")
# Action
c.setFont(FONT_BOLD, 5.8)
c.setFillColor(tx_c)
c.drawString(ax + 1.5*mm, ry + 2*mm, f"Tx: {action}")
# Colour key
ky = ey - 3 * row_h - 4 * mm
c.setFont(FONT_ITALIC, 5.5)
c.setFillColor(GRAY3)
c.drawString(ax, ky, "Colour at fracture: White=I Yellow=II Pink/Red=III")
# ── COLUMN 2: POSITIONAL INJURIES ──
px = c2x
py = y0 - 2 * mm
pw2 = col_w - 2 * mm
col_divider(c, c2x - 1*mm, y0 - 1*mm, ybot)
filled_rect(c, px, py - 5*mm, pw2, 4.5*mm, TEAL, radius=1)
c.setFont(FONT_BOLD, 7)
c.setFillColor(WHITE)
c.drawCentredString(px + pw2/2, py - 3.5*mm, "POSITIONAL INJURIES")
pos_injuries = [
("CONCUSSION",
"Shaken — NOT displaced",
"Tender to percussion\nNo mobility",
"Soft diet\nDental follow-up",
LBLUE, DBLUE),
("SUBLUXATION",
"Loose — NOT displaced",
"Mobile + sulcal bleeding\nNormal position",
"Splint if needed\nDental 48 hrs",
LBLUE, DBLUE),
("EXTRUSIVE LUX.",
"Partially out — appears long",
"Mobile, elongated\nPDL torn",
"Reposition + splint\nURGENT",
LTEAL, TEAL),
("INTRUSIVE LUX.",
"Pushed INTO socket",
"Appears short/absent\nNOT mobile",
"DO NOT manipulate!\nRefer <24 hrs",
LORANGE, ORANGE),
("LATERAL LUX.",
"Displaced sideways",
"Fixed in new position\n±Alveolar Fx",
"Reposition + splint\nURGENT",
LTEAL, TEAL),
]
pi_row_h = 15.5 * mm
for idx, (name, desc, signs, mgmt, bg_c, tc) in enumerate(pos_injuries):
ry2 = py - 6*mm - (idx+1)*pi_row_h - idx*0.8*mm
filled_rect(c, px, ry2, pw2, pi_row_h, bg_c,
stroke=HexColor("#BBBBBB"), lw=0.3, radius=2)
# Name badge
badge(c, name, px + 1*mm, ry2 + pi_row_h - 5.5*mm,
pw2 - 2*mm, 4.5*mm, tc, WHITE, size=6)
# Description
c.setFont(FONT_ITALIC, 6)
c.setFillColor(GRAY2)
c.drawString(px + 1.5*mm, ry2 + pi_row_h - 9.5*mm, desc)
# Signs
c.setFont(FONT_REG, 5.8)
c.setFillColor(GRAY1)
for li, line in enumerate(signs.split("\n")):
c.drawString(px + 1.5*mm, ry2 + pi_row_h - 12.5*mm - li*5.5, "• " + line)
# Management
c.setFont(FONT_BOLD, 5.8)
c.setFillColor(tc)
for li, line in enumerate(mgmt.split("\n")):
c.drawString(px + pw2*0.5, ry2 + pi_row_h - 12.5*mm - li*5.5, "→ " + line)
# ── COLUMN 3: AVULSION ──
col_divider(c, c3x - 1*mm, y0 - 1*mm, ybot)
vx = c3x
vy = y0 - 2*mm
vw = col_w - 1*mm
filled_rect(c, vx, vy - 5*mm, vw, 4.5*mm, RED, radius=1)
c.setFont(FONT_BOLD, 7)
c.setFillColor(WHITE)
c.drawCentredString(vx + vw/2, vy - 3.5*mm, "AVULSION (Tooth Knocked Out)")
# Primary vs Permanent
half_w = (vw - 1*mm) / 2
# Primary
filled_rect(c, vx, vy - 6*mm - 19*mm, half_w, 19*mm,
LRED, stroke=HexColor("#CCAAAA"), lw=0.4, radius=2)
c.setFont(FONT_BOLD, 6.5)
c.setFillColor(RED)
c.drawCentredString(vx + half_w/2, vy - 7.5*mm - 1*mm, "PRIMARY TOOTH")
c.setFont(FONT_BOLD, 7)
c.setFillColor(RED)
c.drawCentredString(vx + half_w/2, vy - 10.5*mm - 1*mm, "NEVER REPLANT")
c.setFont(FONT_REG, 6)
c.setFillColor(GRAY1)
lines_p = [
"Ankylosis blocks",
"permanent eruption",
"→ Dental follow-up",
"→ Space maintainer",
]
for li, line in enumerate(lines_p):
c.drawCentredString(vx + half_w/2, vy - 13.5*mm - li*5.5, line)
# Permanent
filled_rect(c, vx + half_w + 1*mm, vy - 6*mm - 19*mm, half_w, 19*mm,
LGREEN, stroke=HexColor("#AACCAA"), lw=0.4, radius=2)
c.setFont(FONT_BOLD, 6.5)
c.setFillColor(GREEN)
c.drawCentredString(vx + half_w + 1*mm + half_w/2, vy - 7.5*mm - 1*mm, "PERMANENT TOOTH")
c.setFont(FONT_BOLD, 6.5)
c.setFillColor(GREEN)
c.drawCentredString(vx + half_w + 1*mm + half_w/2, vy - 10.5*mm - 1*mm, "DENTAL EMERGENCY")
c.setFont(FONT_REG, 6)
c.setFillColor(GRAY1)
lines_pm = [
"Replant ≤ 60 min",
"Hold by CROWN",
"→ Urgent dentist",
"→ Root canal later",
]
for li, line in enumerate(lines_pm):
c.drawCentredString(vx + half_w + 1*mm + half_w/2, vy - 13.5*mm - li*5.5, line)
# 60-MINUTE RULE banner
rule_y = vy - 27*mm
filled_rect(c, vx, rule_y, vw, 7*mm, NAVY, radius=2)
c.setFont(FONT_BOLD, 7)
c.setFillColor(HexColor("#FFD700"))
c.drawCentredString(vx + vw/2, rule_y + 4*mm, "⏱ THE 60-MINUTE RULE")
c.setFont(FONT_REG, 6)
c.setFillColor(WHITE)
c.drawCentredString(vx + vw/2, rule_y + 1*mm, "PDL cells die after 60 min dry → TIME IS CRITICAL")
# Replantation steps
rep_y = rule_y - 2*mm
c.setFont(FONT_BOLD, 6.5)
c.setFillColor(DBLUE)
c.drawString(vx, rep_y - 4*mm, "REPLANTATION STEPS:")
steps = [
("1", "Hold tooth by CROWN — never touch the root"),
("2", "Rinse gently with saline — do NOT scrub"),
("3", "Insert root into socket (concave side → tongue)"),
("4", "Bite on gauze to hold position"),
("5", "Splinting by dentist within 60 minutes"),
("6", "Antibiotics (penicillin) + tetanus if needed"),
]
for si, (num, step) in enumerate(steps):
sy = rep_y - 7.5*mm - si*7*mm
filled_rect(c, vx, sy, 4.5*mm, 5*mm, DBLUE, radius=1)
c.setFont(FONT_BOLD, 6.5)
c.setFillColor(WHITE)
c.drawCentredString(vx + 2.25*mm, sy + 1*mm, num)
c.setFont(FONT_REG, 6)
c.setFillColor(GRAY1)
c.drawString(vx + 5.5*mm, sy + 1*mm, step)
# Storage media table
sm_y = rep_y - 7.5*mm - 6*7*mm - 3*mm
c.setFont(FONT_BOLD, 6.5)
c.setFillColor(DBLUE)
c.drawString(vx, sm_y, "STORAGE MEDIA (if immediate replant not possible):")
media = [
("Hank's BSS / Save-A-Tooth", "12–24 h", GREEN, LGREEN),
("Cold milk", "4–8 h", TEAL, LTEAL),
("Saliva (buccal sulcus)", "30–60 min",AMBER, LAMBER),
("Saline", "30–60 min",AMBER, LAMBER),
("Water", "< 30 min", RED, LRED),
("DRY", "< 15 min", RED, LRED),
]
for mi, (medium, duration, tc2, bg2) in enumerate(media):
my = sm_y - 5.5*mm - mi*5.5*mm
filled_rect(c, vx, my, vw, 5*mm, bg2, stroke=HexColor("#BBBBBB"), lw=0.2)
c.setFont(FONT_REG, 5.8)
c.setFillColor(GRAY1)
c.drawString(vx + 1.5*mm, my + 1.2*mm, medium)
c.setFont(FONT_BOLD, 5.8)
c.setFillColor(tc2)
c.drawRightString(vx + vw - 1.5*mm, my + 1.2*mm, duration)
# ── BOTTOM FOOTER ──
filled_rect(c, 0, 0, PW, 7*mm, LGRAY)
c.setFont(FONT_ITALIC, 5.5)
c.setFillColor(GRAY3)
c.drawString(M, 2.5*mm,
"Sources: Tintinalli's Emergency Medicine · Rosen's Emergency Medicine · Roberts & Hedges' Clinical Procedures · Harriet Lane Handbook 23e")
c.setFont(FONT_BOLD, 5.5)
c.setFillColor(NAVY)
c.drawRightString(PW - M, 2.5*mm, "PAGE 1 / 2")
# ══════════════════════════════════════════════════════════════════
# PAGE 2 — BACK CARD
# ══════════════════════════════════════════════════════════════════
def page2(c):
bg(c, WHITE)
# ── TOP HEADER ──
hdr_h = 14 * mm
filled_rect(c, 0, PH - hdr_h, PW, hdr_h, DBLUE)
c.setFont(FONT_BOLD, 11)
c.setFillColor(WHITE)
c.drawString(M, PH - hdr_h + 5*mm, "DENTAL TRAUMA — Management Summary & Protocols")
c.setFont(FONT_REG, 7)
c.setFillColor(HexColor("#B8CCE8"))
c.drawRightString(PW - M, PH - hdr_h + 5*mm, "Quick-Reference | Orris Medical Library")
y0 = PH - hdr_h - 2.5*mm
ybot = 8*mm
# Two main columns
col_lw = (PW - 2*M - 3*mm) * 0.48
col_rw = (PW - 2*M - 3*mm) * 0.52
lx = M
rx = M + col_lw + 3*mm
col_divider(c, rx - 1.5*mm, y0, ybot)
# ── LEFT COLUMN ──────────────────────────────────────────────
# === MANAGEMENT SUMMARY TABLE ===
filled_rect(c, lx, y0 - 5*mm, col_lw, 4.5*mm, NAVY, radius=1)
c.setFont(FONT_BOLD, 7)
c.setFillColor(WHITE)
c.drawCentredString(lx + col_lw/2, y0 - 3.2*mm, "MANAGEMENT SUMMARY")
mgmt_data = [
# (type, injury, action, urgency, bg)
("FRACTURE", "Infraction", "No treatment; dental follow-up", "Routine", HexColor("#F9F9F9")),
("FRACTURE", "Ellis I (enamel)", "Smooth edge; keep fragment moist", "Routine", LGRAY),
("FRACTURE", "Ellis II (enamel+dentin)","Ca(OH)₂ dressing; cover exposed dentin", "24–48 h", HexColor("#FFFBF0")),
("FRACTURE", "Ellis III (pulp exp.)", "Cover; urgent endodontic referral", "SAME DAY", HexColor("#FFF0F0")),
("FRACTURE", "Root fracture", "Stabilise; urgent dental referral", "Urgent", LGRAY),
("FRACTURE", "Alveolar bone Fx", "Reposition + splint; oral surgery", "Urgent", HexColor("#F0F4FF")),
("POSITION", "Concussion", "Soft diet; routine dental follow-up", "Routine", HexColor("#F0F8FF")),
("POSITION", "Subluxation", "Flexible splint if mobile; 48 hr dentist", "48 h", HexColor("#F0F8FF")),
("POSITION", "Extrusive luxation", "Reposition + splint immediately", "Urgent", HexColor("#F0FFF4")),
("POSITION", "Intrusive luxation", "NO manipulation — refer within 24 h", "24 h", HexColor("#FFF8E8")),
("POSITION", "Lateral luxation", "Reposition + splint; urgent referral", "Urgent", HexColor("#F0FFF4")),
("AVULSION", "Primary tooth", "DO NOT replant — dental follow-up", "Routine", HexColor("#FFF0F0")),
("AVULSION", "Permanent tooth", "Replant / milk storage — URGENT dentist", "EMERGENCY", HexColor("#FFF0F0")),
]
type_colors = {
"FRACTURE": (DBLUE, HexColor("#E8F0FF")),
"POSITION": (TEAL, HexColor("#E0F4F4")),
"AVULSION": (RED, HexColor("#FFE8E8")),
}
row_h_t = 5.8*mm
last_type = None
cur_y = y0 - 6.5*mm
# Header row
filled_rect(c, lx, cur_y - 4.5*mm, col_lw, 4.5*mm, HexColor("#334455"))
col_xs = [lx, lx + 14*mm, lx + 43*mm, col_lw - 14*mm + lx]
hdrs = ["Type", "Injury", "ED Action", "Urgency"]
for hci, (hx, ht) in enumerate(zip(col_xs, hdrs)):
c.setFont(FONT_BOLD, 6)
c.setFillColor(WHITE)
if hci < len(hdrs) - 1:
c.drawString(hx + 1*mm, cur_y - 2.5*mm, ht)
else:
c.drawRightString(col_lw + lx - 1*mm, cur_y - 2.5*mm, ht)
cur_y -= 5*mm
for (typ, injury, action, urg, row_bg) in mgmt_data:
tc_type, _ = type_colors[typ]
urg_col = (RED if "EMERGENCY" in urg or "SAME DAY" in urg
else AMBER if "Urgent" in urg or "24" in urg or "48" in urg
else GREEN)
filled_rect(c, lx, cur_y - row_h_t, col_lw, row_h_t,
row_bg, stroke=HexColor("#DDDDDD"), lw=0.2)
# Type badge
if typ != last_type:
badge(c, typ, lx + 0.5*mm, cur_y - row_h_t + 1.2*mm,
12*mm, 3.5*mm, tc_type, size=5)
last_type = typ
# Injury
c.setFont(FONT_BOLD if "Ellis III" in injury or "Primary" in injury or "Permanent" in injury else FONT_REG, 6)
c.setFillColor(GRAY1)
c.drawString(lx + 14.5*mm, cur_y - row_h_t + 2*mm, injury)
# Action
c.setFont(FONT_REG, 5.8)
c.setFillColor(GRAY2)
c.drawString(lx + 43.5*mm, cur_y - row_h_t + 2*mm, action)
# Urgency
c.setFont(FONT_BOLD, 5.8)
c.setFillColor(urg_col)
c.drawRightString(col_lw + lx - 1*mm, cur_y - row_h_t + 2*mm, urg)
cur_y -= row_h_t + 0.3*mm
# === PRIMARY vs PERMANENT QUICK RULE ===
prule_y = cur_y - 3*mm
filled_rect(c, lx, prule_y - 13*mm, col_lw, 13*mm,
HexColor("#EEF4FF"), stroke=NAVY, lw=0.5, radius=2)
c.setFont(FONT_BOLD, 6.5)
c.setFillColor(NAVY)
c.drawString(lx + 2*mm, prule_y - 3*mm, "PRIMARY TOOTH → Conservative (observe)")
c.setFont(FONT_BOLD, 6.5)
c.setFillColor(RED)
c.drawString(lx + 2*mm, prule_y - 7*mm, "PERMANENT TOOTH → Aggressive (urgent action)")
c.setFont(FONT_ITALIC, 5.8)
c.setFillColor(GRAY2)
c.drawString(lx + 2*mm, prule_y - 11*mm,
"Memory: Primary = Passive. Permanent = Pursue urgently.")
# ── RIGHT COLUMN ─────────────────────────────────────────────
ry_cur = y0
# === EXAMINATION APPROACH ===
filled_rect(c, rx, ry_cur - 5*mm, col_rw, 4.5*mm, TEAL, radius=1)
c.setFont(FONT_BOLD, 7)
c.setFillColor(WHITE)
c.drawCentredString(rx + col_rw/2, ry_cur - 3.2*mm, "CLINICAL EXAMINATION APPROACH")
ry_cur -= 5.5*mm
exam_steps = [
("H HISTORY",
["When + how did injury occur?",
"How long was tooth outside mouth?",
"Where is the tooth now?",
"Pain / sensitivity / bite change?"]),
("L LOOK",
["Fracture: colour at fracture line (white/yellow/pink)",
"Displacement: direction? Multiple teeth as block?",
"Soft tissue: lacerations, embedded fragments"]),
("F FEEL",
["Percussion test: tenderness = concussion/sublux",
"Mobility test: two tongue blades gently",
"Occlusion: malocclusion = alveolar Fx"]),
("X X-RAY",
["Always X-ray if tooth cannot be located",
"CXR if aspiration risk (unconscious patient)",
"Below diaphragm → passes naturally (no retrieval)"]),
]
exam_colors = [DBLUE, TEAL, AMBER, RED]
for ei, (heading, bullets) in enumerate(exam_steps):
eh = (len(bullets) + 1) * 5.2*mm + 1.5*mm
filled_rect(c, rx, ry_cur - eh, col_rw, eh,
LGRAY, stroke=HexColor("#CCCCCC"), lw=0.2, radius=1)
c.setFont(FONT_BOLD, 6.5)
c.setFillColor(exam_colors[ei])
c.drawString(rx + 1.5*mm, ry_cur - 4.5*mm, heading)
for bi, bl in enumerate(bullets):
c.setFont(FONT_REG, 5.8)
c.setFillColor(GRAY1)
c.drawString(rx + 3*mm, ry_cur - 7*mm - bi*5.2*mm, "• " + bl)
ry_cur -= eh + 1*mm
# === MEDICATIONS ===
ry_cur -= 1*mm
filled_rect(c, rx, ry_cur - 5*mm, col_rw, 4.5*mm, AMBER, radius=1)
c.setFont(FONT_BOLD, 7)
c.setFillColor(WHITE)
c.drawCentredString(rx + col_rw/2, ry_cur - 3.2*mm, "MEDICATIONS AFTER DENTAL TRAUMA")
ry_cur -= 5.5*mm
meds = [
("Analgesia", "NSAIDs or paracetamol", "All significant injuries"),
("Antibiotics", "Penicillin V / Amoxicillin 500 mg TDS", "Replantation, open Fx, contamination"),
("CHX rinse", "Chlorhexidine 0.12% BD × 1 wk", "Post-replantation; ANUG"),
("Tetanus", "Update if not immunised within 5 yr", "Open/contaminated injuries"),
]
med_row_h = 7*mm
for mi, (drug, dose, indication) in enumerate(meds):
my2 = ry_cur - (mi+1)*med_row_h - mi*0.5*mm
filled_rect(c, rx, my2, col_rw, med_row_h,
LAMBER if mi%2==0 else WHITE,
stroke=HexColor("#DDDDDD"), lw=0.2)
c.setFont(FONT_BOLD, 6)
c.setFillColor(AMBER)
c.drawString(rx + 1.5*mm, my2 + 4.5*mm, drug)
c.setFont(FONT_REG, 5.8)
c.setFillColor(GRAY1)
c.drawString(rx + 1.5*mm, my2 + 1.2*mm, dose)
c.setFont(FONT_ITALIC, 5.5)
c.setFillColor(GRAY3)
c.drawRightString(rx + col_rw - 1.5*mm, my2 + 1.2*mm, indication)
ry_cur -= 4 * med_row_h + 5*mm
# === KEY MNEMONICS PANEL ===
filled_rect(c, rx, ry_cur - 5*mm, col_rw, 4.5*mm, RED, radius=1)
c.setFont(FONT_BOLD, 7)
c.setFillColor(WHITE)
c.drawCentredString(rx + col_rw/2, ry_cur - 3.2*mm, "KEY MNEMONICS & RULES")
ry_cur -= 5.5*mm
mnemonics = [
("COLOUR RULE",
"White = Ellis I Yellow = Ellis II Pink/Red = Ellis III",
DBLUE, HexColor("#EEF0FF")),
("60-MIN RULE",
"Permanent tooth must be replanted or stored in milk within 60 minutes",
GREEN, LGREEN),
("NEVER REPLANT",
"Primary (baby) teeth — ankylosis blocks permanent tooth eruption",
RED, LRED),
("NO MANIPULATION",
"Intruded teeth — do NOT pull/push; refer within 24 hours",
ORANGE, LORANGE),
("MISSING TOOTH",
"Cannot locate tooth → CXR to rule out aspiration",
TEAL, LTEAL),
]
for mi2, (label, rule, tc2, bg2) in enumerate(mnemonics):
mnem_h = 8.5*mm
my3 = ry_cur - (mi2+1)*mnem_h - mi2*0.8*mm
filled_rect(c, rx, my3, col_rw, mnem_h, bg2,
stroke=HexColor("#CCCCCC"), lw=0.2, radius=2)
badge(c, label, rx + 1*mm, my3 + mnem_h - 4.8*mm,
22*mm, 3.8*mm, tc2, size=5.5)
c.setFont(FONT_REG, 6)
c.setFillColor(GRAY1)
# wrap text
words = rule.split()
line_t = ""; lines_out = []
for w in words:
test = (line_t + " " + w).strip()
if c.stringWidth(test, FONT_REG, 6) <= col_rw - 26*mm:
line_t = test
else:
lines_out.append(line_t); line_t = w
if line_t: lines_out.append(line_t)
for li, ln in enumerate(lines_out):
c.drawString(rx + 24*mm, my3 + mnem_h - 3.5*mm - li*5.5, ln)
# ── BOTTOM FOOTER ──
filled_rect(c, 0, 0, PW, 7*mm, LGRAY)
c.setFont(FONT_ITALIC, 5.5)
c.setFillColor(GRAY3)
c.drawString(M, 2.5*mm,
"For educational use only. Clinical decisions must be made by qualified clinicians based on individual patient assessment.")
c.setFont(FONT_BOLD, 5.5)
c.setFillColor(NAVY)
c.drawRightString(PW - M, 2.5*mm, "PAGE 2 / 2")
# ══════════════════════════════════════════════════════════════════
# BUILD PDF
# ══════════════════════════════════════════════════════════════════
c = new_canvas()
page1(c)
c.showPage()
page2(c)
c.showPage()
c.save()
print(f"SAVED: {OUT}")
cd /tmp/workspace/dental-card && python3 build_card.py 2>&1
ls -lh /tmp/workspace/dental-card/Dental_Trauma_Pocket_Card.pdf
Dental Trauma Pocket Card
PDF Document · PDF
| Column | Content |
|---|---|
| Left | Tooth anatomy cross-section diagram (enamel, dentin, pulp, root, PDL, alveolar bone — all labelled) + Ellis Classification cards (Class I green / Class II amber / Class III red — with colour, pain, urgency, and ED treatment for each) |
| Centre | 5 positional injury cards — Concussion, Subluxation, Extrusive Luxation, Intrusive Luxation, Lateral Luxation — each with description, signs, and management |
| Right | Avulsion section — Primary vs. Permanent split panel + the 60-Minute Rule banner + 6-step replantation guide + storage media table ranked best to worst (Hank's BSS → milk → saliva → saline → water → dry) with survival times |
| Column | Content |
|---|---|
| Left | Full 13-row management summary table covering all fracture types and all positional injuries with urgency colour-coding (green/amber/red) + Primary vs. Permanent quick rule box |
| Right | Clinical examination approach (History → Look → Feel → X-ray, each with bullet points) + Medications after trauma (analgesia, antibiotics, CHX rinse, tetanus) + 5 key mnemonics panel (Colour Rule, 60-Min Rule, Never Replant, No Manipulation, Missing Tooth/CXR) |
Create a quick reference pdf of this
python3 -c "import reportlab; print('OK', reportlab.Version)" && mkdir -p /tmp/workspace/perio-card
~/perio-card/build_perio_card.py
"""
Periodontal Disease Quick-Reference Pocket Card
2 pages A5 landscape — built with ReportLab 4.x
"""
from reportlab.lib.pagesizes import A5, landscape
from reportlab.lib.units import mm
from reportlab.lib import colors
from reportlab.pdfgen import canvas
from reportlab.lib.colors import HexColor
from lxml import etree # not used but imported safely
import math
OUT = "/tmp/workspace/perio-card/Periodontal_Disease_Pocket_Card.pdf"
PW, PH = landscape(A5) # 595 x 420 pt (210 x 148 mm)
M = 6.5 * mm
# ── Colour palette ─────────────────────────────────────────────
NAVY = HexColor("#0D2B5E")
DBLUE = HexColor("#1A3F7A")
LBLUE = HexColor("#D6E4F0")
LLBLUE = HexColor("#EEF4FB")
TEAL = HexColor("#0E6B7A")
LTEAL = HexColor("#D0EFF4")
GREEN = HexColor("#1A6B3C")
LGREEN = HexColor("#D4EDDA")
AMBER = HexColor("#7B4D00")
LAMBER = HexColor("#FFF3CD")
RED = HexColor("#8B0000")
DRED = HexColor("#CC2222")
LRED = HexColor("#FFE0E0")
PINK = HexColor("#FFCCCC")
PURPLE = HexColor("#4A235A")
LPURPLE = HexColor("#F0E6F6")
GRAY1 = HexColor("#1A1A1A")
GRAY2 = HexColor("#444444")
GRAY3 = HexColor("#777777")
LGRAY = HexColor("#F2F4F6")
LGRAY2 = HexColor("#E8EAED")
WHITE = colors.white
BLACK = colors.black
ORANGE = HexColor("#7A3B00")
LORANGE = HexColor("#FFE8CC")
RUST = HexColor("#8B2500")
LRUST = HexColor("#FFE8D8")
FONT_REG = "Helvetica"
FONT_BOLD = "Helvetica-Bold"
FONT_IT = "Helvetica-Oblique"
FONT_BI = "Helvetica-BoldOblique"
# ── Helpers ────────────────────────────────────────────────────
def new_canvas():
c = canvas.Canvas(OUT, pagesize=(PW, PH))
c.setTitle("Periodontal Disease Quick-Reference Pocket Card")
c.setAuthor("Orris Medical Library")
c.setSubject("Periodontal Disease | Clinical Quick Reference")
return c
def bg(c, col=WHITE):
c.setFillColor(col)
c.rect(0, 0, PW, PH, fill=1, stroke=0)
def filled_rect(c, x, y, w, h, fill, stroke=None, lw=0.5, radius=0):
c.setFillColor(fill)
if stroke:
c.setStrokeColor(stroke)
c.setLineWidth(lw)
if radius:
c.roundRect(x, y, w, h, radius, fill=1, stroke=1)
else:
c.rect(x, y, w, h, fill=1, stroke=1)
else:
if radius:
c.roundRect(x, y, w, h, radius, fill=1, stroke=0)
else:
c.rect(x, y, w, h, fill=1, stroke=0)
def txt(c, text, x, y, size=7, font=FONT_REG, color=GRAY1, align="left"):
c.setFont(font, size)
c.setFillColor(color)
if align == "center":
c.drawCentredString(x, y, text)
elif align == "right":
c.drawRightString(x, y, text)
else:
c.drawString(x, y, text)
def section_hdr(c, label, x, y, w, bg_col=NAVY, text_col=WHITE, size=7, h=4.5*mm, radius=1):
filled_rect(c, x, y, w, h, bg_col, radius=radius)
c.setFont(FONT_BOLD, size)
c.setFillColor(text_col)
c.drawCentredString(x + w/2, y + (h - size)/2 + 0.8, label)
return y + h # returns top edge
def badge(c, label, x, y, w, h, bg_col, text_col=WHITE, size=6, radius=2):
filled_rect(c, x, y, w, h, bg_col, radius=radius)
c.setFont(FONT_BOLD, size)
c.setFillColor(text_col)
c.drawCentredString(x + w/2, y + (h - size)/2 + 0.8, label)
def bullet_block(c, items, x, y, w, size=6, color=GRAY1, leading=5.8, indent=3.5*mm):
"""Draw a bulleted list. Returns final y (decreasing)."""
c.setFont(FONT_REG, size)
c.setFillColor(color)
for item in items:
# bold if starts with **
if item.startswith("**") and item.endswith("**"):
c.setFont(FONT_BOLD, size)
c.setFillColor(DBLUE)
c.drawString(x + indent, y, item[2:-2])
c.setFont(FONT_REG, size)
c.setFillColor(color)
elif ":" in item and item.index(":") < 22:
# key: value — bold the key
colon_pos = item.index(":")
key = item[:colon_pos + 1]
rest = item[colon_pos + 1:]
c.setFont(FONT_BOLD, size)
c.setFillColor(DBLUE)
c.drawString(x + indent, y, "• " + key)
kw = c.stringWidth("• " + key, FONT_BOLD, size)
c.setFont(FONT_REG, size)
c.setFillColor(color)
c.drawString(x + indent + kw, y, rest)
else:
c.drawString(x + indent, y, "• " + item)
y -= leading
return y
def hline(c, x, y, w, col=HexColor("#CCCCCC"), lw=0.4):
c.setStrokeColor(col)
c.setLineWidth(lw)
c.line(x, y, x + w, y)
def vline(c, x, y, h, col=HexColor("#CCCCCC"), lw=0.4):
c.setStrokeColor(col)
c.setLineWidth(lw)
c.line(x, y, x, y + h)
def wrap_text(c, text, x, y, maxw, size=6, font=FONT_REG, color=GRAY1, leading=5.8):
"""Word-wrap. Returns final y."""
words = text.split()
line = ""
c.setFont(font, size)
c.setFillColor(color)
for word in words:
test = (line + " " + word).strip()
if c.stringWidth(test, font, size) <= maxw:
line = test
else:
c.drawString(x, y, line)
y -= leading
line = word
if line:
c.drawString(x, y, line)
y -= leading
return y
# ══════════════════════════════════════════════════════════════
# PAGE 1 — FRONT
# ══════════════════════════════════════════════════════════════
def page1(c):
bg(c)
# ── HEADER ──
HDR_H = 16 * mm
filled_rect(c, 0, PH - HDR_H, PW, HDR_H, NAVY)
txt(c, "PERIODONTAL DISEASE", M, PH - HDR_H + 6.8*mm,
size=15, font=FONT_BOLD, color=WHITE)
txt(c, "Quick-Reference Pocket Card | Anatomy · Microbiology · Classification · Pathogenesis",
M, PH - HDR_H + 2.5*mm, size=7.5, font=FONT_REG, color=HexColor("#B8CCE8"))
# Right badge
filled_rect(c, PW - 44*mm, PH - HDR_H + 3*mm, 37*mm, 11*mm,
HexColor("#1A3F7A"), radius=2)
txt(c, "ORRIS MEDICAL LIBRARY", PW - 25.5*mm, PH - HDR_H + 8*mm,
size=6.5, font=FONT_BOLD, color=WHITE, align="center")
txt(c, "Emergency Reference Card", PW - 25.5*mm, PH - HDR_H + 4*mm,
size=6, font=FONT_IT, color=HexColor("#B8CCE8"), align="center")
y0 = PH - HDR_H - 2.5*mm
ybot = 7.5*mm
cw = (PW - 2*M - 4*mm) / 3
c1x = M
c2x = M + cw + 2*mm
c3x = M + 2*cw + 4*mm
vline(c, c2x - 1*mm, ybot, y0 - ybot)
vline(c, c3x - 1*mm, ybot, y0 - ybot)
# ════════════════════════════════
# COLUMN 1 — ANATOMY + PATHOGENS
# ════════════════════════════════
x1 = c1x
y = y0
# Anatomy section
section_hdr(c, "PERIODONTIUM ANATOMY", x1, y - 5*mm, cw, NAVY, h=4.5*mm)
y -= 6.5*mm
# Mini drawn tooth
dh = 28*mm
tx = x1 + 4*mm
tw = cw - 22*mm
# Bone
filled_rect(c, tx - 2*mm, y - dh, tw + 4*mm, dh * 0.55,
HexColor("#E8DDD0"))
# Root
filled_rect(c, tx + tw*0.15, y - dh + 1*mm, tw*0.7, dh*0.55 - 1*mm,
HexColor("#F5ECD5"))
# PDL lines
c.setStrokeColor(HexColor("#4CAF50")); c.setLineWidth(0.8)
c.line(tx + tw*0.13, y - dh + 1*mm, tx + tw*0.13, y - dh*0.45)
c.line(tx + tw*0.87, y - dh + 1*mm, tx + tw*0.87, y - dh*0.45)
# Cementum
filled_rect(c, tx + tw*0.12, y - dh + 1*mm, tw*0.04, dh*0.54 - 1*mm,
HexColor("#C8A060"))
filled_rect(c, tx + tw*0.84, y - dh + 1*mm, tw*0.04, dh*0.54 - 1*mm,
HexColor("#C8A060"))
# Crown dentin
filled_rect(c, tx + tw*0.1, y - dh*0.45 - dh*0.38, tw*0.8, dh*0.39,
HexColor("#F5ECD5"))
# Enamel
filled_rect(c, tx + tw*0.1, y - dh*0.45 - dh*0.38, tw*0.8, dh*0.38,
HexColor("#F0EDE5"), stroke=HexColor("#AAAAAA"), lw=0.3)
# Pulp
filled_rect(c, tx + tw*0.33, y - dh*0.45 - dh*0.3, tw*0.34, dh*0.72,
HexColor("#C0392B"), radius=1)
# Gingiva blocks
filled_rect(c, tx - 2*mm, y - dh*0.45 - dh*0.18,
tw*0.13 + 2*mm, dh*0.18, HexColor("#FFAAAA"))
filled_rect(c, tx + tw*0.87, y - dh*0.45 - dh*0.18,
tw*0.13 + 2*mm, dh*0.18, HexColor("#FFAAAA"))
# Gingival margin line
c.setStrokeColor(HexColor("#EF9A9A")); c.setLineWidth(0.5)
c.line(tx - 2*mm, y - dh*0.45, tx + tw + 2*mm, y - dh*0.45)
# Labels right side
lx_lbl = x1 + cw - 1*mm
annotations = [
(y - dh*0.45 - dh*0.18*0.5, "Gingiva", HexColor("#AA4444")),
(y - dh*0.45, "CEJ", DBLUE),
(y - dh*0.45 - dh*0.18 - dh*0.1, "Cementum", HexColor("#8B6914")),
(y - dh*0.45 - dh*0.18 - dh*0.25, "PDL", GREEN),
(y - dh*0.7, "Pulp", RED),
(y - dh*0.9, "Alv. Bone", HexColor("#8B7355")),
]
for (ay2, lbl, lcol) in annotations:
c.setStrokeColor(HexColor("#AAAAAA")); c.setLineWidth(0.2)
c.line(tx + tw*0.87, ay2, lx_lbl - 3*mm, ay2)
txt(c, lbl, lx_lbl - 2.5*mm, ay2 - 1.8, size=5.5, color=lcol)
y -= dh + 1*mm
txt(c, "CEJ=cemento-enamel junction PDL=periodontal ligament",
x1, y, size=5, font=FONT_IT, color=GRAY3)
y -= 4*mm
# Sulcus rule box
filled_rect(c, x1, y - 7*mm, cw, 7*mm, LBLUE, stroke=NAVY, lw=0.5, radius=2)
txt(c, "Normal sulcus depth: 2–3 mm", x1 + 1.5*mm, y - 3*mm,
size=6.5, font=FONT_BOLD, color=NAVY)
txt(c, "Depth > 3 mm on probing = PATHOLOGICAL POCKET",
x1 + 1.5*mm, y - 6.5*mm, size=6, color=GRAY1)
y -= 9*mm
# PATHOGENS
section_hdr(c, "KEY PERIODONTAL PATHOGENS", x1, y - 5*mm, cw, TEAL, h=4.5*mm)
y -= 6.5*mm
pathogens = [
("RED COMPLEX", RED, LRED, ["Porphyromonas gingivalis (Pg) — MAJOR",
"Tannerella forsythia (Tf)",
"Treponema denticola (Td)"]),
("AGGRESSIVE", PURPLE, LPURPLE,["Aggregatibacter actinomycetemcomitans (Aa)",
"Causes LAP in young patients"]),
("ORANGE COMPLEX",AMBER, LAMBER, ["Prevotella intermedia",
"Fusobacterium nucleatum (bridges)"]),
("ANUG TRIAD", TEAL, LTEAL, ["Treponema + Fusobacterium + Selenomonas",
"Onset within 24 hours"]]),
]
for (label, tc, bgc, bullets) in pathogens:
bh = (len(bullets) + 0.8) * 5.5*mm
filled_rect(c, x1, y - bh, cw, bh, bgc,
stroke=HexColor("#CCCCCC"), lw=0.2, radius=2)
badge(c, label, x1 + 0.5*mm, y - 4.5*mm, cw - 1*mm, 3.8*mm, tc, size=5.8)
by = y - 7.5*mm
for bl in bullets:
txt(c, "• " + bl, x1 + 1.5*mm, by, size=5.8, color=GRAY1)
by -= 5.5
y -= bh + 1*mm
# Red complex mnemonic
filled_rect(c, x1, y - 6*mm, cw, 6*mm, NAVY, radius=2)
txt(c, 'Mnemonic: "P G T"', x1 + 1.5*mm, y - 3*mm,
size=6, font=FONT_BOLD, color=HexColor("#FFD700"))
txt(c, "Pg + Tf + Td = RED COMPLEX",
x1 + 1.5*mm, y - 6*mm + 1*mm, size=6, color=WHITE)
# ════════════════════════════════
# COLUMN 2 — CLASSIFICATION
# ════════════════════════════════
x2 = c2x
y2 = y0
section_hdr(c, "CLASSIFICATION OF PERIODONTAL DISEASES", x2, y2 - 5*mm,
cw - 1*mm, DBLUE, h=4.5*mm)
y2 -= 6*mm
diseases = [
("Chronic Gingivitis",
"Redness, BOP, swelling. Plaque-induced.",
"NO bone loss", "YES",
LGREEN, GREEN),
("Chronic Periodontitis",
"Pockets, bone loss, attachment loss. PAINLESS.",
"YES", "NO",
LGRAY2, DBLUE),
("Aggressive Periodontitis (LAP)",
"Age <30 yrs. 1st molars + incisors. Aa pathogen.",
"YES — rapid", "NO",
LPURPLE, PURPLE),
("Aggressive Periodontitis (GAP)",
"Young patients. Generalised (≥3 sites).",
"YES", "NO",
LPURPLE, PURPLE),
("ANUG",
"TRIAD: Pain + Punched-out papillae + Bleeding.",
"No (unless NUP)", "Partial",
LRED, RED),
("NUP",
"ANUG + bone exposure. HIV/immunosuppressed.",
"YES", "NO",
LRED, RED),
("Pregnancy Gingivitis",
"Exaggerated plaque response. Resolves post-partum.",
"No", "YES",
LGREEN, GREEN),
("Drug-induced Gingival Overgrowth",
"PCN: Phenytoin, Ciclosporin, Nifedipine. Painless fibrosis.",
"No", "Partial",
LAMBER, AMBER),
("Systemic Disease Periodontitis",
"Papillon-Lefèvre, Chediak-Higashi, Down syndrome, DM.",
"YES", "Varies",
LORANGE, ORANGE),
]
# Header row
hrow_y = y2 - 4.2*mm
filled_rect(c, x2, hrow_y, cw - 1*mm, 4.2*mm, HexColor("#334455"))
hcols = [(0, "Disease"), (35*mm, "Bone"), (46*mm, "Rev?")]
for hoff, hlbl in hcols:
txt(c, hlbl, x2 + hoff + 1*mm, hrow_y + 1.2*mm,
size=5.8, font=FONT_BOLD, color=WHITE)
y2 -= 4.5*mm
row_h = 10.2*mm
for idx, (name, desc, bone, rev, bgc, tc) in enumerate(diseases):
ry = y2 - (idx+1)*row_h - idx*0.3*mm
filled_rect(c, x2, ry, cw - 1*mm, row_h, bgc,
stroke=HexColor("#DDDDDD"), lw=0.2)
# Name
txt(c, name, x2 + 1*mm, ry + row_h - 3.8*mm,
size=6, font=FONT_BOLD, color=tc)
# Desc — truncate to fit
desc_s = desc if len(desc) <= 52 else desc[:50] + "…"
txt(c, desc_s, x2 + 1*mm, ry + 4.2*mm,
size=5.5, color=GRAY2)
# Bone loss
bone_col = RED if "YES" in bone else GREEN
txt(c, bone, x2 + 1*mm, ry + 1.2*mm, size=5.5, font=FONT_BOLD, color=bone_col)
# Rev badge
rev_col = GREEN if rev == "YES" else (RED if rev == "NO" else AMBER)
badge(c, rev, x2 + 46*mm, ry + 2*mm, 10*mm, 5*mm, rev_col, size=5.5)
# ════════════════════════════════
# COLUMN 3 — PATHOGENESIS + RISK FACTORS
# ════════════════════════════════
x3 = c3x
y3 = y0
section_hdr(c, "DISEASE PROGRESSION", x3, y3 - 5*mm, cw, RUST, h=4.5*mm)
y3 -= 6*mm
# Progression flow boxes
prog = [
("HEALTHY", "Sulcus\n2–3 mm", HexColor("#D4EDDA"), GREEN),
("GINGIVITIS", "BOP only\nBone safe",HexColor("#FFF3CD"), AMBER),
("EARLY PERIO", "Pocket\n4–5 mm", HexColor("#FFE0B2"), ORANGE),
("MODERATE PERIO", "Pocket\n5–7 mm", HexColor("#FFCCBC"), RUST),
("SEVERE PERIO", "Pocket\n>7 mm", HexColor("#FFCDD2"), RED),
]
bw = (cw - 4*0.8*mm) / 5
for pi, (stage, sub, bgc, tc) in enumerate(prog):
bx2 = x3 + pi*(bw + 0.8*mm)
filled_rect(c, bx2, y3 - 14*mm, bw, 14*mm, bgc,
stroke=tc, lw=0.4, radius=1)
stage_lines = stage.split()
for li, sl in enumerate(stage_lines):
txt(c, sl, bx2 + bw/2, y3 - 3.5*mm - li*5.8,
size=5.5, font=FONT_BOLD, color=tc, align="center")
for li2, sl2 in enumerate(sub.split("\n")):
txt(c, sl2, bx2 + bw/2, y3 - 10*mm + li2*(-5),
size=5, color=GRAY2, align="center")
if pi < 4:
txt(c, "→", bx2 + bw + 0.1*mm, y3 - 7.5*mm,
size=6.5, color=GRAY3, align="center")
y3 -= 16*mm
# KEY RULE
filled_rect(c, x3, y3 - 6.5*mm, cw, 6.5*mm, NAVY, radius=2)
txt(c, "KEY RULE:", x3 + 1.5*mm, y3 - 3*mm,
size=6.5, font=FONT_BOLD, color=HexColor("#FFD700"))
txt(c, "Gingivitis = REVERSIBLE | Periodontitis = IRREVERSIBLE",
x3 + 1.5*mm, y3 - 6*mm + 1*mm, size=6, color=WHITE)
y3 -= 8.5*mm
# MECHANISM
section_hdr(c, "MECHANISM OF BONE DESTRUCTION", x3, y3 - 5*mm,
cw, TEAL, h=4.5*mm)
y3 -= 6*mm
mech_steps = [
("1", "Plaque bacteria release LPS & toxins"),
("2", "PMNs + macrophages → IL-1β, TNF-α, MMPs"),
("3", "RANKL upregulation → osteoclast activation"),
("4", "Alveolar bone resorption (PERMANENT)"),
("5", "Pocket deepens → more anaerobes → vicious cycle"),
]
for si, (num, step) in enumerate(mech_steps):
sy = y3 - si*6.5*mm - 3*mm
badge(c, num, x3, sy, 4.5*mm, 5*mm, TEAL, size=6)
txt(c, step, x3 + 5.5*mm, sy + 1*mm, size=6, color=GRAY1)
y3 -= len(mech_steps)*6.5*mm + 5*mm
# RISK FACTORS
section_hdr(c, "RISK FACTORS", x3, y3 - 5*mm, cw, AMBER, h=4.5*mm)
y3 -= 6*mm
risks_mod = [("Dental plaque/calculus","Primary cause"),
("Smoking/tobacco","Doubles risk; masks BOP"),
("Diabetes mellitus","Bidirectional relationship"),
("Hormonal changes","Puberty, pregnancy, menopause"),
("Vitamin C deficiency","Impaired collagen synthesis"),
("Xerostomia","Reduced antimicrobial saliva")]
risks_nonmod = [("HIV/AIDS","Susceptibility to NUP/ANUG"),
("Genetic (IL-1 polymorphism)","Family history"),
("Systemic diseases","Leuk., neutrophil disorders")]
half = (cw - 1*mm)/2
# Modifiable
filled_rect(c, x3, y3 - 4*mm, half, 3.8*mm, GREEN, radius=1)
txt(c, "MODIFIABLE", x3 + half/2, y3 - 2.3*mm,
size=6, font=FONT_BOLD, color=WHITE, align="center")
ry3 = y3 - 5.5*mm
for (rf, note) in risks_mod:
filled_rect(c, x3, ry3 - 5*mm, half, 5*mm,
LGREEN if risks_mod.index((rf,note))%2==0 else WHITE,
stroke=HexColor("#BBBBBB"), lw=0.2)
txt(c, rf, x3 + 1*mm, ry3 - 2.5*mm, size=5.5, font=FONT_BOLD, color=GREEN)
txt(c, note, x3 + 1*mm, ry3 - 5*mm + 0.5*mm, size=5, color=GRAY2)
ry3 -= 5.3*mm
# Non-modifiable
filled_rect(c, x3 + half + 1*mm, y3 - 4*mm, half, 3.8*mm, DRED, radius=1)
txt(c, "NON-MODIFIABLE", x3 + half + 1*mm + half/2, y3 - 2.3*mm,
size=6, font=FONT_BOLD, color=WHITE, align="center")
ry4 = y3 - 5.5*mm
for (rf, note) in risks_nonmod:
filled_rect(c, x3 + half + 1*mm, ry4 - 5*mm, half, 5*mm,
LRED if risks_nonmod.index((rf,note))%2==0 else WHITE,
stroke=HexColor("#BBBBBB"), lw=0.2)
txt(c, rf, x3 + half + 2*mm, ry4 - 2.5*mm, size=5.5, font=FONT_BOLD, color=DRED)
txt(c, note, x3 + half + 2*mm, ry4 - 5*mm + 0.5*mm, size=5, color=GRAY2)
ry4 -= 5.3*mm
# ── FOOTER ──
filled_rect(c, 0, 0, PW, 7*mm, LGRAY)
txt(c, "Sources: Harrison's Principles of Internal Medicine 22e · Robbins & Cotran · Junqueira's Basic Histology · Sherris Medical Microbiology",
M, 2.5*mm, size=5.5, font=FONT_IT, color=GRAY3)
txt(c, "PAGE 1 / 2", PW - M, 2.5*mm, size=5.5, font=FONT_BOLD,
color=NAVY, align="right")
# ══════════════════════════════════════════════════════════════
# PAGE 2 — BACK
# ══════════════════════════════════════════════════════════════
def page2(c):
bg(c)
HDR_H = 13 * mm
filled_rect(c, 0, PH - HDR_H, PW, HDR_H, DBLUE)
txt(c, "PERIODONTAL DISEASE — Diagnosis · Treatment · Prevention · Systemic Links",
M, PH - HDR_H + 5*mm, size=9, font=FONT_BOLD, color=WHITE)
txt(c, "Quick Reference | Orris Medical Library",
PW - M, PH - HDR_H + 5*mm, size=7, font=FONT_IT,
color=HexColor("#B8CCE8"), align="right")
y0 = PH - HDR_H - 2*mm
ybot = 7.5*mm
cw = (PW - 2*M - 4*mm) / 3
c1x = M
c2x = M + cw + 2*mm
c3x = M + 2*cw + 4*mm
vline(c, c2x - 1*mm, ybot, y0 - ybot)
vline(c, c3x - 1*mm, ybot, y0 - ybot)
# ════════════════════════════════
# COLUMN 1 — DIAGNOSIS + ANUG
# ════════════════════════════════
x1 = c1x
y1 = y0
section_hdr(c, "DIAGNOSIS & INVESTIGATIONS", x1, y1 - 5*mm,
cw, NAVY, h=4.5*mm)
y1 -= 6*mm
diag = [
("Periodontal probe", "Pocket depth (mm)", "> 3 mm = pocket", DBLUE),
("BOP", "Bleeding on probing", "Present = inflamed", DRED),
("Recession", "Distance from CEJ to margin", "Any = attachment loss", AMBER),
("Mobility", "Grade 0–3 tooth loosening", "Grade 1+ = bone loss", ORANGE),
("Furcation", "Root fork bone loss", "Class I/II/III", TEAL),
("Periapical X-ray", "Individual tooth bone level", "Crestal bone loss", DBLUE),
("OPG", "Full-mouth bone overview", "Generalised loss", DBLUE),
("CBCT", "3-D bone architecture", "Surgical planning", DBLUE),
("Blood tests", "DM, blood dyscrasias", "HbA1c, FBC, ESR", GREEN),
]
# Header
filled_rect(c, x1, y1 - 4*mm, cw, 4*mm, HexColor("#334455"))
for hx2, hl in [(0, "Test"), (22*mm, "Purpose"), (44*mm, "Abnormal")]:
txt(c, hl, x1 + hx2 + 1*mm, y1 - 2.5*mm,
size=5.5, font=FONT_BOLD, color=WHITE)
y1 -= 4.5*mm
for di, (test, purp, abnl, tc) in enumerate(diag):
rh = 5.8*mm
ry = y1 - (di+1)*rh - di*0.2*mm
filled_rect(c, x1, ry, cw, rh,
LGRAY if di%2==0 else WHITE,
stroke=HexColor("#DDDDDD"), lw=0.2)
txt(c, test, x1+1*mm, ry+2*mm, size=5.8, font=FONT_BOLD, color=tc)
txt(c, purp, x1+22*mm, ry+2*mm, size=5.5, color=GRAY2)
txt(c, abnl, x1+44*mm, ry+2*mm, size=5.5, color=GRAY1)
y1 -= 9*5.8*mm + 3*mm
# ANUG
section_hdr(c, "ANUG — SPECIFIC CONDITION", x1, y1 - 5*mm,
cw, RED, h=4.5*mm)
y1 -= 6*mm
filled_rect(c, x1, y1 - 22*mm, cw, 22*mm, LRED,
stroke=HexColor("#CCAAAA"), lw=0.4, radius=2)
txt(c, "DIAGNOSTIC TRIAD:", x1+1.5*mm, y1 - 3*mm,
size=6.5, font=FONT_BOLD, color=RED)
triad = ["PAIN (only painful periodontal condition)",
"PUNCHED-OUT interdental papillae",
"BLEEDING + grey pseudomembrane"]
for ti, titem in enumerate(triad):
txt(c, "• " + titem, x1+1.5*mm, y1 - 6.5*mm - ti*5.5,
size=6, font=FONT_BOLD, color=GRAY1)
txt(c, "Treatment:", x1+1.5*mm, y1 - 19.5*mm,
size=6, font=FONT_BOLD, color=TEAL)
txt(c, "CHX 0.12% rinse + Debridement + Metronidazole (if systemic signs)",
x1+1.5*mm, y1 - 22*mm + 1.5*mm, size=5.8, color=GRAY1)
y1 -= 24*mm
# Drug-induced gingival overgrowth
filled_rect(c, x1, y1 - 18*mm, cw, 18*mm, LAMBER,
stroke=HexColor("#CCBB88"), lw=0.4, radius=2)
txt(c, "DRUG-INDUCED GINGIVAL OVERGROWTH", x1+1.5*mm, y1 - 3*mm,
size=6, font=FONT_BOLD, color=AMBER)
filled_rect(c, x1 + 1*mm, y1 - 7*mm, cw - 2*mm, 3.5*mm, AMBER, radius=1)
txt(c, '"Please Check Now" (PCN)', x1 + cw/2, y1 - 5*mm,
size=6.5, font=FONT_BOLD, color=WHITE, align="center")
pcn = [("P", "Phenytoin", "Anti-epileptic"),
("C", "Ciclosporin", "Immunosuppressant"),
("N", "Nifedipine", "Ca-channel blocker")]
for pi, (letter, drug, cls) in enumerate(pcn):
py2 = y1 - 9.5*mm - pi*2.8*mm
badge(c, letter, x1+1.5*mm, py2-1.5*mm, 4*mm, 4*mm, AMBER, size=7)
txt(c, drug, x1+6.5*mm, py2, size=6, font=FONT_BOLD, color=GRAY1)
txt(c, cls, x1+24*mm, py2, size=5.8, font=FONT_IT, color=GRAY3)
# ════════════════════════════════
# COLUMN 2 — TREATMENT
# ════════════════════════════════
x2 = c2x
y2 = y0
section_hdr(c, "TREATMENT — 4-PHASE APPROACH", x2, y2 - 5*mm,
cw - 1*mm, NAVY, h=4.5*mm)
y2 -= 6*mm
phases = [
("PHASE 1", "Systemic",
["Treat underlying systemic disease (DM, blood disorders)",
"Adjust medications causing gingival overgrowth",
"Smoking cessation + Vitamin C supplementation"],
HexColor("#EEF4FB"), DBLUE),
("PHASE 2", "Causal / Hygiene",
["Oral hygiene instruction (Bass technique)",
"Supragingival scaling (remove calculus)",
"Root planing — SRP (subgingival debridement)",
"Reassess at 6–8 weeks"],
LBLUE, DBLUE),
("PHASE 3", "Surgical",
["Flap surgery — direct root surface access",
"Guided Tissue Regeneration (GTR)",
"Bone grafts — intrabony defects",
"Gingivectomy — drug-induced overgrowth"],
LGREEN, GREEN),
("PHASE 4", "Maintenance",
["Supportive periodontal therapy q 3–6 months",
"Repeat clinical indices at each visit",
"Lifelong — no cure, only control"],
LAMBER, AMBER),
]
for phi, (ph_num, ph_name, items, bgc, tc) in enumerate(phases):
ph_h = (len(items)+1) * 5.8*mm + 3*mm
filled_rect(c, x2, y2 - ph_h, cw - 1*mm, ph_h, bgc,
stroke=HexColor("#CCCCCC"), lw=0.3, radius=2)
# Header bar
filled_rect(c, x2, y2 - 5*mm, cw - 1*mm, 5*mm, tc, radius=1)
txt(c, ph_num + " — " + ph_name, x2 + 2*mm, y2 - 3.2*mm,
size=6.5, font=FONT_BOLD, color=WHITE)
by = y2 - 8.5*mm
for item in items:
txt(c, "• " + item, x2 + 2*mm, by, size=6, color=GRAY1)
by -= 5.8
y2 -= ph_h + 1.5*mm
# ABSCESS TREATMENT
section_hdr(c, "PERIODONTAL ABSCESS", x2, y2 - 5*mm,
cw - 1*mm, DRED, h=4.5*mm)
y2 -= 6*mm
absc = ["Pain, swelling, pus — rapid onset",
"Vitality test: tooth usually VITAL",
"Tx: I&D + SRP + Amoxicillin/Metronidazole",
"Follow-up definitive periodontal treatment"]
for ai, ab in enumerate(absc):
txt(c, "• " + ab, x2 + 2*mm, y2 - ai*5.8 - 3*mm, size=6, color=GRAY1)
# ════════════════════════════════
# COLUMN 3 — SYSTEMIC LINKS + PREVENTION + MNEMONICS
# ════════════════════════════════
x3 = c3x
y3 = y0
section_hdr(c, "SYSTEMIC LINKS OF PERIODONTAL DISEASE", x3, y3 - 5*mm,
cw, PURPLE, h=4.5*mm)
y3 -= 6*mm
txt(c, "Bacteraemia → systemic inflammation (LPS, IL-1β, TNF-α)",
x3 + 1*mm, y3 - 3*mm, size=6, font=FONT_IT, color=GRAY2)
y3 -= 5*mm
systemic = [
("Cardiovascular Disease", "Bidirectional", DBLUE, LBLUE),
("Diabetes Mellitus", "Bidirectional", GREEN, LGREEN),
("Adverse Pregnancy", "Preterm, low BW",AMBER, LAMBER),
("Infective Endocarditis", "Bacteraemia risk",RED, LRED),
("Aspiration Pneumonia", "Oral bacteria", TEAL, LTEAL),
("Rheumatoid Arthritis", "Shared autoimmune",PURPLE,LPURPLE),
("Alzheimer's / Brain Abs.","Emerging evidence",ORANGE,LORANGE),
]
sys_h = 5.5*mm
for si, (cond, note, tc, bgc) in enumerate(systemic):
ry = y3 - (si+1)*sys_h - si*0.3*mm
filled_rect(c, x3, ry, cw, sys_h, bgc,
stroke=HexColor("#CCCCCC"), lw=0.2)
badge(c, "↔" if "Bi" in note else "→",
x3 + 0.5*mm, ry+0.8*mm, 5*mm, 4*mm, tc, size=7)
txt(c, cond, x3 + 6.5*mm, ry + 3.2*mm,
size=6, font=FONT_BOLD, color=tc)
txt(c, note, x3 + 6.5*mm, ry + 0.5*mm, size=5.5, color=GRAY3)
y3 -= 7*sys_h + 4*mm
# CRITICAL RULE
filled_rect(c, x3, y3 - 7*mm, cw, 7*mm, NAVY, radius=2)
txt(c, "CRITICAL RULE:", x3 + 1.5*mm, y3 - 3*mm,
size=6.5, font=FONT_BOLD, color=HexColor("#FFD700"))
txt(c, "Disease is PAINLESS until abscess or late stage.",
x3 + 1.5*mm, y3 - 6*mm + 0.8*mm, size=6, color=WHITE)
y3 -= 9*mm
# PREVENTION
section_hdr(c, "PREVENTION", x3, y3 - 5*mm, cw, GREEN, h=4.5*mm)
y3 -= 6*mm
prev_items = [
("Brushing", "Twice daily, 2 min, modified Bass"),
("Flossing", "Once daily — interdental cleaning"),
("Professional", "Scaling every 6 months (3-monthly high risk)"),
("Electric brush", "Superior plaque removal"),
("Quit smoking", "Biggest modifiable risk factor"),
("DM control", "HbA1c < 7% — bidirectional benefit"),
("CHX rinse", "0.12% short-term only (max 2 wk)"),
]
prev_h = 5.5*mm
for pi2, (item, note) in enumerate(prev_items):
pry = y3 - (pi2+1)*prev_h - pi2*0.2*mm
filled_rect(c, x3, pry, cw, prev_h,
LGREEN if pi2%2==0 else WHITE,
stroke=HexColor("#BBBBBB"), lw=0.2)
txt(c, item, x3 + 1*mm, pry + 2.2*mm,
size=5.8, font=FONT_BOLD, color=GREEN)
txt(c, note, x3 + 20*mm, pry + 2.2*mm, size=5.5, color=GRAY1)
y3 -= 7*prev_h + 4*mm
# MNEMONICS
section_hdr(c, "KEY MNEMONICS", x3, y3 - 5*mm, cw, AMBER, h=4.5*mm)
y3 -= 6*mm
mnems = [
("RED COMPLEX", '"PGT"', "Pg + Tf + Td", RED, LRED),
("PCN DRUGS", '"Please Check Now"',"Phenytoin·Ciclosporin·Nifedipine", AMBER, LAMBER),
("ANUG TRIAD", "Pain + Papillae + Bleeding", "", TEAL, LTEAL),
("GINGIVITIS", "REVERSIBLE", "Bone intact", GREEN, LGREEN),
("PERIODONTITIS","IRREVERSIBLE", "Bone gone forever", RED, LRED),
]
for mi, (label, mnem, detail, tc, bgc) in enumerate(mnems):
mry = y3 - (mi+1)*8*mm - mi*0.3*mm
filled_rect(c, x3, mry, cw, 8*mm, bgc,
stroke=HexColor("#CCCCCC"), lw=0.2, radius=2)
badge(c, label, x3 + 0.5*mm, mry + 3.5*mm, 19*mm, 4*mm, tc, size=5)
txt(c, mnem, x3 + 20.5*mm, mry + 6*mm,
size=6, font=FONT_BOLD, color=tc)
txt(c, detail, x3 + 20.5*mm, mry + 1.5*mm, size=5.5, color=GRAY2)
# FOOTER
filled_rect(c, 0, 0, PW, 7*mm, LGRAY)
txt(c, "For educational use only. Clinical decisions require qualified clinical assessment. Orris Medical Library — Periodontal Disease Pocket Card",
M, 2.5*mm, size=5.5, font=FONT_IT, color=GRAY3)
txt(c, "PAGE 2 / 2", PW - M, 2.5*mm, size=5.5, font=FONT_BOLD,
color=NAVY, align="right")
# ══════════════════════════════════════════════════════════════
# BUILD
# ══════════════════════════════════════════════════════════════
c = new_canvas()
page1(c)
c.showPage()
page2(c)
c.showPage()
c.save()
print(f"SAVED: {OUT}")
cd /tmp/workspace/perio-card && python3 build_perio_card.py 2>&1
~/perio-card/build_perio_card.py
Successfully replaced 1 instance(s) of the string in '/tmp/workspace/perio-card/build_perio_card.py'
cd /tmp/workspace/perio-card && python3 build_perio_card.py 2>&1
Periodontal Disease Pocket Card
PDF Document · PDF
| Column | Content |
|---|---|
| Left | Drawn tooth cross-section diagram (enamel, dentin, pulp, cementum, PDL, alveolar bone, gingiva — all labelled with colour coding) + sulcus depth rule box + Key Periodontal Pathogens (Red Complex, Aggressive, Orange Complex, ANUG Triad — each as a colour-coded card) + "PGT" mnemonic banner |
| Centre | Full Classification Table — 9 diseases with bone loss status and reversibility badge (green = YES, red = NO) for each: Chronic Gingivitis, Chronic Periodontitis, LAP, GAP, ANUG, NUP, Pregnancy Gingivitis, Drug-induced Overgrowth, Systemic Disease Periodontitis |
| Right | 5-stage disease progression strip (Healthy → Gingivitis → Early → Moderate → Severe Periodontitis) + KEY RULE banner (Reversible vs. Irreversible) + 5-step mechanism of bone destruction + Risk Factors split into Modifiable (6 items) and Non-Modifiable (3 items) panels |
| Column | Content |
|---|---|
| Left | 9-row Diagnosis & Investigations table (probe, BOP, recession, mobility, furcation, X-ray, OPG, CBCT, blood tests) + ANUG diagnostic triad box with treatment + Drug-induced Gingival Overgrowth with "PCN — Please Check Now" mnemonic (Phenytoin / Ciclosporin / Nifedipine) |
| Centre | 4-Phase Treatment approach (Phase 1 Systemic → Phase 2 Causal/SRP → Phase 3 Surgical → Phase 4 Maintenance) each as a colour-coded box + Periodontal Abscess management |
| Right | 7 Systemic Links with bidirectional arrows (CVD, DM, Pregnancy, Endocarditis, Pneumonia, RA, Alzheimer's) + CRITICAL RULE (disease is painless until late) + Prevention table (7 measures with frequency) + 5 Key Mnemonics panel (PGT, PCN, ANUG Triad, Gingivitis = Reversible, Periodontitis = Irreversible) |
Create a quick reference pdf of dental trauma
ls -lh /tmp/workspace/dental-card/ 2>/dev/null && echo "EXISTS" || echo "NOT FOUND"
Dental Trauma Pocket Card
PDF Document · PDF
| Column | Content |
|---|---|
| Left | Drawn tooth cross-section diagram (enamel, dentin, pulp, PDL, alveolar bone, cementum — all labelled) + Ellis Classification cards (Class I green / Class II amber / Class III red — colour at fracture, pain level, urgency, ED treatment) |
| Centre | 5 positional injury cards — Concussion, Subluxation, Extrusive Luxation, Intrusive Luxation, Lateral Luxation — each with mechanism, signs, and management |
| Right | Avulsion section — Primary vs. Permanent split panel + 60-Minute Rule banner + 6-step replantation guide + storage media table ranked best to worst (Hank's BSS → milk → saliva → saline → water → dry) |
| Column | Content |
|---|---|
| Left | Full 13-row management summary table for all fracture and positional injury types with urgency colour coding + Primary vs. Permanent quick rule box |
| Right | Clinical examination approach (History → Look → Feel → X-ray) + Medications after trauma (analgesia, antibiotics, CHX rinse, tetanus) + 5 key mnemonics panel (Colour Rule, 60-Min Rule, Never Replant, No Manipulation, Missing Tooth/CXR) |
Text are overlapping please fix it
~/dental-card/build_card_v2.py
"""
Dental Trauma Quick-Reference Card — FIXED LAYOUT v2
A4 landscape, 2 pages. Every element uses a strict top-down cursor.
No overlapping. Generous spacing.
"""
from reportlab.lib.pagesizes import A4, landscape
from reportlab.lib.units import mm
from reportlab.lib.colors import HexColor, white, black
from reportlab.pdfgen import canvas as rl_canvas
OUT = "/tmp/workspace/dental-card/Dental_Trauma_Pocket_Card_v2.pdf"
# ── Page geometry ──────────────────────────────────────────────
PW, PH = landscape(A4) # 297 × 210 mm → 841.9 × 595.3 pt
M = 8 * mm # outer margin
# ── Palette ────────────────────────────────────────────────────
NAVY = HexColor("#0D2B5E")
DBLUE = HexColor("#1A3F7A")
LBLUE = HexColor("#D6E4F0")
LLBLUE = HexColor("#EEF4FB")
TEAL = HexColor("#0E6B7A")
LTEAL = HexColor("#D0EFF4")
GREEN = HexColor("#1A6B3C")
LGREEN = HexColor("#D4EDDA")
AMBER = HexColor("#7B4D00")
LAMBER = HexColor("#FFF3CD")
RED = HexColor("#8B0000")
DRED = HexColor("#CC2222")
LRED = HexColor("#FFE0E0")
ORANGE = HexColor("#7A3B00")
LORANG = HexColor("#FFE8CC")
G1 = HexColor("#1A1A1A")
G2 = HexColor("#444444")
G3 = HexColor("#888888")
LGRAY = HexColor("#F2F4F6")
LGRAY2 = HexColor("#E4E8EC")
WHITE = white
RB = "Helvetica-Bold"
RR = "Helvetica"
RI = "Helvetica-Oblique"
# ══════════════════════════════════════════════════════════════
# LOW-LEVEL HELPERS (all coordinates in points)
# ══════════════════════════════════════════════════════════════
def frect(cv, x, y, w, h, fill, stroke=None, lw=0.5, r=0):
"""Draw filled rectangle. y = BOTTOM edge."""
cv.setFillColor(fill)
if stroke:
cv.setStrokeColor(stroke)
cv.setLineWidth(lw)
if r:
cv.roundRect(x, y, w, h, r, fill=1, stroke=1)
else:
cv.rect(x, y, w, h, fill=1, stroke=1)
else:
if r:
cv.roundRect(x, y, w, h, r, fill=1, stroke=0)
else:
cv.rect(x, y, w, h, fill=1, stroke=0)
def text(cv, s, x, y, size=8, font=RR, color=G1, align="left"):
cv.setFont(font, size)
cv.setFillColor(color)
if align == "c":
cv.drawCentredString(x, y, s)
elif align == "r":
cv.drawRightString(x, y, s)
else:
cv.drawString(x, y, s)
def hdr_bar(cv, x, y_top, w, h, label, bg=NAVY, fg=WHITE, fsize=8):
"""Draw a header bar. y_top = TOP edge. Returns bottom edge."""
frect(cv, x, y_top - h, w, h, bg, r=2)
text(cv, label, x + w/2, y_top - h + (h - fsize)/2 + 1, size=fsize,
font=RB, color=fg, align="c")
return y_top - h # bottom = new cursor
def badge(cv, label, x, y_top, w, h, bg, fg=WHITE, fsize=6.5):
"""Filled badge. y_top = top edge. Returns bottom edge."""
frect(cv, x, y_top - h, w, h, bg, r=2)
text(cv, label, x + w/2, y_top - h + (h - fsize)/2 + 1,
size=fsize, font=RB, color=fg, align="c")
return y_top - h
def row_table(cv, rows, x, y_top, col_widths,
row_height=7*mm, header_bg=DBLUE, header_fg=WHITE,
alt_bg=LGRAY, hfsize=7, bfsize=6.5):
"""
Draw a table. y_top = TOP edge of table.
rows[0] treated as header.
Returns bottom edge (y_bottom).
"""
n_rows = len(rows)
n_cols = len(col_widths)
total_w = sum(col_widths)
y = y_top
for ri, row in enumerate(rows):
is_hdr = (ri == 0)
bg_col = header_bg if is_hdr else (alt_bg if ri % 2 == 1 else WHITE)
# row background
frect(cv, x, y - row_height, total_w, row_height, bg_col,
stroke=HexColor("#CCCCCC"), lw=0.3)
cx = x
for ci, cell in enumerate(row):
cw = col_widths[ci]
fg = header_fg if is_hdr else G1
fs = hfsize if is_hdr else bfsize
fn = RB if is_hdr else RR
# truncate if too wide
max_chars = int(cw / (fs * 0.52))
cell_s = str(cell)
if len(cell_s) > max_chars and max_chars > 4:
cell_s = cell_s[:max_chars-1] + "…"
text(cv, cell_s, cx + 1.5*mm, y - row_height + row_height*0.3,
size=fs, font=fn, color=fg)
cx += cw
y -= row_height
return y # bottom edge
def info_card(cv, x, y_top, w,
title, title_bg, title_fg,
body_lines, # list of (text, font, size, color)
body_bg=LGRAY, gap=1.5*mm, title_h=6*mm, line_h=5.8*mm):
"""
Draw a labelled card with title bar + body lines.
y_top = top edge. Returns bottom edge.
"""
n = len(body_lines)
body_h = n * line_h + 2 * gap
total_h = title_h + body_h
# background
frect(cv, x, y_top - total_h, w, total_h, body_bg,
stroke=HexColor("#CCCCCC"), lw=0.3, r=2)
# title bar
frect(cv, x, y_top - title_h, w, title_h, title_bg, r=2)
text(cv, title, x + w/2, y_top - title_h + (title_h - 7)/2 + 1,
size=7, font=RB, color=title_fg, align="c")
# body
cy = y_top - title_h - gap - line_h + line_h * 0.25
for (line_txt, fn, fs, fc) in body_lines:
text(cv, line_txt, x + 2*mm, cy, size=fs, font=fn, color=fc)
cy -= line_h
return y_top - total_h
def step_list(cv, steps, x, y_top, w,
num_bg=NAVY, num_fg=WHITE, num_w=5*mm, num_h=5*mm,
fsize=6.5, line_h=6.5*mm, gap=1*mm):
"""
Numbered step list. y_top = top edge. Returns bottom edge.
steps = list of strings
"""
y = y_top
for i, step in enumerate(steps):
# number badge
badge(cv, str(i+1), x, y, num_w, num_h, num_bg, num_fg, fsize=6)
# text — truncate to fit
max_c = int((w - num_w - 2*mm) / (fsize * 0.52))
step_s = step if len(step) <= max_c else step[:max_c-1] + "…"
text(cv, step_s, x + num_w + 2*mm,
y - num_h + (num_h - fsize)/2 + 1,
size=fsize, font=RR, color=G1)
y -= num_h + gap
return y
# ══════════════════════════════════════════════════════════════
# GLOBAL HEADER / FOOTER
# ══════════════════════════════════════════════════════════════
def global_header(cv, subtitle):
h = 17 * mm
frect(cv, 0, PH - h, PW, h, NAVY)
text(cv, "DENTAL TRAUMA", M, PH - h + 9*mm,
size=16, font=RB, color=WHITE)
text(cv, subtitle, M, PH - h + 3.5*mm,
size=8, font=RI, color=HexColor("#B8CCE8"))
# right badge
bw, bh = 52*mm, 11*mm
frect(cv, PW - M - bw, PH - h + 3*mm, bw, bh, DBLUE, r=2)
text(cv, "ORRIS MEDICAL LIBRARY | Emergency Reference",
PW - M - bw/2, PH - h + 3*mm + (bh - 6.5)/2 + 1,
size=6.5, font=RB, color=WHITE, align="c")
return PH - h # returns top of content area
def global_footer(cv, page_label):
h = 7.5 * mm
frect(cv, 0, 0, PW, h, LGRAY2)
text(cv, "Sources: Tintinalli's Emergency Medicine · Rosen's Emergency Medicine · "
"Roberts & Hedges' Clinical Procedures · Harriet Lane Handbook 23e",
M, h*0.38, size=6, font=RI, color=G3)
text(cv, page_label, PW - M, h*0.38,
size=6.5, font=RB, color=NAVY, align="r")
# ══════════════════════════════════════════════════════════════
# PAGE 1 — FRACTURES | POSITIONAL INJURIES | AVULSION
# ══════════════════════════════════════════════════════════════
def page1(cv):
frect(cv, 0, 0, PW, PH, WHITE)
y_content = global_header(cv, "Quick-Reference Card | Fractures · Positional Injuries · Avulsion")
global_footer(cv, "PAGE 1 / 2")
FOOT_H = 7.5 * mm
content_bottom = FOOT_H + 2*mm
GAP = 5 * mm # gap between columns
INNER_GAP = 3 * mm # gap between sections within a column
# Three equal columns
total_w = PW - 2 * M
col_w = (total_w - 2 * GAP) / 3
cx = [M, M + col_w + GAP, M + 2*(col_w + GAP)]
# ─────────────────────────────────────────────
# COLUMN A — Anatomy + Ellis Classification
# ─────────────────────────────────────────────
x = cx[0]
y = y_content - 3*mm # start just below header
# — ANATOMY HEADER —
y = hdr_bar(cv, x, y, col_w, 7*mm, "TOOTH ANATOMY", bg=NAVY)
y -= 2*mm
# Draw a schematic tooth (all positions calculated from current y)
# Available height for diagram: ~42mm
D_H = 40 * mm
D_W = col_w * 0.55
d_x = x + (col_w - D_W) / 2
d_y_top = y # top of diagram area
d_y_bot = y - D_H
crown_h = D_H * 0.40
root_h = D_H * 0.60
tooth_w = D_W * 0.50
t_left = d_x + D_W * 0.25
t_right = t_left + tooth_w
# alveolar bone
frect(cv, d_x, d_y_bot, D_W, root_h * 0.85,
HexColor("#E8DDD0"), stroke=HexColor("#BBBBBB"), lw=0.3)
# root (dentin)
frect(cv, t_left, d_y_bot + 1*mm, tooth_w, root_h - 1*mm,
HexColor("#F5ECD5"), stroke=HexColor("#CCBBAA"), lw=0.3)
# PDL (thin green strips on sides of root)
pdl_t = 1.2
cv.setStrokeColor(HexColor("#4CAF50"))
cv.setLineWidth(pdl_t)
cv.line(t_left, d_y_bot + 1*mm, t_left, d_y_top - crown_h)
cv.line(t_right, d_y_bot + 1*mm, t_right, d_y_top - crown_h)
# cementum (slightly inside PDL)
cv.setStrokeColor(HexColor("#C8A060"))
cv.setLineWidth(0.8)
cv.line(t_left + 0.8*mm, d_y_bot + 1*mm, t_left + 0.8*mm, d_y_top - crown_h)
cv.line(t_right - 0.8*mm, d_y_bot + 1*mm, t_right - 0.8*mm, d_y_top - crown_h)
# crown dentin
frect(cv, t_left, d_y_top - crown_h, tooth_w, crown_h,
HexColor("#F5ECD5"), stroke=HexColor("#CCBBAA"), lw=0.3)
# enamel overlay
frect(cv, t_left, d_y_top - crown_h, tooth_w, crown_h * 0.85,
HexColor("#F0EDE5"), stroke=HexColor("#AAAAAA"), lw=0.4)
# pulp
pulp_w = tooth_w * 0.30
pulp_x = t_left + (tooth_w - pulp_w) / 2
frect(cv, pulp_x, d_y_bot + 0.5*mm, pulp_w, root_h + crown_h * 0.65,
HexColor("#C0392B"), r=1)
# gingiva (pink blocks flanking crown base)
g_w = D_W * 0.22
frect(cv, d_x, d_y_top - crown_h * 0.45, g_w, crown_h * 0.45,
HexColor("#FFAAAA"), stroke=HexColor("#EE9999"), lw=0.3)
frect(cv, d_x + D_W - g_w, d_y_top - crown_h * 0.45, g_w, crown_h * 0.45,
HexColor("#FFAAAA"), stroke=HexColor("#EE9999"), lw=0.3)
# gingival margin line
gm_y = d_y_top - crown_h * 0.45
cv.setStrokeColor(HexColor("#DD7777"))
cv.setLineWidth(0.5)
cv.line(d_x, gm_y, d_x + D_W, gm_y)
# Label column (right of diagram)
lbl_x = d_x + D_W + 1.5*mm
lbl_w = col_w - (D_W + 3*mm)
labels = [
(d_y_top - crown_h * 0.22, "Enamel", HexColor("#555555")),
(d_y_top - crown_h * 0.55, "Dentin", HexColor("#8B6914")),
(d_y_top - crown_h * 0.75, "Pulp", HexColor("#C0392B")),
(d_y_top - crown_h * 1.05, "CEJ", DBLUE),
(d_y_top - crown_h - root_h*0.35, "PDL", GREEN),
(d_y_top - crown_h - root_h*0.60, "Cem.", HexColor("#C8A060")),
(d_y_top - crown_h - root_h*0.80, "Bone", HexColor("#8B7355")),
]
for (ly, lbl, lc) in labels:
cv.setStrokeColor(HexColor("#BBBBBB"))
cv.setLineWidth(0.3)
cv.line(t_right, ly, lbl_x, ly)
text(cv, lbl, lbl_x + 0.5*mm, ly - 2, size=5.8, color=lc)
y = d_y_bot - 2*mm
# Sulcus rule
box_h = 9 * mm
frect(cv, x, y - box_h, col_w, box_h, LBLUE,
stroke=NAVY, lw=0.6, r=2)
text(cv, "Normal sulcus depth: 2 – 3 mm",
x + 2*mm, y - 4.5*mm, size=7, font=RB, color=NAVY)
text(cv, "> 3 mm on probing = PATHOLOGICAL POCKET",
x + 2*mm, y - 8.5*mm, size=6.5, font=RR, color=G1)
y -= box_h + INNER_GAP
# — ELLIS CLASSIFICATION HEADER —
y = hdr_bar(cv, x, y, col_w, 7*mm, "ELLIS FRACTURE CLASSIFICATION", bg=DBLUE)
y -= 2*mm
ellis = [
("Class I",
"ENAMEL ONLY",
"White chip | No pain | Not sensitive",
"Smooth edge · routine dental follow-up",
LGREEN, GREEN),
("Class II",
"ENAMEL + DENTIN",
"Yellow surface | Cold/air sensitive",
"Ca(OH)\u2082 dressing · refer within 24\u201348 h",
LAMBER, AMBER),
("Class III",
"ENAMEL + DENTIN + PULP",
"Pink/red dot | SEVERE pain",
"Cover · SAME-DAY urgent endodontic referral",
LRED, DRED),
]
for (cls, layers, signs, action, bg_c, tc) in ellis:
card_h = 19 * mm
frect(cv, x, y - card_h, col_w, card_h, bg_c,
stroke=HexColor("#BBBBBB"), lw=0.4, r=3)
# class badge
b_h = 5.5 * mm
frect(cv, x + 1*mm, y - 1.5*mm - b_h, 16*mm, b_h, tc, r=2)
text(cv, cls, x + 1*mm + 8*mm, y - 1.5*mm - b_h + (b_h-7)/2 + 1,
size=7, font=RB, color=WHITE, align="c")
# colour dot
dot_colors = {GREEN: HexColor("#E8E8E8"),
AMBER: HexColor("#F5ECD5"),
DRED: HexColor("#FFCCCC")}
frect(cv, x + col_w - 7*mm, y - 1.5*mm - b_h, 5.5*mm, b_h,
dot_colors[tc], stroke=tc, lw=0.5, r=1)
# layers
text(cv, layers, x + 18.5*mm, y - 3*mm, size=6.5, font=RB, color=tc)
# signs
text(cv, signs, x + 2*mm, y - 9*mm, size=6.2, color=G1)
# action
text(cv, "\u2192 " + action, x + 2*mm, y - 13.5*mm,
size=6.2, font=RB, color=tc)
y -= card_h + 2*mm
# colour key
text(cv, "Colour at fracture: White = I Yellow = II Pink/Red = III",
x, y, size=6, font=RI, color=G3)
# ─────────────────────────────────────────────
# COLUMN B — Positional Injuries
# ─────────────────────────────────────────────
x = cx[1]
y = y_content - 3*mm
y = hdr_bar(cv, x, y, col_w, 7*mm, "POSITIONAL INJURIES", bg=TEAL)
y -= 2*mm
injuries = [
("CONCUSSION",
"Shaken — NOT displaced",
[("Position:", "Normal — no movement"),
("Signs:","Tender to percussion only"),
("Mobility:","None"),],
"Soft diet | Routine dental follow-up",
LLBLUE, DBLUE),
("SUBLUXATION",
"Loose — NOT displaced",
[("Position:", "Normal"),
("Signs:", "Mobile + sulcal bleeding"),
("Mobility:", "YES — abnormally mobile"),],
"Flexible splint if needed | Dentist 48 h",
LLBLUE, DBLUE),
("EXTRUSIVE LUXATION",
"Partially pulled out — appears LONG",
[("Position:", "Displaced out of socket axially"),
("Signs:", "Elongated, mobile, PDL torn"),
("Mobility:", "YES"),],
"Reposition + splint | URGENT",
LTEAL, TEAL),
("INTRUSIVE LUXATION",
"Pushed INTO socket — appears SHORT/absent",
[("Position:", "Driven apically into bone"),
("Signs:", "Short or invisible, NOT mobile"),
("WARNING:", "DO NOT manipulate! X-ray first"),],
"Refer within 24 h | No ED manipulation",
LORANG, ORANGE),
("LATERAL LUXATION",
"Displaced SIDEWAYS + alveolar bone Fx",
[("Position:", "Tilted/displaced laterally"),
("Signs:", "Fixed in new position, malocclusion"),
("Bone:", "Alveolar fracture usually present"),],
"Reposition + splint | URGENT referral",
LTEAL, TEAL),
]
for (name, tagline, details, mgmt, bg_c, tc) in injuries:
card_h = 24 * mm
frect(cv, x, y - card_h, col_w, card_h, bg_c,
stroke=HexColor("#BBBBBB"), lw=0.4, r=3)
# name badge
badge(cv, name, x + 1*mm, y - 1*mm, col_w - 2*mm, 5.5*mm, tc)
# tagline
text(cv, tagline, x + 2*mm, y - 9.5*mm, size=6.2, font=RI, color=G2)
# detail rows
dy = y - 13*mm
for (k, v) in details:
text(cv, k, x + 2*mm, dy, size=6, font=RB, color=DBLUE)
kw = cv.stringWidth(k, RB, 6) + 1.5*mm
text(cv, v, x + 2*mm + kw, dy, size=6, color=G1)
dy -= 4.8
# management
frect(cv, x + 1*mm, y - card_h + 1.5*mm, col_w - 2*mm, 5*mm,
tc, r=1)
text(cv, "\u2192 " + mgmt,
x + 2*mm, y - card_h + 1.5*mm + (5*mm - 6.5)/2 + 1,
size=6.5, font=RB, color=WHITE)
y -= card_h + 2.5*mm
# ─────────────────────────────────────────────
# COLUMN C — Avulsion
# ─────────────────────────────────────────────
x = cx[2]
y = y_content - 3*mm
y = hdr_bar(cv, x, y, col_w, 7*mm, "AVULSION (Tooth Completely Out)", bg=DRED)
y -= 3*mm
# Primary vs Permanent
half = (col_w - 2*mm) / 2
# Primary
card_h = 30 * mm
frect(cv, x, y - card_h, half, card_h, LRED,
stroke=HexColor("#CCAAAA"), lw=0.5, r=3)
badge(cv, "PRIMARY TOOTH", x + 0.5*mm, y - 0.5*mm, half - 1*mm, 5.5*mm, DRED)
text(cv, "NEVER REPLANT",
x + half/2, y - 10*mm, size=9, font=RB, color=DRED, align="c")
reasons = ["Causes ankylosis",
"Blocks permanent",
"tooth eruption",
"\u2192 Dental follow-up",
"\u2192 Space maintainer"]
ry2 = y - 15*mm
for r2 in reasons:
text(cv, r2, x + 2*mm, ry2, size=6.5, color=G1)
ry2 -= 4.8
# Permanent
frect(cv, x + half + 2*mm, y - card_h, half, card_h, LGREEN,
stroke=HexColor("#AACCAA"), lw=0.5, r=3)
badge(cv, "PERMANENT TOOTH", x + half + 2.5*mm, y - 0.5*mm,
half - 1*mm, 5.5*mm, GREEN)
text(cv, "DENTAL EMERGENCY",
x + half + 2*mm + half/2, y - 9.5*mm,
size=7.5, font=RB, color=GREEN, align="c")
perm_lines = ["Replant \u2264 60 min",
"Hold by CROWN only",
"Never touch root",
"\u2192 Urgent dentist",
"\u2192 Root canal later"]
ry3 = y - 15*mm
for pl in perm_lines:
text(cv, pl, x + half + 4*mm, ry3, size=6.5, color=G1)
ry3 -= 4.8
y -= card_h + 3*mm
# 60-min rule banner
banner_h = 10 * mm
frect(cv, x, y - banner_h, col_w, banner_h, NAVY, r=3)
text(cv, "\u23f1 THE 60-MINUTE RULE",
x + col_w/2, y - 4.5*mm, size=8.5, font=RB,
color=HexColor("#FFD700"), align="c")
text(cv, "PDL cells die after 60 min dry \u2192 TIME IS CRITICAL",
x + col_w/2, y - 8.5*mm, size=7, color=WHITE, align="c")
y -= banner_h + 4*mm
# Replantation steps header
y = hdr_bar(cv, x, y, col_w, 6.5*mm,
"REPLANTATION STEPS (permanent tooth)", bg=GREEN)
y -= 2*mm
steps = [
"Pick up by CROWN — never touch the root",
"Rinse gently with saline — do NOT scrub",
"Insert root into socket (concave side toward tongue)",
"Bite on gauze to hold position",
"Dentist for splinting within 60 minutes",
"Antibiotics (penicillin) + tetanus update",
]
y = step_list(cv, steps, x, y, col_w,
num_bg=GREEN, line_h=5.5*mm, gap=0.8*mm, fsize=6.5)
y -= 3*mm
# Storage media header
y = hdr_bar(cv, x, y, col_w, 6.5*mm,
"STORAGE MEDIA (if can't replant immediately)", bg=TEAL)
y -= 1.5*mm
media = [
("Hank's BSS / Save-A-Tooth", "12 – 24 h", GREEN, LGREEN),
("Cold milk", "4 – 8 h", TEAL, LTEAL),
("Saliva (buccal sulcus)", "30 – 60 min", AMBER, LAMBER),
("Saline", "30 – 60 min", AMBER, LAMBER),
("Water", "< 30 min", DRED, LRED),
("DRY", "< 15 min", DRED, LRED),
]
row_h = 5.8 * mm
for mi, (medium, dur, tc2, bg2) in enumerate(media):
frect(cv, x, y - row_h, col_w, row_h, bg2,
stroke=HexColor("#BBBBBB"), lw=0.25)
text(cv, medium, x + 1.5*mm, y - row_h + row_h*0.28, size=6.5, color=G1)
text(cv, dur, x + col_w - 1.5*mm, y - row_h + row_h*0.28,
size=6.5, font=RB, color=tc2, align="r")
y -= row_h
# ══════════════════════════════════════════════════════════════
# PAGE 2 — MANAGEMENT TABLE | EXAMINATION | MNEMONICS
# ══════════════════════════════════════════════════════════════
def page2(cv):
frect(cv, 0, 0, PW, PH, WHITE)
y_content = global_header(
cv, "Quick-Reference Card | Management · Examination · Medications · Key Rules")
global_footer(cv, "PAGE 2 / 2")
FOOT_H = 7.5 * mm
content_bottom = FOOT_H + 2*mm
GAP = 5 * mm
INNER = 3 * mm
total_w = PW - 2 * M
# Two columns: left wider (management table), right narrower
col_l = total_w * 0.58
col_r = total_w * 0.42 - GAP
xl = M
xr = M + col_l + GAP
# ─────────────────────────────────────────────
# LEFT — Full Management Summary Table
# ─────────────────────────────────────────────
x = xl
y = y_content - 3*mm
y = hdr_bar(cv, x, y, col_l, 7.5*mm,
"COMPLETE MANAGEMENT SUMMARY", bg=NAVY, fsize=9)
y -= 2*mm
mgmt_rows = [
# header
["TYPE", "INJURY", "ED ACTION", "URGENCY"],
# fractures
["FRACTURE", "Infraction (enamel crack)",
"No treatment needed; routine follow-up", "Routine"],
["FRACTURE", "Ellis I (enamel only)",
"Smooth sharp edge; keep fragment moist", "Routine"],
["FRACTURE", "Ellis II (enamel + dentin)",
"Ca(OH)\u2082 dressing on exposed dentin", "24 – 48 h"],
["FRACTURE", "Ellis III (pulp exposed)",
"Temporary cover; urgent endodontic referral", "SAME DAY"],
["FRACTURE", "Root fracture",
"Stabilise; do not extract; urgent dental ref.", "Urgent"],
["FRACTURE", "Alveolar bone fracture",
"Reposition + splint; maxillofacial surgery", "Urgent"],
# positional
["POSITION", "Concussion",
"Soft diet; percussion pain only; follow-up", "Routine"],
["POSITION", "Subluxation",
"Flexible splint if mobile; dentist 48 h", "48 h"],
["POSITION", "Extrusive luxation",
"Reposition + splint immediately", "Urgent"],
["POSITION", "Intrusive luxation",
"NO manipulation; X-ray; refer \u2264 24 h", "24 h"],
["POSITION", "Lateral luxation",
"Reposition + splint; urgent referral", "Urgent"],
# avulsion
["AVULSION", "Primary tooth avulsed",
"DO NOT replant; reassure; dental follow-up", "Routine"],
["AVULSION", "Permanent tooth avulsed",
"Replant / store in milk; URGENT dentist", "EMERGENCY"],
]
# Column widths for this table
tw1 = col_l
cw_type = 17*mm
cw_inj = 47*mm
cw_act = tw1 - cw_type - cw_inj - 22*mm
cw_urg = 22*mm
col_ws = [cw_type, cw_inj, cw_act, cw_urg]
RH_HDR = 7 * mm
RH_BODY = 7 * mm
# Urgency colour map
urg_colors = {
"EMERGENCY": DRED,
"SAME DAY": DRED,
"Urgent": AMBER,
"24 h": AMBER,
"24 – 48 h": AMBER,
"48 h": AMBER,
"Routine": GREEN,
}
type_colors = {
"FRACTURE": DBLUE,
"POSITION": TEAL,
"AVULSION": DRED,
}
# Draw header row manually
rh = RH_HDR
frect(cv, x, y - rh, tw1, rh, HexColor("#1E3A5F"))
hdr_labels = ["TYPE", "INJURY", "ED ACTION", "URGENCY"]
hx = x
for ci, (lbl, cw) in enumerate(zip(hdr_labels, col_ws)):
text(cv, lbl, hx + 1.5*mm, y - rh + rh*0.3,
size=7, font=RB, color=WHITE)
hx += cw
y -= rh
last_type = None
for ri, row in enumerate(mgmt_rows[1:]): # skip header
rh = RH_BODY
typ, inj, act, urg = row
# Row background
alt = LGRAY if ri % 2 == 0 else WHITE
frect(cv, x, y - rh, tw1, rh, alt,
stroke=HexColor("#DDDDDD"), lw=0.25)
hx = x
# TYPE cell — badge only when type changes
if typ != last_type:
tc2 = type_colors.get(typ, DBLUE)
frect(cv, hx + 0.5*mm, y - rh + 1*mm,
cw_type - 1*mm, rh - 2*mm, tc2, r=2)
text(cv, typ, hx + cw_type/2, y - rh + rh*0.28,
size=6, font=RB, color=WHITE, align="c")
last_type = typ
hx += cw_type
# INJURY cell
text(cv, inj, hx + 1.5*mm, y - rh + rh*0.28,
size=6.5, font=RB, color=G1)
hx += cw_inj
# ACTION cell — may need truncation
max_c = int(cw_act / (6 * 0.52))
act_s = act if len(act) <= max_c else act[:max_c-1] + "…"
text(cv, act_s, hx + 1.5*mm, y - rh + rh*0.28,
size=6, color=G1)
hx += cw_act
# URGENCY cell
uc = urg_colors.get(urg, AMBER)
frect(cv, hx + 0.5*mm, y - rh + 1*mm,
cw_urg - 1*mm, rh - 2*mm, uc, r=2)
text(cv, urg, hx + cw_urg/2, y - rh + rh*0.28,
size=6, font=RB, color=WHITE, align="c")
y -= rh
y -= INNER
# Primary vs Permanent rule
box_h = 13 * mm
frect(cv, x, y - box_h, col_l, box_h, LLBLUE,
stroke=NAVY, lw=0.6, r=3)
text(cv, "PRIMARY TOOTH \u2192 Conservative (observe / no replant)",
x + 2*mm, y - 4.5*mm, size=7.5, font=RB, color=GREEN)
text(cv, "PERMANENT TOOTH \u2192 Aggressive (urgent replant / reposition)",
x + 2*mm, y - 9*mm, size=7.5, font=RB, color=DRED)
text(cv, "Memory aid: Primary = Passive. Permanent = Pursue urgently.",
x + 2*mm, y - 12.5*mm, size=6.5, font=RI, color=G2)
y -= box_h + INNER
# ─────────────────────────────────────────────
# RIGHT — Examination + Medications + Mnemonics
# ─────────────────────────────────────────────
x = xr
y = y_content - 3*mm
# Examination
y = hdr_bar(cv, x, y, col_r, 7*mm,
"CLINICAL EXAMINATION (HLFF)", bg=DBLUE)
y -= 2*mm
exam = [
("H HISTORY",
DBLUE,
["When/how? Time tooth was out? Where is tooth now?",
"Pain? Sensitivity? Bite changed?"]),
("L LOOK",
TEAL,
["Fracture colour: white / yellow / pink-red",
"Displacement direction? Multiple teeth as block?",
"Soft tissue: lacerations, embedded fragments"]),
("F FEEL",
AMBER,
["Percussion: pain = concussion / subluxation",
"Mobility test: 2 tongue blades gently",
"Occlusion: malocclusion = alveolar fracture"]),
("F (IMAGE)",
DRED,
["X-ray if tooth location uncertain",
"CXR if aspiration risk (unconscious patient)",
"Below diaphragm = passes naturally"]),
]
for (hd, tc2, bullets) in exam:
bh = (len(bullets)) * 5.5*mm + 10*mm
frect(cv, x, y - bh, col_r, bh, LGRAY,
stroke=HexColor("#CCCCCC"), lw=0.3, r=2)
badge(cv, hd, x + 1*mm, y - 1*mm, col_r - 2*mm, 5.5*mm, tc2)
by2 = y - 9*mm
for bl in bullets:
text(cv, "\u2022 " + bl, x + 2*mm, by2, size=6.2, color=G1)
by2 -= 5.5
y -= bh + 2*mm
# Medications
y = hdr_bar(cv, x, y, col_r, 6.5*mm, "MEDICATIONS", bg=AMBER)
y -= 1.5*mm
meds = [
("Analgesia", "NSAIDs / Paracetamol", "All significant injuries", AMBER, LAMBER),
("Antibiotics", "Penicillin V / Amoxicillin 500mg","Replantation, open fracture", DBLUE, LBLUE),
("CHX rinse", "0.12% BD \u00d7 1 week", "Post-replantation, ANUG", TEAL, LTEAL),
("Tetanus", "Update if >5 yr since last dose", "Open / contaminated wounds", GREEN, LGREEN),
]
med_rh = 7 * mm
for (drug, dose, ind, tc2, bg2) in meds:
frect(cv, x, y - med_rh, col_r, med_rh, bg2,
stroke=HexColor("#CCCCCC"), lw=0.25)
text(cv, drug, x + 1.5*mm, y - 2.5*mm,
size=6.5, font=RB, color=tc2)
text(cv, dose, x + 1.5*mm, y - med_rh + 1.5*mm,
size=6.2, color=G1)
text(cv, ind, x + col_r - 1.5*mm, y - med_rh + 1.5*mm,
size=6, font=RI, color=G3, align="r")
y -= med_rh
y -= 3*mm
# Mnemonics
y = hdr_bar(cv, x, y, col_r, 6.5*mm, "KEY RULES & MNEMONICS", bg=DRED)
y -= 2*mm
mnems = [
("COLOUR RULE",
"White = Ellis I Yellow = Ellis II Pink/Red = Ellis III",
DBLUE, LBLUE),
("60-MIN RULE",
"Permanent tooth PDL cells die after 60 min dry",
GREEN, LGREEN),
("NEVER REPLANT",
"Primary (baby) teeth — causes ankylosis",
DRED, LRED),
("NO MANIPULATION",
"Intruded tooth — DO NOT pull/push; X-ray & refer",
ORANGE, LORANG),
("MISSING TOOTH",
"Cannot find tooth \u2192 CXR to rule out aspiration",
TEAL, LTEAL),
("BELOW DIAPHRAGM",
"If swallowed, visible below diaphragm \u2192 no retrieval",
AMBER, LAMBER),
]
mnem_rh = 9 * mm
for (lbl, rule, tc2, bg2) in mnems:
frect(cv, x, y - mnem_rh, col_r, mnem_rh, bg2,
stroke=HexColor("#BBBBBB"), lw=0.3, r=2)
badge(cv, lbl, x + 0.5*mm, y - 0.8*mm, 22*mm, 4.5*mm, tc2, size=5.5)
# wrap rule text into max 2 lines
max_c2 = int((col_r - 25*mm) / (6 * 0.52))
words = rule.split()
line1 = ""; line2 = ""; switched = False
for w in words:
test_l = (line1 + " " + w).strip()
if not switched and len(test_l) <= max_c2:
line1 = test_l
else:
switched = True
line2 = (line2 + " " + w).strip()
text(cv, line1, x + 24*mm, y - 3*mm, size=6, color=G1)
if line2:
text(cv, line2, x + 24*mm, y - 7*mm, size=6, color=G1)
y -= mnem_rh + 1.5*mm
# ══════════════════════════════════════════════════════════════
# BUILD
# ══════════════════════════════════════════════════════════════
cv = rl_canvas.Canvas(OUT, pagesize=(PW, PH))
cv.setTitle("Dental Trauma Quick-Reference Pocket Card")
cv.setAuthor("Orris Medical Library")
page1(cv)
cv.showPage()
page2(cv)
cv.showPage()
cv.save()
print(f"SAVED: {OUT}")
cd /tmp/workspace/dental-card && python3 build_card_v2.py 2>&1
badge function uses fsize not size:~/dental-card/build_card_v2.py
Successfully replaced 1 instance(s) of the string in '/tmp/workspace/dental-card/build_card_v2.py'
cd /tmp/workspace/dental-card && python3 build_card_v2.py 2>&1
ls -lh /tmp/workspace/dental-card/Dental_Trauma_Pocket_Card_v2.pdf
Dental Trauma Pocket Card v2
PDF Document · PDF
| Page | Left Column | Centre Column | Right Column |
|---|---|---|---|
| 1 | Tooth anatomy diagram + sulcus rule + Ellis I/II/III cards | 5 positional injury cards (Concussion → Lateral Luxation) | Avulsion (Primary vs Permanent) + 60-min rule + Replantation steps + Storage media table |
| 2 | Full 14-row management summary table + Primary vs Permanent rule | (merged into left — wider column) | Clinical exam (HLFF) + Medications + 6 key mnemonics |
Provide me concept about gingivitis and periodontitic alone with figure also