~/prostho-pdf/generate_pdf.py
#!/usr/bin/env python3
"""
Prosthodontics Revision PDF - Metal-Ceramic & All-Ceramic Crowns
Tooth Preparation Stages with Diagrams
Final Year BDS
"""
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.patches import FancyArrowPatch, Arc, Wedge, FancyBboxPatch
import matplotlib.patheffects as pe
import numpy as np
import os
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import mm, cm
from reportlab.platypus import (SimpleDocTemplate, Paragraph, Spacer, Image,
Table, TableStyle, HRFlowable, KeepTogether,
PageBreak)
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
from reportlab.platypus import BaseDocTemplate, Frame, PageTemplate
OUT_DIR = "/home/daytona/workspace/prostho-pdf"
IMG_DIR = os.path.join(OUT_DIR, "imgs")
os.makedirs(IMG_DIR, exist_ok=True)
# ──────────────────────────────────────────────────────────────────────────────
# COLOUR PALETTE
# ──────────────────────────────────────────────────────────────────────────────
C_ENAMEL = "#F5E6C8" # cream/ivory
C_DENTINE = "#E8C99A" # warm tan
C_PULP = "#F4A0A0" # pink
C_METAL = "#B0B8C8" # steel blue-grey
C_OPAQUE = "#FFFFFF" # white
C_BODY_PRC = "#F0D0B0" # body porcelain
C_ENAMEL_P = "#D4EEF4" # enamel porcelain light blue
C_ZIRCONIA = "#E8E8E8" # light grey
C_LITH_DIS = "#C8DCF0" # light blue
C_PREP = "#D0E8D0" # prepared surface (green tint)
C_REDUCTION= "#FFD0D0" # reduction highlight (red tint)
C_CEMENT = "#FFFACD" # cement layer yellow
C_BG = "#FAFAFA"
def save_fig(fig, name):
path = os.path.join(IMG_DIR, f"{name}.png")
fig.savefig(path, dpi=150, bbox_inches='tight', facecolor=C_BG)
plt.close(fig)
return path
# ──────────────────────────────────────────────────────────────────────────────
# HELPER: DRAW TOOTH OUTLINE (anterior, cross-section style)
# ──────────────────────────────────────────────────────────────────────────────
def draw_tooth_base(ax, x0=0, y0=0, w=2.0, h=4.0, title="", alpha=1.0,
show_layers=True, prepared=False, reduction_map=None):
"""Draw a simplified cross-section of an anterior/premolar tooth."""
# Enamel outer shell
enamel_pts = np.array([
[x0, y0],
[x0 - 0.1, y0 + h*0.6],
[x0 - 0.05,y0 + h],
[x0 + w*0.5, y0 + h + 0.2],
[x0 + w, y0 + h],
[x0 + w + 0.1, y0 + h*0.6],
[x0 + w, y0],
])
enamel_patch = plt.Polygon(enamel_pts, closed=True, facecolor=C_ENAMEL,
edgecolor='#8B6914', linewidth=1.5, alpha=alpha, zorder=2)
ax.add_patch(enamel_patch)
# Dentine inner
shrink = 0.2
dentin_pts = np.array([
[x0 + shrink, y0 + shrink*0.5],
[x0 + shrink - 0.05, y0 + h*0.55],
[x0 + shrink, y0 + h*0.9],
[x0 + w*0.5, y0 + h + 0.05],
[x0 + w - shrink, y0 + h*0.9],
[x0 + w - shrink + 0.05, y0 + h*0.55],
[x0 + w - shrink, y0 + shrink*0.5],
])
dentin_patch = plt.Polygon(dentin_pts, closed=True, facecolor=C_DENTINE,
edgecolor='none', alpha=alpha, zorder=3)
ax.add_patch(dentin_patch)
# Pulp
pulp_cx = x0 + w/2
pulp_cy = y0 + h*0.4
pulp = mpatches.Ellipse((pulp_cx, pulp_cy), width=w*0.28, height=h*0.45,
facecolor=C_PULP, edgecolor='none', alpha=alpha*0.9, zorder=4)
ax.add_patch(pulp)
if title:
ax.text(x0 + w/2, y0 - 0.4, title, ha='center', va='top',
fontsize=8, fontweight='bold', color='#333333')
def draw_prepared_tooth(ax, x0=0, y0=0, w=2.0, h=4.0, title="",
reduction_facial=1.5, reduction_lingual=0.7,
reduction_incisal=2.0, finish_line="shoulder",
crown_type="pfm"):
"""Draw a prepared tooth cross-section with reduction shown."""
shrink_f = reduction_facial / 10.0 # scale for visual
shrink_l = reduction_lingual / 10.0
shrink_i = reduction_incisal / 10.0
# Original enamel outline (ghost)
enamel_pts = np.array([
[x0, y0],
[x0 - 0.1, y0 + h*0.6],
[x0 - 0.05,y0 + h],
[x0 + w*0.5, y0 + h + 0.2],
[x0 + w, y0 + h],
[x0 + w + 0.1, y0 + h*0.6],
[x0 + w, y0],
])
ghost = plt.Polygon(enamel_pts, closed=True, facecolor='none',
edgecolor='#CCCCCC', linewidth=1.0, linestyle='--',
alpha=0.5, zorder=1)
ax.add_patch(ghost)
# Prepared outline
f = 0.25 if crown_type == "pfm" else 0.20
l = 0.12 if crown_type == "pfm" else 0.20
i = 0.30
prep_pts = np.array([
[x0 + f, y0],
[x0 + f - 0.05, y0 + h*0.55],
[x0 + f, y0 + h - i],
[x0 + w*0.5, y0 + h - i + 0.1],
[x0 + w - f, y0 + h - i],
[x0 + w - l + 0.05, y0 + h*0.55],
[x0 + w - l, y0],
])
prep_patch = plt.Polygon(prep_pts, closed=True, facecolor=C_PREP,
edgecolor='#2E7D32', linewidth=2.0, zorder=5)
ax.add_patch(prep_patch)
# Dentine
shrink = 0.18
dentin_pts = np.array([
[x0 + f + shrink, y0 + shrink*0.5],
[x0 + f + shrink - 0.04, y0 + h*0.5],
[x0 + f + shrink, y0 + h*0.85 - i],
[x0 + w*0.5, y0 + h - i - 0.05],
[x0 + w - f - shrink, y0 + h*0.85 - i],
[x0 + w - l - shrink + 0.04, y0 + h*0.5],
[x0 + w - l - shrink, y0 + shrink*0.5],
])
dentin_patch = plt.Polygon(dentin_pts, closed=True, facecolor=C_DENTINE,
edgecolor='none', alpha=0.9, zorder=6)
ax.add_patch(dentin_patch)
# Pulp
pulp = mpatches.Ellipse((x0 + w/2, y0 + h*0.38), width=w*0.28, height=h*0.42,
facecolor=C_PULP, edgecolor='none', alpha=0.9, zorder=7)
ax.add_patch(pulp)
# Finish line indicator
fl_col = '#E53935' if finish_line == "shoulder" else '#1565C0'
ax.plot([x0 + f - 0.05, x0 + f + 0.15], [y0, y0],
color=fl_col, linewidth=3, zorder=8)
ax.plot([x0 + w - l - 0.05, x0 + w - l + 0.15], [y0, y0],
color='#1565C0', linewidth=3, zorder=8)
if title:
ax.text(x0 + w/2, y0 - 0.4, title, ha='center', va='top',
fontsize=8, fontweight='bold', color='#1a1a1a')
# ──────────────────────────────────────────────────────────────────────────────
# DIAGRAM 1 – TOOTH PREPARATION STAGES (PFM)
# ──────────────────────────────────────────────────────────────────────────────
def fig_pfm_prep_stages():
fig, axes = plt.subplots(1, 5, figsize=(16, 6))
fig.patch.set_facecolor(C_BG)
fig.suptitle("PFM Crown – Tooth Preparation Stages", fontsize=14,
fontweight='bold', color='#1a237e', y=1.01)
stages = [
("1. Unprepared\nTooth", False, {}),
("2. Incisal/Occlusal\nReduction\n(1.5-2mm)", True, {"incisal": True}),
("3. Facial\nReduction\n(1.2-1.5mm)", True, {"incisal": True, "facial": True}),
("4. Lingual\nReduction\n(0.5-0.7mm)", True, {"incisal": True, "facial": True, "lingual": True}),
("5. Finish Line\n& Final Form\n(Shoulder / Chamfer)", True, {"incisal": True, "facial": True, "lingual": True, "finish": True}),
]
for i, (title, prepped, opts) in enumerate(stages):
ax = axes[i]
ax.set_facecolor(C_BG)
ax.set_xlim(-0.5, 3.5)
ax.set_ylim(-1.0, 6.5)
ax.set_aspect('equal')
ax.axis('off')
x0, y0, w, h = 0.5, 0.5, 2.0, 4.5
if not prepped:
# Full intact tooth
enamel_pts = np.array([
[x0, y0],
[x0 - 0.1, y0 + h*0.6],
[x0 - 0.05,y0 + h],
[x0 + w*0.5, y0 + h + 0.25],
[x0 + w, y0 + h],
[x0 + w + 0.1, y0 + h*0.6],
[x0 + w, y0],
])
ax.add_patch(plt.Polygon(enamel_pts, closed=True, facecolor=C_ENAMEL,
edgecolor='#8B6914', linewidth=2, zorder=2))
shrink = 0.22
dentin_pts = np.array([
[x0 + shrink, y0 + shrink*0.5],
[x0 + shrink - 0.05, y0 + h*0.55],
[x0 + shrink, y0 + h*0.88],
[x0 + w*0.5, y0 + h + 0.1],
[x0 + w - shrink, y0 + h*0.88],
[x0 + w - shrink + 0.05, y0 + h*0.55],
[x0 + w - shrink, y0 + shrink*0.5],
])
ax.add_patch(plt.Polygon(dentin_pts, closed=True, facecolor=C_DENTINE,
edgecolor='none', zorder=3))
ax.add_patch(mpatches.Ellipse((x0 + w/2, y0 + h*0.4),
width=w*0.28, height=h*0.44,
facecolor=C_PULP, edgecolor='none', zorder=4))
else:
fi = 0.30 if opts.get("facial") else 0.02
li = 0.15 if opts.get("lingual") else 0.02
inc = 0.55 if opts.get("incisal") else 0.02
# ghost
enamel_pts = np.array([
[x0, y0],
[x0 - 0.1, y0 + h*0.6],
[x0 - 0.05,y0 + h],
[x0 + w*0.5, y0 + h + 0.25],
[x0 + w, y0 + h],
[x0 + w + 0.1, y0 + h*0.6],
[x0 + w, y0],
])
ax.add_patch(plt.Polygon(enamel_pts, closed=True, facecolor='none',
edgecolor='#BBBBBB', linewidth=1, linestyle='--',
alpha=0.6, zorder=1))
# prepared outline
prep_pts = np.array([
[x0 + fi, y0],
[x0 + fi - 0.05, y0 + h*0.58],
[x0 + fi, y0 + h - inc],
[x0 + w*0.5, y0 + h - inc + 0.12],
[x0 + w - li, y0 + h - inc],
[x0 + w - li + 0.05, y0 + h*0.58],
[x0 + w - li, y0],
])
fc = C_PREP if opts.get("finish") else C_ENAMEL
ax.add_patch(plt.Polygon(prep_pts, closed=True, facecolor=fc,
edgecolor='#2E7D32', linewidth=2.5, zorder=5))
shrink = 0.20
dentin_pts = np.array([
[x0 + fi + shrink, y0 + shrink*0.5],
[x0 + fi + shrink - 0.04, y0 + h*0.52],
[x0 + fi + shrink, y0 + h*0.82 - inc],
[x0 + w*0.5, y0 + h - inc - 0.1],
[x0 + w - li - shrink, y0 + h*0.82 - inc],
[x0 + w - li - shrink + 0.04, y0 + h*0.52],
[x0 + w - li - shrink, y0 + shrink*0.5],
])
ax.add_patch(plt.Polygon(dentin_pts, closed=True, facecolor=C_DENTINE,
edgecolor='none', alpha=0.9, zorder=6))
ax.add_patch(mpatches.Ellipse((x0 + w/2, y0 + h*0.38),
width=w*0.28, height=h*0.4,
facecolor=C_PULP, edgecolor='none', alpha=0.9, zorder=7))
# Reduction arrows & annotations
if opts.get("facial"):
ax.annotate('', xy=(x0 + fi - 0.02, y0 + h*0.65),
xytext=(x0 - 0.05, y0 + h*0.65),
arrowprops=dict(arrowstyle='<->', color='#C62828', lw=1.5))
ax.text(x0 + fi/2 - 0.1, y0 + h*0.7, '1.2-\n1.5mm',
ha='center', va='bottom', fontsize=6, color='#C62828', fontweight='bold')
if opts.get("lingual"):
ax.annotate('', xy=(x0 + w - li + 0.02, y0 + h*0.65),
xytext=(x0 + w + 0.05, y0 + h*0.65),
arrowprops=dict(arrowstyle='<->', color='#1565C0', lw=1.5))
ax.text(x0 + w - li/2 + 0.1, y0 + h*0.7, '0.5-\n0.7mm',
ha='center', va='bottom', fontsize=6, color='#1565C0', fontweight='bold')
if opts.get("incisal"):
ax.annotate('', xy=(x0 + w*0.5, y0 + h - inc + 0.12),
xytext=(x0 + w*0.5, y0 + h + 0.2),
arrowprops=dict(arrowstyle='<->', color='#6A1B9A', lw=1.5))
ax.text(x0 + w*0.5 + 0.6, y0 + h - inc/2 + 0.3,
'1.5-2mm', ha='left', va='center', fontsize=6, color='#6A1B9A',
fontweight='bold')
if opts.get("finish"):
ax.plot([x0 + fi - 0.1, x0 + fi + 0.3], [y0, y0],
color='#E53935', linewidth=4, zorder=9, solid_capstyle='round')
ax.text(x0 + fi - 0.15, y0 - 0.3, 'Shoulder\n(facial)',
ha='center', fontsize=6, color='#E53935', fontweight='bold')
ax.plot([x0 + w - li - 0.1, x0 + w - li + 0.3], [y0, y0],
color='#1565C0', linewidth=4, zorder=9, solid_capstyle='round')
ax.text(x0 + w - li + 0.4, y0 - 0.3, 'Chamfer\n(lingual)',
ha='center', fontsize=6, color='#1565C0', fontweight='bold')
# Stage title
ax.text(x0 + w/2, y0 - 0.5, title, ha='center', va='top',
fontsize=8, fontweight='bold', color='#1a237e',
multialignment='center')
# Arrow between stages
if i < 4:
ax.annotate('', xy=(3.2, y0 + h/2), xytext=(3.0, y0 + h/2),
arrowprops=dict(arrowstyle='->', color='#555555', lw=2))
plt.tight_layout(pad=1.5)
return save_fig(fig, "pfm_prep_stages")
# ──────────────────────────────────────────────────────────────────────────────
# DIAGRAM 2 – FINISH LINE TYPES
# ──────────────────────────────────────────────────────────────────────────────
def fig_finish_lines():
fig, axes = plt.subplots(1, 5, figsize=(15, 5))
fig.patch.set_facecolor(C_BG)
fig.suptitle("Finish Line (Cervical Margin) Types", fontsize=13,
fontweight='bold', color='#1a237e', y=1.03)
types = [
("Knife Edge", [(0, 2), (0.5, 1.5), (2, 0)], "RPD / full metal\ncrowns"),
("Chamfer", [(0, 2), (0.4, 1.6), (0.7, 0.5), (2, 0)], "PFM lingual\nAll-ceramic"),
("Shoulder\n(Butt Joint)", [(0, 2), (0.5, 1.5), (0.5, 0.3), (2, 0.3), (2, 0)], "PFM facial\nAll-ceramic"),
("Bevel", [(0, 2), (0.5, 1.5), (0.8, 0.7), (1.3, 0), (2, 0)], "Full metal\ncrowns"),
("Shoulder\nwith Bevel", [(0, 2), (0.5, 1.5), (0.5, 0.5), (0.9, 0.1), (2, 0.1), (2, 0)], "Metal ceramic\nfacial margin"),
]
for i, (name, pts, use) in enumerate(types):
ax = axes[i]
ax.set_facecolor(C_BG)
ax.set_xlim(-0.3, 2.5)
ax.set_ylim(-0.8, 2.8)
ax.set_aspect('equal')
ax.axis('off')
# Tooth body
tooth_x = [p[0] for p in pts]
tooth_y = [p[1] for p in pts]
tooth_x_full = tooth_x + [2.0, 2.0, 0.0]
tooth_y_full = tooth_y + [0.0, -0.6, -0.6]
ax.fill(tooth_x_full, tooth_y_full, color=C_ENAMEL, zorder=2)
ax.plot(tooth_x, tooth_y, color='#8B6914', linewidth=2.5, zorder=3)
# Highlight finish line zone
ax.plot(tooth_x[-3:], tooth_y[-3:], color='#E53935', linewidth=3.5,
zorder=4, solid_capstyle='round')
# Gingival line
ax.axhline(y=0.4, color='#E91E63', linewidth=1.5, linestyle=':', alpha=0.7, zorder=1)
ax.text(2.1, 0.4, 'GML', fontsize=6, color='#E91E63', va='center')
ax.text(1.0, 2.5, name, ha='center', va='top', fontsize=8,
fontweight='bold', color='#1a237e', multialignment='center')
ax.text(1.0, -0.5, use, ha='center', va='top', fontsize=6.5,
color='#444444', multialignment='center',
style='italic')
plt.tight_layout(pad=1.0)
return save_fig(fig, "finish_lines")
# ──────────────────────────────────────────────────────────────────────────────
# DIAGRAM 3 – PFM CROWN LAYERS
# ──────────────────────────────────────────────────────────────────────────────
def fig_pfm_layers():
fig, ax = plt.subplots(figsize=(12, 6))
fig.patch.set_facecolor(C_BG)
ax.set_facecolor(C_BG)
ax.set_xlim(0, 12)
ax.set_ylim(0, 7)
ax.set_aspect('equal')
ax.axis('off')
ax.set_title("PFM Crown – Layer Structure (Cross-Section)", fontsize=13,
fontweight='bold', color='#1a237e', pad=12)
# Draw from inside out (left = tooth structure)
layers = [
# (x_start, width, color, label, note)
(0.3, 1.4, C_DENTINE, "Tooth Dentine", ""),
(1.7, 0.1, C_CEMENT, "Cement Layer\n~25 µm", ""),
(1.8, 0.7, C_METAL, "Metal Coping\n0.3-0.5mm", "Provides strength"),
(2.5, 0.3, C_OPAQUE, "Opaque\n0.2-0.3mm", "Masks metal\nchemical bond"),
(2.8, 1.0, C_BODY_PRC, "Body Porcelain\n(Dentine)\n0.7-1.0mm", "Provides shade"),
(3.8, 0.6, C_ENAMEL_P, "Enamel/Incisal\nPorcelain\n0.3-0.5mm", "Translucency"),
(4.4, 0.1, '#E8E0D0', "Glaze", "Surface finish"),
]
y_base = 1.0
bar_h = 4.0
for (x, w, col, lbl, note) in layers:
rect = mpatches.FancyBboxPatch((x, y_base), w, bar_h,
boxstyle="round,pad=0.02",
facecolor=col, edgecolor='#666666',
linewidth=1.2, zorder=3)
ax.add_patch(rect)
# Label above
ax.text(x + w/2, y_base + bar_h + 0.2, lbl, ha='center', va='bottom',
fontsize=7.5, fontweight='bold', color='#222222',
multialignment='center')
# Note below
if note:
ax.text(x + w/2, y_base - 0.15, note, ha='center', va='top',
fontsize=6, color='#555555', multialignment='center',
style='italic')
# Thickness callouts with braces
ax.annotate('', xy=(1.8, 0.2), xytext=(4.5, 0.2),
arrowprops=dict(arrowstyle='<->', color='#C62828', lw=1.5))
ax.text(3.15, 0.05, 'Total ceramic build-up: 1.2-2.0 mm (facial)',
ha='center', fontsize=7, color='#C62828', fontweight='bold')
# Legend on right
legend_x = 6.0
items = [
(C_DENTINE, "Tooth structure"),
(C_CEMENT, "Luting cement"),
(C_METAL, "Metal coping (Ni-Cr / Au-Pt)"),
(C_OPAQUE, "Opaque porcelain"),
(C_BODY_PRC, "Body (dentine) porcelain"),
(C_ENAMEL_P, "Enamel/incisal porcelain"),
('#E8E0D0', "Glaze layer"),
]
ax.text(legend_x, 6.5, "LEGEND", fontsize=10, fontweight='bold', color='#1a237e')
for j, (col, txt) in enumerate(items):
yy = 5.8 - j * 0.7
ax.add_patch(mpatches.Rectangle((legend_x, yy), 0.5, 0.45, facecolor=col,
edgecolor='#666666', linewidth=0.8, zorder=3))
ax.text(legend_x + 0.7, yy + 0.22, txt, va='center', fontsize=8, color='#333333')
# CTE note
ax.text(6.0, 0.5,
"CTE Rule: Metal (14-15 × 10⁻⁶/°C) > Porcelain (13-14 × 10⁻⁶/°C)\n"
"→ Porcelain under slight COMPRESSION → prevents crack propagation",
fontsize=8, color='#1B5E20', fontweight='bold',
bbox=dict(boxstyle='round,pad=0.4', facecolor='#E8F5E9', edgecolor='#2E7D32'))
plt.tight_layout()
return save_fig(fig, "pfm_layers")
# ──────────────────────────────────────────────────────────────────────────────
# DIAGRAM 4 – ALL-CERAMIC TYPES COMPARISON
# ──────────────────────────────────────────────────────────────────────────────
def fig_all_ceramic_types():
fig, ax = plt.subplots(figsize=(13, 6))
fig.patch.set_facecolor(C_BG)
ax.set_facecolor(C_BG)
ax.axis('off')
ax.set_title("All-Ceramic Systems – Classification & Properties", fontsize=13,
fontweight='bold', color='#1a237e', pad=10)
# Table data
headers = ["System", "Type", "Trade Name", "Strength\n(MPa)", "Aesthetics", "Main Use"]
rows = [
["Feldspathic", "Glass", "Various", "60-70", "★★★★★\n(Best)", "Veneers, Anterior"],
["Leucite-reinforced", "Glass-ceramic", "IPS Empress I", "120-160", "★★★★☆", "Anterior crowns\ninlays/onlays"],
["Lithium Disilicate", "Glass-ceramic", "IPS e.max", "360-500", "★★★★☆\n(High translucency)", "Ant & post crowns\n3-unit FPD to PM"],
["Alumina\n(In-Ceram)", "Polycrystalline", "In-Ceram Al", "400-600", "★★★☆☆", "Anterior crowns\n(less used now)"],
["Zirconia (Y-TZP)", "Polycrystalline", "Lava / Cercon", "900-1200", "★★☆☆☆\n(Opaque)", "Posterior crowns\nFPD retainers\nImplant crowns"],
]
col_widths = [1.8, 1.5, 1.7, 1.2, 1.8, 2.2]
x_starts = [0.2]
for cw in col_widths[:-1]:
x_starts.append(x_starts[-1] + cw)
row_h = 0.85
header_y = 5.5
# Header row
for j, (hdr, xs, cw) in enumerate(zip(headers, x_starts, col_widths)):
rect = mpatches.FancyBboxPatch((xs, header_y), cw - 0.05, row_h*0.85,
boxstyle="round,pad=0.03",
facecolor='#1a237e', edgecolor='none', zorder=2)
ax.add_patch(rect)
ax.text(xs + cw/2 - 0.025, header_y + row_h*0.42, hdr,
ha='center', va='center', fontsize=8.5, fontweight='bold',
color='white', multialignment='center')
row_colors = ['#E3F2FD', '#E8F5E9', '#FFF9C4', '#F3E5F5', '#FFEBEE']
for i, row in enumerate(rows):
ry = header_y - (i + 1) * row_h
for j, (cell, xs, cw) in enumerate(zip(row, x_starts, col_widths)):
rect = mpatches.FancyBboxPatch((xs, ry), cw - 0.05, row_h*0.88,
boxstyle="round,pad=0.03",
facecolor=row_colors[i],
edgecolor='#AAAAAA', linewidth=0.6, zorder=2)
ax.add_patch(rect)
ax.text(xs + cw/2 - 0.025, ry + row_h*0.44, cell,
ha='center', va='center', fontsize=7.5, color='#1a1a1a',
multialignment='center')
ax.set_xlim(0, 11)
ax.set_ylim(0, 7)
# Strength scale bar at bottom
ax.text(0.2, 0.6, "STRENGTH SCALE:", fontsize=8, fontweight='bold', color='#333')
strength_vals = [70, 150, 450, 550, 1100]
labels_s = ["Feldspathic\n70", "Leucite\n150", "Li-Disilicate\n450", "Alumina\n550", "Zirconia\n1100"]
max_s = 1200
bar_y = 0.1
for k, (sv, sl) in enumerate(zip(strength_vals, labels_s)):
bw = sv / max_s * 1.6
bx = 1.8 + k * 1.8
gradient_col = plt.cm.RdYlGn(sv / max_s)
rect = mpatches.Rectangle((bx, bar_y), bw, 0.35,
facecolor=gradient_col, edgecolor='#555', linewidth=0.8)
ax.add_patch(rect)
ax.text(bx + bw/2, bar_y + 0.5, sl, ha='center', va='bottom',
fontsize=6.5, color='#222', multialignment='center')
ax.text(10.5, 0.4, "MPa", fontsize=8, color='#333', va='center')
plt.tight_layout()
return save_fig(fig, "all_ceramic_types")
# ──────────────────────────────────────────────────────────────────────────────
# DIAGRAM 5 – ALL-CERAMIC TOOTH PREP vs PFM (cross-section comparison)
# ──────────────────────────────────────────────────────────────────────────────
def fig_prep_comparison():
fig, axes = plt.subplots(1, 3, figsize=(14, 7))
fig.patch.set_facecolor(C_BG)
fig.suptitle("Tooth Preparation Cross-Section Comparison", fontsize=13,
fontweight='bold', color='#1a237e', y=1.02)
tooth_configs = [
{
"title": "UNPREPARED TOOTH",
"subtitle": "(Reference)",
"fi": 0.0, "li": 0.0, "inc": 0.0,
"fc": C_ENAMEL,
"annotations": []
},
{
"title": "PFM PREPARATION",
"subtitle": "Shoulder (facial) + Chamfer (lingual)",
"fi": 0.32, "li": 0.14, "inc": 0.55,
"fc": C_PREP,
"annotations": [
("facial", "1.2-1.5mm\n(Shoulder)", "#E53935"),
("lingual", "0.5-0.7mm\n(Chamfer)", "#1565C0"),
("incisal", "1.5-2.0mm", "#6A1B9A"),
]
},
{
"title": "ALL-CERAMIC PREPARATION",
"subtitle": "Deep Chamfer / Shoulder (uniform)",
"fi": 0.28, "li": 0.26, "inc": 0.55,
"fc": C_LITH_DIS,
"annotations": [
("facial", "1.0-1.5mm\n(Shoulder)", "#E53935"),
("lingual", "1.0-1.5mm\n(Deep Chamfer)", "#1565C0"),
("incisal", "1.5-2.0mm", "#6A1B9A"),
]
},
]
for idx, cfg in enumerate(tooth_configs):
ax = axes[idx]
ax.set_facecolor(C_BG)
ax.set_xlim(-0.5, 4.0)
ax.set_ylim(-2.5, 7.5)
ax.set_aspect('equal')
ax.axis('off')
x0, y0, w, h = 0.5, 0.5, 2.5, 5.5
fi, li, inc = cfg["fi"], cfg["li"], cfg["inc"]
# Ghost original
if idx > 0:
orig_pts = np.array([
[x0, y0], [x0 - 0.12, y0 + h*0.6], [x0 - 0.06, y0 + h],
[x0 + w*0.5, y0 + h + 0.3],
[x0 + w, y0 + h], [x0 + w + 0.12, y0 + h*0.6], [x0 + w, y0],
])
ax.add_patch(plt.Polygon(orig_pts, closed=True, facecolor='none',
edgecolor='#CCCCCC', linewidth=1,
linestyle='--', alpha=0.5, zorder=1))
# Prep outline
prep_pts = np.array([
[x0 + fi, y0],
[x0 + fi - 0.06, y0 + h*0.58],
[x0 + fi, y0 + h - inc],
[x0 + w*0.5, y0 + h - inc + 0.15],
[x0 + w - li, y0 + h - inc],
[x0 + w - li + 0.06, y0 + h*0.58],
[x0 + w - li, y0],
])
ax.add_patch(plt.Polygon(prep_pts, closed=True, facecolor=cfg["fc"],
edgecolor='#2E7D32', linewidth=2.5, zorder=5))
# Dentine
s = 0.22
d_pts = np.array([
[x0 + fi + s, y0 + s*0.5],
[x0 + fi + s - 0.05, y0 + h*0.52],
[x0 + fi + s, y0 + h*0.83 - inc],
[x0 + w*0.5, y0 + h - inc - 0.12],
[x0 + w - li - s, y0 + h*0.83 - inc],
[x0 + w - li - s + 0.05, y0 + h*0.52],
[x0 + w - li - s, y0 + s*0.5],
])
ax.add_patch(plt.Polygon(d_pts, closed=True, facecolor=C_DENTINE,
edgecolor='none', alpha=0.9, zorder=6))
# Pulp
ax.add_patch(mpatches.Ellipse((x0 + w/2, y0 + h*0.4),
width=w*0.30, height=h*0.44,
facecolor=C_PULP, edgecolor='none',
alpha=0.9, zorder=7))
# CEJ / gingival line
ax.axhline(y=y0 + 0.15, xmin=0.05, xmax=0.95, color='#E91E63',
linewidth=1.5, linestyle=':', alpha=0.7, zorder=8)
ax.text(3.5, y0 + 0.2, 'CEJ', fontsize=7, color='#E91E63', va='center')
# Annotations
for ann_type, label, col in cfg.get("annotations", []):
if ann_type == "facial":
ax.annotate('', xy=(x0 + fi, y0 + h*0.68),
xytext=(x0, y0 + h*0.68),
arrowprops=dict(arrowstyle='<->', color=col, lw=1.8))
ax.text(x0 + fi/2, y0 + h*0.74, label, ha='center', va='bottom',
fontsize=7, color=col, fontweight='bold', multialignment='center')
elif ann_type == "lingual":
ax.annotate('', xy=(x0 + w - li, y0 + h*0.68),
xytext=(x0 + w, y0 + h*0.68),
arrowprops=dict(arrowstyle='<->', color=col, lw=1.8))
ax.text(x0 + w - li/2, y0 + h*0.74, label, ha='center', va='bottom',
fontsize=7, color=col, fontweight='bold', multialignment='center')
elif ann_type == "incisal":
ax.annotate('', xy=(x0 + w*0.5, y0 + h - inc + 0.15),
xytext=(x0 + w*0.5, y0 + h + 0.25),
arrowprops=dict(arrowstyle='<->', color=col, lw=1.8))
ax.text(x0 + w*0.5 + 0.7, y0 + h - inc*0.5, label,
ha='left', va='center', fontsize=7, color=col,
fontweight='bold', multialignment='center')
# Finish line markers
if idx == 1: # PFM
ax.plot([x0 + fi - 0.1, x0 + fi + 0.35], [y0, y0],
color='#E53935', linewidth=5, zorder=9, solid_capstyle='round')
ax.text(x0 + fi - 0.2, y0 - 0.35, 'Shoulder', ha='center',
fontsize=7, color='#E53935', fontweight='bold')
ax.plot([x0 + w - li - 0.1, x0 + w - li + 0.35], [y0, y0],
color='#1565C0', linewidth=5, zorder=9, solid_capstyle='round')
ax.text(x0 + w - li + 0.3, y0 - 0.35, 'Chamfer', ha='center',
fontsize=7, color='#1565C0', fontweight='bold')
elif idx == 2: # All-ceramic
ax.plot([x0 + fi - 0.1, x0 + fi + 0.35], [y0, y0],
color='#E53935', linewidth=5, zorder=9, solid_capstyle='round')
ax.text(x0 + fi - 0.2, y0 - 0.35, 'Shoulder', ha='center',
fontsize=7, color='#E53935', fontweight='bold')
ax.plot([x0 + w - li - 0.1, x0 + w - li + 0.35], [y0, y0],
color='#1565C0', linewidth=5, zorder=9, solid_capstyle='round')
ax.text(x0 + w - li + 0.15, y0 - 0.35, 'Deep\nChamfer', ha='center',
fontsize=7, color='#1565C0', fontweight='bold')
# Title
ax.text(x0 + w/2, y0 - 1.0, cfg["title"], ha='center', va='top',
fontsize=9, fontweight='bold', color='#1a237e')
ax.text(x0 + w/2, y0 - 1.55, cfg["subtitle"], ha='center', va='top',
fontsize=7.5, color='#555555', style='italic')
# Key difference box
if idx == 2:
ax.text(x0 + w/2, y0 - 2.0,
"KEY: Uniform reduction all round\n(No differential facial/lingual)",
ha='center', va='top', fontsize=7, color='#1B5E20',
fontweight='bold', multialignment='center',
bbox=dict(boxstyle='round', facecolor='#E8F5E9', edgecolor='#2E7D32'))
plt.tight_layout(pad=2.0)
return save_fig(fig, "prep_comparison")
# ──────────────────────────────────────────────────────────────────────────────
# DIAGRAM 6 – CEMENTATION PROTOCOL (All-Ceramic)
# ──────────────────────────────────────────────────────────────────────────────
def fig_cementation():
fig, ax = plt.subplots(figsize=(13, 5.5))
fig.patch.set_facecolor(C_BG)
ax.set_facecolor(C_BG)
ax.axis('off')
ax.set_title("Cementation Protocol for All-Ceramic (Glass-Based) Crowns", fontsize=12,
fontweight='bold', color='#1a237e', pad=10)
steps = [
("1", "HF Acid Etch\nIntaglio Surface",
"Feldspathic: 60s\nLeusite: 20-60s\nLi-Disilicate: 20s",
"#FFCDD2", "#C62828"),
("2", "Rinse &\nDry",
"Rinse ≥60s\nAir-dry thoroughly\n(no desiccation)",
"#F3E5F5", "#6A1B9A"),
("3", "Silane\nCoupling Agent",
"Apply silane primer\nWait 60s\nAir-dry gently",
"#E3F2FD", "#1565C0"),
("4", "Adhesive on\nTooth",
"Etch tooth: 37% H₃PO₄\n15-30s enamel / 15s dentin\nApply bonding resin",
"#E8F5E9", "#2E7D32"),
("5", "Apply Resin\nCement",
"Mix resin cement\nLoad into crown\nSeat under pressure",
"#FFF9C4", "#F57F17"),
("6", "Light-Cure\n& Clean",
"Tack-cure 2s each side\nRemove gross excess\nFull-cure 40s / surface",
"#F1F8E9", "#33691E"),
]
box_w = 1.8
box_h = 3.8
gap = 0.25
total_w = len(steps) * (box_w + gap)
x_off = (13 - total_w) / 2
for i, (num, title, body, bgcol, bordcol) in enumerate(steps):
bx = x_off + i * (box_w + gap)
by = 0.5
rect = mpatches.FancyBboxPatch((bx, by), box_w, box_h,
boxstyle="round,pad=0.08",
facecolor=bgcol, edgecolor=bordcol,
linewidth=2, zorder=2)
ax.add_patch(rect)
# Step number circle
circ = plt.Circle((bx + box_w/2, by + box_h - 0.45), 0.32,
facecolor=bordcol, edgecolor='none', zorder=3)
ax.add_patch(circ)
ax.text(bx + box_w/2, by + box_h - 0.45, num,
ha='center', va='center', fontsize=9, fontweight='bold',
color='white', zorder=4)
ax.text(bx + box_w/2, by + box_h - 1.05, title,
ha='center', va='center', fontsize=8.5, fontweight='bold',
color=bordcol, multialignment='center')
ax.text(bx + box_w/2, by + box_h*0.32, body,
ha='center', va='center', fontsize=7.5, color='#333333',
multialignment='center')
# Arrow to next step
if i < len(steps) - 1:
ax_x = bx + box_w + 0.02
ax.annotate('', xy=(ax_x + gap - 0.02, by + box_h/2),
xytext=(ax_x, by + box_h/2),
arrowprops=dict(arrowstyle='->', color='#555555', lw=2.0))
# Note at bottom
ax.text(6.5, 0.2,
"⚠ HF acid does NOT work on Zirconia/Alumina. For Zirconia: Sandblast (Al₂O₃) + MDP-based primer (Panavia / Rely X)",
ha='center', fontsize=8, color='#B71C1C', fontweight='bold',
bbox=dict(boxstyle='round', facecolor='#FFEBEE', edgecolor='#C62828'))
ax.set_xlim(0, 13)
ax.set_ylim(0, 5.5)
plt.tight_layout()
return save_fig(fig, "cementation")
# ──────────────────────────────────────────────────────────────────────────────
# DIAGRAM 7 – TAPER & RETENTION DIAGRAM
# ──────────────────────────────────────────────────────────────────────────────
def fig_taper():
fig, axes = plt.subplots(1, 3, figsize=(12, 6))
fig.patch.set_facecolor(C_BG)
fig.suptitle("Axial Wall Taper & Retention Form", fontsize=13,
fontweight='bold', color='#1a237e', y=1.02)
configs = [
("Under-tapered\n< 2° total", 0.5, "#FFCDD2", "Difficulty in seating\nBinding / lock"),
("Ideal Taper\n2-6° total", 3.0, "#C8E6C9", "Best retention\n& seating"),
("Over-tapered\n> 12° total", 12.0, "#FFECB3", "Poor retention\nEasily displaced"),
]
for i, (title, taper_deg, col, note) in enumerate(configs):
ax = axes[i]
ax.set_facecolor(C_BG)
ax.set_xlim(-2.5, 2.5)
ax.set_ylim(-1.0, 6.5)
ax.set_aspect('equal')
ax.axis('off')
half_taper_rad = np.deg2rad(taper_deg / 2)
h_tooth = 5.0
half_base = 1.0
half_top = half_base + h_tooth * np.tan(half_taper_rad)
left_x = [-half_base, -half_top]
right_x = [ half_base, half_top]
y_pts = [0.5, 0.5 + h_tooth]
# Prepared tooth trapezoid
pts = np.array([
[-half_base, 0.5],
[ half_base, 0.5],
[ half_top, 0.5 + h_tooth],
[-half_top, 0.5 + h_tooth],
])
ax.add_patch(plt.Polygon(pts, closed=True, facecolor=col,
edgecolor='#555555', linewidth=2, zorder=2))
# Central axis line
ax.axvline(x=0, ymin=0, ymax=0.95, color='#AAAAAA',
linewidth=1, linestyle=':', alpha=0.6, zorder=1)
# Taper angle annotation
ax.annotate('', xy=(-half_top * 0.7, 0.5 + h_tooth * 0.55),
xytext=(0, 0.5 + h_tooth * 0.55),
arrowprops=dict(arrowstyle='->', color='#E53935', lw=1.5))
ax.text(-half_top * 0.35, 0.5 + h_tooth * 0.62,
f'{taper_deg/2:.1f}°', ha='center', fontsize=8,
color='#E53935', fontweight='bold')
ax.text(0, 0.5 + h_tooth + 0.3, title, ha='center', va='bottom',
fontsize=8.5, fontweight='bold', color='#1a237e',
multialignment='center')
ax.text(0, 0.1, note, ha='center', va='top', fontsize=7.5,
color='#444444', multialignment='center', style='italic')
plt.tight_layout(pad=1.5)
return save_fig(fig, "taper")
# ──────────────────────────────────────────────────────────────────────────────
# DIAGRAM 8 – PORCELAIN FRACTURE CAUSES (mind map style)
# ──────────────────────────────────────────────────────────────────────────────
def fig_porcelain_fracture():
fig, ax = plt.subplots(figsize=(12, 8))
fig.patch.set_facecolor(C_BG)
ax.set_facecolor(C_BG)
ax.axis('off')
ax.set_title("Porcelain Fracture in PFM – Causes & Management", fontsize=13,
fontweight='bold', color='#1a237e', pad=12)
cx, cy = 6, 4.5
# Central node
ax.add_patch(mpatches.Ellipse((cx, cy), 2.6, 1.1, facecolor='#1a237e',
edgecolor='none', zorder=3))
ax.text(cx, cy, "PORCELAIN\nFRACTURE", ha='center', va='center',
fontsize=11, fontweight='bold', color='white', zorder=4)
causes = [
(2.0, 7.2, "Inadequate\ntooth reduction", "#FFCDD2", "#C62828"),
(4.5, 7.5, "CTE mismatch\n(metal > ceramic too much)", "#FFE0B2", "#E65100"),
(7.5, 7.5, "Unsupported porcelain\n(no metal backing)", "#FFF9C4", "#F57F17"),
(9.5, 6.5, "Sharp internal angles\nin metal coping", "#E1F5FE", "#0277BD"),
(10.2, 4.0, "Parafunctions\n(bruxism/clenching)", "#F3E5F5", "#6A1B9A"),
(9.5, 2.2, "Porcelain fired\ntoo many times", "#E8F5E9", "#2E7D32"),
(7.5, 1.2, "Firing defects\n(voids/bubbles)", "#FCE4EC", "#AD1457"),
(4.5, 1.0, "Occlusal overload\n(premature contact)", "#E3F2FD", "#1565C0"),
(2.0, 2.0, "Operator error\n(thin areas/uneven)", "#FFF3E0", "#BF360C"),
]
for (nx, ny, label, bgc, bdc) in causes:
ax.add_patch(mpatches.FancyBboxPatch((nx - 1.1, ny - 0.45), 2.2, 0.9,
boxstyle="round,pad=0.08",
facecolor=bgc, edgecolor=bdc,
linewidth=1.5, zorder=3))
ax.text(nx, ny, label, ha='center', va='center',
fontsize=7.5, color='#1a1a1a', multialignment='center', zorder=4)
ax.annotate('', xy=(cx + (nx - cx) * 0.38, cy + (ny - cy) * 0.38),
xytext=(nx + (cx - nx) * 0.35, ny + (cy - ny) * 0.35),
arrowprops=dict(arrowstyle='-', color='#888888', lw=1.2), zorder=2)
# Management box
mgmt_x, mgmt_y = 0.3, 0.2
ax.add_patch(mpatches.FancyBboxPatch((mgmt_x, mgmt_y), 5.2, 1.2,
boxstyle="round,pad=0.1",
facecolor='#E8F5E9', edgecolor='#2E7D32',
linewidth=1.5, zorder=3))
ax.text(mgmt_x + 2.6, mgmt_y + 0.6,
"MANAGEMENT: Small chip → Polish / Composite repair + silane\n"
"Large fracture → Remake restoration",
ha='center', va='center', fontsize=8, color='#1B5E20', fontweight='bold')
ax.set_xlim(0, 12)
ax.set_ylim(0, 9)
plt.tight_layout()
return save_fig(fig, "porcelain_fracture")
# ──────────────────────────────────────────────────────────────────────────────
# BUILD THE PDF
# ──────────────────────────────────────────────────────────────────────────────
def build_pdf(img_paths):
pdf_path = os.path.join(OUT_DIR, "Prosthodontics_Crown_Revision.pdf")
doc = SimpleDocTemplate(
pdf_path,
pagesize=A4,
leftMargin=15*mm, rightMargin=15*mm,
topMargin=15*mm, bottomMargin=15*mm,
)
styles = getSampleStyleSheet()
# Custom styles
style_title = ParagraphStyle('MainTitle',
parent=styles['Title'],
fontSize=22, textColor=colors.HexColor('#1a237e'),
spaceAfter=4, alignment=TA_CENTER, fontName='Helvetica-Bold')
style_subtitle = ParagraphStyle('SubTitle',
parent=styles['Normal'],
fontSize=12, textColor=colors.HexColor('#37474f'),
spaceAfter=6, alignment=TA_CENTER, fontName='Helvetica')
style_h1 = ParagraphStyle('H1',
parent=styles['Heading1'],
fontSize=14, textColor=colors.white,
backColor=colors.HexColor('#1a237e'),
borderPadding=(4, 8, 4, 8),
spaceBefore=10, spaceAfter=6,
fontName='Helvetica-Bold')
style_h2 = ParagraphStyle('H2',
parent=styles['Heading2'],
fontSize=11, textColor=colors.HexColor('#1565C0'),
spaceBefore=8, spaceAfter=4,
fontName='Helvetica-Bold')
style_body = ParagraphStyle('Body',
parent=styles['Normal'],
fontSize=9, textColor=colors.HexColor('#212121'),
spaceAfter=4, leading=13, alignment=TA_JUSTIFY)
style_bullet = ParagraphStyle('Bullet',
parent=styles['Normal'],
fontSize=9, textColor=colors.HexColor('#212121'),
leftIndent=14, spaceAfter=3, leading=13,
bulletIndent=4, bulletFontSize=9)
style_caption = ParagraphStyle('Caption',
parent=styles['Normal'],
fontSize=8, textColor=colors.HexColor('#555555'),
spaceAfter=6, alignment=TA_CENTER,
fontName='Helvetica-Oblique')
style_key = ParagraphStyle('Key',
parent=styles['Normal'],
fontSize=9, textColor=colors.HexColor('#1B5E20'),
backColor=colors.HexColor('#E8F5E9'),
borderPadding=(4, 6, 4, 6),
spaceAfter=6, leading=13,
fontName='Helvetica-Bold')
style_warn = ParagraphStyle('Warn',
parent=styles['Normal'],
fontSize=9, textColor=colors.HexColor('#B71C1C'),
backColor=colors.HexColor('#FFEBEE'),
borderPadding=(4, 6, 4, 6),
spaceAfter=6, leading=13)
W = A4[0] - 30*mm # usable width
def H(s, style): return Paragraph(s, style)
def SP(n=6): return Spacer(1, n)
def HR(): return HRFlowable(width='100%', thickness=0.5,
color=colors.HexColor('#BDBDBD'), spaceAfter=6)
def IMG(path, caption="", width_frac=1.0):
w = W * width_frac
elem = [Image(path, width=w, height=w * 0.45)]
if caption:
elem.append(H(caption, style_caption))
return elem
def table(data, col_widths=None, header_color='#1a237e'):
t = Table(data, colWidths=col_widths or [W/len(data[0])]*len(data[0]))
ts = TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(header_color)),
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, 0), 9),
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
('FONTSIZE', (0, 1), (-1, -1), 8.5),
('ROWBACKGROUNDS', (0, 1), (-1, -1),
[colors.HexColor('#F5F5F5'), colors.white]),
('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#BDBDBD')),
('TOPPADDING', (0, 0), (-1, -1), 5),
('BOTTOMPADDING', (0, 0), (-1, -1), 5),
('LEFTPADDING', (0, 0), (-1, -1), 6),
('RIGHTPADDING', (0, 0), (-1, -1), 6),
])
t.setStyle(ts)
return t
story = []
# ── COVER ──
story.append(SP(20))
story.append(H("PROSTHODONTICS REVISION", style_title))
story.append(H("Metal-Ceramic & All-Ceramic Crowns", style_subtitle))
story.append(H("Tooth Preparation — Diagrams & Notes", style_subtitle))
story.append(SP(6))
story.append(H("Final Year BDS | Fixed Prosthodontics", ParagraphStyle('cov',
parent=styles['Normal'], fontSize=10,
textColor=colors.HexColor('#546E7A'), alignment=TA_CENTER)))
story.append(SP(8))
story.append(HR())
story.append(SP(8))
# Quick reference table
qr_data = [
["Crown Type", "Facial Reduction", "Lingual Reduction", "Incisal Reduction", "Finish Line"],
["PFM", "1.2-1.5 mm", "0.5-0.7 mm", "1.5-2.0 mm", "Shoulder (F) / Chamfer (L)"],
["All-Ceramic", "1.0-1.5 mm", "1.0-1.5 mm (uniform)", "1.5-2.0 mm", "Shoulder / Deep Chamfer"],
["Full Metal", "0.5 mm", "0.5 mm", "1.0-1.5 mm", "Chamfer / Knife-edge"],
]
col_w = [W*0.22, W*0.18, W*0.22, W*0.18, W*0.20]
story.append(table(qr_data, col_w))
story.append(SP(4))
story.append(H("Table 1: Tooth Reduction Requirements at a Glance", style_caption))
# ── SECTION 1: PREP STAGES ──
story.append(PageBreak())
story.append(H("1. PFM Crown – Tooth Preparation Stages", style_h1))
story.append(SP(4))
story += IMG(img_paths['pfm_prep_stages'],
"Fig 1. Sequential stages of tooth preparation for a PFM crown. "
"Dashed outline = original tooth. Green = prepared surface. "
"Red = shoulder finish line. Blue = chamfer finish line.")
story.append(SP(6))
story.append(H("Key Points: PFM Preparation Sequence", style_h2))
bullets = [
"Step 1 – Depth grooves: Use depth-cutting bur (0.5mm or 1.0mm diamond) to establish reduction depth before bulk reduction",
"Step 2 – Incisal/Occlusal reduction first (1.5-2.0 mm): Allows visualization of reduction amount without distorting axial walls",
"Step 3 – Facial (labial) reduction (1.2-1.5 mm): Provides space for metal coping (0.3-0.5mm) + opaque + body + enamel porcelain",
"Step 4 – Lingual reduction (0.5-0.7 mm): Only space for thin metal coping; no porcelain on lingual (except porcelain lingual option)",
"Step 5 – Axial wall refinement + finish line: 2-6° taper, shoulder margin facially, chamfer or knife-edge lingually",
]
for b in bullets:
story.append(H(f"• {b}", style_bullet))
story.append(SP(6))
story.append(H("⭐ EXAM KEY: WHY DIFFERENTIAL REDUCTION IN PFM?",
ParagraphStyle('kk', parent=style_key, fontSize=9)))
story.append(H(
"Facial surface needs more reduction (1.2-1.5mm) to accommodate BOTH metal AND porcelain layers. "
"Lingual surface only needs space for thin metal (0.5-0.7mm). This differential reduction is a "
"defining feature of PFM prep vs. All-ceramic prep (which is uniform all round).",
style_body))
# ── SECTION 2: FINISH LINES ──
story.append(PageBreak())
story.append(H("2. Finish Line Types", style_h1))
story.append(SP(4))
story += IMG(img_paths['finish_lines'],
"Fig 2. Five types of cervical finish lines. Red line = finish line zone. "
"Pink dotted = gingival margin level (GML). Each type serves specific crown materials.")
story.append(SP(4))
fl_data = [
["Finish Line", "Angle", "Tooth Loss", "Uses", "Advantage"],
["Knife Edge", "~30°", "Least", "Full metal crowns", "Min. tooth removal"],
["Chamfer", "~45°", "Moderate", "PFM lingual; All-ceramic", "Good bulk; easy prep"],
["Shoulder (Butt)", "90°", "More", "PFM facial; All-ceramic", "Defined margin; porcelain support"],
["Bevel", "~45° bevel", "Moderate", "Full metal crowns", "Better marginal seal"],
["Shoulder + Bevel", "90° + bevel", "Most", "PFM facial margin option", "Metal bevel marginal fit"],
]
cw2 = [W*0.18, W*0.10, W*0.12, W*0.28, W*0.30]
story.append(table(fl_data, cw2))
story.append(SP(4))
story.append(H(
"⭐ All-ceramic crowns MUST NOT have knife-edge margins — ceramic cannot reproduce "
"fine margins and stress concentrations cause fracture.",
style_warn))
# ── SECTION 3: PREP COMPARISON ──
story.append(PageBreak())
story.append(H("3. PFM vs All-Ceramic – Preparation Comparison", style_h1))
story.append(SP(4))
story += IMG(img_paths['prep_comparison'],
"Fig 3. Cross-sectional comparison: Unprepared tooth (left), PFM preparation (centre), "
"All-ceramic preparation (right). Note differential reduction in PFM vs uniform in All-ceramic.")
story.append(SP(6))
story.append(H("Critical Differences: PFM vs All-Ceramic Prep", style_h2))
diff_data = [
["Feature", "PFM Crown", "All-Ceramic Crown"],
["Facial reduction", "1.2-1.5 mm", "1.0-1.5 mm"],
["Lingual reduction", "0.5-0.7 mm (metal only)", "1.0-1.5 mm (SAME as facial)"],
["Reduction type", "Differential (F > L)", "Uniform circumferential"],
["Incisal / Occlusal", "1.5-2.0 mm", "1.5-2.0 mm"],
["Facial margin", "Shoulder (90°)", "Shoulder or deep chamfer"],
["Lingual margin", "Chamfer or knife-edge", "Deep chamfer (NOT knife-edge)"],
["Internal angles", "Can have angles", "Must be rounded (stress!)"],
["Axial taper", "2-6° total", "2-6° total (critical: no undercuts)"],
["Preparation taper importance", "Moderate", "HIGH — ceramic needs precise fit"],
]
cw3 = [W*0.32, W*0.34, W*0.34]
story.append(table(diff_data, cw3))
# ── SECTION 4: PFM LAYERS ──
story.append(PageBreak())
story.append(H("4. PFM Crown – Layer Structure & CTE", style_h1))
story.append(SP(4))
story += IMG(img_paths['pfm_layers'],
"Fig 4. PFM crown layer structure from tooth to outer surface. "
"Each layer serves a specific mechanical or aesthetic function. "
"CTE rule ensures porcelain is under compressive stress.")
story.append(SP(6))
story.append(H("Porcelain Bonding to Metal – 3 Mechanisms", style_h2))
bond_data = [
["Mechanism", "Description", "Importance"],
["1. Chemical Bond", "Metal oxide layer (from oxidation bake) bonds to opaque porcelain", "MOST important"],
["2. Mechanical Bond", "Surface roughness / beads / air-abrasion of metal coping", "Secondary"],
["3. Compressive Bond", "CTE metal > CTE ceramic → porcelain under compression", "Prevents fracture"],
]
cw4 = [W*0.20, W*0.55, W*0.25]
story.append(table(bond_data, cw4))
story.append(SP(6))
story.append(H(
"CTE Values: Metal alloy: 13.5-14.5 × 10⁻⁶/°C | Dental porcelain: 13.0-14.0 × 10⁻⁶/°C | "
"Metal CTE must be 0.5 × 10⁻⁶ HIGHER than porcelain CTE",
style_key))
# ── SECTION 5: ALL-CERAMIC TYPES ──
story.append(PageBreak())
story.append(H("5. All-Ceramic Systems", style_h1))
story.append(SP(4))
story += IMG(img_paths['all_ceramic_types'],
"Fig 5. Classification of all-ceramic systems with strength comparison. "
"Zirconia has highest flexural strength; feldspathic has best aesthetics.")
story.append(SP(6))
story.append(H("Zirconia Special Points", style_h2))
zr_bullets = [
"Full name: Yttrium-stabilized Tetragonal Zirconia Polycrystal (Y-TZP)",
"Transformation toughening: Under stress → tetragonal to monoclinic phase → absorbs crack energy",
"Cannot be HF etched — use sandblasting (Al₂O₃ 50µm) + MDP-based primer (Panavia, Rely X Unicem)",
"Low-temperature degradation (aging): Hydrothermal conditions → spontaneous t→m transformation → surface damage",
"Flexural strength: 900-1200 MPa (strongest all-ceramic)",
"CAD/CAM milled only (cannot be cast or pressed)",
"White/opaque — inferior aesthetics but excellent for posterior/implant crowns",
"Full-contour zirconia: no veneering ceramic → eliminates chipping problem",
]
for b in zr_bullets:
story.append(H(f"• {b}", style_bullet))
story.append(SP(6))
story.append(H("Lithium Disilicate (IPS e.max) Special Points", style_h2))
ed_bullets = [
"Flexural strength: ~400-500 MPa (Press), ~360 MPa (CAD) — best balance of strength + aesthetics",
"Can be pressed (lost-wax) OR milled (CAD/CAM)",
"High translucency — closest to natural tooth appearance",
"HF etch 20 seconds → rinse → silane primer → adhesive resin cement (mandatory)",
"Anterior + posterior single crowns; 3-unit FPD up to 2nd premolar",
"Sensitivity to cement color — shade of cement affects final crown appearance",
]
for b in ed_bullets:
story.append(H(f"• {b}", style_bullet))
# ── SECTION 6: CEMENTATION ──
story.append(PageBreak())
story.append(H("6. Cementation Protocol – All-Ceramic", style_h1))
story.append(SP(4))
story += IMG(img_paths['cementation'],
"Fig 6. Step-by-step cementation protocol for glass-based all-ceramic crowns (Li-Disilicate / Feldspathic).")
story.append(SP(6))
cem_data = [
["Cement Type", "PFM", "Feldspathic", "Li-Disilicate", "Zirconia", "Note"],
["Zinc Phosphate", "✓", "✗", "✗", "✗", "Old standard; brittle; film thickness 25µm"],
["Glass Ionomer (GIC)", "✓", "✗", "✗", "Limited", "Chemical bond to tooth; fluoride release"],
["RMGIC", "✓", "Limited", "Limited", "Limited", "Better bond than GIC; light+chemical cure"],
["Resin Cement", "✓", "✓ (MUST)", "✓ (MUST)", "✓ (MDP)", "Best marginal seal; needs adhesive protocol"],
["ZOE (temp only)", "Temp", "Avoid", "Avoid", "Avoid", "Eugenol inhibits resin polymerisation"],
]
cw5 = [W*0.20, W*0.08, W*0.13, W*0.13, W*0.10, W*0.35]
story.append(table(cem_data, cw5))
# ── SECTION 7: TAPER ──
story.append(PageBreak())
story.append(H("7. Axial Wall Taper & Retention", style_h1))
story.append(SP(4))
story += IMG(img_paths['taper'],
"Fig 7. Effect of axial wall taper on retention. Ideal taper = 2-6° total convergence angle. "
"Under-taper causes seating problems; over-taper reduces retention.")
story.append(SP(4))
taper_data = [
["Taper (Total Convergence)", "Effect", "Clinical Result"],
["< 2° (Under-tapered)", "Tooth undercuts present", "Crown cannot fully seat; binding during try-in"],
["2-6° (IDEAL)", "Optimal resistance & retention form", "Best clinical outcome"],
["6-12° (Acceptable)", "Reduced retention", "Acceptable if crown height adequate"],
["> 12° (Over-tapered)", "Poor retention & resistance", "Crown easily dislodged; requires recement/remake"],
]
cw6 = [W*0.28, W*0.32, W*0.40]
story.append(table(taper_data, cw6))
story.append(SP(6))
story.append(H("Retention Formula (Jørgensen):", style_h2))
story.append(H(
"Retention increases with: ↑ Surface area | ↑ Crown height | ↓ Taper angle | Rougher surface | "
"Smaller crown diameter | Correct cement film thickness (25µm optimal)",
style_body))
# ── SECTION 8: PORCELAIN FRACTURE ──
story.append(PageBreak())
story.append(H("8. Porcelain Fracture – Causes & Management", style_h1))
story.append(SP(4))
story += IMG(img_paths['porcelain_fracture'],
"Fig 8. Mind map of porcelain fracture causes in PFM crowns. "
"Most common complication of PFM crowns in clinical practice.")
# ── SECTION 9: QUICK REVISION TABLE ──
story.append(PageBreak())
story.append(H("9. High-Yield Exam Summary", style_h1))
story.append(SP(4))
hye_data = [
["Question", "Answer"],
["Most common PFM complication", "Porcelain fracture / chipping"],
["Why metal CTE > ceramic CTE?", "Keeps porcelain under compression (stronger in compression)"],
["Strongest all-ceramic material", "Zirconia Y-TZP (900-1200 MPa)"],
["Best aesthetic all-ceramic", "Lithium Disilicate e.max (high translucency)"],
["HF etch time for Li-Disilicate", "20 seconds"],
["HF etch time for Feldspathic", "60 seconds"],
["Finish line for all-ceramic lingual", "Shoulder or deep chamfer (NEVER knife-edge)"],
["Finish line for PFM lingual", "Chamfer or knife-edge"],
["Why rounded line angles in all-ceramic?", "Reduce stress concentration; prevent ceramic fracture"],
["Facial reduction – PFM", "1.2-1.5 mm"],
["Lingual reduction – PFM vs All-ceramic", "PFM: 0.5-0.7mm | All-ceramic: 1.0-1.5mm"],
["Ideal taper", "2-6° total convergence (3° per wall ideal)"],
["Opaque layer function (dual)", "1) Mask metal color 2) Chemical bond to metal"],
["Transformation toughening", "Zirconia: tetragonal → monoclinic phase under stress"],
["Low temperature degradation", "Zirconia (Y-TZP) in moist/hydrothermal environment"],
["Cement for zirconia", "MDP-containing resin (Panavia, Rely-X Unicem)"],
["Why no HF on zirconia?", "HF only etches glass-phase ceramics; zirconia is polycrystalline"],
["Crown for bruxer", "Full-contour monolithic zirconia (posterior)"],
["Crown for metal allergy", "All-ceramic crown"],
["Crown for long-span posterior FPD", "PFM (metal connectors) or zirconia FPD framework"],
]
cw7 = [W*0.50, W*0.50]
t = Table(hye_data, colWidths=cw7)
ts = TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#1a237e')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, -1), 8.5),
('ALIGN', (0, 0), (0, -1), 'LEFT'),
('ALIGN', (1, 0), (1, -1), 'LEFT'),
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
('FONTNAME', (0, 1), (0, -1), 'Helvetica-Bold'),
('FONTNAME', (1, 1), (1, -1), 'Helvetica'),
('ROWBACKGROUNDS', (0, 1), (-1, -1),
[colors.HexColor('#FFF9C4'), colors.white]),
('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#BDBDBD')),
('TOPPADDING', (0, 0), (-1, -1), 5),
('BOTTOMPADDING', (0, 0), (-1, -1), 5),
('LEFTPADDING', (0, 0), (-1, -1), 8),
('RIGHTPADDING', (0, 0), (-1, -1), 8),
])
t.setStyle(ts)
story.append(t)
story.append(SP(10))
story.append(HR())
story.append(H(
"Prepared for Final Year BDS Prosthodontics | Fixed Partial Denture — Crowns & Bridges | "
"Orris AI Revision Notes",
ParagraphStyle('foot', parent=styles['Normal'], fontSize=7.5,
textColor=colors.HexColor('#9E9E9E'), alignment=TA_CENTER)))
doc.build(story)
return pdf_path
# ──────────────────────────────────────────────────────────────────────────────
# MAIN
# ──────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
print("Generating diagrams...")
img_paths = {}
img_paths['pfm_prep_stages'] = fig_pfm_prep_stages()
print(" ✓ PFM prep stages")
img_paths['finish_lines'] = fig_finish_lines()
print(" ✓ Finish lines")
img_paths['pfm_layers'] = fig_pfm_layers()
print(" ✓ PFM layers")
img_paths['all_ceramic_types'] = fig_all_ceramic_types()
print(" ✓ All-ceramic types")
img_paths['prep_comparison'] = fig_prep_comparison()
print(" ✓ Prep comparison")
img_paths['cementation'] = fig_cementation()
print(" ✓ Cementation protocol")
img_paths['taper'] = fig_taper()
print(" ✓ Taper diagram")
img_paths['porcelain_fracture']= fig_porcelain_fracture()
print(" ✓ Porcelain fracture")
print("\nBuilding PDF...")
pdf = build_pdf(img_paths)
print(f"\n✓ PDF created: {pdf}")