Create a quick reference PDF on PCOS diagnostic workup including all hormone tests
ls /home/daytona/workspace/ 2>/dev/null && mkdir -p /home/daytona/workspace/pcos-pdf
~/pcos-pdf/build_pcos_pdf.py
#!/usr/bin/env python3
"""
PCOS Diagnostic Workup - Quick Reference PDF
Sources: Berek & Novak's Gynecology, Harrison's Principles of Internal Medicine 22E,
Tietz Textbook of Laboratory Medicine 7th Ed
"""
import urllib.request
import io
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.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, KeepTogether, Image
)
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
from reportlab.platypus.flowables import HRFlowable
# ─── Colors ────────────────────────────────────────────────────────────────────
TEAL = colors.HexColor('#006D77')
TEAL_LT = colors.HexColor('#83C5BE')
TEAL_BG = colors.HexColor('#EDF6F9')
PURPLE = colors.HexColor('#5C4B8A')
PURPLE_LT = colors.HexColor('#B5A9D4')
PURPLE_BG = colors.HexColor('#F3F0FA')
ORANGE = colors.HexColor('#E07000')
ORANGE_BG = colors.HexColor('#FFF3E0')
RED_BG = colors.HexColor('#FEE2E2')
RED_DK = colors.HexColor('#B91C1C')
GREEN_BG = colors.HexColor('#DCFCE7')
GREEN_DK = colors.HexColor('#166534')
GREY_DARK = colors.HexColor('#1E293B')
GREY_MED = colors.HexColor('#475569')
GREY_LT = colors.HexColor('#F1F5F9')
WHITE = colors.white
BLACK = colors.black
# ─── Styles ────────────────────────────────────────────────────────────────────
styles = getSampleStyleSheet()
def S(name, **kw):
base = styles['Normal'] if name not in styles else styles[name]
return ParagraphStyle(name + '_custom_' + str(id(kw)), parent=base, **kw)
title_style = S('title', fontSize=22, textColor=WHITE, leading=28, alignment=TA_CENTER, fontName='Helvetica-Bold')
subtitle_style = S('sub', fontSize=10, textColor=TEAL_LT, leading=14, alignment=TA_CENTER, fontName='Helvetica')
h1_style = S('h1', fontSize=12, textColor=WHITE, leading=16, fontName='Helvetica-Bold', spaceAfter=0)
h2_style = S('h2', fontSize=10, textColor=TEAL, leading=14, fontName='Helvetica-Bold', spaceBefore=6, spaceAfter=3)
h3_style = S('h3', fontSize=9, textColor=PURPLE, leading=13, fontName='Helvetica-Bold', spaceBefore=4, spaceAfter=2)
body_style = S('body', fontSize=8, textColor=GREY_DARK, leading=12, fontName='Helvetica')
body_sm_style = S('body_sm', fontSize=7.5,textColor=GREY_DARK, leading=11, fontName='Helvetica')
small_style = S('small', fontSize=7, textColor=GREY_MED, leading=10, fontName='Helvetica')
small_ital = S('si', fontSize=7, textColor=GREY_MED, leading=10, fontName='Helvetica-Oblique')
warn_style = S('warn', fontSize=7.5,textColor=RED_DK, leading=11, fontName='Helvetica-Bold')
note_style = S('note', fontSize=7.5,textColor=GREEN_DK, leading=11, fontName='Helvetica')
orange_style = S('orange', fontSize=8, textColor=ORANGE, leading=12, fontName='Helvetica-Bold')
cell_hdr_style = S('chdr', fontSize=8, textColor=WHITE, leading=11, fontName='Helvetica-Bold', alignment=TA_CENTER)
cell_body_style = S('cbody', fontSize=7.5,textColor=GREY_DARK, leading=11, fontName='Helvetica')
cell_ctr_style = S('cctr', fontSize=7.5,textColor=GREY_DARK, leading=11, fontName='Helvetica', alignment=TA_CENTER)
def P(text, style=body_style): return Paragraph(text, style)
def SP(h=4): return Spacer(1, h)
def HR(color=TEAL_LT, thickness=0.5, width='100%'): return HRFlowable(width=width, thickness=thickness, color=color, spaceAfter=4, spaceBefore=2)
# ─── Helper: section header bar ────────────────────────────────────────────────
def sec_header(text, bg=TEAL, text_col=WHITE, icon=''):
tbl = Table([[P(f'<b>{icon} {text}</b>', S('sh', fontSize=10, textColor=text_col, leading=14, fontName='Helvetica-Bold'))]], colWidths=['100%'])
tbl.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,-1), bg),
('LEFTPADDING', (0,0), (-1,-1), 8),
('RIGHTPADDING', (0,0), (-1,-1), 4),
('TOPPADDING', (0,0), (-1,-1), 5),
('BOTTOMPADDING', (0,0), (-1,-1), 5),
('ROUNDEDCORNERS', [4,4,4,4]),
]))
return tbl
def colored_box(content_flowables, bg=TEAL_BG, border=TEAL_LT):
"""Wraps flowables in a colored table cell."""
tbl = Table([[content_flowables if isinstance(content_flowables, list) else [content_flowables]]], colWidths=['100%'])
tbl.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,-1), bg),
('BOX', (0,0), (-1,-1), 0.8, border),
('LEFTPADDING', (0,0), (-1,-1), 8),
('RIGHTPADDING', (0,0), (-1,-1), 8),
('TOPPADDING', (0,0), (-1,-1), 6),
('BOTTOMPADDING', (0,0), (-1,-1), 6),
]))
return tbl
# ─── Document ──────────────────────────────────────────────────────────────────
OUTPUT = '/home/daytona/workspace/pcos-pdf/PCOS_Diagnostic_Workup.pdf'
doc = SimpleDocTemplate(
OUTPUT,
pagesize=A4,
leftMargin=1.5*cm, rightMargin=1.5*cm,
topMargin=1.5*cm, bottomMargin=2*cm,
title='PCOS Diagnostic Workup – Quick Reference',
author='Orris Medical Reference',
)
W = A4[0] - 3*cm # usable width
story = []
# ═══════════════════════════════════════════════════════════════════════════════
# TITLE BANNER
# ═══════════════════════════════════════════════════════════════════════════════
banner = Table([
[P('<b>PCOS DIAGNOSTIC WORKUP</b>', title_style)],
[P('Polycystic Ovary Syndrome · Hormone Tests & Clinical Evaluation · Quick Reference', subtitle_style)],
[P('Sources: Berek & Novak\'s Gynecology · Harrison\'s Principles of Internal Medicine 22E · Tietz Lab Medicine 7th Ed', small_ital)],
], colWidths=[W])
banner.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,-1), TEAL),
('LEFTPADDING', (0,0), (-1,-1), 12),
('RIGHTPADDING', (0,0), (-1,-1), 12),
('TOPPADDING', (0,0), (0,0), 12),
('BOTTOMPADDING', (0,-1), (0,-1), 8),
('TOPPADDING', (0,1), (0,-1), 3),
('BOTTOMPADDING', (0,0), (0,-2), 2),
]))
story.append(banner)
story.append(SP(10))
# ═══════════════════════════════════════════════════════════════════════════════
# ROW 1: DIAGNOSTIC CRITERIA | WHEN TO SUSPECT
# ═══════════════════════════════════════════════════════════════════════════════
story.append(sec_header('DIAGNOSTIC CRITERIA (Rotterdam 2003)', bg=TEAL, icon='▶'))
story.append(SP(4))
crit_data = [
[P('<b>Criterion</b>', cell_hdr_style), P('<b>Definition</b>', cell_hdr_style)],
[P('<b>1. Ovulatory dysfunction</b>', cell_body_style),
P('Oligomenorrhea (<9 cycles/yr) or amenorrhea; or anovulatory cycles with regular menses (rare)', cell_body_style)],
[P('<b>2. Hyperandrogenism (HA)</b>', cell_body_style),
P('<b>Clinical:</b> hirsutism, acne, androgenic alopecia<br/><b>Biochemical:</b> elevated testosterone (bioavailable T most sensitive)', cell_body_style)],
[P('<b>3. PCO morphology (PCOM)</b>', cell_body_style),
P('≥20 follicles per ovary (2–9 mm) on US <i>and/or</i> ovarian volume >10 mL (single ovary sufficient)', cell_body_style)],
[P('<b>Diagnosis requires</b>', S('req', fontSize=8, textColor=ORANGE, leading=12, fontName='Helvetica-Bold')),
P('<b>2 of 3 criteria</b>, after excluding other etiologies', S('req2', fontSize=8, textColor=ORANGE, leading=12, fontName='Helvetica-Bold'))],
]
crit_tbl = Table(crit_data, colWidths=[W*0.32, W*0.68])
crit_tbl.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), TEAL),
('BACKGROUND', (0,1), (-1,1), TEAL_BG),
('BACKGROUND', (0,2), (-1,2), WHITE),
('BACKGROUND', (0,3), (-1,3), TEAL_BG),
('BACKGROUND', (0,4), (-1,4), ORANGE_BG),
('GRID', (0,0), (-1,-1), 0.4, TEAL_LT),
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
('LEFTPADDING', (0,0), (-1,-1), 7),
('RIGHTPADDING', (0,0), (-1,-1), 7),
('TOPPADDING', (0,0), (-1,-1), 5),
('BOTTOMPADDING', (0,0), (-1,-1), 5),
('VALIGN', (0,0), (-1,-1), 'TOP'),
]))
story.append(crit_tbl)
story.append(SP(4))
# 4 phenotypes note
pheno_data = [
[P('<b>4 PCOS Phenotypes</b>', S('ph', fontSize=8, textColor=PURPLE, fontName='Helvetica-Bold'))],
[P('A: HA + Ovulatory dysfunction + PCOM | B: HA + Ovulatory dysfunction | C: HA + PCOM (ovulatory) | D: Ovulatory dysfunction + PCOM (no HA)', body_sm_style)],
]
pheno_tbl = Table(pheno_data, colWidths=[W])
pheno_tbl.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,-1), PURPLE_BG),
('BOX', (0,0), (-1,-1), 0.6, PURPLE_LT),
('LEFTPADDING', (0,0), (-1,-1), 8), ('RIGHTPADDING', (0,0), (-1,-1), 8),
('TOPPADDING', (0,0), (-1,-1), 4), ('BOTTOMPADDING', (0,0), (-1,-1), 4),
]))
story.append(pheno_tbl)
story.append(SP(8))
# ═══════════════════════════════════════════════════════════════════════════════
# ROW 2: HORMONE TESTS TABLE (full width)
# ═══════════════════════════════════════════════════════════════════════════════
story.append(sec_header('HORMONE & LABORATORY TESTS', bg=PURPLE, icon='⚗'))
story.append(SP(4))
hdr = [P(t, cell_hdr_style) for t in ['Test', 'Normal Range (female)', 'PCOS Relevance / Action']]
lab_rows = [
# Androgens
[P('ANDROGENS', S('grp', fontSize=7.5, textColor=WHITE, fontName='Helvetica-Bold', leading=11, alignment=TA_CENTER)),
P('', cell_body_style), P('', cell_body_style)],
[P('Total Testosterone', cell_body_style),
P('20–80 ng/dL', cell_ctr_style),
P('Elevated in ~80% PCOS; >200 ng/dL → work up for ovarian/adrenal tumor', cell_body_style)],
[P('Free Testosterone (calculated)', cell_body_style),
P('0.6–6.8 pg/mL', cell_ctr_style),
P('<b>Most sensitive index</b> of androgen excess. >6.85 pg/mL best predictor of androgen-secreting tumor (Sens 82%, Spec 97%)', cell_body_style)],
[P('Bioavailable Testosterone', cell_body_style),
P('1.6–19.1 ng/dL', cell_ctr_style),
P('Most accurate bioactive T assessment without equilibrium dialysis; includes free + weakly-bound T', cell_body_style)],
[P('SHBG', cell_body_style),
P('18–114 nmol/L', cell_ctr_style),
P('Low in PCOS/hyperinsulinemia → increases free T. Measure to calculate free T', cell_body_style)],
[P('Androstenedione', cell_body_style),
P('20–250 ng/dL', cell_ctr_style),
P('Most commonly elevated androgen in non-tumoral androgen excess (93%)', cell_body_style)],
[P('DHEAS', cell_body_style),
P('100–350 µg/dL (upper limit varies)', cell_ctr_style),
P('Adrenal marker; >7000 µg/L (≈700 µg/dL) suggests adrenal tumor. Moderate elevations common in PCOS/obesity — limited specificity', cell_body_style)],
# Gonadotropins
[P('GONADOTROPINS', S('grp2', fontSize=7.5, textColor=WHITE, fontName='Helvetica-Bold', leading=11, alignment=TA_CENTER)),
P('', cell_body_style), P('', cell_body_style)],
[P('LH', cell_body_style),
P('Follicular: 2–15 mIU/mL', cell_ctr_style),
P('Elevated in lean PCOS; pulsatile — single random value unreliable. LH:FSH ratio >3:1 classic but not diagnostic', cell_body_style)],
[P('FSH', cell_body_style),
P('Follicular: 3–10 mIU/mL', cell_ctr_style),
P('Low-normal in PCOS. Measure to exclude premature ovarian insufficiency (POI)', cell_body_style)],
# Exclusion tests
[P('EXCLUSION TESTS', S('grp3', fontSize=7.5, textColor=WHITE, fontName='Helvetica-Bold', leading=11, alignment=TA_CENTER)),
P('', cell_body_style), P('', cell_body_style)],
[P('17-OHP (follicular phase, AM)', cell_body_style),
P('30–200 ng/dL', cell_ctr_style),
P('Exclude non-classic CAH (21-OH deficiency):<br/><300 ng/dL → likely unaffected<br/>300–10,000 ng/dL → ACTH stim test<br/>>10,000 ng/dL → virtually diagnostic of CAH', cell_body_style)],
[P('Prolactin', cell_body_style),
P('<20–25 ng/mL', cell_ctr_style),
P('Exclude hyperprolactinemia (mimics anovulation/androgen excess)', cell_body_style)],
[P('TSH', cell_body_style),
P('0.4–4.0 mIU/L', cell_ctr_style),
P('Exclude hypothyroidism (causes menstrual irregularity and can mimic PCOS)', cell_body_style)],
[P('hCG (urine or serum)', cell_body_style),
P('Negative', cell_ctr_style),
P('Rule out pregnancy — always first step in evaluation of oligo/amenorrhea', cell_body_style)],
# AMH
[P('ADDITIONAL MARKERS', S('grp4', fontSize=7.5, textColor=WHITE, fontName='Helvetica-Bold', leading=11, alignment=TA_CENTER)),
P('', cell_body_style), P('', cell_body_style)],
[P('AMH (Anti-Müllerian Hormone)', cell_body_style),
P('Varies by age/lab', cell_ctr_style),
P('Elevated in PCOS; reflects high antral follicle count. Under evaluation as a PCOS diagnostic criterion (may eventually replace US)', cell_body_style)],
[P('Fasting insulin / Glucose:Insulin ratio', cell_body_style),
P('Fasting insulin <20–24 µIU/mL', cell_ctr_style),
P('Glucose:insulin <4.5 suggests IR. Fasting insulin >25 µIU/mL in HAIR-AN syndrome. Not routinely recommended alone', cell_body_style)],
]
col_w = [W*0.26, W*0.22, W*0.52]
lab_tbl = Table([hdr] + lab_rows, colWidths=col_w, repeatRows=1)
# Build style commands
style_cmds = [
('BACKGROUND', (0,0), (-1,0), PURPLE),
('GRID', (0,0), (-1,-1), 0.3, GREY_LT),
('LEFTPADDING', (0,0), (-1,-1), 6), ('RIGHTPADDING', (0,0), (-1,-1), 6),
('TOPPADDING', (0,0), (-1,-1), 4), ('BOTTOMPADDING', (0,0), (-1,-1), 4),
('VALIGN', (0,0), (-1,-1), 'TOP'),
]
# Group header rows: rows 1 (index), 8, 11, 15 (0-based counting from data rows)
# hdr = row 0, lab_rows starts at row 1
group_rows = [1, 8, 11, 15] # row indices in the Table (including header row at 0)
group_colors = [colors.HexColor('#B45309'), colors.HexColor('#065F46'), colors.HexColor('#1E40AF'), colors.HexColor('#4C1D95')]
for i, (row_idx, col) in enumerate(zip(group_rows, group_colors)):
style_cmds += [
('BACKGROUND', (0, row_idx), (-1, row_idx), col),
('SPAN', (0, row_idx), (-1, row_idx)),
]
# Alternate row shading for data rows
data_row_indices = [i for i in range(1, len(lab_rows)+1) if i not in group_rows]
for i, ri in enumerate(data_row_indices):
bg = TEAL_BG if i % 2 == 0 else WHITE
style_cmds.append(('BACKGROUND', (0, ri), (-1, ri), bg))
lab_tbl.setStyle(TableStyle(style_cmds))
story.append(lab_tbl)
story.append(SP(8))
# ═══════════════════════════════════════════════════════════════════════════════
# ROW 3: EXCLUSION WORKUP | TUMOR THRESHOLDS side by side
# ═══════════════════════════════════════════════════════════════════════════════
story.append(sec_header('DIFFERENTIAL DIAGNOSIS — DISORDERS TO EXCLUDE', bg=colors.HexColor('#1E40AF'), icon='✖'))
story.append(SP(4))
excl_rows = [
[P('<b>Disorder</b>', cell_hdr_style), P('<b>Key Test(s)</b>', cell_hdr_style), P('<b>Distinguishing Feature</b>', cell_hdr_style)],
[P('Non-classic CAH', cell_body_style), P('17-OHP (follicular AM) ± ACTH stim', cell_body_style),
P('17-OHP >300 ng/dL → ACTH test; mimics PCOS clinically', cell_body_style)],
[P('Hyperprolactinemia', cell_body_style), P('Prolactin', cell_body_style),
P('Galactorrhea, headache; suppress ovulation and cause androgen excess', cell_body_style)],
[P('Cushing\'s Syndrome', cell_body_style), P('1-mg overnight DST; 24h urinary cortisol', cell_body_style),
P('Central obesity, striae, moon facies, HTN; cortisol not suppressed <1.8 µg/dL', cell_body_style)],
[P('Androgen-secreting tumor', cell_body_style), P('Free T >6.85 pg/mL; total T >200 ng/dL; 11-desoxycortisol >7 ng/mL', cell_body_style),
P('Rapid virilization; all three androgens (T, A, DHEAS) elevated in 56% of adrenal tumors', cell_body_style)],
[P('Thyroid disease', cell_body_style), P('TSH', cell_body_style),
P('Hypothyroidism → anovulation, weight gain, androgen-like picture', cell_body_style)],
[P('Premature Ovarian Insufficiency', cell_body_style), P('FSH, estradiol', cell_body_style),
P('FSH >25 mIU/mL; low estradiol; hot flashes; before age 40', cell_body_style)],
[P('Hypo/Hypergonadotropic amenorrhea', cell_body_style), P('LH, FSH, estradiol', cell_body_style),
P('Hypothalamic: low LH/FSH. Assess for eating disorder, exercise, stress', cell_body_style)],
]
excl_tbl = Table(excl_rows, colWidths=[W*0.22, W*0.33, W*0.45], repeatRows=1)
excl_tbl.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), colors.HexColor('#1E40AF')),
('BACKGROUND', (0,1), (-1,1), colors.HexColor('#EFF6FF')),
('BACKGROUND', (0,2), (-1,2), WHITE),
('BACKGROUND', (0,3), (-1,3), colors.HexColor('#EFF6FF')),
('BACKGROUND', (0,4), (-1,4), RED_BG),
('BACKGROUND', (0,5), (-1,5), colors.HexColor('#EFF6FF')),
('BACKGROUND', (0,6), (-1,6), WHITE),
('BACKGROUND', (0,7), (-1,7), colors.HexColor('#EFF6FF')),
('GRID', (0,0), (-1,-1), 0.3, colors.HexColor('#BFDBFE')),
('LEFTPADDING', (0,0), (-1,-1), 6), ('RIGHTPADDING', (0,0), (-1,-1), 6),
('TOPPADDING', (0,0), (-1,-1), 4), ('BOTTOMPADDING', (0,0), (-1,-1), 4),
('VALIGN', (0,0), (-1,-1), 'TOP'),
]))
story.append(excl_tbl)
story.append(SP(8))
# ═══════════════════════════════════════════════════════════════════════════════
# ROW 4: METABOLIC SCREENING | RED FLAGS
# ═══════════════════════════════════════════════════════════════════════════════
# Two columns
left_content = [
sec_header('METABOLIC SCREENING', bg=colors.HexColor('#0F766E'), icon='♥'),
SP(4),
P('<b>Glucose/Diabetes (all PCOS patients with risk factors)</b>', h3_style),
P('• <b>Oral glucose tolerance test (OGTT) 75g:</b> preferred — detects IGT and T2DM; also yields insulin levels', body_sm_style),
P('• <b>HbA1c:</b> use for confirmed chronic hyperglycemia; may miss early IGT', body_sm_style),
P('• IGT: 2-hr glucose 140–199 mg/dL; T2DM: ≥200 mg/dL', body_sm_style),
P('• 30–45% of obese PCOS have IGT or T2DM; 10%/1.5% even in non-obese', body_sm_style),
SP(4),
P('<b>Lipid Panel (fasting)</b>', h3_style),
P('Total cholesterol · HDL · LDL · Triglycerides — dyslipidemia common (elevated TC, TG, LDL; low HDL)', body_sm_style),
SP(4),
P('<b>Anthropometrics</b>', h3_style),
P('BMI + waist circumference (android obesity → ↑ IR risk)', body_sm_style),
P('Blood pressure measurement', body_sm_style),
SP(4),
P('<b>Cardiovascular Risk Screening</b>', h3_style),
P('Assess: family Hx early CVD, smoking, IGT/T2DM, HTN, dyslipidemia, obstructive sleep apnea, obesity', body_sm_style),
]
right_content = [
sec_header('ALARM / RED FLAGS', bg=RED_DK, icon='⚠'),
SP(4),
colored_box([
P('<b>Rapid virilization</b> → androgen-secreting tumor', warn_style),
SP(2),
P('Total T >200 ng/dL → ovarian or adrenal tumor work-up (imaging)', body_sm_style),
P('Free T >6.85 pg/mL → Sens 82%, Spec 97% for tumor', body_sm_style),
P('DHEAS >700 µg/dL (>7000 µg/L) → adrenal tumor', body_sm_style),
SP(3),
P('<b>Cortisol excess features</b> → suspect Cushing\'s', warn_style),
P('Striae, central obesity, HTN, moon facies', body_sm_style),
P('→ 1-mg overnight DST or 24-hr urine cortisol', body_sm_style),
SP(3),
P('<b>17-OHP >10,000 ng/dL</b>', warn_style),
P('→ virtually diagnostic of classic CAH', body_sm_style),
SP(3),
P('<b>FSH >25 mIU/mL + low estradiol (age <40)</b>', warn_style),
P('→ Premature Ovarian Insufficiency — not PCOS', body_sm_style),
SP(3),
P('<b>HAIR-AN Syndrome</b>', warn_style),
P('HA + Insulin Resistance + Acanthosis Nigricans', body_sm_style),
P('Testosterone >150 ng/dL; fasting insulin >25 µIU/mL', body_sm_style),
], bg=RED_BG, border=RED_DK),
]
two_col = Table([[left_content, right_content]], colWidths=[W*0.52, W*0.46])
two_col.setStyle(TableStyle([
('VALIGN', (0,0), (-1,-1), 'TOP'),
('LEFTPADDING', (0,0), (-1,-1), 0),
('RIGHTPADDING', (0,0), (0,0), 6),
('RIGHTPADDING', (1,0), (1,0), 0),
('TOPPADDING', (0,0), (-1,-1), 0),
('BOTTOMPADDING', (0,0), (-1,-1), 0),
]))
story.append(two_col)
story.append(SP(8))
# ═══════════════════════════════════════════════════════════════════════════════
# ROW 5: DEXAMETHASONE SUPPRESSION + ADOLESCENT NOTE
# ═══════════════════════════════════════════════════════════════════════════════
misc_left = [
sec_header('DEXAMETHASONE ANDROGEN SUPPRESSION TEST', bg=ORANGE, icon='⊕'),
SP(4),
P('<b>Protocol:</b> Dexamethasone 0.5 mg PO q6h × 4 days; measure unbound T before and after', body_sm_style),
P('<b>Adrenal source:</b> free T suppresses to normal range', body_sm_style),
P('<b>Ovarian source:</b> incomplete suppression', body_sm_style),
P('<b>1-mg overnight DST</b> (measure 8AM cortisol) if Cushing\'s suspected', body_sm_style),
SP(4),
P('<b>Ultrasound</b>', h3_style),
P('Transvaginal US: PCOM = ≥20 follicles (2–9 mm) per ovary or volume >10 mL. Note: PCO appearance in ~23% of normal women; US alone has limited specificity — not always required for diagnosis', body_sm_style),
]
misc_right = [
sec_header('ADOLESCENT DIAGNOSIS NOTE', bg=colors.HexColor('#7C3AED'), icon='✱'),
SP(4),
colored_box([
P('Rotterdam criteria NOT directly applicable in adolescents within 2–3 years of menarche (irregular cycles are normal)', body_sm_style),
SP(2),
P('<b>Adolescent PCOS requires:</b><br/>Persistent anovulation (≥2 yrs post-menarche) AND clinical or biochemical HA', body_sm_style),
SP(2),
P('PCO morphology on US alone is insufficient in adolescents', body_sm_style),
], bg=colors.HexColor('#F5F3FF'), border=colors.HexColor('#7C3AED')),
]
misc_tbl = Table([[misc_left, misc_right]], colWidths=[W*0.53, W*0.47])
misc_tbl.setStyle(TableStyle([
('VALIGN', (0,0), (-1,-1), 'TOP'),
('LEFTPADDING', (0,0), (-1,-1), 0),
('RIGHTPADDING', (0,0), (0,0), 6),
('RIGHTPADDING', (1,0), (1,0), 0),
('TOPPADDING', (0,0), (-1,-1), 0),
('BOTTOMPADDING', (0,0), (-1,-1), 0),
]))
story.append(misc_tbl)
story.append(SP(8))
# ═══════════════════════════════════════════════════════════════════════════════
# DIAGNOSTIC ALGORITHM IMAGE
# ═══════════════════════════════════════════════════════════════════════════════
story.append(sec_header('DIAGNOSTIC ALGORITHM (Berek & Novak\'s Gynecology, Fig. 35-4)', bg=TEAL, icon='▶'))
story.append(SP(4))
img_url = 'https://cdn.orris.care/cdss_images/a8e477659d36847c186f0522e0162a767cd30f6b9c7ad88c27e675678b05a2ac.png'
try:
with urllib.request.urlopen(img_url, timeout=15) as resp:
img_data = resp.read()
img_buf = io.BytesIO(img_data)
img = Image(img_buf, width=W, height=W*0.5)
story.append(img)
story.append(P('<i>Figure: PCOS diagnostic algorithm showing diagnostic criteria, phenotypes, differential diagnoses to exclude, and ancillary studies. (Modified from Berek & Novak\'s Gynecology)</i>', small_ital))
except Exception as e:
story.append(P(f'[Diagnostic algorithm image unavailable: {e}]', small_ital))
story.append(SP(8))
# ═══════════════════════════════════════════════════════════════════════════════
# REFERENCE RANGES SUMMARY BOX
# ═══════════════════════════════════════════════════════════════════════════════
story.append(sec_header('ANDROGEN REFERENCE RANGES SUMMARY (Follicular Phase)', bg=PURPLE, icon='⚗'))
story.append(SP(4))
ref_rows = [
[P('Hormone', cell_hdr_style), P('Normal Range', cell_hdr_style), P('Unit', cell_hdr_style),
P('Hormone', cell_hdr_style), P('Normal Range', cell_hdr_style), P('Unit', cell_hdr_style)],
[P('Total Testosterone', cell_body_style), P('20–80', cell_ctr_style), P('ng/dL', cell_ctr_style),
P('17-Hydroxyprogesterone', cell_body_style), P('30–200', cell_ctr_style), P('ng/dL', cell_ctr_style)],
[P('Free Testosterone (calculated)', cell_body_style), P('0.6–6.8', cell_ctr_style), P('pg/mL', cell_ctr_style),
P('DHEAS', cell_body_style), P('100–350', cell_ctr_style), P('µg/dL', cell_ctr_style)],
[P('Bioavailable Testosterone', cell_body_style), P('1.6–19.1', cell_ctr_style), P('ng/dL', cell_ctr_style),
P('Androstenedione', cell_body_style), P('20–250', cell_ctr_style), P('ng/dL', cell_ctr_style)],
[P('SHBG', cell_body_style), P('18–114', cell_ctr_style), P('nmol/L', cell_ctr_style),
P('Albumin', cell_body_style), P('3300–4800', cell_ctr_style), P('mg/dL', cell_ctr_style)],
]
col_w6 = [W*0.22, W*0.1, W*0.07, W*0.22, W*0.1, W*0.07] # won't add to 1.0 exactly
# normalize
total = sum(col_w6)
col_w6 = [v/total*W for v in col_w6]
ref_tbl = Table(ref_rows, colWidths=col_w6, repeatRows=1)
ref_tbl.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), PURPLE),
('BACKGROUND', (0,1), (-1,1), PURPLE_BG),
('BACKGROUND', (0,2), (-1,2), WHITE),
('BACKGROUND', (0,3), (-1,3), PURPLE_BG),
('BACKGROUND', (0,4), (-1,4), WHITE),
('GRID', (0,0), (-1,-1), 0.3, PURPLE_LT),
('LINEAFTER', (2,0), (2,-1), 1.0, PURPLE),
('LEFTPADDING', (0,0), (-1,-1), 5), ('RIGHTPADDING', (0,0), (-1,-1), 5),
('TOPPADDING', (0,0), (-1,-1), 4), ('BOTTOMPADDING', (0,0), (-1,-1), 4),
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
]))
story.append(ref_tbl)
story.append(SP(4))
story.append(P('* Normal values may vary between laboratories. Free testosterone calculated from total T + SHBG; bioavailable T includes free + albumin-bound.', small_ital))
story.append(SP(8))
# ═══════════════════════════════════════════════════════════════════════════════
# FOOTER
# ═══════════════════════════════════════════════════════════════════════════════
HR(color=TEAL_LT, thickness=1.0)
story.append(HR(color=TEAL_LT, thickness=1.0))
story.append(SP(3))
footer_tbl = Table([[
P('<b>PCOS Diagnostic Workup – Quick Reference</b>', S('fl', fontSize=7, textColor=GREY_MED, fontName='Helvetica-Bold')),
P('Berek & Novak\'s Gynecology · Harrison\'s IM 22E · Tietz Lab Medicine 7th Ed', S('fc', fontSize=7, textColor=GREY_MED, fontName='Helvetica', alignment=TA_CENTER)),
P('For educational use only. Always apply clinical judgment.', S('fr', fontSize=7, textColor=GREY_MED, fontName='Helvetica', alignment=TA_RIGHT)),
]], colWidths=[W*0.3, W*0.4, W*0.3])
footer_tbl.setStyle(TableStyle([
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
('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(footer_tbl)
# ─── Build ────────────────────────────────────────────────────────────────────
doc.build(story)
print(f"PDF created: {OUTPUT}")
Failed to create session: unauthorized: authentication failed: Bearer token is invalid
We encountered an error while processing your request. Please try again or reach out to us for help.
Again
mkdir -p /home/daytona/workspace/pcos-pdf && echo "dir ready"
~/pcos-pdf/build_pcos_pdf.py
#!/usr/bin/env python3
"""PCOS Diagnostic Workup - Quick Reference PDF
Sources: Berek & Novak's Gynecology; Harrison's Principles of Internal Medicine 22E;
Tietz Textbook of Laboratory Medicine 7th Ed
"""
import urllib.request, io
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import cm
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, Image, KeepTogether
)
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
# ── Colour Palette ────────────────────────────────────────────────────────────
TEAL = colors.HexColor('#005F73')
TEAL_LT = colors.HexColor('#94D2BD')
TEAL_BG = colors.HexColor('#EAF4F4')
PURPLE = colors.HexColor('#4A2C6D')
PURPLE_LT = colors.HexColor('#B39DDB')
PURPLE_BG = colors.HexColor('#F3EEF9')
INDIGO = colors.HexColor('#1D4ED8')
INDIGO_BG = colors.HexColor('#EFF6FF')
ORANGE = colors.HexColor('#C2601A')
ORANGE_BG = colors.HexColor('#FFF3E0')
RED_DK = colors.HexColor('#991B1B')
RED_BG = colors.HexColor('#FEF2F2')
RED_LT = colors.HexColor('#FECACA')
GREEN_DK = colors.HexColor('#14532D')
GREEN_BG = colors.HexColor('#F0FDF4')
AMBER = colors.HexColor('#92400E')
AMBER_BG = colors.HexColor('#FFFBEB')
GREY_DK = colors.HexColor('#1E293B')
GREY_MD = colors.HexColor('#475569')
GREY_LT = colors.HexColor('#F1F5F9')
GREY_BD = colors.HexColor('#CBD5E1')
WHITE = colors.white
BLACK = colors.black
# ── Style factory ─────────────────────────────────────────────────────────────
_styles = getSampleStyleSheet()
def mk(name, **kw):
base = _styles.get('Normal', _styles['Normal'])
return ParagraphStyle(f'_{name}_{abs(hash(str(kw)))}', parent=base, **kw)
TS = mk('ts', fontSize=20, textColor=WHITE, leading=26, alignment=TA_CENTER, fontName='Helvetica-Bold')
SS = mk('ss', fontSize=9, textColor=TEAL_LT, leading=13, alignment=TA_CENTER, fontName='Helvetica')
SRC = mk('src', fontSize=7, textColor=colors.HexColor('#AEE9D4'), leading=10, alignment=TA_CENTER, fontName='Helvetica-Oblique')
H2 = mk('h2', fontSize=10, textColor=WHITE, leading=14, fontName='Helvetica-Bold')
H3 = mk('h3', fontSize=9, textColor=TEAL, leading=13, fontName='Helvetica-Bold', spaceBefore=4, spaceAfter=2)
H3P = mk('h3p', fontSize=9, textColor=PURPLE, leading=13, fontName='Helvetica-Bold', spaceBefore=4, spaceAfter=2)
H3R = mk('h3r', fontSize=9, textColor=RED_DK, leading=13, fontName='Helvetica-Bold', spaceBefore=4, spaceAfter=2)
BD = mk('bd', fontSize=8, textColor=GREY_DK, leading=12, fontName='Helvetica')
BDS = mk('bds', fontSize=7.5,textColor=GREY_DK, leading=11, fontName='Helvetica')
SM = mk('sm', fontSize=7, textColor=GREY_MD, leading=10, fontName='Helvetica')
SMI = mk('smi', fontSize=7, textColor=GREY_MD, leading=10, fontName='Helvetica-Oblique')
CHR = mk('chr', fontSize=8, textColor=WHITE, leading=11, fontName='Helvetica-Bold', alignment=TA_CENTER)
CB = mk('cb', fontSize=7.5,textColor=GREY_DK, leading=11, fontName='Helvetica')
CC = mk('cc', fontSize=7.5,textColor=GREY_DK, leading=11, fontName='Helvetica', alignment=TA_CENTER)
GHR = mk('ghr', fontSize=7.5,textColor=WHITE, leading=11, fontName='Helvetica-Bold', alignment=TA_CENTER)
WARN = mk('wrn', fontSize=7.5,textColor=RED_DK, leading=11, fontName='Helvetica-Bold')
NOTE = mk('nt', fontSize=7.5,textColor=GREEN_DK, leading=11, fontName='Helvetica')
ORN = mk('orn', fontSize=8, textColor=ORANGE, leading=12, fontName='Helvetica-Bold')
def P(t, s=BD): return Paragraph(t, s)
def SP(h=4): return Spacer(1, h)
def HR(**kw): return HRFlowable(width='100%', thickness=0.5, color=TEAL_LT, spaceAfter=3, spaceBefore=2, **kw)
def sec_hdr(text, bg=TEAL, icon=''):
lbl = f'{icon} {text}' if icon else text
t = Table([[P(f'<b>{lbl}</b>', H2)]], colWidths=['100%'])
t.setStyle(TableStyle([
('BACKGROUND', (0,0),(-1,-1), bg),
('LEFTPADDING',(0,0),(-1,-1), 8),
('RIGHTPADDING',(0,0),(-1,-1),6),
('TOPPADDING',(0,0),(-1,-1), 5),
('BOTTOMPADDING',(0,0),(-1,-1),5),
]))
return t
def box(items, bg=TEAL_BG, border=TEAL_LT, lp=8, rp=8, tp=6, bp=6):
if not isinstance(items, list): items = [items]
t = Table([[ items ]], colWidths=['100%'])
t.setStyle(TableStyle([
('BACKGROUND',(0,0),(-1,-1),bg),
('BOX',(0,0),(-1,-1),0.7,border),
('LEFTPADDING',(0,0),(-1,-1),lp),
('RIGHTPADDING',(0,0),(-1,-1),rp),
('TOPPADDING',(0,0),(-1,-1),tp),
('BOTTOMPADDING',(0,0),(-1,-1),bp),
]))
return t
# ── Document setup ────────────────────────────────────────────────────────────
OUT = '/home/daytona/workspace/pcos-pdf/PCOS_Diagnostic_Workup.pdf'
doc = SimpleDocTemplate(
OUT, pagesize=A4,
leftMargin=1.5*cm, rightMargin=1.5*cm,
topMargin=1.4*cm, bottomMargin=1.8*cm,
title='PCOS Diagnostic Workup – Quick Reference',
author='Orris Medical Reference'
)
W = A4[0] - 3*cm
story = []
# ══════════════════════════════════════════════════════════════════════════════
# TITLE BANNER
# ══════════════════════════════════════════════════════════════════════════════
banner = Table([
[P('<b>PCOS DIAGNOSTIC WORKUP</b>', TS)],
[P('Polycystic Ovary Syndrome · Hormone Tests & Clinical Evaluation · Quick Reference', SS)],
[P('Berek & Novak\'s Gynecology · Harrison\'s Principles of Internal Medicine 22E · Tietz Textbook of Laboratory Medicine 7th Ed', SRC)],
], colWidths=[W])
banner.setStyle(TableStyle([
('BACKGROUND',(0,0),(-1,-1),TEAL),
('LEFTPADDING',(0,0),(-1,-1),10), ('RIGHTPADDING',(0,0),(-1,-1),10),
('TOPPADDING',(0,0),(0,0),10), ('BOTTOMPADDING',(0,-1),(0,-1),8),
('TOPPADDING',(0,1),(0,-1),3), ('BOTTOMPADDING',(0,0),(0,-2),2),
]))
story.append(banner)
story.append(SP(8))
# ══════════════════════════════════════════════════════════════════════════════
# SECTION 1 – DIAGNOSTIC CRITERIA
# ══════════════════════════════════════════════════════════════════════════════
story.append(sec_hdr('DIAGNOSTIC CRITERIA (Rotterdam 2003 / Updated 2023 International Guidelines)', TEAL, '▶'))
story.append(SP(4))
crit = Table([
[P('<b>Criterion</b>',CHR), P('<b>Details</b>',CHR), P('<b>Clinical Notes</b>',CHR)],
[P('<b>1. Ovulatory / Menstrual Dysfunction</b>',CB),
P('Oligomenorrhea (<9 cycles/yr) or amenorrhea; cycle length >35 days',CB),
P('Regular cycles with anovulation present in ~21% of HA women — not excludes PCOS',CB)],
[P('<b>2. Hyperandrogenism (HA)</b>',CB),
P('<b>Clinical:</b> hirsutism (mFG score ≥4–6 by ethnicity), acne, androgenic alopecia<br/><b>Biochemical:</b> elevated total or free testosterone',CB),
P('Biochemical HA: bioavailable T most sensitive. Occurs in ~80% of PCOS',CB)],
[P('<b>3. Polycystic Ovarian Morphology (PCOM)</b>',CB),
P('≥20 antral follicles (2–9 mm) per ovary <i>or</i> ovarian volume ≥10 mL (single ovary sufficient)',CB),
P('<b>2023 update:</b> elevated AMH may substitute for ultrasound PCOM criterion',CB)],
[P('<b>Diagnosis = 2 of 3 criteria</b> (after exclusion of other causes)',
mk('req',fontSize=8,textColor=ORANGE,fontName='Helvetica-Bold',leading=12)),
P('Resulting in 4 phenotypes (A–D, see below)',
mk('req2',fontSize=8,textColor=ORANGE,fontName='Helvetica-Bold',leading=12)),
P('<b>PCOS is a diagnosis of exclusion</b>',
mk('req3',fontSize=8,textColor=ORANGE,fontName='Helvetica-Bold',leading=12))],
], colWidths=[W*0.27, W*0.40, W*0.33])
crit.setStyle(TableStyle([
('BACKGROUND',(0,0),(-1,0),TEAL),
('BACKGROUND',(0,1),(-1,1),TEAL_BG),
('BACKGROUND',(0,2),(-1,2),WHITE),
('BACKGROUND',(0,3),(-1,3),TEAL_BG),
('BACKGROUND',(0,4),(-1,4),ORANGE_BG),
('GRID',(0,0),(-1,-1),0.3,TEAL_LT),
('LEFTPADDING',(0,0),(-1,-1),6),('RIGHTPADDING',(0,0),(-1,-1),6),
('TOPPADDING',(0,0),(-1,-1),4),('BOTTOMPADDING',(0,0),(-1,-1),4),
('VALIGN',(0,0),(-1,-1),'TOP'),
]))
story.append(crit)
story.append(SP(4))
pheno = Table([[
P('<b>Phenotype A:</b> HA + Ovulatory dysfunction + PCOM | '
'<b>Phenotype B:</b> HA + Ovulatory dysfunction | '
'<b>Phenotype C:</b> HA + PCOM (ovulatory) | '
'<b>Phenotype D:</b> Ovulatory dysfunction + PCOM (non-hyperandrogenic)',
mk('ph',fontSize=7.5,textColor=PURPLE,fontName='Helvetica',leading=11))
]], colWidths=[W])
pheno.setStyle(TableStyle([
('BACKGROUND',(0,0),(-1,-1),PURPLE_BG),
('BOX',(0,0),(-1,-1),0.6,PURPLE_LT),
('LEFTPADDING',(0,0),(-1,-1),8),('RIGHTPADDING',(0,0),(-1,-1),8),
('TOPPADDING',(0,0),(-1,-1),4),('BOTTOMPADDING',(0,0),(-1,-1),4),
]))
story.append(pheno)
story.append(SP(8))
# ══════════════════════════════════════════════════════════════════════════════
# SECTION 2 – HORMONE & LAB TESTS
# ══════════════════════════════════════════════════════════════════════════════
story.append(sec_hdr('HORMONE & LABORATORY TESTS', PURPLE, '⚗'))
story.append(SP(4))
HDR_ROW = [P(t,CHR) for t in ['Test', 'Normal Range (adult female)', 'PCOS Significance / Action Threshold']]
AMBER_GRP = colors.HexColor('#92400E')
GREEN_GRP = colors.HexColor('#065F46')
BLUE_GRP = colors.HexColor('#1E3A8A')
VIOL_GRP = colors.HexColor('#4C1D95')
lab_data = [
HDR_ROW,
# ---- Group: Androgens
[P('ANDROGENS',GHR), P('',CB), P('',CB)],
[P('Total Testosterone',CB), P('20–80 ng/dL', CC),
P('Elevated in most PCOS; >200 ng/dL → work up for ovarian/adrenal androgen-secreting tumor', CB)],
[P('Free Testosterone (calculated)',CB), P('0.6–6.8 pg/mL', CC),
P('<b>Most sensitive index</b> of androgen excess. >6.85 pg/mL: Sens 82%, Spec 97% for androgen-secreting tumor. Calculated from total T + SHBG.', CB)],
[P('Bioavailable Testosterone',CB), P('1.6–19.1 ng/dL', CC),
P('Most accurate bioactive T without equilibrium dialysis (free + albumin-bound T). Calculate from total T, SHBG, albumin.', CB)],
[P('% Free Testosterone',CB), P('0.4–2.4%', CC),
P('Screening marker; elevated when SHBG low (hyperinsulinemia reduces SHBG)', CB)],
[P('SHBG',CB), P('18–114 nmol/L', CC),
P('Low in insulin resistance and obesity → increases free T fraction. Required for free T calculation.', CB)],
[P('Androstenedione',CB), P('20–250 ng/dL', CC),
P('Most commonly elevated androgen in non-tumoral androgen excess (93%); mixed ovarian/adrenal source', CB)],
[P('DHEAS',CB), P('100–350 µg/dL', CC),
P('Adrenal marker; >700 µg/dL (>7000 µg/L) → adrenal tumor work-up. Moderate elevations common in PCOS/obesity — limited specificity alone', CB)],
# ---- Group: Gonadotropins
[P('GONADOTROPINS',GHR), P('',CB), P('',CB)],
[P('LH',CB), P('Follicular phase: 2–15 mIU/mL', CC),
P('Elevated in lean PCOS (high due to ↑ GnRH pulsatility). LH:FSH ratio >3:1 classic but not diagnostic (pulsatile secretion makes single value unreliable)', CB)],
[P('FSH',CB), P('Follicular phase: 3–10 mIU/mL', CC),
P('Normal to low-normal in PCOS. Measure to exclude premature ovarian insufficiency (POI: FSH >25 mIU/mL)', CB)],
# ---- Group: Exclusion tests
[P('EXCLUSION / DIFFERENTIAL TESTS',GHR), P('',CB), P('',CB)],
[P('hCG (serum or urine)',CB), P('Negative', CC),
P('<b>Always first.</b> Rule out pregnancy before any further evaluation of oligo/amenorrhea', CB)],
[P('TSH',CB), P('0.4–4.0 mIU/L', CC),
P('Exclude hypothyroidism — causes anovulation, weight gain, ↑ androgen picture mimicking PCOS', CB)],
[P('Prolactin',CB), P('<20–25 ng/mL', CC),
P('Exclude hyperprolactinemia — causes anovulation + androgen excess; check for galactorrhea, headache, visual fields', CB)],
[P('17-Hydroxyprogesterone (17-OHP)<br/><i>Follicular phase, AM sample</i>',CB), P('30–200 ng/dL', CC),
P('<b>Exclude non-classic CAH (21-OH deficiency):</b><br/><300 ng/dL → likely unaffected<br/>300–10,000 ng/dL → ACTH stimulation test<br/>>10,000 ng/dL → virtually diagnostic of classic CAH', CB)],
[P('1-mg Overnight Dexamethasone Suppression Test',CB), P('8AM cortisol <1.8 µg/dL', CC),
P('Exclude Cushing\'s syndrome when clinically suspected (central obesity, striae, HTN, moon facies, easy bruising)', CB)],
# ---- Group: Additional markers
[P('ADDITIONAL MARKERS',GHR), P('',CB), P('',CB)],
[P('AMH (Anti-Müllerian Hormone)',CB), P('Age-dependent (varies by lab)', CC),
P('Elevated in PCOS (reflects high antral follicle count). 2023 guidelines: elevated AMH may substitute for PCOM on US as diagnostic criterion', CB)],
[P('Estradiol',CB), P('Follicular: 20–150 pg/mL', CC),
P('Low + high FSH → POI. Low in hypothalamic amenorrhea. Normal/high normal in PCOS', CB)],
[P('Fasting glucose + 2-hr OGTT (75g)',CB), P('Fasting <100 mg/dL; 2-hr <140 mg/dL', CC),
P('Preferred for metabolic screening: detects IGT (140–199) and T2DM (≥200 mg/dL). 30–45% of obese PCOS have IGT/T2DM; 10%/1.5% even in non-obese', CB)],
[P('HbA1c',CB), P('<5.7%', CC),
P('Supplementary to OGTT; use when chronic hyperglycemia present. May miss early IGT', CB)],
[P('Fasting lipid panel',CB), P('TC <200, LDL <100, HDL >50, TG <150 mg/dL', CC),
P('Dyslipidemia common in PCOS: ↑TC, ↑TG, ↑LDL, ↓HDL. Most characteristic: ↓HDL2α', CB)],
[P('Fasting insulin',CB), P('<20–24 µIU/mL', CC),
P('Not recommended as routine screening alone. Glucose:insulin ratio <4.5 suggests IR; HAIR-AN: fasting insulin >25 µIU/mL', CB)],
]
col_w = [W*0.26, W*0.20, W*0.54]
lab_tbl = Table(lab_data, colWidths=col_w, repeatRows=1)
grp_rows = [1, 9, 12, 18]
grp_colors= [AMBER_GRP, GREEN_GRP, BLUE_GRP, VIOL_GRP]
style_cmd = [
('BACKGROUND',(0,0),(-1,0),PURPLE),
('GRID',(0,0),(-1,-1),0.25,GREY_BD),
('LEFTPADDING',(0,0),(-1,-1),5),('RIGHTPADDING',(0,0),(-1,-1),5),
('TOPPADDING',(0,0),(-1,-1),4),('BOTTOMPADDING',(0,0),(-1,-1),4),
('VALIGN',(0,0),(-1,-1),'TOP'),
]
for ri, gc in zip(grp_rows, grp_colors):
style_cmd += [
('BACKGROUND',(0,ri),(-1,ri), gc),
('SPAN',(0,ri),(-1,ri)),
]
data_rows = [i for i in range(1,len(lab_data)) if i not in grp_rows]
for idx, ri in enumerate(data_rows):
style_cmd.append(('BACKGROUND',(0,ri),(-1,ri), TEAL_BG if idx%2==0 else WHITE))
lab_tbl.setStyle(TableStyle(style_cmd))
story.append(lab_tbl)
story.append(SP(4))
story.append(P('* Normal ranges vary between laboratories. Free testosterone calculated from total T + SHBG; bioavailable T = free + albumin-bound. Assess in early morning, follicular phase (days 4–10) where possible.', SMI))
story.append(SP(8))
# ══════════════════════════════════════════════════════════════════════════════
# SECTION 3 – DIFFERENTIAL DIAGNOSIS
# ══════════════════════════════════════════════════════════════════════════════
story.append(sec_hdr('DIFFERENTIAL DIAGNOSIS — CONDITIONS TO EXCLUDE', INDIGO, '✖'))
story.append(SP(4))
diff_hdr = [P(t,CHR) for t in ['Condition', 'Key Test(s)', 'Distinguishing Features']]
diff_data = [
diff_hdr,
[P('Non-classic CAH<br/>(21-OH deficiency)',CB),
P('17-OHP (follicular AM) ± ACTH stimulation test',CB),
P('17-OHP >300 ng/dL → ACTH stim; clinically identical to PCOS. Must exclude in ALL hirsute women regardless of PCO morphology', CB)],
[P('Hyperprolactinemia',CB),
P('Prolactin',CB),
P('Galactorrhea, visual field defects, headache; prolactin suppresses GnRH → anovulation + androgen excess', CB)],
[P('Cushing\'s Syndrome',CB),
P('1-mg overnight DST (8AM cortisol); 24-h urinary free cortisol',CB),
P('Central obesity, violaceous striae, easy bruising, moon facies, HTN, proximal myopathy; cortisol not suppressed <1.8 µg/dL on DST', CB)],
[P('<b>Androgen-secreting tumor</b><br/>(ovarian or adrenal)',
mk('rdb',fontSize=7.5,textColor=RED_DK,fontName='Helvetica-Bold',leading=11)),
P('Free T >6.85 pg/mL; total T >200 ng/dL; DHEAS >700 µg/dL; 11-desoxycortisol >7 ng/mL; CT/MRI adrenals; TV-US ovaries',CB),
P('<b>Rapid virilization.</b> All 3 androgens elevated in 56% of adrenal tumors. 11-desoxycortisol: Sens 89%, Spec 100% for adrenocortical tumor',
mk('rbds',fontSize=7.5,textColor=RED_DK,fontName='Helvetica',leading=11))],
[P('Thyroid Disease',CB),
P('TSH',CB),
P('Hypothyroidism → anovulation, weight gain, hair loss, fatigue — closely mimics PCOS', CB)],
[P('Premature Ovarian Insufficiency',CB),
P('FSH, estradiol',CB),
P('FSH >25 mIU/mL + low estradiol in women <40; hot flashes, night sweats', CB)],
[P('Hypothalamic Amenorrhea',CB),
P('LH, FSH, estradiol; clinical history',CB),
P('Low LH/FSH; history of excessive exercise, weight loss, stress, eating disorder; low BMI', CB)],
[P('Hyperthecosis',CB),
P('Testosterone, LH, FSH',CB),
P('Higher T than typical PCOS; both LH and FSH low or low-normal; more severe androgenization; responds to GnRH agonist suppression', CB)],
]
col_diff = [W*0.20, W*0.29, W*0.51]
diff_tbl = Table(diff_data, colWidths=col_diff, repeatRows=1)
diff_tbl.setStyle(TableStyle([
('BACKGROUND',(0,0),(-1,0),INDIGO),
('BACKGROUND',(0,1),(-1,1),INDIGO_BG),
('BACKGROUND',(0,2),(-1,2),WHITE),
('BACKGROUND',(0,3),(-1,3),INDIGO_BG),
('BACKGROUND',(0,4),(-1,4),RED_BG),
('BACKGROUND',(0,5),(-1,5),INDIGO_BG),
('BACKGROUND',(0,6),(-1,6),WHITE),
('BACKGROUND',(0,7),(-1,7),INDIGO_BG),
('BACKGROUND',(0,8),(-1,8),WHITE),
('GRID',(0,0),(-1,-1),0.25,colors.HexColor('#BFDBFE')),
('LEFTPADDING',(0,0),(-1,-1),6),('RIGHTPADDING',(0,0),(-1,-1),6),
('TOPPADDING',(0,0),(-1,-1),4),('BOTTOMPADDING',(0,0),(-1,-1),4),
('VALIGN',(0,0),(-1,-1),'TOP'),
]))
story.append(diff_tbl)
story.append(SP(8))
# ══════════════════════════════════════════════════════════════════════════════
# SECTION 4 – TWO COLUMNS: METABOLIC SCREENING | RED FLAGS
# ══════════════════════════════════════════════════════════════════════════════
met_left = [
sec_hdr('METABOLIC SCREENING', colors.HexColor('#065F46'), '♥'),
SP(4),
P('<b>Glucose / Diabetes (high-risk patients)</b>', H3),
P('• <b>OGTT 75g (2-hr):</b> preferred — detects IGT & T2DM; provides insulin levels', BDS),
P('• <b>HbA1c:</b> supplementary; may miss early IGT', BDS),
P('• IGT: 2-hr glucose 140–199 mg/dL', BDS),
P('• T2DM: 2-hr glucose ≥200 mg/dL', BDS),
P('• Obese PCOS: 30–45% have IGT/T2DM', BDS),
P('• Non-obese PCOS: 10% IGT, 1.5% T2DM', BDS),
SP(4),
P('<b>Screen if:</b> obese PCOS, <i>or</i> non-obese PCOS with family Hx diabetes, HTN, acanthosis nigricans, or IGT risk factors',
mk('sc',fontSize=7.5,textColor=AMBER,fontName='Helvetica',leading=11)),
SP(4),
P('<b>Fasting Lipid Panel</b>', H3),
P('TC, LDL, HDL, TG — dyslipidemia common (↑TC, ↑TG, ↑LDL, ↓HDL). Most characteristic: ↓HDL2α', BDS),
SP(4),
P('<b>Anthropometrics</b>', H3),
P('• BMI calculation', BDS),
P('• Waist circumference (android obesity → ↑IR risk, CVD risk)', BDS),
P('• Blood pressure measurement', BDS),
SP(4),
P('<b>Cardiovascular Risk Screening</b>', H3),
P('Family Hx early CVD · smoking · IGT/T2DM · HTN · dyslipidemia · obstructive sleep apnea · obesity · depression (PHQ-2)', BDS),
SP(4),
P('<b>Endometrial Risk</b>', H3),
P('Chronic anovulation → endometrial hyperplasia/cancer (2–6x ↑ risk). Endometrial biopsy if risk factors present.', BDS),
]
met_right = [
sec_hdr('RED FLAGS & ALARM THRESHOLDS', RED_DK, '⚠'),
SP(4),
box([
P('<b>Rapid virilization → Androgen-secreting tumor</b>', WARN),
SP(2),
P('• Total T >200 ng/dL → ovarian/adrenal tumor (CT/MRI + TV-US)', BDS),
P('• Free T >6.85 pg/mL → Sens 82%, Spec 97% for tumor', BDS),
P('• DHEAS >700 µg/dL → adrenal tumor work-up', BDS),
P('• 11-desoxycortisol >7 ng/mL → adrenal tumor (Sens 89%, Spec 100%)', BDS),
SP(3),
P('<b>Features of Cushing\'s → DST</b>', WARN),
P('Central obesity, striae, bruising, HTN, moon facies', BDS),
SP(3),
P('<b>17-OHP >10,000 ng/dL → Classic CAH</b>', WARN),
SP(3),
P('<b>FSH >25 mIU/mL + low E2 (age <40) → POI, not PCOS</b>', WARN),
SP(3),
P('<b>HAIR-AN Syndrome</b>', WARN),
P('HA + Insulin Resistance + Acanthosis Nigricans', BDS),
P('Testosterone >150 ng/dL; fasting insulin >25 µIU/mL', BDS),
P('2-hr insulin response >300 µIU/mL on OGTT', BDS),
], bg=RED_BG, border=RED_LT),
]
two_col = Table([[met_left, SP(1), met_right]], colWidths=[W*0.505, W*0.01, W*0.485])
two_col.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(two_col)
story.append(SP(8))
# ══════════════════════════════════════════════════════════════════════════════
# SECTION 5 – ANDROGEN REFERENCE RANGES + ADOLESCENT NOTE
# ══════════════════════════════════════════════════════════════════════════════
ref_left = [
sec_hdr('ANDROGEN REFERENCE RANGES (Berek & Novak, Table 35-1)', PURPLE, '⚗'),
SP(4),
]
ref_rows = [
[P('<b>Hormone</b>',CHR), P('<b>Normal Range</b>',CHR), P('<b>Unit</b>',CHR)],
[P('Total Testosterone',CB), P('20–80',CC), P('ng/dL',CC)],
[P('Free Testosterone (calculated)',CB), P('0.6–6.8',CC), P('pg/mL',CC)],
[P('% Free Testosterone',CB), P('0.4–2.4',CC), P('%',CC)],
[P('Bioavailable Testosterone',CB), P('1.6–19.1',CC), P('ng/dL',CC)],
[P('SHBG',CB), P('18–114',CC), P('nmol/L',CC)],
[P('Androstenedione',CB), P('20–250',CC), P('ng/dL',CC)],
[P('DHEAS',CB), P('100–350',CC), P('µg/dL',CC)],
[P('17-Hydroxyprogesterone (follicular)',CB), P('30–200',CC), P('ng/dL',CC)],
[P('Albumin (for free T calc)',CB), P('3300–4800',CC), P('mg/dL',CC)],
]
rw = [W*0.48*0.56, W*0.48*0.27, W*0.48*0.17]
ref_tbl = Table(ref_rows, colWidths=rw, repeatRows=1)
ref_tbl.setStyle(TableStyle([
('BACKGROUND',(0,0),(-1,0),PURPLE),
*[('BACKGROUND',(0,i),(-1,i), PURPLE_BG if i%2==1 else WHITE) for i in range(1,len(ref_rows))],
('GRID',(0,0),(-1,-1),0.25,PURPLE_LT),
('LEFTPADDING',(0,0),(-1,-1),5),('RIGHTPADDING',(0,0),(-1,-1),5),
('TOPPADDING',(0,0),(-1,-1),3),('BOTTOMPADDING',(0,0),(-1,-1),3),
('VALIGN',(0,0),(-1,-1),'MIDDLE'),
]))
ref_left.append(ref_tbl)
ref_left.append(SP(3))
ref_left.append(P('Values vary between labs. Albumin assumed normal for calculated free T.', SMI))
adol_right = [
sec_hdr('ADOLESCENT DIAGNOSIS', colors.HexColor('#7C3AED'), '✱'),
SP(4),
box([
P('<b>Rotterdam criteria NOT directly applied within 2–3 yrs of menarche</b> — irregular cycles are physiologically normal.', BDS),
SP(3),
P('<b>Adolescent PCOS requires ALL of:</b>', mk('ab',fontSize=8,textColor=PURPLE,fontName='Helvetica-Bold',leading=12)),
P('• Persistent anovulation ≥2–3 yrs post-menarche', BDS),
P('• Clinical <i>or</i> biochemical hyperandrogenism', BDS),
SP(3),
P('<b>PCO morphology on US alone</b> is insufficient in adolescents.', BDS),
P('<b>AMH criteria</b> are not yet established for adolescents (2023 guideline).', BDS),
SP(3),
P('<b>Dex Androgen Suppression Test</b>', mk('dst',fontSize=8,textColor=PURPLE,fontName='Helvetica-Bold',leading=12)),
P('Dexamethasone 0.5 mg PO q6h × 4 days:', BDS),
P('• Adrenal source → free T suppresses to normal', BDS),
P('• Ovarian source → incomplete suppression', BDS),
], bg=PURPLE_BG, border=PURPLE_LT),
]
bot_tbl = Table([[ref_left, SP(1), adol_right]], colWidths=[W*0.50, W*0.01, W*0.49])
bot_tbl.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(bot_tbl)
story.append(SP(8))
# ══════════════════════════════════════════════════════════════════════════════
# DIAGNOSTIC ALGORITHM IMAGE
# ══════════════════════════════════════════════════════════════════════════════
IMG_URL = 'https://cdn.orris.care/cdss_images/a8e477659d36847c186f0522e0162a767cd30f6b9c7ad88c27e675678b05a2ac.png'
story.append(sec_hdr("DIAGNOSTIC ALGORITHM (Berek & Novak's Gynecology, Fig. 35-4)", TEAL, '▶'))
story.append(SP(4))
try:
with urllib.request.urlopen(IMG_URL, timeout=15) as r:
img_bytes = r.read()
img = Image(io.BytesIO(img_bytes), width=W, height=W*0.49)
story.append(img)
story.append(SP(2))
story.append(P('<i>PCOS diagnostic algorithm: criteria required (left), conditions to exclude (right), phenotypes (centre), and ancillary screening studies (bottom). '
'Modified from Berek & Novak\'s Gynecology.</i>', SMI))
except Exception as e:
story.append(P(f'[Diagnostic algorithm image could not be loaded: {e}]', SMI))
story.append(SP(8))
# ══════════════════════════════════════════════════════════════════════════════
# FOOTER
# ══════════════════════════════════════════════════════════════════════════════
story.append(HRFlowable(width='100%', thickness=1.0, color=TEAL_LT, spaceAfter=3, spaceBefore=2))
ft = Table([[
P('<b>PCOS Diagnostic Workup – Quick Reference</b>',
mk('fl',fontSize=7,textColor=GREY_MD,fontName='Helvetica-Bold')),
P('Sources: Berek & Novak\'s Gynecology · Harrison\'s IM 22E · Tietz Lab Medicine 7th Ed',
mk('fc',fontSize=7,textColor=GREY_MD,fontName='Helvetica',alignment=TA_CENTER)),
P('For educational use only. Apply clinical judgment. Ranges vary by laboratory.',
mk('fr',fontSize=7,textColor=GREY_MD,fontName='Helvetica',alignment=TA_RIGHT)),
]], colWidths=[W*0.30, W*0.42, W*0.28])
ft.setStyle(TableStyle([
('VALIGN',(0,0),(-1,-1),'MIDDLE'),
('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(ft)
# ── Build ─────────────────────────────────────────────────────────────────────
doc.build(story)
print(f'PDF created: {OUT}')
cd /home/daytona/workspace/pcos-pdf && python build_pcos_pdf.py 2>&1
ls -lh /home/daytona/workspace/pcos-pdf/PCOS_Diagnostic_Workup.pdf
PCOS Diagnostic Workup
PDF Document · PDF