SuperAPI AI Directory

powerpoint-automation

Office & productivity

View repository

Updated 10-15 21:40

About this skill

PowerPoint Automation

AI-powered PPTX generation using Orchestrator-Workers pattern.

When to Use

  • PowerPoint, PPTX, create presentation, slides
  • Convert web articles/blog posts to presentations
  • Translate English PPTX to Japanese
  • Create presentations using custom templates

Quick Start

From Web Article:

Create a 15-slide presentation from: https://zenn.dev/example/article

From Existing PPTX:

Translate this presentation to Japanese: input/presentation.pptx

Workflow

TRIAGE → PLAN → PREPARE_TEMPLATE → EXTRACT → TRANSLATE → BUILD → REVIEW → DONE
Phase Script/Agent Description
EXTRACT extract_images.py Content → content.json
BUILD create_from_template.py Generate PPTX
REVIEW PPTX Reviewer Quality check

Key Scripts

→ references/SCRIPTS.md for complete reference

Script Purpose
create_from_template.py Generate PPTX from content.json (main)
reconstruct_analyzer.py Convert PPTX → content.json
extract_images.py Extract images from PPTX/web
validate_content.py Validate content.json schema
validate_pptx.py Detect text overflow

content.json (IR)

All agents communicate via this intermediate format:

{
  "slides": [
    { "type": "title", "title": "Title", "subtitle": "Sub" },
    { "type": "content", "title": "Topic", "items": ["Point 1"] }
  ]
}

→ references/schemas/content.schema.json

Templates

Template Purpose Layouts
assets/template.pptx Default (Japanese, 16:9) 4 layouts

template layout details

Index Name Category Purpose
0 Title Slide title Presentation opening
1 Title and Content content Standard content
2 1_Title and Content content Standard content (alternate version)
3 Section heading section Section divider

Usage example:

python scripts/create_from_template.py assets/template.pptx content.json output.pptx --config assets/template_layouts.json

Template management best practices

Organizing multiple designs (slide masters)

If a template PPTX contains multiple slide masters, output can become unstable.

How to check:

python scripts/create_from_template.py assets/template.pptx --list-layouts

Solution:

  1. Open the template in PowerPoint
  2. Select [View] → [Slide Master]
  3. Delete unnecessary slide masters
  4. After saving,template_layouts.json regenerate
python scripts/analyze_template.py assets/template.pptx

Hierarchical structure of content.json

To give bullet points a hierarchical structure (indentation), use the items rather than bullets format (items is displayed flat):

{"type": "content", "bullets": [
  {"text": "項目1", "level": 0},
  {"text": "詳細1", "level": 1},
  {"text": "項目2", "level": 0}
]}

Agents

→ references/agents/ for definitions

Agent Purpose
Orchestrator Pipeline coordination
Localizer Translation (EN ↔ JA)
PPTX Reviewer Final quality check

Design Principles

  • SSOT: content.json is canonical
  • SRP: Each agent/script has one purpose
  • Fail Fast: Max 3 retries per phase
  • Human in Loop: User confirms at PLAN phase

URL Format in Slides

Reference URLs must use "Title - URL" format for APPENDIX slides:

VPN Gateway の新機能 - https://learn.microsoft.com/ja-jp/azure/vpn-gateway/whats-new

→ references/content-guidelines.md for details

References

File Content
SCRIPTS.md Script documentation
USE_CASES.md Workflow examples
content-guidelines.md URL format, bullets
agents/ Agent definitions
schemas/ JSON schemas

Technical Content Addition (Azure/MS Topics)

When adding Azure/Microsoft technical content to slides, follow the same verification workflow as QA:

Workflow

[Content Request] → [Researcher] → [Reviewer] → [PPTX Update]
                         ↓              ↓
                   Docs MCP 検索    内容検証

Required Steps

  1. Research Phase: Use microsoft_docs_search / microsoft_docs_fetch to gather official information
  2. Review Phase: Verify the accuracy of content before adding to slides
  3. Build Phase: Update content.json and regenerate PPTX

