main
1#!/usr/bin/env python3
2"""
3Rearrange PowerPoint slides based on a sequence of indices.
4
5Usage:
6 python rearrange.py template.pptx output.pptx 0,34,34,50,52
7
8This will create output.pptx using slides from template.pptx in the specified order.
9Slides can be repeated (e.g., 34 appears twice).
10"""
11
12import argparse
13import shutil
14import sys
15from copy import deepcopy
16from pathlib import Path
17
18import six
19from pptx import Presentation
20
21
22def main():
23 parser = argparse.ArgumentParser(
24 description="Rearrange PowerPoint slides based on a sequence of indices.",
25 formatter_class=argparse.RawDescriptionHelpFormatter,
26 epilog="""
27Examples:
28 python rearrange.py template.pptx output.pptx 0,34,34,50,52
29 Creates output.pptx using slides 0, 34 (twice), 50, and 52 from template.pptx
30
31 python rearrange.py template.pptx output.pptx 5,3,1,2,4
32 Creates output.pptx with slides reordered as specified
33
34Note: Slide indices are 0-based (first slide is 0, second is 1, etc.)
35 """,
36 )
37
38 parser.add_argument("template", help="Path to template PPTX file")
39 parser.add_argument("output", help="Path for output PPTX file")
40 parser.add_argument(
41 "sequence", help="Comma-separated sequence of slide indices (0-based)"
42 )
43
44 args = parser.parse_args()
45
46 # Parse the slide sequence
47 try:
48 slide_sequence = [int(x.strip()) for x in args.sequence.split(",")]
49 except ValueError:
50 print(
51 "Error: Invalid sequence format. Use comma-separated integers (e.g., 0,34,34,50,52)"
52 )
53 sys.exit(1)
54
55 # Check template exists
56 template_path = Path(args.template)
57 if not template_path.exists():
58 print(f"Error: Template file not found: {args.template}")
59 sys.exit(1)
60
61 # Create output directory if needed
62 output_path = Path(args.output)
63 output_path.parent.mkdir(parents=True, exist_ok=True)
64
65 try:
66 rearrange_presentation(template_path, output_path, slide_sequence)
67 except ValueError as e:
68 print(f"Error: {e}")
69 sys.exit(1)
70 except Exception as e:
71 print(f"Error processing presentation: {e}")
72 sys.exit(1)
73
74
75def duplicate_slide(pres, index):
76 """Duplicate a slide in the presentation."""
77 source = pres.slides[index]
78
79 # Use source's layout to preserve formatting
80 new_slide = pres.slides.add_slide(source.slide_layout)
81
82 # Collect all image and media relationships from the source slide
83 image_rels = {}
84 for rel_id, rel in six.iteritems(source.part.rels):
85 if "image" in rel.reltype or "media" in rel.reltype:
86 image_rels[rel_id] = rel
87
88 # CRITICAL: Clear placeholder shapes to avoid duplicates
89 for shape in new_slide.shapes:
90 sp = shape.element
91 sp.getparent().remove(sp)
92
93 # Copy all shapes from source
94 for shape in source.shapes:
95 el = shape.element
96 new_el = deepcopy(el)
97 new_slide.shapes._spTree.insert_element_before(new_el, "p:extLst")
98
99 # Handle picture shapes - need to update the blip reference
100 # Look for all blip elements (they can be in pic or other contexts)
101 # Using the element's own xpath method without namespaces argument
102 blips = new_el.xpath(".//a:blip[@r:embed]")
103 for blip in blips:
104 old_rId = blip.get(
105 "{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed"
106 )
107 if old_rId in image_rels:
108 # Create a new relationship in the destination slide for this image
109 old_rel = image_rels[old_rId]
110 # get_or_add returns the rId directly, or adds and returns new rId
111 new_rId = new_slide.part.rels.get_or_add(
112 old_rel.reltype, old_rel._target
113 )
114 # Update the blip's embed reference to use the new relationship ID
115 blip.set(
116 "{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed",
117 new_rId,
118 )
119
120 # Copy any additional image/media relationships that might be referenced elsewhere
121 for rel_id, rel in image_rels.items():
122 try:
123 new_slide.part.rels.get_or_add(rel.reltype, rel._target)
124 except Exception:
125 pass # Relationship might already exist
126
127 return new_slide
128
129
130def delete_slide(pres, index):
131 """Delete a slide from the presentation."""
132 rId = pres.slides._sldIdLst[index].rId
133 pres.part.drop_rel(rId)
134 del pres.slides._sldIdLst[index]
135
136
137def reorder_slides(pres, slide_index, target_index):
138 """Move a slide from one position to another."""
139 slides = pres.slides._sldIdLst
140
141 # Remove slide element from current position
142 slide_element = slides[slide_index]
143 slides.remove(slide_element)
144
145 # Insert at target position
146 slides.insert(target_index, slide_element)
147
148
149def rearrange_presentation(template_path, output_path, slide_sequence):
150 """
151 Create a new presentation with slides from template in specified order.
152
153 Args:
154 template_path: Path to template PPTX file
155 output_path: Path for output PPTX file
156 slide_sequence: List of slide indices (0-based) to include
157 """
158 # Copy template to preserve dimensions and theme
159 if template_path != output_path:
160 shutil.copy2(template_path, output_path)
161 prs = Presentation(output_path)
162 else:
163 prs = Presentation(template_path)
164
165 total_slides = len(prs.slides)
166
167 # Validate indices
168 for idx in slide_sequence:
169 if idx < 0 or idx >= total_slides:
170 raise ValueError(f"Slide index {idx} out of range (0-{total_slides - 1})")
171
172 # Track original slides and their duplicates
173 slide_map = [] # List of actual slide indices for final presentation
174 duplicated = {} # Track duplicates: original_idx -> [duplicate_indices]
175
176 # Step 1: DUPLICATE repeated slides
177 print(f"Processing {len(slide_sequence)} slides from template...")
178 for i, template_idx in enumerate(slide_sequence):
179 if template_idx in duplicated and duplicated[template_idx]:
180 # Already duplicated this slide, use the duplicate
181 slide_map.append(duplicated[template_idx].pop(0))
182 print(f" [{i}] Using duplicate of slide {template_idx}")
183 elif slide_sequence.count(template_idx) > 1 and template_idx not in duplicated:
184 # First occurrence of a repeated slide - create duplicates
185 slide_map.append(template_idx)
186 duplicates = []
187 count = slide_sequence.count(template_idx) - 1
188 print(
189 f" [{i}] Using original slide {template_idx}, creating {count} duplicate(s)"
190 )
191 for _ in range(count):
192 duplicate_slide(prs, template_idx)
193 duplicates.append(len(prs.slides) - 1)
194 duplicated[template_idx] = duplicates
195 else:
196 # Unique slide or first occurrence already handled, use original
197 slide_map.append(template_idx)
198 print(f" [{i}] Using original slide {template_idx}")
199
200 # Step 2: DELETE unwanted slides (work backwards)
201 slides_to_keep = set(slide_map)
202 print(f"\nDeleting {len(prs.slides) - len(slides_to_keep)} unused slides...")
203 for i in range(len(prs.slides) - 1, -1, -1):
204 if i not in slides_to_keep:
205 delete_slide(prs, i)
206 # Update slide_map indices after deletion
207 slide_map = [idx - 1 if idx > i else idx for idx in slide_map]
208
209 # Step 3: REORDER to final sequence
210 print(f"Reordering {len(slide_map)} slides to final sequence...")
211 for target_pos in range(len(slide_map)):
212 # Find which slide should be at target_pos
213 current_pos = slide_map[target_pos]
214 if current_pos != target_pos:
215 reorder_slides(prs, current_pos, target_pos)
216 # Update slide_map: the move shifts other slides
217 for i in range(len(slide_map)):
218 if slide_map[i] > current_pos and slide_map[i] <= target_pos:
219 slide_map[i] -= 1
220 elif slide_map[i] < current_pos and slide_map[i] >= target_pos:
221 slide_map[i] += 1
222 slide_map[target_pos] = target_pos
223
224 # Save the presentation
225 prs.save(output_path)
226 print(f"\nSaved rearranged presentation to: {output_path}")
227 print(f"Final presentation has {len(prs.slides)} slides")
228
229
230if __name__ == "__main__":
231 main()