Generate a pdf for the dental histology 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/maxillary-incisor-pdf && ls /home/daytona/works…

Writing File

~/maxillary-incisor-pdf/generate_pdf.py

#!/usr/bin/env python3
"""
Dental Histology - Maxillary Central Incisor
1st Year BDS Study Paper
Generates a comprehensive PDF with all 5 aspects + diagrams
"""

from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import inch, cm, mm
from reportlab.lib import colors
from reportlab.platypus import (
    SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
    PageBreak, HRFlowable, KeepTogether
)
from reportlab.graphics.shapes import (
    Drawing, Path, Circle, Ellipse, Line, Rect, String, Polygon, Group
)
from reportlab.graphics import renderPDF
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
from reportlab.graphics.shapes import ArcPath
import math

# ─── Colours ──────────────────────────────────────────────────────────────────
IVORY       = colors.HexColor('#FFFDE7')
ENAMEL_CLR  = colors.HexColor('#E8F4FD')
DENTIN_CLR  = colors.HexColor('#FFF3CD')
PULP_CLR    = colors.HexColor('#FFB3BA')
CEMENTUM_CLR= colors.HexColor('#C8E6C9')
OUTLINE_CLR = colors.HexColor('#1A237E')
ANNOT_CLR   = colors.HexColor('#D32F2F')
LABEL_CLR   = colors.HexColor('#0D47A1')
BG_CLR      = colors.HexColor('#F0F4FF')
TITLE_CLR   = colors.HexColor('#1A237E')
SECTION_CLR = colors.HexColor('#283593')
SUBHEAD_CLR = colors.HexColor('#3949AB')

# ─── Page Setup ───────────────────────────────────────────────────────────────
PAGE_W, PAGE_H = A4
MARGIN = 2 * cm

doc = SimpleDocTemplate(
    '/home/daytona/workspace/maxillary-incisor-pdf/Maxillary_Central_Incisor_BDS1.pdf',
    pagesize=A4,
    rightMargin=MARGIN, leftMargin=MARGIN,
    topMargin=MARGIN, bottomMargin=MARGIN,
    title='Dental Histology – Maxillary Central Incisor',
    author='1st Year BDS – Dental Anatomy & Histology',
)

styles = getSampleStyleSheet()

title_style = ParagraphStyle('Title', parent=styles['Title'],
    fontSize=22, textColor=TITLE_CLR, spaceAfter=4,
    fontName='Helvetica-Bold', alignment=TA_CENTER)

subtitle_style = ParagraphStyle('Subtitle', parent=styles['Normal'],
    fontSize=13, textColor=SECTION_CLR, spaceAfter=2,
    fontName='Helvetica-Bold', alignment=TA_CENTER)

section_style = ParagraphStyle('Section', parent=styles['Heading1'],
    fontSize=15, textColor=TITLE_CLR, spaceBefore=10, spaceAfter=4,
    fontName='Helvetica-Bold',
    borderPad=4, borderWidth=0,
    backColor=colors.HexColor('#E8EAF6'),
    leftIndent=-5, rightIndent=-5,
    borderRadius=4)

subsection_style = ParagraphStyle('Subsection', parent=styles['Heading2'],
    fontSize=12, textColor=SUBHEAD_CLR, spaceBefore=6, spaceAfter=3,
    fontName='Helvetica-Bold')

body_style = ParagraphStyle('Body', parent=styles['Normal'],
    fontSize=10, leading=15, spaceAfter=4,
    fontName='Helvetica', alignment=TA_JUSTIFY,
    leftIndent=5)

bullet_style = ParagraphStyle('Bullet', parent=styles['Normal'],
    fontSize=10, leading=14, spaceAfter=2,
    fontName='Helvetica', leftIndent=15, bulletIndent=5)

caption_style = ParagraphStyle('Caption', parent=styles['Normal'],
    fontSize=9, textColor=SECTION_CLR, spaceAfter=6,
    fontName='Helvetica-BoldOblique', alignment=TA_CENTER)

table_header_style = ParagraphStyle('TableHeader', parent=styles['Normal'],
    fontSize=9, textColor=colors.white, fontName='Helvetica-Bold', alignment=TA_CENTER)

table_cell_style = ParagraphStyle('TableCell', parent=styles['Normal'],
    fontSize=9, fontName='Helvetica', leading=12)

note_style = ParagraphStyle('Note', parent=styles['Normal'],
    fontSize=9, textColor=colors.HexColor('#4A148C'),
    fontName='Helvetica-Oblique', leftIndent=10,
    backColor=colors.HexColor('#F3E5F5'),
    borderPad=4, spaceAfter=6)

# ═══════════════════════════════════════════════════════════════════════════════
#  DIAGRAM HELPERS
# ═══════════════════════════════════════════════════════════════════════════════

def add_label(d, x, y, text, anchor='middle', color=LABEL_CLR, size=7.5, bold=False):
    font = 'Helvetica-Bold' if bold else 'Helvetica'
    d.add(String(x, y, text, fontSize=size, fillColor=color,
                 textAnchor=anchor, fontName=font))

def add_arrow_label(d, lx, ly, tx, ty, text, color=ANNOT_CLR, size=7.5):
    """Arrow from label text to tooth surface."""
    d.add(Line(lx, ly, tx, ty, strokeColor=color, strokeWidth=0.6,
               strokeDashArray=[2, 1]))
    d.add(Circle(tx, ty, 1.5, fillColor=color, strokeColor=color, strokeWidth=0))
    anchor = 'end' if lx < tx else 'start'
    d.add(String(lx, ly + 2, text, fontSize=size, fillColor=color,
                 textAnchor=anchor, fontName='Helvetica'))


