Generate a pdf for the dental anatomy paper of 1st year bds for maxillary cental incisor showing all the labial,lingual,mesial,distal and insical aspect with diagrams

Reading File
Finding Sources
Reading File
Finding Sources
Finding Sources
Finding Sources
Running Command

mkdir -p /home/daytona/workspace/dental-anatomy && echo "Directory ready"

Writing File

~/dental-anatomy/generate_pdf.py

#!/usr/bin/env python3
"""
Dental Anatomy PDF Generator
Maxillary Central Incisor - All 5 Aspects
For 1st Year BDS Students
"""

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, Table, TableStyle,
    PageBreak, HRFlowable, KeepTogether
)
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
from reportlab.graphics.shapes import (
    Drawing, Rect, Ellipse, Circle, Line, Path, String,
    Polygon, PolyLine, ArcPath, Arc
)
from reportlab.graphics import renderPDF
from reportlab.platypus import Flowable
import math

# ─────────────────────────────────────────────
# Color Palette
# ─────────────────────────────────────────────
NAVY        = colors.HexColor("#1a2744")
BLUE        = colors.HexColor("#2563eb")
LIGHT_BLUE  = colors.HexColor("#dbeafe")
CREAM       = colors.HexColor("#fffdf5")
ENAMEL_COL  = colors.HexColor("#f5f0e8")
DENTIN_COL  = colors.HexColor("#e8d5b0")
PULP_COL    = colors.HexColor("#f4a261")
LINE_COL    = colors.HexColor("#2d3748")
LABEL_COL   = colors.HexColor("#1e3a5f")
ACCENT      = colors.HexColor("#e63946")
GREEN       = colors.HexColor("#2d6a4f")
LIGHT_GREEN = colors.HexColor("#d8f3dc")
GOLD        = colors.HexColor("#d4a017")
GRAY_LIGHT  = colors.HexColor("#f1f5f9")
GRAY_MID    = colors.HexColor("#cbd5e1")

W, H = A4  # 595.27 x 841.89 pts

# ─────────────────────────────────────────────
# Styles
# ─────────────────────────────────────────────
styles = getSampleStyleSheet()

title_style = ParagraphStyle(
    'MainTitle', parent=styles['Title'],
    fontSize=22, textColor=NAVY, spaceAfter=4,
    fontName='Helvetica-Bold', alignment=TA_CENTER, leading=28
)
subtitle_style = ParagraphStyle(
    'Subtitle', parent=styles['Normal'],
    fontSize=13, textColor=BLUE, spaceAfter=2,
    fontName='Helvetica', alignment=TA_CENTER, leading=18
)
section_style = ParagraphStyle(
    'SectionHead', parent=styles['Heading1'],
    fontSize=15, textColor=colors.white, spaceAfter=0,
    fontName='Helvetica-Bold', alignment=TA_LEFT, leading=20,
    backColor=NAVY, borderPad=6
)
sub_section_style = ParagraphStyle(
    'SubSection', parent=styles['Heading2'],
    fontSize=12, textColor=NAVY, spaceAfter=4,
    fontName='Helvetica-Bold', alignment=TA_LEFT, leading=16,
    borderPad=4
)
body_style = ParagraphStyle(
    'Body', parent=styles['Normal'],
    fontSize=10, textColor=colors.HexColor("#2d3748"),
    spaceAfter=4, fontName='Helvetica', leading=15,
    alignment=TA_JUSTIFY
)
bullet_style = ParagraphStyle(
    'Bullet', parent=styles['Normal'],
    fontSize=10, textColor=colors.HexColor("#2d3748"),
    spaceAfter=3, fontName='Helvetica', leading=14,
    leftIndent=14, alignment=TA_LEFT
)
caption_style = ParagraphStyle(
    'Caption', parent=styles['Normal'],
    fontSize=9, textColor=colors.HexColor("#475569"),
    spaceAfter=6, fontName='Helvetica-Oblique', leading=13,
    alignment=TA_CENTER
)
label_small = ParagraphStyle(
    'LabelSmall', parent=styles['Normal'],
    fontSize=8, textColor=LABEL_COL,
    fontName='Helvetica-Bold', leading=10
)
highlight_style = ParagraphStyle(
    'Highlight', parent=styles['Normal'],
    fontSize=10, textColor=NAVY,
    spaceAfter=4, fontName='Helvetica-Bold', leading=14,
    backColor=LIGHT_BLUE, borderPad=5, leftIndent=8
)


# ─────────────────────────────────────────────
# Helper: section header block
# ─────────────────────────────────────────────
def section_header(title, subtitle=""):
    d = Drawing(W - 80, 36)
    d.add(Rect(0, 0, W - 80, 32, fillColor=NAVY, strokeColor=None))
    d.add(String(12, 10, title, fontSize=14, fillColor=colors.white,
                 fontName='Helvetica-Bold'))
    elems = [Spacer(1, 8), d]
    if subtitle:
        elems.append(Paragraph(subtitle, caption_style))
    return elems


