don't change the format or other slides, but add slides on assessment using "look, feel move format" and also add a case scenario to each presentation
cd /tmp/workspace && ls -la; mkdir -p ortho && cd ortho && cp "/tmp/workspace/attachments/8c01cab5-8fd9-4c7e-ab40-8951484ba9c1/CLAVICLE FRACTURES.pptx" clav.pptx && cp "/tmp/workspace/attachments/8c01cab5-8fd9-4c7e-ab40-8951484ba9c1/GENU VARUM & VALGUM.pptx" genu.pptx && python -c " from pptx import Presentation for f in ['clav.pptx','genu.pptx']: p=Presentation(f) print('=====',f,p.slide_width,p.slide_height,len(p.slides.__iter__.__self__._sldIdLst)) for i,s in enumerate(p.slides,1): print('--- slide',i,s.slide_layout.name) for sh in s.shapes: t=sh.text_frame.text[:300].replace('\n',' | ') if sh.has_text_frame else '' print(' ',sh.shape_type,sh.name,sh.left,sh.top,sh.width,sh.height,repr(t)) "
"clavicle"[MeSH Terms] AND "fractures, bone"[MeSH Terms]
genu valgum OR genu varum OR Blount disease
cd /tmp/workspace/ortho && python /tmp/skills/pptx/scripts/office/unpack.py clav.pptx clav_x >/dev/null && python /tmp/skills/pptx/scripts/office/unpack.py genu.pptx genu_x >/dev/null && ls clav_x/ppt/slides genu_x/ppt/slides | head -70 && cat clav_x/ppt/slides/slide6.xml | head -150
cd /tmp/workspace/ortho && cat > dump.py <<'EOF' import sys from pptx import Presentation from lxml import etree f,n=sys.argv[1],int(sys.argv[2]) p=Presentation(f); s=p.slides[n-1] ns={'a':'http://schemas.openxmlformats.org/drawingml/2006/main'} bg=s._element.find('.//{http://schemas.openxmlformats.org/presentationml/2006/main}bg') print('BG', etree.tostring(bg).decode()[:300] if bg is not None else None) for sh in s.shapes: el=sh._element sp=el.find('{http://schemas.openxmlformats.org/presentationml/2006/main}spPr') fill=None;ln=None;geom=None if sp is not None: sf=sp.find('a:solidFill/a:srgbClr',ns); fill=sf.get('val') if sf is not None else None l=sp.find('a:ln',ns); ln=etree.tostring(l).decode()[:200].replace('xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" ','') if l is not None else None g=sp.find('a:prstGeom',ns); geom=g.get('prst') if g is not None else None av=sp.find('a:prstGeom/a:avLst/a:gd',ns); if av is not None: geom+=str(av.get('fmla')) print('#',sh.name,geom,'fill',fill,'ln',ln) if sh.has_text_frame and sh.text_frame.text.strip(): bp=el.find('.//a:bodyPr',ns) print(' bodyPr',dict(bp.attrib), [c.tag.split('}')[1] for c in bp]) for para in sh.text_frame.paragraphs[:2]: pp=para._p.find('a:pPr',ns) print(' pPr',etree.tostring(pp).decode()[:250].replace('xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" ','') if pp is not None else None) for r in para.runs[:1]: print(' run',etree.tostring(r._r.find('a:rPr',ns)).decode()[:300].replace('xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" ',''), '|', r.text[:50]) EOF python dump.py clav.pptx 6; echo =========; python dump.py clav.pptx 5 | tail -30
cd /tmp/workspace/ortho && python dump.py genu.pptx 7; echo ======; python dump.py genu.pptx 16 | head -60
cd /tmp/workspace/ortho && python - <<'EOF' from pptx import Presentation ns={'a':'http://schemas.openxmlformats.org/drawingml/2006/main'} g=Presentation('genu.pptx') for n in (6,13,10): s=g.slides[n-1] for sh in s.shapes: if sh.has_table: t=sh.table print('TABLE slide',n,len(t.rows),'x',len(t.columns),[c.width for c in t.columns],[r.height for r in t.rows]) for r in t.rows: print(' ',[c.text.replace('\n',' / ') for c in r.cells]) elif sh.has_text_frame and n==10 and sh.name=='TextBox 7': print(sh.text_frame.text) from lxml import etree s=g.slides[5] for sh in s.shapes: if sh.has_table: x=etree.tostring(sh._element,pretty_print=True).decode() print(x[:6000]) EOF python - <<'EOF' from pptx import Presentation from lxml import etree c=Presentation('/tmp/workspace/ortho/clav.pptx') s=c.slides[19] for sh in s.shapes: if sh.name in ('Rounded Rectangle 11','Rounded Rectangle 12','TextBox 13','TextBox 14'): print(etree.tostring(sh._element,pretty_print=True).decode()[:3500]); print('-----') s=c.slides[15] for sh in s.shapes: if sh.name in ('Rounded Rectangle 30','TextBox 31'): print(etree.tostring(sh._element,pretty_print=True).decode()[:3000]); print('-----') EOF
mkdir -p /tmp/workspace/ortho/output && cd /tmp/workspace/ortho && cat > build.py <<'EOF' import copy, re from xml.sax.saxutils import escape from pptx import Presentation from pptx.oxml import parse_xml from pptx.oxml.ns import nsdecls from pptx.util import Emu NS='{http://schemas.openxmlformats.org/drawingml/2006/main}' PNS='{http://schemas.openxmlformats.org/presentationml/2006/main}' WARN=[] def clone(prs, src): new = prs.slides.add_slide(src.slide_layout) for ph in list(new.shapes): ph._element.getparent().remove(ph._element) sp_new = new.shapes._spTree bg = src._element.find('.//'+PNS+'bg') if bg is not None: new._element.cSld.insert(0, copy.deepcopy(bg)) for ch in src.shapes._spTree: if ch.tag in (PNS+'nvGrpSpPr', PNS+'grpSpPr'): continue sp_new.append(copy.deepcopy(ch)) return new def by_name(slide, name): for sh in slide.shapes: if sh.name == name: return sh raise KeyError(name) def set_simple(sh, text): tf = sh.text_frame p0 = tf.paragraphs[0] for p in tf.paragraphs[1:]: p._p.getparent().remove(p._p) for r in p0.runs[1:]: r._r.getparent().remove(r._r) p0.runs[0].text = text def run_xml(text, size, bold, color): return ('<a:r %s><a:rPr lang="en-GB" sz="%d" b="%d" i="0" dirty="0"><a:solidFill><a:srgbClr val="%s"/></a:solidFill>' '<a:latin typeface="Calibri"/></a:rPr><a:t>%s</a:t></a:r>') % (nsdecls('a'), size, 1 if bold else 0, color, escape(text)) def fill(sh, paras, size, lead_col, txt_col, avail_in=None): """paras: list of str or (lead, rest).""" txBody = sh._element.find(PNS+'txBody') old = txBody.findall(NS+'p') pPr = copy.deepcopy(old[0].find(NS+'pPr')) for p in old: txBody.remove(p) for item in paras: p = parse_xml('<a:p %s/>' % nsdecls('a')) p.append(copy.deepcopy(pPr)) if isinstance(item, tuple): p.append(parse_xml(run_xml(item[0]+' ', size, True, lead_col))) p.append(parse_xml(run_xml(item[1], size, False, txt_col))) else: p.append(parse_xml(run_xml(item, size, False, txt_col))) txBody.append(p) # estimate height w_in = Emu(sh.width).inches - 0.1 - 0.26 cpl = w_in / (size/100/72*0.49) lines = 0 for item in paras: t = item if isinstance(item,str) else item[0]+' '+item[1] lines += max(1, -(-len(t)//int(cpl))) h = lines*size/100*1.2/72 + len(paras)*5/72 + 0.08 sh.height = Emu(int(h*914400)) if avail_in is not None and h > avail_in: WARN.append('OVERFLOW %s est %.2f in > avail %.2f in' % (sh.name, h, avail_in)) return h def move(prs, slide, after_idx): lst = prs.slides._sldIdLst ids = list(lst) el = ids[-1] # newly added is last lst.remove(el) lst.insert(after_idx, el) def renumber(prs, boxname): n = len(prs.slides) for i, s in enumerate(prs.slides, 1): if i == 1 or i == n: continue for sh in s.shapes: if sh.name == boxname and sh.has_text_frame and sh.text_frame.text.strip().isdigit(): set_simple(sh, str(i)) def emu_in(e): return Emu(e).inches # =============== CLAVICLE =============== GL, GT = '8C6D12', '2A2315' prs = Presentation('clav.pptx') orig = list(prs.slides) S6, S5, S20, S16 = orig[5], orig[4], orig[19], orig[15] # A1: look feel move (insert after original slide 6 -> position 6) a1 = clone(prs, S6) set_simple(by_name(a1,'TextBox 6'), 'Assessment: look, feel, move') for nm, t in (('TextBox 12','L'),('TextBox 17','F'),('TextBox 22','M')): set_simple(by_name(a1,nm), t) for nm, t in (('TextBox 13','Look'),('TextBox 18','Feel'),('TextBox 23','Move')): set_simple(by_name(a1,nm), t) fill(by_name(a1,'TextBox 15'), [ 'Expose both shoulders; view from front and behind', 'Drooped, shortened shoulder; arm supported by the other hand', 'Bruising, swelling, visible step or bump', 'Skin tenting, abrasion or open wound', 'Compare with the other side'], 1600, GL, GT, 3.45) fill(by_name(a1,'TextBox 20'), [ 'Palpate the whole clavicle, sternum to acromion', 'Tenderness, step and crepitus at the fracture', 'Skin tension over the bone ends', 'SC joint, AC joint, scapula, ribs', 'Radial pulse, capillary refill, sensation'], 1600, GL, GT, 3.45) fill(by_name(a1,'TextBox 25'), [ 'Gentle active shoulder motion: painful, limited', 'Do not force passive motion at the fracture', 'Elbow, wrist, hand: full active range', 'Brachial plexus motor screen: deltoid to hand', 'Watch breathing; chest expansion, air entry'], 1600, GL, GT, 3.45) move(prs, a1, 6) # A2: by region (after A1 -> position 7) a2 = clone(prs, S20) set_simple(by_name(a2,'TextBox 6'), 'Look, feel, move by region') for nm, t in (('TextBox 13','Medial third'),('TextBox 17','Midshaft'),('TextBox 21','Lateral third')): set_simple(by_name(a2,nm), t) fill(by_name(a2,'TextBox 14'), [ ('Look:','swelling or prominence at the sternal end; compare sides'), ('Feel:','tender SC joint; posterior displacement; both radial pulses'), ('Move:','ask about swallowing, breathing, hoarseness'), ('Alert:','posterior displacement can compress mediastinal structures: CT')], 1600, GL, GT, 4.4) fill(by_name(a2,'TextBox 18'), [ ('Look:','droop, shortening, bruising; tenting over a spike'), ('Feel:','step, crepitus, mobile fragments; skin tension'), ('Move:','painful active elevation; distal neurovascular screen'), ('Alert:','threatened skin needs urgent surgery')], 1600, GL, GT, 4.4) fill(by_name(a2,'TextBox 22'), [ ('Look:','swelling or step over the AC region'), ('Feel:','tender AC joint and coracoid; assess vertical stability'), ('Move:','pain on cross-body adduction and overhead reach'), ('Alert:','medial fragment riding up suggests unstable Neer II or V')], 1600, GL, GT, 4.4) move(prs, a2, 7) def case_slide(prs, title, lh, rh, left, right, strip, src, striptpl, size=1500, strip_size=1500): s = clone(prs, src) set_simple(by_name(s,'TextBox 6'), title) # shrink boxes TOPH = 3017520 by_name(s,'Rounded Rectangle 11').height = Emu(TOPH) by_name(s,'Rounded Rectangle 15').height = Emu(TOPH) set_simple(by_name(s,'TextBox 13'), lh); set_simple(by_name(s,'TextBox 17'), rh) avail = emu_in(1508760+TOPH-2075688) - 0.05 fill(by_name(s,'TextBox 14'), left, size, GL, GT, avail) fill(by_name(s,'TextBox 18'), right, size, GL, GT, avail) # strip copies rr = copy.deepcopy(by_name(striptpl,'Rounded Rectangle 30')._element) tb = copy.deepcopy(by_name(striptpl,'TextBox 31')._element) s.shapes._spTree.append(rr); s.shapes._spTree.append(tb) mx = max(int(e.get('id')) for e in s.shapes._spTree.iter(PNS+'cNvPr')) rr.find('.//'+PNS+'cNvPr').set('id', str(mx+1)); tb.find('.//'+PNS+'cNvPr').set('id', str(mx+2)) rrs = by_name(s,'Rounded Rectangle 30'); tbs = by_name(s,'TextBox 31') top = 1508760+TOPH+137160 rrs.left, rrs.top, rrs.width, rrs.height = Emu(640080), Emu(top), Emu(10908792), Emu(6263640-top) tbs.left, tbs.top, tbs.width = Emu(822960), Emu(top+60000), Emu(10515600) fill(tbs, strip, strip_size, GL, GT, emu_in(6263640-top)-0.1) return s c1 = case_slide(prs, 'Case scenario', 'The patient', 'Look, feel, move and imaging', ['28-year-old right-hand-dominant electrician; fit, non-smoker', 'Came off his road bike at speed onto the point of the left shoulder', 'Supports the arm; no tingling, breathlessness or difficulty swallowing', 'Wants to return to work and cycling as soon as possible'], [('Look:','drooped, shortened left shoulder; bruising; skin intact, no tenting'), ('Feel:','step and crepitus at midshaft; radial pulse and sensation normal'), ('Move:','painful, limited elevation; elbow, wrist, hand normal; chest clear'), ('Film:','AP with cephalic tilt: completely displaced, comminuted, 2.4 cm shortening')], ['Which findings matter most, and what must you exclude before deciding treatment?', 'Classify the fracture (Allman and Robinson)', 'Operative or nonoperative care: justify your choice with the evidence', 'Outline fixation, rehabilitation and what you will tell him'], S5, S16) move(prs, c1, 22) # after original slide 21 (now at index 22 after two inserts => positions: orig21 is 23rd) fixed below c2 = case_slide(prs, 'Case discussion', 'Assessment and classification', 'Management', ['Allman group I, Robinson 2B2: displaced and comminuted', 'No red flags: skin intact, pulses and sensation normal, chest clear; still check scapula and ribs on film', 'Nonunion and malunion risk: complete displacement, shortening above 2 cm, comminution', 'Manual worker and high demand: relative indication for surgery'], [('Decide:','shared decision; plate or nail lowers nonunion versus sling in displaced fractures'), ('Fix:','precontoured plate; lag screw for wedge fragments; six cortices each side'), ('Rehab:','sling and pendulums 0 to 2 weeks; active-assisted 2 to 6; strengthen when united; heavy work after 12 weeks'), ('Counsel:','prominent hardware, numbness, infection, nonunion; removal only if symptomatic')], ['Look, feel, move first: exclude red flags, then classify by third and displacement', 'Choose by the patient and the fracture together, not the radiograph alone'], S5, S16) move(prs, c2, 23) renumber(prs, 'TextBox 5') prs.save('output/CLAVICLE_FRACTURES_updated.pptx') print('CLAV slides', len(prs.slides)) for i,s in enumerate(prs.slides,1): t=[sh.text_frame.text for sh in s.shapes if sh.name in('TextBox 6',) and sh.has_text_frame] print(i,t[:1]) # =============== GENU =============== GL, GT = '8B0A1F', '2B1A1C' prs = Presentation('genu.pptx') orig = list(prs.slides) G7, G6, G16 = orig[6], orig[5], orig[15] b1 = clone(prs, G7) set_simple(by_name(b1,'TextBox 3'), 'Assessment: look, feel, move') for nm, t in (('TextBox 9','Look'),('TextBox 13','Feel'),('TextBox 17','Move')): set_simple(by_name(b1,nm), t) fill(by_name(b1,'TextBox 10'), [ 'Stand barefoot, feet together, patellae forward', 'Measure the gap: intercondylar (varus) or intermalleolar (valgus)', 'Symmetric or one-sided; proportionate to age', 'Gait: lateral thrust, foot progression', 'Height, weight, scars, rachitic signs'], 1600, GL, GT, 4.6) fill(by_name(b1,'TextBox 14'), [ 'Medial proximal tibial prominence (Blount)', 'Physeal tenderness or warmth; knee effusion', 'Bony lumps: osteochondroma, Ollier disease', 'Wrist widening and rachitic rosary', 'Hips and spine for associated pathology'], 1600, GL, GT, 4.6) fill(by_name(b1,'TextBox 18'), [ 'Knee range: full; check flexion contracture', 'Fixed or correctable deformity?', 'Varus and valgus stress for ligament laxity', 'Hip motion including rotation', 'Thigh-foot angle for tibial torsion; limb length'], 1600, GL, GT, 4.6) move(prs, b1, 7) b2 = clone(prs, G6) set_simple(by_name(b2,'TextBox 3'), 'Look, feel, move: reassuring versus worrying') tbl_sh = by_name(b2,'Table 7'); tbl = tbl_sh.table rows = [ ['Step','Reassuring','Worrying'], ['Look','Symmetric, mild, proportionate to age; smooth bow','Asymmetric, severe or progressive; angulation at proximal tibia'], ['Feel','Non-tender; no lumps; normal wrists and ribs','Medial tibial prominence, physeal tenderness, wrist widening'], ['Move','Full painless motion; stable ligaments; normal hips','Fixed deformity, laxity, limited hip motion, abnormal torsion'], ['Gait','Normal, or mild in-toeing','Lateral thrust, limp, limb length discrepancy'], ['Measure','Gap settles with age; within physiologic range','Varus beyond 2 to 3 years; valgus over 15 degrees: image it'], ] trs = tbl._tbl.findall(NS+'tr') tbl._tbl.remove(trs[-1]) # 7 rows -> 6 for r, row in zip(tbl.rows, rows): for c, t in zip(r.cells, row): c.text_frame.paragraphs[0].runs[0].text = t tbl_sh.height = Emu(384048+5*731520) set_simple(by_name(b2,'TextBox 8'), 'Findings guide the work-up; confirm with a standing long-leg film, patellae forward.') move(prs, b2, 8) def case_g(prs, title, lh, rh, left, right, strip, size=1500): s = clone(prs, G16) set_simple(by_name(s,'TextBox 3'), title) set_simple(by_name(s,'TextBox 9'), lh); set_simple(by_name(s,'TextBox 13'), rh) avail = emu_in(1417320+3108960-2057400) - 0.05 fill(by_name(s,'TextBox 10'), left, size, GL, GT, avail) fill(by_name(s,'TextBox 14'), right, size, GL, GT, avail) fill(by_name(s,'TextBox 16'), strip, size, GL, GT, emu_in(1508760)-0.1) return s d1 = case_g(prs, 'Case scenario', 'The child', 'Look, feel, move and investigations', ['2 year 8 month old boy; walked at 10 months; weight above the 97th centile', 'Bowlegs worsening over 8 months, both knees; painless', 'Normal diet, vitamin D given; no family history of rickets or short stature', 'Otherwise well; no trauma or infection'], [('Look:','obese; marked symmetric varus; lateral thrust in stance; internal tibial torsion'), ('Feel:','prominence over medial proximal tibia; no effusion; wrists normal'), ('Move:','full knee and hip motion; stable ligaments; equal limb lengths'), ('Tests:','Ca, PO4, ALP, vitamin D normal; standing film: MDA 20 and 18 degrees, medial beaking')], ['Which look, feel, move findings separate this from physiologic bowing?', 'Most likely diagnosis, and which causes must you exclude?', 'How do you stage it (Langenskiold)?', 'What do you offer now, and what if it progresses?']) move(prs, d1, 23) d2 = case_g(prs, 'Case discussion', 'Diagnosis and reasoning', 'Management', ['Infantile Blount disease: early walker, obese, bilateral, medial tibial prominence', 'MDA above 16 degrees with beaking is pathologic (physiologic bowing is below 11)', 'Langenskiold stage II (irregular, beaked medial metaphysis)', 'Rickets excluded by normal bloods and wrists; dysplasia by normal proportions'], [('Now:','weight control and close review with standing long-leg films'), ('Brace:','an option under 3 years, but less likely to work if bilateral and obese; compliance is hard'), ('Progression:','stage II over 3 years or stage III: proximal tibial and fibular valgus osteotomy, overcorrected'), ('Counsel:','recurrence risk; guided growth plates fail more in obese children')], ['Do not call bowing physiologic without checking age, symmetry, MDA and bloods', 'Look, feel, move, then image; treat the cause and the stage']) move(prs, d2, 24) renumber(prs, 'TextBox 4') prs.save('output/GENU_VARUM_VALGUM_updated.pptx') print('GENU slides', len(prs.slides)) for i,s in enumerate(prs.slides,1): t=[sh.text_frame.text for sh in s.shapes if sh.name in('TextBox 3','TextBox 6') and sh.has_text_frame] print(i,t[:1]) print('\n'.join(WARN) or 'no overflow warnings') EOF python build.py
cd /tmp/workspace/ortho && sed -i 's/move(prs, c1, 22).*/move(prs, c1, 23)/; s/move(prs, c2, 23)/move(prs, c2, 24)/' build.py && python build.py | sed -n '1,32p' | tr '\n' ' ' && python - <<'EOF' from pptx import Presentation for f,box in (('output/CLAVICLE_FRACTURES_updated.pptx','TextBox 5'),('output/GENU_VARUM_VALGUM_updated.pptx','TextBox 4')): p=Presentation(f); print('\n',f,len(p.slides)) nums=[] for i,s in enumerate(p.slides,1): for sh in s.shapes: if sh.name==box and sh.text_frame.text.strip().isdigit(): nums.append((i,sh.text_frame.text)) print([n for n in nums if str(n[0])!=n[1]] or 'numbers consistent') # out-of-bounds check for i,s in enumerate(p.slides,1): for sh in s.shapes: if sh.left is not None and sh.top+sh.height>6858000+1 and i not in (1,2,3) and sh.name.startswith('TextBox'): print('beyond slide', i, sh.name, (sh.top+sh.height)/914400) EOF
cd /tmp/workspace/ortho && python - <<'EOF' from pptx import Presentation import re def sig(s): return [(sh.name, sh.text_frame.text if sh.has_text_frame else '') for sh in s.shapes] for o,n,box,newidx in (('clav.pptx','output/CLAVICLE_FRACTURES_updated.pptx','TextBox 5',{7,8,24,25}),('genu.pptx','output/GENU_VARUM_VALGUM_updated.pptx','TextBox 4',{8,9,24,25})): a=list(Presentation(o).slides); b=[s for i,s in enumerate(Presentation(n).slides,1) if i not in newidx] diff=0 for x,y in zip(a,b): sx=[t for t in sig(x) if not (t[0]==box and t[1].isdigit())]; sy=[t for t in sig(y) if not (t[0]==box and t[1].isdigit())] if sx!=sy: diff+=1 print(o,len(a),len(b),'slides with changed content:',diff) EOF python -c " import zipfile for f in ['output/CLAVICLE_FRACTURES_updated.pptx','output/GENU_VARUM_VALGUM_updated.pptx']: print(f, zipfile.ZipFile(f).testzip())"
CLAVICLE FRACTURES updated
Presentation · PPTX
GENU VARUM VALGUM updated
Presentation · PPTX