Forbidden

  • ❌ Adding technical content without MCP verification
  • ❌ Skipping review for "simple additions"
  • ❌ Generating PPTX while PowerPoint has the file open

File Lock Prevention

Before generating PPTX, check if the file is locked:

# Check if file is locked
$path = "path/to/file.pptx"
try { [IO.File]::OpenWrite($path).Close(); "File is writable" }
catch { "File is LOCKED - close PowerPoint first" }

Shape-based Architecture Diagrams

When creating network/architecture diagrams, use PowerPoint shapes instead of ASCII art text boxes. ASCII art is unreadable in presentation mode.

Design Pattern

from pptx.enum.shapes import MSO_SHAPE
from pptx.dml.color import RGBColor
from pptx.util import Cm, Pt

# Color scheme
AZURE_BLUE = RGBColor(0, 120, 212)
LIGHT_BLUE = RGBColor(232, 243, 255)
ONPREM_GREEN = RGBColor(16, 124, 65)
LIGHT_GREEN = RGBColor(232, 248, 237)

# Outer frame (Azure VNet)
box = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, left, top, w, h)
box.fill.solid()
box.fill.fore_color.rgb = LIGHT_BLUE
box.line.color.rgb = AZURE_BLUE

# Dashed connector (tunnel)
conn = slide.shapes.add_connector(1, x1, y1, x2, y2)  # 1 = straight
conn.line.color.rgb = AZURE_BLUE
conn.line.dash_style = 2  # dash

Layout Tips

  • Use Cm() for positioning (not Inches()) — easier to reason about on metric-based slides
  • Leave at least 1.5cm vertical gap between Azure and on-premises zones for tunnel lines
  • Place labels inside boxes (not overlapping edges) to avoid visual clutter
  • Use color coding to distinguish zones: blue = Azure, green = on-premises, orange = cross-connect
  • For dual diagrams (side-by-side), split slide into left/right halves with 12cm left margin for the right diagram

Anti-patterns

❌ ASCII art in textboxes (unreadable in presentation mode)
❌ Overlapping shapes due to insufficient spacing
❌ Placing labels outside their parent containers
❌ Using absolute EMU values without helper functions

Hyperlink Batch Processing

Batch-add hyperlinks and page titles to all URLs in a presentation:

Workflow

import re
url_pattern = re.compile(r'(https?://[^\s\))]+)')

# 1. Build URL→Title map (use MCP docs_search or fetch_webpage)
URL_TITLES = {
    'https://learn.microsoft.com/.../whats-new': 'Azure VPN Gateway の新機能',
    ...
}

# 2. Iterate all runs and add hyperlinks
for slide in prs.slides:
    for shape in slide.shapes:
        if not shape.has_text_frame:
            continue
        for para in shape.text_frame.paragraphs:
            for run in para.runs:
                urls = url_pattern.findall(run.text)
                for url in urls:
                    if not (run.hyperlink and run.hyperlink.address):
                        run.hyperlink.address = url.rstrip('/')
                    # Prepend title if missing
                    title = URL_TITLES.get(url.rstrip('/'))
                    if title and title not in run.text:
                        run.text = f'{title}\n{url}'

Verification

hlink_count = sum(
    1 for slide in prs.slides
    for shape in slide.shapes if shape.has_text_frame
    for para in shape.text_frame.paragraphs
    for run in para.runs
    if run.hyperlink and run.hyperlink.address
)
print(f'Hyperlinks: {hlink_count}')

XML Direct Hyperlink Insertion (L16)

run.hyperlink.address does not work (e.g. after modifying the layout of an existing PPTX), it is more reliable to insert the XML element a:hlinkClick directly.

from lxml import etree
from pptx.oxml.ns import qn
from pptx.dml.color import RGBColor
import re

url_pattern = re.compile(r'(https?://[^\s\))」、。]+)')