# ─────────────────────────────────────────────
# DIAGRAM 1 — Labial Aspect
# ─────────────────────────────────────────────
def draw_labial_aspect():
    dw, dh = 300, 400
    d = Drawing(dw, dh)

    # Background
    d.add(Rect(0, 0, dw, dh, fillColor=GRAY_LIGHT, strokeColor=None))

    # Title bar
    d.add(Rect(0, dh - 30, dw, 30, fillColor=NAVY, strokeColor=None))
    d.add(String(dw/2 - 60, dh - 20, "LABIAL ASPECT", fontSize=12,
                 fillColor=colors.white, fontName='Helvetica-Bold'))

    cx = 150  # centre x
    crown_top = dh - 60   # top of incisal edge (y up)
    crown_bot = 200       # CEJ level
    root_bot = 40         # root apex

    # ── Enamel outline (labial surface of crown) ──
    # Crown: roughly trapezoidal with rounded incisal corners
    # Mesial side: relatively straight, slight convexity
    # Distal side: more rounded
    # Incisal: slightly curved/flat

    crown_pts = [
        (cx - 22, crown_bot),          # mesio-cervical
        (cx - 24, crown_bot + 60),     # mesio-middle third
        (cx - 20, crown_bot + 120),    # mesio-incisal third
        (cx - 10, crown_top + 10),     # mesio-incisal angle (sharper)
        (cx,      crown_top),          # incisal tip midpoint
        (cx + 12, crown_top + 15),     # disto-incisal angle (rounder)
        (cx + 22, crown_bot + 110),    # disto-incisal third
        (cx + 24, crown_bot + 55),     # disto-middle third
        (cx + 20, crown_bot),          # disto-cervical
    ]

    # Fill crown enamel
    pts_flat = []
    for p in crown_pts:
        pts_flat += [p[0], p[1]]
    d.add(Polygon(pts_flat, fillColor=ENAMEL_COL, strokeColor=LINE_COL, strokeWidth=1.5))

    # Dentin layer (inner lighter band, ~2mm inside)
    dentin_pts = [
        (cx - 17, crown_bot + 5),
        (cx - 19, crown_bot + 60),
        (cx - 15, crown_bot + 115),
        (cx - 7,  crown_top + 14),
        (cx,      crown_top + 8),
        (cx + 9,  crown_top + 22),
        (cx + 17, crown_bot + 105),
        (cx + 19, crown_bot + 52),
        (cx + 15, crown_bot + 5),
    ]
    dp_flat = []
    for p in dentin_pts:
        dp_flat += [p[0], p[1]]
    d.add(Polygon(dp_flat, fillColor=DENTIN_COL, strokeColor=None))

    # CEJ line (cervical line)
    d.add(Line(cx - 22, crown_bot, cx + 20, crown_bot,
               strokeColor=ACCENT, strokeWidth=1.5, strokeDashArray=[4, 3]))

    # Root outline (narrows to apex)
    root_pts = [
        (cx - 22, crown_bot),
        (cx - 14, crown_bot - 60),
        (cx - 7,  crown_bot - 120),
        (cx - 2,  root_bot + 10),
        (cx,      root_bot),           # apex
        (cx + 2,  root_bot + 10),
        (cx + 7,  crown_bot - 120),
        (cx + 14, crown_bot - 60),
        (cx + 20, crown_bot),
    ]
    rp_flat = []
    for p in root_pts:
        rp_flat += [p[0], p[1]]
    d.add(Polygon(rp_flat, fillColor=DENTIN_COL, strokeColor=LINE_COL, strokeWidth=1.5))

    # Root canal
    canal_pts = [
        (cx - 3, crown_bot - 5),
        (cx - 2, crown_bot - 70),
        (cx,     root_bot + 15),
        (cx + 2, crown_bot - 70),
        (cx + 3, crown_bot - 5),
    ]
    cp_flat = []
    for p in canal_pts:
        cp_flat += [p[0], p[1]]
    d.add(Polygon(cp_flat, fillColor=PULP_COL, strokeColor=None))

    # Pulp chamber in crown
    pulp_pts = [
        (cx - 8, crown_bot + 10),
        (cx - 9, crown_bot + 50),
        (cx - 6, crown_bot + 90),
        (cx,     crown_bot + 100),
        (cx + 6, crown_bot + 90),
        (cx + 9, crown_bot + 50),
        (cx + 8, crown_bot + 10),
    ]
    pp_flat = []
    for p in pulp_pts:
        pp_flat += [p[0], p[1]]
    d.add(Polygon(pp_flat, fillColor=PULP_COL, strokeColor=None))

    # ── Surface features ──
    # Development grooves (faint lines)
    d.add(Line(cx - 8, crown_top + 5, cx - 6, crown_bot + 30,
               strokeColor=colors.HexColor("#b0a090"), strokeWidth=0.7))
    d.add(Line(cx + 6, crown_top + 8, cx + 5, crown_bot + 30,
               strokeColor=colors.HexColor("#b0a090"), strokeWidth=0.7))

    # Mamelons at incisal edge (3 rounded lobes in young teeth)
    for mx in [cx - 7, cx, cx + 7]:
        d.add(Arc(mx - 4, crown_top - 2, mx + 4, crown_top + 8,
                  startAng=0, extent=180, strokeColor=LINE_COL, strokeWidth=1))

    # ── Annotation lines and labels ──
    def ann(x1, y1, x2, y2, label, anchor='left'):
        d.add(Line(x1, y1, x2, y2, strokeColor=LABEL_COL,
                   strokeWidth=0.8, strokeDashArray=[3, 2]))
        if anchor == 'left':
            d.add(String(x2 + 3, y2 - 3, label, fontSize=7.5,
                         fillColor=LABEL_COL, fontName='Helvetica-Bold'))
        else:
            d.add(String(x2 - len(label) * 5.5 - 4, y2 - 3, label, fontSize=7.5,
                         fillColor=LABEL_COL, fontName='Helvetica-Bold'))

    ann(cx + 25, crown_top + 5,   dw - 5,  crown_top + 5,   "Incisal Edge / Mamelons")
    ann(cx + 25, crown_top + 40,  dw - 5,  crown_top + 35,  "Disto-incisal Angle (rounded)")
    ann(cx - 22, crown_top + 40,  5,       crown_top + 30,  "Mesio-incisal Angle (sharp)", 'right')
    ann(cx + 25, crown_bot + 80,  dw - 5,  crown_bot + 80,  "Distal Convexity")
    ann(cx + 25, crown_bot + 10,  dw - 5,  crown_bot + 10,  "Disto-cervical")
    ann(cx - 22, crown_bot + 10,  5,       crown_bot + 10,  "Mesio-cervical", 'right')
    ann(cx + 2,  crown_bot - 5,   dw - 5,  crown_bot - 5,   "CEJ (Cervical Line)", )
    ann(cx + 2,  crown_bot - 80,  dw - 5,  crown_bot - 80,  "Root (single, conical)")
    ann(cx + 2,  root_bot + 5,    dw - 5,  root_bot + 5,    "Apex (blunt)")

    # Length/width indicators
    d.add(Line(cx - 35, crown_top, cx - 35, crown_bot,
               strokeColor=GREEN, strokeWidth=1, strokeDashArray=[2, 2]))
    d.add(Line(cx - 40, crown_top, cx - 30, crown_top,
               strokeColor=GREEN, strokeWidth=1))
    d.add(Line(cx - 40, crown_bot, cx - 30, crown_bot,
               strokeColor=GREEN, strokeWidth=1))
    d.add(String(2, (crown_top + crown_bot) // 2, "Crown\nLength\n~10.5mm",
                 fontSize=6.5, fillColor=GREEN, fontName='Helvetica-Bold'))

    d.add(Line(cx - 25, crown_bot - 20, cx + 21, crown_bot - 20,
               strokeColor=GOLD, strokeWidth=1, strokeDashArray=[2, 2]))
    d.add(String(cx - 5, crown_bot - 30, "Width ~8.5mm",
                 fontSize=6.5, fillColor=GOLD, fontName='Helvetica-Bold'))

    return d


# ─────────────────────────────────────────────
# DIAGRAM 2 — Lingual Aspect
# ─────────────────────────────────────────────
def draw_lingual_aspect():
    dw, dh = 300, 400
    d = Drawing(dw, dh)
    d.add(Rect(0, 0, dw, dh, fillColor=GRAY_LIGHT, strokeColor=None))
    d.add(Rect(0, dh - 30, dw, 30, fillColor=NAVY, strokeColor=None))
    d.add(String(dw/2 - 65, dh - 20, "LINGUAL ASPECT", fontSize=12,
                 fillColor=colors.white, fontName='Helvetica-Bold'))

    cx = 150
    crown_top = dh - 60
    crown_bot = 200
    root_bot = 40

    # Crown outline (lingual is narrower due to lingual convergence)
    crown_pts = [
        (cx - 18, crown_bot),
        (cx - 19, crown_bot + 55),
        (cx - 16, crown_bot + 110),
        (cx - 9,  crown_top + 12),
        (cx,      crown_top),
        (cx + 10, crown_top + 18),
        (cx + 18, crown_bot + 108),
        (cx + 20, crown_bot + 52),
        (cx + 16, crown_bot),
    ]
    pts_flat = []
    for p in crown_pts:
        pts_flat += [p[0], p[1]]
    d.add(Polygon(pts_flat, fillColor=ENAMEL_COL, strokeColor=LINE_COL, strokeWidth=1.5))

    # Cingulum (prominent oval elevation at cervical 1/3)
    d.add(Ellipse(cx - 12, crown_bot + 8, cx + 12, crown_bot + 55,
                  fillColor=colors.HexColor("#d4c4a0"),
                  strokeColor=LINE_COL, strokeWidth=1))

    # Marginal ridges
    # Mesial marginal ridge
    d.add(Line(cx - 16, crown_bot + 55, cx - 9, crown_top + 15,
               strokeColor=LINE_COL, strokeWidth=1.8))
    # Distal marginal ridge
    d.add(Line(cx + 14, crown_bot + 50, cx + 9, crown_top + 20,
               strokeColor=LINE_COL, strokeWidth=1.8))

    # Lingual fossa (concave area between ridges)
    # Draw as a slightly concave polygon fill
    fossa_pts = [
        (cx - 14, crown_bot + 55),
        (cx - 8,  crown_top + 16),
        (cx,      crown_top + 5),
        (cx + 8,  crown_top + 22),
        (cx + 12, crown_bot + 52),
        (cx,      crown_bot + 60),
    ]
    fp_flat = []
    for p in fossa_pts:
        fp_flat += [p[0], p[1]]
    d.add(Polygon(fp_flat, fillColor=colors.HexColor("#ece0c8"), strokeColor=None))

    # Lingual ridge (vertical ridge down midline in some teeth)
    d.add(Line(cx, crown_top + 5, cx, crown_bot + 56,
               strokeColor=colors.HexColor("#b09878"), strokeWidth=0.8,
               strokeDashArray=[3, 2]))

    # CEJ
    d.add(Line(cx - 18, crown_bot, cx + 16, crown_bot,
               strokeColor=ACCENT, strokeWidth=1.5, strokeDashArray=[4, 3]))

    # Root
    root_pts = [
        (cx - 18, crown_bot),
        (cx - 12, crown_bot - 55),
        (cx - 6,  crown_bot - 115),
        (cx - 2,  root_bot + 12),
        (cx,      root_bot),
        (cx + 2,  root_bot + 12),
        (cx + 6,  crown_bot - 115),
        (cx + 12, crown_bot - 55),
        (cx + 16, crown_bot),
    ]
    rp_flat = []
    for p in root_pts:
        rp_flat += [p[0], p[1]]
    d.add(Polygon(rp_flat, fillColor=DENTIN_COL, strokeColor=LINE_COL, strokeWidth=1.5))

    # Annotations
    def ann(x1, y1, x2, y2, label, anchor='left'):
        d.add(Line(x1, y1, x2, y2, strokeColor=LABEL_COL,
                   strokeWidth=0.8, strokeDashArray=[3, 2]))
        if anchor == 'left':
            d.add(String(x2 + 3, y2 - 3, label, fontSize=7.5,
                         fillColor=LABEL_COL, fontName='Helvetica-Bold'))
        else:
            d.add(String(x2 - len(label) * 5.5 - 4, y2 - 3, label, fontSize=7.5,
                         fillColor=LABEL_COL, fontName='Helvetica-Bold'))

    ann(cx + 19, crown_top + 5,   dw - 5, crown_top + 5,   "Incisal Edge")
    ann(cx + 19, crown_top + 50,  dw - 5, crown_top + 50,  "Lingual Fossa (concave)")
    ann(cx - 16, crown_top + 60,  5,      crown_top + 60,  "Mesial Marginal Ridge", 'right')
    ann(cx + 16, crown_top + 70,  dw - 5, crown_top + 75,  "Distal Marginal Ridge")
    ann(cx,      crown_bot + 35,  dw - 5, crown_bot + 35,  "Cingulum")
    ann(cx - 18, crown_bot + 2,   5,      crown_bot + 2,   "CEJ", 'right')
    ann(cx + 2,  crown_bot - 80,  dw - 5, crown_bot - 80,  "Root (lingual surface)")
    ann(cx + 2,  root_bot + 5,    dw - 5, root_bot + 5,    "Apex")

    return d


# ─────────────────────────────────────────────
# DIAGRAM 3 — Mesial Aspect
# ─────────────────────────────────────────────
def draw_mesial_aspect():
    dw, dh = 300, 400
    d = Drawing(dw, dh)
    d.add(Rect(0, 0, dw, dh, fillColor=GRAY_LIGHT, strokeColor=None))
    d.add(Rect(0, dh - 30, dw, 30, fillColor=NAVY, strokeColor=None))
    d.add(String(dw/2 - 60, dh - 20, "MESIAL ASPECT", fontSize=12,
                 fillColor=colors.white, fontName='Helvetica-Bold'))

    cx = 150
    crown_top = dh - 60
    crown_bot = 200
    root_bot = 40

    # From mesial side: see labio-lingual profile
    # Labial surface is convex; lingual is concave with cingulum
    # Crown appears triangular: wide at incisal, narrow at cervical

    # Labial outline (left side of drawing = labial)
    # Lingual outline (right side = lingual)
    lab_x = cx - 30   # labial face x
    lin_x = cx + 25   # lingual face x (narrower)

    # Crown outline - wedge-shaped
    # Incisal = thin edge at top
    # Cervical = wider
    inc_pt  = cx - 5, crown_top         # incisal tip (labially placed)
    mci_lab = lab_x,  crown_top + 15    # labio-incisal
    mid_lab = lab_x - 3, crown_bot + 80 # labial convexity (middle third)
    cej_lab = lab_x + 5, crown_bot      # labio-cervical CEJ
    cej_lin = lin_x - 5, crown_bot      # linguo-cervical CEJ
    mid_lin = lin_x + 4, crown_bot + 40 # lingual cervical convexity (cingulum)
    mci_lin = lin_x - 2, crown_top + 20 # linguo-incisal (slightly behind)

    crown_outline = [
        inc_pt,
        mci_lab,
        mid_lab,
        cej_lab,
        cej_lin,
        mid_lin,
        mci_lin,
    ]
    co_flat = []
    for p in crown_outline:
        co_flat += [p[0], p[1]]
    d.add(Polygon(co_flat, fillColor=ENAMEL_COL, strokeColor=LINE_COL, strokeWidth=1.5))

    # Cingulum bump on lingual side
    d.add(Ellipse(lin_x - 8, crown_bot + 10, lin_x + 8, crown_bot + 45,
                  fillColor=colors.HexColor("#d4c4a0"), strokeColor=LINE_COL, strokeWidth=1))

    # CEJ curve (convex toward root on mesial - pronounced)
    d.add(Line(lab_x + 5, crown_bot, lin_x - 5, crown_bot,
               strokeColor=ACCENT, strokeWidth=1.5, strokeDashArray=[4, 3]))

    # Root (from mesial - oval/broad)
    root_pts = [
        (lab_x + 5, crown_bot),
        (lab_x + 3, crown_bot - 50),
        (cx - 8,    crown_bot - 110),
        (cx - 3,    root_bot + 10),
        (cx,        root_bot),
        (cx + 3,    root_bot + 10),
        (cx + 6,    crown_bot - 110),
        (lin_x - 8, crown_bot - 50),
        (lin_x - 5, crown_bot),
    ]
    rp_flat = []
    for p in root_pts:
        rp_flat += [p[0], p[1]]
    d.add(Polygon(rp_flat, fillColor=DENTIN_COL, strokeColor=LINE_COL, strokeWidth=1.5))

    # Labial profile line indicator
    d.add(String(5, crown_bot + 100, "Lab.", fontSize=7.5,
                 fillColor=BLUE, fontName='Helvetica-Bold'))
    d.add(String(dw - 30, crown_bot + 100, "Ling.", fontSize=7.5,
                 fillColor=BLUE, fontName='Helvetica-Bold'))

    # Annotations
    def ann(x1, y1, x2, y2, label, anchor='left'):
        d.add(Line(x1, y1, x2, y2, strokeColor=LABEL_COL,
                   strokeWidth=0.8, strokeDashArray=[3, 2]))
        if anchor == 'left':
            d.add(String(x2 + 3, y2 - 3, label, fontSize=7.5,
                         fillColor=LABEL_COL, fontName='Helvetica-Bold'))
        else:
            d.add(String(max(2, x2 - len(label) * 5 - 4), y2 - 3, label, fontSize=7.5,
                         fillColor=LABEL_COL, fontName='Helvetica-Bold'))

    ann(inc_pt[0], inc_pt[1], inc_pt[0], crown_top + 5, "")
    d.add(String(inc_pt[0] - 5, crown_top + 5, "Incisal Ridge", fontSize=7.5,
                 fillColor=LABEL_COL, fontName='Helvetica-Bold'))

    ann(lab_x - 3, crown_bot + 80, 5, crown_bot + 80, "Labial Convexity\n(middle 1/3)", 'right')
    ann(lin_x + 5, crown_bot + 30, dw - 5, crown_bot + 30, "Cingulum")
    ann(cx,       crown_bot + 3, dw - 5, crown_bot + 3,  "CEJ (mesial arc ~3.5mm)")
    ann(cx - 5,   crown_bot - 70, 5,     crown_bot - 70, "Root (broad mesio-distally)", 'right')
    ann(cx,       root_bot + 5,   dw - 5, root_bot + 5,  "Apex")

    return d


# ─────────────────────────────────────────────
# DIAGRAM 4 — Distal Aspect
# ─────────────────────────────────────────────
def draw_distal_aspect():
    dw, dh = 300, 400
    d = Drawing(dw, dh)
    d.add(Rect(0, 0, dw, dh, fillColor=GRAY_LIGHT, strokeColor=None))
    d.add(Rect(0, dh - 30, dw, 30, fillColor=NAVY, strokeColor=None))
    d.add(String(dw/2 - 58, dh - 20, "DISTAL ASPECT", fontSize=12,
                 fillColor=colors.white, fontName='Helvetica-Bold'))

    cx = 150
    crown_top = dh - 60
    crown_bot = 200
    root_bot = 40

    lab_x = cx - 28
    lin_x = cx + 22

    # Distal aspect: similar to mesial but CEJ curvature smaller (~2.5mm)
    # Cingulum still visible, slight distal tilt

    crown_outline = [
        (cx - 4, crown_top),
        (lab_x,  crown_top + 18),
        (lab_x - 2, crown_bot + 75),
        (lab_x + 6, crown_bot),
        (lin_x - 4, crown_bot),
        (lin_x + 3, crown_bot + 38),
        (lin_x - 1, crown_top + 22),
    ]
    co_flat = []
    for p in crown_outline:
        co_flat += [p[0], p[1]]
    d.add(Polygon(co_flat, fillColor=ENAMEL_COL, strokeColor=LINE_COL, strokeWidth=1.5))

    # Cingulum
    d.add(Ellipse(lin_x - 7, crown_bot + 10, lin_x + 7, crown_bot + 42,
                  fillColor=colors.HexColor("#d4c4a0"), strokeColor=LINE_COL, strokeWidth=1))

    # CEJ
    d.add(Line(lab_x + 6, crown_bot, lin_x - 4, crown_bot,
               strokeColor=ACCENT, strokeWidth=1.5, strokeDashArray=[4, 3]))

    # Root
    root_pts = [
        (lab_x + 6, crown_bot),
        (lab_x + 4, crown_bot - 52),
        (cx - 7,    crown_bot - 108),
        (cx - 2,    root_bot + 12),
        (cx,        root_bot),
        (cx + 2,    root_bot + 12),
        (cx + 5,    crown_bot - 108),
        (lin_x - 7, crown_bot - 52),
        (lin_x - 4, crown_bot),
    ]
    rp_flat = []
    for p in root_pts:
        rp_flat += [p[0], p[1]]
    d.add(Polygon(rp_flat, fillColor=DENTIN_COL, strokeColor=LINE_COL, strokeWidth=1.5))

    d.add(String(5, crown_bot + 100, "Lab.", fontSize=7.5,
                 fillColor=BLUE, fontName='Helvetica-Bold'))
    d.add(String(dw - 30, crown_bot + 100, "Ling.", fontSize=7.5,
                 fillColor=BLUE, fontName='Helvetica-Bold'))

    def ann(x1, y1, x2, y2, label, anchor='left'):
        d.add(Line(x1, y1, x2, y2, strokeColor=LABEL_COL,
                   strokeWidth=0.8, strokeDashArray=[3, 2]))
        if anchor == 'left':
            d.add(String(x2 + 3, y2 - 3, label, fontSize=7.5,
                         fillColor=LABEL_COL, fontName='Helvetica-Bold'))
        else:
            d.add(String(max(2, x2 - len(label) * 5 - 4), y2 - 3, label, fontSize=7.5,
                         fillColor=LABEL_COL, fontName='Helvetica-Bold'))

    d.add(String(cx - 8, crown_top + 3, "Incisal Edge", fontSize=7.5,
                 fillColor=LABEL_COL, fontName='Helvetica-Bold'))
    ann(lab_x - 2, crown_bot + 75, 5, crown_bot + 75, "Labial Convexity", 'right')
    ann(lin_x + 3, crown_bot + 28, dw - 5, crown_bot + 28, "Cingulum")
    ann(cx,        crown_bot + 3,  dw - 5, crown_bot + 3,  "CEJ (distal arc ~2.5mm)")
    ann(cx - 5,    crown_bot - 70, 5,     crown_bot - 70,  "Root (tapers distally)", 'right')
    ann(cx,        root_bot + 5,   dw - 5, root_bot + 5,   "Apex (may curve distally)")

    return d


# ─────────────────────────────────────────────
# DIAGRAM 5 — Incisal Aspect
# ─────────────────────────────────────────────
def draw_incisal_aspect():
    dw, dh = 300, 340
    d = Drawing(dw, dh)
    d.add(Rect(0, 0, dw, dh, fillColor=GRAY_LIGHT, strokeColor=None))
    d.add(Rect(0, dh - 30, dw, 30, fillColor=NAVY, strokeColor=None))
    d.add(String(dw/2 - 62, dh - 20, "INCISAL ASPECT", fontSize=12,
                 fillColor=colors.white, fontName='Helvetica-Bold'))

    cx, cy = 150, 155

    # From incisal: tooth looks like a rounded triangle / D-shape
    # Labial face = straight/slightly curved = wide
    # Lingual face = narrower, cingulum visible
    # Mesial angle = sharper
    # Distal angle = more rounded

    # Outer enamel boundary (ovoid/D-shape from incisal)
    # Labial = bottom in this view, lingual = top
    lab_y   = cy + 70   # labial face (bottom)
    lin_y   = cy - 40   # lingual (top, narrower)
    mes_x   = cx - 42   # mesial side (left)
    dis_x   = cx + 44   # distal side (right)

    outline = [
        (cx - 30, lab_y),          # mesio-labial
        (cx,      lab_y + 12),     # labial midpoint (slight convexity)
        (cx + 32, lab_y),          # disto-labial
        (dis_x,   cy + 20),        # disto-labial line angle
        (dis_x,   cy - 10),        # distal surface
        (cx + 22, lin_y),          # disto-lingual
        (cx,      lin_y - 8),      # lingual midpoint
        (cx - 20, lin_y),          # mesio-lingual
        (mes_x,   cy - 10),        # mesial surface
        (mes_x,   cy + 20),        # mesio-labial line angle
    ]
    of_flat = []
    for p in outline:
        of_flat += [p[0], p[1]]
    d.add(Polygon(of_flat, fillColor=ENAMEL_COL, strokeColor=LINE_COL, strokeWidth=2))

    # Dentin inner oval
    din_outline = [
        (cx - 20, lab_y - 5),
        (cx,      lab_y + 5),
        (cx + 22, lab_y - 5),
        (dis_x - 8,  cy + 18),
        (dis_x - 8,  cy - 5),
        (cx + 14, lin_y + 8),
        (cx,      lin_y + 2),
        (cx - 12, lin_y + 8),
        (mes_x + 8, cy - 5),
        (mes_x + 8, cy + 18),
    ]
    din_flat = []
    for p in din_outline:
        din_flat += [p[0], p[1]]
    d.add(Polygon(din_flat, fillColor=DENTIN_COL, strokeColor=None))

    # Pulp outline (oval, more toward labial)
    d.add(Ellipse(cx - 8, cy - 15, cx + 10, cy + 35,
                  fillColor=PULP_COL, strokeColor=None))

    # Cingulum (oval on lingual)
    d.add(Ellipse(cx - 12, lin_y, cx + 12, lin_y + 25,
                  fillColor=colors.HexColor("#d4c4a0"),
                  strokeColor=LINE_COL, strokeWidth=1))

    # Direction labels
    d.add(String(cx - 8, lab_y + 14, "LABIAL", fontSize=7.5,
                 fillColor=BLUE, fontName='Helvetica-Bold'))
    d.add(String(cx - 10, lin_y - 18, "LINGUAL", fontSize=7.5,
                 fillColor=BLUE, fontName='Helvetica-Bold'))
    d.add(String(mes_x - 22, cy, "MESIAL", fontSize=7, fillColor=BLUE,
                 fontName='Helvetica-Bold'))
    d.add(String(dis_x + 3,  cy, "DISTAL", fontSize=7, fillColor=BLUE,
                 fontName='Helvetica-Bold'))

    # Annotations
    def ann(x1, y1, x2, y2, label, anchor='right'):
        d.add(Line(x1, y1, x2, y2, strokeColor=LABEL_COL,
                   strokeWidth=0.8, strokeDashArray=[3, 2]))
        if anchor == 'left':
            d.add(String(x2 + 3, y2 - 3, label, fontSize=7.5,
                         fillColor=LABEL_COL, fontName='Helvetica-Bold'))
        else:
            d.add(String(max(2, x2 - len(label) * 5 - 4), y2 - 3, label, fontSize=7.5,
                         fillColor=LABEL_COL, fontName='Helvetica-Bold'))

    ann(cx - 30, lab_y + 2, 5, lab_y + 15, "Labial Surface (broad convex)", 'right')
    ann(cx - 20, lin_y + 5, 5, lin_y - 10, "Cingulum + Lingual Surface", 'right')
    ann(mes_x, cy + 5,     5, cy - 25,    "Mesial Angle (sharper)", 'right')
    ann(dis_x, cy + 5, dw - 5, cy - 20,   "Distal Angle (rounded)", 'left')
    ann(cx, lab_y + 5, cx + 50, lab_y + 25, "Labio-incisal convexity", 'left')

    return d


# ─────────────────────────────────────────────
# Data tables
# ─────────────────────────────────────────────
def make_measurements_table():
    data = [
        ["Parameter", "Measurement", "Clinical Note"],
        ["Crown Length (Cervico-incisal)", "10.5 mm", "Longest anterior crown"],
        ["Crown Width (Mesio-distal)", "8.5 mm", "Wider than lateral incisor"],
        ["Crown Thickness (Labio-lingual)", "7.0 mm", ""],
        ["Root Length", "13.0 mm", "Single, conical root"],
        ["Crown-Root Length Ratio", "~1:1.25", ""],
        ["CEJ Curvature - Mesial", "3.5 mm", "More pronounced than distal"],
        ["CEJ Curvature - Distal", "2.5 mm", "Less curvature"],
        ["Labio-incisal angle", "Sharp (mesial)", "Important for ID"],
        ["Distoincisal angle", "Rounded", "Key differentiating feature"],
    ]
    t = Table(data, colWidths=[170, 100, 150])
    t.setStyle(TableStyle([
        ('BACKGROUND',  (0, 0), (-1, 0), NAVY),
        ('TEXTCOLOR',   (0, 0), (-1, 0), colors.white),
        ('FONTNAME',    (0, 0), (-1, 0), 'Helvetica-Bold'),
        ('FONTSIZE',    (0, 0), (-1, 0), 10),
        ('FONTNAME',    (0, 1), (-1, -1), 'Helvetica'),
        ('FONTSIZE',    (0, 1), (-1, -1), 9),
        ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, GRAY_LIGHT]),
        ('GRID',        (0, 0), (-1, -1), 0.5, GRAY_MID),
        ('ALIGN',       (1, 0), (1, -1), 'CENTER'),
        ('VALIGN',      (0, 0), (-1, -1), 'MIDDLE'),
        ('TOPPADDING',  (0, 0), (-1, -1), 5),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 5),
    ]))
    return t


