~/gingivitis-pptx/build_pptx.py
import json, subprocess, base64
from io import BytesIO
from pptx import Presentation
from pptx.util import Inches, Pt, Emu
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.enum.shapes import MSO_SHAPE_TYPE
from pptx.util import Inches, Pt
from PIL import Image
# ── Colour palette ───────────────────────────────────────────────
DARK_TEAL = RGBColor(0x00, 0x6E, 0x7F) # primary header bg
MID_TEAL = RGBColor(0x00, 0x9B, 0xAD) # accent
LIGHT_TEAL = RGBColor(0xD6, 0xF0, 0xF3) # content bg
WHITE = RGBColor(0xFF, 0xFF, 0xFF)
DARK_TEXT = RGBColor(0x1A, 0x1A, 0x2E)
ORANGE = RGBColor(0xE8, 0x6A, 0x10)
LIGHT_GRAY = RGBColor(0xF4, 0xF7, 0xF9)
BORDER_GRAY = RGBColor(0xCC, 0xD6, 0xDD)
RED_ACCENT = RGBColor(0xC0, 0x39, 0x2B)
# ── Image URLs ────────────────────────────────────────────────────
IMG_URLS = [
"https://cdn.orris.care/cdss_images/33356f5f496fb283cc24558359756a3b881076f3538147250b5e8f6bcc49d971.png", # chronic periodontitis
"https://cdn.orris.care/cdss_images/2e0ecc7ac02e9f8b4258175ade4bf4bdede06a0097b16e2ac4914a6abb11abd6.png", # normal gingiva
"https://cdn.orris.care/cdss_images/6476ed713e2de55cb2834230b1e1c2383ebd66d0a2c4f89105cb99c4b80d9286.png", # advanced periodontitis
]
# Fetch all images
raw_result = json.loads(subprocess.check_output(
["python", "/tmp/skills/shared/scripts/fetch_images.py"] + IMG_URLS
))
imgs = []
for r in raw_result:
if r["base64"]:
imgs.append(base64.b64decode(r["base64"].split(",", 1)[1]))
else:
imgs.append(None)
# ── Helper functions ──────────────────────────────────────────────
prs = Presentation()
prs.slide_width = Inches(13.333)
prs.slide_height = Inches(7.5)
BLANK = prs.slide_layouts[6]
def add_slide():
return prs.slides.add_slide(BLANK)
def bg(slide, color):
"""Fill slide background."""
fill = slide.background.fill
fill.solid()
fill.fore_color.rgb = color
def rect(slide, l, t, w, h, fill_color, line_color=None, line_w=0):
from pptx.enum.shapes import MSO_SHAPE
shp = slide.shapes.add_shape(1, Inches(l), Inches(t), Inches(w), Inches(h)) # 1=RECTANGLE
shp.fill.solid()
shp.fill.fore_color.rgb = fill_color
if line_color:
shp.line.color.rgb = line_color
shp.line.width = Pt(line_w)
else:
shp.line.fill.background()
shp.shadow.inherit = False
return shp
def textbox(slide, text, l, t, w, h, size=18, bold=False, color=DARK_TEXT,
align=PP_ALIGN.LEFT, wrap=True, italic=False, font="Calibri",
valign=MSO_ANCHOR.TOP, margin_l=0.08, margin_t=0.05):
tb = slide.shapes.add_textbox(Inches(l), Inches(t), Inches(w), Inches(h))
tf = tb.text_frame
tf.word_wrap = wrap
tf.vertical_anchor = valign
tf.margin_left = Inches(margin_l)
tf.margin_right = Inches(0.05)
tf.margin_top = Inches(margin_t)
tf.margin_bottom = Inches(0.05)
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
def add_bullet_tf(slide, bullets, l, t, w, h, size=15, color=DARK_TEXT,
bullet_char="•", indent_l=0.12, font="Calibri"):
tb = slide.shapes.add_textbox(Inches(l), Inches(t), Inches(w), Inches(h))
tf = tb.text_frame
tf.word_wrap = True
tf.margin_left = Inches(0.08)
tf.margin_right = Inches(0.05)
tf.margin_top = Inches(0.05)
tf.margin_bottom = Inches(0.05)
for i, (btext, level) in enumerate(bullets):
p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
p.level = level
run = p.add_run()
prefix = (" " * level) + bullet_char + " "
run.text = prefix + btext
run.font.name = font
run.font.size = Pt(size)
run.font.color.rgb = color
return tf
def place_image(slide, img_bytes, l, t, max_h_in):
if img_bytes is None:
return
buf = BytesIO(img_bytes)
pil = Image.open(BytesIO(img_bytes))
w_px, h_px = pil.size
max_h = Inches(max_h_in)
disp_w = int(max_h * (w_px / h_px))
slide.shapes.add_picture(BytesIO(img_bytes), Inches(l), Inches(t), height=max_h)
def header_bar(slide, title, subtitle=None):
rect(slide, 0, 0, 13.333, 1.15, DARK_TEAL)
# left accent stripe
rect(slide, 0, 0, 0.18, 1.15, MID_TEAL)
textbox(slide, title, 0.3, 0.08, 10, 0.7,
size=30, bold=True, color=WHITE, align=PP_ALIGN.LEFT, valign=MSO_ANCHOR.MIDDLE)
if subtitle:
textbox(slide, subtitle, 0.3, 0.78, 12, 0.38,
size=13, color=RGBColor(0xB2, 0xE4, 0xEC), italic=True)
def section_label(slide, text, l, t, w, color=MID_TEAL):
rect(slide, l, t, w, 0.32, color)
textbox(slide, text, l+0.08, t, w-0.1, 0.32,
size=13, bold=True, color=WHITE, valign=MSO_ANCHOR.MIDDLE)
def footer(slide):
rect(slide, 0, 7.22, 13.333, 0.28, DARK_TEAL)
textbox(slide, "Gingivitis & Periodontitis | Medical Education Series",
0.3, 7.22, 10, 0.28, size=9, color=RGBColor(0xB2, 0xE4, 0xEC), valign=MSO_ANCHOR.MIDDLE)
textbox(slide, "Sources: Robbins Pathology • Sherris Microbiology • Harrison's IM • ROSEN's EM",
7.5, 7.22, 5.7, 0.28, size=8, color=RGBColor(0xB2, 0xE4, 0xEC),
align=PP_ALIGN.RIGHT, valign=MSO_ANCHOR.MIDDLE)
# ═══════════════════════════════════════════════════════════════════
# SLIDE 1 – TITLE SLIDE
# ═══════════════════════════════════════════════════════════════════
s = add_slide()
bg(s, DARK_TEAL)
# decorative large circle
from pptx.util import Emu
shp = s.shapes.add_shape(9, Inches(8.5), Inches(-1.2), Inches(6.5), Inches(6.5)) # 9=OVAL
shp.fill.solid(); shp.fill.fore_color.rgb = MID_TEAL
shp.line.fill.background(); shp.shadow.inherit = False
shp2 = s.shapes.add_shape(9, Inches(9.5), Inches(-0.5), Inches(4.5), Inches(4.5))
shp2.fill.solid(); shp2.fill.fore_color.rgb = DARK_TEAL
shp2.line.fill.background(); shp2.shadow.inherit = False
# white card
rect(s, 0.6, 1.3, 7.5, 4.8, WHITE)
rect(s, 0.6, 1.3, 0.22, 4.8, ORANGE) # left accent
textbox(s, "GINGIVITIS &\nPERIODONTITIS", 1.0, 1.6, 6.8, 2.0,
size=44, bold=True, color=DARK_TEAL, font="Calibri")
textbox(s, "A Comprehensive Clinical Overview", 1.0, 3.6, 6.8, 0.6,
size=20, italic=True, color=MID_TEAL)
textbox(s, "Oral Cavity — Diseases of Teeth & Supporting Structures",
1.0, 4.3, 6.8, 0.5, size=14, color=DARK_TEXT)
textbox(s, "Medical Education Series | 2026", 1.0, 5.5, 6.8, 0.4,
size=12, color=RGBColor(0x88, 0x88, 0x99))
# place periodontitis image on right
if imgs[0]:
buf = BytesIO(imgs[0])
pil = Image.open(BytesIO(imgs[0]))
w_px, h_px = pil.size
max_h = Inches(3.5)
disp_w = int(max_h * (w_px / h_px))
s.shapes.add_picture(BytesIO(imgs[0]), Inches(9.2), Inches(2.0), height=max_h)
textbox(s, "Chronic Periodontitis", 9.0, 5.6, 4.0, 0.4,
size=10, italic=True, color=WHITE, align=PP_ALIGN.CENTER)
# ═══════════════════════════════════════════════════════════════════
# SLIDE 2 – OVERVIEW / OUTLINE
# ═══════════════════════════════════════════════════════════════════
s = add_slide()
bg(s, LIGHT_GRAY)
header_bar(s, "Overview", "Periodontal Disease — Scope & Agenda")
rect(s, 0, 7.22, 13.333, 0.28, DARK_TEAL)
topics = [
("1", "Introduction — What are periodontal diseases?"),
("2", "Gingivitis — Definition, Causes & Pathogenesis"),
("3", "Gingivitis — Clinical Features & Types"),
("4", "Periodontitis — Definition & Pathogenesis"),
("5", "Periodontitis — Classification & Clinical Features"),
("6", "Key Differences — Gingivitis vs. Periodontitis"),
("7", "Microbiology of Periodontal Disease"),
("8", "Systemic Associations"),
("9", "Management & Treatment"),
("10", "Prevention"),
]
col_w = 5.8
for i, (num, topic) in enumerate(topics):
row = i % 5
col = i // 5
lx = 0.5 + col * 6.5
ty = 1.35 + row * 1.1
rect(s, lx, ty, col_w, 0.85, WHITE, BORDER_GRAY, 0.8)
rect(s, lx, ty, 0.55, 0.85, MID_TEAL)
textbox(s, num, lx, ty, 0.55, 0.85, size=20, bold=True, color=WHITE,
align=PP_ALIGN.CENTER, valign=MSO_ANCHOR.MIDDLE)
textbox(s, topic, lx+0.6, ty+0.05, col_w-0.65, 0.75, size=13.5,
color=DARK_TEXT, valign=MSO_ANCHOR.MIDDLE)
footer(s)
# ═══════════════════════════════════════════════════════════════════
# SLIDE 3 – INTRODUCTION
# ═══════════════════════════════════════════════════════════════════
s = add_slide()
bg(s, LIGHT_GRAY)
header_bar(s, "Introduction", "Diseases of Teeth & Supporting Structures")
rect(s, 0.4, 1.3, 12.5, 5.6, WHITE, BORDER_GRAY, 0.6)
rect(s, 0.4, 1.3, 0.18, 5.6, MID_TEAL)
textbox(s, "Bacteria in the oral cavity are directly or indirectly responsible for the most "
"common disorders of the teeth and gums: dental caries, gingivitis, and periodontitis.",
0.7, 1.4, 12.0, 0.9, size=15, color=DARK_TEXT, italic=True)
# 3 info boxes
info = [
("PLAQUE", "A sticky biofilm of bacteria, salivary proteins & desquamated epithelial cells that forms on tooth surfaces."),
("CALCULUS\n(Tartar)", "Mineralized plaque. Provides a rough surface that harbours more bacteria and perpetuates inflammation."),
("GINGIVAL\nSULCUS", "The physiological space between the free gingiva and the tooth. Deepens into a periodontal pocket in disease."),
]
box_colors = [MID_TEAL, ORANGE, DARK_TEAL]
for i, (title, body) in enumerate(info):
lx = 0.55 + i * 4.12
rect(s, lx, 2.5, 3.8, 0.55, box_colors[i])
textbox(s, title, lx, 2.5, 3.8, 0.55, size=14, bold=True,
color=WHITE, align=PP_ALIGN.CENTER, valign=MSO_ANCHOR.MIDDLE)
rect(s, lx, 3.05, 3.8, 2.0, WHITE, BORDER_GRAY, 0.5)
textbox(s, body, lx+0.1, 3.1, 3.6, 1.9, size=13, color=DARK_TEXT, wrap=True)
textbox(s, "Two main diseases arise from this plaque-bacterial interaction:", 0.7, 5.2, 12.0, 0.4,
size=14, bold=True, color=DARK_TEAL)
for i, (label, desc) in enumerate([
("GINGIVITIS", "Reversible inflammation limited to the gingiva — no bone loss"),
("PERIODONTITIS", "Irreversible inflammation with destruction of periodontal ligament & alveolar bone"),
]):
lx = 0.7 + i * 6.2
rect(s, lx, 5.65, 5.7, 0.65, LIGHT_TEAL, MID_TEAL, 1.0)
textbox(s, f"{label}: {desc}", lx+0.12, 5.65, 5.5, 0.65,
size=13, color=DARK_TEAL, valign=MSO_ANCHOR.MIDDLE, bold=False)
footer(s)
# ═══════════════════════════════════════════════════════════════════
# SLIDE 4 – GINGIVITIS: DEFINITION & PATHOGENESIS
# ═══════════════════════════════════════════════════════════════════
s = add_slide()
bg(s, LIGHT_GRAY)
header_bar(s, "Gingivitis", "Definition, Causes & Pathogenesis")
# Left column
rect(s, 0.4, 1.3, 6.1, 5.85, WHITE, BORDER_GRAY, 0.6)
rect(s, 0.4, 1.3, 0.18, 5.85, MID_TEAL)
section_label(s, "DEFINITION", 0.6, 1.35, 5.7, MID_TEAL)
textbox(s, "Inflammation involving the squamous mucosa (gingiva) and associated soft tissues "
"surrounding the teeth. Confined to the gingiva — NO bone resorption.",
0.65, 1.72, 5.7, 1.0, size=13.5, color=DARK_TEXT, wrap=True)
section_label(s, "PRIMARY CAUSE", 0.6, 2.8, 5.7, MID_TEAL)
textbox(s, "Poor oral hygiene → plaque & calculus accumulation between and on tooth surfaces.",
0.65, 3.17, 5.7, 0.55, size=13.5, color=DARK_TEXT)
section_label(s, "PATHOGENESIS", 0.6, 3.8, 5.7, DARK_TEAL)
steps = [
"Plaque forms at gingival margin",
"Mineralizes → calculus (tartar)",
"Subgingival plaque enters gingival sulcus",
"PMNs, lymphocytes & plasma cells infiltrate",
"Collagen lost from inflamed connective tissue",
"Gingivitis develops within 2 weeks of poor hygiene",
]
for i, st in enumerate(steps):
ty = 4.18 + i * 0.43
rect(s, 0.65, ty, 0.38, 0.35, MID_TEAL)
textbox(s, str(i+1), 0.65, ty, 0.38, 0.35, size=11, bold=True, color=WHITE,
align=PP_ALIGN.CENTER, valign=MSO_ANCHOR.MIDDLE)
textbox(s, st, 1.1, ty, 5.2, 0.38, size=12.5, color=DARK_TEXT, valign=MSO_ANCHOR.MIDDLE)
# Right column
rect(s, 6.85, 1.3, 6.1, 5.85, WHITE, BORDER_GRAY, 0.6)
rect(s, 6.85, 1.3, 0.18, 5.85, ORANGE)
section_label(s, "KEY FACTS", 6.9, 1.35, 5.75, ORANGE)
facts = [
("Most prevalent / severe", "Adolescence (40–60% of teenagers)"),
("Onset", "Within 2 weeks of poor hygiene"),
("Histology", "PMN + lymphocyte + plasma cell infiltrate; collagen loss"),
("Bacterial invasion", "Absent in early disease"),
("Reversibility", "YES — with brushing, flossing, cleaning"),
]
for i, (k, v) in enumerate(facts):
ty = 1.72 + i * 0.82
rect(s, 6.9, ty, 5.75, 0.75, LIGHT_TEAL, MID_TEAL, 0.5)
textbox(s, k, 7.0, ty+0.02, 2.3, 0.35, size=12, bold=True, color=DARK_TEAL)
textbox(s, v, 7.0, ty+0.35, 5.5, 0.38, size=12.5, color=DARK_TEXT)
section_label(s, "NOTE", 6.9, 5.75, 5.75, RED_ACCENT)
textbox(s, "No direct bacterial invasion of gingival tissues occurs in early gingivitis — "
"damage is immune-mediated.", 7.0, 6.12, 5.6, 0.6, size=12.5, color=DARK_TEXT, italic=True)
footer(s)
# ═══════════════════════════════════════════════════════════════════
# SLIDE 5 – GINGIVITIS: CLINICAL FEATURES & TYPES
# ═══════════════════════════════════════════════════════════════════
s = add_slide()
bg(s, LIGHT_GRAY)
header_bar(s, "Gingivitis", "Clinical Features, Special Types & Signs")
# Clinical features box
rect(s, 0.4, 1.3, 5.8, 5.9, WHITE, BORDER_GRAY, 0.6)
rect(s, 0.4, 1.3, 0.18, 5.9, MID_TEAL)
section_label(s, "CLINICAL FEATURES", 0.6, 1.35, 5.6, MID_TEAL)
features = [
("Erythema", "Redness of the gingival margin"),
("Edema", "Swelling of the gums"),
("Bleeding", "On brushing or probing (key sign)"),
("Halitosis", "Bad breath may accompany"),
("Pain", "Usually mild or absent"),
("No mobility", "Teeth remain firmly in place"),
("No bone loss", "Distinguishes from periodontitis"),
]
for i, (feat, desc) in enumerate(features):
ty = 1.78 + i * 0.7
rect(s, 0.65, ty, 1.6, 0.55, MID_TEAL)
textbox(s, feat, 0.65, ty, 1.6, 0.55, size=11.5, bold=True,
color=WHITE, align=PP_ALIGN.CENTER, valign=MSO_ANCHOR.MIDDLE)
textbox(s, desc, 2.35, ty+0.05, 3.8, 0.5, size=12.5, color=DARK_TEXT)
# Special types box
rect(s, 6.5, 1.3, 6.5, 5.9, WHITE, BORDER_GRAY, 0.6)
rect(s, 6.5, 1.3, 0.18, 5.9, ORANGE)
section_label(s, "SPECIAL TYPES OF GINGIVITIS", 6.65, 1.35, 6.25, ORANGE)
types = [
("ANUG", "Acute Necrotizing Ulcerative Gingivitis\n(Trench Mouth / Vincent's Infection)", RED_ACCENT),
("NUP", "Necrotizing Ulcerative Periodontitis\n— Spectrum with ANUG", DARK_TEAL),
("HIV-related", "Linear gingival erythema; rapidly\nprogressive in immunocompromised", MID_TEAL),
("Pregnancy", "Exacerbated by hormonal changes;\ngranuloma pyogenicum may form", ORANGE),
("Drug-induced", "Phenytoin, cyclosporine, CCBs\ncause gingival hyperplasia", RGBColor(0x6A, 0x5A, 0xCD)),
]
for i, (abbr, desc, col) in enumerate(types):
ty = 1.78 + i * 1.08
rect(s, 6.65, ty, 1.0, 0.9, col)
textbox(s, abbr, 6.65, ty, 1.0, 0.9, size=9.5, bold=True,
color=WHITE, align=PP_ALIGN.CENTER, valign=MSO_ANCHOR.MIDDLE)
textbox(s, desc, 7.72, ty+0.05, 5.1, 0.85, size=12, color=DARK_TEXT)
footer(s)
# ═══════════════════════════════════════════════════════════════════
# SLIDE 6 – PERIODONTITIS: DEFINITION & PATHOGENESIS
# ═══════════════════════════════════════════════════════════════════
s = add_slide()
bg(s, LIGHT_GRAY)
header_bar(s, "Periodontitis", "Definition, Pathogenesis & Key Pathogens")
rect(s, 0.4, 1.3, 12.5, 5.9, WHITE, BORDER_GRAY, 0.6)
rect(s, 0.4, 1.3, 0.18, 5.9, RED_ACCENT)
# Definition
section_label(s, "DEFINITION", 0.65, 1.35, 12.0, RED_ACCENT)
textbox(s, "An inflammatory process that destroys the supporting structures of teeth — "
"periodontal ligament, cementum, and alveolar bone. "
"Unlike gingivitis, it is IRREVERSIBLE — lost bone does not regenerate.",
0.65, 1.73, 11.9, 0.8, size=14, color=DARK_TEXT, bold=False, italic=False)
# Progression steps
section_label(s, "PROGRESSION: GINGIVITIS → PERIODONTITIS", 0.65, 2.65, 12.0, DARK_TEAL)
prog = [
("Plaque accumulates\nat gingival margin", MID_TEAL),
("Gingivitis\n(reversible)", MID_TEAL),
("Subgingival plaque\ndeepens", ORANGE),
("Bone & ligament\ndestruction begins", ORANGE),
("Periodontal pocket\nforms (>3 mm)", RED_ACCENT),
("Tooth\nloosening / loss", RED_ACCENT),
]
arr_colors = [MID_TEAL, MID_TEAL, ORANGE, ORANGE, RED_ACCENT]
box_w = 1.9
for i, (label, col) in enumerate(prog):
lx = 0.65 + i * 2.1
rect(s, lx, 3.05, box_w, 0.85, col)
textbox(s, label, lx, 3.05, box_w, 0.85, size=10.5, bold=True,
color=WHITE, align=PP_ALIGN.CENTER, valign=MSO_ANCHOR.MIDDLE)
if i < 5:
textbox(s, "→", lx + box_w, 3.2, 0.25, 0.55, size=16, bold=True,
color=DARK_TEXT, align=PP_ALIGN.CENTER)
# Key pathogens
section_label(s, "KEY PATHOGENS (Subgingival Plaque)", 0.65, 4.05, 12.0, DARK_TEAL)
pathogens = [
("Porphyromonas\ngingivalis", "Extracellular proteases\n(tissue destruction)"),
("Treponema\ndenticola", "Serum factor binding\n(complement evasion)"),
("Aggregatibacter\nactinomycetemcomitans", "Leukotoxin production\n(juvenile periodontitis)"),
("Prevotella\nintermedia", "Associated with adult &\npregnancy-related forms"),
]
for i, (name, role) in enumerate(pathogens):
lx = 0.65 + i * 3.05
rect(s, lx, 4.45, 2.85, 0.52, DARK_TEAL)
textbox(s, name, lx, 4.45, 2.85, 0.52, size=10.5, bold=True,
color=WHITE, align=PP_ALIGN.CENTER, valign=MSO_ANCHOR.MIDDLE)
rect(s, lx, 4.97, 2.85, 0.55, LIGHT_TEAL, MID_TEAL, 0.5)
textbox(s, role, lx+0.08, 4.97, 2.75, 0.55, size=11, color=DARK_TEAL, valign=MSO_ANCHOR.MIDDLE)
section_label(s, "MECHANISM", 0.65, 5.6, 12.0, MID_TEAL)
textbox(s, "Synergism between P. gingivalis & T. denticola fosters disease progression. "
"Bacterial interactions with Toll-like receptors (TLRs) trigger the destructive inflammatory cascade.",
0.65, 5.98, 12.0, 0.65, size=12.5, color=DARK_TEXT, italic=True)
footer(s)
# ═══════════════════════════════════════════════════════════════════
# SLIDE 7 – PERIODONTITIS: CLASSIFICATION & CLINICAL FEATURES
# ═══════════════════════════════════════════════════════════════════
s = add_slide()
bg(s, LIGHT_GRAY)
header_bar(s, "Periodontitis", "Classification & Clinical Features")
# Left: Clinical features
rect(s, 0.4, 1.3, 5.6, 5.9, WHITE, BORDER_GRAY, 0.6)
rect(s, 0.4, 1.3, 0.18, 5.9, RED_ACCENT)
section_label(s, "CLINICAL FEATURES", 0.6, 1.35, 5.4, RED_ACCENT)
cf = [
"Deepened gingival pockets (>3 mm on probing)",
"Bleeding on probing",
"Attachment loss (clinical & radiographic)",
"Alveolar bone resorption (X-ray)",
"Tooth mobility in advanced disease",
"Gingival recession",
"Periodontal abscess (acute exacerbation)",
"Halitosis",
"Eventually — tooth exfoliation / loss",
]
for i, feat in enumerate(cf):
ty = 1.78 + i * 0.58
rect(s, 0.65, ty+0.1, 0.22, 0.22, RED_ACCENT)
textbox(s, feat, 0.95, ty, 4.9, 0.55, size=12.5, color=DARK_TEXT)
# Right: Classification table
rect(s, 6.3, 1.3, 6.7, 5.9, WHITE, BORDER_GRAY, 0.6)
rect(s, 6.3, 1.3, 0.18, 5.9, ORANGE)
section_label(s, "CLASSIFICATION", 6.45, 1.35, 6.5, ORANGE)
classes = [
("Chronic Adult\nPeriodontitis", "Most common. Slow progression after age 35–40. Main cause of adult tooth loss.", DARK_TEAL),
("Localized Aggressive\n(Juvenile)", "Adolescents. Rapid bone loss. A. actinomycetemcomitans + leukotoxin.", ORANGE),
("Generalized\nAggressive", "Young adults. Rapid, widespread destruction.", RED_ACCENT),
("Necrotizing Ulcerative\nPeriodontitis (NUP)", "HIV/immunocompromised patients. Rapidly destructive; gangrenous.", MID_TEAL),
("AIDS-Related", "Resembles NUP or severe chronic form; sometimes noma-like.", RGBColor(0x6A, 0x5A, 0xCD)),
]
for i, (name, desc, col) in enumerate(classes):
ty = 1.78 + i * 1.04
rect(s, 6.45, ty, 1.6, 0.88, col)
textbox(s, name, 6.45, ty, 1.6, 0.88, size=9.5, bold=True,
color=WHITE, align=PP_ALIGN.CENTER, valign=MSO_ANCHOR.MIDDLE)
rect(s, 8.05, ty, 4.75, 0.88, LIGHT_GRAY, BORDER_GRAY, 0.5)
textbox(s, desc, 8.12, ty+0.05, 4.6, 0.82, size=11.5, color=DARK_TEXT)
footer(s)
# ═══════════════════════════════════════════════════════════════════
# SLIDE 8 – CLINICAL FIGURES
# ═══════════════════════════════════════════════════════════════════
s = add_slide()
bg(s, LIGHT_GRAY)
header_bar(s, "Clinical Figures", "Normal Gingiva vs. Periodontitis")
captions = [
"Fig. A — Normal Gingiva\nPink, firm, stippled; sharp interdental papillae;\nfits snugly around teeth.",
"Fig. B — Advanced Periodontitis\nSeverely inflamed, deep red/hemorrhagic gingiva;\nmarked gingival recession & bone destruction.",
"Fig. C — Chronic Periodontitis\nHeavy plaque & calculus accumulation;\nswollen gums, loss of normal contour.",
]
img_indices = [1, 2, 0] # normal, advanced perio, chronic perio
col_colors = [MID_TEAL, RED_ACCENT, ORANGE]
for i, (idx, cap, col) in enumerate(zip(img_indices, captions, col_colors)):
lx = 0.4 + i * 4.35
rect(s, lx, 1.3, 4.0, 5.4, WHITE, BORDER_GRAY, 0.8)
rect(s, lx, 1.3, 4.0, 0.32, col)
if imgs[idx]:
buf_b = BytesIO(imgs[idx])
pil_b = Image.open(BytesIO(imgs[idx]))
w_px, h_px = pil_b.size
max_h = Inches(3.0)
disp_w = int(max_h * (w_px / h_px))
# Centre image in card
card_w_emu = Inches(4.0)
left_offset = int((card_w_emu - disp_w) / 2)
s.shapes.add_picture(BytesIO(imgs[idx]),
Inches(lx) + left_offset,
Inches(1.65),
height=max_h)
textbox(s, cap, lx + 0.08, 4.75, 3.85, 1.9, size=11.5, color=DARK_TEXT, wrap=True)
footer(s)
# ═══════════════════════════════════════════════════════════════════
# SLIDE 9 – KEY DIFFERENCES TABLE
# ═══════════════════════════════════════════════════════════════════
s = add_slide()
bg(s, LIGHT_GRAY)
header_bar(s, "Key Differences", "Gingivitis vs. Periodontitis — At a Glance")
# Table headers
headers = ["Feature", "Gingivitis", "Periodontitis"]
header_colors = [DARK_TEAL, MID_TEAL, RED_ACCENT]
col_widths = [3.2, 4.5, 4.5]
col_starts = [0.4, 3.65, 8.2]
for i, (h, col, w) in enumerate(zip(headers, header_colors, col_widths)):
rect(s, col_starts[i], 1.3, w, 0.5, col)
textbox(s, h, col_starts[i], 1.3, w, 0.5, size=14, bold=True,
color=WHITE, align=PP_ALIGN.CENTER, valign=MSO_ANCHOR.MIDDLE)
rows = [
("Site of inflammation", "Gingiva ONLY", "Gingiva + periodontal ligament + alveolar bone"),
("Bone resorption", "ABSENT", "PRESENT"),
("Pocket depth", "Normal (<3 mm)", "Deepened (>3 mm)"),
("Reversibility", "YES — reversible", "NO — irreversible"),
("Tooth mobility", "Absent", "Present in advanced disease"),
("Predominant bacteria", "Mixed Gram +/−", "Gram-negative anaerobes"),
("Tooth loss", "Does NOT occur", "May occur if untreated"),
("Treatment goal", "Restore health by hygiene", "Halt progression; surgical repair"),
]
row_bg = [WHITE, LIGHT_TEAL]
for ri, (feat, ging, peri) in enumerate(rows):
ty = 1.85 + ri * 0.64
bg_c = row_bg[ri % 2]
for ci, (text, cw, cx) in enumerate(zip([feat, ging, peri], col_widths, col_starts)):
rect(s, cx, ty, cw, 0.6, bg_c, BORDER_GRAY, 0.4)
col_c = DARK_TEXT if ci == 0 else (MID_TEAL if ci == 1 else RED_ACCENT)
bld = ci > 0
textbox(s, text, cx+0.1, ty+0.02, cw-0.15, 0.58, size=12.5,
color=col_c, bold=bld, valign=MSO_ANCHOR.MIDDLE)
footer(s)
# ═══════════════════════════════════════════════════════════════════
# SLIDE 10 – MICROBIOLOGY
# ═══════════════════════════════════════════════════════════════════
s = add_slide()
bg(s, LIGHT_GRAY)
header_bar(s, "Microbiology", "Subgingival Plaque & Key Organisms")
rect(s, 0.4, 1.3, 12.5, 5.9, WHITE, BORDER_GRAY, 0.6)
rect(s, 0.4, 1.3, 0.18, 5.9, MID_TEAL)
section_label(s, "PLAQUE MICROBIOLOGY", 0.65, 1.35, 12.0, MID_TEAL)
textbox(s, "Both gingivitis and periodontitis are caused by bacteria in dental plaque adjacent to tooth necks. "
"Subgingival plaque within the gingival crevice/sulcus houses the primary etiologic agents.",
0.65, 1.75, 11.9, 0.65, size=13.5, color=DARK_TEXT, italic=True)
# Bacteria panels
orgs = [
{
"name": "Porphyromonas gingivalis",
"gram": "Gram-negative anaerobe",
"role": "Major periodontopathogen. Produces powerful extracellular proteases (gingipains). Interferes with complement and evades host immunity.",
"col": DARK_TEAL,
},
{
"name": "Treponema denticola",
"gram": "Spirochete (anaerobe)",
"role": "Binds serum factors to block complement deposition. Synergizes with P. gingivalis to accelerate tissue destruction.",
"col": RED_ACCENT,
},
{
"name": "Aggregatibacter\nactinomycetemcomitans",
"gram": "Gram-negative capnophile",
"role": "Produces leukotoxin that destroys PMNs. Primary pathogen in localized aggressive (juvenile) periodontitis.",
"col": ORANGE,
},
{
"name": "Prevotella intermedia",
"gram": "Gram-negative anaerobe",
"role": "Associated with adult & pregnancy-related periodontitis. Produces proteases and short-chain fatty acids.",
"col": MID_TEAL,
},
]
for i, org in enumerate(orgs):
lx = 0.6 + i * 3.08
rect(s, lx, 2.55, 2.85, 0.5, org["col"])
textbox(s, org["name"], lx, 2.55, 2.85, 0.5, size=10.5, bold=True,
color=WHITE, align=PP_ALIGN.CENTER, valign=MSO_ANCHOR.MIDDLE)
rect(s, lx, 3.05, 2.85, 0.3, LIGHT_TEAL)
textbox(s, org["gram"], lx+0.05, 3.05, 2.78, 0.3, size=10, italic=True,
color=DARK_TEAL, valign=MSO_ANCHOR.MIDDLE)
rect(s, lx, 3.35, 2.85, 1.7, LIGHT_GRAY, BORDER_GRAY, 0.4)
textbox(s, org["role"], lx+0.08, 3.38, 2.72, 1.65, size=11.5, color=DARK_TEXT, wrap=True)
section_label(s, "VIRULENCE MECHANISMS", 0.65, 5.15, 12.0, DARK_TEAL)
textbox(s, "• TLR activation → inflammatory cascade "
"• Proteases → collagen destruction "
"• Complement evasion → survival in tissue "
"• Leukotoxin → PMN destruction "
"• Cross-feeding synergism accelerates plaque pathogenicity",
0.65, 5.52, 12.0, 0.85, size=12.5, color=DARK_TEXT)
footer(s)
# ═══════════════════════════════════════════════════════════════════
# SLIDE 11 – SYSTEMIC ASSOCIATIONS
# ═══════════════════════════════════════════════════════════════════
s = add_slide()
bg(s, LIGHT_GRAY)
header_bar(s, "Systemic Associations", "Periodontal Disease & Systemic Health")
assocs = [
("Diabetes\nMellitus", "Bidirectional relationship. Poorly controlled diabetes worsens periodontitis; "
"periodontitis may impair glycemic control.", MID_TEAL),
("Cardiovascular\nDisease", "Epidemiologic association between chronic periodontal inflammation and "
"atherosclerosis / coronary heart disease / stroke (causal role unproven).", RED_ACCENT),
("HIV /\nImmuno-\ncompromised", "Necrotizing forms (NUG/NUP) occur most often in HIV, poorly controlled diabetes, "
"long-term immunosuppressive therapy.", DARK_TEAL),
("Papillon-Lefèvre\nSyndrome", "Rare autosomal recessive. Severe periodontitis + hyperkeratosis of palms & soles. "
"Associated with cathepsin C mutation.", ORANGE),
("Pregnancy", "Hormonal changes exacerbate gingivitis. Pyogenic granuloma (pregnancy tumor) may form.", MID_TEAL),
("Down\nSyndrome", "Impaired neutrophil chemotaxis → severe early-onset periodontitis.", RED_ACCENT),
]
for i, (title, body, col) in enumerate(assocs):
row = i % 3
col_n = i // 3
lx = 0.4 + col_n * 6.55
ty = 1.35 + row * 1.98
rect(s, lx, ty, 6.1, 1.8, WHITE, BORDER_GRAY, 0.6)
rect(s, lx, ty, 1.5, 1.8, col)
textbox(s, title, lx, ty, 1.5, 1.8, size=11.5, bold=True,
color=WHITE, align=PP_ALIGN.CENTER, valign=MSO_ANCHOR.MIDDLE)
textbox(s, body, lx+1.6, ty+0.12, 4.35, 1.6, size=12, color=DARK_TEXT, wrap=True)
footer(s)
# ═══════════════════════════════════════════════════════════════════
# SLIDE 12 – MANAGEMENT & TREATMENT
# ═══════════════════════════════════════════════════════════════════
s = add_slide()
bg(s, LIGHT_GRAY)
header_bar(s, "Management & Treatment", "Gingivitis and Periodontitis")
# Gingivitis column
rect(s, 0.4, 1.3, 6.0, 5.9, WHITE, BORDER_GRAY, 0.6)
rect(s, 0.4, 1.3, 0.18, 5.9, MID_TEAL)
section_label(s, "GINGIVITIS MANAGEMENT", 0.6, 1.35, 5.75, MID_TEAL)
g_items = [
("1st Line", "Twice-daily brushing + flossing\n→ removes plaque & calculus"),
("Antiseptic\nRinses", "Chlorhexidine 0.12–0.2% (preferred)\n3% H₂O₂ diluted 1:1 for severe cases"),
("Antibiotics\n(if severe/NUP)", "Metronidazole 500 mg BID × 10 days\nAmox/clavulanate 500/125 mg TID × 10 d\nClindamycin 300 mg QID × 10 d (PCN allergy)"),
("Analgesia", "Ibuprofen 400–600 mg q6-8h\nAcetaminophen 650 mg q6h\nViscous lidocaine (topical, small areas)"),
("NUP/ANUG", "Dental debridement of necrotic tissue\nSystemic antibiotics mandatory"),
("Smoking", "Cessation counseling — #1 modifiable\nrisk factor in HIV-negative patients"),
]
for i, (label, text) in enumerate(g_items):
ty = 1.78 + i * 0.87
rect(s, 0.65, ty, 1.5, 0.78, LIGHT_TEAL, MID_TEAL, 0.5)
textbox(s, label, 0.65, ty, 1.5, 0.78, size=10.5, bold=True,
color=DARK_TEAL, align=PP_ALIGN.CENTER, valign=MSO_ANCHOR.MIDDLE)
textbox(s, text, 2.22, ty+0.05, 4.0, 0.76, size=11.5, color=DARK_TEXT, wrap=True)
# Periodontitis column
rect(s, 6.75, 1.3, 6.2, 5.9, WHITE, BORDER_GRAY, 0.6)
rect(s, 6.75, 1.3, 0.18, 5.9, RED_ACCENT)
section_label(s, "PERIODONTITIS MANAGEMENT", 6.9, 1.35, 5.95, RED_ACCENT)
p_items = [
("Scaling &\nRoot Planing", "Non-surgical debridement — removes\nsubgingival plaque & calculus"),
("Chlorhexidine", "0.12–0.2% rinse as adjunct\nto mechanical debridement"),
("Antibiotics", "Amoxicillin ± metronidazole\nDoxycycline for adjunctive use"),
("Surgical\nTherapy", "Open flap debridement\nGuided tissue regeneration (GTR)\nOsseous surgery"),
("Risk Factor\nControl", "Glycemic control in diabetics\nSmoking cessation\nImmunosuppression management"),
("Extraction", "When bone support is critically lost\nor pocket depth is uncontrollable"),
]
for i, (label, text) in enumerate(p_items):
ty = 1.78 + i * 0.87
rect(s, 6.9, ty, 1.5, 0.78, RGBColor(0xFC, 0xE8, 0xE6), RED_ACCENT, 0.5)
textbox(s, label, 6.9, ty, 1.5, 0.78, size=10.5, bold=True,
color=RED_ACCENT, align=PP_ALIGN.CENTER, valign=MSO_ANCHOR.MIDDLE)
textbox(s, text, 8.47, ty+0.05, 4.3, 0.76, size=11.5, color=DARK_TEXT, wrap=True)
footer(s)
# ═══════════════════════════════════════════════════════════════════
# SLIDE 13 – ANTIBIOTIC TABLE
# ═══════════════════════════════════════════════════════════════════
s = add_slide()
bg(s, LIGHT_GRAY)
header_bar(s, "Antibiotic Therapy", "Recommended Antibiotics for Severe Periodontal Disease")
rect(s, 0.4, 1.3, 12.5, 5.9, WHITE, BORDER_GRAY, 0.6)
rect(s, 0.4, 1.3, 0.18, 5.9, DARK_TEAL)
headers = ["Antibiotic", "Dosage", "Duration", "Notes"]
hcols = [2.8, 3.5, 1.8, 4.2]
hstarts = [0.65, 3.5, 7.05, 8.9]
for h, hw, hx in zip(headers, hcols, hstarts):
rect(s, hx, 1.45, hw, 0.45, DARK_TEAL)
textbox(s, h, hx+0.05, 1.45, hw-0.1, 0.45, size=13, bold=True,
color=WHITE, align=PP_ALIGN.CENTER, valign=MSO_ANCHOR.MIDDLE)
ab_rows = [
("Penicillin V", "500 mg PO TID-QID", "10 days", "First-line agent"),
("Amoxicillin/Clavulanate", "500/125 mg PO TID\n(or 875/125 mg BID)", "10 days", "Broader spectrum; covers anaerobes"),
("Metronidazole", "500 mg PO BID", "10 days", "If allergic to penicillin; excellent anaerobic cover"),
("Clindamycin", "300 mg PO QID", "10 days", "Penicillin allergy alternative"),
("Nystatin (if immunocomp.)", "100,000 U/mL; 5 mL swish/spit QID", "10 days", "If immunocompromised or candidal co-infection"),
("Doxycycline (adjunctive)", "100 mg PO OD", "21 days", "Sub-antimicrobial dose (20 mg BID) for MMP inhibition in chronic disease"),
]
row_bg2 = [WHITE, LIGHT_TEAL]
for ri, row in enumerate(ab_rows):
ty = 1.95 + ri * 0.77
bg_c = row_bg2[ri % 2]
for ci, (text, hw, hx) in enumerate(zip(row, hcols, hstarts)):
rect(s, hx, ty, hw, 0.72, bg_c, BORDER_GRAY, 0.3)
col_c = DARK_TEAL if ci == 0 else DARK_TEXT
textbox(s, text, hx+0.06, ty+0.03, hw-0.1, 0.68,
size=12 if ci != 3 else 11, color=col_c,
bold=(ci == 0), valign=MSO_ANCHOR.MIDDLE, wrap=True)
footer(s)
# ═══════════════════════════════════════════════════════════════════
# SLIDE 14 – PREVENTION
# ═══════════════════════════════════════════════════════════════════
s = add_slide()
bg(s, LIGHT_GRAY)
header_bar(s, "Prevention", "Oral Hygiene & Risk Reduction")
rect(s, 0.4, 1.3, 12.5, 5.9, WHITE, BORDER_GRAY, 0.6)
rect(s, 0.4, 1.3, 0.18, 5.9, MID_TEAL)
section_label(s, "PREVENTIVE STRATEGIES", 0.65, 1.35, 12.0, MID_TEAL)
prev_items = [
("Daily Oral Hygiene", "Regular brushing with fluoride toothpaste (twice daily) + daily flossing. "
"Disrupts plaque biofilm before it mineralizes.", MID_TEAL),
("Professional Cleaning", "Periodic dental scaling removes calculus that home hygiene cannot eliminate. "
"Recommended every 6 months (more frequent in high-risk groups).", DARK_TEAL),
("Fluoride", "Fluoride-containing toothpaste + professional treatments where indicated. "
"Incorporated into enamel hydroxyapatite as fluoroapatite (acid-resistant).", MID_TEAL),
("Smoking Cessation", "Most important modifiable risk factor in HIV-negative patients. "
"Smoking impairs gingival vascularity and immune response.", RED_ACCENT),
("Glycemic Control", "Tight control of blood glucose in diabetics reduces severity of periodontal disease. "
"Bidirectional relationship — treat both simultaneously.", ORANGE),
("High-Risk Populations", "Extra surveillance needed for: diabetics, smokers, HIV/immunocompromised, "
"xerostomia, Down syndrome, elderly in care homes, disabled individuals.", DARK_TEAL),
]
for i, (title, body, col) in enumerate(prev_items):
row = i % 3
col_n = i // 3
lx = 0.55 + col_n * 6.35
ty = 1.78 + row * 1.74
rect(s, lx, ty, 5.95, 1.6, LIGHT_GRAY, BORDER_GRAY, 0.5)
rect(s, lx, ty, 0.25, 1.6, col)
textbox(s, title, lx+0.35, ty+0.08, 5.5, 0.42, size=13, bold=True, color=col)
textbox(s, body, lx+0.35, ty+0.52, 5.5, 1.05, size=12, color=DARK_TEXT, wrap=True)
footer(s)
# ═══════════════════════════════════════════════════════════════════
# SLIDE 15 – SUMMARY / TAKE-HOME
# ═══════════════════════════════════════════════════════════════════
s = add_slide()
bg(s, DARK_TEAL)
# decorative circles
shp = s.shapes.add_shape(9, Inches(10.0), Inches(4.5), Inches(5.0), Inches(5.0))
shp.fill.solid(); shp.fill.fore_color.rgb = MID_TEAL
shp.line.fill.background(); shp.shadow.inherit = False
shp2 = s.shapes.add_shape(9, Inches(11.0), Inches(5.2), Inches(3.2), Inches(3.2))
shp2.fill.solid(); shp2.fill.fore_color.rgb = DARK_TEAL
shp2.line.fill.background(); shp2.shadow.inherit = False
rect(s, 0.5, 0.5, 9.5, 6.5, WHITE)
rect(s, 0.5, 0.5, 0.22, 6.5, ORANGE)
textbox(s, "Key Take-Home Points", 0.85, 0.6, 9.0, 0.7,
size=26, bold=True, color=DARK_TEAL)
takeaways = [
"Both gingivitis and periodontitis originate from dental plaque — prevention is the same: good oral hygiene.",
"Gingivitis is REVERSIBLE; periodontitis is IRREVERSIBLE. Early intervention prevents permanent damage.",
"Key pathogens: Porphyromonas gingivalis, Treponema denticola, Aggregatibacter actinomycetemcomitans.",
"Periodontitis is the leading cause of tooth loss in adults over 35.",
"Systemic diseases (diabetes, HIV, cardiovascular disease) have a bidirectional link with periodontal disease.",
"Chlorhexidine 0.12–0.2% rinse + mechanical debridement = cornerstone of non-surgical treatment.",
"Aggressive/necrotizing forms require systemic antibiotics (metronidazole ± amoxicillin).",
"Smoking is the #1 modifiable risk factor — always counsel on cessation.",
]
for i, pt in enumerate(takeaways):
ty = 1.4 + i * 0.68
rect(s, 0.75, ty+0.1, 0.3, 0.3, ORANGE if i % 2 == 0 else MID_TEAL)
textbox(s, pt, 1.15, ty, 8.7, 0.65, size=13, color=DARK_TEXT, wrap=True)
textbox(s, "Sources: Robbins & Kumar Basic Pathology · Sherris & Ryan's Medical Microbiology · "
"Harrison's Principles of Internal Medicine 22E · ROSEN's Emergency Medicine · Scott-Brown's ORL",
0.85, 6.7, 8.8, 0.4, size=9, italic=True, color=RGBColor(0x88, 0x88, 0x99))
# ── Save ──────────────────────────────────────────────────────────
out = "/tmp/workspace/gingivitis-pptx/Gingivitis_and_Periodontitis.pptx"
from io import BytesIO as _BIO
buf = _BIO()
prs.save(buf)
with open(out, "wb") as f:
f.write(buf.getvalue())
print("Saved:", out)
import os
print("Size:", os.path.getsize(out), "bytes")