for slide in prs.slides:
    for shape in slide.shapes:
        if not shape.has_text_frame:
            continue
        for para in shape.text_frame.paragraphs:
            for run in para.runs:
                if run._r.find(qn('a:hlinkClick')) is not None:
                    continue  # Already has link
                urls = url_pattern.findall(run.text)
                for url in urls:
                    url_clean = url.rstrip('.,;:')
                    # Add external relationship
                    rel = slide.part.relate_to(
                        url_clean,
                        'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink',
                        is_external=True)
                    # Get or create rPr element
                    rPr = run._r.find(qn('a:rPr'))
                    if rPr is None:
                        rPr = etree.SubElement(run._r, qn('a:rPr'))
                        t_elem = run._r.find(qn('a:t'))
                        if t_elem is not None:
                            run._r.remove(rPr)
                            run._r.insert(0, rPr)
                    # Add hlinkClick
                    hlinkClick = etree.SubElement(rPr, qn('a:hlinkClick'))
                    hlinkClick.set(qn('r:id'), rel)
                    # Visual styling
                    run.font.underline = True
                    run.font.color.rgb = RGBColor(0x00, 0x78, 0xD4)

Font Theme Token Resolution (ZIP-level)

python-pptx sometimes leaves theme tokens (+mn-ea, +mj-lt) unresolved, causing font fallback. Fix via ZIP-level string replacement:

import zipfile, re, shutil

FONT_JA = 'BIZ UDPゴシック'
FONT_LATIN = 'BIZ UDPGothic'

tmp = out + '.tmp'
shutil.copy2(out, tmp)
with zipfile.ZipFile(tmp, 'r') as zin:
    with zipfile.ZipFile(out, 'w', zipfile.ZIP_DEFLATED) as zout:
        for item in zin.infolist():
            data = zin.read(item.filename)
            if item.filename.endswith('.xml'):
                content = data.decode('utf-8')
                content = content.replace('+mn-ea', FONT_JA)
                content = content.replace('+mj-ea', FONT_JA)
                content = content.replace('+mn-lt', FONT_LATIN)
                content = content.replace('+mj-lt', FONT_LATIN)
                content = re.sub(
                    r'(<a:ea typeface=")[^"]*(")',
                    f'\\g<1>{FONT_JA}\\2', content
                )
                data = content.encode('utf-8')
            zout.writestr(item, data)
os.remove(tmp)

⚠️ Always do this after prs.save(), not before.

Section Management via XML

PowerPoint sections are stored as an extension in ppt/presentation.xml. python-pptx has no native section API.

Adding/Updating Sections

import re, uuid, zipfile

SECTION_URI = '{521415D9-36F7-43E2-AB2F-B90AF26B5E84}'
P14_NS = 'http://schemas.microsoft.com/office/powerpoint/2010/main'

# Read presentation.xml from ZIP
with zipfile.ZipFile(pptx_path) as z:
    pres_xml = z.read('ppt/presentation.xml').decode('utf-8')

# Ensure p14 namespace is declared
if f'xmlns:p14="{P14_NS}"' not in pres_xml:
    pres_xml = pres_xml.replace('<p:presentation',
        f'<p:presentation xmlns:p14="{P14_NS}"', 1)

# Extract slide IDs
slide_ids = re.findall(r'<p:sldId id="(\d+)"', pres_xml)

# Define sections: (name, start_slide_0based)
sections = [("表紙", 0), ("本編", 2), ("Appendix", 15)]

# Build section XML
section_parts = []
for idx, (name, start) in enumerate(sections):
    end = sections[idx+1][1] if idx+1 < len(sections) else len(slide_ids)
    refs = ''.join(f'<p14:sldId id="{slide_ids[i]}"/>'
                   for i in range(start, min(end, len(slide_ids))))
    sec_id = '{' + str(uuid.uuid4()).upper() + '}'
    section_parts.append(
        f'<p14:section name="{name}" id="{sec_id}">'
        f'<p14:sldIdLst>{refs}</p14:sldIdLst></p14:section>'
    )

