defduplicate_slide(prs, slide):
new_slide = prs.slides.add_slide(slide.slide_layout)
for shape in slide.shapes:
new_slide.shapes._spTree.append(copy.deepcopy(shape.element))
if slide.background.fill.typeisnotNone:
new_slide.background._element.getparent().replace(
new_slide.background._element, copy.deepcopy(slide.background._element))
return new_slide
defmove_slide(prs, old_idx, new_idx):
xml_slides = prs.slides._sldIdLst
slides = list(xml_slides)
el = slides[old_idx]
xml_slides.remove(el)
xml_slides.insert(new_idx, el) if new_idx < len(slides) else xml_slides.append(el)
Modifying Existing Files
shutil.copy("/home/user/original.pptx", "/home/user/modified.pptx") # ALWAYS copy first
prs = Presentation("/home/user/modified.pptx")
for shape in prs.slides[0].shapes:
if shape.has_text_frame:
for para in shape.text_frame.paragraphs:
for run in para.runs:
run.font.name = "Times New Roman"
prs.save("/home/user/modified.pptx")
for shape in prs.slides[0].shapes:
if shape.has_text_frame:
for para in shape.text_frame.paragraphs:
# para.text, para.level, para.alignment (PP_ALIGN or None)for run in para.runs:
# run.text, run.font.name, run.font.size (EMU), run.font.bold/italic/underline (True/False/None)if run.font.color.typeisnotNone:
rgb = run.font.color.rgb # RGBColor; str(rgb) → "FF0000"
strike = run.font._element.attrib.get('strike', 'noStrike')
for shape in slide.shapes:
if shape.shape_type == MSO_SHAPE_TYPE.TABLE:
table = shape.table
# len(table.rows), len(table.columns), table.cell(r, c).textfor para in table.cell(0, 0).text_frame.paragraphs:
for run in para.runs:
# run.font.bold, run.font.color.rgb, etc.pass
Verifying Images
for shape in slide.shapes:
if shape.shape_type == MSO_SHAPE_TYPE.PICTURE: # type 13# shape.left, shape.top, shape.width, shape.height
img_blob = shape.image.blob # bytes — use for identity comparison
Verifying Background & Notes
defget_slide_background_rgb(slide):
fill = slide.background.fill
if fill.type == 1: return fill.fore_color.rgb
elif fill.type == 5: # inherited from master
master_fill = slide.slide_layout.slide_master.background.fill
return master_fill.fore_color.rgb if master_fill.type == 1elseNonereturnNonedefget_slide_notes(slide):
try: return slide.notes_slide.notes_text_frame.text.strip()
except: return""
Getting All Text Shapes (Including Groups)
defget_all_text_shapes(slide):
defextract(shape):
results = []
ifhasattr(shape, "text") andhasattr(shape, "text_frame"):
results.append(shape)
ifhasattr(shape, 'shapes'):
for sub in shape.shapes:
results.extend(extract(sub))
return results
out = []
for shape in slide.shapes:
out.extend(extract(shape))
return out
Verifying Transitions (via ZIP/XML)
defcheck_transition(pptx_path, slide_idx, expected_type):
"""slide_idx is 0-based. expected_type: 'dissolve', 'fade', 'push', etc."""
ns = {'p': 'http://schemas.openxmlformats.org/presentationml/2006/main'}
with zipfile.ZipFile(pptx_path, 'r') as zf:
try:
with zf.open(f'ppt/slides/slide{slide_idx + 1}.xml') as f:
root = ET.parse(f).getroot()
tr = root.find('.//p:transition', ns)
return tr isnotNoneand tr.find(f'.//p:{expected_type}', ns) isnotNoneexcept KeyError:
returnFalse
Verifying Bullets & Page Number Colors (via ZIP/XML)
defextract_bullets(pptx_path, slide_idx):
ns = {'a': 'http://schemas.openxmlformats.org/drawingml/2006/main'}
bullets = []
with zipfile.ZipFile(pptx_path, 'r') as zf:
with zf.open(f'ppt/slides/slide{slide_idx + 1}.xml') as f:
root = ET.parse(f).getroot()
for para in root.findall('.//a:p', ns):
pPr = para.find('a:pPr', ns)
lvl = pPr.get('lvl') if pPr isnotNoneelseNone
buChar = pPr.find('a:buChar', ns) if pPr isnotNoneelseNone
char = buChar.get('char') if buChar isnotNoneelseNone
text = "".join(t.text or""for t in para.findall('.//a:t', ns))
if text.strip():
bullets.append((lvl, char, text))
return bullets
defget_page_number_color(pptx_path):
ns = {'a': 'http://schemas.openxmlformats.org/drawingml/2006/main',
'p': 'http://schemas.openxmlformats.org/presentationml/2006/main'}
with zipfile.ZipFile(pptx_path, 'r') as zf:
with zf.open('ppt/slideMasters/slideMaster1.xml') as f:
root = ET.parse(f).getroot()
for ph in root.findall('.//p:ph[@type="sldNum"]', ns):
clr = ph.find('.//a:solidFill//a:srgbClr', ns)
if clr isnotNone:
return clr.get('val') # e.g. "FF0000"returnNone
Comparison Helpers for reward.py
defcheck_font_prop(run, prop, expected):
actual = getattr(run.font, prop)
if prop in ('bold', 'italic'): # normalize None→False
actual = Falseif actual isNoneelse actual
expected = Falseif expected isNoneelse expected
return actual == expected
defnonempty_runs(para):
return [r for r in para.runs if (r.text or"").strip()]
3. Bitter Lessons
None vs False for bold/italic.run.font.bold returns None (inherit), True, or False. Treat None and False as equivalent ("not bold"). Same for italic.
Alignment None means LEFT.para.alignment is None when no explicit alignment is set, defaulting to left. Normalize None to PP_ALIGN.LEFT when comparing.
Strikethrough requires XML access. No run.font.strikethrough. Use run.font._element.attrib.get('strike', 'noStrike'). Values: 'noStrike', 'sngStrike', 'dblStrike'.
Empty paragraph run normalization. Empty paragraphs may have 0 runs or 1 empty run. Filter with [r for r in para.runs if (r.text or "").strip()] to avoid false mismatches.
Font color can be None or theme-based.run.font.color.rgb raises AttributeError if type is not RGB. Always check run.font.color.type is not None first, or wrap in try/except.
Shape positions use EMU, not inches. All values are EMU (914400 = 1 inch). Use Inches() / Emu(). Never compare to float inches.
Transitions & page number colors need ZIP/XML. Not accessible via python-pptx API. Parse ppt/slides/slideN.xml for transitions, ppt/slideMasters/slideMaster1.xml for page number colors.
Background fill type 5 = inherited from master. Fall back to slide.slide_layout.slide_master.background.fill for actual color.
Layout indices vary by template. When modifying existing files, use the slide's own layout instead of assuming index mappings.
Copy-then-modify for golden files.shutil.copy(initial, golden) then modify. From-scratch files lack theme/master data, causing comparison failures.
GROUP shapes hide nested text. Recursively traverse shape.shapes to find text in GROUP shapes. The evaluator checks these.