def make_labial_diagram(w=220, h=290):
    """
    Labial aspect of maxillary central incisor.
    Crown: trapezoid-like shape wider at incisal edge.
    Root: tapered cone.
    """
    d = Drawing(w, h)
    d.add(Rect(0, 0, w, h, fillColor=BG_CLR, strokeColor=None))

    cx = w / 2
    root_base_y = 20
    cementoenamel_y = 105   # CEJ
    incisal_y = 245

    # ── Root ──
    root_l = cx - 14
    root_r = cx + 14
    root_tip_x = cx
    root_tip_y = root_base_y

    root_path = Path(fillColor=CEMENTUM_CLR, strokeColor=OUTLINE_CLR, strokeWidth=1.4)
    root_path.moveTo(root_l, cementoenamel_y)
    root_path.curveTo(root_l - 2, cementoenamel_y - 30, root_tip_x - 6, root_tip_y + 20, root_tip_x, root_tip_y)
    root_path.curveTo(root_tip_x + 6, root_tip_y + 20, root_r + 2, cementoenamel_y - 30, root_r, cementoenamel_y)
    root_path.closePath()
    d.add(root_path)

    # ── Crown ──
    crown_ml = cx - 27   # mesial labial at CEJ
    crown_mr = cx + 20   # distal labial at CEJ (asymmetric)
    crown_il = cx - 30   # mesial incisal
    crown_ir = cx + 26   # distal incisal

    crown_path = Path(fillColor=ENAMEL_CLR, strokeColor=OUTLINE_CLR, strokeWidth=1.6)
    crown_path.moveTo(crown_ml, cementoenamel_y)
    # mesial surface – nearly straight
    crown_path.curveTo(crown_il + 2, cementoenamel_y + 30, crown_il, incisal_y - 20, crown_il, incisal_y)
    # incisal edge
    crown_path.lineTo(crown_ir, incisal_y)
    # distal surface – slight convexity
    crown_path.curveTo(crown_ir, incisal_y - 20, crown_mr + 4, cementoenamel_y + 30, crown_mr, cementoenamel_y)
    crown_path.closePath()
    d.add(crown_path)

    # ── CEJ line ──
    d.add(Line(crown_ml - 5, cementoenamel_y, crown_mr + 5, cementoenamel_y,
               strokeColor=colors.HexColor('#6D4C41'), strokeWidth=1, strokeDashArray=[4,2]))

    # ── Incisal edge ridge (highlight) ──
    d.add(Line(crown_il + 3, incisal_y, crown_ir - 3, incisal_y,
               strokeColor=colors.HexColor('#4FC3F7'), strokeWidth=2.5))

    # ── Developmental grooves on labial surface ──
    for gx in [cx - 8, cx + 8]:
        d.add(Line(gx, incisal_y - 8, gx, incisal_y - 50,
                   strokeColor=colors.HexColor('#B0BEC5'), strokeWidth=0.8, strokeDashArray=[3, 3]))

    # ── Pulp canal outline inside root ──
    canal_path = Path(fillColor=PULP_CLR, strokeColor=ANNOT_CLR, strokeWidth=0.8)
    canal_path.moveTo(cx - 5, cementoenamel_y - 5)
    canal_path.curveTo(cx - 4, cementoenamel_y - 25, cx - 2, root_tip_y + 30, cx, root_tip_y + 12)
    canal_path.curveTo(cx + 2, root_tip_y + 30, cx + 4, cementoenamel_y - 25, cx + 5, cementoenamel_y - 5)
    canal_path.closePath()
    d.add(canal_path)

    # ── Pulp chamber outline inside crown ──
    pc_path = Path(fillColor=PULP_CLR, strokeColor=ANNOT_CLR, strokeWidth=0.8)
    pc_path.moveTo(cx - 9, cementoenamel_y + 5)
    pc_path.curveTo(cx - 10, cementoenamel_y + 50, cx - 7, incisal_y - 25, cx - 5, incisal_y - 12)
    pc_path.lineTo(cx + 5, incisal_y - 12)
    pc_path.curveTo(cx + 7, incisal_y - 25, cx + 10, cementoenamel_y + 50, cx + 9, cementoenamel_y + 5)
    pc_path.closePath()
    d.add(pc_path)

    # ── Pulp horn ──
    d.add(Line(cx - 5, incisal_y - 12, cx - 10, incisal_y - 5,
               strokeColor=PULP_CLR, strokeWidth=2))
    d.add(Line(cx + 5, incisal_y - 12, cx + 10, incisal_y - 5,
               strokeColor=PULP_CLR, strokeWidth=2))

    # ── Annotations ──
    # Enamel
    add_arrow_label(d, 12, incisal_y - 40, crown_ml + 5, incisal_y - 60, 'Enamel')
    # Dentin (inside crown)
    add_arrow_label(d, 12, incisal_y - 80, crown_ml + 10, incisal_y - 100, 'Dentine')
    # Pulp chamber
    add_arrow_label(d, w - 15, incisal_y - 60, cx + 7, incisal_y - 40, 'Pulp Chamber', color=ANNOT_CLR)
    # CEJ
    add_arrow_label(d, w - 15, cementoenamel_y + 5, crown_mr + 2, cementoenamel_y, 'CEJ', color=colors.HexColor('#4E342E'))
    # Cementum
    add_arrow_label(d, w - 15, cementoenamel_y - 30, root_r - 2, cementoenamel_y - 25, 'Cementum')
    # Root canal
    add_arrow_label(d, w - 15, root_base_y + 30, cx + 4, root_base_y + 40, 'Root Canal')
    # Mesial contact
    add_label(d, crown_il + 4, incisal_y + 6, 'Mesial', size=7, color=colors.HexColor('#1B5E20'))
    # Distal contact
    add_label(d, crown_ir - 4, incisal_y + 6, 'Distal', size=7, color=colors.HexColor('#1B5E20'))
    # Incisal edge
    add_label(d, cx, incisal_y + 14, 'Incisal Edge', size=7.5, color=colors.HexColor('#006064'), bold=True)

    # ── Dimension arrows (crown length) ──
    d.add(Line(crown_ml - 20, cementoenamel_y, crown_ml - 20, incisal_y,
               strokeColor=colors.HexColor('#546E7A'), strokeWidth=0.7))
    d.add(Line(crown_ml - 23, cementoenamel_y, crown_ml - 17, cementoenamel_y,
               strokeColor=colors.HexColor('#546E7A'), strokeWidth=0.7))
    d.add(Line(crown_ml - 23, incisal_y, crown_ml - 17, incisal_y,
               strokeColor=colors.HexColor('#546E7A'), strokeWidth=0.7))
    add_label(d, crown_ml - 32, (cementoenamel_y + incisal_y) / 2, '~10.5mm',
              size=6.5, color=colors.HexColor('#546E7A'), anchor='end')

    # ── Title ──
    add_label(d, cx, h - 12, 'LABIAL ASPECT', size=9, color=TITLE_CLR, bold=True)

    return d


def make_lingual_diagram(w=220, h=290):
    """
    Lingual aspect – narrower, shows cingulum, marginal ridges, lingual fossa.
    """
    d = Drawing(w, h)
    d.add(Rect(0, 0, w, h, fillColor=BG_CLR, strokeColor=None))

    cx = w / 2
    cementoenamel_y = 105
    incisal_y = 245
    root_base_y = 20

    # ── Root ──
    root_path = Path(fillColor=CEMENTUM_CLR, strokeColor=OUTLINE_CLR, strokeWidth=1.4)
    root_path.moveTo(cx - 13, cementoenamel_y)
    root_path.curveTo(cx - 12, cementoenamel_y - 28, cx - 5, root_base_y + 20, cx, root_base_y)
    root_path.curveTo(cx + 5, root_base_y + 20, cx + 12, cementoenamel_y - 28, cx + 13, cementoenamel_y)
    root_path.closePath()
    d.add(root_path)

    # ── Crown (lingual – narrower) ──
    crown_path = Path(fillColor=ENAMEL_CLR, strokeColor=OUTLINE_CLR, strokeWidth=1.6)
    crown_path.moveTo(cx - 23, cementoenamel_y)   # mesial CEJ
    # mesial margin
    crown_path.curveTo(cx - 24, cementoenamel_y + 30, cx - 26, incisal_y - 20, cx - 26, incisal_y)
    # incisal
    crown_path.lineTo(cx + 22, incisal_y)
    # distal margin
    crown_path.curveTo(cx + 22, incisal_y - 20, cx + 20, cementoenamel_y + 30, cx + 19, cementoenamel_y)
    crown_path.closePath()
    d.add(crown_path)

    # ── Cingulum (cervical bulge on lingual) ──
    cingulum = Path(fillColor=colors.HexColor('#FFF9C4'), strokeColor=OUTLINE_CLR, strokeWidth=1.0)
    cingulum.moveTo(cx - 14, cementoenamel_y + 4)
    cingulum.curveTo(cx - 15, cementoenamel_y + 20, cx - 10, cementoenamel_y + 32, cx, cementoenamel_y + 32)
    cingulum.curveTo(cx + 10, cementoenamel_y + 32, cx + 15, cementoenamel_y + 20, cx + 14, cementoenamel_y + 4)
    cingulum.closePath()
    d.add(cingulum)

    # ── Mesial marginal ridge ──
    d.add(Line(cx - 26, incisal_y - 15, cx - 15, cementoenamel_y + 55,
               strokeColor=colors.HexColor('#8D6E63'), strokeWidth=1.2))
    # ── Distal marginal ridge ──
    d.add(Line(cx + 22, incisal_y - 15, cx + 12, cementoenamel_y + 55,
               strokeColor=colors.HexColor('#8D6E63'), strokeWidth=1.2))

    # ── Lingual fossa (ellipse) ──
    fossa = Ellipse(cx, (cementoenamel_y + 40 + incisal_y - 20) / 2,
                    12, 28, fillColor=colors.HexColor('#DCEDC8'),
                    strokeColor=colors.HexColor('#558B2F'), strokeWidth=0.9)
    d.add(fossa)

    # ── CEJ line ──
    d.add(Line(cx - 26, cementoenamel_y, cx + 22, cementoenamel_y,
               strokeColor=colors.HexColor('#6D4C41'), strokeWidth=1, strokeDashArray=[4, 2]))

    # ── Pulp horn outline ──
    pc_path = Path(fillColor=PULP_CLR, strokeColor=ANNOT_CLR, strokeWidth=0.7)
    pc_path.moveTo(cx - 8, cementoenamel_y + 6)
    pc_path.curveTo(cx - 9, cementoenamel_y + 45, cx - 5, incisal_y - 30, cx - 4, incisal_y - 15)
    pc_path.lineTo(cx + 4, incisal_y - 15)
    pc_path.curveTo(cx + 5, incisal_y - 30, cx + 9, cementoenamel_y + 45, cx + 8, cementoenamel_y + 6)
    pc_path.closePath()
    d.add(pc_path)

    # ── Root canal ──
    canal_path = Path(fillColor=PULP_CLR, strokeColor=ANNOT_CLR, strokeWidth=0.7)
    canal_path.moveTo(cx - 4, cementoenamel_y - 3)
    canal_path.curveTo(cx - 3, cementoenamel_y - 22, cx - 2, root_base_y + 28, cx, root_base_y + 10)
    canal_path.curveTo(cx + 2, root_base_y + 28, cx + 3, cementoenamel_y - 22, cx + 4, cementoenamel_y - 3)
    canal_path.closePath()
    d.add(canal_path)

    # ── Annotations ──
    add_arrow_label(d, 10, cementoenamel_y + 20, cx - 12, cementoenamel_y + 18, 'Cingulum', color=colors.HexColor('#F57F17'))
    add_arrow_label(d, 10, cementoenamel_y + 60, cx - 18, cementoenamel_y + 55, 'Mesial\nMarginal Ridge', color=colors.HexColor('#4E342E'))
    add_arrow_label(d, w - 12, cementoenamel_y + 60, cx + 15, cementoenamel_y + 55, 'Distal\nMarginal Ridge', color=colors.HexColor('#4E342E'))
    add_arrow_label(d, w - 12, cementoenamel_y + 100, cx + 10, (cementoenamel_y + 80 + incisal_y - 20) / 2, 'Lingual\nFossa', color=colors.HexColor('#33691E'))
    add_arrow_label(d, w - 12, cementoenamel_y - 25, cx + 11, cementoenamel_y - 20, 'Cementum')
    add_arrow_label(d, 10, incisal_y - 50, cx - 5, incisal_y - 40, 'Pulp\nChamber', color=ANNOT_CLR)
    add_label(d, cx, cementoenamel_y - 10, 'CEJ', size=7, color=colors.HexColor('#4E342E'), bold=True)
    add_label(d, cx, h - 12, 'LINGUAL ASPECT', size=9, color=TITLE_CLR, bold=True)

    return d


