본문으로 건너뛰기
cccskills
무료GitHub에 공개

libreoffice-impress

How to programmatically create, modify, and verify LibreOffice Impress (.pptx) files using Python python-pptx. For setup-gen and reward-gen agents.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

xlang-ai/CUA-Gym스타 224(GitHub 224 + cccskills 0)
연동 도구PowerPoint

실행 점수 미검증

설치 방법 보기

포함된 파일(1)

  • SKILL.md12.5 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

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()
# Layouts: 0=Title Slide, 1=Title+Content, 5=Blank, 6=Title Only (default template)
slide = prs.slides.add_slide(prs.slide_layouts[0])
slide.shapes.title.text = "My Presentation"
slide.placeholders[1].text = "Subtitle"

# Portrait orientation: width < height
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])  # blank
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  # LEFT, CENTER, RIGHT, JUSTIFY

# Font on runs
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)

# Add run, add paragraph
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  # indent level (0-8), controls bullet depth

Strikethrough & Bullets via XML

from pptx.oxml.ns import qn

# Strikethrough (no direct API)
run.font._element.attrib['strike'] = 'sngStrike'  # 'noStrike', 'sngStrike', 'dblStrike'

# Custom bullet character + color
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)

# Style cell text
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

# Add with position/size; stretch to fill; center on slide
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

# Background
fill = slide.background.fill
fill.solid()
fill.fore_color.rgb = RGBColor(0x1A, 0x1A, 0x2E)

# Notes
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")  # 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")

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:
            # 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.type is not None:
                    rgb = run.font.color.rgb  # RGBColor; str(rgb) → "FF0000"
                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]
# shape.left, shape.top, shape.width, shape.height — all in EMU (914400 EMU = 1 inch)

Verifying Tables

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).text
        for 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

def get_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 == 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')  # e.g. "FF0000"
    return None

Comparison Helpers for reward.py

def check_font_prop(run, prop, expected):
    actual = getattr(run.font, prop)
    if prop in ('bold', 'italic'):  # normalize None→False
        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

  1. 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.

  2. 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.

  3. Strikethrough requires XML access. No run.font.strikethrough. Use run.font._element.attrib.get('strike', 'noStrike'). Values: 'noStrike', 'sngStrike', 'dblStrike'.

  4. 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.

  5. 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.

  6. Shape positions use EMU, not inches. All values are EMU (914400 = 1 inch). Use Inches() / Emu(). Never compare to float inches.

  7. 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.

  8. Background fill type 5 = inherited from master. Fall back to slide.slide_layout.slide_master.background.fill for actual color.

  9. Layout indices vary by template. When modifying existing files, use the slide's own layout instead of assuming index mappings.

  10. Copy-then-modify for golden files. shutil.copy(initial, golden) then modify. From-scratch files lack theme/master data, causing comparison failures.

  11. GROUP shapes hide nested text. Recursively traverse shape.shapes to find text in GROUP shapes. The evaluator checks these.

  12. Image identity = binary blob. shape.image.blob matches byte-for-byte. Resize/reposition doesn't change the blob.

  13. Position tolerance: 0.5%. Use relative tolerance abs(a-b)/max(a,b) <= 0.005 for shape comparisons.

  14. Accessing slide.notes_slide creates one. If notes shouldn't exist, don't access this property during setup.

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

blender

무료

How to programmatically create, modify, and verify Blender 3D scenes using Python bpy (headless background mode, .blend files, render output). For setup-gen and reward-gen agents.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

xlang-ai/CUA-Gym2242026년 8월 13일 업데이트

chrome

무료

How to programmatically set up, control, and verify Google Chrome browser state using Python (Playwright CDP, SQLite, JSON configs). For setup-gen and reward-gen agents.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

xlang-ai/CUA-Gym2242026년 8월 13일 업데이트

drawio

무료

How to programmatically set up, manipulate, and verify draw.io diagram state using Python (Playwright CDP, XML injection via Edit Diagram menu, DOM verification). For setup-gen and reward-gen agents.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

xlang-ai/CUA-Gym2242026년 8월 13일 업데이트

How to programmatically set up, manipulate, and verify Excalidraw whiteboard state using Python (Playwright CDP, localStorage JSON, scene elements). For setup-gen and reward-gen agents.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

xlang-ai/CUA-Gym2242026년 8월 13일 업데이트

gimp

무료

How to programmatically create, modify, and verify images using Python Pillow, NumPy, OpenCV, and scikit-image. For setup-gen and reward-gen agents.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

xlang-ai/CUA-Gym2242026년 8월 13일 업데이트

grafana

무료

How to set up and manipulate Grafana dashboard, alerting, and admin state via HTTP APIs for CUA-Gym tasks. For setup-gen and reward-gen agents.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

xlang-ai/CUA-Gym2242026년 8월 13일 업데이트

xlang-ai의 스킬 모두 보기

이 스킬의 문제 신고