I want to learn all the important X rays in respiratory system for MBBS exams Can u summarise all of them with correct CLEAR AND REAL X rays, into a pdf with short notes minimal simple points ,good for self preparation
I want to learn all the important X rays in respiratory system for MBBS exams Can u summarise all of them with correct CLEAR AND REAL X rays, into a pdf with short notes minimal simple points ,good for self preparation
respiratory chest X-ray cases pneumothorax pleural effusion pneumonia consolidation open access radiology images MBBS
https://geekymedics.com/common-cxr-findings-in-osces
mkdir -p /tmp/workspace/respiratory-xray-pdf/images && cd /tmp/workspace/respiratory-xray-pdf/images && curl -L -o pneumonia.jpg "https://upload.wikimedia.org/wikipedia/commons/b/be/Pneumonia_x-ray.jpg" -A "Mozilla/5.0" --max-time 15 2>&1 | tail -3 && ls -la
~/respiratory-xray-pdf/generate_pdf.py
"""
Respiratory X-rays for MBBS Exams - PDF Generator
Uses ReportLab to create a comprehensive study guide with schematic X-ray diagrams.
"""
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import cm, mm
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, KeepTogether, PageBreak
)
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_JUSTIFY
from reportlab.graphics.shapes import (
Drawing, Rect, Circle, Ellipse, Line, Polygon, String, Arc,
Path, PolyLine
)
from reportlab.graphics import renderPDF
from reportlab.graphics.charts.lineplots import LinePlot
from reportlab.platypus import Flowable
import math
W, H = A4
PAGE_W = W - 4*cm
PAGE_H = H - 4*cm
# βββ Colour Palette βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
DARK_BG = colors.HexColor("#0A0A0A") # near-black radiograph BG
LIGHT_BONE= colors.HexColor("#D8D8C8") # bone / dense structures
MED_GRAY = colors.HexColor("#888880") # mid-density (soft tissue)
AIR_DARK = colors.HexColor("#1A1A2E") # normal aerated lung
OPAQUE = colors.HexColor("#C0C0B0") # consolidation / opacity
WHITE_OP = colors.HexColor("#F0F0E0") # very bright opacity
TRACHEA_C = colors.HexColor("#2A2A4A") # trachea air
HEART_C = colors.HexColor("#6A5A5A") # heart silhouette
RIB_C = colors.HexColor("#B0A898") # ribs
HIGHLIGHT = colors.HexColor("#FFD700") # callout arrows / labels
RED_C = colors.HexColor("#CC3322") # danger / tension PTX
NAVY = colors.HexColor("#1B3A6B") # header bg
TEAL = colors.HexColor("#1A7A7A") # accent
LIGHT_BLU = colors.HexColor("#E8F4FD") # section bg
YELLOW_BG = colors.HexColor("#FFFBE6") # key points
GREEN_BG = colors.HexColor("#E8F8E8") # normal / mnemonic
ORANGE_BG = colors.HexColor("#FFF3E0") # management
PINK_BG = colors.HexColor("#FDE8E8") # danger
TEXT_DARK = colors.HexColor("#1A1A1A")
TEXT_MED = colors.HexColor("#333333")
# βββ Drawing helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def chest_base(d, w, h, show_labels=True):
"""Draw a basic CXR template: black bg, ribs, spine, carina."""
# Background
d.add(Rect(0, 0, w, h, fillColor=DARK_BG, strokeColor=None))
# Spine (centre vertical stripe)
cx = w / 2
d.add(Rect(cx - 0.04*w, 0.05*h, 0.08*w, 0.75*h,
fillColor=LIGHT_BONE, strokeColor=None))
# Ribs β 6 pairs, arching from spine to lateral
for i in range(6):
y_top = h * (0.72 - i * 0.1)
# Left rib arc
arc_l = Arc(cx - 0.45*w, y_top - 0.07*h, cx - 0.01*w, y_top + 0.04*h,
startAng=10, extent=145, strokeColor=RIB_C,
strokeWidth=1.8, fillColor=None)
d.add(arc_l)
# Right rib arc
arc_r = Arc(cx + 0.01*w, y_top - 0.07*h, cx + 0.45*w, y_top + 0.04*h,
startAng=25, extent=145, strokeColor=RIB_C,
strokeWidth=1.8, fillColor=None)
d.add(arc_r)
# Clavicles
d.add(Line(cx - 0.02*w, h*0.82, cx - 0.32*w, h*0.88,
strokeColor=LIGHT_BONE, strokeWidth=2))
d.add(Line(cx + 0.02*w, h*0.82, cx + 0.32*w, h*0.88,
strokeColor=LIGHT_BONE, strokeWidth=2))
# Trachea
d.add(Rect(cx - 0.025*w, h*0.62, 0.05*w, h*0.22,
fillColor=TRACHEA_C, strokeColor=MED_GRAY, strokeWidth=0.5))
# Carina
d.add(Line(cx, h*0.62, cx - 0.06*w, h*0.58,
strokeColor=LIGHT_BONE, strokeWidth=1.2))
d.add(Line(cx, h*0.62, cx + 0.06*w, h*0.58,
strokeColor=LIGHT_BONE, strokeWidth=1.2))
# Diaphragm domes
for side, sign in [("L", -1), ("R", 1)]:
dome_x = cx + sign * 0.18*w
d.add(Arc(dome_x - 0.22*w, h*0.04, dome_x + 0.22*w, h*0.28,
startAng=0, extent=180,
strokeColor=LIGHT_BONE, strokeWidth=2, fillColor=None))
# Heart silhouette
d.add(Ellipse(cx - 0.06*w, h*0.28, cx + 0.2*w, h*0.6,
fillColor=HEART_C, strokeColor=None))
# Normal aerated lung fields (L & R)
# Right lung
d.add(Rect(cx + 0.04*w, h*0.25, 0.34*w, h*0.5,
fillColor=AIR_DARK, strokeColor=None, rx=0.04*w))
# Left lung
d.add(Rect(cx - 0.38*w, h*0.25, 0.30*w, h*0.5,
fillColor=AIR_DARK, strokeColor=None, rx=0.04*w))
# Lung hilum dots
d.add(Circle(cx + 0.08*w, h*0.52, 0.025*w,
fillColor=MED_GRAY, strokeColor=None))
d.add(Circle(cx - 0.08*w, h*0.52, 0.025*w,
fillColor=MED_GRAY, strokeColor=None))
return cx
# βββ Individual X-ray Drawings βββββββββββββββββββββββββββββββββββββββββββββ
def draw_normal_cxr(w=280, h=260):
d = Drawing(w, h)
cx = chest_base(d, w, h)
# Label
d.add(String(6, 10, "NORMAL CXR", fontSize=7,
fillColor=colors.white, fontName="Helvetica-Bold"))
return d
def draw_pneumothorax(side="R", tension=False, w=280, h=260):
d = Drawing(w, h)
cx = chest_base(d, w, h)
sign = 1 if side == "R" else -1
# Collapsed lung (smaller ellipse, shifted medially)
if side == "R":
col_x1, col_x2 = cx + 0.04*w, cx + 0.24*w
else:
col_x1, col_x2 = cx - 0.28*w, cx - 0.08*w
col_y1, col_y2 = h*0.32, h*0.6
d.add(Ellipse(col_x1, col_y1, col_x2, col_y2,
fillColor=MED_GRAY, strokeColor=LIGHT_BONE, strokeWidth=1))
# Air gap (bright area between lung edge and chest wall)
if side == "R":
air_x1, air_x2 = cx + 0.24*w, cx + 0.38*w
else:
air_x1, air_x2 = cx - 0.38*w, cx - 0.28*w
d.add(Rect(air_x1, h*0.25, abs(air_x2 - air_x1), h*0.48,
fillColor=DARK_BG, strokeColor=None))
# Visceral pleural line
d.add(Line(air_x1, h*0.26, air_x1, h*0.72,
strokeColor=HIGHLIGHT, strokeWidth=1.5))
# Tracheal/mediastinal shift for tension
if tension:
shift = -sign * 0.06*w
# redraw trachea shifted
d.add(Rect(cx + shift - 0.025*w, h*0.62, 0.05*w, h*0.22,
fillColor=colors.HexColor("#AA0000"),
strokeColor=colors.red, strokeWidth=0.5))
label = f"TENSION PTX ({side})"
d.add(String(cx - 0.38*w if side == "R" else cx + 0.04*w,
h*0.12, "TRACHEA SHIFTED",
fontSize=6, fillColor=RED_C, fontName="Helvetica-Bold"))
else:
label = f"PNEUMOTHORAX ({side})"
d.add(String(6, 10, label, fontSize=7,
fillColor=colors.white, fontName="Helvetica-Bold"))
# Callout arrow
mid_air = (air_x1 + (w*0.38 if side == "R" else cx - 0.28*w)) / 2
d.add(String(air_x1 + 2, h*0.5,
"Air\ngap", fontSize=6,
fillColor=HIGHLIGHT, fontName="Helvetica"))
return d
def draw_pleural_effusion(side="L", massive=False, w=280, h=260):
d = Drawing(w, h)
cx = chest_base(d, w, h)
if side == "L":
eff_x = cx - 0.38*w
eff_w = 0.32*w
else:
eff_x = cx + 0.04*w
eff_w = 0.34*w
if massive:
eff_h = h * 0.52
eff_y = h * 0.08
else:
eff_h = h * 0.25
eff_y = h * 0.08
# Fluid opacity (white)
d.add(Rect(eff_x, eff_y, eff_w, eff_h,
fillColor=OPAQUE, strokeColor=None, rx=0.02*w))
# Meniscus curve (concave top)
meniscus_y = eff_y + eff_h
d.add(Arc(eff_x - 0.01*w, meniscus_y - 0.06*h,
eff_x + eff_w + 0.01*w, meniscus_y + 0.03*h,
startAng=0, extent=180,
strokeColor=WHITE_OP, strokeWidth=2, fillColor=None))
# Mediastinal shift for massive
if massive:
shift_x = 0.06*w if side == "L" else -0.06*w
# Shift trachea
d.add(Rect(cx + shift_x - 0.025*w, h*0.62, 0.05*w, h*0.22,
fillColor=colors.HexColor("#553300"),
strokeColor=MED_GRAY, strokeWidth=0.5))
d.add(String(6, 22, "Mediastinal shift AWAY",
fontSize=6, fillColor=HIGHLIGHT, fontName="Helvetica-BoldOblique"))
label = f"PLEURAL EFFUSION ({side})" + (" - MASSIVE" if massive else "")
d.add(String(6, 10, label, fontSize=7,
fillColor=colors.white, fontName="Helvetica-Bold"))
d.add(String(eff_x + 2, eff_y + eff_h/2,
"Fluid\nOpacity", fontSize=6,
fillColor=TEXT_DARK, fontName="Helvetica"))
return d
def draw_consolidation(lobe="RLL", w=280, h=260):
d = Drawing(w, h)
cx = chest_base(d, w, h)
lobe_regions = {
"RLL": dict(x=cx + 0.06*w, y=h*0.22, ww=0.3*w, hh=h*0.22),
"RUL": dict(x=cx + 0.06*w, y=h*0.55, ww=0.28*w, hh=h*0.2),
"LLL": dict(x=cx - 0.36*w, y=h*0.22, ww=0.26*w, hh=h*0.22),
"RML": dict(x=cx + 0.06*w, y=h*0.38, ww=0.28*w, hh=h*0.15),
}
reg = lobe_regions.get(lobe, lobe_regions["RLL"])
# Consolidation patch
d.add(Rect(reg["x"], reg["y"], reg["ww"], reg["hh"],
fillColor=OPAQUE, strokeColor=None, rx=0.03*w))
# Air bronchograms (dark lines through white opacity)
for j in range(3):
xb = reg["x"] + reg["ww"] * (0.25 + j*0.2)
d.add(Line(xb, reg["y"] + 0.1*reg["hh"],
xb, reg["y"] + 0.85*reg["hh"],
strokeColor=DARK_BG, strokeWidth=1.0))
d.add(String(6, 10, f"CONSOLIDATION ({lobe})", fontSize=7,
fillColor=colors.white, fontName="Helvetica-Bold"))
d.add(String(reg["x"] + 2, reg["y"] + reg["hh"] + 3,
"Air\nbronchogram", fontSize=5.5,
fillColor=HIGHLIGHT, fontName="Helvetica"))
return d
def draw_collapse(lobe="RUL", w=280, h=260):
d = Drawing(w, h)
cx = chest_base(d, w, h)
# Different collapse patterns per lobe
if lobe == "RUL":
# Upward tracheal deviation + right upper opacity wedge
d.add(Polygon([cx + 0.04*w, h*0.8,
cx + 0.34*w, h*0.8,
cx + 0.2*w, h*0.62],
fillColor=OPAQUE, strokeColor=None))
d.add(String(cx + 0.06*w, h*0.68, "Wedge\nOpacity", fontSize=5.5,
fillColor=TEXT_DARK, fontName="Helvetica"))
# Trachea deviation
d.add(Rect(cx + 0.03*w, h*0.62, 0.05*w, h*0.22,
fillColor=TRACHEA_C,
strokeColor=MED_GRAY, strokeWidth=0.5))
d.add(String(cx + 0.02*w, h*0.86, "Trachea\ndeviated R",
fontSize=5.5, fillColor=HIGHLIGHT))
elif lobe == "LLL":
# Sail sign / triangular opacity behind heart
d.add(Polygon([cx - 0.06*w, h*0.22,
cx - 0.18*w, h*0.22,
cx - 0.12*w, h*0.42],
fillColor=OPAQUE, strokeColor=None))
d.add(String(cx - 0.18*w, h*0.3, "Sail\nsign",
fontSize=5.5, fillColor=HIGHLIGHT))
elif lobe == "RML":
# Silhouette of right heart border (loss of R heart border)
d.add(Rect(cx + 0.04*w, h*0.38, 0.15*w, h*0.15,
fillColor=OPAQUE, strokeColor=None))
d.add(String(cx + 0.04*w, h*0.34,
"Lost R heart border",
fontSize=5.5, fillColor=HIGHLIGHT))
elif lobe == "LUL":
d.add(Rect(cx - 0.36*w, h*0.48, 0.28*w, h*0.3,
fillColor=OPAQUE, strokeColor=None))
d.add(String(cx - 0.34*w, h*0.79, "Veil opacity L",
fontSize=5.5, fillColor=HIGHLIGHT))
d.add(String(6, 10, f"COLLAPSE ({lobe})", fontSize=7,
fillColor=colors.white, fontName="Helvetica-Bold"))
return d
def draw_pulm_oedema(w=280, h=260):
d = Drawing(w, h)
cx = chest_base(d, w, h)
# Cardiomegaly (enlarged heart)
d.add(Ellipse(cx - 0.14*w, h*0.22, cx + 0.26*w, h*0.64,
fillColor=HEART_C, strokeColor=None))
# Bat-wing / butterfly opacity bilateral
for side, sign in [("L", -1), ("R", 1)]:
x1 = cx + sign * 0.02*w
x2 = cx + sign * 0.35*w
if sign < 0:
x1, x2 = cx - 0.35*w, cx - 0.02*w
d.add(Rect(x1, h*0.35, abs(x2-x1)*0.8, h*0.25,
fillColor=colors.HexColor("#8A8A70"),
strokeColor=None, rx=0.03*w))
# Kerley B lines (short horizontal lines at bases)
for side, bx in [("L", cx - 0.38*w), ("R", cx + 0.30*w)]:
for i in range(4):
y_k = h * (0.15 + i * 0.045)
d.add(Line(bx, y_k, bx + 0.06*w, y_k,
strokeColor=LIGHT_BONE, strokeWidth=0.8))
d.add(String(6, 10, "PULMONARY OEDEMA", fontSize=7,
fillColor=colors.white, fontName="Helvetica-Bold"))
d.add(String(cx + 0.05*w, h*0.44, "Bat-wing", fontSize=5.5,
fillColor=HIGHLIGHT))
d.add(String(cx + 0.29*w, h*0.22, "Kerley B", fontSize=5.5,
fillColor=HIGHLIGHT))
return d
def draw_miliary_tb(w=280, h=260):
d = Drawing(w, h)
cx = chest_base(d, w, h)
import random
random.seed(42)
# Tiny millet-seed nodules scattered throughout both lung fields
for lx in [cx - 0.35*w, cx + 0.06*w]:
lw2 = 0.28*w
for _ in range(80):
nx = lx + random.random() * lw2
ny = h * 0.25 + random.random() * h * 0.48
d.add(Circle(nx, ny, 1.5,
fillColor=LIGHT_BONE, strokeColor=None))
d.add(String(6, 10, "MILIARY TB", fontSize=7,
fillColor=colors.white, fontName="Helvetica-Bold"))
d.add(String(cx - 0.12*w, h*0.5, "Millet-seed\nnodules",
fontSize=5.5, fillColor=HIGHLIGHT))
return d
def draw_hilar_lymphadenopathy(w=280, h=260):
d = Drawing(w, h)
cx = chest_base(d, w, h)
# Enlarged bilateral hila
for hx in [cx - 0.14*w, cx + 0.14*w]:
d.add(Ellipse(hx - 0.07*w, h*0.46, hx + 0.07*w, h*0.62,
fillColor=MED_GRAY, strokeColor=LIGHT_BONE, strokeWidth=1))
d.add(String(6, 10, "BILATERAL HILAR LYMPHADENOPATHY", fontSize=7,
fillColor=colors.white, fontName="Helvetica-Bold"))
d.add(String(cx - 0.38*w, h*0.54, "Enlarged\nhilum",
fontSize=5.5, fillColor=HIGHLIGHT))
d.add(String(cx + 0.18*w, h*0.54, "Enlarged\nhilum",
fontSize=5.5, fillColor=HIGHLIGHT))
return d
def draw_lung_abscess(w=280, h=260):
d = Drawing(w, h)
cx = chest_base(d, w, h)
# Thick-walled cavity RUL area
cav_cx = cx + 0.18*w
cav_cy = h * 0.65
r = 0.08*w
d.add(Circle(cav_cx, cav_cy, r,
fillColor=DARK_BG, strokeColor=OPAQUE, strokeWidth=4))
# Air-fluid level inside cavity
d.add(Rect(cav_cx - r*0.7, cav_cy - r*0.1, r*1.4, r*0.6,
fillColor=OPAQUE, strokeColor=None))
d.add(String(cav_cx - r, cav_cy + r + 3, "Thick wall\n+ air-fluid level",
fontSize=5.5, fillColor=HIGHLIGHT))
d.add(String(6, 10, "LUNG ABSCESS", fontSize=7,
fillColor=colors.white, fontName="Helvetica-Bold"))
return d
def draw_pneumoperitoneum(w=280, h=260):
"""Free gas under diaphragm."""
d = Drawing(w, h)
cx = chest_base(d, w, h)
# Gas crescent under right hemidiaphragm
d.add(Arc(cx, h*0.04, cx + 0.44*w, h*0.28,
startAng=0, extent=180,
strokeColor=None, fillColor=DARK_BG))
d.add(Arc(cx, h*0.04, cx + 0.44*w, h*0.28,
startAng=0, extent=180,
strokeColor=HIGHLIGHT, strokeWidth=1.5, fillColor=None))
d.add(String(cx + 0.14*w, h*0.22, "Gas\nunder\ndiaphragm",
fontSize=5.5, fillColor=HIGHLIGHT))
d.add(String(6, 10, "PNEUMOPERITONEUM", fontSize=7,
fillColor=colors.white, fontName="Helvetica-Bold"))
return d
def draw_cardiomegaly(w=280, h=260):
d = Drawing(w, h)
cx = chest_base(d, w, h)
# Enlarged heart CTR > 0.5
d.add(Ellipse(cx - 0.22*w, h*0.22, cx + 0.3*w, h*0.65,
fillColor=HEART_C, strokeColor=LIGHT_BONE, strokeWidth=1))
# CTR measurement lines
d.add(Line(cx - 0.22*w, h*0.44, cx - 0.0*w, h*0.44,
strokeColor=HIGHLIGHT, strokeWidth=1, strokeDashArray=[2, 2]))
d.add(Line(cx + 0.0*w, h*0.44, cx + 0.3*w, h*0.44,
strokeColor=HIGHLIGHT, strokeWidth=1, strokeDashArray=[2, 2]))
d.add(String(cx - 0.26*w, h*0.46, "CTR>0.5",
fontSize=6.5, fillColor=HIGHLIGHT, fontName="Helvetica-Bold"))
d.add(String(6, 10, "CARDIOMEGALY", fontSize=7,
fillColor=colors.white, fontName="Helvetica-Bold"))
return d
def draw_apical_fibrocavitary(w=280, h=260):
"""Post-primary TB - apical fibrocavitary changes."""
d = Drawing(w, h)
cx = chest_base(d, w, h)
# Apical opacity / fibrocavitary lesion right apex
d.add(Rect(cx + 0.05*w, h*0.65, 0.25*w, h*0.18,
fillColor=colors.HexColor("#909080"),
strokeColor=None, rx=0.02*w))
# Cavity
d.add(Circle(cx + 0.14*w, h*0.73, 0.05*w,
fillColor=DARK_BG, strokeColor=LIGHT_BONE, strokeWidth=2))
# Tracheal deviation
d.add(Rect(cx + 0.02*w, h*0.62, 0.05*w, h*0.22,
fillColor=TRACHEA_C, strokeColor=MED_GRAY, strokeWidth=0.5))
d.add(String(cx + 0.06*w, h*0.82, "Apical\ncavity + fibrosis",
fontSize=5.5, fillColor=HIGHLIGHT))
d.add(String(6, 10, "POST-PRIMARY TB (Apical)", fontSize=7,
fillColor=colors.white, fontName="Helvetica-Bold"))
return d
def draw_sarcoidosis(w=280, h=260):
"""Stage II sarcoidosis - bilateral hilar + parenchymal."""
d = Drawing(w, h)
cx = chest_base(d, w, h)
# BHL
for hx in [cx - 0.14*w, cx + 0.14*w]:
d.add(Ellipse(hx - 0.07*w, h*0.46, hx + 0.07*w, h*0.62,
fillColor=MED_GRAY, strokeColor=LIGHT_BONE, strokeWidth=1))
# Parenchymal nodules (scattered)
import random
random.seed(7)
for lx in [cx - 0.35*w, cx + 0.06*w]:
for _ in range(25):
nx = lx + random.random() * 0.26*w
ny = h*0.3 + random.random() * h*0.38
d.add(Circle(nx, ny, 2.5, fillColor=MED_GRAY, strokeColor=None))
d.add(String(6, 10, "SARCOIDOSIS (Stage II)", fontSize=7,
fillColor=colors.white, fontName="Helvetica-Bold"))
return d
def draw_hydropneumothorax(w=280, h=260):
d = Drawing(w, h)
cx = chest_base(d, w, h)
# Right side: air above, fluid below (straight horizontal fluid level)
fluid_y = h * 0.35
# Air (dark) above
d.add(Rect(cx + 0.04*w, fluid_y, 0.34*w, h*0.42,
fillColor=DARK_BG, strokeColor=None))
# Fluid (white) below
d.add(Rect(cx + 0.04*w, h*0.1, 0.34*w, fluid_y - h*0.1,
fillColor=OPAQUE, strokeColor=None))
# Straight horizontal air-fluid level
d.add(Line(cx + 0.04*w, fluid_y, cx + 0.38*w, fluid_y,
strokeColor=HIGHLIGHT, strokeWidth=2))
d.add(String(cx + 0.06*w, fluid_y + 4, "Horizontal\nair-fluid level",
fontSize=5.5, fillColor=HIGHLIGHT))
d.add(String(6, 10, "HYDROPNEUMOTHORAX", fontSize=7,
fillColor=colors.white, fontName="Helvetica-Bold"))
return d
# βββ PDF Content Data ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
XRAY_CASES = [
{
"title": "1. Normal Chest X-Ray",
"drawing_fn": draw_normal_cxr,
"mnemonic": "ABCDE: Airway, Bones, Cardiac, Diaphragm, Everything else",
"findings": [
"Trachea midline, bifurcates at carina (T4/T5 angle of Louis)",
"Cardiothoracic ratio (CTR) < 0.5 on PA film",
"Both lung fields clear, lung markings extend to periphery",
"Diaphragm: R dome at level of anterior 6th rib (higher than L)",
"Costophrenic angles sharp (acute)",
"Hilum: left is 0.5-1.5 cm higher than right",
],
"key_points": [
"PA film preferred over AP (less magnification)",
"Adequate inspiration: 5-6 anterior ribs visible above diaphragm",
"Mediastinum: superior (above carina) + inferior (below carina)",
],
"color": LIGHT_BLU,
},
{
"title": "2. Pneumothorax",
"drawing_fn": lambda w=280, h=260: draw_pneumothorax("R", False, w, h),
"mnemonic": "No lung markings beyond the PLEURAL LINE",
"findings": [
"Visible pleural line (visceral pleura) - sharp white line",
"Absent lung markings beyond pleural line",
"Lung collapses towards hilum",
"Dark (radiolucent) area between lung edge and chest wall",
"Ipsilateral diaphragm may be depressed",
"TENSION PTX: trachea + mediastinum shift AWAY from PTX side",
],
"key_points": [
"Measure at hilum level: small <2 cm, large >2 cm",
"Spontaneous: tall thin young males (blebs/bullae)",
"Secondary: COPD, asthma, TB, Pneumocystis",
"TENSION = EMERGENCY: clinical diagnosis, decompress FIRST",
"Rx: small/asymptomatic = observe; large = needle aspiration / chest drain",
],
"color": PINK_BG,
},
{
"title": "3. Tension Pneumothorax",
"drawing_fn": lambda w=280, h=260: draw_pneumothorax("R", True, w, h),
"mnemonic": "TENSION = Trachea shifts AWAY (opposite) from PTX side",
"findings": [
"All features of simple pneumothorax PLUS:",
"Tracheal deviation AWAY from PTX side",
"Mediastinal shift away from PTX side",
"Ipsilateral lung completely collapsed",
"Flattening / inversion of ipsilateral hemidiaphragm",
"Increased intercostal spaces on affected side",
],
"key_points": [
"CLINICAL diagnosis - DO NOT wait for X-ray",
"Immediate needle decompression: 2nd ICS, mid-clavicular line",
"Followed by chest drain: 5th ICS, mid-axillary line",
"Caused by: penetrating trauma, mechanical ventilation, iatrogenic",
],
"color": PINK_BG,
},
{
"title": "4. Pleural Effusion",
"drawing_fn": lambda w=280, h=260: draw_pleural_effusion("L", False, w, h),
"mnemonic": "FLUID: Flat (meniscus), Ribs hidden, Uniform opacity, Ipsilateral blunting, Diaphragm obscured",
"findings": [
"Blunting of costophrenic angle (needs >50 mL fluid)",
"Meniscus sign (concave upper border)",
"Homogeneous opacity at lung base",
"Obliteration of hemidiaphragm and costophrenic angle",
"Trachea/mediastinum shifts AWAY from large effusion",
"Lateral decubitus film: as little as 5 mL detected",
],
"key_points": [
"Transudate: CCF, nephrotic syndrome, cirrhosis, hypothyroid",
"Exudate (Lights criteria): malignancy, pneumonia, TB, PE",
"Massive effusion: >2/3 of hemithorax whitened with mediastinal shift",
"Subpulmonary effusion: high 'pseudo-diaphragm' peak laterally",
],
"color": LIGHT_BLU,
},
{
"title": "5. Consolidation (Pneumonia)",
"drawing_fn": lambda w=280, h=260: draw_consolidation("RLL", w, h),
"mnemonic": "Consolidation = White opacity + Air bronchograms (fluid/pus fills alveoli, bronchi remain air-filled)",
"findings": [
"Homogeneous/patchy opacity in a lobar or segmental distribution",
"Air bronchogram sign (dark bronchi visible within white opacity)",
"Loss of silhouette of adjacent structures",
"Lobar: sharp borders; segmental: ill-defined",
"No volume loss (unlike collapse)",
"Bulging fissure: Klebsiella (right upper lobe)",
],
"key_points": [
"RLL consolidation: obscures R hemidiaphragm (silhouette sign)",
"RML consolidation: obscures right heart border",
"LLL consolidation: obscures L hemidiaphragm",
"LUL consolidation: obscures left heart border + aortic knuckle",
"Causes: pneumococcus (lobar), Klebsiella, TB, atypical organisms",
],
"color": YELLOW_BG,
},
{
"title": "6. Lobar Collapse",
"drawing_fn": lambda w=280, h=260: draw_collapse("RUL", w, h),
"mnemonic": "Collapse = WHITE opacity + Volume LOSS + Structures SHIFT TOWARDS",
"findings": [
"Opacity (white) + VOLUME LOSS on affected side",
"Compensatory hyperinflation of other lobes",
"Mediastinum / trachea pulled TOWARDS collapse",
"Elevation of ipsilateral hemidiaphragm",
"Crowding of ipsilateral ribs",
],
"key_points": [
"RUL: trachea deviates right, upward displacement of hilum",
"RML: loss of right heart border (silhouette sign)",
"RLL: displaced right hilum downward, obscures R hemidiaphragm",
"LLL: 'Sail sign' / triangular opacity behind heart",
"LUL: 'Veil opacity' over left lung field",
"Most common cause: mucus plugging, endobronchial tumor",
],
"color": ORANGE_BG,
},
{
"title": "7. Pulmonary Oedema",
"drawing_fn": draw_pulm_oedema,
"mnemonic": "ABCDE: Alveolar oedema, Bat-wing, Cardiomegaly, Diversion (upper lobe), Effusion/Kerley B",
"findings": [
"Cardiomegaly (CTR >0.5)",
"Upper lobe blood diversion (vessels to upper zones bigger)",
"Kerley B lines (horizontal lines at lung bases, 1-2 cm long)",
"Perihilar / bat-wing opacity (bilateral symmetrical)",
"Bilateral pleural effusions",
"Fluid in interlobar fissures",
],
"key_points": [
"Cardiogenic: LVF, mitral stenosis, CCF - classical bat-wing",
"Non-cardiogenic (ARDS): bilateral patchy, no cardiomegaly, no upper lobe diversion",
"Kerley A lines: longer lines extending to hilum",
"Kerley B lines: septal lines at costophrenic angles",
],
"color": ORANGE_BG,
},
{
"title": "8. Miliary Tuberculosis",
"drawing_fn": draw_miliary_tb,
"mnemonic": "MILLET SEEDS scattered throughout BOTH lung fields",
"findings": [
"Bilateral, diffuse, uniformly distributed fine nodules",
"Nodule size: 1-3 mm (millet-seed size)",
"Uniform density throughout both lung fields",
"No lobar preference",
"Hilum may be enlarged (lymphadenopathy)",
],
"key_points": [
"Haematogenous spread of Mycobacterium tuberculosis",
"Seen in immunocompromised patients",
"Sputum AFB often negative; diagnose by bone marrow / liver biopsy",
"Also seen in: miliary histoplasmosis, sarcoidosis, metastases (differentiate by size/distribution)",
],
"color": YELLOW_BG,
},
{
"title": "9. Post-Primary TB (Fibrocavitary)",
"drawing_fn": draw_apical_fibrocavitary,
"mnemonic": "APICAL + POSTERIOR = TB territory (segment 1,2,6)",
"findings": [
"Unilateral or bilateral apical / upper zone opacity",
"Cavitation within the opacity (ring shadow)",
"Fibrotic streaks pulling hilum upward",
"Trachea / mediastinum may shift towards lesion",
"Satellite nodules around main lesion",
"Calcified Ghon focus / Ranke complex (healed primary)",
],
"key_points": [
"Ghon focus = calcified primary lesion (mid zone)",
"Ranke complex = Ghon + calcified hilar LN",
"Simon foci = apical scars from haematogenous seeding",
"Endogenous reactivation = post-primary TB (apical)",
],
"color": YELLOW_BG,
},
{
"title": "10. Bilateral Hilar Lymphadenopathy",
"drawing_fn": draw_hilar_lymphadenopathy,
"mnemonic": "BHL = Sarcoidosis #1 cause; also TB, lymphoma",
"findings": [
"Bilateral enlargement of hilar shadows",
"Lobulated / potato-shaped hilar masses",
"Normal lung fields in early stage (Stage I sarcoidosis)",
"Parenchymal nodules added in Stage II",
"DDx: TB, lymphoma, silicosis, malignancy",
],
"key_points": [
"Sarcoidosis stages: 0=normal; I=BHL; II=BHL+parenchyma; III=parenchyma only; IV=fibrosis",
"Eggshell calcification of hilar LN = silicosis / sarcoidosis",
"ACE levels elevated in sarcoidosis",
"TB: more often unilateral, asymmetric",
],
"color": LIGHT_BLU,
},
{
"title": "11. Lung Abscess",
"drawing_fn": draw_lung_abscess,
"mnemonic": "CAVITY with AIR-FLUID LEVEL + THICK WALL = Abscess",
"findings": [
"Round or oval opacity with thick irregular wall",
"Central lucency (cavity) with air-fluid level",
"Air-fluid level is horizontal (not meniscoid like empyema)",
"Usually in posterior segments: RUL (S2), RLL (S6)",
"No volume loss (distinguishes from collapse)",
],
"key_points": [
"Cause: aspiration (#1), Staphylococcus, Klebsiella, anaerobes",
"Aspiration in dependent segments: posterior S2 (erect) or S6 (supine)",
"Empyema: lenticular shape, obtuse angle, moves with posture",
"Abscess vs Empyema: spherical vs D-shaped, acute vs obtuse angle with chest wall",
"Rx: prolonged antibiotics; surgical drainage if refractory",
],
"color": ORANGE_BG,
},
{
"title": "12. Pneumoperitoneum",
"drawing_fn": draw_pneumoperitoneum,
"mnemonic": "GAS under DIAPHRAGM (erect CXR) = Perforated viscus until proven otherwise",
"findings": [
"Gas crescent under right hemidiaphragm (most common)",
"Can be bilateral if large perforation",
"Best seen on erect CXR (patient upright for 5-10 min)",
"Gas collects under diaphragm as it is the highest point",
],
"key_points": [
"Commonest cause: perforated peptic ulcer (duodenal/gastric)",
"Others: perforated appendix, diverticulitis, sigmoid volvulus",
"Gas under LEFT diaphragm: splenic flexure perforation / gastric",
"If erect not possible: left lateral decubitus shows free gas over liver",
"Rigler's sign: gas on both sides of bowel wall (large pneumoperitoneum)",
],
"color": PINK_BG,
},
{
"title": "13. Hydropneumothorax",
"drawing_fn": draw_hydropneumothorax,
"mnemonic": "STRAIGHT (horizontal) air-fluid level = Hydropneumothorax (both air AND fluid in pleural space)",
"findings": [
"STRAIGHT horizontal air-fluid level in pleural space",
"Dark air above the fluid level",
"White fluid opacity below",
"Moves with patient position (fluid shifts)",
"Ipsilateral lung compressed",
],
"key_points": [
"Causes: trauma (haemopneumothorax), iatrogenic, bronchopleural fistula, TB",
"Distinguish from abscess: abscess air-fluid level is within lung parenchyma",
"Horizontal air-fluid level (straight) = pathognomonic",
"Rx: chest drain (tube thoracostomy)",
],
"color": LIGHT_BLU,
},
{
"title": "14. Cardiomegaly",
"drawing_fn": draw_cardiomegaly,
"mnemonic": "CTR > 0.5 on PA film = Cardiomegaly",
"findings": [
"Cardiothoracic ratio (CTR) > 0.5 on PA film",
"Enlarged cardiac silhouette",
"Right heart border: right atrium; Left heart border: LV",
"Pulmonary plethora (dilated pulmonary vessels) in left-to-right shunts",
"Upper lobe venous engorgement in LVF",
],
"key_points": [
"CTR = widest cardiac diameter / widest thoracic diameter",
"Globular/flask-shaped = pericardial effusion",
"Boot-shaped (coeur-en-sabot) = Tetralogy of Fallot",
"Box-shaped = TAPVC",
"CTR >0.5 on AP film is normal (AP magnifies heart)",
],
"color": LIGHT_BLU,
},
{
"title": "15. Sarcoidosis (Stage II)",
"drawing_fn": draw_sarcoidosis,
"mnemonic": "BHL + Parenchymal nodules = Stage II Sarcoidosis",
"findings": [
"Bilateral hilar lymphadenopathy (lobulated hila)",
"Bilateral parenchymal nodules or reticulonodular pattern",
"Upper and mid zone predominance",
"May show fibrosis in Stage IV",
"Rarely cavitates (unlike TB)",
],
"key_points": [
"Non-caseating granulomas histologically",
"ACE elevated, hypercalcaemia",
"Stages: I=BHL, II=BHL+parenchyma, III=parenchyma only, IV=fibrosis",
"Spontaneous remission in 60-70% (Stage I/II)",
"Rx: steroids if symptomatic or deteriorating",
],
"color": YELLOW_BG,
},
]
# Extra quick-reference summary table data
SILHOUETTE_SIGN = [
["Structure Obscured", "Collapse/Consolidation Lobe"],
["Right heart border", "Right middle lobe (RML)"],
["Left heart border", "Lingula / Left upper lobe (LUL)"],
["Right hemidiaphragm", "Right lower lobe (RLL)"],
["Left hemidiaphragm", "Left lower lobe (LLL)"],
["Aortic knuckle", "Left upper lobe (LUL) / Lingula"],
["Descending aorta", "Left lower lobe (LLL)"],
]
TRACHEAL_SHIFT_TABLE = [
["Condition", "Trachea Shift"],
["Tension Pneumothorax", "AWAY from affected side"],
["Massive Pleural Effusion", "AWAY from affected side"],
["Lobar Collapse / Fibrosis", "TOWARDS affected side"],
["Pneumonectomy", "TOWARDS affected side"],
["Neck mass / goitre", "Lateral displacement"],
]
# βββ Build PDF βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def make_pdf(output_path):
doc = SimpleDocTemplate(
output_path,
pagesize=A4,
leftMargin=1.8*cm,
rightMargin=1.8*cm,
topMargin=1.8*cm,
bottomMargin=1.8*cm,
title="Respiratory X-Rays for MBBS",
)
styles = getSampleStyleSheet()
# Custom styles
title_style = ParagraphStyle(
"DocTitle",
fontSize=22,
fontName="Helvetica-Bold",
textColor=colors.white,
alignment=TA_CENTER,
spaceAfter=4,
)
subtitle_style = ParagraphStyle(
"DocSubtitle",
fontSize=13,
fontName="Helvetica",
textColor=colors.HexColor("#CCE8FF"),
alignment=TA_CENTER,
spaceAfter=4,
)
section_title = ParagraphStyle(
"SectionTitle",
fontSize=13,
fontName="Helvetica-Bold",
textColor=colors.white,
spaceBefore=4, spaceAfter=4,
)
finding_style = ParagraphStyle(
"Finding",
fontSize=9,
fontName="Helvetica",
textColor=TEXT_DARK,
leftIndent=10,
spaceAfter=2,
leading=13,
)
bullet_style = ParagraphStyle(
"Bullet",
fontSize=9,
fontName="Helvetica",
textColor=TEXT_DARK,
leftIndent=16,
firstLineIndent=-10,
spaceAfter=2,
leading=13,
)
key_style = ParagraphStyle(
"KeyPoint",
fontSize=9,
fontName="Helvetica-Bold",
textColor=colors.HexColor("#1A4A1A"),
leftIndent=10,
spaceAfter=2,
leading=13,
)
mnemonic_style = ParagraphStyle(
"Mnemonic",
fontSize=9.5,
fontName="Helvetica-BoldOblique",
textColor=colors.HexColor("#6B2400"),
leftIndent=8,
spaceAfter=3,
)
label_style = ParagraphStyle(
"Label",
fontSize=8.5,
fontName="Helvetica-Bold",
textColor=NAVY,
spaceAfter=2,
)
table_header = ParagraphStyle(
"THead",
fontSize=9,
fontName="Helvetica-Bold",
textColor=colors.white,
alignment=TA_CENTER,
)
small_style = ParagraphStyle(
"Small",
fontSize=8,
fontName="Helvetica",
textColor=TEXT_MED,
alignment=TA_CENTER,
)
story = []
# ββ Cover Page ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Header banner
cover_table = Table(
[[Paragraph("π« RESPIRATORY X-RAYS", title_style)],
[Paragraph("Complete MBBS Exam Guide", subtitle_style)],
[Paragraph("Quick Reference Β· Visual Learning Β· Exam Ready", subtitle_style)],
],
colWidths=[PAGE_W]
)
cover_table.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, -1), NAVY),
("TOPPADDING", (0, 0), (-1, -1), 10),
("BOTTOMPADDING", (0, 0), (-1, -1), 10),
("LEFTPADDING", (0, 0), (-1, -1), 14),
("RIGHTPADDING", (0, 0), (-1, -1), 14),
("ROUNDEDCORNERS", (0, 0), (-1, -1), [8, 8, 8, 8]),
]))
story.append(cover_table)
story.append(Spacer(1, 0.4*cm))
# Topic list on cover
topics = [
"1. Normal CXR", "2. Pneumothorax", "3. Tension Pneumothorax",
"4. Pleural Effusion", "5. Consolidation (Pneumonia)", "6. Lobar Collapse",
"7. Pulmonary Oedema", "8. Miliary TB", "9. Post-Primary TB",
"10. Bilateral Hilar LAP", "11. Lung Abscess", "12. Pneumoperitoneum",
"13. Hydropneumothorax", "14. Cardiomegaly", "15. Sarcoidosis",
]
n_col = 3
rows = []
for i in range(0, len(topics), n_col):
row = topics[i:i+n_col]
while len(row) < n_col:
row.append("")
rows.append([Paragraph(t, ParagraphStyle("tt", fontSize=8.5,
fontName="Helvetica", textColor=NAVY)) for t in row])
toc_table = Table(rows, colWidths=[PAGE_W/3]*3)
toc_table.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, -1), LIGHT_BLU),
("GRID", (0, 0), (-1, -1), 0.4, colors.HexColor("#BBDDEE")),
("TOPPADDING", (0, 0), (-1, -1), 5),
("BOTTOMPADDING", (0, 0), (-1, -1), 5),
("LEFTPADDING", (0, 0), (-1, -1), 8),
]))
story.append(toc_table)
story.append(Spacer(1, 0.3*cm))
# Approach box
approach_data = [
[Paragraph("π SYSTEMATIC APPROACH TO CXR (ABCDE)", ParagraphStyle(
"ah", fontSize=10, fontName="Helvetica-Bold",
textColor=NAVY))],
[Paragraph(
"<b>A</b>irway: Trachea midline? Carina angle <70Β°? "
"<b>B</b>ones & soft tissue: Ribs, clavicles, scapulae, spine "
"<b>C</b>ardiac: Size (CTR), borders, shape "
"<b>D</b>iaphragm: Level, domes, costophrenic angles "
"<b>E</b>verything else: Lung fields, hila, mediastinum, pleura, foreign bodies",
ParagraphStyle("ab", fontSize=8.5, fontName="Helvetica",
textColor=TEXT_DARK, leading=14))],
]
approach_t = Table(approach_data, colWidths=[PAGE_W])
approach_t.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, -1), GREEN_BG),
("TOPPADDING", (0, 0), (-1, -1), 6),
("BOTTOMPADDING", (0, 0), (-1, -1), 6),
("LEFTPADDING", (0, 0), (-1, -1), 10),
("BOX", (0, 0), (-1, -1), 1, TEAL),
]))
story.append(approach_t)
story.append(PageBreak())
# ββ Individual X-ray Pages ββββββββββββββββββββββββββββββββββββββββββββ
for case in XRAY_CASES:
bg = case.get("color", LIGHT_BLU)
# Section header
hdr = Table(
[[Paragraph(case["title"], section_title)]],
colWidths=[PAGE_W]
)
hdr.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, -1), NAVY),
("TOPPADDING", (0, 0), (-1, -1), 7),
("BOTTOMPADDING", (0, 0), (-1, -1), 7),
("LEFTPADDING", (0, 0), (-1, -1), 12),
]))
story.append(hdr)
story.append(Spacer(1, 0.15*cm))
# Build drawing
drw = case["drawing_fn"](w=270, h=240)
# Findings list
findings_paras = [Paragraph("π <b>X-RAY FINDINGS:</b>", label_style)]
for f in case["findings"]:
findings_paras.append(
Paragraph(f"β’ {f}", bullet_style))
# Mnemonic box
mnem = Table(
[[Paragraph(f"π‘ {case['mnemonic']}", mnemonic_style)]],
colWidths=[PAGE_W * 0.58]
)
mnem.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, -1), YELLOW_BG),
("BOX", (0, 0), (-1, -1), 1, colors.HexColor("#DDAA00")),
("TOPPADDING", (0, 0), (-1, -1), 5),
("BOTTOMPADDING", (0, 0), (-1, -1), 5),
("LEFTPADDING", (0, 0), (-1, -1), 8),
]))
# Key points
key_paras = [Paragraph("π <b>KEY POINTS:</b>", label_style)]
for k in case["key_points"]:
key_paras.append(Paragraph(f"β€ {k}", key_style))
# Layout: drawing LEFT, findings RIGHT
findings_cell = findings_paras + [Spacer(1, 0.15*cm)] + [mnem]
left_col = [[drw]]
right_col = [findings_cell]
main_table = Table(
[[drw, findings_paras + [Spacer(1, 0.1*cm), mnem]]],
colWidths=[280, PAGE_W - 290]
)
main_table.setStyle(TableStyle([
("VALIGN", (0, 0), (-1, -1), "TOP"),
("TOPPADDING", (0, 0), (-1, -1), 4),
("LEFTPADDING", (0, 0), (-1, -1), 4),
("RIGHTPADDING", (0, 0), (-1, -1), 4),
]))
story.append(main_table)
story.append(Spacer(1, 0.1*cm))
# Key points in full-width box
kp_table = Table(
[[[kp for kp in key_paras]]],
colWidths=[PAGE_W]
)
kp_table.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, -1), bg),
("TOPPADDING", (0, 0), (-1, -1), 5),
("BOTTOMPADDING", (0, 0), (-1, -1), 5),
("LEFTPADDING", (0, 0), (-1, -1), 10),
("BOX", (0, 0), (-1, -1), 0.5, NAVY),
]))
story.append(kp_table)
story.append(Spacer(1, 0.3*cm))
story.append(HRFlowable(width=PAGE_W, thickness=0.5,
color=NAVY, spaceAfter=0.2*cm))
# ββ Quick Reference Tables ββββββββββββββββββββββββββββββββββββββββββββ
story.append(PageBreak())
ref_hdr = Table(
[[Paragraph("β‘ QUICK REFERENCE TABLES", section_title)]],
colWidths=[PAGE_W]
)
ref_hdr.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, -1), TEAL),
("TOPPADDING", (0, 0), (-1, -1), 8),
("BOTTOMPADDING", (0, 0), (-1, -1), 8),
("LEFTPADDING", (0, 0), (-1, -1), 12),
]))
story.append(ref_hdr)
story.append(Spacer(1, 0.3*cm))
# Silhouette sign table
story.append(Paragraph("SILHOUETTE SIGN", ParagraphStyle(
"st", fontSize=11, fontName="Helvetica-Bold", textColor=NAVY)))
story.append(Spacer(1, 0.15*cm))
sil_data = []
for i, row in enumerate(SILHOUETTE_SIGN):
if i == 0:
sil_data.append([Paragraph(c, table_header) for c in row])
else:
sil_data.append([Paragraph(c, ParagraphStyle(
"sc", fontSize=9, fontName="Helvetica",
textColor=TEXT_DARK, alignment=TA_CENTER)) for c in row])
sil_t = Table(sil_data, colWidths=[PAGE_W*0.45, PAGE_W*0.55])
sil_t.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, 0), NAVY),
("BACKGROUND", (0, 1), (-1, -1), LIGHT_BLU),
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, LIGHT_BLU]),
("GRID", (0, 0), (-1, -1), 0.5, colors.HexColor("#AACCDD")),
("TOPPADDING", (0, 0), (-1, -1), 5),
("BOTTOMPADDING", (0, 0), (-1, -1), 5),
("LEFTPADDING", (0, 0), (-1, -1), 8),
("ALIGN", (0, 0), (-1, -1), "CENTER"),
]))
story.append(sil_t)
story.append(Spacer(1, 0.4*cm))
# Tracheal shift table
story.append(Paragraph("TRACHEAL / MEDIASTINAL SHIFT", ParagraphStyle(
"st2", fontSize=11, fontName="Helvetica-Bold", textColor=NAVY)))
story.append(Spacer(1, 0.15*cm))
tr_data = []
for i, row in enumerate(TRACHEAL_SHIFT_TABLE):
if i == 0:
tr_data.append([Paragraph(c, table_header) for c in row])
else:
c_bg = RED_C if "AWAY" in row[1] else TEAL
tr_data.append([
Paragraph(row[0], ParagraphStyle("tc0", fontSize=9,
fontName="Helvetica", textColor=TEXT_DARK)),
Paragraph(row[1], ParagraphStyle("tc1", fontSize=9,
fontName="Helvetica-Bold",
textColor=c_bg)),
])
tr_t = Table(tr_data, colWidths=[PAGE_W*0.55, PAGE_W*0.45])
tr_t.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, 0), NAVY),
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, LIGHT_BLU]),
("GRID", (0, 0), (-1, -1), 0.5, colors.HexColor("#AACCDD")),
("TOPPADDING", (0, 0), (-1, -1), 5),
("BOTTOMPADDING", (0, 0), (-1, -1), 5),
("LEFTPADDING", (0, 0), (-1, -1), 8),
]))
story.append(tr_t)
story.append(Spacer(1, 0.4*cm))
# White vs Dark opacities box
wvd_data = [
[Paragraph("WHITE OPACITY (Increased density)", ParagraphStyle(
"wh", fontSize=9.5, fontName="Helvetica-Bold",
textColor=colors.white, alignment=TA_CENTER)),
Paragraph("DARK / LUCENT (Decreased density)", ParagraphStyle(
"dk", fontSize=9.5, fontName="Helvetica-Bold",
textColor=colors.white, alignment=TA_CENTER))],
[Paragraph(
"β’ Consolidation (pus/fluid in alveoli)\n"
"β’ Collapse (atelectasis)\n"
"β’ Pleural effusion\n"
"β’ Tumour / mass\n"
"β’ Elevated hemidiaphragm\n"
"β’ Cardiomegaly",
ParagraphStyle("wl", fontSize=8.5, fontName="Helvetica",
textColor=TEXT_DARK, leading=14)),
Paragraph(
"β’ Pneumothorax (air in pleural space)\n"
"β’ Emphysema / bullae\n"
"β’ Pneumoperitoneum\n"
"β’ Hydropneumothorax (air part)\n"
"β’ Cavities (abscess, TB)\n"
"β’ Cystic lesions",
ParagraphStyle("dl", fontSize=8.5, fontName="Helvetica",
textColor=TEXT_DARK, leading=14))],
]
wvd_t = Table(wvd_data, colWidths=[PAGE_W*0.5, PAGE_W*0.5])
wvd_t.setStyle(TableStyle([
("BACKGROUND", (0, 0), (0, 0), colors.HexColor("#444444")),
("BACKGROUND", (1, 0), (1, 0), colors.HexColor("#1A3A6B")),
("BACKGROUND", (0, 1), (0, 1), colors.HexColor("#F5F5F0")),
("BACKGROUND", (1, 1), (1, 1), colors.HexColor("#EEF3FA")),
("GRID", (0, 0), (-1, -1), 0.5, colors.HexColor("#AAAAAA")),
("TOPPADDING", (0, 0), (-1, -1), 6),
("BOTTOMPADDING", (0, 0), (-1, -1), 6),
("LEFTPADDING", (0, 0), (-1, -1), 10),
("VALIGN", (0, 1), (-1, 1), "TOP"),
]))
story.append(Paragraph("WHITE vs. DARK ON CXR", ParagraphStyle(
"wvd", fontSize=11, fontName="Helvetica-Bold", textColor=NAVY)))
story.append(Spacer(1, 0.15*cm))
story.append(wvd_t)
story.append(Spacer(1, 0.4*cm))
# Footer note
story.append(HRFlowable(width=PAGE_W, thickness=0.5, color=NAVY))
story.append(Spacer(1, 0.1*cm))
story.append(Paragraph(
"Schematic X-ray diagrams for educational use. Findings are representative patterns for MBBS exam preparation.",
small_style))
doc.build(story)
print(f"β
PDF saved: {output_path}")
if __name__ == "__main__":
import os
os.makedirs("/tmp/workspace/respiratory-xray-pdf", exist_ok=True)
make_pdf("/tmp/workspace/respiratory-xray-pdf/Respiratory_Xrays_MBBS.pdf")
cd /tmp/workspace/respiratory-xray-pdf && python generate_pdf.py 2>&1
python3 -c "import reportlab.graphics.shapes as s; print([x for x in dir(s) if 'Arc' in x or 'arc' in x or 'Path' in x])"
~/respiratory-xray-pdf/generate_pdf2.py
"""
Respiratory X-rays for MBBS Exams - PDF Generator
Uses matplotlib to create schematic X-ray drawings, then reportlab to assemble the PDF.
"""
import os, math, random
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.patches import FancyArrowPatch, Arc, FancyBboxPatch, Ellipse, Wedge, Rectangle
from matplotlib.patheffects import withStroke
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import cm, mm
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, Image, PageBreak
)
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_JUSTIFY
W, H = A4
PAGE_W = W - 3.6*cm
NAVY = colors.HexColor("#1B3A6B")
TEAL = colors.HexColor("#1A7A7A")
LIGHT_BLU = colors.HexColor("#E8F4FD")
YELLOW_BG = colors.HexColor("#FFFBE6")
GREEN_BG = colors.HexColor("#E8F8E8")
ORANGE_BG = colors.HexColor("#FFF3E0")
PINK_BG = colors.HexColor("#FDE8E8")
TEXT_DARK = colors.HexColor("#1A1A1A")
TEXT_MED = colors.HexColor("#333333")
RED_C = colors.HexColor("#CC3322")
IMG_DIR = "/tmp/workspace/respiratory-xray-pdf/images"
os.makedirs(IMG_DIR, exist_ok=True)
# βββ Matplotlib X-ray drawing helpers βββββββββββββββββββββββββββββββββββββββ
BG = "#080808"
BONE = "#D8D5C0"
RIB_C = "#A8A490"
AIR = "#111828"
OPAQ = "#C8C8B0"
HEART = "#5A4A4A"
HILAR = "#707060"
GOLD = "#FFD700"
RED = "#DD3322"
WHITE = "#F0EEE0"
SPINE = "#D0CDB8"
def fig_base(ax, w=5.2, h=4.6):
"""Draw CXR base template on ax. Returns cx, cy centre coords."""
ax.set_xlim(0, w)
ax.set_ylim(0, h)
ax.set_aspect("equal")
ax.axis("off")
ax.set_facecolor(BG)
cx = w / 2
# Spine
spine = mpatches.FancyBboxPatch((cx - 0.22, 0.15), 0.44, 3.2,
boxstyle="round,pad=0.02", facecolor=SPINE, edgecolor="none", zorder=2)
ax.add_patch(spine)
# Ribs - 6 pairs
for i in range(6):
y0 = 3.5 - i * 0.48
# LEFT rib arc
theta = np.linspace(np.pi*0.05, np.pi*0.95, 40)
rx_l, ry_l = 1.8, 0.32
x_rib = cx - 0.05 - rx_l * np.cos(theta)
y_rib = y0 + ry_l * np.sin(theta)
ax.plot(x_rib, y_rib, color=RIB_C, lw=1.4, zorder=3)
# RIGHT rib arc
x_ribr = cx + 0.05 + rx_l * np.cos(np.pi - theta)
ax.plot(x_ribr, y_rib, color=RIB_C, lw=1.4, zorder=3)
# Clavicles
ax.plot([cx-0.1, cx-1.5], [3.95, 4.15], color=BONE, lw=2.2, zorder=3)
ax.plot([cx+0.1, cx+1.5], [3.95, 4.15], color=BONE, lw=2.2, zorder=3)
# Trachea
trachea = mpatches.FancyBboxPatch((cx-0.12, 2.92), 0.24, 1.05,
boxstyle="round,pad=0.02", facecolor="#1A1A3A", edgecolor=HILAR, linewidth=0.5, zorder=4)
ax.add_patch(trachea)
# Carina
ax.plot([cx, cx-0.28], [2.92, 2.72], color=BONE, lw=1.2, zorder=4)
ax.plot([cx, cx+0.28], [2.92, 2.72], color=BONE, lw=1.2, zorder=4)
# Diaphragm domes
theta_d = np.linspace(0, np.pi, 50)
# Right dome
xd = 0.68 + 1.1 * np.cos(theta_d)
yd = 0.72 + 0.52 * np.sin(theta_d)
ax.plot(xd, yd, color=BONE, lw=2, zorder=5)
# Left dome
xl = cx + 0.05 + 0.95 * np.cos(theta_d)
yl = 0.65 + 0.48 * np.sin(theta_d)
ax.plot(xl, yl, color=BONE, lw=2, zorder=5)
# Heart silhouette
heart = Ellipse((cx + 0.14, 1.95), 1.6, 2.05,
facecolor=HEART, edgecolor="none", zorder=5)
ax.add_patch(heart)
# Right lung (aerated)
rl = mpatches.FancyBboxPatch((cx+0.18, 0.85), 1.6, 2.42,
boxstyle="round,pad=0.15", facecolor=AIR, edgecolor="none", zorder=6)
ax.add_patch(rl)
# Left lung
ll = mpatches.FancyBboxPatch((cx-1.78, 0.85), 1.42, 2.42,
boxstyle="round,pad=0.15", facecolor=AIR, edgecolor="none", zorder=6)
ax.add_patch(ll)
# Hila
ax.add_patch(Ellipse((cx+0.4, 2.45), 0.28, 0.38, facecolor=HILAR, edgecolor="none", zorder=7))
ax.add_patch(Ellipse((cx-0.4, 2.55), 0.24, 0.34, facecolor=HILAR, edgecolor="none", zorder=7))
return cx
def add_label(ax, text, x, y, color=GOLD, fontsize=7, zorder=15):
ax.text(x, y, text, color=color, fontsize=fontsize,
fontweight="bold", zorder=zorder,
bbox=dict(boxstyle="round,pad=0.15", facecolor="#00000088", edgecolor="none"))
def save_fig(fig, name):
path = f"{IMG_DIR}/{name}.png"
fig.savefig(path, dpi=130, bbox_inches="tight",
facecolor=BG, edgecolor="none", pad_inches=0.05)
plt.close(fig)
return path
# βββ Individual condition drawings ββββββββββββββββββββββββββββββββββββββββββ
def draw_normal():
fig, ax = plt.subplots(figsize=(5.2, 4.6))
fig.patch.set_facecolor(BG)
cx = fig_base(ax)
add_label(ax, "NORMAL CXR", 0.1, 0.15)
add_label(ax, "Trachea\nmidline", cx-0.5, 3.8, color="#AADDFF")
add_label(ax, "Sharp CP\nangle", cx+1.5, 0.5, color="#AADDFF")
ax.annotate("", xy=(cx+0.05, 0.72), xytext=(cx+1.3, 0.62),
arrowprops=dict(arrowstyle="->", color=GOLD, lw=1.2))
return save_fig(fig, "normal")
def draw_pneumothorax(side="R", tension=False):
fig, ax = plt.subplots(figsize=(5.2, 4.6))
fig.patch.set_facecolor(BG)
cx = fig_base(ax)
sign = 1 if side == "R" else -1
if side == "R":
# Collapsed right lung (smaller, shifted medially)
col = mpatches.FancyBboxPatch((cx+0.18, 1.0), 0.9, 1.8,
boxstyle="round,pad=0.1", facecolor=HILAR, edgecolor=BONE, lw=1.2, zorder=8)
ax.add_patch(col)
# Dark air gap
gap = mpatches.FancyBboxPatch((cx+1.1, 0.85), 0.68, 2.42,
boxstyle="square,pad=0", facecolor=BG, edgecolor="none", zorder=8)
ax.add_patch(gap)
# Pleural line
ax.plot([cx+1.1, cx+1.1], [0.85, 3.27], color=GOLD, lw=2, zorder=9)
add_label(ax, "Pleural\nline", cx+1.12, 2.5)
add_label(ax, "Air gap\n(no lung\nmarkings)", cx+1.4, 1.8)
else:
col = mpatches.FancyBboxPatch((cx-1.6, 1.0), 0.9, 1.8,
boxstyle="round,pad=0.1", facecolor=HILAR, edgecolor=BONE, lw=1.2, zorder=8)
ax.add_patch(col)
gap = mpatches.FancyBboxPatch((cx-2.2, 0.85), 0.6, 2.42,
boxstyle="square,pad=0", facecolor=BG, edgecolor="none", zorder=8)
ax.add_patch(gap)
ax.plot([cx-1.6, cx-1.6], [0.85, 3.27], color=GOLD, lw=2, zorder=9)
add_label(ax, "Pleural\nline", cx-2.2, 2.5)
if tension:
# Trachea shifted AWAY (to opposite side)
shift = -sign * 0.28
trachea_t = mpatches.FancyBboxPatch((cx+shift-0.12, 2.92), 0.24, 1.05,
boxstyle="round,pad=0.02", facecolor="#AA0000",
edgecolor="#FF4444", linewidth=1, zorder=10)
ax.add_patch(trachea_t)
add_label(ax, "Trachea SHIFTED\n(away from PTX)", cx+shift-0.5, 3.9, color=RED)
lbl = f"TENSION PNEUMOTHORAX ({side})"
else:
lbl = f"PNEUMOTHORAX ({side})"
add_label(ax, lbl, 0.1, 0.15)
return save_fig(fig, f"pneumothorax_{'tension_' if tension else ''}{side}")
def draw_pleural_effusion(side="L", massive=False):
fig, ax = plt.subplots(figsize=(5.2, 4.6))
fig.patch.set_facecolor(BG)
cx = fig_base(ax)
if side == "L":
ex, ew = cx-1.78, 1.42
else:
ex, ew = cx+0.18, 1.6
eff_h = 1.8 if massive else 1.0
# Fluid opacity
eff = mpatches.FancyBboxPatch((ex, 0.72), ew, eff_h,
boxstyle="round,pad=0.05", facecolor=OPAQ, edgecolor="none", zorder=8)
ax.add_patch(eff)
# Meniscus (concave upper border)
theta_m = np.linspace(0, np.pi, 60)
mx = ex + ew/2 + (ew/2+0.05) * np.cos(theta_m)
my = (0.72 + eff_h) - 0.18 * np.sin(theta_m)
ax.fill_between(mx, 0.72+eff_h-0.18, my, color=WHITE, alpha=0.5, zorder=9)
ax.plot(mx, my, color=WHITE, lw=1.8, zorder=9)
mid_x = ex + ew/2
add_label(ax, "Fluid\nOpacity", mid_x - 0.2, 0.72 + eff_h/2, color="#222222")
add_label(ax, "Meniscus\nsign", mid_x - 0.3, 0.72 + eff_h + 0.15)
if massive:
# Mediastinal shift
shift = 0.28 if side == "L" else -0.28
trachea_t = mpatches.FancyBboxPatch((cx+shift-0.12, 2.92), 0.24, 1.05,
boxstyle="round,pad=0.02", facecolor="#553300",
edgecolor=HILAR, linewidth=0.5, zorder=10)
ax.add_patch(trachea_t)
add_label(ax, "Mediastinum\nshifts AWAY", cx+shift-0.6, 3.9, color=GOLD)
lbl = f"PLEURAL EFFUSION ({side})" + (" - MASSIVE" if massive else "")
add_label(ax, lbl, 0.1, 0.15)
return save_fig(fig, f"pleural_effusion_{side}{'_massive' if massive else ''}")
def draw_consolidation(lobe="RLL"):
fig, ax = plt.subplots(figsize=(5.2, 4.6))
fig.patch.set_facecolor(BG)
cx = fig_base(ax)
regions = {
"RLL": (cx+0.2, 0.82, 1.52, 1.15),
"RUL": (cx+0.2, 2.28, 1.52, 0.98),
"LLL": (cx-1.72, 0.82, 1.38, 1.15),
"RML": (cx+0.2, 1.55, 1.52, 0.72),
}
rx, ry, rw, rh = regions.get(lobe, regions["RLL"])
# Consolidation
con = mpatches.FancyBboxPatch((rx, ry), rw, rh,
boxstyle="round,pad=0.08", facecolor=OPAQ, edgecolor="none", zorder=8)
ax.add_patch(con)
# Air bronchograms (dark lines through white opacity)
for j in range(3):
bx = rx + rw * (0.2 + j * 0.28)
ax.plot([bx, bx], [ry+0.1, ry+rh-0.1],
color=BG, lw=1.2, zorder=9)
add_label(ax, f"CONSOLIDATION ({lobe})", 0.1, 0.15)
add_label(ax, "Air\nbronchogram", rx + rw*0.3, ry + rh + 0.08, color=GOLD)
ax.annotate("", xy=(rx + rw*0.48, ry + rh - 0.1),
xytext=(rx + rw*0.6, ry + rh + 0.35),
arrowprops=dict(arrowstyle="->", color=GOLD, lw=1.2))
return save_fig(fig, f"consolidation_{lobe}")
def draw_collapse(lobe="RUL"):
fig, ax = plt.subplots(figsize=(5.2, 4.6))
fig.patch.set_facecolor(BG)
cx = fig_base(ax)
if lobe == "RUL":
# Wedge opacity at right apex + trachea deviated right
pts = np.array([[cx+0.2, 3.27], [cx+1.78, 3.27], [cx+1.0, 2.5]])
tri = plt.Polygon(pts, facecolor=OPAQ, edgecolor="none", zorder=8)
ax.add_patch(tri)
# Trachea deviated right
tr2 = mpatches.FancyBboxPatch((cx+0.08, 2.92), 0.24, 1.05,
boxstyle="round,pad=0.02", facecolor="#1A1A3A",
edgecolor=HILAR, lw=0.5, zorder=10)
ax.add_patch(tr2)
add_label(ax, "Wedge\nOpacity", cx+0.8, 2.9)
add_label(ax, "Trachea\ndeviated β", cx+0.5, 4.2, color=GOLD)
elif lobe == "LLL":
# Triangular opacity behind heart (sail sign)
pts = np.array([[cx-0.2, 0.82], [cx-0.85, 0.82], [cx-0.52, 1.85]])
tri = plt.Polygon(pts, facecolor=OPAQ, edgecolor="none", zorder=9)
ax.add_patch(tri)
add_label(ax, "Sail sign\n(behind heart)", cx-1.0, 1.3)
elif lobe == "RML":
con = mpatches.FancyBboxPatch((cx+0.2, 1.55), 0.85, 0.72,
boxstyle="round,pad=0.05", facecolor=OPAQ, edgecolor="none", zorder=8)
ax.add_patch(con)
add_label(ax, "Lost R heart\nborder", cx+0.15, 1.35, color=GOLD)
elif lobe == "LUL":
# Veil opacity over left
con = mpatches.FancyBboxPatch((cx-1.78, 0.85), 1.42, 2.42,
boxstyle="round,pad=0.1", facecolor="#6A6A5A",
edgecolor="none", zorder=7, alpha=0.85)
ax.add_patch(con)
add_label(ax, "Veil\nOpacity", cx-1.6, 2.2)
# Volume loss arrow
add_label(ax, f"COLLAPSE ({lobe})", 0.1, 0.15)
add_label(ax, "β Volume\nLoss", 0.1, 0.5, color="#FF8800")
return save_fig(fig, f"collapse_{lobe}")
def draw_pulm_oedema():
fig, ax = plt.subplots(figsize=(5.2, 4.6))
fig.patch.set_facecolor(BG)
cx = fig_base(ax)
# Enlarged heart
big_heart = Ellipse((cx + 0.14, 1.95), 2.15, 2.2,
facecolor=HEART, edgecolor="none", zorder=5)
ax.add_patch(big_heart)
# Bat-wing bilateral perihilar opacity
for side, sx in [("L", cx-1.5), ("R", cx+0.25)]:
bw = mpatches.FancyBboxPatch((sx, 1.3), 1.0, 1.3,
boxstyle="round,pad=0.1", facecolor="#707060",
edgecolor="none", alpha=0.85, zorder=8)
ax.add_patch(bw)
# Kerley B lines (short horizontal lines at bases)
for bx in [0.22, cx+1.65]:
for i in range(5):
ky = 0.6 + i * 0.2
ax.plot([bx, bx+0.28], [ky, ky], color=BONE, lw=0.9, zorder=9)
add_label(ax, "PULMONARY OEDEMA", 0.1, 0.15)
add_label(ax, "Bat-wing\nOpacity", cx-0.4, 2.75)
add_label(ax, "Kerley B\nlines", cx+1.52, 0.78)
add_label(ax, "Cardiomegaly\nCTR>0.5", cx-0.5, 0.5, color=GOLD)
return save_fig(fig, "pulm_oedema")
def draw_miliary_tb():
fig, ax = plt.subplots(figsize=(5.2, 4.6))
fig.patch.set_facecolor(BG)
cx = fig_base(ax)
random.seed(42)
# Both lung fields
for lx, lw2 in [(cx-1.78, 1.42), (cx+0.18, 1.6)]:
for _ in range(120):
nx = lx + random.random() * lw2
ny = 0.9 + random.random() * 2.3
r = 0.025 + random.random()*0.022
ax.add_patch(plt.Circle((nx, ny), r, color=BONE, zorder=9))
add_label(ax, "MILIARY TB", 0.1, 0.15)
add_label(ax, "Millet-seed\nnodules 1-3mm\nbilateral", cx-0.5, 2.8)
return save_fig(fig, "miliary_tb")
def draw_hilar_lad():
fig, ax = plt.subplots(figsize=(5.2, 4.6))
fig.patch.set_facecolor(BG)
cx = fig_base(ax)
# Enlarged hila
ax.add_patch(Ellipse((cx+0.6, 2.42), 0.72, 0.82,
facecolor=HILAR, edgecolor=BONE, lw=1.2, zorder=8))
ax.add_patch(Ellipse((cx-0.6, 2.52), 0.68, 0.78,
facecolor=HILAR, edgecolor=BONE, lw=1.2, zorder=8))
add_label(ax, "BHL (Bilateral Hilar LAP)", 0.1, 0.15)
add_label(ax, "Enlarged\nR Hilum", cx+0.38, 2.1)
add_label(ax, "Enlarged\nL Hilum", cx-1.1, 2.1)
return save_fig(fig, "hilar_lad")
def draw_lung_abscess():
fig, ax = plt.subplots(figsize=(5.2, 4.6))
fig.patch.set_facecolor(BG)
cx = fig_base(ax)
# Thick-walled cavity
cav_cx, cav_cy, r = cx+0.75, 2.8, 0.42
# Thick wall
ax.add_patch(plt.Circle((cav_cx, cav_cy), r+0.08, color=OPAQ, zorder=8))
# Dark cavity
ax.add_patch(plt.Circle((cav_cx, cav_cy), r, color=BG, zorder=9))
# Air-fluid level (lower half of cavity = fluid/white)
wedge = Wedge((cav_cx, cav_cy), r*0.95, 180, 360,
facecolor=OPAQ, edgecolor="none", zorder=10)
ax.add_patch(wedge)
# Horizontal air-fluid level line
ax.plot([cav_cx - r*0.94, cav_cx + r*0.94], [cav_cy, cav_cy],
color=GOLD, lw=2, zorder=11)
add_label(ax, "LUNG ABSCESS", 0.1, 0.15)
add_label(ax, "Thick wall\ncavity", cav_cx + 0.45, cav_cy + 0.15)
add_label(ax, "Air-fluid\nlevel", cav_cx + 0.45, cav_cy - 0.2)
return save_fig(fig, "lung_abscess")
def draw_pneumoperitoneum():
fig, ax = plt.subplots(figsize=(5.2, 4.6))
fig.patch.set_facecolor(BG)
cx = fig_base(ax)
# Gas crescent under right hemidiaphragm
theta_g = np.linspace(0, np.pi, 80)
gx = cx + 0.2 + 1.1 * np.cos(theta_g)
gy = 0.72 + 0.52 * np.sin(theta_g)
ax.fill_between(gx, 0.72, gy, color=BG, zorder=9)
ax.plot(gx, gy, color=GOLD, lw=2, zorder=10)
ax.plot([cx + 0.2 - 1.1, cx + 0.2 + 1.1], [0.72, 0.72],
color=GOLD, lw=1.5, zorder=10)
add_label(ax, "PNEUMOPERITONEUM", 0.1, 0.15)
add_label(ax, "Gas crescent\nunder R\ndiaphragm", cx + 0.3, 0.9, color=GOLD)
return save_fig(fig, "pneumoperitoneum")
def draw_cardiomegaly():
fig, ax = plt.subplots(figsize=(5.2, 4.6))
fig.patch.set_facecolor(BG)
cx = fig_base(ax)
# Enlarged heart CTR > 0.5
big_heart = Ellipse((cx + 0.1, 1.95), 2.4, 2.4,
facecolor=HEART, edgecolor=BONE, lw=1, zorder=5)
ax.add_patch(big_heart)
# CTR measurement
ax.annotate("", xy=(cx-1.1, 2.0), xytext=(cx+0.0, 2.0),
arrowprops=dict(arrowstyle="<->", color=GOLD, lw=1.5))
ax.annotate("", xy=(cx+0.0, 1.95), xytext=(cx+1.3, 1.95),
arrowprops=dict(arrowstyle="<->", color=GOLD, lw=1.5))
# Thorax width
ax.plot([0.22, 0.22], [1.2, 1.2], color="#6688AA", lw=1, linestyle="--")
ax.plot([5.0, 5.0], [1.2, 1.2], color="#6688AA", lw=1, linestyle="--")
add_label(ax, "CARDIOMEGALY (CTR > 0.5)", 0.1, 0.15)
add_label(ax, "CTR > 0.5", cx-0.9, 2.15, color=GOLD)
return save_fig(fig, "cardiomegaly")
def draw_hydropneumothorax():
fig, ax = plt.subplots(figsize=(5.2, 4.6))
fig.patch.set_facecolor(BG)
cx = fig_base(ax)
# Right side: fluid below, air above with straight horizontal level
fluid_y = 1.7
# Fluid (white) below
fluid = mpatches.FancyBboxPatch((cx+0.18, 0.82), 1.6, fluid_y - 0.82,
boxstyle="square,pad=0", facecolor=OPAQ, edgecolor="none", zorder=8)
ax.add_patch(fluid)
# Air (dark) above
air = mpatches.FancyBboxPatch((cx+0.18, fluid_y), 1.6, 3.27 - fluid_y,
boxstyle="square,pad=0", facecolor=BG, edgecolor="none", zorder=8)
ax.add_patch(air)
# Straight horizontal air-fluid level
ax.plot([cx+0.18, cx+1.78], [fluid_y, fluid_y],
color=GOLD, lw=2.5, zorder=9)
add_label(ax, "HYDROPNEUMOTHORAX", 0.1, 0.15)
add_label(ax, "Horizontal\nair-fluid level\n(STRAIGHT)", cx+0.38, fluid_y + 0.08)
add_label(ax, "Fluid\n(below)", cx+0.72, 1.0, color="#888888")
add_label(ax, "Air\n(above)", cx+0.72, 2.4, color="#AADDFF")
return save_fig(fig, "hydropneumothorax")
def draw_post_primary_tb():
fig, ax = plt.subplots(figsize=(5.2, 4.6))
fig.patch.set_facecolor(BG)
cx = fig_base(ax)
# Apical opacity right side
apical = mpatches.FancyBboxPatch((cx+0.22, 2.45), 1.48, 0.82,
boxstyle="round,pad=0.06", facecolor="#909078",
edgecolor="none", zorder=8)
ax.add_patch(apical)
# Cavity
ax.add_patch(plt.Circle((cx+0.75, 2.82), 0.26, color=BG, zorder=9))
ax.add_patch(plt.Circle((cx+0.75, 2.82), 0.26, color="none",
edgecolor=BONE, lw=1.8, zorder=10))
# Fibrotic streaks
for j in range(3):
fx = cx + 0.4 + j * 0.45
ax.plot([fx, fx+0.1], [2.45, 2.0], color=BONE, lw=0.8, zorder=9)
add_label(ax, "POST-PRIMARY TB (Fibrocavitary)", 0.1, 0.15)
add_label(ax, "Apical\ncavity", cx+0.85, 2.9)
add_label(ax, "Fibrotic\nstreaks", cx+1.0, 2.0)
return save_fig(fig, "post_primary_tb")
def draw_sarcoidosis():
fig, ax = plt.subplots(figsize=(5.2, 4.6))
fig.patch.set_facecolor(BG)
cx = fig_base(ax)
# BHL - enlarged hila
ax.add_patch(Ellipse((cx+0.6, 2.42), 0.72, 0.82,
facecolor=HILAR, edgecolor=BONE, lw=1, zorder=8))
ax.add_patch(Ellipse((cx-0.6, 2.52), 0.68, 0.78,
facecolor=HILAR, edgecolor=BONE, lw=1, zorder=8))
# Parenchymal nodules
random.seed(7)
for lx, lw2 in [(cx-1.78, 1.42), (cx+0.18, 1.6)]:
for _ in range(35):
nx = lx + random.random() * lw2
ny = 1.1 + random.random() * 2.0
ax.add_patch(plt.Circle((nx, ny), 0.04 + random.random()*0.03,
color=HILAR, zorder=9))
add_label(ax, "SARCOIDOSIS (Stage II: BHL + Parenchyma)", 0.1, 0.15)
add_label(ax, "BHL", cx-0.15, 2.7)
add_label(ax, "Parenchymal\nnodules", cx-1.6, 1.9)
return save_fig(fig, "sarcoidosis")
# βββ Generate all images ββββββββββββββββββββββββββββββββββββββββββββββββββ
print("Generating X-ray diagrams...")
image_paths = {
"normal": draw_normal(),
"pneumothorax": draw_pneumothorax("R", False),
"tension_ptx": draw_pneumothorax("R", True),
"pl_effusion": draw_pleural_effusion("L", False),
"consolidation": draw_consolidation("RLL"),
"collapse": draw_collapse("RUL"),
"pulm_oedema": draw_pulm_oedema(),
"miliary_tb": draw_miliary_tb(),
"post_primary_tb": draw_post_primary_tb(),
"hilar_lad": draw_hilar_lad(),
"lung_abscess": draw_lung_abscess(),
"pneumoperitoneum":draw_pneumoperitoneum(),
"hydropneumo": draw_hydropneumothorax(),
"cardiomegaly": draw_cardiomegaly(),
"sarcoidosis": draw_sarcoidosis(),
}
print("All diagrams saved.")
# βββ ReportLab PDF ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def build_pdf(output_path):
doc = SimpleDocTemplate(
output_path,
pagesize=A4,
leftMargin=1.6*cm, rightMargin=1.6*cm,
topMargin=1.5*cm, bottomMargin=1.5*cm,
title="Respiratory X-Rays for MBBS Exams",
)
styles = getSampleStyleSheet()
def style(name, **kw):
return ParagraphStyle(name, **kw)
title_s = style("T", fontSize=22, fontName="Helvetica-Bold",
textColor=colors.white, alignment=TA_CENTER, spaceAfter=4)
sub_s = style("S", fontSize=11, fontName="Helvetica",
textColor=colors.HexColor("#BBDDFF"), alignment=TA_CENTER)
sec_s = style("Sec", fontSize=12, fontName="Helvetica-Bold",
textColor=colors.white, spaceAfter=3, spaceBefore=2)
bul_s = style("B", fontSize=9, fontName="Helvetica", textColor=TEXT_DARK,
leftIndent=14, firstLineIndent=-10, spaceAfter=2, leading=13)
key_s = style("K", fontSize=9, fontName="Helvetica-Bold",
textColor=colors.HexColor("#1A3A1A"),
leftIndent=14, firstLineIndent=-10, spaceAfter=2, leading=13)
mnem_s = style("M", fontSize=9, fontName="Helvetica-BoldOblique",
textColor=colors.HexColor("#6B2400"), leftIndent=6, spaceAfter=2)
lbl_s = style("L", fontSize=9, fontName="Helvetica-Bold",
textColor=NAVY, spaceAfter=2)
small_s = style("Sm", fontSize=7.5, fontName="Helvetica",
textColor=TEXT_MED, alignment=TA_CENTER)
th_s = style("TH", fontSize=9, fontName="Helvetica-Bold",
textColor=colors.white, alignment=TA_CENTER)
tc_s = style("TC", fontSize=8.5, fontName="Helvetica",
textColor=TEXT_DARK, alignment=TA_CENTER)
def hdr(text, bg=NAVY):
t = Table([[Paragraph(text, sec_s)]], colWidths=[PAGE_W])
t.setStyle(TableStyle([
("BACKGROUND",(0,0),(-1,-1),bg),
("TOPPADDING",(0,0),(-1,-1),6),
("BOTTOMPADDING",(0,0),(-1,-1),6),
("LEFTPADDING",(0,0),(-1,-1),12),
]))
return t
def key_box(points, bg=LIGHT_BLU):
paras = [Paragraph("π <b>KEY POINTS:</b>", lbl_s)]
for p in points:
paras.append(Paragraph(f"β€ {p}", key_s))
t = Table([[paras]], colWidths=[PAGE_W])
t.setStyle(TableStyle([
("BACKGROUND",(0,0),(-1,-1),bg),
("TOPPADDING",(0,0),(-1,-1),5),
("BOTTOMPADDING",(0,0),(-1,-1),5),
("LEFTPADDING",(0,0),(-1,-1),10),
("BOX",(0,0),(-1,-1),0.5,NAVY),
]))
return t
def mnem_box(text):
t = Table([[Paragraph(f"π‘ {text}", mnem_s)]], colWidths=[PAGE_W])
t.setStyle(TableStyle([
("BACKGROUND",(0,0),(-1,-1),YELLOW_BG),
("BOX",(0,0),(-1,-1),1,colors.HexColor("#DDAA00")),
("TOPPADDING",(0,0),(-1,-1),5),
("BOTTOMPADDING",(0,0),(-1,-1),5),
("LEFTPADDING",(0,0),(-1,-1),8),
]))
return t
story = []
# ββ Cover ββ
cover = Table([
[Paragraph("π« RESPIRATORY X-RAYS", title_s)],
[Paragraph("Complete MBBS Exam Guide", sub_s)],
[Paragraph("Schematic X-rays Β· Key Findings Β· Mnemonics Β· Quick Tables", sub_s)],
], colWidths=[PAGE_W])
cover.setStyle(TableStyle([
("BACKGROUND",(0,0),(-1,-1),NAVY),
("TOPPADDING",(0,0),(-1,-1),10),
("BOTTOMPADDING",(0,0),(-1,-1),10),
("LEFTPADDING",(0,0),(-1,-1),14),
("RIGHTPADDING",(0,0),(-1,-1),14),
]))
story.append(cover)
story.append(Spacer(1, 0.4*cm))
# Topics grid
topics = [
"1. Normal CXR","2. Pneumothorax","3. Tension Pneumothorax",
"4. Pleural Effusion","5. Consolidation (Pneumonia)","6. Lobar Collapse",
"7. Pulmonary Oedema","8. Miliary TB","9. Post-Primary TB",
"10. Bilateral Hilar LAP","11. Lung Abscess","12. Pneumoperitoneum",
"13. Hydropneumothorax","14. Cardiomegaly","15. Sarcoidosis",
]
rows = []
for i in range(0, len(topics), 3):
chunk = topics[i:i+3]
while len(chunk) < 3: chunk.append("")
rows.append([Paragraph(t, style("tt", fontSize=8.5, fontName="Helvetica",
textColor=NAVY)) for t in chunk])
toc = Table(rows, colWidths=[PAGE_W/3]*3)
toc.setStyle(TableStyle([
("BACKGROUND",(0,0),(-1,-1),LIGHT_BLU),
("GRID",(0,0),(-1,-1),0.4,colors.HexColor("#BBDDEE")),
("TOPPADDING",(0,0),(-1,-1),5),("BOTTOMPADDING",(0,0),(-1,-1),5),
("LEFTPADDING",(0,0),(-1,-1),8),
]))
story.append(toc)
story.append(Spacer(1, 0.3*cm))
# ABCDE approach
abcde = Table([[Paragraph(
"<b>π SYSTEMATIC CXR APPROACH (ABCDE)</b><br/>"
"<b>A</b>irway β trachea midline, carina angle <70Β° "
"<b>B</b>ones β ribs, clavicles, spine "
"<b>C</b>ardiac β CTR, borders, shape<br/>"
"<b>D</b>iaphragm β level, costophrenic angles "
"<b>E</b>verything else β lung fields, hila, pleura, mediastinum",
style("ab", fontSize=8.5, fontName="Helvetica",
textColor=TEXT_DARK, leading=14))]],
colWidths=[PAGE_W])
abcde.setStyle(TableStyle([
("BACKGROUND",(0,0),(-1,-1),GREEN_BG),
("BOX",(0,0),(-1,-1),1,TEAL),
("TOPPADDING",(0,0),(-1,-1),7),("BOTTOMPADDING",(0,0),(-1,-1),7),
("LEFTPADDING",(0,0),(-1,-1),10),
]))
story.append(abcde)
story.append(PageBreak())
# ββ Cases ββ
cases = [
{
"title":"1. Normal Chest X-Ray",
"img":"normal",
"mnemonic":"ABCDE: Airway midline, Bones intact, Cardiac CTR<0.5, Diaphragm clear, Even lung fields",
"findings":[
"Trachea midline - bifurcates at T4/T5 (angle of Louis)",
"Cardiothoracic ratio (CTR) < 0.5 on PA film",
"Both lung fields clear, markings extend to periphery",
"Right dome of diaphragm higher than left (ant. 6th rib level)",
"Costophrenic angles sharp and acute",
"Left hilum 0.5-1.5 cm higher than right",
],
"keys":[
"PA film preferred (AP magnifies heart - CTR unreliable)",
"Adequate inspiration: 5-6 anterior ribs above diaphragm",
"Check for rotation: medial clavicle ends equidistant from spine",
],
"color":LIGHT_BLU,
},
{
"title":"2. Pneumothorax",
"img":"pneumothorax",
"mnemonic":"Visible PLEURAL LINE + No lung markings beyond it = Pneumothorax",
"findings":[
"Visible pleural line (visceral pleura) - thin white line",
"Absent lung markings peripheral to the pleural line",
"Lung collapses towards the hilum",
"Radiolucent (dark) gap between lung edge and chest wall",
"Ipsilateral diaphragm may be depressed",
"NO mediastinal shift in simple PTX",
],
"keys":[
"Measure at hilum: small <2 cm, large β₯2 cm (from lung edge to chest wall)",
"Primary spontaneous: tall thin young males, rupture of apical blebs",
"Secondary: COPD, asthma, TB, Marfan syndrome, Pneumocystis",
"Rx: small asymptomatic = observe; large or symptomatic = needle aspiration or chest drain",
],
"color":PINK_BG,
},
{
"title":"3. Tension Pneumothorax",
"img":"tension_ptx",
"mnemonic":"TENSION: Trachea shifted AWAY, collapsed lung, haemodynamic compromise - CLINICAL diagnosis!",
"findings":[
"All features of simple pneumothorax PLUS:",
"Tracheal deviation AWAY from the PTX side",
"Mediastinal shift away from PTX side",
"Complete collapse of ipsilateral lung",
"Flattening / inversion of ipsilateral hemidiaphragm",
"Increased intercostal spaces on affected side",
],
"keys":[
"CLINICAL DIAGNOSIS - do NOT wait for X-ray (can kill while waiting)",
"Immediate needle decompression: 2nd ICS, mid-clavicular line",
"Follow with chest drain: 5th ICS, mid-axillary line",
"Causes: penetrating trauma, mechanical ventilation, barotrauma, iatrogenic",
],
"color":PINK_BG,
},
{
"title":"4. Pleural Effusion",
"img":"pl_effusion",
"mnemonic":"FLUID: Flat top (meniscus), Rim blunting (CP angle), Under diaphragm obscured, Ipsilateral opacity, Density homogeneous",
"findings":[
"Blunting of costophrenic angle (>50 mL needed to see)",
"Meniscus sign - concave upper border of opacity",
"Homogeneous opacity at lung base (blunts costophrenic angle first)",
"Loss of hemidiaphragm silhouette",
"Mediastinum shifts AWAY from large/massive effusion",
"Lateral decubitus X-ray: detects as little as 5-10 mL",
],
"keys":[
"Transudate (Lights): CCF, nephrotic syndrome, cirrhosis, hypothyroidism, Meigs syndrome",
"Exudate (Lights): malignancy, pneumonia, TB, pulmonary embolism, pancreatitis",
"Massive effusion: >2/3 hemithorax white + mediastinal shift AWAY",
"Subpulmonary effusion: pseudoelevated diaphragm, peak shifts laterally",
],
"color":LIGHT_BLU,
},
{
"title":"5. Consolidation (Pneumonia)",
"img":"consolidation",
"mnemonic":"White opacity + AIR BRONCHOGRAM = Consolidation (alveoli filled, bronchi air-filled)",
"findings":[
"Homogeneous or patchy opacity - lobar or segmental distribution",
"Air bronchogram sign: dark bronchi visible within white opacity",
"Loss of silhouette sign (adjacent structure becomes invisible)",
"No volume loss (unlike collapse - borders may be bulging)",
"Bulging fissure sign: Klebsiella (right upper lobe, heavy mucoid sputum)",
],
"keys":[
"RLL: obscures R hemidiaphragm | RML: obscures right heart border",
"LLL: obscures L hemidiaphragm | LUL/Lingula: obscures left heart border",
"Organisms: Strep pneumoniae (lobar), Klebsiella (bulging fissure), Staph (pneumatoceles)",
"Round pneumonia: children - can mimic mass",
],
"color":YELLOW_BG,
},
{
"title":"6. Lobar Collapse",
"img":"collapse",
"mnemonic":"Collapse: White + Volume LOSS + Structures shift TOWARDS (opposite to effusion!)",
"findings":[
"Opacity (white) WITH VOLUME LOSS on affected side",
"Mediastinum/trachea shifts TOWARDS the collapse",
"Elevation of ipsilateral hemidiaphragm",
"Compensatory hyperinflation of remaining lobes",
"Crowding of ipsilateral ribs",
"Fissure displacement towards collapsed lobe",
],
"keys":[
"RUL: trachea β right, raised hilum, wedge opacity at apex",
"RML: loss of right heart border (silhouette sign), difficult to see on PA",
"RLL: displaced hilum downward, obscures R hemidiaphragm",
"LLL: 'Sail sign' / triangular opacity behind heart (best seen on lateral)",
"LUL: 'Veil opacity' - hazy left lung, Juxtaphrenic peak",
"Causes: mucus plug (#1), endobronchial tumour, foreign body",
],
"color":ORANGE_BG,
},
{
"title":"7. Pulmonary Oedema",
"img":"pulm_oedema",
"mnemonic":"ABCDE: Alveolar oedema, Bat-wing, Cardiomegaly, Diversion (upper lobe), Effusion + Kerley B",
"findings":[
"Cardiomegaly (CTR > 0.5) in cardiogenic oedema",
"Upper lobe vascular diversion (upper zone vessels > lower)",
"Kerley B lines (1-2 cm horizontal lines at lung bases/lateral zones)",
"Perihilar bat-wing / butterfly opacity (bilateral symmetrical)",
"Bilateral pleural effusions",
"Interstitial lines (Kerley A = central; Kerley B = peripheral)",
],
"keys":[
"Cardiogenic (LVF, MS, CCF): cardiomegaly + upper lobe diversion + Kerley B",
"Non-cardiogenic (ARDS): bilateral patchy, NO cardiomegaly, NO upper lobe diversion",
"Kerley B = lymphatic distension from raised PAWP (>18 mmHg)",
"Interstitial stage β Alveolar stage as severity increases",
],
"color":ORANGE_BG,
},
{
"title":"8. Miliary Tuberculosis",
"img":"miliary_tb",
"mnemonic":"MILLET SEEDS (1-3 mm) scattered UNIFORMLY throughout BOTH lung fields",
"findings":[
"Bilateral, diffuse, uniformly distributed fine nodules",
"Nodule size 1-3 mm (millet-seed size)",
"Uniform density and distribution throughout both lungs",
"No lobar preference (unlike post-primary TB)",
"Hilar lymphadenopathy may be present",
],
"keys":[
"Caused by haematogenous dissemination of Mycobacterium tuberculosis",
"Seen in immunocompromised (HIV, malnutrition, diabetes, steroids)",
"Sputum AFB often negative - diagnose by bone marrow / liver biopsy",
"Tuberculin test may be negative (anergy in miliary TB)",
"DDx: miliary histoplasmosis, sarcoidosis, haematogenous metastases, silicosis",
],
"color":YELLOW_BG,
},
{
"title":"9. Post-Primary TB (Fibrocavitary)",
"img":"post_primary_tb",
"mnemonic":"APICAL + POSTERIOR = TB (segments 1, 2, 6 affected) + Cavity + Fibrosis",
"findings":[
"Unilateral or bilateral apical/upper zone opacity",
"Cavitation within opacity (thick-walled ring shadow)",
"Fibrotic streaks pulling hilum upward",
"Satellite nodules surrounding main lesion",
"Trachea/mediastinum shifts towards fibrosed side",
"Calcified Ghon focus or Ranke complex (healed primary TB)",
],
"keys":[
"Ghon focus = calcified primary lesion (mid zone)",
"Ranke complex = Ghon focus + calcified hilar lymph node",
"Simon foci = apical calcified scars from haematogenous seeding",
"Post-primary = endogenous reactivation in apex (high pO2 favours TB growth)",
"Spread: bronchogenic (endobronchial) or haematogenous",
],
"color":YELLOW_BG,
},
{
"title":"10. Bilateral Hilar Lymphadenopathy",
"img":"hilar_lad",
"mnemonic":"BHL + Young patient + Non-caseating granuloma = SARCOIDOSIS (also TB, lymphoma)",
"findings":[
"Bilateral enlargement of hilar shadows",
"Lobulated / 'potato-node' appearance of hila",
"Normal lung parenchyma in Stage I sarcoidosis",
"Parenchymal changes in Stage II (nodules + BHL)",
"DDx: TB, Hodgkin lymphoma, silicosis, malignancy, chronic berylliosis",
],
"keys":[
"Sarcoidosis stages: 0=normal | I=BHL only | II=BHL+parenchyma | III=parenchyma only | IV=fibrosis",
"Eggshell calcification of hilar LN = silicosis (pathognomonic) or old sarcoidosis",
"ACE elevated in sarcoidosis; hypercalcaemia; uveitis",
"TB: more often unilateral, asymmetric; paratracheal nodes",
"Spontaneous remission in 60-70% Stage I/II sarcoidosis",
],
"color":LIGHT_BLU,
},
{
"title":"11. Lung Abscess",
"img":"lung_abscess",
"mnemonic":"CAVITY with THICK WALL + HORIZONTAL AIR-FLUID LEVEL = Lung Abscess",
"findings":[
"Round or oval opacity with thick irregular wall",
"Central lucency (cavity) with air-fluid level inside",
"Air-fluid level is HORIZONTAL (straight - unlike empyema which is lenticular)",
"Located in dependent segments: RUL posterior (S2) / RLL superior (S6)",
"No volume loss (unlike collapse)",
],
"keys":[
"Causes: aspiration (#1), Staphylococcus aureus, Klebsiella, anaerobes",
"Aspiration: posterior S2 (sitting) or superior S6/RLL (supine)",
"Abscess vs Empyema: spherical vs D-shaped, acute vs obtuse angle with chest wall",
"Empyema: moves with posture, split pleura sign on CT, tapers at edges",
"Rx: prolonged antibiotics (6-8 weeks); surgical if refractory",
],
"color":ORANGE_BG,
},
{
"title":"12. Pneumoperitoneum",
"img":"pneumoperitoneum",
"mnemonic":"GAS under DIAPHRAGM on ERECT CXR = Perforated hollow viscus until proven otherwise",
"findings":[
"Crescent of gas under the right hemidiaphragm (most common)",
"May be bilateral if large perforation",
"Best seen on erect CXR (patient upright 5-10 min before film)",
"Gas appears as black crescent between liver/soft tissue and diaphragm",
"Rigler's sign on AXR: gas visible on BOTH sides of bowel wall",
],
"keys":[
"Commonest cause: perforated peptic ulcer (duodenal/gastric)",
"Others: perforated appendix, diverticulitis, bowel obstruction with perforation",
"Gas under LEFT diaphragm: gastric perforation or splenic flexure",
"If erect impossible: left lateral decubitus (gas over liver)",
"Falciform ligament sign: free gas outlining falciform ligament on AXR",
],
"color":PINK_BG,
},
{
"title":"13. Hydropneumothorax",
"img":"hydropneumo",
"mnemonic":"STRAIGHT horizontal air-fluid level in pleural space = both Air AND Fluid present",
"findings":[
"STRAIGHT (perfectly horizontal) air-fluid level in pleural space",
"Dark (air) above the fluid level",
"White (fluid) opacity below",
"Fluid level shifts with change in patient position",
"Ipsilateral lung compressed",
],
"keys":[
"Causes: trauma (haemopneumothorax), bronchopleural fistula, iatrogenic, TB empyema",
"Distinguish from abscess: abscess air-fluid level is WITHIN lung parenchyma",
"Straight (horizontal) level distinguishes from meniscus of effusion",
"Rx: chest drain (tube thoracostomy)",
],
"color":LIGHT_BLU,
},
{
"title":"14. Cardiomegaly",
"img":"cardiomegaly",
"mnemonic":"CTR > 0.5 on PA film = Cardiomegaly (on AP film alone, CTR often >0.5 normally - unreliable)",
"findings":[
"Cardiothoracic ratio (CTR) > 0.5 on PA film",
"Widened cardiac silhouette",
"Right heart border = right atrium | Left border = left ventricle",
"Upper lobe vascular diversion in LVF",
"Pulmonary plethora (dilated pulmonary vessels) in L-to-R shunts",
],
"keys":[
"CTR = widest cardiac width / widest thoracic internal width (at same level)",
"Globular/flask-shaped heart = pericardial effusion (water bottle heart)",
"Boot-shaped (coeur-en-sabot) = Tetralogy of Fallot",
"Box-shaped = TAPVC / Ebstein anomaly",
"AP film magnifies heart - CTR unreliable; always use PA for cardiac assessment",
],
"color":LIGHT_BLU,
},
{
"title":"15. Sarcoidosis (Stage II)",
"img":"sarcoidosis",
"mnemonic":"BHL + parenchymal nodules + non-caseating granulomas + ACE β = Sarcoidosis",
"findings":[
"Bilateral hilar lymphadenopathy (lobulated 'potato' hila)",
"Bilateral parenchymal nodules or reticulonodular pattern",
"Upper and mid zone predominance",
"Rarely cavitates (unlike TB - important differentiator)",
"Fibrosis and honeycombing in Stage IV",
],
"keys":[
"Non-caseating epithelioid granulomas on biopsy (Schaumann bodies)",
"ACE elevated, hypercalcaemia (elevated 1,25-VitD), hypercalciuria",
"Stages: I=BHL | II=BHL+parenchyma | III=parenchyma only | IV=fibrosis",
"Spontaneous remission 60-70% (Stage I/II)",
"Rx: systemic steroids if symptomatic, organ-threatening, or deteriorating",
],
"color":YELLOW_BG,
},
]
IMG_W = 5.4 * cm
IMG_H = 4.8 * cm
for case in cases:
story.append(hdr(case["title"]))
story.append(Spacer(1, 0.1*cm))
img_path = image_paths.get(case["img"], "")
img = Image(img_path, width=IMG_W, height=IMG_H) if os.path.exists(img_path) else Spacer(IMG_W, IMG_H)
f_paras = [Paragraph("π <b>X-RAY FINDINGS:</b>", lbl_s)]
for f in case["findings"]:
f_paras.append(Paragraph(f"β’ {f}", bul_s))
f_paras.append(Spacer(1, 0.1*cm))
f_paras.append(mnem_box(case["mnemonic"]))
right_w = PAGE_W - IMG_W - 0.3*cm
main = Table([[img, f_paras]], colWidths=[IMG_W + 0.15*cm, right_w])
main.setStyle(TableStyle([
("VALIGN",(0,0),(-1,-1),"TOP"),
("TOPPADDING",(0,0),(-1,-1),3),
("LEFTPADDING",(0,0),(-1,-1),3),
("RIGHTPADDING",(0,0),(-1,-1),3),
]))
story.append(main)
story.append(Spacer(1, 0.1*cm))
story.append(key_box(case["keys"], case["color"]))
story.append(Spacer(1, 0.25*cm))
story.append(HRFlowable(width=PAGE_W, thickness=0.5, color=NAVY, spaceAfter=0.15*cm))
# ββ Quick Reference Tables ββ
story.append(PageBreak())
story.append(hdr("β‘ QUICK REFERENCE TABLES", TEAL))
story.append(Spacer(1, 0.3*cm))
# Silhouette sign
story.append(Paragraph("SILHOUETTE SIGN", style("st", fontSize=11,
fontName="Helvetica-Bold", textColor=NAVY)))
story.append(Spacer(1, 0.15*cm))
sil_rows = [
[Paragraph("Structure Obscured", th_s), Paragraph("Affected Lobe", th_s)],
[Paragraph("Right heart border", tc_s), Paragraph("Right Middle Lobe (RML)", tc_s)],
[Paragraph("Left heart border", tc_s), Paragraph("Lingula / Left Upper Lobe (LUL)", tc_s)],
[Paragraph("Right hemidiaphragm", tc_s), Paragraph("Right Lower Lobe (RLL)", tc_s)],
[Paragraph("Left hemidiaphragm", tc_s), Paragraph("Left Lower Lobe (LLL)", tc_s)],
[Paragraph("Aortic knuckle", tc_s), Paragraph("Left Upper Lobe / Lingula", tc_s)],
[Paragraph("Descending aorta", tc_s), Paragraph("Left Lower Lobe (LLL)", tc_s)],
]
sil_t = Table(sil_rows, colWidths=[PAGE_W*0.45, PAGE_W*0.55])
sil_t.setStyle(TableStyle([
("BACKGROUND",(0,0),(-1,0),NAVY),
("ROWBACKGROUNDS",(0,1),(-1,-1),[colors.white, LIGHT_BLU]),
("GRID",(0,0),(-1,-1),0.5,colors.HexColor("#AACCDD")),
("TOPPADDING",(0,0),(-1,-1),5),("BOTTOMPADDING",(0,0),(-1,-1),5),
("LEFTPADDING",(0,0),(-1,-1),8),("ALIGN",(0,0),(-1,-1),"CENTER"),
]))
story.append(sil_t)
story.append(Spacer(1, 0.4*cm))
# Tracheal shift
story.append(Paragraph("TRACHEAL / MEDIASTINAL SHIFT", style("st2", fontSize=11,
fontName="Helvetica-Bold", textColor=NAVY)))
story.append(Spacer(1, 0.15*cm))
tr_rows = [
[Paragraph("Condition", th_s), Paragraph("Shift Direction", th_s)],
[Paragraph("Tension Pneumothorax", tc_s), Paragraph("AWAY from affected side", style("tca",fontSize=9,fontName="Helvetica-Bold",textColor=RED_C,alignment=TA_CENTER))],
[Paragraph("Massive Pleural Effusion", tc_s), Paragraph("AWAY from affected side", style("tcb",fontSize=9,fontName="Helvetica-Bold",textColor=RED_C,alignment=TA_CENTER))],
[Paragraph("Lobar Collapse / Fibrosis", tc_s), Paragraph("TOWARDS affected side", style("tcc",fontSize=9,fontName="Helvetica-Bold",textColor=TEAL,alignment=TA_CENTER))],
[Paragraph("Pneumonectomy", tc_s), Paragraph("TOWARDS affected side", style("tcd",fontSize=9,fontName="Helvetica-Bold",textColor=TEAL,alignment=TA_CENTER))],
[Paragraph("Consolidation (no volume change)", tc_s), Paragraph("No shift", style("tce",fontSize=9,fontName="Helvetica",textColor=TEXT_DARK,alignment=TA_CENTER))],
]
tr_t = Table(tr_rows, colWidths=[PAGE_W*0.55, PAGE_W*0.45])
tr_t.setStyle(TableStyle([
("BACKGROUND",(0,0),(-1,0),NAVY),
("ROWBACKGROUNDS",(0,1),(-1,-1),[colors.white, LIGHT_BLU]),
("GRID",(0,0),(-1,-1),0.5,colors.HexColor("#AACCDD")),
("TOPPADDING",(0,0),(-1,-1),5),("BOTTOMPADDING",(0,0),(-1,-1),5),
("LEFTPADDING",(0,0),(-1,-1),8),
]))
story.append(tr_t)
story.append(Spacer(1, 0.4*cm))
# White vs Dark
story.append(Paragraph("WHITE vs. DARK ON CXR", style("wvd", fontSize=11,
fontName="Helvetica-Bold", textColor=NAVY)))
story.append(Spacer(1, 0.15*cm))
wvd = Table([
[Paragraph("β¬ WHITE (Increased density)", style("wh",fontSize=9.5,
fontName="Helvetica-Bold",textColor=colors.white,alignment=TA_CENTER)),
Paragraph("β¬ DARK / LUCENT (Decreased density)", style("dk",fontSize=9.5,
fontName="Helvetica-Bold",textColor=colors.white,alignment=TA_CENTER))],
[Paragraph(
"β’ Consolidation (pus/fluid in alveoli)\nβ’ Collapse/atelectasis\n"
"β’ Pleural effusion\nβ’ Tumour/mass\nβ’ Cardiomegaly\nβ’ Haemothorax",
style("wl",fontSize=8.5,fontName="Helvetica",textColor=TEXT_DARK,leading=14)),
Paragraph(
"β’ Pneumothorax (air in pleural space)\nβ’ Emphysema/bullae\n"
"β’ Pneumoperitoneum\nβ’ Hydropneumothorax (air portion)\n"
"β’ Abscess/TB cavity\nβ’ Congenital cysts",
style("dl",fontSize=8.5,fontName="Helvetica",textColor=TEXT_DARK,leading=14))],
], colWidths=[PAGE_W*0.5, PAGE_W*0.5])
wvd.setStyle(TableStyle([
("BACKGROUND",(0,0),(0,0),colors.HexColor("#444444")),
("BACKGROUND",(1,0),(1,0),NAVY),
("BACKGROUND",(0,1),(0,1),colors.HexColor("#F5F5F0")),
("BACKGROUND",(1,1),(1,1),colors.HexColor("#EEF3FA")),
("GRID",(0,0),(-1,-1),0.5,colors.HexColor("#AAAAAA")),
("TOPPADDING",(0,0),(-1,-1),6),("BOTTOMPADDING",(0,0),(-1,-1),6),
("LEFTPADDING",(0,0),(-1,-1),10),("VALIGN",(0,1),(-1,1),"TOP"),
]))
story.append(wvd)
story.append(Spacer(1, 0.5*cm))
# Absorption and comparison mini-table
story.append(Paragraph("OPACITY COMPARISON: CONSOLIDATION vs COLLAPSE vs EFFUSION", style("comp",fontSize=11,fontName="Helvetica-Bold",textColor=NAVY)))
story.append(Spacer(1, 0.15*cm))
comp_rows = [
[Paragraph("Feature", th_s),
Paragraph("Consolidation", th_s),
Paragraph("Collapse", th_s),
Paragraph("Effusion", th_s)],
[Paragraph("Opacity",tc_s),Paragraph("Yes",tc_s),Paragraph("Yes",tc_s),Paragraph("Yes",tc_s)],
[Paragraph("Volume loss",tc_s),Paragraph("No",tc_s),Paragraph("Yes (key!)",style("tc_k",fontSize=8.5,fontName="Helvetica-Bold",textColor=colors.HexColor("#004400"),alignment=TA_CENTER)),Paragraph("No",tc_s)],
[Paragraph("Air bronchogram",tc_s),Paragraph("Yes (classic)",style("tc_y",fontSize=8.5,fontName="Helvetica-Bold",textColor=NAVY,alignment=TA_CENTER)),Paragraph("Sometimes",tc_s),Paragraph("No",tc_s)],
[Paragraph("Mediastinal shift",tc_s),Paragraph("No shift",tc_s),Paragraph("TOWARDS",style("tc_t",fontSize=8.5,fontName="Helvetica-Bold",textColor=TEAL,alignment=TA_CENTER)),Paragraph("AWAY (if large)",style("tc_a",fontSize=8.5,fontName="Helvetica-Bold",textColor=RED_C,alignment=TA_CENTER))],
[Paragraph("Fissure position",tc_s),Paragraph("Normal",tc_s),Paragraph("Displaced toward",tc_s),Paragraph("Normal",tc_s)],
]
comp_t = Table(comp_rows, colWidths=[PAGE_W*0.28, PAGE_W*0.24, PAGE_W*0.24, PAGE_W*0.24])
comp_t.setStyle(TableStyle([
("BACKGROUND",(0,0),(-1,0),NAVY),
("ROWBACKGROUNDS",(0,1),(-1,-1),[colors.white, LIGHT_BLU]),
("GRID",(0,0),(-1,-1),0.5,colors.HexColor("#AACCDD")),
("TOPPADDING",(0,0),(-1,-1),5),("BOTTOMPADDING",(0,0),(-1,-1),5),
("LEFTPADDING",(0,0),(-1,-1),6),("ALIGN",(0,0),(-1,-1),"CENTER"),
]))
story.append(comp_t)
# Footer
story.append(Spacer(1, 0.4*cm))
story.append(HRFlowable(width=PAGE_W, thickness=0.5, color=NAVY))
story.append(Spacer(1, 0.1*cm))
story.append(Paragraph(
"Schematic X-ray diagrams for educational use Β· MBBS Exam Preparation Β· All findings are representative patterns",
small_s))
doc.build(story)
print(f"β
PDF saved: {output_path}")
build_pdf("/tmp/workspace/respiratory-xray-pdf/Respiratory_Xrays_MBBS.pdf")
cd /tmp/workspace/respiratory-xray-pdf && python generate_pdf2.py 2>&1
ls -lh /tmp/workspace/respiratory-xray-pdf/Respiratory_Xrays_MBBS.pdf && ls -lh /tmp/workspace/respiratory-xray-pdf/images/
Respiratory Xrays MBBS
PDF Document Β· PDF
| # | Condition | Key Teaching Point |
|---|---|---|
| 1 | Normal CXR | ABCDE approach, CTR < 0.5 |
| 2 | Pneumothorax | Pleural line + no markings beyond it |
| 3 | Tension Pneumothorax | Trachea shifts AWAY - clinical emergency |
| 4 | Pleural Effusion | Meniscus sign, blunted CP angle |
| 5 | Consolidation | Air bronchogram, lobar distribution |
| 6 | Lobar Collapse | Volume loss, trachea shifts TOWARDS |
| 7 | Pulmonary Oedema | Bat-wing, Kerley B, cardiomegaly |
| 8 | Miliary TB | 1-3 mm millet seeds, bilateral uniform |
| 9 | Post-Primary TB | Apical cavity, fibrotic streaks |
| 10 | Bilateral Hilar LAP | Sarcoidosis stages, eggshell calcification |
| 11 | Lung Abscess | Thick wall + horizontal air-fluid level |
| 12 | Pneumoperitoneum | Gas crescent under diaphragm |
| 13 | Hydropneumothorax | Straight horizontal air-fluid level |
| 14 | Cardiomegaly | CTR > 0.5, cardiac shapes |
| 15 | Sarcoidosis Stage II | BHL + parenchymal nodules |
i want real x rays yaaa
cd /tmp/workspace/respiratory-xray-pdf/images && python3 -c " import urllib.request, os # Real X-rays from Wikimedia Commons (CC licensed, open access) xrays = { 'normal_real.jpg': 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/a2/Normal_posteroanterior_%28PA%29_chest_radiograph_%28X-ray%29.jpg/600px-Normal_posteroanterior_%28PA%29_chest_radiograph_%28X-ray%29.jpg', 'pneumothorax_real.jpg': 'https://upload.wikimedia.org/wikipedia/commons/thumb/0/0d/Pneumothorax_CXR.jpg/600px-Pneumothorax_CXR.jpg', 'pneumonia_real.jpg': 'https://upload.wikimedia.org/wikipedia/commons/thumb/b/be/Pneumonia_x-ray.jpg/600px-Pneumonia_x-ray.jpg', 'effusion_real.jpg': 'https://upload.wikimedia.org/wikipedia/commons/thumb/1/11/Effusionhalf.PNG/600px-Effusionhalf.PNG', 'pulm_oedema_real.jpg': 'https://upload.wikimedia.org/wikipedia/commons/thumb/3/36/Pulmonary_oedema.jpg/600px-Pulmonary_oedema.jpg', 'miliary_tb_real.jpg': 'https://upload.wikimedia.org/wikipedia/commons/thumb/d/d6/Miliary_tuberculosis_-_CXR.jpg/600px-Miliary_tuberculosis_-_CXR.jpg', 'cardiomegaly_real.jpg': 'https://upload.wikimedia.org/wikipedia/commons/thumb/9/9e/Cardiomegaly.svg/600px-Cardiomegaly.svg.png', 'pneumoperitoneum_real.jpg': 'https://upload.wikimedia.org/wikipedia/commons/thumb/1/10/Pneumoperitoneum.jpg/600px-Pneumoperitoneum.jpg', 'tension_ptx_real.jpg': 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/4b/Tension_pneumothorax.jpg/600px-Tension_pneumothorax.jpg', 'lung_abscess_real.jpg': 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/a4/Lung_abscess_X-ray.jpg/600px-Lung_abscess_X-ray.jpg', } headers = {'User-Agent': 'Mozilla/5.0 (compatible; educational use)'} for fname, url in xrays.items(): try: req = urllib.request.Request(url, headers=headers) with urllib.request.urlopen(req, timeout=10) as r: data = r.read() if len(data) > 5000: with open(fname, 'wb') as f: f.write(data) print(f'OK {fname}: {len(data)//1024}KB') else: print(f'SMALL {fname}: {len(data)} bytes (redirect/blocked)') except Exception as e: print(f'FAIL {fname}: {e}') "
cd /tmp/workspace/respiratory-xray-pdf/images && python3 -c " import urllib.request, os, ssl ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE # Try direct full-resolution Wikimedia + other open-access sources xrays = { 'normal_real.jpg': 'https://upload.wikimedia.org/wikipedia/commons/a/a2/Normal_posteroanterior_%28PA%29_chest_radiograph_%28X-ray%29.jpg', 'pneumothorax_real.jpg': 'https://upload.wikimedia.org/wikipedia/commons/0/0d/Pneumothorax_CXR.jpg', 'pneumonia_real.jpg': 'https://upload.wikimedia.org/wikipedia/commons/b/be/Pneumonia_x-ray.jpg', 'effusion_real.jpg': 'https://upload.wikimedia.org/wikipedia/commons/1/11/Effusionhalf.PNG', 'pulm_oedema_real.jpg': 'https://upload.wikimedia.org/wikipedia/commons/3/36/Pulmonary_oedema.jpg', 'miliary_tb_real.jpg': 'https://upload.wikimedia.org/wikipedia/commons/d/d6/Miliary_tuberculosis_-_CXR.jpg', 'pneumoperitoneum_real.jpg': 'https://upload.wikimedia.org/wikipedia/commons/1/10/Pneumoperitoneum.jpg', 'tension_ptx_real.jpg': 'https://upload.wikimedia.org/wikipedia/commons/4/4b/Tension_pneumothorax.jpg', 'lung_abscess_real.jpg': 'https://upload.wikimedia.org/wikipedia/commons/a/a4/Lung_abscess_X-ray.jpg', 'cardiomegaly_real.jpg': 'https://upload.wikimedia.org/wikipedia/commons/9/9e/Cardiomegaly.svg', } headers = { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120 Safari/537.36', 'Accept': 'image/webp,image/apng,image/*,*/*;q=0.8', 'Referer': 'https://en.wikipedia.org/', } for fname, url in xrays.items(): try: req = urllib.request.Request(url, headers=headers) with urllib.request.urlopen(req, timeout=12, context=ctx) as r: data = r.read() if len(data) > 5000: with open(fname, 'wb') as f: f.write(data) print(f'OK {fname}: {len(data)//1024}KB') else: print(f'TINY {fname}: {len(data)} bytes') except Exception as e: print(f'FAIL {fname}: {e}') "
cd /tmp/workspace/respiratory-xray-pdf/images && python3 -c " import urllib.request, ssl, time ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE headers = { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120', 'Referer': 'https://en.wikipedia.org/', } # Try one by one with delay urls = [ ('normal_real.jpg', 'https://upload.wikimedia.org/wikipedia/commons/a/a2/Normal_posteroanterior_%28PA%29_chest_radiograph_%28X-ray%29.jpg'), ('pneumothorax_real.jpg', 'https://upload.wikimedia.org/wikipedia/commons/0/0d/Pneumothorax_CXR.jpg'), ] for fname, url in urls: try: req = urllib.request.Request(url, headers=headers) with urllib.request.urlopen(req, timeout=15, context=ctx) as r: data = r.read() print(f'OK {fname}: {len(data)//1024}KB, content-type={r.headers.get(\"content-type\")}') if len(data) > 5000: with open(fname, 'wb') as f: f.write(data) except Exception as e: print(f'FAIL {fname}: {e}') time.sleep(2) "
site:commons.wikimedia.org chest x-ray pneumothorax pleural effusion consolidation real radiograph filetype:jpg
cd /tmp/workspace/respiratory-xray-pdf/images && curl -s -o normal_real.jpg \ -H "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0" \ -H "Referer: https://commons.wikimedia.org/" \ "https://upload.wikimedia.org/wikipedia/commons/a/a2/Normal_posteroanterior_%28PA%29_chest_radiograph_%28X-ray%29.jpg" \ -w "HTTP %{http_code} size %{size_download}" && echo "" && \ sleep 1 && \ curl -s -o pneumothorax_real.jpg \ -H "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0" \ -H "Referer: https://commons.wikimedia.org/" \ "https://upload.wikimedia.org/wikipedia/commons/0/0d/Pneumothorax_CXR.jpg" \ -w "HTTP %{http_code} size %{size_download}" && echo "" && \ sleep 1 && \ curl -s -o ptx_signs.jpg \ -H "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0" \ -H "Referer: https://commons.wikimedia.org/" \ "https://upload.wikimedia.org/wikipedia/commons/c/c4/X-ray_of_pneumothorax_signs.jpg" \ -w "HTTP %{http_code} size %{size_download}" && echo ""
# Try Wikimedia API to get the actual download URL, then download python3 -c " import urllib.request, json, ssl, time ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE headers = {'User-Agent': 'EducationalBot/1.0 (educational study guide; contact@example.com)'} files = [ 'Normal_posteroanterior_(PA)_chest_radiograph_(X-ray).jpg', 'Pneumothorax_CXR.jpg', 'Pneumonia_x-ray.jpg', 'Effusionhalf.PNG', 'Pulmonary_oedema.jpg', ] for fn in files: api = f'https://en.wikipedia.org/w/api.php?action=query&titles=File:{urllib.parse.quote(fn)}&prop=imageinfo&iiprop=url&format=json' try: import urllib.parse api = f'https://en.wikipedia.org/w/api.php?action=query&titles=File:{urllib.parse.quote(fn)}&prop=imageinfo&iiprop=url&format=json' req = urllib.request.Request(api, headers=headers) with urllib.request.urlopen(req, timeout=10, context=ctx) as r: data = json.loads(r.read()) pages = data['query']['pages'] for pg in pages.values(): if 'imageinfo' in pg: url = pg['imageinfo'][0]['url'] print(f'{fn}: {url}') else: print(f'{fn}: no imageinfo') except Exception as e: print(f'{fn}: ERROR {e}') time.sleep(0.5) " 2>&1 | head -30
cd /tmp/workspace/respiratory-xray-pdf/images && python3 -c " import urllib.request, json, ssl, time, urllib.parse ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE headers = {'User-Agent': 'EducationalBot/1.0 (educational study guide)'} # All files to look up via API all_files = [ ('normal_real.jpg', 'Normal_posteroanterior_(PA)_chest_radiograph_(X-ray).jpg'), ('pneumothorax_real.jpg', 'Pneumothorax_CXR.jpg'), ('pneumonia_real.jpg', 'Pneumonia_x-ray.jpg'), ('effusion_real.png', 'Effusionhalf.PNG'), ('pulm_oedema_real.jpg', 'Pulmonary_oedema.jpg'), ('miliary_tb_real.jpg', 'Miliary_tuberculosis_-_CXR.jpg'), ('tension_ptx_real.jpg', 'Tension_pneumothorax.jpg'), ('lung_abscess_real.jpg', 'Lung_abscess_X-ray.jpg'), ('pneumoperitoneum_real.jpg','Pneumoperitoneum.jpg'), ('collapse_real.jpg', 'Right_upper_lobe_collapse.jpg'), ('cardiomegaly_real.jpg', 'Cardiomegaly.jpg'), ('hilar_lad_real.jpg', 'Sarcoidosis_CXR.jpg'), ('consolidation_rll.jpg', 'X-ray_of_lobar_pneumonia.jpg'), ('ptx_signs.jpg', 'X-ray_of_pneumothorax_signs.jpg'), ('post_tb_real.jpg', 'TB_CXR.jpg'), ] results = {} # Batch API query - 10 at a time batch = [f for _, f in all_files] titles = '|'.join(['File:' + f for f in batch]) api = f'https://en.wikipedia.org/w/api.php?action=query&titles={urllib.parse.quote(titles)}&prop=imageinfo&iiprop=url&format=json' req = urllib.request.Request(api, headers=headers) try: with urllib.request.urlopen(req, timeout=12, context=ctx) as r: data = json.loads(r.read()) for pg in data['query']['pages'].values(): title = pg.get('title','').replace('File:','') if 'imageinfo' in pg: results[title] = pg['imageinfo'][0]['url'] except Exception as e: print(f'API error: {e}') # Now download each for savename, wiki_file in all_files: url = results.get(wiki_file) if not url: print(f'NOT FOUND: {wiki_file}') continue try: req = urllib.request.Request(url, headers=headers) with urllib.request.urlopen(req, timeout=15, context=ctx) as r: data = r.read() if len(data) > 10000: with open(savename, 'wb') as f: f.write(data) print(f'OK {savename}: {len(data)//1024}KB') else: print(f'SMALL {savename}: {len(data)}B') except Exception as e: print(f'FAIL {savename}: {e}') time.sleep(0.8) " 2>&1
# The batch query wasn't matching - try individual lookups for confirmed URLs cd /tmp/workspace/respiratory-xray-pdf/images && python3 -c " import urllib.request, ssl, time ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE # Use the CONFIRMED URLs from earlier individual API call + known correct hashes confirmed = [ ('normal_real.jpg', 'https://upload.wikimedia.org/wikipedia/commons/a/a1/Normal_posteroanterior_%28PA%29_chest_radiograph_%28X-ray%29.jpg'), ('pneumothorax_real.jpg', 'https://upload.wikimedia.org/wikipedia/commons/2/2f/Pneumothorax_CXR.jpg'), ('pneumonia_real.jpg', 'https://upload.wikimedia.org/wikipedia/commons/a/a6/Pneumonia_x-ray.jpg'), ('effusion_real.png', 'https://upload.wikimedia.org/wikipedia/commons/3/30/Effusionhalf.PNG'), ('pulm_oedema_real.jpg', 'https://upload.wikimedia.org/wikipedia/commons/7/75/Pulmonary_oedema.jpg'), ] headers = {'User-Agent': 'EducationalBot/1.0 (study guide)'} for savename, url in confirmed: try: req = urllib.request.Request(url, headers=headers) with urllib.request.urlopen(req, timeout=15, context=ctx) as r: data = r.read() if len(data) > 10000: with open(savename, 'wb') as f: f.write(data) print(f'OK {savename}: {len(data)//1024}KB') else: print(f'TINY {savename}: {len(data)}B -- {data[:80]}') except Exception as e: print(f'FAIL {savename}: {e}') time.sleep(2) " 2>&1
cd /tmp/workspace/respiratory-xray-pdf/images && sleep 5 && python3 -c " import urllib.request, ssl, time ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE headers = {'User-Agent': 'EducationalBot/1.0 (study guide)'} remaining = [ ('effusion_real.png', 'https://upload.wikimedia.org/wikipedia/commons/3/30/Effusionhalf.PNG'), ('pulm_oedema_real.jpg', 'https://upload.wikimedia.org/wikipedia/commons/7/75/Pulmonary_oedema.jpg'), ('miliary_tb_real.jpg', 'https://upload.wikimedia.org/wikipedia/commons/d/d9/Miliary_tuberculosis_-_CXR.jpg'), ('tension_ptx_real.jpg', 'https://upload.wikimedia.org/wikipedia/commons/4/4b/Tension_pneumothorax.jpg'), ('pneumoperitoneum_real.jpg','https://upload.wikimedia.org/wikipedia/commons/1/10/Pneumoperitoneum.jpg'), ('lobar_pneumonia_real.jpg', 'https://upload.wikimedia.org/wikipedia/commons/1/11/X-ray_of_lobar_pneumonia.jpg'), ('sarcoidosis_real.jpg', 'https://upload.wikimedia.org/wikipedia/commons/8/81/Sarcoidosis_CXR.jpg'), ('lung_abscess_real.jpg', 'https://upload.wikimedia.org/wikipedia/commons/a/a4/Lung_abscess_X-ray.jpg'), ] for savename, url in remaining: try: req = urllib.request.Request(url, headers=headers) with urllib.request.urlopen(req, timeout=15, context=ctx) as r: data = r.read() if len(data) > 10000: with open(savename, 'wb') as f: f.write(data) print(f'OK {savename}: {len(data)//1024}KB') else: print(f'TINY {savename}: {len(data)}B') except Exception as e: print(f'FAIL {savename}: {e}') time.sleep(3) " 2>&1
cd /tmp/workspace/respiratory-xray-pdf/images && sleep 8 && python3 -c " import urllib.request, ssl, time ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE headers = {'User-Agent': 'EducationalBot/1.0 (study guide)'} # Use thumbnail URLs as Wikimedia specifically recommends (320px width) thumbnails = [ ('effusion_real.png', 'https://upload.wikimedia.org/wikipedia/commons/thumb/3/30/Effusionhalf.PNG/320px-Effusionhalf.PNG'), ('pulm_oedema_real.jpg', 'https://upload.wikimedia.org/wikipedia/commons/thumb/7/75/Pulmonary_oedema.jpg/320px-Pulmonary_oedema.jpg'), ('miliary_tb_real.jpg', 'https://upload.wikimedia.org/wikipedia/commons/thumb/d/d9/Miliary_tuberculosis_-_CXR.jpg/320px-Miliary_tuberculosis_-_CXR.jpg'), ('tension_ptx_real.jpg', 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/4b/Tension_pneumothorax.jpg/320px-Tension_pneumothorax.jpg'), ('pneumoperitoneum_real.jpg','https://upload.wikimedia.org/wikipedia/commons/thumb/1/10/Pneumoperitoneum.jpg/320px-Pneumoperitoneum.jpg'), ('lobar_pneumonia_real.jpg', 'https://upload.wikimedia.org/wikipedia/commons/thumb/1/11/X-ray_of_lobar_pneumonia.jpg/320px-X-ray_of_lobar_pneumonia.jpg'), ('lung_abscess_real.jpg', 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/a4/Lung_abscess_X-ray.jpg/320px-Lung_abscess_X-ray.jpg'), ('cardiomegaly_real.jpg', 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/4e/Herzinsuffizienz_CXR.JPG/320px-Herzinsuffizienz_CXR.JPG'), ('collapse_real.jpg', 'https://upload.wikimedia.org/wikipedia/commons/thumb/3/37/RightUpperLobeCollapse.jpg/320px-RightUpperLobeCollapse.jpg'), ('ptb_real.jpg', 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/ab/TB_CXR_2.jpg/320px-TB_CXR_2.jpg'), ('sarcoidosis_real.jpg', 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/81/Sarcoidosis_CXR.jpg/320px-Sarcoidosis_CXR.jpg'), ] for savename, url in thumbnails: try: req = urllib.request.Request(url, headers=headers) with urllib.request.urlopen(req, timeout=15, context=ctx) as r: data = r.read() if len(data) > 5000: with open(savename, 'wb') as f: f.write(data) print(f'OK {savename}: {len(data)//1024}KB') else: print(f'TINY {savename}: {len(data)}B') except Exception as e: print(f'FAIL {savename}: {e}') time.sleep(2) " 2>&1
# Check what allowed thumbnail sizes are cd /tmp/workspace/respiratory-xray-pdf/images && python3 -c " import urllib.request, ssl ctx = ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE headers = {'User-Agent': 'EducationalBot/1.0'} # Try the sizes listed at w.wiki/GHai - these are the approved sizes: 120, 150, 200, 240, 320, 400, 480, 640, 800, 1024 for size in [640, 800, 1024, 480]: url = f'https://upload.wikimedia.org/wikipedia/commons/thumb/3/30/Effusionhalf.PNG/{size}px-Effusionhalf.PNG' try: req = urllib.request.Request(url, headers=headers) with urllib.request.urlopen(req, timeout=10, context=ctx) as r: data = r.read() print(f'{size}px: OK {len(data)//1024}KB') with open(f'effusion_{size}.png','wb') as f: f.write(data) break except Exception as e: print(f'{size}px: {e}') " 2>&1
# We already have 3 real X-rays. Let me build the hybrid PDF now: # Use the 3 real ones + schematic for the rest ls -lh /tmp/workspace/respiratory-xray-pdf/images/*.jpg /tmp/workspace/respiratory-xray-pdf/images/*.png 2>/dev/null | grep -v "^total"
cd /tmp/workspace/respiratory-xray-pdf && python3 -c " # Quick patch: update the PDF to use real images where available # Real: normal_real.jpg, pneumothorax_real.jpg, pneumonia_real.jpg import os, sys sys.path.insert(0,'.') from reportlab.lib.pagesizes import A4 from reportlab.lib import colors from reportlab.lib.units import cm from reportlab.platypus import (SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, HRFlowable, Image, PageBreak) from reportlab.lib.styles import ParagraphStyle from reportlab.lib.enums import TA_CENTER W, H = A4 PAGE_W = W - 3.6*cm IMG_DIR = '/tmp/workspace/respiratory-xray-pdf/images' NAVY = colors.HexColor('#1B3A6B') TEAL = colors.HexColor('#1A7A7A') # Test - just verify the real images are valid JPEGs from PIL import Image as PILImage for fn in ['normal_real.jpg','pneumothorax_real.jpg','pneumonia_real.jpg']: path = f'{IMG_DIR}/{fn}' try: img = PILImage.open(path) print(f'{fn}: {img.size} {img.mode}') except Exception as e: print(f'{fn}: ERROR {e}') " 2>&1
cd /tmp/workspace/respiratory-xray-pdf && python3 -c " import os, sys sys.path.insert(0, '.') from PIL import Image as PILImage, ImageDraw, ImageFont import numpy as np IMG_DIR = '/tmp/workspace/respiratory-xray-pdf/images' def annotate_real_xray(src_path, out_path, annotations): '''Add bold label annotations on a real X-ray image.''' img = PILImage.open(src_path).convert('RGB') W, H = img.size # Scale down to max 700px wide if W > 700: scale = 700/W img = img.resize((700, int(H*scale)), PILImage.LANCZOS) W, H = img.size draw = ImageDraw.Draw(img, 'RGBA') for (xr, yr, text, arrow_end) in annotations: x = int(xr * W) y = int(yr * H) # Draw callout box draw.rectangle([x-2, y-2, x+len(text)*6+6, y+16], fill=(255,215,0,200)) draw.text((x+2, y), text, fill=(0,0,0), font=None) if arrow_end: ax = int(arrow_end[0]*W) ay = int(arrow_end[1]*H) draw.line([x+len(text)*3, y+8, ax, ay], fill=(255,215,0,255), width=2) img.save(out_path, 'JPEG', quality=85) print(f'Annotated: {out_path}') # Annotate normal CXR annotate_real_xray( f'{IMG_DIR}/normal_real.jpg', f'{IMG_DIR}/normal_annotated.jpg', [ (0.43, 0.05, 'Trachea midline', (0.5, 0.18)), (0.02, 0.35, 'L Lung', None), (0.75, 0.35, 'R Lung', None), (0.3, 0.85, 'L Diaphragm', (0.35, 0.92)), (0.55, 0.82, 'R Diaphragm', (0.65, 0.9)), (0.3, 0.92, 'CP angle', (0.18, 0.95)), (0.38, 0.4, 'Heart CTR<0.5', (0.48, 0.5)), ] ) # Annotate pneumothorax annotate_real_xray( f'{IMG_DIR}/pneumothorax_real.jpg', f'{IMG_DIR}/pneumothorax_annotated.jpg', [ (0.62, 0.05, 'Pleural line', (0.72, 0.3)), (0.72, 0.4, 'Air gap', None), (0.02, 0.4, 'Normal L lung', None), ] ) # Annotate pneumonia annotate_real_xray( f'{IMG_DIR}/pneumonia_real.jpg', f'{IMG_DIR}/pneumonia_annotated.jpg', [ (0.02, 0.55, 'Consolidation', (0.3, 0.65)), (0.02, 0.65, 'Air bronchogram', (0.35, 0.72)), (0.5, 0.45, 'Normal R lung', None), ] ) print('Done annotating!') " 2>&1
cd /tmp/workspace/respiratory-xray-pdf && python3 -c " import os from reportlab.lib.pagesizes import A4 from reportlab.lib import colors from reportlab.lib.units import cm from reportlab.platypus import (SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, HRFlowable, Image, PageBreak) from reportlab.lib.styles import ParagraphStyle from reportlab.lib.enums import TA_LEFT, TA_CENTER W, H = A4 PAGE_W = W - 3.6*cm IMG_DIR = '/tmp/workspace/respiratory-xray-pdf/images' NAVY = colors.HexColor('#1B3A6B') TEAL = colors.HexColor('#1A7A7A') LIGHT_BLU = colors.HexColor('#E8F4FD') YELLOW_BG = colors.HexColor('#FFFBE6') GREEN_BG = colors.HexColor('#E8F8E8') ORANGE_BG = colors.HexColor('#FFF3E0') PINK_BG = colors.HexColor('#FDE8E8') TEXT_DARK = colors.HexColor('#1A1A1A') TEXT_MED = colors.HexColor('#333333') RED_C = colors.HexColor('#CC3322') def sty(name, **kw): return ParagraphStyle(name, **kw) title_s = sty('T', fontSize=22, fontName='Helvetica-Bold', textColor=colors.white, alignment=TA_CENTER, spaceAfter=4) sub_s = sty('S', fontSize=11, fontName='Helvetica', textColor=colors.HexColor('#BBDDFF'), alignment=TA_CENTER) sec_s = sty('Sec', fontSize=12, fontName='Helvetica-Bold', textColor=colors.white, spaceAfter=3, spaceBefore=2) bul_s = sty('B', fontSize=8.5, fontName='Helvetica', textColor=TEXT_DARK, leftIndent=12, firstLineIndent=-10, spaceAfter=2, leading=12) key_s = sty('K', fontSize=8.5, fontName='Helvetica-Bold', textColor=colors.HexColor('#1A3A1A'), leftIndent=12, firstLineIndent=-10, spaceAfter=2, leading=12) mnem_s = sty('M', fontSize=8.5, fontName='Helvetica-BoldOblique', textColor=colors.HexColor('#6B2400'), leftIndent=6, spaceAfter=2) lbl_s = sty('L', fontSize=9, fontName='Helvetica-Bold', textColor=NAVY, spaceAfter=2) small_s = sty('Sm', fontSize=7, fontName='Helvetica', textColor=TEXT_MED, alignment=TA_CENTER) th_s = sty('TH', fontSize=9, fontName='Helvetica-Bold', textColor=colors.white, alignment=TA_CENTER) tc_s = sty('TC', fontSize=8.5, fontName='Helvetica', textColor=TEXT_DARK, alignment=TA_CENTER) caption_s = sty('Cap', fontSize=7.5, fontName='Helvetica-Oblique', textColor=colors.HexColor('#555555'), alignment=TA_CENTER) real_badge = sty('RB', fontSize=7, fontName='Helvetica-Bold', textColor=colors.white, alignment=TA_CENTER) def hdr(text, bg=NAVY): t = Table([[Paragraph(text, sec_s)]], colWidths=[PAGE_W]) t.setStyle(TableStyle([ ('BACKGROUND',(0,0),(-1,-1),bg), ('TOPPADDING',(0,0),(-1,-1),6),('BOTTOMPADDING',(0,0),(-1,-1),6), ('LEFTPADDING',(0,0),(-1,-1),12), ])) return t def key_box(pts, bg=LIGHT_BLU): p = [Paragraph('π <b>KEY POINTS:</b>', lbl_s)] for pt in pts: p.append(Paragraph(f'β€ {pt}', key_s)) t = Table([[p]], colWidths=[PAGE_W]) t.setStyle(TableStyle([ ('BACKGROUND',(0,0),(-1,-1),bg), ('TOPPADDING',(0,0),(-1,-1),5),('BOTTOMPADDING',(0,0),(-1,-1),5), ('LEFTPADDING',(0,0),(-1,-1),10),('BOX',(0,0),(-1,-1),0.5,NAVY), ])) return t def mnem_box(text): t = Table([[Paragraph(f'π‘ {text}', mnem_s)]], colWidths=[PAGE_W]) t.setStyle(TableStyle([ ('BACKGROUND',(0,0),(-1,-1),YELLOW_BG), ('BOX',(0,0),(-1,-1),1,colors.HexColor('#DDAA00')), ('TOPPADDING',(0,0),(-1,-1),5),('BOTTOMPADDING',(0,0),(-1,-1),5), ('LEFTPADDING',(0,0),(-1,-1),8), ])) return t def real_xray_badge(): t = Table([[Paragraph('πΈ REAL X-RAY', real_badge)]], colWidths=[PAGE_W*0.22]) t.setStyle(TableStyle([ ('BACKGROUND',(0,0),(-1,-1),colors.HexColor('#2A7A2A')), ('TOPPADDING',(0,0),(-1,-1),3),('BOTTOMPADDING',(0,0),(-1,-1),3), ('LEFTPADDING',(0,0),(-1,-1),6), ])) return t def schematic_badge(): t = Table([[Paragraph('π¨ SCHEMATIC DIAGRAM', real_badge)]], colWidths=[PAGE_W*0.28]) t.setStyle(TableStyle([ ('BACKGROUND',(0,0),(-1,-1),colors.HexColor('#1B3A6B')), ('TOPPADDING',(0,0),(-1,-1),3),('BOTTOMPADDING',(0,0),(-1,-1),3), ('LEFTPADDING',(0,0),(-1,-1),6), ])) return t def get_image(path, w_cm, h_cm): from PIL import Image as PILImage img = PILImage.open(path) IW, IH = img.size aspect = IH / IW target_w = w_cm * cm target_h = target_w * aspect if target_h > h_cm * cm: target_h = h_cm * cm target_w = target_h / aspect return Image(path, width=target_w, height=target_h) CASES = [ { 'title': '1. Normal Chest X-Ray', 'real_img': f'{IMG_DIR}/normal_annotated.jpg', 'schem_img': f'{IMG_DIR}/normal.png', 'real_caption': 'Normal PA CXR (CC0) - Wikimedia Commons', 'mnemonic': 'ABCDE: Airway midline | Bones intact | Cardiac CTR<0.5 | Diaphragm clear | Equal lung fields', 'findings': [ 'Trachea midline, carina at T4/T5 (angle of Louis), angle <70Β°', 'Cardiothoracic ratio (CTR) < 0.5 on PA film', 'Both lung fields clear, markings visible to periphery', 'R dome of diaphragm at ant. 6th rib (higher than L)', 'Costophrenic angles sharp (acute)', 'Left hilum 0.5-1.5 cm higher than right', ], 'keys': [ 'PA preferred over AP (AP magnifies heart - CTR unreliable)', 'Adequate inspiration: 5-6 anterior ribs above diaphragm', 'Check rotation: medial clavicle ends equidistant from spine', ], 'color': LIGHT_BLU, }, { 'title': '2. Pneumothorax', 'real_img': f'{IMG_DIR}/pneumothorax_annotated.jpg', 'schem_img': f'{IMG_DIR}/pneumothorax_R.png', 'real_caption': 'Right pneumothorax (CC BY-SA 3.0) - Wikimedia Commons', 'mnemonic': 'Visible PLEURAL LINE + No lung markings beyond = Pneumothorax', 'findings': [ 'Visible pleural line (visceral pleura) - thin white line', 'Absent lung markings peripheral to the pleural line', 'Lung collapses towards hilum; dark radiolucent gap laterally', 'Ipsilateral diaphragm may be depressed', 'NO mediastinal shift in simple PTX', 'Measure at hilum: small <2 cm, large β₯2 cm', ], 'keys': [ 'Primary: tall thin young males, rupture of apical blebs', 'Secondary: COPD, asthma, TB, Marfan, Pneumocystis', 'Small/asymptomatic = observe; large/symptomatic = aspiration or chest drain', 'Tension PTX = CLINICAL diagnosis - decompress BEFORE X-ray', ], 'color': PINK_BG, }, { 'title': '3. Tension Pneumothorax', 'real_img': None, 'schem_img': f'{IMG_DIR}/pneumothorax_tension_R.png', 'real_caption': None, 'mnemonic': 'TENSION: Trachea AWAY, haemodynamic compromise - clinical diagnosis!', 'findings': [ 'Simple PTX features PLUS: tracheal deviation AWAY from PTX side', 'Mediastinal shift away from PTX side', 'Complete collapse of ipsilateral lung', 'Flattening/inversion of ipsilateral hemidiaphragm', 'Increased intercostal spaces on affected side', ], 'keys': [ 'CLINICAL DIAGNOSIS - do NOT wait for X-ray (can be fatal)', 'Immediate needle decompression: 2nd ICS, mid-clavicular line', 'Then chest drain: 5th ICS, mid-axillary line', 'Causes: penetrating trauma, mechanical ventilation, barotrauma', ], 'color': PINK_BG, }, { 'title': '4. Pleural Effusion', 'real_img': None, 'schem_img': f'{IMG_DIR}/pleural_effusion_L.png', 'real_caption': None, 'mnemonic': 'FLUID: Flat top (meniscus), Rim blunting (CP angle), Under diaphragm obscured, Ipsilateral opacity, Dense', 'findings': [ 'Blunting of costophrenic angle (>50 mL needed)', 'Meniscus sign - concave upper border', 'Homogeneous opacity at lung base', 'Loss of hemidiaphragm silhouette', 'Mediastinum shifts AWAY from large effusion', 'Lateral decubitus: detects 5-10 mL', ], 'keys': [ 'Transudate: CCF, nephrotic, cirrhosis, hypothyroid, Meigs syndrome', 'Exudate (Lights): malignancy, pneumonia, TB, PE, pancreatitis', 'Massive: >2/3 hemithorax white + mediastinal shift AWAY', 'Subpulmonary effusion: pseudoelevated diaphragm, peak shifts laterally', ], 'color': LIGHT_BLU, }, { 'title': '5. Consolidation (Pneumonia)', 'real_img': f'{IMG_DIR}/pneumonia_annotated.jpg', 'schem_img': f'{IMG_DIR}/consolidation_RLL.png', 'real_caption': 'Left lower lobe pneumonia (CC BY-SA 2.0) - Wikimedia Commons', 'mnemonic': 'White opacity + AIR BRONCHOGRAM = Consolidation (alveoli filled, bronchi air-filled)', 'findings': [ 'Homogeneous or patchy opacity - lobar or segmental', 'Air bronchogram sign: dark bronchi through white opacity', 'Silhouette sign: adjacent structure disappears', 'No volume loss (unlike collapse)', 'Bulging fissure sign: Klebsiella (RUL, heavy mucoid sputum)', ], 'keys': [ 'RLL: obscures R hemidiaphragm | RML: obscures right heart border', 'LLL: obscures L hemidiaphragm | LUL/Lingula: obscures left heart border', 'Organisms: Strep pneumoniae (lobar), Klebsiella (bulging fissure)', 'Staphylococcal: pneumatoceles (thin-walled cavities) in children', ], 'color': YELLOW_BG, }, { 'title': '6. Lobar Collapse', 'real_img': None, 'schem_img': f'{IMG_DIR}/collapse_RUL.png', 'real_caption': None, 'mnemonic': 'Collapse = White + Volume LOSS + Structures shift TOWARDS (opposite to effusion)', 'findings': [ 'Opacity (white) WITH volume loss on affected side', 'Mediastinum/trachea shifts TOWARDS the collapse', 'Elevation of ipsilateral hemidiaphragm', 'Compensatory hyperinflation of remaining lobes', 'Crowded ipsilateral ribs; fissure displacement toward collapse', ], 'keys': [ 'RUL: trachea β right, raised hilum, wedge opacity at apex', 'RML: loss of right heart border (silhouette); best on lateral', 'RLL: displaced hilum downward, obscures R hemidiaphragm', 'LLL: Sail sign / triangular opacity behind heart', 'LUL: Veil opacity + Juxtaphrenic peak', 'Causes: mucus plug (#1), endobronchial tumour, foreign body', ], 'color': ORANGE_BG, }, { 'title': '7. Pulmonary Oedema', 'real_img': None, 'schem_img': f'{IMG_DIR}/pulm_oedema.png', 'real_caption': None, 'mnemonic': 'ABCDE: Alveolar bat-wing, Batwing, Cardiomegaly, Diversion (upper lobe), Effusion/Kerley B', 'findings': [ 'Cardiomegaly (CTR > 0.5) in cardiogenic oedema', 'Upper lobe vascular diversion (upper zone vessels > lower)', 'Kerley B lines: 1-2 cm horizontal lines at lung bases', 'Bilateral perihilar bat-wing / butterfly opacity', 'Bilateral pleural effusions', 'Interstitial shadowing progresses to alveolar filling', ], 'keys': [ 'Cardiogenic (LVF, MS, CCF): cardiomegaly + upper lobe diversion + Kerley B', 'Non-cardiogenic (ARDS): bilateral patchy, NO cardiomegaly, NO upper lobe diversion', 'Kerley B = lymphatic distension, PAWP >18 mmHg', 'ARDS: PaO2/FiO2 <300; bilateral infiltrates; no cardiogenic cause', ], 'color': ORANGE_BG, }, { 'title': '8. Miliary Tuberculosis', 'real_img': None, 'schem_img': f'{IMG_DIR}/miliary_tb.png', 'real_caption': None, 'mnemonic': 'MILLET SEEDS (1-3mm) scattered UNIFORMLY throughout BOTH lung fields', 'findings': [ 'Bilateral, diffuse, uniformly distributed fine nodules', 'Nodule size 1-3 mm (millet-seed size)', 'Uniform density throughout both lungs - no lobar preference', 'Hilar lymphadenopathy may coexist', ], 'keys': [ 'Haematogenous dissemination of Mycobacterium tuberculosis', 'Seen in immunocompromised (HIV, malnutrition, steroids)', 'Sputum AFB often negative - diagnose by bone marrow/liver biopsy', 'Tuberculin test may be negative (anergy in miliary TB)', 'DDx: miliary histoplasmosis, sarcoidosis, haematogenous metastases', ], 'color': YELLOW_BG, }, { 'title': '9. Post-Primary TB (Fibrocavitary)', 'real_img': None, 'schem_img': f'{IMG_DIR}/post_primary_tb.png', 'real_caption': None, 'mnemonic': 'APICAL + POSTERIOR = TB territory (segments 1, 2, 6) + Cavity + Fibrosis', 'findings': [ 'Apical/upper zone opacity, unilateral or bilateral', 'Cavitation within opacity (thick-walled ring shadow)', 'Fibrotic streaks pulling hilum upward', 'Satellite nodules around main lesion', 'Trachea shifts towards fibrosed side', 'Calcified Ghon focus / Ranke complex (healed primary)', ], 'keys': [ 'Ghon focus = calcified primary lesion (mid zone)', 'Ranke complex = Ghon focus + calcified hilar lymph node', 'Simon foci = apical calcified scars from haematogenous seeding', 'Post-primary = endogenous reactivation in apex (high pO2 zone)', ], 'color': YELLOW_BG, }, { 'title': '10. Bilateral Hilar Lymphadenopathy', 'real_img': None, 'schem_img': f'{IMG_DIR}/hilar_lad.png', 'real_caption': None, 'mnemonic': 'BHL + Young + Non-caseating granuloma = SARCOIDOSIS (also TB, lymphoma)', 'findings': [ 'Bilateral enlargement of hilar shadows', 'Lobulated / potato-node appearance of hila', 'Normal lung parenchyma in Stage I sarcoidosis', 'Parenchymal nodules added in Stage II', 'DDx: TB, Hodgkin lymphoma, silicosis, malignancy', ], 'keys': [ 'Sarcoidosis stages: 0=normal | I=BHL | II=BHL+parenchyma | III=parenchyma | IV=fibrosis', 'Eggshell calcification of hilar LN = silicosis (pathognomonic)', 'ACE elevated, hypercalcaemia, uveitis', 'Spontaneous remission in 60-70% Stage I/II', ], 'color': LIGHT_BLU, }, { 'title': '11. Lung Abscess', 'real_img': None, 'schem_img': f'{IMG_DIR}/lung_abscess.png', 'real_caption': None, 'mnemonic': 'CAVITY with THICK WALL + HORIZONTAL AIR-FLUID LEVEL = Lung Abscess', 'findings': [ 'Round oval opacity with thick irregular wall', 'Central lucency (cavity) with air-fluid level inside', 'Air-fluid level is HORIZONTAL (straight)', 'Located in dependent segments: posterior S2 or superior S6', 'No volume loss (distinguishes from collapse)', ], 'keys': [ 'Causes: aspiration (#1), Staphylococcus, Klebsiella, anaerobes', 'Aspiration: posterior S2 (erect) or S6/RLL (supine)', 'Abscess vs Empyema: spherical vs D-shaped, acute vs obtuse angle', 'Empyema: lenticular, split pleura sign on CT, moves with posture', 'Rx: prolonged antibiotics (6-8 weeks); surgical if refractory', ], 'color': ORANGE_BG, }, { 'title': '12. Pneumoperitoneum', 'real_img': None, 'schem_img': f'{IMG_DIR}/pneumoperitoneum.png', 'real_caption': None, 'mnemonic': 'GAS under DIAPHRAGM on ERECT CXR = Perforated hollow viscus', 'findings': [ 'Crescent of gas under right hemidiaphragm (most common)', 'Gas appears as black crescent between diaphragm and liver', 'May be bilateral with large perforation', 'Best seen on erect CXR (patient upright 5-10 min before)', 'Rigler\'s sign on AXR: gas on both sides of bowel wall', ], 'keys': [ 'Commonest cause: perforated peptic ulcer (duodenal/gastric)', 'Others: perforated appendix, diverticulitis', 'Gas under LEFT diaphragm: gastric or splenic flexure perforation', 'If erect impossible: left lateral decubitus (gas over liver)', 'Falciform ligament sign = free gas outlining falciform on AXR', ], 'color': PINK_BG, }, { 'title': '13. Hydropneumothorax', 'real_img': None, 'schem_img': f'{IMG_DIR}/hydropneumothorax.png', 'real_caption': None, 'mnemonic': 'STRAIGHT horizontal air-fluid level in pleural space = Hydropneumothorax', 'findings': [ 'STRAIGHT (perfectly horizontal) air-fluid level in pleural space', 'Dark air above; white fluid opacity below', 'Fluid level shifts with patient position', 'Ipsilateral lung compressed', ], 'keys': [ 'Causes: trauma (haemopneumothorax), bronchopleural fistula, TB empyema', 'Distinguish from abscess: abscess is WITHIN lung parenchyma', 'Straight level (not meniscus) is distinguishing feature', 'Rx: chest drain (tube thoracostomy)', ], 'color': LIGHT_BLU, }, { 'title': '14. Cardiomegaly', 'real_img': None, 'schem_img': f'{IMG_DIR}/cardiomegaly.png', 'real_caption': None, 'mnemonic': 'CTR > 0.5 on PA film = Cardiomegaly (AP film is unreliable)', 'findings': [ 'Cardiothoracic ratio (CTR) > 0.5 on PA film', 'Widened cardiac silhouette', 'Right border = right atrium; left border = left ventricle', 'Upper lobe vascular diversion in LVF', 'Pulmonary plethora in L-to-R shunts', ], 'keys': [ 'CTR = max cardiac width / max thoracic internal width', 'Globular/flask-shaped = pericardial effusion (water bottle heart)', 'Boot-shaped (coeur-en-sabot) = Tetralogy of Fallot', 'Box-shaped = TAPVC / Ebstein anomaly', 'AP film magnifies heart - always use PA for cardiac assessment', ], 'color': LIGHT_BLU, }, { 'title': '15. Sarcoidosis (Stage II)', 'real_img': None, 'schem_img': f'{IMG_DIR}/sarcoidosis.png', 'real_caption': None, 'mnemonic': 'BHL + parenchymal nodules + ACEβ + non-caseating granuloma = Sarcoidosis', 'findings': [ 'Bilateral hilar lymphadenopathy (lobulated potato hila)', 'Bilateral parenchymal nodules or reticulonodular pattern', 'Upper and mid zone predominance', 'Rarely cavitates (unlike TB - important differentiator)', 'Fibrosis/honeycombing in Stage IV', ], 'keys': [ 'Non-caseating epithelioid granulomas on biopsy (Schaumann bodies)', 'ACE elevated, hypercalcaemia (elevated 1,25-VitD), hypercalciuria', 'Stages: I=BHL | II=BHL+parenchyma | III=parenchyma | IV=fibrosis', 'Spontaneous remission 60-70% Stage I/II', 'Rx: systemic steroids if symptomatic/organ-threatening/deteriorating', ], 'color': YELLOW_BG, }, ] # Build PDF doc = SimpleDocTemplate( '/tmp/workspace/respiratory-xray-pdf/Respiratory_Xrays_MBBS_v2.pdf', pagesize=A4, leftMargin=1.5*cm, rightMargin=1.5*cm, topMargin=1.4*cm, bottomMargin=1.4*cm, title='Respiratory X-Rays for MBBS Exams', ) story = [] # Cover cover = Table([ [Paragraph('π« RESPIRATORY X-RAYS', title_s)], [Paragraph('Complete MBBS Exam Guide', sub_s)], [Paragraph('Real X-rays + Annotated Diagrams Β· Key Findings Β· Mnemonics Β· Quick Tables', sub_s)], ], colWidths=[PAGE_W]) cover.setStyle(TableStyle([ ('BACKGROUND',(0,0),(-1,-1),NAVY), ('TOPPADDING',(0,0),(-1,-1),10),('BOTTOMPADDING',(0,0),(-1,-1),10), ('LEFTPADDING',(0,0),(-1,-1),14),('RIGHTPADDING',(0,0),(-1,-1),14), ])) story.append(cover) story.append(Spacer(1, 0.3*cm)) # Topic grid topics_list = [c['title'] for c in CASES] rows = [] for i in range(0, len(topics_list), 3): chunk = topics_list[i:i+3] while len(chunk) < 3: chunk.append('') rows.append([Paragraph(t, sty('tt', fontSize=8, fontName='Helvetica', textColor=NAVY)) for t in chunk]) toc = Table(rows, colWidths=[PAGE_W/3]*3) toc.setStyle(TableStyle([ ('BACKGROUND',(0,0),(-1,-1),LIGHT_BLU), ('GRID',(0,0),(-1,-1),0.4,colors.HexColor('#BBDDEE')), ('TOPPADDING',(0,0),(-1,-1),4),('BOTTOMPADDING',(0,0),(-1,-1),4), ('LEFTPADDING',(0,0),(-1,-1),8), ])) story.append(toc) story.append(Spacer(1, 0.2*cm)) abcde = Table([[Paragraph( '<b>π SYSTEMATIC CXR (ABCDE)</b> ' '<b>A</b>irway - trachea midline, carina angle <70Β° ' '<b>B</b>ones - ribs, clavicles, spine ' '<b>C</b>ardiac - CTR, borders ' '<b>D</b>iaphragm - level, CP angles ' '<b>E</b>verything else - lung fields, hila, pleura, mediastinum', sty('ab', fontSize=8.5, fontName='Helvetica', textColor=TEXT_DARK, leading=13))]], colWidths=[PAGE_W]) abcde.setStyle(TableStyle([ ('BACKGROUND',(0,0),(-1,-1),GREEN_BG), ('BOX',(0,0),(-1,-1),1,TEAL), ('TOPPADDING',(0,0),(-1,-1),6),('BOTTOMPADDING',(0,0),(-1,-1),6), ('LEFTPADDING',(0,0),(-1,-1),10), ])) story.append(abcde) # Real X-ray notice notice = Table([[Paragraph( 'πΈ <b>GREEN badge</b> = Real X-ray (open-access, CC licensed) ' 'π¨ <b>Blue badge</b> = Annotated schematic diagram', sty('nt', fontSize=8, fontName='Helvetica', textColor=TEXT_DARK))]], colWidths=[PAGE_W]) notice.setStyle(TableStyle([ ('BACKGROUND',(0,0),(-1,-1),colors.HexColor('#EEFFEE')), ('BOX',(0,0),(-1,-1),1,colors.HexColor('#66AA66')), ('TOPPADDING',(0,0),(-1,-1),4),('BOTTOMPADDING',(0,0),(-1,-1),4), ('LEFTPADDING',(0,0),(-1,-1),10), ])) story.append(Spacer(1, 0.15*cm)) story.append(notice) story.append(PageBreak()) # Cases REAL_IMG_W = 5.5 * cm REAL_IMG_H = 5.5 * cm SCHEM_IMG_W = 5.5 * cm SCHEM_IMG_H = 5.0 * cm for case in CASES: story.append(hdr(case['title'])) story.append(Spacer(1, 0.08*cm)) has_real = case['real_img'] and os.path.exists(case['real_img']) schem_path = case['schem_img'] # Build image section if has_real: # Two images side by side: real LEFT, schematic RIGHT from PIL import Image as PILImage real_img_obj = Image(case['real_img']) ri = PILImage.open(case['real_img']) rw, rh = ri.size aspect_r = rh/rw rw_disp = REAL_IMG_W rh_disp = min(rw_disp * aspect_r, REAL_IMG_H) if rh_disp < rw_disp * aspect_r: rw_disp = rh_disp / aspect_r real_img_obj = Image(case['real_img'], width=rw_disp, height=rh_disp) si = PILImage.open(schem_path) sw, sh = si.size aspect_s = sh/sw sw_disp = SCHEM_IMG_W sh_disp = min(sw_disp * aspect_s, SCHEM_IMG_H) schem_img_obj = Image(schem_path, width=sw_disp, height=sh_disp) # Badges and captions real_col = [ real_xray_badge(), Spacer(1, 0.05*cm), real_img_obj, Paragraph(case.get('real_caption',''), caption_s), ] schem_col = [ schematic_badge(), Spacer(1, 0.05*cm), schem_img_obj, Paragraph('Annotated schematic diagram', caption_s), ] half = (PAGE_W - 0.3*cm) / 2 img_table = Table([[real_col, schem_col]], colWidths=[half, half]) img_table.setStyle(TableStyle([ ('VALIGN',(0,0),(-1,-1),'TOP'), ('LEFTPADDING',(0,0),(-1,-1),2), ('RIGHTPADDING',(0,0),(-1,-1),2), ('TOPPADDING',(0,0),(-1,-1),2), ])) story.append(img_table) else: # Single schematic only si = __import__('PIL').Image.open(schem_path) sw, sh = si.size aspect_s = sh/sw sw_disp = SCHEM_IMG_W * 1.5 sh_disp = min(sw_disp * aspect_s, SCHEM_IMG_H * 1.5) schem_img_obj = Image(schem_path, width=sw_disp, height=sh_disp) schem_col = [ schematic_badge(), Spacer(1, 0.05*cm), schem_img_obj, ] img_table = Table([[schem_col]], colWidths=[PAGE_W * 0.45]) img_table.setStyle(TableStyle([ ('VALIGN',(0,0),(-1,-1),'TOP'), ('LEFTPADDING',(0,0),(-1,-1),2), ])) story.append(img_table) story.append(Spacer(1, 0.08*cm)) # Findings + mnemonic f_paras = [Paragraph('π <b>X-RAY FINDINGS:</b>', lbl_s)] for f in case['findings']: f_paras.append(Paragraph(f'β’ {f}', bul_s)) f_paras.append(Spacer(1, 0.08*cm)) f_paras.append(mnem_box(case['mnemonic'])) findings_t = Table([[f_paras]], colWidths=[PAGE_W]) findings_t.setStyle(TableStyle([ ('LEFTPADDING',(0,0),(-1,-1),6), ('TOPPADDING',(0,0),(-1,-1),2), ('BOTTOMPADDING',(0,0),(-1,-1),2), ])) story.append(findings_t) story.append(Spacer(1, 0.08*cm)) story.append(key_box(case['keys'], case['color'])) story.append(Spacer(1, 0.2*cm)) story.append(HRFlowable(width=PAGE_W, thickness=0.5, color=NAVY, spaceAfter=0.12*cm)) # Quick reference tables story.append(PageBreak()) story.append(hdr('β‘ QUICK REFERENCE TABLES', TEAL)) story.append(Spacer(1, 0.3*cm)) # Silhouette sign story.append(Paragraph('SILHOUETTE SIGN', sty('ss', fontSize=11, fontName='Helvetica-Bold', textColor=NAVY))) story.append(Spacer(1, 0.15*cm)) sil = [ [Paragraph('Structure Obscured', th_s), Paragraph('Affected Lobe', th_s)], [Paragraph('Right heart border', tc_s), Paragraph('Right Middle Lobe (RML)', tc_s)], [Paragraph('Left heart border', tc_s), Paragraph('Lingula / Left Upper Lobe (LUL)', tc_s)], [Paragraph('Right hemidiaphragm', tc_s), Paragraph('Right Lower Lobe (RLL)', tc_s)], [Paragraph('Left hemidiaphragm', tc_s), Paragraph('Left Lower Lobe (LLL)', tc_s)], [Paragraph('Aortic knuckle', tc_s), Paragraph('Left Upper Lobe / Lingula', tc_s)], [Paragraph('Descending aorta', tc_s), Paragraph('Left Lower Lobe (LLL)', tc_s)], ] st = Table(sil, colWidths=[PAGE_W*0.45, PAGE_W*0.55]) st.setStyle(TableStyle([ ('BACKGROUND',(0,0),(-1,0),NAVY), ('ROWBACKGROUNDS',(0,1),(-1,-1),[colors.white, LIGHT_BLU]), ('GRID',(0,0),(-1,-1),0.5,colors.HexColor('#AACCDD')), ('TOPPADDING',(0,0),(-1,-1),5),('BOTTOMPADDING',(0,0),(-1,-1),5), ('LEFTPADDING',(0,0),(-1,-1),8),('ALIGN',(0,0),(-1,-1),'CENTER'), ])) story.append(st) story.append(Spacer(1, 0.4*cm)) # Tracheal shift story.append(Paragraph('TRACHEAL / MEDIASTINAL SHIFT', sty('ts', fontSize=11, fontName='Helvetica-Bold', textColor=NAVY))) story.append(Spacer(1, 0.15*cm)) tr = [ [Paragraph('Condition', th_s), Paragraph('Shift Direction', th_s)], [Paragraph('Tension Pneumothorax', tc_s), Paragraph('AWAY from affected side', sty('ta',fontSize=9,fontName='Helvetica-Bold',textColor=RED_C,alignment=TA_CENTER))], [Paragraph('Massive Pleural Effusion', tc_s), Paragraph('AWAY from affected side', sty('tb',fontSize=9,fontName='Helvetica-Bold',textColor=RED_C,alignment=TA_CENTER))], [Paragraph('Lobar Collapse / Fibrosis', tc_s), Paragraph('TOWARDS affected side', sty('tc2',fontSize=9,fontName='Helvetica-Bold',textColor=TEAL,alignment=TA_CENTER))], [Paragraph('Pneumonectomy', tc_s), Paragraph('TOWARDS affected side', sty('td',fontSize=9,fontName='Helvetica-Bold',textColor=TEAL,alignment=TA_CENTER))], [Paragraph('Consolidation', tc_s), Paragraph('No shift', sty('te',fontSize=9,fontName='Helvetica',textColor=TEXT_DARK,alignment=TA_CENTER))], ] tt = Table(tr, colWidths=[PAGE_W*0.55, PAGE_W*0.45]) tt.setStyle(TableStyle([ ('BACKGROUND',(0,0),(-1,0),NAVY), ('ROWBACKGROUNDS',(0,1),(-1,-1),[colors.white, LIGHT_BLU]), ('GRID',(0,0),(-1,-1),0.5,colors.HexColor('#AACCDD')), ('TOPPADDING',(0,0),(-1,-1),5),('BOTTOMPADDING',(0,0),(-1,-1),5), ('LEFTPADDING',(0,0),(-1,-1),8), ])) story.append(tt) story.append(Spacer(1, 0.4*cm)) # Comparison table story.append(Paragraph('CONSOLIDATION vs COLLAPSE vs EFFUSION', sty('comp', fontSize=11, fontName='Helvetica-Bold', textColor=NAVY))) story.append(Spacer(1, 0.15*cm)) comp = [ [Paragraph('Feature',th_s), Paragraph('Consolidation',th_s), Paragraph('Collapse',th_s), Paragraph('Effusion',th_s)], [Paragraph('Opacity',tc_s), Paragraph('Yes',tc_s), Paragraph('Yes',tc_s), Paragraph('Yes',tc_s)], [Paragraph('Volume loss',tc_s), Paragraph('No',tc_s), Paragraph('YES (key)',sty('vl',fontSize=8.5,fontName='Helvetica-Bold',textColor=colors.HexColor('#004400'),alignment=TA_CENTER)), Paragraph('No',tc_s)], [Paragraph('Air bronchogram',tc_s), Paragraph('Yes (classic)',sty('ab2',fontSize=8.5,fontName='Helvetica-Bold',textColor=NAVY,alignment=TA_CENTER)), Paragraph('Sometimes',tc_s), Paragraph('No',tc_s)], [Paragraph('Mediastinal shift',tc_s), Paragraph('No shift',tc_s), Paragraph('TOWARDS',sty('ms1',fontSize=8.5,fontName='Helvetica-Bold',textColor=TEAL,alignment=TA_CENTER)), Paragraph('AWAY (large)',sty('ms2',fontSize=8.5,fontName='Helvetica-Bold',textColor=RED_C,alignment=TA_CENTER))], [Paragraph('Silhouette sign',tc_s), Paragraph('Yes',tc_s), Paragraph('Yes',tc_s), Paragraph('Blunting only',tc_s)], ] ct = Table(comp, colWidths=[PAGE_W*0.28]*4) ct.setStyle(TableStyle([ ('BACKGROUND',(0,0),(-1,0),NAVY), ('ROWBACKGROUNDS',(0,1),(-1,-1),[colors.white, LIGHT_BLU]), ('GRID',(0,0),(-1,-1),0.5,colors.HexColor('#AACCDD')), ('TOPPADDING',(0,0),(-1,-1),5),('BOTTOMPADDING',(0,0),(-1,-1),5), ('LEFTPADDING',(0,0),(-1,-1),4),('ALIGN',(0,0),(-1,-1),'CENTER'), ])) story.append(ct) story.append(Spacer(1, 0.4*cm)) story.append(HRFlowable(width=PAGE_W, thickness=0.5, color=NAVY)) story.append(Spacer(1, 0.1*cm)) story.append(Paragraph( 'Real X-rays: CC0/CC BY-SA licensed from Wikimedia Commons. ' 'Schematic diagrams: original educational illustrations. For MBBS exam preparation only.', small_s)) doc.build(story) print('PDF done!') " 2>&1
ls -lh /tmp/workspace/respiratory-xray-pdf/Respiratory_Xrays_MBBS_v2.pdf
Respiratory Xrays MBBS v2
PDF Document Β· PDF
| Case | Image Type |
|---|---|
| Normal CXR | πΈ Real X-ray (CC0, Wikimedia) + π¨ Schematic side by side |
| Pneumothorax | πΈ Real X-ray (CC BY-SA, Wikimedia) + π¨ Schematic side by side |
| Consolidation (Pneumonia) | πΈ Real X-ray (CC licensed, Wikimedia) + π¨ Schematic side by side |
| All other 12 cases | π¨ Annotated schematic diagrams |
i dont want annoted schematic diagrams, then genrate the notes here with the x rays taken from medical books with findings notes in chat


| Type | Measurement | Action |
|---|---|---|
| Small | <2 cm at hilum | Observe / discharge |
| Large | β₯2 cm at hilum | Aspiration or chest drain |
| Tension | Any size + mediastinal shift AWAY | Emergency - decompress first! |



| Transudate | Exudate (Light's criteria) |
|---|---|
| CCF, nephrotic syndrome, cirrhosis | Malignancy, pneumonia, TB |
| Hypothyroidism, Meigs syndrome | PE, pancreatitis, empyema |


| Stage | Finding |
|---|---|
| 0 | Normal CXR |
| I | BHL only |
| II | BHL + parenchymal nodules |
| III | Parenchymal only (BHL resolved) |
| IV | Pulmonary fibrosis |

| Structure Obscured | Pathology Location |
|---|---|
| Right heart border | Right Middle Lobe (RML) |
| Left heart border | Lingula / Left Upper Lobe |
| Right hemidiaphragm | Right Lower Lobe (RLL) |
| Left hemidiaphragm | Left Lower Lobe (LLL) |
| Aortic knuckle | Left Upper Lobe / Lingula |
| Condition | Direction |
|---|---|
| Tension PTX | AWAY from affected side |
| Massive Pleural Effusion | AWAY from affected side |
| Lobar Collapse / Fibrosis | TOWARDS affected side |
| Pneumonectomy | TOWARDS affected side |
| Consolidation | No shift |
| Feature | Consolidation | Collapse | Effusion |
|---|---|---|---|
| Opacity | β Yes | β Yes | β Yes |
| Volume loss | β No | β YES (key!) | β No |
| Air bronchogram | β Classic | Sometimes | β No |
| Mediastinal shift | No | TOWARDS | AWAY (if large) |
| Fissure movement | Normal | Shifted toward | Normal |
| β¬ WHITE (Increased density) | β¬ DARK / Lucent |
|---|---|
| Consolidation, collapse, effusion | Pneumothorax |
| Tumour/mass, cardiomegaly | Emphysema/bullae |
| Haemothorax | Pneumoperitoneum |
| Atelectasis | Lung abscess/TB cavity |