def make_mesial_diagram(w=220, h=290):
    """
    Mesial aspect – shows triangular crown profile, contact area, and labio-lingual width.
    """
    d = Drawing(w, h)
    d.add(Rect(0, 0, w, h, fillColor=BG_CLR, strokeColor=None))

    cx = w / 2
    cementoenamel_y = 100
    incisal_y = 240
    root_base_y = 18

    # ── Root (mesial view – oval/round cross section shown as tapered outline) ──
    root_path = Path(fillColor=CEMENTUM_CLR, strokeColor=OUTLINE_CLR, strokeWidth=1.4)
    root_path.moveTo(cx - 15, cementoenamel_y)
    root_path.curveTo(cx - 14, cementoenamel_y - 28, cx - 5, root_base_y + 22, cx, root_base_y)
    root_path.curveTo(cx + 5, root_base_y + 22, cx + 14, cementoenamel_y - 28, cx + 15, cementoenamel_y)
    root_path.closePath()
    d.add(root_path)

    # ── Crown (triangular, labial side left, lingual side right) ──
    lab_l = cx - 20   # labial surface
    ling_r = cx + 18  # lingual surface
    cej_y = cementoenamel_y

    crown_path = Path(fillColor=ENAMEL_CLR, strokeColor=OUTLINE_CLR, strokeWidth=1.6)
    crown_path.moveTo(lab_l, cej_y)
    # labial profile – convex outward
    crown_path.curveTo(lab_l - 5, cej_y + 40, lab_l - 6, incisal_y - 30, lab_l, incisal_y)
    # incisal edge (tip)
    crown_path.lineTo(lab_l + 10, incisal_y + 5)   # slight labial inclination
    # lingual profile – slightly concave (lingual fossa region)
    crown_path.curveTo(ling_r + 4, incisal_y - 20, ling_r + 6, cementoenamel_y + 50, ling_r, cej_y)
    crown_path.closePath()
    d.add(crown_path)

    # ── Cingulum bump on lingual ──
    d.add(Ellipse(ling_r + 2, cej_y + 22, 5, 12,
                  fillColor=colors.HexColor('#FFF9C4'),
                  strokeColor=OUTLINE_CLR, strokeWidth=0.8))

    # ── CEJ line ──
    d.add(Line(lab_l - 2, cej_y, ling_r + 2, cej_y,
               strokeColor=colors.HexColor('#6D4C41'), strokeWidth=1, strokeDashArray=[4, 2]))

    # ── Contact area (a small highlighted oval near incisal 1/3) ──
    contact_y = incisal_y - 35
    contact_x = lab_l - 2
    d.add(Ellipse(contact_x, contact_y, 4, 9,
                  fillColor=colors.HexColor('#FF8A65'),
                  strokeColor=colors.HexColor('#BF360C'), strokeWidth=1))

    # ── Enamel + dentin differentiation ──
    enamel_thick = 4
    # enamel outline on labial
    d.add(Line(lab_l, cej_y + 20, lab_l - enamel_thick, cej_y + 20,
               strokeColor=ENAMEL_CLR, strokeWidth=2))

    # ── Pulp chamber ──
    pc_path = Path(fillColor=PULP_CLR, strokeColor=ANNOT_CLR, strokeWidth=0.8)
    pc_path.moveTo(cx - 7, cej_y + 5)
    pc_path.curveTo(cx - 8, cej_y + 40, cx - 7, incisal_y - 28, cx - 5, incisal_y - 12)
    pc_path.lineTo(cx + 4, incisal_y - 12)
    pc_path.curveTo(cx + 6, incisal_y - 28, cx + 7, cej_y + 40, cx + 6, cej_y + 5)
    pc_path.closePath()
    d.add(pc_path)

    # ── Root canal ──
    canal_path = Path(fillColor=PULP_CLR, strokeColor=ANNOT_CLR, strokeWidth=0.7)
    canal_path.moveTo(cx - 4, cej_y - 2)
    canal_path.curveTo(cx - 3, cej_y - 24, cx - 2, root_base_y + 28, cx, root_base_y + 10)
    canal_path.curveTo(cx + 2, root_base_y + 28, cx + 3, cej_y - 24, cx + 4, cej_y - 2)
    canal_path.closePath()
    d.add(canal_path)

    # ── Annotations ──
    add_arrow_label(d, 12, incisal_y - 5, contact_x - 2, contact_y, 'Contact\nArea', color=colors.HexColor('#BF360C'))
    add_arrow_label(d, 12, incisal_y - 50, lab_l - 3, incisal_y - 70, 'Labial\nSurface')
    add_arrow_label(d, w - 12, incisal_y - 50, ling_r + 3, incisal_y - 70, 'Lingual\nSurface', color=colors.HexColor('#33691E'))
    add_arrow_label(d, w - 12, cej_y + 20, ling_r + 2, cej_y + 20, 'Cingulum', color=colors.HexColor('#F57F17'))
    add_arrow_label(d, 12, cej_y - 25, cx - 12, cej_y - 20, 'Root\n(Cementum)')
    add_arrow_label(d, w - 12, cej_y - 15, cx + 3, cej_y - 8, 'CEJ', color=colors.HexColor('#4E342E'))
    add_label(d, cx, h - 12, 'MESIAL ASPECT', size=9, color=TITLE_CLR, bold=True)

    # ── Labio-lingual dimension ──
    dim_y = cej_y - 40
    d.add(Line(lab_l - 6, dim_y, ling_r + 2, dim_y,
               strokeColor=colors.HexColor('#546E7A'), strokeWidth=0.7))
    d.add(Line(lab_l - 6, dim_y - 3, lab_l - 6, dim_y + 3,
               strokeColor=colors.HexColor('#546E7A'), strokeWidth=0.7))
    d.add(Line(ling_r + 2, dim_y - 3, ling_r + 2, dim_y + 3,
               strokeColor=colors.HexColor('#546E7A'), strokeWidth=0.7))
    add_label(d, (lab_l + ling_r) / 2, dim_y + 6, '~7mm (L-L)',
              size=6.5, color=colors.HexColor('#546E7A'))

    return d