# Insert into extLst
new_ext = (f'<p:ext uri="{SECTION_URI}">'
           f'<p14:sectionLst xmlns:p14="{P14_NS}">'
           + ''.join(section_parts)
           + '</p14:sectionLst></p:ext>')

# Write back to ZIP

Important Notes

  • The URI {521415D9-36F7-43E2-AB2F-B90AF26B5E84} is specific to the presenter's PowerPoint version; some versions use different URIs
  • Always remove existing section XML before inserting new ones (avoid duplicates)
  • Section changes only show in PowerPoint's slide sorter view after re-opening the file

Slide Layout Change (Safe Pattern)

python-pptx does NOT safely support direct layout swapping. Use the add-move-hide-cleanup pattern:

  1. add_slide(target_layout) — new slide at the end
  2. Set title text on the new slide's placeholder (placeholder_format.idx == 0)
  3. Move new slide to old slide's position via sldIdLst XML manipulation (reverse order)
  4. Hide & clear old slide (show='0', remove shapes)
  5. Save, re-open, delete hidden slides in a separate pass
# Step 3: Move new slide (last) before old slide
sldIdLst = prs.part._element.find(qn('p:sldIdLst'))
slides_list = list(sldIdLst)
new_el = slides_list[-1]
old_el = list(sldIdLst)[target_idx]
sldIdLst.remove(new_el)
sldIdLst.insert(list(sldIdLst).index(old_el), new_el)

# Step 4: Hide old slide (now at target_idx + 1)
old_slide._element.set('show', '0')
for shape in list(old_slide.shapes):
    shape._element.getparent().remove(shape._element)

Forbidden Patterns (★ Critical)

Pattern Problem Result
rel._target = new_layout.part without ZIP dedup Duplicate ZIP entries corrupt layout PowerPoint repair dialog
prs.part.drop_rel(rId) for slide deletion Orphan XML in ZIP Duplicate name warning → corruption
show='0' while indices shift Wrong slides hidden Content silently disappears
Changing layout but keeping empty placeholders Ghost text ("テキストを入力") visible Unprofessional appearance

Layout Change via rel._target (Safe Pattern with ZIP Dedup)

L12: rel._target method works safely if used together with ZIP dedup (LAST priority). python-pptx's save() produces duplicate entries, but this can be resolved in post-processing.

from collections import Counter
import zipfile

# 1. Change layout relationship
blank_part = layout_parts['Blank']
for rel in slide.part.rels.values():
    if 'slideLayout' in rel.reltype:
        rel._target = blank_part
        break

# 2. Save (will have duplicate ZIP entries)
prs.save(raw_path)

# 3. Dedup ZIP: keep LAST entry for duplicates (has updated rels)
with zipfile.ZipFile(raw_path, 'r') as zin:
    items = zin.infolist()
    counts = Counter(i.filename for i in items)
    dups = {n for n, c in counts.items() if c > 1}
    last_idx = {}
    for idx, item in enumerate(items):
        if item.filename in dups:
            last_idx[item.filename] = idx
    seen = set()
    with zipfile.ZipFile(final_path, 'w', zipfile.ZIP_DEFLATED) as zout:
        for idx, item in enumerate(items):
            if item.filename in dups:
                if idx == last_idx[item.filename]:
                    zout.writestr(item, zin.read(item.filename))
            elif item.filename not in seen:
                seen.add(item.filename)
                zout.writestr(item, zin.read(item.filename))

⚠️ With FIRST priority, the pre-change rels XML remains, and layout changes are not reflected. Always use LAST priority。

Ghost Placeholder Elimination

L13: When adding a slide to an existing PPTX, using theTitle and Content や Section Title layout causes empty placeholders ("Click to add text", "Click to add title") to be displayed as ghosts.

Solution: For new slides, use the Blank layout, and for the title, either enter a value in an existing placeholder or place it manually.

# Strategy: Fill placeholder with actual title, remove empty ones
ns_p = '{http://schemas.openxmlformats.org/presentationml/2006/main}'

