Created
July 16, 2026 10:00
-
-
Save stephenturner/a4ac0863adaccec0ed6ce4ca324218cb to your computer and use it in GitHub Desktop.
convert-pptx-merge-pdf.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env -S uv run | |
| # /// script | |
| # dependencies = [ | |
| # "pypdf", | |
| # ] | |
| # /// | |
| """ | |
| Convert all PPTX files in a folder to PDF and merge them using Microsoft PowerPoint. | |
| This preserves formatting perfectly by using PowerPoint's native export. | |
| Requirements: | |
| - uv (install with: curl -LsSf https://astral.sh/uv/install.sh | sh) | |
| - Microsoft PowerPoint for Mac must be installed | |
| Usage: ./convert_merge_pptx_mac.py <input_folder> <output_pdf> | |
| Example: ./convert_merge_pptx_mac.py ./presentations merged_output.pdf | |
| """ | |
| import os | |
| import sys | |
| import glob | |
| import subprocess | |
| import tempfile | |
| from pathlib import Path | |
| from pypdf import PdfWriter, PdfReader | |
| def check_powerpoint_installed(): | |
| """Check if Microsoft PowerPoint is installed.""" | |
| powerpoint_paths = [ | |
| "/Applications/Microsoft PowerPoint.app", | |
| "/Applications/Microsoft Office/Microsoft PowerPoint.app" | |
| ] | |
| for path in powerpoint_paths: | |
| if os.path.exists(path): | |
| return True | |
| return False | |
| def convert_pptx_to_pdf_with_powerpoint(pptx_file, output_pdf): | |
| """Convert PPTX to PDF using Microsoft PowerPoint via AppleScript.""" | |
| # Convert to absolute paths | |
| pptx_abs = os.path.abspath(pptx_file) | |
| pdf_abs = os.path.abspath(output_pdf) | |
| # Ensure output directory exists | |
| os.makedirs(os.path.dirname(pdf_abs), exist_ok=True) | |
| # Use a more robust AppleScript that references the document differently | |
| applescript = f''' | |
| set inputFile to POSIX file "{pptx_abs}" | |
| set outputFile to POSIX file "{pdf_abs}" | |
| tell application "Microsoft PowerPoint" | |
| activate | |
| delay 2 | |
| try | |
| open inputFile | |
| delay 3 | |
| -- Get the active presentation | |
| set activeDoc to active presentation | |
| -- Save as PDF | |
| save activeDoc in outputFile as save as PDF | |
| -- Close without saving changes | |
| close activeDoc saving no | |
| return "success" | |
| on error errMsg number errNum | |
| try | |
| close active presentation saving no | |
| end try | |
| return "error: " & errMsg & " (error " & errNum & ")" | |
| end try | |
| end tell | |
| ''' | |
| try: | |
| result = subprocess.run( | |
| ['osascript'], | |
| input=applescript, | |
| capture_output=True, | |
| text=True, | |
| timeout=180 | |
| ) | |
| output = result.stdout.strip() | |
| if "success" in output and os.path.exists(output_pdf): | |
| return True | |
| else: | |
| if result.stderr: | |
| print(f" stderr: {result.stderr}") | |
| if output and "error:" in output: | |
| print(f" {output}") | |
| elif output: | |
| print(f" Output: {output}") | |
| return False | |
| except subprocess.TimeoutExpired: | |
| print(f" Error: Conversion timeout") | |
| return False | |
| except Exception as e: | |
| print(f" Error: {e}") | |
| return False | |
| def merge_pdfs(pdf_files, output_file): | |
| """Merge multiple PDF files into a single PDF.""" | |
| print(f"\nMerging {len(pdf_files)} PDFs...") | |
| writer = PdfWriter() | |
| for pdf_file in pdf_files: | |
| try: | |
| reader = PdfReader(pdf_file) | |
| for page in reader.pages: | |
| writer.add_page(page) | |
| print(f" Added {os.path.basename(pdf_file)} ({len(reader.pages)} pages)") | |
| except Exception as e: | |
| print(f"Error reading {pdf_file}: {e}") | |
| with open(output_file, "wb") as output: | |
| writer.write(output) | |
| print(f"\nMerged PDF saved to: {output_file}") | |
| def quit_powerpoint(): | |
| """Quit PowerPoint to clean up.""" | |
| try: | |
| subprocess.run( | |
| ['osascript', '-e', 'tell application "Microsoft PowerPoint" to quit'], | |
| capture_output=True, | |
| timeout=10 | |
| ) | |
| except: | |
| pass | |
| def main(): | |
| if len(sys.argv) != 3: | |
| print("Usage: ./convert_merge_pptx_mac.py <input_folder> <output_pdf>") | |
| print("Example: ./convert_merge_pptx_mac.py ./presentations merged.pdf") | |
| sys.exit(1) | |
| input_folder = sys.argv[1] | |
| output_pdf = sys.argv[2] | |
| # Validate input folder | |
| if not os.path.isdir(input_folder): | |
| print(f"Error: {input_folder} is not a valid directory") | |
| sys.exit(1) | |
| # Check if PowerPoint is installed | |
| if not check_powerpoint_installed(): | |
| print("Error: Microsoft PowerPoint for Mac is not installed.") | |
| print("\nAlternative solutions:") | |
| print("1. Install Microsoft PowerPoint") | |
| print("2. Use the Keynote version (convert_merge_pptx_keynote.py)") | |
| print("3. Use an online converter (not automated)") | |
| sys.exit(1) | |
| # Find all PPTX files | |
| pptx_pattern = os.path.join(input_folder, "*.pptx") | |
| pptx_files = sorted(glob.glob(pptx_pattern)) | |
| if not pptx_files: | |
| print(f"No PPTX files found in {input_folder}") | |
| sys.exit(1) | |
| print(f"Found {len(pptx_files)} PPTX file(s)") | |
| print("Converting using Microsoft PowerPoint...") | |
| # Create temporary directory for intermediate PDFs | |
| with tempfile.TemporaryDirectory() as temp_dir: | |
| # Convert all PPTX files to PDF | |
| pdf_files = [] | |
| for i, pptx_file in enumerate(pptx_files, 1): | |
| print(f"\n[{i}/{len(pptx_files)}] Converting {os.path.basename(pptx_file)}...") | |
| base_name = os.path.splitext(os.path.basename(pptx_file))[0] | |
| temp_pdf = os.path.join(temp_dir, f"{base_name}.pdf") | |
| if convert_pptx_to_pdf_with_powerpoint(pptx_file, temp_pdf): | |
| pdf_files.append(temp_pdf) | |
| print(f" ✓ Success") | |
| else: | |
| print(f" ✗ Failed to convert {os.path.basename(pptx_file)}") | |
| if not pdf_files: | |
| print("\nError: No PDFs were successfully created") | |
| sys.exit(1) | |
| # Merge all PDFs | |
| merge_pdfs(pdf_files, output_pdf) | |
| print(f"\n✓ Success! {len(pdf_files)} presentations merged into {output_pdf}") | |
| # Clean up PowerPoint | |
| quit_powerpoint() | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment