rearrange.py 8.3 KB

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