~/gynae-rounds-pdf/build_pdf.py
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import cm, mm
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, KeepTogether, PageBreak
)
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
from reportlab.platypus import Frame, PageTemplate
from reportlab.pdfgen import canvas
from datetime import date
OUTPUT = "/tmp/workspace/gynae-rounds-pdf/Gynae_Short_Cases_Quick_Reference.pdf"
# ── Colour palette ──────────────────────────────────────────────────────────
NAVY = colors.HexColor("#1B3A6B")
TEAL = colors.HexColor("#007F8B")
SAGE = colors.HexColor("#E8F5F7")
ACCENT = colors.HexColor("#D94F4F")
GOLD = colors.HexColor("#F0A500")
LIGHT_GRY = colors.HexColor("#F5F5F5")
MID_GRY = colors.HexColor("#CCCCCC")
WHITE = colors.white
BLACK = colors.black
# ── Styles ──────────────────────────────────────────────────────────────────
styles = getSampleStyleSheet()
def S(name, **kw):
"""Quick style builder."""
return ParagraphStyle(name, **kw)
TITLE_STYLE = S("Title", fontSize=20, textColor=WHITE, alignment=TA_CENTER,
fontName="Helvetica-Bold", spaceAfter=2)
SUBTITLE_STYLE = S("Sub", fontSize=11, textColor=colors.HexColor("#B0D8E0"),
alignment=TA_CENTER, fontName="Helvetica", spaceAfter=4)
CASE_HEADER = S("CaseH", fontSize=13, textColor=WHITE, fontName="Helvetica-Bold",
alignment=TA_LEFT, leading=16)
SECTION_LABEL = S("SecL", fontSize=8, textColor=TEAL, fontName="Helvetica-Bold",
spaceAfter=1, spaceBefore=4)
BODY = S("Body", fontSize=8.5, textColor=BLACK, fontName="Helvetica",
leading=12, spaceAfter=1)
BULLET = S("Bul", fontSize=8.5, textColor=BLACK, fontName="Helvetica",
leading=12, leftIndent=10, bulletIndent=0, spaceAfter=1)
DIAG_STYLE = S("Diag", fontSize=10, textColor=NAVY, fontName="Helvetica-Bold",
spaceAfter=2)
SMALL = S("Sm", fontSize=7.5, textColor=colors.HexColor("#555555"),
fontName="Helvetica", leading=10)
TABLE_HDR = S("TH", fontSize=8, textColor=WHITE, fontName="Helvetica-Bold",
alignment=TA_CENTER, leading=10)
TABLE_CELL = S("TC", fontSize=8, textColor=BLACK, fontName="Helvetica",
alignment=TA_LEFT, leading=10)
# ── Header/Footer canvas callback ───────────────────────────────────────────
def header_footer(canvas_obj, doc):
canvas_obj.saveState()
W, H = A4
# Footer
canvas_obj.setFont("Helvetica", 7)
canvas_obj.setFillColor(colors.HexColor("#888888"))
canvas_obj.drawString(1.5*cm, 0.8*cm,
f"Gynaecology Short Cases | Quick Reference | {date.today().strftime('%B %Y')}")
canvas_obj.drawRightString(W - 1.5*cm, 0.8*cm,
f"Page {doc.page}")
canvas_obj.setStrokeColor(MID_GRY)
canvas_obj.setLineWidth(0.3)
canvas_obj.line(1.5*cm, 1.0*cm, W - 1.5*cm, 1.0*cm)
canvas_obj.restoreState()
# ── Helper: coloured banner row for a case ──────────────────────────────────
def case_banner(case_num, title, diagnosis):
data = [[
Paragraph(f"CASE {case_num}", CASE_HEADER),
Paragraph(title, CASE_HEADER),
Paragraph(f"Dx: {diagnosis}", S("DxBan", fontSize=9.5, textColor=GOLD,
fontName="Helvetica-Bold", alignment=TA_LEFT, leading=13)),
]]
col_widths = [2.0*cm, 8.0*cm, 7.5*cm]
t = Table(data, colWidths=col_widths)
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), NAVY),
("TOPPADDING", (0,0), (-1,-1), 6),
("BOTTOMPADDING",(0,0), (-1,-1), 6),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING", (0,0), (-1,-1), 6),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("LINEBELOW", (0,0), (-1,-1), 1, TEAL),
]))
return t
# ── Helper: two-column workup + management table ────────────────────────────
def workup_mgmt_table(inv_rows, mgmt_bullets):
"""inv_rows: list of (Investigation, Purpose) | mgmt_bullets: list of strings"""
left_data = [[
Paragraph("INVESTIGATIONS / WORKUP", TABLE_HDR),
Paragraph("PURPOSE", TABLE_HDR),
]]
for inv, pur in inv_rows:
left_data.append([
Paragraph(inv, S("TCI", fontSize=8, fontName="Helvetica-Bold",
textColor=NAVY, leading=10)),
Paragraph(pur, TABLE_CELL),
])
right_data = [[Paragraph("MANAGEMENT", TABLE_HDR)]]
for b in mgmt_bullets:
right_data.append([Paragraph(f"\u2022 {b}", BULLET)])
# Build left table
lt = Table(left_data, colWidths=[4.5*cm, 5.5*cm])
lt.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), TEAL),
("BACKGROUND", (0,1), (-1,-1), LIGHT_GRY),
("ROWBACKGROUNDS",(0,1),(-1,-1),[WHITE, LIGHT_GRY]),
("GRID", (0,0), (-1,-1), 0.3, MID_GRY),
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING",(0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 4),
("VALIGN", (0,0), (-1,-1), "TOP"),
]))
# Build right table
rt = Table(right_data, colWidths=[7.0*cm])
rt.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), ACCENT),
("BACKGROUND", (0,1), (-1,-1), colors.HexColor("#FFF5F5")),
("ROWBACKGROUNDS",(0,1),(-1,-1),[WHITE, colors.HexColor("#FFF5F5")]),
("GRID", (0,0), (-1,-1), 0.3, MID_GRY),
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING",(0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 6),
("RIGHTPADDING", (0,0), (-1,-1), 4),
("VALIGN", (0,0), (-1,-1), "TOP"),
]))
combined = Table([[lt, Spacer(0.3*cm,0), rt]], colWidths=[10.2*cm, 0.3*cm, 7.2*cm])
combined.setStyle(TableStyle([
("VALIGN", (0,0), (-1,-1), "TOP"),
("LEFTPADDING", (0,0), (-1,-1), 0),
("RIGHTPADDING", (0,0), (-1,-1), 0),
("TOPPADDING", (0,0), (-1,-1), 0),
("BOTTOMPADDING", (0,0), (-1,-1), 0),
]))
return combined
# ── Helper: key facts ribbon ────────────────────────────────────────────────
def facts_ribbon(facts):
"""facts: list of (label, value)"""
cells = []
for label, val in facts:
cells.append(Paragraph(
f'<font color="#007F8B"><b>{label}:</b></font> {val}',
S("FR", fontSize=8, fontName="Helvetica", leading=10, textColor=BLACK)
))
t = Table([cells], colWidths=[17.7*cm/len(facts)]*len(facts))
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), SAGE),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING",(0,0),(-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 6),
("RIGHTPADDING",(0,0),(-1,-1), 6),
("BOX", (0,0),(-1,-1), 0.5, TEAL),
("INNERGRID", (0,0),(-1,-1), 0.3, MID_GRY),
("VALIGN", (0,0),(-1,-1), "MIDDLE"),
]))
return t
# ── Case data ────────────────────────────────────────────────────────────────
CASES = [
# ── CASE 1 ──────────────────────────────────────────────────────────────
dict(
num=1, short="65F | DM 30y | Postmenopausal 18y | PMB 4 months | Uterus 8wks",
diagnosis="Endometrial Carcinoma (Type I Endometrioid)",
facts=[("Age","65F"), ("Key Sx","Postmenopausal bleeding"), ("Risk","DM + HTN"),
("FIGO","Confirm by MRI + biopsy"), ("Stage clue","Ut enlarged 8wks")],
inv=[
("Endometrial Biopsy (Pipelle)", "Gold standard tissue diagnosis"),
("TVUS endometrial thickness", ">4 mm in PMB is significant"),
("Fractional D&C", "If biopsy inconclusive / Cx involvement"),
("MRI Pelvis", "Myometrial invasion depth, Cx spread, staging"),
("CT Chest/Abdomen/Pelvis", "Lymph nodes, distant mets"),
("CA-125", "Baseline; elevated in advanced disease"),
("CBC, LFT, RFT, HbA1c", "Preop + DM assessment"),
],
mgmt=[
"TAH + BSO + pelvic & para-aortic lymph node dissection",
"Adjuvant vaginal brachytherapy: Stage IB (deep invasion)",
"Pelvic EBRT: Stage II-III disease",
"Chemotherapy (advanced/recurrent): Carboplatin + Paclitaxel",
"Hormonal therapy: MPA/Megace for Grade 1 unfit for surgery",
"Optimize DM and HTN preoperatively",
],
),
# ── CASE 2 ──────────────────────────────────────────────────────────────
dict(
num=2, short="30F | Nullipara | Menorrhagia 4y | Dysmenorrhoea | Uterus 10wks symmetrical",
diagnosis="Fibroid Uterus (Leiomyoma) - Submucosal/Intramural",
facts=[("Age","30F"), ("Parity","Nulliparous"), ("Key Sx","Menorrhagia + dysmenorrhoea"),
("Uterus","10 wks, symmetrical"), ("Pallor","++")],
inv=[
("Transvaginal USG", "Fibroid number, size, subtype"),
("CBC + Iron studies", "Degree of iron-deficiency anaemia"),
("Hysteroscopy", "Gold standard submucosal fibroids"),
("MRI pelvis", "Mapping before myomectomy/UAE"),
("TSH", "Rule out hypothyroidism"),
("Endometrial biopsy", "Abnormal bleeding pattern workup"),
("Coagulation profile", "Rule out bleeding disorder"),
],
mgmt=[
"MEDICAL: Iron supplementation, Tranexamic acid, NSAIDs",
"OCP or LNG-IUS (Mirena) to reduce menstrual flow",
"GnRH agonist (Leuprolide) x3-6 months preop - shrinks fibroid",
"SURGICAL: Myomectomy (fertility-preserving) - laparoscopic/hysteroscopic",
"Hysteroscopic myomectomy for submucosal fibroids",
"UAE (Uterine Artery Embolisation) - minimally invasive option",
],
),
# ── CASE 3 ──────────────────────────────────────────────────────────────
dict(
num=3, short="64F | Postmenopausal 20y | P3L3 | Pelvic mass + gross ascites | Left ovarian complex mass 10x8x9 cm",
diagnosis="Carcinoma Ovary - Epithelial (HGSC), Stage IIIC",
facts=[("Age","64F"), ("Triad","PM + Adnexal mass + Ascites"), ("Mass","Left ovary 10x8x9 cm complex"),
("Stage","IIIC - peritoneal spread"), ("Marker","CA-125 + HE4/ROMA")],
inv=[
("CA-125 + HE4 / ROMA score", "Malignancy risk stratification"),
("CEA, CA 19-9", "Rule out GI primary"),
("Contrast CT abdomen/pelvis", "Extent, nodes, peritoneal deposits"),
("MRI pelvis", "Ovarian mass characterisation"),
("Diagnostic paracentesis", "Cytology for malignant cells"),
("BRCA1/BRCA2 gene testing", "Treatment + genetic implications"),
("CBC, LFT, albumin, RFT", "Nutritional/preop status"),
("CXR / CT chest", "Pleural effusion, Stage IV"),
],
mgmt=[
"Optimal cytoreductive surgery: TAH + BSO + omentectomy + peritoneal stripping",
"Aim for no residual disease (<1 cm) - key prognostic factor",
"Chemotherapy: Carboplatin AUC5 + Paclitaxel 175 mg/m², 6 cycles",
"Bevacizumab add-on for high-risk Stage III/IV",
"PARP inhibitors (Olaparib/Niraparib) maintenance if BRCA-mutated",
"NACT + interval debulking if unresectable / poor performance status",
"Symptomatic ascites: serial paracentesis / PleurX catheter",
],
),
# ── CASE 4 ──────────────────────────────────────────────────────────────
dict(
num=4, short="30F | P2 | LCB 3y | Menorrhagia + pelvic pain 6m | Uterus 16wks irregular",
diagnosis="Fibroid Uterus - Multiple (Family Complete)",
facts=[("Age","30F"), ("Parity","Para 2 - family complete"), ("Uterus","16 wks, irregular"),
("Key Sx","Menorrhagia + dysmenorrhoea"), ("Plan","Hysterectomy preferred")],
inv=[
("Transvaginal/Abdominal USG", "Fibroid mapping, number, size"),
("MRI pelvis", "Preoperative planning"),
("Pap smear", "Cervical cytology - standard of care"),
("CBC + iron studies", "Severity of anaemia"),
("HbA1c, blood group", "Preoperative workup"),
("Coagulation profile", "Rule out bleeding disorder"),
],
mgmt=[
"Definitive: Total abdominal hysterectomy (TAH) - family complete",
"Laparoscopic hysterectomy if surgical expertise available",
"GnRH agonist preop: shrink fibroid, correct anaemia",
"Iron + Folic acid supplementation preoperatively",
"Target Hb >10 g/dL before surgery",
"If fertility desired: Myomectomy (high recurrence risk at 16 wks size)",
],
),
# ── CASE 5 ──────────────────────────────────────────────────────────────
dict(
num=5, short="35F | DM Type 2 | P2 | Perineal itching | Curdy discharge | White flakes with oozing spots",
diagnosis="Vulvovaginal Candidiasis (Complicated - Diabetic Patient)",
facts=[("Age","35F"), ("Risk","DM Type 2 - poor glucose control"), ("D/C","Thick curdy/cottage-cheese"),
("Sign","White flakes; bleed on removal"), ("pH","<4.5 (normal)")],
inv=[
("Wet mount + KOH prep", "Budding yeast + pseudohyphae - POC diagnosis"),
("HVS culture + sensitivity", "Speciation (C. glabrata = azole-resistant)"),
("Vaginal pH", "<4.5 in VVC; >4.5 in BV/Trichomonas"),
("Fasting glucose + HbA1c", "Assess diabetic control"),
("Urine R/E", "Glucosuria"),
("HIV serology", "Immunocompromise if recurrent VVC"),
],
mgmt=[
"Local: Clotrimazole 500 mg pessary single dose OR 100 mg x7d",
"Miconazole cream for external vulval involvement",
"Systemic: Fluconazole 150 mg oral single dose (1st line)",
"Severe/complicated: Fluconazole 150 mg q72h x 3 doses",
"Recurrent VVC (≥4 episodes/yr): Fluconazole 150 mg weekly x 6 months",
"STRICT glycaemic control (HbA1c target <7%) - prevent recurrence",
"Avoid tight synthetic clothing, local irritants",
],
),
# ── CASE 6 ──────────────────────────────────────────────────────────────
dict(
num=6, short="58F | P7 | Low SES | Irregular PVB 1y | Foul discharge | Fungating Cx | Bilateral parametrium to lateral wall",
diagnosis="Carcinoma Cervix - Stage IIIB",
facts=[("Age","58F"), ("Parity","P7 - multipara"), ("Stage","IIIB - parametrium bilateral to lat wall"),
("Cx","Fungating, bleeds on touch"), ("Rectum","Free (not IVA)")],
inv=[
("Cervical punch biopsy", "Histopath confirmation (SCC 80%)"),
("MRI pelvis", "Parametrial invasion, nodal involvement"),
("CT chest/abdomen/pelvis", "Nodal mets, distant spread"),
("Cystoscopy", "Rule out bladder = Stage IVA"),
("Proctosigmoidoscopy", "Rule out rectal involvement"),
("IVU / CT urography", "Hydronephrosis = Stage IIIB criterion"),
("CBC, LFT, RFT", "Anaemia +++, organ function"),
("Blood group + cross-match", "May need transfusion preRT"),
],
mgmt=[
"Stage IIIB: NOT operable - Concurrent Chemoradiation (CCRT)",
"EBRT to pelvis: 45-50 Gy in 25 fractions over 5 weeks",
"Intracavitary brachytherapy (ICBT): central disease boost",
"Cisplatin 40 mg/m² IV weekly concurrent (radiosensitiser)",
"Correct anaemia to Hb >10 g/dL BEFORE radiotherapy",
"Ureteric obstruction: stenting or nephrostomy",
"Nutritional support + palliative care team involvement",
],
),
# ── CASE 7 ──────────────────────────────────────────────────────────────
dict(
num=7, short="50F | Menopausal 1y | P4 | Chronic cough | Cystocele + Rectocele | Cervix outside introitus | Decubitus ulcers",
diagnosis="3rd Degree Uterovaginal Prolapse + Cystocele + Rectocele",
facts=[("Age","50F"), ("Parity","P4 - rapid succession"), ("Grade","3rd degree / Complete prolapse"),
("Complication","Decubitus ulcers on Cx lips"), ("RF","Cough + Menopause + Prolonged labour")],
inv=[
("POP-Q assessment", "Objective staging of prolapse"),
("Biopsy of decubitus ulcer", "MUST rule out carcinomatous change"),
("Pap smear + colposcopy", "Cervical cytology"),
("Urodynamics", "If stress incontinence; pre-surgical planning"),
("Urine R/E + C&S", "Associated UTI"),
("USG pelvis", "Uterine/adnexal pathology"),
("CXR + spirometry", "Chronic cough - preop chest assessment"),
("CBC, blood sugar, ECG", "Preoperative assessment"),
],
mgmt=[
"PRE-OP: Local oestrogen + saline soaks + ring pessary x 4-6 wks to heal ulcers",
"DEFINITIVE SURGICAL: Vaginal hysterectomy (procedure of choice)",
"Anterior colporrhaphy: cystocele repair",
"Posterior colpoperineorrhaphy: rectocele repair",
"Pelvic floor repair at same sitting",
"CONSERVATIVE (unfit): Ring pessary + pelvic floor exercises",
"Treat chronic cough (bronchodilators) to prevent recurrence",
],
),
# ── CASE 8 ──────────────────────────────────────────────────────────────
dict(
num=8, short="28F | P2 | LCB 3y | Post-coital bleeding 2m | Fungating growth post lip Cx 3cm | Parametrium free",
diagnosis="Carcinoma Cervix - Stage IB1 (Lesion ≤4 cm, confined to cervix)",
facts=[("Age","28F"), ("Stage","IB1 - visible lesion ≤4 cm"), ("Growth","3 cm post lip"),
("Parametrium","Free"), ("Preference","Surgery - young patient, preserve ovaries")],
inv=[
("Cervical punch biopsy", "Histopath confirmation"),
("Colposcopy + directed biopsy", "Map lesion extent"),
("MRI pelvis", "Tumour size, stroma, nodes, parametrium"),
("PET-CT", "Nodal staging - surgical candidate"),
("Cystoscopy + Proctoscopy", "Rule out Stage IVA"),
("HPV DNA genotyping", "Documentation (HPV 16/18)"),
("CBC, LFT, RFT, CXR", "Preoperative workup"),
],
mgmt=[
"Stage IB1: SURGICAL preferred in young patient",
"Wertheim's Radical Hysterectomy: uterus + upper vaginal cuff + parametria + PLND",
"Advantage over CCRT: ovaries preserved, no RT vaginal stenosis",
"Adjuvant CCRT post-surgery if: +ve nodes, +ve margins, parametrial involvement",
"Alternative: CCRT (equivalent oncological outcome if surgery contraindicated)",
"Fertility-sparing: Radical trachelectomy if lesion ≤2 cm (this lesion 3 cm - MDT decision)",
],
),
# ── CASE 9 ──────────────────────────────────────────────────────────────
dict(
num=9, short="26F | Recently married | Burning micturition + Foul discharge 10d | Frothy greenish D/C | Punctate haemorrhagic spots vaginal walls",
diagnosis="Trichomoniasis (Trichomonas vaginalis)",
facts=[("Age","26F"), ("Discharge","Thin, frothy, greenish-yellow, foul"), ("Sign","Strawberry cervix/vagina"),
("pH","Elevated >4.5 (alkaline)"), ("STI","Screen for coinfections")],
inv=[
("Wet mount microscopy", "Motile pear-shaped flagellated trichomonads - POC"),
("Vaginal pH", ">4.5 alkaline in Trichomonas"),
("NAAT (gold standard)", "Highest sensitivity ~98%"),
("HVS culture (Diamond's medium)", "If wet mount negative"),
("Gram stain", "Rule out concurrent BV"),
("HIV, VDRL, Chlamydia, GC NAAT", "Full STI screen - coinfections common"),
("Partner testing", "Essential for eradication"),
],
mgmt=[
"Metronidazole 2 g orally, SINGLE DOSE (1st line)",
"Tinidazole 2 g single dose (alternative, better GI tolerance)",
"If failure: Metronidazole 400-500 mg BD x 7 days",
"TREAT SEXUAL PARTNER SIMULTANEOUSLY (prevent reinfection)",
"Avoid alcohol during + 48h after metronidazole (disulfiram reaction)",
"Retest at 3 months; safe sex counselling",
"Notify STI contacts",
],
),
# ── CASE 10 ─────────────────────────────────────────────────────────────
dict(
num=10, short="30F | Vaginal delivery 7m ago (home, forceps, 2-day labour, stillbirth 3.5kg) | Continuous dribbling urine 7m | Hole 0.5x1 cm vesico-cervical junction",
diagnosis="Vesico-Vaginal Fistula (VVF) - Obstetric / Iatrogenic",
facts=[("Age","30F"), ("Cause","Obstructed labour + forceps"), ("Fistula","0.5x1 cm vesico-cervical jn."),
("Symptom","Continuous urine dribbling"), ("Wait","Repair after 3 months")],
inv=[
("Dye test (methylene blue)", "200 mL in bladder → blue seen vaginally = VVF"),
("Three-swab test", "Differentiate VVF vs ureterovaginal fistula"),
("Cystoscopy + vaginoscopy", "Site, size, distance from ureteric orifices"),
("IVU / CT urography", "Rule out ureterovaginal fistula; upper tracts"),
("MRI pelvis", "Complex fistulas, tissue necrosis extent"),
("Urine R/E + C&S", "Treat UTI before surgery"),
("RFT + CBC", "Renal function + preoperative status"),
("Biopsy fistula edge", "Rule out malignancy (if radiation-related)"),
],
mgmt=[
"CONSERVATIVE (small fresh <72h or <3mm): Foley catheter x 4-6 wks",
"Antibiotics + anticholinergics during conservative management",
"SURGICAL: WAIT 3 MONTHS for oedema/inflammation to resolve",
"Transvaginal layered repair (1st line for obstetric VVF)",
"Latzko operation for post-hysterectomy VVF",
"Transabdominal (O'Connor) for complex/recurrent VVF",
"Principles: wide mobilisation, tension-free closure, layered repair",
"Post-repair: Foley catheter x 14-21 days + anticholinergics + pelvic rest",
],
),
]
# ── Summary table data ───────────────────────────────────────────────────────
SUMMARY_ROWS = [
["Case", "Diagnosis", "Key Investigation", "Management"],
["1", "Endometrial Carcinoma", "Endometrial Biopsy + MRI Pelvis", "TAH+BSO+LND ± RT/Chemo"],
["2", "Fibroid Uterus (Nullipara)", "TVUS + Hysteroscopy", "Myomectomy (fertility-preserving)"],
["3", "Carcinoma Ovary Stage IIIC", "CA-125 + CT Abdomen/Pelvis", "Debulking + Carbo/Taxol"],
["4", "Fibroid Uterus (P2, family complete)", "TVUS + MRI", "TAH (abdominal hysterectomy)"],
["5", "Vulvovaginal Candidiasis (DM)", "KOH Wet Mount + HVS Culture", "Fluconazole 150 mg + glycaemic control"],
["6", "Ca Cervix Stage IIIB", "Cervical Biopsy + MRI + Cystoscopy", "CCRT (Cisplatin + EBRT + ICBT)"],
["7", "3° Uterovaginal Prolapse", "POP-Q + Ulcer Biopsy + Urodynamics", "Vaginal Hyst. + Colporrhaphy"],
["8", "Ca Cervix Stage IB1", "Cervical Biopsy + MRI + PET-CT", "Wertheim's Radical Hysterectomy"],
["9", "Trichomoniasis", "Wet Mount + NAAT + STI Screen", "Metronidazole 2 g stat + treat partner"],
["10", "Vesico-Vaginal Fistula (Obstetric)", "Dye Test + Cystoscopy + IVU", "Transvaginal fistula repair (after 3 mths)"],
]
# ── Build PDF ────────────────────────────────────────────────────────────────
doc = SimpleDocTemplate(
OUTPUT, pagesize=A4,
leftMargin=1.3*cm, rightMargin=1.3*cm,
topMargin=1.8*cm, bottomMargin=1.8*cm,
)
story = []
# ── Cover band ───────────────────────────────────────────────────────────────
cover_data = [[
Paragraph("GYNAECOLOGY SHORT CASES", TITLE_STYLE),
]]
cover_sub = [[
Paragraph("Clinical Summary for Quick Reference During Rounds", SUBTITLE_STYLE),
]]
for d in [cover_data, cover_sub]:
t = Table(d, colWidths=[17.7*cm])
bg = NAVY if d is cover_data else TEAL
t.setStyle(TableStyle([
("BACKGROUND", (0,0),(-1,-1), bg),
("TOPPADDING", (0,0),(-1,-1), 10 if d is cover_data else 6),
("BOTTOMPADDING",(0,0),(-1,-1), 10 if d is cover_data else 6),
("LEFTPADDING", (0,0),(-1,-1), 10),
("RIGHTPADDING",(0,0),(-1,-1), 10),
]))
story.append(t)
story.append(Spacer(1, 0.25*cm))
# meta line
meta = Table([[
Paragraph("10 Clinical Cases | Diagnosis + Workup + Management", SMALL),
Paragraph(f"Date: {date.today().strftime('%d %B %Y')}", S("MR", fontSize=7.5,
textColor=colors.HexColor("#555555"), alignment=TA_LEFT, fontName="Helvetica")),
]], colWidths=[13*cm, 4.7*cm])
meta.setStyle(TableStyle([
("TOPPADDING",(0,0),(-1,-1),2), ("BOTTOMPADDING",(0,0),(-1,-1),2),
("LEFTPADDING",(0,0),(-1,-1),2), ("RIGHTPADDING",(0,0),(-1,-1),2),
]))
story.append(meta)
story.append(HRFlowable(width="100%", thickness=0.5, color=TEAL, spaceAfter=6))
# ── Individual cases ─────────────────────────────────────────────────────────
for c in CASES:
block = []
block.append(case_banner(c["num"], c["short"], c["diagnosis"]))
block.append(Spacer(1, 0.2*cm))
block.append(facts_ribbon(c["facts"]))
block.append(Spacer(1, 0.2*cm))
block.append(workup_mgmt_table(c["inv"], c["mgmt"]))
block.append(Spacer(1, 0.4*cm))
story.append(KeepTogether(block[:3])) # banner + facts together
story.extend(block[3:]) # table can flow
# ── Summary table ────────────────────────────────────────────────────────────
story.append(PageBreak())
story.append(Paragraph("QUICK REFERENCE SUMMARY TABLE", S("ST", fontSize=13,
textColor=NAVY, fontName="Helvetica-Bold", spaceAfter=8)))
story.append(HRFlowable(width="100%", thickness=1, color=TEAL, spaceAfter=8))
sm_data = []
for i, row in enumerate(SUMMARY_ROWS):
if i == 0:
sm_data.append([Paragraph(c, TABLE_HDR) for c in row])
else:
styled = [
Paragraph(row[0], S("SN", fontSize=8.5, fontName="Helvetica-Bold",
textColor=WHITE, alignment=TA_CENTER)),
Paragraph(row[1], S("SD", fontSize=8, fontName="Helvetica-Bold", textColor=NAVY)),
Paragraph(row[2], S("SI", fontSize=8, fontName="Helvetica", textColor=BLACK)),
Paragraph(row[3], S("SM", fontSize=8, fontName="Helvetica", textColor=BLACK)),
]
sm_data.append(styled)
sm_table = Table(sm_data, colWidths=[1.1*cm, 5.2*cm, 5.7*cm, 5.7*cm])
row_bgs = []
for i in range(1, len(SUMMARY_ROWS)):
bg = LIGHT_GRY if i % 2 == 0 else WHITE
row_bgs.append(("BACKGROUND", (0,i), (-1,i), bg))
if i % 2 == 1:
row_bgs.append(("BACKGROUND", (0,i), (0,i), NAVY)) # case number cell NAVY
else:
row_bgs.append(("BACKGROUND", (0,i), (0,i), TEAL)) # case number cell TEAL
sm_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), NAVY),
*row_bgs,
("GRID", (0,0), (-1,-1), 0.4, MID_GRY),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING",(0,0),(-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING",(0,0),(-1,-1), 5),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("ALIGN", (0,0), (0,-1), "CENTER"),
("FONTNAME", (0,1),(0,-1), "Helvetica-Bold"),
("TEXTCOLOR", (0,0),(-1,0), WHITE),
("TEXTCOLOR", (0,1),(0,-1), WHITE),
]))
story.append(sm_table)
story.append(Spacer(1, 0.5*cm))
story.append(HRFlowable(width="100%", thickness=0.3, color=MID_GRY, spaceAfter=4))
story.append(Paragraph(
"Reference: Berek & Novak's Gynecology | Robbins Pathology | Campbell-Walsh Urology | Harrison's Internal Medicine",
S("Ref", fontSize=7, textColor=colors.HexColor("#888888"), fontName="Helvetica-Oblique",
alignment=TA_CENTER)
))
# ── Build ─────────────────────────────────────────────────────────────────────
doc.build(story, onFirstPage=header_footer, onLaterPages=header_footer)
print(f"PDF created: {OUTPUT}")