def make_comparison_table():
    data = [
        ["Feature", "Maxillary Central", "Maxillary Lateral", "Mandibular Central"],
        ["Crown Width", "8.5 mm", "6.5 mm", "5.0 mm"],
        ["Crown Length", "10.5 mm", "9.0 mm", "9.0 mm"],
        ["Root", "Single, conical", "Single, may be curved", "Single, narrow"],
        ["Incisal edge", "Perpendicular to long axis", "Tilts slightly", "Symmetric"],
        ["Mesio-incisal angle", "Nearly 90°", "More rounded", "~90°"],
        ["Cingulum", "Prominent", "More prominent", "Small"],
        ["Mamelons", "3 lobes visible in young", "3 lobes", "3 lobes"],
    ]
    t = Table(data, colWidths=[130, 115, 115, 115])
    t.setStyle(TableStyle([
        ('BACKGROUND',  (0, 0), (-1, 0), BLUE),
        ('TEXTCOLOR',   (0, 0), (-1, 0), colors.white),
        ('FONTNAME',    (0, 0), (-1, 0), 'Helvetica-Bold'),
        ('FONTSIZE',    (0, 0), (-1, 0), 9),
        ('FONTNAME',    (0, 1), (-1, -1), 'Helvetica'),
        ('FONTSIZE',    (0, 1), (-1, -1), 8.5),
        ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, LIGHT_BLUE]),
        ('GRID',        (0, 0), (-1, -1), 0.5, GRAY_MID),
        ('ALIGN',       (0, 0), (-1, -1), 'CENTER'),
        ('VALIGN',      (0, 0), (-1, -1), 'MIDDLE'),
        ('TOPPADDING',  (0, 0), (-1, -1), 4),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 4),
        ('BACKGROUND',  (0, 1), (0, -1), GRAY_LIGHT),
        ('FONTNAME',    (0, 1), (0, -1), 'Helvetica-Bold'),
    ]))
    return t


def make_pulp_table():
    data = [
        ["Pulp Feature", "Description"],
        ["Pulp Horns", "3 horns corresponding to mamelons (mesial, middle, distal)"],
        ["Pulp Chamber Shape", "Follows outer contour; wider at incisal in young"],
        ["Root Canal", "Single, wide, oval at cervical, tapers to apex"],
        ["Pulp Volume", "Decreases with age (secondary dentin formation)"],
        ["Apical Foramen", "Usually at or near apex; may be 0.5-1mm off apex"],
    ]
    t = Table(data, colWidths=[160, 310])
    t.setStyle(TableStyle([
        ('BACKGROUND',  (0, 0), (-1, 0), colors.HexColor("#e07b39")),
        ('TEXTCOLOR',   (0, 0), (-1, 0), colors.white),
        ('FONTNAME',    (0, 0), (-1, 0), 'Helvetica-Bold'),
        ('FONTSIZE',    (0, 0), (-1, 0), 9.5),
        ('FONTNAME',    (0, 1), (-1, -1), 'Helvetica'),
        ('FONTSIZE',    (0, 1), (-1, -1), 9),
        ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor("#fff3e0")]),
        ('GRID',        (0, 0), (-1, -1), 0.5, GRAY_MID),
        ('VALIGN',      (0, 0), (-1, -1), 'MIDDLE'),
        ('TOPPADDING',  (0, 0), (-1, -1), 5),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 5),
    ]))
    return t


# ─────────────────────────────────────────────
# Custom Flowable for Drawings
# ─────────────────────────────────────────────
class DrawingFlowable(Flowable):
    def __init__(self, drawing, hAlign='CENTER'):
        Flowable.__init__(self)
        self.drawing = drawing
        self.hAlign = hAlign
        self.width  = drawing.width
        self.height = drawing.height

    def draw(self):
        renderPDF.draw(self.drawing, self.canv, 0, 0)


# ─────────────────────────────────────────────
# Cover page background
# ─────────────────────────────────────────────
def cover_background(canvas, doc):
    canvas.saveState()
    canvas.setFillColor(NAVY)
    canvas.rect(0, H - 120, W, 120, fill=1, stroke=0)
    canvas.setFillColor(BLUE)
    canvas.rect(0, 0, W, 40, fill=1, stroke=0)
    canvas.restoreState()


def page_background(canvas, doc):
    canvas.saveState()
    # Header bar
    canvas.setFillColor(NAVY)
    canvas.rect(0, H - 30, W, 30, fill=1, stroke=0)
    canvas.setFillColor(colors.white)
    canvas.setFont('Helvetica-Bold', 9)
    canvas.drawString(20, H - 20, "DENTAL ANATOMY - 1st Year BDS")
    canvas.drawRightString(W - 20, H - 20, "Maxillary Central Incisor")
    # Footer
    canvas.setFillColor(NAVY)
    canvas.rect(0, 0, W, 20, fill=1, stroke=0)
    canvas.setFillColor(colors.white)
    canvas.setFont('Helvetica', 8)
    canvas.drawString(20, 6, "For Educational Use Only")
    canvas.drawRightString(W - 20, 6, f"Page {doc.page}")
    canvas.restoreState()


