| name | libreoffice-impress |
| description | How to programmatically create, modify, and verify LibreOffice Impress (.pptx) files using Python python-pptx. For setup-gen and reward-gen agents. |
| user-invocable | false |
LibreOffice Impress — Python Manipulation Guide
This skill teaches setup-gen (create/modify pptx) and reward-gen (read/verify pptx) how to work with presentation files using pure Python code.
- Library:
python-pptx
- Install:
pip3 install python-pptx Pillow
0. GUI Startup on VM (for setup-gen)
After generating /home/user/<task_id>_initial.pptx, setup-gen should open it in LibreOffice Impress for the GUI agent.
CRITICAL VM LIMIT: GUI launches must set DISPLAY=:0.
import os
import shlex
import subprocess
import time
def launch_gui(command: str, delay_sec: float = 1.0):
env = os.environ.copy()
env["DISPLAY"] = ":0"
subprocess.Popen(
shlex.split(command),
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
env=env,
)
time.sleep(delay_sec)
launch_gui('libreoffice --impress "/home/user/<task_id>_initial.pptx"', delay_sec=2.0)
Guidelines:
- Open initial artifact, never golden artifact.
- Use non-blocking launch (
Popen) and short delays.
1. Creating & Writing Files (setup-gen)
Basic Presentation & Slides
from pptx import Presentation
from pptx.util import Inches, Pt, Emu
from pptx.enum.text import PP_ALIGN
from pptx.dml.color import RGBColor
from pptx.enum.shapes import MSO_SHAPE_TYPE
import shutil, copy, zipfile, xml.etree.ElementTree as ET
prs = Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[0])
slide.shapes.title.text = "My Presentation"
slide.placeholders[1].text = "Subtitle"
prs.slide_width = Inches(7.5)
prs.slide_height = Inches(10)
prs.save("/home/user/output.pptx")
Text Boxes & Font Formatting
slide = prs.slides.add_slide(prs.slide_layouts[5])
txBox = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(6), Inches(2))
tf = txBox.text_frame
tf.word_wrap = True
p = tf.paragraphs[0]
p.text = "Centered text"
p.alignment = PP_ALIGN.CENTER
run = p.runs[0]
run.font.name = "Arial"
run.font.size = Pt(24)
run.font.bold = True
run.font.italic = True
run.font.underline = True
run.font.color.rgb = RGBColor(0xFF, 0x00, 0x00)
run2 = p.add_run()
run2.text = " blue text"
run2.font.color.rgb = RGBColor(0x00, 0x00, 0xFF)
p2 = tf.add_paragraph()
p2.text = "Second paragraph"
p2.level = 1
Strikethrough & Bullets via XML
from pptx.oxml.ns import qn
run.font._element.attrib['strike'] = 'sngStrike'
pPr = p._p.get_or_add_pPr()
pPr.append(pPr.makeelement(qn('a:buChar'), {'char': '●'}))
buClr = pPr.makeelement(qn('a:buClr'), {})
buClr.append(buClr.makeelement(qn('a:srgbClr'), {'val': 'FF0000'}))
pPr.append(buClr)
Tables
table_shape = slide.shapes.add_table(4, 3, Inches(1), Inches(2), Inches(6), Inches(2))
table = table_shape.table
table.cell(0, 0).text = "Header"
table.columns[0].width = Inches(2)
for run in table.cell(0, 0).text_frame.paragraphs[0].runs:
run.font.bold = True
run.font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF)
Images
pic = slide.shapes.add_picture("/home/user/img.png", Inches(1), Inches(1), Inches(4), Inches(3))
pic = slide.shapes.add_picture("/home/user/img.png", 0, 0, prs.slide_width, prs.slide_height)
w, h = Inches(4), Inches(3)
pic = slide.shapes.add_picture("/home/user/img.png", (prs.slide_width-w)//2, (prs.slide_height-h)//2, w, h)
Background Color & Notes
fill = slide.background.fill
fill.solid()
fill.fore_color.rgb = RGBColor(0x1A, 0x1A, 0x2E)
slide.notes_slide.notes_text_frame.text = "Speaker notes here"
Duplicate & Reorder Slides
def duplicate_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.type is not None:
new_slide.background._element.getparent().replace(
new_slide.background._element, copy.deepcopy(slide.background._element))
return new_slide
def move_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")
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")
2. Reading & Verifying Files (reward-gen)
Loading & Basic Properties
prs = Presentation("/path/to/file.pptx")
num_slides = len(prs.slides)
is_portrait = prs.slide_width < prs.slide_height
Reading Text & Font Properties
for shape in prs.slides[0].shapes:
if shape.has_text_frame:
for para in shape.text_frame.paragraphs:
for run in para.runs:
if run.font.color.type is not None:
rgb = run.font.color.rgb
strike = run.font._element.attrib.get('strike', 'noStrike')
Verifying Shape Position & Size
def is_approximately_equal(val1, val2, tolerance=0.005):
if val1 == val2: return True
if val1 == 0 or val2 == 0: return val1 == val2
return abs(val1 - val2) / max(abs(val1), abs(val2)) <= tolerance
shape = prs.slides[0].shapes[0]
Verifying Tables
for shape in slide.shapes:
if shape.shape_type == MSO_SHAPE_TYPE.TABLE:
table = shape.table
for para in table.cell(0, 0).text_frame.paragraphs:
for run in para.runs:
pass
Verifying Images
for shape in slide.shapes:
if shape.shape_type == MSO_SHAPE_TYPE.PICTURE:
img_blob = shape.image.blob
Verifying Background & Notes
def get_slide_background_rgb(slide):
fill = slide.background.fill
if fill.type == 1: return fill.fore_color.rgb
elif fill.type == 5:
master_fill = slide.slide_layout.slide_master.background.fill
return master_fill.fore_color.rgb if master_fill.type == 1 else None
return None
def get_slide_notes(slide):
try: return slide.notes_slide.notes_text_frame.text.strip()
except: return ""
Getting All Text Shapes (Including Groups)
def get_all_text_shapes(slide):
def extract(shape):
results = []
if hasattr(shape, "text") and hasattr(shape, "text_frame"):
results.append(shape)
if hasattr(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)
def check_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 is not None and tr.find(f'.//p:{expected_type}', ns) is not None
except KeyError:
return False
Verifying Bullets & Page Number Colors (via ZIP/XML)
def extract_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 is not None else None
buChar = pPr.find('a:buChar', ns) if pPr is not None else None
char = buChar.get('char') if buChar is not None else None
text = "".join(t.text or "" for t in para.findall('.//a:t', ns))
if text.strip():
bullets.append((lvl, char, text))
return bullets
def get_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 is not None:
return clr.get('val')
return None
Comparison Helpers for reward.py
def check_font_prop(run, prop, expected):
actual = getattr(run.font, prop)
if prop in ('bold', 'italic'):
actual = False if actual is None else actual
expected = False if expected is None else expected
return actual == expected
def nonempty_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.
-
Image identity = binary blob. shape.image.blob matches byte-for-byte. Resize/reposition doesn't change the blob.
-
Position tolerance: 0.5%. Use relative tolerance abs(a-b)/max(a,b) <= 0.005 for shape comparisons.
-
Accessing slide.notes_slide creates one. If notes shouldn't exist, don't access this property during setup.