~/palm-coein/generate_pdf.py
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,
HRFlowable, KeepTogether
)
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
from reportlab.platypus import FrameBreak
OUTPUT = "/home/daytona/workspace/palm-coein/PALM_COEIN_Quick_Reference.pdf"
# ── Colour palette ────────────────────────────────────────────────────────────
DARK_BLUE = colors.HexColor("#1a3c5e")
PALM_COLOR = colors.HexColor("#c0392b") # red for PALM
COEIN_COLOR = colors.HexColor("#1a6b3e") # green for COEIN
LIGHT_RED = colors.HexColor("#fdecea")
LIGHT_GREEN = colors.HexColor("#e8f5e9")
LIGHT_BLUE = colors.HexColor("#e8f0fe")
GOLD = colors.HexColor("#d4a017")
GREY_BG = colors.HexColor("#f4f6f9")
MID_GREY = colors.HexColor("#7f8c8d")
WHITE = colors.white
BLACK = colors.HexColor("#1c1c1c")
doc = SimpleDocTemplate(
OUTPUT,
pagesize=A4,
rightMargin=15*mm, leftMargin=15*mm,
topMargin=15*mm, bottomMargin=15*mm,
title="PALM-COEIN Classification – Quick Reference",
author="Orris Medical Reference"
)
W, H = A4
COL = W - 30*mm # usable width
styles = getSampleStyleSheet()
def style(name, **kw):
return ParagraphStyle(name, **kw)
# ── Custom styles ─────────────────────────────────────────────────────────────
TITLE_S = style("Title_S", fontSize=22, textColor=WHITE,
fontName="Helvetica-Bold", alignment=TA_CENTER,
leading=28, spaceAfter=2)
SUB_S = style("Sub_S", fontSize=11, textColor=colors.HexColor("#cfe2ff"),
fontName="Helvetica", alignment=TA_CENTER, leading=14)
FOOT_S = style("Foot_S", fontSize=7.5, textColor=MID_GREY,
fontName="Helvetica-Oblique", alignment=TA_CENTER)
SEC_HDR = style("SecHdr", fontSize=13, textColor=WHITE,
fontName="Helvetica-Bold", alignment=TA_CENTER,
leading=16)
CELL_HDR= style("CellHdr", fontSize=9.5, textColor=WHITE,
fontName="Helvetica-Bold", alignment=TA_CENTER, leading=12)
CELL_TXT= style("CellTxt", fontSize=8.5, textColor=BLACK,
fontName="Helvetica", leading=11, alignment=TA_LEFT)
CELL_KEY= style("CellKey", fontSize=9, textColor=BLACK,
fontName="Helvetica-Bold", leading=12, alignment=TA_LEFT)
BODY_S = style("Body_S", fontSize=8.5, textColor=BLACK,
fontName="Helvetica", leading=12, alignment=TA_JUSTIFY)
BOLD_S = style("Bold_S", fontSize=8.5, textColor=BLACK,
fontName="Helvetica-Bold", leading=12)
NOTE_S = style("Note_S", fontSize=8, textColor=MID_GREY,
fontName="Helvetica-Oblique", leading=11)
PILL_S = style("Pill_S", fontSize=9, textColor=WHITE,
fontName="Helvetica-Bold", alignment=TA_CENTER, leading=11)
story = []
# ══════════════════════════════════════════════════════════════════════════════
# HEADER BANNER
# ══════════════════════════════════════════════════════════════════════════════
header_data = [[
Paragraph("<b>PALM-COEIN</b>", TITLE_S),
]]
header_table = Table(header_data, colWidths=[COL])
header_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), DARK_BLUE),
("TOPPADDING", (0,0), (-1,-1), 12),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 10),
("RIGHTPADDING", (0,0), (-1,-1), 10),
("ROUNDEDCORNERS",(0,0), (-1,-1), [6,6,0,0]),
]))
story.append(header_table)
sub_data = [[
Paragraph("Classification of Abnormal Uterine Bleeding (AUB) — FIGO 2011", SUB_S),
]]
sub_table = Table(sub_data, colWidths=[COL])
sub_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), colors.HexColor("#2c5282")),
("TOPPADDING", (0,0), (-1,-1), 6),
("BOTTOMPADDING", (0,0), (-1,-1), 8),
("LEFTPADDING", (0,0), (-1,-1), 10),
("ROUNDEDCORNERS",(0,0), (-1,-1), [0,0,6,6]),
]))
story.append(sub_table)
story.append(Spacer(1, 8))
# ── Definition box ─────────────────────────────────────────────────────────
def_data = [[
Paragraph(
"<b>AUB</b> = Any uterine bleeding that is abnormal in <b>regularity, frequency, "
"duration, or volume</b> in the absence of pregnancy.",
BODY_S)
]]
def_table = Table(def_data, colWidths=[COL])
def_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), LIGHT_BLUE),
("BOX", (0,0), (-1,-1), 0.8, DARK_BLUE),
("TOPPADDING", (0,0), (-1,-1), 7),
("BOTTOMPADDING", (0,0), (-1,-1), 7),
("LEFTPADDING", (0,0), (-1,-1), 10),
("RIGHTPADDING", (0,0), (-1,-1), 10),
("ROUNDEDCORNERS",(0,0), (-1,-1), [4,4,4,4]),
]))
story.append(def_table)
story.append(Spacer(1, 10))
# ══════════════════════════════════════════════════════════════════════════════
# PALM — STRUCTURAL CAUSES
# ══════════════════════════════════════════════════════════════════════════════
palm_hdr = Table([[Paragraph("PALM — Structural Causes", SEC_HDR)]], colWidths=[COL])
palm_hdr.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), PALM_COLOR),
("TOPPADDING", (0,0), (-1,-1), 8),
("BOTTOMPADDING", (0,0), (-1,-1), 8),
("ROUNDEDCORNERS",(0,0), (-1,-1), [6,6,0,0]),
]))
story.append(palm_hdr)
palm_col_w = [12*mm, 28*mm, 35*mm, 50*mm, COL - 12*mm - 28*mm - 35*mm - 50*mm]
palm_rows = [
# header
[
Paragraph("Code", CELL_HDR),
Paragraph("Category", CELL_HDR),
Paragraph("Pathology", CELL_HDR),
Paragraph("Clinical Features", CELL_HDR),
Paragraph("Investigation", CELL_HDR),
],
# P
[
Paragraph("<b>P</b>", style("PL", fontSize=16, textColor=PALM_COLOR,
fontName="Helvetica-Bold", alignment=TA_CENTER, leading=18)),
Paragraph("<b>Polyp</b>\n(AUB-P)", CELL_KEY),
Paragraph("Endometrial or endocervical epithelial overgrowth", CELL_TXT),
Paragraph("• Intermenstrual bleeding (IMB)\n• Postcoital bleeding\n• Variable flow", CELL_TXT),
Paragraph("TVUS / SIS\nHysteroscopy", CELL_TXT),
],
# A
[
Paragraph("<b>A</b>", style("AL", fontSize=16, textColor=PALM_COLOR,
fontName="Helvetica-Bold", alignment=TA_CENTER, leading=18)),
Paragraph("<b>Adenomyosis</b>\n(AUB-A)", CELL_KEY),
Paragraph("Endometrial glands + stroma within myometrium", CELL_TXT),
Paragraph("• Heavy menstrual bleeding (HMB)\n• Dysmenorrhea\n• Bulky, tender uterus", CELL_TXT),
Paragraph("TVUS / MRI", CELL_TXT),
],
# L
[
Paragraph("<b>L</b>", style("LL", fontSize=16, textColor=PALM_COLOR,
fontName="Helvetica-Bold", alignment=TA_CENTER, leading=18)),
Paragraph("<b>Leiomyoma</b>\n(AUB-L)\n\nSubmucosal → AUB-LSM\nOther → AUB-LO", CELL_KEY),
Paragraph("Benign smooth muscle tumour; submucosal type most likely to cause AUB", CELL_TXT),
Paragraph("• HMB, prolonged bleeding\n• Pelvic pressure/bulk\n• Irregular uterus on exam", CELL_TXT),
Paragraph("TVUS / SIS\nMRI for mapping", CELL_TXT),
],
# M
[
Paragraph("<b>M</b>", style("ML", fontSize=16, textColor=PALM_COLOR,
fontName="Helvetica-Bold", alignment=TA_CENTER, leading=18)),
Paragraph("<b>Malignancy &\nHyperplasia</b>\n(AUB-M)", CELL_KEY),
Paragraph("Endometrial carcinoma; atypical hyperplasia; cervical/other malignancy", CELL_TXT),
Paragraph("• Postmenopausal bleeding\n• Irregular, painless bleeding\n• Risk: obesity, nulliparity, unopposed estrogen", CELL_TXT),
Paragraph("Endometrial biopsy\nD&C\nHysteroscopy\nMRI staging", CELL_TXT),
],
]
palm_table = Table(palm_rows, colWidths=palm_col_w, repeatRows=1)
palm_table.setStyle(TableStyle([
# header row
("BACKGROUND", (0,0), (-1,0), colors.HexColor("#8b1a10")),
("TEXTCOLOR", (0,0), (-1,0), WHITE),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("FONTSIZE", (0,0), (-1,0), 9),
# alternating rows
("BACKGROUND", (0,1), (-1,1), LIGHT_RED),
("BACKGROUND", (0,2), (-1,2), WHITE),
("BACKGROUND", (0,3), (-1,3), LIGHT_RED),
("BACKGROUND", (0,4), (-1,4), WHITE),
# grid
("GRID", (0,0), (-1,-1), 0.4, colors.HexColor("#e0b0b0")),
("BOX", (0,0), (-1,-1), 0.8, PALM_COLOR),
# padding
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 5),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("ALIGN", (0,0), (0,-1), "CENTER"),
("ROUNDEDCORNERS",(0,0), (-1,-1), [0,0,6,6]),
]))
story.append(palm_table)
story.append(Spacer(1, 10))
# ══════════════════════════════════════════════════════════════════════════════
# COEIN — NON-STRUCTURAL CAUSES
# ══════════════════════════════════════════════════════════════════════════════
coein_hdr = Table([[Paragraph("COEIN — Non-Structural Causes", SEC_HDR)]], colWidths=[COL])
coein_hdr.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), COEIN_COLOR),
("TOPPADDING", (0,0), (-1,-1), 8),
("BOTTOMPADDING", (0,0), (-1,-1), 8),
("ROUNDEDCORNERS",(0,0), (-1,-1), [6,6,0,0]),
]))
story.append(coein_hdr)
coein_col_w = [12*mm, 30*mm, 38*mm, 48*mm, COL - 12*mm - 30*mm - 38*mm - 48*mm]
coein_rows = [
[
Paragraph("Code", CELL_HDR),
Paragraph("Category", CELL_HDR),
Paragraph("Mechanism", CELL_HDR),
Paragraph("Clinical Features", CELL_HDR),
Paragraph("Key Tests", CELL_HDR),
],
# C
[
Paragraph("<b>C</b>", style("CL", fontSize=16, textColor=COEIN_COLOR,
fontName="Helvetica-Bold", alignment=TA_CENTER, leading=18)),
Paragraph("<b>Coagulopathy</b>\n(AUB-C)", CELL_KEY),
Paragraph("vWD, ITP, anticoagulant therapy, liver disease", CELL_TXT),
Paragraph("• HMB since menarche\n• Easy bruising, prolonged bleeding\n• Family history of bleeding", CELL_TXT),
Paragraph("PT, aPTT\nvWF antigen\nPlatelet count", CELL_TXT),
],
# O
[
Paragraph("<b>O</b>", style("OL", fontSize=16, textColor=COEIN_COLOR,
fontName="Helvetica-Bold", alignment=TA_CENTER, leading=18)),
Paragraph("<b>Ovulatory\nDysfunction</b>\n(AUB-O)", CELL_KEY),
Paragraph("Anovulation → unopposed estrogen\nCauses: PCOS, thyroid, hyperprolactinemia, perimenopause, adolescence", CELL_TXT),
Paragraph("• Irregular, unpredictable bleeding\n• May be heavy or light\n• Intermenstrual bleeding", CELL_TXT),
Paragraph("TSH, Prolactin\nFSH, LH\nTestosterone\nTVUS", CELL_TXT),
],
# E
[
Paragraph("<b>E</b>", style("EL", fontSize=16, textColor=COEIN_COLOR,
fontName="Helvetica-Bold", alignment=TA_CENTER, leading=18)),
Paragraph("<b>Endometrial</b>\n(AUB-E)", CELL_KEY),
Paragraph("Primary endometrial disorder; abnormal local haemostasis (↓ vasoconstrictors, ↑ fibrinolysis)", CELL_TXT),
Paragraph("• Regular cycles but HMB\n• No structural cause found\n• Normal ovulation", CELL_TXT),
Paragraph("Endometrial biopsy\nHysteroscopy\n(diagnosis of exclusion)", CELL_TXT),
],
# I
[
Paragraph("<b>I</b>", style("IL", fontSize=16, textColor=COEIN_COLOR,
fontName="Helvetica-Bold", alignment=TA_CENTER, leading=18)),
Paragraph("<b>Iatrogenic</b>\n(AUB-I)", CELL_KEY),
Paragraph("Hormonal contraceptives, IUDs, anticoagulants, antipsychotics (→ hyperprolactinemia)", CELL_TXT),
Paragraph("• Breakthrough bleeding on OCP\n• Irregular bleeding on progestin-only\n• Drug history positive", CELL_TXT),
Paragraph("Drug history\nProlactin if on antipsychotics", CELL_TXT),
],
# N
[
Paragraph("<b>N</b>", style("NL", fontSize=16, textColor=COEIN_COLOR,
fontName="Helvetica-Bold", alignment=TA_CENTER, leading=18)),
Paragraph("<b>Not Yet\nClassified</b>\n(AUB-N)", CELL_KEY),
Paragraph("Rare / poorly defined entities not fitting other categories", CELL_TXT),
Paragraph("• Arteriovenous malformations\n• Myometrial hypertrophy\n• Endometritis (rare)", CELL_TXT),
Paragraph("Case-dependent\nspecialist workup", CELL_TXT),
],
]
coein_table = Table(coein_rows, colWidths=coein_col_w, repeatRows=1)
coein_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), colors.HexColor("#145232")),
("TEXTCOLOR", (0,0), (-1,0), WHITE),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("FONTSIZE", (0,0), (-1,0), 9),
("BACKGROUND", (0,1), (-1,1), LIGHT_GREEN),
("BACKGROUND", (0,2), (-1,2), WHITE),
("BACKGROUND", (0,3), (-1,3), LIGHT_GREEN),
("BACKGROUND", (0,4), (-1,4), WHITE),
("BACKGROUND", (0,5), (-1,5), LIGHT_GREEN),
("GRID", (0,0), (-1,-1), 0.4, colors.HexColor("#a8d5b5")),
("BOX", (0,0), (-1,-1), 0.8, COEIN_COLOR),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 5),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("ALIGN", (0,0), (0,-1), "CENTER"),
("ROUNDEDCORNERS",(0,0), (-1,-1), [0,0,6,6]),
]))
story.append(coein_table)
story.append(Spacer(1, 10))
# ══════════════════════════════════════════════════════════════════════════════
# NORMAL MENSTRUAL PARAMETERS + MANAGEMENT OVERVIEW (side by side)
# ══════════════════════════════════════════════════════════════════════════════
half = (COL - 6*mm) / 2
# --- Normal Parameters ---
norm_title = Table([[Paragraph("Normal Menstrual Parameters", style("NT",
fontSize=9.5, textColor=WHITE, fontName="Helvetica-Bold",
alignment=TA_CENTER, leading=12))]],
colWidths=[half])
norm_title.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), DARK_BLUE),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("ROUNDEDCORNERS",(0,0), (-1,-1), [4,4,0,0]),
]))
norm_rows = [
[Paragraph("<b>Parameter</b>", CELL_HDR), Paragraph("<b>Normal Range</b>", CELL_HDR)],
[Paragraph("Frequency", CELL_TXT), Paragraph("Every 24–38 days", CELL_TXT)],
[Paragraph("Duration", CELL_TXT), Paragraph("4–8 days", CELL_TXT)],
[Paragraph("Volume", CELL_TXT), Paragraph("5–80 mL per cycle", CELL_TXT)],
[Paragraph("Regularity",CELL_TXT), Paragraph("Variation ≤7-9 days cycle-to-cycle", CELL_TXT)],
]
norm_table = Table(norm_rows, colWidths=[half*0.5, half*0.5])
norm_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), colors.HexColor("#34495e")),
("TEXTCOLOR", (0,0), (-1,0), WHITE),
("BACKGROUND", (0,1), (-1,1), GREY_BG),
("BACKGROUND", (0,2), (-1,2), WHITE),
("BACKGROUND", (0,3), (-1,3), GREY_BG),
("BACKGROUND", (0,4), (-1,4), WHITE),
("GRID", (0,0), (-1,-1), 0.4, colors.HexColor("#bdc3c7")),
("BOX", (0,0), (-1,-1), 0.8, DARK_BLUE),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 5),
("ROUNDEDCORNERS",(0,0), (-1,-1), [0,0,4,4]),
]))
# --- Management Summary ---
mgmt_title = Table([[Paragraph("Management Overview", style("MT",
fontSize=9.5, textColor=WHITE, fontName="Helvetica-Bold",
alignment=TA_CENTER, leading=12))]],
colWidths=[half])
mgmt_title.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), colors.HexColor("#6b2d8b")),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("ROUNDEDCORNERS",(0,0), (-1,-1), [4,4,0,0]),
]))
mgmt_rows = [
[Paragraph("<b>Situation</b>", CELL_HDR), Paragraph("<b>First-line Treatment</b>", CELL_HDR)],
[Paragraph("Acute / haemodynamic instability", CELL_TXT),
Paragraph("IV conj. estrogen 25 mg q4-6h; D&C; UAE", CELL_TXT)],
[Paragraph("Anovulatory (stable)", CELL_TXT),
Paragraph("Combined OCP 1 tab q6h × 5–7 days, then cyclic", CELL_TXT)],
[Paragraph("HMB (non-structural)", CELL_TXT),
Paragraph("Tranexamic acid 1.3 g TDS × 5 days; LNG-IUD", CELL_TXT)],
[Paragraph("Structural cause (fibroid/polyp)", CELL_TXT),
Paragraph("Hysteroscopic resection; myomectomy; UAE", CELL_TXT)],
[Paragraph("Postmenopausal AUB", CELL_TXT),
Paragraph("Endometrial biopsy FIRST; treat cause", CELL_TXT)],
[Paragraph("Refractory / family complete", CELL_TXT),
Paragraph("Endometrial ablation; hysterectomy", CELL_TXT)],
]
mgmt_table = Table(mgmt_rows, colWidths=[half*0.52, half*0.48])
mgmt_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), colors.HexColor("#4a1a6b")),
("TEXTCOLOR", (0,0), (-1,0), WHITE),
("BACKGROUND", (0,1), (-1,1), colors.HexColor("#f3e8ff")),
("BACKGROUND", (0,2), (-1,2), WHITE),
("BACKGROUND", (0,3), (-1,3), colors.HexColor("#f3e8ff")),
("BACKGROUND", (0,4), (-1,4), WHITE),
("BACKGROUND", (0,5), (-1,5), colors.HexColor("#f3e8ff")),
("BACKGROUND", (0,6), (-1,6), WHITE),
("GRID", (0,0), (-1,-1), 0.4, colors.HexColor("#d8b4fe")),
("BOX", (0,0), (-1,-1), 0.8, colors.HexColor("#6b2d8b")),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 5),
("ROUNDEDCORNERS",(0,0), (-1,-1), [0,0,4,4]),
]))
# combine left/right
norm_block = [norm_title, norm_table]
mgmt_block = [mgmt_title, mgmt_table]
from reportlab.platypus import BalancedColumns
side_data = [[norm_title, Spacer(6,1), mgmt_title],
[norm_table, Spacer(6,1), mgmt_table]]
side_table = Table(
[[norm_title, Spacer(6,1), mgmt_title],
[norm_table, Spacer(6,1), mgmt_table]],
colWidths=[half, 6, half]
)
side_table.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(side_table)
story.append(Spacer(1, 8))
# ══════════════════════════════════════════════════════════════════════════════
# INVESTIGATION ALGORITHM BOX
# ══════════════════════════════════════════════════════════════════════════════
inv_title = Table([[Paragraph("Step-wise Investigation Algorithm", style("IT",
fontSize=9.5, textColor=WHITE, fontName="Helvetica-Bold",
alignment=TA_CENTER, leading=12))]],
colWidths=[COL])
inv_title.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), colors.HexColor("#b7600c")),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("ROUNDEDCORNERS",(0,0), (-1,-1), [4,4,0,0]),
]))
story.append(inv_title)
steps = [
["1", "Beta-hCG", "Exclude pregnancy — always the FIRST step"],
["2", "CBC + Platelets", "Assess anaemia; detect thrombocytopenia"],
["3", "TSH", "Thyroid dysfunction → anovulation"],
["4", "Prolactin", "Hyperprolactinemia → anovulation"],
["5", "Coagulation (PT, aPTT, vWF)", "Exclude coagulopathy (esp. adolescents)"],
["6", "FSH, LH, Testosterone", "Evaluate HPO axis; hyperandrogenism (PCOS)"],
["7", "TVUS / SIS", "Structural causes — polyps, fibroids, endometrial thickness"],
["8", "Endometrial biopsy", "Women >35 y, prolonged anovulation, risk factors for Ca"],
["9", "Hysteroscopy", "Gold standard for intracavitary pathology; directed biopsy"],
]
step_rows = [[Paragraph("<b>#</b>", CELL_HDR), Paragraph("<b>Test</b>", CELL_HDR), Paragraph("<b>Purpose</b>", CELL_HDR)]]
for s in steps:
step_rows.append([
Paragraph(f"<b>{s[0]}</b>", style("SN", fontSize=11, textColor=colors.HexColor("#b7600c"),
fontName="Helvetica-Bold", alignment=TA_CENTER, leading=14)),
Paragraph(s[1], CELL_KEY),
Paragraph(s[2], CELL_TXT),
])
step_table = Table(step_rows, colWidths=[10*mm, 55*mm, COL - 65*mm])
step_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), colors.HexColor("#7c3a05")),
("TEXTCOLOR", (0,0), (-1,0), WHITE),
*[("BACKGROUND", (0,i), (-1,i), GREY_BG if i%2==1 else WHITE) for i in range(1, 10)],
("GRID", (0,0), (-1,-1), 0.4, colors.HexColor("#e0c9b0")),
("BOX", (0,0), (-1,-1), 0.8, colors.HexColor("#b7600c")),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 5),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("ALIGN", (0,0), (0,-1), "CENTER"),
("ROUNDEDCORNERS",(0,0), (-1,-1), [0,0,4,4]),
]))
story.append(step_table)
story.append(Spacer(1, 6))
# ── Footer ─────────────────────────────────────────────────────────────────
HRFlowable(width=COL, thickness=0.5, color=MID_GREY)
story.append(HRFlowable(width=COL, thickness=0.5, color=MID_GREY))
story.append(Spacer(1, 3))
story.append(Paragraph(
"FIGO 2011 Classification • Sources: Goldman-Cecil Medicine · Berek & Novak's Gynecology · Rosen's Emergency Medicine • For educational use only",
FOOT_S))
# ══════════════════════════════════════════════════════════════════════════════
# BUILD
# ══════════════════════════════════════════════════════════════════════════════
doc.build(story)
print(f"PDF saved: {OUTPUT}")