~/lower-limb-study/generate_pdf.py
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import cm, mm
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, PageBreak, KeepTogether
)
from reportlab.platypus.flowables import HRFlowable
from reportlab.lib.colors import HexColor
OUTPUT = "/home/daytona/workspace/lower-limb-study/Lower_Limb_Bone_Study_Guide.pdf"
# ── Colour palette ─────────────────────────────────────────────────────────────
NAVY = HexColor("#1B3A6B")
TEAL = HexColor("#1A7A8A")
GOLD = HexColor("#E8A020")
LIGHT_BG = HexColor("#F0F5FA")
ROW_ALT = HexColor("#E8F2F8")
WHITE = colors.white
BLACK = colors.black
DARK_GREY = HexColor("#333333")
MED_GREY = HexColor("#666666")
SIDE_CLR = HexColor("#FFF3CD") # warm yellow for siding rows
# ── Styles ──────────────────────────────────────────────────────────────────────
def make_styles():
base = getSampleStyleSheet()
cover_title = ParagraphStyle("cover_title",
fontName="Helvetica-Bold", fontSize=30, textColor=WHITE,
alignment=TA_CENTER, spaceAfter=8, leading=36)
cover_sub = ParagraphStyle("cover_sub",
fontName="Helvetica", fontSize=14, textColor=HexColor("#CCDFEE"),
alignment=TA_CENTER, spaceAfter=4)
cover_source = ParagraphStyle("cover_source",
fontName="Helvetica-Oblique", fontSize=9, textColor=HexColor("#AACCDD"),
alignment=TA_CENTER)
h1 = ParagraphStyle("h1",
fontName="Helvetica-Bold", fontSize=16, textColor=WHITE,
spaceAfter=4, spaceBefore=10, leading=20)
h2 = ParagraphStyle("h2",
fontName="Helvetica-Bold", fontSize=12, textColor=NAVY,
spaceAfter=3, spaceBefore=8, leading=15)
h3 = ParagraphStyle("h3",
fontName="Helvetica-Bold", fontSize=10, textColor=TEAL,
spaceAfter=2, spaceBefore=5)
body = ParagraphStyle("body",
fontName="Helvetica", fontSize=9, textColor=DARK_GREY,
spaceAfter=4, leading=13, alignment=TA_JUSTIFY)
bullet = ParagraphStyle("bullet",
fontName="Helvetica", fontSize=9, textColor=DARK_GREY,
spaceAfter=2, leading=12, leftIndent=12, bulletIndent=0)
note = ParagraphStyle("note",
fontName="Helvetica-Oblique", fontSize=8, textColor=MED_GREY,
spaceAfter=3, leading=11)
mnemonic = ParagraphStyle("mnemonic",
fontName="Helvetica-Bold", fontSize=9, textColor=HexColor("#5C3D00"),
spaceAfter=2, leading=12, leftIndent=8)
tbl_hdr = ParagraphStyle("tbl_hdr",
fontName="Helvetica-Bold", fontSize=8.5, textColor=WHITE,
alignment=TA_LEFT)
tbl_cell = ParagraphStyle("tbl_cell",
fontName="Helvetica", fontSize=8.5, textColor=DARK_GREY,
leading=11, alignment=TA_LEFT)
tbl_bold = ParagraphStyle("tbl_bold",
fontName="Helvetica-Bold", fontSize=8.5, textColor=DARK_GREY,
leading=11)
page_num = ParagraphStyle("page_num",
fontName="Helvetica", fontSize=8, textColor=MED_GREY,
alignment=TA_CENTER)
return dict(cover_title=cover_title, cover_sub=cover_sub,
cover_source=cover_source, h1=h1, h2=h2, h3=h3,
body=body, bullet=bullet, note=note, mnemonic=mnemonic,
tbl_hdr=tbl_hdr, tbl_cell=tbl_cell, tbl_bold=tbl_bold,
page_num=page_num)
S = make_styles()
# ── Table helpers ───────────────────────────────────────────────────────────────
def make_table(headers, rows, col_widths, alt_rows=True, siding_col=None):
"""Build a styled table. siding_col = index of a 'Siding' header row (highlighted)."""
data = [[Paragraph(h, S["tbl_hdr"]) for h in headers]]
for i, row in enumerate(rows):
data.append([Paragraph(str(cell), S["tbl_cell"]) for cell in row])
style = [
# Header
("BACKGROUND", (0, 0), (-1, 0), NAVY),
("TOPPADDING", (0, 0), (-1, 0), 5),
("BOTTOMPADDING",(0, 0), (-1, 0), 5),
("LEFTPADDING", (0, 0), (-1, -1), 6),
("RIGHTPADDING",(0, 0), (-1, -1), 6),
("GRID", (0, 0), (-1, -1), 0.4, HexColor("#BBCDD8")),
("ROWBACKGROUNDS", (0, 1), (-1, -1),
[WHITE, ROW_ALT] if alt_rows else [WHITE]),
("VALIGN", (0, 0), (-1, -1), "TOP"),
("TOPPADDING", (0, 1), (-1, -1), 4),
("BOTTOMPADDING", (0, 1), (-1, -1), 4),
]
t = Table(data, colWidths=col_widths, repeatRows=1)
t.setStyle(TableStyle(style))
return t
def section_header(title, subtitle=None):
"""Navy banner with white title text."""
inner = [Paragraph(title, S["h1"])]
if subtitle:
inner.append(Paragraph(subtitle, ParagraphStyle(
"banner_sub", fontName="Helvetica-Oblique", fontSize=9,
textColor=HexColor("#AACCDD"), spaceAfter=0)))
tbl = Table([[inner]], colWidths=[17*cm])
tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), NAVY),
("LEFTPADDING", (0,0), (-1,-1), 10),
("RIGHTPADDING",(0,0), (-1,-1), 10),
("TOPPADDING", (0,0), (-1,-1), 7),
("BOTTOMPADDING",(0,0), (-1,-1), 7),
("ROUNDEDCORNERS", [4]),
]))
return tbl
def siding_box(text):
"""Gold-background siding tip box."""
inner = [Paragraph("<b>SIDING TIP</b>", ParagraphStyle(
"stip_hdr", fontName="Helvetica-Bold", fontSize=8.5,
textColor=HexColor("#7A5000"))),
Paragraph(text, ParagraphStyle(
"stip_body", fontName="Helvetica", fontSize=8.5,
textColor=HexColor("#3D2800"), leading=12))]
tbl = Table([[inner]], colWidths=[17*cm])
tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), HexColor("#FFF3CD")),
("LEFTPADDING", (0,0), (-1,-1), 10),
("RIGHTPADDING",(0,0), (-1,-1), 10),
("TOPPADDING", (0,0), (-1,-1), 6),
("BOTTOMPADDING",(0,0), (-1,-1), 6),
("BOX", (0,0), (-1,-1), 1.2, GOLD),
("ROUNDEDCORNERS", [3]),
]))
return tbl
def info_box(text, colour=LIGHT_BG, border=TEAL):
inner = Paragraph(text, S["body"])
tbl = Table([[inner]], colWidths=[17*cm])
tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), colour),
("LEFTPADDING", (0,0), (-1,-1), 10),
("TOPPADDING", (0,0), (-1,-1), 6),
("BOTTOMPADDING",(0,0), (-1,-1), 6),
("BOX", (0,0), (-1,-1), 1, border),
]))
return tbl
# ── Page callbacks ──────────────────────────────────────────────────────────────
def on_later_page(canvas, doc):
canvas.saveState()
w, h = A4
# Top bar
canvas.setFillColor(NAVY)
canvas.rect(0, h - 1.1*cm, w, 1.1*cm, fill=1, stroke=0)
canvas.setFillColor(WHITE)
canvas.setFont("Helvetica-Bold", 9)
canvas.drawCentredString(w/2, h - 0.72*cm,
"Lower Limb Bone Identification & Siding – Study Guide")
# Bottom bar
canvas.setFillColor(LIGHT_BG)
canvas.rect(0, 0, w, 0.85*cm, fill=1, stroke=0)
canvas.setFillColor(MED_GREY)
canvas.setFont("Helvetica", 8)
canvas.drawCentredString(w/2, 0.32*cm,
f"Page {doc.page} | Sources: Gray's Anatomy for Students · Forensic Anthropology (2e)")
canvas.restoreState()
def on_first_page(canvas, doc):
on_later_page(canvas, doc)
# ── Document build ──────────────────────────────────────────────────────────────
def build_pdf():
doc = SimpleDocTemplate(
OUTPUT,
pagesize=A4,
leftMargin=2*cm, rightMargin=2*cm,
topMargin=1.8*cm, bottomMargin=1.8*cm,
title="Lower Limb Bone Study Guide",
author="Orris Medical Education",
)
story = []
SP = Spacer
# ── COVER ──────────────────────────────────────────────────────────────────
# Full-page navy cover box
cover_data = [[
[
SP(1, 2.5*cm),
Paragraph("Lower Limb Bone", S["cover_title"]),
Paragraph("Identification & Siding", S["cover_title"]),
SP(1, 0.4*cm),
HRFlowable(width="60%", color=GOLD, thickness=2),
SP(1, 0.4*cm),
Paragraph("Comprehensive Study Guide for Anatomy & Forensic Anthropology", S["cover_sub"]),
SP(1, 1*cm),
Paragraph("Femur · Patella · Tibia · Fibula · Tarsals · Metatarsals · Phalanges", S["cover_sub"]),
SP(1, 1.5*cm),
Paragraph("Sources: Gray's Anatomy for Students (9780323934237) & "
"Forensic Anthropology: A Comprehensive Introduction, 2e (9781498736121)",
S["cover_source"]),
SP(1, 0.5*cm),
]
]]
cover = Table(cover_data, colWidths=[17*cm], rowHeights=[22*cm])
cover.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), NAVY),
("LEFTPADDING", (0,0), (-1,-1), 20),
("RIGHTPADDING",(0,0), (-1,-1), 20),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
]))
story.append(cover)
story.append(PageBreak())
# ── OVERVIEW ───────────────────────────────────────────────────────────────
story.append(section_header("Overview: Bones of the Lower Limb",
"30 bones per limb | 4 anatomical regions"))
story.append(SP(1, 0.3*cm))
overview_rows = [
["Thigh", "Femur", "1", "Hip joint → Knee joint"],
["Knee", "Patella", "1", "Sesamoid in quadriceps tendon"],
["Leg", "Tibia + Fibula", "2", "Knee joint → Ankle joint"],
["Tarsus", "Talus, Calcaneus, Navicular, Cuboid, 3 Cuneiforms", "7", "Proximal foot"],
["Metatarsus", "Metatarsals 1–5", "5", "Midfoot arch"],
["Toes", "Phalanges (2 in hallux, 3 in toes 2–5)", "14", "Distal foot"],
["TOTAL", "", "30", ""],
]
story.append(make_table(
["Region", "Bones", "Count", "Location"],
overview_rows,
[3*cm, 6.5*cm, 2*cm, 5.5*cm]
))
story.append(SP(1, 0.4*cm))
story.append(info_box(
"<b>Siding rules – general principles:</b> Once you orient the bone proximally vs. distally, "
"identify which surface is anterior (smooth/articular) vs. posterior (ridges/foramina). "
"Nutrient foramina on the femur and tibia open <b>away from the knee</b> (distally); "
"the fibula foramen opens <b>toward the knee</b> (proximally). "
"The lateral malleolus (fibula) descends <b>more distally</b> than the medial malleolus (tibia)."
))
story.append(SP(1, 0.5*cm))
# ── FEMUR ─────────────────────────────────────────────────────────────────
story.append(section_header("1. Femur", "Longest and heaviest bone in the body"))
story.append(SP(1, 0.3*cm))
story.append(Paragraph(
"The femur articulates proximally with the acetabulum (hip joint) and distally with the tibia "
"and patella (knee joint). Its shaft angles 7° from vertical so the distal end is closer to "
"the midline than the proximal end.",
S["body"]))
story.append(SP(1, 0.3*cm))
story.append(Paragraph("Proximal End", S["h2"]))
femur_prox = [
["Head", "Spherical (more so than humeral head). Articulates with acetabulum via ball-and-socket joint."],
["Fovea capitis", "Small pit on medial surface of head. Attachment for ligamentum teres."],
["Neck", "Cylindrical strut; projects supero-medially ~125° to shaft, slightly forward."],
["Greater trochanter", "Large lateral prominence. Insertions: gluteus minimus, gluteus medius, obturator internus, piriformis."],
["Trochanteric fossa", "Deep groove on posterior-medial surface of greater trochanter."],
["Lesser trochanter", "Smaller, blunt, posteromedial projection. Attachment: iliopsoas (psoas major + iliacus)."],
["Intertrochanteric line", "Anterior ridge connecting the two trochanters."],
["Intertrochanteric crest", "Posterior ridge; bears the quadrate tubercle for quadratus femoris."],
]
story.append(make_table(["Feature", "Description"], femur_prox, [4.5*cm, 12.5*cm]))
story.append(SP(1, 0.3*cm))
story.append(Paragraph("Shaft", S["h2"]))
femur_shaft = [
["Linea aspera", "Prominent posterior ridge; attachment for adductors, some quadriceps & hamstrings."],
["Pectineal line", "Proximal medial continuation of linea aspera (curls under lesser trochanter)."],
["Gluteal tuberosity", "Proximal lateral continuation of linea aspera; may form a 3rd trochanter."],
["Nutrient foramen", "Posterior surface near linea aspera. Opens DISTALLY ('flees the knee')."],
]
story.append(make_table(["Feature", "Description"], femur_shaft, [4.5*cm, 12.5*cm]))
story.append(SP(1, 0.3*cm))
story.append(Paragraph("Distal End", S["h2"]))
femur_distal = [
["Medial condyle", "Large articular surface; projects MORE distally than the lateral condyle."],
["Lateral condyle", "Articular surface for tibia; raised lateral lip guards against patellar dislocation."],
["Medial epicondyle", "Projection on medial condyle surface; ligamentous attachment."],
["Lateral epicondyle", "Projection on lateral condyle; ligament & muscle attachment."],
["Patellar articular surface", "Smooth anterior groove for patella glide during flexion/extension."],
["Intercondylar fossa", "Deep posterior groove between condyles; cruciate ligament attachments."],
["Popliteal surface", "Smooth triangular posterior area above intercondylar fossa."],
]
story.append(make_table(["Feature", "Description"], femur_distal, [4.5*cm, 12.5*cm]))
story.append(SP(1, 0.3*cm))
story.append(siding_box(
"Head is PROXIMAL and MEDIAL. Greater trochanter is LATERAL. Lesser trochanter and linea aspera "
"are POSTERIOR. Patellar articular surface is ANTERIOR. Nutrient foramen opens DISTALLY "
"('flees the knee'). The medial condyle projects more distally than the lateral condyle. "
"Key distinction from humerus: femoral head is a full sphere; humeral head is hemispheric."
))
story.append(SP(1, 0.5*cm))
# ── PATELLA ──────────────────────────────────────────────────────────────
story.append(section_header("2. Patella", "Largest sesamoid bone — the kneecap"))
story.append(SP(1, 0.3*cm))
story.append(Paragraph(
"The patella develops and is encased within the quadriceps femoris tendon. It articulates "
"with the patellar articular surface of the distal femur and improves the mechanical advantage "
"of the quadriceps by acting as a pulley.",
S["body"]))
story.append(SP(1, 0.3*cm))
patella_rows = [
["Apex", "Pointed non-articular inferior tip; points DISTALLY."],
["Base", "Broad superior aspect; attachment of quadriceps tendon."],
["Anterior surface", "Rough, convex; no articular cartilage. Palpable in vivo."],
["Medial articular facet", "Posterior surface — SMALLER of the two facets."],
["Lateral articular facet", "Posterior surface — LARGER of the two facets."],
]
story.append(make_table(["Feature", "Description"], patella_rows, [4.5*cm, 12.5*cm]))
story.append(SP(1, 0.3*cm))
story.append(siding_box(
"Place the patella on a flat surface with the articular processes touching the surface and "
"apex pointing AWAY from you. The patella will fall toward the side it came from — "
"because the LARGER articular facet is LATERAL and tips it that way."
))
story.append(SP(1, 0.5*cm))
# ── TIBIA ─────────────────────────────────────────────────────────────────
story.append(PageBreak())
story.append(section_header("3. Tibia", "Larger, medial bone of the leg — the shin bone"))
story.append(SP(1, 0.3*cm))
story.append(Paragraph(
"The tibia supports the majority of weight placed on the leg. It articulates superiorly "
"with the femoral condyles, laterally with the fibula at both ends, and distally with "
"the talus to form the ankle joint.",
S["body"]))
story.append(SP(1, 0.3*cm))
story.append(Paragraph("Proximal End", S["h2"]))
tibia_prox = [
["Tibial plateau", "Proximal articular surface for femoral condyles; divided into medial and lateral condyles."],
["Medial condyle", "Larger, oval; slightly concave articular surface."],
["Lateral condyle", "Smaller; has the superior fibular articular facet on its postero-inferior surface."],
["Tibial tuberosity", "Anterior prominence just below the plateau; attachment for the patellar ligament."],
["Gerdy's tubercle", "Anterolateral condyle; attachment for iliotibial band."],
]
story.append(make_table(["Feature", "Description"], tibia_prox, [4.5*cm, 12.5*cm]))
story.append(SP(1, 0.3*cm))
story.append(Paragraph("Shaft", S["h2"]))
tibia_shaft = [
["Anterior crest", "Sharp ridge forming the palpable 'shin.' Runs from tibial tuberosity to medial malleolus."],
["Interosseous crest", "Lateral sharp ridge; attachment for interosseous membrane binding tibia to fibula."],
["Nutrient foramen", "Posterior surface, proximal to midshaft. Opens DISTALLY ('flees the knee')."],
["Soleal line", "Oblique posterior ridge near proximal end; origin of soleus muscle."],
]
story.append(make_table(["Feature", "Description"], tibia_shaft, [4.5*cm, 12.5*cm]))
story.append(SP(1, 0.3*cm))
story.append(Paragraph("Distal End", S["h2"]))
tibia_distal = [
["Medial malleolus", "Bony projection on disto-MEDIAL tibia. Forms the medial ankle bulge; articulates with talus."],
["Fibular notch", "Disto-lateral concavity; site of the distal tibiofibular syndesmosis."],
["Inferior fibular articular surface", "Articular surface on inferior fibular notch — part of ankle mortise."],
["Inferior articular surface", "Distal face of tibia; articulates with superior trochlea of talus."],
]
story.append(make_table(["Feature", "Description"], tibia_distal, [4.5*cm, 12.5*cm]))
story.append(SP(1, 0.3*cm))
story.append(siding_box(
"Tibial plateau is PROXIMAL. Medial malleolus is DISTAL and MEDIAL. Fibular notch is DISTAL "
"and LATERAL. Tibial tuberosity and anterior crest are ANTERIOR. Nutrient foramen opens "
"DISTALLY ('flees the knee'). Soleal line is on the POSTERIOR surface."
))
story.append(SP(1, 0.5*cm))
# ── FIBULA ────────────────────────────────────────────────────────────────
story.append(section_header("4. Fibula", "Smaller, lateral bone of the leg — non-weight-bearing"))
story.append(SP(1, 0.3*cm))
story.append(Paragraph(
"The fibula is a slender bone providing lateral ankle stability and muscle attachment "
"surfaces. It bears virtually no body weight — that function belongs to the tibia. "
"The common fibular (peroneal) nerve winds around the fibular neck and is vulnerable "
"to injury there.",
S["body"]))
story.append(SP(1, 0.3*cm))
fibula_rows = [
["Head", "Proximal end; articulates with postero-inferior surface of lateral tibial condyle."],
["Neck", "Just distal to head. Common fibular nerve passes here — clinically important."],
["Shaft", "Long, slender, quadrilateral/triangular cross-section."],
["Interosseous crest", "Medial ridge; attachment for interosseous membrane (faces tibia)."],
["Nutrient foramen", "Opens PROXIMALLY — toward the knee (OPPOSITE to femur and tibia)."],
["Lateral malleolus", "Distal end; forms lateral ankle bulge. Extends MORE distally than medial malleolus."],
["Malleolar articular surface", "Medial surface of lateral malleolus; articulates with lateral surface of talus."],
]
story.append(make_table(["Feature", "Description"], fibula_rows, [4.5*cm, 12.5*cm]))
story.append(SP(1, 0.3*cm))
story.append(siding_box(
"Head is PROXIMAL. Lateral malleolus is DISTAL. Interosseous crest faces MEDIALLY (toward tibia). "
"Nutrient foramen opens PROXIMALLY ('toward the knee') — opposite to the femur and tibia. "
"The lateral malleolus extends more DISTALLY than the medial malleolus (tibia)."
))
story.append(SP(1, 0.5*cm))
# ── TARSALS ───────────────────────────────────────────────────────────────
story.append(PageBreak())
story.append(section_header("5. Tarsal Bones (7)", "Proximal foot skeleton"))
story.append(SP(1, 0.3*cm))
tarsal_rows = [
["Talus", "Trochlea (pulley body), head, neck",
"Tibia & fibula above; calcaneus below; navicular anteriorly",
"ONLY bone in the body with NO muscle attachments. Trochlea wider anteriorly."],
["Calcaneus", "Calcaneal tuber (heel), sustentaculum tali, anterior articular surface",
"Talus above; cuboid anteriorly",
"Largest tarsal. Sustentaculum tali supports talus medially."],
["Navicular", "Concave proximal surface, 3-part distal surface, medial tubercle",
"Talus proximally; 3 cuneiforms distally",
"Bowl-shaped. Medial tubercle is palpable. Concave surface faces the talus."],
["Cuboid", "Cuboid tuberosity (infero-lateral), groove for fibularis longus",
"Calcaneus proximally; navicular & lat. cuneiform medially; MT 4–5 distally",
"Lateral foot. Peroneal groove on inferior surface."],
["Medial (1st) cuneiform", "L-shaped facet medially",
"Navicular proximally; 1st MT distally; 2nd cuneiform laterally",
"LARGEST cuneiform."],
["Intermediate (2nd) cuneiform", "Pistol-shaped facet",
"Navicular proximally; 2nd MT distally",
"SMALLEST cuneiform."],
["Lateral (3rd) cuneiform", "Butterfly-wing double facet",
"Navicular proximally; 3rd MT distally; cuboid laterally",
"Mid-sized cuneiform."],
]
story.append(make_table(
["Bone", "Key Features", "Articulations", "ID / Siding Tips"],
tarsal_rows,
[2.8*cm, 4.2*cm, 5.2*cm, 4.8*cm]
))
story.append(SP(1, 0.5*cm))
# ── METATARSALS ───────────────────────────────────────────────────────────
story.append(section_header("6. Metatarsals (5)", "Arch of the foot — each has a base, shaft, and head"))
story.append(SP(1, 0.3*cm))
mt_rows = [
["1st (Hallux)", "Shortest but most ROBUST. Single round head inferior groove.",
"1st cuneiform", "Flat non-articular medial surface. Grooved inferior head."],
["2nd", "LONGEST metatarsal. Base keyed between 3 cuneiforms.",
"2nd cuneiform (primarily)", "Most stable; most common 'march fracture' site."],
["3rd", "Intermediate length.",
"Lateral cuneiform", "Standard morphology."],
["4th", "Slender shaft.",
"Cuboid + lateral cuneiform", "Base articulates with cuboid laterally."],
["5th", "Styloid TUBEROSITY at lateral base (very prominent).",
"Cuboid", "Only MT with large lateral base tuberosity. Avulsion fx here (peroneus brevis). Most lateral."],
]
story.append(make_table(
["Metatarsal", "Key Feature", "Proximal Articulation", "ID Notes"],
mt_rows,
[2.5*cm, 5*cm, 4.5*cm, 5*cm]
))
story.append(SP(1, 0.3*cm))
story.append(info_box(
"<b>General siding for metatarsals:</b> Base (concave/flat) is proximal; rounded head is distal. "
"The lateral side of the head has an articular facet for the adjacent (lateral) metatarsal; "
"the medial side has one for the adjacent (medial) metatarsal. The 1st MT has no medial facet. "
"The 5th MT has no lateral facet. Use facet presence/absence plus the styloid to side the bone."
))
story.append(SP(1, 0.5*cm))
# ── PHALANGES ─────────────────────────────────────────────────────────────
story.append(section_header("7. Phalanges (14)", "Toe bones — 2 in hallux, 3 in toes 2–5"))
story.append(SP(1, 0.3*cm))
pha_rows = [
["Proximal phalanx", "Largest of the three in each ray. Concave oval base (for MT head). Trochlear head."],
["Middle phalanx", "Absent in hallux. Trochlear base and head."],
["Distal phalanx", "Smallest. Ungual tuberosity at distal tip (supports nail bed)."],
]
story.append(make_table(
["Type", "Key Features"],
pha_rows,
[4*cm, 13*cm]
))
story.append(SP(1, 0.3*cm))
story.append(info_box(
"<b>Hallux vs. lesser toe phalanges:</b> Hallux (great toe) phalanges are considerably larger and "
"more robust. The distal phalanx has a flat ungual tuberosity. For siding phalanges: "
"The base is proximal (concave); the head is distal. Medial vs. lateral can be determined by "
"articular facet orientation and any asymmetry of the base."
))
story.append(SP(1, 0.5*cm))
# ── QUICK-REFERENCE SIDING SUMMARY ────────────────────────────────────────
story.append(PageBreak())
story.append(section_header("Quick-Reference: Siding Summary",
"Essential rules for left vs. right identification"))
story.append(SP(1, 0.3*cm))
siding_summary = [
["Femur",
"Head proximal & medial; GT lateral; Lesser trochanter posteromedial; "
"Linea aspera posterior; Patellar surface anterior; "
"Nutrient foramen opens DISTALLY."],
["Patella",
"Apex distal; Larger articular facet is LATERAL — bone falls to its side of origin."],
["Tibia",
"Plateau proximal; Medial malleolus distal-medial; Fibular notch distal-lateral; "
"Tibial tuberosity & anterior crest anterior; "
"Nutrient foramen opens DISTALLY."],
["Fibula",
"Head proximal; Lateral malleolus distal (extends more distally than medial malleolus); "
"Interosseous crest medial; "
"Nutrient foramen opens PROXIMALLY (toward knee)."],
["Talus",
"Trochlea superior; head/neck anterior; no muscle attachments. "
"Lateral process larger than medial."],
["Calcaneus",
"Calcaneal tuber posterior; sustentaculum tali medial; anterior facet for cuboid."],
["Navicular",
"Concave surface proximal (faces talus); three-part surface distal; tubercle medial."],
["Cuboid",
"Flat/non-articular surface lateral; tuberosity infero-lateral; "
"'ballerina's foot' — toe points to side of origin."],
["1st cuneiform",
"L-shaped facet anteriorly — tip of L points to side of origin."],
["2nd cuneiform",
"Pistol-shaped facet — barrel points to side of origin."],
["3rd cuneiform",
"Butterfly-wing double facet — narrower side to side of origin."],
["1st metatarsal",
"Flat non-articular medial surface; grooved inferior head; NO medial facet."],
["5th metatarsal",
"Prominent styloid tuberosity at lateral base; NO lateral facet. Most lateral."],
]
story.append(make_table(
["Bone", "Siding Rule"],
siding_summary,
[3.5*cm, 13.5*cm]
))
story.append(SP(1, 0.5*cm))
# ── MNEMONICS & CLINICAL NOTES ─────────────────────────────────────────────
story.append(section_header("Mnemonics & Clinical Pearls"))
story.append(SP(1, 0.3*cm))
mnemonics = [
("Nutrient foramen direction",
"Femur & Tibia: foramen 'FLEES THE KNEE' (opens distally).\n"
"Fibula: foramen FACES the knee (opens proximally) — the exception to the rule."),
("Medial vs. lateral malleolus height",
"Lateral malleolus (fibula) sits LOWER (more distal) than the medial malleolus (tibia). "
"Remember: 'Lateral is Lower.'"),
("Talus — the muscle-free bone",
"The talus is the only bone in the body with NO muscular attachments — "
"only ligamentous and articular connections. Useful for identification."),
("Femur vs. Humerus — head shape",
"Femoral head = full SPHERE. Humeral head = HEMISPHERE. "
"If it looks like a complete ball, it belongs to the lower limb."),
("March fracture",
"Stress fracture through the 2nd (or 3rd) metatarsal shaft from repetitive loading — "
"the 2nd MT is most tightly wedged between the cuneiforms and least mobile."),
("Peroneus brevis avulsion",
"A forceful inversion of the ankle can avulse the styloid process of the 5th metatarsal "
"where peroneus (fibularis) brevis inserts. Key differential from a Jones fracture "
"(transverse fracture 1.5–2 cm distal to base of 5th MT)."),
("Fibular neck fracture",
"The common fibular nerve wraps around the fibular neck. A fracture here "
"causes foot drop (loss of dorsiflexion and eversion)."),
("Sustentaculum tali",
"The medial shelf of the calcaneus that 'sustains' (supports) the head of the talus. "
"The flexor hallucis longus tendon passes in a groove beneath it."),
]
for title, text in mnemonics:
row_data = [
[Paragraph(title, S["h3"]),
Paragraph(text.replace("\n", "<br/>"), S["body"])]
]
t = Table(row_data, colWidths=[4.5*cm, 12.5*cm])
t.setStyle(TableStyle([
("VALIGN", (0,0), (-1,-1), "TOP"),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 4),
("LINEBELOW", (0,0), (-1,-1), 0.4, HexColor("#DDDDDD")),
]))
story.append(t)
story.append(SP(1, 0.5*cm))
# ── SELF-TEST ─────────────────────────────────────────────────────────────
story.append(PageBreak())
story.append(section_header("Self-Test: Bone Feature Identification"))
story.append(SP(1, 0.3*cm))
story.append(Paragraph(
"Cover the 'Answer' column and test yourself. These questions reflect the kind of "
"identification challenges encountered in anatomy practicals and forensic anthropology labs.",
S["body"]))
story.append(SP(1, 0.3*cm))
quiz = [
["1", "You find a bone with a spherical head, fovea capitis, greater and lesser trochanters, "
"and a posterior linea aspera.", "Femur"],
["2", "A small triangular bone with a larger posterior facet on one side "
"and a pointed inferior tip.", "Patella"],
["3", "A long bone with a tibial plateau, prominent anterior crest, "
"medial malleolus, and nutrient foramen opening distally.", "Tibia"],
["4", "A slender long bone whose nutrient foramen opens proximally and "
"whose distal end extends further than the corresponding tibia landmark.", "Fibula"],
["5", "The only tarsal bone with no muscle attachments, "
"with a pulley-shaped superior surface.", "Talus"],
["6", "The largest tarsal bone, forming the heel, with a medial shelf "
"supporting another tarsal bone.", "Calcaneus"],
["7", "A tarsal bone with a bowl-shaped concave proximal surface "
"and a three-part distal articular surface.", "Navicular"],
["8", "The shortest but most robust metatarsal, with a grooved inferior head "
"and a flat non-articular medial surface.", "1st Metatarsal"],
["9", "A metatarsal with a very prominent lateral styloid process at its base; "
"common site of avulsion fracture.", "5th Metatarsal"],
["10", "The smallest cuneiform, with a pistol-shaped articular facet.", "Intermediate (2nd) Cuneiform"],
["11", "You are siding a femur. The nutrient foramen opens toward the distal end. "
"The head is on the LEFT side of the bone. Which side?", "LEFT femur"],
["12", "You are siding a fibula. The lateral malleolus is in your hand. "
"The interosseous crest (roughened medial border) faces left. Which side?", "LEFT fibula"],
]
story.append(make_table(
["#", "Clues", "Answer"],
quiz,
[0.8*cm, 11.2*cm, 5*cm]
))
story.append(SP(1, 0.6*cm))
story.append(Paragraph(
"Sources: Gray's Anatomy for Students, 4e (ISBN 9780323934237) · "
"Forensic Anthropology: A Comprehensive Introduction, 2e (ISBN 9781498736121)",
S["note"]))
# ── BUILD ──────────────────────────────────────────────────────────────────
doc.build(story,
onFirstPage=on_first_page,
onLaterPages=on_later_page)
print(f"PDF created: {OUTPUT}")
if __name__ == "__main__":
build_pdf()