~/cholecystectomy-guide/build_pdf.py
#!/usr/bin/env python3
"""
Cholecystectomy Surgical Procedure Guide - Intraoperative Reference PDF
Generated with ReportLab
"""
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import mm, cm
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_JUSTIFY
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, KeepTogether, PageBreak
)
from reportlab.platypus.flowables import BalancedColumns
from reportlab.lib.colors import HexColor
import os
OUTPUT_PATH = "/home/daytona/workspace/cholecystectomy-guide/Cholecystectomy_Surgical_Guide.pdf"
# ── Color palette ──────────────────────────────────────────────────────────
C_NAVY = HexColor("#1a2e4a")
C_BLUE = HexColor("#1e5fa8")
C_LIGHT_BLUE= HexColor("#dbeafe")
C_TEAL = HexColor("#0f766e")
C_TEAL_LT = HexColor("#ccfbf1")
C_RED = HexColor("#b91c1c")
C_RED_LT = HexColor("#fee2e2")
C_AMBER = HexColor("#d97706")
C_AMBER_LT = HexColor("#fef3c7")
C_GREEN = HexColor("#15803d")
C_GREEN_LT = HexColor("#dcfce7")
C_GRAY_DARK = HexColor("#374151")
C_GRAY = HexColor("#6b7280")
C_GRAY_LT = HexColor("#f3f4f6")
C_WHITE = colors.white
C_BLACK = colors.black
C_HEADER_BG = HexColor("#1a2e4a")
C_DIVIDER = HexColor("#e5e7eb")
W, H = A4 # 210 x 297 mm
# ── Document setup ─────────────────────────────────────────────────────────
doc = SimpleDocTemplate(
OUTPUT_PATH,
pagesize=A4,
topMargin=18*mm, bottomMargin=18*mm,
leftMargin=16*mm, rightMargin=16*mm,
title="Cholecystectomy Surgical Procedure Guide",
author="Orris Medical AI",
subject="Intraoperative Reference"
)
# ── Styles ─────────────────────────────────────────────────────────────────
base = getSampleStyleSheet()
def S(name, **kw):
return ParagraphStyle(name, **kw)
sTitle = S("sTitle",
fontName="Helvetica-Bold", fontSize=22,
textColor=C_WHITE, alignment=TA_CENTER,
spaceAfter=2*mm, leading=26)
sSubtitle = S("sSubtitle",
fontName="Helvetica", fontSize=11,
textColor=HexColor("#cbd5e1"), alignment=TA_CENTER,
spaceAfter=2*mm)
sDisclaimer = S("sDisclaimer",
fontName="Helvetica-Oblique", fontSize=7.5,
textColor=HexColor("#94a3b8"), alignment=TA_CENTER)
sSectionHeader = S("sSectionHeader",
fontName="Helvetica-Bold", fontSize=11,
textColor=C_WHITE, leading=15, spaceAfter=0)
sSubSection = S("sSubSection",
fontName="Helvetica-Bold", fontSize=9.5,
textColor=C_NAVY, leading=13, spaceBefore=4*mm, spaceAfter=1.5*mm)
sBody = S("sBody",
fontName="Helvetica", fontSize=8.5,
textColor=C_GRAY_DARK, leading=12.5,
spaceAfter=1.5*mm)
sBold = S("sBold",
fontName="Helvetica-Bold", fontSize=8.5,
textColor=C_NAVY, leading=12.5, spaceAfter=1*mm)
sBullet = S("sBullet",
fontName="Helvetica", fontSize=8.5,
textColor=C_GRAY_DARK, leading=12, leftIndent=8*mm,
bulletIndent=2*mm, spaceAfter=1*mm)
sWarning = S("sWarning",
fontName="Helvetica-Bold", fontSize=8.5,
textColor=C_RED, leading=12, spaceAfter=1*mm)
sStep = S("sStep",
fontName="Helvetica-Bold", fontSize=8.5,
textColor=C_TEAL, leading=12.5)
sStepBody = S("sStepBody",
fontName="Helvetica", fontSize=8.2,
textColor=C_GRAY_DARK, leading=12, leftIndent=5*mm, spaceAfter=1.5*mm)
sTableHeader = S("sTableHeader",
fontName="Helvetica-Bold", fontSize=8,
textColor=C_WHITE, alignment=TA_CENTER, leading=10)
sTableCell = S("sTableCell",
fontName="Helvetica", fontSize=7.8,
textColor=C_GRAY_DARK, leading=10.5)
sTableCellB = S("sTableCellB",
fontName="Helvetica-Bold", fontSize=7.8,
textColor=C_NAVY, leading=10.5)
sSmall = S("sSmall",
fontName="Helvetica", fontSize=7.5,
textColor=C_GRAY, leading=10)
sAlert = S("sAlert",
fontName="Helvetica-Bold", fontSize=8.5,
textColor=C_RED, leading=12)
sGreen = S("sGreen",
fontName="Helvetica-Bold", fontSize=8.5,
textColor=C_GREEN, leading=12)
# ── Helpers ────────────────────────────────────────────────────────────────
def section_header(title, color=C_HEADER_BG):
tbl = Table([[Paragraph(title.upper(), sSectionHeader)]], colWidths=[W - 32*mm])
tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), color),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING", (0,0), (-1,-1), 6),
("ROUNDEDCORNERS", [4,4,4,4]),
]))
return KeepTogether([Spacer(1, 4*mm), tbl, Spacer(1, 2*mm)])
def hr(color=C_DIVIDER):
return HRFlowable(width="100%", thickness=0.5, color=color, spaceAfter=2*mm, spaceBefore=1*mm)
def two_col_table(rows, col1_w=55*mm, col2_w=None, header_color=C_NAVY):
"""Generic 2-column table with styled header row."""
col2_w = col2_w or (W - 32*mm - col1_w)
header_row = rows[0]
data = [
[Paragraph(str(header_row[0]), sTableHeader), Paragraph(str(header_row[1]), sTableHeader)]
] + [
[Paragraph(str(r[0]), sTableCellB), Paragraph(str(r[1]), sTableCell)]
for r in rows[1:]
]
tbl = Table(data, colWidths=[col1_w, col2_w])
style = TableStyle([
("BACKGROUND", (0,0), (-1,0), header_color),
("BACKGROUND", (0,1), (-1,-1), C_WHITE),
("ROWBACKGROUNDS",(0,1), (-1,-1), [C_WHITE, C_GRAY_LT]),
("GRID", (0,0), (-1,-1), 0.4, C_DIVIDER),
("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), "TOP"),
])
tbl.setStyle(style)
return tbl
def three_col_table(rows, widths, header_color=C_NAVY):
data = [
[Paragraph(str(r), sTableHeader) for r in rows[0]]
] + [
[Paragraph(str(r[0]), sTableCellB), Paragraph(str(r[1]), sTableCell), Paragraph(str(r[2]), sTableCell)]
for r in rows[1:]
]
tbl = Table(data, colWidths=widths)
tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), header_color),
("ROWBACKGROUNDS",(0,1), (-1,-1), [C_WHITE, C_GRAY_LT]),
("GRID", (0,0), (-1,-1), 0.4, C_DIVIDER),
("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), "TOP"),
]))
return tbl
def alert_box(text, bg=C_RED_LT, border=C_RED, text_style=None):
ts = text_style or sAlert
tbl = Table([[Paragraph(text, ts)]], colWidths=[W - 32*mm])
tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), bg),
("LINEABOVE", (0,0), (-1,-1), 2, border),
("LINELEFT", (0,0), (0,-1), 3, border),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING",(0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING", (0,0), (-1,-1), 6),
]))
return KeepTogether([tbl, Spacer(1, 2*mm)])
def step_box(num, title, body_lines):
"""Numbered step box."""
num_cell = Paragraph(f"<b>{num}</b>", S("n",
fontName="Helvetica-Bold", fontSize=13,
textColor=C_WHITE, alignment=TA_CENTER, leading=16))
title_p = Paragraph(title, S("st",
fontName="Helvetica-Bold", fontSize=9,
textColor=C_NAVY, leading=13))
body_content = [title_p] + [Paragraph(f"• {l}", sStepBody) for l in body_lines]
tbl = Table([[num_cell, body_content]], colWidths=[10*mm, W - 32*mm - 12*mm])
tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (0,-1), C_TEAL),
("BACKGROUND", (1,0), (1,-1), C_WHITE),
("LINEBELOW", (0,0), (-1,-1), 0.5, C_DIVIDER),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING",(0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (0,-1), 2),
("RIGHTPADDING", (0,0), (0,-1), 2),
("LEFTPADDING", (1,0), (1,-1), 6),
("VALIGN", (0,0), (-1,-1), "TOP"),
("ROUNDEDCORNERS", [3,3,3,3]),
]))
return KeepTogether([tbl, Spacer(1, 1.5*mm)])
# ══════════════════════════════════════════════════════════════════════════
# BUILD CONTENT
# ══════════════════════════════════════════════════════════════════════════
story = []
# ── COVER HEADER ──────────────────────────────────────────────────────────
cover = Table(
[[
Paragraph("CHOLECYSTECTOMY", sTitle),
Paragraph("Surgical Procedure Guide", sSubtitle),
Paragraph("Open & Laparoscopic | Intraoperative Reference", sSubtitle),
Spacer(1, 3*mm),
Paragraph("For use by qualified surgical teams in an operative setting.", sDisclaimer),
Paragraph("Schwartz's 11e · Bailey & Love 28e · Maingot's · Fischer's Mastery 8e", sDisclaimer),
]],
colWidths=[W - 32*mm]
)
cover.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), C_NAVY),
("TOPPADDING", (0,0), (-1,-1), 12),
("BOTTOMPADDING",(0,0), (-1,-1), 10),
("LEFTPADDING", (0,0), (-1,-1), 14),
("RIGHTPADDING", (0,0), (-1,-1), 14),
("ROUNDEDCORNERS", [6,6,6,6]),
]))
story.append(cover)
story.append(Spacer(1, 4*mm))
# ── 1. INDICATIONS ────────────────────────────────────────────────────────
story.append(section_header("1. Indications for Cholecystectomy", C_BLUE))
ind_rows = [
["Indication", "Notes"],
["Biliary colic / Symptomatic cholelithiasis", "Most common. >80% recurrence/complication risk once symptomatic."],
["Acute cholecystitis", "Early (same-admission) laparoscopic preferred."],
["Chronic cholecystitis", "Recurrent symptomatic attacks."],
["Choledocholithiasis", "With or without cholangitis."],
["Gallstone pancreatitis", "Same admission or within 2 weeks."],
["Cholangitis / Obstructive jaundice", "After biliary decompression (ERCP/PTC)."],
["Acalculous cholecystitis / Biliary dyskinesia", "Typical biliary symptoms + HIDA EF <35%."],
["Mirizzi syndrome / Cholecystoenteric fistula", "Complex – higher conversion risk."],
]
story.append(two_col_table(ind_rows, col1_w=65*mm))
story.append(Spacer(1, 2*mm))
story.append(Paragraph("Prophylactic Indications (Asymptomatic Gallstones)", sSubSection))
story.append(Paragraph(
"Asymptomatic gallstones carry <20% lifetime risk of symptoms. "
"Prophylactic cholecystectomy is justified in:", sBody))
bullets_prop = [
"Sickle cell disease (vaso-occlusive crisis mimics acute cholecystitis)",
"Gallbladder polyp ≥10 mm (malignant potential)",
"Open bariatric surgery (concurrent; laparoscopic bariatric – avoid due to added risk)",
"Porcelain gallbladder / Gallstones >2.5 cm in high-risk populations (Native Americans)",
"Long-term total parenteral nutrition",
"Chronic immunosuppression post solid-organ transplant",
"Congenital hemolytic anemias (hereditary spherocytosis, thalassaemia)",
"No access to healthcare (military, missionaries)",
]
for b in bullets_prop:
story.append(Paragraph(f"• {b}", sBullet))
story.append(Spacer(1, 2*mm))
# ── 2. CONTRAINDICATIONS / CONVERSION ────────────────────────────────────
story.append(section_header("2. Contraindications & Conversion Criteria", C_RED))
story.append(Paragraph("Absolute Contraindications to Laparoscopic Approach", sSubSection))
abs_ci = [
["Condition", "Rationale"],
["Hemodynamic instability", "Cannot tolerate pneumoperitoneum; open preferred"],
["Uncontrolled coagulopathy", "Bleeding risk precludes laparoscopy"],
["Frank peritonitis", "Requires open exploration"],
["Severe COPD / CHF (EF <20%)", "Cannot tolerate CO₂ pneumoperitoneum"],
]
story.append(two_col_table(abs_ci, col1_w=65*mm, header_color=C_RED))
story.append(Spacer(1, 2*mm))
story.append(Paragraph("Indications to Convert to Open (Intraoperatively)", sSubSection))
conv = [
["Trigger", "Action"],
["Unable to tolerate pneumoperitoneum", "Convert immediately"],
["Cannot achieve Critical View of Safety (CVS)", "Bailout strategy → convert if needed"],
["Intraoperative complication not manageable laparoscopically", "Convert + repair"],
["No progress after a set time period", "Convert; do not persist"],
["Suspected or confirmed bile duct injury", "Convert for open repair at HPB center"],
]
story.append(two_col_table(conv, col1_w=70*mm, header_color=C_AMBER))
story.append(Spacer(1, 1*mm))
story.append(Paragraph(
"Conversion rate: ~5% elective | 10–30% emergency/complicated cases. "
"Conversion is NOT a failure; discuss preoperatively with patient.", sSmall))
story.append(Spacer(1, 2*mm))
# ── 3. PREOPERATIVE PREPARATION ───────────────────────────────────────────
story.append(section_header("3. Preoperative Checklist", C_TEAL))
pre_data = [
["Checklist Item", "Detail"],
["Bloods", "FBC, U&E, LFTs, coagulation (PT/INR)"],
["Imaging", "RUQ ultrasound (confirm stones, CBD diameter, anatomy)"],
["ECG / CXR", "If medically indicated (age >40, cardiac/respiratory history)"],
["Antibiotic prophylaxis", "2nd-generation cephalosporin (e.g., cefuroxime 1.5g IV) at induction"],
["DVT prophylaxis", "LMWH (e.g., enoxaparin 40mg SC) + TED stockings"],
["Bladder", "Patient to void before OR; avoid urinary catheterization"],
["Orogastric tube", "Insert if stomach distended; remove at end of procedure"],
["Consent", "Procedure, alternatives, BDI risk (~0.3–0.6%), conversion to open"],
["Team briefing / WHO checklist", "Confirm site marking, allergies, antibiotic given"],
]
story.append(two_col_table(pre_data, col1_w=52*mm))
story.append(Spacer(1, 2*mm))
story.append(Paragraph("Predictors of Difficult Cholecystectomy", sSubSection))
diff_data = [
["Domain", "Risk Factors"],
["History", "Male sex, age >65 yrs, onset >72–96 hrs (acute cholecystitis), prior upper abdominal surgery, prior cholecystostomy"],
["Examination", "Morbid obesity, high ASA score, palpable gallbladder mass"],
["Laboratory", "Elevated WCC, abnormal LFTs, elevated bilirubin"],
["Imaging (USS/CT/MRCP)", "Wall thickness >4–5 mm, pericholecystic fluid, impacted stone in neck, contracted gallbladder, Mirizzi syndrome, suspected fistula"],
]
story.append(two_col_table(diff_data, col1_w=42*mm, header_color=C_AMBER))
story.append(Spacer(1, 2*mm))
# ── 4. LAPAROSCOPIC CHOLECYSTECTOMY ───────────────────────────────────────
story.append(PageBreak())
story.append(section_header("4. Laparoscopic Cholecystectomy", C_NAVY))
story.append(Paragraph("Patient Position", sSubSection))
pos_lap = [
["Position", "Details"],
["Primary position", "Supine; surgeon on patient's LEFT side"],
["Alternative", "Split-leg (French) position; surgeon between patient's legs – ergonomic for RUQ"],
["Table tilt", "Reverse Trendelenburg (15–20°) + left lateral tilt – bowel falls away from operative field"],
["Arm", "Tuck one arm (ipsilateral) if cholangiogram planned – allows fluoroscopy machine access"],
["Monitor", "Placed at patient's right shoulder/head end"],
]
story.append(two_col_table(pos_lap, col1_w=45*mm))
story.append(Spacer(1, 2*mm))
story.append(Paragraph("Port Placement (Standard 4-Port)", sSubSection))
port_data = [
["Port", "Size", "Location", "Purpose"],
["Port 1 – Camera", "5 or 10 mm", "Supra-umbilical (umbilicus)", "30° laparoscope (camera port)"],
["Port 2 – Main working", "10 or 12 mm", "Epigastrium (subxiphoid, midline)", "Clips, scissors, dissector, cholangiogram"],
["Port 3 – Infundibulum", "5 mm", "Right mid-clavicular line (RUQ)", "Grasper – retract infundibulum inferolaterally"],
["Port 4 – Fundus", "5 mm", "Right flank / anterior axillary line", "Locking grasper – retract fundus to right shoulder"],
]
port_tbl = Table(
[[Paragraph(r, sTableHeader) for r in port_data[0]]] +
[[Paragraph(str(c), sTableCellB if i==0 else sTableCell) for i,c in enumerate(row)]
for row in port_data[1:]],
colWidths=[35*mm, 20*mm, 55*mm, 62*mm]
)
port_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), C_NAVY),
("ROWBACKGROUNDS",(0,1), (-1,-1), [C_WHITE, C_GRAY_LT]),
("GRID", (0,0), (-1,-1), 0.4, C_DIVIDER),
("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), "TOP"),
]))
story.append(port_tbl)
story.append(Paragraph(
"Additional ports may be placed as needed for retraction in difficult cases. "
"Hasson (open) technique preferred if previous surgery/scars at umbilicus. "
"Fascial defects >10 mm must be closed at end.", sSmall))
story.append(Spacer(1, 3*mm))
story.append(Paragraph("Operative Steps", sSubSection))
steps_lap = [
("1", "Establish Pneumoperitoneum",
["CO₂ to 12–15 mmHg",
"Technique: Closed Veress needle (supraumbilical), OR Open Hasson cut-down, OR Optical viewing trocar",
"Confirm intraperitoneal position before insufflation (aspiration test, saline hanging drop)",
"Insert camera port first; inspect for entry injuries before other ports"]),
("2", "Insert Remaining Ports",
["Under direct vision (not blind)",
"Port 2 (epigastric) → working port",
"Port 3 (RMC line) → infundibulum grasper",
"Port 4 (right flank) → fundus grasper"]),
("3", "Retraction – Open Hepatocystic Triangle",
["Port 4 (assistant): Grasp fundus → retract CEPHALAD over liver edge toward patient's RIGHT SHOULDER",
"Port 3 (surgeon): Grasp infundibulum → retract INFEROLATERALLY (toward patient's right side)",
"This opens the hepatocystic triangle, increases cystic duct–CBD angle, limits dissection to safe zone",
"B-SAFE landmarks: Bile duct | Sulcus of Rouvière | hepatic Artery | umbilical Fissure | Enteric (duodenum)"]),
("4", "Dissect Hepatocystic Triangle",
["Hook electrocautery (monopolar ~30 W, low setting, intermittent short bursts – avoid thermal spread)",
"Clear ALL fat, fibrous, and areolar tissue from BOTH anterior AND posterior aspects of the triangle",
"Safe zone: cephalad to R4U line (Rouvière's sulcus → umbilical fissure across base of segment IV)",
"Do NOT use blind/deep cautery; divide small amounts of tissue at a time"]),
("5", "Achieve Critical View of Safety (CVS) ← MANDATORY",
["THREE criteria ALL must be met before ANY clipping:",
"① Hepatocystic triangle CLEARED of all fat/fibrous tissue",
"② Lower ⅓ of gallbladder SEPARATED from cystic plate/liver bed",
"③ ONLY TWO structures seen entering the gallbladder (cystic duct + cystic artery)",
"DOCUMENT CVS by photograph or video clip in the operative record",
"If CVS cannot be achieved → STOP → employ bailout strategy"]),
("6", "Divide Cystic Artery and Cystic Duct",
["Clip cystic artery: 2 clips proximally + 1 clip on gallbladder side → divide",
"Clip cystic duct: 2 clips at base (proximal) + 1 clip on gallbladder side → divide",
"Dilated cystic duct (too wide for clips): use endoloop, laparoscopic stapler, or suture ligation",
"CONFIRM: only cystic structures divided – NOT the CBD or right hepatic artery"]),
("7", "Intraoperative Cholangiogram (Selective)",
["Indications: abnormal LFTs, prior pancreatitis/jaundice, dilated CBD on USS, unclear anatomy",
"Technique: proximal clip on cystic duct → small anterior incision → insert cholangiogram catheter",
"Ideal IOC: fills right + left hepatic ducts, drains into duodenum, no filling defects, no air bubbles",
"Routine IOC detects CBD stones in ~7% of patients"]),
("8", "Dissect Gallbladder from Liver Bed",
["Electrocautery dissection on the cystic plate (staying on the gallbladder side)",
"Watch for aberrant posterior bile ducts or arteries",
"Before final detachment: use gallbladder as retractor for a final field evaluation",
"Check: bleeding points, bile staining, clip positions on cystic duct and artery"]),
("9", "Remove Gallbladder & Close",
["Remove via epigastric or umbilical port (retrieval bag recommended – prevents stone spillage)",
"Enlarge fascial incision if needed for large or inflamed gallbladder",
"Retrieve ALL spilled stones (risk of delayed abscess, fistula)",
"Drain: NOT routine; use if gangrenous, bile spill, or anticipated accumulation",
"Close fascial defects ≥10 mm to prevent port-site hernia",
"Skin closure with absorbable sutures or skin glue"]),
]
for num, title, body in steps_lap:
story.append(step_box(num, title, body))
story.append(Spacer(1, 2*mm))
# CVS alert box
story.append(alert_box(
"⚠ CRITICAL VIEW OF SAFETY (CVS): No structure is to be clipped or divided until ALL THREE CVS criteria "
"are satisfied and documented. This is the single most important safety principle in cholecystectomy.",
bg=C_RED_LT, border=C_RED
))
# Bailout strategies
story.append(Paragraph("Bailout Strategies (When CVS Cannot Be Achieved)", sSubSection))
bail_data = [
["Strategy", "When to Use"],
["1. Abort + return electively", "Acute inflammation too severe; stable patient – plan interval cholecystectomy"],
["2. Convert to open cholecystectomy", "Ongoing difficulty, suspected injury, anatomical uncertainty"],
["3. Tube cholecystostomy (14 Fr Foley)", "Decompression only; bridge to definitive procedure in unstable patient"],
["4. Subtotal cholecystectomy (fenestrating or reconstituting)", "Safer than risky dissection; leave cystic duct/part of wall if adherent"],
["5. Fundus-first (retrograde) approach", "Severe adhesions/inflammation at Calot's – dissect from fundus downward"],
]
story.append(two_col_table(bail_data, col1_w=70*mm, header_color=C_AMBER))
story.append(Spacer(1, 2*mm))
# ── 5. OPEN CHOLECYSTECTOMY ───────────────────────────────────────────────
story.append(PageBreak())
story.append(section_header("5. Open Cholecystectomy", C_TEAL))
story.append(Paragraph("Patient Position & Incision", sSubSection))
open_pos = [
["Parameter", "Detail"],
["Position", "Supine; optional right-sided bolster/roll under flank to extend RUQ"],
["Incision options", "① Right subcostal (Kocher) – most common; 2–3 cm below costal margin, over lateral rectus border\n② Upper midline – faster, better for exploration\n③ Right upper transverse – alternative"],
["Retraction", "Self-retaining retractor (Finochietto, Thompson) OR assistant's left hand – \"the left hand of the assistant does all the work\" (Moynihan)"],
["Exposure", "Packs on hepatic flexure of colon, duodenum, lesser omentum"],
]
story.append(two_col_table(open_pos, col1_w=42*mm))
story.append(Spacer(1, 2*mm))
story.append(Paragraph("Operative Steps – Open Cholecystectomy", sSubSection))
steps_open = [
("1", "Incision & Entry",
["Kocher incision: skin → subcut fat → anterior rectus sheath → split/divide rectus → posterior sheath → peritoneum",
"Confirm gallbladder position; run fingers along undersurface of liver"]),
("2", "Exposure",
["Place moist packs: hepatic flexure of colon, duodenum, lesser omentum",
"Retract liver superiorly with liver retractor",
"Place Duval/Allis forceps on infundibulum for traction"]),
("3", "Identify Triangle of Calot",
["Dissect peritoneum over hepatoduodenal ligament",
"Palpate CBD (usually right-sided, rounded cord) to confirm location BEFORE any dissection",
"Clear fat/areolar tissue from Calot's triangle (cystic duct + cystic artery + liver margin)"]),
("4", "Ligate Cystic Artery",
["Trace artery from Calot's triangle to gallbladder wall",
"Pass 2-0 absorbable ligatures (or clips) proximal and distal",
"Divide between ligatures",
"Beware: right hepatic artery can be mistaken for cystic artery – confirm origin"]),
("5", "Ligate Cystic Duct",
["Dissect cystic duct free from CBD junction under direct vision",
"Confirm NOT dividing CBD (palpate, intraoperative cholangiogram if uncertain)",
"Ligate with 2-0 absorbable sutures (proximal + distal) and divide",
"Leave adequate cystic duct stump (≥5 mm) to prevent stump leak"]),
("6", "Intraoperative Cholangiogram (Selective)",
["Same indications as laparoscopic (see Section 4)",
"Proximal tie on cystic duct → small anterior ductal incision → insert cholangiogram catheter → contrast under fluoroscopy"]),
("7", "Dissect Gallbladder from Liver Bed",
["Retrograde (fundus-first) OR antegrade (infundibulum-first) dissection",
"Electrocautery or sharp dissection on cystic plate",
"Secure hemostasis from liver bed with diathermy / hemostatic agents",
"If gallbladder perforates: retrieve all stones; bile irrigation"]),
("8", "Partial/Subtotal Cholecystectomy (If Anatomy Unclear)",
["Remove as much gallbladder mucosa as possible (ablate remaining mucosa with diathermy)",
"Oversew or close cystic duct stump with absorbable sutures",
"Wide drainage of the area – mandatory"]),
("9", "Closure",
["Check for bile leak (bile staining, bilious fluid) and bleeding",
"Drain placement (sub-hepatic closed-suction drain) if: severe inflammation, uncertain duct stump, bile spill",
"Close peritoneum (1-0 absorbable continuous) → posterior sheath → rectus → anterior sheath (1-0 absorbable loop or PDS)",
"Skin: subcuticular absorbable or staples"]),
]
for num, title, body in steps_open:
story.append(step_box(num, title, body))
story.append(Spacer(1, 2*mm))
# ── 6. INTRAOPERATIVE CHOLANGIOGRAM ──────────────────────────────────────
story.append(section_header("6. Intraoperative Cholangiogram (IOC)", C_BLUE))
ioc_data = [
["IOC: Selective Indications", "Technique Summary"],
["History of jaundice or abnormal LFTs", "1. Clip proximal cystic duct"],
["Prior biliary pancreatitis", "2. Small anterior incision on cystic duct"],
["Dilated CBD on preoperative ultrasound", "3. Insert and secure cholangiogram catheter"],
["Large duct + small stones on imaging", "4. Inject dilute contrast (50%) under fluoroscopy (live)"],
["Failed or unavailable preoperative ERCP", "5. Avoid air bubbles (mimic filling defects)"],
["Intraoperative anatomical uncertainty", "Ideal result: R+L hepatic ducts filled, drainage into duodenum, no defects"],
]
story.append(two_col_table(ioc_data, col1_w=75*mm))
story.append(Paragraph(
"Routine IOC detects CBD stones in ~7% of patients. "
"No consensus on routine vs selective use; all surgeons performing cholecystectomy "
"should be proficient with the technique. (Schwartz's 11e)", sSmall))
story.append(Spacer(1, 2*mm))
# ── 7. COMPLICATIONS ──────────────────────────────────────────────────────
story.append(PageBreak())
story.append(section_header("7. Complications", C_RED))
story.append(Paragraph("Intraoperative Complications", sSubSection))
intraop_comp = [
["Complication", "Incidence / Notes", "Immediate Action"],
["Bile duct injury (BDI)", "Lap: 0.3–0.6% | Open: 0.2–0.3%\nMost feared; major cause of litigation", "Stop dissection. Convert to open. Refer to HPB centre if complex repair needed."],
["Right hepatic artery injury", "Often accompanies BDI (close anatomic proximity)", "Control bleeding. Vascular repair or ligation if necessary."],
["Major vascular injury (aorta/IVC/portal vein)", "Trocar/Veress insertion – rare but life-threatening", "Direct pressure immediately. Call for vascular surgeon. Open laparotomy."],
["Bowel injury (stomach/duodenum/colon)", "Veress or trocar insertion", "Repair immediately (primary or with loop if contaminated)."],
["Gallbladder perforation / Stone spillage", "5–40% of laparoscopic cases", "Irrigate thoroughly. Retrieve ALL stones. Document in operative note."],
["Bleeding from cystic artery / liver bed", "Common", "Clip, tie or cauterise. Do not clip blindly."],
]
comp_tbl = Table(
[[Paragraph(r, sTableHeader) for r in intraop_comp[0]]] +
[[Paragraph(str(c), sTableCellB if i==0 else sTableCell) for i,c in enumerate(row)]
for row in intraop_comp[1:]],
colWidths=[45*mm, 55*mm, 72*mm]
)
comp_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), C_RED),
("ROWBACKGROUNDS",(0,1), (-1,-1), [C_WHITE, C_RED_LT]),
("GRID", (0,0), (-1,-1), 0.4, C_DIVIDER),
("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), "TOP"),
]))
story.append(comp_tbl)
story.append(Spacer(1, 3*mm))
story.append(Paragraph("Postoperative Complications", sSubSection))
postop_comp = [
["Complication", "Presentation", "Investigation / Management"],
["Bile leak", "RUQ pain, fever, bilioma on USS", "USS → MRCP/ERCP (stent if cystic stump leak). Drain if collection."],
["Bile duct stricture / injury", "Jaundice, cholangitis (early or late)", "USS → MRCP. ERCP/stent or surgical hepaticojejunostomy at HPB centre."],
["Retained CBD stones", "Jaundice, cholangitis (Charcot's triad)", "MRCP → ERCP with sphincterotomy + stone extraction."],
["Intra-abdominal abscess", "Fever, RUQ pain, sepsis (esp. if stones spilled)", "CT abdomen → radiological drainage ± antibiotics."],
["Port-site / wound hernia", "Bulge at port site; can obstruct", "Repair electively (or urgently if obstructed)."],
["Acute pancreatitis", "Epigastric pain, raised amylase", "Conservative management; ERCP if CBD stone."],
["Post-cholecystectomy syndrome", "Residual/recurrent RUQ symptoms (~10–15%)", "Exclude retained stone (MRCP), sphincter of Oddi dysfunction (ERCP manometry)."],
["Pulmonary complications", "Atelectasis, pneumonia (more common – open)", "Physiotherapy, analgesia, early mobilisation."],
]
postop_tbl = Table(
[[Paragraph(r, sTableHeader) for r in postop_comp[0]]] +
[[Paragraph(str(c), sTableCellB if i==0 else sTableCell) for i,c in enumerate(row)]
for row in postop_comp[1:]],
colWidths=[42*mm, 48*mm, 82*mm]
)
postop_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), C_NAVY),
("ROWBACKGROUNDS",(0,1), (-1,-1), [C_WHITE, C_GRAY_LT]),
("GRID", (0,0), (-1,-1), 0.4, C_DIVIDER),
("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), "TOP"),
]))
story.append(postop_tbl)
story.append(Spacer(1, 3*mm))
# Strasberg classification table
story.append(Paragraph("Strasberg Classification of Bile Duct Injuries", sSubSection))
stras_data = [
["Type", "Description", "Management Principle"],
["A", "Bile leak from cystic duct stump or duct of Luschka (minor radical in GB fossa)", "ERCP + stent; drain collection"],
["B", "Occluded (clipped) right posterior sectoral duct – no leak", "Depends on symptoms; often conservative vs Roux-en-Y"],
["C", "Bile leak from divided (unoccluded) right posterior sectoral duct", "ERCP if amenable; otherwise Roux-en-Y hepaticojejunostomy"],
["D", "Lateral laceration/leak from main bile duct without tissue loss", "ERCP + stent (minor); primary repair or hepaticojejunostomy (major)"],
["E1", "Transected main bile duct; stricture >2 cm from hilum", "Hepaticojejunostomy at HPB centre"],
["E2", "Transected main bile duct; stricture <2 cm from hilum", "Hepaticojejunostomy at HPB centre"],
["E3", "Stricture at hilum; R + L ducts in communication", "High hepaticojejunostomy"],
["E4", "Stricture at hilum; R + L ducts separated", "Complex biliary reconstruction"],
["E5", "Right aberrant sectoral duct + main duct involved", "Complex biliary reconstruction"],
]
stras_tbl = Table(
[[Paragraph(r, sTableHeader) for r in stras_data[0]]] +
[[Paragraph(str(c), sTableCellB if i==0 else sTableCell) for i,c in enumerate(row)]
for row in stras_data[1:]],
colWidths=[15*mm, 80*mm, 77*mm]
)
stras_tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), C_RED),
("ROWBACKGROUNDS",(0,1), (-1,-1), [C_WHITE, C_RED_LT]),
("GRID", (0,0), (-1,-1), 0.4, C_DIVIDER),
("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), "TOP"),
]))
story.append(stras_tbl)
story.append(Spacer(1, 2*mm))
story.append(alert_box(
"SUSPECTED POSTOPERATIVE BDI PROTOCOL: Resuscitate → IV antibiotics → Urgent RUQ USS → "
"MRCP (anatomy) → ERCP if therapeutic intervention needed → Surgical repair (hepaticojejunostomy) "
"for major injuries AT AN HPB CENTRE. Do NOT attempt repair without expertise.",
bg=C_RED_LT, border=C_RED
))
# ── 8. OPEN vs LAPAROSCOPIC COMPARISON ───────────────────────────────────
story.append(PageBreak())
story.append(section_header("8. Open vs Laparoscopic – Comparison", C_NAVY))
comp_table_data = [
["Parameter", "Laparoscopic", "Open"],
["Gold standard?", "YES – treatment of choice", "When lap not feasible/safe"],
["Incision", "4 × 5–12 mm ports", "Kocher / midline (10–15 cm)"],
["Pneumoperitoneum", "CO₂, 12–15 mmHg required", "Not required"],
["Pain (postop)", "Significantly less", "More; requires adequate analgesia"],
["Hospital stay", "Day case to 1–2 days", "3–5 days"],
["Return to work", "1–2 weeks", "4–6 weeks"],
["Wound infection", "Lower", "Higher"],
["Bile duct injury rate", "0.3–0.6% (historically higher; converging)", "0.2–0.3%"],
["Stone spillage risk", "5–40%", "Less common; direct retrieval"],
["Mortality", "~0.1%", "<1%"],
["Visualization", "Magnified laparoscopic view (10–30°)", "Direct (wide field)"],
["Conversion", "~5% elective; 10–30% emergency", "N/A (already open)"],
]
comp_tbl2 = Table(
[[Paragraph(r, sTableHeader) for r in comp_table_data[0]]] +
[[Paragraph(str(c), sTableCellB if i==0 else
(S("g", fontName="Helvetica-Bold", fontSize=7.8, textColor=C_GREEN, leading=10.5) if i==1 else sTableCell)
) for i,c in enumerate(row)]
for row in comp_table_data[1:]],
colWidths=[45*mm, 72*mm, 55*mm]
)
comp_tbl2.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), C_NAVY),
("ROWBACKGROUNDS",(0,1), (-1,-1), [C_WHITE, C_GRAY_LT]),
("GRID", (0,0), (-1,-1), 0.4, C_DIVIDER),
("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), "TOP"),
]))
story.append(comp_tbl2)
story.append(Spacer(1, 2*mm))
# ── 9. KEY ANATOMICAL LANDMARKS ───────────────────────────────────────────
story.append(section_header("9. Key Anatomical Landmarks & Safety Principles", C_TEAL))
story.append(Paragraph("Triangle of Calot (Hepatocystic Triangle)", sSubSection))
story.append(Paragraph(
"Bounded by: <b>cystic duct</b> (inferiorly), <b>common hepatic duct</b> (medially), and "
"<b>inferior surface of the liver / cystic plate</b> (superiorly). "
"Contains the cystic artery and lymph node of Calot (Lund's node). "
"Note: 'Triangle of Calot' strictly includes the cystic artery; the broader 'hepatocystic triangle' "
"is the modern preferred term used in CVS criteria.", sBody))
anat_data = [
["Landmark", "Significance"],
["Rouvière's sulcus", "Constant hepatic fissure on right liver surface. Safe dissection zone is CEPHALAD to a line from its roof to the umbilical fissure (R4U line)."],
["Cystic plate", "Peritoneal reflection where GB attaches to liver. Exposing the medial ⅓ of the cystic plate is the 3rd CVS criterion."],
["Lund's node (Node of Calot)", "Lymph node at junction of cystic duct and hepatic duct; landmark for cystic artery."],
["Hartmann's pouch", "Infundibulum of GB; the grasping point for inferolateral retraction."],
["Duct of Luschka", "Small bile duct from liver directly into GB bed; if injured → Type A bile leak."],
["Right hepatic artery", "Typically passes behind CHD into Calot's triangle; caterpillar hump variant mimics cystic artery."],
["B-SAFE method", "5 landmarks: Bile duct | Sulcus of Rouvière | hepatic Artery | umbilical Fissure | Enteric (duodenum). Use to orient cognitive map during difficult dissection."],
]
story.append(two_col_table(anat_data, col1_w=50*mm))
story.append(Spacer(1, 2*mm))
story.append(alert_box(
"SAFE ENERGY USE (Hook Cautery): Low setting ~30 W | Intermittent short bursts | "
"Small tissue bites at a time | AVOID blind cautery near hepatocystic triangle | "
"Ultrasonic energy = less lateral spread (but cumbersome in tight triangle).",
bg=C_AMBER_LT, border=C_AMBER,
text_style=S("amb", fontName="Helvetica-Bold", fontSize=8.2, textColor=C_AMBER, leading=12)
))
# ── 10. QUICK REFERENCE / SAFETY CHECKLIST ───────────────────────────────
story.append(section_header("10. Intraoperative Safety Checklist", C_GREEN))
checklist_data = [
["☐ Pre-incision WHO surgical safety check completed"],
["☐ Antibiotic given within 60 minutes of incision"],
["☐ DVT prophylaxis in place"],
["☐ Patient positioned correctly (supine / split-leg); table tilted"],
["☐ 4 ports placed under direct vision"],
["☐ B-SAFE landmarks identified"],
["☐ Correct retraction: fundus → right shoulder; infundibulum → inferolateral"],
["☐ Hepatocystic triangle dissected ANTERIORLY and POSTERIORLY"],
["☐ CVS criteria ALL met – confirmed by surgeon"],
["☐ CVS DOCUMENTED (photo or video)"],
["☐ Only 2 structures entering gallbladder before clipping"],
["☐ Cystic duct clipped: 2 proximal + 1 distal → divided"],
["☐ Cystic artery clipped: 2 proximal + 1 distal → divided"],
["☐ IOC performed if indicated; results reviewed"],
["☐ Gallbladder dissected off cystic plate (not avulsed)"],
["☐ Final field check: bleeding? bile staining? clip positions?"],
["☐ ALL spilled stones retrieved"],
["☐ Retrieval bag used for specimen"],
["☐ Drain placed if indicated (gangrenous GB / bile spill)"],
["☐ Fascial defects ≥10 mm closed"],
["☐ Instrument and swab count confirmed correct"],
["☐ Post-procedure briefing with team completed"],
]
check_tbl = Table(
[[Paragraph(r[0], sBody)] for r in checklist_data],
colWidths=[W - 32*mm]
)
check_tbl.setStyle(TableStyle([
("ROWBACKGROUNDS", (0,0), (-1,-1), [C_GREEN_LT, C_WHITE]),
("GRID", (0,0), (-1,-1), 0.3, C_DIVIDER),
("TOPPADDING", (0,0), (-1,-1), 3),
("BOTTOMPADDING", (0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING", (0,0), (-1,-1), 6),
]))
story.append(check_tbl)
story.append(Spacer(1, 3*mm))
# ── FOOTER / REFERENCES ───────────────────────────────────────────────────
story.append(hr(C_DIVIDER))
story.append(Paragraph(
"<b>References:</b> Schwartz's Principles of Surgery 11e (pp. 1438–1443) · "
"Bailey & Love's Short Practice of Surgery 28e (pp. 1270–1274) · "
"Maingot's Abdominal Operations (pp. 1014–1016) · "
"Mulholland & Greenfield's Surgery 7e (pp. 3065–3066) · "
"Fischer's Mastery of Surgery 8e · "
"Current Surgical Therapy 14e",
sSmall))
story.append(Paragraph(
"<b>Disclaimer:</b> This guide is intended as a quick intraoperative reference for qualified surgical teams and "
"does not replace surgical judgment, institutional protocols, or applicable training. "
"Generated by Orris Medical AI · June 2026.",
sSmall))
# ── BUILD ──────────────────────────────────────────────────────────────────
os.makedirs(os.path.dirname(OUTPUT_PATH), exist_ok=True)
doc.build(story)
print(f"PDF generated successfully: {OUTPUT_PATH}")