Files
mantle-ai-trader/skills/ppt/scripts/rearrange.py
2026-06-06 05:21:10 +00:00

145 lines
5.1 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Rearrange PowerPoint slides based on a sequence of indices.
Usage:
python rearrange.py template.pptx output.pptx 0,34,34,50,52
Slides are 0-indexed. Indices can repeat to duplicate slides.
"""
import argparse
import sys
from copy import deepcopy
from pathlib import Path
from pptx import Presentation
from pptx.oxml.ns import qn
def copy_slide(src_prs: Presentation, dst_prs: Presentation, index: int, dst_layouts: dict) -> None:
"""Append a copy of slide[index] from src_prs into dst_prs."""
src_slide = src_prs.slides[index]
# Match layout by name across all masters; fall back to first available layout
layout_name = src_slide.slide_layout.name
dst_layout = dst_layouts.get(layout_name) or dst_prs.slide_layouts[0]
new_slide = dst_prs.slides.add_slide(dst_layout)
# Clear auto-added placeholder shapes
for shape in list(new_slide.shapes):
sp = shape.element
sp.getparent().remove(sp)
# Copy ALL non-layout relationships from source and build old→new rId mapping.
# This covers images, media, charts, hyperlinks, videos, and any other embedded content.
# Without this, relationship attributes (r:embed, r:id, r:link) in copied shapes would
# reference rIds that don't exist in the new slide, causing PowerPoint repair dialogs.
R_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
SKIP_TYPES = {"slideLayout", "notesSlide", "slide"} # handled by python-pptx infrastructure
rId_mapping: dict = {}
for rel_id, rel in src_slide.part.rels.items():
rel_short = rel.reltype.split("/")[-1]
if rel_short in SKIP_TYPES:
continue
new_rId = new_slide.part.rels.get_or_add(rel.reltype, rel._target)
rId_mapping[rel_id] = new_rId
# Copy all shape elements
r_embed = f"{{{R_NS}}}embed"
r_id = f"{{{R_NS}}}id"
r_link = f"{{{R_NS}}}link"
for shape in src_slide.shapes:
new_el = deepcopy(shape.element)
new_slide.shapes._spTree.insert_element_before(new_el, "p:extLst")
# Remap ALL relationship references (images, charts, hyperlinks, video, etc.)
for el in new_el.iter():
for attr in (r_embed, r_id, r_link):
old_rId = el.get(attr)
if old_rId and old_rId in rId_mapping:
el.set(attr, rId_mapping[old_rId])
# Copy slide-level background if defined.
# p:bg lives inside p:cSld, not directly under p:sld.
src_cSld = src_slide.element.find(qn("p:cSld"))
dst_cSld = new_slide.element.find(qn("p:cSld"))
if src_cSld is not None and dst_cSld is not None:
src_bg = src_cSld.find(qn("p:bg"))
if src_bg is not None:
existing_bg = dst_cSld.find(qn("p:bg"))
if existing_bg is not None:
dst_cSld.remove(existing_bg)
dst_cSld.insert(0, deepcopy(src_bg))
def rearrange_presentation(
template_path: Path, output_path: Path, slide_sequence: list[int]
) -> None:
src_prs = Presentation(template_path)
total = len(src_prs.slides)
for idx in slide_sequence:
if idx < 0 or idx >= total:
raise ValueError(f"Slide index {idx} out of range (0{total - 1})")
# Build a fresh presentation with the same dimensions
dst_prs = Presentation(template_path)
# Remove all existing slides from dst_prs
sldIdLst = dst_prs.slides._sldIdLst
for sldId in list(sldIdLst):
rId = sldId.get(qn("r:id")) # must use full namespace via qn(), not bare "r:id"
if rId:
dst_prs.part.drop_rel(rId)
sldIdLst.remove(sldId)
# Search all slide masters for layout matching (templates may have multiple masters)
all_layouts = {
layout.name: layout
for master in dst_prs.slide_masters
for layout in master.slide_layouts
}
# Append slides in requested order (duplicates included)
for idx in slide_sequence:
copy_slide(src_prs, dst_prs, idx, all_layouts)
output_path.parent.mkdir(parents=True, exist_ok=True)
dst_prs.save(output_path)
print(f"Saved {len(slide_sequence)} slides → {output_path}")
def main() -> None:
parser = argparse.ArgumentParser(
description="Rearrange PowerPoint slides.",
epilog="Example: python rearrange.py template.pptx output.pptx 0,34,34,50,52",
)
parser.add_argument("template", help="Path to template PPTX")
parser.add_argument("output", help="Path for output PPTX")
parser.add_argument("sequence", help="Comma-separated 0-based slide indices")
args = parser.parse_args()
template_path = Path(args.template)
if not template_path.exists():
print(f"Error: Template not found: {args.template}")
sys.exit(1)
try:
slide_sequence = [int(x.strip()) for x in args.sequence.split(",")]
except ValueError:
print("Error: sequence must be comma-separated integers (e.g. 0,34,34,50,52)")
sys.exit(1)
try:
rearrange_presentation(template_path, Path(args.output), slide_sequence)
except ValueError as e:
print(f"Error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()