# ─────────────────────────────────────────────
# BUILD THE PDF
# ─────────────────────────────────────────────
def build_pdf(output_path):
    doc = SimpleDocTemplate(
        output_path, pagesize=A4,
        leftMargin=25*mm, rightMargin=25*mm,
        topMargin=18*mm, bottomMargin=18*mm,
        title="Maxillary Central Incisor - Dental Anatomy",
        author="Dental Anatomy - 1st Year BDS",
        subject="Five Aspects of Maxillary Central Incisor",
    )

    story = []

    # ══════════════════════════════════════════
    # COVER PAGE
    # ══════════════════════════════════════════
    # Top decoration drawing
    cover_top = Drawing(W - 50*mm, 80)
    cover_top.add(Rect(0, 0, W - 50*mm, 80, fillColor=NAVY, strokeColor=None))
    cover_top.add(String(20, 50, "DENTAL ANATOMY", fontSize=28,
                         fillColor=colors.white, fontName='Helvetica-Bold'))
    cover_top.add(String(20, 25, "First Year BDS  |  Tooth Morphology",
                         fontSize=14, fillColor=LIGHT_BLUE, fontName='Helvetica'))
    story.append(DrawingFlowable(cover_top))
    story.append(Spacer(1, 14))

    story.append(Paragraph("Maxillary Central Incisor", ParagraphStyle(
        'CoverTitle', fontSize=26, textColor=NAVY, fontName='Helvetica-Bold',
        alignment=TA_CENTER, leading=32, spaceAfter=6)))

    story.append(Paragraph(
        "A Comprehensive Study of All Five Aspects",
        ParagraphStyle('CoverSub', fontSize=14, textColor=BLUE,
                       fontName='Helvetica', alignment=TA_CENTER, spaceAfter=4)
    ))

    story.append(HRFlowable(width="100%", thickness=2, color=NAVY, spaceAfter=10))

    # Info box
    info_data = [
        ["Tooth", "Maxillary Central Incisor (FDI: 11 / 21 | Universal: #8 / #9)"],
        ["Dentition", "Permanent Dentition"],
        ["Eruption Time", "7 - 8 years"],
        ["Root Completion", "10 years"],
        ["Subject", "Dental Anatomy & Histology"],
        ["Year", "First Year BDS"],
    ]
    info_table = Table(info_data, colWidths=[130, 310])
    info_table.setStyle(TableStyle([
        ('FONTNAME',    (0, 0), (0, -1), 'Helvetica-Bold'),
        ('FONTNAME',    (1, 0), (1, -1), 'Helvetica'),
        ('FONTSIZE',    (0, 0), (-1, -1), 10),
        ('ROWBACKGROUNDS', (0, 0), (-1, -1), [LIGHT_BLUE, colors.white]),
        ('GRID',        (0, 0), (-1, -1), 0.5, GRAY_MID),
        ('TOPPADDING',  (0, 0), (-1, -1), 6),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 6),
        ('LEFTPADDING', (0, 0), (-1, -1), 8),
    ]))
    story.append(info_table)
    story.append(Spacer(1, 12))

    story.append(Paragraph(
        "This document covers the detailed morphology of the maxillary central incisor from "
        "all five study aspects: Labial, Lingual, Mesial, Distal, and Incisal. Each section "
        "includes an annotated diagram and key anatomical features as per the 1st Year BDS "
        "dental anatomy curriculum.",
        body_style
    ))

    story.append(Spacer(1, 10))

    # Quick contents box
    toc_data = [
        ["Section", "Content", "Aspect"],
        ["1", "Introduction & General Features", "Overview"],
        ["2", "Labial Aspect", "Diagram + Points"],
        ["3", "Lingual Aspect", "Diagram + Points"],
        ["4", "Mesial Aspect", "Diagram + Points"],
        ["5", "Distal Aspect", "Diagram + Points"],
        ["6", "Incisal Aspect", "Diagram + Points"],
        ["7", "Pulp Morphology", "Details"],
        ["8", "Measurements & Comparison", "Tables"],
    ]
    toc_table = Table(toc_data, colWidths=[30, 280, 130])
    toc_table.setStyle(TableStyle([
        ('BACKGROUND',  (0, 0), (-1, 0), NAVY),
        ('TEXTCOLOR',   (0, 0), (-1, 0), colors.white),
        ('FONTNAME',    (0, 0), (-1, 0), 'Helvetica-Bold'),
        ('FONTNAME',    (0, 1), (-1, -1), 'Helvetica'),
        ('FONTSIZE',    (0, 0), (-1, -1), 9),
        ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, GRAY_LIGHT]),
        ('GRID',        (0, 0), (-1, -1), 0.5, GRAY_MID),
        ('ALIGN',       (0, 0), (0, -1), 'CENTER'),
        ('TOPPADDING',  (0, 0), (-1, -1), 5),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 5),
    ]))
    story.append(toc_table)

    story.append(PageBreak())

    # ══════════════════════════════════════════
    # SECTION 1 — Introduction
    # ══════════════════════════════════════════
    for el in section_header("1. INTRODUCTION & GENERAL FEATURES",
                              "Overview of the Maxillary Central Incisor"):
        story.append(el)
    story.append(Spacer(1, 8))

    story.append(Paragraph("General Description", sub_section_style))
    story.append(Paragraph(
        "The maxillary central incisor is the largest and most prominent tooth of the anterior "
        "segment. It is designated #8 (right) and #9 (left) in the Universal Numbering System, "
        "and 11 / 21 in the FDI (ISO) system. It is bilaterally symmetrical in the dental arch "
        "and serves the primary functions of incising food and contributing to esthetics, "
        "phonetics, and lip support.", body_style))

    story.append(Paragraph("Classification", sub_section_style))
    class_data = [
        ["Property", "Detail"],
        ["Arch",            "Maxillary (upper jaw)"],
        ["Position",        "Anterior - Central (1st from midline)"],
        ["Function",        "Incising / cutting food"],
        ["Nomenclature",    "FDI: #11 (right), #21 (left)  |  Universal: #8 (right), #9 (left)"],
        ["Number of roots", "One (single-rooted)"],
        ["Number of canals","One (single canal)"],
        ["Number of cusps", "None (incisor - has incisal edge, not cusp)"],
        ["Number of lobes", "4 (3 labial + 1 lingual = cingulum lobe)"],
    ]
    ct = Table(class_data, colWidths=[160, 310])
    ct.setStyle(TableStyle([
        ('BACKGROUND',  (0, 0), (-1, 0), GREEN),
        ('TEXTCOLOR',   (0, 0), (-1, 0), colors.white),
        ('FONTNAME',    (0, 0), (-1, 0), 'Helvetica-Bold'),
        ('FONTNAME',    (0, 1), (0, -1), 'Helvetica-Bold'),
        ('FONTNAME',    (1, 1), (1, -1), 'Helvetica'),
        ('FONTSIZE',    (0, 0), (-1, -1), 9.5),
        ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, LIGHT_GREEN]),
        ('GRID',        (0, 0), (-1, -1), 0.5, GRAY_MID),
        ('TOPPADDING',  (0, 0), (-1, -1), 5),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 5),
        ('LEFTPADDING', (0, 0), (-1, -1), 8),
    ]))
    story.append(ct)
    story.append(Spacer(1, 10))

    story.append(Paragraph("Key Identifying Features", sub_section_style))
    key_features = [
        ("Largest anterior tooth", "Wider crown than all other anterior teeth (8.5 mm MD width)."),
        ("Bilaterally symmetrical", "The right (#11) and left (#21) are mirror images."),
        ("Mesio-incisal angle", "Nearly 90 degrees - sharp right angle; key identification feature."),
        ("Disto-incisal angle", "More rounded and obtuse - another key ID feature."),
        ("Single root", "Conical, well-developed, straight root with a blunt apex."),
        ("Cingulum", "Well-developed oval elevation on lingual cervical third."),
        ("Mamelons", "Three rounded lobes on incisal edge in newly erupted teeth (wear away with age)."),
        ("Labial surface", "Smooth, convex, with two faint developmental grooves dividing three lobes."),
        ("Lingual fossa", "Concave area bounded by mesial/distal marginal ridges and cingulum."),
    ]
    for feat, desc in key_features:
        story.append(Paragraph(
            f"<b>• {feat}:</b> {desc}", bullet_style))

    story.append(Spacer(1, 10))

    story.append(Paragraph("Developmental Lobes", sub_section_style))
    story.append(Paragraph(
        "The crown develops from 4 lobes - 3 labial lobes (mesioincisal, central, distoincisal) "
        "and 1 lingual lobe (cingulum). The mamelons seen on the incisal edge of newly erupted "
        "teeth correspond to the 3 labial lobes. The junction between lobes is marked by "
        "developmental grooves on the labial surface.", body_style))

    story.append(PageBreak())

    # ══════════════════════════════════════════
    # SECTION 2 — Labial Aspect
    # ══════════════════════════════════════════
    for el in section_header("2. LABIAL ASPECT",
                              "View from the front (facial/anterior surface)"):
        story.append(el)
    story.append(Spacer(1, 8))

    # Two-column: diagram left, text right
    labial_diag = DrawingFlowable(draw_labial_aspect())

    labial_text = [
        Paragraph("Crown Outline", sub_section_style),
        Paragraph("<b>• Shape:</b> Trapezoidal from labial view. Wider at incisal, narrower at cervical.", bullet_style),
        Paragraph("<b>• Mesial outline:</b> Relatively straight from cervix to incisal, slight convexity at middle third.", bullet_style),
        Paragraph("<b>• Distal outline:</b> More convex and rounded throughout.", bullet_style),
        Paragraph("<b>• Incisal outline:</b> Nearly straight/flat (may be slightly curved downward).", bullet_style),
        Paragraph("<b>• Cervical outline:</b> Curves apically (convex toward root).", bullet_style),
        Spacer(1, 4),
        Paragraph("Line Angles", sub_section_style),
        Paragraph("<b>• Mesio-incisal angle:</b> Sharp, nearly 90° - KEY distinguishing feature.", bullet_style),
        Paragraph("<b>• Disto-incisal angle:</b> Rounded, more obtuse - helps identify right vs left.", bullet_style),
        Spacer(1, 4),
        Paragraph("Surface Features", sub_section_style),
        Paragraph("<b>• Two developmental grooves</b> divide the labial surface into 3 lobes.", bullet_style),
        Paragraph("<b>• Mamelons:</b> 3 rounded prominences on incisal edge in newly erupted teeth.", bullet_style),
        Paragraph("<b>• Labial surface</b> is smooth and convex both mesio-distally and cervico-incisally.", bullet_style),
        Spacer(1, 4),
        Paragraph("Root (Labial View)", sub_section_style),
        Paragraph("<b>• Shape:</b> Conical, tapers to rounded apex.", bullet_style),
        Paragraph("<b>• Length:</b> ~13 mm; straight or with slight apical bend.", bullet_style),
        Paragraph("<b>• Width:</b> Widest at cervix, tapers uniformly.", bullet_style),
        Paragraph("<b>• Longitudinal lines</b> (root striations) may be visible.", bullet_style),
    ]

    labial_combined = Table(
        [[labial_diag, labial_text]],
        colWidths=[220, 300]
    )
    labial_combined.setStyle(TableStyle([
        ('VALIGN', (0, 0), (-1, -1), 'TOP'),
        ('LEFTPADDING', (0, 0), (-1, -1), 0),
        ('RIGHTPADDING', (0, 0), (-1, -1), 0),
        ('TOPPADDING', (0, 0), (-1, -1), 0),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 0),
    ]))
    story.append(labial_combined)
    story.append(Spacer(1, 4))
    story.append(Paragraph(
        "Fig. 1 - Labial Aspect: Trapezoidal crown with sharp mesio-incisal angle, "
        "rounded disto-incisal angle, three mamelons, and a conical root.", caption_style))

    story.append(Spacer(1, 8))
    story.append(Paragraph(
        "<b>Clinical Note:</b> The sharp mesio-incisal angle and rounded disto-incisal angle "
        "are the most reliable features to distinguish right from left maxillary central incisors. "
        "The labial surface contour guides the placement of veneers and composite restorations.",
        highlight_style))

    story.append(PageBreak())

    # ══════════════════════════════════════════
    # SECTION 3 — Lingual Aspect
    # ══════════════════════════════════════════
    for el in section_header("3. LINGUAL ASPECT",
                              "View from the palatal/posterior surface"):
        story.append(el)
    story.append(Spacer(1, 8))

    lingual_text = [
        Paragraph("Crown Outline", sub_section_style),
        Paragraph("<b>• Shape:</b> Similar to labial but narrower due to lingual convergence.", bullet_style),
        Paragraph("<b>• Crown converges</b> from incisal toward cervical on lingual side.", bullet_style),
        Spacer(1, 4),
        Paragraph("Landmark Features", sub_section_style),
        Paragraph("<b>• Cingulum:</b> Oval/rounded prominence at cervical 1/3. Represents lingual developmental lobe.", bullet_style),
        Paragraph("<b>• Mesial marginal ridge:</b> Elevated ridge on mesial border of lingual surface.", bullet_style),
        Paragraph("<b>• Distal marginal ridge:</b> Elevated ridge on distal border; slightly shorter than mesial.", bullet_style),
        Paragraph("<b>• Lingual fossa:</b> Concave, smooth area between the marginal ridges and cingulum.", bullet_style),
        Paragraph("<b>• Lingual ridge:</b> Faint vertical ridge running down midline of lingual surface (variable).", bullet_style),
        Spacer(1, 4),
        Paragraph("Root (Lingual View)", sub_section_style),
        Paragraph("<b>• Narrower</b> than from labial view due to taper.", bullet_style),
        Paragraph("<b>• Longitudinal depression</b> may be present on lingual root surface.", bullet_style),
        Spacer(1, 4),
        Paragraph("Clinical Importance", sub_section_style),
        Paragraph("<b>• Cingulum</b> provides a ledge for mandibular incisors during occlusion (centric stop).", bullet_style),
        Paragraph("<b>• Palatal surface</b> is the key access point in root canal therapy (access cavity).", bullet_style),
        Paragraph("<b>• Lingual fossa</b> is a common site for pit and fissure sealants in young patients.", bullet_style),
    ]

    lingual_diag = DrawingFlowable(draw_lingual_aspect())
    lingual_combined = Table(
        [[lingual_text, lingual_diag]],
        colWidths=[300, 220]
    )
    lingual_combined.setStyle(TableStyle([
        ('VALIGN', (0, 0), (-1, -1), 'TOP'),
        ('LEFTPADDING', (0, 0), (-1, -1), 0),
        ('RIGHTPADDING', (0, 0), (-1, -1), 0),
        ('TOPPADDING', (0, 0), (-1, -1), 0),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 0),
    ]))
    story.append(lingual_combined)
    story.append(Paragraph(
        "Fig. 2 - Lingual Aspect: Well-developed cingulum, mesial/distal marginal ridges, "
        "concave lingual fossa, and lingual convergence of the crown.", caption_style))

    story.append(Spacer(1, 8))
    story.append(Paragraph(
        "<b>Exam Point:</b> On the lingual aspect, the cingulum is the most prominent feature. "
        "The lingual fossa is concave (vs. convex labial surface). Marginal ridges border the fossa "
        "on mesial and distal sides.",
        highlight_style))

    story.append(PageBreak())

    # ══════════════════════════════════════════
    # SECTION 4 — Mesial Aspect
    # ══════════════════════════════════════════
    for el in section_header("4. MESIAL ASPECT",
                              "View from the side closest to the midline"):
        story.append(el)
    story.append(Spacer(1, 8))

    mesial_diag = DrawingFlowable(draw_mesial_aspect())

    mesial_text = [
        Paragraph("Crown Profile", sub_section_style),
        Paragraph("<b>• Shape:</b> Roughly triangular/wedge-shaped with thick base (cervical) and narrow incisal tip.", bullet_style),
        Paragraph("<b>• Labial outline:</b> Slightly convex from cervix to incisal (greatest convexity at junction of cervical and middle third).", bullet_style),
        Paragraph("<b>• Lingual outline:</b> Concave in middle third (lingual fossa), convex at cervical (cingulum), nearly straight at incisal.", bullet_style),
        Paragraph("<b>• Incisal edge:</b> Labially positioned relative to root axis.", bullet_style),
        Spacer(1, 4),
        Paragraph("CEJ (Cervical Line)", sub_section_style),
        Paragraph("<b>• Mesial CEJ curvature:</b> ~3.5 mm - highly curved toward incisal.", bullet_style),
        Paragraph("<b>• This is the most curved CEJ of any tooth</b> - important exam fact.", bullet_style),
        Spacer(1, 4),
        Paragraph("Root Profile", sub_section_style),
        Paragraph("<b>• Broad</b> mesio-distally, tapers to a rounded apex.", bullet_style),
        Paragraph("<b>• Relatively straight</b> long axis; may have slight distal tilt.", bullet_style),
        Paragraph("<b>• Mesial surface</b> of root has a flat/slight concavity.", bullet_style),
        Spacer(1, 4),
        Paragraph("Enamel Thickness", sub_section_style),
        Paragraph("<b>• Thickest</b> at incisal edge.", bullet_style),
        Paragraph("<b>• Thinnest</b> at cervical margin (CEJ).", bullet_style),
    ]

    mesial_combined = Table(
        [[mesial_diag, mesial_text]],
        colWidths=[220, 300]
    )
    mesial_combined.setStyle(TableStyle([
        ('VALIGN', (0, 0), (-1, -1), 'TOP'),
        ('LEFTPADDING', (0, 0), (-1, -1), 0),
        ('RIGHTPADDING', (0, 0), (-1, -1), 0),
        ('TOPPADDING', (0, 0), (-1, -1), 0),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 0),
    ]))
    story.append(mesial_combined)
    story.append(Paragraph(
        "Fig. 3 - Mesial Aspect: Wedge-shaped crown with labial convexity, concave lingual profile, "
        "prominent CEJ curvature (~3.5 mm), and broad root.", caption_style))

    story.append(Spacer(1, 8))
    story.append(Paragraph(
        "<b>Key Fact:</b> The mesial CEJ curvature (3.5 mm) is the greatest curvature of any CEJ "
        "in the permanent dentition. This is a frequently tested fact in 1st year BDS exams.",
        highlight_style))

    story.append(PageBreak())

    # ══════════════════════════════════════════
    # SECTION 5 — Distal Aspect
    # ══════════════════════════════════════════
    for el in section_header("5. DISTAL ASPECT",
                              "View from the side away from the midline"):
        story.append(el)
    story.append(Spacer(1, 8))

    distal_text = [
        Paragraph("Crown Profile", sub_section_style),
        Paragraph("<b>• Very similar to mesial aspect</b> but with the following differences:", bullet_style),
        Paragraph("<b>• Distal CEJ curvature:</b> ~2.5 mm - LESS curved than mesial (~3.5 mm).", bullet_style),
        Paragraph("<b>• Distal outline</b> of crown is more rounded/convex than mesial.", bullet_style),
        Paragraph("<b>• Incisal edge</b> appears slightly tilted distally when viewed from this side.", bullet_style),
        Spacer(1, 4),
        Paragraph("CEJ Comparison", sub_section_style),
        Paragraph("<b>• Mesial CEJ:</b> 3.5 mm curvature (more pronounced).", bullet_style),
        Paragraph("<b>• Distal CEJ:</b> 2.5 mm curvature (less pronounced).", bullet_style),
        Paragraph("<b>• Both curve toward incisal</b> (convex toward crown).", bullet_style),
        Spacer(1, 4),
        Paragraph("Root Profile", sub_section_style),
        Paragraph("<b>• Similar to mesial</b> but slightly narrower at cervical.", bullet_style),
        Paragraph("<b>• Distal root surface</b> is flatter than mesial.", bullet_style),
        Paragraph("<b>• May taper or curve slightly distally</b> at apex.", bullet_style),
        Spacer(1, 4),
        Paragraph("Contact Area", sub_section_style),
        Paragraph("<b>• Distal contact area</b> is at the junction of incisal and middle thirds.", bullet_style),
        Paragraph("<b>• Contact is with</b> the mesial surface of the maxillary lateral incisor.", bullet_style),
        Paragraph("<b>• More cervically positioned</b> than mesial contact area.", bullet_style),
    ]

    distal_diag = DrawingFlowable(draw_distal_aspect())
    distal_combined = Table(
        [[distal_text, distal_diag]],
        colWidths=[300, 220]
    )
    distal_combined.setStyle(TableStyle([
        ('VALIGN', (0, 0), (-1, -1), 'TOP'),
        ('LEFTPADDING', (0, 0), (-1, -1), 0),
        ('RIGHTPADDING', (0, 0), (-1, -1), 0),
        ('TOPPADDING', (0, 0), (-1, -1), 0),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 0),
    ]))
    story.append(distal_combined)
    story.append(Paragraph(
        "Fig. 4 - Distal Aspect: Similar to mesial but with reduced CEJ curvature (2.5 mm). "
        "Distal contact with lateral incisor at incisal-middle third junction.", caption_style))

    story.append(Spacer(1, 8))
    story.append(Paragraph(
        "<b>Memory Aid:</b> Mesial CEJ = More curved (3.5 mm). Distal CEJ = Less curved (2.5 mm). "
        "Both CEJ curves are convex toward the crown (incisally directed).",
        highlight_style))

    story.append(PageBreak())

    # ══════════════════════════════════════════
    # SECTION 6 — Incisal Aspect
    # ══════════════════════════════════════════
    for el in section_header("6. INCISAL ASPECT",
                              "View from above (looking down the long axis of the tooth)"):
        story.append(el)
    story.append(Spacer(1, 8))

    incisal_diag = DrawingFlowable(draw_incisal_aspect())

    incisal_text = [
        Paragraph("Crown Outline", sub_section_style),
        Paragraph("<b>• Shape:</b> Ovoid/D-shaped when viewed from incisal.", bullet_style),
        Paragraph("<b>• Labial outline:</b> Broad, slightly convex arch - the widest part.", bullet_style),
        Paragraph("<b>• Lingual outline:</b> Narrower, shows cingulum prominently.", bullet_style),
        Paragraph("<b>• Mesial outline:</b> Slightly flatter/straighter.", bullet_style),
        Paragraph("<b>• Distal outline:</b> More rounded/convex.", bullet_style),
        Spacer(1, 4),
        Paragraph("Labio-lingual Profile", sub_section_style),
        Paragraph("<b>• Labial surface</b> is broader and more labially positioned.", bullet_style),
        Paragraph("<b>• Lingual surface</b> is narrower; cingulum shifts mass lingually at cervical.", bullet_style),
        Paragraph("<b>• Enamel thickness</b> is greatest at incisal edge from this view.", bullet_style),
        Spacer(1, 4),
        Paragraph("Angles and Surfaces", sub_section_style),
        Paragraph("<b>• 4 line angles:</b> Mesio-labial, disto-labial, mesio-lingual, disto-lingual.", bullet_style),
        Paragraph("<b>• 4 surfaces:</b> Labial, lingual, mesial, distal.", bullet_style),
        Paragraph("<b>• Pulp horn positions</b> correspond to 3 mamelons.", bullet_style),
        Spacer(1, 4),
        Paragraph("Root Axis (Incisal View)", sub_section_style),
        Paragraph("<b>• Root is centered</b> under crown from this view.", bullet_style),
        Paragraph("<b>• Crown width > root width</b> (crown overhang labially).", bullet_style),
    ]

    incisal_combined = Table(
        [[incisal_diag, incisal_text]],
        colWidths=[220, 300]
    )
    incisal_combined.setStyle(TableStyle([
        ('VALIGN', (0, 0), (-1, -1), 'TOP'),
        ('LEFTPADDING', (0, 0), (-1, -1), 0),
        ('RIGHTPADDING', (0, 0), (-1, -1), 0),
        ('TOPPADDING', (0, 0), (-1, -1), 0),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 0),
    ]))
    story.append(incisal_combined)
    story.append(Paragraph(
        "Fig. 5 - Incisal Aspect: D-shaped/ovoid outline with broad labial arch, narrow lingual "
        "showing cingulum, sharp mesial angle, and rounded distal angle.", caption_style))

    story.append(Spacer(1, 8))
    story.append(Paragraph(
        "<b>Key Observation:</b> From the incisal view, the crown is wider labio-lingually than "
        "any other incisor. The cingulum is visible on the lingual side. The mesio-labial line "
        "angle is more acute than the disto-labial line angle.",
        highlight_style))

    story.append(PageBreak())

    # ══════════════════════════════════════════
    # SECTION 7 — Pulp Morphology
    # ══════════════════════════════════════════
    for el in section_header("7. PULP MORPHOLOGY",
                              "Internal pulp cavity features"):
        story.append(el)
    story.append(Spacer(1, 8))

    story.append(Paragraph(
        "The pulp cavity of the maxillary central incisor follows the external contour of the "
        "crown and root. Understanding pulp morphology is essential for endodontic access cavity "
        "preparation and root canal treatment.", body_style))
    story.append(Spacer(1, 6))

    story.append(make_pulp_table())
    story.append(Spacer(1, 10))

    story.append(Paragraph("Pulp Horns", sub_section_style))
    story.append(Paragraph(
        "There are 3 pulp horns corresponding to the 3 developmental lobes (mamelons). "
        "In young patients, the pulp horns extend close to the incisal edge. The mesial pulp horn "
        "is typically the longest, followed by the central and distal pulp horns.", body_style))

    story.append(Paragraph("Root Canal", sub_section_style))
    story.append(Paragraph(
        "The root canal is single and wide. It is oval in cross-section at the cervical third "
        "(labio-lingually wider), becomes more circular in the middle third, and tapers to the "
        "apical foramen. The canal is readily accessible from the lingual access cavity.", body_style))

    story.append(Paragraph("Access Cavity", sub_section_style))
    story.append(Paragraph(
        "The endodontic access cavity is prepared on the lingual surface, typically a triangular "
        "outline with the base at the incisal and apex toward the cingulum. The outline follows the "
        "shape of the pulp chamber.", body_style))

    story.append(Spacer(1, 8))
    story.append(Paragraph(
        "<b>Clinical Note:</b> Trauma to the maxillary central incisor is common in children. "
        "In traumatic injuries, the wide pulp chamber in young teeth makes pulp capping procedures "
        "more feasible. In adults, the narrowed pulp space may complicate canal negotiation.",
        highlight_style))

    story.append(PageBreak())

    # ══════════════════════════════════════════
    # SECTION 8 — Measurements & Comparison
    # ══════════════════════════════════════════
    for el in section_header("8. MEASUREMENTS & COMPARISON TABLE",
                              "Key dimensions and comparison with adjacent teeth"):
        story.append(el)
    story.append(Spacer(1, 8))

    story.append(Paragraph("Standard Measurements (Ash/Nelson)", sub_section_style))
    story.append(make_measurements_table())
    story.append(Spacer(1, 10))

    story.append(Paragraph("Comparison with Adjacent Teeth", sub_section_style))
    story.append(make_comparison_table())
    story.append(Spacer(1, 10))

    # ── Contact areas summary ──
    story.append(Paragraph("Contact Areas", sub_section_style))
    contact_data = [
        ["Surface", "Contact With", "Level of Contact"],
        ["Mesial", "Contralateral central incisor (#21/#11)", "Incisal third"],
        ["Distal", "Maxillary lateral incisor (#12/#22)", "Junction of incisal & middle thirds"],
    ]
    ct2 = Table(contact_data, colWidths=[100, 250, 175])
    ct2.setStyle(TableStyle([
        ('BACKGROUND',  (0, 0), (-1, 0), NAVY),
        ('TEXTCOLOR',   (0, 0), (-1, 0), colors.white),
        ('FONTNAME',    (0, 0), (-1, 0), 'Helvetica-Bold'),
        ('FONTNAME',    (0, 1), (-1, -1), 'Helvetica'),
        ('FONTSIZE',    (0, 0), (-1, -1), 9.5),
        ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, GRAY_LIGHT]),
        ('GRID',        (0, 0), (-1, -1), 0.5, GRAY_MID),
        ('ALIGN',       (0, 0), (-1, -1), 'CENTER'),
        ('VALIGN',      (0, 0), (-1, -1), 'MIDDLE'),
        ('TOPPADDING',  (0, 0), (-1, -1), 6),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 6),
    ]))
    story.append(ct2)
    story.append(Spacer(1, 10))

    # ── Exam high-yield points ──
    story.append(Paragraph("High-Yield Exam Points (Summary)", sub_section_style))
    exam_pts = [
        "Largest anterior tooth in the mouth",
        "Mesio-incisal angle = nearly 90° (sharp); Disto-incisal angle = rounded",
        "Mesial CEJ curvature = 3.5 mm (MOST curved CEJ in dentition)",
        "Distal CEJ curvature = 2.5 mm",
        "4 developmental lobes: 3 labial + 1 lingual (cingulum)",
        "3 mamelons on incisal edge (correspond to 3 labial lobes)",
        "Single, conical root: ~13 mm long",
        "Cingulum = most prominent feature of lingual surface",
        "Lingual fossa = concave area between marginal ridges and cingulum",
        "Incisal view: D-shaped / ovoid outline",
        "FDI: #11 (right), #21 (left) | Universal: #8 (right), #9 (left)",
        "Eruption: 7-8 years; Root completion: 10 years",
    ]
    for i, pt in enumerate(exam_pts, 1):
        story.append(Paragraph(f"<b>{i}.</b> {pt}", bullet_style))

    story.append(Spacer(1, 8))
    story.append(HRFlowable(width="100%", thickness=1, color=GRAY_MID))
    story.append(Spacer(1, 4))
    story.append(Paragraph(
        "References: Wheeler's Dental Anatomy, Physiology and Occlusion | "
        "Ash & Nelson - Wheeler's Dental Anatomy | Berkovitz, Holland & Moxham - Oral Anatomy",
        caption_style))

    # Build
    doc.build(story, onFirstPage=page_background, onLaterPages=page_background)
    print(f"PDF generated: {output_path}")


if __name__ == "__main__":
    out = "/home/daytona/workspace/dental-anatomy/Maxillary_Central_Incisor_Dental_Anatomy.pdf"
    build_pdf(out)
Running Command

cd /home/daytona/workspace/dental-anatomy && python generate_pdf.py 2>&1

Your credits are exhausted. Your daily credits reset tomorrow, or you can purchase a top-up for more.
This is a shared conversation. Sign in to Orris to start your own chat.