def make_distal_diagram(w=220, h=290):
    """
    Distal aspect – similar to mesial but contact area is lower (middle 1/3) 
    and crown is slightly shorter on distal.
    """
    d = Drawing(w, h)
    d.add(Rect(0, 0, w, h, fillColor=BG_CLR, strokeColor=None))

    cx = w / 2
    cej_y = 100
    incisal_y = 235  # slightly shorter crown
    root_base_y = 18

    lab_l = cx - 20
    ling_r = cx + 18

    # ── Root ──
    root_path = Path(fillColor=CEMENTUM_CLR, strokeColor=OUTLINE_CLR, strokeWidth=1.4)
    root_path.moveTo(cx - 14, cej_y)
    root_path.curveTo(cx - 13, cej_y - 26, cx - 5, root_base_y + 22, cx, root_base_y)
    root_path.curveTo(cx + 5, root_base_y + 22, cx + 13, cej_y - 26, cx + 14, cej_y)
    root_path.closePath()
    d.add(root_path)

    # ── Crown ──
    crown_path = Path(fillColor=ENAMEL_CLR, strokeColor=OUTLINE_CLR, strokeWidth=1.6)
    crown_path.moveTo(lab_l, cej_y)
    crown_path.curveTo(lab_l - 5, cej_y + 38, lab_l - 5, incisal_y - 28, lab_l, incisal_y)
    crown_path.lineTo(lab_l + 10, incisal_y + 5)
    crown_path.curveTo(ling_r + 5, incisal_y - 18, ling_r + 6, cej_y + 48, ling_r, cej_y)
    crown_path.closePath()
    d.add(crown_path)

    # ── Cingulum ──
    d.add(Ellipse(ling_r + 2, cej_y + 22, 5, 12,
                  fillColor=colors.HexColor('#FFF9C4'),
                  strokeColor=OUTLINE_CLR, strokeWidth=0.8))

    # ── CEJ (distal CEJ curves slightly coronally) ──
    d.add(Line(lab_l - 2, cej_y, ling_r + 2, cej_y,
               strokeColor=colors.HexColor('#6D4C41'), strokeWidth=1, strokeDashArray=[4, 2]))

    # ── Contact area – middle 1/3 on distal ──
    contact_y = incisal_y - 70
    contact_x = lab_l - 2
    d.add(Ellipse(contact_x, contact_y, 4, 9,
                  fillColor=colors.HexColor('#FF8A65'),
                  strokeColor=colors.HexColor('#BF360C'), strokeWidth=1))

    # ── Pulp ──
    pc_path = Path(fillColor=PULP_CLR, strokeColor=ANNOT_CLR, strokeWidth=0.8)
    pc_path.moveTo(cx - 7, cej_y + 5)
    pc_path.curveTo(cx - 8, cej_y + 38, cx - 7, incisal_y - 28, cx - 5, incisal_y - 12)
    pc_path.lineTo(cx + 4, incisal_y - 12)
    pc_path.curveTo(cx + 6, incisal_y - 28, cx + 7, cej_y + 38, cx + 6, cej_y + 5)
    pc_path.closePath()
    d.add(pc_path)
    canal_path = Path(fillColor=PULP_CLR, strokeColor=ANNOT_CLR, strokeWidth=0.7)
    canal_path.moveTo(cx - 4, cej_y - 2)
    canal_path.curveTo(cx - 3, cej_y - 22, cx - 2, root_base_y + 28, cx, root_base_y + 10)
    canal_path.curveTo(cx + 2, root_base_y + 28, cx + 3, cej_y - 22, cx + 4, cej_y - 2)
    canal_path.closePath()
    d.add(canal_path)

    # ── Annotations ──
    add_arrow_label(d, 12, incisal_y - 25, contact_x - 2, contact_y, 'Distal\nContact\n(Middle 1/3)', color=colors.HexColor('#BF360C'))
    add_arrow_label(d, 12, incisal_y - 65, lab_l - 3, incisal_y - 80, 'Labial\nSurface')
    add_arrow_label(d, w - 12, incisal_y - 65, ling_r + 3, incisal_y - 80, 'Lingual\nSurface', color=colors.HexColor('#33691E'))
    add_arrow_label(d, w - 12, cej_y + 20, ling_r + 2, cej_y + 20, 'Cingulum', color=colors.HexColor('#F57F17'))
    add_arrow_label(d, w - 12, cej_y - 15, cx + 3, cej_y - 8, 'CEJ', color=colors.HexColor('#4E342E'))
    add_arrow_label(d, 12, cej_y - 20, cx - 12, cej_y - 15, 'Root\n(Cementum)')
    add_label(d, cx, h - 12, 'DISTAL ASPECT', size=9, color=TITLE_CLR, bold=True)

    return d


def make_incisal_diagram(w=220, h=220):
    """
    Incisal / Occlusal aspect – looking from incisal edge down.
    Shows trapezoid outline, labial convexity, cingulum, lingual fossa.
    """
    d = Drawing(w, h)
    d.add(Rect(0, 0, w, h, fillColor=BG_CLR, strokeColor=None))

    cx = w / 2
    cy = h / 2 + 5

    # ── Overall outline: trapezoid with convex labial, concave lingual ──
    lab_w = 54   # labial (wider)
    ling_w = 38  # lingual (narrower)
    depth = 50   # labio-lingual depth

    lab_y = cy - depth / 2
    ling_y = cy + depth / 2

    # Crown outline
    crown = Path(fillColor=ENAMEL_CLR, strokeColor=OUTLINE_CLR, strokeWidth=1.6)
    crown.moveTo(cx - lab_w / 2, lab_y)
    # labial edge – convex
    crown.curveTo(cx - lab_w / 2 + 5, lab_y - 10, cx + lab_w / 2 - 5, lab_y - 10, cx + lab_w / 2, lab_y)
    # distal side
    crown.lineTo(cx + ling_w / 2, ling_y)
    # lingual – slightly concave
    crown.curveTo(cx + ling_w / 2 - 4, ling_y + 6, cx - ling_w / 2 + 4, ling_y + 6, cx - ling_w / 2, ling_y)
    # mesial side
    crown.lineTo(cx - lab_w / 2, lab_y)
    crown.closePath()
    d.add(crown)

    # ── Enamel layer (inner offset outline shows dentin beneath) ──
    en_thick = 6
    dentin = Path(fillColor=DENTIN_CLR, strokeColor=colors.HexColor('#C8A86A'), strokeWidth=1)
    dentin.moveTo(cx - lab_w / 2 + en_thick, lab_y + en_thick / 2)
    dentin.curveTo(cx - lab_w / 2 + en_thick + 3, lab_y - 4,
                   cx + lab_w / 2 - en_thick - 3, lab_y - 4,
                   cx + lab_w / 2 - en_thick, lab_y + en_thick / 2)
    dentin.lineTo(cx + ling_w / 2 - en_thick, ling_y - en_thick / 2)
    dentin.curveTo(cx + ling_w / 2 - en_thick - 3, ling_y + 3,
                   cx - ling_w / 2 + en_thick + 3, ling_y + 3,
                   cx - ling_w / 2 + en_thick, ling_y - en_thick / 2)
    dentin.closePath()
    d.add(dentin)

    # ── Pulp in cross section ──
    d.add(Ellipse(cx, cy + 5, 10, 14, fillColor=PULP_CLR,
                  strokeColor=ANNOT_CLR, strokeWidth=0.9))

    # ── Cingulum (raised oval on lingual) ──
    d.add(Ellipse(cx, ling_y - 8, 12, 8,
                  fillColor=colors.HexColor('#FFF9C4'),
                  strokeColor=colors.HexColor('#F9A825'), strokeWidth=1))

    # ── Lingual fossa (depression oval) ──
    d.add(Ellipse(cx, cy + 6, 10, 14,
                  fillColor=colors.HexColor('#DCEDC8'),
                  strokeColor=colors.HexColor('#558B2F'), strokeWidth=0.8,
                  strokeDashArray=[2, 2]))

    # ── Marginal ridges ──
    d.add(Line(cx - lab_w / 2 + 4, lab_y + 2, cx - ling_w / 2 + 3, ling_y - 3,
               strokeColor=colors.HexColor('#8D6E63'), strokeWidth=1.2))
    d.add(Line(cx + lab_w / 2 - 4, lab_y + 2, cx + ling_w / 2 - 3, ling_y - 3,
               strokeColor=colors.HexColor('#8D6E63'), strokeWidth=1.2))

    # ── Incisal edge highlight ──
    d.add(Line(cx - lab_w / 2 + 3, lab_y - 3, cx + lab_w / 2 - 3, lab_y - 3,
               strokeColor=colors.HexColor('#4FC3F7'), strokeWidth=2.5))

    # ── Axis labels ──
    add_label(d, cx, lab_y - 18, 'LABIAL', size=8, color=LABEL_CLR, bold=True)
    add_label(d, cx, ling_y + 20, 'LINGUAL', size=8, color=colors.HexColor('#2E7D32'), bold=True)
    add_label(d, cx - lab_w / 2 - 18, cy, 'MESIAL', size=8, color=LABEL_CLR, bold=True)
    add_label(d, cx + lab_w / 2 + 14, cy, 'DISTAL', size=8, color=LABEL_CLR, bold=True)

    # ── Annotation arrows ──
    add_arrow_label(d, 15, lab_y + 28, cx - lab_w / 2 + 3, lab_y + 5, 'Enamel\n(6 labels at rim)')
    add_arrow_label(d, w - 15, lab_y + 28, cx + lab_w / 2 - 3, lab_y + 5, 'Incisal Edge')
    add_arrow_label(d, 15, ling_y - 12, cx - ling_w / 2 + 4, ling_y - 6, 'Mesial\nMarginal Ridge', color=colors.HexColor('#4E342E'))
    add_arrow_label(d, w - 15, ling_y - 12, cx + ling_w / 2 - 4, ling_y - 6, 'Distal\nMarginal Ridge', color=colors.HexColor('#4E342E'))
    add_arrow_label(d, w - 15, cy + 5, cx + 12, cy + 5, 'Pulp Cross-\nSection', color=ANNOT_CLR)
    add_arrow_label(d, 15, cy + 15, cx - 9, ling_y - 8, 'Cingulum', color=colors.HexColor('#F57F17'))

    add_label(d, cx, h - 10, 'INCISAL ASPECT', size=9, color=TITLE_CLR, bold=True)

    return d