for shape in slide.shapes:
    ph_elem = shape._element.find(f'.//{ns_p}ph')
    if ph_elem is None:
        continue
    ph_type = ph_elem.get('type', 'body')
    if ph_type == 'title' and not shape.text_frame.text.strip():
        # Fill with actual title text
        shape.text_frame.text = slide_title
        for run in shape.text_frame.paragraphs[0].runs:
            run.font.size = Pt(28)
            run.font.bold = True
    elif not shape.text_frame.text.strip():
        # Remove empty placeholder
        shape._element.getparent().remove(shape._element)

Consistent Title Positioning (Cross-slide Alignment)

L14: To align the title position of newly added slides with existing slides, measure the position of a reference slide and apply it to all slides.

# 1. Measure reference slide (e.g., slide 4)
ref_slide = prs.slides[3]
for shape in ref_slide.shapes:
    ph = shape._element.find(f'.//{ns_p}ph')
    if ph is not None and ph.get('type') == 'title':
        REF_LEFT = shape.left      # 588263
        REF_TOP = shape.top        # 457200
        REF_WIDTH = shape.width    # 11018520
        REF_HEIGHT = shape.height  # 553998
        break

# 2. Apply to all new slides
for slide in new_slides:
    title_ph.left = REF_LEFT
    title_ph.top = REF_TOP
    title_ph.width = REF_WIDTH
    title_ph.height = REF_HEIGHT

Preserving Original Layouts When Modifying Existing PPTX

L15: When reconstructing an existing PPTX, to preserve the layout of the original slides, create a mapping keyed on the title text.

# Build original layout map
prs_orig = Presentation('original.pptx')
orig_layouts = {}
for slide in prs_orig.slides:
    for shape in slide.shapes:
        if shape.has_text_frame and shape.text_frame.text.strip():
            title = shape.text_frame.text.replace('\n', ' ')[:50]
            orig_layouts[title] = slide.slide_layout.name
            break

# Apply: ORIG slides keep original layout, NEW slides use Blank
for slide in prs_edit.slides:
    title = get_slide_title(slide)
    if title in orig_layouts:
        restore_layout(slide, orig_layouts[title])
    else:
        set_layout(slide, 'Blank')

Safe Hidden Slide Cleanup

Delete hidden slides in a separate script/pass after saving, in reverse index order:

# Cleanup pass (separate from insertion)
prs = Presentation(saved_file)
sldIdLst = prs.part._element.find(qn('p:sldIdLst'))

for i, slide in enumerate(prs.slides):
    if slide._element.get('show') == '0':
        # Verify truly empty before deleting
        has_content = any(
            para.text.strip()
            for shape in slide.shapes if shape.has_text_frame
            for para in shape.text_frame.paragraphs
        )
        if has_content:
            del slide._element.attrib['show']  # Restore, not delete

# Delete empty hidden slides (reverse order)
for idx in reversed(empty_hidden_indices):
    el = list(sldIdLst)[idx]
    rId = el.get(qn('r:id'))
    sldIdLst.remove(el)
    prs.part.drop_rel(rId)

prs.save(output_new_name)  # Always save to NEW filename

Post-Processing (URL Linkification)

⚠️ create_from_template.py does not process footer_url. Post-processing required.

Items Requiring Post-Processing

Item Processing
footer_url Add linked textbox at slide bottom
URLs in bullets Convert to hyperlinks
Reference URLs Linkify URLs in Appendix

Save with Different Name (File Lock Workaround)

PowerPoint locks open files. Saving with the same name results in PermissionError , so always save under a different name:

prs.save('file_withURL.pptx')
Processing Suffix
URL added _withURL
Final version _final
Fixed version _fixed

16:9 Slide Centering (Known Issue)

L9: Presentation() 's default placeholder is based on 4:3 (25.4cm). Even if you change to 16:9 with slide_width = Cm(33.867) , the placeholder positions remain 4:3 → all slides are displayed shifted to the left.

Recommended pattern: Blank + manual placement

prs = Presentation()
prs.slide_width = Cm(33.867)  # 16:9
prs.slide_height = Cm(19.05)
SW = prs.slide_width

# Blank layout (プレースホルダなし) を使う
slide = prs.slides.add_slide(prs.slide_layouts[6])

# SW 基準で中央配置
margin = Cm(3)
tb = slide.shapes.add_textbox(margin, Cm(5), SW - margin * 2, Cm(3))
p = tb.text_frame.paragraphs[0]
p.text = "Centered Title"
p.alignment = PP_ALIGN.CENTER

Anti-patterns

❌ Layout 0-5 を 16:9 スライドで使う(プレースホルダが 25.4cm 基準で左寄り)
❌ slide_width 変更後にプレースホルダ位置を未調整のまま使う
✅ Blank レイアウト + add_textbox() で SW 基準の対称マージン配置
✅ テンプレート PPTX 自体が 16:9 で作成されていれば Layout 0-5 も OK

Template Corruption Recovery

L10: .gitattributes の *.pptx binary が git add later If added to , the binary is corrupted by CRLF/encoding conversion (UTF-8 replacement char EF BF BD is mixed in).

Diagnosis method

with open('template.pptx', 'rb') as f:
    data = f.read()
count = data.count(b'\xef\xbf\xbd')
print(f'UTF-8 replacement chars: {count}')  # 0 以外なら破損

Recovery method

# python-pptx で空テンプレートを再生成
from pptx import Presentation
prs = Presentation()
prs.slide_width = Cm(33.867)  # 16:9
prs.slide_height = Cm(19.05)
prs.save('template_new.pptx')
# → 11 layouts が自動生成される(4:3 プレースホルダ注意)

Preventive measures

  • .gitattributes は Before the first commit set to
  • When using an automatic installer such as skill-ninja,.gitignore Check the consistency of exclusion and binary management by

Video Embedding (ZIP Direct Manipulation)

L11: python-pptx does not officially support MP4 embedding. However, since PPTX is a ZIP, lxml + zipfile can be used to manipulate it directly and enable embedding.

Required operations

  1. slide XML: p:pic に a:videoFile + p14:media Inject
  2. slide rels: Add video/image relationships (rId)
  3. [Content_Types].xml: <Default Extension="mp4" ContentType="video/mp4"/> Add
  4. ZIP: ppt/media/ Store the MP4 file and poster image in

XML pattern

<p:pic>
  <p:nvPicPr>
    <p:cNvPr id="100" name="Video 1">
      <a:hlinkClick r:id="" action="ppaction://media"/>
    </p:cNvPr>
    <p:cNvPicPr><a:picLocks noChangeAspect="1"/></p:cNvPicPr>
    <p:nvPr>
      <a:videoFile r:link="rId10"/>
      <p:extLst>
        <p:ext uri="{DAA4B4D4-6D71-4841-9C94-3DE7FCFB9230}">
          <p14:media r:embed="rId11"/>
        </p:ext>
      </p:extLst>
    </p:nvPr>
  </p:nvPicPr>
  <p:blipFill>
    <a:blip r:embed="rId12"/>  <!-- poster image -->
    <a:stretch><a:fillRect/></a:stretch>
  </p:blipFill>
  <p:spPr>
    <a:xfrm>
      <a:off x="720000" y="1260000"/>
      <a:ext cx="10752120" cy="5058000"/>
    </a:xfrm>
    <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
  </p:spPr>
</p:pic>

Slide Rels (3 relationships required)

<Relationship Id="rId10" Type=".../relationships/video"
              Target="../media/video.mp4" TargetMode="Internal"/>
<Relationship Id="rId11" Type=".../2007/relationships/media"
              Target="../media/video.mp4"/>
<Relationship Id="rId12" Type=".../relationships/image"
              Target="../media/poster.png"/>