# ═══════════════════════════════════════════════════════════════════════════════
#  CONTENT BUILDER
# ═══════════════════════════════════════════════════════════════════════════════

def P(text, style=None):
    return Paragraph(text, style or body_style)

def Bullet(text):
    return Paragraph(f'<bullet>\u2022</bullet> {text}', bullet_style)

def H1(text):
    return Paragraph(text, section_style)

def H2(text):
    return Paragraph(text, subsection_style)

def HR():
    return HRFlowable(width='100%', thickness=1.2, color=SECTION_CLR, spaceAfter=4)

def Note(text):
    return Paragraph(f'<i>Note: {text}</i>', note_style)


# ═══════════════════════════════════════════════════════════════════════════════
#  BUILD DOCUMENT
# ═══════════════════════════════════════════════════════════════════════════════

story = []

# ────────────────────────── COVER ──────────────────────────────────────────────
story.append(Spacer(1, 0.5 * cm))

# College header table
header_data = [[
    Paragraph('<b>DENTAL HISTOLOGY &amp; ANATOMY</b><br/>'
              '<font size="10">1st Year Bachelor of Dental Surgery (BDS)</font>',
              ParagraphStyle('hdr', parent=styles['Normal'],
                             fontSize=14, textColor=colors.white,
                             fontName='Helvetica-Bold', alignment=TA_CENTER,
                             leading=20))
]]
header_tbl = Table(header_data, colWidths=[PAGE_W - 2 * MARGIN])
header_tbl.setStyle(TableStyle([
    ('BACKGROUND', (0, 0), (-1, -1), TITLE_CLR),
    ('ROWPADDING', (0, 0), (-1, -1), 12),
    ('ROUNDEDCORNERS', [6]),
]))
story.append(header_tbl)
story.append(Spacer(1, 0.4 * cm))

story.append(Paragraph('MAXILLARY CENTRAL INCISOR', title_style))
story.append(Paragraph('Complete Five-Surface Analysis with Annotated Diagrams', subtitle_style))
story.append(Spacer(1, 0.2 * cm))
story.append(HR())
story.append(Spacer(1, 0.2 * cm))

# ─── Quick Reference Table ─────────────────────────────────────────────────────
story.append(H1('Quick Reference: Measurements & Key Features'))
story.append(Spacer(1, 0.2 * cm))

ref_data = [
    [Paragraph('<b>Parameter</b>', table_header_style),
     Paragraph('<b>Value</b>', table_header_style),
     Paragraph('<b>Clinical Significance</b>', table_header_style)],
    [P('Total tooth length'), P('22.5 mm (average)'), P('Varies 18-26 mm; reference for implant planning')],
    [P('Crown length (cervico-incisal)'), P('10.5 mm'), P('Reference for crown-to-root ratio')],
    [P('Root length'), P('12.0 mm'), P('Single straight root; apical 1/3 may curve')],
    [P('Crown width (mesio-distal)'), P('8.5 mm (max)'), P('Wider than any other incisor; key aesthetic unit')],
    [P('Crown thickness (labio-lingual)'), P('7.0 mm'), P('Thinner lingual due to cingulum')],
    [P('Number of roots'), P('1'), P('Single, straight, conical root')],
    [P('Number of root canals'), P('1 (rarely 2)'), P('Wide oval canal; tapers apically')],
    [P('Pulp horns'), P('2 (mesial + distal)'), P('Mesial horn larger; important in pulp capping')],
    [P('Eruption (permanent)'), P('7-8 years'), P('Replaced deciduous central incisor at age 6-7')],
    [P('Root complete'), P('10 years'), P('Root apex closes ~3 years post-eruption')],
]