Notes

  • PowerPoint may ask "Do you want to repair?" (minor XML inconsistency) → It will be automatically repaired by selecting "Yes"
  • A poster image is required (thumbnail for display)
  • Note the file size: ZIP-compressing videos bloats the PPTX. LFS is recommended for Git management

XML Serialization Artifacts (★ Critical)

L17: python-pptx の save() reserializes all XML. At that time, it is converted to " → '(attribute quotes),\r\n → \n(newlines). This alone breaks the rendering of the layout background (image blipFill), and the slide becomescompletely whitemay become.

Cause

  • Slide master/layout <p:bg> が <a:blipFill r:embed="rId2"> reference the background image with
  • python-pptx reserializes the layout XML → PowerPoint dislikes the subtle differences and does not display the background

Solution: ZIP reconstruction pattern (restore the original)

After editing the slide content with python-pptx, restore the layout/master/theme/media filesbyte-for-byte from the original PPTXdo.

import zipfile
from collections import Counter

orig_files = {}
with zipfile.ZipFile('original.pptx', 'r') as z:
    for item in z.infolist():
        fn = item.filename
        if any(p in fn for p in ['slideLayout', 'slideMaster', 'theme', '/media/']):
            orig_files[fn] = z.read(fn)

with zipfile.ZipFile('edited.pptx', 'r') as zin:
    with zipfile.ZipFile('output.pptx', 'w', zipfile.ZIP_DEFLATED) as zout:
        seen = set()
        for item in zin.infolist():
            fn = item.filename
            if fn in seen:
                continue
            seen.add(fn)
            if fn in orig_files:
                zout.writestr(item, orig_files[fn])  # byte-for-byte original
            else:
                zout.writestr(item, zin.read(fn))
        for fn, data in orig_files.items():
            if fn not in seen:
                zout.writestr(fn, data)
                seen.add(fn)

SVG Content Type missing (L18)

python-pptx は [Content_Types].xml may drop SVG entries. If the theme/master references SVG, the display breaks.

ct_data = z.read('[Content_Types].xml').decode('utf-8')
if 'image/svg+xml' not in ct_data:
    ct_data = ct_data.replace(
        '</Types>',
        '<Default Extension="svg" ContentType="image/svg+xml"/></Types>'
    )

Safe TextBox content replacement (L19)

text_frame.clear() may cause inconsistency with python-pptx's internal paragraph list. Instead, txBody の <a:p> manipulate the element directly.

from lxml import etree
from pptx.oxml.ns import qn

def set_textbox_content(shape, lines):
    """Safe textbox rewrite via XML manipulation.
    lines: list of (text, bold, size_pt) tuples.
    """
    txBody = shape._element.find(qn('p:txBody'))
    if txBody is None:
        txBody = shape._element.find(qn('a:txBody'))
    
    # Remove existing paragraphs
    for old_p in txBody.findall(qn('a:p')):
        txBody.remove(old_p)
    
    # Add new paragraphs
    for text, bold, size in lines:
        p = etree.SubElement(txBody, qn('a:p'))
        r = etree.SubElement(p, qn('a:r'))
        rPr = etree.SubElement(r, qn('a:rPr'))
        rPr.set('lang', 'ja-JP')
        rPr.set('sz', str(int(size * 100)))
        if bold:
            rPr.set('b', '1')
        solidFill = etree.SubElement(rPr, qn('a:solidFill'))
        srgbClr = etree.SubElement(solidFill, qn('a:srgbClr'))
        srgbClr.set('val', '333333')
        t = etree.SubElement(r, qn('a:t'))
        t.text = text

Recommended workflow (when editing an existing PPTX)

1. python-pptx でスライド内容 (sp, txBody) を編集
2. prs.save('raw.pptx')
3. ZIP 再構築: layout/master/theme/media を原本から復元
4. [Content_Types].xml で SVG 等の欠落を補完
5. ZIP dedup (LAST 優先) で重複エントリを除去
6. output.pptx を別名で保存

Done Criteria

  • content.json generated and validated
  • PPTX file created successfully
  • No text overflow detected
  • User confirmed output quality