ref_table = Table(ref_data, colWidths=[5.5 * cm, 3.5 * cm, 8 * cm])
ref_table.setStyle(TableStyle([
    ('BACKGROUND', (0, 0), (-1, 0), TITLE_CLR),
    ('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
    ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
    ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
    ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#EEF2FF')]),
    ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#9FA8DA')),
    ('ROWPADDING', (0, 0), (-1, -1), 5),
    ('FONTSIZE', (0, 0), (-1, -1), 9),
]))
story.append(ref_table)

story.append(PageBreak())

# ═══════════════════════════════════════════════════════════════════════════════
#  SECTION 1 – LABIAL ASPECT
# ═══════════════════════════════════════════════════════════════════════════════
story.append(H1('1. Labial Aspect'))
story.append(Spacer(1, 0.1 * cm))

# Diagram + text side by side
labial_dwg = make_labial_diagram(220, 290)
labial_text = [
    [H2('General Shape'),
     P('The labial surface is the most visible surface facing the lips. The crown outline '
       'when viewed from the front is roughly trapezoidal - widest at the incisal edge '
       'and narrower at the cervical margin.')],
    [H2('Crown Outline'),
     P('<b>Incisal edge:</b> Nearly straight in a newly erupted tooth; develops wear facets with age. '
       'Mamelons (three lobes) are visible in newly erupted teeth and wear away quickly. '
       '<b>Mesial outline:</b> Straighter, meets the incisal edge at an almost right angle. '
       '<b>Distal outline:</b> More rounded, meets incisal edge at a rounded angle. '
       '<b>Cervical outline (CEJ):</b> Curves toward the root apex (apically convex).')],
    [H2('Surface Texture'),
     P('Fine labial developmental grooves run vertically dividing the surface into three '
       'lobes: mesio-labial, labio-incisal, and disto-labial. The labial surface is convex '
       'in both directions (cervico-incisally and mesio-distally).')],
    [H2('Contact Areas'),
     P('<b>Mesial contact:</b> Located in the incisal third, very close to the incisal angle. '
       '<b>Distal contact:</b> Located in the middle-incisal third junction, slightly lower than mesial.')],
    [H2('CEJ on Labial'),
     P('The cervico-enamel junction curves toward the incisal edge (coronally) on the mesial '
       'side. On the labial view, the CEJ curvature is approximately 3.5 mm deep.')],
]

lab_detail_table = Table(
    [[renderPDF.GraphicsFlowable(labial_dwg),
      Table(labial_text, colWidths=[3.5 * cm, 9 * cm],
            style=TableStyle([('VALIGN', (0,0), (-1,-1), 'TOP'),
                               ('ROWPADDING', (0,0), (-1,-1), 3)]))]],
    colWidths=[6 * cm, PAGE_W - 2 * MARGIN - 6 * cm]
)
lab_detail_table.setStyle(TableStyle([
    ('VALIGN', (0, 0), (-1, -1), 'TOP'),
    ('LEFTPADDING', (0, 0), (-1, -1), 0),
]))
story.append(lab_detail_table)

story.append(Spacer(1, 0.2 * cm))
story.append(Note('Key exam point: The mesial angle is sharper (more right-angled) '
                   'than the distal angle (more rounded). This distinguishes right from left.'))

story.append(HR())

# ═══════════════════════════════════════════════════════════════════════════════
#  SECTION 2 – LINGUAL ASPECT
# ═══════════════════════════════════════════════════════════════════════════════
story.append(H1('2. Lingual Aspect'))
story.append(Spacer(1, 0.1 * cm))

lingual_dwg = make_lingual_diagram(220, 290)

lingual_text = [
    [H2('Cingulum'),
     P('The <b>cingulum</b> is the most distinctive feature of the lingual surface. '
       'It is a rounded enamel bulge at the cervical third of the lingual surface. '
       'It represents the lingual lobe. It is well-developed in the maxillary central incisor.')],
    [H2('Lingual Fossa'),
     P('A shallow, smooth <b>concave depression</b> occupying the middle and incisal thirds '
       'of the lingual surface between the marginal ridges. Bound cervically by the cingulum '
       'and incisally by the lingual surface of the incisal edge.')],
    [H2('Marginal Ridges'),
     P('<b>Mesial marginal ridge:</b> Runs from the mesio-incisal angle to the cingulum. '
       'Slightly more prominent than the distal marginal ridge. '
       '<b>Distal marginal ridge:</b> Runs from the disto-incisal angle to the cingulum. '
       'Both form the lateral borders of the lingual fossa.')],
    [H2('CEJ on Lingual'),
     P('The CEJ on the lingual side curves <b>less apically</b> (toward the root) than '
       'the labial side. The curvature is approximately 2.5 mm. This is a general rule '
       'for all teeth - labial CEJ curvature > lingual CEJ curvature.')],
    [H2('Lingual Ridge'),
     P('Some texts describe a faint <b>lingual ridge</b> running from the cingulum toward '
       'the incisal edge, dividing the lingual fossa into mesial and distal fossae. '
       'This is variable and may be absent.')],
]

ling_detail_table = Table(
    [[renderPDF.GraphicsFlowable(lingual_dwg),
      Table(lingual_text, colWidths=[3.5 * cm, 9 * cm],
            style=TableStyle([('VALIGN', (0,0), (-1,-1), 'TOP'),
                               ('ROWPADDING', (0,0), (-1,-1), 3)]))]],
    colWidths=[6 * cm, PAGE_W - 2 * MARGIN - 6 * cm]
)
ling_detail_table.setStyle(TableStyle([
    ('VALIGN', (0, 0), (-1, -1), 'TOP'),
    ('LEFTPADDING', (0, 0), (-1, -1), 0),
]))
story.append(ling_detail_table)

story.append(Spacer(1, 0.2 * cm))
story.append(Note('Cingulum is the remnant of the lingual lobe - present in all maxillary anterior teeth. '
                   'Pronounced cingulum = higher caries risk in the lingual fossa pit.'))

story.append(PageBreak())

# ═══════════════════════════════════════════════════════════════════════════════
#  SECTION 3 – MESIAL ASPECT
# ═══════════════════════════════════════════════════════════════════════════════
story.append(H1('3. Mesial Aspect'))
story.append(Spacer(1, 0.1 * cm))

mesial_dwg = make_mesial_diagram(220, 290)

mesial_text = [
    [H2('Crown Profile'),
     P('When viewed from the mesial side, the crown is roughly <b>triangular</b> with '
       'the apex at the incisal edge and the base at the CEJ. '
       'The labial profile is convex. The lingual profile is concave in the middle '
       '(lingual fossa region) and convex at the cervical (cingulum).')],
    [H2('Incisal Edge'),
     P('The incisal edge is positioned <b>labial to the long axis</b> of the root. '
       'This labial inclination is important for anterior guidance in occlusion.')],
    [H2('Contact Area'),
     P('The <b>mesial contact area</b> is located in the <b>incisal third</b> of the '
       'mesial surface, very close to the mesio-incisal angle. This is the highest '
       'contact area among all posterior and anterior teeth.')],
    [H2('CEJ Curvature'),
     P('The CEJ curves <b>incisally (coronally)</b> on the mesial surface to a depth of '
       'approximately <b>3.5 mm</b>. This is the greatest CEJ curvature of any tooth in the '
       'mouth and is a key identifying feature.')],
    [H2('Root Outline'),
     P('The root appears broad at the cervix and tapers smoothly to a rounded apex. '
       'A <b>developmental depression</b> may be present on the mesial root surface. '
       'Root is slightly concave on the mesial surface near the apex.')],
]

mes_detail_table = Table(
    [[renderPDF.GraphicsFlowable(mesial_dwg),
      Table(mesial_text, colWidths=[3.5 * cm, 9 * cm],
            style=TableStyle([('VALIGN', (0,0), (-1,-1), 'TOP'),
                               ('ROWPADDING', (0,0), (-1,-1), 3)]))]],
    colWidths=[6 * cm, PAGE_W - 2 * MARGIN - 6 * cm]
)
mes_detail_table.setStyle(TableStyle([
    ('VALIGN', (0, 0), (-1, -1), 'TOP'),
    ('LEFTPADDING', (0, 0), (-1, -1), 0),
]))
story.append(mes_detail_table)

story.append(Spacer(1, 0.2 * cm))
story.append(Note('Mesial CEJ curvature of 3.5 mm is the MOST curvature of any tooth surface in the dentition. '
                   'Exam favourite for identification purposes.'))

story.append(HR())

# ═══════════════════════════════════════════════════════════════════════════════
#  SECTION 4 – DISTAL ASPECT
# ═══════════════════════════════════════════════════════════════════════════════
story.append(H1('4. Distal Aspect'))
story.append(Spacer(1, 0.1 * cm))

distal_dwg = make_distal_diagram(220, 290)

distal_text = [
    [H2('Differences from Mesial'),
     P('The distal aspect is similar to the mesial but has several distinguishing features: '
       '(1) the crown appears slightly <b>shorter</b>; '
       '(2) the incisal edge appears more rounded; '
       '(3) the overall crown width is slightly less.')],
    [H2('Contact Area'),
     P('The <b>distal contact area</b> is in the <b>junction of incisal and middle thirds</b>, '
       'positioned slightly <b>more cervically</b> than the mesial contact area. '
       'This is a key difference used to identify right vs. left central incisor.')],
    [H2('CEJ Curvature'),
     P('The CEJ curvature on the distal is approximately <b>2.5 mm</b> - less than the mesial '
       '(3.5 mm). This rule applies: mesial CEJ curvature &gt; distal CEJ curvature for all anterior teeth.')],
    [H2('Crown Profile'),
     P('The labial surface is convex. The lingual profile shows the cingulum at the cervical '
       'and a slight concavity corresponding to the lingual fossa in the middle third. '
       'The outline is slightly more convex distally than mesially.')],
    [H2('Root'),
     P('The distal root surface is usually <b>convex</b> (less likely to have developmental '
       'depression compared to mesial). Root tapers to a blunt, rounded apex that may '
       'show a slight distal inclination in some individuals.')],
]

dis_detail_table = Table(
    [[renderPDF.GraphicsFlowable(distal_dwg),
      Table(distal_text, colWidths=[3.5 * cm, 9 * cm],
            style=TableStyle([('VALIGN', (0,0), (-1,-1), 'TOP'),
                               ('ROWPADDING', (0,0), (-1,-1), 3)]))]],
    colWidths=[6 * cm, PAGE_W - 2 * MARGIN - 6 * cm]
)
dis_detail_table.setStyle(TableStyle([
    ('VALIGN', (0, 0), (-1, -1), 'TOP'),
    ('LEFTPADDING', (0, 0), (-1, -1), 0),
]))
story.append(dis_detail_table)

story.append(Spacer(1, 0.2 * cm))
story.append(Note('Key: Mesial contact = incisal 1/3; Distal contact = incisal-middle 1/3 junction. '
                   'Use this to determine side (right vs. left).'))

story.append(PageBreak())

# ═══════════════════════════════════════════════════════════════════════════════
#  SECTION 5 – INCISAL ASPECT
# ═══════════════════════════════════════════════════════════════════════════════
story.append(H1('5. Incisal Aspect'))
story.append(Spacer(1, 0.1 * cm))

incisal_dwg = make_incisal_diagram(220, 220)

incisal_text = [
    [H2('Overall Outline'),
     P('Viewed from the incisal edge, the crown appears <b>trapezoid</b> with the wider '
       'base labially and narrower portion lingually. It is widest at the incisal edge '
       'and narrows as it goes toward the lingual.')],
    [H2('Labial Surface'),
     P('The labial surface appears <b>convex</b>. The incisal edge (visible as a line) '
       'is nearly straight in a newly erupted tooth. The labio-incisal angle on the '
       'mesial is sharper; on the distal it is more rounded.')],
    [H2('Lingual Features'),
     P('<b>Cingulum:</b> Appears as a raised rounded area at the lingual. '
       '<b>Lingual fossa:</b> The concave region between the cingulum and incisal edge, '
       'bounded by the mesial and distal marginal ridges. '
       '<b>Marginal ridges</b> are visible on both sides.')],
    [H2('Enamel Thickness'),
     P('The cross-section reveals <b>enamel thickest at the incisal edge</b> (up to 2 mm) '
       'and progressively thinning toward the cervical margin. The labial enamel is thicker '
       'than the lingual enamel. The dentin core is central.')],
    [H2('Mesio-Distal Asymmetry'),
     P('From the incisal view, the <b>mesial half is slightly wider</b> than the distal half. '
       'This mesio-distal asymmetry, combined with the angulation of the incisal '
       'edge, helps identify right vs. left tooth. '
       'The tooth tilts slightly to the distal when the mesial surface is viewed.')],
]

inc_detail_table = Table(
    [[renderPDF.GraphicsFlowable(incisal_dwg),
      Table(incisal_text, colWidths=[3.5 * cm, 9 * cm],
            style=TableStyle([('VALIGN', (0,0), (-1,-1), 'TOP'),
                               ('ROWPADDING', (0,0), (-1,-1), 3)]))]],
    colWidths=[6 * cm, PAGE_W - 2 * MARGIN - 6 * cm]
)
inc_detail_table.setStyle(TableStyle([
    ('VALIGN', (0, 0), (-1, -1), 'TOP'),
    ('LEFTPADDING', (0, 0), (-1, -1), 0),
]))
story.append(inc_detail_table)

story.append(Spacer(1, 0.2 * cm))
story.append(Note('The incisal view is critical for wax carving practicals. '
                   'Remember: labial side is wider and convex; lingual side narrower with cingulum.'))

story.append(HR())

# ═══════════════════════════════════════════════════════════════════════════════
#  SECTION 6 – PULP CAVITY
# ═══════════════════════════════════════════════════════════════════════════════
story.append(H1('6. Pulp Cavity'))
story.append(Spacer(1, 0.1 * cm))

pulp_data = [
    [Paragraph('<b>Region</b>', table_header_style),
     Paragraph('<b>Description</b>', table_header_style)],
    [P('<b>Pulp Chamber</b>'), P('Broad mesiodistally in the coronal third; two pulp horns (mesial > distal). '
                                  'Tapers incisally toward the pulp horns and cervically toward the root canal.')],
    [P('<b>Pulp Horns</b>'), P('Two horns pointing toward the mesio-incisal and disto-incisal corners. '
                                'Mesial horn is larger. Important clinically during cavity preparation to avoid pulp exposure.')],
    [P('<b>Root Canal</b>'), P('Single canal, wide at cervical third, oval in cross section, tapers to a point near apex. '
                                'Average length of root canal = 12 mm. Rarely bifurcated.')],
    [P('<b>Apical Foramen</b>'), P('Located at or near the anatomic apex. Varies in position - may be slightly '
                                    'lateral to the geometric apex in some teeth.')],
    [P('<b>Accessory Canals</b>'), P('Can be present especially in the apical third. Important for endodontic treatment success.')],
]

pulp_table = Table(pulp_data, colWidths=[4 * cm, 13 * cm])
pulp_table.setStyle(TableStyle([
    ('BACKGROUND', (0, 0), (-1, 0), TITLE_CLR),
    ('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
    ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#EEF2FF')]),
    ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#9FA8DA')),
    ('ROWPADDING', (0, 0), (-1, -1), 5),
    ('VALIGN', (0, 0), (-1, -1), 'TOP'),
    ('FONTSIZE', (0, 0), (-1, -1), 9),
]))
story.append(pulp_table)

story.append(Spacer(1, 0.3 * cm))

# ═══════════════════════════════════════════════════════════════════════════════
#  SECTION 7 – HISTOLOGY OF DENTAL TISSUES
# ═══════════════════════════════════════════════════════════════════════════════
story.append(H1('7. Histology of Dental Tissues'))
story.append(Spacer(1, 0.1 * cm))

histo_data = [
    [Paragraph('<b>Tissue</b>', table_header_style),
     Paragraph('<b>Composition</b>', table_header_style),
     Paragraph('<b>Key Histological Features</b>', table_header_style)],
    [P('<b>Enamel</b>'),
     P('96% inorganic (HAP), 1% organic, 3% water'),
     P('Enamel rods (prisms) run from DEJ to surface. '
       'Hunter-Schreger bands (alternating rod direction). '
       'Striae of Retzius (incremental lines). '
       'Enamel spindles at DEJ. '
       'Neonatal line (if present). No cells - cannot regenerate.')],
    [P('<b>Dentine</b>'),
     P('70% inorganic (HAP), 20% organic (collagen), 10% water'),
     P('Dentinal tubules containing odontoblast processes (Tomes fibres). '
       'Peritubular dentine (hypermineralised) around each tubule. '
       'Intertubular dentine between tubules. '
       'Incremental lines: Lines of von Ebner (daily), Owen\'s contour lines. '
       'Interglobular dentine (unmineralised). '
       'Dead tracts = empty dentinal tubules.')],
    [P('<b>Cementum</b>'),
     P('50% inorganic, 50% organic (collagen) + water'),
     P('Acellular (primary) cementum: covers coronal 2/3 of root. '
       'Cellular (secondary) cementum: covers apical 1/3. '
       'Sharpey\'s fibres = embedded extrinsic fibres of PDL. '
       'Incremental lines of Salter.')],
    [P('<b>Pulp</b>'),
     P('Soft connective tissue; cells, fibres, ground substance, blood vessels, nerves'),
     P('Odontoblastic layer (peripheral). '
       'Cell-free zone of Weil. '
       'Cell-rich zone. '
       'Pulp core: fibroblasts, macrophages, blood vessels (arterioles), '
       'nerve fibres (A-delta, C fibres). '
       'Dental pulp stem cells present.')],
]

histo_table = Table(histo_data, colWidths=[3 * cm, 5 * cm, 9 * cm])
histo_table.setStyle(TableStyle([
    ('BACKGROUND', (0, 0), (-1, 0), TITLE_CLR),
    ('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
    ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#EEF2FF')]),
    ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#9FA8DA')),
    ('ROWPADDING', (0, 0), (-1, -1), 5),
    ('VALIGN', (0, 0), (-1, -1), 'TOP'),
    ('FONTSIZE', (0, 0), (-1, -1), 9),
]))
story.append(histo_table)

story.append(PageBreak())

# ═══════════════════════════════════════════════════════════════════════════════
#  SECTION 8 – RULES FOR IDENTIFICATION (LAWS OF ASHLEY HOWE)
# ═══════════════════════════════════════════════════════════════════════════════
story.append(H1('8. Rules for Identification (Right vs. Left)'))
story.append(Spacer(1, 0.1 * cm))

story.append(P('<b>Laws of Ashley Howe</b> (applied to maxillary central incisor):'))
story.append(Spacer(1, 0.1 * cm))

rules_data = [
    [Paragraph('<b>Rule</b>', table_header_style),
     Paragraph('<b>Feature</b>', table_header_style),
     Paragraph('<b>Application</b>', table_header_style)],
    [P('1. Curvature of CEJ'), P('Mesial &gt; Distal'), P('Place tooth so CEJ curves more on the side you call mesial')],
    [P('2. Contact area location'), P('Mesial in incisal 1/3\nDistal in incisal-middle 1/3 junction'),
     P('Mesial contact is closer to incisal edge; distal is slightly lower')],
    [P('3. Labial surface outline'), P('Mesial angle = right angle\nDistal angle = rounded'),
     P('The sharper corner faces the midline (mesial).')],
    [P('4. Curvature of crown'),
     P('More curvature on labial surface distally'),
     P('The labial surface curves away more on the distal side; mesial is flatter.')],
    [P('5. Root inclination'), P('Root tip inclines distally'),
     P('When the labial surface faces you, the root tip points to the side away from midline = distal')],
]

rules_table = Table(rules_data, colWidths=[3.5 * cm, 5 * cm, 8.5 * cm])
rules_table.setStyle(TableStyle([
    ('BACKGROUND', (0, 0), (-1, 0), SECTION_CLR),
    ('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
    ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#EEF2FF')]),
    ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#9FA8DA')),
    ('ROWPADDING', (0, 0), (-1, -1), 5),
    ('VALIGN', (0, 0), (-1, -1), 'TOP'),
    ('FONTSIZE', (0, 0), (-1, -1), 9),
]))
story.append(rules_table)

story.append(Spacer(1, 0.3 * cm))

# ═══════════════════════════════════════════════════════════════════════════════
#  SECTION 9 – DECIDUOUS vs. PERMANENT
# ═══════════════════════════════════════════════════════════════════════════════
story.append(H1('9. Deciduous vs. Permanent Maxillary Central Incisor'))
story.append(Spacer(1, 0.1 * cm))

dec_perm_data = [
    [Paragraph('<b>Feature</b>', table_header_style),
     Paragraph('<b>Deciduous</b>', table_header_style),
     Paragraph('<b>Permanent</b>', table_header_style)],
    [P('Total length'), P('~16 mm'), P('~22.5 mm')],
    [P('Crown length'), P('~6 mm'), P('~10.5 mm')],
    [P('Crown width (M-D)'), P('~6.5 mm'), P('~8.5 mm')],
    [P('Enamel colour'), P('More opaque, chalky white'), P('More translucent, off-white')],
    [P('Crown:Root ratio'), P('Crown appears larger relative to root'), P('Normal proportions')],
    [P('Cervical constriction'), P('More pronounced'), P('Less pronounced')],
    [P('Labial surface'), P('More convex'), P('Flatter, with developmental grooves')],
    [P('Pulp chamber'), P('Relatively larger'), P('Proportionally smaller')],
    [P('Root'), P('Narrow, tapers rapidly'), P('Broad, regular taper')],
    [P('Eruption'), P('6-10 months (central incisor)'), P('7-8 years')],
]

dec_table = Table(dec_perm_data, colWidths=[5 * cm, 5.5 * cm, 6.5 * cm])
dec_table.setStyle(TableStyle([
    ('BACKGROUND', (0, 0), (-1, 0), SECTION_CLR),
    ('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
    ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#EEF2FF')]),
    ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#9FA8DA')),
    ('ROWPADDING', (0, 0), (-1, -1), 5),
    ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
    ('FONTSIZE', (0, 0), (-1, -1), 9),
]))
story.append(dec_table)

story.append(Spacer(1, 0.3 * cm))

# ═══════════════════════════════════════════════════════════════════════════════
#  SECTION 10 – CLINICAL SIGNIFICANCE
# ═══════════════════════════════════════════════════════════════════════════════
story.append(H1('10. Clinical Significance'))
story.append(Spacer(1, 0.1 * cm))

clin_data = [
    [Paragraph('<b>Clinical Aspect</b>', table_header_style),
     Paragraph('<b>Relevance</b>', table_header_style)],
    [P('<b>Aesthetic</b>'), P('Most visible tooth in the smile; cornerstone of smile design and cosmetic dentistry. '
                               'Golden proportion: width/height ratio ~75-80%.')],
    [P('<b>Trauma</b>'), P('Most commonly injured tooth (crown fractures, avulsions, luxations) due to anterior position. '
                            'Immature apex after trauma requires special endodontic management (apexogenesis/apexification).')],
    [P('<b>Endodontics</b>'), P('Straight single root canal - relatively straightforward RCT. '
                                  'Access cavity is a rounded triangle/shovel shape from lingual. '
                                  'Working length ~22.5 mm.')],
    [P('<b>Restorations</b>'), P('Class III cavities (proximal without incisal angle) and Class IV (including incisal angle) '
                                  'are common. Composite resin is preferred for aesthetics.')],
    [P('<b>Orthodontics</b>'), P('Key landmark for occlusal analysis. Used to assess midline deviation, overjet, and overbite. '
                                  'Most commonly bonded for orthodontic brackets.')],
    [P('<b>Implantology</b>'), P('Single implant replacement for congenitally missing or extracted central incisor. '
                                   'Root length and bone availability are key pre-implant assessment factors.')],
]

clin_table = Table(clin_data, colWidths=[4 * cm, 13 * cm])
clin_table.setStyle(TableStyle([
    ('BACKGROUND', (0, 0), (-1, 0), SECTION_CLR),
    ('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
    ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#EEF2FF')]),
    ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#9FA8DA')),
    ('ROWPADDING', (0, 0), (-1, -1), 5),
    ('VALIGN', (0, 0), (-1, -1), 'TOP'),
    ('FONTSIZE', (0, 0), (-1, -1), 9),
]))
story.append(clin_table)

story.append(Spacer(1, 0.3 * cm))
story.append(HR())

# ═══════════════════════════════════════════════════════════════════════════════
#  FOOTER
# ═══════════════════════════════════════════════════════════════════════════════
footer_text = ('References: Wheeler\'s Dental Anatomy, Physiology and Occlusion | '
               'Ash & Nelson Dental Anatomy | '
               'Orban\'s Oral Histology and Embryology | '
               'Bath-Balogh Illustrated Dental Embryology, Histology & Anatomy')
story.append(P(f'<font size="8" color="#546E7A"><i>{footer_text}</i></font>'))

# ─── Build ──────────────────────────────────────────────────────────────────────
def add_page_number(canvas, doc):
    canvas.saveState()
    canvas.setFont('Helvetica', 8)
    canvas.setFillColor(colors.HexColor('#546E7A'))
    canvas.drawCentredString(PAGE_W / 2, 1 * cm,
                              f'Maxillary Central Incisor - Dental Histology & Anatomy  |  1st Year BDS  |  Page {doc.page}')
    canvas.restoreState()

doc.build(story, onFirstPage=add_page_number, onLaterPages=add_page_number)
print("PDF generated successfully!")
Running Command

cd /home/daytona/workspace/maxillary-incisor-pdf && 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.